/* Dehydrate — direction K, "Plain confident".
   Near-white canvas, near-black ink, DM Sans throughout, pill actions.
   The colour budget is spent almost entirely on the forest cards: the check
   in the hero and the price. Everything else stays monochrome so those land.

   Scale: root is back to 100%. Forcing 125% made the type big but froze the
   proportions that worked, and on a 2560 screen the result used 48% of the width
   with 300px of dead air top and bottom. Size now comes from fluid clamps keyed
   to the viewport instead, so a large screen gets genuinely larger type rather
   than the same layout inflated. */

html{font-size:100%}

:root{
  --canvas:#fdfcfa;
  /* The alternating band tone. #f6f4f0 was 1.07:1 against the canvas, a 7%
     step that measured as an alternation and read as one flat white page.
     This is 1.19:1, which is the first value you actually see. It cannot go
     much further: at #e6e1d5 (1.27:1) --muted text on an alt band falls to
     4.39:1 and fails AA, so the ceiling here is contrast law, not taste. */
  --surface:#ece8df;
  --white:#fff;

  --ink:#0a0a0a;
  --body:#45403a;
  --muted:#6b655d;

  --line:#e8e5e0;
  --line-soft:#f1efeb;

  --forest:#0e3b2f;
  --forest-soft:#16513f;
  --forest-line:rgba(255,255,255,.12);
  --mint:#7ef0c0;
  --amber:#fbd88a;
  --on-forest:#c3d6ce;

  /* The only hue outside the forest/mint/amber budget, and it is a signal
     rather than colour: a field that will not submit, and a page that did not
     render. Two values because the two grounds are opposite — 7.5:1 on canvas,
     7.3:1 on forest. Both were loose hex literals until they were named. */
  --error:#9a2b1c;
  --error-on-forest:#ff9c8a;

  /* The ink band. Not colour — a ground, the same one the footer stands on —
     so the two forest surfaces keep the whole colour budget. Card lifts off
     the band, well recesses back toward it. All three text tints clear AA on
     the well, the darker of the two surfaces. */
  --ink-card:#171717;
  --ink-well:#0e0e0e;
  --ink-line:rgba(255,255,255,.10);
  --on-ink:#c9c5c0;      /* 10.5:1 on card */
  --on-ink-soft:#a5a09a; /* 7.4:1 on well */
  --on-ink-dim:#85807a;  /* 4.9:1 on well */

  --r-sm:.5rem;
  --r-md:.75rem;
  --r-lg:1rem;
  --r-hero:2rem;
  --pill:9999px;

  --nav-h:4.25rem;
  /* Band padding and the gap under a band's heading. Both were tighter and,
     worse, the heading gap was five different values across five sections. */
  --band:clamp(4.5rem,9.5vw,10rem);
  --gap-head:clamp(2.25rem,4vw,3.5rem);
  --pad:clamp(1.5rem,4vw,4.5rem);

  --ease:cubic-bezier(.16,1,.3,1);
}

*{box-sizing:border-box}

/* the nav is sticky, so without this every anchor in it parks its own section
   heading underneath the bar it was clicked from */
html{scroll-behavior:smooth;scroll-padding-top:calc(var(--nav-h) + 1.5rem)}
body{
  margin:0;
  background:var(--canvas);
  color:var(--ink);
  font-family:'DM Sans',system-ui,-apple-system,sans-serif;
  font-size:1rem;
  line-height:1.5;
  -webkit-font-smoothing:antialiased;
}

.shell{width:min(100% - 3rem,76rem);margin-inline:auto}
.shell.narrow{width:min(100% - 3rem,48rem)}
@media (max-width:700px){
  .shell,.shell.narrow{width:min(100% - 2rem,76rem)}
}

h1,h2,h3{margin:0;letter-spacing:-.035em;text-wrap:balance}
h1{font-size:clamp(2.2rem,5.75vw,3.7rem);font-weight:600;line-height:1.1;letter-spacing:-.04em}
h2{font-size:clamp(1.75rem,3vw,2.5rem);font-weight:600;line-height:1.15}
h3{font-size:1.125rem;font-weight:600;line-height:1.35;letter-spacing:-.02em}
p{margin:0}
a{color:inherit}

.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}

.skip{position:absolute;left:-9999px;top:0;background:var(--ink);color:#fff;
  padding:.75rem 1.25rem;border-radius:0 0 var(--r-sm) 0;z-index:99;font-weight:500}
.skip:focus{left:0}

:where(a,button,summary,input):focus-visible{
  outline:2px solid var(--ink);outline-offset:3px;border-radius:4px}

.mono{font-family:'DM Mono',ui-monospace,monospace;font-size:.9em}

/* ---------- nav ---------- */

/* Opaque, not frosted. At rest the nav is in flow above the hero and overlaps
   nothing, so the translucency bought nothing there; once stuck it overlapped
   four ink bands and the white body text under it stayed legible straight
   through the bar. A flat fill plus the hairline below is also what the rest of
   the system does, and it drops a 10px backdrop blur off a sticky element. */
.nav-wrap{position:sticky;top:0;z-index:20;background:var(--canvas);
  border-bottom:1px solid transparent;
  transition:border-color .25s var(--ease)}
.nav-wrap.stuck{border-bottom-color:var(--line)}
.nav{display:flex;align-items:center;gap:2rem;height:var(--nav-h)}
/* Wordmark only. The mark is a licensed stock glyph, which is the right call
   for a 16px browser tab and the wrong one next to a wordmark this
   distinctive: at 26px it added a generic element to an identity whose
   strength is its lettering. It also made the nav overflow at 320px. The
   favicon keeps it, where being an identifier matters more than being owned. */
.mark{font-size:1.1875rem;font-weight:700;letter-spacing:-.04em;text-decoration:none}
.nav-links{margin-left:auto;display:flex;gap:1.75rem;font-size:.875rem;font-weight:500;
  color:var(--body)}
.nav-links a{text-decoration:none;transition:color .15s var(--ease)}
.nav-links a:hover{color:var(--ink)}
/* The links used to vanish outright below 1075px, which left a phone and even
   a 768px tablet with no way to reach any section but the top. Now they only
   tighten there, and become a panel on a phone. */
.nav-toggle{display:none;align-items:center;justify-content:center;width:2.75rem;
  height:2.75rem;margin-left:.25rem;margin-right:-.6rem;border:0;background:none;
  color:var(--ink);cursor:pointer;border-radius:var(--r-sm)}

@media (max-width:1075px){
  .nav-links{gap:1.15rem;font-size:.8125rem}
}
@media (max-width:820px){
  .nav-links{gap:.9rem}
  .nav{gap:1rem}
}

/* Kept from the lockup era, when the icon pushed the bar 16px past a 320px
   viewport. The wordmark alone fits without it, but the bar is still the
   tightest row on the page there, so the margin stays. */
@media (max-width:380px){
  .nav{gap:.5rem}
  .mark{font-size:1.0625rem}
  .nav .pill{padding:0 1rem;font-size:.8125rem}
}

@media (max-width:699px){
  .nav-toggle{display:inline-flex}
  .nav .pill{margin-left:auto}
  /* .nav-wrap is sticky, so it is already the containing block for this */
  .nav-links{position:absolute;top:100%;left:0;right:0;margin:0;
    flex-direction:column;gap:0;font-size:1rem;
    background:var(--canvas);border-bottom:1px solid var(--line);
    padding:.35rem 0 .6rem;display:none}
  .nav-links.open{display:flex}
  .nav-links a{padding:.85rem max(1rem,calc((100vw - min(100% - 2rem,76rem)) / 2));
    color:var(--ink)}
  .nav-links a + a{border-top:1px solid var(--line-soft)}
}


.pill{display:inline-flex;align-items:center;justify-content:center;height:2.5rem;
  padding:0 1.375rem;border-radius:var(--pill);font-size:.875rem;font-weight:600;
  text-decoration:none;background:var(--ink);color:#fff;border:0;cursor:pointer;
  white-space:nowrap;transition:background .16s var(--ease),transform .16s var(--ease)}
.pill:hover{background:#2a2a2a}
.pill:active{transform:translateY(1px)}
.pill.light{background:var(--mint);color:var(--forest)}
.pill.light:hover{background:#96f5cd}

/* ---------- hero ---------- */

/* One viewport, and nothing below it. `svh` not `vh` so a phone's shrinking
   URL bar cannot push the next band into view, minus the nav because the nav
   is in flow above this.

   The earlier attempt at this pinned the hero to 100svh and centred a short
   composition in a tall box, which banked the remainder as dead air. The fix
   is not to give up on the viewport, it is to let the composition itself grow
   into a tall one: every gap in the stack below is keyed to `vh` with a
   ceiling high enough to matter, so a 1440-tall display spends its height on
   the spacing rather than on one lump of padding. What is left after that is
   centred, and on a display that tall the remainder reads as air around a
   composition rather than a composition stranded in a box.

   Above roughly 1200px of viewport height the h1 is pinned at the craft
   floor's 6rem display ceiling and cannot take up more, so some centred
   remainder is the honest cost of the guarantee. */
/* the nav is --nav-h tall plus its own 1px bottom hairline, and that hairline
   is the difference between landing on the fold and hanging 1px past it */
.hero{min-height:calc(100svh - var(--nav-h) - 1px);
  display:flex;flex-direction:column;justify-content:center;
  padding-block:clamp(1.5rem,5.6vh,7rem);text-align:center}
.hero h1{max-width:17ch;margin:0 auto;
  /* the vh term protects a short laptop, the vw term lets a big display use its
     size; 6rem is the craft floor's display ceiling. A 5rem cap sat below what
     the viewport was already asking for (109px at 2560, 82px at 1920) and banked
     the difference as dead air. Nothing changes at 1440 and below. */
  font-size:clamp(2.2rem,min(5.2vw,7.6vh),6rem)}
/* The headline marker. A real highlighter: a mint band sized to the x-height,
   sitting behind ink type rather than reversing it out. Because the band is
   shorter than the glyphs it can never touch the line above, which is what a
   filled slab could not promise (its box came out 77px against a 75px line
   box and sat on the descenders of the line above).

   Painted as a background gradient rather than a positioned pseudo-element so
   that `box-decoration-break:clone` carries it across a line break: on a phone
   the phrase wraps, and a ::before would have banded only the first fragment. */
.hl{
  background-image:linear-gradient(var(--mint),var(--mint));
  background-repeat:no-repeat;
  /* Measured from DM Sans itself, not guessed: at 68.4px the x-height top sits
     0.482em below the content-box top and the baseline at 0.994em. The band
     starts a hair above the first and ends a hair below the second, so it
     covers the letter bodies and clears the cap heights entirely. */
  background-size:100% .60em;
  background-position:0 .45em;
  padding-inline:.09em;
  -webkit-box-decoration-break:clone;box-decoration-break:clone;
}

.lede{font-size:clamp(1rem,min(1.3vw,2vh),1.25rem);line-height:1.55;color:var(--body);
  max-width:52ch;margin:clamp(.9rem,2.2vh,2.25rem) auto 0;text-wrap:pretty}

/* The product's one control, at full strength. It was 40px tall and outlined in
   `--line` — a white pill on a near-white canvas, held by a hairline you have to
   look for — on a page whose display type runs to 96px. The closing band is the
   worse case: there the field is the only object, and it read as a placeholder.

   The amplification is the system's own language rather than a new one: hairline
   chrome turned up to `--ink`, and the control sized to match what it is. No
   colour is spent, so the two forest cards keep the whole budget. */
.field{display:flex;gap:.5rem;background:var(--white);border:1px solid var(--ink);
  border-radius:var(--pill);padding:.4rem .4rem .4rem 1.7rem;align-items:center;
  max-width:46rem;margin:clamp(1.4rem,3.2vh,3.25rem) auto 0}
/* The resting border is now ink, so `border-color` can no longer say "focused".
   This is the system's own focus ring — the same 2px ink at 3px offset that
   every other control uses — moved onto the wrapper, with the input's ring
   suppressed so the two do not stack. `:has(input:focus)` rather than
   `:focus-within` keeps the ring off the field when the button inside is the
   thing being tabbed to; the button draws its own. */
.field:has(input:focus){outline:2px solid var(--ink);outline-offset:3px}
.field input:focus-visible{outline:0}
.field input{flex:1;border:0;outline:0;font:inherit;font-size:1.125rem;background:none;
  min-width:0;color:var(--ink);height:3.5rem}
.field input::placeholder{color:#78716a} /* 4.8:1 on white; #9d968c was 2.9:1 */
.field button{border:0;background:var(--ink);color:#fff;font:inherit;font-weight:600;
  font-size:1rem;height:3.5rem;padding:0 1.9rem;border-radius:var(--pill);cursor:pointer;
  white-space:nowrap;transition:background .16s var(--ease),transform .16s var(--ease)}
.field button:hover{background:#2a2a2a}
/* the same press the nav pill has. It is the button the whole page drives at;
   it should not be the one control that gives nothing back on the way down */
.field button:active:not([disabled]){transform:translateY(1px)}
/* was #b6b1a9, which is 2.13:1 against white — the status indicator during
   the longest wait on the site, unreadable. #5f5a53 is 6.5:1 and still
   reads as 'not pressable' beside the ink of the enabled state. */
.field button[disabled]{background:#5f5a53;cursor:default}

@media (max-width:700px){
  .field{flex-direction:column;gap:.5rem;border-radius:1.375rem;padding:.9rem;align-items:stretch}
  /* left-aligned, not centred with the band: a URL is content being edited, and
     a centred caret drifts as it is typed.

     `flex:none` is load-bearing. The form turns into a column here, so `flex:1`
     from the base rule applies to the vertical axis, and its `flex-basis:0%`
     outranks `height`. The site's primary input was rendering 21px tall on
     every phone because of it: half its intended size and far under any touch
     guideline. */
  .field input{flex:none;height:3rem;padding:0 .65rem;text-align:left;font-size:1.0625rem}
  .field button{width:100%;height:3.125rem}
}

/* scroll-margin so scrollIntoView('nearest') leaves the line clear of the edge
   rather than flush against it */
.field-error{max-width:46rem;margin:.75rem auto 0;font-size:.875rem;color:var(--error);font-weight:500;
  scroll-margin-block:2.5rem;
  /* the line drops out of the field rather than popping into the gap. Discrete
     `display` plus @starting-style means the `hidden` toggle in app.js animates
     on its own, both ways, with no JS. Same .16s the field border uses, so a
     rejected submit reads as one movement. */
  opacity:1;transform:none;
  transition:opacity .16s var(--ease),transform .16s var(--ease),
             display .16s allow-discrete}
.field-error[hidden]{opacity:0;transform:translateY(-.25rem)}
@starting-style{.field-error{opacity:0;transform:translateY(-.25rem)}}
.sub{font-size:.9375rem;color:var(--muted);margin-top:.9rem}

/* lives inside the forest floor now, so it is tinted from that hue, not grey */
.demo-note{font-size:.875rem;color:var(--on-forest);line-height:1.5;
  margin-top:clamp(.7rem,1.5vh,1.1rem);padding-top:clamp(.7rem,1.5vh,1.1rem);
  border-top:1px solid var(--forest-line)}
.demo-note.has-action{display:flex;align-items:center;gap:1.25rem;flex-wrap:wrap}
.demo-note.has-action .pill{height:2.75rem;padding:0 1.75rem;font-size:.9375rem}
.note-aside{max-width:52ch}

/* ---------- the live export panel ---------- */

/* A contained card in the column, not a full-bleed floor. Same width as the
   field above it, so the claim, the action and the proof read as one stack. */
.hero-floor{max-width:44rem;margin:clamp(1.4rem,3.2vh,3.25rem) auto 0;
  background:var(--forest);color:#fff;border-radius:1.5rem;text-align:left}
.floor-in{padding:1.25rem 1.5rem 1.4rem}

/* The swap from pipeline to verdict, which is the whole product in one moment.
   It used to be a single frame: the stage stack vanished, and the card jumped
   several hundred pixels while the rows were still fading in underneath. Now
   the stack steps aside first, then the card grows into its new size and the
   rows settle into the space as it opens.

   The height itself is animated in app.js, where both measurements exist; all
   this needs to do is clip while the box is moving, so growing content cannot
   spill the radius on the way. */
.floor-idle{transition:opacity .14s var(--ease),transform .14s var(--ease)}
.floor-idle.leaving{opacity:0;transform:translateY(-.25rem)}
.hero-floor.resizing{overflow:hidden}
.hero-foot{margin-top:clamp(1rem,2.4vh,2.5rem);font-size:.875rem;color:var(--muted);
  max-width:56ch;margin-inline:auto;text-wrap:pretty}

.floor-head{display:flex;align-items:center;gap:1rem;flex-wrap:wrap;
  padding-bottom:clamp(.6rem,1.4vh,1.15rem);border-bottom:1px solid var(--forest-line)}
.floor-head strong{font-size:1.0625rem;font-weight:600;letter-spacing:-.015em;flex:1;
  min-width:0;overflow-wrap:break-word}
/* Wedged between two nowrap siblings, the visitor's own hostname was
   rendering in a 33px column across six lines at 360px — a product whose
   one claim is 'your site will not break' showing their domain broken, in
   the panel that exists to prove it. Given its own line, it has the full
   width and needs no breaking at all. */
@media (max-width:560px){
  .floor-head{row-gap:.5rem}
  .floor-head strong{flex-basis:100%;order:-1;font-size:1rem}
}
.live{display:inline-flex;align-items:center;gap:.55rem;font-size:.875rem;
  color:var(--on-forest);white-space:nowrap}
.live i{width:.5rem;height:.5rem;border-radius:50%;background:rgba(255,255,255,.32)}
[data-state="running"] .live i{background:var(--mint);animation:beat 1.4s ease-in-out infinite}
[data-state="done"] .live i{background:var(--mint)}
@keyframes beat{50%{opacity:.35;transform:scale(.8)}}

/* idle: the real pipeline, waiting. Nothing here is invented. */
.floor-idle{padding-top:clamp(.9rem,2vh,1.4rem)}
.floor-title{font-size:1.125rem;font-weight:600;letter-spacing:-.02em;
  margin-bottom:clamp(.8rem,1.8vh,1.15rem)}
.stage-chips{list-style:none;margin:0;padding:0;display:grid;
  grid-template-columns:repeat(auto-fit,minmax(6.5rem,1fr));gap:.5rem}
.stage-chips li{display:flex;align-items:center;gap:.5rem;border:1px solid var(--forest-line);
  border-radius:.6rem;padding:.7rem .75rem;font-size:.8125rem;
  /* .42 composited over forest is 3.51:1 — an AA failure on the most
     prominent text above the fold, and it contradicted this system's own
     'verified, not assumed' claim. .66 is 5.4:1 and the dim/lit contrast
     still reads clearly against .done and .now below. */
  color:rgba(255,255,255,.66);
  transition:color .3s var(--ease),border-color .3s var(--ease)}
.stage-chips li::before{content:"";width:.5rem;height:.5rem;border-radius:50%;flex:none;
  box-shadow:inset 0 0 0 1px currentColor;transition:background .3s var(--ease)}
.stage-chips li.done{color:var(--on-forest);border-color:rgba(255,255,255,.2)}
.stage-chips li.done::before{background:var(--mint);box-shadow:none}
.stage-chips li.now{color:#fff;border-color:var(--mint)}
.stage-chips li.now::before{background:var(--mint);box-shadow:none;
  animation:beat 1.4s ease-in-out infinite}
/* The stage's own count, printed inside the chip it belongs to rather than in a
   separate readout. "Crawling 7/10" answers the question the bar cannot: not how
   far along, but how much there turned out to be. */
.stage-chips li b{margin-left:auto;font-weight:600;font-variant-numeric:tabular-nums;
  color:#fff;font-size:.8125rem}
.stage-chips li b:empty{display:none}

/* Counters. Tabular figures because these tick upward and a proportional font
   makes the whole row jitter on every increment. */
.meters{display:grid;grid-template-columns:repeat(4,1fr);gap:.5rem;
  margin-top:clamp(.85rem,1.9vh,1.2rem)}
.meters div{background:rgba(0,0,0,.22);border:1px solid var(--forest-line);
  border-radius:.6rem;padding:.7rem .5rem;text-align:center}
.meters b{display:block;font-size:clamp(1.25rem,3.4vw,1.6rem);font-weight:600;
  letter-spacing:-.02em;font-variant-numeric:tabular-nums;line-height:1.1}
.meters b i{font-style:normal;font-size:.6em;font-weight:500;margin-left:.1em;
  color:var(--on-forest)}
.meters span{display:block;margin-top:.25rem;font-size:.75rem;color:var(--on-forest)}
@media (max-width:520px){
  .meters{grid-template-columns:repeat(2,1fr)}
}

/* The log. aria-hidden in the markup on purpose: every line here is also spoken
   through #report-live as one short sentence per stage, and letting a screen
   reader follow a filename-per-frame feed would be unusable. This is for the
   eyes — evidence that something real is happening to real files. */
.log{list-style:none;margin:.6rem 0 0;padding:.6rem .75rem;border-radius:.6rem;
  background:rgba(0,0,0,.28);border:1px solid var(--forest-line);
  font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.75rem;
  line-height:1.7;height:8.5rem;overflow:hidden;display:flex;
  flex-direction:column;justify-content:flex-end}
.log li{display:flex;gap:.6rem;color:var(--on-forest);white-space:nowrap}
/* .45 composites to ~4.3:1 on the well and fails AA at 12px. .55 is ~5.7:1 and
   still reads as the quieter half of the line. */
.log li span{color:rgba(255,255,255,.55);flex:none;font-variant-numeric:tabular-nums}
.log li em{font-style:normal;overflow:hidden;text-overflow:ellipsis}
.log li:last-child{color:#fff}
@media (prefers-reduced-motion:no-preference){
  .log li{animation:logIn .18s var(--ease) both}
}
@keyframes logIn{from{opacity:0;transform:translateY(.3rem)}}

/* ---------- the preview ---------- */

/* Browser chrome around the visitor's own rebuilt site. The frame is deliberately
   plain: everything inside it belongs to someone else's design, and a decorated
   surround would compete with the one thing this panel exists to show. */
.viewer{margin:0 0 1.15rem;border-radius:.75rem;overflow:hidden;
  border:1px solid var(--forest-line);background:var(--ink-well)}
.viewer-bar{display:flex;align-items:center;gap:.75rem;padding:.55rem .75rem;
  background:rgba(255,255,255,.06);border-bottom:1px solid var(--forest-line)}
.dots{display:flex;gap:.3rem;flex:none}
.dots i{width:.55rem;height:.55rem;border-radius:50%;background:rgba(255,255,255,.22)}
.viewer-url{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font-size:.75rem;color:var(--on-forest);background:rgba(0,0,0,.3);
  border-radius:var(--pill);padding:.3rem .7rem}
.viewer-sizes{display:flex;gap:.25rem;flex:none}
.viewer-sizes button{border:1px solid transparent;background:none;font:inherit;
  font-size:.75rem;color:var(--on-forest);padding:.3rem .55rem;border-radius:.4rem;
  cursor:pointer;transition:background .2s var(--ease),color .2s var(--ease)}
.viewer-sizes button:hover{background:rgba(255,255,255,.09);color:#fff}
.viewer-sizes button.on{background:rgba(255,255,255,.14);color:#fff;
  border-color:var(--forest-line)}
.viewer-stage{background:#fff;display:flex;justify-content:center;
  height:clamp(19rem,46vh,28rem);transition:background .3s var(--ease)}
.viewer-stage.framed{background:var(--ink-well);padding:.9rem 0}
/* Width snaps rather than animating, and the fade carries the change instead.
   `transform:scaleX` is not the alternative here: squashing the render is the
   opposite of what the toggle is for — the visitor is asking how the site *lays
   out* at 390px, so the iframe has to be genuinely 390px wide. But transitioning
   that width means reflowing a full third-party React document every frame for
   350ms, which is real jank on a real machine rather than a theoretical cost.
   A viewport toggle is a discrete choice anyway; browsers' own device modes snap
   too. Opacity is compositor-only and says "this changed" for free. */
.viewer iframe{width:100%;height:100%;border:0;display:block;background:#fff;
  transition:opacity .2s var(--ease)}
.viewer-stage.resizing iframe{opacity:.35}
.viewer-stage.framed iframe{border-radius:.4rem;box-shadow:0 .5rem 2rem rgba(0,0,0,.4)}
.viewer-note{margin:0;padding:.6rem .75rem;font-size:.75rem;color:var(--on-forest);
  background:rgba(0,0,0,.22);border-top:1px solid var(--forest-line);line-height:1.5}
@media (max-width:560px){
  /* Three labelled buttons plus an address pill will not fit; the address is the
     one that carries meaning, so the size toggles wrap under it. */
  .viewer-bar{flex-wrap:wrap;row-gap:.5rem}
  .viewer-url{order:-1;flex-basis:100%}
  .viewer-sizes{margin-left:auto}
}

.floor-hint{margin-top:clamp(.85rem,1.9vh,1.2rem);font-size:.875rem;color:var(--on-forest);
  max-width:62ch;line-height:1.55}

/* ---------- the page picker ---------- */

.picker{padding-top:clamp(.9rem,2vh,1.4rem)}
.picker-sum{margin:.35rem 0 0;font-size:.875rem;color:var(--on-forest);line-height:1.55}
.picker-sum b{color:#fff;font-weight:600}
.picker-sum.over b{color:var(--amber)}
.picker-acts{display:flex;gap:.5rem;margin:.85rem 0 .6rem}
.picker-acts button{border:1px solid var(--forest-line);background:none;font:inherit;
  font-size:.75rem;color:var(--on-forest);padding:.35rem .7rem;border-radius:var(--pill);
  cursor:pointer;transition:background .2s var(--ease),color .2s var(--ease)}
.picker-acts button:hover{background:rgba(255,255,255,.1);color:#fff}
.picker-list{list-style:none;margin:0;padding:.25rem;max-height:16rem;overflow-y:auto;
  border:1px solid var(--forest-line);border-radius:.6rem;background:rgba(0,0,0,.22)}
.picker-list li{margin:0}
.picker-list label{display:flex;align-items:center;gap:.65rem;padding:.55rem .6rem;
  border-radius:.4rem;cursor:pointer;font-size:.875rem;color:var(--on-forest);
  min-height:2.75rem;transition:background .15s var(--ease)}
.picker-list label:hover{background:rgba(255,255,255,.07);color:#fff}
.picker-list input{accent-color:var(--mint);width:1.05rem;height:1.05rem;flex:none;margin:0}
.picker-list input:focus-visible{outline:2px solid var(--mint);outline-offset:2px}
.picker-list .p-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;color:#fff}
.picker-list .p-route{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:.75rem;color:rgba(255,255,255,.55);flex:none;max-width:45%;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.picker-go{display:flex;align-items:center;gap:1rem;margin-top:.9rem;flex-wrap:wrap}
.picker-go .pill{border:0;cursor:pointer;font:inherit}
.picker-go .pill[disabled]{opacity:.5;cursor:default}
.picker-cancel{background:none;border:0;font:inherit;font-size:.875rem;
  color:var(--on-forest);text-decoration:underline;text-underline-offset:3px;
  cursor:pointer;padding:.5rem 0;min-height:2.75rem}
.picker-cancel:hover{color:#fff}

/* ---------- export scope ---------- */

.modes{border:0;margin:.85rem auto 0;padding:0;display:flex;gap:.4rem;
  justify-content:center;flex-wrap:wrap}
.modes label{position:relative}
.modes input{position:absolute;inset:0;opacity:0;cursor:pointer;margin:0}
.modes span{display:block;border:1px solid var(--line);background:var(--white);
  color:var(--body);font-size:.8125rem;padding:.5rem .95rem;border-radius:var(--pill);
  cursor:pointer;transition:border-color .2s var(--ease),color .2s var(--ease),
    background .2s var(--ease)}
.modes label:hover span{border-color:#c9c4bc;color:var(--ink)}
.modes input:checked + span{background:var(--ink);border-color:var(--ink);color:#fff;
  font-weight:500}
.modes input:focus-visible + span{outline:2px solid var(--ink);outline-offset:2px}
/* 44px on the pointer-coarse path only: at 21px of text the pill is 34px tall on
   a mouse, which is fine, and padding it to 44 everywhere would break the row. */
@media (pointer:coarse){
  .modes span{padding-block:.7rem;min-height:2.75rem;display:flex;align-items:center}
}
.chip{background:var(--mint);color:var(--forest);font-size:.8125rem;font-weight:600;
  padding:.3rem .75rem;border-radius:var(--pill);white-space:nowrap;
  transition:background .3s var(--ease),color .3s var(--ease)}
.chip.warn{background:var(--amber);color:#3d2c05}
.chip.run{background:var(--forest-soft);color:var(--on-forest)}

/* Capped at three. auto-fit gave seven columns at 2560, which flattened the whole
   slab into a single-row status bar; three keeps it a panel with real height. */
.rows{display:grid;grid-template-columns:1fr;
  column-gap:clamp(1.75rem,3.5vw,4.5rem);
  padding:clamp(.2rem,.6vh,.35rem) 0 clamp(.5rem,1.4vh,1.15rem)}
@media (min-width:761px){.rows{grid-template-columns:1fr 1fr}}
.row{display:flex;align-items:center;gap:.9rem;
  padding:clamp(.42rem,1vh,.7rem) 0;font-size:1.0625rem;
  border-bottom:1px solid rgba(255,255,255,.09)}
/* The label used to be `flex:1; overflow-wrap:anywhere` against a `nowrap`
   verdict. A real engine message is a sentence — "page rendered blank with the
   network off" — so the nowrap sibling took the whole row and the label
   collapsed to a ~10px column, rendering one letter per line in a 150px-tall
   row. That fired on exactly the finding this product exists to surface.

   `min-width` gives the label a floor it cannot be squeezed below, the verdict
   is allowed to wrap, and `break-word` replaces `anywhere` so ordinary words
   stay whole; only an unbroken string like a long hostname is split. */
.row span:first-child{flex:1 1 auto;min-width:8ch;color:var(--on-forest);
  overflow-wrap:break-word}
.row .st{flex:0 1 auto;font-size:.8125rem;color:var(--mint);white-space:normal;
  text-align:right;overflow-wrap:break-word}
.tick{width:1.375rem;height:1.375rem;border-radius:var(--pill);background:var(--forest-soft);
  flex:none;display:grid;place-items:center}
.row.warn span:first-child,.row.warn .st{color:var(--amber)}
.tick.warn{background:rgba(251,216,138,.16)}

/* A page that did not render at all is not the same as a page that rendered
   with a font missing, and the engine already tells them apart via
   `issue.severity`. The report was throwing that away and painting both amber.
   Errors get the red, a filled plate and the row's own weight; warnings keep
   amber. #ff9c8a is 7.3:1 on forest. */
.row.failed span:first-child{color:var(--error-on-forest);font-weight:500}
.row.failed .st{color:var(--error-on-forest)}
.tick.failed{background:rgba(255,156,138,.18)}
.tick.failed svg path{stroke:var(--error-on-forest)}

/* the overflow row is a count, not a verdict: no plate, no tick */
.row.more span:first-child,.row.more .st{color:var(--on-forest);opacity:.72}

/* Focal moment: a result landing, not a fade. The label lifts out of dim, the
   verdict arrives from the right, the tick pops to size. Runs once, when real
   results arrive — there is no idle loop to perform any more. */
.row{animation:row-settle .5s var(--ease) both}
.row .st{animation:verdict .5s var(--ease) both}
.row .tick{animation:stamp .45s var(--ease) both}
.row:nth-child(1),.row:nth-child(1) *{animation-delay:.04s}
.row:nth-child(2),.row:nth-child(2) *{animation-delay:.08s}
.row:nth-child(3),.row:nth-child(3) *{animation-delay:.12s}
.row:nth-child(4),.row:nth-child(4) *{animation-delay:.16s}
.row:nth-child(5),.row:nth-child(5) *{animation-delay:.2s}
.row:nth-child(6),.row:nth-child(6) *{animation-delay:.24s}
.row:nth-child(7),.row:nth-child(7) *{animation-delay:.28s}
.row:nth-child(8),.row:nth-child(8) *{animation-delay:.32s}
.row:nth-child(n+9),.row:nth-child(n+9) *{animation-delay:.36s}

@keyframes row-settle{from{opacity:.3}to{opacity:1}}
@keyframes verdict{from{opacity:0;transform:translateX(.5rem)}to{opacity:1;transform:none}}
@keyframes stamp{0%{opacity:0;transform:scale(.72)}70%{opacity:1;transform:scale(1.06)}
  100%{opacity:1;transform:scale(1)}}
@keyframes arrive{from{opacity:0;transform:translateY(.4rem)}to{opacity:1;transform:none}}

/* The rows are the evidence, the summary and the download are the conclusion,
   and the conclusion used to be sitting there at full opacity while the rows
   were still arriving. Ordered behind them instead: the summary rides the same
   `verdict` motion the row statuses do, and the download lands last, at .92s,
   as the final row settles. Rare moment, earned beat. */
.card-foot{display:flex;align-items:baseline;gap:.6rem;flex-wrap:wrap;font-size:.9375rem;
  color:var(--on-forest);line-height:1.5;animation:verdict .4s var(--ease) .42s both}
.card-foot b{color:#fff;font-weight:500}
.floor-result .demo-note{animation:arrive .4s var(--ease) .52s both}

/* progress bar, shown only while a real export is running */
.bar{height:.25rem;border-radius:var(--pill);background:var(--forest-soft);overflow:hidden;
  margin-top:clamp(.8rem,1.8vh,1.2rem)}
.bar span{display:block;height:100%;width:100%;background:var(--mint);border-radius:var(--pill);
  transform:scaleX(0);transform-origin:left;transition:transform .5s var(--ease)}

/* ---------- bands ---------- */

.band{padding:var(--band) 0}
.band.alt{background:var(--surface)}
.band > .shell > h2{max-width:22ch}

.split{display:grid;grid-template-columns:1fr 1fr;gap:clamp(2rem,5vw,4.5rem);
  align-items:start;margin-top:var(--gap-head)}
@media (max-width:1100px){.split{grid-template-columns:1fr}}
.split h2{margin-bottom:1.15rem}
.split p{color:var(--body);max-width:60ch}
.split p + p{margin-top:1rem}

/* 2 · reasons, and the year that makes the case
   ------------------------------------------------------------------
   This was three columns of prose, the second thing anyone sees and the
   most text-heavy band on the page. The reasons became a stacked list so
   the argument has room to be drawn beside it. */

.why-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.05fr);
  gap:clamp(2rem,5vw,4.5rem);align-items:start;margin-top:var(--gap-head)}
@media (max-width:1100px){.why-split{grid-template-columns:1fr;gap:2.5rem}}

.reasons{display:grid;grid-template-columns:1fr;gap:0;margin:0}
.reasons > div{padding:1.15rem 0;border-top:1px solid var(--ink-line)}
.reasons > div:first-child{border-top:0;padding-top:0}
.reasons h3{margin-bottom:.4rem}
.reasons p{font-size:.9375rem;max-width:46ch}

/* Twelve identical sites, twelve charges. No price is printed anywhere:
   PRODUCT.md rules Framer's figure out, and the repetition says it without
   one. The last month dims rather than stopping, because the bill does not
   stop either. */
.months{margin:0;background:var(--ink-card);border:1px solid var(--ink-line);
  border-radius:var(--r-lg);padding:clamp(1.35rem,2.4vw,1.85rem)}
.months-head{display:flex;align-items:baseline;justify-content:space-between;
  gap:1rem;margin-bottom:1.15rem}
.months-head strong{font-size:.9375rem;font-weight:500;color:#fff}
.months-head span{font-size:.8125rem;color:var(--on-ink-dim)}

.months-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:.55rem}
.m{display:flex;flex-direction:column;gap:.4rem}
.m i{display:block;height:2.1rem;border-radius:.28rem;background:var(--ink-well);
  border:1px solid var(--ink-line);position:relative;overflow:hidden}
.m i::before{content:"";position:absolute;left:.28rem;right:.28rem;top:.3rem;height:.28rem;
  border-radius:2px;background:rgba(255,255,255,.34)}
.m i::after{content:"";position:absolute;left:.28rem;right:1.1rem;top:.85rem;height:.2rem;
  border-radius:2px;background:rgba(255,255,255,.11)}
.m b{display:block;height:.3rem;border-radius:2px;background:var(--mint);opacity:.85}
.m.dim i,.m.dim b{opacity:.3}

.months-foot{margin-top:1.15rem;padding-top:1.05rem;border-top:1px solid var(--ink-line);
  display:flex;gap:1.25rem;flex-wrap:wrap;font-size:.8125rem;color:var(--on-ink-dim)}
.months-foot span{display:inline-flex;align-items:center;gap:.45rem}
.months-foot em{width:.75rem;height:.3rem;border-radius:2px;background:var(--mint);
  font-style:normal;flex:none}
.months-foot u{width:.75rem;height:.55rem;border-radius:2px;background:var(--ink-well);
  border:1px solid var(--ink-line);text-decoration:none;flex:none}

@media (max-width:520px){.months-grid{grid-template-columns:repeat(4,1fr)}}

/* 3 · folder tree */

.tree{background:var(--white);border:1px solid var(--line);border-radius:var(--r-lg);
  overflow:hidden;font-size:.9375rem}
.tree-head{padding:.9rem 1.15rem;border-bottom:1px solid var(--line);background:var(--surface);
  font-family:'DM Mono',ui-monospace,monospace;font-size:.875rem;font-weight:500}
.tree ul{list-style:none;margin:0;padding:.5rem .35rem}
.tree li{display:flex;align-items:baseline;gap:1rem;padding:.5rem 1rem;
  font-family:'DM Mono',ui-monospace,monospace;font-size:.875rem}
.tree li span{margin-left:auto;font-family:'DM Sans',sans-serif;font-size:.8125rem;
  color:var(--muted);text-align:right}
.tree li.dir{font-weight:500}

/* 4 · the check */
.strong-line{margin-top:1.25rem!important;font-weight:500;color:var(--ink)!important}
.widths{display:grid;grid-template-columns:1.55fr 1fr .58fr;gap:1.35rem;align-items:end}
.widths figure{margin:0}
.screen{position:relative;background:var(--white);border:1px solid var(--line);
  border-radius:var(--r-md);padding:.7rem;display:flex;flex-direction:column;gap:.4rem}
.screen.desktop{height:12.875rem}
.screen.tablet{height:11.375rem}
.screen.phone{height:9.625rem}
.scr-hd{flex:none;height:.8125rem;border-radius:3px;background:var(--ink);opacity:.82}
.scr-hero{flex:1.5;border-radius:4px;background:var(--surface)}
.scr-line{flex:none;height:.3125rem;border-radius:3px;background:var(--line-soft)}
.scr-line.short{width:55%}
.scr-tiles{display:flex;gap:.4rem;flex:1}
.scr-tiles i{flex:1;border-radius:4px;background:var(--line-soft)}
.ok{position:absolute;right:-.5rem;bottom:-.5rem;width:1.625rem;height:1.625rem;
  border-radius:var(--pill);background:var(--forest);display:grid;place-items:center;
  border:2px solid var(--canvas)}
/* The heading above these says we open every page at three widths and look at
   what renders. So that is what they do: each mock builds the way a page paints
   — header, hero, copy, tiles — and only once it is standing does the check
   land on it. The order is the argument. The tick uses the same keyframe the
   live report's ticks do, so the demonstration and the real thing speak with
   one voice.

   All of it hangs off `.in`, the same class the reveal system sets, so the
   figures arriving and their contents building are one event, driven by one
   observer. Fires once, never on scroll back, and without the class the mocks
   are simply there. */
.widths.in .screen > span:not(.ok){animation:build .4s var(--ease) both}
.widths.in .screen > span:not(.ok):nth-child(1){animation-delay:.12s}
.widths.in .screen > span:not(.ok):nth-child(2){animation-delay:.17s}
.widths.in .screen > span:not(.ok):nth-child(3){animation-delay:.22s}
.widths.in .screen > span:not(.ok):nth-child(4){animation-delay:.27s}
.widths.in .screen > span:not(.ok):nth-child(5){animation-delay:.32s}
@keyframes build{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}

.widths.in .ok{animation:stamp .45s var(--ease) both}
.widths.in figure:nth-child(1) .ok{animation-delay:.44s}
.widths.in figure:nth-child(2) .ok{animation-delay:.52s}
.widths.in figure:nth-child(3) .ok{animation-delay:.60s}
.widths figcaption{margin-top:.9rem;font-size:.8125rem;color:var(--muted)}
.widths figcaption b{color:var(--ink);font-weight:500}
@media (max-width:650px){
  .widths{grid-template-columns:1.4fr 1fr .6fr;gap:.85rem}
  .screen.desktop{height:9.375rem}
  .screen.tablet{height:8.25rem}
  .screen.phone{height:7rem}
}

/* 4a · the scale
   ------------------------------------------------------------------
   A legend for a real instrument, so it is built from the report's own
   parts: the same three status colours the panel uses, on the hairlines
   the limits ledger uses. Not a card and not a three-up grid — this is the
   band's second element and it must read as a footnote to the check,
   not as a competing block. */
.scale{margin-top:clamp(2.5rem,4.5vw,3.75rem)}
.scale h3{color:#fff;margin-bottom:1.15rem}
.scale dl{margin:0;border-top:1px solid var(--ink-line)}
.scale-row{display:grid;grid-template-columns:minmax(0,11rem) minmax(0,1fr);
  gap:.35rem clamp(1rem,3vw,2.5rem);padding:1.05rem 0;
  border-bottom:1px solid var(--ink-line);align-items:baseline}
.scale-row dt{display:flex;align-items:baseline;gap:.65rem;flex-wrap:wrap}
.scale-row dd{margin:0;color:var(--on-ink);font-size:.9375rem;max-width:56ch;
  text-wrap:pretty}
.scale-row dd b{color:#fff;font-weight:500}
.scale-row dt em{font-style:normal;font-size:.8125rem;color:var(--on-ink-dim)}
/* the report's own chips, at the report's own weights */
.scale-chip{display:inline-flex;align-items:center;height:1.5rem;padding:0 .6rem;
  border-radius:var(--pill);font-size:.75rem;font-weight:600;letter-spacing:.01em}
.scale-row.pass .scale-chip{background:rgba(126,240,192,.16);color:var(--mint)}
.scale-row.warn .scale-chip{background:rgba(251,216,138,.16);color:var(--amber)}
.scale-row.fail .scale-chip{background:rgba(255,156,138,.16);color:var(--error-on-forest)}
.scale-note{margin-top:1.15rem;font-size:.9375rem;color:#fff;max-width:60ch;
  text-wrap:pretty}

@media (max-width:640px){
  .scale-row{grid-template-columns:1fr;gap:.5rem}
}

/* 4b · the comparison
   ------------------------------------------------------------------
   Both competitors' single most convincing element is a table, and we had
   nothing on this axis. Built on hairlines rather than as a card, because the
   ledger in the limits band proves hairlines carry weight here without
   decoration.

   It is a real <table>. It was divs, on the argument that this is a position
   rather than data; the argument does not survive being read aloud, where the
   divs lost column association after the header row. Nothing visual changed.

   The winning column is the only one in full white; the other is dimmed. No
   ticks and crosses: a cross against a competitor is a claim about them, and
   the honest version is simply what each side does.

   The column proportions were 1.1fr / 1fr / 1.2fr with a flex gap. As a table
   they are the same ratio as fixed percentages, and the gap moves into the
   cells' own padding, because a table has no gap of its own. */
.versus{width:100%;margin-top:clamp(2.5rem,4.5vw,3.75rem);
  border-top:1px solid var(--ink-line);border-collapse:collapse;
  text-align:left;font-size:.9375rem}
.v-label{width:33.3%}
.v-usual{width:30.3%}
.v-ours{width:36.4%}
.versus-row > *{padding:1.05rem 0;border-bottom:1px solid var(--ink-line);
  vertical-align:baseline;font-weight:400;color:var(--on-ink-dim)}
.versus-row > *:not(:last-child){padding-right:clamp(1rem,3vw,2.5rem)}
.versus-row b{color:#fff;font-weight:500}
.versus-head > *{padding-block:.75rem;font-size:.8125rem;letter-spacing:.02em;
  text-transform:uppercase}
.versus-head b{font-weight:600;letter-spacing:.02em}
.versus-note{margin-top:1.15rem;font-size:.8125rem;color:var(--on-ink-dim);max-width:62ch}

/* Stacked, every cell becomes its own line and the header row goes away, so
   each value has to name its own column. The roles in the markup are what keep
   this a table to a screen reader once `display` stops saying so. */
@media (max-width:760px){
  .versus,.versus tbody,.versus tr,.versus th,.versus td{display:block}
  .versus colgroup,.versus thead{display:none}
  .versus-row{padding:.95rem 0;border-bottom:1px solid var(--ink-line)}
  .versus-row > *{padding:0;border-bottom:0}
  .versus-row > * + *{margin-top:.2rem}
  .versus-row td:first-of-type::before{content:"The usual: "}
  .versus-row b::before{content:"Dehydrate: ";color:var(--on-ink-dim);font-weight:400}
}

/* 5 · limits */
.lead-line{margin-top:1rem;color:var(--body);font-size:1.0625rem;max-width:60ch}
.limits{margin:var(--gap-head) 0 0;display:grid;gap:0}
.limits > div{display:grid;grid-template-columns:16rem 1fr;gap:2rem;padding:1.35rem 0;
  border-top:1px solid var(--line)}
.limits > div:last-child{border-bottom:1px solid var(--line)}
@media (max-width:950px){.limits > div{grid-template-columns:1fr;gap:.35rem}}
.limits dt{font-weight:600;letter-spacing:-.015em}
.limits dd{margin:0;color:var(--body);max-width:60ch}
.limits a{color:var(--ink);text-decoration-thickness:1px;text-underline-offset:3px}

/* 6 · steps — the band that turns the page over.

   This used to be three columns of heading-plus-paragraph under a hairline,
   which is the same shape the reasons band uses higher up, so the page ran
   the same move twice. It was also the only band carrying no artifact, on the
   section the nav's "How it works" points at.

   Now: near-black ground, three lifted cards, and each card ends in a
   recessed well holding the artifact that step actually produces. The well is
   what keeps this from being a card inside a card — the artifact sits in a
   recess of the card's own material, not in a second box. Still monochrome:
   ink is a ground, not colour, so the two forest surfaces stay the only
   saturated things on the page. */

/* The page alternates canvas and ink. Four bands are dark, so this is not a
   background swap: every component those bands carry has to be built for ink,
   or the band is just a dark rectangle with light-mode parts sitting on it. */
.band.ink{background:var(--ink);color:#fff}
.band.ink h2,.band.ink h3,.band.ink dt{color:#fff}
.band.ink p,.band.ink dd{color:var(--on-ink)}
.band.ink .lead-line{color:var(--on-ink)}
.band.ink .strong-line{color:#fff!important}

/* the ledger */
.band.ink .limits > div{border-top-color:var(--ink-line)}
.band.ink .limits > div:last-child{border-bottom-color:var(--ink-line)}
.band.ink .limits a{color:#fff}

/* the accordion */
.band.ink .faq{border-top-color:var(--ink-line)}
.band.ink .faq details{border-bottom-color:var(--ink-line)}
.band.ink .faq summary{color:#fff}
.band.ink .faq summary::after{border-color:var(--on-ink-dim)}
@media (hover:hover) and (pointer:fine){
  .band.ink .faq summary:hover{color:var(--on-ink)}
  .band.ink .faq summary:hover::after{border-color:#fff}
}

/* The width mocks. On ink these stop being paper and start being screens,
   which is closer to what they are meant to depict anyway: a lit panel in a
   dark room rather than a printed page. */
.band.ink .screen{background:var(--ink-card);border-color:var(--ink-line)}
.band.ink .scr-hd{background:#fff;opacity:.86}
.band.ink .scr-hero{background:rgba(255,255,255,.13)}
.band.ink .scr-line,.band.ink .scr-tiles i{background:rgba(255,255,255,.07)}
/* the tick's ring punches out of the band it sits on, so it follows the band */
.band.ink .ok{border-color:var(--ink)}
.band.ink figcaption{color:var(--on-ink-dim)}
.band.ink figcaption b{color:#fff}

/* the folder tree, if it ever lands on a dark band */
.band.ink .tree{background:var(--ink-card);border-color:var(--ink-line)}
.band.ink .tree-head{background:var(--ink-well);border-bottom-color:var(--ink-line);color:#fff}
.band.ink .tree li{color:var(--on-ink)}
.band.ink .tree li span{color:var(--on-ink-dim)}

/* focus has to stay visible on ink */
.band.ink :where(a,button,summary,input):focus-visible{outline-color:#fff}

/* Two rows shared across all three cards, so every well starts on the same
   line and the recess reads as one floor rather than three ledges. The wells
   size to the tallest content instead of a guessed height; without subgrid
   the min-height below is the fallback and the floor goes slightly ragged. */
.steps{list-style:none;counter-reset:s;margin:var(--gap-head) 0 0;padding:0;
  display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(1rem,1.5vw,1.35rem);
  grid-template-rows:1fr auto}
.steps li{counter-increment:s;display:flex;flex-direction:column;
  background:var(--ink-card);border:1px solid var(--ink-line);
  border-radius:var(--r-lg);overflow:hidden}
@supports (grid-template-rows:subgrid){
  .steps li{display:grid;grid-row:span 2;grid-template-rows:subgrid}
}

.step-body{flex:1;padding:clamp(1.5rem,2.3vw,1.9rem)}
/* the counter is on the li, and counters inherit, so the numeral can live
   inside the padded body instead of above it */
.step-body::before{content:counter(s);display:block;
  font-size:clamp(2.25rem,3.6vw,3rem);font-weight:600;letter-spacing:-.05em;
  line-height:1;color:#fff;margin-bottom:clamp(1rem,2vw,1.5rem)}
.steps h3{color:#fff;margin-bottom:.5rem}
.steps p{color:var(--on-ink);font-size:.9375rem}

.step-well{background:var(--ink-well);border-top:1px solid var(--ink-line);
  padding:clamp(1.15rem,1.8vw,1.5rem);min-height:8.5rem;
  display:flex;flex-direction:column;justify-content:center}

/* 1 · the field, the real control in the real shape */
.art-field{display:flex;align-items:center;gap:.5rem;background:rgba(255,255,255,.05);
  border:1px solid rgba(255,255,255,.14);border-radius:var(--pill);
  padding:.28rem .28rem .28rem .95rem}
.art-field .u{flex:1;min-width:0;font-family:'DM Mono',ui-monospace,monospace;
  font-size:.8125rem;color:var(--on-ink-soft);overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
.art-field .b{flex:none;background:#fff;color:var(--ink);font-size:.75rem;font-weight:600;
  border-radius:var(--pill);padding:.45rem .9rem}

/* 2 · the pipeline, borderless so the well stays one surface */
.art-run{display:grid;grid-template-columns:1fr 1fr;gap:.5rem .9rem}
.art-run span{display:flex;align-items:center;gap:.5rem;font-size:.8125rem;
  color:var(--on-ink-dim)}
.art-run span::before{content:"";width:.4375rem;height:.4375rem;border-radius:50%;flex:none;
  box-shadow:inset 0 0 0 1px currentColor}
.art-run .done{color:var(--on-ink-soft)}
.art-run .done::before{background:var(--on-ink-soft);box-shadow:none}
.art-run .now{color:#fff;font-weight:500}
.art-run .now::before{background:#fff;box-shadow:none}
.art-bar{margin-top:.9rem;height:3px;border-radius:var(--pill);
  background:rgba(255,255,255,.12);overflow:hidden}
.art-bar i{display:block;height:100%;width:68%;background:#fff;border-radius:var(--pill);
  transform-origin:left}
/* A still progress bar is a lie about what a progress bar is. It fills once,
   when the band arrives, riding the same .in the stagger already sets. */
.steps.in .art-bar i{animation:bar-fill .9s var(--ease) .3s both}
@keyframes bar-fill{from{transform:scaleX(0)}to{transform:scaleX(1)}}

/* 3 · the folder */
.art-files{display:grid}
.art-files span{display:flex;align-items:baseline;gap:.75rem;
  font-family:'DM Mono',ui-monospace,monospace;font-size:.8125rem;color:var(--on-ink-soft);
  padding:.36rem 0;border-bottom:1px solid var(--ink-line)}
.art-files span:first-child{padding-top:0}
.art-files span:last-child{border-bottom:0;padding-bottom:0}
.art-files span i{margin-left:auto;font-style:normal;font-family:'DM Sans',sans-serif;
  font-size:.75rem;color:var(--on-ink-dim)}

/* Stacked, the shared rows have nothing left to align, and the 1fr row just
   opens a gap under the first card's copy. Back to plain flex columns. */
@media (max-width:1100px){
  .steps{grid-template-columns:1fr;gap:1rem;grid-template-rows:none}
  .steps li{display:flex;grid-row:auto}
  .step-well{min-height:0}
}

/* 7 · price — the comparison and the number, as one decision.
   The whole band is forest: this is the second of the two places the colour
   budget is spent, and the two columns sit on it as cards so the choice reads
   as a pair rather than as a pitch with a strawman beside it. */
.price-head{text-align:center;max-width:20ch;margin-inline:auto}
.price-sub{text-align:center;color:var(--muted);margin:.85rem auto 0;max-width:44ch;
  text-wrap:pretty}

.ledger{margin-top:var(--gap-head);display:grid;grid-template-columns:repeat(2,1fr);
  gap:clamp(1rem,2vw,1.5rem);align-items:start}
@media (max-width:820px){.ledger{grid-template-columns:1fr}}

.ledger-col{background:var(--forest);color:#fff;border-radius:var(--r-lg);
  padding:clamp(1.5rem,3vw,2.25rem);display:flex;flex-direction:column;height:100%}
/* The chosen column is the same card, lifted — a ring and a lighter ground, not
   a different component. Anything louder turns a comparison into an ad. */
.ledger-col.pick{background:var(--forest-soft);
  box-shadow:0 0 0 2px var(--mint),0 1.25rem 2.5rem rgba(14,59,47,.18)}
.ledger-col h3{font-size:1rem;font-weight:500;color:var(--on-forest);margin:0;
  letter-spacing:0}
.ledger-amount{font-size:clamp(2.75rem,6vw,4rem);font-weight:600;letter-spacing:-.05em;
  line-height:1;margin:.85rem 0 0}
.ledger-amount span{display:block;font-size:.9375rem;font-weight:400;letter-spacing:0;
  color:var(--on-forest);margin-top:.7rem}
/* A marker, not a link: the note it points at is a few lines below and already
   on screen, so a 20px anchor bought nothing and failed the touch-target floor. */
.ledger-amount sup{font-size:.8em;line-height:0;vertical-align:super;margin-left:.1em}
.ledger-col ul{list-style:none;margin:1.5rem 0 0;padding:0;display:grid;gap:0;
  border-top:1px solid var(--forest-line)}
.ledger-col li{padding:.85rem 0;border-bottom:1px solid var(--forest-line);
  font-size:.9375rem;color:#fff;text-wrap:pretty}
.ledger-col .pill{margin-top:1.5rem;align-self:flex-start}
/* Pushes the button to the bottom edge so both cards' actions line up even when
   the lists are different lengths. */
.ledger-col.pick ul{margin-bottom:auto}

.price-note{margin:1.25rem 0 0;font-size:.8125rem;color:var(--muted);
  max-width:74ch;line-height:1.6;text-wrap:pretty}
.price-note .mono{font-size:.95em}

/* proof, in the same band */
.voices{margin-top:clamp(3rem,6vw,5rem);border-top:1px solid var(--line);
  padding-top:clamp(2rem,4vw,3rem)}
.voices h3{font-size:1rem;font-weight:500;color:var(--muted);margin:0 0 1.5rem;
  letter-spacing:0}
.voice-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(1rem,2vw,1.5rem);
  align-items:start}
@media (max-width:900px){.voice-grid{grid-template-columns:1fr}}
.voice{margin:0;background:var(--surface);border-radius:var(--r-md);
  padding:clamp(1.25rem,2.5vw,1.75rem)}
.voice p{margin:0;font-size:.9375rem;line-height:1.65;color:var(--body);text-wrap:pretty}
.voice cite{display:block;margin-top:1.15rem;font-style:normal;font-size:.8125rem;
  color:var(--muted)}
.voice-stats{display:grid;gap:clamp(1rem,2vw,1.5rem)}
.voice-stats div{background:var(--surface);border-radius:var(--r-md);
  padding:clamp(1.25rem,2.5vw,1.75rem)}
.voice-stats b{display:block;font-size:clamp(2rem,4vw,2.75rem);font-weight:600;
  letter-spacing:-.04em;line-height:1;color:var(--ink)}
.voice-stats b i{font-style:normal;font-size:.55em;font-weight:500}
.voice-stats span{display:block;margin-top:.5rem;font-size:.875rem;color:var(--muted)}

/* 8 · faq */
.faq{margin-top:var(--gap-head);border-top:1px solid var(--line)}
.faq details{border-bottom:1px solid var(--line)}
.faq summary{display:flex;align-items:center;gap:1rem;padding:1.15rem 0;cursor:pointer;
  font-weight:500;list-style:none;letter-spacing:-.01em;color:var(--ink);
  transition:color .15s var(--ease)}
.faq summary::-webkit-details-marker{display:none}
/* nine rows that only changed the cursor; the chevron answers the pointer too */
@media (hover:hover) and (pointer:fine){
  .faq summary:hover{color:var(--body)}
  .faq summary:hover::after{border-color:var(--ink)}
}
.faq summary::after{content:"";margin-left:auto;flex:none;width:.625rem;height:.625rem;
  border-right:2px solid var(--muted);border-bottom:2px solid var(--muted);
  transform:rotate(45deg) translateY(-3px);
  transition:transform .22s var(--ease),border-color .15s var(--ease)}
.faq details[open] summary::after{transform:rotate(225deg) translateY(-3px)}
.faq details p{padding:0 0 1.35rem;color:var(--body);max-width:64ch}
.faq details[open] p{animation:answer .22s var(--ease) both}
@keyframes answer{from{opacity:0;transform:translateY(-.35rem)}to{opacity:1;transform:none}}

/* Nine of these in a stack, so opening one used to teleport everything below
   it: the answer faded in politely while the box it sat in snapped to full
   height. `::details-content` makes the box itself animate, on the exact .22s
   the chevron already turns on, so arrow and answer move as one thing.
   The @supports wrapper is the fallback: where this is not understood the rule
   set is dropped whole and the old fade stays, which is today's behaviour. */
@supports (interpolate-size:allow-keywords) and selector(details::details-content){
  :root{interpolate-size:allow-keywords}
  .faq details[open] p{animation:none}
  .faq details::details-content{
    block-size:0;opacity:0;overflow:hidden;
    transition:block-size .22s var(--ease),opacity .22s var(--ease),
               content-visibility .22s allow-discrete}
  .faq details[open]::details-content{block-size:auto;opacity:1}
}

/* 9 · close */
.band.close{text-align:center;padding-bottom:calc(var(--band) + 1rem)}
.band.close h2{margin:0 auto 1.75rem;max-width:20ch}
.band.close .sub{margin-top:1rem}

/* ---------- footer ---------- */

.footer{background:var(--ink);color:#fff;padding:clamp(3rem,5vw,4rem) 0 2rem}

/* Grid, not flex-wrap. `margin-left:auto` kept shoving the link row to the right
   wall *after* it had wrapped, so between roughly 700 and 1000px the links sat
   ragged-left in mid-air under a left-aligned wordmark. A grid either has two
   columns or one; it cannot half-wrap.

   `last baseline`, not `flex-start` or `flex-end`. The brand is two lines, and
   the links used to align to the wordmark's box top — optically high against
   19px/700 type. This sits them on the *tagline's* baseline instead: 14px
   against 15px, which is a true baseline match rather than a box match. */
.footer-in{display:grid;grid-template-columns:auto auto;
  justify-content:space-between;align-items:last baseline;gap:2rem 3rem;
  padding-bottom:2.5rem;border-bottom:1px solid var(--ink-line)}
@media (max-width:900px){
  .footer-in{grid-template-columns:1fr;justify-content:start;gap:1.75rem}
}
.footer .mark{font-size:1.1875rem;font-weight:700;letter-spacing:-.04em;margin-bottom:.4rem}
.footer-line{color:var(--on-ink-soft);font-size:.9375rem}
.footer-links{display:flex;gap:1.5rem;flex-wrap:wrap;font-size:.875rem}
.footer-links a{color:var(--on-ink);text-decoration:none;transition:color .15s var(--ease)}
.footer-links a:hover{color:#fff}
/* No 60ch cap. The measure rule is for body copy; this is 13px compliance
   boilerplate in the dimmest tint on the page, and it reads as a footnote on one
   line rather than as a two-line paragraph. It wraps on its own below ~900px. */
.legal{margin-top:1.5rem;font-size:.8125rem;color:var(--on-ink-dim);text-wrap:pretty}

/* ---------- scroll reveal ---------- */

/* One primitive for the whole page rather than a rule per section. An element
   opts in with `data-reveal`; a group opts in with `data-stagger` and its own
   children ride the `--i` index below. app.js only ever adds `.in`.

   The `.reveal` class this all hangs off is set by an inline script in <head>,
   and only when there is an IntersectionObserver and motion is wanted. So the
   page's resting state is *visible*: no JS, old browser, or reduced motion and
   nothing is hidden waiting for a class that will never arrive. That is the
   right way round for a page whose whole job is to be read.

   Keyframes rather than transitions here, deliberately: several of these
   elements already own a `transition` (the field's border, the pill's
   background) and a reveal built on transitions would overwrite it. These fire
   once on scroll, so there is nothing to retarget. */
.reveal [data-reveal],
.reveal [data-stagger] > *{opacity:0}
.reveal [data-reveal].in,
.reveal [data-stagger].in > *{
  animation:reveal .5s var(--ease) both;
  animation-delay:calc(var(--i,0) * var(--step,60ms))}
@keyframes reveal{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}

[data-stagger] > *:nth-child(1){--i:0}
[data-stagger] > *:nth-child(2){--i:1}
[data-stagger] > *:nth-child(3){--i:2}
[data-stagger] > *:nth-child(4){--i:3}
[data-stagger] > *:nth-child(5){--i:4}
[data-stagger] > *:nth-child(6){--i:5}
[data-stagger] > *:nth-child(7){--i:6}
[data-stagger] > *:nth-child(8){--i:7}
[data-stagger] > *:nth-child(n+9){--i:8}

/* eight rows at 60ms would trail half a second behind the card they sit in */
.tree ul{--step:40ms}

/* ---------- touch targets ---------- */

/* Held to 44px on a coarse pointer only, so a mouse keeps the tighter
   proportions the design was drawn at. This has to sit near the end of the
   file: every rule below is a plain class selector, so it wins on source order
   rather than specificity, and an earlier placement silently lost to the base
   `.pill` height.

   `min-height` with a flex centre rather than `height`, so a control that
   wraps to two lines grows instead of clipping. */
@media (pointer:coarse){
  .pill{height:auto;min-height:2.75rem}
  .mark{display:inline-flex;align-items:center;min-height:2.75rem}

  .nav-links a{display:inline-flex;align-items:center;min-height:2.75rem}
  .footer-links{gap:.35rem 1.25rem}
  .footer-links a{display:inline-flex;align-items:center;min-height:2.75rem}

  .field input{height:2.75rem}
  .field button{height:2.75rem}
  .faq summary{padding-block:1.35rem}
}

/* ---------- motion preferences ---------- */

@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{
    animation-duration:.001ms!important;
    animation-iteration-count:1!important;
    transition-duration:.001ms!important;
  }
  /* the row keyframes open on a dim frame; with motion off, show them outright */
  .row,.row .st,.row .tick{opacity:1!important;transform:none!important}
  .card-foot,.demo-note,.ok,.screen > span{opacity:1!important;transform:none!important}
  /* the head script will not arm the reveal under this preference, but it can
     be switched on after the page is already loaded and armed. Without this the
     page would be left holding its content at opacity 0. */
  [data-reveal],[data-stagger] > *{opacity:1!important;transform:none!important}

  /* Fewer and gentler, not none. Every movement above is gone: no rise, no
     slide, no stagger, and app.js reads this same query to swap the card's
     height outright instead of growing it. The two changes that would
     otherwise happen with nothing at all to mark them keep a plain opacity
     fade, because a hard cut in the hero panel, or a validation line blinking
     into existence under the field, is harder to follow rather than easier. */
  .floor-idle{transition-duration:.14s!important}
  .floor-idle.leaving{transform:none}
  .field-error{transition-duration:.16s!important}
  .field-error[hidden]{transform:none}
}
