/* ─────────────────────────────────────────────
   worklist — the home page (index.html) styles.
   The hero (name/role/bio in the bar) is row −1 of the work index; the
   focus slot is the middle of the screen, and the 65% canvas on the right
   shows the focused project. See js/worklist.js for the mechanics.
───────────────────────────────────────────── */

/* ── ONE knob for the space between sections (content-to-label). The
      worklist's structural floor is 50vh + 21px (the pinned tail below the
      last resting row + label leading); the gap is set to 75% of that
      (Kosuke, 2026-07-13) — the career block's clamped negative margin
      pulls it up over the pin's blank tail to reach below the floor. ── */
:root { --section-gap: calc(50vh + 48px); }   /* the floor: the tightest gap where a focused row and Career never share the screen (the last row focuses at the mid-screen slot, so Career must clear the lower half-viewport). The +25vh canvas-fade runway was tried and retired — too airy (Kosuke, 2026-07-13). */

.worklist {
  position: relative;
  /* v3: the section owns the page from the very top — the bar (name + role)
     is row −1 of the list, resting in the MIDDLE, and the first scroll walks
     the focus straight to J.LEAGUE in that same middle slot. */
  margin-top: 0;
  background: var(--white);
  /* post-list runway (the pinned era's synthetic height provided this):
     matched so ABOUT crests the viewport just as the last row's zone ends,
     same rhythm as before the flow conversion */
  padding-bottom: 65vh;
}

/* ── INTRO SNAP — the first gesture is spent landing J.League in the focus
   slot, never an arbitrary distance. The whole list occupies ~340px of
   scroll while a trackpad fling is commonly 800-1500px, so at rest one
   flick used to clear the section outright and the reader had to come back
   up for the first project.

   Native CSS snapping, deliberately: the reference (yichenxie.com) is pure
   `scroll-snap-type: y mandatory` with no wheel handler and no JS easing,
   so matching the mechanism is what makes the curve identical — a
   hand-rolled ease could only approximate the browser's own.

   Exactly TWO stops, the hero rest and the first project, and nothing to
   rest on between them. That is also why JS owns the on/off (below):
   mandatory snapping with only these two stops would make the entire page
   past the first project unreachable. ── */
html.is-snap-intro { scroll-snap-type: y mandatory; }

.worklist__snap {
  position: absolute;
  left: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
  scroll-snap-align: start;
}
.worklist__snap--hero { top: 0; }
/* js/worklist.js overwrites this `top` from rowScrollY(0) — the same
   function the arrow keys, row clicks and the WORK anchor all land on, so
   the stop can never drift from where a focused row actually rests. The
   value here only has to be sane for the frame before the first
   measurement, and on the desktop metrics it already is: the rows begin at
   102vh and the focus line is a third of the way down. */
.worklist__snap--work { top: calc(102vh - 33.3vh - 16px); }

/* ── Top fade — the mirror of .worklist__info::before at the other end:
   rows dissolve as they walk up under the bar instead of being cut off by
   its edge. Over the index column only; the canvas owns the right 65% and
   keeps its own crisp top edge under the nav. ── */
.worklist__topfade {
  position: fixed;
  left: 0;
  top: 0;
  width: 35%;
  height: 120px;
  z-index: 3;
  pointer-events: none;
  background: linear-gradient(to bottom, var(--white) 34%, rgba(255, 255, 255, 0));
  opacity: 0;
  transition: opacity .45s ease;
}
.worklist.is-engaged .worklist__topfade { opacity: 1; }
.worklist.is-done .worklist__topfade { opacity: 0; }

.worklist__pin {
  /* FLOW, not a pinned frame (the ma5a conversion, 2026-08-22): the rows
     are ordinary page content and the compositor owns their motion. The
     old sticky/100vh/overflow pin — with a JS transform countering it —
     is what made scrolling feel a frame late and event-quantized. */
  position: relative;
  display: flow-root;          /* BFC: the rows' viewport-sized margin-top
                                  must not COLLAPSE through and shove the
                                  whole section's box down the page */
  background: var(--white);
}

/* ── rows — ordinary page content, scrolled by the browser itself:
      no transform, no JS in the motion path (the ma5a conversion) ── */
.worklist__rows {
  position: relative;
  width: 35%;                  /* the index owns the left 35%; the canvas owns the rest */
  /* J.LEAGUE rests one --hero-gap BELOW the bar's baseline (50vh): the
     runway between the hero and the list. Fully below the fold at rest;
     the first scroll reveals it. Tune the feel here — one knob. */
  --hero-gap: 52vh;
  margin-top: calc(50vh + var(--hero-gap, 0px));
  z-index: 1;
}

/* The highlight lives ON the focused row itself (project color background,
   window ink) — glued to the row, it can never misalign, wherever the
   scroll rests. Final call 2026-08-22: simple flat 1:1 scroll. */

.workrow {
  display: flex;
  gap: 16px;
  padding: 8px 24px 8px 96px;  /* right edge = 24px off the canvas seam */
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: .01em;
  line-height: normal;
  color: var(--black);
  text-decoration: none;
  opacity: var(--dim, 1);
  /* NO transition — like ma5a: the highlight and dims FLIP the instant the
     reading line crosses a row top. A crossfade here read as smear. */
}
.workrow__name { flex: 1 0 0; min-width: 0; transition: transform .2s cubic-bezier(.2, .8, .2, 1); }
.workrow__right { display: none; }   /* category + year moved to the preview info */

.workrow:not(.is-active):hover { opacity: .85; }
.workrow:focus,
.workrow:focus-visible { outline: none; }   /* no browser focus ring, ever */

.workrow.is-active {
  background: var(--row-color, var(--orange));
  color: var(--row-text, #fff);
}

/* ── canvas — the right half of the view, full height (runs under the frosted
      nav): the project's color background full-bleed with the gallery still
      contained on top, never cropped — the tile language at hero scale.
      Hidden until the first row gets focused (the pin), then fades in + rises. ── */
.worklist__preview {
  /* Fixed, not absolute: the sticky pin is a stacking context, which would
     trap this under the nav fade (z4) no matter its z-index. As a fixed
     element it resolves against the root, so z6 beats the fade and the
     image runs crisp under the nav text. Visibility is fully gated by
     is-engaged / is-done, so it only ever shows while the section is live. */
  position: fixed;
  left: 35%;                   /* the image takes 65% of the screen width */
  right: 0;
  top: 0;
  bottom: 0;
  background: var(--white);
  z-index: 6;                  /* above the nav fade — the image runs crisp under the nav text */
  opacity: 0;
  transform: translateY(28px);
  pointer-events: none;        /* inert while hidden */
  transition: opacity .45s ease, transform .6s cubic-bezier(.16, .84, .28, 1);
}
.worklist.is-engaged .worklist__preview {
  opacity: 1;
  transform: none;
  pointer-events: auto;        /* the canvas is a link to the focused project */
}
.worklist__media {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
/* dissolve layers — two stacked copies of the composition; the swap is a
   ~140ms crossfade, decode-gated by worklist.js (never a blank flash) */
.worklist__layer {
  position: absolute;
  inset: 0;
  opacity: 1;
  transition: opacity .14s ease;
}
.worklist__layer.is-back { opacity: 0; }
.worklist__bg {
  position: absolute;
  inset: 0;
  background-size: cover;      /* the color background can crop freely */
  background-position: center;
}
.worklist__flash {
  position: absolute;
  inset: 7%;                   /* breathing room around the still, like the tiles */
  background-size: contain;    /* always the full image — never cropped */
  background-repeat: no-repeat;
  background-position: center;
  /* Per-project size trim (--fg, set from the row's data-fg). A transform,
     not a background-size: `contain` resolves against whichever axis is
     limiting, and that axis changes with viewport aspect — a percentage
     size would mean something different on every screen. Scaling the box
     multiplies the fitted result itself, so 1.25 is 1.25 everywhere. The
     square's transparent margin may pass outside the canvas; the artwork
     stays inside it (verified per project). */
  transform: scale(var(--fg, 1));
  transform-origin: center;    /* centered box + centered origin = centered at
                                  every viewport aspect, scaled or not */
}
/* Product stills fit against the FULL canvas width (the mark projects keep the
   tighter all-round inset), so they read larger. The vertical inset stays
   symmetrical: an asymmetric box cannot center, and how far off-center it
   pushed the artwork changed with viewport aspect — which is why the stills
   drifted low on some screens and not others. */
.worklist__flash[data-mode="full"] {
  inset: 7% 0;
}
/* Bottom-anchored exception (Remotion): its artwork runs to the very bottom
   of its own square, so the still is composed to bleed off the canvas edge
   rather than sit in the middle. Dropping the bottom inset and anchoring the
   position keeps it flush whichever axis is limiting the fit — every other
   project stays centered. */
.worklist__flash[data-anchor="bottom"] {
  inset: 7% 0 0;
  background-position: center bottom;
  transform-origin: center bottom;
}

/* Info — bottom-left under the index, on the page's own white (no scrim games) */
.worklist__info {
  position: fixed;             /* the pin used to be its viewport; in flow it
                                  anchors to the real one (same rest point) */
  left: 96px;
  bottom: 64px;
  width: min(420px, calc(35% - 120px));
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 32px;
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: .01em;
  color: var(--black);
  opacity: 0;
  transform: translateY(12px);
  transition: opacity .45s ease, transform .6s cubic-bezier(.16, .84, .28, 1);
}
.worklist.is-engaged .worklist__info {
  opacity: 1;
  transform: none;
}
/* Invisible ≠ gone: at rest this block still sat over the walking rows and
   swallowed their clicks — its "View project" link (still pointing at the
   last-previewed project) opened the WRONG project when a row's text was
   clicked in the hero view. It only accepts input while it's actually on
   screen. */
.worklist__info { pointer-events: none; }
.worklist.is-engaged:not(.is-done) .worklist__info { pointer-events: auto; }
/* v3: the walking rows pass under this corner — a progressive white panel
   behind the info fades them out so the text always reads clean. */
.worklist__info::before {
  content: "";
  position: absolute;
  left: -96px;                 /* cover the full gutter behind the text */
  right: -48px;
  top: -72px;                  /* the fade begins above the title */
  bottom: -64px;               /* solid to the viewport edge below */
  background: linear-gradient(to top, var(--white) 78%, transparent);
  z-index: -1;
  pointer-events: none;
}
/* View project — the site's text-button voice: orange at rest, black with
   the orange blob sliding in behind on hover. */
.worklist__cta {
  position: relative;
  isolation: isolate;          /* keeps the z:-1 blob behind the label */
  align-self: flex-start;
  font-family: var(--font-mono-medium);   /* weight bump (a11y audit — the
                                  orange-on-white ratio stays 3.86:1, but the
                                  heavier face carries the small size better;
                                  Kosuke's call: weight, not size) */
  color: var(--orange);
  text-decoration: none;
  cursor: var(--cursor-pointer);
  transition: color .2s ease;
}
.worklist__cta:hover { color: var(--black); }
.worklist__cta::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: var(--blob-radius);
  background: var(--orange);
  transform: translate(-50%, -50%) scale(0);
  transition: transform .35s cubic-bezier(.2, .8, .2, 1);
  z-index: -1;
}
.worklist__cta:hover::before { transform: translate(-50%, -50%) scale(1); }

/* Past the last row (2D1 has passed the bar): the canvas + info fade
   out while the section scrolls away. After .is-engaged so it wins. */
.worklist.is-done .worklist__preview,
.worklist.is-done .worklist__info {
  opacity: 0;
  pointer-events: none;
}
.worklist__head { display: flex; flex-direction: column; gap: 8px; }
.worklist__title { margin: 0; font-family: var(--font-mono-medium); }
.worklist__desc { margin: 0; }
/* role · year — styled like the project pages' .project__meta, muted */
.worklist__meta {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: .08em; /* the one caps tracking */
  text-transform: uppercase;
  color: var(--gray-300);
}
.topnav__brand .hero__name,
.hero__name {
  font-family: var(--font-mono-medium);
}
.topnav .topnav__role,
.hero__role,
.about-bio,
.exp-row__desc,
.exp-row__company,
.exp-row__role {
  font-family: var(--font-mono);
}
.topnav__brand .hero__name,
.topnav .topnav__role { letter-spacing: 0; }
/* Role matches the name's weight — both Rand Mono Medium (the identity line
   reads as one unit). main.css sets the role proportional Rand Regular for
   project pages; this puts the home role on the same mono-medium family. */
.topnav .topnav__role { font-family: var(--font-mono-medium); }
.about-bio {
  font-size: 13px;
  letter-spacing: .01em;
  line-height: 1.6;
}

.topnav__contact,
.topnav-contact { display: none; }

/* Page anchors — fixed at the top-right corner, stacked vertically, uppercase.
   Same voice as the old contact links (11px mono, black, orange blob sliding
   in behind on hover). Flip white over dark canvases together with the role
   (worklist.js toggles is-on-dark). */
html { scroll-behavior: smooth; }
/* (Native scroll-snap was tried for the picker settle and REMOVED: Chromium
   re-snaps on style/layout changes and can re-target a remembered snap
   position — including the page top — which read as the list teleporting.
   The settle is pure JS now: worklist.js trySettle glides to the nearest
   row once the scroll goes quiet inside the picker's own domain.) */
/* ── Home-only: the bar's ride is NATIVE — position: sticky. The old ride
   wrote the bar's position from JS on every scroll frame, and iOS Safari
   delivers scroll events decoupled from its composited scroll: the bar
   trailed the finger through the whole hero stretch and read as lag. Sticky
   is the same contract (rest mid-viewport, ride up 1:1 with the page, pin
   at the top) run entirely by the compositor — JS only flips classes now.
   The bar joins the FLOW to do it: margin-top sets its rest, height pins
   its box (every child is absolute, so nothing else sizes it), and the
   negative margin-bottom (= −(margin-top + height)) zeroes its footprint —
   everything after it keeps its exact position. svh = the small viewport
   (toolbar showing), the height the rest is designed against; the vh lines
   are the no-svh fallback. */
/* v5 (2026-09-26): the bar RIDES the scroll again — and never sticks.
   Name, role and bio travel up 1:1 with the page and leave the top of the
   screen like any other content. The identity does not GLIDE into the bar
   at the end of that journey; it TELEPORTS, arriving as the intro snap
   settles and the first project takes the slot.

   Relative, not sticky: sticky would catch the bar at the top edge and
   glide it in, which is exactly the motion being replaced. The flow trick
   is the pre-v4 one — margin-top sets the rest, height pins the box (every
   child is absolute, so nothing else sizes it), and the negative
   margin-bottom zeroes the footprint so everything after it keeps its
   exact position. svh = the small viewport (toolbar showing), the height
   the rest is designed against; the vh lines are the no-svh fallback. */
.topnav {
  position: relative;
  top: auto;
  height: 48px;
  margin-top: calc(50vh - 48px);
  margin-top: calc(50svh - 48px);
  margin-bottom: calc(0px - 50vh);
  margin-bottom: calc(0px - 50svh);
}
/* The arrival. Position is not animatable, so the jump hides inside a fast
   opacity dip: out in 60ms, the box moves while nothing is on screen, back
   in over 60ms. Reads as instant without the one-frame flicker a hard cut
   gives on a high-refresh display, and it runs the same way in reverse. */
.topnav.is-teleported {
  position: fixed;
  top: 0;
  margin: 0;
}
.topnav__brand,
.topnav .topnav__role {
  transition: opacity .06s linear;
}
.topnav.is-teleporting .topnav__brand,
.topnav.is-teleporting .topnav__role {
  opacity: 0;
}
/* The bio makes the same exit and does NOT come back — there is nowhere in
   a 48px bar for three lines of paragraph to land. */
.topnav.is-teleported .topnav__bio {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
.topnav__anchors {
  position: fixed;             /* locked in the corner — where index.html keeps its controls */
  top: 8px;
  right: 8px;
  z-index: 91;                 /* above the canvas (z6) and nav (z90) */
  display: flex;
  flex-direction: column;
  align-items: flex-end;       /* right-aligned stack */
  gap: 0;
  pointer-events: auto;
}
.topnav__anchor {
  position: relative;
  isolation: isolate;
  padding: 3px 0;
  color: var(--black);
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: normal;
  letter-spacing: .08em; /* the one caps tracking */
  text-transform: uppercase;
  white-space: nowrap;
  text-decoration: none;
  transition: color .2s ease;
}
.topnav__anchor::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: var(--blob-radius);
  background: var(--orange);
  transform: translate(-50%, -50%) scale(0);
  transition: transform .35s cubic-bezier(.2, .8, .2, 1);
  z-index: -1;
}
.topnav__anchor:hover::before { transform: translate(-50%, -50%) scale(1); }
.topnav__anchors.is-on-dark .topnav__anchor { color: #fff; }
/* The MUTE toggle is a <button> wearing the anchor voice — strip the UA
   button chrome. It sits alone in the BOTTOM-right corner (fixed, the
   mirror of the anchors above); the container's is-on-dark still cascades
   to it, so it inverts over dark canvases like everything else. */
.topnav__mute.is-on-dark { color: #fff; }
.topnav__mute {
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  margin: 0;
  text-align: right;
  position: fixed;
  right: 8px;
  bottom: 8px;
  z-index: 91;                 /* above the canvas, same layer as the anchors */
}


/* Contact — a stacked list: CONTACT label, email, then the socials, one per
   line. Each item is black with the orange blob sliding in behind on hover
   (same voice as the old nav contact links); the ↗ rotates. The email keeps
   its own copy-orb interaction (main.js), so its plain blob is suppressed. */
.outro__contact {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* 4px between the links themselves; the label gets the same 16px breath
     the CAREER label has (.about-block__inner gap) — see the label rule below */
  gap: 4px;
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: .01em;
  line-height: 1.6;
}
.outro__label {
  margin: 0 0 12px;          /* + the container's 4px gap = 16px, matching CAREER */
  font-family: var(--font-mono-medium);
  text-transform: uppercase;
  letter-spacing: .08em; /* the one caps tracking */
  color: var(--black);
  font-size: 13px;
  line-height: 1.5;
}
.outro__item {
  position: relative;
  isolation: isolate;          /* keeps the z:-1 blob behind the text, above the page */
  width: fit-content;
  color: var(--black);
  text-decoration: none;
  transition: color .2s ease;
}
.outro__item::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: var(--blob-radius);
  background: var(--orange);
  transform: translate(-50%, -50%) scale(0);
  transition: transform .35s cubic-bezier(.2, .8, .2, 1);
  z-index: -1;
}
.outro__item:hover::before { transform: translate(-50%, -50%) scale(1); }
.outro__item span { display: inline-block; transition: transform .25s ease; }
.outro__item:hover span { transform: rotate(45deg); }
/* Socials — one inline row, smaller than the email */
.outro__socials {
  display: flex;
  gap: 24px;
  margin-top: 8px;             /* 4px column gap + 8px = 12px below the email */
}
.outro__socials .outro__item { font-size: 11px; }
/* Email: orange at rest; its rich copy-orb (main.js) owns the hover/copy
   visual — suppress the plain blob so there's never two orange shapes, and
   re-point the copied-state color (main.css keyed it to .outro__copy a). */
.outro__email { color: var(--orange); }
.outro__email::before { display: none; }
.outro__email.is-copy-active { color: var(--black); }

/* ── Experience — clean left align: the timeline (line + visible dots) goes,
      the company/role column starts on the 96px edge, and the year moves to
      the right, just left of the plus. The dots stay in the layout as
      invisible anchors hanging in the gutter, so the existing hover blob
      (main.js slides it between rows) lands left of exp-row__info — sized up
      to match the other text-link blobs. ── */
.exp-list { --text-x: 0px; }             /* expanded bodies align with the header content */
.exp-list::before { display: none; }     /* the vertical timeline */
.exp-row__dot {
  background: transparent;               /* invisible, but still the blob's anchor */
  margin-left: calc(-1 * (var(--dot-size) + var(--row-gap)));   /* hangs in the gutter; info lands on the 96px edge */
}
.exp-row__header { position: relative; }
#experience .exp-row__date { display: none; }   /* the year lives in the role line now (worklist.js merges it) */
/* Role · year — same voice as worklist__meta */
#experience .exp-row__role {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: .02em;
  text-transform: none;        /* authored casing, e.g. "Lead Designer · 2024~" */
  color: var(--gray-300);
  transition: color .25s ease;
}
/* Row hover flips the role orange too (the gray base above outranks main.css's
   plain hover rule, so re-assert it here at #experience specificity) */
#experience .exp-row__header:hover .exp-row__role { color: var(--orange); }
#experience .exp-row__details { flex: 0 1 auto; }   /* hug the text — the chevron sits right next to it */
.exp-list__blob { display: none; }       /* no hover blob — the orange text is the hover state */

/* The plus is a right chevron — hidden until the row is engaged (DESKTOP
   only; mobile keeps it always visible — see the mobile block): it appears
   on hover and stays on the OPEN row, where it morphs into a close (✕).
   Both states are the same two bars, so the change glides. Color rides
   currentColor (row hover orange via main.css). */
#experience .exp-row__plus {
  position: absolute;          /* out of the row flow — hangs in the LEFT gutter */
  left: -20px;                 /* header starts on the 96px edge; icon centers ≈78px */
  top: 22px;                   /* centered on the company line (12px padding + half its 19.5px line box) */
  margin-top: -6px;
  width: 12px;
  height: 12px;
  opacity: 0;                  /* hidden at rest — only the engaged row shows it */
  color: var(--gray-200);      /* light muted; orange on row hover */
  transition: opacity .2s ease, color .25s ease;
}
#experience .exp-row__header:hover .exp-row__plus { opacity: 1; }
#experience .exp-row__plus svg { display: none; }
#experience .exp-row__plus::before,
#experience .exp-row__plus::after {
  content: "";
  position: absolute;
  left: calc(50% - 2.625px);
  top: calc(50% - 0.5px);
  width: 5.25px;               /* 25% smaller */
  height: 1px;                 /* slightly thinner */
  background: currentColor;
  transition: transform .25s cubic-bezier(.2, .8, .2, 1), width .25s ease, left .25s ease;
}
#experience .exp-row__plus::before { transform: translateY(-1.65px) rotate(45deg); }
#experience .exp-row__plus::after  { transform: translateY(1.65px) rotate(-45deg); }
/* open: always visible, and the chevron becomes a ✕
   (#experience-prefixed so these outrank the ID-scoped base rules) */
#experience .exp-row.is-open .exp-row__plus { opacity: 1; }
#experience .exp-row.is-open .exp-row__plus::before,
#experience .exp-row.is-open .exp-row__plus::after {
  left: calc(50% - 4.125px);
  width: 8.25px;               /* 25% smaller */
}
#experience .exp-row.is-open .exp-row__plus::before { transform: rotate(45deg); }
#experience .exp-row.is-open .exp-row__plus::after  { transform: rotate(-45deg); }

/* While a row is open, the REST of the record recedes — company AND role
   drop to the light muted gray, so the active row is the only full-ink
   text on the left, pairing it with the panel's black text across the gap
   (the worklist's focused/receded grammar). Desktop only: the mobile
   inline accordion keeps its plain rows. Hover still lifts a receded row
   to orange (re-asserted to outrank the recede's specificity). */
@media (min-width: 840px) {
  #experience .exp-list:has(.exp-row.is-open) .exp-row:not(.is-open) .exp-row__company,
  #experience .exp-list:has(.exp-row.is-open) .exp-row:not(.is-open) .exp-row__role {
    color: var(--gray-200);
  }
  #experience .exp-list:has(.exp-row.is-open) .exp-row:not(.is-open) .exp-row__header:hover .exp-row__company,
  #experience .exp-list:has(.exp-row.is-open) .exp-row:not(.is-open) .exp-row__header:hover .exp-row__role {
    color: var(--orange);
  }
}

/* ── Experience detail panel — no accordion. Clicking a row shows its
      description + thumbs in a panel on the RIGHT of the list, top-aligned
      with the selected row; the list itself never moves. The reveal speaks
      the site's panel language (fade + rise, like the canvas), content swaps
      instantly, and the panel glides between rows. main.js still owns the
      is-open state (exclusivity, aria, plus → minus). ── */
#about .about-block--first { padding-bottom: 0; }
/* Section gaps — all derived from --section-gap (see the top of this file).
   Worklist→CAREER: no negative pull-up (that's what dragged Career on-screen
   under a still-lit row). Career simply flows below the pinned section; its
   padding pushes the label the rest of the way past the mid-screen slot so it
   clears the fold while the last row is focused. 8px = half a row's overhang
   below the slot center, so the perceived gap equals --section-gap. */
#about .about-block--first {
  padding-top: calc(var(--section-gap) - 50vh - 22px);
}
/* Anchor landings keep the CAREER label clear of the pinned 48px bar even if
   the paddings above ever change again (the old negative-margin gap pulled
   the label under the nav at exactly this landing). */
#about, #contact, #work { scroll-margin-top: 48px; }
/* Career→CONTACT: the record ends at its last row (main.css's --last 160px
   tail folded into the variable), the outro opens with exactly one gap. */
#experience { padding-bottom: 0; }
.outro { padding-top: var(--section-gap); padding-bottom: 256px; }   /* bottom decoupled from the knob — the foot sits at the original fixed breath */
/* Experience is folded into About — no sub-label (the record announces
   itself), just a hairline and a breath after the bio. */
#experience {
  padding-top: 16px;           /* a tight breath under the EXPERIENCE header */
  position: relative;          /* the detail panel positions against this block */
  transition: min-height .35s ease;   /* grows softly for tall thumb sets */
}
#experience .exp-row__body { display: none; }   /* details live in the panel, never in-flow */
.exp-detail {
  position: absolute;
  left: 50%;
  right: 96px;
  opacity: 0;
  transform: translateY(12px);
  transition: opacity .45s ease, transform .6s cubic-bezier(.16, .84, .28, 1), top .35s ease;
  pointer-events: none;
}
.exp-detail.is-on {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
.exp-detail__desc {
  margin: 0 0 24px;            /* full panel width — matches the thumbs stage below */
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: .01em;
  line-height: 1.6;
  color: var(--black);
}
/* Same presentation as the accordion's thumbs: one 8/5 stage, slides
   crossfading via .is-active (cycled by worklist.js) */
.exp-detail__thumbs {
  position: relative;
  aspect-ratio: 8 / 5;
  background: var(--gray-100);
  overflow: hidden;
}
.exp-detail__thumbs:empty { display: none; }
/* Outbound links in the panel — the site's link voice: orange blob scaling
   in behind, the ↗ spinning 45°, color stays black. */
.exp-detail__links { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; margin-top: .5em; pointer-events: auto; }   /* the panel is inert (decorative), but its links are real; .5em under the thumbs */
/* Inline accordion (mobile): same breath under the thumbs — the thumbs'
   own 32px bottom margin would collapse OVER the .5em, so it steps aside
   when links follow. */
.exp-row__thumbs + .exp-row__links { margin-top: .5em; }
.exp-row__thumbs:has(+ .exp-row__links) { margin-bottom: .5em; }
.exp-detail__links:empty { display: none; }
.exp-detail__links a {
  position: relative;
  isolation: isolate;              /* keeps the z:-1 blob behind the label */
  font-family: var(--font-mono);
  font-size: 11px;                 /* matches the Contact socials */
  letter-spacing: .01em;
  line-height: 1.6;
  color: var(--black);
  text-decoration: none;
}
.exp-detail__links a::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: var(--blob-radius);
  background: var(--orange);
  transform: translate(-50%, -50%) scale(0);
  transition: transform .35s cubic-bezier(.2, .8, .2, 1);
  z-index: -1;
}
.exp-detail__links a:hover::before { transform: translate(-50%, -50%) scale(1); }
.exp-detail__links a span {
  display: inline-block;
  transition: transform .25s cubic-bezier(.2, .8, .2, 1);
}
.exp-detail__links a:hover span { transform: rotate(45deg); }

/* ── Collapsible bio — only the first sentence at rest; a "READ MORE" text
      button below it expands the rest (0fr → 1fr, like exp-row bodies) and
      relabels to "HIDE" while open. ── */
.about-bio__first p { margin: 0; }
.about-bio__more {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.35s ease;
}
.about-bio__more-inner { overflow: hidden; }
.about-bio.is-open .about-bio__more { grid-template-rows: 1fr; }
.about-bio__more-inner p { margin: 22px 0 0; }   /* keeps the bio's own paragraph rhythm */
.about-bio__toggle {
  position: relative;
  isolation: isolate;          /* keeps the z:-1 blob behind the label */
  display: block;              /* its own line, below the bio */
  margin: 16px 0 0;
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: .08em; /* the one caps tracking */
  text-transform: uppercase;
  color: var(--orange);
  cursor: var(--cursor-pointer);
  transition: color .2s ease;
}
/* Hover: black label + the orange blob sliding in behind (site link voice) */
.about-bio__toggle:hover { color: var(--black); }
.about-bio__toggle::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: var(--blob-radius);
  background: var(--orange);
  transform: translate(-50%, -50%) scale(0);
  transition: transform .35s cubic-bezier(.2, .8, .2, 1);
  z-index: -1;
}
.about-bio__toggle:hover::before { transform: translate(-50%, -50%) scale(1); }

/* ── The cursor never changes — the site's plain default arrow on every
      v2-built link (anchors are <a>, so the UA hand would show otherwise) ── */
.outro__item,
.topnav__anchor,
.workrow,
.worklist__preview { cursor: var(--cursor-pointer); }

/* The outro's plain text keeps the arrow — no I-beam — but the hand is
   for the links alone. (This rule blanketed the section in
   var(--cursor-pointer) back when that variable meant "the site's plain
   arrow"; once it became the literal `pointer`, the whole section read
   as clickable.) */
.outro,
.outro * { cursor: default; }
.outro .outro__item,
.outro .outro__item * { cursor: var(--cursor-pointer); }   /* the ↗ glyph too */

/* ── Alignment: the page's fixed furniture matches the worklist gutter (96px).
      The brand and role are absolutely positioned in main.css (left:16px / 50%),
      so they need their own overrides — the bar's padding doesn't move them. ── */
.topnav { padding-left: 96px; padding-right: 96px; }
.topnav__brand { left: 96px; }
.topnav .topnav__role {
  left: 50%;                   /* at half the screen width */
  transition: color .25s ease;
}
/* Over a dark canvas the role flips white (worklist.js reads the row's
   data-dark flag) so it's legible at all times. */
.topnav .topnav__role.is-on-dark { color: #fff; }
.corner-blob { transform: translate(96px, 16px); }

/* No nav background — content dissolves where it meets the nav instead.
   A fixed gradient sits just under the nav (z4): rows, About, Outro, and the
   footer (all z<4) fade out against it; the canvas (z6) runs crisp beneath
   the nav text, with the role's white flip handling legibility there. */
.topnav.is-stuck {
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.v2-navfade {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 116px;
  background: linear-gradient(to bottom, var(--white) 48px, transparent);
  z-index: 4;
  pointer-events: none;
  opacity: 0;                  /* dormant until the nav is fixed at the top */
  transition: opacity .3s ease;
}
body:has(.topnav.is-stuck) .v2-navfade { opacity: 1; }
/* About / Experience / Outro / foot trail share the worklist's left gutter
   (main.css puts them at the 50% column; v2 is one flush-left table) */
.about-block { padding-left: 96px; padding-right: 96px; }
.outro { padding-left: 96px; padding-right: 96px; }
.foot { padding-left: 96px; padding-right: 96px; }

/* ── Detent — the visual layer of the picker "click": worklist.js runs a
      tiny lateral nudge on the landing row via WAAPI (audio tick and
      vibration ride along there too). ── */


/* ── The focused row points at its project — a → on the right edge ── */
.workrow { position: relative; }
.workrow.is-active::after {
  content: "→";
  position: absolute;
  right: 24px;
  top: 50%;
  transform: translateY(-50%);
  transition: transform .2s cubic-bezier(.2, .8, .2, 1);
}
/* Hover the focused row — nudge its title and arrow a touch to the right */
.workrow.is-active:hover .workrow__name { transform: translateX(4px); }
.workrow.is-active:hover::after { transform: translate(4px, -50%); }

/* ── Blob quote — poke the blob and it speaks: it shrinks (rather than
      fully ducking into its hole) while the line types out beside it
      (worklist.js runs the glyph resolve) ── */
.corner-blob.is-hiding:not(.is-scrolled-away) .corner-blob__hole { transform: scale(0.45); }
.blobquote {
  position: fixed;
  z-index: 7;
  /* top-aligned to the blob (JS sets top = blob top); long quotes wrap
     downward and never rise above it */
  transform: none;
  /* JS sets left (beside the blob's hole); the right edge stops well short
     of the corner anchors so long lines WRAP instead of running under
     WORK/CAREER/CONTACT — the project hero description's treatment. */
  right: 148px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: .01em;
  line-height: 1.6;            /* reading leading once it can wrap */
  color: var(--black);
  pointer-events: none;
  opacity: 0;
  transition: opacity .2s ease;
}
.blobquote.is-on { opacity: 1; }

/* ── Role line: hoverable (main.css sets pointer-events: none on it) ── */
.topnav .topnav__role { pointer-events: auto; }
/* ── The hero row's second preview line — the bio, riding the bar right
      below the role (a child of .topnav, so bar + role + bio move as one
      unit). Same instant swap contract as the role. ── */
.topnav .topnav__bio {
  position: absolute;
  left: 50%;
  top: 61.5px;                 /* bar height 48 + a 13.5px breath (−25% again) */
  width: min(40vw, calc(50vw - 96px));
  pointer-events: auto;
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: .01em;
  line-height: 1.6;
  color: var(--black);
}
/* The swap is a FADE now, not a cut (Kosuke, 2026-09-26): coming back up
   from the list the hero used to pop into place. visibility is delayed out
   so the fade is visible, and restored instantly on the way in. */
.topnav .topnav__bio,
.topnav .topnav__role {
  transition: opacity .32s ease, visibility 0s linear 0s;
}
.topnav .topnav__bio.is-role-off {
  opacity: 0;
  visibility: hidden;          /* hidden = untouchable — an invisible fixed
                                  paragraph must never swallow taps/clicks
                                  on the content beneath it */
  pointer-events: none;
  transition: opacity .32s ease, visibility 0s linear .32s;
}
/* While a project holds the middle slot, the hero's "preview" (the role)
   is swapped out — instantly, the way the project previews swap. */
.topnav .topnav__role.is-role-off {
  opacity: 0;
  visibility: hidden;          /* same contract as the bio: hidden = untouchable */
  pointer-events: none;
  transition: opacity .32s ease, visibility 0s linear .32s;
}

/* The blob leaves whenever a project holds the slot — the canvas owns the
   view then (and is-scrolled-away's >40px trigger misses the first focus,
   which lands at exactly 40). Covers returning from a project mid-list. */
body:has(.worklist.is-engaged) .corner-blob__hole { transform: scale(0); }

/* ── Engaged state (desktop) — the hero name RIDES UP with the bar and
   stays pinned top-left while a project holds the slot (it used to fade
   out here; Kosuke wants the identity present over the picker). The
   anchors + MUTE stay ABOVE the canvas (z91 vs the canvas's z6) and
   is-on-dark flips them white over dark canvases. ── */

/* ── Screensaver — idle long enough and the screen gradually pixelates:
      grid-aligned translucent cells mosaic the page over (the content reads
      through the ink). Activity releases every pixel into a gravity fall. ── */
.saver {
  position: fixed;
  inset: 0;
  z-index: 150;
  pointer-events: none;
}
.saver__px {
  position: absolute;
  animation: px-in .12s ease both;
}
@keyframes px-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.saver.is-falling .saver__px {
  animation: px-fall var(--fall-d, .7s) cubic-bezier(.5, 0, .85, .5) var(--fall-delay, 0s) both;
}
@keyframes px-fall {
  from { transform: none; }
  to   { transform: translateY(120vh) rotate(var(--fall-rot, 0deg)); }
}

/* ── small screens: the panes flip for the thumb — the CANVAS owns the top
      half, the ROWS own the bottom half (scroll + tap in the reach zone).
      The hero (name / role / bio) stacks in the top half at rest; once a
      project takes the slot the preview covers that space and the corner
      text flips white over dark canvases (same signal as desktop). ── */
@media (max-width: 839px) {
  .workrow { padding: 8px 16px; }
  /* The container RESTS with the NAME's top edge ON the midline (Kosuke,
     2026-09-01) and the scroll walks it to 0 (name pins at 16, like
     desktop). 16 = the name's offset inside the container, so bar rest =
     50% − 16. Same sticky flow trick as desktop: footprint = margin-top
     + 48 height + margin-bottom = 0. */
  .topnav { padding-left: 16px; padding-right: 16px; border-bottom: 0; }
  .topnav {
    margin-top: calc(50vh - 16px);
    margin-top: calc(50svh - 16px);
    margin-bottom: calc(-32px - 50vh);
    margin-bottom: calc(-32px - 50svh);
  }
  /* The engagement snap — the moment a row takes the line, the name is AT
     the top (the canvas owns the top half then). Fixed, not sticky: on
     phones the bar has pinned naturally by this point anyway; on tall
     tablets this jumps it the rest of the way. Its flow footprint is zero,
     so leaving the flow shifts nothing. */
  /* is-snapped is retired — the teleport puts the bar at the top on every
     screen, so there is nothing left to jump the rest of the way. */
  .corner-blob { transform: translate(16px, 16px); }
  .about-block { padding-left: 16px; padding-right: 16px; }
  .about-block__inner { max-width: 100%; }   /* fill the container */
  .outro { padding-left: 16px; padding-right: 16px; }
  .foot { padding-left: 16px; padding-right: 16px; }

  /* An OPEN career row gets extra air below it, so the expanded content
     doesn't crowd the next row. Closed rows keep the tight rhythm. */
  .exp-row.is-open { margin-bottom: 30px; }

  /* quote hangs below the corner blob here (worklist.js positions it);
     the right edge stops short of the WORK/CAREER/CONTACT column so the
     two can never touch. 80, not the old 112: the anchors start at x=314 on
     a 375 screen, so 112 left 51px of air doing nothing and broke the short
     lines — the Yokoo one wrapped on every iPhone but the Pro Max. At 80 it
     fits on one line down to 375 and still clears the column by ~20px. */
  .blobquote { transform: none; right: 80px; }

  /* While the bio is OPEN the bar (which carries it) paints solid: the
     rows walk up 1:1 with the scroll and used to slide straight through
     the fixed bio text. Now they pass behind an opaque panel (topnav z90
     vs rows z1) and re-emerge below it. */
  body:has(.about-bio.is-open) .topnav { background: var(--white); }

  /* the canvas foreground steps up 1.125x in the stacked view (multiplies
     any per-project data-fg scale; was 1.25, brought down 10%) */
  .worklist__flash { transform: scale(calc(var(--fg, 1) * 1.125)); }
  .exp-row { transition: margin-bottom .35s ease; }

  /* The anchors drop to the name's line: WORK's text top (13px box + 3px
     padding) meets the pinned name's 16px — one shared top line. */
  .topnav__anchors { top: 13px; }

  /* Hero stack: name / role / bio, anchored to the BOTTOM of the top half.
     These offsets are inside the traveling .topnav container — worklist.js
     rests it at 50vh − 206px and scroll walks it to 0, so at rest the
     collapsed bio's bottom kisses 50vh − 24 and after scrolling the name
     pins 16px from the top, like desktop. (206 = 16 name-top + 23 name +
     49 role+gap + 94 collapsed bio + 24 breath; role→bio gap 28 matches
     the desktop rhythm.) */
  .topnav__brand { left: 16px; top: 16px; transform: none; height: auto; }
  .topnav .topnav__role { left: 16px; top: 39px; height: auto; width: auto; }
  .topnav .topnav__bio {
    left: 16px;
    top: 70px;                 /* role bottom + 16 — a tighter mobile breath */
    width: calc(100vw - 32px);
  }
  .about-bio__toggle { margin-top: 2rem; }   /* a breath above About me / HIDE */
  /* The About-me invitation doesn't ride to the top with the identity — it
     fades out as the bar scrolls up (is-off-top lands at 40px) and back in
     at the rest. It STAYS while the bio is open: the same button is the
     Hide control there, and the open-bio auto-scroll would otherwise fade
     the very control that closes it. */
  .about-bio__toggle { transition: color .2s ease, opacity .3s ease; }
  body.is-off-top:not(:has(.about-bio.is-open)) .about-bio__toggle {
    opacity: 0;
    pointer-events: none;
  }
  /* Small screens carry no resting bio — just the "About me" invitation.
     Opening reveals the whole thing (lead paragraph included); the rows
     step down by the full reveal (bioCollapse measures both parts). */
  .about-bio__first { display: none; }
  /* Opening pushes the text a paragraph-beat below the role (the 22px
     rhythm); no bottom margin — the more-block's first paragraph carries
     the same 22px, so every gap inside the reveal reads identical. The
     collapsed layout is untouched (the lead is display:none there). */
  .about-bio.is-open .about-bio__first { display: block; margin: 22px 0 0; }

  /* Worklist: preview TOP half, rows BOTTOM half. Rows start 48px under the
     midline so the focus slot (center 50vh + 24px) sits right below the
     canvas edge; walked-past rows slide UNDER the opaque preview. */
  .worklist__preview {
    left: 0;
    right: 0;
    top: 0;
    bottom: 50%;
  }
  .worklist { padding-bottom: 32vh; }   /* the stacked rhythm's tighter tail */
  .worklist__rows {
    width: 100%;
    /* The hero→worklist breath MATCHES the worklist→career rhythm (Kosuke,
       2026-08-22): career's visible gap = the section's 32vh tail + the
       about block's 25vh+50px head = 57vh + 50px. The hero cluster (name/
       role/bio toggle) bottoms out around 52vh on phones, so the rows
       start one full career-gap below it. --rows-shift still carries the
       open bio's height, so the list steps down as one unit. (+80px: the
       bar's rest dropped by that much when the name moved to the midline —
       the cluster bottoms out lower, and the gap rides along.) */
    --stack-gap: calc(57vh + 50px);
    margin-top: calc(52vh + 80px + var(--stack-gap) + var(--rows-shift, 0px));
    transition: margin-top .35s ease;   /* same curve as the bio's 0fr→1fr */
  }

  /* ── Tablet: give the canvas more height ──
     The still is fitted with `contain`, so it sizes to the canvas's SHORT axis.
     A phone's top half is nearly square (390x422) and fits efficiently, but a
     portrait tablet's is wide and short (756x512 at 768px), so the same image
     rendered at only 57% of the canvas width — the one width where the stills
     read as small. A 62/38 split makes the box squarer and takes it to ~71%,
     in line with every other breakpoint. Phones keep 50/50: their canvas is
     already square, and stealing height there would only shrink the artwork. */
  @media (min-width: 600px) {
    .worklist__preview { bottom: 38%; }
    /* rest point below the taller tablet canvas; focus stays at ~63%
       via the same layout() gap as phones */
    .worklist__rows { margin-top: calc(45vh + 80px + var(--stack-gap) + var(--rows-shift, 0px)); }   /* tablet hero sits higher (+80px: same rest drop as phones) */
  }
  /* While the bio is OPEN the list stays stepped down by the reveal height
     (--bio-extra, measured by bioCollapse) — worklist.js defers the picker
     by the same amount, so the scroll reads the whole bio first and the
     rows never jump when a row finally takes the slot. */
  body:has(.about-bio.is-open) .worklist__rows {
    --rows-shift: var(--bio-extra, 0px);
  }
  .worklist__info { display: none; }

  /* The brand sits over the canvas here (full-width top half) — flip it
     with the same signal the role carries. */
  .topnav:has(.topnav__role.is-on-dark) .topnav__brand .hero__name { color: #fff; }
  /* The name's arrival at the top is TIMED: it waits out the corner blob's
     exit (.34s hole scale-down), then fades in — a handoff, not a pile-up. */
  body:has(.worklist.is-engaged) .topnav__brand {
    animation: brand-arrive .25s ease .34s both;
  }
  /* No sound on mobile — no MUTE either; interactions speak through the
     vibration motor instead (see pressedState in worklist.js). */
  .topnav__mute { display: none; }

  /* Worklist→CAREER: the mobile slot rests at 75vh, so the pinned tail is
     25vh − 15.5px — the 37.5vh + 15.75px gap needs 12.5vh + 18px on top
     (margin-top clears the desktop pull-up). */
  #about .about-block--first { padding-top: calc(25vh + 50px); }   /* slot at 75vh → clear the 25vh below it, gap = --section-gap */
  .about-content { margin-top: 0; }   /* the desktop pull-up doesn't apply here */

  /* Experience: the side panel is a desktop layout — the classic in-flow
     accordion returns (worklist.js skips the panel module under 720px) */
  #experience .exp-row__body { display: grid; }
  /* The chevron moves out of the left gutter to the row's RIGHT edge (the
     worklist arrow's spot), always visible, black. The open ✕ morph and
     orange row-hover ride the same currentColor as before. */
  #experience .exp-row__plus {
    left: auto;
    right: 0;
    opacity: 1;
    color: var(--gray-200);    /* same light muted voice as desktop */
  }
  /* ...and it points DOWN (the accordion opens below); the open ✕ morph
     keeps its higher-specificity rules. */
  #experience .exp-row__plus::before { transform: translateX(-1.65px) rotate(45deg); }
  #experience .exp-row__plus::after  { transform: translateX(1.65px) rotate(-45deg); }
  .workrow.is-active::after { right: 16px; }   /* arrow rides the 16px gutter */


  /* ── Touch: hover becomes the PRESSED state. Each desktop :hover is
        re-declared at its rest value (so the sticky post-tap hover shows
        nothing), and the pressed style rides :is(:active, .is-pressed) OR .is-pressed (a JS
        touch shim — :is(:active, .is-pressed) alone is unreliable under touch heuristics).
        Released or scrolled, everything returns. ── */
  /* work rows */
  .workrow:not(.is-active):hover { opacity: var(--dim, 1); }
  .workrow:not(.is-active):is(:active, .is-pressed) { opacity: .85; }
  .workrow.is-active:hover .workrow__name { transform: none; }
  .workrow.is-active:hover::after { transform: translateY(-50%); }
  .workrow.is-active:is(:active, .is-pressed) .workrow__name { transform: translateX(4px); }
  .workrow.is-active:is(:active, .is-pressed)::after { transform: translate(4px, -50%); }
  /* text buttons (anchors, mute, CTA, bio toggle) — blob pops while pressed */
  .topnav__anchor:hover::before { transform: translate(-50%, -50%) scale(0); }
  .topnav__anchor:is(:active, .is-pressed)::before { transform: translate(-50%, -50%) scale(1); }
  .worklist__cta:hover { color: var(--orange); }
  .worklist__cta:hover::before { transform: translate(-50%, -50%) scale(0); }
  .worklist__cta:is(:active, .is-pressed) { color: var(--black); }
  .worklist__cta:is(:active, .is-pressed)::before { transform: translate(-50%, -50%) scale(1); }
  .about-bio__toggle:hover { color: var(--orange); }
  .about-bio__toggle:hover::before { transform: translate(-50%, -50%) scale(0); }
  .about-bio__toggle:is(:active, .is-pressed) { color: var(--black); }
  .about-bio__toggle:is(:active, .is-pressed)::before { transform: translate(-50%, -50%) scale(1); }
  /* career rows — orange while pressed only */
  .exp-row__header:hover .exp-row__dot { opacity: 1; }
  .exp-row__header:hover .exp-row__company { color: var(--black); }
  #experience .exp-row__header:hover .exp-row__role { color: var(--gray-300); }
  #experience .exp-row__header:hover .exp-row__plus { color: var(--gray-200); }
  .exp-row__header:is(:active, .is-pressed) .exp-row__dot { opacity: 0; }
  .exp-row__header:is(:active, .is-pressed) .exp-row__company,
  #experience .exp-row__header:is(:active, .is-pressed) .exp-row__role,
  #experience .exp-row__header:is(:active, .is-pressed) .exp-row__plus { color: var(--orange); }
  /* accordion body links */
  .exp-row__links a:hover { color: inherit; }
  .exp-row__links a:hover span { transform: none; }
  .exp-row__links a:is(:active, .is-pressed) { color: var(--black); }
  .exp-row__links a:is(:active, .is-pressed) span { transform: rotate(45deg); }
  /* contact + foot */
  .outro__item:hover::before { transform: translate(-50%, -50%) scale(0); }
  .outro__item:hover span { transform: none; }
  .outro__item:is(:active, .is-pressed)::before { transform: translate(-50%, -50%) scale(1); }
  .outro__item:is(:active, .is-pressed) span { transform: rotate(45deg); }
  .is-pressed) { color: var(--black); }
  .is-pressed)::before { transform: translate(-50%, -50%) scale(1); }
  .foot a:hover { color: var(--gray-300); }
  .foot a:hover span { transform: none; }
  .foot a:is(:active, .is-pressed) { color: var(--black); }
  .foot a:is(:active, .is-pressed) span { transform: rotate(45deg); }
}


/* ── Close's push-down (vt-slidedown): Home must arrive as ONE descending
   sheet. Without this, the bar pieces (name/role/controls) detach into
   their own new-only groups and merely fade in on top of the slide — the
   project side strips its names too (is-leaving-home, project.css). ── */
html.vt-slidedown .topnav__brand,
html.vt-slidedown .topnav .topnav__role,
html.vt-slidedown .topnav__controls {
  view-transition-name: none;
}
@keyframes brand-arrive {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
}

/* ── Display (≥1920): the reference design holds; type steps up ONE notch so
   the index doesn't read as a caption on a large monitor. The voice stays
   small-type-in-space — this is a nudge, not a rescale. ── */
@media (min-width: 1920px) {
  .workrow { font-size: 15px; padding-top: 10px; padding-bottom: 10px; }
  .worklist__info { font-size: 14px; width: min(480px, calc(35% - 120px)); }
  .topnav__anchors, .topnav__mute { font-size: 14px; }
}

/* ── Compact split (840–1023): the desktop 96px left gutter is a third of
   the list column here — use the stacked view's 16px so the rows and
   sections get the width back. ── */
@media (min-width: 840px) and (max-width: 1023px) {
  .workrow { padding-left: 16px; }
  .worklist__info { left: 16px; }
  .topnav { padding-left: 16px; }
  .about-block, .outro, .foot { padding-left: 16px; }
  /* the brand and blob are absolutely positioned (the bar's padding does
     not move them) — they join the same 16px gutter */
  .topnav__brand { left: 16px; }
  .corner-blob { transform: translate(16px, 16px); }
}

/* ── Touch targets (a11y audit 2026-08-22, Kosuke-approved): expand HIT
      AREAS only — layout-neutral ::after overlays, coarse pointers only,
      so desktop hover geometry is untouched. The anchor stack has ZERO
      vertical gap (pitch = row height), so anchors grow horizontally and
      at the stack's outer edges only; isolated targets grow to ~44px. ── */
@media (pointer: coarse) {
  .topnav__anchor, .topnav__mute, .about-bio__toggle, .foot a, .worklist__cta {
    position: relative;
  }
  .topnav__anchor::after,
  .topnav__mute::after,
  .about-bio__toggle::after,
  .foot a::after,
  .worklist__cta::after {
    content: '';
    position: absolute;
    inset: 0;
  }
  .topnav__anchor::after { inset: 0 -14px; }            /* wider, not taller */
  .topnav__anchor:first-child::after { top: -12px; }    /* stack's outer edges */
  .topnav__anchor:last-of-type::after { bottom: -12px; }
  .topnav__mute::after { inset: -12px -14px; }
  .about-bio__toggle::after { inset: -16px -14px; }     /* 13px -> ~45px tall */
  .foot a::after { inset: -11px -6px; }                 /* 21px -> ~43px tall */
  .worklist__cta::after { inset: -10px -12px; }
}

/* ── Project gate — the unlock sheet (see projectGate in worklist.js).
      The lock page's lights-off voice — white on near-black — lowered from the top like
      a drawer: it PUSHES the page down by its own height (JS moves the
      sheet and the page layers on one clock, the page push's own curve),
      no overlay. The title glyph-resolves as it lands and the field +
      buttons follow a beat behind. Above the nav (z 90–95), under the
      screensaver (z 150) so idle stillness still mosaics everything.
      Hidden by visibility, not display, so main.js can measure the email's
      COPIED pill at init. ── */
html.is-gated { overflow: hidden; }   /* the gutter is reserved — no width jump */
.gate {
  position: fixed;
  inset: 0;
  z-index: 120;
  visibility: hidden;
  pointer-events: none;
}
.gate.is-live { visibility: visible; pointer-events: auto; }
/* invisible: it only catches the click-outside that closes the sheet, and
   the wheel/touch that would scroll the frozen page */
.gate__scrim {
  position: absolute;
  inset: 0;
}
.gate__sheet {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 30vh;
  min-height: 240px;                  /* short landscape screens keep the form whole */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px 16px;
  background: #050505;
  color: #fff;
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.6;
  letter-spacing: .05em;
  text-align: center;
  transform: translateY(-100%);        /* parked; the slide itself is WAAPI */
}
.gate.is-open .gate__sheet { transform: none; }

.gate__form {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
}
.gate__title { margin: 0; }
/* the field + buttons trail the sheet in, one beat each; out, they just go */
.gate__form > * {
  opacity: 0;
  transform: translateY(-8px);
  transition: opacity .2s ease, transform .2s ease;
}
.gate.is-open .gate__form > * {
  opacity: 1;
  transform: none;
  transition: opacity .5s ease, transform .6s cubic-bezier(.2, .8, .2, 1);
}
.gate.is-open .gate__title   { transition-delay: .24s; }
.gate.is-open .gate__input   { transition-delay: .32s; }
.gate.is-open .gate__actions { transition-delay: .4s; }
.gate.is-open .gate__request { transition-delay: .48s; }

/* the lock page's field: borderless, centered */
.gate__input {
  width: 180px;
  padding: 6px 2px;
  background: none;
  border: none;
  border-radius: 0;
  color: #fff;
  font: inherit;
  letter-spacing: .05em;
  text-align: center;
  outline: none;
}
.gate__input::placeholder { color: #5c5c62; letter-spacing: .05em; }
/* wrong password: the detent's lateral nudge, once */
.gate__input.is-wrong { animation: gate-nudge .32s cubic-bezier(.3, .8, .2, 1); }
@keyframes gate-nudge {
  0%, 100% { translate: 0; }
  30% { translate: -5px; }
  65% { translate: 3px; }
}

/* Unlock closes the centered group; the error hangs below it out of flow,
   so title / field / Unlock stay optically centered in the sheet */
.gate__actions { position: relative; }
/* Unlock — the .worklist__cta voice: orange at rest, white on the orange
   blob sliding in behind on hover. */
.gate__unlock {
  position: relative;
  isolation: isolate;
  padding: 0;
  background: none;
  border: none;
  font: inherit;
  font-family: var(--font-mono-medium);
  letter-spacing: inherit;
  color: var(--orange);
  cursor: var(--cursor-pointer);
  transition: color .2s ease;
}
.gate__unlock::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: var(--blob-radius);
  background: var(--orange);
  transform: translate(-50%, -50%) scale(0);
  transition: transform .35s cubic-bezier(.2, .8, .2, 1);
  z-index: -1;
}
@media (hover: hover) {
  .gate__unlock:hover { color: #fff; }
  .gate__unlock:hover::before { transform: translate(-50%, -50%) scale(1); }
}
.gate__unlock:is(:active, .is-pressed) { color: #fff; }
.gate__unlock:is(:active, .is-pressed)::before { transform: translate(-50%, -50%) scale(1); }

/* "Email … to request access" — pinned to the sheet's bottom edge, out of
   the centered group. The address is the Contact section's: orange, and
   main.js's copy-orb ([data-copy]) owns hover + copy. Positioned + isolated
   so the orb's z -1 lands behind the text, not behind the sheet. */
.gate__request {
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: 8px;
  isolation: isolate;
  margin: 0;
  font-size: 11px;
  color: rgba(255, 255, 255, .68);
}
.gate__email { color: var(--orange); cursor: var(--cursor-pointer); outline: none; }
.gate__email.is-copy-active { color: #fff; }   /* black would vanish off the orb */

/* error: softened and smaller (Rand Mono ships no lighter cut), hung
   under Unlock */
.gate__error {
  position: absolute;
  top: 100%;
  left: 50%;
  translate: -50% 0;
  margin: 8px 0 0;
  white-space: nowrap;
  font-size: 11px;
  color: rgba(255, 255, 255, .68);
  opacity: 0;
  transition: opacity .2s ease;
}
.gate__error.is-on { opacity: 1; }

/* Stacked tiers: a deeper sheet and the iOS zoom fix — a focused input
   under 16px zooms the page and never zooms back. The placeholder holds
   the body size (iOS reads the input's own size). */
@media (max-width: 839px) {
  .gate__sheet { height: 40vh; min-height: 300px; }
  .gate__input { font-size: 16px; }
  .gate__input::placeholder { font-size: 13px; }
}
@media (pointer: coarse) {
  .gate__unlock::after {
    content: '';
    position: absolute;
    inset: -10px -12px;           /* ~44px targets, layout-neutral */
  }
}
/* Reduced motion: no travel — no push; the sheet simply fades in over the page. */
@media (prefers-reduced-motion: reduce) {
  .gate__sheet { transform: none; opacity: 0; transition: opacity .2s ease; }
  .gate.is-open .gate__sheet { opacity: 1; transition-duration: .2s; }
  .gate__form > *,
  .gate.is-open .gate__form > * { transform: none; transition-delay: 0s; }
  .gate__input.is-wrong { animation: none; }
}
