/* ============================================================================
   ATL Studio — core.css
   Tokens, typography, page chrome, the portrait rig, the preloader.

   Load order on every page:
       atl-scroll.css  →  core.css  →  home.css | page.css  →  reduce.css
   reduce.css is LAST on purpose: media queries add no specificity, so a
   prefers-reduced-motion block only wins its ties by source order.
   ========================================================================== */

/* ------------------------------------------------------------------ fonts --
   Self-hosted. Playfair carries the voice, Poppins carries the facts — a
   deliberate split, not decoration: the display face is what makes the page
   feel expensive and the geometric sans is what makes a price look like a
   number rather than a pitch. Chinese has no Playfair, so the CJK stacks below
   substitute a system serif for display and a system sans for text. */
@font-face {
  font-family: "Playfair Display";
  src: url("../assets/fonts/PlayfairDisplay-latin-var.woff2") format("woff2-variations");
  font-weight: 400 900; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212;
}
@font-face { font-family: "Poppins"; src: url("../assets/fonts/Poppins-400-latin.woff2") format("woff2"); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: "Poppins"; src: url("../assets/fonts/Poppins-500-latin.woff2") format("woff2"); font-weight: 500; font-style: normal; font-display: swap; }
@font-face { font-family: "Poppins"; src: url("../assets/fonts/Poppins-600-latin.woff2") format("woff2"); font-weight: 600; font-style: normal; font-display: swap; }
@font-face { font-family: "Poppins"; src: url("../assets/fonts/Poppins-700-latin.woff2") format("woff2"); font-weight: 700; font-style: normal; font-display: swap; }

/* ----------------------------------------------------------------- tokens --
   Six colour roles, one accent hue at two lightnesses. The dark family is a
   cold graphite with a green cast — a machinist's steel, not a warm espresso —
   and the accent is the ember of a workshop still lit at dusk. The two light
   bands are bone, not cream: the cream-and-brass artisan palette is the
   default every craft brand reaches for and it is refused here on purpose. */
:root {
  --canvas-deep:  #0A0D0C;   /* the close */
  --canvas:       #101413;   /* the page ground */
  --canvas-slate: #161B19;   /* the one raised dark band */
  --paper:        #E6E5E0;   /* hard-cut daylight, used twice, for money */
  --paper-edge:   #D5D4CD;

  --ink:            #F2F0EA;
  --ink-soft:       #9BA5A0;
  /* Tuned against the LIGHTEST dark ground on the site (--canvas-slate), not
     against the base canvas: at #7C867F this measured 5.0:1 on the base and
     4.44:1 on the slate band, which is the one place it actually had to hold. */
  --ink-dim:        #828C85;
  --ink-paper:      #131715;
  --ink-paper-soft: #4B534F;

  /* Shadow and lip, as tokens rather than as literals. ROUND 11 adds these
     because a new palette is next and a palette change must not mean hunting
     six hand-written hsl() shadows through two stylesheets. Cast is the ground
     colour at very low alpha; lip is the light a raised edge catches. */
  --shadow-cast: hsl(160 30% 2% / 0.55);
  --shadow-cast-soft: hsl(160 30% 2% / 0.7);
  --shadow-warm: hsl(30 25% 30% / 0.42);
  --edge-lip: hsl(40 30% 70% / 0.14);

  --ember:      #E1703B;   /* accent on the dark family        — 5.9:1 */
  --ember-band: #A8451A;   /* the same hue on paper            — 4.8:1 */
  --ember-ink:  #100C09;

  /* Ground-aware aliases. A section that changes ground restates these AND
     restates `color`, because `color` inherits as a computed value and a
     redefined token alone never re-inks text that resolved further up. */
  --fg:        var(--ink);
  --fg-soft:   var(--ink-soft);
  --fg-dim:    var(--ink-dim);
  --accent:    var(--ember);
  --rule:      color-mix(in oklab, var(--ink) 15%, transparent);
  --rule-firm: color-mix(in oklab, var(--ink) 30%, transparent);

  /* Engine token overrides — this is the whole rebrand of atl-scroll.css. */
  --sc-canvas: var(--canvas);
  --sc-surface: var(--canvas-slate);
  --sc-ink: var(--ink);
  --sc-ink-soft: var(--ink-soft);
  --sc-accent: var(--ember);
  --sc-accent-ink: var(--ember-ink);
  --sc-font-display: "Playfair Display", Georgia, "Times New Roman", serif;
  --sc-font-text: "Poppins", system-ui, -apple-system, "Segoe UI", sans-serif;

  /* Hard edges. A steel rule has no fillet. One radius scale, held. */
  --sc-r-sm: 2px; --sc-r-md: 3px; --sc-r-lg: 4px; --sc-r-pill: 4px;

  /* Type. The poster tier sits above the engine ramp and is the only place
     display type is allowed past 6rem — this grammar is one long hero. */
  --t-poster: clamp(3.5rem, 15.5vw, 12.5rem);
  --t-quiet:  clamp(0.8rem, 0.77rem + 0.14vw, 0.875rem);
  --track-poster: -0.045em;

  --sc-maxw: 78rem;
  --sc-gutter: clamp(1.25rem, 5vw, 5rem);
}

html[lang^="zh"] {
  --sc-font-display: "Noto Serif SC", "Source Han Serif SC", "Songti SC", "STSong", SimSun, serif;
  --sc-font-text: "Noto Sans SC", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
  --track-poster: -0.01em;   /* CJK is already tightly set; negative tracking collides */
}

/* Light ground. Every band that flips restates `color` here, not just tokens. */
.on-paper {
  background: var(--paper);
  color: var(--ink-paper);
  --fg: var(--ink-paper);
  --fg-soft: var(--ink-paper-soft);
  --fg-dim: var(--ink-paper-soft);
  --accent: var(--ember-band);
  --rule: color-mix(in oklab, var(--ink-paper) 18%, transparent);
  --rule-firm: color-mix(in oklab, var(--ink-paper) 38%, transparent);
}

/* ------------------------------------------------------------------- base -- */
/* The ground stays on --sc-canvas, not on --canvas: the engine writes
   --sc-canvas inline on <html> as drift moves between acts, and hard-coding
   the literal here would silently kill the drift while looking identical on
   the first screen. --sc-canvas resolves to --canvas until an act says
   otherwise. */
body { background: var(--sc-canvas); color: var(--ink); font-weight: 400; }
main { display: block; }

::selection { background: var(--ember); color: var(--ember-ink); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* Browser surfaces, themed rather than inherited. */
:root { caret-color: var(--ember); accent-color: var(--ember); }
@supports (scrollbar-color: auto) {
  html { scrollbar-color: color-mix(in oklab, var(--ink) 22%, transparent) transparent; }
}

.skip {
  position: absolute; left: var(--sc-gutter); top: 0;
  translate: 0 -140%;
  z-index: 500; padding: 0.7rem 1.1rem;
  background: var(--ember); color: var(--ember-ink);
  font-size: var(--t-quiet); font-weight: 600; letter-spacing: 0.06em;
  text-transform: uppercase; text-decoration: none;
  transition: translate 180ms var(--sc-ease-out);
}
.skip:focus-visible { translate: 0 0.6rem; }

/* --------------------------------------------------------- type primitives -- */
.poster {
  font-family: var(--sc-font-display);
  font-weight: 500;
  font-size: var(--t-poster);
  line-height: 0.86;
  letter-spacing: var(--track-poster);
  margin: 0;
  text-wrap: balance;
}
.poster em { font-style: italic; font-weight: 400; }

h1, h2, h3, h4 { font-family: var(--sc-font-display); font-weight: 500; text-wrap: balance; margin: 0; }
h1, h2 { letter-spacing: -0.025em; line-height: 1.02; }
h3, h4 { letter-spacing: -0.015em; line-height: 1.14; }

.d-xl { font-size: clamp(2.6rem, 1.5rem + 5vw, 5.6rem); }
.d-lg { font-size: clamp(2rem, 1.3rem + 3.1vw, 3.6rem); }
.d-md { font-size: clamp(1.5rem, 1.15rem + 1.5vw, 2.3rem); }
.d-sm { font-size: clamp(1.15rem, 1.02rem + 0.6vw, 1.5rem); }

.lede {
  font-family: var(--sc-font-text);
  font-size: clamp(1.05rem, 0.98rem + 0.55vw, 1.4rem);
  line-height: 1.5; letter-spacing: -0.008em;
  color: var(--fg); max-width: 34ch; margin: 0; text-wrap: pretty;
}
.prose { color: var(--fg-soft); max-width: 62ch; margin: 0; text-wrap: pretty; }
.prose + .prose { margin-top: 1rem; }
.prose strong { color: var(--fg); font-weight: 600; }
.small { font-size: var(--t-quiet); line-height: 1.62; color: var(--fg-dim); max-width: 68ch; }

.label {
  font-family: var(--sc-font-text);
  font-size: 0.7rem; font-weight: 600;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--accent); margin: 0;
}
.label--quiet { color: var(--fg-dim); }

.num { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }

/* Links inside prose read as links, and the underline is designed. */
.prose a, .small a, .runlink {
  color: var(--fg); text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 1px; text-underline-offset: 0.24em;
  transition: text-decoration-thickness 140ms var(--sc-ease-out), color 140ms var(--sc-ease-out);
}
.prose a:hover, .small a:hover, .runlink:hover { color: var(--accent); text-decoration-thickness: 2px; }

/* The one action, everywhere. One label, one shape. */
.act-cta {
  display: inline-flex; align-items: center; gap: 0.7rem;
  padding: 0.95rem 1.6rem;
  background: var(--ember); color: var(--ember-ink);
  font-family: var(--sc-font-text); font-size: 0.95rem; font-weight: 600;
  letter-spacing: -0.005em; text-decoration: none; white-space: nowrap;
  border: 0; border-radius: var(--sc-r-md); cursor: pointer;
  box-shadow: 0 1px 2px hsl(160 30% 2% / 0.4), 0 6px 18px -6px hsl(20 60% 20% / 0.5);
  transition: transform 140ms var(--sc-ease-out), background-color 140ms var(--sc-ease-out);
}
.act-cta::after { content: "→"; font-size: 1.05em; line-height: 1; translate: 0 -0.03em; }
@media (hover: hover) and (pointer: fine) {
  .act-cta:hover { background: #EE8551; transform: translateY(-2px); }
}
.act-cta:active { transform: translateY(1px) scale(0.985); }

.ghost-cta {
  display: inline-flex; align-items: center; gap: 0.55rem;
  font-family: var(--sc-font-text); font-size: 0.92rem; font-weight: 500;
  color: var(--fg); text-decoration: none;
  padding-bottom: 0.28rem;
  border-bottom: 1px solid var(--rule-firm);
  transition: border-color 140ms var(--sc-ease-out), color 140ms var(--sc-ease-out);
}
.ghost-cta::after { content: "→"; opacity: 0.6; transition: translate 160ms var(--sc-ease-out); }
@media (hover: hover) and (pointer: fine) {
  .ghost-cta:hover { color: var(--accent); border-color: var(--accent); }
  .ghost-cta:hover::after { translate: 3px 0; }
}

.rule { height: 1px; border: 0; background: var(--rule); margin: 0; }

/* ---------------------------------------------------------------- layout -- */
.wrap { width: 100%; max-width: var(--sc-maxw); margin-inline: auto; padding-inline: var(--sc-gutter); }
.wrap--wide { max-width: 92rem; }
.stack > * + * { margin-top: 1rem; }
.stack > :is(h1, h2, h3) { margin-top: 2.5rem; }
.stack > :first-child { margin-top: 0; }

/* ------------------------------------------------------------------- nav --
   No bar, ever. On the homepage the composition-scale masthead IS the mark,
   and once it has scrolled past, this surfaceless line of running text takes
   over: ink directly on the page ground, no surface, no blur, no border.
   Inner pages show it from the first pixel. */
.topline {
  position: fixed; inset-inline: 0; top: 0; z-index: 60;
  display: flex; align-items: baseline; gap: clamp(0.9rem, 2.2vw, 2rem);
  padding: 0.95rem var(--sc-gutter) 0.9rem;
  font-family: var(--sc-font-text); font-size: 0.8125rem; letter-spacing: 0.01em;
  color: var(--fg);
  opacity: 0; visibility: hidden;
  transition: opacity 300ms var(--sc-ease-out), visibility 0s linear 300ms;
  --topline-ground: var(--sc-canvas);
}
/* Surfaceless does not mean unreadable. Content scrolls underneath a fixed
   line of ink, and two opaque texts crossing is a legibility failure, not a
   style. This is a fade of the page's OWN ground rather than a bar: no edge,
   no border, no blur, nothing that reads as a surface — the page simply goes
   opaque as it reaches the top of the screen. It tracks the ground colour the
   engine is drifting, and flips with the daylight bands. */
.topline::before {
  content: ""; position: absolute; inset: 0 0 auto; height: 260%;
  background: linear-gradient(to bottom,
    var(--topline-ground) 0%,
    color-mix(in oklab, var(--topline-ground) 88%, transparent) 46%,
    transparent 100%);
  pointer-events: none; z-index: -1;
}
.topline.is-on { opacity: 1; visibility: visible; transition-delay: 0s; }
/* On the homepage the minimised portrait docks in the same corner, so the
   running line reserves the width rather than painting under a face. */
body.home .topline { padding-inline-end: calc(var(--sc-gutter) + 5.75rem); }
/* The running line is a full-width fixed bar at z-index 60, so it lies over the
   minimised portrait in the same corner. Reserving the padding keeps the TEXT
   off the face, but the bar still swallowed the click that asks him a question.
   The bar itself is therefore transparent to the pointer; only its own controls
   take events back. */
.topline { pointer-events: none; }
.topline a, .topline button { pointer-events: auto; }

/* Surfaceless chrome has no ground of its own, so when a daylight band arrives
   under it the ink has to flip. Giving it a surface instead would turn the
   running line into the bar this design is refusing to have.

   This redefines the ROLE TOKENS rather than listing every element's colour,
   for a reason that cost a measured failure: a rule like
   `.topline--onpaper a { color: … }` has exactly the same specificity as
   `.topline a { color: var(--fg-soft) }` further down this file, so the later
   one wins and the nav links stay light on paper — 2.01:1, verified against
   rendered pixels. Tokens are read at use time, so position stops mattering.
   `color` is restated because it inherits as a computed value and a redefined
   token alone never re-inks text that already resolved. */
.topline--onpaper {
  color: var(--ink-paper);
  --fg: var(--ink-paper);
  --fg-soft: var(--ink-paper-soft);
  --fg-dim: var(--ink-paper-soft);
  --accent: var(--ember-band);
  --rule: color-mix(in oklab, var(--ink-paper) 18%, transparent);
  --rule-firm: color-mix(in oklab, var(--ink-paper) 38%, transparent);
  --topline-ground: var(--paper);
}
.topline__mark {
  font-family: var(--sc-font-display); font-weight: 600; font-size: 1rem;
  letter-spacing: -0.02em; text-decoration: none; color: var(--fg);
  margin-inline-end: auto;
}
.topline__mark span { color: var(--accent); }
.topline__links { display: flex; gap: clamp(0.85rem, 1.8vw, 1.75rem); }
.topline a { color: var(--fg-soft); text-decoration: none; transition: color 140ms var(--sc-ease-out); }
.topline a:hover { color: var(--fg); }
.topline a[aria-current="page"] {
  color: var(--fg);
  text-decoration: underline; text-decoration-color: var(--accent);
  text-decoration-thickness: 1px; text-underline-offset: 0.35em;
}
.topline__end { display: flex; align-items: baseline; gap: 1rem; }

.lang { display: inline-flex; gap: 0.4rem; align-items: baseline; }
.lang__btn {
  background: none; border: 0; padding: 0.15rem 0.1rem; cursor: pointer;
  font-family: var(--sc-font-text); font-size: 0.8125rem; color: var(--fg-dim);
  transition: color 140ms var(--sc-ease-out);
}
.lang__btn.is-on { color: var(--fg); text-decoration: underline; text-decoration-color: var(--accent); text-underline-offset: 0.3em; }
.lang__btn:hover { color: var(--fg); }
.lang__sep { color: var(--fg-dim); opacity: 0.5; }

/* Mobile: the running line collapses to the mark plus one word. */
.topline__toggle {
  display: none; background: none; border: 0; cursor: pointer; padding: 0.2rem 0;
  font-family: var(--sc-font-text); font-size: 0.8125rem; color: var(--fg);
  border-bottom: 1px solid var(--rule-firm);
}
.menu {
  position: fixed; inset: 0; z-index: 90;
  background: var(--canvas-deep);
  padding: 5rem var(--sc-gutter) 2.5rem;
  display: flex; flex-direction: column; justify-content: center; gap: 0.2rem;
  opacity: 0; visibility: hidden;
  transition: opacity 220ms var(--sc-ease-out), visibility 0s linear 220ms;
}
.menu[hidden] { display: none; }
.menu.is-open { opacity: 1; visibility: visible; transition-delay: 0s; }
.menu a {
  font-family: var(--sc-font-display); font-size: clamp(2rem, 11vw, 3.2rem);
  line-height: 1.18; letter-spacing: -0.03em; color: var(--ink); text-decoration: none;
}
.menu a[aria-current="page"] { color: var(--ember); }
.menu__close {
  position: absolute; top: 1rem; right: var(--sc-gutter);
  background: none; border: 0; color: var(--ink); cursor: pointer;
  font-family: var(--sc-font-text); font-size: 0.8125rem;
  border-bottom: 1px solid var(--rule-firm); padding-bottom: 0.15rem;
}
.menu__foot { margin-top: 2.5rem; display: flex; gap: 1.25rem; align-items: baseline; }

@media (max-width: 860px) {
  .topline__links { display: none; }
  .topline__toggle { display: inline-block; }
}

/* ---------------------------------------------------------------- footer -- */
.foot {
  border-top: 1px solid var(--rule);
  padding: clamp(2.5rem, 6vw, 4.5rem) 0 clamp(2rem, 4vw, 3rem);
  font-family: var(--sc-font-text);
}
.foot__grid {
  display: grid; gap: 2.25rem clamp(1.5rem, 4vw, 4rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
}
.foot h2 { font-family: var(--sc-font-text); font-size: 0.7rem; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: var(--fg-dim); margin: 0 0 0.9rem; }
.foot ul { list-style: none; margin: 0; padding: 0; }
.foot li + li { margin-top: 0.45rem; }
.foot a { color: var(--fg-soft); text-decoration: none; font-size: 0.9rem; }
.foot a:hover { color: var(--accent); }
.foot__legal { margin-top: 2.5rem; padding-top: 1.5rem; border-top: 1px solid var(--rule); font-size: var(--t-quiet); color: var(--fg-dim); display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; }
.foot__mark { font-family: var(--sc-font-display); font-size: 1.15rem; font-weight: 600; letter-spacing: -0.02em; color: var(--fg); }

/* ----------------------------------------------------- the portrait rig --
   Three layers: the portrait with its irises painted out, the two irises as
   sprites that translate — inside a container masked to
   the eye openings, so an iris crops at the corner the
   way a real one does. Geometry below is in the source image's own pixels
   (905 x 1100), emitted by the portfolio's build-eyes pass.

   Everything moves on transform only. Nothing here is ever the LCP element
   that gets faded: the portrait paints at opacity 1 and stays there. */
.head { position: relative; display: block; width: 100%; perspective: 900px; }
.head__img { display: block; width: 100%; height: auto; user-select: none; }
.head__eyes {
  position: absolute; inset: 0; pointer-events: none;
  -webkit-mask-image: url("../assets/character/eye-mask.png");
  mask-image: url("../assets/character/eye-mask.png");
  -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
}
/* BOTH axes are given as percentages of the portrait box, never left to
   `height: auto`. An absolutely-positioned <img> with a percentage width and
   auto height does NOT scale its height in Chrome — it keeps the intrinsic
   97px, which rendered these square sprites at 20 x 97 on a phone: a
   five-times vertical stretch that the eye mask hid, because a stretched iris
   cropped to an eye aperture still looks like an iris. Lighthouse caught it as
   two best-practices failures before any screenshot did.
   97/905 across, 97/1100 down, against the source portrait. */
.head__iris { position: absolute; will-change: transform; }
.head__iris--l { left: 27.96%; top: 48.50%; width: 10.72%; height: 8.82%; }
.head__iris--r { left: 60.99%; top: 48.82%; width: 10.83%; height: 8.91%; }
/* The whole head takes a lazy rotation toward whatever it is looking at.
   Its own element, so it never fights the iris transforms — each
   independently-animating transform gets its own node. No descendant is
   3D-positioned, so the mask on .head__eyes has nothing to flatten. */
.head__tilt { display: block; will-change: transform; }

/* -------------------------------------------------------------- preloader --
   Under 2.5s, one idea, then gone. Once per session, 3s failsafe, never shown
   to a coarse pointer or under reduced motion.

   The curtain is opaque and it does cover the hero — that is fine and it is
   free. What is NOT free is fading the hero underneath it: LCP does no
   occlusion testing, so covered text still records a normal paint, but
   opacity < 1 disqualifies the element outright. The h1 below never fades. */
.pre {
  position: fixed; inset: 0; z-index: 400;
  background: var(--canvas-deep);
  place-items: center;
  transition: opacity 520ms var(--sc-ease-out), visibility 0s linear 520ms;

  /* HIDDEN BY DEFAULT, AND THE DEFAULT IS THE SAFE STATE. This is the whole
     lesson of the worst bug in this build. The curtain was `display: grid`
     with the *hiding* left to a class, so every path where the sequence does
     not run — a phone, any coarse pointer, a second visit in the same
     session, and above all a visitor with no JavaScript at all — kept a
     full-viewport opaque layer over the page forever. The entire site was a
     black screen on mobile.

     It survived Lighthouse, which scored the page 98 with LCP 2.3s, because
     LCP does no occlusion testing and happily measured an h1 nobody could
     see. It survived every desktop screenshot, because desktop is the one
     path where the sequence does run. Only a 360px capture caught it.

     Inverted, the failure mode is a preloader that does not play — which is
     the graceful one. Nothing may make this element visible except the class
     the inline gate adds when it has decided the sequence should run. */
  display: none;
}
html.pre-on .pre { display: grid; }
.pre[hidden] { display: none !important; }
.pre.is-done { opacity: 0; visibility: hidden; pointer-events: none; }

/* The film fills the curtain. `object-fit: cover` on a 16:9 clip means a tall
   window crops the sides rather than letterboxing — the laptop sits left of
   centre in frame, so the crop is biased right to keep it composed. */
.pre__film {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: 62% 50%;
  opacity: 0;
  transition: opacity 620ms var(--sc-ease-out);
}
.pre.is-lit .pre__film { opacity: 1; }

/* Poster fallback. Only when the film could not run: the clip already contains
   its own dolly, and adding a CSS scale on top of it would double the move. */
.pre.is-still .pre__film {
  transform: scale(1.055);
  transition: opacity 620ms var(--sc-ease-out), transform 2600ms linear;
}
.pre.is-still.is-lit .pre__film { transform: scale(1); }

/* The clip is a lit desk in a dark room; the vignette carries its edges into
   the page ground so the curtain lifts out of the frame instead of off it. */
.pre__vig {
  position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(125% 92% at 46% 56%, transparent 34%, color-mix(in oklab, var(--canvas-deep) 88%, transparent) 82%, var(--canvas-deep) 100%);
}

/* --------------------------------------------------- short-viewport display --
   `--t-poster` is sized off width alone (15.5vw). On a landscape phone that is
   a 115px display face inside a 360px-tall viewport, so the wordmark plus the
   reserved portrait band consumed the whole first screen and the H2, lede and
   CTA all fell below the fold — QC read it, fairly, as a crop bug.
   Type that only knows about width has the same flaw the portrait band had:
   it is one-axis reasoning about a two-axis box. Let whichever axis is scarce
   decide, exactly as the band does. */
@media (max-height: 560px) {
  :root { --t-poster: clamp(2.25rem, min(15.5vw, 14vh), 12.5rem); }
}

/* ============================================================================
   ROUND 10 — THE PRELOADER TELLS A STORY AND THEN BECOMES THE PAGE

   Daniel's reference was JL Sparkle: its firework effect does not finish and
   hand over to a website, it turns INTO the website. The connective tissue here
   is the brand itself. The lamp wakes the desk, its light draws the wordmark
   out of the dark, his head rises into that light — and then the room dims to
   the page's own ground colour while the wordmark flies up into the masthead
   and the head lands in the hero dock. Nothing cuts.

   Both marks are copies that fly onto the real elements underneath, so at the
   moment the curtain fades there is nothing to notice.
   ========================================================================== */
.pre__brand {
  position: relative; z-index: 3;
  display: grid; justify-items: center; gap: clamp(0.5rem, 2vh, 1.25rem);
  pointer-events: none;
}
.pre__mark {
  font-family: var(--sc-font-display); font-weight: 700;
  font-size: clamp(2.6rem, 11vw, 7rem); line-height: 1;
  letter-spacing: -0.04em; color: var(--paper);
  /* The light draws it: a left-to-right wipe, not a fade-in. */
  clip-path: inset(0 100% 0 0);
  opacity: 0;
  transition: clip-path 780ms var(--sc-ease-out), opacity 240ms linear,
              transform 660ms var(--sc-ease-out);
  will-change: clip-path, transform;
}
.pre.is-mark .pre__mark { clip-path: inset(0 0 0 0); opacity: 1; }

.pre__head {
  width: min(30vw, 13rem); height: auto; display: block;
  opacity: 0; transform: translateY(12%) scale(0.97);
  transition: opacity 520ms var(--sc-ease-out), transform 660ms var(--sc-ease-out);
  filter: drop-shadow(0 18px 40px hsl(160 30% 2% / 0.55));
  will-change: transform;
}
.pre.is-head .pre__head { opacity: 1; transform: none; }

/* The room dims to the page ground. The curtain's own background IS the page
   ground, so fading the film is the whole effect — there is nothing to match. */
.pre.is-out .pre__film,
.pre.is-out .pre__vig { opacity: 0; transition: opacity 620ms var(--sc-ease-out); }
/* The flight itself. Longer than the fade so the marks are still travelling
   when the curtain goes, which is what makes it read as continuous. */
.pre.is-out .pre__mark,
.pre.is-out .pre__head { transition: transform 720ms var(--sc-ease-out), opacity 520ms linear; }
