/* ═════════════════════════════════════════════════════════════════════════════
   mentionchecker.com — the PAGE. Nothing here is a design decision.

   ⛔ Tokens, type scale, buttons, chips, fields, meters and the section rhythm all come
   from `tokens.css` + `components.css`, which are GENERATED copies of `design/` — the
   same two files the app imports. This stylesheet may only describe things that exist
   on THIS page and nowhere in the product: the stage, the shell of the app-shaped hero
   mock, the pricing ladder, the FAQ.

   ⚠️ If you find yourself writing a colour, a font stack, a radius or a heading size
   here, stop: that belongs in design/tokens.css and the app needs it too. Two copies of
   one fact is exactly how this site and the app ended up in opposite languages —
   a bordered, pill-buttoned, 76px-rhythm site in front of a borderless 96px app.
   ═════════════════════════════════════════════════════════════════════════════ */

/* ── the page's own aliases. The markup predates the system; these map its vocabulary
      onto the tokens rather than rewriting 400 lines of HTML. ⛔ Not new values. ── */
:root {
  /* ⭐ THE WARM GROUND — this site only. calendly.com's two tones, sampled: #FCFBF8 page,
     #F5F3EE hero panel. ⛔ Bound here and nowhere else; the app stays cool. */
  --page: var(--paper);
  --band: var(--paper-2);
  --plane: var(--paper-2);
  --well: var(--paper-3);
  --hair: var(--paper-hair);
  --hair-2: var(--paper-hair-2);

  --bg: var(--page);
  --panel: var(--band);
  --panel-2: var(--plane);
  --card: var(--page);
  --line: var(--hair);
  --line-2: var(--hair-2);
  --fg: var(--ink);
  --fg-2: var(--ink-2);
  --fg-3: var(--ink-3);
  --accent-2: var(--accent-press);
  --accent-bg: var(--accent-wash);
  --good: var(--ok);
  --good-bg: var(--ok-wash);
  --max: 1200px;
  --gutter: var(--s6);
}

html { scroll-behavior: smooth; background: var(--page); }
body { background: var(--page); }
section { padding: var(--s9) 0; }
/* ⭐⭐ THE SEAM BETWEEN TWO UNTINTED SECTIONS — 96 bottom + 96 top is 192px, and DESIGN.md rule 1
   says sections breathe at 96–128. ⛔ A BANDED section does not have this problem: its padding
   sits INSIDE the tint, so it separates nothing and the seam is the single 96 next to it. That is
   why hero→pricing measured 103px while pricing→features measured 199px — the rhythm was only
   ever wrong where two sections both sat on the page ground.
   ⭐ So the top half of THAT seam alone is cut to 32, landing every seam on the page in range:
   hero→pricing 96 · pricing→features 128 · features→faq 128 · faq→close 96.
   ⚠️ THIS IS THE FIX THE FAQ'S OWN COMMENT DEMANDED (index.html, 2026-09-07): it removed an
   inline `padding-top:0` on #faq and said that if the air is wrong it is "the RHYTHM being wrong
   for every section, not for this one". ⛔ So this lives HERE, in the stylesheet, and applies to
   every such pair — never as an inline override on one section.
   ⛔⛔ EVERY TINTED SECTION MUST BE EXCLUDED ON BOTH SIDES, and this rule has already been wrong
   once for exactly that reason: it was written when .hero and .close were the only two, then
   #features was given .band and nobody came back here. A band's padding sits INSIDE the tint, so
   cutting it to 32 does not close a gap — it crushes the space above that section's own heading,
   which is what Pavel spotted. ⚠️ If you tint another section, add it here in the same breath. */
section:not(.hero):not(.band):not(#asked) + section:not(.close):not(.band) { padding-top: var(--s6); }
/* ⚠️ #asked is in that list since 2026-09-11 (Pavel: "there is no padding/space before questions")
   — it is a full-bleed gradient now, coloured like a band, and the FAQ under it needs its full s9. */
section[id] { scroll-margin-top: 78px; }
.wrap { max-width: var(--max); padding: 0 var(--gutter); }
/* ⚠️ BALANCED, because these headlines are now sentences rather than labels. "Everything you
   need. Nothing you don't." was breaking after "you" and stranding `don't.` alone on line two;
   balancing splits it at the full stop, which is where it wanted to break anyway.
   ⭐ Scoped to .sec-h rather than bare h2 — the h2 SIZE is a shared scale every section moves
   together (see below), but wrapping is per-headline and .close h2 already balances its own. */
.sec-h { max-width: 660px; margin-bottom: var(--s7); }
/* ⭐ 800 BECAUSE THE SENTENCE NEEDS 725. "Everything you need. Nothing you don't." measures 725px
   on one line at the headline's largest size (44px, ≥1280) and was wrapping inside the shared
   660 — measured, not guessed, with a nowrap clone. ⛔ 660 stays for the heads that carry a
   sub-line: it is a reading measure for the PARAGRAPH, and this head has none on purpose.
   ⚠️ The 75px of slack is for a font that renders wider than this machine's, not for a longer
   headline — re-measure if the words change. */
.sec-h-wide { max-width: 800px; }
.sec-h h2 { text-wrap: balance; }
.sec-h p { margin-top: var(--s4); font-size: 1.125rem; line-height: 1.55; color: var(--fg-2); }
/* ⭐ The section headlines are DISPLAY too — same face as the hero, so the page has one
   voice at large sizes and Geist everywhere below them.
   ⚠️ Bumped from a 2.25rem cap. Against a 4rem hero the old size left the sections reading
   as subheads rather than as the next thing to look at. ⛔ This is the SHARED scale — every
   section headline moves together. A page where one section head is larger than the rest
   reads as a mistake, not as emphasis. */
h2 { font-family: var(--font-display); font-weight: var(--w-display); font-size: clamp(2rem, 3.2vw, 2.75rem); letter-spacing: -.032em; line-height: 1.12; }
h3 { font-size: 1.0625rem; letter-spacing: -.02em; }

/* ── nav ──────────────────────────────────────────────────────────────────── */
/* ⛔ NO BOTTOM BORDER. calendly.com's header sits on the page with nothing under it —
   a hairline across the full viewport is the loudest thing a quiet header can contain. */
.nav { position: sticky; top: 0; z-index: 60; background: var(--page);
       transition: background var(--t-fast) ease; }
/* ⭐ CALENDLY'S GEOMETRY, MEASURED off their 1512px viewport (Pavel, 2026-09-11: "do exactly"):
   the bar's content sits 47px in from the viewport edge, NOT on the page's .wrap grid; the card
   that appears on scroll is inset by that same 47px, 72px tall, with ~18px of padding inside it.
   ⭐ So the content is placed for the CARD from the start — at rest the plate is simply not
   drawn — and nothing moves when it appears. ⛔ `.nav-in` is no longer a .wrap; the hero's
   grid is centred and does not need the bar to line up with it, and theirs does not either. */
.nav-in { display: flex; align-items: center; height: 72px; gap: var(--s5);
          margin: 0 47px; padding: 0 18px; }
/* ⭐⭐ THE FLOATING CARD (Pavel, 2026-09-11, calendly.com's header): at the top of the page the
   header is a plain bar on the page ground. Once nav.js sees scroll, the bar's ground goes and
   .nav-card becomes a white card inset by the hero panel's own margin — the same edge the hero,
   the stage and the footer sit on — with the system's lift. ⚠️ The INNER .wrap keeps its width,
   so the links stay on the page grid; only the plate around them appears.
   ⛔ Nothing changes size — a header that shrinks under the reader's cursor moves the thing they
   were about to click. */
/* ⚠️ The 12px drop is a TRANSFORM, not a margin-top: a top margin on the card collapses through the
   sticky header (no padding, no border) and the card sits flush with the viewport; padding on
   the header would change its height and shift the page by 12px every time the class toggles.
   A transform does neither. */
.nav-card { transition: margin var(--t-fast) ease, background var(--t-fast) ease, transform var(--t-fast) ease,
                        box-shadow var(--t-fast) ease, border-radius var(--t-fast) ease; }
.nav.is-scrolled { background: transparent; }
.nav.is-scrolled .nav-card { margin: 0 47px; transform: translateY(var(--s3)); background: #FFFFFF;
                             border-radius: 16px; box-shadow: 0 8px 30px rgba(11, 27, 54, .10); }
.nav.is-scrolled .nav-in { margin: 0; }
/* ⭐ The lockup, as artwork. The site has no theme toggle, so the navy wordmark is safe
   as a fixed fill here; the APP draws the mark and sets its own live text instead. */
.brand { display: inline-flex; align-items: center; }
/* ⚠️ 32px, not 26 — calendly.com's bar carries its lockup at ~34 css px in a 72px bar; ours at 26
   read small in the same bar. */
.brand .blogo { display: block; height: 32px; width: auto; }
.nav-mid { position: absolute; left: 50%; translate: -50% 0; display: flex; gap: var(--s1); }
/* ⭐ MEASURED, not guessed. calendly.com: 14px / w500 / #071A31 — their FULL ink.
   xero.com: 15px / w500 / #000856 — also their full ink. Neither mutes its nav links to a
   secondary grey, and neither leaves them at 400. Ours did both, which is why they read
   faint next to either reference. */
/* ⚠️ "Sign in" is one of these, not a second button. It is for the customer who already bought
   and is not reading the page — giving it a button would put two CTAs side by side and make the
   quiet one compete with the one that earns. */
/* ⛔ NOWRAP. These are flex items in a bar that can run out of room, and a nav link is never two
   lines — when the room goes, the label has to stay whole and the sizes below have to give. */
.nav-mid a, .nav-in-link {
  font-size: .875rem; font-weight: var(--w-med); color: var(--ink);
  padding: 8px 12px; border-radius: var(--r); white-space: nowrap;
  transition: background var(--t-fast) ease;
}
/* ⛔ NO UNDERLINE. Measured off calendly.com: the hover is a soft chip — #F5F3EE on their
   #FCFBF8 page, 8px padding, 16px radius. Those two tones are exactly our --band on
   --paper, so this is the same move in our own tokens rather than a copied hex.
   The ink does not change: the links already sit at full --ink, and rule 4 keeps the
   accent off nav. */
.nav-mid a:hover, .nav-in-link:hover { background: var(--band); }
.nav-r { margin-left: auto; display: flex; align-items: center; gap: var(--s3); }

/* ── hero ─────────────────────────────────────────────────────────────────── */
/* ⭐ Display size comes from the scale, not from here — the h1 IS .t-display. Both
   references run a hero two to three times anything near it; the old 45px cap was the
   main reason this page read smaller than either of them. */
/* ⭐ THE PANEL, straight off calendly.com: the hero is not a section of the page, it is an
   inset block in the deeper warm tone with big corners, and the product visual bleeds off
   its bottom edge rather than floating inside it.
   ⭐ CENTRED for the same reason — left-aligned, a 64px display in a 1200px column leaves
   half the fold empty. */
.hero {
  margin: 0 var(--s5);
  padding: var(--s9) 0 0;
  background: var(--band);
  border-radius: var(--r-xl);
  overflow: hidden;
  text-align: center;
}
/* ⚠️ Spacing only — the chip's colour, radius and size come from components.css. */
.hero-tag { margin-bottom: var(--s4); }
/* ⭐ 5rem cap since 2026-09-11 (Pavel: "on desktop — make title larger, 4 -> 5"); the vw term moves
   with it so the cap is reached at the same width as before (~1230px), and the mark cluster is
   em-sized so it grows in step. Phones keep their own rule below. */
.hero h1 { font-family: var(--font-display); font-weight: var(--w-display); margin: 0 auto; max-width: 21ch; font-size: clamp(2.6rem, 6.5vw, 5rem); line-height: 1.04; letter-spacing: -.035em; text-wrap: balance; }
/* ⚠️ 44ch is MEASURED, not chosen for looks: it breaks the lede after "recommendations —" so
   the em dash ENDS line one rather than starting line two, which is where a dash belongs. The
   window is 43–45ch and 44 is the middle of it, so there is 1ch of slack either side; below 43
   the dash drops to line two, above 45 "your brand" climbs to line one.
   ⛔ This number belongs to that exact sentence, not to the layout — every edit to the lede so
   far has moved the window (54→44→46→44). Change a word, re-measure, or the break wanders. */
.hero .lede { margin: var(--s5) auto 0; max-width: 44ch; font-size: 1.125rem; line-height: 1.55; color: var(--fg-2); }
.hero .lede b { font-weight: var(--w-med); color: var(--fg); }
.checkbar { margin: var(--s6) auto 0; max-width: 540px; }
.hero-note { justify-content: center; }
/* The stage is a picture of the product; it does not inherit the hero's centring, and it
   runs off the bottom of the panel, so its lower corners never round away from the edge. */
.stage { text-align: left; }
.hero .stage { border-radius: var(--r-xl) var(--r-xl) 0 0; margin-bottom: 0; }
/* ⭐ The hero field is a REAL input (was a span — "this page is a picture of the product").
   The page is now the door into the app, so the one control the whole pitch points at has to
   be the one control that works. components.css `.checkbar .field` carries the shared look. */
.checkbar .field { flex: 1; min-width: 0; font-size: 1.0625rem; padding-right: var(--s3); }
.hero-note { margin-top: var(--s4); display: flex; align-items: center; gap: 9px; font-size: .875rem; color: var(--fg-3); flex-wrap: wrap; }
.hero-note .it { white-space: nowrap; }
.hero-note .it + .it::before { content: "·"; color: var(--line-2); margin-right: 9px; }
.hero-note b { color: var(--fg-2); font-weight: var(--w-med); }

/* ── stage: the product, floating on colour ───────────────────────────────────
   ⭐ Both references put the product shot on a soft colour field rather than on the page. */
.stage {
  /* ⭐ THE PRODUCT PICTURE IS DRAWN IN THE PRODUCT'S PALETTE — which is the WARM one. This block
     used to reset --page/--band/--plane/--well to the cool set on the argument that "the app
     stays cool". It does not: app/globals.css binds the same warm paper this page binds at :root
     (#FCFBF8 sheet on #F5F3EE band), and Pavel called the cool mock out on 2026-09-11 — "we use
     there warm theme, not blue". ⛔ Nothing is reset here now; the app inside the stage inherits
     exactly the tokens the app renders with. The FIELD behind it stays blue — that is the colour
     the product floats on, not the product. */
  position: relative; border-radius: var(--r-xl); overflow: hidden;
  /* ⭐ THE FIELD IS A MESH, LIKE CALENDLY'S (Pavel, 2026-09-11, with their screenshot): a saturated
     blue core top-centre, pink and lavender on the left, pale yellow at the right edge, mint and
     aqua along the bottom. Nine soft radial blobs over a blue→aqua base — every one of them is a
     brand-adjacent tint (the accent blue, the CTA's mint) except the pink, which is the one hue
     borrowed for the resemblance. ⚠️ Blob sizes are percentages so the mesh scales with the stage as
     it grows; ⛔ no `filter: blur()` — the softness is the gradient's own falloff, which costs
     nothing to paint while the stage is resizing under a scroll. */
  background:
    radial-gradient(38% 55% at  2% 58%, rgba(236, 184, 247, .95), transparent 70%),
    radial-gradient(30% 42% at 12% 30%, rgba(196, 200, 250, .95), transparent 70%),
    radial-gradient(44% 58% at 54% 14%, rgba( 76, 132, 240, .92), transparent 70%),
    radial-gradient(36% 48% at 30%  4%, rgba(158, 200, 250, .95), transparent 70%),
    radial-gradient(28% 60% at 99% 42%, rgba(240, 245, 214, .95), transparent 70%),
    radial-gradient(42% 58% at 90% 88%, rgba(122, 226, 200, .95), transparent 70%),
    radial-gradient(38% 46% at 22% 100%, rgba(168, 236, 226, .90), transparent 70%),
    radial-gradient(30% 40% at 62% 100%, rgba(150, 205, 235, .80), transparent 70%),
    linear-gradient(160deg, #BFD7F8 0%, #8FB8F2 45%, #A9E4DA 100%);
  padding: var(--s7) var(--s7) 0;
  /* ⭐⭐ IT GROWS AS YOU SCROLL TO IT (Pavel, 2026-09-11, calendly.com's move). `--p` runs 0→1 as
     the stage rises through the viewport — set by stage.js, which is the only script involved —
     and every dimension that changes is a calc() on it: at 0 the stage is the .wrap's width and
     the frame inside it is 1060px; at 1 both fill the hero panel. ⛔ Nothing here animates on its
     own clock: no transition, no keyframes. Scroll IS the timeline, so it never fights the user.
     ⚠️ The stage sits OUTSIDE .wrap for this — its containing block is the hero, which is the
     width it grows into; the 1136px / 100% − 64px below is exactly what .wrap would have given
     it. Without JS, --p stays 0 and the layout is the static one. */
  --p: 0;
  max-width: calc(min(1136px, 100% - 2 * var(--s6)) * (1 - var(--p)) + 100% * var(--p));
  margin: var(--s8) auto 0;
}

/* ⭐⭐ A FIXED FRAME, AND THE APP SCROLLS INSIDE IT (Pavel, 2026-09-11: "don't make image too
   long. fit it to normal aspect ratio, just enable scroll inside the image"). The report is
   taller than any picture should be, so the frame is 16:10 — a laptop's shape — and the app
   inside it is the scroll container. ⛔ The frame, not `.ap`, carries the radius, the shadow and
   the clip: `overflow: auto` on a zoomed element would put the scrollbar in the zoomed
   coordinate space, and a shadow on the scrolled element is clipped by its own scrollport.
   ⛔ NO `overscroll-behavior: contain` — it was here and it TRAPPED THE PAGE: reaching the end of
   the report stopped the wheel dead instead of handing the scroll back to the page (Pavel:
   "when i scroll to the end of screen, i can't continue scrolling the rest of my page").
   ⚠️ The bottom edge stays FLUSH with the hero's edge — the frame is what runs off the panel now,
   so the picture still reads as "the app continues below" and the scroll says so.
   ⭐ It widens with the stage: 1060px at --p 0, the stage's full content width at 1. */
.stage-in { position: relative; margin: 0 auto; aspect-ratio: 16 / 10;
            max-width: calc(1060px * (1 - var(--p)) + 100% * var(--p));
            overflow-y: auto; overflow-x: hidden;
            border-radius: var(--r-lg) var(--r-lg) 0 0; box-shadow: var(--lift-3);
            background: var(--band); }

/* ── the app, at screenshot scale ─────────────────────────────────────────────
   ⭐⭐ THE MOCK IS THE APP (Pavel, 2026-09-11). The old mock was an invented browser window — a
   traffic-light bar, four engine columns, a "question by question" grid — none of which the
   product draws. This is app/app/[id]/Console.js's finished report inside the app's own shell.
   ⛔ Everything TYPOGRAPHIC comes from the shared classes (tokens.css / components.css) exactly as
   the app uses them; only the SHELL is drawn here, and each number is app/globals.css's:
   `.top` 16/20px padding and 36px controls, `.bsw` 216px, `.canvas` inset --s4 with --lift and
   --r-xl, `.bgrid` 196px column + --s9 gap, `.col` padding --s7 --s6, `.bnav-item` 9px --s3.
   ⭐ ONE `zoom` brings the whole thing to picture scale. It is the app at 1440 shown at 72%, so a
   64px display headline reads at 46px inside a 1060px stage. ⛔ No per-element font sizes: the
   moment one is added the mock stops being the app and starts being a drawing of it.
   ⚠️ `zoom` is standard in every current browser (Chrome, Safari, Firefox 126+); a browser
   without it shows the same picture larger, which is a smaller failure than a transform's
   (transform does not affect layout, so the stage would keep the unscaled height). */
/* ⭐⭐ THE ZOOM FOLLOWS THE FRAME (Pavel, 2026-09-11: "the content itself didn't grow/scale"). The app
   is laid out at ONE virtual width — `--ap-vw`, 1472px, which is 1060 / .72, the frame at rest —
   and stage.js sets `--z` to frameWidth / --ap-vw whenever the frame changes, so the picture scales
   with its frame instead of gaining empty width. At rest that is .72; grown at 1440 it is .88.
   ⛔ The fallback is the rest value, so without JS the picture is the static one. */
.ap { zoom: var(--z, .72); background: var(--band); }
.stage { --ap-vw: 1472; }
.ap-top { display: flex; align-items: center; justify-content: space-between; gap: var(--s4);
          padding: var(--s4) var(--s5); }
.ap-top-l, .ap-top-r { display: flex; align-items: center; min-width: 0; }
.ap-top-l { gap: var(--s5); }
.ap-top-r { gap: var(--s4); }
.ap-top img { width: 173px; height: 26px; }
.ap-bsw, .ap-pill, .ap-round { height: 36px; box-sizing: border-box; display: inline-flex;
  align-items: center; gap: var(--s2); font-size: .875rem; color: var(--ink-3);
  border-radius: var(--r-sm); box-shadow: inset 0 0 0 1px var(--hair); padding: 0 var(--s3); }
.ap-bsw { width: 216px; gap: var(--s3); color: var(--ink); font-weight: var(--w-med); }
.ap-bsw-n { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ap-chev { width: 14px; height: 14px; color: var(--ink-4); flex: none; }
.ap-pill b { color: var(--ink); font-weight: 550; }
.ap-ico { width: 15px; height: 15px; flex: none; opacity: .72; }
/* ⛔ `--well` WITH THE HAIRLINE, as `.thm` and `.usr-face` are in app/globals.css — the mock had
   `--plane` and no ring, which on the sheet is a bare glyph beside a bare letter (Pavel, 2026-09-12:
   "wrong style of these buttons"). The app's own comment records the same accident once already. */
.ap-round { width: 36px; justify-content: center; padding: 0; border-radius: var(--r-pill);
            background: var(--well); color: var(--ink-3); box-shadow: inset 0 0 0 1px var(--hair); }
.ap-round .ap-ico { opacity: 1; }
.ap-usr { font-size: .8125rem; font-weight: 600; color: var(--ink-3); }

/* the sheet: --page inset from --band, lifted — `.canvas` in the app */
/* ⚠️ `.canvas`'s own bottom: --s4 margin, --s9 padding, all four corners — inside the scroller the
   sheet's foot is reachable, so it is drawn as the app draws it. */
.ap-sheet { margin: 0 var(--s4) var(--s4); background: var(--page); border-radius: var(--r-xl);
            box-shadow: var(--lift); padding: var(--s7) var(--s6) var(--s9);
            display: grid; grid-template-columns: 196px minmax(0, 1fr); gap: var(--s9); }

/* the brand column — `.bnav` */
.ap-nav { display: flex; flex-direction: column; align-items: stretch; gap: var(--s1); }
.ap-nav-name { display: flex; align-items: center; gap: var(--s3); padding: 0 var(--s3) var(--s4);
               margin: 0; font-size: 1.0625rem; font-weight: var(--w-med); color: var(--ink); }
.ap-nav-item { display: block; padding: 9px var(--s3); border-radius: var(--r-sm);
               font-size: .875rem; color: var(--ink-3); }
.ap-nav-item.on { background: var(--plane); color: var(--ink); font-weight: var(--w-med); }
.ap-nav-cta { width: 100%; justify-content: center; margin-top: var(--s5); }

/* the report — Console.js, in its order */
/* ⚠️ NOT capped at 840 — that is `.col` on the account screens. The brand page's grid is
   `.canvas > .col:has(> .bgrid)`, capped at 1600px, so the report fills the sheet and Repeat /
   Export / Share sit at its right edge; the ROWS are what stay short (`.report-row`, 80%).
   ⚠️ An 840 cap here left a third of the sheet empty with the buttons floating mid-sheet
   (Pavel, 2026-09-11: "aligned by right side of screen always … but here we have empty space"). */
.ap-body { min-width: 0; }
.ap-meta { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s5); }
/* ⭐ Two lines, two facts (Pavel, 2026-09-12): the label line carries when · answers · market,
   the second line the engines — Console.js's masthead, same day. */
.ap-meta-l { position: relative; display: flex; align-items: center; gap: var(--s3); margin: 0; flex-wrap: wrap; } /* relative: the questions panel (.qs-pop) anchors to this line */
.ap-meta-l .ap-eng { gap: var(--s3); }
.ap-when { text-transform: none; letter-spacing: 0; }
.ap-dot { color: var(--ink-4); }
.ap-meta-e { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; margin-top: var(--s2); }
.ap-eng { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.ap-mk { width: 14px; height: 14px; flex: none; }
.ap-mk.e-ppx { width: 12px; }
.ap-flag { width: 18px; height: 12.5px; flex: none; }
.ap-acts { display: flex; gap: var(--s2); flex: none; }

/* ⚠️ A <p>, not an <h1>: the page's one h1 is the hero headline above it. The CLASS is the
   app's, and the cap (46ch) and the top margin are Console.js's own inline values. */
.ap-h1 { margin: var(--s7) 0 0; max-width: 46ch; }
.ap-acc { color: var(--accent-display); }
.ap-lede { margin-top: var(--s5); max-width: 56ch; }
.ap-lead { color: var(--ink); font-weight: 590; white-space: nowrap; display: inline-flex;
           align-items: center; gap: 6px; vertical-align: -3px; }

/* the three figures — `Figure` in Console.js: t-label, Newsreader 2.9rem at --w-accent, t-xs */
.ap-figs { display: grid; gap: var(--s6); grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
           margin-top: var(--s8); }
.ap-figs p { margin: 0; }
/* ⚠️ `Figure` in Console.js: tone "accent" → --accent-display for the Mentioned figure (the same blue
   as the headline's percentage), the other two --ink. ⛔ The colour is NOT set here — `.ap-acc`
   sets it on the one figure and the rest inherit ink; a `color` on this rule once overrode the
   blue and painted all three black (Pavel, 2026-09-11: "why this is black?"). */
.ap-fig { margin-top: var(--s3) !important; font-size: 2.9rem; font-family: var(--font-accent);
          font-weight: var(--w-accent); letter-spacing: -.01em; }
.ap-figs .t-xs { margin-top: var(--s2) !important; }

/* "Who the answers name" — `Standing` in Console.js */
/* ⚠️ `padding: 0` because this page's bare `section { padding: var(--s9) 0 }` would otherwise reach
   in — the app's `.sec` is margin only. */
.ap-sec { margin-top: var(--s8); padding: 0; }
.ap-sec .sec-head h4 { margin: 0; }
.ap-sec .sec-head p { margin: 0; }
.ap-row { padding: var(--s4) 0; }
.ap-row-h { display: flex; align-items: center; gap: var(--s3); margin-bottom: var(--s3); }
.ap-row-n { font-size: .9375rem; font-weight: 450; color: var(--ink-2); overflow: hidden;
            text-overflow: ellipsis; white-space: nowrap; }
.ap-row.you .ap-row-n { font-weight: 590; color: var(--ink); }
.ap-row-v { margin-left: auto; font-size: 1rem; color: var(--ink-2); }
.ap-row.you .ap-row-v { color: var(--accent-ink); }
.ap-row .meter i { background: var(--ink-4); }
.ap-row.you .meter i { background: var(--accent-ink); }
/* ⚠️ `.report-row` in the app: no row runs the whole column. */
.ap-row, .ap-rep { max-width: max(80%, 34ch); }

/* 20px favicon in a `.row` — the "Mentioned instead of you" list left the mock 2026-09-11; kept for the source rows */
.ap-fav-20 { width: 20px; height: 20px; }
.ap-lost-n { flex: 1; min-width: 0; font-size: .9375rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ap-tnum { font-variant-numeric: tabular-nums; }

/* "What the answers were built from" — the Source row: the reader's own site is --ok */
.ap-src.own .ap-row-n { color: var(--ok); font-weight: 590; }
.ap-src.own .meter i { background: var(--ok); }
.ap-more { margin-top: var(--s4); }

/* "Every question" — the qrow: a dot (won / named / neither), the question, who took it, the
   four marks, the count. ⚠️ Same flex subtlety the app records: the question is the ONLY item
   that can shrink, so it carries `min-width: 0`, and the lost list is capped at 220px. */
.ap-q { border-top: 1px solid var(--hair); }
.ap-qrow { display: flex; align-items: center; gap: var(--s4); padding: var(--s5) 0; }
.ap-qdot { width: 7px; height: 7px; border-radius: 999px; flex: none; background: var(--hair-2); }
.ap-qdot.ok { background: var(--ok); }
.ap-qdot.warn { background: var(--warn); }
.ap-qq { flex: 1 1 0%; min-width: 0; font-size: .9375rem; line-height: 1.5; color: var(--ink); }
.ap-qlost { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ap-qeng { display: flex; gap: var(--s1); flex: none; }
.ap-qeng .ap-mk { width: 16px; height: 16px; }
.ap-qn { flex: none; width: 30px; text-align: right; }
.ap-qof { color: var(--ink-4); }

/* the repeat card — `ScheduleCard` in Repeat.js on a `.plane` */
/* ⭐ THE CARD FITS ITS CONTENT (Pavel, 2026-09-11: "why does this section too wide? it should fit
   content"). It used to span the report column, so a three-line card carried a screen's width of
   empty plane to its right. ⚠️ `.rp-on-plane` in the app carries the same rule — the mock is the
   app, and a card that is full-width in one and fitted in the other is a lie in one of them. */
.ap-rp { width: fit-content; max-width: 100%; }
.ap-rp-h { margin: 0; }
.ap-rp-p { margin: var(--s3) 0 0; max-width: 62ch; }
.ap-rp-acts { margin-top: var(--s6); display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap; }
.ap-rp-acts .t-xs { flex: 1; min-width: 180px; margin: 0; }

/* ── the domain favicon ───────────────────────────────────────────────────────
   ⭐⭐ THE REAL MARKS, INLINED AS data: URIs. ⛔ NOT loaded from the live domains and NOT from a
   favicon service: that is a third-party request per icon on a marketing page, which hands every
   visitor's IP to six other companies and breaks the page offline. They were fetched ONCE, scaled
   to 64px and base64'd into the HTML — about 14kb for all six, and nothing leaves the visitor's
   browser. ⭐ Same standing as the four engine marks in the hero, which CLAUDE.md already requires
   be "REAL LOGOS, each from its own official source, NOT flat one-colour stand-ins": nominative
   use, naming the sites an answer cited.
   ⚠️ AN EARLIER PASS DREW LETTER TILES INSTEAD (a coloured square with the initial) to dodge the
   trademark question. Rejected on sight — "i don't see real icons" — and the engine-mark rule says
   the same thing, so the stand-in was never consistent with the page it sits on.
   ⛔ `.ap-fav` stays square: these are third-party artwork and must not be recoloured or
   cropped; the --plate well behind them is the app's own frame (see [[plate-vs-raised]]: a
   third party's logo needs a white ground in both themes). ⚠️ To refresh one, re-fetch and re-encode — do NOT hotlink it.
   ⭐ yourbrand.com keeps a letter tile on purpose: it is the READER's own site and has no logo.
   ⚠️ The data URIs live in index.html; this comment moved here from the old `.fav` rule on
   2026-09-11 when the hero mock became the app and `.fav` stopped matching anything. */
/* Drawn as the app draws it (`Favicon` in marks.js): 18px, a --plate well with a hairline, the
   artwork at 65%. "Your brand" has no site, so it gets the app's letter tile — a --well square
   with the initial — and not a coloured chip. */
.ap-fav { width: 18px; height: 18px; flex: none; border-radius: 5px; background: var(--plate);
          box-shadow: inset 0 0 0 1px var(--hair); object-fit: contain; padding: 3px; box-sizing: border-box; }
.ap-fav-you { display: inline-grid; place-items: center; background: var(--well); box-shadow: none;
              padding: 0; font-size: 9.4px; font-weight: 600; font-style: normal; color: var(--ink-4); }

/* ── what a check gives you ───────────────────────────────────────────────── */
/* ⭐⭐ THE NINE CARDS BETWEEN THE HERO AND THE PRICE.
   ⛔ NOT ONE COLOUR, RADIUS OR TYPE SIZE FOR THE CARD ITSELF IS SET HERE. The box is `.plane`
   (components.css) — the same object as the pack cards and the worked example — so `.ft` is
   layout and body copy only. ⚠️ This file's rule: no colours, fonts, radii or heading sizes;
   those are upstream. A card painted here would drift the day the system moves.
   ⚠️ SIX CARDS IN FOUR ROWS, and the ROW SHAPES are the grouping: two-up with mocks (the two
   claims a rival cannot match), two-up with mocks (what you set, what you get), one full-width
   mock (where the answers came from), one full-width strip with no mock at all (the account).
   ⛔ There are no sub-headings and there must not be — an eyebrow label over a heading is banned
   outright (Pavel, 2026-09-10) — so shape and order carry it. ⛔ Do not reorder to even out a
   column count, and ⛔ do not give the strip a mock to make the rows match.
   ⚠️ THIS COMMENT SAID "NINE CARDS… three across" AND THEN "six identical boxes" while seven were
   rendering two-up. It had been wrong through two rewrites; the count belongs in one place. */
/* ⛔ THE CARD IS `.raised` (components.css) — white, on this section's band. Nothing here paints
   it. ⚠️ Two per row, because each one now carries a mock rather than a second sentence. */
/* ⛔⛔ align-items: start — DO NOT REMOVE IT, AND DO NOT SET A ROW HEIGHT.
   ⚠️ A grid stretches every card to the tallest in its row, and with a FIXED-height crop the extra
   height fell BELOW the panel: measured 83px of bare plate under the engine mock and 80px under
   the questions mock, on cards whose panels were 229 and 214 tall in a 300 frame. ⭐ The panels
   are different heights because the things they show are different lengths — four engines is four
   rows and there is no fifth. Let the cards be different heights too; the alternative is either a
   hole or padding a mock out to a number. */
/* ⭐⭐ STRETCH, NOT START — changed 2026-09-10. ⚠️ `align-items: start` let every card be exactly
   as tall as its own copy, so a two-line sub-line pushed its picture 24px below its neighbour's
   (48px at 900px, where it wraps to three). The row read as two things that had slipped.
   ⭐ The pictures are the same size already; what was uneven was where the copy above them ended.
   Stretching the cards and pushing the mock to the BOTTOM aligns both edges of every picture in
   a row without touching the copy, and it holds however the sub-lines wrap. */
.ft-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s5); align-items: stretch; }
/* ⛔ Both mock wrappers take `margin-top: auto` at their own rules below — declared there and
   not here, because each already sets a margin and the later declaration would win. */
.ft { display: flex; flex-direction: column; }
/* ⚠️ margin-BOTTOM here, not on the mock: the mock now takes `margin-top: auto` to bottom-align
   across a row, and an auto top margin cannot also guarantee a minimum gap. The copy owns it. */
.ft > p { margin: var(--s3) 0 var(--s5); font-size: .9375rem; line-height: 1.6; color: var(--fg-2); }
/* ⭐ The one card that spans the row. Seven buckets and their legend need the full width, and the
   break stops a column of identical boxes reading as a spreadsheet. ⚠️ Below 860 the grid is one column
   and this rule is a no-op, which is why it needs no counterpart in the media query. */
.ft.wide { grid-column: 1 / -1; }
.ft.wide > p { max-width: 74ch; }
/* ⭐⭐ THE ODD CARD OUT, AND THIS RULE EXISTS SO NO CARD EVER CHANGES SIZE TO SATISFY THE GRID
   AGAIN. Two columns means an odd number of two-up cards leaves a visible empty half, and the
   answer to that had been widening whichever card was nearest — which is how the account card ended
   up full-width without anyone asking for it (Pavel: "why did you make it full size, i haven't
   asked about it"). A lone LAST card now spans the row and centres itself at one column's width, so
   it stays the same size as its neighbours and the row reads as a deliberate closing beat.
   ⚠️ `:nth-child(odd)` on the LAST child is the test for "the count is odd"; below 860 the grid is
   one column and this is a no-op. */
.ft-grid > .ft:last-child:nth-child(odd):not(.wide) {
  grid-column: 1 / -1; justify-self: center; width: calc(50% - var(--s5) / 2);
}
/* 🪤 AND IT HAS TO BE UNDONE BELOW THE COLLAPSE. At one column `50%` is half of the ONLY column, so
   the rule above made the last card half-width on a phone — measured at 390 / 560 / 860 before this
   line existed. ⛔ Any rule written in per-cent of the grid needs a counterpart here. */
@media (max-width: 860px) {
  .ft-grid > .ft:last-child:nth-child(odd):not(.wide) { width: auto; justify-self: stretch; }
}

/* ⭐ THE WAY OUT. ⛔ Not a new destination — #start is the anchor the pricing CTA already uses. */
.ft-out { display: flex; justify-content: center; margin-top: var(--s7); }

/* ⭐ The card clips its tile. ⚠️ ~30 rules for the HTML "vignette" mocks that preceded the SVG
   tiles were deleted here on 2026-09-11 — none matched anything. Two of their lessons still
   apply to the tiles: ⛔ NOT ROTATED (Pavel, 2026-09-08) — no `perspective`/`rotate*` as a
   depth tweak, the tilt forced a horizontal bleed that kept slicing figures off; and ⛔ never
   give an SVG-facing class an unscoped `width`/`height`/`x`/`y`/`r` (see the note below). */
.ft { overflow: hidden; }
/* ⛔⛔ THREE RULES WERE DELETED HERE AND THEY HAD BEEN BREAKING EVERY TILE. The HTML crops styled
   their share bars with an UNSCOPED `.vg-bar { width: 30%; height: 8px }`. `width` and `height` are
   real CSS geometry properties for an SVG <rect>, and CSS beats a presentation attribute — so the
   tiles' ground bars, authored as 24 × 104 and described in their own comments as a field of
   vertical bars, were rendering as identical 168 × 8 horizontal streaks. Measured, not guessed:
   attrWidth 24 / cssWidth 168px / bbox 168 × 8.
   ⚠️ It was invisible because the field sits behind a 52px blur at .18 opacity and nobody had
   reason to compare it against the numbers in the markup. ⛔ Never give an SVG-facing class an
   unscoped `width`, `height`, `x`, `y` or `r` rule meant for HTML. */

/* ── the questions people ask — three drifting rows ──────────────────────────
   ⭐ See the section comment in index.html. The rows are transform-only marquees: each `.qm-row` is
   two copies of one `.qm-track`, and the keyframe slides the row by one track's width, so the
   second copy arrives exactly where the first started and the loop has no seam. Rows 1 and 3 drift
   left, row 2 right (the same keyframe, reversed). ~70s a loop — slow enough to read a chip as it
   passes. Hover pauses it. ⛔ No `filter`, no layout property, no per-chip animation. */
/* ⚠️ 1000px, not .sec-h-wide's 800: the headline is two sentences and wants two lines, one each. */
.sec-h-center { max-width: 1000px; margin-left: auto; margin-right: auto; text-align: center; }
/* ⭐⭐ THE QUESTIONS SIT ON A MESH LIKE CALENDLY'S "Real customers. Real results." (Pavel,
   2026-09-11, with their screenshot): a saturated blue at the top left falling through violet to
   pink and lavender at the foot, in an inset panel with the hero's corners, the headline in white.
   The chips stay white on it — they are the app's rows and read like cards on the field.
   ⚠️ Same construction as `.stage`'s mesh: radial blobs over a linear base, no `filter: blur()`.
   ⚠️ Inset like `.hero` and `.foot` (margin 0 --s5), so the three coloured panels on the page share
   one edge. */
#asked { color: #FFFFFF; min-height: 92vh; display: flex; flex-direction: column; justify-content: center;
  /* ⭐ FULL-BLEED, NOT A PANEL (Pavel, 2026-09-11: "this should be full-size gradient like calendly").
     Their "Real customers" section runs edge to edge and viewport-tall: saturated blue across the
     top, lavender and pink through the middle, a warm cream at the lower right, and the whole
     thing DISSOLVES INTO THE PAGE at the foot — so the section below starts on plain ground with no
     edge. The last stop is --page for exactly that reason; ⛔ do not put a radius or a margin back. */
  background:
    radial-gradient(48% 46% at 6% 62%, rgba(236, 178, 240, .85), transparent 70%),
    radial-gradient(40% 40% at 96% 58%, rgba(216, 168, 236, .80), transparent 70%),
    radial-gradient(36% 34% at 88% 84%, rgba(250, 232, 200, .90), transparent 70%),
    radial-gradient(52% 40% at 50%  4%, rgba( 62, 112, 232, .90), transparent 70%),
    linear-gradient(180deg, #4A78E8 0%, #6E82E0 30%, #B49AE6 58%, #E9C9E8 80%, var(--page) 100%); }
#asked .sec-h h2 { color: #FFFFFF; }
.qm-out { display: flex; justify-content: center; margin-top: var(--s8); }
@media (max-width: 860px) { #asked { min-height: 0; } }
.qm-br { display: inline; }
.qm { display: grid; gap: var(--s4); overflow: hidden;
      /* the fade at both edges — a mask, so the chips genuinely run off rather than under a plate */
      -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
              mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent); }
.qm-row { display: flex; width: max-content; animation: qm-drift 70s linear infinite; animation-delay: -12s; }
/* ⚠️ Negative delays so the three rows do not load flush-left like a grid — each starts a third of
   the way into its own loop. (A negative delay seeks FORWARD; see the tiles' notes.) */
.qm-row:nth-child(2) { animation-direction: reverse; animation-duration: 84s; animation-delay: -31s; }
.qm-row:nth-child(3) { animation-duration: 76s; animation-delay: -52s; }
.qm:hover .qm-row { animation-play-state: paused; }
.qm-track { display: flex; gap: var(--s3); flex: none; padding-right: var(--s3); }
@keyframes qm-drift { to { transform: translateX(-50%); } }
/* a chip: the app's row voice — white, a hairline, the mark quieter than the words beside it */
.qm-chip { display: inline-flex; align-items: center; gap: 10px; white-space: nowrap;
           padding: 14px 18px; border-radius: var(--r); background: var(--page);
           box-shadow: inset 0 0 0 1px var(--hair); font-size: 1rem; color: var(--fg); }
.qm-chip .ap-mk { width: 18px; height: 18px; flex: none; }
.qm-chip .e-ppx { width: 15px; }
@media (prefers-reduced-motion: reduce) { .qm-row { animation: none; } }

/* ── what a credit is ─────────────────────────────────────────────────────── */
/* ⭐ THE DEFINING LINE, AS A LABEL ABOVE THE LADDER. ⛔ The pill is the design system's
   (.chip.chip-acc, components.css) — nothing here restyles it, only places it. */
/* ⭐ THE UNIT LABEL RIDES THE HEADLINE'S BASELINE. `align-items: baseline` on the flex row aligns
   the chip's own first baseline with the <h2>'s FIRST line, so it stays on the title's line even
   when the title wraps — which `flex-start` would not do, because the two boxes have different
   leading. ⛔ Do not swap it for `center`: at two lines of headline the chip drifts to the middle.
   ⚠️ `.sec-h` is shared with the features and FAQ heads; everything here hangs off the
   `.sec-h-aside` modifier so those two do not move. */
.price-key { margin: 0; }
.sec-h-aside { max-width: none; display: flex; align-items: baseline;
               justify-content: space-between; gap: var(--s4) var(--s6); }
.sec-h-aside .sec-h-main { max-width: 660px; }
.sec-h-aside .price-key { flex: none; }
/* 🪤 SIDE BY SIDE ONLY WHILE THERE IS ROOM. Below the collapse the headline already uses the whole
   width, so the chip stacks under it and takes back the gap the flex row was providing. */
@media (max-width: 860px) {
  .sec-h-aside { display: block; }
  .sec-h-aside .price-key { margin-top: var(--s5); }
}

/* ⭐⭐ THE TWO PANELS UNDER THE CARDS, SIDE BY SIDE — the worked example and the volume quote.
   ⭐ EQUAL HEIGHTS via the grid's default stretch, and the EXAMPLE is what was cut down to fit
   (see `.key-line`): the quote panel was already the right size, so stretching IT to match was
   backwards. ⚠️ They stack below 860px, where the quote's own row breaks into a column. */
.pk-row { margin-top: var(--s4); display: grid; grid-template-columns: 1fr 1fr; gap: var(--s4); }
/* ⛔ The panel carries its own top margin for when it stands alone; inside the row the grid owns
   the spacing. Overridden here rather than removed there — the markup is GENERATED. */
.pk-row .pk-quote { margin-top: 0; }

/* ⭐⭐ THE LINK TO THE PRICE LIST — 2026-09-14. Every pack is a page of its own (/pricing) because
   the AI engines this product checks read a page's words and nothing else: not the JSON-LD offers,
   and not a closed <details> either — that was tried here the same day and ChatGPT reported "the
   page source only exposes the calculator's default state". A plain link is what a crawler follows
   and a person clicks, and it carries no figure, so the $85 ceiling above it is untouched.
   ⛔ Not a button — it is a footnote to the row, not a fourth thing to buy. */
.pk-more { margin-top: var(--s5); font-size: .9375rem; font-weight: var(--w-med); letter-spacing: -.01em; }
.pk-more a { color: var(--accent-ink); text-decoration: none; }
.pk-more a:hover { text-decoration: underline; }

/* ⭐ THE WORKED EXAMPLE. ⛔ QUIET (Pavel, 2026-09-07: "less screaming"). It was an accent-wash
   panel with accent hairlines between every row — the loudest object on the page, for a footnote.
   ⭐ It is now a panel in the CARDS' language, the same one `.pk-quote` speaks: `--plane`, the
   large radius, the same padding. The two read as a pair because they are literally the same box.
   ⚠️ WAS `.lev-key`, beside five `.lev` lever cards in a section of its own. That section is gone
   and the cards with it — the sum states three of those choices in the act of using them. */
.key { background: var(--plane); border-radius: var(--r-lg); padding: var(--s6);
       font-size: .9375rem; line-height: 1.6; color: var(--fg-2); }
.key b { color: var(--fg); font-weight: var(--w-semi); }
/* ⛔ Not an <h3>: the section already has one heading, and this names a panel rather than opening
   a new part of the page. */
.key-t { margin-bottom: var(--s4); font-size: 1.0625rem; font-weight: var(--w-semi);
         letter-spacing: -.024em; color: var(--fg); }

/* ⭐ THE SUM, ON ONE LINE. ⛔ It was three rows and a rule, which is what made this panel nearly
   twice the height of the quote panel beside it. ⭐ The `×` are the operators those rows only
   implied, so one line now carries more of the arithmetic than four did.
   ⚠️ line-height 1.2 (Pavel, 2026-09-13: "make line height 1.20 instead of 2 here") — it was 2,
   argued as room for the mark cluster on a wrapped line; the rows are spaced by their own margin
   now, and a wrap on a narrow screen is checked by eye rather than padded for. */
/* ⭐ TWO EXAMPLES ARE A LIST, so the bullet is the list's own and not a drawn decoration — the
   marker is what a screen reader counts and what the eye pairs the two lines by. */
.key-ex { list-style: none; margin: 0; padding: 0; }
/* ⚠️ The dot is ABSOLUTE with a hanging indent, so a line that wraps on a narrow screen sits under
   its own text rather than back under its bullet. */
.key-line { position: relative; margin-top: var(--s3); padding-left: var(--s4); line-height: 1.2; }
/* ⭐ --accent, not --accent-ink: this is a MARK, not prose, so it is not held to the 3:1 text
   floor tokens.css sets for anything readable (rule 4). ⚠️ 3.5px is half the dot; `.6em` is half a
   line at line-height 1.2, which is what centres it on the FIRST line and not on the block. */
.key-line::before {
  content: ""; position: absolute; left: 0; top: calc(.6em - 3.5px);
  width: 7px; height: 7px; border-radius: var(--r-pill); background: var(--accent);
}
.key-line b { color: var(--fg); font-weight: var(--w-semi); font-variant-numeric: tabular-nums; }
/* ⭐ The operator, quiet and not italic — it is a multiplication sign, not an emphasis. */
.key-line i { font-style: normal; color: var(--fg-3); padding: 0 1px; }
/* ⚠️ The cluster is em-sized in components.css, so this one declaration is the whole of its
   sizing here. ⛔ Do not restate width or plate colour — rule 6 of that component. */
/* ⚠️ 1.25em since the cluster grew to FIVE plates (2026-09-11): at 1.5em the third line — 25 × 6
   with five marks and a three-digit total — wrapped its own answer onto a second line at 1440.
   MEASURED: the line needs ~490px of the panel's 480 at 1.35em; 1.25em and 1px operator padding
   bring it under. */
.key-line .engines { font-size: 1.25em; }
/* ⛔ The total never splits: "150" on one line and "credits" on the next is a sum with no answer. */
.key-line .tot { white-space: nowrap; }
/* ⛔⛔ THE ONE THING THIS PAGE OVERRIDES ON THAT COMPONENT, AND IT IS SCOPED TO THIS LINE (Pavel,
   2026-09-07). `components.css` tilts each plate by a different odd angle and says the unevenness
   is deliberate — "regularise any one of them and the row snaps back to a stock logo strip". ⭐ That
   is right where it lives: a HEADLINE, where the cluster is a flourish. ⚠️ Here the marks are a
   TERM IN AN EQUATION, sitting between "4 places" and "×", and tilted glyphs in a sum read as
   misprinted rather than as hand-placed.
   ⛔ SCOPED TO `.key-line` ON PURPOSE — the hero's cluster is untouched and must stay tilted.
   ⚠️ `none` also drops each plate's translateY nudge, which is the same declaration; that nudge
   exists to seat a TILTED plate and has nothing to seat once the tilt is gone. */
/* ⭐ ONE SIZE AND ONE CORNER FOR ALL FOUR (Pavel, 2026-09-07), for the same reason as the tilt
   above: `components.css` varies width (.86 / .78 / .84 / .80em) and radius (31 / 34 / 29 / 33%)
   so the row reads hand-placed in a HEADLINE. ⚠️ In an equation the same variation reads as four
   marks of different importance — and they are not: this line's whole claim is that the four are
   interchangeable, so a row where one is visibly bigger argues against the sentence it is in.
   ⚠️ .82em is the MEAN of the four widths, so the cluster keeps the mass it had; 31% is p1's
   corner, the one the row already opens on. ⛔ Scoped to `.key-line` — the hero keeps all of it. */
/* ⚠️ The calculator's checkboxes get the SAME two overrides for the same reason: a form row is a
   list of equals, and a tilted plate that is bigger than the one under it reads as a state, not a
   style. ⛔ Still scoped — the hero keeps every bit of its irregularity. */
.key-line .engines .plate {
  transform: none;
  width: .82em; height: .82em; border-radius: 31%;
}
/* ⚠️ Sizing only — the cluster is em-based in components.css, so this is the whole of it. */
/* ⭐ THE ANSWER, ON THE END OF THE SUM RATHER THAN UNDER IT. ⛔ It had its own line, a rule and
   the space either side of that rule — ~46px, on a panel whose whole budget is 159. ⭐ An equals
   sign is what marks a total; it does not also need a line of its own to sit on.
   ⛔ NOT the accent serif and NOT larger: design/tokens.css allows that face in two places and
   both are REAL money (the pack price, the quote output). This is a worked example. */
.key-line .eq { color: var(--fg-3); padding: 0 4px; }
.key-line .tot { color: var(--fg); font-weight: var(--w-semi); }

/* ── pricing: five cards ──────────────────────────────────────────────────── */
/* ⭐⭐ THE PACK CARDS **AND** THE QUOTE PANEL MOVED TO design/components.css ON 2026-09-08.
   They are on BOTH surfaces now — the app's Billing screen sells the same eleven rungs out of the
   same `lib/packs.js` — and `design/` is where a component on both surfaces lives.
   ⛔ Do not re-add `.pk`, `.save` or any `.pk-quote-*` rule here. This file held one copy, the app
   held another, and the app's drifted from this one three times in a single sitting.
   ⚠️ The canonical block is THIS branch's version, aliases resolved: design/ cannot use `--fg-3`
   or `--accent-bg`, so it names --ink-3 and --accent-wash, which those alias to anyway.
   ⭐ What stays here is LAYOUT this page alone has — `.pk-row`, and the narrow overrides below. */

/* ── faq ──────────────────────────────────────────────────────────────────── */
/* ⭐⭐ THE RULES RUN THE FULL COLUMN, THE PROSE DOES NOT — changed 2026-09-10.
   ⛔ This was `max-width: 820px` on the whole block, which is a reading measure applied to the
   wrong box. The answers need one; the hairlines and the +/- do not, and capping all three
   left ~380px of dead page to the right of every rule at 1200px — the only section that did
   not reach its own gutter, directly under three that do.
   ⚠️ The heading STAYS ABOVE, which is why this is the fix and a heading-rail was not: every
   other section on this page introduces itself from above, and moving one into a left column
   buys the same width back by making this section the odd one out. */
/* ⭐ TWO COLUMNS, LIKE REDWATCHER (Pavel, 2026-09-11) — `.faq` is a grid, each `.faq-col` carries
   its own top rule, and the marker is Redwatcher's chevron rather than the +/– this had before.
   ⚠️ One column under 860, where the second column's top rule would double the first's bottom. */
.faq { display: grid; grid-template-columns: 1fr 1fr; gap: 0 var(--s8); align-items: start; }
.faq-col { min-width: 0; border-top: 1px solid var(--line); }
.faq details { border-bottom: 1px solid var(--line); }
.faq summary { list-style: none; cursor: pointer; position: relative; padding: var(--s5) 40px var(--s5) 0; font-size: 1rem; font-weight: var(--w-med); letter-spacing: -.02em; }
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: ""; position: absolute; right: 6px; top: 50%; width: 7px; height: 7px;
                      border-right: 1.5px solid var(--fg-3); border-bottom: 1.5px solid var(--fg-3);
                      translate: 0 -70%; rotate: 45deg; transition: rotate .18s; }
.faq details[open] summary::after { rotate: -135deg; translate: 0 -20%; }
@media (max-width: 860px) {
  .faq { grid-template-columns: 1fr; }
  .faq-col + .faq-col { border-top: none; }
}
/* ⚠️ THE CAP CARRIES THE PADDING, because `box-sizing: border-box` is global (tokens.css) and
   would otherwise eat 44px of the measure. The 44px is the gutter that keeps prose off the +/-,
   and it is still needed now the row runs the full column.
   ⛔ `ch` IS NOT A CHARACTER HERE — MEASURED, because the obvious number is wrong by half. `ch` is
   the advance of "0", and in this geometric sans that is ~9.9px against a ~6.6px average glyph:
   `68ch` renders 103 characters a line, not 68. The old `820px` block cap was ~118.
   ⚠️ 56ch = ~88 characters, and that is a COMPROMISE, not the textbook measure. 46ch lands the
   canonical 67 and was rejected on sight: a 440px block of prose under a 1136px hairline reads
   lopsided, and these answers are two to four lines, where a long measure costs least. ⭐ If the
   answers ever grow into real paragraphs, 46ch is the number to come back to. */
.faq .ans { max-width: calc(56ch + 44px); padding: 0 44px var(--s5) 0; font-size: .9375rem; color: var(--fg-2); line-height: 1.6; margin-top: -4px; }

/* ── close ────────────────────────────────────────────────────────────────── */
/* ⭐ The band: the page changes colour, it does not grow another box. */
.close { text-align: center; background: var(--band); }
/* ⭐ The closing headline — the second and last place the accent serif appears. */
/* ⭐ Two sentences of near-equal length (24 and 20 chars), so `balance` puts one on each line
   and the max-width barely matters — MEASURED identical from 18ch to 28ch. ⚠️ That tolerance
   belongs to THIS copy: the previous line was one long sentence and needed exactly 22ch to
   avoid stranding its last word. Re-measure if the headline stops being two short sentences. */
.close h2 { font-family: var(--font-accent); font-weight: var(--w-accent); letter-spacing: -.02em; max-width: 22ch; margin: 0 auto; text-wrap: balance; }
.close .checkbar { margin: var(--s6) auto 0; }
.close .hero-note { justify-content: center; }

/* ── footer: a navy block, inset like the hero ───────────────────────────────
   ⭐ calendly.com's footer (Pavel, 2026-09-11): the page's darkest ground, inset by the same margin
   as the hero panel so the top and the bottom of the page rhyme, a display-size tagline on the
   left, the link columns on the right. ⚠️ Ink INSIDE it is literal white at three strengths —
   the tokens' --ink is navy and would vanish — same reasoning as the permanently-white cards in
   the tiles. ⛔ The old footer's rule that it "stays on the page ground" was about a grey tint
   under the grey .close band; a navy block under it is contrast, not repetition. */
.foot { margin: 0 var(--s5) var(--s5); background: var(--ink); color: #FFFFFF;
        border-radius: var(--r-xl); padding: var(--s9) var(--s8) var(--s7); }
.foot-in { max-width: 1136px; margin: 0 auto; }
.f-top { display: grid; grid-template-columns: 1.2fr 1fr; gap: var(--s8); align-items: stretch; }
/* the brand cell: the tagline at the top, the lockup at the foot, level with the columns' end */
.f-brand { display: flex; flex-direction: column; }
.f-home { display: inline-block; margin-top: auto; padding-top: var(--s8); width: fit-content; }
/* ⭐ 48px tall — calendly.com's footer lockup measured at ~48 css px. `color` paints the wordmark. */
.f-logo { display: block; height: 48px; width: auto; color: #FFFFFF; }
.f-tag { margin: 0; font-family: var(--font-display); font-weight: var(--w-display);
         font-size: clamp(2.2rem, 4.4vw, 3.5rem); line-height: 1.06; letter-spacing: -.03em;
         text-wrap: balance; }
.f-cols { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s6); }
.f-col { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s3); }
/* ⚠️ The heading is the LOUDER thing here, as in the reference — white, sentence case, a size up —
   and the links beneath it sit at 72% white. The old footer's uppercase micro-label is gone. */
.f-h { font-size: 1.125rem; font-weight: var(--w-med); color: #FFFFFF; margin-bottom: var(--s2); }
.f-col a { font-size: .9375rem; color: rgba(255, 255, 255, .72); transition: color var(--t-fast) ease; }
.f-col a:hover { color: #FFFFFF; }
.f-mail { display: inline-block; }
.f-bot { display: flex; align-items: baseline; gap: var(--s3) var(--s5); flex-wrap: wrap;
  margin-top: var(--s9); padding-top: var(--s5); border-top: 1px solid rgba(255, 255, 255, .14);
  font-size: .75rem; color: rgba(255, 255, 255, .56); }
.f-tm { margin-left: auto; max-width: 62ch; }

@media (max-width: 1080px) {
  /* ⚠️ The mock's shell narrows the way the app's does: the brand column stays (it is 196px of a
     ~1400px virtual width at this zoom) but the top bar's right-hand controls go, so the lockup
     and the brand switcher keep their room. */
  .ap-top-r .ap-pill { display: none; }
  /* ⚠️ 2, not 3 — four cards break 2+2. Three would leave one card alone on the second row.
     ⚠️ The row was briefly THREE cards on 2026-09-09 and this rule came out for it; the reprice
     the same day put the count back to four, and so this back. */
  .pk-grid { grid-template-columns: repeat(2, 1fr); }
  /* ⛔ .ft-grid stays at TWO here — six cards break 2+2+2 at any width down to the 860 collapse,
     and a mock in a third-width card is too small to read. */
}
@media (max-width: 860px) {
  :root { --gutter: var(--s4); }
  section { padding: var(--s8) 0; }
  .hero { margin: 0 var(--s3); padding: var(--s7) 0 0; }
  .stage { padding: var(--s5) var(--s4) 0; margin-top: var(--s7); max-width: calc((100% - 2 * var(--s4)) * (1 - var(--p)) + 100% * var(--p)); }
  .pk-grid { grid-template-columns: 1fr; }
  .pk-row { grid-template-columns: 1fr; }
  /* ⚠️ Stacked, and the answer stays left-aligned with the question rather than stranded. */
  .pk-quote-live { display: block; }
  .pk-quote-out { margin-top: var(--s5); text-align: left; }
  .pk-quote-end { display: block; }
  .pk-quote-buy { margin-top: var(--s4); width: 100%; }
  /* ⭐ THE MOCK ON A PHONE IS THE APP ON A PHONE. app/globals.css drops the sticky column and lays
     `.bnav` out as a row at this width; here the column goes entirely — the report is the picture,
     and a 390px stage has no room to spend on navigation. The zoom steps down so the display
     headline (clamp's 2.6rem floor) still fits the sheet, and the meta line's action buttons go
     the way `.rpt-head .share` used to. ⚠️ Measured at 390: re-measure if the zoom changes. */
  .ap { zoom: var(--z, .62); }
  .stage { --ap-vw: 487; }   /* 302 / .62 — the frame at 390px, laid out at the app's phone width */
  /* ⛔ NO SCROLL-LINKED GROWTH HERE — stage.js pins --p at 1 below 860px and on any touch screen; a
     zoom change per scroll frame re-lays-out the whole report and an iPhone stutters through it
     (Pavel, 2026-09-11). The phone gets the finished width from the first paint. */
  /* ⚠️ 16:10 at 334px is 209px tall — the headline alone. A phone's frame is taller than wide. */
  .stage-in { aspect-ratio: 4 / 5; }
  .ap-sheet { grid-template-columns: minmax(0, 1fr); gap: 0; padding: var(--s5) var(--s4) var(--s7); margin: 0 var(--s3) var(--s3); }
  /* ⭐ THE NAV STAYS ON A PHONE NOW THAT IT WORKS (2026-09-12) — as the app lays `.bnav` out at this
     width (app/globals.css @860): the name on its own line, the three items in a row, the CTA
     beside them. It was `display: none` while it was decoration. The action buttons still go. */
  .ap-acts, .ap-top-r { display: none; }
  .ap-nav { flex-direction: row; align-items: center; flex-wrap: wrap; gap: var(--s2); margin-bottom: var(--s6); }
  .ap-nav-name { width: 100%; padding: 0 0 var(--s2); }
  .ap-nav-cta { width: auto; margin-top: 0; }
  /* ⚠️ `button.ap-nav-item { width: 100% }` is declared at the END of this file (the views block),
     so the phone override lives there too — see `.ap-nav .ap-nav-item` below it. */
  .ap-top { padding: var(--s3) var(--s4); }
  .qm-br { display: none; }
  .qm-chip { padding: 11px 14px; font-size: .9375rem; }
  /* the app's narrow qrow: the question takes the line, the rest wraps under it */
  .ap-row, .ap-rep { max-width: none; }   /* the app's `.report-row` below 720px */
  .ap-qrow { flex-wrap: wrap; row-gap: var(--s2); }
  .ap-qq { flex: 1 0 calc(100% - 7px - var(--s4)); }
  .ap-qlost { max-width: none; min-width: 0; flex: 1 1 auto; margin-left: calc(7px + var(--s4)); }
  .ap-qeng { margin-left: auto; }
  .ft-grid { grid-template-columns: 1fr; }
  .nav-mid { display: none; }
  /* the bar's 47px inset is a laptop's; a phone gets the page's own --s3 margin and a 60px bar */
  .nav-in { margin: 0 var(--s3); padding: 0 var(--s3); height: 60px; }
  .nav.is-scrolled .nav-card { margin: 0 var(--s3); }
  .f-logo { height: 40px; }
  /* ⚠️ 2×2 — brand + Product on the first row, Account + Legal on the second. Three columns
     beside the brand block at this width leaves each too narrow for "Best-value pack" length
     labels, and four is what we are collapsing away from. */
  .f-top { grid-template-columns: 1fr; gap: var(--s7); }
}
/* ⛔⛔ ON A TOUCH SCREEN THE FRAME DOES NOT SCROLL — it is a CROP (Pavel, 2026-09-13: "whenever i
   scroll to the end of mockup, i'm stuck, and the actual page doesn't scroll further"). iOS hands a
   swipe to the innermost scroll container under the finger, and the frame is most of a phone's
   viewport; once the report inside is at its end the page should take over, and on the zoomed
   report it does not — the frame keeps claiming the gesture and nothing moves. Desktop already had
   the wheel version of this and lost `overscroll-behavior: contain` for it (`.stage-in` above); a
   touch screen needs the stronger cut, because there is no chaining to fix, only a container to
   stop being one. ⚠️ `overflow: hidden` is not a scroll container for touch, so a swipe that starts
   on the picture scrolls the page. The nav inside still works (it swaps views, it does not scroll).
   ⚠️ `pointer: coarse`, not a width — an iPad is wide and has the same finger. Same key stage.js
   uses to pin --p on touch. */
/* ⭐ THE CIRCLES — "See full" on the frame's bottom-right corner, "Close" top-right once it is
   open (Pavel, 2026-09-13). ⚠️ SOLID, NOT GLASS — Pavel, same day, off the phone: "make button not
   glass, but rather #4a96ee with white text". That is `--accent-press`, the accent's pressed
   shade, so it is a token and not a typed colour. ⛔ Not the CTA green and not a pill: it is a
   control on a picture, not a step on the page. The class name kept its first word so the two
   buttons and stage.js need no rename for a colour change. */
.glass-round { display: none; position: absolute; z-index: 5; width: 64px; height: 64px; border-radius: 50%;
  align-items: center; justify-content: center; text-align: center; padding: 0; cursor: pointer;
  font: var(--w-semi) .75rem/1.15 var(--font-sans); letter-spacing: -.01em; color: #FFFFFF;
  background: var(--accent-press); border: 0; box-shadow: 0 6px 20px rgba(11, 27, 54, .22); }
.glass-round:active { transform: scale(.96); }
.glass-round[hidden] { display: none; }
/* ⚠️ Anchored to the STAGE, whose bottom edge is the frame's — the frame is flush with it. The
   right offset is the stage's own side padding plus a 12px inset into the picture. */
.stage-full { right: calc(var(--s4) + 12px); bottom: 12px; width: 56px; height: 56px; font-size: .6875rem; }
/* ⭐⭐ THE FULL VIEW: the same frame, fixed to the viewport. Same node, so `--z` (set on `.stage`)
   still reaches it and stage.js's fit() re-zooms the app to the viewport's width; same click
   listener, so the nav works. ⛔ `overflow-y: auto` HERE is safe on touch: the frame is the whole
   screen, so there is no page behind it to trap. `html.stage-lock` freezes the page underneath. */
.stage-in.is-full { position: fixed; inset: 0; z-index: 90; max-width: none; margin: 0; aspect-ratio: auto;
  height: 100dvh; border-radius: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; box-shadow: none; }
html.stage-lock { overflow: hidden; }
.stage-close { position: fixed; top: max(12px, env(safe-area-inset-top)); right: 12px; z-index: 91; }
.stage-close:not([hidden]) { display: flex; }
@media (pointer: coarse) {
  .stage-in { overflow: hidden; }
  /* ⚠️ After `.glass-round`'s display:none in source order, or it never shows. */
  .stage-full:not([hidden]) { display: flex; }
}
/* ⚠️ THE FOOTER'S OWN BREAK, and it is not one of the hero's. 560px is where the brand
   paragraph and a link column stop coexisting: below it the 38ch paragraph is the full measure
   and a second column has nothing left.
   ⭐ THE THREE LINK COLUMNS STAY IN A ROW — they do NOT stack the way Redwatcher's do, and that
   is the one place this footer departs from it. MEASURED at 375px: a full stack is 750px of
   footer, three-up is ~430px. Every label here is one short word ("Questions" is the longest,
   ~62px at .8125rem) against a ~80px column even at 320px, so the row costs nothing to read and
   saves a phone-screen of scrolling. ⛔ Re-measure before adding a longer label to any column. */
@media (max-width: 560px) {
  .foot { margin: 0 var(--s3) var(--s3); padding: var(--s8) var(--s5) var(--s6); }
  .f-bot { margin-top: var(--s7); }
  .f-cols { gap: var(--s5) var(--s4); }
  .f-tm { margin-left: 0; }
}
/* ⭐ THE MOBILE SPLIT, STATED RATHER THAN MEASURED. Below 544px the lede runs to three lines
   and the natural wrap strands "instead." alone on the last one. No max-width produces the
   wanted split — line one has to fit "…people ask" but not "…ask for", while line two has to
   fit "…your brand," but not "…brand, or", and in proportional type those two windows do not
   overlap. `text-wrap: balance` evens the lines but breaks mid-phrase. So the breaks are in
   the markup and switched on here:
       See who AI mentions when people ask
       for recommendations — your brand,
       or your competitors instead.
   ⛔ OFF above 544px, where the sentence fits on two lines and the 44ch rule above governs.
   544px is MEASURED — 545px still fits on two lines, 543px does not. */
/* ⭐ THE PHONE HEADLINE IS TWO LINES (Pavel, 2026-09-11, from his phone: "make main title in hero fit
   2 or 3 lines on mobile instead of four; maybe decrease font size a bit"). It had been three lines
   of text plus the four marks on a fourth. Under 421px the size drops to ~8.8vw (31–37px) and the
   break is stated in the markup — `Check your presence` / `in AI search` — with the marks trailing
   the second line as they do on desktop. MEASURED at 360/390/421: two lines, no overflow.
   ⛔ OFF above 421px, where the sentence fits on two lines at the clamp's floor and the marks trail. */
.hero h1 .br-h { display: none; }
@media (max-width: 421px) {
  .hero h1 { font-size: clamp(1.9rem, 8.8vw, 2.35rem); max-width: none; }
  .hero h1 .br-h { display: inline; }
}
.hero .lede .br-m { display: none; }
@media (min-width: 368px) and (max-width: 544px) {
  .hero .lede .br-m { display: inline; }
}
/* ⚠️ Below 368px line one ("…when people ask") no longer fits and "ask" drops alone, which is
   worse than the widow this was fixing. So the stated breaks switch OFF and `balance` evens
   the natural wrap instead. 368px is MEASURED — it fits at 368, not at 367. A 360px Android
   lands here; a 375px iPhone SE gets the stated split. */
@media (max-width: 367px) {
  .hero .lede { text-wrap: balance; }
}

/* ⭐ THE BAR IS MADE TO FIT rather than emptied. MEASURED at the old sizes on a 390px iPhone:
   brand 170 + gap 24 + "Sign in" 68 + gap 12 + CTA 89 = 363px of content against 358px of room,
   so the link — the one flex item that could — shrank and wrapped to two lines. It read as a
   broken header on the most common phone there is.
   ⚠️ WHY THE OLD 370px RULE MISSED IT: it was measured by watching for OVERFLOW, and a flex row
   never overflows while an item can still shrink. The bar "fit" to 364px because "Sign in" was
   folding, not because it was whole. ⛔ Measure this bar by the LINK'S HEIGHT, not scrollWidth.
   ⭐ The three savings are all in the chrome, none in the two labels: a 22px lockup (144px wide,
   the same artwork), the nav's own gap down one step, and the link's chip padding down one step.
   Content is then 317px, which fits from 349px up — so a 360px Android keeps "Sign in" too.
   ⚠️ Below that the bar is genuinely out of room and the link drops: the CTA is what earns its
   place all the way down a sticky header, and "Sign in" is repeated in the footer.
   ⛔ Tied to the two LABELS, not to the layout — lengthen either and re-measure. */
@media (max-width: 480px) {
  .nav-in { gap: var(--s3); }
  .brand .blogo { height: 24px; }
  .nav-in-link { padding: 8px 8px; }
}
@media (max-width: 348px) {
  .nav-in-link { display: none; }
}

/* ── legal pages: privacy + terms ─────────────────────────────────────────────
   ⭐ One readable column and nothing else. A privacy policy that is hard to read is the
   tell that it is not meant to be read — and Google's OAuth review looks at these pages,
   so they are a surface, not an afterthought. ⛔ No colours or sizes invented here: the
   scale and the ink come from tokens.css like everywhere else. */
/* ⭐ NUMBERED CLAUSES, LIKE REDWATCHER (Pavel, 2026-09-11): `main.wrap > article.legal`, a
   `.legal-h` head with version + effective date, then one <section> per clause with an
   <h2><span class="n">n.</span> heading. ⚠️ The old `main.legal > .wrap` shape is gone from both
   pages; `.eff`, `.lede-l` and `.contact` have no markup any more. */
.legal { max-width: 760px; margin: 0 auto; padding: var(--s8) 0 var(--s9); }
.legal h1 { font-size: clamp(2rem, 5vw, 2.75rem); letter-spacing: -.03em; line-height: 1.08; }
.legal .upd { margin-top: var(--s3); font-size: .8125rem; color: var(--fg-3); }
.legal .lead { margin-top: var(--s6); font-size: 1.0625rem; line-height: 1.6; color: var(--fg-2); }
.legal section { padding: 0; margin-top: var(--s7); }  /* ⚠️ `section { padding: var(--s9) 0 }` is the page rhythm; a clause is not a page section. */
.legal h2 { font-size: 1.125rem; letter-spacing: -.015em; }
.legal h2 .n { color: var(--fg-3); font-weight: var(--w-med); margin-right: .35em; }
.legal p { margin-top: var(--s4); line-height: 1.65; color: var(--fg-2); }
.legal b, .legal strong { color: var(--fg); font-weight: var(--w-med); }
.legal a { color: var(--accent-ink); font-weight: var(--w-med); }
.legal ul, .legal ol { margin-top: var(--s4); padding-left: 1.15em; }
.legal li { margin-top: var(--s3); line-height: 1.65; color: var(--fg-2); }
.legal .mono { font-family: var(--mono, ui-monospace, monospace); font-size: .875em; }
.legal section[id] { scroll-margin-top: 96px; }
/* ⚠️ Tables carry the processor list, which is the part a reader actually scans. It must
   scroll inside itself rather than pushing the page sideways on a phone. */
/* ⚠️ The bare .tbl rules used to live with the report mock's surface table. That mock is
   gone; privacy.html's processor tables are the only .tbl left, so the base sits here now.
   Every declaration below is overridden by a .legal .tbl rule except the header hairline. */
.tbl { width: 100%; border-collapse: collapse; font-size: .8125rem; background: var(--page); border-radius: var(--r); overflow: hidden; }
.tbl th { text-align: left; font-size: .6875rem; font-weight: var(--w-med); letter-spacing: .085em; text-transform: uppercase; color: var(--ink-4); padding: var(--s3) var(--s4); border-bottom: 1px solid var(--line); }
.tbl td { padding: var(--s3) var(--s4); border-bottom: 1px solid var(--line); color: var(--fg-2); }
.tbl tr:last-child td { border-bottom: 0; }
.legal .tbl { width: 100%; margin-top: var(--s5); border-collapse: collapse; display: block; overflow-x: auto; }
.legal .tbl th { text-align: left; font-size: .75rem; text-transform: uppercase; letter-spacing: .06em; color: var(--fg-3); font-weight: var(--w-med); padding: 0 var(--s4) var(--s3) 0; }
.legal .tbl td { padding: var(--s3) var(--s4) var(--s3) 0; border-top: 1px solid var(--line); font-size: .9375rem; line-height: 1.55; color: var(--fg-2); vertical-align: top; }

/* ⭐ THE PRICE LIST on /pricing (2026-09-14) — the Terms table's rules, bounded to a reading width,
   with the price column right-aligned in the ink and tabular figures so the rungs line up.
   Plain markup on purpose: a crawler reads "1,000 credits $45" with nothing to open or run. */
.legal .price-tbl { display: table; max-width: 480px; }
.price-tbl th:last-child, .price-tbl td:last-child { text-align: right; padding-right: 0; }
.price-tbl td:last-child { color: var(--fg); font-weight: var(--w-semi); font-variant-numeric: tabular-nums; white-space: nowrap; }
.price-cta { display: flex; flex-wrap: wrap; gap: var(--s3); }
.price-cta .btn { height: 44px; padding: 0 var(--s5); }
/* ⚠️ `.legal a` paints every anchor in the article accent-blue — a button is an anchor too, so the
   two CTAs need their own colours restated at the higher specificity. Same values as components.css. */
.legal .price-cta .btn-a { color: var(--on-cta); }
.legal .price-cta .btn-o { color: var(--ink); }

/* ── the contact dialog ───────────────────────────────────────────────────────
   ⛔⛔ NO EMAIL ADDRESS IS WRITTEN IN ANY PAGE ON THIS SITE (Pavel, 2026-09-04) — a bare `mailto:`
   in served HTML is a harvested address. The markup is built by `contact.js`, which is also where
   the reasoning lives. ⚠️ Ported from Redwatcher, same decision, 2026-08-29.
   ⛔ No colours or sizes invented here: everything comes from tokens.css like the rest of the page. */
.cdlg { width: min(520px, calc(100vw - 32px)); padding: 0; border: 1px solid var(--line);
        border-radius: var(--r); background: var(--card); color: var(--fg); box-shadow: var(--lift-3); }
.cdlg::backdrop { background: rgba(var(--ink-rgb), .42); backdrop-filter: blur(2px); }
.cform { padding: var(--s5); }
.cdlg-h { display: flex; align-items: center; gap: var(--s3); }
.cdlg-h h2 { font-size: 1.125rem; font-weight: var(--w-semi); letter-spacing: -.024em; }
.cdlg-x { margin-left: auto; width: 30px; height: 30px; border-radius: var(--r-xs); border: 0;
          background: transparent; color: var(--fg-3); font-size: 1.35rem; line-height: 1; cursor: pointer; }
.cdlg-x:hover { background: var(--plane); color: var(--fg); }
.cdlg-s { margin-top: 5px; font-size: .8125rem; color: var(--fg-2); }
.cfield { display: block; margin-top: var(--s4); }
.cfield span { display: block; margin-bottom: 6px; font-size: .78rem; font-weight: var(--w-med); color: var(--fg-2); }
.cfield input, .cfield textarea { width: 100%; padding: 9px 11px; border: 1px solid var(--line-2);
        border-radius: var(--r-xs); background: var(--bg); color: var(--fg); font: inherit; font-size: .875rem; resize: vertical; }
.cfield textarea { min-height: 96px; line-height: 1.5; }
.cfield input:focus, .cfield textarea:focus { outline: 0; border-color: var(--accent-ink);
        box-shadow: 0 0 0 3px var(--accent-wash); }
/* ⛔ OFF-SCREEN, NOT display:none — a bot that skips hidden inputs still fills this one. */
.chp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.cmsg { margin-top: var(--s3); font-size: .8125rem; color: var(--fg-2); min-height: 1.2em; }
.cmsg.bad { color: var(--bad); }
.cdlg-f { display: flex; justify-content: flex-end; gap: var(--s2); margin-top: var(--s4); }
/* ⚠️ A <button> sitting inside prose on the legal pages, where the address used to be. It has to
   read as the link it replaced, so it borrows the link styling and none of the button chrome. */
.cbtn.lk { border: 0; background: none; padding: 0; font: inherit; cursor: pointer;
           color: var(--accent-ink); font-weight: var(--w-med); }
.cbtn.lk:hover { text-decoration: underline; }

/* ── the illustration tile ────────────────────────────────────────────────────
   ⭐⭐ THE calendly.com REGISTER IS A SIMPLE ILLUSTRATION, NOT A SCREENSHOT. Their tile is a
   saturated gradient square, one white card floating on it with a soft ring, and a HANDFUL of
   large chunky controls with a lot of air. ⛔ It is not a picture of the real UI: a dense,
   faithful miniature of the product was built here on 2026-09-10 and rejected on sight
   ("i asked for simple illustration, not UI"). Detail is the failure mode, not the goal.

   ⚠️ NO CROP, NO BLEED, AND THAT IS THE POINT. The tile is self-contained with its own rounded
   corners, so `aspect-ratio` alone sizes it and there is nothing to cut. The whole dead-plate /
   cut-through-the-text class of bug (5b390ea) cannot occur here, because nothing is hidden.

   ⛔⛔ THE CARD INSIDE IS PERMANENTLY WHITE, SO ITS INK IS LITERAL. `--ink` flips to #EEF1F7 in
   dark mode; on a white card that is invisible. Same reasoning as `--plate` being #FFFFFF in both
   themes — a fixed surface needs fixed ink. ⛔ Do not "tidy" these hexes into tokens. */
/* ── the tile palettes ────────────────────────────────────────────────────────
   ⭐ ONE GROUND PER CARD, so a row does not read as the same asset printed twice. ⚠️ They are
   two mixes of the SAME two brand hues, not two new colours: the engines tile is blue carrying a
   mint mass, the schedule tile is teal carrying a blue one. Sibling, not stranger.
   ⛔ Set here and not in the markup: `var()` inside a presentation ATTRIBUTE is not reliable, so
   the stops and the masses carry classes and take their paint from CSS, which does win over the
   attribute still in the file. ⭐ The next four cards each claim a block here. */
.ftv-tile {
  --g-a: #9AC8FA; --g-b: #5BA4F6; --g-c: #2A66B8;
  --m-1: #DCEBFE; --m-2: #12468C; --m-3: #6FE3C6; --m-4: #3E8BE8;
}
.ft-sched .ftv-tile {
  --g-a: #B7E9E4; --g-b: #5AC4CE; --g-c: #1F6E9C;
  --m-1: #DFF6F1; --m-2: #0F5570; --m-3: #8FD3FA; --m-4: #3FA8C4;
}
.vgx .g-a { stop-color: var(--g-a); }
.vgx .g-b { stop-color: var(--g-b); }
.vgx .g-c { stop-color: var(--g-c); }
.vgx .vg-b1 { fill: var(--m-1); }
.vgx .vg-b2 { fill: var(--m-2); }
.vgx .vg-b3 { fill: var(--m-3); }
.vgx .vg-b4 { fill: var(--m-4); }

.ftv-tile { margin-top: auto; }
.vgx { display: block; width: 100%; height: auto; font-family: var(--font-sans); }
/* ⚠️ THE GOOGLE CARD READS "AI Overviews", NOT "Google" — `lib/engine-options.js` is the one
   place an engine is named, and it labels this one "AI Overviews" paired with Google's own
   logo, which is exactly what that card draws. ⛔ The card's PROSE spells out "Google AI
   Overviews" because a sentence has no logo beside it; the picture does, so it does not. */
.vgx .vg-nm { fill: #0B1B36; font-size: 20px; font-weight: 500; letter-spacing: -.02em; }
/* ── the motion ───────────────────────────────────────────────────────────────
   ⭐⭐ THE REFERENCE MOVES, AND THE STILLS DO NOT SAY SO. calendly.com's tiles drift and their
   illustrations cycle; three passes here were built from screenshots and were therefore dead.

   ⭐ TWO LOOPS, DELIBERATELY UNRELATED. The ground drifts on 26/34/30s primes so the three masses
   never re-align into a pattern the eye can learn, and the accent walks the rows on 14s. ⛔ Do not
   sync them: a background that pulses in time with the foreground reads as a slideshow.

   ⭐ THE ACCENT MOVING IS THE ARGUMENT. A fixed blue row said "Gemini is your best engine", which
   is one brand's story on a card whose heading is "All four AI engines, every time". Walking it
   says the thing the heading claims. ⚠️ It is also why the percentages are no longer coloured in
   the markup — the colour belongs to whichever row the pill is under.

   ⛔ TRANSFORMS ONLY — no width/height/x/y keyframes. These paint inside a filtered SVG group and
   an animated geometry attribute re-rasterises the 52px blur every frame. */
/* 🪤 THE FIRST VERSION MOVED AND NOBODY COULD TELL, WHICH IS THE SAME AS NOT MOVING. 58 units of
   travel over 26s is ~2px a second, behind a 52px blur, in the ~70px frame of gradient the card
   does not cover. Pixels changed and a diff proved it; an eye could not. ⛔ "The animation is
   running" is not the test — the test is whether the ground visibly flows.
   ⭐ Amplitude roughly tripled and the cycles roughly halved, so each mass crosses a meaningful
   part of the visible frame within a few seconds. Four masses now, not three: three could sit in
   a configuration where the frame went quiet for seconds at a time. */
@keyframes vgDrift1 { 50% { transform: translate(196px, 132px) scale(1.28); } }
@keyframes vgDrift2 { 50% { transform: translate(-214px, -148px) scale(1.22); } }
@keyframes vgDrift3 { 50% { transform: translate(168px, 178px) scale(.78); } }
@keyframes vgDrift4 { 50% { transform: translate(150px, -186px) scale(1.20); } }
.vgx .vg-b1 { animation: vgDrift1 15s ease-in-out infinite; }
.vgx .vg-b2 { animation: vgDrift2 19s ease-in-out infinite; }
.vgx .vg-b3 { animation: vgDrift3 13s ease-in-out infinite; }
.vgx .vg-b4 { animation: vgDrift4 17s ease-in-out infinite; }

/* ⚠️ The bars breathe, barely, and only in scale from the baseline they stand on. */
@keyframes vgBreathe { 50% { transform: scaleY(1.07); } }
.vgx .vg-bar { transform-box: fill-box; transform-origin: 50% 100%;
               animation: vgBreathe 9s ease-in-out infinite; animation-delay: calc(var(--i) * -.7s); }

/* ⛔⛔ STILL BY DEFAULT FOR ANYONE WHO ASKED. Every loop stops, and each tile parks on a frame that
   is a coherent statement rather than a frozen mid-gesture — see each tile's own block.

   ⛔⛔ AND THE GROUND IS STILL ON A PHONE — this block, ALONE of the eleven, also fires at
   `(max-width: 860px)`. ⚠️ Do NOT copy that condition onto a tile's own park block below: the
   foreground is affordable and is meant to keep moving. What a phone cannot pay for is the ground
   DRIFTING, and the distinction is the whole finding. Measured 2026-09-10 after an iPhone 12 Pro
   report, reproduced at 390×840 @dpr3 parked on one tile:

     CPU throttle             4x           6x
     ground drifting      90fps 99%    60fps 99%     ⛔ janks
     ground frozen        87fps 25%    85fps 35%     ✅ shipped
     everything parked     0fps  0%     0fps  0%

   🪤 WHY MOVING THE GROUND COSTS SO MUCH MORE THAN MOVING ANYTHING ELSE. None of this motion is
   composited — animating `transform` on an SVG CHILD goes through layout and paint, so ANY moving
   element repaints the WHOLE tile. The difference is what that repaint then has to redo: with the
   blob group still, the browser caches the `feGaussianBlur stdDeviation="52"` as a texture and
   reuses it; with the blobs moving, its input changes every frame and a 200%×200% filter region
   is re-blurred every frame, forever. ⭐ A phone loses its headroom three ways at once — less
   throughput, dpr 3 (9× the filter pixels of a dpr-1 monitor), and Chrome on iOS is WebKit, which
   rasterises SVG filters on the CPU — so a desk browser absorbs this and a phone does not.

   🪤 ⛔ DO NOT "OPTIMISE" THIS BY DELETING THE FILTER. Measured, same rig: dropping `vgSoft`
   entirely is 42%/51%, i.e. WORSE than keeping it frozen, because four large circles then
   re-rasterise as vector geometry on every repaint instead of being one cached texture. The blur
   is not the enemy; the blur being re-computed is.

   ⚠️ The bars ride along in this block for the same reason — 32 of them per tile, breathing, each
   one enough on its own to invalidate the tile every frame. */
@media (prefers-reduced-motion: reduce), (max-width: 860px) {
  .vgx .vg-b1, .vgx .vg-b2, .vgx .vg-b3, .vgx .vg-b4, .vgx .vg-bar { animation: none; }
}

/* `tiles.js` hangs `is-idle` on the tiles outside the viewport; this is the whole of what that
   class does. ⚠️ IT IS WORTH ABOUT 4 POINTS OF MAIN THREAD AND NO MORE — 38%→35% at 1x, 72%→68%
   at 2x, nothing measurable at 4x. The reasoning that predicted far more, and why it was wrong,
   is written at the top of tiles.js; the short version is that the browser already skips PAINT
   for what is off screen, and paint is nearly the whole bill. ⛔ Do not reach for this class to
   fix a slow tile — the cost is dominated by the tiles that are ON screen.

   ⛔ PAUSED, NOT `animation: none`. The park blocks below are a DESTINATION — they move a tile to
   a chosen frame, which is right when the answer is "this tile will not move again" and wrong
   here, where the tile is coming back the moment it is scrolled to. `paused` freezes each loop
   exactly where it stood and resumes it there, so nothing jumps at the edge of the viewport.
   ⚠️ `!important` for the reason tokens.css uses it: `animation:` shorthand resets play-state to
   `running`, and every rule above sets the shorthand. A guard has to outrank what it guards.
   ⚠️ It cannot touch `.vg-card` — the deck moves on a TRANSITION, not an animation. deck.js stops
   its own timer offscreen, which is the same guarantee by the other route. */
.vgx.is-idle, .vgx.is-idle * { animation-play-state: paused !important; }


/* ── the schedule tile ────────────────────────────────────────────────────────
   ⭐ Same register as the engines tile and deliberately NOT the same picture: a field showing its
   own value, a rule, and a radio group. The walking accent means something different here —
   on the engines card it says "all four, every time", here it is literally the control being used.

   ⛔⛔ THE TWO TILES MUST NOT BREATHE IN TIME. Identical classes drove identical animations, so
   both grounds surged and settled together and the pair read as one repeated asset. The masses
   start from different corners AND run on different periods; the walk is a separate keyframe
   because the rows are 52px apart here, not 62. */
.vgx-b .vg-b1 { animation-duration: 21s; animation-delay: -7s; }
.vgx-b .vg-b2 { animation-duration: 16s; animation-delay: -4s; }
.vgx-b .vg-b3 { animation-duration: 25s; animation-delay: -11s; }
.vgx-b .vg-b4 { animation-duration: 19s; animation-delay: -6s; }

.vgx .vg-lbl  { fill: #626E86; font-size: 15px; }
.vgx .vg-val  { fill: #206ECE; font-size: 17px; font-weight: 500; letter-spacing: -.02em; }
.vgx .vg-hair { stroke: #E7ECF4; stroke-width: 1.5; }

@keyframes vgWalkB {
  0%,   19% { transform: translateY(0); }
  25%,  44% { transform: translateY(52px); }
  50%,  69% { transform: translateY(104px); }
  75%,  94% { transform: translateY(156px); }
  100%      { transform: translateY(0); }
}
.vgx .vg-selB { animation: vgWalkB 14s cubic-bezier(.65,0,.35,1) infinite; }
/* ⚠️ ONE keyframe, staggered by --i, for both the radio and the header value — they are two
   read-outs of a single state and must never disagree. ⛔ Do not give them separate timings.
   ⚠️ Positive delay, for the reason written on the engines tile: a negative one seeks forward
   and staggers the set backwards. */
@keyframes vgPick { 0%, 19% { opacity: 1; } 24%, 100% { opacity: 0; } }
.vgx .vg-dot, .vgx .vg-val { opacity: 0; animation: vgPick 14s linear infinite;
                             animation-delay: calc(var(--i) * 3.5s); }

@media (prefers-reduced-motion: reduce) {
  /* ⛔ Parked, not frozen — on "Weekly", with its radio and its header value showing and the
     other three hidden. A still frame of this card has to be a coherent choice, not a pill
     halfway between two options and four values stacked on top of each other. */
  .vgx .vg-selB { animation: none; transform: translateY(104px); }
  .vgx .vg-dot, .vgx .vg-val { animation: none; opacity: 0; }
  .vgx .vg-dot[style*="--i:2"], .vgx .vg-val[style*="--i:2"] { opacity: 1; }
}

/* ── the prompts tile ─────────────────────────────────────────────────────────
   ⛔⛔ REBUILT THREE TIMES; ALL THREE FAILURES ARE KEPT BECAUSE EACH IS EASY TO REPEAT.
   1. The other two tiles copied a third time — white card, centred, four rows, walking accent.
   2. Over-corrected: card thrown away, seven objects edge to edge, no margin, no focal point.
      Loose is not the same as designed.
   3. Right composition, wrong motion: the cards sat still and only their text cross-faded, which
      reads as a slideshow, not as a deck.
   ⭐ It is now an actual deck. Four cards, three visible slots and one off-stage; the front card
   drops away, every other card moves up one, and the one that left rejoins at the back.

   ⚠️ THE ROLL IS DRIVEN BY deck.js AND IT HAS TO BE. SVG has no z-index — paint order is document
   order — so the card at the front must be the LAST child, and which card that is changes every
   step. With a fixed order the back cards draw over the front card's own text. CSS cannot reorder
   elements; that is the whole reason for the script.
   ⭐ These slot rules are what makes the page correct BEFORE the script runs and if it never does:
   markup ships as a stack with card 0 at the front, which is a perfectly good still. */
.ft-qs .ftv-tile {
  --g-a: #BFEBD8; --g-b: #57CBA0; --g-c: #1C6E74;
  --m-1: #E4F8EE; --m-2: #125A5E; --m-3: #8FD3FA; --m-4: #3EB894;
}
/* 🪤 A DELAY OF HALF THE PERIOD IS NOT AN OFFSET. These are ease-in-out curves around a single
   50% keyframe, so masses at 33% and 67% of their own cycles sit at the IDENTICAL point — my first
   values put this ground and the schedule ground in the same state at t=0, page load and the
   most-looked-at frame there is. ⛔ Check phase at t=0, not only mid-cycle. */
.vgx-c .vg-b1 { animation-duration: 18s; animation-delay: -2s; }
.vgx-c .vg-b2 { animation-duration: 23s; animation-delay: -14s; }
.vgx-c .vg-b3 { animation-duration: 14s; animation-delay: -5s; }
.vgx-c .vg-b4 { animation-duration: 20s; animation-delay: -17s; }

/* ⚠️ transform-origin in USER UNITS, not percentages: it is the front card's own centre
   (92 + 376/2, 118 + 198/2), so a card scales about itself instead of about the tile's corner. */
.vgx .vg-card { transform-origin: 280px 217px;
                transition: transform .62s cubic-bezier(.3,.9,.3,1), opacity .62s ease; }
.vgx .vg-card[data-p="0"] { transform: none;                          opacity: 1;   }
.vgx .vg-card[data-p="1"] { transform: translateY(-33px) scale(.851); opacity: .58; }
.vgx .vg-card[data-p="2"] { transform: translateY(-60px) scale(.734); opacity: .32; }
/* ⭐ Slot 3 leaves DOWNWARD and slightly larger — dealt toward the reader, not shuffled upward,
   so the eye reads it as the card that just finished rather than one arriving. */
.vgx .vg-card[data-p="3"] { transform: translateY(34px) scale(1.04);  opacity: 0;   }
/* ⛔ Only the front card carries readable text. A back card at .32 with 20px ink behind the front
   card's own copy is the muddle this tile spent two rebuilds getting away from. */
.vgx .vg-face { transition: opacity .34s ease; }
.vgx .vg-card:not([data-p="0"]) .vg-face { opacity: 0; }

/* ⛔ THE ENGINE MARKS ARE THIRD-PARTY ARTWORK AND KEEP THEIR OWN COLOURS. For once they need no
   `--plate`: the card they sit on is already white, which is the exact condition that token exists
   to guarantee. ⚠️ Gemini's diamond and Perplexity's stroke read small beside solid marks, so they
   are sized per engine in the markup — the same optical correction components.css makes for the
   inline cluster. ⛔ Never recolour, desaturate or flatten one to match the tile.

   🪤 A TRAP THIS TILE ALREADY PAID FOR, kept although the sparkle it happened to is gone. Nudging
   the stack down 8 units I ran replace("146","154") over the markup — and "146" also appeared
   INSIDE a path's coordinates, so a four-point star rendered as a triangle and only the render
   said so. ⛔ Never string-replace a bare number in path data; regenerate the path. */
.vgx .vg-mk { overflow: visible; }
.vgx .vg-eyebrow { fill: #7A8699; font-size: 11px; font-weight: 500; letter-spacing: .1em; }
/* ⭐ The prompt is the biggest thing on the card, because it is the only thing the card is about. */
.vgx .vg-qt      { fill: #0B1B36; font-size: 20px; font-weight: 500; letter-spacing: -.02em; }
.vgx .vg-pip     { fill: #DCE3EC; }
.vgx .vg-pip-on  { fill: #23A06E; opacity: 0; transition: opacity .34s ease; }
.vgx .vg-pip-on.is-on { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  /* ⛔ deck.js already declines to start under this setting, and watches for it changing
     mid-visit. This kills the travel too, so a card that is mid-flight when it flips lands
     instead of gliding. */
  .vgx .vg-card, .vgx .vg-face, .vgx .vg-pip-on { transition: none; }
}

/* ── the rank tile ────────────────────────────────────────────────────────────
   ⭐⭐ NO WHITE CARD, AND THAT IS LOAD-BEARING. The version before this one was thrown out entire
   — *"i don't like this at all, need complete redo, different idea/layout strategy completely"* —
   after passing every rule written below it. It had its own motion and the section's stock
   composition, a rounded white card centred on a gradient with content inside it, and that alone
   was enough. ⭐ **"Its own composition" is the LAYOUT, not the contents of the layout.**
   ⛔ Do not "bring this tile back into line" by giving it a card. The line it is out of is the
   problem two rebuilds were spent on.
   ⭐ FIFTH GROUND, and row 2 is a deliberate inverse pair: the schedule tile beside it runs light
   teal → deep BLUE, this one runs light blue → deep SEA-GREEN. */
.ft-cnt .ftv-tile {
  --g-a: #BCE0F5; --g-b: #4E9BD4; --g-c: #14705A;
  --m-1: #DEEEFC; --m-2: #0D5A4C; --m-3: #7FE0C0; --m-4: #3D86C8;
}
/* 🪤 PHASE CHECKED AT t=0 AGAINST THE TILE IT SHARES A ROW WITH, not by watching the two mid-cycle.
   Ease-in-out around one 50% keyframe means progress p and 1−p are the SAME position, so what has
   to differ is the triangle fold, not the delay. Folded, this tile sits at .84 / .92 / .40 / .87
   where the schedule tile sits at .67 / .50 / .88 / .63. */
.vgx-e .vg-b1 { animation-duration: 19s; animation-delay: -11s; }
.vgx-e .vg-b2 { animation-duration: 26s; animation-delay: -12s; }
.vgx-e .vg-b3 { animation-duration: 15s; animation-delay: -3s;  }
.vgx-e .vg-b4 { animation-duration: 23s; animation-delay: -13s; }

/* ⛔ LITERAL WHITE, never var(--ink) or its inverse: this type sits on a painted gradient that does
   not change with the theme, so neither may the ink on it. Same reasoning as `--plate`. */
.vgx-e .vg-hd   { fill: #FFFFFF; fill-opacity: .82; font-size: 19px; font-weight: 500; letter-spacing: -.02em; }
.vgx-e .vg-rule { stroke: #FFFFFF; stroke-opacity: .26; stroke-width: 1.5; }
.vgx-e .vg-rk   { fill: #FFFFFF; fill-opacity: .5; font-size: 17px; font-weight: 500; }
.vgx-e .vg-bn   { fill: #FFFFFF; fill-opacity: .93; font-size: 21px; font-weight: 500; letter-spacing: -.02em; }
.vgx-e .vg-bv   { fill: #FFFFFF; fill-opacity: .93; font-size: 21px; font-weight: 600; letter-spacing: -.03em; }
.vgx-e .vg-bn.is-you, .vgx-e .vg-bv.is-you { fill-opacity: 1; }
.vgx-e .vg-hl   { fill: #FFFFFF; fill-opacity: .14; }
/* ⚠️ The owned brand's mark is the one plate that is NOT white — it is a letter, not a logo, so it
   needs no white ground and a dark chip is what tells it apart from the five real marks. */
.vgx-e .vg-favy { fill: #FFFFFF; font-size: 15px; font-weight: 600; }

/* ── the motion ───────────────────────────────────────────────────────────────
   ⭐⭐ ITS VERB IS A RE-SORT, and nothing else on this page has one: the prompts tile deals a deck,
   the engines and schedule tiles walk an accent down four rows, the trend tile draws its lines.
   Here the LIST reorders — your brand passes the one above it and the two trade places. It is the
   only motion a ranking actually has, and it is the thing a repeat check is for.

   ⭐⭐ THE RANK DIGITS DO NOT MOVE, AND THAT IS THE WHOLE READING. 1–6 are POSITIONS; the brands
   travel between them. Animating the digits instead would say the ranks changed, which is not a
   thing that can happen. ⛔ Do not put the digits inside the row groups.

   ⚠️⚠️ THE COUNT MOVES WITH THE BRAND OR THE PICTURE LIES. Your brand cannot overtake Klaviyo's 15
   while still reading 14, so the row carries two values and swaps them on the way up. ⛔ Change one
   number here and check the other five still sort into the order they are drawn in.

   🪤 A LOOPING RE-SORT HAS TO GET BACK DOWN, and being seen doing it would read as a demotion the
   product just invented. The two moving rows fade out at 95.71%, snap back to their starting
   positions and counts inside that blind window, and fade in again — 0.4s of the 14s loop.
   ⛔ The snap keyframe pair (95.71% / 95.72%) must stay inside the opacity-0 window; widen the fade
   and the jump becomes visible.
   🪤 t=0 IS MID-HOLD, bought with a −11s on every delay, so page load — the one frame everybody
   sees — is the settled ranking and not the rows in flight. Same lesson as the drifting masses. */
@keyframes vgRise {
  0%, 57.14%    { transform: translateY(0);     opacity: 1; }
  62.86%, 92.86% { transform: translateY(-48px); opacity: 1; }
  95.71%        { transform: translateY(-48px); opacity: 0; }
  95.72%        { transform: translateY(0);     opacity: 0; }
  98.57%, 100%  { transform: translateY(0);     opacity: 1; }
}
@keyframes vgFall {
  0%, 57.14%    { transform: translateY(0);    opacity: 1; }
  62.86%, 92.86% { transform: translateY(48px); opacity: 1; }
  95.71%        { transform: translateY(48px); opacity: 0; }
  95.72%        { transform: translateY(0);    opacity: 0; }
  98.57%, 100%  { transform: translateY(0);    opacity: 1; }
}
@keyframes vgWas { 0%, 58% { opacity: 1; } 61%, 95.71% { opacity: 0; } 95.72%, 100% { opacity: 1; } }
@keyframes vgNow { 0%, 58% { opacity: 0; } 61%, 95.71% { opacity: 1; } 95.72%, 100% { opacity: 0; } }
.vgx-e .vg-rise { animation: vgRise 14s cubic-bezier(.65,0,.35,1) infinite; animation-delay: -11s; }
.vgx-e .vg-fall { animation: vgFall 14s cubic-bezier(.65,0,.35,1) infinite; animation-delay: -11s; }
.vgx-e .vg-was  { animation: vgWas 14s linear infinite; animation-delay: -11s; }
.vgx-e .vg-now  { opacity: 0; animation: vgNow 14s linear infinite; animation-delay: -11s; }

@media (prefers-reduced-motion: reduce) {
  /* ⛔ Parked on the SETTLED ranking, which is also the frame the loop holds for two thirds of its
     length: your brand already past Klaviyo, and reading 16 rather than the 14 it started from.
     ⚠️ A still of a re-sort mid-flight is two rows overlapping a rank digit, which is why this
     parks on an end state rather than on frame 0. */
  .vgx-e .vg-rise { animation: none; transform: translateY(-48px); }
  .vgx-e .vg-fall { animation: none; transform: translateY(48px); }
  .vgx-e .vg-was  { animation: none; opacity: 0; }
  .vgx-e .vg-now  { animation: none; opacity: 1; }
}

/* ── the trend tile ───────────────────────────────────────────────────────────
   ⭐ FOURTH GROUND. It was the section's one WIDE tile — 1120×392, a chart beside a four-brand
   ranking — and it is now 560×400 like everything else in a two-up row (Pavel, 2026-09-10:
   "remove it, only keep line. and make it like other cards, not full screen").
   ⭐⭐ WHAT THE WIDTH WAS ACTUALLY PAYING FOR WAS THE RANKING, AND THE RANKING MOVED. The tile in
   row 2 is now a six-brand ranked list, so keeping four ranked brands here drew the same league
   table twice. With one series and no panel, a chart fits a two-up tile easily.
   ⛔ Do not add rival lines back "for context". One line is the whole point: this card owns the
   direction of travel, the rank tile owns who is ahead. ⚠️ The old narrow-layout machinery — a
   1120/392 aspect-ratio, `slice`, two card grounds and a 560px breakpoint that swapped the chart
   for the ranking — was deleted with it. It existed only because a wide picture cannot survive a
   phone; a 560×400 one scales like its neighbours and needs none of it. */
.ft-hist .ftv-tile {
  --g-a: #CBD8FA; --g-b: #7C97EE; --g-c: #2C3E96;
  --m-1: #E6EDFE; --m-2: #16247A; --m-3: #8FD3FA; --m-4: #5D74E0;
}
.vgx-d .vg-b1 { animation-duration: 25s; animation-delay: -9s;  }
.vgx-d .vg-b2 { animation-duration: 17s; animation-delay: -3s;  }
.vgx-d .vg-b3 { animation-duration: 29s; animation-delay: -19s; }
.vgx-d .vg-b4 { animation-duration: 22s; animation-delay: -13s; }

.vgx .vg-ct    { fill: #0B1B36; font-size: 17px; font-weight: 600; letter-spacing: -.02em; }
/* ⚠️ The chart's sub-line is "Against competitors" (Pavel, 2026-09-10). It used to name all four
   engines — four names are evidence, a category is a claim — but at 420 units that wrapped past
   the card, and the four names are still on the page as marks in the rank tile's header, which is
   the stronger form of the same argument. */
.vgx .vg-cs    { fill: #8A93A5; font-size: 13px; }
.vgx .vg-ax    { fill: #98A2B3; font-size: 13px; font-variant-numeric: tabular-nums; }
.vgx .vg-grid  { stroke: #EAEEF4; stroke-width: 1.5; }
/* ⭐ ONE ACCENT, AND IT IS THE CUSTOMER. The three rivals are grey on purpose — this picture is
   about the blue line crossing one of them in September, and coloured rivals would bury that.
   ⚠️ Restored 2026-09-10 with the rival lines. The chart's four September values are the rank
   tile's top four, and its August values are the state that tile animates FROM. ⛔ Change one and
   check the other. */
.vgx .vg-line { fill: none; stroke-width: 3.5; stroke-linecap: round; }
/* ⚠️ A WHITE RING ON EVERY END DOT. September puts your 16 and Klaviyo's 15 within 6.8 units, and
   two 5-radius dots at that distance merge into one blob — on the one pair the whole picture is
   about. The ring separates them without moving a value. */
.vgx .vg-end  { stroke: #FFFFFF; stroke-width: 2.5; }
.vgx .vg-rv,  .vgx .vg-rv2  { stroke: #B9C2D2; }
.vgx .vg-rv3  { stroke: #CBD3E0; }
.vgx .vg-rvf  { fill: #B9C2D2; }
.vgx .vg-rv2  { stroke: #98A3B7; }
.vgx .vg-rv2f { fill: #98A3B7; }
.vgx .vg-rv3f { fill: #CBD3E0; }
/* ⭐⭐ THE BRAND BLUE, NOT THE READABLE ONE — #5BA4F6, the hex in the logo (Pavel, 2026-09-10:
   "we have light blue like in our logo as a main blue, not deep blue").
   ⭐ tokens.css already drew this line and the tiles were on the wrong side of it: `--accent`
   (#5BA4F6) is FILLS — buttons, washes, the mark, chips, and anything drawn rather than read;
   `--accent-ink` (#206ECE) is the same blue darkened only so far as TEXT needs. A line, a tick and
   a bar are drawn, not read, so they take the brand blue.
   ⛔ TEXT STAYS #206ECE and that is not an oversight. #5BA4F6 on white is under the contrast floor
   for prose, which is the entire reason two blues exist. */
.vgx .vg-you  { stroke: #5BA4F6; }
.vgx .vg-youf { fill: #5BA4F6; }

/* ⭐ THE MOTION IS THE SERIES BEING DRAWN, which is the one thing a chart can honestly do and no
   other tile here does. ⚠️ `pathLength="100"` is kept although there is one line left: it costs
   nothing and it is what lets the keyframe talk in percentages rather than a measured length.
   ⛔ It fades OUT and restarts from undrawn rather than un-drawing: the reset happens at
   dashoffset 100, where there is nothing on screen to see it. */
@keyframes vgDraw {
  0%        { stroke-dashoffset: 100; opacity: 1; }
  22%, 88%  { stroke-dashoffset: 0;   opacity: 1; }
  96%, 100% { stroke-dashoffset: 0;   opacity: 0; }
}
@keyframes vgDot { 0%, 20% { opacity: 0; } 26%, 88% { opacity: 1; } 96%, 100% { opacity: 0; } }
/* 🪤 THE BASE dashoffset HAS TO BE 100. With a positive stagger, a line shows its BASE state until
   its own animation begins — and the initial dashoffset is 0, i.e. fully drawn. So on load the
   second and third lines appeared complete for half a second each, then snapped to undrawn and
   drew themselves in. Measured at t=0.1s: [99.8, 0, 0]. ⛔ Any staggered draw needs this. */
.vgx .vg-line { stroke-dasharray: 100; stroke-dashoffset: 100;
                animation: vgDraw 14s ease-in-out infinite;
                animation-delay: calc(var(--i) * .5s); }
.vgx .vg-end  { opacity: 0; animation: vgDot 14s ease-in-out infinite;
                animation-delay: calc(var(--i) * .5s); }

@media (prefers-reduced-motion: reduce) {
  /* ⛔ Parked fully drawn. A chart that has stopped halfway through drawing itself is not a still,
     it is a broken picture. */
  .vgx .vg-line { animation: none; stroke-dashoffset: 0; opacity: 1; }
  .vgx .vg-end  { animation: none; opacity: 1; }
}


/* ── the sources tile ─────────────────────────────────────────────────────────
   ⭐ SIXTH GROUND: pale mint → deep teal-blue, the mint-forward mix the set had not used.
   🪤 A WARM GROUND WAS BUILT FIRST AND RENDERED SIDE BY SIDE WITH ITS ROW-MATE BEFORE BEING
   THROWN AWAY. The argument for it was real — five tiles in, blue, teal, mint, indigo and
   blue→sea-green are taken, and a sixth COOL mix risks being a near-copy of one of them, which is
   the exact failure the palette rule exists to prevent. ⛔ It was still wrong: terracotta beside
   the indigo trend tile was the only warm thing on the whole page and read as a different
   product's card. ⭐ The rule at the top of the palettes holds — two hues, many mixes — and the
   way to tell six tiles apart is which hue LEADS and how deep the third stop goes, not a new hue.
   ⚠️ Contrast with the indigo beside it comes from the mint lead, not from warmth. */
.ft-src .ftv-tile {
  --g-a: #CFE9DA; --g-b: #4FAE86; --g-c: #124A63;
  --m-1: #E6F6EC; --m-2: #0E3F4E; --m-3: #A8E6C6; --m-4: #3E9C8E;
}
/* 🪤 PHASE CHECKED AT t=0 AGAINST ITS ROW-MATE, the trend tile, whose masses fold to
   .72 / .35 / .69 / .82. These fold to .21 / .56 / .47 / .10, so no mass is anywhere near its
   opposite number at page load. ⚠️ b2 started at −14s, which folds to .44 against the trend tile's
   .35 — close enough to matter, so it moved to −5s. ⛔ The fold is what matters, not the delay: ease-in-out around one
   50% keyframe puts progress p and 1−p at the IDENTICAL position. */
.vgx-f .vg-b1 { animation-duration: 24s; animation-delay: -2.5s;  }
.vgx-f .vg-b2 { animation-duration: 18s; animation-delay: -5s;  }
.vgx-f .vg-b3 { animation-duration: 30s; animation-delay: -7s;  }
.vgx-f .vg-b4 { animation-duration: 21s; animation-delay: -1s;  }

/* ⛔ Ink inside a permanently-white card is a literal hex — `--ink` flips to #EEF1F7 in dark and
   the card underneath does not. Same reasoning as `--plate`. */
.vgx-f .vg-sn   { fill: #3B475F; font-size: 17px; }
/* ⚠️ The domain is the subject on this card, so it is set in the same face as a name would
   be — not in a mono "code" face, which would make a citation look like a config value. */
.vgx-f .vg-fy   { fill: #FFFFFF; font-size: 13px; font-weight: 700; }
.vgx-f .vg-sv   { fill: #0B1B36; font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; }
.vgx-f .vg-strk { fill: #EEF1F6; }
.vgx-f .vg-sbar { fill: #98A3B7; }
/* ⭐ ONE ACCENT, AND IT IS THE CUSTOMER'S OWN PAGES. Seven coloured buckets would be a pie chart
   with extra steps; the only row that needs finding is the reader's. */
.vgx-f .vg-srhl { fill: #F1F5FC; }
.vgx-f .vg-sn.is-you, .vgx-f .vg-sv.is-you { fill: #206ECE; font-weight: 600; }
/* ⚠️ THE BAR IS A FILL, SO IT IS THE BRAND BLUE — and tokens.css's `--you` comment argues the
   opposite ("a light bar cannot out-read a grey rival"). Rendered and compared: the rivals are
   NEUTRAL greys, so the blue wins on saturation long before it needs to win on value. The comment
   was written about a bar among coloured bars. */
.vgx-f .vg-sbar.is-you { fill: #5BA4F6; }

/* ── the motion ───────────────────────────────────────────────────────────────
   ⭐⭐ SEVEN BARS GROW AT ONCE AND LAND TOGETHER, which is not the same event as the trend tile
   beside it drawing one line left to right: there, a single head travels at a constant speed; here
   seven lengths expand at seven different speeds and arrive on the same beat. ⛔ Do not stagger
   them — a cascade is a reveal, and a reveal is what the row-mate already does.
   ⭐ ONE KEYFRAME FOR ALL SEVEN, because `scaleX(1)` means "my own width" — the lengths live in the
   markup as real rect widths, so nothing here has to know a percentage.
   🪤 THE RESET IS INSIDE A BLIND WINDOW, same device as the rank tile: the bars fade at 90%, snap
   back to zero at 94.01% while invisible, and fade in again. Shrinking them in view would read as
   the shares falling, which is a claim this card does not make.
   🪤 t=0 IS MID-HOLD, bought with a −7s. Page load is the finished breakdown, not seven stubs. */
@keyframes vgGrow {
  0%         { transform: scaleX(0); opacity: 1; }
  24%, 90%   { transform: scaleX(1); opacity: 1; }
  94%        { transform: scaleX(1); opacity: 0; }
  94.01%     { transform: scaleX(0); opacity: 0; }
  97%, 100%  { transform: scaleX(0); opacity: 1; }
}
.vgx-f .vg-sbar { transform-box: fill-box; transform-origin: 0 50%;
                  animation: vgGrow 13s cubic-bezier(.25,.9,.3,1) infinite; animation-delay: -7s; }

@media (prefers-reduced-motion: reduce) {
  /* ⛔ Parked full, which is also the base state the markup ships with. */
  .vgx-f .vg-sbar { animation: none; transform: none; opacity: 1; }
}

/* ── the engines tile ─────────────────────────────────────────────────────────
   ⭐⭐ ITS VERB IS CHOOSING, and it is the only tile where a POINTER appears. The others deal a
   deck, walk an accent down a list, re-sort a ranking, draw lines, grow bars — all things the
   product does to data. This one shows the reader doing something, which is what its sub-line
   promises: "check all six at once, or pick the ones you care about most".
   ⛔ Do not give this tile a single white card with four rows in it again. That was the old
   construction and it was indistinguishable from the schedule tile beside it in the same row.

   ⭐⭐ THE POINTER AND THE TICKS ARE ONE CLOCK, SPELT TWICE. The cursor arrives at 10% / 21.7% /
   33.3% of a 12s loop and each card's selection turns on 2.4% later — the gap is the press. ⛔ If
   you move a cursor keyframe you must move the matching selection keyframe by the same amount, or
   the tile ticks a box the pointer has not reached.
   🪤 THREE KEYFRAMES, NOT ONE SHARED ONE WITH DELAYS. A shared keyframe shifted by a delay shifts
   its END too, so the three would have DESELECTED one after another over 2.4s while the cursor sat
   still — and worse, each would still be lit from the previous cycle when the pointer set off
   again. They appear in sequence and clear together, which needs the clear written three times.
   🪤 t=0 IS MID-HOLD, bought with a −7s on every delay: page load is three cards picked and the
   pointer parked on the last one, not an empty grid. */
/* ⚠️ THE TIP PARKS BELOW THE TICK, NOT ON IT. Aimed at the tick's own centre the arrow sat over
   the check it had just made and hid it — the one thing the frame is there to show. 20 units lower
   clears the circle and still reads as having pressed it. */
/* ⭐ 3 × 2 SINCE 2026-09-11 (six engines). Tips: card 1's tick (164,97), card 2's (332,97), card 6's
   (500,246) — each parked 20 below. Same three presses, same clock as before. */
@keyframes vgCur {
  0%, 4%      { transform: translate(500px, 266px); }
  10%, 17%    { transform: translate(164px, 117px); }
  21.7%, 28%  { transform: translate(332px, 117px); }
  33.3%, 100% { transform: translate(500px, 266px); }
}
@keyframes vgOn1 { 0%, 9.6%  { opacity: 0; } 12%,   88% { opacity: 1; } 92%, 100% { opacity: 0; } }
@keyframes vgOn2 { 0%, 21.3% { opacity: 0; } 23.7%, 88% { opacity: 1; } 92%, 100% { opacity: 0; } }
@keyframes vgOn3 { 0%, 32.9% { opacity: 0; } 35.3%, 88% { opacity: 1; } 92%, 100% { opacity: 0; } }
.vgx-a .vg-cur { animation: vgCur 12s cubic-bezier(.5,0,.2,1) infinite; animation-delay: -7s; }
.vgx-a .vg-on1, .vgx-a .vg-on2, .vgx-a .vg-on3 { opacity: 0; animation-duration: 12s;
                                                 animation-timing-function: linear;
                                                 animation-iteration-count: infinite;
                                                 animation-delay: -7s; }
.vgx-a .vg-on1 { animation-name: vgOn1; }
.vgx-a .vg-on2 { animation-name: vgOn2; }
.vgx-a .vg-on3 { animation-name: vgOn3; }
.vgx-a .vg-en  { fill: #0B1B36; font-size: 17px; font-weight: 500; letter-spacing: -.02em; }

@media (prefers-reduced-motion: reduce) {
  /* ⛔ Parked on the finished choice — three picked, pointer resting on the last one it pressed.
     A still of a cursor mid-flight between two cards is a picture of nothing. */
  .vgx-a .vg-cur { animation: none; transform: translate(500px, 266px); }
  .vgx-a .vg-on1, .vgx-a .vg-on2, .vgx-a .vg-on3 { animation: none; opacity: 1; }
}

/* ── the answer tile ──────────────────────────────────────────────────────────
   ⭐ SEVENTH GROUND, and the darkest: slate-navy carrying a mint mass. It has no row-mate to be
   told apart from, so the job of its palette is tone rather than contrast — this is the evidence
   card, and evidence should read quiet. ⚠️ Still the same two brand hues, deepened; the one warm
   ground that was tried on this page was rendered, looked at, and thrown away. */
.ft-ans .ftv-tile {
  --g-a: #BFD3E6; --g-b: #4E7398; --g-c: #16283C;
  --m-1: #E4EEF8; --m-2: #0E1E2E; --m-3: #86D9C4; --m-4: #3E6D96;
}
.vgx-g .vg-b1 { animation-duration: 27s; animation-delay: -4s;  }
.vgx-g .vg-b2 { animation-duration: 20s; animation-delay: -15s; }
.vgx-g .vg-b3 { animation-duration: 31s; animation-delay: -9s;  }
.vgx-g .vg-b4 { animation-duration: 24s; animation-delay: -2s;  }

/* ⛔ Literal hex on a permanently-white card — `--ink` flips to #EEF1F7 in dark and the card does
   not. Same reasoning as `--plate`. */
/* ⚠️ 16, DOWN FROM 22 IN A VIEWBOX THAT ALSO HALVED — the prose renders at ~14px on a desktop card
   where it used to render at 21 (Pavel: "font size of ai response smaller too"). ⛔ It is now the
   same size as every other tile's type, which is the trade the smaller card buys: this card no
   longer reads larger than its neighbours, and on a phone it is texture like all of them. */
.vgx-g .vg-ex  { fill: #1B2740; font-size: 16px; letter-spacing: -.01em; }
.vgx-g .vg-eng { fill: #0B1B36; font-size: 16px; font-weight: 600; letter-spacing: -.02em; }
/* ⚠️ The question is the quiet half: it is the same on every slot, so it must not compete
   with the answer that changes under it. */
.vgx-g .vg-prm { fill: #6B7790; font-size: 16px; }
.vgx-g .vg-hi.is-riv { fill: #0B1B36; font-weight: 600; }
.vgx-g .vg-hi.is-you { fill: #206ECE; font-weight: 600; }
.vgx-g .vg-chip      { fill: #EEF1F6; }
.vgx-g .vg-chip.is-you { fill: #DBEAFB; }

/* ── the motion ───────────────────────────────────────────────────────────────
   ⭐⭐ ITS VERB IS ADVANCING: one answer at a time, in full, and the card steps to the next. It is
   not the prompts tile's deck (a 3D stack dealt toward the reader), not a cross-fade — a cross-fade
   between two blocks of prose is the slideshow this section already threw a tile out for — and not
   the continuous feed that was rejected earlier today. It holds, it slides one card height, it
   holds. ⛔ Do not shorten the holds to "show more": the hold IS the claim, which is that you can
   read the whole thing.
   ⭐ FOUR SLOTS, THREE ANSWERS, AND THE FOURTH IS THE FIRST AGAIN. The reel translates 0 → −308 →
   −616 → −924 and wraps; at −924 the window shows a copy of slot 0, which is exactly what it shows
   at 0, so the wrap has nothing to hide. ⛔ Three slots would need a two-slot reverse slide to get
   home, which reads as the answers running backwards.
   ⚠️ 224 IS THE CLIP WINDOW'S HEIGHT — the card below the question, not the whole card. Change
   either and all four numbers move with it.
   ⚠️⚠️ THE MARKS NO LONGER ANIMATE, and the note that used to live here is worth keeping for its
   rule: a shared keyframe with delays was correct for them because they are OFF at both 0% and
   100%, which is what makes each one's blind window run contiguously across the wrap — unlike the
   rank and engine tiles, whose elements have to be ON at the end and therefore need a keyframe
   each. It is gone because the reel already moves; a second motion under it is noise. */
@keyframes vgReel {
  0%,     28.3% { transform: translateY(0);      }
  33.3%,  61.7% { transform: translateY(-224px); }
  66.7%,  95%   { transform: translateY(-448px); }
  100%          { transform: translateY(-672px); }
}
.vgx-g .vg-reel { animation: vgReel 12s cubic-bezier(.6,0,.25,1) infinite; }

@media (prefers-reduced-motion: reduce) {
  /* ⛔ Parked on the first answer, whole. A reel stopped between two slots is two half answers. */
  .vgx-g .vg-reel { animation: none; transform: none; }
}

/* ── the geo tile ─────────────────────────────────────────────────────────────
   ⭐ EIGHTH GROUND: periwinkle → deep violet. Its row-mate is the teal schedule tile, which is as
   far from violet as this palette goes.
   ⭐⭐ IT IS A FIELD WITH A MENU FLOATING OVER IT, and the schedule tile is a label, a hairline and
   an inline radio list. Those are two different pictures of "choosing" and the difference is
   carried by the LIFT — the menu has its own shadow and sits off the card. ⛔ Flatten the menu into
   the card and this becomes the schedule tile in a different colour. */
.ft-geo .ftv-tile {
  --g-a: #D6D9F8; --g-b: #8E86E8; --g-c: #3B2E86;
  --m-1: #E8EAFE; --m-2: #2A1F6E; --m-3: #9FE0D8; --m-4: #7566DC;
}
/* 🪤 Folded at t=0: .32 / .61 / .13 / .78, against the schedule tile's .67 / .50 / .88 / .63.
   ⛔ The FOLD is what has to differ, not the delay — ease-in-out around one 50% keyframe puts
   progress p and 1−p at the identical position. */
.vgx-h .vg-b1 { animation-duration: 25s; animation-delay: -4s;  }
.vgx-h .vg-b2 { animation-duration: 18s; animation-delay: -12.5s; }
.vgx-h .vg-b3 { animation-duration: 30s; animation-delay: -2s;  }
.vgx-h .vg-b4 { animation-duration: 22s; animation-delay: -13.5s; }
/* ⛔ Literal hex on a permanently-white card — `--ink` flips in dark and the card does not. */
.vgx-h .vg-fv { fill: #0B1B36; font-size: 19px; font-weight: 500; letter-spacing: -.02em; }
.vgx-h .vg-mo { fill: #3B475F; font-size: 18px; }

/* ⭐⭐ ITS VERB IS CHOOSING ONE — the selection moves down the menu and the field follows it.
   ⛔ IT USED TO ACCUMULATE THREE TICKS AND COUNT THEM IN THE FIELD ("United States +2"). Pavel,
   2026-09-11: "we only allow selecting one country" — the wizard's market step is a single
   <select>, and a picture of a multi-select was a picture of a control the product does not have.
   ⭐ THE FIELD IS DERIVED, NOT TYPED: flag, name, band and tick for one country all run the SAME
   keyframe, so the field can never name a country the menu has not chosen. ⛔ Never give the field
   its own timing — two clocks for one quantity drift.
   ⚠️ Three keyframes, not one with delays: state 1 is ON at 0% and OFF at 100%, which is the case
   a shared keyframe cannot wrap (see the account tile's note for the case it can).
   ⚠️ The schedule tile also picks one of a list — with a RADIO on a flat card. This is a MENU with
   a lift, a band and a tick, so the two compositions stay apart even though both choose.
   🪤 t=0 IS MID-HOLD on the United States, bought with a −2s on every delay. */
@keyframes vgSel1 { 0%, 30% { opacity: 1; } 34%, 96% { opacity: 0; } 100% { opacity: 1; } }
@keyframes vgSel2 { 0%, 30% { opacity: 0; } 34%, 63% { opacity: 1; } 67%, 100% { opacity: 0; } }
@keyframes vgSel3 { 0%, 63% { opacity: 0; } 67%, 96% { opacity: 1; } 100% { opacity: 0; } }
@keyframes vgPop1 { 0%, 30% { opacity: 1; transform: none; } 34%, 96% { opacity: 0; transform: scale(.6); } 100% { opacity: 1; transform: none; } }
@keyframes vgPop2 { 0%, 30% { opacity: 0; transform: scale(.6); } 34%, 63% { opacity: 1; transform: none; } 67%, 100% { opacity: 0; transform: scale(.6); } }
@keyframes vgPop3 { 0%, 63% { opacity: 0; transform: scale(.6); } 67%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; transform: scale(.6); } }
.vgx-h .vg-sel1, .vgx-h .vg-sel2, .vgx-h .vg-sel3,
.vgx-h .vg-ff1,  .vgx-h .vg-ff2,  .vgx-h .vg-ff3,
.vgx-h .vg-v1,   .vgx-h .vg-v2,   .vgx-h .vg-v3   { opacity: 0; animation-duration: 12s; animation-timing-function: linear;
                                                    animation-iteration-count: infinite; animation-delay: -2s; }
.vgx-h .vg-ck1, .vgx-h .vg-ck2, .vgx-h .vg-ck3 { transform-box: fill-box; transform-origin: 50% 50%; opacity: 0;
                                                 animation-duration: 12s; animation-timing-function: cubic-bezier(.3,1.3,.5,1);
                                                 animation-iteration-count: infinite; animation-delay: -2s; }
.vgx-h .vg-sel1, .vgx-h .vg-ff1, .vgx-h .vg-v1 { animation-name: vgSel1; }
.vgx-h .vg-sel2, .vgx-h .vg-ff2, .vgx-h .vg-v2 { animation-name: vgSel2; }
.vgx-h .vg-sel3, .vgx-h .vg-ff3, .vgx-h .vg-v3 { animation-name: vgSel3; }
.vgx-h .vg-ck1 { animation-name: vgPop1; }
.vgx-h .vg-ck2 { animation-name: vgPop2; }
.vgx-h .vg-ck3 { animation-name: vgPop3; }

@media (prefers-reduced-motion: reduce) {
  /* ⛔ Parked on the United States — the first state, and the one the field opens on. */
  .vgx-h .vg-sel1, .vgx-h .vg-ff1, .vgx-h .vg-v1, .vgx-h .vg-ck1 { animation: none; opacity: 1; transform: none; }
  .vgx-h .vg-sel2, .vgx-h .vg-sel3, .vgx-h .vg-ff2, .vgx-h .vg-ff3,
  .vgx-h .vg-v2, .vgx-h .vg-v3, .vgx-h .vg-ck2, .vgx-h .vg-ck3 { animation: none; opacity: 0; }
}

/* ── the account tile ─────────────────────────────────────────────────────────
   ⭐ NINTH GROUND: pale cyan → deep teal, and it is the LIGHTEST tile in the section on purpose.
   ⚠️ It is the lightest tile of the nine and it closes the section on a lift. */
.ft-brands .ftv-tile {
  --g-a: #DDEFF6; --g-b: #86C8D8; --g-c: #2E7F8E;
  --m-1: #EDF9FC; --m-2: #1C5F70; --m-3: #A9EAD8; --m-4: #5FAFC6;
}
/* 🪤 Folded at t=0: .17 / .89 / .43 / .55, against the answer tile's .30 / .50 / .58 / .17. */
.vgx-i .vg-b1 { animation-duration: 23s; animation-delay: -2s;  }
.vgx-i .vg-b2 { animation-duration: 19s; animation-delay: -8.5s; }
.vgx-i .vg-b3 { animation-duration: 28s; animation-delay: -6s;  }
.vgx-i .vg-b4 { animation-duration: 21s; animation-delay: -16s; }
.vgx-i .vg-ws  { fill: #0B1B36; font-size: 19px; font-weight: 600; letter-spacing: -.02em; }
.vgx-i .vg-wsn { fill: #8A93A5; font-size: 15px; }
.vgx-i .vg-av  { fill: #FFFFFF; font-size: 14px; font-weight: 600; }
.vgx-i .vg-bnm { fill: #0B1B36; font-size: 19px; font-weight: 500; letter-spacing: -.02em; }
.vgx-i .vg-bnm.is-you { font-weight: 600; }
/* ⭐ Each row ends in the app's two quiet actions — `btn btn-q btn-xs` in
   app/app/account/brands/Brands.js, drawn as text: --ink-3, no ring, no fill.
   ⛔ IT USED TO BE "16 of 25" (Pavel, 2026-09-11: "not clear what these numbers are. just
   'manage', 'remove'"). A score with no column head on a card about HOLDING brands explained
   nothing; the actions say what the list is for. ⚠️ "Manage" is the site's word for the app's
   "Settings" button — same destination, plainer verb. */
.vgx-i .vg-bct { fill: #626E86; font-size: 15px; font-weight: 500; }
/* ⭐ The button's label is --ink, like `.btn-o`, not an accent: an outline button on this site is a
   SECONDARY action and colouring it would promote it over the list it sits above. */
.vgx-i .vg-addtx { fill: #0B1B36; font-size: 16px; font-weight: 500; letter-spacing: -.02em; }

/* ⭐⭐ ITS VERB IS ADDING, which nothing else here does: a fifth client lands under four that are
   already there, and the count in the header goes with it. ⭐ THE COUNT MOVES WITH THE ROW OR THE
   CARD LIES — the same rule the rank tile pays for its overtake with. ⛔ Two clocks for one
   quantity drift; these share one keyframe envelope.
   ⚠️ A shared keyframe with a delay IS correct for these three, unlike the geo rows above: they are
   OFF at both 0% and 100%, so each one's blind window runs contiguously across the wrap. The "4
   brands" label is the inverse and needs its own. */
@keyframes vgAdd { 0%, 18% { opacity: 0; transform: translateY(14px); } 26%, 86% { opacity: 1; transform: none; } 92%, 100% { opacity: 0; transform: translateY(14px); } }
@keyframes vgWas4 { 0%, 20% { opacity: 1; } 26%, 88% { opacity: 0; } 93%, 100% { opacity: 1; } }
/* ⚠️ The button does not move any more. It sits in the header, so a brand arriving at the bottom of
   the list never displaces it — which is also how the real screen behaves. */
.vgx-i .vg-new, .vgx-i .vg-n5 { opacity: 0; animation: vgAdd 11s cubic-bezier(.3,.9,.3,1) infinite; animation-delay: -6s; }
.vgx-i .vg-n4 { animation: vgWas4 11s linear infinite; animation-delay: -6s; }

@media (prefers-reduced-motion: reduce) {
  /* ⛔ Parked with the fifth brand present and the header agreeing — five, not four. */
  .vgx-i .vg-new, .vgx-i .vg-n5 { animation: none; opacity: 1; transform: none; }
  .vgx-i .vg-n4 { animation: none; opacity: 0; }
}

/* ── the share tile ───────────────────────────────────────────────────────────
   ⭐ TENTH GROUND: pale periwinkle → near-black indigo, the deepest of the ten. Its row-mate is the
   pale-cyan account tile, so the pair is the widest tonal contrast in the section and it closes the
   grid the way the answer/account pair used to.
   ⚠️ The two-hue palette is genuinely spent at ten tiles. What still separates them is which hue
   LEADS and how deep the third stop goes — ⛔ not a new hue. The one new hue this page tried was
   warm; it was rendered beside its row-mate, looked at, and thrown away. */
.ft-share .ftv-tile {
  --g-a: #BFCBEE; --g-b: #5A6FC8; --g-c: #1C2350;
  --m-1: #E2E8FB; --m-2: #14183A; --m-3: #8FD3FA; --m-4: #4457AE;
}
/* 🪤 Folded at t=0: .44 / .16 / .87 / .64, against the account tile's .17 / .89 / .43 / .55. */
.vgx-j .vg-b1 { animation-duration: 26s; animation-delay: -20.3s; }
.vgx-j .vg-b2 { animation-duration: 17s; animation-delay: -1.4s;  }
.vgx-j .vg-b3 { animation-duration: 29s; animation-delay: -16.4s; }
.vgx-j .vg-b4 { animation-duration: 20s; animation-delay: -6.4s;  }
/* ⛔ Literal hex on a permanently-white card — `--ink` flips in dark and the card does not. */
.vgx-j .vg-url  { fill: #3B475F; font-size: 13px; }
.vgx-j .vg-btx  { fill: #0B1B36; font-size: 15px; font-weight: 500; letter-spacing: -.02em; }
.vgx-j .vg-btx.is-done { fill: #206ECE; font-weight: 600; }
/* ⚠️ `.exp-item` is .875rem and `.exp-hint` .75rem with `margin-left: auto` — 16 and 13 here, hint
   flushed right. ⛔ Do not centre the hint; the app pushes it to the edge and the column of
   extensions down the right is what makes the menu scannable. */
.vgx-j .vg-mi  { fill: #3B475F; font-size: 16px; }
.vgx-j .vg-mih { fill: #98A3B7; font-size: 13px; }

/* ── the motion ───────────────────────────────────────────────────────────────
   ⭐⭐ ITS VERB IS A CONFIRMATION, and it is the only card whose motion is a piece of FEEDBACK
   rather than a change in the data: the button says Copy, you press it, it says Copied, it goes
   back. Nothing else on this page acknowledges anything. ⛔ Do not animate the three formats as
   well — a file list that flickers reads as loading, and the link is the subject.
   ⚠️ Two states on one envelope, inverted, so the button can never be blank or doubled. Both are
   OFF-then-ON-then-OFF across the wrap, which is the case where a shared keyframe with a delay is
   safe (see the answer tile's note for when it is not).
   🪤 t=0 IS MID-CONFIRMATION, bought with a −5s: page load is "Copied", because that is the frame
   that says the link went somewhere. */
@keyframes vgCopyOff { 0%, 22% { opacity: 1; } 26%, 62% { opacity: 0; } 66%, 100% { opacity: 1; } }
@keyframes vgCopyOn  { 0%, 22% { opacity: 0; } 26%, 62% { opacity: 1; } 66%, 100% { opacity: 0; } }
.vgx-j .vg-copy   { animation: vgCopyOff 9s linear infinite; animation-delay: -5s; }
.vgx-j .vg-copied { opacity: 0; animation: vgCopyOn 9s linear infinite; animation-delay: -5s; }

@media (prefers-reduced-motion: reduce) {
  /* ⛔ Parked on "Copy", not on "Copied": a still that claims a press nobody made is a lie about
     state, and the resting label is the one a reader arrives at. */
  .vgx-j .vg-copy   { animation: none; opacity: 1; }
  .vgx-j .vg-copied { animation: none; opacity: 0; }
}

/* ── the mock's three views (Pavel, 2026-09-12: "make history button clickable … same with settings")
   ⭐ `.ap[data-view]` shows ONE `.ap-view`; stage.js flips it from the nav. The nav items are real
   buttons now, so they take the app's `.bnav-item` look through the same `.ap-nav-item` rule — a
   button's default font and border are reset here and nowhere else.
   ⛔ Everything below is SHELL: the History and Settings screens reuse the app's own class names
   (trd-kpis, trd-tile, trd-spark, trd-legend, trd-multi, row, chip, seg, field, wz-top, wz-bar)
   with the handful of app/globals.css rules they need copied under `.ap` — the same rule as the
   report: no per-element font sizes, one zoom. */
.ap-view { display: none; }
.ap[data-view="latest"]   .ap-view[data-view="latest"],
.ap[data-view="history"]  .ap-view[data-view="history"],
.ap[data-view="settings"] .ap-view[data-view="settings"] { display: block; }
button.ap-nav-item { appearance: none; border: 0; background: none; font: inherit; text-align: left; cursor: pointer; width: 100%; }
button.ap-nav-item:hover { color: var(--ink); }
.ap-linkbtn { appearance: none; border: 0; background: none; font: inherit; cursor: pointer; }
.ap-fav-l { display: inline-grid; place-items: center; background: var(--well); box-shadow: none; font-size: .6875rem; font-style: normal; font-weight: var(--w-med); color: var(--ink-3); }
.ap-row .chip { flex: none; }
.ap-row .chip-btn { cursor: default; }
/* the seg as spans — the picture's controls do nothing, and .seg styles buttons and links only */
.ap-seg { margin-bottom: var(--s5); }
.ap-seg span { padding: 6px 12px; border-radius: 9px; font-size: .8125rem; font-weight: var(--w-med); color: var(--ink-3); line-height: 1; }
.ap-seg span.on { background: var(--page); color: var(--ink); box-shadow: var(--lift); }
.ap-sec-head-tight { margin-bottom: var(--s4) !important; }
.ap-sec-head-tight .ap-seg { margin: var(--s3) 0 0; }
/* history — app/globals.css .trd-* copied verbatim for the numbers that matter */
.ap-kpis { margin-top: var(--s7); }
.trd-kpis { display: grid; gap: var(--s5) var(--s6); grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.trd-tile p { margin: 0; }
.trd-tile-v { margin-top: var(--s2) !important; font-size: 2.25rem; line-height: 1; color: var(--ink); display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap; }
.ap-fig-face { font-family: var(--font-accent); font-weight: var(--w-accent); letter-spacing: -.01em; }
.trd-delta { font-family: var(--font-sans); font-size: .875rem; font-weight: var(--w-med); letter-spacing: 0; }
.trd-tile .t-xs { margin-top: var(--s2) !important; color: var(--ink-3); }
.ap-up { color: var(--ok); }
.ap-ink { color: var(--ink); }
.ap-ink2 { color: var(--ink-2); }
.ap-of { color: var(--ink-4); font-weight: 450; }
.ap-move { margin: var(--s5) 0 0; }
.trd-prose { max-width: 84ch; }
.ap-chart { width: 100%; }
.trd-legend { display: flex; gap: var(--s4); margin: var(--s2) 0 0; color: var(--ink-3); }
.trd-legend span { display: inline-flex; align-items: center; gap: var(--s2); }
.trd-legend i { display: inline-block; width: 10px; height: 10px; border-radius: 3px; flex: none; }
/* ⚠️ Three a row, not the app's auto-fill: six engines at this width fell 5 + 1, and a lone card
   under a row is a layout the app only produces at one exact width. Two rows of three is the same
   picture the app draws a little narrower. */
.trd-multi { display: grid; gap: var(--s4); grid-template-columns: repeat(3, 1fr); }
.trd-spark { position: relative; padding: var(--s4); background: var(--plane); border-radius: var(--r); }
.trd-spark-h { display: flex; align-items: center; gap: var(--s2); margin: 0 0 var(--s3); }
.trd-spark-h b { white-space: nowrap; }
.trd-spark-plot { position: relative; }
.trd-spark-dots { position: absolute; inset: 0; pointer-events: none; }
.trd-spark-dots i { position: absolute; width: 8px; height: 8px; margin: -4px 0 0 -4px; border-radius: 50%; background: var(--page); box-shadow: inset 0 0 0 2px var(--you); }
.ap-mk-15 { width: 15px; height: 15px; }
.ap-flip { align-items: flex-start; }
.ap-flip-e { flex: none; display: flex; align-items: center; gap: var(--s2); width: 128px; }
.ap-flip-q { flex: 1; min-width: 0; color: var(--ink-2); }
.ap-flip .chip { flex: none; }
.ap-chk { padding: var(--s5) 0; border-top: 1px solid var(--hair); display: block; }
.ap-chk-h { display: flex; align-items: baseline; gap: var(--s4); flex-wrap: wrap; }
.ap-chk-n { font-size: 1.05rem; color: var(--ink); flex: none; }
.ap-chk-e { margin-top: var(--s3); display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.ap-chk-eng { display: inline-flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.ap-chk-eng .ap-mk { width: 13px; height: 13px; }
.trd-open { color: var(--accent-ink); flex: none; }
/* settings — Settings.js's .setform, .wz-top, .row, .field, the two tiers, the sticky bar */
.ap-set { max-width: 760px; }
.ap-set .wz-top { display: flex; align-items: flex-start; gap: var(--s4); }
.ap-set .wz-top p { margin: 0; }
.ap-wz-title { font-size: 1.375rem; font-weight: var(--w-semi); letter-spacing: -.028em; margin: var(--s2) 0 0; }
.ap-field { display: flex; align-items: center; width: 100%; box-sizing: border-box; color: var(--ink); }
.ap-field-ph { color: var(--ink-4); }
.ap-qs { display: flex; align-items: center; gap: var(--s4); padding: var(--s4) 0; }
.ap-qs + .ap-qs { border-top: 1px solid var(--hair); }
.ap-qs-t { flex: 1; min-width: 0; font-size: .9375rem; color: var(--ink); }
.ap-x { color: var(--ink-4); flex: none; padding: 0 var(--s1); }
.ap-addrow { display: flex; gap: var(--s2); margin-top: var(--s5); }
.ap-addrow .ap-field { flex: 1; }
.ap-suggest { margin: var(--s3) 0 0; }
.ap-chips { display: flex; flex-wrap: wrap; gap: var(--s2); }
.ap-chipr { height: 30px; padding: 0 12px 0 7px; }
.ap-chipx { opacity: .8; }
.ap-tier { margin: var(--s5) 0 var(--s3) !important; }
.ap-tier-p { margin: 0 0 var(--s3) !important; }
.ap-bar { position: sticky; bottom: 0; z-index: 4; margin-top: var(--s7); padding-top: 28px;
          background: linear-gradient(to top, var(--page) calc(100% - 28px), transparent); }
.ap-bar-in { border-top: 1px solid var(--hair); background: var(--page); padding: var(--s5) 0; display: flex; align-items: center; gap: var(--s5); flex-wrap: wrap; }
.ap-bar .wz-acts { display: flex; align-items: center; gap: var(--s2); flex: none; margin-left: auto; }
@media (max-width: 860px) { .ap-nav button.ap-nav-item { width: auto; } }

/* ── /ai-instructions (2026-09-18) — the facts table and the "Open in …" row ──────────────────
   The facts table is a two-column list with a row header: the label column stays narrow and the
   value wraps, on a phone too, so it is a real `display: table` with a fixed layout rather than the
   scrolling block the Terms tables use. The "Open in" row is four outline buttons under the
   article, each opening an AI engine with "summarise this page" and the URL. */
.legal .facts { display: table; width: 100%; table-layout: fixed; }
.legal .facts th[scope="row"] { width: 9.5rem; text-transform: none; letter-spacing: 0; font-size: .9375rem; line-height: 1.55; color: var(--fg); font-weight: var(--w-med); padding: var(--s3) var(--s4) var(--s3) 0; border-top: 1px solid var(--line); border-bottom: 0; vertical-align: top; }
/* ⚠️ The article's FAQ is `#faq` with NO class — `.faq` is the home page's two-column grid and it swallowed the h3/p pairs on the first render. */
.legal #faq h3 { margin-top: var(--s6); }
.legal .facts td { padding-right: 0; }
@media (max-width: 480px) { .legal .facts th[scope="row"] { width: 6.5rem; } }
.open-in { display: flex; flex-wrap: wrap; gap: var(--s2); }
.open-in .btn { height: 36px; padding: 0 var(--s4); }
/* ⚠️ `.legal a` paints anchors accent-blue — restate the outline button's ink, as .price-cta does. */
.legal .open-in .btn-o { color: var(--ink); }

/* ── THE ARTICLE LAYOUT (2026-09-18, Pavel: "look at actual layout") ─────────────────────────────
   Redwatcher's arrangement, measured off its live comparison pages, in OUR palette and type:
   a two-column grid, the article on the left at reading width, a sticky rail on the right with
   "On this page" (numbered, the section in view marked by an accent bar) and "Discuss with AI"
   (engine marks as links). In the article: a category pill, the title, the Updated line, an
   "In short" box, the product card, a three-column "How we checked" box, then the sections.
   Every page built with the aeo-page skill uses it: `<article class="legal article">` holding
   `.art-head`, `.art-aside`, `.art-body`. Below 1024px the rail sits between the head and the body
   as an ordinary block, so a phone reads: title, in short, card, how we checked, contents, article.
   ⛔ `.legal` keeps the typography; this only places things. */
.legal.article { max-width: 1120px; display: grid; grid-template-columns: minmax(0, 1fr) 260px; grid-template-rows: auto 1fr; column-gap: var(--s9); row-gap: 0; align-items: start; }
.art-head { grid-column: 1; grid-row: 1; max-width: 760px; }
.art-body { grid-column: 1; grid-row: 2; max-width: 760px; }
.art-aside { grid-column: 2; grid-row: 1 / span 2; position: sticky; top: 96px; padding-top: var(--s3); }
.art-body > section:first-child { margin-top: var(--s8); }
@media (max-width: 1023px) {
  .legal.article { display: block; max-width: 760px; }
  .art-aside { position: static; margin-top: var(--s7); padding: var(--s5) 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
  .art-body > section:first-child { margin-top: var(--s7); }
}

/* the pill above the title, and the title itself a size down from the legal pages' */
.eyebrow { display: inline-block; margin: 0 0 var(--s4); padding: 5px 12px; border-radius: 999px; font-size: .75rem; font-weight: var(--w-med); letter-spacing: .06em; text-transform: uppercase; color: var(--accent-ink); background: color-mix(in srgb, var(--accent) 12%, transparent); }
.legal.article h1 { font-size: clamp(1.9rem, 4.2vw, 2.5rem); }
.legal.article h2 { font-size: 1.5rem; letter-spacing: -.02em; line-height: 1.2; }
.legal.article h3 { font-size: 1.0625rem; margin-top: var(--s6); }

/* boxes: "In short" and "How we checked" */
.callout { margin-top: var(--s6); padding: var(--s5) var(--s6); border: 1px solid var(--line); border-radius: var(--r); background: var(--raised, #fff); }
.callout-h { margin: 0 0 var(--s3); font-size: .75rem; font-weight: var(--w-med); letter-spacing: .06em; text-transform: uppercase; color: var(--fg-3); }
.callout.short { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }
.legal .callout.short .lead { margin-top: 0; }
.callout.short .lead strong { color: var(--fg); font-weight: var(--w-semi); }
.how-cols { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s5); }
.how-cols b { display: block; color: var(--fg); font-weight: var(--w-med); font-size: .9375rem; }
.legal .how-cols p { margin-top: var(--s2); font-size: .875rem; line-height: 1.55; }
@media (max-width: 640px) { .how-cols { grid-template-columns: 1fr; } .callout { padding: var(--s5); } }

/* the product card */
.cta-card { margin-top: var(--s5); padding: var(--s5) var(--s6); border: 1px solid var(--line); border-radius: var(--r); background: var(--raised, #fff); }
.cta-top { display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap; padding-bottom: var(--s4); border-bottom: 1px solid var(--line); }
.cta-top img { display: block; }
.cta-tag { font-size: .9375rem; color: var(--fg-2); }
.cta-row { display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap; margin-top: var(--s4); }
.legal .cta-row .btn-a { color: var(--on-cta); height: 44px; padding: 0 var(--s5); }
.cta-note { font-size: .9375rem; color: var(--fg-2); }

/* the rail */
.rail-h { margin: 0 0 var(--s3); font-size: .8125rem; font-weight: var(--w-med); letter-spacing: .08em; text-transform: uppercase; color: var(--fg-3); }
.legal .toc ol { margin: 0; padding: 0; list-style: none; counter-reset: toc; }
.legal .toc li { counter-increment: toc; margin: 0; padding: 6px 0 6px 26px; position: relative; font-size: .9375rem; line-height: 1.4; border-left: 2px solid transparent; }
.legal .toc li::before { content: counter(toc) "."; position: absolute; left: 4px; top: 7px; font-size: .8125rem; line-height: 1.4; color: var(--fg-3); }
.legal .toc li.is-active { border-left-color: var(--accent-ink); }
.legal .toc li.is-active a { color: var(--fg); font-weight: var(--w-med); }
.legal .toc a { color: var(--fg-2); font-weight: 400; text-decoration: none; }
.legal .toc a:hover { color: var(--accent-ink); }
.discuss { margin-top: var(--s6); padding-top: var(--s5); border-top: 1px solid var(--line); }
.legal .open-in { margin: 0; padding: 0; list-style: none; }
.legal .open-in li { margin: 0; padding: 5px 0; font-size: .9375rem; }
@media (min-width: 1024px) { .art-aside .open-in { display: block; } }
.legal .open-in a { display: inline-flex; align-items: center; gap: 10px; color: var(--fg-2); font-weight: 400; text-decoration: none; }
.legal .open-in a:hover { color: var(--fg); }
.d-ico { display: inline-flex; width: 22px; height: 22px; align-items: center; justify-content: center; border-radius: 6px; background: var(--plate); box-shadow: inset 0 0 0 1px var(--line); }
.d-ico svg { width: 14px; height: 14px; display: block; }
@media (max-width: 1023px) {
  .art-aside { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--s6); }
  .discuss { margin-top: 0; padding-top: 0; border-top: 0; }
}
@media (max-width: 640px) { .art-aside { grid-template-columns: 1fr; } .discuss { margin-top: var(--s5); padding-top: var(--s5); border-top: 1px solid var(--line); } }

/* the footnote line under the article */
.legal .fine { margin-top: var(--s7); padding-top: var(--s5); border-top: 1px solid var(--line); font-size: .875rem; color: var(--fg-3); line-height: 1.6; }

/* figures on article pages: a real screenshot or a measured card at column width, a hairline frame, a dated caption */
.legal .fig { margin: var(--s5) 0 0; }
.legal .fig img { display: block; width: 100%; height: auto; border: 1px solid var(--line); border-radius: var(--r); background: #fff; }
.legal .fig figcaption { margin-top: var(--s3); font-size: .875rem; line-height: 1.5; color: var(--fg-3); }

/* ── Article tables (2026-09-18, the ChatGPT guide's SEO/AEO pass) ──────────────────────────────
   Two kinds, both plain <table> so a crawler reads them as rows. `.cmp` is the side-by-side
   (a row header and two columns): the row header reads like the .facts label, not the uppercase
   column header, and the corner cell's text is for screen readers only. `.price-tbl` is reused as
   is for the credits table. On a phone `.legal .tbl` already scrolls inside its own box. */
.legal .cmp th[scope="row"] { text-transform: none; letter-spacing: 0; font-size: .9375rem; line-height: 1.55; color: var(--fg); font-weight: var(--w-med); padding: var(--s3) var(--s4) var(--s3) 0; border-top: 1px solid var(--line); border-bottom: 0; vertical-align: top; white-space: nowrap; }
.legal .cmp td { min-width: 11rem; }
.vh { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
/* ⚠️ On a phone the third column (the check, the one that matters) fell off the right edge behind an
   inner scroll with no affordance (2026-09-18 render at 390px). Under 640px the rows stack: the row
   header, then each cell prefixed by its column's name from `data-h`. The <thead> stays in the DOM
   for crawlers and is hidden here. */
@media (max-width: 640px) {
  .legal .cmp, .legal .cmp tbody, .legal .cmp tr { display: block; overflow: visible; }
  .legal .cmp thead { display: none; }
  .legal .cmp tr { padding: var(--s3) 0; border-top: 1px solid var(--line); }
  .legal .cmp th[scope="row"] { display: block; border: 0; padding: 0 0 var(--s1); white-space: normal; }
  .legal .cmp td { display: block; border: 0; padding: 2px 0; min-width: 0; }
  .legal .cmp td::before { content: attr(data-h) ": "; color: var(--fg-3); }
}

/* ── Screens open full size on click (2026-09-18, Pavel: "just open full on click") ─────────────
   The figure links to the full-size file; lightbox.js draws it over the page. Without the script
   the link opens it in a new tab, so the page is complete without JavaScript. */
.legal .fig a.zoom { display: block; cursor: zoom-in; }
.lb { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(11, 27, 54, .82); cursor: zoom-out; }
.lb img { max-width: 100%; max-height: 100%; width: auto; height: auto; border-radius: var(--r); box-shadow: 0 24px 80px rgba(0, 0, 0, .45); background: #fff; }
