/* Portfolio site — production styles (derived from approved prototype v6) */
:root { color-scheme: dark; }
html, body { margin: 0; background: #0a0a0b; color: #ececec;
  /* LINE Seed JP sits AFTER the latin faces: per-glyph fallback means latin keeps resolving
     from system-ui and only CJK codepoints reach LINE Seed, so one stack covers both scripts
     without the Japanese face touching any English text. */
  font-family: system-ui, -apple-system, 'LINE Seed JP', sans-serif; }

/* Japanese headline states, applied by initCyclers() via .cyc-ja as it swaps text in.
   Body copy (#hero .bio) keeps default tracking — -0.1em is a display setting and would
   close 15px prose into mush.

   The tight track is scoped to the hero name because that is the only Japanese string in
   the site that is katakana (10 of 11 chars). Katakana are uniformly square and take the
   squeeze well; the other headlines are kanji + hiragana, where -0.1em collides the
   strokes of adjacent glyphs. They keep the base -0.02em from their own rules.
   If Japanese copy with mixed katakana ever lands in those headlines, this needs to become
   per-character rather than per-element — see initCyclers(). */
:root { --ja-track: -0.15em; }
/* LINE Seed FIRST here, not third.
   The body stack puts it after system-ui, relying on per-glyph fallback to carry kana past
   the latin faces. Chromium does that; WebKit frequently satisfies CJK from the OS fallback
   chain behind system-ui (Hiragino) and never reaches the author's third choice — so the
   Japanese rendered in the system face and the 700 weight never loaded.
   .cyc-ja is only ever set while a Japanese state is on screen, so naming the Japanese face
   first here cannot affect any English text. */
.cyc-ja { font-family: 'LINE Seed JP', system-ui, -apple-system, sans-serif; }
#hero h1.cyc-ja { font-weight: 700; letter-spacing: var(--ja-track); }
#contact h2.cyc-ja, .page header h1.cyc-ja { font-weight: 700; }
/* Pre-JS scroll height so #contact at top:100vh is reachable and the page doesn't jump
   from 0 height. app.js replaces this with the measured wheel length on first layout. */
#spacer { width: 1px; height: 200vh; }
#stage { position: fixed; inset: 0; z-index: 1; }
canvas { display: block; }

/* persistent name/logo */
#logo {
  position: fixed; top: 18px; left: 22px; z-index: 2100; cursor: pointer;
  display: flex; align-items: center; gap: 12px;
  font-weight: 800; text-transform: uppercase; letter-spacing: .04em;
  font-size: 14px; line-height: 1.1;
  opacity: 0; transform: translateY(-6px); pointer-events: none;
  transition: opacity .4s cubic-bezier(.65,0,.35,1), transform .4s cubic-bezier(.65,0,.35,1);
}
#logo.on { opacity: 1; transform: none; pointer-events: auto; }
#logo .mark { width: 28px; height: 28px; background: #fff; flex: none; }
/* Uploaded logo: .has-logo drops the white fallback fill so a knockout mark (transparent
   letterforms) shows the background through instead of turning into a solid white square.
   The img fills the span and is contained, so any aspect fits the square box. */
.mark.has-logo { background: none !important; }
.mark.has-logo img { display: block; width: 100%; height: 100%; object-fit: contain; }
#logo small {
  display: block; font-family: ui-monospace, Menlo, monospace; font-weight: 400;
  font-size: 9px; letter-spacing: .3em; opacity: .55; margin-top: 3px;
}

/* top page links */
#topnav {
  position: fixed; top: 16px; left: 50%; transform: translate(-50%, -6px); z-index: 2100;
  display: flex; gap: 2px; opacity: 0; pointer-events: none;
  transition: opacity .4s cubic-bezier(.65,0,.35,1), transform .4s cubic-bezier(.65,0,.35,1);
}
#topnav.on { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }
#topnav a {
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 11px; letter-spacing: .16em; text-transform: uppercase; cursor: pointer;
  padding: 8px 14px; color: #d6d6d6; background: rgba(10,10,11,.6);
  border: 1px solid rgba(255,255,255,.18); backdrop-filter: blur(6px);
  transition: all .2s;
}
#topnav a:hover, #topnav a.active { background: #ececec; color: #0a0a0b; border-color: #ececec; }

/* mobile hamburger + fullscreen menu */
#menuBtn {
  position: fixed; top: 16px; right: 16px; z-index: 2100; width: 46px; height: 46px;
  display: none; place-items: center; cursor: pointer;
  background: rgba(10,10,11,.75); border: 1px solid rgba(255,255,255,.22);
  color: #ececec; backdrop-filter: blur(6px);
  opacity: 0; pointer-events: none; transition: opacity .4s cubic-bezier(.65,0,.35,1);
}
#menuBtn.on { opacity: 1; pointer-events: auto; }
#mobileMenu {
  display: none; position: fixed; inset: 0; z-index: 2300; background: #0a0a0b;
  flex-direction: column; justify-content: center; padding: 0 8vw;
}
body.menu #mobileMenu { display: flex; }
#mobileMenu a {
  font-weight: 800; text-transform: uppercase; letter-spacing: -0.01em;
  font-size: clamp(34px, 10vw, 64px); line-height: 1.25; color: #ececec; cursor: pointer;
  padding: 14px 8px; border-bottom: 1px solid rgba(255,255,255,.12);
}
#mobileMenu a.active { background: #ececec; color: #0a0a0b; }
#mobileMenu .mclose {
  position: absolute; top: 16px; right: 16px; width: 46px; height: 46px;
  display: grid; place-items: center; cursor: pointer;
  border: 1px solid rgba(255,255,255,.25); color: #ececec; font-size: 20px;
}

/* landing hero */
#hero { position: absolute; top: 0; left: 0; right: 0; height: 100vh; z-index: 5; overflow: hidden; will-change: transform; }
#hero video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
#hero .shade { position: absolute; inset: 0; background:
  linear-gradient(180deg, rgba(10,10,11,.55) 0%, rgba(10,10,11,.35) 55%, #0a0a0b 100%); }
#hero .inner {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  justify-content: center; padding: 0 6vw; gap: 20px;
  --hero-fs: clamp(48px, 9vw, 140px);   /* h1 size; the logo mark derives from it */
  --hero-lh: .92;
}
#hero .namerow { display: flex; align-items: flex-start; gap: clamp(14px, 2.2vw, 32px); }
/* The mark's height is computed directly from --hero-fs/--hero-lh (2 lines), NOT via flex
   align-items:stretch. Stretch was the original approach and works for an EMPTY .mark, but
   once a real uploaded logo <img> sits inside it, the img's own intrinsic pixel size (e.g.
   356x356) leaks into the flex/aspect-ratio hypothetical-size pass — confirmed by removing
   the img, which drops a wrongly-262px mark straight back to the correct ~88px at mobile
   widths. An explicit height sidesteps that resolution entirely: aspect-ratio derives width
   from it, and max-height stays as a hard ceiling for extreme font-scale settings. */
#hero .namerow .mark {
  flex: none; aspect-ratio: 1; background: #fff;
  height: calc(var(--hero-fs) * var(--hero-lh) * 2);
  max-height: 262px;
}
#hero h1 {
  margin: 0; font-weight: 800; text-transform: uppercase; letter-spacing: -0.02em;
  font-size: var(--hero-fs); line-height: var(--hero-lh);
}
#hero .bio { max-width: 520px; font-size: 15px; line-height: 1.6; opacity: .8; }
/* Cycling headlines are written by JS as textContent with real newlines rather than <br>,
   so every target needs pre-line. Applied to the elements rather than [data-cycle] so the
   no-JS render (which also carries newlines now) breaks identically. */
#hero h1, #hero .bio, #contact h2, .page header h1 { white-space: pre-line; }
#hero nav { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
#hero nav a {
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 11px; letter-spacing: .16em; text-transform: uppercase; text-decoration: none;
  padding: 9px 18px; border: 1px solid rgba(255,255,255,.35); color: #ececec;
  transition: all .2s; cursor: pointer;
}
#hero nav a:hover { background: #ececec; color: #0a0a0b; border-color: #ececec; }
#hero .scrollhint {
  position: absolute; bottom: 28px; left: 50%; transform: translateX(-50%);
  font-family: ui-monospace, Menlo, monospace; font-size: 10px; letter-spacing: .3em;
  text-transform: uppercase; opacity: .55; animation: bob 2.2s ease-in-out infinite;
}
@keyframes bob { 50% { transform: translate(-50%, 8px); } }

/* contact end-cap */
#contact {
  /* top is recomputed by app.js once the wheel is measured, but it MUST have a sane
     default: with top:auto this absolutely-positioned block falls at its static position
     (the top of the page, since #spacer has no height until JS runs) and — sharing
     z-index 5 with #hero while coming later in the DOM — paints OVER the hero until the
     module loads. That reads as "the contact form loads before the hero". */
  top: 100vh;
  position: absolute; left: 0; right: 0; height: 100vh; z-index: 5;
  display: flex; flex-direction: column; justify-content: center; align-items: flex-start;
  padding: 0 6vw; gap: 22px;
  background: linear-gradient(180deg, rgba(10,10,11,0) 0%, #0a0a0b 22%);
}
#contact h2 {
  margin: 0; font-weight: 800; text-transform: uppercase; letter-spacing: -0.02em;
  font-size: clamp(34px, 6vw, 96px); line-height: .95;
}
#contact .email {
  font-family: ui-monospace, Menlo, monospace; font-size: clamp(15px, 2.2vw, 26px);
  color: #ececec; text-decoration: none; border-bottom: 1px solid rgba(255,255,255,.4);
  padding-bottom: 2px;
}
#contact .email:hover { border-color: #ececec; }
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chips a, .chips span {
  font-family: ui-monospace, Menlo, monospace; font-size: 11px; letter-spacing: .16em;
  text-transform: uppercase; text-decoration: none; padding: 9px 18px;
  border: 1px solid rgba(255,255,255,.3); color: #d6d6d6; transition: all .2s;
}
.chips a:hover { background: #ececec; color: #0a0a0b; border-color: #ececec; }

/* contact form */
#contactForm { margin-top: 6px; width: min(640px, 100%); display: flex; flex-direction: column; gap: 10px; }
#contactForm .cfrow { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; }
#contactForm input, #contactForm textarea {
  font: inherit; font-size: 14px; color: #ececec; padding: 12px;
  background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.22); border-radius: 0;
}
#contactForm textarea { min-height: 96px; resize: vertical; }
#contactForm input:focus, #contactForm textarea:focus { outline: none; border-color: rgba(255,255,255,.6); }
#contactForm button {
  align-self: flex-start; cursor: pointer;
  font-family: ui-monospace, Menlo, monospace; font-size: 11px; letter-spacing: .16em;
  text-transform: uppercase; padding: 13px 30px;
  background: #ececec; color: #0a0a0b; border: 1px solid #ececec; transition: all .2s;
}
#contactForm button:hover { background: transparent; color: #ececec; }
#contactForm .hp { position: absolute; left: -5000px; width: 1px; height: 1px; opacity: 0; }
#contactForm .cfstatus { font-family: ui-monospace, Menlo, monospace; font-size: 12px; letter-spacing: .1em; opacity: .75; }

/* category filter — slider pill, bottom center */
#filters {
  position: fixed; bottom: 16px; left: 50%; transform: translate(-50%, 10px); z-index: 15;
  display: flex; gap: 4px; padding: 4px;
  background: rgba(10,10,11,.75); border: 1px solid rgba(255,255,255,.22); border-radius: 0;
  backdrop-filter: blur(6px);
  opacity: 0; pointer-events: none;
  transition: opacity .4s cubic-bezier(.65,0,.35,1), transform .4s cubic-bezier(.65,0,.35,1);
}
#filters.on { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }
#filters .knob {
  position: absolute; top: 4px; height: calc(100% - 8px); border-radius: 0;
  background: #ececec;
  transition: left .3s cubic-bezier(.65,0,.35,1), width .3s cubic-bezier(.65,0,.35,1);
}
#filters button {
  position: relative; z-index: 1; border: none; background: transparent; cursor: pointer;
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
  padding: 9px 16px; border-radius: 0; color: #d6d6d6; transition: color .3s;
}
#filters button.active { color: #0a0a0b; }

/* mobile filter popout */
#filterBtn {
  position: fixed; bottom: 16px; left: 50%; transform: translate(-50%, 10px); z-index: 2100;
  display: none; align-items: center; gap: 10px; cursor: pointer;
  font-family: ui-monospace, Menlo, monospace; font-size: 11px; letter-spacing: .16em;
  text-transform: uppercase; padding: 13px 22px;
  background: rgba(10,10,11,.75); border: 1px solid rgba(255,255,255,.22);
  color: #ececec; backdrop-filter: blur(6px);
  opacity: 0; pointer-events: none; transition: all .4s cubic-bezier(.65,0,.35,1);
}
#filterBtn.on { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }
#filterSheet {
  display: none; position: fixed; left: 0; right: 0; bottom: 0; z-index: 2300;
  background: #141416; border-top: 1px solid rgba(255,255,255,.18);
  padding: 10px 18px calc(24px + env(safe-area-inset-bottom, 0px));
}
body.filter #filterSheet { display: block; }
#filterSheet .lbl {
  font-family: ui-monospace, Menlo, monospace; font-size: 10px; letter-spacing: .3em;
  text-transform: uppercase; opacity: .5; padding: 10px 12px 6px;
}
#filterSheet button {
  display: block; width: 100%; text-align: left; cursor: pointer;
  font-family: ui-monospace, Menlo, monospace; font-size: 13px; letter-spacing: .14em;
  text-transform: uppercase; padding: 16px 12px;
  background: transparent; border: none; border-bottom: 1px solid rgba(255,255,255,.1);
  color: #d6d6d6;
}
#filterSheet button:last-child { border-bottom: none; }
#filterSheet button.active { background: #ececec; color: #0a0a0b; }

/* grid / list slider toggle */
#viewToggle {
  position: fixed; left: 16px; bottom: 16px; z-index: 15;
  display: flex; gap: 4px; padding: 4px;
  background: rgba(10,10,11,.75); border: 1px solid rgba(255,255,255,.22); border-radius: 0;
  backdrop-filter: blur(6px);
}
#viewToggle .knob {
  position: absolute; top: 4px; left: 4px; width: 42px; height: 42px; border-radius: 0;
  background: #ececec; transition: transform .3s cubic-bezier(.65,0,.35,1);
}
#viewToggle.list .knob { transform: translateX(46px); }
#viewToggle button {
  position: relative; z-index: 1; width: 42px; height: 42px; border-radius: 0;
  border: none; background: transparent; display: grid; place-items: center;
  cursor: pointer; color: #e8e8e8; transition: color .3s;
}
#viewToggle button.active { color: #0a0a0b; }

/* "Let's Chat" CTA */
#chat {
  position: fixed; right: 16px; bottom: 16px; z-index: 2100; cursor: pointer;
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 11px; letter-spacing: .16em; text-transform: uppercase;
  padding: 15px 22px; background: #ececec; color: #0a0a0b;
  border: 1px solid #ececec;
  opacity: 0; transform: translateY(10px); pointer-events: none;
  transition: all .3s cubic-bezier(.65,0,.35,1);
}
#chat.on { opacity: 1; transform: none; pointer-events: auto; }
#chat:hover { background: transparent; color: #ececec; }
body.list #chat { opacity: 1; transform: none; pointer-events: auto; }

/* hover title + tags */
/* Always-on wheel labels — one per project, positioned every frame by syncTitles().
   z-index 2 sits ABOVE the canvas (#stage, 1) but BELOW #hero/#contact (5), so a label is
   occluded by the hero exactly like the wheel behind it. At z-index 10 (where the old
   single hover title lived) every label would float on top of the hero, which only went
   unnoticed before because hover was separately suppressed up there. */
#titles { position: fixed; inset: 0; z-index: 2; pointer-events: none; }
.wt {
  position: absolute; top: 0; left: 0; pointer-events: none;
  opacity: .45; filter: blur(2px);
  transition: opacity .35s cubic-bezier(.65,0,.35,1), filter .35s cubic-bezier(.65,0,.35,1);
  /* Only transform/opacity/filter ever change, all compositor properties — writing
     left/top on ~10 labels per frame would force layout on every one of them. */
  will-change: transform, opacity, filter;
}
.wt.hot { opacity: 1; filter: blur(0); }
.wt h2 {
  margin: 0; font-weight: 800; text-transform: uppercase;
  line-height: .95; letter-spacing: -0.015em; color: #ececec;
}
/* Role pills belong to the hovered label only, but their space is reserved in BOTH states:
   the box is bottom-anchored (translateY(-100%)), so revealing them by changing layout
   would shove the title upward on every hover. Opacity-only keeps the title still. */
.wt .tags { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 6px;
            opacity: 0; transition: opacity .35s cubic-bezier(.65,0,.35,1); }
.wt.hot .tags { opacity: 1; }
.wt .tags span, .row .tags span, .expcard .tags span {
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 11px; letter-spacing: .16em; text-transform: uppercase;
  padding: 5px 12px; border: 1px solid rgba(255,255,255,.28); border-radius: 0;
  color: #d6d6d6; background: rgba(10,10,11,.6); backdrop-filter: blur(4px);
}

/* list view */
#list { display: none; padding: 96px 32px 140px; max-width: 1500px; margin: 0 auto; }
body.list #list { display: block; }
body.list #stage, body.list #spacer, body.list #titles,
body.list #hero, body.list #contact { display: none; }
body.list #logo, body.list #topnav, body.list #filters { opacity: 1; transform: translate(-50%,0); pointer-events: auto; }
body.list #logo { transform: none; }
#list h3 {
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 12px; letter-spacing: .2em; opacity: .45; font-weight: 400;
  margin: 56px 0 0; padding: 0 16px 10px;
}
#list section:first-child h3 { margin-top: 0; }
.row {
  position: relative; overflow: hidden; cursor: pointer;
  display: grid; grid-template-columns: minmax(0, 1fr) 220px minmax(300px, 26vw);
  gap: 20px; align-items: center;
  padding: 20px 16px; border-top: 1px solid rgba(255,255,255,.12);
}
#list section .row:last-child { border-bottom: 1px solid rgba(255,255,255,.12); }
.row .bg {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-size: cover; background-position: center;
  opacity: 0; transition: opacity .35s cubic-bezier(.65,0,.35,1);
}
/* Scrim stays asymmetric — heavier on the LEFT, where the title sits, and nearly clear on
   the right so the frame itself is legible. Two things dim this image and they multiply:
   this gradient and .row:hover .bg's opacity. At the old .75/.45 over opacity .55 the
   thumbnail survived at only 14% of its brightness on the left and 30% on the right, which
   read as a near-black smear rather than a frame. */
.row .bg::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(90deg, rgba(10,10,11,.5), rgba(10,10,11,.12));
}
.row:hover .bg { opacity: .9; }
.row > * { position: relative; z-index: 1; }
.row .t {
  font-weight: 800; text-transform: uppercase; letter-spacing: -0.01em;
  font-size: clamp(18px, 2.4vw, 32px); line-height: 1;
  transition: transform .3s cubic-bezier(.65,0,.35,1);
}
.row:hover .t { transform: translateX(8px); }
.row .c {
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 12px; letter-spacing: .14em; text-transform: uppercase; opacity: .55;
}
.row .tags { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }
.row.hidden { display: none; }
@media (max-width: 900px) {
  .row { grid-template-columns: 1fr; gap: 8px; }
  .row .tags { justify-content: flex-start; }
}

/* overlay pages */
.page {
  display: none; position: fixed; inset: 0; background: #0a0a0b;
  overflow-y: auto; overscroll-behavior: contain;
}
#project { z-index: 2000; }
#about, #experiments { z-index: 1900; }
body.project #project, body.about #about, body.experiments #experiments { display: block; }
/* Two scrollbars otherwise: #spacer keeps the document tall (it is what drives the wheel),
   so the window's own bar stays on screen beside the overlay's overflow-y. While a full-screen
   layer is open it owns the scrolling, so the document's bar is suppressed. The UA propagates
   body's overflow to the viewport while html's is visible, which it is here, and scrollY is
   preserved — the wheel is exactly where it was when the layer closes.
   body.menu (the mobile nav) is the same full-screen case. */
body.project, body.about, body.experiments, body.menu { overflow: hidden; }
.page .back {
  display: inline-flex; align-items: center; gap: 10px;
  margin: 0 0 36px; padding: 9px 18px; cursor: pointer;
  font-family: ui-monospace, Menlo, monospace; font-size: 11px; letter-spacing: .16em;
  text-transform: uppercase; color: #d6d6d6;
  border: 1px solid rgba(255,255,255,.25);
  transition: all .2s;
}
.page .back:hover { background: #ececec; color: #0a0a0b; border-color: #ececec; }
.page .bottomback {
  display: inline-flex; align-items: center; gap: 10px;
  margin: 0 0 96px 6vw; padding: 12px 26px; cursor: pointer;
  font-family: ui-monospace, Menlo, monospace; font-size: 11px; letter-spacing: .16em;
  text-transform: uppercase; color: #d6d6d6;
  border: 1px solid rgba(255,255,255,.25); transition: all .2s;
}
.page .bottomback:hover { background: #ececec; color: #0a0a0b; border-color: #ececec; }
.page header { padding: 10vh 6vw 48px; }
.page header .kicker {
  font-family: ui-monospace, Menlo, monospace; font-size: 12px; letter-spacing: .24em;
  text-transform: uppercase; opacity: .5; margin-bottom: 18px;
}
.page header h1 {
  margin: 0 0 28px; font-weight: 800; text-transform: uppercase; letter-spacing: -0.02em;
  font-size: clamp(44px, 8vw, 128px); line-height: .92;
}
.page .meta { display: flex; flex-wrap: wrap; gap: 24px; align-items: center; }
.page .meta .kv {
  font-family: ui-monospace, Menlo, monospace; font-size: 12px; letter-spacing: .14em;
  text-transform: uppercase; opacity: .6;
}
.page .meta .tags { display: flex; gap: 6px; flex-wrap: wrap; }
.page .meta .tags span {
  font-family: ui-monospace, Menlo, monospace; font-size: 11px; letter-spacing: .16em;
  text-transform: uppercase; padding: 9px 18px;
  border: 1px solid rgba(255,255,255,.3); color: #d6d6d6;
}
#project .heroMedia { width: 100%; aspect-ratio: 16 / 9; background: #131315; }
#project .heroMedia video, #project .heroMedia iframe {
  width: 100%; height: 100%; object-fit: cover; display: block; border: 0;
}
.page .body {
  display: grid; grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr);
  gap: 48px; padding: 72px 6vw; max-width: 1500px;
}
.page .body .facts { font-family: ui-monospace, Menlo, monospace; font-size: 12px;
  letter-spacing: .12em; text-transform: uppercase; line-height: 2.2; opacity: .65; }
.page .body .desc { font-size: 17px; line-height: 1.75; opacity: .9; max-width: 640px; white-space: pre-line; }
/* Links inside body copy. No accent colour — the underline carries the affordance, and
   break-word keeps a long URL from forcing a horizontal scroll on a phone. */
.page .body .desc a {
  color: #fff; text-decoration: underline; text-underline-offset: 3px;
  text-decoration-color: rgba(255,255,255,.4); overflow-wrap: break-word;
  transition: text-decoration-color .2s cubic-bezier(.65,0,.35,1);
}
.page .body .desc a:hover { text-decoration-color: #fff; }
/* Additional videos — same rhythm and gutter as .stills so the two sections read as one
   column. object-fit is contain, not cover: unlike the hero (a deliberate 16/9 banner)
   these can be any aspect and cropping someone's 2.39:1 grade would be worse than
   letterboxing it. */
#project .videos { display: grid; gap: 12px; padding: 0 6vw; }
#project .videos[hidden] { display: none; }
#project .videos .vitem { width: 100%; }
#project .videos video, #project .videos iframe {
  width: 100%; aspect-ratio: 16 / 9; display: block; border: 0;
  background: #131315; object-fit: contain;
}
#project .videos .vcap {
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 11px; letter-spacing: .16em; text-transform: uppercase;
  opacity: .7; margin-top: 8px;
}
#project .videos + .stills { padding-top: 12px; }

/* Process material (pre-vis / behind the scenes). Deliberately a tighter multi-up grid than
   .stills: these are reference snaps, not the graded frames, and showing them full-width
   would give them the same visual weight as the finished work. */
#project .process { padding: 0 6vw 72px; }
#project .process[hidden] { display: none; }
#project .process .lbl {
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 11px; letter-spacing: .16em; text-transform: uppercase;
  opacity: .6; margin: 40px 0 14px; padding-top: 28px;
  border-top: 1px solid rgba(255,255,255,.14);
}
#project .process .pgrid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
}
#project .process .pgrid img { width: 100%; display: block; background: #131315; }
#project .process .vitem { margin-bottom: 12px; }
#project .process video, #project .process iframe {
  width: 100%; aspect-ratio: 16 / 9; display: block; border: 0;
  background: #131315; object-fit: contain;
}
#project .process .vcap {
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 11px; letter-spacing: .16em; text-transform: uppercase;
  opacity: .7; margin-top: 8px;
}

#project .stills { display: grid; gap: 12px; padding: 0 6vw 72px; }
#project .stills img { width: 100%; display: block; background: #131315; }
#project .stills .pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
/* Four-up reference row for screenshot projects. Drops to two columns at the same 820px
   breakpoint the project body already collapses at, so it never shrinks UI past reading. */
#project .stills .quad { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 12px; }
@media (max-width: 820px) { #project .stills .quad { grid-template-columns: repeat(2, minmax(0,1fr)); } }
#project .next {
  display: block; border-top: 1px solid rgba(255,255,255,.14); padding: 56px 6vw 96px;
  cursor: pointer; position: relative; overflow: hidden;
}
#project .next .lbl, .ba .lbl, #about .clients .lbl {
  font-family: ui-monospace, Menlo, monospace; font-size: 11px; letter-spacing: .24em;
  text-transform: uppercase; opacity: .5; margin-bottom: 12px;
}
#project .next .nt {
  font-weight: 800; text-transform: uppercase; letter-spacing: -0.02em;
  font-size: clamp(34px, 6vw, 96px); line-height: .95;
  transition: transform .3s cubic-bezier(.65,0,.35,1);
}
#project .next:hover .nt { transform: translateX(14px); }
#project .next .nbg {
  position: absolute; inset: 0; z-index: -1; background-size: cover; background-position: center;
  opacity: 0; transition: opacity .35s;
}
#project .next:hover .nbg { opacity: .25; }
@media (max-width: 820px) { .page .body { grid-template-columns: 1fr; gap: 24px; } }

/* before / after grade slider */
#project .ba { padding: 0 6vw 72px; }
.baWrap { position: relative; width: 100%; aspect-ratio: 16 / 9; overflow: hidden; background: #131315; --x: 50%; }
.baWrap img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.baWrap .baBefore { clip-path: inset(0 calc(100% - var(--x)) 0 0); }
.baWrap .baLine {
  position: absolute; top: 0; bottom: 0; left: var(--x); width: 2px;
  background: #ececec; transform: translateX(-1px); pointer-events: none;
}
.baWrap .baLine::after {
  content: '◂ ▸'; position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%); background: #ececec; color: #0a0a0b;
  font-size: 11px; padding: 6px 8px; letter-spacing: 2px; white-space: nowrap;
}
.baWrap .baRange {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; cursor: ew-resize; margin: 0;
}

/* about page extras */
/* Behind-the-scenes strip, between the bio and the client grid. Borders are drawn the same
   way as .clients .grid — container owns top+left, each cell owns right+bottom — so the two
   blocks stack into one continuous hairline rule rather than doubling along the seam. */
#about .btsstrip {
  /* 3-up rather than a single row: at six frames a one-row strip puts each at ~1/6 of the
     page, and these are detail-heavy on-set photographs that stop reading at that size.
     Three columns divides six evenly with no empty cells. */
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  margin: 8px 6vw 48px;
  border-top: 1px solid rgba(255,255,255,.12); border-left: 1px solid rgba(255,255,255,.12);
}
#about .btsstrip figure {
  margin: 0; position: relative; overflow: hidden;
  border-right: 1px solid rgba(255,255,255,.12); border-bottom: 1px solid rgba(255,255,255,.12);
}
#about .btsstrip img {
  width: 100%; aspect-ratio: 3/2; object-fit: cover; display: block; background: #131315;
  opacity: .82; transition: opacity .3s cubic-bezier(.65,0,.35,1), transform .5s cubic-bezier(.65,0,.35,1);
}
#about .btsstrip figure:hover img { opacity: 1; transform: scale(1.03); }
#about .btsstrip figcaption {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 10px 12px;
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: #ececec;
  background: linear-gradient(0deg, rgba(10,10,11,.85), rgba(10,10,11,0));
}
/* 820px matches the breakpoint .page .body already collapses at, so the strip reflows at the
   same moment the two-column bio above it does. */
@media (max-width: 820px) { #about .btsstrip { grid-template-columns: repeat(2, minmax(0,1fr)); } }

/* About — the left rail states the name, then the capabilities. Scoped to #about: .facts is
   shared with the project pages, where it still renders the mono credit block. The parent's
   opacity is reset because it would otherwise compound with each child's own. */
#about .body .facts { opacity: 1; }
/* Portrait above the name, full width of the rail. */
#about .body .facts .portrait {
  display: block; width: 100%; aspect-ratio: 4 / 5; object-fit: cover;
  margin: 0 0 28px; background: #131315;
}
#about .body .facts .who {
  font-family: system-ui, -apple-system, 'LINE Seed JP', sans-serif;
  margin: 0 0 24px; font-weight: 800; text-transform: uppercase; letter-spacing: -0.02em;
  font-size: clamp(30px, 3.6vw, 56px); line-height: .95;
}
#about .body .facts .caps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
#about .body .facts .caps li {
  font-family: ui-monospace, Menlo, monospace; font-size: 12px; letter-spacing: .12em;
  text-transform: uppercase; line-height: 1.5; opacity: .62;
  padding: 11px 0; border-top: 1px solid rgba(255,255,255,.12);
}
#about .body .facts .caps li:last-child { border-bottom: 1px solid rgba(255,255,255,.12); }

#about .clients { padding: 0 6vw 72px; }
#about .clients .grid {
  /* app.js writes --sc here as the grid crosses the overlay's viewport; declaring the fallback
     on this same element (not on the cells) is what lets it inherit down to the slabs. */
  --sc: 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
  border-top: 1px solid rgba(255,255,255,.12); border-left: 1px solid rgba(255,255,255,.12);
  overflow: hidden;
}
#about .clients .grid span {
  --pad-y: 30px; --pad-x: 20px;
  perspective: 620px; transform-style: preserve-3d;
  padding: var(--pad-y) var(--pad-x); font-weight: 900; text-transform: uppercase;
  font-size: clamp(22px, 2.4vw, 38px); letter-spacing: -0.01em; line-height: 1.05;
  border-right: 1px solid rgba(255,255,255,.12); border-bottom: 1px solid rgba(255,255,255,.12);
}
/* At rest each wordmark carries a faint sideways smear and is split by two vertical
   fractures whose slabs slide up and down; hover pulls everything back and the name resolves
   clean. Monochrome throughout — the site carries no accent colour anywhere else and the grid
   should not be the exception.

   The smear is a stack of text-shadows stepping outward in x, each fainter and blurrier than
   the last. Stacked offsets are what give it direction; filter: blur() would be round in every
   direction and would take the cell's borders with it, softening the whole grid. The two
   fractured slabs are pseudo-elements clipped to vertical bands with inset(), sized to the
   WORD rather than the cell (width: max-content) so a short name fractures through itself
   instead of through empty cell.

   PHP seeds --spread/--lean/--cut/--w/--lift per client from a hash of the name (see
   client_fracture_vars), so no two names break alike and a reload never reshuffles them.

   Gated on (hover: hover): with no cursor to pull it back, a touch screen would be left with
   permanently broken client names, so there they render plainly. */
#about .clients .grid .fx { position: absolute; inset: 0; pointer-events: none; }

@media (hover: hover) {
  #about .clients .grid span { position: relative; }

  #about .clients .grid span[data-t] {
    color: #fff;
    text-shadow:
      calc(var(--lean) * 1px) 0 calc(var(--spread) * 0.30px) rgba(236,236,236,.26),
      calc(var(--spread) * -0.22px + var(--lean) * 1px) 0 calc(var(--spread) * 0.14px) rgba(236,236,236,.46),
      calc(var(--spread) * -0.46px + var(--lean) * 1px) 0 calc(var(--spread) * 0.24px) rgba(236,236,236,.30),
      calc(var(--spread) * -0.74px + var(--lean) * 1px) 0 calc(var(--spread) * 0.36px) rgba(236,236,236,.18),
      calc(var(--spread) *  0.22px + var(--lean) * 1px) 0 calc(var(--spread) * 0.14px) rgba(236,236,236,.46),
      calc(var(--spread) *  0.46px + var(--lean) * 1px) 0 calc(var(--spread) * 0.24px) rgba(236,236,236,.30),
      calc(var(--spread) *  0.74px + var(--lean) * 1px) 0 calc(var(--spread) * 0.36px) rgba(236,236,236,.18);
    transition: text-shadow .45s cubic-bezier(.65,0,.35,1), color .45s cubic-bezier(.65,0,.35,1);
  }

  #about .clients .grid .fx::before,
  #about .clients .grid .fx::after {
    content: attr(data-t);
    /* width: max-content, not inset:0 — a cell is far wider than its word, and a slab cut as a
       % of the cell would fracture mostly empty space. max-width caps it at the cell so a name
       that wraps (THE RITZ-CARLTON) wraps here too instead of running out across its
       neighbour. */
    position: absolute; top: 0; bottom: 0; left: 0; width: max-content; max-width: 100%;
    padding: var(--pad-y) var(--pad-x);
    color: rgba(236,236,236,.92); pointer-events: none; will-change: transform;
    text-shadow:
      calc(var(--spread) * -0.4px) 0 calc(var(--spread) * 0.26px) rgba(236,236,236,.32),
      calc(var(--spread) *  0.4px) 0 calc(var(--spread) * 0.26px) rgba(236,236,236,.32);
    transition: transform .45s cubic-bezier(.65,0,.35,1), opacity .45s cubic-bezier(.65,0,.35,1), filter .45s cubic-bezier(.65,0,.35,1);
  }
  /* Vertical slabs: a column of the word, lifted out of line and standing off the face in
     depth. --z pushes one slab toward the viewer and the other behind, --ry turns each a few
     degrees, and the cell's own perspective projects them — so the near slab reads slightly
     larger and the far one slightly smaller, and they separate as the page moves.

     --sc is scroll position, written by app.js as the grid travels the overlay's viewport
     (-1 above, 0 centred, +1 below). Multiplied by --par it drives the parallax: each slab
     answers the scroll by a different amount and in the opposite direction to its partner,
     so the layers slide across one another rather than moving as one block. */
  #about .clients .grid .fx::before {
    clip-path: inset(0 calc((100 - var(--cut1) - var(--w1)) * 1%) 0 calc(var(--cut1) * 1%));
    transform:
      translate3d(calc(var(--drift) * 1px),
                  calc(var(--lift1) * 1px + var(--sc) * var(--par1) * 1px),
                  calc(var(--z1) * 1px))
      rotateY(calc(var(--ry) * 1deg));
    filter: blur(0.5px); opacity: .82;
  }
  #about .clients .grid .fx::after {
    clip-path: inset(0 calc((100 - var(--cut2) - var(--w2)) * 1%) 0 calc(var(--cut2) * 1%));
    transform:
      translate3d(calc(var(--drift) * -1px),
                  calc(var(--lift2) * 1px + var(--sc) * var(--par2) * 1px),
                  calc(var(--z2) * 1px))
      rotateY(calc(var(--ry) * -1deg));
    filter: blur(0.5px); opacity: .82;
  }

  #about .clients .grid span[data-t]:hover {
    color: #fff;
    text-shadow:
      0 0 0 rgba(236,236,236,0), 0 0 0 rgba(236,236,236,0),
      0 0 0 rgba(236,236,236,0), 0 0 0 rgba(236,236,236,0),
      0 0 0 rgba(236,236,236,0), 0 0 0 rgba(236,236,236,0),
      0 0 0 rgba(236,236,236,0);
  }
  #about .clients .grid span[data-t]:hover .fx::before,
  #about .clients .grid span[data-t]:hover .fx::after {
    transform: translate3d(0, 0, 0) rotateY(0deg); opacity: 0; filter: blur(0);
  }

  /* an uploaded mark is an <img>, which text-shadow cannot reach — blur the image itself.
     The filter is on the img, not the cell, so the borders stay sharp either way. */
  #about .clients .grid span.haslogo img { filter: blur(1.2px); transition: filter .45s cubic-bezier(.65,0,.35,1); }
  #about .clients .grid span.haslogo:hover img { filter: blur(0); }
}
@media (prefers-reduced-motion: reduce) {
  #about .clients .grid span,
  #about .clients .grid .fx::before,
  #about .clients .grid .fx::after,
  #about .clients .grid span.haslogo img { transition: none; }
}
/* A cell holding an uploaded mark. Every logo gets the SAME optical box rather than its own
   natural size — otherwise a wide wordmark and a square badge in neighbouring cells read as
   two different weights of client. Fixed row height + contain keeps the grid even while the
   list is a mix of marks and wordmarks. */
#about .clients .grid span.haslogo { display: flex; align-items: center; justify-content: center; }
#about .clients .grid span.haslogo img {
  max-width: 100%; max-height: 34px; width: auto; height: auto;
  object-fit: contain; display: block;
}
#about .cta { padding: 32px 6vw 60px; }

/* experiments */
#experiments .tagfilter { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 28px; }
#experiments .tagfilter button {
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 11px; letter-spacing: .16em; text-transform: uppercase;
  padding: 8px 16px; border-radius: 0; cursor: pointer;
  color: #d6d6d6; background: transparent; border: 1px solid rgba(255,255,255,.25);
  transition: all .2s cubic-bezier(.65,0,.35,1);
}
#experiments .tagfilter button:hover { border-color: rgba(255,255,255,.6); color: #fff; }
#experiments .tagfilter button.active { background: #ececec; color: #0a0a0b; border-color: #ececec; }
.expcard.hidden { display: none; }
#experiments .expgrid {
  display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 14px;
  padding: 0 6vw 60px;
}
.expcard { position: relative; cursor: pointer; overflow: hidden; }
.expcard img { width: 100%; aspect-ratio: 16/9; object-fit: cover; display: block;
  transition: transform .4s cubic-bezier(.65,0,.35,1); background: #131315; }
.expcard:hover img { transform: scale(1.04); }
.expcard .info {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 18px;
  background: linear-gradient(0deg, rgba(10,10,11,.85), rgba(10,10,11,0));
  display: flex; justify-content: space-between; align-items: flex-end; gap: 12px;
}
.expcard .info .t { font-weight: 800; text-transform: uppercase; font-size: clamp(18px, 2vw, 28px); }
.expcard .tags { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
@media (max-width: 820px) { #experiments .expgrid { grid-template-columns: 1fr; } }

#count {
  position: fixed; bottom: 76px; left: 22px; z-index: 10; font-size: 11px; opacity: .5;
  letter-spacing: .12em; text-transform: uppercase;
  font-family: ui-monospace, Menlo, monospace;
}

/* no-JS fallback */
.nojs { padding: 40px 6vw; }
.nojs a { color: #ececec; }

/* reduced motion: no smooth entrance moves */
@media (prefers-reduced-motion: reduce) {
  #hero .scrollhint { animation: none; }
  * { transition-duration: .01s !important; }
}

/* mobile overrides — kept LAST so they beat every base rule above */
@media (max-width: 768px) {
  #topnav { display: none !important; }
  #menuBtn { display: grid; }

  /* #logo is fixed-position with no width of its own, so the tag line ran the full viewport
     and slid under the menu button — measured at 375px: logo right edge 375, button left
     edge 311, so 64px of the tag sat beneath a 48px opaque button. The cap also shrinks the
     logo's hit area, which until now spanned the whole width and sent taps in the empty
     top-right strip to the home route.
     100px = 22px left offset + 48px button + 16px button right offset + 14px breathing gap.
     The looser tracking is what buys the second line: at .3em the first two segments are
     259px against 235px of room, so "Creative Technology" was pushed to a third line and
     the header grew 10px. At .2em they measure 230px and it holds two lines at 375px,
     degrading to one segment per line on a 320px phone. Mobile only — the desktop header
     has the full width and keeps .3em. */
  #logo { max-width: calc(100vw - 100px); }
  #logo small { letter-spacing: .2em; }
  #logo small span { white-space: nowrap; }
  #filters { display: none !important; }
  #filterBtn { display: inline-flex; }
  #contactForm .cfrow { grid-template-columns: 1fr; }

  /* Bottom bar: one uniform 42px control height across view-toggle / filter / chat.
     42px is #filterBtn's NATURAL height, so it is left untouched and the other two come
     to meet it — raising the filter instead would push the row closer to the home
     indicator on gesture-nav devices. Sizes are content-box (this sheet never sets
     border-box), so the declared height excludes the 1px borders:
       #chat        40 + 0 padding + 2 border = 42
       #viewToggle  32 button + 8 padding + 2 border = 42
     Only the toggle's HEIGHT is reduced; its 42px button width is kept so the tap target
     stays wide and `#viewToggle.list .knob`'s translateX(46px) (42 + 4 gap) still lands. */
  #chat {
    height: 40px; padding-top: 0; padding-bottom: 0;
    display: flex; align-items: center;
  }
  #viewToggle .knob, #viewToggle button { height: 32px; }

  /* Process grid drops 3-up -> 2-up: at phone width a third of the column is too small to
     read a setup photo, which defeats the point of showing it. */
  #project .process .pgrid { grid-template-columns: repeat(2, 1fr); }
}

/* No mobile override for the logo mark: a lower ceiling here would clamp it flat across
   560–768px and then jump ~30px at the breakpoint. The base rule (stretch + 262px cap)
   lets it scale continuously with the type at every width. */
