/* ============================================================================
   home.css — the Lane B scroll experience

   Grammar: TYPOGRAPHIC POSTER. Type is the imagery. There is no photographic
   ground, no scrub film, no scrim and — deliberately — no card anywhere on
   this page. Rhythm comes from scale, not from motion.

   The one piece of media is the portrait, and it is the brand mark rather than
   decoration, which is the single exception this grammar earns.
   ========================================================================== */

/* A pinned act's own padding silently eats its sticky travel: the engine sets
   the section's height in vh, box-sizing is border-box, so padding comes out
   of the content box and the stage can end up with nothing to travel through.
   Zero it while the engine is alive; the breathing room lives on the stage. */
html.sc-ready [data-sc-act="pin"],
html.sc-ready [data-sc-act="pan"] { padding-block: 0; }

/* THE PINNED HEIGHTS ARE RESERVED HERE, and they must stay in step with each
   act's data-sc-span. The engine writes `height: <span * 100>vh` as an inline
   style when it mounts, which is after first paint — so without these the
   hero grows from one viewport to two and a half, everything below it drops
   140vh, and the page charges a layout shift for it. Measured: CLS 0.325 →
   0.00 with nothing else changed, worth 13 Lighthouse points on mobile.
   Same value in CSS as the engine will write = no shift at mount. */
#arrival { height: 240vh; }   /* data-sc-span 2.4 */
#proof   { height: 180vh; }   /* data-sc-span 1.8 */
#floor   { height: 160vh; }   /* data-sc-span 1.6 */
#process { height: 230vh; }   /* data-sc-span 2.3 */
#close   { height: 120vh; }   /* data-sc-span 1.2 */

/* ROUND 11 — #process was pinned with a span and NO reserved height, so the
   engine grew it from one viewport to 4.4 at mount and everything below it
   dropped 340vh after first paint. It was the one pinned act missing from this
   list; the shortened span and the reservation land together. Anything added to
   this block must also be added to reduce.css's release, or a reduced-motion
   visitor gets viewport-heights of empty ground where the pin would have been. */

.act { position: relative; }
.stage-pad {
  height: 100%;
  display: flex; flex-direction: column; justify-content: center;
  padding-block: clamp(4.5rem, 10vh, 8rem);
}

/* Painted grounds, not drifted ones. `drift` interpolates the page canvas, and
   interpolating between a dark pole and a light one walks every piece of text
   through the background colour on the way. The two daylight bands therefore
   paint their own opaque ground and restate `color`; drift only ever moves
   inside the dark family. */
.band { position: relative; }
.band--paper { background: var(--paper); }
.band--slate { background: var(--canvas-slate); }
.band--deep  { background: var(--canvas-deep); }

/* ------------------------------------------------------- 1 · being seen -- */
.hero { position: relative; }
.hero .sc-stage { display: grid; align-items: end; }
.hero__plate {
  width: 100%; max-width: var(--sc-maxw);
  margin-inline: auto; padding-inline: var(--sc-gutter);
  padding-block-end: clamp(3rem, 9vh, 6rem);
  transform-origin: 0 100%;
  /* The masthead recedes as you scroll it, and the running-text nav takes its
     place at the top edge: the composition literally re-sets into chrome. */
  transform: translateY(calc(var(--sc-p, 0) * -7vh)) scale(calc(1 - var(--sc-p, 0) * 0.18));
  will-change: transform;
}
/* LCP: transform-only, never a fade. `opacity: 0` disqualifies an element as
   an LCP candidate outright, and this title is the largest text on the page.
   The preloader may cover it — LCP does no occlusion testing — but nothing
   here may make it transparent. */
.hero__title { color: var(--ink); }
/* Each line is its own mask, and the thing that moves is the inner span — one
   element per independently-animating transform, so the rise and the plate's
   own scale never fight over the same node. `overflow: clip` rather than
   `hidden`: clip creates no scroll container, which matters on an element this
   wide. The padding is descender room; without it the tail of a Q or a comma
   is shaved by the mask in the resting state. */
.hero__title .ln {
  display: block; overflow: clip;
  padding-block-end: 0.045em; margin-block-end: -0.045em;
}
.hero__title .ln__i { display: block; will-change: transform; }
/* The topline's hand-over is driven from act one's own progress in home.js,
   not from a sentinel element: a zero-height span is unreliable for
   IntersectionObserver, and an observer on a node BELOW a 240vh act reads
   "not intersecting" at scroll 0 — which is exactly when the masthead is
   still the mark and the running line must not exist yet. */
/* THE REST STATE IS VISIBLE, AND THAT IS A PERFORMANCE DECISION.

   The obvious way to write a masked line reveal is to park the inner span
   outside its clip box and release it when the preloader hands over. Measured,
   that costs a Lighthouse point on this page and it is worth understanding why,
   because it is the PLAYBOOK's LCP rule wearing a different hat: `opacity: 0`
   disqualifies an element as an LCP candidate — and so does being clipped out
   of existence. The h1 then cannot be a candidate until `pre-done` lands three
   seconds in.

   So the reveal is an ANIMATION FROM a masked start rather than a transition
   OUT OF a persisted hidden state. At rest the title is simply there, painted,
   eligible from the first frame; the rise plays once, when the curtain hands
   over. Still transform-only, still masked, still staggered — and it costs the
   LCP nothing. */
@keyframes ln-rise {
  from { transform: translateY(105%); }
  to   { transform: none; }
}
html.pre-done .hero__title .ln__i,
html.no-pre .hero__title .ln__i {
  animation: ln-rise 900ms var(--sc-ease-out) both;
}
html.pre-done .hero__title .ln:nth-child(2) .ln__i,
html.no-pre .hero__title .ln:nth-child(2) .ln__i { animation-delay: 110ms; }

html.pre-on:not(.pre-done) .hero__sub { transform: translateY(20px); }
.hero__sub { transition: transform 900ms 380ms var(--sc-ease-out); }

.hero__sub {
  font-family: var(--sc-font-text);
  font-size: clamp(1.05rem, 0.95rem + 0.62vw, 1.45rem);
  font-weight: 400; line-height: 1.34; letter-spacing: -0.015em;
  color: var(--ink); max-width: 26ch;
  margin: clamp(1.25rem, 3vh, 2rem) 0 0;
}
.hero__go { margin-top: clamp(1.5rem, 4vh, 2.5rem); display: flex; flex-wrap: wrap; gap: 1.25rem 1.75rem; align-items: center; }

/* Chrome fades; the title does not. */
html.pre-on:not(.pre-done) :is(.ask, .hero__go, .hero__foot, .topline) { opacity: 0; }
:is(.ask, .hero__go, .hero__foot) { transition: opacity 700ms 380ms var(--sc-ease-out); }

.hero__foot {
  position: absolute; inset-inline: 0; bottom: clamp(0.9rem, 2.5vh, 1.6rem);
  padding-inline: var(--sc-gutter);
  font-size: var(--t-quiet); color: var(--ink-dim);
  display: flex; flex-wrap: wrap; gap: 0.35rem 1.25rem;
}
.hero__foot .lang__btn { font-size: var(--t-quiet); }
.hero__foot .lang { gap: 0.35rem; }

/* -------------------------------------------------------- the head dock --
   One instance, fixed at body level so no ancestor's `overflow: clip` can
   catch it, moved between three states with transform alone — never width,
   never top. `--dock-p` is act one's own progress, written by home.js. */
/* ROUND 11 - THE CHARACTER IS THE CONSTANT.

   He used to have three fixed positions, one of which (--min) held for seven of
   the nine acts. He now travels between the four corners of the viewport, and
   WHICH corner is measured per act by companion.js against the real text and
   control boxes on screen - never guessed from viewport units, which is the
   arithmetic-not-relationship trap that has already cost this build a round.

   So this file no longer writes the parked geometry at all. It owns the anchor,
   the travel, and the states. companion.js writes one transform per act change,
   from measured numbers, and clears it in the hero where the reserved-band CSS
   below is still in charge.

   z-index 65: above the masthead's 60, because a companion the navigation
   paints over is a bug, and below the bubble's 70 and the menu's 90. */
.dock {
  position: fixed; top: 0; right: 0; z-index: 65;
  width: min(30vw, 24rem);
  transform-origin: 100% 0;
  pointer-events: none;
  transition: transform 900ms var(--sc-ease-out), opacity 400ms var(--sc-ease-out);
  will-change: transform;
}
/* Act one only. Every other position on the page is measured. */
.dock--hero {
  transform:
    translate(calc(-7vw - var(--dock-p, 0) * 1vw), calc(19vh - var(--dock-p, 0) * 3.5vh))
    scale(calc(1 - var(--dock-p, 0) * 0.07));
}
/* Before companion.js has had a frame, and forever without JavaScript: he must
   not sit wherever an unwritten transform leaves him. The hero geometry is the
   safe default, and reduce.css already anchors him to the top of the document
   in that state. */
html:not(.sc-ready) .dock { transform: translate(-7vw, 19vh); }

.dock .head__img { filter: drop-shadow(0 18px 40px var(--shadow-cast)); }
/* A cut-out has no ground of its own. Over the daylight band a cold black
   shadow reads as a sticker cut from another page, so it warms with the ground
   the way every other ink on this site does. */
.dock--onpaper .head__img { filter: drop-shadow(0 14px 30px var(--shadow-warm)); }

/* While the page is genuinely moving he gets out of the way. Only the parked
   states: in the hero and at the close he IS the composition.
   The scale goes on .head, which sits BETWEEN the dock and the rotated
   .head__tilt - a second transform on its own element, never stacked onto a
   node that is already animating something else. */
.dock--busy.dock--park { opacity: 0.6; }
.dock--busy.dock--park .head { transform: scale(0.82); }
.dock--park .head { transition: transform 380ms var(--sc-ease-out); }

/* --------------------------------------------------------- 2 · good faith -- */
.offer { padding-block: clamp(3.5rem, 10vh, 6.5rem); }
.offer__grid {
  display: grid; gap: clamp(2rem, 5vw, 4.5rem);
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  align-items: start;
}
.terms { list-style: none; margin: 0; padding: 0; }
.terms li { padding: 1rem 0; border-top: 1px solid var(--rule); }
.terms li:last-child { border-bottom: 1px solid var(--rule); }
.terms b { display: block; font-family: var(--sc-font-text); font-size: 0.82rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent); margin-bottom: 0.3rem; }
.terms span { color: var(--fg-soft); font-size: 0.95rem; }

/* ------------------------------------------------------ 3 · nothing hidden -- */

/* ---------------------------------------------------------- 4 · evidence -- */
/* The rail is CENTRED in its stage, not stretched to it. Stretched plates
   with their content hanging from the bottom left the top half of a 100vh
   pinned stage empty in every frame of the contact sheet — the harness reads
   that as live scroll because the rail is moving, but a reader sees half a
   screen of nothing. */
.rail { display: flex; align-items: center; height: 100%; gap: clamp(2rem, 5vw, 4rem); padding-inline: var(--sc-gutter); }
.rail__lead, .rail__note { flex: 0 0 min(78vw, 22rem); }
.plate {
  flex: 0 0 min(82vw, 26rem);
  padding-top: 1.1rem;
  border-top: 1px solid var(--rule);
}
.plate h3 { font-size: clamp(1.6rem, 1.2rem + 1.6vw, 2.4rem); margin-bottom: 0.65rem; }
.plate h3 em { font-style: italic; color: var(--ink-soft); font-size: 0.62em; display: block; margin-top: 0.35rem; letter-spacing: -0.01em; }
.plate__meta { font-family: var(--sc-font-text); font-size: var(--t-quiet); letter-spacing: 0.1em; text-transform: uppercase; color: var(--ember); margin: 0 0 0.8rem; }
.plate__meta--muted { color: var(--ink-dim); }
.plate p { color: var(--ink-soft); font-size: 0.95rem; max-width: 34ch; margin: 0 0 1.1rem; }

/* ------------------------------------------------------- 5 · recognition -- */

/* ------------------------------------------------------- 6 · reassurance --
   The whole act is rebuilt at the foot of this file (ROUND 11). The rules that
   stood here stacked four statements absolutely inside one box and cross-faded
   them, which is why three of them printed on top of each other at most scroll
   positions. Deleted rather than overridden: an `position: absolute` left
   behind here would have survived every later rule that did not think to reset
   it. */

/* ------------------------------------------------------- 7 · practicality -- */
.process { padding-block: clamp(4rem, 9vh, 6.5rem); }
.process__list { list-style: none; counter-reset: s; margin: clamp(1.5rem, 4vh, 2.25rem) 0 0; padding: 0; display: grid; gap: 0; }
.process__list li { counter-increment: s; display: grid; grid-template-columns: 2.4rem minmax(0, 1fr); gap: 1rem; padding: 0.95rem 0; border-top: 1px solid var(--rule); align-items: baseline; }
.process__list li::before { content: counter(s); font-family: var(--sc-font-text); font-size: 0.78rem; font-weight: 600; letter-spacing: 0.1em; color: var(--accent); }
.process__list li:last-child { border-bottom: 1px solid var(--rule); }
.process__list p { margin: 0; color: var(--fg-soft); font-size: 0.98rem; max-width: 58ch; }
.process__list p b { color: var(--fg); font-weight: 500; }
.process__note { margin-top: 1.4rem; }

/* ------------------------------------------------------------ 8 · choice -- */
.ladder__list { list-style: none; margin: clamp(1.5rem, 4vh, 2.5rem) 0 0; padding: 0; }
.ladder__list li {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0.4rem 1.5rem;
  align-items: baseline; padding: clamp(0.75rem, 2vh, 1.15rem) 0;
  border-top: 1px solid var(--rule);
}
.ladder__list li:last-child { border-bottom: 1px solid var(--rule); }
.ladder__name { font-family: var(--sc-font-display); font-size: clamp(1.3rem, 1rem + 1.5vw, 2.2rem); letter-spacing: -0.025em; color: var(--ink-paper); }
.ladder__name small { font-family: var(--sc-font-text); display: block; font-size: 0.8125rem; letter-spacing: 0; color: var(--ink-paper-soft); margin-top: 0.15rem; }
.ladder__price { font-family: var(--sc-font-text); font-size: clamp(1.05rem, 0.9rem + 0.7vw, 1.45rem); font-weight: 600; color: var(--ink-paper); white-space: nowrap; }
.ladder__tag { display: inline-block; margin-left: 0.6rem; font-size: 0.7rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ember-band); }

/* ----------------------------------------------------------- 9 · candour -- */
.faq { padding-block: clamp(3.25rem, 8vh, 5.5rem); }
.faq__list { margin-top: clamp(1.25rem, 3.5vh, 2rem); }
.qa { border-top: 1px solid var(--rule); }
.qa:last-child { border-bottom: 1px solid var(--rule); }
.qa > summary {
  list-style: none; cursor: pointer; padding: 1rem 2rem 1rem 0; position: relative;
  font-family: var(--sc-font-text); font-size: 1rem; font-weight: 500; color: var(--fg);
}
.qa > summary::-webkit-details-marker { display: none; }
.qa > summary::after {
  content: "+"; position: absolute; right: 0.2rem; top: 50%; translate: 0 -50%;
  color: var(--accent); font-size: 1.1rem; transition: transform 200ms var(--sc-ease-out);
}
.qa[open] > summary::after { transform: rotate(45deg); }
.qa > summary:hover { color: var(--accent); }
.qa__a { padding: 0 0 1.25rem; }
.qa__a p { color: var(--fg-soft); font-size: 0.95rem; max-width: 64ch; margin: 0; }
.qa__a p + p { margin-top: 0.7rem; }

/* ----------------------------------------------------------- 10 · resolve --
   The close pattern: the scale collapses. Everything on the page has been
   getting smaller and more factual since the first screen, and this is the
   quietest thing on it — the ask set as one line of running text, not a
   button island. The cue holds on a single value so the last screen still has
   something on it. */
.close .sc-stage { display: grid; }
.close__inner { align-self: center; width: 100%; max-width: var(--sc-maxw); margin-inline: auto; padding-inline: var(--sc-gutter); }
.close__ask { font-size: clamp(1.9rem, 1.2rem + 3.4vw, 4rem); line-height: 1.04; margin: 0 0 1.25rem; max-width: 15ch; }
.close__lines { display: grid; gap: 0.75rem; margin-top: clamp(1.5rem, 4vh, 2.25rem); font-size: 0.95rem; }
.close__lines a { color: var(--ink); }
.close__small { margin-top: clamp(1.75rem, 5vh, 3rem); }

/* These four are the shortened spans for the pinned acts, and they are scoped
   OUT of reduced motion deliberately. They are ID selectors carrying
   !important, which is what it takes to outrank the engine's inline
   `height: <span>00vh` — but that same weight also outranked reduce.css's
   `:is(.hero, .close, .proof, .floor) { height: auto !important }` release,
   because an id beats a class no matter which file loads last. The result was
   that a reduced-motion visitor under 860px got the proof act clamped to 175vh
   while its rail — stacked into a column because nothing pans under reduced
   motion — needed 2539px, and the 1139px of overflow painted straight over the
   acts below it. A span is only meaningful while an act is pinned, so the
   shortening simply does not apply when nothing is. */
@media (max-width: 860px) and (prefers-reduced-motion: no-preference) {
  #arrival { height: 150vh !important; }
  #proof   { height: 150vh !important; }
  #floor   { height: 128vh !important; }
  #process { height: 168vh !important; }
  #close   { height: 120vh !important; }
}

/* ---------------------------------------------------------------- narrow -- */
@media (max-width: 900px) {
  .offer__grid { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 860px) {
  /* Phones pay for scroll in thumb-distance, and copy that wraps to three
     lines on a 360px screen makes every act taller than the desktop one it
     was measured at. The pinned acts are therefore shortened here — the
     engine writes its span as an inline height, and a stylesheet !important
     outranks a non-important inline style, so this is the one place a span
     may be overridden. Every one stays above the ~1.2 minimum where a pin's
     travel collapses to a single pixel.
     Measured: 16.17vh unadjusted → 13.6vh at 360x780. */

  .offer, .money, .make, .faq { padding-block: clamp(2.25rem, 5.5vh, 3.25rem); }

  .dock { width: min(46vw, 15rem); }
  .dock--hero { transform: translate(-5vw, calc(9vh - var(--dock-p, 0) * 2vh)) scale(1); }
  html:not(.sc-ready) .dock { transform: translate(-5vw, 9vh); }
  /* ROUND 11 - the parked corners are measured, so there is nothing to override
     here any more. companion.js steps his parked SIZE down on a narrow viewport
     (58px under 560, 68px under 900). It is a size and not a position, and it
     lives in JS because the same number has to be divided back out of the focus
     ring below to keep the ring visible through the scale. */
  .hero .sc-stage { align-items: end; }
  /* 5.5rem of this reserved the old uppercase facts strip, which the ask
     replaced. Left in place it printed ~150px of dead ground between the CTA
     and the SSM line on a phone. Only the foot has to clear now. */
  .hero__plate { padding-block-end: clamp(3rem, 9vh, 4.5rem); transform: translateY(calc(var(--sc-p, 0) * -4vh)); }
  .rail__lead, .rail__note { flex-basis: min(80vw, 20rem); }
  .plate { flex-basis: min(84vw, 21rem); }
}

/* ------------------------------------------- narrow viewports: type vs face --
   QC round 2. The dock is positioned with vw/vh translates on a box that knows
   nothing about where the headline's measure ends, so on a narrow or a SHORT
   viewport the face lands on the type: at 320 the brand name reads "ATL STUD",
   at 600x800 the jaw halves the final O. Nudging the offsets only moves which
   viewport breaks, because the collision is viewport arithmetic, not a value.

   So the composition is constrained instead: the face gets its own band at the
   top of the hero, and the type starts below that band by construction. The
   stage is `align-items: end`, so padding-block-start reserves the band and the
   content cannot rise into it at any viewport height.
   The two custom properties are shared with reduce.css so both motion modes
   compute the identical band.

   The band is sized against BOTH axes. A first pass keyed on width alone still
   collided at 740x360 — a landscape phone is wide but short, and the collision
   is driven by height as much as width. min(34vw, 22vh, ...) lets whichever
   axis is scarce decide the size.

   The upper bound is 767px, NOT a round 600. Scoping it to 600 relocated the
   defect rather than removing it: at 601px the custom properties fell out of
   scope, the dock snapped from 152px back to 240px wide, and the final O of
   STUDIO was sliced again. 0.0% at 600 and 11.7% at 605 is a media-query
   boundary, not viewport arithmetic. 768px up is clean by different means —
   there the type's measure ends before the dock's box on its own. */
@media (max-width: 767px), (max-height: 560px) {
  :root {
    --dock-band-w: min(34vw, 22vh, 9.5rem);
    /* ROUND 11 — the band now starts BELOW the masthead. He outranks it at
       z-index 65, which is required (a companion the navigation paints over is
       a bug) but is not permission to sit on it: on a phone the top-right
       corner holds the Menu button, and his hit area was swallowing the only
       navigation control on the screen. `--top-band` is the masthead's measured
       height, published by companion.js; the fallback clears it at every width
       this site supports, so the no-JS path is safe too. */
    --dock-band-y: calc(var(--top-band, 3.6rem) + 0.65rem);
  }
  .dock { width: var(--dock-band-w); }
  .dock--hero { transform: translate(-4vw, var(--dock-band-y)) scale(1); }
  html:not(.sc-ready) .dock { transform: translate(-4vw, var(--dock-band-y)); }
  .hero .sc-stage {
    padding-block-start: calc(var(--dock-band-y) + var(--dock-band-w) + 1.25rem);
  }

  /* Reserving the band costs vertical room, and `.hero__foot` is anchored to
     the bottom of the stage with `position: absolute` — so on a short viewport
     the flow content grew down into it and the CTA, the ghost link, the SSM
     line and the language pair all printed on top of each other at 320x568.
     An absolutely-positioned strip cannot participate in the very stack it has
     to clear, so at these sizes it rejoins the flow. */
  .hero__foot {
    position: static;
    margin-top: 1.1rem;
    padding-inline: var(--sc-gutter);
  }
}

/* ------------------------------------------------- the head's turn, restored --
   ROUND 11, second attempt - and the first attempt is the whole lesson.

   Round 10 put perspective: 900px on .dock and called the turn fixed. It was
   not. PERSPECTIVE APPLIES TO AN ELEMENT'S DIRECT CHILDREN ONLY, and the chain
   was .dock > .dock__hit > .head__tilt, so the rotated element was a grandchild
   of the perspective root, had no projection to divide by, and still rendered
   as a pure affine squash: at 40deg the width was exactly cos(40) x its own,
   which is a scale and not a turn. The rule was present, computed, and inert -
   the worst kind of fix, because every probe that reads the declaration passes.

   The fix is a real .head element in the markup wrapped DIRECTLY around
   .head__tilt, carrying the perspective core.css has always declared for that
   class. The value on .dock is kept as a harmless outer default.

   Do not put a filter, an opacity or a mask on .head itself: a grouping
   property between a perspective root and its 3D child forces the subtree flat,
   silently. That is the trap that would undo this a third time, so the
   drop-shadow lives on .head__img, the leaf, and the busy-state opacity lives
   on .dock, outside the root. */
.dock { perspective: 900px; }
.dock .head { display: block; width: 100%; perspective: 900px; }

/* ============================================================================
   ROUND 5 — the page stops being a document and starts being a thing you use.

   Everything below was added after the first build read as "correct but dry":
   the only motion on nine of ten acts was text fading upward, and the one
   piece of media was the portrait. These components each answer that with
   something drawn or something the visitor can touch, and none of them is a
   raised card — the poster grammar holds. Hairlines, ground, and ink.
   ========================================================================== */

/* ------------------------------------------------------------- the ask --
   The hero conversation. Two rows of chips and a result, sized so all three
   states occupy the same block: the hero is a pinned, bottom-aligned stage,
   and a step that changed height would shift the composition under the reader
   mid-answer. */
/* The block is RESERVED at the height of its tallest state and its contents
   are centred inside it, so moving between question, question and answer never
   moves a single pixel of the composition around it. The hero is a pinned,
   bottom-anchored stage: anything that changes height here pushes the wordmark
   up under the reader mid-answer, which measured 120px before the shared CTA
   moved out of the answer. */
.ask {
  margin-top: clamp(1rem, 3vh, 1.75rem); max-width: 44rem;
  min-height: 8.5rem; display: flex; flex-direction: column; justify-content: center;
}
.ask__q {
  font-family: var(--sc-font-display);
  font-size: clamp(1.05rem, 0.95rem + 0.5vw, 1.35rem);
  line-height: 1.25; letter-spacing: -0.02em;
  color: var(--ember); margin: 0 0 0.85rem; max-width: 30ch;
}
.ask__row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.ask__row[hidden] { display: none; }

.chip {
  font-family: var(--sc-font-text); font-size: 0.875rem; font-weight: 500;
  color: var(--ink); background: transparent;
  border: 1px solid var(--rule-firm); border-radius: var(--sc-r-pill);
  padding: 0.6rem 0.95rem; min-height: 2.75rem;   /* thumb target */
  cursor: pointer; text-align: left;
  transition: color 160ms var(--sc-ease-out), border-color 160ms var(--sc-ease-out),
              background-color 160ms var(--sc-ease-out), transform 160ms var(--sc-ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .chip:hover { color: var(--ember-ink); background: var(--ember); border-color: var(--ember); transform: translateY(-1px); }
}
.chip:active { transform: translateY(1px) scale(0.985); }

.ask__out[hidden] { display: none; }
/* Sized to land on one line at desktop and two at most on a phone: at 1.5rem
   this ran to two lines everywhere and was most of the 120px the answer state
   used to add. */
.ask__read {
  font-family: var(--sc-font-display);
  font-size: clamp(1rem, 0.95rem + 0.42vw, 1.25rem);
  line-height: 1.28; letter-spacing: -0.02em;
  color: var(--ink); margin: 0; max-width: 44ch;
}
.ask__price {
  font-family: var(--sc-font-text); font-size: 0.9rem; font-weight: 600;
  letter-spacing: 0.02em; color: var(--ember); margin: 0.55rem 0 0;
  display: flex; flex-wrap: wrap; gap: 0.4rem 1.1rem; align-items: baseline;
}
.ask__again {
  background: none; border: 0; padding: 0; cursor: pointer;
  font-family: var(--sc-font-text); font-size: var(--t-quiet);
  color: var(--ink-dim); text-decoration: underline;
  text-decoration-color: var(--rule-firm); text-underline-offset: 4px;
}
.ask__again:hover { color: var(--ember); }
/* No JavaScript: the ask never turns on and the plain CTA pair stays. */
.ask:not(.is-live) { display: none; }

/* ---------------------------------------------------- the drawn browser --
   The offer, demonstrated rather than described: a page assembling itself out
   of nothing. Pure CSS, no image, no request. Every part starts collapsed on
   its left edge and is released in sequence when the act enters. */
.mock {
  border: 1px solid var(--rule-firm);
  background: color-mix(in oklab, var(--ink) 4%, transparent);
  align-self: start;
}
.mock__bar {
  display: flex; align-items: center; gap: 0.4rem;
  padding: 0.65rem 0.8rem; border-bottom: 1px solid var(--rule);
}
.mock__bar i { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--rule-firm); }
.mock__url { flex: 1; height: 0.5rem; margin-left: 0.5rem; background: var(--rule); border-radius: 1px; }
.mock__body { padding: clamp(0.9rem, 2vw, 1.4rem); display: grid; gap: 0.7rem; }
/* The hero band is the one saturated thing in the frame — at 26% over a dark
   ground it mixed down to a muddy brown and read as a stain rather than as the
   page's own accent. */
.mock__hero {
  height: clamp(4.5rem, 12vw, 7.5rem);
  background: linear-gradient(150deg,
    color-mix(in oklab, var(--ember) 78%, transparent),
    color-mix(in oklab, var(--ember) 34%, transparent));
}
.mock__ln { height: 0.55rem; background: var(--rule-firm); }
.mock__ln--w1 { width: 82%; }
.mock__ln--w2 { width: 64%; }
.mock__ln--w3 { width: 74%; }
.mock__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; }
.mock__grid span { height: clamp(2rem, 5vw, 3rem); background: var(--rule); }
.mock__btn { width: 7.5rem; height: 1.7rem; background: var(--ember); }

.mock :is(.mock__hero, .mock__ln, .mock__grid span, .mock__btn, .mock__url) {
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform 620ms var(--sc-ease-out);
}
.mock.sc-in :is(.mock__hero, .mock__ln, .mock__grid span, .mock__btn, .mock__url) { transform: scaleX(1); }
.mock.sc-in .mock__url        { transition-delay: 60ms; }
.mock.sc-in .mock__hero       { transition-delay: 200ms; }
.mock.sc-in .mock__ln--w1     { transition-delay: 380ms; }
.mock.sc-in .mock__ln--w2     { transition-delay: 460ms; }
.mock.sc-in .mock__grid span:nth-child(1) { transition-delay: 560ms; }
.mock.sc-in .mock__grid span:nth-child(2) { transition-delay: 620ms; }
.mock.sc-in .mock__grid span:nth-child(3) { transition-delay: 680ms; }
.mock.sc-in .mock__ln--w3     { transition-delay: 780ms; }
.mock.sc-in .mock__btn        { transition-delay: 900ms; }

/* ------------------------------------------------------- 3 · the money --
   One act now, not two: the dial selects, the ladder answers. */
.money { padding-block: clamp(3.5rem, 10vh, 7rem); overflow: clip; }

.dial { margin-top: clamp(1.5rem, 4vh, 2.5rem); max-width: 34rem; }
.dial__lab {
  display: block; font-family: var(--sc-font-text); font-size: 0.82rem;
  font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--accent); margin-bottom: 0.9rem;
}
/* Native input, deliberately. `accent-color` already resolves to the band's
   own accent through the ground-aware token, a custom track buys nothing here,
   and the native control arrives keyboard-operable and labelled for free. */
.dial__range { width: 100%; accent-color: var(--accent); height: 1.75rem; cursor: pointer; }
.dial__out {
  margin: 0.75rem 0 0; font-family: var(--sc-font-display);
  font-size: clamp(1.4rem, 1.1rem + 1.5vw, 2.2rem);
  letter-spacing: -0.03em; color: var(--ink-paper);
}

/* The ladder is the dial's readout as much as it is a price list. */
.ladder__list li { cursor: pointer; padding-left: 0.85rem; position: relative; transition: background-color 200ms var(--sc-ease-out); }
.ladder__list li::before {
  content: ""; position: absolute; left: 0; top: -1px; bottom: 0; width: 2px;
  background: var(--ember-band); transform: scaleY(0); transform-origin: 50% 0;
  transition: transform 260ms var(--sc-ease-out);
}
.ladder__list li.is-on::before { transform: scaleY(1); }
/* NO TINT ON THE SELECTED ROW. `--ember-band` is tuned to 4.8:1 against bare
   paper; a 7% ember wash under it pulled both the package name and the
   "Most chosen" tag under 4.5:1 and cost the page its 100 accessibility score.
   The 2px bar and the weight change carry the selection on their own — the
   band was never doing work the bar was not already doing. */
.ladder__list li.is-on .ladder__name { color: var(--ember-band); font-weight: 500; }
@media (hover: hover) and (pointer: fine) {
  .ladder__list li:hover { background: color-mix(in oklab, var(--ink-paper) 4%, transparent); }
}

.money__after { margin-top: clamp(1.5rem, 4vh, 2.25rem); display: grid; gap: 1.1rem; }
.money__kill {
  font-family: var(--sc-font-display);
  font-size: clamp(1.15rem, 1rem + 0.9vw, 1.7rem);
  line-height: 1.25; letter-spacing: -0.02em;
  color: var(--ink-paper); margin: 0; max-width: 40ch;
}

/* -------------------------------------------------------- 4 · the shots --
   The evidence act was three columns of type making a claim about visual work.
   The screenshots were already in the build, used only on the work page. */
.plate__shot {
  display: block; margin-bottom: 1.1rem; overflow: hidden;
  border: 1px solid var(--rule); aspect-ratio: 16 / 10; background: var(--canvas-deep);
}
.plate__shot img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 0; transition: transform 700ms var(--sc-ease-out); }
@media (hover: hover) and (pointer: fine) {
  .plate:hover .plate__shot img { transform: scale(1.035); }
}
.plate__meta { display: flex; align-items: center; gap: 0.5rem; }
.dot { width: 0.45rem; height: 0.45rem; border-radius: 50%; background: currentColor; flex: none; opacity: 0.55; }
.dot--live { background: var(--ember); opacity: 1; animation: dot-pulse 2.6s ease-in-out infinite; }
@keyframes dot-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--ember) 55%, transparent); }
  50%      { box-shadow: 0 0 0 0.32rem color-mix(in oklab, var(--ember) 0%, transparent); }
}

/* --------------------------------------------------------- 5 · the tiles --
   A hairline lexicon, not six cards: one shared grid line between cells, the
   ground showing through, and a drawn mark per trade. */
/* THREE COLUMNS, STATED. `auto-fit` with a 15rem floor resolved to four at
   1440 and left the sixth cell of a six-item list as a two-column hole with
   the hairline ground showing through it. A fixed lexicon of six wants a
   factor of six, not whatever the viewport arithmetic lands on. */
.tiles {
  list-style: none; margin: clamp(1.5rem, 4vh, 2.5rem) 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px; background: var(--rule); border: 1px solid var(--rule);
}
@media (max-width: 900px) { .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px) { .tiles { grid-template-columns: minmax(0, 1fr); } }
.tiles li { background: var(--sc-canvas); }
.tiles a {
  display: grid; gap: 0.55rem; align-content: start;
  padding: clamp(1.1rem, 2.6vw, 1.6rem);
  text-decoration: none; color: var(--fg); height: 100%;
  transition: background-color 200ms var(--sc-ease-out);
}
.tile__ico { width: 1.9rem; height: 1.9rem; color: var(--accent); transition: transform 300ms var(--sc-ease-out); }
.tiles a span {
  font-family: var(--sc-font-display); font-size: clamp(1.15rem, 1rem + 0.6vw, 1.5rem);
  letter-spacing: -0.025em; line-height: 1.12;
}
.tiles a small { font-family: var(--sc-font-text); font-size: 0.8125rem; color: var(--fg-dim); max-width: 26ch; }
@media (hover: hover) and (pointer: fine) {
  .tiles a:hover { background: color-mix(in oklab, var(--ink) 5%, transparent); }
  .tiles a:hover span { color: var(--accent); }
  .tiles a:hover .tile__ico { transform: translateY(-2px) scale(1.06); }
}

/* ----------------------------------------------------- 7 · the drawn rail --
   The flattest act on the page gets the cheapest possible piece of life: each
   step's own rule draws itself as the step arrives. The engine already tags
   these children with .sc-in for the stagger, so this costs no JavaScript. */
.process__list li { position: relative; }
.process__list li::after {
  content: ""; position: absolute; top: -1px; left: 0; width: 100%; height: 1px;
  background: var(--accent); transform: scaleX(0); transform-origin: 0 50%;
  transition: transform 760ms var(--sc-ease-out);
}
.process__list li.sc-in::after { transform: scaleX(1); }

/* ---------------------------------------------------------------- narrow -- */
@media (max-width: 620px) {
  /* Six chips at full size push the hero's flow content into the portrait
     band on a small phone. Shrinking the chip rather than the type keeps the
     thumb target legal at 44px while giving the row a third less height. */
  .chip { font-size: 0.8125rem; padding: 0.55rem 0.75rem; }
  .ask { margin-top: 0.9rem; }
  .ask__q { margin-bottom: 0.6rem; }
}


/* ------------------------------------------------------- the head answers --
   `.dock` is a fixed overlay at z-index 40 with `pointer-events: none`, so it
   cannot steal a click from the content underneath it. Exactly one child takes
   pointer events back, and only across the face itself. */
.dock__hit {
  display: block; width: 100%;
  margin: 0; padding: 0; border: 0; background: none; font: inherit; color: inherit;
  pointer-events: auto; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* A focus ring inside a transformed ancestor is scaled with it, and the dock
   scales to 0.145 when minimised — a 2px outline would land at a third of a
   pixel and effectively vanish. The three dock states are discrete classes, so
   the ring is simply pre-multiplied for each one. */
/* ROUND 11 - the ring is computed, not guessed. The parked scale is a measured
   ratio now (parked pixels over the dock's layout width) rather than one of
   three hard-coded factors, so any fixed outline width would render anywhere
   between 0.6px and 3px depending on the viewport. companion.js publishes
   --ring as exactly the width that lands 3 CSS pixels AFTER the scale, and the
   harness measures the on-screen product rather than the declaration. */
.dock__hit:focus-visible {
  outline: var(--ring, 3px) solid var(--accent);
  outline-offset: calc(var(--ring, 3px) * 2);
}

/* The bubble is a SIBLING of the dock at body level, never a child: anything
   parented to the dock inherits its scale and would render at 14% in the
   minimised state. talk.js positions it against the dock's measured rect. */
.say {
  /* ROUND 10.1 — z-index 41 put this UNDER the masthead (core.css, z-index 60),
     so when place() flipped the bubble above the face the nav links printed
     straight through the question. Above the topline, still below the full-page
     menu overlay at 90. place() also now refuses to park it in the topline's
     band at all; both halves are needed, because painting over the nav would
     merely have made an unreadable overlap legible. */
  position: fixed; z-index: 70;
  max-width: min(23rem, calc(100vw - 2rem));
  background: var(--paper); color: var(--ink-paper);
  padding: 0.8rem 1.05rem; border-radius: 0.85rem;
  box-shadow: 0 18px 40px hsl(160 30% 2% / 0.45);
  opacity: 0; translate: 0 -6px;
  transition: opacity 200ms var(--sc-ease-out), translate 200ms var(--sc-ease-out);
  pointer-events: none;
}
.say.is-on { opacity: 1; translate: 0 0; }
.say__text { margin: 0; font-size: var(--t-quiet); line-height: 1.55; }
/* Narrow screens: the bubble becomes a foot-anchored line rather than an
   anchored bubble, because under the face is exactly where the wordmark is. */
.say--foot { max-width: none; }


/* ------------------------------------------------------------- 6 · the maker --
   The one act the visitor operates. Controls on the left, a life-size phone on
   the right — near 1:1 with a real handset (19rem = 304px against a phone's
   ~390px), so the type inside it is set at realistic sizes rather than shrunk
   into illegibility the way a scaled-down mock-up would be. */
.make { padding-block: clamp(3.5rem, 10vh, 6.5rem); }
.make__grid {
  display: grid; gap: clamp(2.5rem, 6vw, 5rem);
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.78fr);
  align-items: center;
}
@media (max-width: 860px) { .make__grid { grid-template-columns: 1fr; } }

.cfg { margin-top: clamp(1.75rem, 4vw, 2.5rem); display: grid; gap: 1rem; }
.cfg__row {
  display: grid; grid-template-columns: 4.25rem minmax(0, 1fr);
  gap: 1rem; align-items: start;
}
/* Six trades wrap to two rows at most widths; the label reads as a heading for
   the group, so it aligns with the first row rather than floating in the middle
   of the block. */
.cfg__label { padding-top: 0.85rem; }
@media (max-width: 520px) { .cfg__row { grid-template-columns: 1fr; gap: 0.45rem; } }
.cfg__label {
  font-size: var(--t-quiet); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--fg-dim);
}
.cfg__opts { display: flex; flex-wrap: wrap; gap: 0.45rem; }

/* Selection reads the same as hover, because the selected option IS the one
   the phone is currently showing. */
.chip[aria-pressed="true"] {
  color: var(--ember-ink); background: var(--ember); border-color: var(--ember);
}

/* The ink swatch is nearly the ground colour, so every swatch carries a hairline
   ring by default — without it that one option is invisible on this band. */
.sw {
  width: 1.75rem; height: 1.75rem; min-height: 1.75rem; border-radius: 50%;
  padding: 0; cursor: pointer; border: 2px solid transparent;
  box-shadow: 0 0 0 1px hsl(0 0% 100% / 0.22);
  transition: transform 160ms var(--sc-ease-out), border-color 160ms var(--sc-ease-out);
}
.sw--clay { background: #A34A25; }
.sw--jade { background: #2F6B52; }
.sw--ink  { background: #1D2733; }
.sw--sun  { background: #8A5A12; }
@media (hover: hover) and (pointer: fine) { .sw:hover { transform: scale(1.12); } }
.sw[aria-pressed="true"] { border-color: var(--fg); box-shadow: 0 0 0 1px hsl(0 0% 100% / 0.35); }

.make .act-cta { margin-top: clamp(1.75rem, 3.5vw, 2.5rem); }
.make__stage { display: grid; place-items: center; }
.phone {
  width: min(19rem, 72vw); aspect-ratio: 9 / 16.5;
  background: #0B0F0E; border-radius: 2.1rem; padding: 0.45rem;
  box-shadow: 0 34px 70px hsl(160 30% 2% / 0.62), inset 0 0 0 1px hsl(0 0% 100% / 0.09);
}
@media (max-width: 860px) { .phone { width: min(16.5rem, 62vw); } }

.phone__screen {
  height: 100%; border-radius: 1.75rem; overflow: hidden;
  background: var(--pp); display: grid; grid-template-rows: auto 1fr;
  transition: background 420ms var(--sc-ease-out);
}
.phone__bar {
  padding: 0.55rem 0.75rem; background: var(--pb); display: flex; justify-content: center;
  transition: background 420ms var(--sc-ease-out);
}
.phone__url {
  font-size: 0.6rem; color: var(--ps); letter-spacing: 0.01em;
  transition: color 420ms var(--sc-ease-out);
}
.phone__body {
  padding: 1rem 0.9rem; display: grid; gap: 0.55rem;
  /* The blocks take the leftover height, so the screen reads as a finished
     page rather than a page that stopped halfway down. */
  grid-template-rows: auto auto auto auto minmax(0, 1fr);
}
.phone__eyebrow {
  font-size: 0.62rem; letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--pa); transition: color 420ms var(--sc-ease-out);
}
.phone__h {
  margin: 0; font-family: var(--pf); font-weight: var(--pw); letter-spacing: var(--pt);
  font-size: 1.3rem; line-height: 1.1; color: var(--pi);
  transition: color 420ms var(--sc-ease-out);
}
.phone__p {
  font-size: 0.78rem; line-height: 1.5; color: var(--ps);
  transition: color 420ms var(--sc-ease-out);
}
.phone__cta {
  justify-self: start; font-size: 0.72rem; padding: 0.45rem 0.85rem;
  background: var(--pa); color: var(--po); border-radius: var(--pr);
  transition: background 420ms var(--sc-ease-out), color 420ms var(--sc-ease-out),
              border-radius 420ms var(--sc-ease-out);
}
.phone__grid { display: grid; gap: 0.4rem; margin-top: 0.25rem; min-height: 0; }
.phone__grid span {
  background: var(--pc); border-radius: var(--pr); min-height: 2.2rem;
  transition: background 420ms var(--sc-ease-out), border-radius 420ms var(--sc-ease-out);
}
.phone[data-layout="stack"] .phone__grid { grid-template-columns: 1fr; grid-auto-rows: 1fr; }
.phone[data-layout="split"] .phone__grid { grid-template-columns: 1fr 1fr; grid-auto-rows: 1fr; }
.phone[data-layout="bento"] .phone__grid { grid-template-columns: 1.6fr 1fr; grid-auto-rows: 1fr; }
.phone[data-layout="bento"] .phone__grid span:first-child { grid-row: span 2; }
/* Four blocks: split resolves to a clean 2x2, and bento closes with a full
   width band instead of leaving a hole in the corner. */
.phone[data-layout="bento"] .phone__grid span:last-child { grid-column: span 2; }

/* The first state is authored into the markup, so these must resolve before
   make.js runs — otherwise a no-JS visitor gets an unpainted phone. */
.phone {
  --pa: #A34A25; --pi: #2A211C; --ps: #6B5B52; --pc: #E4D9D1;
  --pp: #F3EDE8; --pb: #E4DBD4; --po: #FFF6F1;
  --pf: "Playfair Display", Georgia, serif; --pw: 500; --pt: -0.01em; --pr: 3px;
}

/* ============================================================================
   ROUND 10 — THE MASCOT IS ALWAYS ON

   Daniel's call, and it reverses an earlier one: the corner watcher used to be
   suppressed below 860px and under reduced motion, on the reasoning that a head
   shadowing you down a phone screen is a decoration rather than a device. The
   brief is now the Bike Bear brief — the character IS the branding, so he has
   to be on screen in every act at every width or the recognition never builds.

   Three things keep him from becoming the nuisance the old rule feared:
   he parks bottom-right, clear of the now-always-visible topline; he shrinks
   while the page is actually moving; and he never takes a pointer event except
   across his own face.
   ========================================================================== */

/* ROUND 11 - the three blocks that used to live here all sized or moved the
   retired `--min` state by hand at a breakpoint. Both jobs moved: the parked
   SIZE is stepped in companion.js (one number, reused by the focus ring), and
   the parked POSITION is measured per act. The busy-state rules moved up beside
   the dock itself so the whole travel story reads in one place. */

/* The foot-anchored toast and the parked head are both bottom-right below
   640px, so the toast has to clear him. It is done in talk.js place(), NOT
   here: place() writes an inline `top`, and a `bottom` in CSS alongside it
   stretches the fixed box between the two edges. Measured before this note was
   written — the bubble came out 712px tall on a 844px phone. */

/* ------------------------------------------------- the conversation, in him --
   ROUND 10. The bubble now has to hold a question and its answers, not just a
   remark. It is a PAPER surface sitting on a dark page, so it re-declares the
   paper tokens locally — the global .chip is coloured for the dark ground and
   would come out invisible in here. */
.say {
  --fg: var(--ink-paper);
  --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);
}
.say--ask {
  max-width: min(26rem, calc(100vw - 2rem));
  padding: 0.95rem 1.05rem 1.05rem;
  /* THE base .say is `pointer-events: none` — correct for a passive one-line
     remark that must never block the content under it, and fatal the moment the
     bubble contains buttons. Measured live: the chips rendered, and every click
     landed on .hero__plate underneath. Only the conversation takes events back. */
  pointer-events: auto;
}

.say__q {
  margin: 0 0 0.7rem;
  font-family: var(--sc-font-display);
  font-size: clamp(1rem, 0.95rem + 0.4vw, 1.18rem);
  line-height: 1.25; letter-spacing: -0.02em;
  color: var(--ink-paper);
}
.say__row { display: flex; flex-wrap: wrap; gap: 0.4rem; }

.say__chip {
  font-family: var(--sc-font-text); font-size: 0.83rem; font-weight: 500;
  color: var(--ink-paper); background: transparent;
  border: 1px solid var(--rule-firm); border-radius: var(--sc-r-pill);
  padding: 0.5rem 0.8rem; min-height: 2.5rem;   /* thumb target */
  cursor: pointer; text-align: left;
  transition: color 160ms var(--sc-ease-out), border-color 160ms var(--sc-ease-out),
              background-color 160ms var(--sc-ease-out), transform 160ms var(--sc-ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .say__chip:hover { color: var(--paper); background: var(--ember-band); border-color: var(--ember-band); transform: translateY(-1px); }
}
.say__chip:active { transform: translateY(1px) scale(0.985); }

/* -- the payoff state ---------------------------------------------------- */
.say__read {
  margin: 0 0 0.55rem;
  font-family: var(--sc-font-display);
  font-size: clamp(0.98rem, 0.94rem + 0.35vw, 1.12rem);
  line-height: 1.3; letter-spacing: -0.02em; color: var(--ink-paper);
}
.say__price {
  margin: 0 0 0.7rem; font-size: 0.9rem; font-weight: 600;
  letter-spacing: 0.02em; color: var(--ember-band);
}
.say__cta {
  display: inline-block; text-decoration: none;
  font-family: var(--sc-font-text); font-size: 0.86rem; font-weight: 600;
  background: var(--ember-band); color: var(--paper);
  border-radius: var(--sc-r-pill); padding: 0.6rem 1.05rem; min-height: 2.6rem;
  line-height: 1.4;
}
@media (hover: hover) and (pointer: fine) {
  .say__cta:hover { background: var(--ink-paper); }
}
.say__again {
  display: inline-block; margin-inline-start: 0.6rem;
  background: none; border: 0; padding: 0.4rem 0;
  font-family: var(--sc-font-text); font-size: 0.78rem;
  color: var(--ink-paper-soft); text-decoration: underline;
  text-underline-offset: 3px; cursor: pointer;
}
@media (hover: hover) and (pointer: fine) { .say__again:hover { color: var(--ink-paper); } }

/* ============================================================================
   7 · THE STORY — the process act, dramatised

   Five steps that used to be five lines of two words. Now one browser window is
   born, priced at nothing, judged, grown and handed over while the copy beats
   cross-fade beside it. Everything is drawn: hairlines, ground and ink, no
   media, nothing generated. The engine owns opacity via the cue windows in the
   markup; this file owns geometry only.
   ========================================================================== */
.story__inner {
  width: 100%; max-width: var(--sc-maxw);
  margin-inline: auto; padding-inline: var(--sc-gutter);
  display: grid; gap: clamp(1.5rem, 4vw, 3.5rem);
  grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
  align-items: center;
}
.story__lead { grid-column: 1 / -1; }
.story__note { margin-top: 1rem; color: var(--fg-dim); }

/* -- the beats: one at a time, same crossfade grammar as the floor act ----- */
.story__beats {
  position: relative; list-style: none; margin: 0; padding: 0;
  min-height: clamp(7rem, 20vh, 10rem);
}
.story__beats li {
  position: absolute; inset: 0 auto auto 0; width: 100%;
  display: grid; grid-template-columns: 2.2rem minmax(0, 1fr); gap: 0.9rem;
  align-items: baseline;
}
.story__beats b {
  font-family: var(--sc-font-text); font-size: 0.78rem; font-weight: 600;
  letter-spacing: 0.1em; color: var(--accent);
}
.story__beats p {
  margin: 0; max-width: 22ch; color: var(--ink);
  font-family: var(--sc-font-display);
  font-size: clamp(1.3rem, 1rem + 1.7vw, 2.4rem);
  line-height: 1.16; letter-spacing: -0.025em;
}

/* -- the scene ------------------------------------------------------------ */
.story__scene { position: relative; aspect-ratio: 4 / 3; width: 100%; }
.story__scene > * { position: absolute; inset: 0; }

/* 1 · two people talking, before anything exists */
.sc-chat { display: flex; flex-direction: column; justify-content: center; gap: 0.7rem; padding: 12%; }
.sc-chat__b { display: block; height: 0.85rem; border-radius: var(--sc-r-pill); background: var(--rule-firm); }
.sc-chat__b--them { width: 58%; }
.sc-chat__b--me   { width: 42%; margin-inline-start: auto; background: color-mix(in oklab, var(--ember) 55%, transparent); }
.sc-chat__b--sm   { width: 34%; }

/* 2-5 · the window itself */
.sc-win { display: grid; place-items: center; }
.sc-sheet {
  position: absolute; inset: 12% 8% 14% 12%;
  border: 1px solid var(--rule); border-radius: 0.55rem; background: var(--sc-ground, transparent);
}
.sc-sheet--b { translate: 3% -3%; }
.sc-sheet--c { translate: 6% -6%; opacity: 0.6; }

.sc-win__frame {
  position: relative; width: 84%; border: 1px solid var(--rule-firm);
  border-radius: 0.55rem; overflow: hidden; background: color-mix(in oklab, var(--ink) 6%, transparent);
}
.sc-win__bar {
  display: flex; align-items: center; gap: 0.3rem;
  padding: 0.5rem 0.7rem; border-bottom: 1px solid var(--rule);
}
.sc-win__bar i { width: 0.4rem; height: 0.4rem; border-radius: 50%; background: var(--rule-firm); }
.sc-win__url {
  margin-inline-start: 0.6rem; flex: 1; height: 1.1rem; border-radius: var(--sc-r-pill);
  background: var(--rule); display: flex; align-items: center; padding-inline: 0.55rem;
  font-family: var(--sc-font-text); font-size: 0.62rem; letter-spacing: 0.02em; color: var(--fg-soft);
}
.sc-win__body { padding: 0.9rem; display: grid; gap: 0.5rem; }
.sc-win__hero { height: clamp(2.6rem, 7vw, 4.2rem); border-radius: 0.3rem; background: color-mix(in oklab, var(--ember) 26%, transparent); }
.sc-win__ln { height: 0.5rem; border-radius: var(--sc-r-pill); background: var(--rule-firm); }
.sc-win__ln--w1 { width: 76%; }
.sc-win__ln--w2 { width: 54%; }
.sc-win__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.45rem; }
.sc-win__grid span { height: clamp(1.1rem, 3vw, 1.9rem); border-radius: 0.25rem; background: var(--rule); }
.sc-win__zh {
  font-family: var(--sc-font-text); font-size: 0.62rem; letter-spacing: 0.06em;
  color: var(--ember); margin-top: 0.1rem;
}

/* the price of the first look, and the version that walks away */
.sc-tag {
  position: absolute; inset: auto auto 12% 4%; width: auto; height: auto;
  font-family: var(--sc-font-text); font-size: 0.95rem; font-weight: 700; letter-spacing: 0.02em;
  color: var(--ember-ink); background: var(--ember);
  border-radius: var(--sc-r-pill); padding: 0.3rem 0.75rem;
}
.sc-no {
  position: absolute; inset: 10% 22% 16% 2%; width: auto; height: auto;
  border: 1px dashed var(--rule-firm); border-radius: 0.55rem;
  translate: -12% 6%; rotate: -4deg;
}
.sc-seal {
  position: absolute; inset: auto 4% 8% auto; width: auto; height: auto;
  font-family: var(--sc-font-text); font-size: 0.68rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--fg-dim);
  border: 1px solid var(--rule-firm); border-radius: var(--sc-r-pill); padding: 0.3rem 0.7rem;
}

@media (max-width: 860px) {
  .story__inner { grid-template-columns: minmax(0, 1fr); gap: clamp(1.25rem, 4vh, 2rem); }
  .story__scene { order: 2; aspect-ratio: 5 / 4; }
  .story__beats { order: 3; min-height: clamp(6rem, 18vh, 8rem); }
  .story__beats p { font-size: clamp(1.2rem, 1rem + 2.4vw, 1.8rem); max-width: 26ch; }
}

/* ------------------------------------------------------- the pricing deck --
   ROUND 10. The five published packages were a flat list of rows; they are
   cards again, and the deck scrolls. It sits on the paper band, so it inherits
   the daylight tokens — and it stays hairline-and-ink, never a raised card,
   because the poster grammar holds here exactly as it does everywhere else.
   The DIAL and the DECK are one control seen from two ends. */
.deck {
  list-style: none; margin: clamp(1.25rem, 4vh, 2rem) 0 0; padding: 0 0 0.5rem;
  display: flex; gap: 0.75rem;
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: thin;
  /* full-bleed edges so the last card does not look clipped by the gutter */
  margin-inline: calc(-1 * var(--sc-gutter)); padding-inline: var(--sc-gutter);
}
.deck__card {
  flex: 0 0 min(15.5rem, 76vw);
  scroll-snap-align: center;
  display: flex; flex-direction: column; gap: 0.4rem;
  padding: 1.05rem 1.1rem 1.15rem;
  border: 1px solid var(--rule); border-radius: 0.7rem;
  cursor: pointer;
  transition: border-color 200ms var(--sc-ease-out), background-color 200ms var(--sc-ease-out),
              transform 200ms var(--sc-ease-out);
}
/* SELECTED IS A DOUBLE STROKE, NOT A TINT.
   The 7% accent wash under the selected card dropped the paper from #E6E5E0 to
   #E2DAD2, which took the card's own ember label from 4.8:1 to 4.3:1 and failed
   the Lighthouse contrast audit — a wash applied to make a card feel selected,
   costing the legibility of the word that names it. A second inset stroke says
   the same thing, costs nothing, and suits a page with no card surfaces on it
   anywhere else. */
.deck__card.is-on {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 2px color-mix(in oklab, var(--accent) 22%, transparent);
  transform: translateY(-2px);
}
@media (hover: hover) and (pointer: fine) {
  .deck__card:hover { border-color: var(--rule-firm); }
}
.deck__name {
  margin: 0; font-family: var(--sc-font-text); font-size: 0.8rem; font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent);
  display: flex; align-items: center; gap: 0.45rem; flex-wrap: wrap;
}
.deck__tag {
  font-size: 0.62rem; font-weight: 600; letter-spacing: 0.06em; text-transform: none;
  color: var(--paper); background: var(--accent);
  border-radius: var(--sc-r-pill); padding: 0.12rem 0.45rem;
}
.deck__price {
  margin: 0; font-family: var(--sc-font-display);
  font-size: clamp(1.5rem, 1.2rem + 1.1vw, 2.05rem);
  line-height: 1.05; letter-spacing: -0.03em; color: var(--fg);
}
.deck__meta { margin: 0; font-size: 0.78rem; color: var(--fg-dim); }
.deck__inc {
  list-style: none; margin: 0.45rem 0 0; padding: 0;
  display: grid; gap: 0.3rem; flex: 1;
}
.deck__inc li {
  font-size: 0.82rem; color: var(--fg-soft); line-height: 1.35;
  padding-inline-start: 0.85rem; position: relative;
}
.deck__inc li::before {
  content: ""; position: absolute; inset-inline-start: 0; top: 0.55em;
  width: 0.3rem; height: 1px; background: var(--accent);
}
.deck__go {
  margin-top: 0.7rem; align-self: flex-start;
  font-family: var(--sc-font-text); font-size: 0.8rem; font-weight: 600;
  color: var(--fg); text-decoration: none;
  border-bottom: 1px solid var(--accent); padding-bottom: 2px;
}
@media (hover: hover) and (pointer: fine) { .deck__go:hover { color: var(--accent); } }

/* --------------------------------------------------- the offer, with teeth --
   ROUND 10. The free page needed to feel finite. No counter — a "3 left" that
   never moves reads as a fake, and Daniel chose against maintaining one. The
   real constraint carries it instead, set at display size so it lands before
   the terms list is read at all. */
.offer__limit {
  margin: 1.6rem 0 0.4rem;
  font-family: var(--sc-font-display);
  font-size: clamp(1.35rem, 1.05rem + 1.6vw, 2.15rem);
  line-height: 1.14; letter-spacing: -0.028em;
  color: var(--ink); max-width: 18ch;
}
.offer__limit-sub {
  margin: 0 0 1.4rem; max-width: 44ch;
  font-size: 0.95rem; color: var(--fg-soft);
}
/* The price of the first look, said on the thing being offered rather than
   about it. Same device the story act uses, so they rhyme. */
.mock { position: relative; }
.mock__tag {
  position: absolute; z-index: 2; inset: auto auto 8% -0.5rem;
  font-family: var(--sc-font-text); font-size: 0.95rem; font-weight: 700;
  letter-spacing: 0.02em; color: var(--ember-ink); background: var(--ember);
  border-radius: var(--sc-r-pill); padding: 0.3rem 0.8rem;
  box-shadow: 0 10px 26px hsl(160 30% 2% / 0.35);
}


/* ============================================================================
   ROUND 11 — WHAT THE PAGE LOOKS AT

   Daniel's note was blunt and correct: "there are still a lot of blank and no
   graphic in the website". The old page was 16.8 viewport-heights of dark grey
   with type on it and three small screenshots, and several acts ended in a
   thousand pixels of nothing.

   Two answers, and only one of them is imagery. Every pinned span came down, so
   the page is ~12.5vh instead of 16.8 — and three places gained a real picture,
   each with a job it alone can do. Nothing here is a photograph put in to fill
   a hole: the grammar is still typographic, and every image is MASKED AWAY from
   the type rather than scrimmed under it, so the copy keeps clean ground and
   the photograph keeps its full contrast.
   ========================================================================== */

/* ---------------------------------------------------------------- grain --
   A flat dark ground bands visibly on a real display, and this page is mostly
   ground. 4-5% of noise is the whole difference between "a dark page" and "a
   lit room" — the cheapest of the five depth tools, and the one this build had
   never used. It sits BEHIND the content, never over it: noise composited on
   top of type is a contrast cost paid on every line for an effect nobody
   asked for. */
.grain {
  position: fixed; inset: 0; z-index: 0;
  pointer-events: none; opacity: 0.05;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* ONLY the two statically-positioned regions get a stacking position here.
   `.topline` and `.menu` are already `position: fixed` and already carry their
   own z-index, and an earlier draft of this line handed them `position:
   relative` as well — which silently un-fixed the masthead on the homepage and
   let it scroll away with the document. A z-index needs a positioned element,
   not necessarily a relative one. */
body.home main, body.home .foot { position: relative; z-index: 1; }
/* The daylight bands paint their own opaque ground, so the grain does not reach
   them. That is correct: paper is meant to read as clean paper. */

/* ------------------------------------------------------ 6 · the floor act --
   Split stage. The copy holds the left column at a legible size and STAYS
   legible at every scroll position; the plate holds the right. What scroll
   drives is an ember marker travelling down the list — attention, not
   legibility. The previous version cross-faded four absolutely-positioned
   statements inside one box, so for most of a parkable pinned act three of them
   printed on top of each other and none could be read. */
.floor .sc-stage { display: grid; }
.floor__inner {
  align-self: center; width: 100%; max-width: var(--sc-maxw);
  margin-inline: auto; padding-inline: var(--sc-gutter);
  display: grid; gap: clamp(1.75rem, 5vw, 4rem);
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.82fr);
  align-items: center;
}
.floor__states { list-style: none; margin: clamp(1.25rem, 4vh, 2.25rem) 0 0; padding: 0; }
.floor__states li {
  display: grid; grid-template-columns: 1.6rem minmax(0, 1fr); gap: 0.9rem;
  align-items: baseline;
  padding: 0.85rem 0; border-top: 1px solid var(--rule);
}
.floor__states li:last-child { border-bottom: 1px solid var(--rule); }
/* The marker. The engine writes its opacity from the cue window, so the
   travelling highlight is engine-driven while the words never move. */
.floor__states i {
  display: block; width: 1.6rem; height: 2px;
  background: var(--ember); align-self: center;
}
.floor__states p {
  margin: 0; max-width: 34ch; color: var(--ink);
  font-family: var(--sc-font-display);
  font-size: clamp(1.05rem, 0.95rem + 0.75vw, 1.55rem);
  line-height: 1.22; letter-spacing: -0.02em;
}
.floor__plate { margin: 0; }
.floor__plate img {
  display: block; width: 100%; height: auto;
  border-radius: var(--sc-r-lg);
  /* Edge light plus a cast shadow with real offset — a raised thing catches
     light on its lip and throws the shadow downward. A zero-offset halo is
     decoration, not depth. */
  box-shadow: 0 1px 0 var(--edge-lip) inset, 0 24px 60px -24px var(--shadow-cast-soft);
}
.floor__plate figcaption { margin-top: 0.9rem; max-width: 34ch; }
@media (max-width: 860px) {
  .floor__inner { grid-template-columns: minmax(0, 1fr); gap: clamp(1.25rem, 4vh, 2rem); }
  .floor__plate { order: -1; }
  .floor__states p { font-size: clamp(1rem, 0.94rem + 0.7vw, 1.3rem); }
}

/* --------------------------------------------------- 5 · the phone's photo --
   Inside the mock-up, above the eyebrow. A grey rectangle here says "this is a
   wireframe"; a photograph of the visitor's own trade says "this is your
   homepage", which is the whole point of the act. Only the selected trade is
   ever fetched (make.js), and the swap fades rather than popping. */
.phone__shot {
  display: block; width: 100%; height: auto; aspect-ratio: 16 / 9;
  object-fit: cover; border-radius: calc(var(--pr) + 1px);
  margin-bottom: 0.15rem;
  transition: opacity 260ms var(--sc-ease-out);
}
.phone__shot.is-swapping { opacity: 0.25; }

/* ------------------------------------------------------ 8 · the faq ground --
   The last third of the page was two thousand pixels of dark grey with a list
   of questions on it. This gives it a place. The picture is masked away from
   the column the questions sit in — an overlay would have cost the photograph
   everything it was generated for, and a scrim over full-bleed media is the
   move this taste floor refuses. */
.faq { position: relative; overflow: clip; }
.faq .wrap { position: relative; z-index: 1; }
.faq .faq__list, .faq .label, .faq h2 { max-width: min(46rem, 100%); }

/* DEFAULT: a band at the foot of the act, not a side panel.

   The side treatment was written first and it is wrong below about 1200px, for
   a reason worth writing down: the question list is capped at 46rem, but the
   page gutter grows with the viewport, so between roughly 900 and 1199px the
   list occupies nearly the full width and there is no right-hand ground for a
   picture to live in. A side image there lands under the questions — measured
   at 1440 it was already grazing them, and the breakpoint I would have reached
   for (900px, the site's usual one) is exactly the width at which it is worst.

   So the band is the default at every width and the side panel is an
   enhancement that only switches on once there is provably room for it. This is
   the "scope the constraint to where the LAYOUT changes, not to a round number"
   rule; 1200px is where the measured clearance turns positive. */
/* The `#000` in the mask stops below is NOT a colour. A mask reads only the
   alpha channel, so the stop is "fully opaque" and the hue is arbitrary; every
   real colour on this page still resolves through a core.css token, so the new
   palette lands here without touching a line. */
.faq__ground {
  position: absolute; inset: auto 0 0 0;
  height: clamp(9rem, 26vh, 14rem); pointer-events: none; z-index: 0;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 62%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 62%);
}
.faq__ground img { width: 100%; height: 100%; object-fit: cover; object-position: 30% 60%; opacity: 0.42; }
.faq { padding-block-end: clamp(9rem, 24vh, 13rem); }

@media (min-width: 1200px) {
  .faq__ground {
    inset: 0 0 0 70%; height: auto;
    -webkit-mask-image: linear-gradient(to right, transparent 0%, hsl(0 0% 0% / 0.5) 26%, #000 72%);
    mask-image: linear-gradient(to right, transparent 0%, hsl(0 0% 0% / 0.5) 26%, #000 72%);
  }
  .faq__ground img { object-position: 22% 50%; opacity: 0.5; }
  .faq { padding-block-end: clamp(4rem, 10vh, 7rem); }
}

/* --------------------------------------------------- the bubble, transient --
   ROUND 11. It is a remark now rather than furniture: small, held for a few
   seconds, gone. `.say--live` is the only state that takes pointer events back,
   and talk.js decides it by looking for a control INSIDE the node rather than
   by trusting a caller to pass a flag — the flag version failed silently and
   totally, with every chip click landing on the plate underneath. */
.say--live { pointer-events: auto; }
.say__note { display: grid; gap: 0.6rem; justify-items: start; }
.say__sub {
  margin: -0.25rem 0 0.2rem; font-size: 0.82rem; line-height: 1.5;
  color: var(--ink-paper-soft); max-width: 30ch;
}
.say__chip--go {
  color: var(--paper); background: var(--ember-band); border-color: var(--ember-band);
  font-weight: 600;
}
@media (hover: hover) and (pointer: fine) {
  .say__chip--go:hover { background: var(--ink-paper); border-color: var(--ink-paper); color: var(--paper); }
}
.say__chip--quiet {
  border-color: transparent; color: var(--ink-paper-soft);
  text-decoration: underline; text-underline-offset: 3px;
}
@media (hover: hover) and (pointer: fine) {
  .say__chip--quiet:hover { background: transparent; border-color: transparent; color: var(--ink-paper); transform: none; }
}
.say__ask--hello .say__row { margin-top: 0.2rem; align-items: center; }


/* ============================================================================
   ROUND 12 — the offer act's right column

   Round 11 filled six of the nine acts and left this one holding a drawn
   browser frame and then several hundred pixels of empty ground, because the
   terms list on the left runs much longer than the mock on the right. The
   column is a stack now: the promise drawn, and under it the place the promise
   lands. Same shoot as everything else, same masking discipline — it sits in
   its own column and never goes behind the type.
   ========================================================================== */
.offer__side { display: grid; align-content: start; gap: clamp(1.5rem, 4vh, 2.5rem); }
.offer__plate { margin: 0; }
.offer__plate img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 3 / 2; object-fit: cover;
  border-radius: var(--sc-r-lg);
  /* The top edge dissolves into the act's ground so the plate reads as an
     undertow under the mock rather than as a second boxed component. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 22%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 22%);
}
@media (max-width: 900px) {
  /* On one column the mock is already the act's object; the photograph becomes
     a quiet foot to it rather than a second full-width picture.

     `width: 100%` is LOAD-BEARING and is not a tidy-up. `.offer__plate` is a
     grid item, and an auto inline margin on a grid item cancels the default
     `justify-self: stretch` — the item then shrink-to-fits, so its width comes
     from the image's INTRINSIC size instead of from the track. With a lazy
     image that is 20px until the bytes arrive and 352px afterwards, which
     reserves no box and drops 233px of layout onto the page mid-scroll.
     Measured: verify-pins failed 1 run in 4 at 390x844 with the document
     growing 11184 -> 11417 between the probe's measurement and its scroll,
     while the same probe passed 4/4 on the round-11 bytes. An explicit width
     makes the box definite before the image exists; max-width still caps it and
     the auto margins still centre it. */
  .offer__plate { width: 100%; max-width: 22rem; margin-inline: auto; }
}
