/* =========================================================================
   Fifth Dimension — Solo D2C

   Display: FK Screamer Bold (licensed brand face) · Body: IBM Plex Mono.

   The palette, the corner-radius ladder and the elevation levels are all
   declared in the :root block below and nowhere else. This header used to
   restate the four brand hexes and had already gone stale on one of them,
   which is the whole argument for the two gates:

     bun run lint:radius   every corner resolves to a rung, and no hand copy
                           of a rung value has drifted
     bun run lint:tokens   no colour literal outside :root, and no var()
                           fallback has drifted from the token it stands in for

   The reference these pages track is access-ui's My 5D surface as a paid buyer
   sees it — SOLO_CUSTOMER + SOLO_UI_2 + FULL_AGENT_ELLIE_V2 — because that is
   the screen on the other side of the pay button.
   ========================================================================= */

@font-face {
  font-family: "FK Screamer";
  src: url("/fonts/FKScreamer-Bold.woff2") format("woff2");
  font-weight: 100 900; /* one bold file serves every weight we request */
  font-style: normal;
  font-display: swap;
}

:root {
  /* Every colour in the app is declared here and nowhere else. A raw hex or a
     literal rgba() below this block is a lint failure (`bun run lint:tokens`),
     for the reason access-ui gives: a hardcoded value opts out of every future
     change to the palette, and nothing tells you it did. */
  /* The three brand colours, unchanged: this is the brand-vivid register, which
     the marketing pages own. The product runs a quieter accent (#ce4c0b) off the
     same brand — do not copy that one here. */
  --orange: #f35202;
  --orange-dark: #c94301;
  --orange-tint: #fce3d4;
  --ink: #201d1d;
  /* Only for the dark button's hover, where --ink is already the resting fill. */
  --ink-deep: #000000;
  --grey: #e2e2e2;
  --white: #ffffff;

  /* Surfaces, cooled. A paid buyer lands on My 5D with FULL_AGENT_ELLIE_V2 on,
     which IS access_ui_2026_redesign (customer-access features.py sets one from
     the other), and that theme mutes access-ui's warm tan surface ramps to a cool
     neutral while PRESERVING each stop's lightness, so contrast and layout do not
     move. These three are the same ramps' analogues here, cooled the same way and
     at the same lightness they had warm:

       --paper       #f7f5f1 → hsl(214 11% 96%)   was hsl(34 27% 96%)
       --line        #cbcac8 → hsl(214  4% 79%)   was hsl(40  3% 79%)
       --line-soft   #ecebea → hsl(214  4% 92%)   was hsl(30  4% 92%)

     The TEXT greys below are deliberately NOT cooled: the redesign re-derives only
     the soft-sand and ivory-lace surface ramps, and leaves charcoal-green — which
     is what --body / --muted / --muted-2 correspond to — exactly as it was. */
  --paper: hsl(214 11% 96%);
  --line: hsl(214 4% 79%);
  --line-soft: hsl(214 4% 92%);

  --body: #3d3938;
  --muted: #5c5856;
  --muted-2: #837f7c;

  /* Text on the ink surfaces — the hero and pay cards, the proof strip, the
     footer. --white and --paper are the loud end of that set; these two are the
     quiet end, and they are separate tokens because a light-on-dark neutral
     cannot be borrowed from the ramp above without losing contrast. */
  --on-ink-soft: #cfcdca;
  --on-ink-muted: #a9a7a4;

  /* The sandbox bar's own colour, deliberately NOT --orange: a brand-orange bar
     reads as part of the page, and the whole point of that bar is that it cannot
     be mistaken for one. */
  --warn: #b3540e;
  --danger: #b3261e;

  --display: "FK Screamer", "Arial Narrow", "Bebas Neue", sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", monospace;

  --maxw: 1120px;
  /* Fixed Solo navigation height. Both the main landing and the regional
     promotion slot reserve this exact space below the out-of-flow topbar. */
  --topbar-flush-h: 43px;

  /* Corner radius — the permanence ladder. Copied from access-ui's
     `src/index.css` (application/access-ui in the m5 monorepo), which is the
     source of truth for the product; these pages are the same product's front
     door and run the same corners.

     Radius encodes what an element IS, not how round it looked good to someone:
     the more permanent a thing is, the squarer it is.

       --radius-flush      0px  meets an edge on at least one side, or is clipped
                                by an already-rounded parent. Geometry, not style.
       --radius-structure  1px  permanent frame: wells, panels, inset tiles
       --radius-control    2px  things you operate: buttons, inputs, chips, badges
       --radius-surface    4px  content that arrives and leaves: cards, list rows
       --radius-overlay    6px  floats above everything: dialogs, the consent card

     6px is the ceiling. Nothing here is rounder except a deliberate circle (50%)
     or a pill. Two invariants hold everywhere: a child is never rounder than its
     parent, and nested things step DOWN — overlay → surface → control as you go
     inward.

     Rescaling the ladder is an edit to these five lines and nothing else,
     because every call site names a rung instead of a size. `bun run lint:radius`
     fails on a corner that resolves to no rung, and on a rung value that has
     drifted from the hand copies in AGENTS.md, CLAUDE.md and the wiki. */
  --radius-flush: 0px;
  --radius-structure: 1px;
  --radius-control: 2px;
  --radius-surface: 4px;
  --radius-overlay: 6px;

  /* Elevation, as My 5D uses it under SOLO_UI_2. Three levels and each one means
     something, which is the whole reason the second arrangement dropped the big
     soft drop shadow it inherited:

       --shadow-xs       a card that is just a card. Barely there.
       --shadow-offset   the ONE object on the page that is being recommended, or
                         bought. A hard offset in brand orange, not a soft glow —
                         so the primary thing is unmistakably the primary thing.
       --shadow-overlay  something floating temporarily above the page.

     The offset is LAYOUT, not decoration: an element carrying it must reserve
     --shadow-offset-gap on its right and bottom, or the offset is clipped by
     whatever is next to it. access-ui writes the same pair as `mr-1.5 mb-1.5`
     beside `shadow-[6px_6px_0_…]`. */
  --shadow-xs: 0 1px 2px 0 color-mix(in srgb, var(--ink) 5%, transparent);
  --shadow-offset: 6px 6px 0 var(--orange);
  /* The same hard offset in INK, and it is a second token rather than a reuse of
     the one above because the one above means something this does not. Orange is
     "this is the object being bought" — one per page, and on the pay step that is
     the plan card. This is "you picked this", worn by however many options a
     buyer ticks, which is why it cannot be the recommendation colour: ten orange
     offsets on S02 would say ten things are the primary action.
     Same geometry, so it reserves the same --shadow-offset-gap. */
  --shadow-offset-ink: 6px 6px 0 var(--ink);
  --shadow-offset-gap: 6px;
  --shadow-overlay: 0 8px 24px color-mix(in srgb, var(--ink) 8%, transparent);

  /* Two declarations, not one: a browser without color-mix() drops the second and
     keeps the flat ring, so the focus state is never invisible. Losing a focus
     ring is the one degradation on this site that is an accessibility failure
     rather than a cosmetic one. */
  --focus: 0 0 0 3px var(--orange);
  --focus: 0 0 0 3px color-mix(in srgb, var(--orange) 35%, transparent);
}

* { box-sizing: border-box; }

/* The `hidden` attribute only hides an element because the USER-AGENT stylesheet
   says `[hidden] { display: none }`, and an author rule beats the user-agent
   sheet on cascade ORIGIN, before specificity is even consulted. So any class
   that sets `display` silently un-hides its element.

   What that shipped: `.taken` (display: flex) and `.btn` (display: inline-flex)
   left the already-registered CTA showing from the moment a visitor opened the
   questionnaire, before anyone had typed an address. Not on the landing page —
   .flow is display:none until it opens — and not as a readable notice either,
   because `data-email-taken-text` is empty in the markup and only renderTaken
   fills it. What a visitor actually saw was a bare orange button sitting between
   the email field and the pitch, with no sentence above it. Nothing caught it:
   the markup and the JS were both right, tests asserted the attribute was there,
   and it was there, and it did nothing.

   !important because the point is to outrank exactly those class rules. A plain
   `[hidden] { display: none }` here would tie `.taken` on specificity — an
   attribute selector and a class selector are both (0,1,0) — and lose on source
   order, since .taken is 700-odd lines below. It would also lose outright to
   anything more specific: `.step.is-active { display: flex }` is (0,2,0) and
   would win from anywhere in the sheet. Hiding an element has to be the one
   instruction here that a later or heavier `display` cannot quietly overturn.

   Two consequences worth knowing before removing this. It beats an inline
   `style.display` too, so revealing a hidden element must mean clearing the
   attribute, not writing a style — which is what everything in public/js
   already does. And solo.js's refused email step depends on it: `.about` and
   `.step__nav` both set display:flex, so without this rule setting `hidden` on
   them is inert and a refused buyer keeps the Next button.

   views/partials/consent-banner.ejs keeps its own `#consent-banner[hidden]` copy
   on purpose: that partial ships self-contained styles so a failed styles.css
   degrades the consent card rather than breaking it, and its `#id` + attribute
   selector wins on specificity without needing !important. */
[hidden] { display: none !important; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--grey);
  font-family: var(--mono);
  color: var(--ink);
  font-size: 15px;
  line-height: 1.6;
  text-wrap: pretty;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { margin: 0; }

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

img, svg { max-width: 100%; }

.display {
  font-family: var(--display);
  text-transform: uppercase;
  font-weight: 400;
  letter-spacing: 0.01em;
  line-height: 0.9;
}

.eyebrow {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--orange);
}

.mono-label {
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted-2);
}

/* --- reusable buttons ------------------------------------------------------ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 15px 26px;
  border-radius: var(--radius-control);
  border: 2px solid var(--orange);
  background: var(--orange);
  color: var(--white);
  font-family: var(--mono);
  font-size: 13px;
  /* 700, not 500, because the base button is the ORANGE one: IBM Plex Mono in
     white on --orange is 3.49:1, and the extra weight is what makes it hold up at
     13px. It does not make it WCAG AA — 4.5:1 needs a darker fill or ink text, see
     AGENTS.md §5 — so do not read this as the box being ticked.
     The ghost and dark variants go back to 500 below; both are high-contrast
     already and have no legibility problem to solve. */
  font-weight: 700;
  letter-spacing: 0.08em;
  line-height: 1;
  text-transform: uppercase;
  cursor: pointer;
  transition: transform 0.12s ease, background 0.12s ease, box-shadow 0.12s ease;
}
.btn:hover { background: var(--orange-dark); border-color: var(--orange-dark); color: var(--white); transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn:focus-visible { outline: none; box-shadow: var(--focus); }

/* Muted Next, for a question with no answer yet. solo.js sets the real `disabled`
   attribute on the multi-select steps' Next until at least one option is ticked,
   so the button is unclickable and unfocusable in the browser's own terms rather
   than merely looking spent — and this rule exists so that state is visible,
   instead of a press that silently does nothing.

   `transform: none` is not redundant: :hover still matches a disabled button, and
   `.btn:hover` lifts it by a pixel. Note that `.btn:hover` and `.btn[disabled]`
   are BOTH (0,2,0) — a pseudo-class and an attribute selector weigh the same — so
   nothing here wins on specificity. It wins on source order, which is the only
   reason this block has to stay below the :hover and :active rules above. */
.btn[disabled] {
  background: var(--grey);
  border-color: var(--grey);
  color: var(--muted-2);
  cursor: not-allowed;
  transform: none;
}

.btn--ghost {
  background: transparent;
  color: var(--ink);
  border-color: var(--line);
  font-weight: 500;
}
.btn--ghost:hover { background: transparent; color: var(--ink); border-color: var(--ink); }

.btn--dark { background: var(--ink); border-color: var(--ink); font-weight: 500; }
.btn--dark:hover { background: var(--ink-deep); border-color: var(--ink-deep); color: var(--white); }

.btn--lg { padding: 17px 30px; font-size: 15px; }

/* --- ruled groups and rows -------------------------------------------------
   My 5D's two structural workhorses under SOLO_UI_2, so a page here is built
   from the same parts as the product a buyer is about to be inside.

   .rule-head — a quiet uppercase label with a hairline under it, then content.
     The second Solo arrangement replaced a pair of cards with exactly this
     ("two lines rather than two cards"): a card says "attend to me", and a
     standing fact is not competing for attention. Reach for this, not a
     bordered card, when the content is a list of facts rather than an object.

   .row-list / .row-item — a list of things you can open. Rows, not cards: a
     border and an indent, with the hover on the row's own fill. Two lines, the
     quiet one on top — the detail is what makes a row worth opening, and the
     name alone rarely says why.

   Both are deliberately shadow-less and 2px at most; the elevation ladder above
   is for objects, and neither of these is one. */
.rule-head {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted-2);
  margin: 0 0 10px;
}
.rule-head + * { border-top: 1px solid var(--line-soft); padding-top: 12px; }
/* Pull "Your answers" down onto its hairline in the last-step summary, so the
   label reads as the divider's caption rather than a line floating above it. */
.pay__summary .rule-head { margin-bottom: 4px; }

.row-list { display: flex; flex-direction: column; list-style: none; margin: 0; padding: 0; }
.row-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 8px;
  border-radius: var(--radius-control);
  color: var(--ink);
  transition: background 0.12s ease;
}
.row-item:hover { background: color-mix(in srgb, var(--paper) 60%, transparent); color: var(--ink); }
.row-item:focus-visible { outline: none; box-shadow: var(--focus); }
.row-item__body { min-width: 0; flex: 1; }
.row-item__detail { display: block; font-size: 11.5px; color: var(--muted-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row-item__name { display: block; font-size: 13.5px; font-weight: 600; }
.row-item__meta { flex: none; font-size: 11.5px; color: var(--muted-2); }

/* The quiet way out of a ruled group — "view all", "manage". An underline at
   text size, never a button: it is a door, not the thing to do. */
.linkish {
  font-size: 11.5px;
  color: var(--muted-2);
  text-decoration: underline;
  text-underline-offset: 2px;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font-family: var(--mono);
  transition: color 0.12s ease;
}
.linkish:hover { color: var(--ink); }
.linkish:focus-visible { outline: none; border-radius: var(--radius-control); box-shadow: var(--focus); }

/* =========================================================================
   Top bar
   ========================================================================= */
.topbar {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Declared once and read by BOTH bars. .topbar--flush restates the padding
     because its vertical value differs, and a second literal there is how the
     narrow-viewport rule at the end of this section came to apply to five
     pages and not the sixth. The variable is the fix for that class of bug,
     not a style: whatever sets it, both bars follow. */
  --bar-pad-x: 32px;
  padding: 16px var(--bar-pad-x);
  /* The two halves are fixed-width objects that must never touch. Without this
     they sit flush at narrow widths and the button reads as sitting ON the
     lockup — which is what it looked like, even though the boxes never
     actually overlapped. */
  gap: 16px;
  background: color-mix(in srgb, var(--grey) 85%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}

/* PENG-2056 — the Solo landing bar only. Fixed rather than sticky, and flat:
   the artboard removes the border, the blur and the grey wash and paints the
   bar --paper, the same ground as the hero, so it reads as part of the page
   until you scroll past the fold.

   This is a MODIFIER and not a change to .topbar because five other views
   (terms, aup, dpa, landing, landings-index) share that class and none of them
   reserves height for an out-of-flow bar. Taking .topbar out of flow globally
   slides a fixed bar over the first paragraph of every legal page.

   z-index sits above the page but below .flow's overlay (z 100) and the
   security dialog (z 120): the bar must never float over a modal. */
.topbar--flush {
  position: fixed;
  left: 0;
  right: 0;
  z-index: 90;
  padding: 4px var(--bar-pad-x);
  background: var(--paper);
  backdrop-filter: none;
  border-bottom: none;
}
.wordmark {
  display: flex;
  align-items: center;
  gap: 12px;
  color: var(--ink);
  /* The lockup gives before the button does: min-width:auto would otherwise
     hold this at the image's intrinsic width and push the bar into overflow. */
  min-width: 0;
}
/* The full brand lockup — emblem plus wordmark in one asset, which is why there is
   no `.wordmark img { width: 30px; height: 30px }` here any more. That rule sized a
   square emblem beside a text node, and being type-and-class it outranked this
   class-only one, so it silently squashed the ~6.3:1 lockup into a blob.

   Sized by HEIGHT with width:auto: what has to line up with the nav beside it is
   the cap height, and that does not move when the aspect ratio does. */
/* object-fit is the load-bearing half. Sized by height alone, the flex layout
   was free to shrink this box while the DEFAULT object-fit: fill stretched the
   artwork to match — at 320px the 6.27:1 lockup rendered at 5.34:1, a brand
   mark squashed 15% narrower with nothing to say so. `contain` makes the
   asset scale instead of distort, and left object-position keeps the emblem on
   the gutter while it does. This is the floor under the breakpoint below, not
   a replacement for it: the sizes there are what stop it being reached. */
.wordmark__lockup {
  display: block;
  width: auto;
  height: 26px;
  max-width: 100%;
  object-fit: contain;
  object-position: left center;
}
.wordmark__lockup--sm { height: 22px; }
/* Never the half that shrinks: a squeezed button wraps its label to two lines
   and grows the bar instead of narrowing it. */
.topbar__right { display: flex; align-items: center; gap: 22px; flex: none; }
/* Still used by views/landing.ejs ("All landing pages"). The Solo bar dropped
   its nav in PENG-2056, but this is not dead — check landing.ejs before you
   delete it. */
.topbar__link { color: var(--ink); font-size: 12px; letter-spacing: 0.04em; }
.topbar__link:hover { color: var(--orange); }
@media (max-width: 720px) { .topbar__link { display: none; } }
/* The bar's button is tighter than the body default. Still --radius-control via
   .btn, like everything else you operate. Only "/" wears it. */
.btn--bar { padding: 9px 18px; }

/* Every bar button, on all seven views — .btn--bar is "/" alone and the other
   six put a plain .btn up there, so a rule written against the modifier fixes
   one page and leaves the rest. nowrap because "Start now" breaking after
   "Start" is what turned a 31px bar into a 56px one at 320px, and a two-line
   label beside a one-line lockup is the thing that read as an overlap. */
.topbar__right .btn { white-space: nowrap; }

/* Narrow viewports, every page with a bar (PENG-2056 left this at one size).
   320px is the floor worth designing for — it is the smallest width still in
   real use, and at the 32px gutter and full-size type the lockup and the
   button together wanted 327px of a 320px bar. Everything steps down together
   so both objects stay at their own proportions rather than one of them
   absorbing the shortfall:
     16px gutters   +64 -> +32 of the width back
     20px lockup    163px -> 126px
     11px button    ~130px at one line on "/", less on the "See Solo" bars
   which lands at 284px inside 320px, with the 16px gap still between them.
   Checked at 320, 360 and 561 on /, /beta-offer, /landing/:slug, /landings
   and the legal pages. */
@media (max-width: 560px) {
  .topbar { --bar-pad-x: 16px; }
  .wordmark__lockup { height: 20px; }
  .wordmark__lockup--sm { height: 18px; }
  .topbar__right .btn { padding: 9px 12px; font-size: 11px; }
}

/* =========================================================================
   Marketing landing (the "learn more" page)
   ========================================================================= */
.landing { }

/* Pairs with .topbar--flush (PENG-2056). That bar is out of flow, so the page
   under it has to reserve its height or the hero starts underneath it.

   The bar's height is set by its TALLEST child, and that is .btn--bar, not the
   lockup: 13px font at line-height 1, + 2 × 9px padding, + 2 × 1px border =
   35px, + the bar's own 2 × 4px padding = 43px. (An earlier version of this
   comment measured the 26px lockup and reserved 34px, which left the hero 9px
   under the bar — invisible only because .hero carries its own 84px top
   padding.) Change .btn--bar's padding or font-size, change this.

   Scoped like the bar is, so no other view inherits a reservation for a bar it
   does not have. */
.landing--flush { padding-top: var(--topbar-flush-h); }

.hero {
  position: relative;
  padding: 84px 32px 92px;
  overflow: hidden;
  border-bottom: 1px solid var(--line);
  background: var(--paper);
}
/* The brand ground: 5D's painted orange texture, the same asset and the same
   role it has on the product's sign-in screen — which is the very next page a
   buyer sees after paying, so the two have to be the same material.

   It used to be two stacked radial-gradients standing in for exactly this image
   (the old comment here asked for it by name). Gradients were the right stand-in
   then and are the wrong answer now: a buyer is provisioned FULL_AGENT_ELLIE_V2,
   which IS access_ui_2026_redesign, and that theme's whole treatment of brand
   gradients is to remove them — the shimmer fills lose their background-image and
   every gradient stop is pointed at its own `from` colour. Painted texture is
   what the redesign put in their place.

   The falloff is a MASK, not a colour ramp, which is why it survives that rule:
   what fades is the image's own alpha, and no brand colour is being interpolated.

   The file is access-ui's public/brand/orange-texture.jpg re-encoded to 1200px /
   138KB. The original is 2400px / 2MB — fine behind auth, far too heavy for the
   hero of a page that paid traffic lands on. Re-encode from that source rather
   than editing this copy, so the brand asset stays single-source. */
.hero__wash {
  position: absolute;
  inset: -20% -10% auto auto;
  width: 60vw;
  height: 60vw;
  max-width: 720px;
  max-height: 720px;
  background-image: url("/img/orange-texture.jpg");
  background-size: cover;
  background-position: center;
  /* `black` rather than a token: this is the mask's alpha carrier, not a colour
     anyone sees, so pointing it at a brand token would be a lie. */
  mask-image: radial-gradient(closest-side, black 35%, transparent 78%);
  pointer-events: none;
}
.hero__inner {
  position: relative;
  max-width: var(--maxw);
  margin: 0 auto;
  display: grid;
  /* minmax(0, ...), not bare fr: an `fr` track's automatic minimum is its
     content's min-content width. The copy column has no min-width guard (only
     .hero__aside does), so the ja/ko title's `white-space: nowrap` below —
     which forces its min-content up to the width of a whole unwrapped title
     line instead of just its widest word — was inflating this track past its
     1.35fr share and squeezing the card/Ellie column narrower than in every
     other locale. Capping both tracks at 0 keeps the 1.35:1 split fixed
     regardless of what either column's content wants to be. */
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: 56px;
  /* start, not center (2026-08-19). With `center`, the right column (price
     card + Ellie + the hand-drawn callout) is centred against the ROW's
     height, which is set by whichever column is taller. In English the two
     columns are close enough in height that this is invisible. A longer
     translated title (e.g. ja-jp's, which wraps one line further than the
     English original) makes the LEFT column much taller — and centering
     splits that extra height so the left column's paragraph slides down by
     nearly the full difference while the right column only slides down by
     half of it. The callout, positioned as a fixed offset within the right
     column, ends up overlapping the paragraph text in the left one: visible,
     literal text-on-text collision, not just a translation gap. `start`
     pins both columns to the row's top regardless of how tall either one
     gets, so they never drift apart like this. Checked against "/" at
     several widths — the two columns are close enough in height there that
     this is not a visible change for English. */
  align-items: start;
}
.hero__title {
  font-family: var(--display);
  text-transform: uppercase;
  font-size: clamp(56px, 8.5vw, 128px);
  line-height: 0.82;
  letter-spacing: 0.005em;
  margin: 14px 0 22px;
}
/* FK Screamer's tight Latin setting overlaps the taller fallback glyph boxes
   used by the Japanese and Korean headlines. Keep the compact Latin lockup,
   but give CJK title rows enough leading to remain visually distinct. */
html:lang(ja) .hero__title,
html:lang(ko) .hero__title {
  line-height: 1.05;
}
/* Keep both CJK lockups on their two authored rows at desktop widths. Their
   fallback glyphs are materially wider than FK Screamer's Latin forms.
   The 6.4vw preferred value scales off the raw viewport, but .hero__inner's
   own width stops growing at --maxw (1120px, ~1280px viewport and up); past
   that point vw kept inflating the font while the nowrap line's column had
   already frozen, and only the (now-removed) automatic min-content growth on
   .hero__inner's grid track was hiding the overflow, by stealing width from
   the card/Ellie column. The ceiling has to fit the longer of the two
   authored lines at the frozen column width, not just look right at whatever
   viewport this was last tuned against: ja-jp's line needs the cap at ≤81px,
   ko-kr's at ≤87px, measured at that frozen width. */
@media (min-width: 721px) {
  html:lang(ja) .hero__title,
  html:lang(ko) .hero__title {
    font-size: clamp(56px, 6.4vw, 80px);
    white-space: nowrap;
  }
  /* The FREE headline is a longer sentence than the paid one ("次のシナリオ分析を。" /
     "다음 시나리오 분석을 시작하세요." vs the paid "Ellieが推論する。"), so its nowrap first row
     overran the Meet-Ellie callout at the frozen column width. A lower cap keeps that
     longest row clear of the callout while staying on its two authored rows. */
  html:lang(ja) .hero__title--free {
    font-size: clamp(44px, 5.1vw, 64px);
  }
  html:lang(ko) .hero__title--free {
    font-size: clamp(36px, 3.7vw, 48px);
  }
}
html:lang(ja) .hero__title-brand,
html:lang(ko) .hero__title-brand {
  font-size: 1.1em;
}
.hero__title .accent { color: var(--orange); }
.hero__lede {
  font-size: 17px;
  line-height: 1.6;
  color: var(--body);
  max-width: 520px;
  margin: 0 0 30px;
}
.hero__cta { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }

/* --- hero proof block (2026-08-17 artboard) ------------------------------------
   The performance figure and the customer strip. Both are signed-off claims
   rather than approved-set copy — see the comment in views/solo.ejs — so if this
   block is ever removed, remove the asset with it rather than leaving an <img>
   for the next person to point at something.
   The logos take `width: 100%` up to a `max-width` of 460px, which is the width
   the artboard draws the strip at — not a sharpness ceiling. The asset is
   1067px wide, so it is always downscaled here and upscaling was never
   reachable; if you widen this block, 460px is a layout decision to re-take
   against the artboard, not a limit the file imposes. */
.hero__proof { margin-top: 36px; }
.hero__proof-line { margin: 0 0 14px; font-size: 13.5px; line-height: 1.5; color: var(--body); }
.hero__proof-figure { position: relative; display: inline-block; font-weight: 600; color: var(--ink); }
.hero__proof-logos { display: block; width: 100%; max-width: 460px; height: auto; }

/* The primary object on the page, so it wears the offset. My 5D's second
   arrangement gives its recommendation card an ink border and a hard offset
   shadow "so the thing Ellie is actually recommending is the only object on the
   page that sits above it"; here the thing being recommended is the plan. The
   card is already ink, so the offset carries the orange on its own — an ink
   border on an ink fill would be invisible.

   The margins are not spacing. They reserve the offset's own footprint; without
   them it is clipped by the grid column. */
.hero__card {
  background: var(--ink);
  color: var(--paper);
  border-radius: var(--radius-surface);
  padding: 30px 30px 26px;
  margin: 0 var(--shadow-offset-gap) var(--shadow-offset-gap) 0;
  box-shadow: var(--shadow-offset);
  /* The whole card starts the flow (see views/solo.ejs), so it wears the
     pointer everywhere, not only over its button. */
  cursor: pointer;
  /* The card is the containing block for anything pinned to it — today the
     beta-offer flag, which hangs off its top-left corner on /beta-offer. It is
     declared here rather than beside the flag so the card's own geometry is
     stated in one place; the phone pass restates it next to a z-index, which is
     what actually lifts the card over Ellie's -12px overlap at that width.
     Positioned with no z-index of its own, so Ellie (z-index 2) still paints
     over the overlap here exactly as she did before. */
  position: relative;
}
.hero__card .price {
  font-family: var(--display);
  font-size: 78px;
  line-height: 0.85;
  color: var(--white);
}
.hero__card .price sup { font-size: 30px; vertical-align: super; color: var(--orange); }
.hero__card .price .per { font-family: var(--mono); font-size: 14px; letter-spacing: 0.04em; color: var(--on-ink-muted); text-transform: none; }
/* The struck list price, revealed beside the replaced figure once GET /coupon
   confirms a discount covering the whole purchase (see applyPagePrice in
   public/js/solo.js). Inline rather than a flex item: .hero__card .price is a
   plain block, so a 30px figure after a 78px one sits on the same baseline
   without turning the price row into a layout of its own.

   Named off .hero__card, NOT off .price, and not for symmetry. A BEM class on
   the price block would contain the string tests/checkout-routes.js greps every
   rendered page for to catch a leaked Stripe price id -- the "price" stem plus
   BEM's double underscore spell that prefix exactly -- so such a class fails that
   test on every page. (Deliberately not written out here; the scan covers HTML
   rather than CSS today, and there is no reason to plant the string anyway.) .pay__card-was, which does this same job on the S04 card, dodged
   it the same way. Styled off that class, so the two halves of the funnel strike
   a price identically. The orange rule over a muted
   figure is the treatment; do not swap it for a lighter grey, which reads as
   disabled rather than superseded. */
.hero__card-was {
  margin-left: 10px;
  font-family: var(--display);
  font-size: 30px;
  line-height: 0.85;
  color: var(--on-ink-muted);
  text-decoration-line: line-through;
  text-decoration-color: var(--orange);
  text-decoration-thickness: 2px;
}
/* Once the figure is a plain replacement the currency marker is part of it, not
   an accent beside it -- the same call .pay__card-was already made on the pay
   card, quoted here because the reasoning is identical: "$47.50", one figure,
   not an orange $ next to a white amount. */
.hero__card .price:has(.hero__card-was:not([hidden])) [data-page-price] sup {
  font-size: 78px;
  vertical-align: baseline;
  color: var(--white);
}
.hero__card ul { list-style: none; margin: 20px 0 24px; padding: 0; display: flex; flex-direction: column; gap: 11px; }
.hero__card li { display: flex; gap: 10px; font-size: 13px; line-height: 1.5; color: var(--grey); }
.hero__card li::before { content: "→"; color: var(--orange); flex: none; }
.hero__card .btn { width: 100%; }

/* --- hero cutout (PENG-2056) ------------------------------------------------
   The second hero column is now the plan card with Ellie beneath it, so it
   needs a column wrapper of its own — .hero__inner's grid places one child per
   column and the card used to be that child. */
.hero__aside { display: flex; flex-direction: column; align-items: stretch; min-width: 0; }

/* HISTORICAL, and no longer an instruction: this said the hero must not clip,
   because Ellie used to hang past its bottom edge. She does not any more —
   `order: -1` below puts her above the plan card — and .hero has carried
   `overflow: hidden` regardless. */
/* `order: -1` at EVERY width, not just on a phone: the artboard puts Ellie above
   the plan card on desktop too. The build had the DOM order (card, then Ellie)
   showing through whenever the hero was two columns, which also stranded the
   "Meet Ellie" callout in the whitespace above the card instead of beside her
   head — the callout is positioned against this element, so it goes wherever
   this element goes.
   Ellie used to overlap the card by a negative bottom margin here — the card
   tucked up under her feet, or on the regional upper-leg crops, up to her
   waist. Product wants her clear of the card instead: she pops out above it,
   fully visible, meeting the card's top edge rather than tucking into it. */
.hero__ellie {
  position: relative;
  z-index: 2;
  order: -1;
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
  margin-bottom: 0;
  pointer-events: none;
  /* Ellie's rendered width, declared ONCE and read by two rules: the image
     itself, and the callout's horizontal offset below. They have to agree, and a
     shared custom property is what makes that structural rather than a
     coincidence someone has to remember. */
  --ellie-w: clamp(200px, 58%, 320px);
}
/* Root used to ship a smaller bust crop than the regional pages' commissioned
   art, hence a narrower --ellie-w here. As of 2026-08-27 root, SG and AU all
   render full-body cutouts from the same set, so this now names the same
   clamp as .hero__ellie--regional below — declared separately, not inherited,
   since the two are still free to diverge again (as they already do on
   mobile, further down). No margin override is needed either: without an
   overlap to compensate for, it does not matter whether a crop ends at the
   feet or the upper leg. Every variant shares the same gap above. */
.hero__ellie--regional { --ellie-w: clamp(200px, 58%, 320px); }
/* AU is a landscape-framed source illustration, unlike the other portrait
   Ellie cuts. Giving it the same width made it much shorter on screen; the
   wider bound normalizes visible stature while the shared --ellie-w still
   keeps the handwritten callout anchored to the art.
   HK used to need the same treatment, but its apparent "landscape" framing
   turned out to be ~195px of near-invisible soft-fade padding baked into the
   PNG on each side (checked with an alpha threshold, not just alpha > 0) —
   that's also why its callout arrow read as pointing at empty air instead of
   at her. Cropped the source file to its real content instead of
   compensating in CSS: public/img/ellie/en-hk.png is now 1013x1117, close
   enough to portrait that it needs no special-casing here. */
.hero__ellie.ellie-art--au { --ellie-w: clamp(235px, 66%, 360px); }
/* pointer-events is restored on the IMAGE alone. .hero__ellie above keeps
   `none` because it is a flex container spanning the whole hero column: enabled
   there, the empty paper beside Ellie would start the sign-up flow. */
.hero__ellie img { display: block; width: var(--ellie-w); height: auto; pointer-events: auto; cursor: pointer; }

/* Positioned against ELLIE, not against this column.
   The artboard nests the callout inside a wrapper that hugs the cutout, so its
   `right: calc(100% - 12px)` measures from the image's left edge. Ported here
   without that wrapper, the same declaration measured from .hero__ellie, which
   is a flex container spanning the whole hero column while the image is only
   `--ellie-w` of it and right-aligned inside it. The callout therefore sat
   (column width - image width) too far left: 251px at 1440, straight through the
   headline, with the arrow pointing at empty paper.
   Offsetting by --ellie-w instead puts the callout's right edge 12px inside the
   image's left edge, which is the relationship the artboard draws. The
   percentage inside the clamp resolves against this same box in both rules, so
   the two stay locked together at every width.
   The wrapper is deliberately NOT added: on a phone the callout and the cutout
   are flex SIBLINGS in a row (see the ≤900px block), which a wrapper around the
   image would break. */
.hero__callout {
  position: absolute;
  right: calc(var(--ellie-w) - 12px);
  top: 4%;
  width: 250px;
}
/* Japanese naturally occupies three rendered rows here. Reserve that same
   desktop rhythm for Korean so the shared arrow begins at the same height. */
@media (min-width: 901px) {
  html:lang(ko) .hero__callout p { min-height: 3.3em; }
}
/* Three rows is the intended rhythm above; WHERE they break is not left to the
   default. At this box width the Japanese label broke as "…エージ" / "ェント",
   which splits エージェント mid-word and opens a line with a small kana — both
   things kinsoku shori prohibits, and the second is the one a reader notices as
   simply wrong rather than tight. `auto` applies only the loose prohibitions,
   which do not include the small kana; `strict` does, and moves the break to a
   legal position without touching the copy or the box. Measured at 901-1600px
   after the change: still three rows, no orphan.

   Scoped to :lang(ja) because it is the only page whose label breaks at all —
   Korean wraps on its own space at every width, and `strict` would additionally
   constrain its punctuation for no gain. */
html:lang(ja) .hero__callout p { line-break: strict; }
/* `word-break: keep-all` was the tempting version of this — it keeps
   エージェント whole and gives two clean rows — and it is wrong here. Measured:
   the unbroken run needs 285px in a 250px box and overflows 35px to the right,
   into Ellie. Widening the box instead moves it LEFT, because it is anchored on
   `right`, and at 901px the callout already clears the title's longest row by
   only 34px. So the break stays, in a legal place. */
.hero__callout p {
  margin: 0;
  padding-right: 34px;
  font-family: Caveat, cursive;
  font-size: 25px;
  line-height: 1.1;
  color: var(--ink);
  text-align: right;
}
/* Anchor below the callout's rendered text rather than at the English two-line
   height: translated labels can wrap to a third line, and a fixed 62px offset
   then draws the arrow through the final row. */
.hero__callout-arrow { position: absolute; right: 2px; top: calc(100% + 4px); overflow: visible; }

@media (max-width: 900px) {
  .hero__inner { grid-template-columns: 1fr; gap: 36px; }
}

/* PENG-1867's other half. The triage guidance is about the cookie banner, but
   the report underneath it says the first mobile fold is "30% white space" as
   well, and the hero's padding is most of that: 84px top and 92px bottom is
   176px of vertical air, set for a 1120px column and still applied at 390px,
   about a third of that width. The eyebrow and the headline are what the paid
   traffic this page is built for came to read, so they move up.

   720px, because that is the width where .topbar__link already sheds its links
   on the pages that still have them — the same "this is a phone now" line,
   rather than a sixth opinion about where one starts. (PENG-2056 removed the
   Solo bar's links outright, so on THIS page there is nothing left to shed; the
   breakpoint is kept for continuity with the other views.) Below the base .hero
   rule on purpose: a media query adds no specificity, so source order is the
   whole reason this wins. */
@media (max-width: 720px) {
  .hero { padding: 40px 24px 56px; }
}

/* --- proof strip ----------------------------------------------------------- */
.proof {
  background: var(--ink);
  color: var(--paper);
  padding: 22px 32px;
}
.proof__inner {
  max-width: var(--maxw);
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  gap: 14px 40px;
  align-items: center;
  justify-content: space-between;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--on-ink-soft);
}
/* `display: block`, NOT flex. As a flex container every text run and every <b>
   inside an item became its own flex item with `nowrap`, so at 375px the items
   stopped reflowing as sentences and split into columns of one word — "Screen
   deals in" / "minutes" / ", not" / "hours" stacked. A claim is a sentence; it
   has to wrap like one. The flex+gap here was left over from the leading-dot
   layout the orange hairlines replaced, and nothing else needed it.
   Fixed at the mechanism rather than by shortening the copy: the next long
   claim would bring it straight back. */
.proof__item { display: block; }
.proof__item b { color: var(--white); font-weight: 600; }
/* PENG-2056: the artboard swaps the leading dot for a hairline BETWEEN items.
   Drawn as a border on the item rather than a separate element, so the markup
   keeps exactly one node per claim and a screen reader never announces a
   decorative separator. */
.proof__item + .proof__item {
  border-left: 1px solid var(--orange);
  padding-left: 18px;
  min-height: 15px;
}
.proof__item:first-child { color: var(--orange); }

/* --- regional promo banner (APAC beta pages only) --------------------------
   Flush: it sits directly under the topbar and meets the viewport edge on
   both sides. Hidden until public/js/regional-banner.js confirms the coupon's
   real terms from GET /coupon — see views/partials/regional-banner.ejs. */
.regional-banner-slot {
  /* The topbar is fixed. Keep its space even while the banner is hidden and
     Stripe is being checked, so neither the banner nor the hero sits under it. */
  padding-top: var(--topbar-flush-h);
}
.regional-banner-slot + .landing--flush { padding-top: 0; }
.regional-banner {
  background: var(--orange);
  color: var(--ink);
  padding: 12px 32px;
  text-align: center;
  font-size: 13px;
  font-weight: 600;
  border-radius: var(--radius-flush);
}
.regional-banner__was {
  margin-right: 4px;
  /* No opacity, deliberately. --ink on --orange measures 4.80:1 (AGENTS.md §5)
     and clears AA for this 13px text with almost nothing to spare, so a fade
     over it does not survive the margin: at the 0.72 this shipped with, the
     colour the eye actually receives measures 3.31:1 and fails. This is a PRICE
     the buyer is meant to read and compare the discounted figure against, not
     decoration, so 1.4.3 applies to it. The line-through already carries the
     "this is the old number" meaning the fade was reaching for, so nothing is
     lost. Do not reintroduce a fade here — de-emphasise with the strike, or with
     size, neither of which spends contrast. */
  text-decoration-line: line-through;
  text-decoration-color: var(--ink);
  text-decoration-thickness: 2px;
}
.regional-banner__now { font-weight: 700; }

/* --- generic marketing section -------------------------------------------- */
.section { padding: 80px 32px; }
.section__inner { max-width: var(--maxw); margin: 0 auto; }
.section--paper { background: var(--paper); }
.section--grey { background: var(--grey); }
.section__head { max-width: 640px; margin-bottom: 44px; }
.section__title {
  font-family: var(--display);
  text-transform: uppercase;
  font-size: clamp(34px, 4.6vw, 54px);
  line-height: 0.94;
  margin: 12px 0 0;
}

/* The swash rides under one word of a title (PENG-2056). The host is the
   inline-block the absolutely-positioned stroke measures itself against, so it
   must wrap the WORD, never the whole heading — wrapping the heading stretches
   one swash across the full line, which is not what the artboard draws. */
.swash-host { position: relative; display: inline-block; }
.swash { position: absolute; left: 0; bottom: -7px; overflow: visible; }
/* partials/swash-sm.ejs — the lighter stroke under body copy sits closer to the
   baseline, because there is less descender space under 13.5px text than under a
   54px heading. */
.swash--sm { bottom: -6px; }


/* --- section head beside a companion block (PENG-2056) -----------------------
   Used once, by #trust, which pairs the head with the security card. #what was
   the other user until the 2026-08-17 artboard gave it a full-width head and put
   the product shot underneath; kept as a two-up pattern rather than folded into
   #trust because that is what it is. auto-fit rather than `1fr 1fr` so it collapses on its
   own, without a breakpoint — at a container width below roughly 644px, being
   2 × the 300px track floor plus the 44px gap. */
.section__split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 44px;
  align-items: center;
  margin-bottom: 44px;
}
.section__split .section__head { max-width: none; margin-bottom: 0; }
.section__lede { margin: 18px 0 0; font-size: 14.5px; line-height: 1.65; color: var(--body); max-width: 62ch; }

/* Head variants for the 2026-08-17 artboard's full-width arrangements: --wide
   lets a head span the section when nothing sits beside it (the lede keeps its
   own 62ch measure), --tight hands the vertical rhythm to whatever follows,
   which then states its own margin-top. */
.section__head--wide { max-width: none; }
.section__head--tight { margin-bottom: 0; }

/* --- section texture washes (2026-08-17 artboard) -----------------------------
   The same painted orange texture the hero ground uses, radially masked to a
   soft blob and hung off alternating edges of the paper sections — the artboard
   alternates left/right down the page. Like .hero__wash, the falloff is a MASK:
   what fades is the image's own alpha, so no brand colour is interpolated and
   the no-gradient rule holds. `black` is the mask's alpha carrier, not a colour
   anyone sees.

   Two overflow postures. The default lets a blob rise past the section's top
   edge into the paper section above (the artboard draws them bridging the
   boundary) while clipping sideways so a 560px blob at -5% never widens the
   page; the plain-`hidden` first declaration is the fallback for engines
   without two-value overflow, which simply clip the blob to the section — a
   quieter render, never a broken one. --wash-tight clips both axes, for the
   one section (#what) whose upstream neighbour is the ink proof strip: a paper
   blob over ink reads as a smear, not a bridge.

   Content stays above via .section__inner's z-index. (The hero cutout's z-2 does
   still outrank it, but that no longer buys an overlap into #what: `order: -1`
   makes Ellie the FIRST item in .hero__aside, so she sits above the plan card
   rather than below it at the hero's bottom edge — and .hero has carried
   `overflow: hidden` since before that change anyway.) */
.section--wash { position: relative; overflow: hidden; overflow: clip visible; }
.section--wash-tight { overflow: clip; }
.section--wash > .section__inner { position: relative; z-index: 1; }
.section__wash {
  position: absolute;
  top: -215px;
  width: min(560px, 54vw);
  aspect-ratio: 1;
  opacity: 0.8;
  background-image: url("/img/orange-texture.jpg");
  background-size: cover;
  background-position: center;
  mask-image: radial-gradient(closest-side, black 35%, transparent 78%);
  pointer-events: none;
}
.section__wash--left { left: -5%; }
.section__wash--right { right: -5%; }
.section__wash--low { top: -70px; }

/* A slot is an asset that has not arrived yet. Deliberately plain, deliberately
   dashed, and deliberately labelled: the one thing it must never do is look
   like an intentional design element, because then nobody chases the asset and
   it ships empty. Height comes from --slot-h, or an aspect-ratio at the call site.

   NOTHING USES THIS TODAY — the last two holes it stood in (the #what product
   shot and the "See Ellie in action" video) have both been filled. It is kept
   because it is the pattern this page reaches for every time an artboard lands
   ahead of its assets, and it has been needed twice in two weeks. Delete it if a
   third pass goes by without it, but do not replace it with a stand-in image. */
.slot {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: var(--slot-h, auto);
  padding: 16px;
  border: 1px dashed var(--line);
  border-radius: var(--radius-surface);
  background: var(--white);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--muted-2);
  text-align: center;
}

/* .feature — the six-card 3-up that stood here was replaced by .dash-grid /
   .dash-feature in the 2026-08-17 artboard, and .grid-3 that laid it out went
   with it. Both are deleted rather than kept: unlike .slot below, this is not a
   pattern the page reaches for again — the artboard moved #what off cards on
   purpose ("nothing here competes with the shot above it"). The two rules that
   OUTLIVED it are recorded where they are still used: the orange halo on .shot,
   and "border only, no lift" on .landings-list a:hover. Restore from git history
   if a 3-up of cards ever comes back. */

/* --- #what product shot (2026-08-17 artboard) ----------------------------------
   Full-width image with the same orange halo the cards wear. TWO crops at TWO
   ratios, and this has now changed twice: the late 2026-08-17 revision collapsed
   both boards onto the single 2212×1179 export, then the 2026-08-18 revision
   added a tighter mobile render (1102×1179) because the wide crop is illegible
   at phone width. So the ratio DOES change at a breakpoint, and three places
   have to agree — see views/solo.ejs and the ≤720px override below.

   `height: auto` is LOAD-BEARING, not tidying. The <img> carries width/height
   attributes, which land as presentational hints; `width: 100%` overrides the
   width hint, but with nothing overriding the height hint an explicit height
   beats aspect-ratio, so the box stayed pinned at the intrinsic height while
   the width shrank and object-fit cropped the shot to a vertical sliver. It was
   663px too tall at 375px. Do not remove this to "let the aspect-ratio do the
   work" — the aspect-ratio is what reserves the box against layout shift, and
   the height hint is what silently outranks it.

   aspect-ratio and the width/height attributes have to move TOGETHER with the
   export. A ratio stated here that the file does not have makes the reserved
   box a lie, which is layout shift with extra steps. object-fit covers the
   rounding error between the stated ratio and the file's own.

   The halo (carried over from PENG-2056, where .feature declared it first) is
   NOT --shadow-offset, the hard offset shadow that stays unique to one object
   per page — it is a diffuse glow, and the artboard repeats it on the plan-list
   card, the trust card and the video frame too. */
.shot {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 2212 / 1179;
  object-fit: cover;
  margin-top: 44px;
  border-radius: var(--radius-surface);
  box-shadow: 0 0 34px color-mix(in srgb, var(--orange) 22%, transparent), var(--shadow-xs);
}

/* --- #what dash columns (2026-08-17 artboard) ----------------------------------
   Three quiet columns under the shot, replacing the six .feature cards: an
   orange dash, an uppercase mono name, a sentence. Not cards — nothing here
   competes with the shot above it. */
.dash-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 36px;
  margin-top: 36px;
}
.dash-feature::before { content: ""; display: block; width: 44px; height: 3px; background: var(--orange); }
.dash-feature h3 {
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink);
  line-height: 1.45;
  margin: 13px 0 0;
}
.dash-feature p { margin: 6px 0 0; font-size: 13px; line-height: 1.55; color: var(--body); }

/* --- what you get with Solo (2026-08-17 artboard) ------------------------------
   Replaces the four-plan comparison matrix: six capability rows in one white
   card beside the plan card. The matrix CSS went with its markup — its config
   (config/plans-matrix.js) is engineering-owned and still loads, see the note
   in views/solo.ejs. */
.plan-split {
  margin-top: 52px;
  display: flex;
  flex-wrap: wrap;
  gap: 40px;
  align-items: stretch;
}
.plan-list {
  flex: 1.7 1 400px;
  min-width: 0;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-surface);
  box-shadow: 0 0 34px color-mix(in srgb, var(--orange) 22%, transparent), var(--shadow-xs);
  padding: 6px 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* Grid areas rather than two DOM shapes: the desktop row is icon | name |
   divider | description on one line; the phone stacks name over description
   beside the icon and the divider bows out. Same markup either way. */
.plan-list__row {
  display: grid;
  grid-template-columns: 22px minmax(150px, 0.95fr) 2px 1.05fr;
  grid-template-areas: "icon name div desc";
  gap: 18px;
  align-items: center;
  padding: 16px 24px;
}
.plan-list__row + .plan-list__row { border-top: 1px solid var(--line-soft); }
.plan-list__icon { grid-area: icon; display: block; }
.plan-list__name {
  grid-area: name;
  margin: 0;
  font-family: var(--mono);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink);
  line-height: 1.45;
}
/* --radius-structure: a fixed inset accent inside the 4px card, one rung down,
   as the ladder says. */
.plan-list__div {
  grid-area: div;
  width: 2px;
  height: 26px;
  background: var(--orange);
  opacity: 0.55;
  border-radius: var(--radius-structure);
}
.plan-list__desc { grid-area: desc; margin: 0; font-size: 12px; line-height: 1.55; color: var(--body); }

/* The second rendering of the plan being bought. AGENTS.md caps the hard
   offset at ONE object per page and the hero card wears it, so this one sits
   flat on --shadow-xs; the margins that reserved the offset's footprint go
   with it. The artboard draws the offset on both — divergence flagged in the
   PR, not an oversight. */
.hero__card--quiet {
  flex: 1 1 300px;
  min-width: 0;
  align-self: center;
  margin: 0;
  box-shadow: var(--shadow-xs);
}

/* The tax and currency note, between the price and the bullets. Sits at the
   quiet end of the on-ink neutrals: it is a disclosure, so it has to be legible,
   but it is not competing with the price above it. */
.hero__card-tax { margin: 10px 0 0; font-size: 11px; line-height: 1.6; color: var(--on-ink-muted); }

/* The quiet way out for teams — a ruled line, not a card, so it never competes
   with the plan card beside it. */
.team-plans { margin-top: 48px; border-top: 1px solid var(--line); padding-top: 22px; }
.team-plans p { margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--body); }
.team-plans a {
  color: var(--orange-dark);
  text-decoration: underline;
  text-underline-offset: 3px;
  font-weight: 600;
}

/* --- security dialog (PENG-2056) ---------------------------------------------
   z 120: above .topbar--flush (90) and .flow (100). The bar must never float
   over a modal. */
.modal {
  position: fixed;
  inset: 0;
  z-index: 120;
  background: color-mix(in srgb, var(--ink) 55%, transparent);
  display: flex;
  justify-content: center;
  padding: 26px 18px;
  overflow-y: auto;
}
/* Agrees with the `hidden` attribute the markup ships, which is what keeps the
   dialog out of the a11y tree even with no stylesheet. */
.modal[hidden] { display: none; }
/* Centred with `margin: auto`, NOT `align-items: center` on the flex parent.
   Both centre a card that fits; they differ when it does not. .modal scrolls,
   and a flex item centred by align-items overflows equally in both directions,
   which puts the top of a tall card ABOVE the scroll origin where no amount of
   scrolling reaches it — the modal's heading becomes unreadable on a short
   screen. Auto margins only absorb POSITIVE free space, so the card centres
   when there is room and falls back to top-aligned when there is not. */
.modal__card {
  width: 100%;
  max-width: 1000px;
  margin: auto;
  padding: 26px 26px 30px;
  background: var(--paper);
  border-radius: var(--radius-overlay);
  box-shadow: var(--shadow-overlay);
}
.modal__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; margin-bottom: 20px; }
.modal__title { font-family: var(--display); text-transform: uppercase; font-size: clamp(24px, 3vw, 34px); line-height: 0.95; margin: 0; }
.modal__source { margin: 20px 0 0; font-size: 12px; color: var(--muted-2); }

.cmp { display: grid; grid-template-columns: minmax(140px, 1fr) minmax(190px, 1.35fr); border-top: 1px solid var(--line); }
.cmp__head {
  padding: 16px 18px 16px 0;
  font-family: var(--display);
  text-transform: uppercase;
  font-size: 19px;
  line-height: 1;
  border-bottom: 1px solid var(--line);
}
.cmp__head--ours { padding: 12px 20px; text-align: center; background: var(--orange-tint); border-bottom: 1px solid var(--orange); }
.cmp__head--ours img { height: 26px; width: auto; display: inline-block; }
.cmp__cap { padding: 15px 18px 15px 0; font-size: 13.5px; line-height: 1.45; color: var(--body); border-bottom: 1px solid var(--line-soft); }
.cmp__ours {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 15px 20px;
  font-size: 13.5px;
  line-height: 1.45;
  color: var(--body);
  background: var(--orange-tint);
  border-bottom: 1px solid var(--line-soft);
}
.cmp__ours svg { flex: none; }

/* --- trust card (PENG-2056) ------------------------------------------------- */
.trust-card {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-surface);
  box-shadow: 0 0 34px color-mix(in srgb, var(--orange) 22%, transparent), var(--shadow-xs);
  padding: 26px 24px;
}
.trust-card__rows { display: flex; flex-direction: column; }
.trust-card__row {
  display: flex;
  gap: 12px;
  align-items: baseline;
  padding: 13px 0;
  border-bottom: 1px solid var(--line-soft);
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--body);
}
.trust-card__row:last-child { border-bottom: none; }
.trust-card__row > span:first-child { color: var(--orange); flex: none; }
.trust-card__row b { color: var(--ink); font-weight: 600; }
.trust-card__tiles { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 22px; }
/* --radius-structure: a permanent inset tile inside the 4px card, not a surface
   that arrives and leaves. One rung down from its parent, as the ladder says. */
.trust-tile {
  flex: 1 1 130px;
  background: var(--ink);
  color: var(--white);
  border-radius: var(--radius-structure);
  padding: 14px 16px;
}
.trust-tile__name { font-family: var(--display); text-transform: uppercase; font-size: 26px; line-height: 0.95; }
.trust-tile__note { font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--on-ink-muted); margin-top: 4px; }
.trust-tile__note--accent { color: var(--orange); }
.trust-card__more { margin: 22px 0 0; }

/* --- final CTA band -------------------------------------------------------- */
.cta-band {
  background: var(--orange);
  color: var(--white);
  padding: 76px 32px;
}
.cta-band__inner { max-width: var(--maxw); margin: 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 30px; flex-wrap: wrap; }
.cta-band__title { font-family: var(--display); text-transform: uppercase; font-size: clamp(36px, 5vw, 68px); line-height: 0.86; max-width: 640px; }
html:lang(ja) .cta-band__title,
html:lang(ko) .cta-band__title {
  line-height: 1.05;
}
/* The band's small print, under the title. Added for /trial's 2026-08-24 boards,
   which put the term here rather than in the headline: the title is the offer and
   this is the condition on it, so it takes the body face at the size the plan
   cards' tax note uses rather than a second display line.

   --ink at 75%, not a lighter token: this sits on the band's ORANGE texture, and
   the quiet greys in the palette are all tuned against --paper or --grey. Mixed
   with transparent so the texture stays visible through it, which is what makes
   it read as printed on the band. */
.cta-band__note {
  max-width: 640px;
  font-size: 11px;
  line-height: 1.6;
  color: color-mix(in srgb, var(--ink) 75%, transparent);
}
.cta-band .btn--dark { flex: none; }

/* --- see Ellie (PENG-2056) --------------------------------------------------- */
.section__head--centred { max-width: none; text-align: center; }
.section__head--centred .section__lede { margin-left: auto; margin-right: auto; }
/* --- the product tour (2026-08-18 videos) -------------------------------------
   Two cuts, one shown per breakpoint. Both <video> elements ship; CSS picks.
   See views/solo.ejs for why that beats one element with a JS-swapped src.

   THE PORTRAIT CUT'S RATIOS. The mobile artboard draws the frame at 4/5 at rest
   and switches it to 16/9 while playing. The 4/5 rest state is honoured here;
   the 16/9 playing state is NOT, and that is a deliberate divergence rather than
   an oversight. The mobile master is 1080×1920 — portrait — and it carries its
   headline at the top of the frame and its caption at the bottom. Cover-cropping
   that into 16/9 keeps a horizontal band through the middle and throws away both
   lines of text throughout the edit. So it plays at its own 9/16
   instead, which crops nothing. Tom Hirst confirmed portrait playback on
   2026-08-18: this is the settled behaviour, so do not "fix" it back to the
   artboard's 16/9 without a new portrait-safe master.

   At rest 4/5 with object-fit: cover does crop the portrait frame, and that is
   fine — at rest the frame is a dark teaser behind the play button, not
   something anyone is reading. */
.video-frame {
  position: relative;
  margin: 40px auto 0;
  max-width: 860px;
  border-radius: var(--radius-surface);
  overflow: hidden;
  background: var(--ink);
  box-shadow: 0 0 34px color-mix(in srgb, var(--orange) 22%, transparent), var(--shadow-xs);
  transition: box-shadow 0.35s ease;
}
.video-frame.is-playing { box-shadow: 0 0 44px color-mix(in srgb, var(--orange) 28%, transparent), var(--shadow-xs); }
.video-el {
  display: block;
  width: 100%;
  height: auto;
  /* Same trap as .shot: the width/height attributes land as presentational
     hints, and an explicit height hint outranks aspect-ratio. `height: auto` is
     what lets the ratios below hold. */
  aspect-ratio: 16 / 9;
  object-fit: cover;
  background: var(--ink);
  opacity: 0.84;
  transition: opacity 0.3s ease;
}
.video-frame.is-playing .video-el { opacity: 1; }
/* Desktop cut by default; the portrait one is swapped in below 720px. */
.video-el--portrait { display: none; }

.video-play {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  padding: 0;
  border: none;
  border-radius: var(--radius-flush);
  background: color-mix(in srgb, var(--ink) 28%, transparent);
  cursor: pointer;
}
/* solo.js sets the `hidden` ATTRIBUTE on this button once playback starts, which
   is what uncovers the native controls underneath. That works because of the
   `[hidden] { display: none !important }` rule at the top of this file — without
   it, `display: flex` above would keep the button on screen over a playing
   video. See the long note there.

   aria-busy is on between the press and the first frame. Both files ARE
   faststart, so this is not waiting on box order — it is the network on a ~5MB
   file, which preload="none" only starts fetching at the press.

   pointer-events, not just a cursor: this is the same in-flight contract the pay
   and Next buttons carry (see .btn[data-buy][aria-busy]), and for the same
   reason — `cursor: progress` stops nothing, and Enter on a focused button is
   ordinary keyboard impatience rather than an edge case. Bounded by
   TOUR_START_TIMEOUT_MS in solo.js, so a stalled fetch cannot leave the visitor
   holding a dead frame. Safe to hand pointer events to the <video> underneath
   while this is set, because the frame's state now derives from the media's own
   `playing` event rather than from this button's click. */
.video-play[aria-busy="true"] { cursor: progress; pointer-events: none; }

/* The failed-tour notice. Wears --danger like .pay__error, because something did
   fail — and it sits below the frame rather than over it so it does not become a
   second thing covering the play button it is telling you to press again. */
.video-error {
  margin: 12px 0 0;
  font-size: 13px;
  color: var(--danger);
}
.video-play__disc {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 84px;
  height: 84px;
  /* Geometry, not a rung: a circle. */
  border-radius: 50%;
  background: var(--orange);
  transition: background 0.12s ease, transform 0.12s ease;
}
.video-play:hover .video-play__disc { background: var(--orange-dark); transform: scale(1.04); }
.video-play:focus-visible { outline: none; box-shadow: inset 0 0 0 3px var(--orange); }
.video-play__disc svg { margin-left: 4px; }

@media (prefers-reduced-motion: reduce) {
  .video-frame,
  .video-el { transition: none; }
}

/* --- CTA band on the orange texture (PENG-2056) ------------------------------
   Ellie meets the band's bottom edge, so the band carries no padding below her
   and `overflow: clip` trims whatever hangs past it. The copy column keeps its
   own bottom padding instead — that asymmetry is the layout, not an oversight. */
.cta-band--texture {
  background-image: url("/img/texture-orange-cta-web.jpg");
  background-size: cover;
  background-position: center;
  padding-top: 46px;
  padding-bottom: 0;
  overflow: clip;
}
.cta-band--texture .cta-band__inner { align-items: flex-end; flex-wrap: nowrap; gap: 40px; padding-bottom: 0; }
.cta-band__copy { display: flex; flex-direction: column; align-items: flex-start; gap: 22px; padding-bottom: 46px; }
.cta-band__ellie { display: block; width: clamp(140px, 17vw, 206px); height: auto; align-self: flex-end; flex: none; pointer-events: none; }
.cta-band__ellie.ellie-art--au { width: clamp(168px, 20vw, 247px); }

/* --- the beta-offer flag (2026-08-19 offer artboards) -------------------------
   A stuck-on paper sticker: two hard 2px ink frames at slightly different
   angles, no radius, no shadow. Squareness is the point — a rounded sticker
   reads as another UI chip, and the ladder's rungs are for things the product
   owns, not for a piece of paper laid over them. So no border-radius is
   declared here at all; the default 0 IS the drawing.

   IT MUST NOT WEAR --shadow-offset. The hero card beneath it already carries
   the page's one hard offset, because the plan is the thing being bought. The
   flag's second frame is the depth it gets, and two offsets in the same corner
   would leave neither of them meaning anything.

   Sizes are read off Solo Site Offer.dc.html; the phone pass at the end of this
   file carries the smaller set from Solo Site Mobile Offer.dc.html. */
.offer-flag {
  position: relative;
  transform: rotate(-4deg);
  /* It used to be pointer-events: none, because a click that landed on the
     sticker instead of the card was a click that did nothing. The card is a
     button now, so the sticker is part of what you can press — including the
     part of it that overhangs the card's edge, since the event follows the DOM
     and not the geometry. cursor comes from .hero__card. */
}
/* The outer frame, offset by rotation rather than by a shadow. ::before and not
   a div so it never reaches the accessibility tree — the words inside the box
   are the offer and are read; this is the paper it is printed on. */
/* Hidden by solo.js when the resolved coupon contradicts what the sticker says
   (verifyOfferClaims). Declared explicitly rather than left to the UA's [hidden]
   rule: .offer-flag--card sets `position: absolute`, and the consent banner has
   already cost this repo a day over exactly that -- a display or layout
   declaration outranking `[hidden] { display: none }` leaves an element the code
   believes is hidden permanently on screen. Nothing sets `display` on
   .offer-flag today; this makes that not matter. */
.offer-flag[hidden] { display: none; }
.offer-flag::before {
  content: "";
  position: absolute;
  inset: -6px;
  border: 2px solid var(--ink);
  transform: rotate(3deg);
}
.offer-flag__box {
  position: relative;
  border: 2px solid var(--ink);
  padding: 10px 20px 12px;
}
.offer-flag__kicker,
.offer-flag__line { display: block; font-family: var(--mono); font-weight: 600; text-transform: uppercase; }
.offer-flag__kicker { font-size: 10px; letter-spacing: 0.22em; }
/* The second rotation is against the box, not with it: the line sits a degree
   and a half off the frame it is inside, which is what makes the whole thing
   read as stuck on rather than designed in. */
.offer-flag__line {
  font-size: 20px;
  letter-spacing: 0.1em;
  color: var(--white);
  margin-top: 3px;
  transform: rotate(-1.5deg);
  white-space: nowrap;
}

/* On a plan card: pinned over its top-left corner, biting 16px into it.
   transform-origin is bottom left so the -4deg pivots about the corner it is
   anchored to and the overlap stays at 16px whatever the flag's width.

   Used twice, on the hero card and on the plans section's second rendering of
   the same card. Same sticker both times, because it is the same plan — the
   quiet card is quieter about its SHADOW, which is an elevation rule, not about
   the offer printed on it. */
.offer-flag--card {
  position: absolute;
  bottom: calc(100% - 16px);
  left: -24px;
  z-index: 3;
  transform-origin: bottom left;
}
.offer-flag--card .offer-flag__box { background: var(--orange); }
/* --ink on --orange is 4.80:1 — the readable half of the pair. The line above
   is white on orange at 20px/600, which is large text and clears 3:1. See
   AGENTS.md §5 for why neither is "fixed" by lightening the orange. */
.offer-flag--card .offer-flag__kicker { color: var(--ink); }

/* The /free page's sticker wears the same paper without the tilt: the -4deg
   stuck-on rotation (and the counter-rotations on its frame and line) are a
   campaign-offer gesture, and the free page's line is an invitation, not a
   sticker shouting over a price. It also drops the offset outer frame — one
   clean box, no second border. Scoped to a modifier so /beta-offer and
   /trial keep theirs. */
.offer-flag--flat,
.offer-flag--flat::before,
.offer-flag--flat .offer-flag__line { transform: none; }
.offer-flag--flat::before { content: none; }

/* Band: in flow at the top of the copy column, on the orange texture. Fill and
   kicker swap so the flag stays the darker object against a lighter ground. */
.offer-flag--band {
  align-self: flex-start;
  margin: 8px 0 6px 8px;
}
.offer-flag--band .offer-flag__box { background: var(--ink); }
.offer-flag--band .offer-flag__kicker { font-size: 11px; color: var(--orange); }
.offer-flag--band .offer-flag__line { font-size: 22px; }

/* --- solo-material mobile pass (PENG-2056) -----------------------------------
   These sit at the END of the landing rules on purpose, after every rule they
   override. A media query adds no specificity, so an override placed ABOVE the
   rule it means to override is silently dead, and nothing about a dead CSS rule
   announces itself. That is the trap tests/mobile-viewport.test.js was written
   to catch after PR #28 — do not move this block up.

   Values read off Solo Site Mobile.dc.html, which is the same content at 18px
   gutters with smaller display sizes. */
@media (max-width: 900px) {
  /* The 2026-08-17 mobile artboard redraws the one-column hero: the callout
     and the cutout become a row ABOVE the plan card — callout right-aligned,
     its arrow now in flow under the text, still curving down towards Ellie
     beside it. order:-1 does the move so the markup keeps its desktop order.
     This replaces the earlier centred-figure treatment. */
  /* order: -1 is now the BASE rule (both boards put Ellie above the card), so
     what is left here is the phone treatment of the same row: the callout goes
     back into flow beside her, its arrow with it. */
  .hero__ellie { align-items: flex-start; justify-content: flex-end; gap: 6px; margin: 0; }
  .hero__ellie img { width: 34%; max-width: 130px; }
  /* Regional cutouts render bigger than the US crop at every width (product
     wanted her more prominent), the same reason the desktop --ellie-w clamp
     above runs wider for these variants. */
  .hero__ellie--regional img { width: 52%; max-width: 198px; }
  .hero__callout { position: static; width: auto; flex: 1; min-width: 0; padding-top: 14px; }
  .hero__callout p { padding-right: 26px; font-size: 23px; }
  .hero__callout-arrow { position: static; display: block; margin: 2px -8px 0 auto; }
  /* NO `flex-wrap: wrap` here. Wrapping is what stacked the band's copy above
     Ellie and left ~260px of empty orange under the button (450px tall against
     the artboard's ~190px). The mobile artboard keeps them side by side at every
     width, so the band stays a row and Ellie takes a share of it instead. */

  /* --- beta-offer flag, phone (Solo Site Mobile Offer.dc.html) ----------------
     Everything steps down together: the frames tighten to 5px, the hero flag
     tucks in to a 10px overhang because the gutter is 18px rather than 64px,
     and the two type sizes drop. The line stays nowrap at every width — "FIRST
     MONTH / FREE" over two lines is a different object. */
  .offer-flag::before { inset: -5px; }
  .offer-flag__box { padding: 8px 16px 10px; }
  .offer-flag__kicker { font-size: 9px; }
  .offer-flag__line { font-size: 16px; margin-top: 2px; }
  .offer-flag--card { bottom: calc(100% - 14px); left: -10px; }
  /* At this width .plan-split has wrapped, so the second plan card sits under
     the feature list with only the 40px flex gap above it — and the flag hangs
     into exactly that gap, landing on the list's bottom edge. The hero card
     never needs this: Ellie's column is above it, and she is decoration the
     flag is drawn to overlap. :has() rather than a modifier class on the card,
     because the card's class attribute is pinned by
     tests/solo-material-plans.test.js as the page's one-hard-shadow contract. */
  .hero__card--quiet:has(.offer-flag) { margin-top: 60px; }
  .offer-flag--band { margin: 6px 0 4px 6px; }
  .offer-flag--band .offer-flag__box { padding: 8px 14px 10px; }
  .offer-flag--band .offer-flag__kicker { font-size: 9px; }
  .offer-flag--band .offer-flag__line { font-size: 15px; }
}

@media (max-width: 720px) {
  .section { padding: 44px 18px; }
  .section__title { font-size: clamp(30px, 8.6vw, 38px); }
  .section__split { gap: 28px; margin-bottom: 32px; }
  .hero__title { font-size: clamp(38px, 11.5vw, 48px); }
  .cta-band { padding-left: 18px; padding-right: 18px; }
  .cta-band__title { font-size: clamp(26px, 7.4vw, 34px); }
  .modal { padding: 14px 10px; }
  .modal__card { padding: 18px 16px 22px; }
  /* One column: at 390px a two-column comparison gives each side about 170px
     and every capability name wraps to four lines. */
  .cmp { grid-template-columns: 1fr; }
  .cmp__head { padding: 12px 0; }
  .cmp__cap { padding: 12px 0 4px; border-bottom: none; font-weight: 600; }
  .cmp__ours { padding: 8px 12px 12px; }
  /* A vertical rule between items that have wrapped onto their own lines is a
     stray mark, not a separator. */
  .proof__item + .proof__item { border-left: none; padding-left: 0; }

  /* The washes shrink towards the artboard's phone values: narrower viewport,
     bigger relative blob, hung further off the edge so most of it is falloff. */
  .section__wash { width: 82%; top: -150px; opacity: 0.75; }
  .section__wash--left { left: -34%; }
  .section__wash--right { right: -34%; }
  .section__wash--low { top: -110px; }

  /* The mobile crop is a different, tighter render (1102×1179) than the desktop
     one, so the reserved box has to change with the <source>. The <img>
     attributes describe the desktop file and cannot follow a source swap; this
     is the declaration that keeps the phone's box honest. Keep it in step with
     the <source> width/height in views/solo.ejs. */
  /* The mobile crop's own ratio. LOAD-BEARING and paired with the <source> in
     views/solo.ejs that serves product-shot-mobile.png at this breakpoint —
     delete one without the other and the reserved box is a lie against the file
     the browser actually picked, which is the vertical-sliver bug again in
     mirror image. The base rule above states the desktop ratio. */
  .shot { aspect-ratio: 1102 / 1179; margin-top: 24px; }

  /* The portrait cut replaces the landscape one, and the frame goes edge to edge
     with the section's 18px gutters rather than keeping the 860px desktop
     measure. 4/5 at rest, and .is-playing opens it to the master's own 9/16 —
     see the note on .video-frame. */
  .video-frame { margin-top: 24px; max-width: none; }
  .video-el--wide { display: none; }
  .video-el--portrait { display: block; aspect-ratio: 4 / 5; }
  .video-frame.is-playing .video-el--portrait { aspect-ratio: 9 / 16; }
  .video-play__disc { width: 68px; height: 68px; }
  .video-play__disc svg { width: 26px; height: 26px; margin-left: 3px; }
  .dash-grid { gap: 26px; margin-top: 30px; }

  /* The mobile artboard's hero: 14px lede, and NO standalone "Start now" — the
     plan card's "Get started" is the only CTA on a phone. At 17px the two
     paragraphs alone ran to about a third of the first fold.

     Hiding .hero__cta takes the hero_start signal off phone traffic (the button
     is display:none, so it is neither clickable nor reported). topbar_start and
     hero_card_get_started still cover the fold, and hero_start still fires on
     desktop, so the funnel keeps a CTA at this position on every device — but a
     breakdown that spans this change will show hero_start going quiet on
     mobile. Called out in the PR. */
  .hero__lede { font-size: 14px; }
  .hero__cta { display: none; }

  /* The proof block's phone values: the strip goes full-width rather than
     stopping at 460px. On a phone the hero column is already narrower than
     460px, so the cap only costs width — note this query reaches up to 720px,
     where the column is about 670px and dropping the cap genuinely widens the
     strip rather than being a no-op. */
  .hero__proof { margin-top: 26px; }
  .hero__proof-line { margin-bottom: 12px; font-size: 13px; }
  .hero__proof-logos { max-width: none; }

  /* The band's phone proportions: tighter gutters and gap, Ellie down to a third
     of the width, and the button full-width beside her. Keeps the row the base
     rule sets up rather than stacking it. */
  .cta-band--texture { padding-top: 30px; }
  .cta-band--texture .cta-band__inner { gap: 10px; }
  .cta-band__copy { gap: 14px; padding-bottom: 30px; }
  .cta-band__copy .btn { width: 100%; }
  .cta-band__ellie { width: 34%; max-width: 130px; }
  /* The phone composition is already width-constrained beside the CTA copy;
     regional desktop normalization must not make that row overflow. */
  .cta-band__ellie.ellie-art--au { width: 34%; max-width: 130px; }

  /* Name over description beside the icon; the divider column bows out. */
  .plan-list__row {
    grid-template-columns: 22px 1fr;
    grid-template-areas: "icon name" "icon desc";
    gap: 14px;
    row-gap: 4px;
    align-items: start;
    padding: 15px 16px;
  }
  .plan-list__div { display: none; }
  .plan-list__icon { margin-top: 1px; }
}

/* --- footer ---------------------------------------------------------------- */
.footer { background: var(--ink); color: var(--on-ink-muted); padding: 40px 32px; }
.footer__inner { max-width: var(--maxw); margin: 0 auto; display: flex; justify-content: space-between; gap: 20px; flex-wrap: wrap; font-size: 12px; }
.footer a { color: var(--on-ink-soft); }
.footer a:hover { color: var(--orange); }
/* The shared legal row (partials/footer-legal.ejs). A hairline above it
   separates it from a page-specific row when one exists; on pages where it is
   the only row the rule reads as the footer's top edge, which is fine. The
   hairline derives from the on-ink text token so it holds on the ink fill. */
.footer__legal { margin-top: 14px; padding-top: 14px; border-top: 1px solid color-mix(in srgb, var(--on-ink-muted) 30%, transparent); }

/* --- legal pages (terms, aup) ---------------------------------------------- */
/* Long-form legal prose. One narrow measure on --paper, body type throughout;
   the display face is reserved for the page title so forty clauses don't
   shout. Clause numbers are literal text in the markup, never list counters:
   the terms cross-reference their own clause numbers ("clause 14(a)",
   "clause 19", "clause 33"), so a renderer must not be able to renumber them. */
.legal { background: var(--paper); padding: 56px 32px 72px; }
.legal__inner { max-width: 840px; margin: 0 auto; }
.legal__inner h1 { font-family: var(--display); font-size: clamp(30px, 5vw, 52px); line-height: 0.95; text-transform: uppercase; margin: 0 0 8px; }
.legal__updated { font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); margin: 0 0 34px; }
.legal__inner h2 { font-size: 17px; margin: 38px 0 14px; }
.legal__inner p, .legal__inner li { font-size: 13.5px; line-height: 1.7; color: var(--body); margin: 0 0 12px; }
.legal__inner ul { padding-left: 22px; margin: 0 0 12px; }
.legal__inner a { color: var(--orange-dark); text-decoration: underline; }
/* Hanging indent for numbered clauses: the label column keeps clause numbers
   aligned, and sub-items step in one column-width per level. The column is
   minmax rather than fixed because the DPA's decimal labels ("5.11.") run
   wider than the terms' single numbers. */
.legal__clause, .legal__sub, .legal__sub2 { display: grid; grid-template-columns: minmax(40px, max-content) 1fr; gap: 0 10px; }
.legal__sub { margin-left: 50px; }
.legal__sub2 { margin-left: 100px; }
.legal__num { color: var(--muted); }

/* =========================================================================
   Onboarding flow (S01–S04) — full-screen overlay stepper
   ========================================================================= */
.flow {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: var(--paper);
  display: none;
  flex-direction: column;
}
.flow.is-open { display: flex; }
body.flow-open { overflow: hidden; }

.flow__bar {
  height: 58px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 28px;
  background: var(--white);
  border-bottom: 1px solid var(--line-soft);
}
/* .flow__who and .flow__close are gone with the elements they styled — the bar
   holds the lockup and nothing else now. See the note above .flow__bar in
   views/solo.ejs for why both were removed and what replaced them.
   `justify-content: space-between` above is left in place: with one child it
   changes nothing, and it is what the bar needs back the moment a second one
   returns. */

/* Four discrete segments, not a percentage bar. My 5D's setup indicator under
   SOLO_UI_2 is the same idea — a short segment per step, orange for done and a
   soft neutral for not — and it is the honest shape for a four-step
   questionnaire: a bar at 40% invites the question "40% of what?", where three
   segments of four answers it.

   Square, not pills. access-ui draws these as rounded-full, but that reads soft
   against a product that is deliberately squarer than the AI labs, and a progress
   segment is not the kind of thing rounded-full is for: it is fixed chrome, not a
   status badge. Flush is the rung for fixed chrome. */
.flow__progress { flex: none; display: flex; gap: 4px; padding: 8px 28px; background: var(--white); border-bottom: 1px solid var(--line-soft); }
.flow__progress span { display: block; height: 5px; width: 13px; border-radius: var(--radius-flush); background: var(--line); transition: background 0.3s ease; }
.flow__progress span.is-done { background: var(--orange); }

/* overscroll-behavior: contain stops a scroll that reaches the end of this box
   from CHAINING to the page underneath, so a flick past the last field no longer
   scrolls the landing page the buyer cannot see and the flow no longer appears
   to drift for no reason (PENG-1872).

   That is all it does, and the distinction matters because there is a second,
   separate problem next to it: `body.flow-open { overflow: hidden }` is what
   stops the document scrolling at all, and iOS Safari does not fully honour it
   (it needs position:fixed on the body). `contain` does not cover that gap, so
   do not read this rule as making the overflow:hidden redundant. Both are
   needed, only one of them is fixed here.

   align-items: flex-start is PENG-2112, and it is load-bearing rather than
   cosmetic. This is a ROW flex container — `display: flex` with
   `justify-content: center` is what centres the step horizontally — so its
   cross axis is VERTICAL, and the default `align-items: stretch` was sizing
   .step to exactly this scroller's height instead of to its content. Measured
   in headless Chromium at 411x640 before the fix: .step's box was 566px,
   identical to clientHeight, at every viewport height tried from 480 to 891.

   Two things followed from that, and the reported symptom is the second:

     - the step's content overflowed its own padding box rather than growing it,
       so the 40px under .step__nav stopped separating Back/Next from the bottom
       edge. Measured 5.8px of gap at 411x640 and -0.2px at 411x560 — the
       buttons sitting ON the edge of the screen, which is the screenshot on the
       ticket;
     - the item's BOX was still exactly the scroller's height, so the scroller
       saw no overflow to scroll (scrollHeight === clientHeight === 566 at 640).
       The padding was not merely tight, it was unreachable.

   flex-start lets the item size to its content instead: 600.2px at 411x640, the
   scroller then reports 600 against a 566 client height, and the gap under Next
   measures 39.8px at the end of the scroll. Nothing inside .step wants the
   stretched height — there is no margin-top:auto or flex:1 on any of its
   children — so on a tall screen, where the step already ended above the fold,
   this changes nothing that renders.

   Do not "simplify" this to flex-direction: column with align-items: center.
   That moves the horizontal centring onto align-items and this declaration then
   pushes the step against the left edge. The test for this pins both. */
.flow__scroll { flex: 1; overflow-y: auto; display: flex; justify-content: center; align-items: flex-start; overscroll-behavior: contain; }

.step {
  display: none;
  width: 100%;
  max-width: 620px;
  padding: 48px 28px 56px;
  flex-direction: column;
  gap: 22px;
}
.step.is-active { display: flex; animation: stepIn 0.4s ease both; }
@keyframes stepIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

.step--wide { max-width: 780px; }

.step__title {
  font-family: var(--display);
  text-transform: uppercase;
  font-size: clamp(38px, 5.4vw, 56px);
  line-height: 0.92;
}
.ellie-say { display: flex; gap: 12px; align-items: flex-start; }
.ellie-say img, .ellie-say svg { width: 34px; height: 34px; border-radius: 50%; flex: none; }
.ellie-say p { margin: 0; font-size: 14px; line-height: 1.6; color: var(--body); }

/* fields */
.field { display: flex; flex-direction: column; gap: 7px; }
.field label { font-size: 13px; font-weight: 500; color: var(--ink); }
.field input {
  font-family: var(--mono);
  font-size: 14px;
  color: var(--ink);
  padding: 14px 15px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--white);
}
.field input::placeholder { color: var(--muted-2); }
.field input:focus { outline: none; border: 2px solid var(--orange); padding: 13px 14px; box-shadow: var(--focus); }
.field .hint { font-size: 11.5px; color: var(--muted-2); }
.field .err { font-size: 11.5px; color: var(--orange-dark); display: none; }
.field.is-error input { border: 2px solid var(--orange-dark); padding: 13px 14px; }
.field.is-error .err { display: block; }

/* free tier — the /free route renders views/solo.ejs with freeMode on. The flow
   is the paid flow; only its last step differs. This styles that step's free
   framing lede. On success the flow navigates to /welcome/free for the "You're
   in / check your email" confirmation, which is styled by the .ph hero rules the
   paid /welcome shares. */
.pay__free-lede { margin: 0; font-size: 14px; line-height: 1.6; color: var(--body); }

/* social divider */

/* "a bit about Fifth Dimension" panel. Deliberately not a card (PENG-1940):
   boxed like .field input below it — a border and a white fill at rest —
   plus a permanent shadow .field input doesn't even carry, which is what
   actually made the old version read as a card rather than the page it sits
   on. Kept as a flex column, not a plain block: the 9px gap here is between
   the label and the rows, and it only does anything inside a flex/grid
   container (.row sets its own gap independently, so that part is
   unaffected). Declaring `display` at all is also why this element needs the
   `!important` on [hidden] near the top of this file — see the note there. */
.about { display: flex; flex-direction: column; gap: 9px; }
.about .mono-label { color: var(--muted); font-weight: 600; }
.about .row { display: flex; gap: 9px; font-size: 13px; line-height: 1.5; color: var(--body); }
.about .row::before { content: "→"; color: var(--orange); flex: none; }

.consent { font-size: 11.5px; line-height: 1.5; color: var(--muted-2); }

/* multi-select option grids

   The right and bottom padding is LAYOUT, not spacing: a selected option wears
   --shadow-offset-ink, and the offset falls outside the tile's box. Without the
   gap reserved on the grid, the rightmost column's shadow is clipped by the
   step's own padding edge and the bottom row's by whatever follows. Same pair
   access-ui writes as `mr-1.5 mb-1.5` beside its offset shadows — see the
   elevation note in :root. */
.opt-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 9px;
  padding-right: var(--shadow-offset-gap);
  padding-bottom: var(--shadow-offset-gap);
}
.opt {
  display: flex; align-items: center; gap: 11px;
  padding: 12px 15px;
  border: 1px solid var(--grey);
  border-radius: var(--radius-control);
  background: var(--white);
  /* Not inherited, and not optional. A <button> does not take `color` from body
     — the UA stylesheet gives it `buttontext` — so omitting this does not mean
     "the page's text colour", it means "whatever this browser decides". Chromium
     decides plain black, near enough to --ink that nobody saw it on a desktop for
     months; iOS Safari decides its system accent, and the asset-class and team
     labels rendered BLUE on a phone. Measured before the fix, the tile's computed
     colour was plain black where body's was --ink, which is the tell: the label
     was never getting a value from this palette at all. Every other control in
     this sheet already states its colour; this one was the omission. */
  color: var(--ink);
  cursor: pointer;
  text-align: left;
  font-family: var(--mono);
  transition: border-color 0.12s ease, background 0.12s ease, box-shadow 0.12s ease;
  min-height: 54px;
}
.opt:hover { border-color: var(--muted-2); }
.opt .label { font-size: 13px; font-weight: 500; }
.opt .tick { margin-left: auto; color: var(--orange); font-size: 15px; opacity: 0; }
/* Selected reads as a RAISED tile: white still, ink border, hard ink offset. The
   orange-tint fill this replaced said "selected" by going warm, which put ten
   competing washes on S02 and left the tick doing nothing; the offset carries it
   instead, and the fill stays white so the label's contrast never moves.
   The 1px of padding given back is the 1px the border took, so the label does not
   shift when it is picked. */
.opt[aria-pressed="true"] {
  border: 2px solid var(--ink);
  background: var(--white);
  box-shadow: var(--shadow-offset-ink);
  padding: 11px 14px;
}
/* Otherwise :hover's --muted-2 would lighten the border of an already-picked
   tile, reading as a partial un-select on the way to clicking it again. */
.opt[aria-pressed="true"]:hover { border-color: var(--ink); }
.opt[aria-pressed="true"] .tick { opacity: 1; }
/* The focus ring replaces the offset rather than stacking with it: two shadows on
   one tile at 6px apart is mud, and while a tile is focused the ring is the more
   useful of the two. */
.opt:focus-visible { outline: none; box-shadow: var(--focus); }

/* free-text reveal for "Other" */
.otherbox { display: none; flex-direction: column; gap: 7px; padding: 15px; background: var(--white); border: 1px solid var(--line-soft); border-radius: var(--radius-surface); box-shadow: var(--shadow-xs); }
.otherbox.is-open { display: flex; }
.otherbox label { font-size: 12px; font-weight: 500; }
/* `color` for the same reason as .opt above: a control does not inherit it, and
   .field input has always stated it while this one did not. Less visible than the
   blue option labels, because an input's UA colour is dark on the browsers we
   have seen, but it is the same omission and the same fix. */
.otherbox input { font-family: var(--mono); font-size: 13px; color: var(--ink); padding: 12px 14px; border: 2px solid var(--orange); border-radius: var(--radius-control); box-shadow: var(--focus); }
.otherbox input:focus { outline: none; }
.otherbox .hint { font-size: 11px; color: var(--muted-2); }

/* PENG-1871. iOS Safari zooms the whole page when it focuses an input whose
   font-size is under 16px, and it does not reliably zoom back out afterwards
   (the restore-on-blur behaviour has varied by release; the zoom-on-focus
   trigger has not). That is the entire reported bug ("have to zoom out in Safari
   on iOS to see whole sign up flow pages ... true for all pages before Stripe"):
   one tap on the name field leaves the rest of the questionnaire oversized. It
   is not horizontal overflow — in headless Chromium at 390px (2026-08) the
   document's scrollWidth equalled the viewport's on every step.

   Keyed on the POINTER, not a width breakpoint, because the trigger is a
   property of the device rather than the screen: a 768px iPad in portrait zooms
   exactly the same way and would sail past a 560px query.

   The alternative fix is maximum-scale=1 on the viewport meta, which works by
   taking pinch-zoom away from everyone, including anyone who needs it to read
   the page at all. Sizing the input is the fix that costs nobody anything.

   Below the two base input rules on purpose: a media query adds no specificity,
   so source order is the only reason this wins. */
@media (pointer: coarse) {
  .field input, .otherbox input { font-size: 16px; }
}

/* step nav

   The buttons in here are smaller than the landing page's, which keeps its
   15px/26px: a CTA competing for a scrolling reader's attention and the Next
   button on a form you are already filling in are not the same size of decision.
   Weight stays 700 on the orange fill for the contrast reason in the .btn comment
   above; the ghost stays 500. */
.step__nav { display: flex; align-items: center; gap: 18px; padding-top: 4px; flex-wrap: wrap; }
.step__nav .btn { padding: 11px 20px; }
.step__nav .btn--ghost { padding: 11px 18px; }
.step__nav .spacer { flex: 1; }
.step__nav .count { font-size: 11.5px; color: var(--muted-2); }

/* --- the flow on a phone ---------------------------------------------------
   One breakpoint for the whole overlay, and the numbers are the storyboard's own
   small-screen rules (Solo Onboarding Storyboard.dc.html, "The same five, on a
   phone"), not a guess at what fits:

     - Option rows drop to a 44px minimum touch target, narrow and low, so ten
       asset classes still fit two-up without a scroll.
     - The grids KEEP two columns and keep column flow, so the alphabetical run
       still reads down the left and then down the right. This is the reason there
       is no `grid-template-columns: 1fr` here: a single column turns ten options
       into a scroll, and re-flowing them across two columns of one row each would
       reorder an alphabetical list into something that looks shuffled.
     - Back collapses to an arrow-only square and Next takes the remaining width,
       so the primary action is thumb-sized at the bottom of every question.
     - Headlines drop to 32px; nothing goes below 11.5px.

   The old rules this replaces were two breakpoints (560px for the grid, 520px for
   the nav) that disagreed about where a phone starts, and the grid one set a
   single column against an inline `grid-auto-flow: column` it could not beat —
   what it actually produced was two columns of unequal width.

   "One breakpoint" still means one opinion about where a phone STARTS, and that
   is 560px and only here. Three later rules answer to something else, each
   because it is keyed to a different question, and none of them is a second
   opinion about phone width:

     - the flow's inputs, to `pointer: coarse` — the iOS zoom trigger is a
       property of the input device, not the screen (PENG-1871)
     - the pitch panel's order, to `max-height` as well — the keyboard takes
       vertical room, and a landscape phone is wide (PENG-1872)
     - the pay step's card, to 780px — it follows .pay__grid's own collapse,
       which predates this and has to stay in step with it (PENG-1866)

   If you add a fourth, say which question it answers here, or the file goes back
   to disagreeing with itself about what a phone is. */
@media (max-width: 560px) {
  .flow__bar { height: 52px; padding: 0 18px; }
  .flow__progress { padding: 8px 18px; }

  .step { padding: 24px 18px 40px; gap: 16px; }
  .step__title { font-size: 32px; line-height: 0.94; }
  .ellie-say { gap: 10px; }
  .ellie-say img, .ellie-say svg { width: 28px; height: 28px; }
  .ellie-say p { font-size: 12px; line-height: 1.5; }

  .opt-grid { gap: 7px; }
  .opt { min-height: 44px; padding: 9px 10px; gap: 8px; }
  .opt .label { font-size: 11.5px; line-height: 1.25; }
  .opt .tick { font-size: 12px; }
  .opt[aria-pressed="true"] { padding: 8px 9px; }

  /* Next fills the row; Back keeps only its arrow. The word is a span in the
     markup purely so it can be dropped here, and [data-back] carries a permanent
     aria-label so losing the text costs the button its look, not its name. */
  .step__nav { gap: 9px; }
  .step__nav .count { width: 100%; order: -1; }
  .step__nav .btn { flex: 1; padding: 14px; }
  .step__nav .btn--ghost { flex: none; padding: 14px 16px; }
  .step__nav .btn--ghost .btn__text { display: none; }
}

/* PENG-1872. With the keyboard up the usable height is roughly halved, and at
   the time this rule was written the pitch panel sat between the email field
   and Next — a buyer filling the form scrolled through the sales argument
   hunting for the way forward, which is what "the info at the bottom scrolls
   etc" was describing. PENG-1940 later moved the pitch in the markup to sit
   under Ellie's introduction, ahead of the fields rather than behind them, so
   the panel no longer sits between the fields and Next — but on a screen this
   short, a pitch panel ahead of the fields is exactly as costly: it is still
   something other than an input pushing the name and email fields toward the
   fold. So the order stays: fields, then Next with its consent line, then the
   pitch for whoever keeps reading, whichever end of the step the markup puts
   it at.

   HEIGHT, not width, and deliberately not folded into the 560px block above.
   The obstacle is a shortage of vertical room, and width is a poor proxy for it:
   a phone in landscape is 844x390, which sails past any max-width a phone would
   match while leaving barely 180px under the keyboard — the worst case in the
   whole set. Keeping the width arm as well means a portrait phone still matches
   before any keyboard appears. (On Chromium the height arm also starts matching
   WHEN the keyboard opens, because interactive-widget=resizes-content shrinks
   the layout viewport; that is a bonus, not the mechanism.) A short desktop
   window matching this is harmless: it moves a pitch panel below a button.

   order rather than moving the markup: source order is also the desktop order
   and the screen-reader order, and the pitch reads correctly before the button
   in both. .about holds no focusable elements, so nothing about focus order
   moves either — which is what makes this safe and is NOT true of every reorder
   in this file (see the note on .pay__card at ≤780px).

   Two things this depends on, both worth checking before touching it. It works
   at all because .step.is-active is display:flex — `order` on a block's children
   does nothing. And it is safe against the refused and waitlisted states because
   solo.js sets the `hidden` attribute directly on .about and .step__nav, which
   each carry [data-email-onward] themselves (there is no wrapper element, and
   views/solo.ejs explains at length why there deliberately is not), while
   `[hidden] { display: none !important }` near the top of this file outranks any
   order or display rule here. A reordered box that is not displayed at all stays
   not displayed. */
@media (max-width: 560px), (max-height: 620px) {
  .step .about { order: 1; }
}

/* =========================================================================
   Payment step (S04) — pricing + Buy (no card capture here)
   ========================================================================= */
.pay { max-width: 860px; }
/* Row gap and column gap are different numbers on purpose. The invoice's amounts
   are right-aligned, so they end at the column boundary and 26px put them almost
   against the plan card's edge — the two columns read as one crowded block. The
   column gap has to clear that; the row gap only applies once the grid has
   collapsed to a single column, where 48px of dead space would be too much. */
.pay__grid { display: grid; grid-template-columns: 1.1fr 1fr; gap: 26px 48px; align-items: start; }
@media (max-width: 780px) { .pay__grid { grid-template-columns: 1fr; } }

.pay__summary { display: flex; flex-direction: column; gap: 18px; }
.pay__summary h2 { font-family: var(--display); text-transform: uppercase; font-size: clamp(40px, 6vw, 64px); line-height: 0.88; }
.pay__lines { display: flex; flex-direction: column; gap: 10px; }
.pay__line { display: flex; justify-content: space-between; font-size: 13.5px; color: var(--body); padding: 10px 0; border-bottom: 1px solid var(--line-soft); }
.pay__line b { color: var(--ink); }
.pay__line--total { border-bottom: none; padding-top: 14px; font-size: 15px; }
.pay__line--total .amt { font-family: var(--display); font-size: 30px; line-height: 1; color: var(--ink); }
.pay__yourchoices { font-size: 12.5px; color: var(--muted); line-height: 1.6; }
.pay__yourchoices b { color: var(--ink); }

/* The fixed-term disclosure, revealed by solo.js only for a code that sells a
   NON-RENEWING subscription (config/fixed-term-coupons.js). Hidden on every
   ordinary purchase, because that one does renew and this wording would be
   false for it.
   A bordered panel rather than fine print, for the same reason .pay__sendto is
   one: it changes what the buyer is getting, and this is the last screen before
   money moves. --radius-structure is the rung for a permanent frame around
   standing facts, and it is squarer than the .pay__card (--radius-surface) it
   sits beside, which is the direction the ladder requires.
   The label is --ink, and deliberately NOT an orange. All three measured against
   this --orange-tint fill, sRGB, WCAG 2.1: --orange is 2.84:1 and --orange-dark
   is 3.98:1, so both miss the 4.5:1 AA bar at this 11px size — the same trap
   AGENTS.md §5 records for .eyebrow. --ink is 13.61:1. The orange border and the
   tint carry the attention; the words carry the disclosure, and this is the one
   label on the page a buyer must actually be able to read. Do not "restore" an
   orange here for consistency with .eyebrow — that one is a known failure, not a
   pattern to copy. */
.pay__term {
  border: 1px solid var(--orange);
  border-radius: var(--radius-structure);
  padding: 14px 16px;
  background: var(--orange-tint);
}
.pay__term-label {
  display: block;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: 6px;
}
.pay__term-text { margin: 0; font-size: 13px; line-height: 1.55; color: var(--ink); }

/* The other primary object, and the last one before money changes hands, so it
   wears the same offset as the hero card. See .hero__card for why the margins
   are load-bearing. */
.pay__card {
  background: var(--ink);
  color: var(--paper);
  border-radius: var(--radius-surface);
  padding: 28px 26px;
  margin: 0 var(--shadow-offset-gap) var(--shadow-offset-gap) 0;
  box-shadow: var(--shadow-offset);
  position: sticky;
  top: 20px;
}
.pay__card .plan { font-family: var(--display); text-transform: uppercase; font-size: 40px; line-height: 1; }
.pay__card .price { display: flex; align-items: baseline; gap: 8px; margin: 8px 0 6px; }
.pay__card-was {
  order: 2;
  align-self: flex-start;
  margin: 7px 0 0 2px;
  font-family: var(--display);
  font-size: 23px;
  line-height: 0.9;
  color: var(--on-ink-muted);
  text-decoration-line: line-through;
  text-decoration-color: var(--orange);
  text-decoration-thickness: 2px;
}
.pay__card-current { order: 1; display: inline-flex; align-items: baseline; gap: 4px; }
.pay__card .price .per { order: 3; }
.pay__card .price .amt { font-family: var(--display); font-size: 72px; line-height: 0.85; color: var(--white); }
.pay__card .price sup { font-family: var(--display); font-size: 26px; vertical-align: super; color: var(--orange); }
/* Once a verified discount reveals the struck regular price, the replacement
   reads as one figure: "$0", not an orange currency marker beside a white
   amount. The ordinary no-coupon price keeps its existing accent treatment. */
.pay__card-was:not([hidden]) + .pay__card-current { gap: 0; }
.pay__card-was:not([hidden]) + .pay__card-current sup {
  font-size: 72px;
  line-height: 0.85;
  color: var(--white);
}
.pay__card .price .per { font-size: 13px; color: var(--on-ink-muted); }
.pay__card-note { font-size: 13px; color: var(--on-ink-muted); margin: 2px 0 0; }
.pay__card-note b { color: var(--white); }
.pay__card ul { list-style: none; margin: 18px 0 22px; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.pay__card li { display: flex; gap: 10px; font-size: 12.5px; line-height: 1.5; color: var(--grey); }
.pay__card li::before { content: "→"; color: var(--orange); flex: none; }
.pay__card .btn { width: 100%; }
.pay__card .secured { margin-top: 12px; display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted-2); }
.pay__card .secured .dot { width: 5px; height: 5px; border-radius: 50%; background: var(--orange); }
/* Sits inside .pay__card, which is var(--ink) — every colour here has to be a
   light-on-dark value. This is the last chance to catch a mistyped address
   before it is locked at Stripe, so it is a bordered panel rather than fine
   print, and the address itself is the highest-contrast text in the card after
   the price. */
.pay__sendto {
  margin: 0 0 14px;
  padding: 12px 14px;
  border: 1px solid color-mix(in srgb, var(--paper) 20%, transparent);
  /* Structure, not surface: a well inside the 4px pay card has to step down. */
  border-radius: var(--radius-structure);
  background: color-mix(in srgb, var(--paper) 5%, transparent);
  text-align: left;
  font-size: 13px;
  line-height: 1.45;
}
.pay__sendto-label { display: block; font-size: 11px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--orange); margin-bottom: 5px; }
.pay__sendto-value { display: block; font-size: 14px; color: var(--white); word-break: break-all; }
.pay__sendto-edit { margin-top: 7px; padding: 0; background: none; border: none; cursor: pointer; font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--grey); text-decoration: underline; text-underline-offset: 3px; }
.pay__sendto-edit:hover { color: var(--orange); }
.pay__sendto-edit:focus-visible { outline: none; border-radius: var(--radius-control); box-shadow: var(--focus); }

.pay__back { background: none; border: none; cursor: pointer; font-family: var(--mono); font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); display: inline-flex; align-items: center; gap: 7px; padding: 4px 0; }
.pay__back:hover { color: var(--orange); }

/* --- the pay step on a phone -----------------------------------------------
   PENG-1866. The single-column collapse above stacks the invoice summary first,
   which put the pay button a full screen and a half below the fold: in headless
   Chromium at 390x664 (2026-08), before this rule, its top edge sat at 1069px.
   "CTA to buy is buried at bottom of the screen" is exactly right, and on the
   last step before money changes hands.

   So on a phone the plan card leads, and inside it the address panel and the
   button come before the feature bullets: plan, price, where the login link is
   going, Pay, "secured by Stripe", and only then the four bullets, which the ul
   ends the card with. The invoice lines and "your answers" follow below the card
   for whoever wants to check them, and "back to questions" goes down there with
   them, which is right — Back is the exception on this step, not the action.

   TWO CONSEQUENCES OF order: -1 THAT ARE PRODUCT DECISIONS, not side effects.
   Both were raised in review on PR #28 and neither is settled here; the design
   review on PENG-1866 owns them.

   The first is that .pay__summary carries the step's eyebrow and heading as well
   as the invoice ("Last step", and the h2 that solo.js personalises with the
   buyer's name), so moving the whole card above it opens the last step before
   payment on a bare dark card with no title over it. Lifting those two elements
   out of .pay__grid would keep the CTA high AND keep the step titled; it is a
   markup change that also moves them on desktop, which is why it is a question
   and not a commit.

   The second is focus order, and it is the one real exception to the "source
   order is also the reading order" argument used elsewhere in this file. Both
   grid items hold focusable controls, so at ≤780px a keyboard or screen-reader
   user reaches "back to questions" BEFORE Change and Pay, while visually Pay is
   above it (WCAG 2.4.3). The card's own children are not affected — the ul holds
   nothing focusable — and neither is S01's .about swap, for the same reason.

   Why this is a SECOND 780px block instead of an addition to the one above the
   grid: a media query adds no specificity, and both of the rules below share
   theirs with base rules that are declared later in this file
   (.pay__card's position: sticky, .pay__card ul's margin). Put them in the
   earlier block and the base rules win on source order — the card would still
   be sticky, and a sticky card that is also order:-1 pins itself over the
   summary it is meant to precede. Nothing about a dead CSS rule announces
   itself, which is why tests/mobile-viewport.test.js anchors its ordering
   assertions on these two rules specifically. It has to: `position: sticky`
   also appears in .topbar, so an assertion that merely searched for that string
   would compare this override against the top bar and pass with the bug fully
   restored. It did, until review caught it.

   order on the card's children rather than reordered markup, for the reason the
   S01 swap gives: source order is also the desktop order and the reading order,
   and "plan, price, what's in it, pay" is correct in both. `display: flex` is
   what makes the child order apply at all — the card is a block box otherwise,
   and order on a block's children does nothing. The ul drops its 22px bottom
   margin and keeps the 18px it already had above: it now ends the card instead
   of separating the price from the button, so the gap it needs is the one on top.

   This holds for the mailto branch too, but not because of the markup: when
   checkout is unavailable the button is replaced by .pay__unavailable and an
   <a class="btn">, and those are LATER siblings of the ul, as are the live
   branch's address panel and pay button. Sibling position does no work here at
   all — `order: 1` on the ul is the only reason the bullets end up last in
   either branch. Delete it believing the DOM already carries it and the CTA goes
   back under four bullets. */
@media (max-width: 780px) {
  .pay__card { order: -1; position: static; display: flex; flex-direction: column; }
  .pay__card ul { order: 1; margin: 18px 0 0; }
}

/* =========================================================================
   Placeholder landing pages
   ========================================================================= */
.ph {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}
.ph__hero {
  flex: 1;
  display: flex;
  align-items: center;
  padding: 80px 32px;
  background: var(--paper);
  position: relative;
  overflow: hidden;
}
.ph__inner { max-width: 820px; margin: 0 auto; position: relative; }
.ph__tag {
  display: inline-block;
  font-family: var(--display);
  font-size: 15px;
  letter-spacing: 0.1em;
  color: var(--white);
  background: var(--orange);
  padding: 5px 12px 3px;
  margin-bottom: 20px;
}
.ph__title { font-family: var(--display); text-transform: uppercase; font-size: clamp(44px, 8vw, 96px); line-height: 0.86; margin-bottom: 22px; }
.ph__lede { font-size: 18px; line-height: 1.6; color: var(--body); max-width: 560px; margin: 0 0 32px; }
.ph__note {
  margin-top: 40px;
  padding: 14px 16px;
  border-left: 2px solid var(--orange);
  background: var(--white);
  /* Flush where the orange rule caps it — that edge is met, not styled. */
  border-radius: var(--radius-flush) var(--radius-surface) var(--radius-surface) var(--radius-flush);
  font-size: 12.5px;
  color: var(--muted);
  max-width: 560px;
}
.ph__note b { color: var(--ink); }

/* /welcome — the post-payment page. Built on .ph so it reads as the same family
   as 404, with its sign-in link layered on top. The countdown rules that sat
   here went with the redirect in PENG-2232. */
/* The way in when the emailed link has not arrived. Always visible since
   PENG-2232 — it was the exception while the page redirected, and it is now the
   only control in the page's own content. (The sandbox and consent banners bring
   their own.) */
.welcome__fallback { margin: 0; }

.landings-list { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
@media (max-width: 820px) { .landings-list { grid-template-columns: 1fr; } }
.landings-list a {
  display: block;
  padding: 24px;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-surface);
  box-shadow: var(--shadow-xs);
  color: var(--ink);
  transition: border-color 0.12s ease;
}
/* Border only, no lift: on hover the border warms to half-strength orange and
   nothing else moves. That is My 5D's card hover — the lift belongs to buttons.
   Stated here because this is now the only card hover on the site; .feature,
   which used to hold this note, went with the 2026-08-17 artboard. */
.landings-list a:hover { border-color: color-mix(in srgb, var(--orange) 50%, transparent); color: var(--ink); }
.landings-list .slug { font-size: 11px; color: var(--muted-2); letter-spacing: 0.06em; }
.landings-list h3 { font-family: var(--mono); font-size: 15px; font-weight: 600; margin: 8px 0; }
.landings-list p { margin: 0; font-size: 13px; color: var(--body); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; }
}

/* Sandbox mode indicator, fixed to the top — louder than a bottom bar, and
   never covers the Buy button. Prevents demoing in sandbox and believing the
   payment worked.

   --sandbox-bar-h reserves space for the bar at the top of body, of .topbar
   (position:sticky — its offset must match or it re-emerges from under the
   bar on scroll), and of .flow (position:fixed;inset:0). Scoped to
   html[data-stripe-mode="sandbox"] so live/unavailable pages are unaffected.

   The two values below are only the pre-JS fallback: the inline script in
   partials/sandbox-banner.ejs measures the bar's real rendered height on
   load/resize and overwrites this variable once JS runs. .sandbox-bar itself
   does NOT read this variable — if it did, an inflated fallback would
   inflate the very box the script measures, permanently pinning the
   fallback past the breakpoint below (an orientation change is enough to
   trigger it). Leaving the bar unsized means it always reports its true
   height.

   Two tiers, not one, because the bar's flex-wrap point (~500-600px) tracks
   font metrics, not a clean breakpoint. 140px covers the wrapped 3-line case
   at 320-375px, where .flow/.topbar/404 need it most; 50px covers the
   single-line case past 600px. Do not collapse to one value — that
   reintroduces the bug this fixed. */
html[data-stripe-mode="sandbox"] {
  --sandbox-bar-h: 140px;
}
@media (min-width: 600px) {
  html[data-stripe-mode="sandbox"] {
    --sandbox-bar-h: 50px;
  }
}

html[data-stripe-mode="sandbox"] body {
  padding-top: var(--sandbox-bar-h);
}

html[data-stripe-mode="sandbox"] .topbar {
  top: var(--sandbox-bar-h);
}

html[data-stripe-mode="sandbox"] .flow {
  top: var(--sandbox-bar-h);
}

.sandbox-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  padding: 0.6rem 1rem;
  background: var(--warn);
  color: var(--white);
  font-size: 0.8rem;
  box-shadow: 0 2px 12px color-mix(in srgb, var(--ink-deep) 25%, transparent);
}

.sandbox-bar__tag {
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  border: 1px solid color-mix(in srgb, var(--white) 60%, transparent);
  border-radius: var(--radius-control);
  padding: 0.1rem 0.4rem;
}

.sandbox-bar__text { flex: 1 1 auto; }

.sandbox-bar__exit {
  color: var(--white);
  text-decoration: underline;
  white-space: nowrap;
}

.pay__unavailable {
  margin: 0 0 0.75rem;
  font-size: 0.9rem;
  line-height: 1.5;
}

/* Shown when POST /checkout fails. Distinct from .pay__unavailable: that one
   means "no checkout exists to offer", this one means "we tried and it did not
   work — try again". Both keep the mailto within reach. */
.pay__error {
  margin-top: 0.75rem;
  font-size: 0.875rem;
  color: var(--danger);
}

/* "You already have an account." Shown at the email step and again beside the
   pay button, and deliberately NOT styled as .pay__error: nothing failed, so it
   does not wear the danger colour. The CTA sits under the sentence rather than
   inside it because signing in IS this buyer's next action, and a link buried in
   prose reads as an aside. */
.taken {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  margin-top: 0.75rem;
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--body);
}

/* The waitlist message, and only that one. Two things make it a panel where the
   already-registered notice stays a footnote. It is the only thing left on S01
   once a waitlisted verdict takes Next and the pitch away, so a 0.875rem line of
   --body under an empty step reads as though something failed to load rather than
   as the answer. And it is the one notice here that reports something the visitor
   WANTED recorded, so it should look affirmed rather than apologetic.

   Nothing invented: the fill and the 2px --ink border are lifted wholesale from
   .opt[aria-pressed="true"], which is already this app's visual language for "this
   is registered". --radius-surface is the rung the ladder defines as content that
   arrives and leaves, which is exactly this box's life. No shadow — the offset one
   belongs to the single object being recommended on a page, and this is not it. */
.taken--waitlist {
  gap: 8px;
  padding: 14px 16px;
  border: 2px solid var(--ink);
  border-radius: var(--radius-surface);
  background: var(--orange-tint);
  color: var(--ink);
  font-size: 1rem;
  font-weight: 500;
}

/* The in-flight state. pointer-events rather than the disabled attribute so the
   button keeps its accessible name and stays announced; aria-busy is what tells
   assistive technology the work is under way. */
.btn[data-buy][aria-busy="true"],
/* Next carries the same state while the email step waits on the eligibility
   answer. Bounded by a timeout in solo.js, so the buyer cannot be left looking
   at this: a slow answer advances them rather than holding the step. */
.btn[data-next][aria-busy="true"] {
  opacity: 0.65;
  pointer-events: none;
}

/* Floating language switcher (views/partials/language-switcher.ejs) — a globe pinned bottom-right
   that opens a short language menu. Purely additive: it re-renders the page in the chosen language
   and touches nothing else. Only declared tokens, per AGENTS.md §5. */
.lang-switcher { position: fixed; right: 16px; bottom: 16px; z-index: 40; }
.lang-switcher__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  color: var(--ink);
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-overlay);
  box-shadow: var(--shadow-overlay);
  cursor: pointer;
}
.lang-switcher__toggle:hover { border-color: var(--muted-2); }
.lang-switcher__toggle:focus-visible { outline: none; border: 2px solid var(--orange); box-shadow: var(--focus); }
.lang-switcher__menu {
  position: absolute;
  right: 0;
  bottom: calc(100% + 8px);
  min-width: 148px;
  margin: 0;
  padding: 4px;
  list-style: none;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-overlay);
  box-shadow: var(--shadow-overlay);
}
.lang-switcher__menu[hidden] { display: none; }
/* The consent notice is a full-width fixed bar pinned to the bottom (z-index 2147483000); the
   switcher sits in its footprint. While the notice is visible, hide the switcher so it can never
   cover, or be covered by, the Accept control — the same overlap that bit the S01 flow in
   PENG-1872. The notice ships with [hidden] set and is a body-level sibling BEFORE the switcher,
   so this general-sibling rule tracks it exactly; when it is dismissed the switcher returns. */
#consent-banner:not([hidden]) ~ .lang-switcher { display: none; }
.lang-switcher__menu li { margin: 0; }
.lang-switcher__option {
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 10px;
  font-size: 14px;
  color: var(--ink);
  background: none;
  border: 0;
  border-radius: var(--radius-control);
  cursor: pointer;
}
.lang-switcher__option:hover { background: var(--orange-tint); }
.lang-switcher__option.is-active { color: var(--orange); font-weight: 600; }
.lang-switcher__option:focus-visible { outline: none; box-shadow: var(--focus); }
