/* foilstack.ai — design system. Colours are oklch throughout: a warm paper
   ground under everything you read, two deep navy bands where the argument
   needs to stop the scroll. See the token block for why. */

/* ---------- Fonts (self-hosted; no third-party requests) ----------

   Baloo 2 and Nunito are variable faces (wght 400-800 and 200-1000), so one
   file per unicode range covers every weight the sheet asks for. IBM Plex Mono
   is not published as a variable font, so 500 and 600 are separate files and
   those are the *only* two weights any rule may request — asking a static face
   for 700 gets you the browser's synthetic bold, which on a 10.5px mono label
   is a smear. Grep for `--mono` before adding a weight. */
@font-face {
  font-family: 'Baloo 2';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/static/fonts/baloo-2-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Baloo 2';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/static/fonts/baloo-2-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Nunito';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/static/fonts/nunito-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Nunito';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/static/fonts/nunito-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/static/fonts/ibm-plex-mono-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/static/fonts/ibm-plex-mono-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/static/fonts/ibm-plex-mono-600-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/static/fonts/ibm-plex-mono-600-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---------- Tokens ----------

   Every colour in the sheet resolves here, so the theme is one block rather
   than 250 rules. That has now paid for itself three times: this is the third
   visual identity the product has worn, and each swap has been a token edit
   plus the landing components, not a rewrite.

   Why paper and ink, 2026-08-08. The previous theme was a foil gradient —
   orange through magenta into violet, the way a holo card tilts — on a cool
   off-white with one near-black hero. The reasoning was sound (the site is
   called *foilstack*) and the execution had one problem it could not argue its
   way out of: a three-stop gradient is the loudest thing on any screen it
   appears on, and this product's entire promise is "real prices, never
   estimates". Iridescence is what the guessing apps look like.

   So the gradient is gone and the brand now lives in the *shape* language
   instead: rounded, chunky, generous radii, two-pixel borders in place of
   shadows, cards that overlap and tilt. Baloo 2 carries that in the type. The
   stack in the logo is still two overlapping cards; it just isn't shouting.

   Colour is down to two hues doing separate jobs, which is the part that keeps
   a pricing tool looking like one:

     * blue  — every interactive thing. Links, buttons, focus, the step markers.
               If it is blue you can click it. The one exception is `.tone-accent`
               inside the demo's data blocks, where blue marks the figure worth
               looking at — the analytics panel contains no links at all, so
               there is nothing there for it to be confused with. Do not carry
               that exception onto a page that has links on it.
     * gold  — the one thing in a band worth pressing or noticing, and never
               more than one per band. On the landing page that is four in
               total: the hero CTA, the dear printing's chip on the card, the
               same figure again in the spread, and the waitlist submit. The
               discipline is per band rather than per page, and the test is that
               you can point at the gold thing and say why it beat everything
               else in that band.

   Everything else is paper and ink. Chroma is spent on those two and nowhere
   else, so the brightest things on any screen stay the card, the number, and
   the one button we want pressed — which was the one rule worth carrying over
   from the foil theme intact.

   What else deliberately did NOT change: the mono numerals, and the contrast
   discipline. Every pair below was measured, not eyeballed; the ratios in the
   comments are WCAG against the surface named. Two values from the reference
   design had to move to get there and they are flagged where they sit.

   Earlier palettes are one `git show` away if this turns out wrong. */
:root {
  /* Ground: warm paper. The page reads on this; the printing-spread band and
     the waitlist panel are the only dark surfaces.

     Warm rather than the cool off-white it replaced, because the accent moved
     from magenta to blue. A cool ground under a cool accent gives you nothing
     to push against — the page goes flat and the CTA stops being the warmest
     thing on it. Paper at hue 95 puts a little distance under both. */
  --paper: oklch(97.8% 0.014 95);
  --paper-raised: oklch(100% 0 0);
  --paper-sunk: oklch(95.5% 0.020 95);
  /* The sticky nav sits over content, so it needs the ground with alpha rather
     than a solid — otherwise it hides whatever scrolls under it. */
  --paper-veil: oklch(97.8% 0.014 95 / 0.94);
  --bubble: oklch(95.5% 0.020 95);
  /* Inputs, chips and other things that need to sit above the ground. */
  --field: oklch(100% 0 0);
  --field-hover: oklch(96.5% 0.018 95);
  --track: oklch(92% 0.022 95);

  /* Ink is navy-leaning rather than neutral: it belongs to the same family as
     the dark bands, so a heading does not change temperature when it crosses
     onto one. Not black — 22% on paper is 16.3:1, past AAA without the hard
     edge pure black gives small type. */
  --ink: oklch(22% 0.03 260);          /* 16.3:1 on paper */
  --ink-muted: oklch(38% 0.03 260);    /*  9.4:1 */
  /* Carries the 10.5px mono stat labels. Held at 50%, not the reference's 52%,
     for the same reason it was held there under the old theme: at 52% these
     measure 5.2:1 and every point matters on the smallest type in the product.
     50% is 5.6:1 on paper, 6.0:1 on the white card surfaces they mostly sit on. */
  --ink-faint: oklch(50% 0.03 260);
  --ink-hover: oklch(14% 0.03 260);
  /* Text on an --ink fill — a dark chip on a light page, so its label is the
     ground. The dark bands flip this back. */
  --on-ink: oklch(98% 0.01 95);

  /* Borders, not shadows. This theme draws edges rather than floating things,
     so `--rule` is a token: 2px reads as deliberate at these radii where 1px
     reads as a hairline artefact, and it has to change in one place. */
  --rule: 2px;
  --line: oklch(90% 0.02 95);
  --line-soft: oklch(94% 0.016 95);
  --line-strong: oklch(84% 0.026 95);

  /* Blue: everything you can click.

     The reference design used oklch(62% 0.16 250) for link text and for the
     primary button fill. Both measured as failures — 3.4:1 as text on paper and
     3.5:1 for a white label on the fill, against the 4.5:1 that 14px bold needs
     (bold does not become "large text" until 18.66px). Since that button is the
     conversion on the only page we buy traffic for, it moved: 50% gives 5.6:1
     as text and 5.8:1 under a white label, and is visually near-identical.

     62% survives as `--accent-bright`, which is decorative only — step markers,
     the logo, focus rings, bar fills. It must never carry text on paper. */
  --accent: oklch(50% 0.16 250);         /* 5.6:1 on paper, 5.9:1 on white */
  --accent-hover: oklch(42% 0.16 255);   /* 8.0:1 */
  --accent-deep: oklch(42% 0.16 255);
  --accent-bright: oklch(62% 0.16 250);  /* decorative only — see above */
  --accent-bg: oklch(93% 0.04 250);
  /* A border that reads as accent without the full-strength hue, for panels
     that must catch the eye but sit inside the conversation rather than on top
     of it — the rating card, the share consent. */
  --accent-line: oklch(62% 0.16 250 / 0.42);

  /* Gold: the one highest-value moment, at most twice per page. It is a
     *surface* colour, never text on paper — 1.8:1 there. On gold, `--gold-ink`
     is 8.6:1; on the dark bands the full-strength gold is 8.6:1 and is the only
     place it is allowed to be type. */
  --gold: oklch(80% 0.15 95);
  --gold-hover: oklch(74% 0.15 92);
  --gold-ink: oklch(25% 0.05 95);
  --gold-bg: oklch(95.5% 0.055 95);

  /* Tone pairs: the text colour and the background it sits on. They are used
     together (.bg-positive sets both), so flipping one without the other is how
     you get green on green. Dark ink on a pale wash, all measured ≥6:1 on paper.

     Note prices are NOT green anywhere. Green means "up/good"; two candidate
     printings are neither, they are two identities, and colouring them green
     would collide with the tone system the analytics tables already use. */
  --positive: oklch(46% 0.14 150);
  --positive-bg: oklch(94% 0.045 150);
  --negative: oklch(50% 0.19 25);
  --negative-bg: oklch(95% 0.035 20);
  --warning: oklch(47% 0.12 70);
  --warning-bg: oklch(95% 0.055 80);
  --info: oklch(48% 0.13 250);
  --info-bg: oklch(93% 0.04 250);

  /* Lifted sections. Declared here so `.is-lifted` is a token swap rather than
     a second set of rules for every component inside one.

     The amplitude is the thing to leave alone. An earlier theme alternated the
     ground with near-white panels four times down the page, and each flip was a
     135x jump in background luminance — four pupil re-adaptations on one
     scroll, which is the single most tiring thing a page can do. Three surfaces
     now, all within three points of each other: sunk 95.5%, paper 97.8%, lifted
     100%. Enough to separate a band, nowhere near enough to cost an adaptation.

     The two dark bands are the deliberate exception, and there are two of them
     rather than four for exactly this reason. */
  --lift-bg: oklch(100% 0 0);
  --lift-raised: oklch(97.8% 0.014 95);
  --lift-line: oklch(91% 0.018 95);
  --lift-line-strong: oklch(85% 0.024 95);

  /* The deep navy band. It inverts against whatever it sits on, so it stops the
     eye. The values swap with the theme, the roles do not: `--invert-bg` is
     always the band, `--invert-ink` always the text on it, and `.btn-invert`
     reads them back the other way round on purpose so a button inside the band
     inverts again. */
  --invert-bg: oklch(25% 0.05 250);
  --invert-raised: oklch(30% 0.05 250);
  --invert-ink: oklch(98% 0.01 95);        /* 15.1:1 on the band */
  --invert-ink-hover: oklch(90% 0.02 95);
  --invert-ink-muted: oklch(78% 0.03 250); /*  8.0:1 */
  --invert-ink-faint: oklch(68% 0.03 250); /*  5.6:1 */
  --invert-line: oklch(45% 0.05 250);

  --sans: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --display: 'Baloo 2', var(--sans);
  --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, monospace;

  /* Generous, because the shape language is the brand now. */
  --radius: 10px;
  --radius-lg: 14px;
  --radius-xl: 18px;
  /* Kept shallow and warm. This theme separates surfaces with `--rule`; the
     shadow is only ever a hint that something overlaps something else. */
  --shadow-card: 0 10px 26px -14px oklch(30% 0.04 260 / 0.22);
  --shadow-float: 0 12px 28px -12px oklch(28% 0.04 260 / 0.34);
}

:root { color-scheme: light; }

/* ---------- Accent helpers ---------- */

/* The high-value surface. Only the hero CTA and the dear printing wear it. */
.gold-fill { background: var(--gold); color: var(--gold-ink); }
.gold-fill:hover:not(:disabled) { background: var(--gold-hover); color: var(--gold-ink); }

/* ---------- Light sections ----------

   A token swap, not a second stylesheet. Every component inside resolves the
   same variable names, so `.feature-row`, `.btn`, `.step` and the rest adapt
   without knowing which ground they landed on. */
/* A section that steps off the ground without inverting against it. The ink
   family is deliberately *not* swapped — the band stays light, so the same text
   colours apply and there is no second theme to keep in sync. */
.section.is-lifted {
  background: var(--lift-bg);
  --paper: var(--lift-bg);
  --paper-raised: var(--lift-raised);
  --paper-sunk: var(--lift-raised);
  --bubble: var(--lift-raised);
  --field: var(--lift-bg);
  --field-hover: var(--lift-raised);
  --on-ink: var(--lift-bg);
  --line: var(--lift-line);
  --line-soft: oklch(94% 0.016 95);
  --line-strong: var(--lift-line-strong);
  color: var(--ink);
}
.section.is-lifted { max-width: none; }
.section.is-lifted > .section-inner { max-width: 1080px; margin: 0 auto; }

/* The dark band. Same trick from the other side: components inside resolve the
   inverted values without knowing they have been inverted. Used twice on the
   landing page and nowhere else — see the amplitude note in the tokens. */
.is-dark {
  background: var(--invert-bg);
  --paper: var(--invert-bg);
  --paper-raised: var(--invert-raised);
  --paper-sunk: oklch(22% 0.05 250);
  --bubble: var(--invert-raised);
  --field: var(--invert-raised);
  --field-hover: oklch(34% 0.05 250);
  --ink: var(--invert-ink);
  --ink-muted: var(--invert-ink-muted);
  --ink-faint: var(--invert-ink-faint);
  --ink-hover: oklch(100% 0 0);
  --on-ink: var(--invert-bg);
  --line: var(--invert-line);
  --line-soft: oklch(36% 0.05 250);
  --line-strong: oklch(52% 0.05 250);
  /* Blue text is unreadable on navy; on the dark bands the accent role is
     handed to gold, which measures 8.6:1 there. */
  --accent: var(--gold);
  --accent-hover: oklch(88% 0.13 95);
  --accent-bright: var(--gold);
  color: var(--ink);
}

*, *::before, *::after { box-sizing: border-box; }

/* `el.hidden = true` only works if nothing outranks the UA's [hidden] rule.
   Any author-level `display` (e.g. .waitlist-form{display:flex}) silently wins
   and the element stays on screen. Keep this above every layout rule. */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hover); }

/* Baloo 2 is a display face with a tall x-height and short descenders, so it
   wants tighter leading than the old grotesk did and no extra tracking. */
h1, h2, h3 {
  font-family: var(--display); font-weight: 700;
  letter-spacing: -0.01em; line-height: 1.15; margin: 0;
}

::selection { background: oklch(85% 0.1 95); color: var(--ink); }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

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

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--ink); color: var(--on-ink); padding: 10px 16px;
  border-radius: 0 0 var(--radius) 0;
}
.skip-link:focus { left: 0; color: var(--on-ink); }

/* ---------- Logo ----------
   Two cards, overlapping and tilted: gold behind, blue in front. It went from
   three cards to two when the theme changed — at 26px the third was a sliver
   that read as a rendering fault rather than as a stack. */
.logo { display: flex; align-items: center; gap: 10px; }
.logo-mark { position: relative; width: 28px; height: 28px; flex-shrink: 0; }
.logo-mark i {
  position: absolute; width: 15px; height: 18px; border-radius: 4px; display: block;
}
.logo-mark i:nth-child(1) { background: var(--gold); transform: rotate(-8deg); top: 6px; left: 0; }
.logo-mark i:nth-child(2) { background: var(--accent-bright); transform: rotate(4deg); top: 3px; left: 8px; }
.logo-word {
  font-family: var(--display); font-weight: 700; font-size: 20px;
  letter-spacing: -0.01em; line-height: 1;
}

.logo.is-small .logo-mark { width: 21px; height: 21px; }
.logo.is-small .logo-mark i { width: 11px; height: 14px; border-radius: 3px; }
.logo.is-small .logo-mark i:nth-child(1) { top: 5px; left: 0; }
.logo.is-small .logo-mark i:nth-child(2) { top: 2px; left: 6px; }
.logo.is-small .logo-word { font-size: 15px; }

/* A small rounded marker. It was a hard rotated square under the old theme;
   softened here so it belongs to the same shape family as everything else. */
.diamond {
  width: 7px; height: 7px; border-radius: 2px; transform: rotate(45deg);
  background: var(--accent-bright); flex-shrink: 0; display: inline-block;
}
.diamond.is-md { width: 8px; height: 8px; }
.diamond.is-lg { width: 10px; height: 10px; border-radius: 3px; }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  font-family: inherit; font-weight: 800; cursor: pointer;
  border-radius: var(--radius); border: var(--rule) solid transparent;
  transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.btn:disabled { opacity: 0.55; cursor: not-allowed; }

/* Blue fill, light label: 5.8:1. See the token note for why this is not the
   brighter blue the reference used. */
.btn-primary { background: var(--accent); color: oklch(99% 0.012 95); }
.btn-primary:hover:not(:disabled) { background: var(--accent-hover); color: oklch(99% 0.012 95); }

.btn-gold { background: var(--gold); color: var(--gold-ink); }
.btn-gold:hover:not(:disabled) { background: var(--gold-hover); color: var(--gold-ink); }

.btn-ghost { background: transparent; color: var(--ink); border-color: var(--line-strong); }
.btn-ghost:hover { background: var(--field-hover); color: var(--ink); }

.btn-invert { background: var(--invert-ink); color: var(--invert-bg); }
.btn-invert:hover { background: var(--invert-ink-hover); color: var(--invert-bg); }

.btn-sm { padding: 9px 18px; font-size: 14.5px; }
.btn-md { padding: 12px 22px; font-size: 15px; }
.btn-lg { padding: 14px 26px; font-size: 16px; }

/* A plain text link that still reads as an action next to a filled button. */
.btn-text {
  font-weight: 800; font-size: 14.5px; color: var(--ink);
  text-decoration: underline; text-underline-offset: 3px;
}
.btn-text:hover { color: var(--accent-hover); }

/* ---------- Landing: nav ----------
   Sticky, unlike the reference comp, which lets the header scroll away. The
   nav carries the demo CTA and this is the only page we buy traffic for, so
   keeping that button reachable through the whole scroll is worth the 60px.
   `[id] { scroll-margin-top }` below exists to pay for that decision. */
.nav {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; row-gap: 12px;
  padding: 16px clamp(20px, 5vw, 48px);
  background: var(--paper-veil);
  backdrop-filter: blur(8px);
  border-bottom: var(--rule) solid var(--line);
}
.nav-links { display: flex; flex-wrap: wrap; align-items: center; gap: 14px clamp(14px, 3vw, 28px); }
/* :not(.btn) so this never wins over a button's own colour — a filled CTA in
   the nav would otherwise get ink-coloured text and vanish. */
.nav-links a:not(.btn) { font-weight: 700; font-size: 14.5px; color: var(--ink-muted); }
.nav-links a:not(.btn):hover { color: var(--ink); }

/* ---------- Landing: hero ----------
   Light, and that is the change. The old hero was the one near-black surface on
   the page — the ground the foil gradient was drawn for. With the gradient gone
   there was nothing left for the darkness to carry, and it was costing the
   first screen its warmth. The dark bands moved down the page to the two places
   that actually want to stop a scroll: the printing spread and the waitlist. */
.hero {
  max-width: 1320px; margin: 0 auto;
  padding: clamp(30px, 7vw, 60px) clamp(20px, 5vw, 48px) 0;
  /* `min(340px, 100%)`, not a bare `340px`. The floor in `minmax()` is a hard
     minimum: below a 376px viewport the content box is narrower than 340px and
     the column refuses to shrink, so the whole hero column — heading, lede,
     buttons, the lot — hung 38px off the right of the page and took the
     document's scroll width with it. Wrapping the floor in `min()` lets it
     collapse to the container on a narrow phone while still being the 340px
     that decides when the grid splits into two columns. No effect at any width
     where the original already fitted. */
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
  gap: 24px clamp(24px, 5vw, 48px); align-items: center;
}
.hero-copy { min-width: 0; }
.eyebrow {
  display: inline-flex; align-items: center; gap: 8px; margin: 0 0 20px;
  font-family: var(--mono); font-weight: 500; font-size: 11.5px;
  letter-spacing: 0.1em; color: var(--accent);
  padding: 6px 13px; border-radius: 999px;
  background: var(--accent-bg);
}
.hero h1 {
  font-size: clamp(38px, 7vw, 60px); line-height: 1.04;
  font-weight: 800; margin-bottom: 24px;
}
.hero p.lede {
  font-size: 18px; font-weight: 600; line-height: 1.55;
  color: var(--ink-muted); margin: 0 0 28px; max-width: 430px;
}
.hero-actions { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; }
/* Sits under the buttons: answers "do I have to sign up?" at the moment the
   question occurs, without spending fold height before the pitch lands. */
.hero-note { margin: 16px 0 0; font-size: 13px; font-weight: 700; color: var(--ink-faint); }

/* ---------- The scan ----------
   A photographed card inside a viewfinder, with the two candidate prices
   floating off it. It replaced a mock chat transcript, which had been chosen so
   the landing page showed the *same component* the demo does. That argument
   still holds and it lost on a different one: the transcript needed three
   bubbles of reading to make its point, and this makes the same point — one
   number, two prices — in a glance, which is all a first screen gets. */
.hero-visual { position: relative; padding: 16px clamp(0px, 4vw, 20px) 52px; min-width: 0; }
.scan { position: relative; width: 100%; max-width: 320px; margin: 0 auto; }
/* `height: auto` is load-bearing, not tidiness. The <img> carries width/height
   attributes so the browser can reserve the box before the bytes land, and
   those map to *presentational hints* for CSS width and height. With both
   dimensions definite there is nothing for `aspect-ratio` to resolve, so it is
   ignored — which rendered this card at its raw 502px attribute height and, in
   the spread band below, pushed both printings and their prices clean off the
   bottom of the section. Overriding height back to auto is what lets the
   intrinsic ratio win while keeping the reservation.

   No `object-fit: cover` either, and no forced ratio: the collector number sits
   along the card's bottom edge, and cropping it away on the one page that keeps
   telling people to go and read it would be its own kind of wrong. */
.scan-shot {
  display: block; width: 100%; height: auto;
  border-radius: var(--radius-xl);
  background: var(--paper-sunk);
}
/* Four viewfinder corners. Drawn as borders on empty spans rather than as an
   SVG so they inherit the accent token and cost nothing to retheme. */
.scan-corner {
  position: absolute; width: 26px; height: 26px;
  border: 0 solid var(--accent-bright); pointer-events: none;
}
.scan-corner.tl { top: -10px; left: -10px; border-top-width: 4px; border-left-width: 4px; border-radius: 8px 0 0 0; }
.scan-corner.tr { top: -10px; right: -10px; border-top-width: 4px; border-right-width: 4px; border-radius: 0 8px 0 0; }
.scan-corner.bl { bottom: -10px; left: -10px; border-bottom-width: 4px; border-left-width: 4px; border-radius: 0 0 0 8px; }
.scan-corner.br { bottom: -10px; right: -10px; border-bottom-width: 4px; border-right-width: 4px; border-radius: 0 0 8px 0; }

/* The stamp callout. Sits above the photo and below the chips in paint order,
   which is what lets each connector line run under its chip and end out of
   sight — see the note in landing.html.

   `inset: 0` over `.scan`, whose box is the photo's box, and a viewBox with the
   photo's intrinsic ratio. Both scale together, so one set of coordinates holds
   from 320px down to a narrow phone with no media query. */
.scan-marks { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.scan-marks use { fill: none; stroke-linecap: round; }
.scan-marks-halo { stroke: var(--paper); stroke-width: 7; opacity: 0.9; }
.scan-marks-ink { stroke: var(--accent-bright); stroke-width: 2.5; }

.scan-chip {
  position: absolute; white-space: nowrap;
  font-family: var(--mono); font-weight: 500; font-size: 12.5px;
  padding: 8px 13px; border-radius: var(--radius);
  box-shadow: var(--shadow-float);
}
.scan-chip .qual { opacity: 0.68; }
/* Gold on navy for the dear printing, plain paper for the cheap one. Which is
   which is the whole argument of the page, so the louder chip has to be the
   expensive one — the first pass had these the other way round, with $4.21 on a
   full gold fill and $138.13 in quieter navy, which reads as the wrong lesson
   at a glance. Gold here also matches the same figure in the spread band, so
   the colour means one thing on the page rather than two. */
.scan-chip.is-high { left: 8px; top: 21%; background: var(--invert-bg); color: var(--gold); }
.scan-chip.is-low {
  right: 8px; bottom: 15%;
  background: var(--paper-raised); color: var(--ink-muted);
  border: var(--rule) solid var(--line);
}
.scan-caption {
  text-align: center; max-width: 340px; margin: 22px auto 0;
  font-size: 13px; font-weight: 700; line-height: 1.5; color: var(--ink-faint);
}

/* ---------- Landing: sections ---------- */
.section { padding: clamp(48px, 8vw, 88px) clamp(20px, 5vw, 48px); max-width: 1140px; margin: 0 auto; }
/* The nav is `position: sticky; top: 0`, so an anchor jump lands the heading
   underneath it. Reserve its height on anything the nav links to. */
[id] { scroll-margin-top: 88px; }
.section.is-sunk { background: var(--paper-sunk); max-width: none; }
.section.is-sunk > .section-inner { max-width: 1080px; margin: 0 auto; }
.section h2 { font-size: clamp(26px, 4vw, 32px); font-weight: 700; margin-bottom: 6px; }
.section .section-sub { color: var(--ink-muted); font-size: 16px; font-weight: 600; margin: 0 0 44px; }
.section.is-centered { text-align: center; }

/* ---------- Steps ----------
   A dot and a rule running to the next one. The rule is on the item rather than
   between items so the last one can simply drop it, and `:last-child` keeps
   that honest if a fourth step is ever added. */
.steps { display: flex; flex-wrap: wrap; gap: 36px; }
.step { flex: 1 1 220px; min-width: 220px; }
.step-rule { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.step-dot { width: 26px; height: 26px; border-radius: 50%; background: var(--accent-bright); flex: none; }
.step-rule::after { content: ''; height: 2px; flex: 1; background: var(--line); }
.step:last-child .step-rule::after { background: transparent; }
.step h3 { font-size: 19px; font-weight: 700; margin-bottom: 8px; }
.step p { font-size: 14.5px; line-height: 1.55; color: var(--ink-muted); margin: 0; }

/* ---------- Feature rows ----------
   A definition list, not a card grid. Three claims about honesty read better as
   a plain ruled list than as three boxes competing for the eye — and the boxes
   were the last thing on the page still carrying a shadow. */
.feature-row {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr));
  gap: 10px 28px; padding: 26px 0; border-top: var(--rule) solid var(--line);
}
.feature-row:last-of-type { border-bottom: var(--rule) solid var(--line); }
.feature-row h3 { font-size: 18px; font-weight: 700; margin: 0; }
.feature-row p { font-size: 15px; font-weight: 600; line-height: 1.6; color: var(--ink-muted); margin: 0; }

/* ---------- Printing spread ----------
   The differentiator, and the only section built entirely from live data. Two
   photographs of the same artwork, tilted and overlapping, with their two very
   different prices under them — the argument is the gap between the numbers, so
   they are set at display size and everything else in the band is quiet.

   The multiple is a watermark rather than a heading: it is the loudest thing in
   the section and it should not also be the thing you read first. */
.spread-band { position: relative; overflow: hidden; padding: clamp(48px, 10vw, 96px) clamp(20px, 5vw, 48px); }
.spread-watermark {
  position: absolute; top: 10px; right: 20px; z-index: 0;
  font-family: var(--display); font-weight: 800; line-height: 1;
  font-size: clamp(60px, 14vw, 180px); color: oklch(30% 0.055 250);
  user-select: none; pointer-events: none;
}
.spread-inner {
  position: relative; z-index: 1; max-width: 1080px; margin: 0 auto;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  gap: 40px; align-items: center;
}
/* Capped, and centred in whatever column the grid hands it. Left to fill a
   520px desktop column the two cards land at opposite ends of it and stop
   reading as a stack — the overlap is the thing that says "these are the same
   card twice". At 330px they overlap by ~38px. */
/* `width: 100%` is not redundant next to `max-width`. Every child here is
   absolutely positioned, so with no in-flow content the box collapses to zero
   and `left`/`right` resolve against nothing — which put the two cards at
   opposite ends of the column in reverse order. */
.spread-stack {
  position: relative; min-height: 340px;
  width: 100%; max-width: 330px; margin-inline: auto;
}
.spread-item { position: absolute; text-align: center; }
/* The cheap printing sits lower and in front; the dear one behind, larger, gold.

   Flush to the edges of the 330px box rather than inset, which leaves the cards
   overlapping by ~26px — enough to read as a stack, and far enough apart that
   the two set names underneath clear each other. Inset by 8px they overlapped
   by 76px and "Phantasmal Flames" ran straight through "Prize Pack Series".

   The `z-index` is on `.is-low`, and it is the one thing here that is about
   evidence rather than composition. The Play! Pokemon stamp sits near the right
   edge of the artwork on both cards — present on one, absent on the other — and
   the two cards overlap in the middle. Whichever card is on top keeps its stamp
   region; the one underneath loses it. Put the dear card in front (the obvious
   choice, and what this was) and you cover the exact patch of the cheap card
   that proves the stamp is missing, so the section asserts its whole argument
   while hiding half the evidence. Cheap card in front, dear card on the right
   with nothing over its right edge, and both regions are visible at once. */
.spread-item.is-low { left: 0; top: 42px; width: 168px; transform: rotate(-9deg); z-index: 1; }
.spread-item.is-high { right: 0; top: 0; width: 188px; transform: rotate(6deg); }
/* `height: auto` for the same reason as `.scan-shot` — see the note there. */
.spread-shot {
  display: block; width: 100%; height: auto;
  border-radius: var(--radius-lg); box-shadow: var(--shadow-float);
}
.spread-price { font-family: var(--display); font-weight: 800; line-height: 1.1; margin-top: 10px; }
.spread-item.is-low .spread-price { font-size: 22px; color: var(--invert-ink-muted); }
.spread-item.is-high .spread-price { font-size: 26px; color: var(--gold); }
.spread-set { font-size: 12px; font-weight: 700; color: var(--invert-ink-faint); margin-top: 2px; }
.spread-copy h2 { margin-bottom: 18px; }
.spread-copy p { font-size: 15.5px; font-weight: 600; line-height: 1.65; margin: 0 0 20px; color: var(--ink-muted); }
.spread-copy p:last-child { margin-bottom: 0; color: var(--invert-ink-hover); }

/* ---------- Split: demo + waitlist ----------
   The two conversions, side by side and colour-coded. Blue panel offers the
   thing you can do now; navy panel asks for the email. Full-bleed on purpose —
   the band runs edge to edge so the page ends on a decision rather than on
   whitespace. */
.split { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); }
.split-half {
  padding: clamp(44px, 8vw, 72px) clamp(24px, 5vw, 56px);
  display: flex; flex-direction: column; justify-content: center;
}
.split-half.is-offer {
  background: var(--accent-bg);
  --paper: var(--accent-bg);
  --paper-raised: oklch(96% 0.03 250);
  --line: oklch(85% 0.05 250);
  --line-strong: oklch(78% 0.06 250);
}
.split-half h2 { font-size: 28px; margin-bottom: 14px; }
.split-half p { font-size: 15px; font-weight: 600; color: var(--ink-muted); margin: 0 0 6px; }
.split-half .btn { align-self: flex-start; margin-top: 20px; }

/* ---------- Waitlist ---------- */
.waitlist-form { display: flex; flex-direction: column; gap: 10px; max-width: 340px; margin-top: 22px; }
.waitlist-form input {
  min-width: 0; padding: 13px 16px; border-radius: var(--radius);
  border: var(--rule) solid var(--line-strong); font-size: 15px; font-family: inherit;
  background: var(--field); color: var(--ink);
}
.waitlist-form input::placeholder { color: var(--ink-faint); }
/* Turnstile is a sibling of the form, not a child of it, so it inherits none of
   the form's sizing — and `size: 'flexible'` means it fills whatever box it is
   given. Unconstrained that came out 608px against the form's 340px, which is
   what made a third-party widget read as a foreign object dropped into the
   panel rather than part of it. Same cap as `.waitlist-form`, deliberately: the
   two are meant to line up on both edges. (Flexible has a 300px floor of its
   own, so this can be narrowed to 300 and no further.)

   It stays a sibling on purpose. It renders lazily, when a visitor scrolls
   near, and inside the form it would appear between the field and the button —
   pushing the button ~73px down the page at the moment someone is reaching for
   it. Below the button the same insertion only moves the fine print. */
#waitlist #turnstile-widget { max-width: 340px; }
/* Only once it has actually rendered — the container is empty until then, and
   an unconditional margin would leave a gap under the button on every view for
   the majority of visitors who never trigger it. */
#waitlist #turnstile-widget:not(:empty) { margin-top: 14px; }
.form-note { font-size: 12.5px; font-weight: 600; color: var(--ink-faint); margin: 14px 0 0; }
.form-status {
  padding: 13px 18px; border-radius: var(--radius); font-weight: 700; font-size: 14px;
  margin-top: 12px;
}
.form-status.is-ok { background: var(--positive-bg); color: var(--positive); }
.form-status.is-error { background: var(--negative-bg); color: var(--negative); }

.site-footer {
  max-width: 1320px; margin: 0 auto;
  padding: 26px clamp(20px, 5vw, 48px);
  display: flex; align-items: center; justify-content: space-between;
  border-top: var(--rule) solid var(--line); gap: 14px; flex-wrap: wrap;
}
.site-footer .logo-word { font-size: 14px; color: var(--ink-faint); }
.site-footer .meta { font-size: 13px; font-weight: 700; color: var(--ink-faint); }

.footer-links { display: flex; gap: 20px; }
.footer-links a { font-size: 13px; font-weight: 700; color: var(--ink-faint); }
.footer-links a:hover { color: var(--accent-hover); }

/* ---------- Demo app shell ---------- */
.app { height: 100dvh; display: flex; flex-direction: column; }
.app-bar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 11px 22px; border-bottom: var(--rule) solid var(--line);
  background: var(--paper-raised); flex-shrink: 0;
}
.app-bar-left { display: flex; align-items: center; gap: 16px; }
.back-link {
  background: none; border: none; font-size: 13.5px; font-weight: 700;
  color: var(--ink-muted); cursor: pointer; font-family: inherit;
  display: flex; align-items: center; gap: 5px; padding: 4px 0;
}
.back-link:hover { color: var(--ink); }
.demo-badge {
  font-family: var(--mono); font-size: 10.5px; font-weight: 600; letter-spacing: 0.03em;
  color: var(--ink-faint); border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px;
}

.mobile-tabs {
  display: none; gap: 6px; padding: 8px 12px;
  border-bottom: var(--rule) solid var(--line); background: var(--paper-raised); flex-shrink: 0;
}
.mobile-tabs button {
  flex: 1; padding: 8px; border-radius: var(--radius); border: 1px solid var(--line-strong);
  font-size: 13px; font-weight: 700; font-family: inherit; cursor: pointer;
  background: var(--field); color: var(--ink-muted);
}
.mobile-tabs button[aria-selected='true'] { background: var(--accent); border-color: var(--accent); color: oklch(99% 0.012 95); }

.app-panels { flex: 1; display: flex; min-height: 0; min-width: 0; overflow: hidden; }

/* ---------- Chat panel ---------- */
.chat-panel {
  width: 380px; flex-shrink: 0; border-right: var(--rule) solid var(--line);
  display: flex; flex-direction: column; min-width: 0; min-height: 0;
  background: var(--paper-raised);
}
.chat-head {
  padding: 13px 16px; border-bottom: var(--rule) solid var(--line);
  display: flex; align-items: center; gap: 7px;
  font-family: var(--display); font-weight: 700; font-size: 14.5px; flex-shrink: 0;
}
.chat-head .status {
  margin-left: auto; display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--mono); font-size: 10.5px; font-weight: 600; color: var(--ink-faint);
}
.chat-head .status::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--positive);
}
.chat-log {
  flex: 1; overflow-y: auto; padding: 16px;
  display: flex; flex-direction: column; gap: 12px; width: 100%; min-width: 0;
}

.bubble { max-width: 88%; padding: 10px 14px; font-size: 14px; line-height: 1.5; }
.bubble.is-user {
  align-self: flex-end; max-width: 82%;
  background: var(--accent-bg); color: var(--ink); border-radius: 14px 14px 4px 14px;
}
.bubble.is-agent {
  align-self: flex-start; background: var(--bubble); border-radius: 14px 14px 14px 4px;
}
.chat-log .bubble { white-space: pre-wrap; overflow-wrap: anywhere; }
.chat-log .bubble.is-agent { max-width: 88%; }
/* Reads as "a photo was attached" without loading a third-party image, which
   would mean widening img-src for a CDN we don't control. */
.chat-thumb {
  width: 150px; max-width: 100%; height: 110px; border-radius: var(--radius);
  margin-bottom: 6px; background-size: cover; background-position: center; display: block;
}

.chat-status {
  align-self: flex-start; font-family: var(--mono); font-size: 11px;
  color: var(--ink-faint); display: flex; align-items: center; gap: 7px;
}
.chat-status::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent-bright); animation: fsPulse 1.2s ease-in-out infinite;
}

.typing {
  align-self: flex-start; background: var(--bubble); padding: 10px 13px;
  border-radius: 14px 14px 14px 4px; display: flex; gap: 5px;
}
.typing span {
  width: 6px; height: 6px; border-radius: 50%; background: var(--ink-faint);
  display: inline-block; animation: fsTypingBounce 1.1s infinite;
}
.typing span:nth-child(2) { animation-delay: 0.15s; }
.typing span:nth-child(3) { animation-delay: 0.3s; }

@keyframes fsTypingBounce {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }
  30% { transform: translateY(-4px); opacity: 1; }
}
@keyframes fsPulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  .typing span, .chat-status::before { animation: none; }
  * { scroll-behavior: auto !important; }
}

/* The post-scan ask. Lives in the chat log, so it is deliberately shaped like
   an agent bubble rather than a banner — it is the next beat of the
   conversation, not an interruption of it. Full width because an email field
   plus a button does not fit in 88% of a phone's chat column. */
.chat-ask {
  position: relative; align-self: stretch;
  background: var(--paper-raised); border: var(--rule) solid var(--accent-bright);
  border-radius: 14px 14px 14px 4px; padding: 15px 16px 14px;
}
.chat-ask-lede {
  font-family: var(--display); font-weight: 700; font-size: 15.5px;
  margin: 0 0 3px; padding-right: 20px;
}
.chat-ask-sub { font-size: 12.5px; color: var(--ink-muted); margin: 0 0 11px; }
.chat-ask-form { display: flex; gap: 7px; }
.chat-ask-form input {
  flex: 1; min-width: 0; padding: 9px 11px; border-radius: 8px;
  border: 1px solid var(--line-strong); font-size: 13px; font-family: inherit;
  background: var(--field); color: var(--ink);
}
.chat-ask-form .btn { padding: 9px 15px; font-size: 13px; white-space: nowrap; }
.chat-ask-note { font-size: 11.5px; color: var(--ink-faint); margin: 9px 0 0; }

/* The buying desk. Shares the ask's shell deliberately — it lands in the same
   place in the log, below the answer, and reads as the next beat of the
   conversation rather than a second kind of interruption.

   Two fields, so it wraps where the ask does not: on a 316px panel an email
   input and a price input side by side leave neither usable. */
.chat-sell {
  position: relative; align-self: stretch;
  background: var(--paper-raised); border: var(--rule) solid var(--accent-bright);
  border-radius: 14px 14px 14px 4px; padding: 15px 16px 14px;
}
.chat-sell-form { display: flex; flex-wrap: wrap; gap: 7px; }
.chat-sell-form input {
  flex: 1 1 100%; min-width: 0; padding: 9px 11px; border-radius: 8px;
  border: 1px solid var(--line-strong); font-size: 13px; font-family: inherit;
  background: var(--field); color: var(--ink);
}
.chat-sell-form .btn { flex: 0 0 auto; padding: 9px 15px; font-size: 13px; white-space: nowrap; }

/* The age gate. The `input` rule above is `flex: 1 1 100%` with 9px padding and
   a border, which a checkbox inherits — it renders as a full-width bordered box
   with a tiny tick floating in it. The override has to be more specific than
   that rule, hence the `.chat-sell-form` prefix.

   Takes a row of its own so the label is never truncated beside the button.
   `.chat-panel` is 380px on desktop, which leaves ~316px inside this panel. */
.chat-sell-age {
  flex: 1 1 100%; display: flex; align-items: center; gap: 8px;
  font-size: 12.5px; color: var(--ink-muted); cursor: pointer;
  margin: 1px 0 2px;
}
.chat-sell-form .chat-sell-age input[type="checkbox"] {
  flex: 0 0 auto; width: 16px; height: 16px; min-width: 0;
  padding: 0; margin: 0; border-radius: 4px;
  accent-color: var(--accent); cursor: pointer;
}

/* The three plans. A price test, not a shop — see `pricing.py`.

   Always one column, at every width, because `.chat-panel` is a fixed 380px on
   desktop and full-width only on mobile. That leaves ~316px inside the ask, and
   an auto-fit grid at `minmax(150px, …)` fits exactly two tracks there — so the
   first version of this rendered two cards and a lonely third on a second row,
   on phones *and* on desktop. Three across in 316px would give each plan ~100px,
   which puts "Marketplace-ready export" on four lines.

   The features stay visible at every size on purpose. They are the question —
   we are asking which features justify which price — and 82% of the people
   answering are on a phone. A mobile layout that hid them would mean the
   primary data came from a version that never showed what was being bought. */
.chat-ask-tiers {
  display: grid; gap: 8px; margin-bottom: 11px;
  grid-template-columns: 1fr;
}
.tier {
  display: flex; flex-direction: column; gap: 3px; text-align: left;
  padding: 11px 12px; cursor: pointer; font-family: inherit; color: var(--ink);
  background: var(--paper); border: 1px solid var(--line-strong); border-radius: 10px;
  transition: border-color .12s, background .12s;
}
/* Name and price on one line — a card costs a line less, and stacked cards are
   the only layout the chat panel ever gets. */
.tier-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
/* Hover styling only where there is a real pointer.

   On a touchscreen `:hover` sticks to whatever was last tapped, and here the
   hover and selected states are deliberately near-identical — so a stuck hover
   reads as "this plan is chosen" when nothing has been chosen. 82% of the
   people answering this are on a phone, which makes that the majority case
   rather than an edge one. `aria-pressed` remains the only thing that marks a
   choice on touch. */
@media (hover: hover) {
  .tier:hover { border-color: var(--accent-bright); background: var(--bubble); }
}
.tier[aria-pressed="true"] {
  border-color: var(--accent-bright); background: var(--bubble);
  box-shadow: inset 0 0 0 1px var(--accent-bright);
}
.tier-name { font-family: var(--display); font-weight: 700; font-size: 13.5px; }
.tier-price { font-size: 17px; font-weight: 700; line-height: 1.1; }
.tier-cadence {
  font-size: 11px; font-weight: 400; color: var(--ink-faint); margin-left: 4px;
}
.tier-blurb { font-size: 11.5px; color: var(--ink-muted); }
.tier-features {
  list-style: none; margin: 5px 0 0; padding: 0;
  font-size: 11.5px; color: var(--ink-muted);
}
.tier-features li { padding-left: 13px; position: relative; margin-top: 2px; }
.tier-features li::before {
  content: "+"; position: absolute; left: 0; color: var(--accent-bright); font-weight: 700;
}
/* Inline in the chat panel: three stacked cards with bulleted lists run to
   ~400px of a scrolling conversation, which buries the email field the panel
   exists to reveal. Wrapping them onto shared lines gets that back without
   dropping a single feature. */
.chat-ask-tiers .tier-features { display: flex; flex-wrap: wrap; gap: 1px 11px; }
.chat-ask-tiers .tier-features li { margin-top: 0; }

/* The same three plans on the landing page's early-access panel, which is
   `.split-half.is-dark` — so the tokens invert rather than the layout changing.
   Kept as a separate class instead of a `.is-dark .tier` descendant so the two
   panels cannot silently restyle each other. */
/* Sized against the panel, not the window.

   `.split-half` is half the viewport on a wide screen, so a viewport media
   query gets this wrong in both directions. Measured on the live page: the
   panel's inner width is about 0.4x the viewport, which puts two 150px tracks
   in reach at 800px and three only past 1165px — so every width from 800 to
   1160 rendered two cards and an orphaned third. At 768px it came out at 307px
   against the 308px two columns need, which is a one-pixel margin and not a
   design.

   One column by default, three once the panel itself can hold them. Never two,
   because three plans in two columns always strands one. */
.split-half.is-dark { container-type: inline-size; container-name: waitlist-panel; }
.tiers-dark {
  display: grid; gap: 8px; margin: 0 0 14px;
  grid-template-columns: 1fr;
}
@container waitlist-panel (min-width: 490px) {
  .tiers-dark { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
}
.tiers-dark .tier {
  background: rgb(255 255 255 / 6%); border-color: rgb(255 255 255 / 18%); color: inherit;
}
@media (hover: hover) {
  .tiers-dark .tier:hover {
    background: rgb(255 255 255 / 11%); border-color: var(--gold, #d8b45a);
  }
}
.tiers-dark .tier[aria-pressed="true"] {
  background: rgb(255 255 255 / 13%); border-color: var(--gold, #d8b45a);
  box-shadow: inset 0 0 0 1px var(--gold, #d8b45a);
}
.tiers-dark .tier-cadence,
.tiers-dark .tier-blurb,
.tiers-dark .tier-features { color: rgb(255 255 255 / 62%); }
.tiers-dark .tier-features li::before { color: var(--gold, #d8b45a); }
.chat-ask .form-status { margin-top: 10px; padding: 10px 13px; font-size: 13px; }
.chat-ask-close {
  position: absolute; top: 7px; right: 8px; width: 24px; height: 24px;
  border: 0; background: none; cursor: pointer; padding: 0;
  font-size: 19px; line-height: 1; color: var(--ink-faint); border-radius: 6px;
}
.chat-ask-close:hover { color: var(--ink); background: var(--bubble); }
/* Turnstile renders an iframe of its own size; keep it from shoving the note. */
.chat-ask #turnstile-widget:not(:empty) { margin-top: 11px; }

/* Rating card under the newest agent answer.

   This used to be two bare emoji at 32% opacity, on the theory that an answer
   the visitor is happy with should not be surrounded by things asking how they
   feel about it. That theory was wrong in the only way that matters: almost
   nobody saw it. A near-invisible control on a phone, where there is no hover
   to reveal it, is not restraint — it is a feature that does not ship. So it
   asks plainly, once, under the most recent answer only (see `addRating`). */
.rate {
  align-self: flex-start;
  /* `width`, not `max-width`. As a shrink-to-fit flex item the card sized
     itself from its content, so it changed width every time `rateStep` swapped
     the row: 285px under the three thumbs, then 208px at the note step, where
     the column layout made `width: 100%` on the textarea resolve circularly and
     the card ended up as wide as "Ask" plus "Never mind" happened to be. It
     visibly snapped narrower mid-conversation.
     A definite width fixes that, and `88%` is the same cap `.bubble` uses, so
     the card now lines up exactly with the agent bubble it sits under. */
  width: min(340px, 88%);
  margin: 2px 0 2px 2px;
  padding: 9px 11px 10px;
  border: var(--rule) solid var(--accent-line);
  border-radius: var(--radius-lg);
  background: var(--paper-raised);
}
.rate-lede {
  margin: 0 0 8px;
  font-family: var(--display); font-weight: 700; font-size: 13.5px;
  color: var(--ink-muted);
}
.rate-row { display: flex; flex-wrap: wrap; gap: 6px; }
.rate-row button {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: inherit; font-size: 12.5px; font-weight: 700; line-height: 1;
  padding: 8px 11px; cursor: pointer;
  color: var(--ink-muted);
  background: var(--field);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.rate-row button:hover, .rate-row button:focus-visible {
  background: var(--field-hover); border-color: var(--accent); color: var(--ink);
}
/* The icon carries the colour, not the button: a green and a red *fill* sitting
   side by side under every answer reads as a verdict the visitor hasn't given
   yet. Tinting the 15px glyph is enough to tell the two apart at a glance. */
.rate-icon { flex-shrink: 0; }
.rate-row .is-up .rate-icon { color: var(--positive); }
.rate-row .is-down .rate-icon { color: var(--negative); }
.rate-row .is-more .rate-icon { color: var(--ink-faint); }

/* The free-text step. Full width inside the card, buttons under the field. */
.rate-row.is-note { flex-direction: column; align-items: stretch; gap: 7px; }
.rate-note {
  width: 100%; box-sizing: border-box; resize: vertical;
  font-family: inherit; font-size: 12.5px; line-height: 1.45;
  padding: 8px 10px; color: var(--ink);
  background: var(--paper-sunk);
  border: 1px solid var(--line-strong); border-radius: var(--radius);
}
.rate-note::placeholder { color: var(--ink-faint); }
.rate-note:focus-visible { outline: none; border-color: var(--accent); }
.rate-note-actions { display: flex; gap: 6px; }
.rate-row .is-send { color: var(--ink); border-color: var(--accent); background: var(--accent-bg); }
.rate-row .is-skip { background: none; }

/* Answered. Keep the receipt, drop the border that was there to catch the eye. */
.rate.is-done { border-color: var(--line); background: none; padding: 4px 2px 2px; }
.rate.is-done .rate-lede { margin: 0; color: var(--ink-faint); font-weight: 600; }
.rate.is-done .rate-row:empty { display: none; }

.chat-share {
  margin: 0 16px 10px; padding: 13px 15px; border-radius: var(--radius-lg);
  background: var(--paper-raised); border: var(--rule) solid var(--accent-line); flex-shrink: 0;
}
.chat-share-lede { font-family: var(--display); font-weight: 700; font-size: 15px; margin: 0 0 4px; }
.chat-share-sub { font-size: 12px; color: var(--ink-muted); margin: 0 0 11px; line-height: 1.5; }
.chat-share-actions { display: flex; gap: 8px; }

/* Shown when vLLM is unreachable. Sits where the answer would have gone and
   borrows the warning tokens rather than the error ones on purpose: this is a
   pause with a known end, not a failure the visitor caused or has to act on. */
.model-down {
  margin: 0 16px 10px; padding: 13px 15px; border-radius: var(--radius-lg);
  background: var(--warning-bg); border: var(--rule) solid var(--warning);
  color: var(--warning); flex-shrink: 0;
}
.model-down strong { font-family: var(--display); font-size: 14.5px; display: block; margin-bottom: 4px; }
.model-down p { font-size: 12.5px; margin: 0; line-height: 1.5; color: var(--ink-muted); }
.model-down a { color: var(--accent); }

.suggestions { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 16px 10px; flex-shrink: 0; }
.suggestions button {
  font-family: inherit; font-size: 12.5px; font-weight: 600; padding: 6px 12px; cursor: pointer;
  background: var(--field); border: 1px solid var(--line-strong); border-radius: 999px; color: var(--ink-muted);
}
.suggestions button:hover { border-color: var(--accent); color: var(--ink); }
/* The sample-card chip is the demo's headline action, so it reads as an action
   rather than as one more thing you could type. */
.suggestions button.is-primary {
  background: var(--accent); border-color: var(--accent);
  color: oklch(99% 0.012 95); font-weight: 800;
}
.suggestions button.is-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: oklch(99% 0.012 95); }
.suggestions button:disabled { opacity: 0.55; cursor: default; }

.chat-form { width: 100%; padding: 10px 16px 16px; display: flex; gap: 7px; flex-shrink: 0; }
.chat-form :disabled { opacity: 0.55; cursor: default; }
.chat-form input[type='text'] {
  flex: 1; min-width: 0; padding: 10px 13px; border-radius: var(--radius);
  border: 1px solid var(--line-strong); font-size: 13.5px; font-family: inherit;
  background: var(--field); color: var(--ink);
}
.icon-btn {
  flex-shrink: 0; width: 38px; height: 38px; border-radius: var(--radius);
  border: 1px solid var(--line-strong); background: var(--field); cursor: pointer;
  /* The camera glyph strokes with currentColor, so this is what draws it. */
  color: var(--ink-muted);
  display: flex; align-items: center; justify-content: center; padding: 0;
}
.icon-btn:hover { border-color: var(--accent); color: var(--ink); }

/* ---------- Analytics panel ---------- */
.analytics-panel { flex: 1; overflow: auto; padding: 16px 24px 24px; min-width: 0; }
.analytics-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.analytics-head h2 { font-size: 20px; font-weight: 700; }

.tabs {
  display: flex; align-items: center; gap: 6px;
  border-bottom: var(--rule) solid var(--line); margin-bottom: 18px; overflow-x: auto;
  scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
.tab-group { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.tab {
  background: none; border: none; border-bottom: 2px solid transparent;
  padding: 8px 4px; font-size: 13.5px; font-weight: 700; color: var(--ink-faint);
  cursor: pointer; font-family: inherit; white-space: nowrap;
  /* Sits on the container's 2px rule; pull it down so the two overlap rather
     than stacking into a 4px step under the selected tab. */
  margin-bottom: calc(-1 * var(--rule));
}
.tab[aria-selected='true'] { color: var(--ink); border-bottom-color: var(--accent); }
.tab-close {
  background: none; border: none; color: var(--ink-faint); cursor: pointer;
  font-size: 13px; line-height: 1; padding: 2px 6px 8px; font-family: inherit;
}
.tab-close:hover { color: var(--negative); }

.tab-new { animation: fsTabIn 260ms ease; }
@keyframes fsTabIn { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }

/* ---------- Blocks ---------- */
.block { margin-bottom: 14px; }
.block-title {
  font-family: var(--mono); font-size: 10.5px; font-weight: 600;
  color: var(--ink-faint); letter-spacing: 0.04em; margin-bottom: 8px;
}

.card-surface { background: var(--paper-raised); border: var(--rule) solid var(--line); border-radius: var(--radius-lg); }

.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(130px, 100%), 1fr)); gap: 12px; }
.stat { padding: 14px; min-width: 0; }
.stat-label { font-family: var(--mono); font-size: 10.5px; font-weight: 600; color: var(--ink-faint); margin-bottom: 5px; }
.stat-value {
  font-family: var(--display); font-size: 20px; font-weight: 700;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.table-wrap { overflow-x: auto; }
.data-table { width: 100%; min-width: 560px; border-collapse: collapse; }
.data-table th {
  text-align: left; padding: 9px 14px; font-family: var(--mono);
  font-size: 10.5px; font-weight: 600; color: var(--ink-faint);
  border-bottom: 1px solid var(--line); white-space: nowrap;
}
.data-table td { padding: 10px 14px; font-size: 13.5px; border-bottom: 1px solid var(--line-soft); }
.data-table tr:last-child td { border-bottom: none; }
.data-table .is-right { text-align: right; }
.data-table .is-mono { font-family: var(--mono); font-weight: 500; }
.data-table .is-emphasis { font-weight: 700; }

.pager { display: flex; align-items: center; justify-content: flex-end; gap: 12px; padding: 10px 2px 0; }
.pager span { font-family: var(--mono); font-size: 11.5px; color: var(--ink-faint); }
.pager button {
  background: var(--field); border: 1px solid var(--line-strong); border-radius: 6px;
  width: 26px; height: 26px; cursor: pointer; font-size: 13px; color: var(--ink-muted);
}
.pager button:disabled { opacity: 0.4; cursor: default; }

.barlist { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
.bar-row { display: flex; align-items: center; gap: 12px; }
.bar-name { width: 150px; flex-shrink: 0; font-size: 13.5px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-track { flex: 1; background: var(--track); border-radius: 5px; height: 10px; position: relative; overflow: hidden; }
.bar-fill { position: absolute; inset: 0 auto 0 0; border-radius: 5px; transition: width 400ms ease; }
/* Mono has no weight above 600 in this theme, so a price leans on size and ink
   rather than on a bold that would be synthesised. */
.bar-value { width: 56px; flex-shrink: 0; text-align: right; font-family: var(--mono); font-weight: 600; font-size: 13px; color: var(--ink); }

.order-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 13px 16px; border-bottom: 1px solid var(--line-soft); gap: 12px;
}
.order-row:last-child { border-bottom: none; }
.order-card { font-weight: 700; font-size: 14px; }
.order-buyer { font-size: 12px; color: var(--ink-faint); }
.order-status {
  font-family: var(--mono); font-size: 11px; font-weight: 600;
  padding: 4px 9px; border-radius: 6px; white-space: nowrap;
}

.note {
  padding: 13px 16px; font-size: 14px; line-height: 1.55;
  border-left: 4px solid var(--accent-bright); background: var(--paper-raised);
  border-radius: 0 var(--radius) var(--radius) 0; color: var(--ink-muted);
}

/* Tone utilities — the single place tone maps to colour. */
.tone-neutral { color: var(--ink); }
.tone-positive { color: var(--positive); }
.tone-negative { color: var(--negative); }
.tone-accent { color: var(--accent); }
.tone-warning { color: var(--warning); }
.bg-neutral { background: var(--bubble); color: var(--ink-muted); }
.bg-positive { background: var(--positive-bg); color: var(--positive); }
.bg-negative { background: var(--negative-bg); color: var(--negative); }
.bg-accent { background: var(--accent-bg); color: var(--accent-hover); }
.bg-warning { background: var(--warning-bg); color: var(--warning); }
.fill-neutral { background: var(--ink-faint); }
.fill-positive { background: oklch(60% 0.16 150); }
.fill-negative { background: oklch(60% 0.20 25); }
.fill-accent { background: var(--accent-bright); }
.fill-warning { background: var(--gold); }
/* `.stroke-*` lived here for the line chart's SVG polyline. Gone with it. */

.empty-state { color: var(--ink-faint); font-size: 14px; padding: 24px 0; }

/* ---------- Responsive ---------- */
@media (max-width: 900px) {
  /* The eyebrow is a nice-to-have; the price chips on the card opposite are the
     only proof on the page. On a 390px screen the pill costs ~46px of a 664px
     fold, and the prices win that trade every time.

     Scoped to the hero. `.eyebrow` is also the label above the heading on the
     legal pages and the 404, where nothing is competing for a fold and hiding
     it just deletes the page's category. */
  .hero .eyebrow { display: none; }

  .hero { padding: 20px 18px 0; gap: 12px; }
  .hero-visual { padding: 4px 0 36px; }
  /* 18px puts the lede on three lines at 360px and pushed the second price
     chip 13px under the fold. Both prices are the only proof on the page and
     both have to be visible without scrolling, so the lede gives up 2px.
     Measured, not guessed — see /tmp fold check in the commit message. */
  .hero p.lede { font-size: 16px; margin-bottom: 24px; }
  .section { padding: 44px 18px; }
  .nav { padding: 12px 16px; }
  .nav-links a:not(.btn) { display: none; }
  .steps { gap: 28px; }
  /* The connector rule reads as a join between columns. Stacked, it points at
     nothing, so it goes rather than becoming a stray dash under each dot. */
  .step-rule::after { display: none; }
  .site-footer { flex-direction: column; align-items: flex-start; gap: 8px; }

  /* Absolute, overlapping cards need a fixed box to overlap inside. Stacked on
     a phone there is no room for the tilt, so the two printings become a plain
     row — the numbers are the entire point of the section and they must not be
     squeezed to preserve a flourish. */
  .spread-stack { position: static; min-height: 0; max-width: none; display: flex; gap: 14px; justify-content: center; }
  .spread-item, .spread-item.is-low, .spread-item.is-high {
    position: static; width: auto; flex: 1 1 0; max-width: 170px; transform: none;
  }
  .spread-watermark { font-size: 84px; top: 4px; right: 10px; }
}

@media (max-width: 760px) {
  .mobile-tabs { display: flex; }
  .app-panels { flex-direction: column; }
  .chat-panel { width: 100%; flex: 1; border-right: none; }
  .analytics-panel { padding: 16px 14px 20px; }
  .app-panels[data-mobile-tab='chat'] .analytics-panel { display: none; }
  .app-panels[data-mobile-tab='analytics'] .chat-panel { display: none; }
  .app-bar { padding: 10px 14px; }
  .demo-badge { display: none; }
  .bar-name { width: 110px; }
}

.chat-log .bubble code {
  font-family: var(--mono); font-size: 0.92em;
  background: var(--field); padding: 1px 5px; border-radius: 4px;
}
/* The user bubble is `--accent-bg`, a pale blue wash; inline code inside it has
   to step further from that, not toward it. */
.chat-log .bubble.is-user code { background: oklch(88% 0.06 250); }

/* ---------- Legal pages ---------- */
.prose { max-width: 720px; padding-top: 48px; padding-bottom: 64px; }
.prose h1 { font-size: 38px; font-weight: 800; margin-bottom: 6px; }
.prose .updated { font-family: var(--mono); font-size: 11.5px; color: var(--ink-faint); margin: 0 0 32px; }
.prose h2 { font-size: 22px; font-weight: 700; margin: 34px 0 10px; }
.prose h3 { font-size: 17px; font-weight: 700; margin: 22px 0 8px; }
.prose p { font-size: 15.5px; line-height: 1.65; color: var(--ink-muted); margin: 0 0 14px; }
.prose ul { margin: 0 0 16px; padding-left: 20px; }
.prose li { font-size: 15.5px; line-height: 1.65; color: var(--ink-muted); margin-bottom: 7px; }
.prose strong { color: var(--ink); font-weight: 700; }
.prose code {
  font-family: var(--mono); font-size: 0.88em;
  background: var(--paper-sunk); padding: 1px 5px; border-radius: 4px; color: var(--ink);
}
.prose hr { border: none; border-top: var(--rule) solid var(--line); margin: 36px 0 18px; }
