/* ==========================================================================
   ClaudCli — one stylesheet, no inline styles anywhere.

   That is a constraint the content security policy imposes, not a preference:
   `style-src 'self'` with no 'unsafe-inline' means a `style=` attribute does
   not apply. Every rule a page needs therefore lives here, which also means
   there is exactly one place to look when something renders wrong.

   DARK ONLY, BY DECISION. There is no light theme and no
   `prefers-color-scheme` override: the palette below is the design, and a
   second one would double the surface area of every colour decision to serve
   a preference this product does not have. What *is* honoured is
   `prefers-reduced-motion`, at the bottom of this file — an animation
   preference is an accessibility need, not a taste.

   Colours are custom properties on :root. Nothing below sets a literal colour
   except inside this block and the font stacks, so a re-theme is one edit
   rather than a search. A reseller's dashboard is read for numbers, so the
   numbers get the contrast and the chrome stays quiet.

   THE FONTS ARE SELF-HOSTED, and that is a security decision rather than a
   performance one. `font-src 'self'` and `style-src 'self'` together refuse
   both a Google Fonts stylesheet and a cross-origin @font-face, so loading
   Inter from a CDN would mean loosening two directives to save a request —
   and it would hand a third party the IP and Referer of every visitor on
   first paint. The files are in static/fonts/, vendored by scripts/vendor_fonts.py.
   ========================================================================== */

/* Latin and latin-ext only. The site is written in English, so the cyrillic,
   greek and vietnamese subsets Google also publishes would be bytes on the
   wire that no visitor renders. A character outside these ranges falls through
   to the next family in --sans, which is the correct behaviour and not a bug.

   THE FILENAMES CARRY A CONTENT HASH, and they have to. Everything under
   /static is served `max-age=31536000, immutable`, which is only safe because
   a changed file gets a changed URL — templates do that with
   `?v={{ asset_version }}`. **A url() inside a stylesheet is not rewritten by
   that query string**, so a font at a fixed path would be pinned in every
   returning visitor's cache for a year with no way to evict it. Hashing the
   bytes into the name is what makes the cache key change when the font does.
   test_web_fonts.py asserts each name still matches its content. */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/inter-latin-ext.34b9c504.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/inter-latin.3100e775.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/jetbrains-mono-latin-ext.db5ff4db.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/jetbrains-mono-latin.83c005d4.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  /* `color-scheme` is what makes native widgets — scrollbars, the date
     picker, autofill, the caret — render dark. Without it the page looks
     correct until a customer types into it and the browser paints a white
     input box and a white scrollbar. */
  color-scheme: dark;

  --bg: #0a0908;
  --surface: #131110;
  --surface-2: #1c1917;
  --surface-3: #262220;
  --border: #2b2622;
  --border-bright: #453d37;

  --text: #f5f2ef;
  --text-dim: #a8a29e;
  /* 5.8:1 on --bg, 5.1:1 on --surface-2, 4.6:1 on --surface-3 — over AA on
     every ground this token is actually used on. This token is footers, table
     headers and field hints: small text, all of it. It has been below the line
     before, which is why the numbers are written down. */
  --text-faint: #918983;

  /* ONE ACCENT, USED SPARINGLY. A reseller site that glows in five colours
     reads as a landing page; this one is read by engineers.

     The ground is warm near-black and the accent is a terracotta orange — the
     ClaudCli mark, and the same warm family the CLI uses. Nothing here is a
     neutral grey picked off a ramp: every value below is warm-shifted, because
     a pure grey next to terracotta reads as a different, colder design that
     happens to share a page.

     The accent is deliberately split across two tokens, because one cannot do
     both jobs. `--accent` is the bright terracotta used as TEXT, as borders and
     as glow on the near-black ground (7.9:1 on --bg); `--accent-fill` is the
     deeper one used as a button FILL. The bright one as a fill cannot hold any
     ink at all — white on it is 2.4:1 — and the deep one as body text is a
     muddy brown on black.

     `--accent-2` is the amber half of the brand gradient — the mark, and the
     payoff half of a headline. It carries the same split for the same reason:
     `--accent-2-fill` is the deep amber the logo tile is filled with, which has
     to hold the glyph, and `--accent-2` is the bright amber used as text on
     near-black (10.3:1).

     **THE INK ON A FILL IS NEAR-BLACK, NOT WHITE**, and that is a measured
     decision rather than a style. White on terracotta is 3.1:1 — under AA for a
     button label — while the warm near-black is 6.0:1. It is also what makes
     the buttons read as ClaudCli rather than as a generic dark-theme UI. */
  --accent: #f0876a;
  --accent-hover: #ffa98f;
  /* The channel triples. Every wash, glow and bloom below is DERIVED from the
     token it is a wash of — `rgba(var(--accent-fill-rgb), 0.16)` rather than a
     second hand-copied literal. `rgba()` cannot take a hex token, so the
     channels have to be a token of their own, and that is the whole reason
     these three lines exist.

     They were not free. Before them the accent's channels were written out
     longhand in eleven places outside this block, so retuning the accent left
     eleven washes behind at the old hue, each one *nearly* right and therefore
     invisible in review. A derived value cannot drift. */
  --accent-rgb: 240, 135, 106;
  --accent-fill: #d97757;
  --accent-fill-rgb: 217, 119, 87;
  /* The hover LIGHTENS, which is the opposite of what this file used to do, and
     the reason is the ink rather than taste. With white on the fill the hover
     had to deepen to keep the label legible; with near-black ink a deeper fill
     moves the label *towards* the fill. #c2603c is where that runs out — ink on
     it is 4.50:1, exactly on the AA line. Lightening takes the same hover to
     7.3:1, so the control still reads as engaging and the label gets easier to
     read rather than harder. */
  --accent-fill-hover: #e8895f;
  --accent-soft: rgba(var(--accent-fill-rgb), 0.16);
  --accent-line: rgba(var(--accent-rgb), 0.34);
  /* Ink for text sitting ON --accent-fill. 6.0:1 on the fill, 7.3:1 on the
     hover, 5.1:1 on --accent-2-fill. */
  --accent-ink: #141110;
  --accent-2: #e8b06a;
  --accent-2-fill: #b8763a;
  --accent-2-fill-rgb: 184, 118, 58;

  /* The washes that are not a colour in their own right, but a relationship
     between the ground and what sits on it. Named for the job, not the value,
     because the value is the one thing about them that should change. */
  --bg-rgb: 10, 9, 8;
  --hairline: rgba(255, 255, 255, 0.06);
  --row-hover: rgba(255, 255, 255, 0.02);
  --sheen: rgba(255, 255, 255, 0.025);
  --grid-line: rgba(255, 255, 255, 0.045);
  --mask-solid: #000;
  /* The tick beside a benefit. An alpha mask rather than a fill, so it is drawn
     in whatever colour the rule sets and one glyph serves every context — the
     stroke is black only because a mask reads the shape's alpha and the colour
     never reaches the page. */
  --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E");

  /* The window traffic lights on a code card. Three literals on purpose and
     deliberately not derived from the palette: they are not a status, they are
     a picture of a window, and a themed one would be nonsense. They are the one
     group of colours on this site that a re-theme should leave alone. */
  --dot-1: rgba(248, 113, 113, 0.8);
  --dot-2: rgba(251, 191, 36, 0.8);
  --dot-3: rgba(52, 211, 153, 0.8);

  /* The code surfaces are darker than `--surface`, because a sample has to read
     as an inset well rather than as one more card. Two steps: `--card-bg` is
     the frame, `--code-bg` is what fills it. */
  --card-bg: #0c0a09;
  --code-bg: #0e0c0b;
  --code-fg: #d6cfc9;

  /* The sparkline ramp. Levels 1-4 step up the terracotta by luminance only —
     the hue is held at the accent's, so a busy hour reads as "warmer" rather
     than as "a different colour". Level 1 is deliberately almost invisible
     against `--surface-2`: a heat map whose quietest cell is legible has no
     range left to show. */
  --level-1: #3a2418;
  --level-2: #6b3a20;
  --level-3: #a85a30;
  /* The top two steps are the accent itself and its hover, rather than two more
     literals that happen to match them. They were already the same colours —
     written out by hand, so retuning the accent would have left the busiest
     hour on the chart at the old hue. */
  --level-4: var(--accent-fill);
  --level-5: var(--accent-hover);

  /* The hero's light. A terracotta core giving way to amber, so the glow reads
     as the brand's two colours meeting rather than as a single orange lamp. */
  --bloom-core: rgba(var(--accent-fill-rgb), 0.30);
  --bloom-mid: rgba(var(--accent-2-fill-rgb), 0.13);

  /* Status colours are kept off the accent's hue on purpose. The accent is a
     terracotta orange, so an amber `--warn` sitting in the same view as an
     accent link reads as the same family twice — and on /admin/traffic a warn
     badge and an accent value appear in the same row. `--warn` is pushed to a
     yellow that shares no hue with the accent, and `--ok` stays green. */
  --ok: #4ade80;
  --ok-soft: rgba(74, 222, 128, 0.12);
  --warn: #facc15;
  --warn-soft: rgba(250, 204, 21, 0.12);
  --danger: #f87171;
  --danger-soft: rgba(248, 113, 113, 0.12);

  /* ---- Radius ------------------------------------------------------------

     A closed set, and that is the point. Before this ramp the file used three
     radius tokens and five literals — 8px, 12px, 4px, 2px and 999px — so the
     corner of a box was decided by whoever wrote that rule rather than by what
     the box is. Six steps is enough for every surface on the site and few
     enough to hold in mind. */
  --radius-xs: 6px;
  --radius-sm: 10px;
  --radius-md: 12px;
  --radius: 16px;
  --radius-lg: 20px;
  --radius-pill: 999px;

  /* ---- Type --------------------------------------------------------------

     One fluid ramp, and `html` sets `font-size: 100%` so `1rem` is genuinely
     16px — the anchor the whole scale is measured from.

     That anchor was missing, which is the finding this ramp exists to fix. The
     body was set to 15px while every `rem` in the file resolved against the
     browser's 16px default, so the scale was not proportional to the text it
     was scaling, and the display sizes had drifted up to compensate. The
     hierarchy had also collapsed: the hero headline was 62px and the section
     headings directly under it were 28px, which is not a step down, it is a
     cliff — there was nothing between the loudest thing on the page and the
     body copy.

     The steps below are the ramp that fills it. The three `clamp()` sizes are
     display type: they scale with the viewport so a 2560px screen gets a
     headline that fills it and a 390px phone gets one that does not overflow,
     from one declaration rather than a breakpoint per size. The floor of each
     clamp is the size at 390px and the ceiling is the size at ~1100px. */
  /* The smallest step, and it exists for one job: the unit on a column of
     numbers — `<th>Billed <span class="unit">tokens</span></th>`. It sits below
     `--text-2xs` because it has to be readable as a word *and* quieter than the
     label it follows, and at the same size the only lever left is opacity,
     which reads as a disabled control rather than as a footnote. */
  --text-3xs: 0.75rem;
  --text-2xs: 0.8125rem;
  --text-xs: 0.875rem;
  --text-sm: 0.9375rem;
  --text-base: 1rem;
  --text-md: 1.0625rem;
  --text-lg: clamp(1.125rem, 0.9rem + 0.6vw, 1.375rem);
  --text-xl: clamp(1.375rem, 1rem + 1vw, 1.75rem);
  --text-2xl: clamp(1.75rem, 1.2rem + 1.6vw, 2.5rem);
  --text-3xl: clamp(2.125rem, 1.2rem + 3vw, 3.5rem);
  --text-display: clamp(2.5rem, 1.1rem + 4.6vw, 4.25rem);

  /* The measure, in characters, for a block of running prose.

     The one type decision that is not about size. `.wrap` is 1280px, and at
     `--text-base` a paragraph set to that width is about 135 characters; the
     range a line can be tracked along is roughly 45 to 75, and past it the eye
     loses the start of the next line on the return sweep. 72ch is the top of
     that range, chosen over 65 because the ground is dark and the leading is
     generous, which both push the comfortable figure up.

     Applied only where prose is not already constrained by its container — the
     docs column, which is otherwise 1080px wide. It is deliberately NOT
     applied inside `.card`, where a paragraph stopping a third short of its
     own border reads as a mistake rather than as a measure; prose cards are
     narrowed instead, which fixes the same problem without the ragged edge. */
  --measure: 72ch;

  /* Tracking is a property of *size*, not of style. A large headline set at
     normal tracking reads as loose because the gaps between letters grow with
     the glyphs; small uppercase text needs the opposite, because uppercase
     forms have no descenders to separate them. These four encode that, so
     `letter-spacing` is a decision made once per role instead of a number
     invented per rule — which is how the file previously arrived at three
     different spacings (0.07em, 0.08em, 0.13em) for the same visual role. */
  --tracking-tight: -0.02em;
  --tracking-display: -0.045em;
  --tracking-label: 0.08em;
  --tracking-label-wide: 0.14em;

  /* ---- Space -------------------------------------------------------------

     A 4px ramp. The file previously had a de-facto one spanning 2px to 108px
     in values nobody had chosen as a set — 22px here, 26px there — which is
     what makes a layout feel almost-aligned. Every gap, pad and margin below
     reads from these, so two sections that should agree do. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-7: 32px;
  --space-8: 40px;
  --space-9: 48px;
  --space-10: 64px;

  /* Three contextual sizes, so a page does not restate the ramp by hand.

     `--section-pad` was 112px, which is 224px between two sections before
     either one's content margins — three quarters of a laptop screen of empty
     ground between one argument and the next. On a near-black page with no
     rules left to divide it, that stopped reading as air and started reading
     as four unrelated documents stacked in one scroll. 96px keeps the pacing
     deliberate and lets the sequence read as a sequence. */
  --gutter: clamp(var(--space-5), 3vw, var(--space-7));
  --section-pad: clamp(var(--space-10), 6.5vw, 96px);
  --card-pad: var(--space-6);

  /* Both families are vendored in static/fonts/ — see the @font-face block at
     the top of this file for why. The system stacks behind them are not
     decoration: a glyph outside the latin ranges falls through to whatever the
     reader's machine has, and a browser that fails to fetch 80KB of font still
     renders the site. */
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo,
    Consolas, monospace;
  --sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;

  /* ---- Material ----------------------------------------------------------

     Two shadows, split by how often the thing carrying them appears.

     `--shadow-tight` is for repeated components — the cards, tiles and stats
     that appear a dozen times on one screen. Its ambient half is a 1px lit top
     edge rather than a large soft halo, which is what stops a grid of twelve
     tiles reading as twelve glowing boxes, and it is materially cheaper to
     paint: an 18px-offset 40px-blur shadow is a large offscreen layer per
     element, and /admin/traffic was painting about eighteen of them at once.

     `--shadow` keeps the halo, and is now reserved for the few surfaces that
     genuinely sit above the page: the hero's code card and the closing CTA. */
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.5), 0 18px 40px -24px rgba(0, 0, 0, 0.9);
  --shadow-tight: 0 1px 2px rgba(0, 0, 0, 0.4), inset 0 1px 0 var(--hairline);
  --card-grad: linear-gradient(180deg, var(--sheen), transparent 72px);
  --glow: 0 0 0 1px var(--accent-line), 0 0 30px -8px rgba(var(--accent-fill-rgb), 0.6);
  --glow-soft: 0 0 52px -16px rgba(var(--accent-fill-rgb), 0.5);
}

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

html {
  /* The anchor the whole type ramp is measured from. `100%` is the reader's own
     browser default — normally 16px — and setting it explicitly is not a
     no-op: with no root size declared, any rule that moved it would rescale
     every `rem` on the site at once. The body below is `--text-base`, so `1rem`
     and body copy are finally the same number, which is what makes the ramp
     proportional to the text it scales rather than merely large. */
  font-size: 100%;

  /* Anchor links inside /docs and /pricing scroll rather than jump. The
     reduced-motion block at the bottom of this file forces this back to
     `auto`, which is what makes it safe to set here. */
  scroll-behavior: smooth;
}

::selection {
  background: var(--accent-fill);
  color: var(--accent-ink);
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: var(--text-base);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  /* No faux bold and no faux italic. Inter is vendored as a variable face with
     a real 400-700 axis, so a synthesized weight is never needed and is always
     worse than the drawn one: the browser smears the outline sideways rather
     than using the designed letterform, which is most visible on exactly the
     large headlines this pass is about. */
  font-synthesis: none;
}

/* A single fixed radial wash, so the near-black ground has depth without an
   image (no CDN, no assets, and `img-src 'self'` would allow one but there is
   no reason to add a request for something CSS paints for free).

   Terracotta from the top, amber from the right corner. Both are the same hues
   as the accent layer at a fraction of the alpha — the brief's "restrained" is
   doing real work here, because a wash the reader can *see* is a wash that has
   stopped being a background. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(1000px 560px at 50% -12%, rgba(var(--accent-fill-rgb), 0.16), transparent 68%),
    radial-gradient(760px 440px at 100% 0%, rgba(var(--accent-2-fill-rgb), 0.09), transparent 70%);
}

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

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

h1,
h2,
h3,
h4 {
  margin: 0 0 0.5em;
  line-height: 1.15;
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  /* A headline breaks where the line lengths balance rather than where the
     column happens to end. Free, progressive, and it does more for a heading
     than any amount of tracking — a three-word last line is the single most
     common tell of an unconsidered headline. */
  text-wrap: balance;
}

h1 {
  font-size: var(--text-2xl);
}

h2 {
  font-size: var(--text-xl);
}

h3 {
  font-size: var(--text-lg);
}

p {
  margin: 0 0 1em;
}

/* A lede is read before it is scanned, so it breaks to avoid a single trailing
   word rather than to balance two lines into a block. `pretty` is the opposite
   trade from `balance` and that is why it is the one used here: a paragraph is
   several lines long, and balancing a paragraph makes the first line short. */
.lede,
.hero .lede,
.page-head p {
  text-wrap: pretty;
}

code,
pre,
kbd {
  font-family: var(--mono);
  font-size: 0.875em;
}

/* Visible focus for keyboard users. `:focus-visible` rather than `:focus`, so
   a mouse click does not leave a ring behind on every button. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* --------------------------------------------------------------------------
   Layout
   -------------------------------------------------------------------------- */

.wrap {
  /* Fluid gutters and a wider ceiling, in one declaration.

     The old rule was a flat `max-width: 1180px` with a fixed 24px pad at every
     viewport, so a 2560px display got a 1132px column: the site stopped growing
     at 1180px and left the outer thirds of a large screen empty, which reads as
     a page that was never looked at on the screen it is being read on.
     `100% - gutter * 2` lets the gutter breathe at every size, and 1280px is
     where the measure stops being comfortable to read rather than a number
     picked because it looked right once. */
  width: min(100% - var(--gutter) * 2, 1280px);
  margin: 0 auto;
}

/* The same wrap with a wider ceiling, for the one section on the site whose
   content is not prose.

   WHY THIS IS A SECOND CLASS AND NOT A WIDER `.wrap`. The 1280px in `.wrap` is
   an argument about a measure — how many characters of running text stay
   comfortable — and widening it would not widen any text: `.section-head .lede`
   is capped at 54ch and `.section > .card` at 820px, so a 1700px wrap would
   leave the lede column ~700px of empty space away from the heading it belongs
   to, and every other page with it. The cards are not prose. They have no
   measure to protect, they are five objects in a row, and a row of objects
   wants the width. So the page keeps `.wrap` for its words and reaches for
   this only for the one thing that is not words.

   `100% - gutter * 2` rather than a flat `calc(100% - 64px)`: the gutter is
   `clamp(20px, 3vw, 32px)`, so this IS 64px of total side room once the
   viewport is wide enough to afford it, and 40px on a laptop — one rule that
   gives the requested result at the size the request was about without
   pinning 32px of margin onto a 900px screen.

   What it does NOT do is change anything below a 1344px viewport: up to there
   `min(100% - gutter*2, 1700px)` and `min(100% - gutter*2, 1280px)` return the
   same number, because the available width is the binding constraint and
   neither ceiling has been reached. The whole change is confined to screens
   with room to spare. */
.wrap-wide {
  width: min(100% - var(--gutter) * 2, 1700px);
  margin: 0 auto;
}

/* No `border-bottom`. There used to be a 1px hairline here and at the foot of
   every `.section` and above the footer, which together ruled the whole site
   into horizontal bands like a printed table. On a near-black ground a 1px
   grey line is the most visible thing on the page, so the eye read the rules
   first and the content second.

   The header still separates itself from what scrolls under it, without a
   line: the 78% ground plus the blur. That is what the blur is for.

   THE BLUR IS 10px, NOT 14px, AND THAT IS A COST DECISION. `backdrop-filter`
   samples everything painted beneath the element and re-blurs it, so on the
   landing page it is re-blurring the hero bloom — which the scroll motion is
   translating every single frame. The blur radius is the dominant term in that
   cost: 14px forces a wider sample and a larger offscreen pass than 10px does,
   for a difference the eye cannot pick out under a 78% ground. */
.site-header {
  background: rgba(var(--bg-rgb), 0.78);
  /* Supported widely enough to matter; the solid colour above is the fallback
     for anything that ignores it. */
  backdrop-filter: blur(10px);
  position: sticky;
  top: 0;
  z-index: 20;
}

.site-header .wrap {
  display: flex;
  align-items: center;
  gap: var(--space-7);
  height: 68px;
}

.brand {
  font-weight: 650;
  font-size: var(--text-md);
  color: var(--text);
  letter-spacing: var(--tracking-tight);
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.brand:hover {
  text-decoration: none;
}

/* The whole logo: a rounded tile with the brand gradient and a bloom behind it,
   carrying the node glyph written into base.html. No image file, so it cannot
   404 and costs no request.

   The gradient runs `--accent-fill` to `--accent-2-fill` — the two DEEP
   tokens, not the bright ones. Its job is to hold the glyph, and white on
   either bright token is 2.4:1 and 2.3:1, which is not a logo, it is a smudge.
   Dark-on-warm is the pair that works. The bright pair is for the gradient that
   has to survive as *text* on the dark ground, which `.grad-text` uses. */
.brand-mark {
  width: 26px;
  height: 26px;
  border-radius: var(--radius-xs);
  background: linear-gradient(135deg, var(--accent-fill), var(--accent-2-fill));
  box-shadow: var(--glow-soft);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  color: var(--accent-ink);
}

.brand-mark svg {
  width: 17px;
  height: 17px;
  display: block;
}

/* `.brand-mark` is an EMPTY SPAN in admin/base.html and dashboard/base.html —
   the glyph is written into base.html only, so the two panel shells have been
   rendering a blank gradient tile with nothing inside it. The include at
   templates/_brand_mark.html is what fixes that; this rule is unchanged and
   merely now has something to hold. */

/* The disclosure the nav lives in. On desktop it is invisible to the layout —
   it holds the nav and takes the `margin-left: auto` that used to right-align
   the nav itself — and its `summary` is not rendered, so there is no control
   to close it with.

   `min-width: 0` because a flex item defaults to `min-width: auto`, which is
   its content's min-content width — and the nav inside does not wrap above the
   phone breakpoint, so that floor would push the header wider than its own
   container instead of letting the row settle. */
.nav-disclosure {
  margin-left: auto;
  min-width: 0;
}

.nav-disclosure > summary {
  display: none;
}

/* The one ascending media query in this file, and it has to be ascending.

   The element ships CLOSED, because a phone must not get an expanded menu on
   first paint and there is no attribute that means "open only when wide". So
   the desktop case is the one that needs forcing, and forcing it means
   overriding the element's own rendering rather than relying on it.

   `::details-content` is the box that wraps a `<details>`'s contents. In the
   current engine it is what carries the hiding, as `content-visibility:
   hidden` — the user-agent stylesheet, which any author declaration beats, so
   `visible` here is the whole override. In the previous engine the hiding was
   a `display: none` on the children, which `.site-nav`'s own `display: flex`
   already beats for the same reason.

   The two together mean this works on either engine, and neither branch is
   load-bearing on its own: if `::details-content` is not understood the rule
   is dropped and the other mechanism is in play.

   Written as `min-width` rather than undoing it again below 820px because the
   mobile block would then have to restate two user-agent defaults to get back
   to where it started, and restating a default is how a rule stops matching
   the browser it runs in. */
@media (min-width: 821px) {
  .nav-disclosure::details-content {
    content-visibility: visible;
  }
}

/* `.site-nav` keeps its own `margin-left: auto`: the two panels render it
   directly under `.wrap` with no disclosure around it, and it is what aligns
   their header row to the right. Inside the disclosure the declaration is
   inert — an auto left margin on a block-level box in a block formatting
   context does not move it — so one rule serves both arrangements. */
.site-nav {
  display: flex;
  gap: var(--space-5);
  margin-left: auto;
  align-items: center;
}

.site-nav a {
  color: var(--text-dim);
  font-size: var(--text-sm);
}

.site-nav a:hover {
  color: var(--text);
  text-decoration: none;
}

.site-nav a.active {
  color: var(--text);
}

/* A button in the nav keeps its own ink.

   `.site-nav a` is 0-1-1 and `.btn-primary` is 0-1-0, so the nav's link colour
   won inside the header CTA and the fill's ink never applied: at rest the label
   rendered `--text-dim`, which is **1.24:1** on `--accent-fill` — a filled
   button whose label is all but invisible. Below 820px the nav rule switches to
   `--text` and it is 2.80:1, which still fails AA. `--accent-ink` is 6.02:1,
   and that flip is the entire reason this palette carries a near-black ink
   token instead of white, so the nav yields.

   `:hover` is deliberately not exempted. The two selectors tie at 0-2-1 and
   this block is later in the file, so a hovered CTA keeps its dark label and
   only the fill lightens — which is what `.btn-primary:hover` intends.

   Only `.btn-primary` is compensated, and that is a decision rather than an
   omission. Both buttons in the nav are primary; a plain `.btn` there would
   take the nav's `--text-dim` on `--surface-2`, which is muted but perfectly
   legible — a styling difference, not the 1.24:1 failure this fixes. Writing a
   rule for a button the markup does not contain is the dead-selector shape
   this file refuses elsewhere. */
.site-nav a.btn-primary {
  color: var(--accent-ink);
}

main {
  padding: var(--space-8) 0 var(--space-10);
}

/* No `border-top` either — see `.site-header`. The 64px of `main` padding
   below the last section is what separates the footer now. */
footer {
  padding: var(--space-7) 0;
  color: var(--text-faint);
  font-size: var(--text-xs);
}

footer .wrap {
  display: flex;
  gap: var(--space-5);
  flex-wrap: wrap;
  align-items: center;
}

footer a {
  color: var(--text-faint);
}

footer a:hover {
  color: var(--text);
}

/* --------------------------------------------------------------------------
   Surfaces and cards
   -------------------------------------------------------------------------- */

/* A card is a repeated component, so it takes the tight shadow and the sheen
   rather than the ambient halo. See `--shadow` in the token block for why that
   split exists: a screen of twelve glowing boxes is the "generic dark
   dashboard" tell, and it is also the most expensive thing this stylesheet
   paints. */
.card {
  background-color: var(--surface);
  background-image: var(--card-grad);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--card-pad);
  margin-bottom: var(--space-5);
  box-shadow: var(--shadow-tight);
}

/* A card that holds PROSE hugs its text instead of spanning the wrap.

   `.wrap` is 1280px and a card has 24px of padding a side, so a paragraph in
   one ran to 1232px — about 135 characters at `--text-base`, against the 45–75
   that a line of text can be tracked along. On /pricing and /contact that
   produced cards holding one or two very long lines in a box twice their
   width, which is the worst of both: unreadable *and* empty.

   Scoped to `.section > .card` rather than `.card`, and the scope is the whole
   point. Every public card is a direct child of a `.section`; the tables are
   in a `.table-scroll` and the panel cards are inside `.shell`, neither of
   which is affected. The measurement to hold in mind is the one this file
   keeps coming back to — 820px of card is 772px of text, which is about 85
   characters, and 85 is where a line stops being comfortable rather than
   where it stops being legible. */
.section > .card {
  max-width: 820px;
}

/* A card whose last child is a button. The card's own bottom padding collapsed
   against the paragraph above it otherwise, because `.card > :last-child` zeroes
   the margin the button relied on. */
.card-actions {
  margin-top: var(--space-4);
}

.card-actions:last-child {
  margin-top: var(--space-4);
  margin-bottom: 0;
}

/* The note under a form field — a requirement the user has to satisfy, so it
   sits with the input rather than in a help block at the top. */
.field-hint {
  font-size: var(--text-sm);
  margin-top: var(--space-1);
  color: var(--text-faint);
}

.card > :last-child {
  margin-bottom: 0;
}

.card-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
  flex-wrap: wrap;
}

.card-head h2,
.card-head h3 {
  margin: 0;
}

.card-head .spacer {
  margin-left: auto;
}

/* --------------------------------------------------------------------------
   Stats
   -------------------------------------------------------------------------- */

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--space-4);
  margin-bottom: var(--space-6);
}

.stat {
  background-color: var(--surface);
  background-image: var(--card-grad);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4) var(--space-5);
  box-shadow: var(--shadow-tight);
}

.stat-label {
  font-family: var(--mono);
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--text-faint);
  margin-bottom: var(--space-1);
  font-weight: 600;
}

.stat-value {
  font-family: var(--mono);
  font-size: var(--text-xl);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-tight);
}

.stat-note {
  font-size: var(--text-sm);
  color: var(--text-dim);
  margin-top: var(--space-1);
}

/* --------------------------------------------------------------------------
   Tables
   -------------------------------------------------------------------------- */

/* The negative margin is a bleed: it lets the table scroll past the card's
   padding instead of stopping short of it. That makes the two numbers the SAME
   number, which is why both now read `--card-pad` — as two literals they were
   coupled by hand, and the moment the card's padding changed the bleed would
   have started 2px short on every table on the site with nothing to catch it.

   `app.js` builds this same div for the in-flight table, inside the same card,
   so the coupling holds for the scripted case too. */
.table-scroll {
  overflow-x: auto;
  margin: 0 calc(var(--card-pad) * -1);
  padding: 0 var(--card-pad);
}

/* The front-page catalogue is a preview of the table `/models` carries in full,
   so it is held to the width of the heading column above it — half the wrap,
   less half the `.section-head` gap.

   At the full 1280px wrap the three short columns put `MODEL` at x=88,
   `CONTEXT` at x=520 and `MAX OUTPUT` at x=888 with the values right-aligned at
   890 and 1352: one row of data spread across the whole page, every label a few
   hundred pixels from the thing it named, and the right-hand third of the table
   empty. Capping it to the heading column also lands the table on the same left
   edge as "Models" instead of running 1200px past it.

   It is gated on the same 900px breakpoint the `.section-head` collapses at,
   and that gate is load-bearing rather than tidy. Below 900px the head is one
   column, so half the wrap is no longer a column width — the formula would go
   on halving a full-width row and cap the table at roughly 400px on a tablet
   that has the room for all of it. The cap only means anything while there are
   two columns to align to.

   Deliberately scoped to `#models` rather than to `table`: on `/models` the
   catalogue IS the page rather than a taster for it, and it is meant to use the
   full width. */
@media (min-width: 901px) {
  #models table {
    max-width: calc((100% - var(--space-9)) / 2);
  }
}

/* The "full catalogue" link is the next step, not another row.

   Sitting directly under the last row at the table's own left edge, it read as
   a continuation of the data — a fourth row whose first cell happened to be a
   sentence. `p` carries no top margin, so the separation has to be explicit. */
#models .table-scroll + p {
  margin-top: var(--space-5);
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

th {
  text-align: left;
  font-family: var(--mono);
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--text-faint);
  font-weight: 600;
  padding: 0 var(--space-3) var(--space-2);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

/* The unit on a column of numbers — `<th>Metered <span class="unit">tokens</span></th>`.

   A bare count and a bare token count are the same three or four digits in the
   same mono face, and this panel puts both on one page: `METERED 52,817` beside
   `BILLED 15` is a metered token count next to a wallet-token count, and without
   a unit the pair reads as a bug in the arithmetic rather than as two different
   things being counted. The unit is what makes a column self-describing, so it
   has to be quieter than the label it follows — same face, smaller, and lighter
   still, because it is read once and then skipped. */
.unit {
  font-size: var(--text-3xs);
  /* Both resets are for the `td` case: the value cell inherits the header's
     uppercase and tracking only if the unit is used outside a `th`, and it is
     cheaper to state the intent once than to depend on where it is placed. */
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-faint);
  opacity: 0.75;
  font-weight: 400;
}

td {
  padding: var(--space-3);
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}

tbody tr:hover td {
  background: var(--row-hover);
}

tbody tr:last-child td {
  border-bottom: none;
}

.num {
  font-variant-numeric: tabular-nums;
  text-align: right;
  font-family: var(--mono);
  font-size: var(--text-xs);
}

/* A numeric column's header sits over the numbers it labels.

   `th` is left-aligned and `td.num` right-aligns, so on the catalogue tables
   "Context" sat roughly 300px from the value underneath it and stopped reading
   as a label at all — the header row and the data row were two unrelated
   fragments sharing a rule.

   Putting `.num` on the `th` would fix the alignment but drag its size up from
   `--text-2xs` to `--text-xs`, leaving the numeric headers a step larger than
   the text header beside them; so the size is pinned back here. `th.num` is
   0-1-1 against `.num`'s 0-1-0, so this wins for `font-size` as well as for
   `text-align`. */
th.num {
  text-align: right;
  font-size: var(--text-2xs);
}

.mono {
  font-family: var(--mono);
  font-size: var(--text-xs);
}

.dim {
  color: var(--text-dim);
}

.faint {
  color: var(--text-faint);
}

.nowrap {
  white-space: nowrap;
}

/* Signed money, in a column that holds both signs.

   A credit and a debit sit in the same column of the same table — the affiliate
   ledger — and the only thing telling them apart is a leading minus at
   `--text-xs` in a monospace column, which is exactly the kind of mark the eye
   skips when it is scanning a column of numbers. So the sign is carried by
   colour as well. The colour is redundant, never the only signal: the minus is
   still in the string, which is what makes this safe for a reader who cannot
   distinguish the two colours.

   Reached from `views.AffiliateLedgerRowView`, which derives the flag from the
   sign of the stored amount rather than from its `kind` — so a fifth ledger
   kind needs no rule here, and a refund and a commission take the same
   treatment because they are the same thing arithmetically. */
.credit {
  color: var(--ok);
}

.debit {
  color: var(--danger);
}

/* --------------------------------------------------------------------------
   The customer manage panel
   -------------------------------------------------------------------------- */

/* A full-width row, not a cell in the row above.

   The panel used to be a `<details>` inside the last `<td>` — a column sized
   for a one-word button. Four width-100% forms were then laid out inside it,
   and because a table cell does not clip its overflow, they painted over the
   columns beside them instead of fitting. Nothing here is positioned: the panel
   is simply given the width of the table, which is the one thing that cell
   could never have. */

.manage-row > td {
  padding: 0;
  border-bottom: 1px solid var(--border);
}

/* A data row highlights under the pointer because it is a row of data. This one
   is a control, and highlighting it makes the two read as a single row. */
.manage-row:hover td {
  background: transparent;
}

.manage-row summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  font-family: var(--mono);
  font-size: var(--text-xs);
  color: var(--text-dim);
  cursor: pointer;
  list-style: none;
  white-space: normal;
}

.manage-row summary::-webkit-details-marker {
  display: none;
}

/* The chevron, as a glyph rather than a background image: a background would
   need a URL, and `style-src 'self'` rules out doing it inline. */
.manage-row summary::before {
  content: "\25B8"; /* ▸ */
  color: var(--text-faint);
  transition: transform 120ms ease;
}

.manage-row details[open] > summary::before {
  transform: rotate(90deg);
}

.manage-row details[open] > summary {
  color: var(--text);
}

.manage-panel {
  max-width: 720px;
  padding: var(--space-1) var(--space-5) var(--space-5);
  white-space: normal;
}

/* The four forms are one panel of stacked operations, so the separation between
   them has to be visible; they are not four paragraphs. */
.manage-panel form + form {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

/* The number and its "No limit" tick are one control, so the input's usual
   trailing margin would push them apart. */
.manage-panel .field-row input[type="number"] {
  margin-bottom: var(--space-2);
}

/* --------------------------------------------------------------------------
   Badges
   -------------------------------------------------------------------------- */

.badge {
  display: inline-block;
  padding: 3px var(--space-2);
  border-radius: var(--radius-pill);
  font-family: var(--mono);
  font-size: var(--text-2xs);
  font-weight: 600;
  background: var(--surface-2);
  color: var(--text-dim);
  border: 1px solid var(--border);
  white-space: nowrap;
}

.badge-ok {
  background: var(--ok-soft);
  color: var(--ok);
  border-color: transparent;
}

.badge-warn {
  background: var(--warn-soft);
  color: var(--warn);
  border-color: transparent;
}

.badge-danger {
  background: var(--danger-soft);
  color: var(--danger);
  border-color: transparent;
}

/* Reached from app/web/views.py, which maps alert level "info" to this class.
   It is not written literally in any template — do not delete it as dead CSS. */
.badge-accent {
  background: var(--accent-soft);
  color: var(--accent);
  border-color: transparent;
}

/* --------------------------------------------------------------------------
   Switcher — a row of links that changes what the page below shows
   -------------------------------------------------------------------------- */

/* `.card-actions` is a margin wrapper, not a layout: it exists so a card's
   bottom padding does not collapse against a trailing button. Two rows of
   buttons need a flex row, and inline-block anchors separated by whitespace
   gave a gap that varied with the indentation of the template. */
.switcher {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
}

/* --------------------------------------------------------------------------
   Sparkline

   A run of cells, drawn at six levels rather than at a computed height, and the
   constraint is not aesthetic. `style-src 'self'` in the CSP
   (app/web/templating.py) forbids an inline `style=` attribute, so no element
   on this site can carry a width or height worked out in Python. Six levels is
   what can be expressed, and six steps is enough to read a shape at a glance,
   which is all the bar is for — the exact count is in the cell's title.

   The level is selected by `data-level` rather than by a second class name.
   That is deliberate and it is worth not "simplifying" back: the class checker
   in tests/test_web_assets.py skips any `class="..."` value containing a brace,
   so `class="cell lvl{{ cell.level }}"` would have hidden the level rules from
   it entirely and left six rules that look dead. See the template's comment.

   The empty cell is a visible outline rather than nothing, because the whole
   point of filling the gaps in `_traffic_histogram` is that a quiet hour must
   be distinguishable from an hour the chart skipped. A cell that renders as
   blank does not achieve that.
   -------------------------------------------------------------------------- */

.spark {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  /* Taller than it was. At 46px the bar was shorter than the label above it and
     read as a divider with texture rather than as a chart; the shape of an hour
     is the one thing this element exists to show. */
  height: 56px;
  overflow-x: auto;
  padding-bottom: 2px;
}

.cell {
  flex: 1 0 3px;
  min-width: 3px;
  height: 100%;
  border-radius: var(--radius-xs);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--border);
}

/* The sparkline ramp, now read from the token block rather than written out
   here — the six levels are values of the design, not of this component. See
   `--level-1` for what the ramp is doing and why level 1 is so quiet. */
.cell[data-level="1"] { background: var(--level-1); }
.cell[data-level="2"] { background: var(--level-2); }
.cell[data-level="3"] { background: var(--level-3); }
.cell[data-level="4"] { background: var(--level-4); }
.cell[data-level="5"] { background: var(--level-5); }

/* --------------------------------------------------------------------------
   Forms
   -------------------------------------------------------------------------- */

label {
  display: block;
  font-size: var(--text-sm);
  font-weight: 600;
  margin-bottom: var(--space-1);
  color: var(--text-dim);
}

/* `--text-base` on the fields, and that is not a size decision. iOS Safari
   zooms the whole page in when a focused input's text is under 16px and does
   not zoom back out, so at 0.92rem every customer logging in on an iPhone got
   a viewport stuck at the zoomed scale for the rest of the session. */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
select,
textarea {
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text);
  font-family: inherit;
  font-size: var(--text-base);
  margin-bottom: var(--space-4);
}

input::placeholder,
textarea::placeholder {
  color: var(--text-faint);
}

input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.field-note {
  font-size: var(--text-xs);
  color: var(--text-faint);
  margin: calc(var(--space-3) * -1) 0 var(--space-4);
}

.field-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 0 var(--space-4);
}

.btn {
  display: inline-block;
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-bright);
  background: var(--surface-2);
  color: var(--text);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 550;
  cursor: pointer;
  line-height: 1.3;
  transition: background 0.16s ease, border-color 0.16s ease,
    box-shadow 0.16s ease, transform 0.16s ease;
}

.btn:hover {
  background: var(--surface-3);
  border-color: var(--accent-line);
  text-decoration: none;
}

.btn-primary {
  background: var(--accent-fill);
  border-color: var(--accent-fill);
  color: var(--accent-ink);
  font-weight: 650;
}

.btn-primary:hover {
  background: var(--accent-fill-hover);
  border-color: var(--accent-fill-hover);
  box-shadow: var(--glow);
}

.btn-danger {
  color: var(--danger);
  border-color: var(--danger);
  background: transparent;
}

.btn-danger:hover {
  background: var(--danger-soft);
  border-color: var(--danger);
}

.btn-sm {
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-xs);
}

.btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  box-shadow: none;
  transform: none;
}

.form-actions {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
}

.inline-form {
  display: inline;
}

/* The rotate-key form sits inside a table cell, where the global input rules
   above are wrong twice over: `width: 100%` would let one field set the width
   of the whole column, and the 16px bottom margin would push the row's buttons
   out of line with the ones beside them.

   Scoped to this class rather than changed globally, because every other form
   on the site wants the global behaviour. */
.key-rotate {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-top: 8px;
}

.key-rotate input[type="password"] {
  width: 160px;
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-xs);
  margin-bottom: 0;
}

/* --------------------------------------------------------------------------
   Notices
   -------------------------------------------------------------------------- */

/* NO `display` VALUE HERE, and that is load-bearing rather than an omission.

   The traffic poller hides the dropped-events warning by setting
   `notice.hidden = true`, and the user-agent rule for `[hidden]` is
   `display: none` at the weakest possible specificity. Any `display` declared
   on this class outranks it, so the notice would stay visible with `hidden`
   set — a warning about dropped events that can never be dismissed, on the page
   whose whole job is to be trusted about numbers. The explicit `[hidden]` rule
   below is the other half: it states the intent where a reader will look. */
.notice {
  border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
  font-size: var(--text-sm);
  border: 1px solid transparent;
}

.notice[hidden] {
  display: none;
}

.notice-ok {
  background: var(--ok-soft);
  color: var(--ok);
  border-color: var(--ok);
}

.notice-warn {
  background: var(--warn-soft);
  color: var(--warn);
  border-color: var(--warn);
}

.notice-danger {
  background: var(--danger-soft);
  color: var(--danger);
  border-color: var(--danger);
}

.notice-info {
  background: var(--accent-soft);
  color: var(--accent);
  border-color: var(--accent);
}

/* --------------------------------------------------------------------------
   Code blocks
   -------------------------------------------------------------------------- */

pre {
  background: var(--code-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-4) var(--space-5);
  overflow-x: auto;
  margin: 0 0 var(--space-4);
  line-height: 1.65;
  color: var(--code-fg);
}

pre code {
  font-size: var(--text-xs);
}

p code,
td code,
li code {
  background: var(--surface-2);
  border: 1px solid var(--border);
  padding: 1px var(--space-1);
  border-radius: var(--radius-xs);
}

/* --------------------------------------------------------------------------
   The code card

   The framed sample on the marketing pages: a chrome bar carrying the file's
   language, then the snippet. It is a presentation device for code a reader is
   reading, NOT a copy target — the dashboard's snippets stay plain <pre>
   elements with a copy button, because a chrome bar on something you are about
   to copy is chrome in the way.

   The highlighting classes below are authored into the template by hand. There
   is no highlighter, no build step and no CDN, and a third-party one could not
   be loaded anyway under `script-src 'self'`.
   -------------------------------------------------------------------------- */

/* One of the two surfaces that keeps the ambient shadow. It sits above the page
   in the hero, it appears once, and the halo is what makes it read as lifted
   off the grid texture behind it. */
.code-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
  /* Narrower than the page and centred. At full width the sample's short lines
     leave two thirds of the card empty and the chrome bar reads as a banner
     rather than as a window. */
  max-width: 840px;
  margin: 0 auto var(--space-5);
}

.code-card-head {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: var(--space-3) var(--space-4);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.code-card-dot {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  flex: none;
}

/* Three colours, on purpose and drawn from the token block like everything
   else — but from the one group in it that a re-theme should leave alone. They
   are not a palette, they are the traffic lights of a window chrome, and a
   themed one would be nonsense. */
.code-card-dot-1 {
  background: var(--dot-1);
}

.code-card-dot-2 {
  background: var(--dot-2);
}

.code-card-dot-3 {
  background: var(--dot-3);
}

.code-card-title {
  font-family: var(--mono);
  font-size: var(--text-2xs);
  color: var(--text-faint);
  margin-left: var(--space-1);
}

.code-card-head .badge {
  margin-left: auto;
}

/* The card supplies the frame, so the <pre> inside drops its own. */
.code-card pre {
  border: none;
  border-radius: 0;
  background: transparent;
  margin: 0;
  padding: var(--space-5);
}

/* Token colours. Each borrows a colour that already had to exist — the accent,
   and the semantic ok/warn pair — so a sample cannot introduce a hue that
   appears nowhere else on the site. */
.code-k {
  color: var(--accent);
}

.code-s {
  color: var(--ok);
}

.code-a {
  color: var(--warn);
}

.code-c {
  color: var(--text-faint);
}

.secret-reveal {
  background: var(--warn-soft);
  border: 1px solid var(--warn);
  border-radius: var(--radius-sm);
  padding: 14px 16px;
  margin-bottom: 18px;
}

.secret-reveal pre {
  background: var(--surface);
  margin-bottom: 10px;
}

/* --------------------------------------------------------------------------
   Auth pages
   -------------------------------------------------------------------------- */

.auth-shell {
  max-width: 400px;
  margin: 8vh auto 0;
  padding: 0 var(--space-6);
}

.auth-shell .card {
  padding: var(--space-7);
}

.auth-shell h1 {
  font-size: var(--text-xl);
  margin-bottom: var(--space-1);
}

.auth-sub {
  color: var(--text-dim);
  font-size: var(--text-md);
  margin-bottom: var(--space-6);
}

/* --------------------------------------------------------------------------
   Dashboard and admin shells
   -------------------------------------------------------------------------- */

.shell {
  display: grid;
  grid-template-columns: 208px 1fr;
  gap: var(--space-7);
  align-items: start;
}

.side {
  position: sticky;
  top: var(--space-6);
}

.side-nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.side-nav a {
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--text-dim);
  font-size: var(--text-sm);
  border: 1px solid transparent;
}

.side-nav a:hover {
  background: var(--surface-2);
  color: var(--text);
  text-decoration: none;
}

.side-nav a.active {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent);
  font-weight: 600;
}

.page-head {
  margin-bottom: var(--space-6);
}

.page-head h1 {
  margin-bottom: var(--space-1);
}

.page-head p {
  color: var(--text-dim);
  margin: 0;
  font-size: var(--text-md);
}

/* --------------------------------------------------------------------------
   Marketing pages — landing, models, pricing, contact

   `.eyebrow` is the monospace label above a heading: the "tech" half of the
   direction, and a cheap way for a section to say what it is before the
   heading is read.
   -------------------------------------------------------------------------- */

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 5px var(--space-4) 5px var(--space-3);
  border-radius: var(--radius-pill);
  border: 1px solid var(--accent-line);
  background: var(--accent-soft);
  font-family: var(--mono);
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label-wide);
  color: var(--accent);
  margin: 0 0 var(--space-5);
}

/* The status dot inside the pill. Painted by CSS rather than authored as a
   <span>, so a template cannot forget it and no page can end up with a
   half-drawn badge. The pulse is stopped by the reduced-motion block at the
   bottom of this file along with every other animation. */
.eyebrow::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  flex: none;
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
  animation: pulse 2.4s ease-in-out infinite;
}

@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }

  50% {
    opacity: 0.35;
  }
}

.hero {
  position: relative;
  /* `isolation` makes the hero its own stacking context, so the two pseudos
     below cannot escape into the page and the z-indexes on them and on the
     content mean what they say. */
  isolation: isolate;
  overflow: hidden;
  /* A viewport-relative top pad, so the headline sits at a consistent height in
     the window on a laptop and on a large display rather than at a fixed
     distance from the top of a page whose header is a fixed 68px everywhere. */
  padding: clamp(88px, 12vh, 160px) 0 80px;
  text-align: center;
}

/* The grid texture behind the hero. Painted as a background rather than an
   image: no request, no asset to fingerprint, and it scales to any viewport.
   The mask fades it out well before the section edge, so it reads as depth
   rather than as a table with visible borders. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to right, var(--grid-line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px);
  /* 40px rather than 46px. The finer grid reads as texture at the viewing
     distance a hero is actually looked at from; at 46px the cells were large
     enough to be counted, which makes it a table rather than a surface. */
  background-size: 40px 40px;
  -webkit-mask-image: radial-gradient(circle at 50% 0%, var(--mask-solid) 0%, transparent 74%);
  mask-image: radial-gradient(circle at 50% 0%, var(--mask-solid) 0%, transparent 74%);
}

/* The bloom behind the headline. A radial gradient rather than a solid block
   with `filter: blur()`: the gradient already fades smoothly, so the blur
   would cost a large offscreen layer to produce an identical result.

   Two stops rather than one: a terracotta core that gives way to amber before
   it fades out, so the light reads as the brand's two colours meeting rather
   than as a single orange lamp. The amber stop is well inside the falloff — put
   it at the rim and it is invisible, because by then the alpha is nearly zero. */
.hero::after {
  content: "";
  position: absolute;
  top: -200px;
  left: 50%;
  z-index: 0;
  width: min(900px, 135%);
  height: 560px;
  transform: translateX(-50%);
  pointer-events: none;
  background: radial-gradient(
    ellipse at center,
    var(--bloom-core),
    var(--bloom-mid) 44%,
    transparent 70%
  );
}

.hero > * {
  position: relative;
  z-index: 1;
}

/* The secondary pages use the same hero with less of it. A page whose job is
   reference material should reach its content sooner than the front page,
   which has to make an argument first. The headline step that goes with this
   is below, with the rest of the hero modifiers. */
.hero-compact {
  padding: 72px 0 44px;
  /* Left, not centred. The five inner pages used to centre the eyebrow, the
     headline, the paragraph and the buttons, and then every section beneath
     them started at the left edge of `.wrap` — two axes on one page, and the
     hero read as a separate document stapled to the front of it.

     It was also the specific failure this file already argues against a few
     hundred lines down, for the split hero: a centred paragraph four or five
     lines long is a ragged block with two soft edges, and the eye has nothing
     to track down. That argument does not depend on the column being half
     width. A 640px centred paragraph is exactly as ragged as a 300px one.

     The paragraph keeps a measure cap rather than filling the wrap: at 1280px
     a lede set to the full width is a 150-character line, which is unreadable
     for the opposite reason. */
  text-align: left;
}

.hero h1 {
  font-size: var(--text-display);
  /* Tighter tracking than any other heading, because this is the largest type
     on the site and the gaps between letters grow with the glyphs — `--tracking-
     display` is the step that exists for exactly this. */
  letter-spacing: var(--tracking-display);
  /* Under 1, so a two-line headline sets as one block rather than two lines
     with air between them. It is the difference between a headline and two
     sentences. */
  line-height: 0.98;
  margin-bottom: var(--space-5);
  font-weight: 700;
}

/* The payoff half of a headline, filled with the brand gradient.

   Guarded by @supports because the technique works by making the text
   transparent — on anything that does not understand background-clip the
   words would disappear rather than merely lose their colour. The fallback is
   the plain accent, which is legible and on-brand on its own. */
.grad-text {
  color: var(--accent);
}

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .grad-text {
    /* Both stops are the BRIGHT pair, so the whole run clears AA on the dark
       ground — measured against `--bg`, the first stop is 7.94:1 and the
       second is 10.28:1, so every point along the ramp is comfortably past
       4.5:1 rather than only the two ends being. A gradient across a headline
       is still text, and half a headline that fails contrast is a headline
       that fails contrast.

       The two DEEP tokens, `--accent-fill` and `--accent-2-fill`, are 6.37:1
       and 5.39:1 — they would also pass here, and they are deliberately not
       used: they are the pair that goes *under* something, as in `.brand-mark`,
       where the glyph on top is what has to be legible against them. */
    background-image: linear-gradient(
      100deg,
      var(--accent) 0%,
      var(--accent-2) 100%
    );
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}

.hero .lede {
  font-size: clamp(1.05rem, 0.95rem + 0.5vw, 1.25rem);
  color: var(--text-dim);
  max-width: 640px;
  margin: 0 auto var(--space-7);
  line-height: 1.6;
}

.hero-actions {
  display: flex;
  gap: var(--space-3);
  justify-content: center;
  flex-wrap: wrap;
}

/* The hero's buttons are larger than the ones in the header or in a card.
   Scoped here rather than exposed as a `.btn-lg` class, so no template has to
   remember to ask for the size its context implies. */
.hero-actions .btn {
  padding: 15px var(--space-6);
  font-size: var(--text-base);
  border-radius: var(--radius-md);
}

/* A row of monospace facts under the hero buttons — models served, protocol,
   billing model. Reads as a spec line, which is what the audience wants, and
   the rule above it is what turns three loose phrases into a claim.

   The list reset is load-bearing rather than tidiness: a <ul> carries a 40px
   `padding-inline-start` from the user agent, and this one is centred, so
   without it the whole row sat 20px right of the headline it belongs to. */
.hero-facts {
  display: flex;
  gap: var(--space-3) var(--space-7);
  justify-content: center;
  flex-wrap: wrap;
  max-width: 760px;
  margin: var(--space-9) auto 0;
  padding: var(--space-7) 0 0;
  border-top: 1px solid var(--border);
  list-style: none;
  font-family: var(--mono);
  font-size: var(--text-2xs);
  color: var(--text-dim);
}

/* The accent marker on each fact. This selector read `.hero-facts span::before`
   for as long as the row has existed, and the row has never contained a
   `<span>` — the facts are bare `<li>`s. So the marker never rendered and the
   row was three loose phrases under a rule. The comment above says the rule is
   what makes them a claim; the marker is what makes each one a separate claim. */
.hero-facts li::before {
  content: "▸ ";
  color: var(--accent);
}

/* --------------------------------------------------------------------------
   Hero modifiers — deliberately BELOW the plain `.hero` rules above.

   `.hero-compact h1`, `.hero-split .lede` and `.hero h1` all have the same
   specificity: one class, one element. The cascade therefore settles it on
   source order, and when `.hero-compact h1` sat above `.hero h1` it lost every
   time — the four inner pages rendered their headlines at the full 3.9rem hero
   size and the smaller step was dead code that looked like it worked. If you
   add a hero modifier, add it here.
   -------------------------------------------------------------------------- */

/* The secondary pages use the same hero with less of it. A page whose job is
   reference material should reach its content sooner than the front page,
   which has to make an argument first. */
.hero-compact h1 {
  font-size: var(--text-3xl);
}

/* These two sit here, and not beside `.hero-compact` above, for the reason the
   block header gives: both are two-class selectors reading the same properties
   as `.hero .lede` and `.hero-actions`, so source order is the whole of the
   cascade between them. */
.hero-compact .lede {
  margin: 0 0 var(--space-7);
  max-width: 62ch;
}

.hero-compact .hero-actions {
  justify-content: flex-start;
}

/* The front page's two-column hero: the argument on the left, the code sample
   on the right, both inside `.wrap` so the columns line up with the nav above
   them and the sections below.

   A modifier rather than a change to `.hero`, because the four inner pages use
   the plain centred hero and their copy is a headline over a paragraph — there
   is no second column for them to balance, and a centred single column is the
   right shape for reference material.

   Left-aligned inside the split, though: a centred paragraph in a half-width
   column is a ragged block with two soft edges, and the eye has nothing to
   track down. The grid gives it an edge. */
.hero-split .wrap {
  display: grid;
  /* The copy gets the larger share. It carried the smaller one before, which is
     backwards: the left column holds a headline, a paragraph and a spec row
     that all have to fit on one line each, and the right column holds a single
     card whose width is set by its own content. */
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
  gap: clamp(var(--space-7), 4vw, var(--space-10));
  align-items: center;
  text-align: left;
}

/* The left column. A wrapper rather than letting the six hero children be grid
   items in their own right — they would each take a cell and the hero would be
   a ladder. `min-width: 0` is what lets the column shrink below the intrinsic
   width of its content: without it a long unbreakable token in the headline
   widens the track and squeezes the code card, which then scrolls. */
.hero-copy {
  min-width: 0;
}

.hero-split .lede {
  margin: 0 0 var(--space-7);
  max-width: 46ch;
}

.hero-split .hero-actions {
  justify-content: flex-start;
}

/* The rule above the facts is what turns three loose phrases into a claim, so
   it stays — but it spans the column rather than a centred 760px block.

   This row is the one place in the hero where the fit is arithmetic rather
   than taste, because all three facts have to share one line under a rule. Get
   it wrong and the third drops to a line of its own, where the rule above it
   reads as a divider with something orphaned beneath it.

   The sum, at the breakpoint below which this hero stacks (1200px), which is
   the narrowest the row ever has to fit:

       .wrap    = 1200 - 2 x 32px of gutter            = 1136px
       gap      = 4vw, capped                           =   48px
       columns  = 1136 - 48                             = 1088px
       left     = 55% of that (1.1fr of 2fr)            =  598px

   and the row itself, at `--text-2xs` with JetBrains Mono advancing 0.6em
   (7.8px a character over 69 characters of text and marker, plus 2 x 8px of
   column gap):

       7.8 x 69 + 16                                    =  554px

   44px of slack, about 7%. The marker glyph is not in JetBrains Mono and falls
   through to the system mono stack, so its advance is not knowable from this
   file — which is why the figure was confirmed by MEASURING the rendered row in
   a browser at 1200px rather than trusted from the arithmetic alone. The row
   does not wrap there.

   If you add a fourth fact or lengthen one, redo that sum and MEASURE it. The
   symptom of skipping that is not an overflow — `flex-wrap` hides it as a
   tidy-looking second line that quietly breaks the design. */
.hero-split .hero-facts {
  justify-content: flex-start;
  max-width: none;
  margin: var(--space-8) 0 0;
  gap: var(--space-3) var(--space-2);
  font-size: var(--text-2xs);
}

/* The card fills its column. Its 840px cap exists for the stacked layout,
   where a full-width card left the sample's short lines floating in two thirds
   of empty chrome. */
.hero-split .code-card {
  max-width: none;
  margin: 0;
}

/* Two steps down from the `--text-xs` a full-width block gets. The longest line
   in the sample is a `base_url=` with a trailing comment, and at half the page
   width the default size pushed it into the `overflow-x: auto` on `pre` — a
   horizontal scrollbar inside the hero, on the one element a visitor is meant
   to read.

   This size, not the facts row, is what sets the 1200px breakpoint below: it is
   the tighter of the two fits, and it is the one that cannot be fixed by
   reflowing because a code line cannot wrap. */
.hero-split .code-card pre code {
  font-size: var(--text-2xs);
}

/* Three features as an editorial band rather than three cards.

   The defect was never the column count, it was that there were three
   identical boxes: same surface, same border, same radius, same shadow, the
   same object three times. So the boxes go and the columns stay — each feature
   is now a claim under a rule, which is the language `.steps` two sections
   below already speaks, so the two read as one system instead of as two
   unrelated layouts that happen to share a page.

   There is also no lift on hover any more, and that is a touch decision rather
   than a style one: a transform on `:hover` latches on a touch screen, so a tap
   left a card stuck in the air until something else was tapped. The rule above
   each feature takes the accent instead, which cannot latch into anything
   broken. */
/* No bottom padding. The band is always the last thing in its section on all
   three pages that use it, so its own 64px was stacking on top of the next
   section's padding and opening a gap between two sections that was neither
   one's doing. The section's padding is the gap. */
.features {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(268px, 1fr));
  gap: var(--space-6) var(--space-8);
  padding: 0;
}

.feature {
  border-top: 1px solid var(--border);
  padding-top: var(--space-5);
  transition: border-color 0.18s ease;
}

.feature:hover {
  border-color: var(--accent-line);
}

/* The tile above a feature heading. One accent for every card rather than a
   different tint per card: three colours of icon turns a spec list into a
   brochure, and the colour is then carrying no meaning at all — which is the
   same reason there is only one accent in the token layer.

   The glyph inside is inline SVG written into the template. There is no icon
   library, no build step and no CDN, and `script-src 'self'` rules one out. */
.feature-icon {
  width: 40px;
  height: 40px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-soft);
  color: var(--accent);
  margin-bottom: var(--space-4);
}

.feature-icon svg {
  width: 20px;
  height: 20px;
  display: block;
}

/* BOTH levels, because the landing page's three features are `h2` — they sit
   directly under the hero with no `h2` above them, so as `h3` the document
   outline read h1, h3, h3, h3, h2, h2, h3, skipping the `h2` level entirely.
   The contact page's features sit under a real `h2` and stay `h3`. One rule
   covers both; the level is the template's business, the size is this file's. */
.feature h2,
.feature h3 {
  margin-bottom: var(--space-2);
  font-size: var(--text-lg);
}

.feature p {
  color: var(--text-dim);
  font-size: var(--text-md);
  line-height: 1.6;
  margin: 0;
}

/* No `border-top` — see `.site-header`. Sections are separated by their own
   `--section-pad` of padding, which at the top of the range is 96px a side and
   therefore a 192px seam between one section's content and the next's.

   One consequence worth knowing before you re-add a rule: on /models the
   sections sit INSIDE `.wrap` while on the landing page they sit outside it,
   so the same declaration drew two different lines — an inset one and a
   full-bleed one — on two pages of the same site. If a divider is ever wanted
   back, decide that nesting first. */
.section {
  padding: var(--section-pad) 0;
}

/* The first section under a hero does not stack its own top padding onto the
   hero's.

   On /models, /pricing and /contact the hero is `.hero-compact` — a masthead
   three lines tall — and a full `--section-pad` above the first section made
   the gap below it roughly twice the height of the thing above it. A short
   masthead followed by a chapter break does not read as generous, it reads as
   a page that failed to load the rest of itself. The hero already ends with a
   deliberate bottom padding; that IS the seam.

   Matches only the `.hero + .wrap > .section:first-child` shape, which is the
   three `hero-compact` pages. It deliberately does NOT match the landing page:
   there the `.wrap` under the hero opens with `.features`, and `#models` is a
   later sibling that follows the features band rather than the hero — so its
   top padding is a real section seam and stays. */
.hero + .wrap > .section:first-child {
  padding-top: 0;
}

/* The two-column section head.

   The single most repeated shape on the site was `h2` then lede then content,
   stacked, at the same width, with the same padding above it — on every section
   of every marketing page. That is the real reason the pages read as one
   undifferentiated scroll rather than as a sequence of arguments: nothing
   varied, so nothing was emphasised.

   The heading and its lede now share a row, the heading at the display step on
   the left and the explanation at a readable measure on the right, with the
   content beginning beneath both. It is a WRAPPER — not one word inside it
   changed, and the eight sections that have both a heading and a lede are the
   only places it appears. Sections with only a heading keep the plain rule
   below. */
.section-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-3) var(--space-9);
  /* Baseline rather than top: a 40px heading and a 17px paragraph aligned at
     their first baselines is the editorial convention, and it is what stops the
     heading looking like it is floating above the text it belongs to. */
  align-items: baseline;
  margin-bottom: var(--space-8);
}

.section-head h2 {
  font-size: var(--text-2xl);
  letter-spacing: var(--tracking-tight);
  margin: 0;
  max-width: 18ch;
}

.section-head .lede {
  color: var(--text-dim);
  font-size: var(--text-md);
  line-height: 1.6;
  margin: 0;
  max-width: 54ch;
}

/* The heading of a section that has no lede to sit beside it. 24px below
   rather than the 12px this carried: at 40px a heading needs more than a
   line-height of air before whatever it introduces, and with 12px the card on
   /contact began close enough to the heading to read as its caption. The
   sections that DO have a lede get their gap from `.section-head`, which is
   40px — the two are not meant to match, because a lede occupies the space
   between them, but they should not be 28px apart either. */
.section > h2 {
  font-size: var(--text-2xl);
  letter-spacing: var(--tracking-tight);
  margin-bottom: var(--space-6);
}

/* There was a `.section > .lede` rule here, carrying the measure and the gap
   for a lede sitting directly under a section heading. It is gone because
   nothing matches it any more: every section on every page that has both a
   heading and a lede now puts them in a `.section-head`, and the eight
   sections that have only a heading have no lede for this to style. It was
   left in place for one revision after the wrapper landed, which is precisely
   the dead-selector shape this file refuses elsewhere — a rule that reads as a
   description of markup that does not exist. */

/* Numbered "how it works" steps. The counter is CSS, so the markup stays an
   ordered list and the numbers are never content. */
.steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--space-6) var(--space-8);
  counter-reset: step;
  list-style: none;
  margin: 0;
  padding: 0;
}

.step {
  counter-increment: step;
  padding-top: var(--space-4);
  border-top: 2px solid var(--border);
}

.step::before {
  content: "0" counter(step);
  font-family: var(--mono);
  font-size: var(--text-2xs);
  color: var(--accent);
  display: block;
  margin-bottom: var(--space-1);
}

.step h3 {
  font-size: var(--text-lg);
  margin-bottom: var(--space-1);
}

.step p {
  color: var(--text-dim);
  font-size: var(--text-sm);
  margin: 0;
}

/* The closing call to action. The accent border and the gradient ground are
   what separate it from an ordinary card: it is the only panel on the site
   asking for something, and it should not read as one more content block. */
/* The second of the two surfaces that keeps the ambient glow. This band appears
   four times across the site with byte-identical markup, and that repetition is
   correct — a closing ask should look the same everywhere it appears. What was
   wrong was that it was one more `.card`, so the ask looked like the content
   above it. The accent edge, the gradient ground and the glow are what make it
   the only panel on the site that is asking for something. */
.cta-band {
  background: linear-gradient(180deg, var(--surface-2), var(--surface));
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-lg);
  padding: var(--space-10) var(--space-7);
  text-align: center;
  box-shadow: var(--glow-soft);
}

/* `h2, h3` rather than `h2`.

   The four `.cta-band` blocks in the templates all head with an `<h3>` — none
   of them is the second heading of its page, and a band that repeats at the
   foot of four pages should not be competing with the section heading above
   it. The rule targeted `h2` alone, so the retune never applied to a single
   band that actually exists: every closing CTA on the site rendered its
   headline at the browser's default h3 size, visibly smaller than the body
   copy beneath it. A selector that matches nothing is not a style, it is a
   comment. */
.cta-band h2,
.cta-band h3 {
  font-size: var(--text-xl);
  margin-bottom: var(--space-2);
}

.cta-band p {
  color: var(--text-dim);
  font-size: var(--text-md);
  max-width: 540px;
  margin: 0 auto var(--space-6);
}

/* A definition-style row used on the pricing page for "what you are charged
   for" — label on the left, explanation on the right.

   The label is a `<strong>` inside the `<li>`, and the explanation is the text
   after it. That is worth stating because this block previously styled
   `.spec-list dt` and `.spec-list .spec-key` — a definition list that no
   template has ever contained — so the fixed label column and the dimmed
   explanation silently never applied, and the rows rendered as a bold phrase
   followed by full-size body text. Matching the markup that exists rather than
   the markup the rule was written for.

   The explanation's colour and size live on the `li` rather than on a wrapper
   element, so no future row can lose them by omitting a `<span>`. */
.spec-list {
  list-style: none;
  margin: 0 0 20px;
  padding: 0;
}

.spec-list li {
  display: flex;
  gap: var(--space-4);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--border);
  color: var(--text-dim);
  font-size: var(--text-md);
}

.spec-list li:last-child {
  border-bottom: none;
}

.spec-list li > strong {
  font-family: var(--mono);
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--accent);
  flex: 0 0 168px;
}

/* The questions block on the landing page.

   A stack of disclosure rows rather than a grid of cards. Six answers as cards
   is a wall, and a reader who wants one of them has to find it among five they
   do not; closed, the block reads as a list of the questions themselves, which
   is what someone deciding whether to read on actually wants.

   `<details>`/`<summary>` because the site is CSP `script-src 'self'` and a
   disclosure is not worth a script. This mirrors `.nav-disclosure`, which is
   the same control in the header — the marker is drawn rather than left to the
   engine, so the affordance is identical in every browser.

   Answers are capped at a reading measure and left-aligned inside the full
   width of the row: a question is a heading and can run wide, but a paragraph
   set to the page width is unreadable at desktop sizes. */
.faq {
  border-top: 1px solid var(--border);
}

.faq-item {
  border-bottom: 1px solid var(--border);
}

.faq-item > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) 0;
  cursor: pointer;
  font-size: var(--text-lg);
  color: var(--text);
  list-style: none;
}

.faq-item > summary::-webkit-details-marker {
  display: none;
}

.faq-item > summary::after {
  content: "";
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  margin-right: 3px;
  border-right: 2px solid var(--text-dim);
  border-bottom: 2px solid var(--text-dim);
  transform: rotate(45deg);
  transition: transform 160ms ease, border-color 160ms ease;
}

.faq-item[open] > summary::after {
  transform: rotate(-135deg);
}

.faq-item > summary:hover {
  color: var(--accent);
}

.faq-item > summary:hover::after {
  border-color: var(--accent);
}

.faq-item p {
  color: var(--text-dim);
  font-size: var(--text-md);
  max-width: 68ch;
  margin: 0 0 var(--space-4);
}

/* --------------------------------------------------------------------------
   Docs
   -------------------------------------------------------------------------- */

.docs {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: var(--space-8);
  align-items: start;
}

.docs-body h2 {
  margin-top: var(--space-8);
  padding-top: var(--space-1);
}

.docs-body h2:first-child {
  margin-top: 0;
}

.docs-body h3 {
  margin-top: var(--space-6);
}

/* Prose takes the measure; the tables and the code samples in the same column
   keep the full width, because they need it and because a docs page where the
   prose is visibly narrower than the table beneath it reads as a decision
   rather than as an inconsistency. */
.docs-body p {
  max-width: var(--measure);
}

.endpoint {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
}

.method {
  font-family: var(--mono);
  font-size: var(--text-2xs);
  font-weight: 700;
  padding: 2px var(--space-2);
  border-radius: var(--radius-xs);
  background: var(--ok-soft);
  color: var(--ok);
}

.method-post {
  background: var(--accent-soft);
  color: var(--accent);
}

/* --------------------------------------------------------------------------
   Empty states
   -------------------------------------------------------------------------- */

.empty {
  text-align: center;
  padding: var(--space-8) var(--space-5);
  color: var(--text-faint);
}

.empty p {
  margin: 0;
  font-size: var(--text-md);
}

/* --------------------------------------------------------------------------
   Utilities
   -------------------------------------------------------------------------- */

/* Offscreen until focused. `app.js` reuses this class for its clipboard
   fallback <textarea>, which is why the offscreen position is a rule of its own
   here and not a style attribute — a CSP with `style-src 'self'` could not
   write one anyway. */
.skip-link {
  position: absolute;
  left: -9999px;
}

.skip-link:focus {
  left: var(--space-3);
  top: var(--space-3);
  background: var(--surface);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-sm);
  z-index: 30;
}

/* --------------------------------------------------------------------------
   Entrance animation

   The important detail is the direction of the opt-in. `.reveal` elements are
   FULLY VISIBLE as authored; the hidden state only exists once JavaScript has
   added `.js-reveal` to <html> and is therefore in a position to un-hide them.
   Written the other way round — `opacity: 0` by default, JS reveals — a
   failed script, a crawler that does not run one, or a reader with JavaScript
   disabled sees a blank page. On a site whose job is to attract customers
   that would be a self-inflicted outage, and it is why the reveal is a
   progressive enhancement rather than an effect.
   -------------------------------------------------------------------------- */

.js-reveal .reveal {
  opacity: 0;
  transform: translateY(14px);
}

.js-reveal .reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity 0.5s ease, transform 0.5s ease;
}

/* --------------------------------------------------------------------------
   Scroll motion

   One rule about where these live: the script writes a number and the
   stylesheet decides what the number looks like. `app.js` sets `--scrub` on the
   hero — its progress out of the viewport, 0 at rest and 1 once it has been
   scrolled past — and `--progress` on the read line. Neither value knows that a
   bloom, a headline or a code card exists, so how far each layer travels is a
   decision made here, next to a comment, rather than a constant buried in a
   frame loop.
   -------------------------------------------------------------------------- */

/* The read line at the top of the viewport: how much of the page is behind you,
   scaled from the left by `--progress`.

   A transform rather than a `width`, because a width change relayouts the
   document on every frame and a transform does not. That is the whole reason
   the element is fixed and full-width with `transform-origin` at the left,
   rather than a box that grows.

   This is the one piece of motion here that the reduced-motion block below
   deliberately leaves alone. The preference is about vestibular movement, and
   this is a readout of the reader's own scroll position rather than an effect
   that moves on its own — disabling it would remove information, not motion.
   What that block does do is drop its transition to nothing, so a
   reduced-motion reader gets the line pinned exactly to the scroll instead of
   gliding behind it, which is simply the un-animated version of the same
   readout. */
.scroll-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  z-index: 30;
  pointer-events: none;
  transform: scaleX(var(--progress, 0));
  transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--accent-fill), var(--accent-2));
  transition: transform 0.12s linear;
}

/* Hero depth: three layers, three speeds, one number.

   `translate:` rather than `transform:`, and that is load-bearing rather than a
   preference. Every element here already carries a transform from somewhere
   else — the bloom is centred with `translateX(-50%)`, and the copy and the
   card are `.reveal` elements whose entrance transition IS a transform. The two
   properties compose (translate, then rotate, then scale, then transform)
   instead of overwriting one another, so the parallax cannot cancel the
   entrance and the entrance cannot cancel the parallax.

   The fallback in `var(--scrub, 0)` is what makes these safe with no script at
   all, and it is not a formality. An unset custom property inside `calc()` does
   not resolve to zero — it makes the whole declaration invalid at
   computed-value time, and `transform` on the bloom would then fall back to
   `none`, sliding the bloom off-centre by half its width. The fallback is the
   difference between no motion and a visibly broken hero.

   The distances are deliberately small. Separation reads as depth at a few tens
   of pixels over a full screen of scrolling; pushed further it stops being
   depth and becomes the page coming apart. */
/* `will-change` on these three and nowhere else, deliberately. It promotes the
   element to its own compositor layer up front, which is a real win for
   something that is translated every frame and a real cost for something that
   is not — each promoted layer is its own texture in GPU memory, and promoting
   thirty elements to animate three is how a page ends up slower for having been
   optimised. */
.hero::after {
  translate: 0 calc(var(--scrub, 0) * 56px);
  will-change: translate;
}

.hero-copy {
  translate: 0 calc(var(--scrub, 0) * -34px);
  will-change: translate;
}

.hero-split .code-card {
  translate: 0 calc(var(--scrub, 0) * -14px);
  will-change: translate;
}

/* The two-column hero stacks well above the phone breakpoint, and higher up
   than it used to. Two columns of prose at 900px are two columns of three-word
   lines; more decisively, the code sample needs roughly 465px of text width
   before its longest line starts scrolling inside its own card, and the right
   column only reaches that at about 1240px. 1200px is that threshold rounded
   down, so a 1280px laptop still gets the two-column hero. */
@media (max-width: 1200px) {
  .hero-split .wrap {
    grid-template-columns: 1fr;
    gap: var(--space-8);
  }

  .hero-split .lede {
    max-width: 62ch;
  }

  /* The card gets its 840px cap back once it is no longer filling a column —
     that cap exists for exactly this, so the sample's short lines do not float
     in two thirds of empty chrome — and the code returns to the full size,
     because a full-width card has the room the column did not.

     `margin: 0`, not `margin: 0 auto`. Centring it under a copy block that is
     still hard against the left edge puts two axes on one screen: the eyebrow,
     the headline, the paragraph, the buttons and the facts all start at one x,
     and the card starts at another. That is the same fault the compact hero
     was centring its whole contents into, seen from the other side — so the
     card lines up with the column it belongs to instead. */
  .hero-split .code-card {
    max-width: 840px;
    margin: 0;
  }

  .hero-split .code-card pre code {
    font-size: var(--text-xs);
  }
}

/* The section head goes back to one column before the phone breakpoint: two
   columns of a 40px heading and a paragraph stop working while there is still
   plenty of width to stack them in. */
@media (max-width: 900px) {
  .section-head {
    grid-template-columns: 1fr;
    gap: var(--space-3);
    align-items: start;
  }

  .section-head h2 {
    max-width: none;
  }
}

/* The header disclosure.

   Its own query, at 820px rather than the 760px the rest of the small-screen
   rules use, because the number is set by a measurement rather than by taste:
   the seven items in the row — five pages, Sign in, and the CTA — need about
   728px of content plus two gutters, so the row stops fitting at roughly
   775px. Anything below that width and the row does not wrap, it overflows.
   820 is that figure with room for the difference between the vendored Inter
   and whatever the measurement was taken in.

   Everything else in the small-screen blocks stays at 760, where it was. */
@media (max-width: 820px) {
  /* The menu control. `list-style` and the WebKit marker both go because
     `summary` draws a disclosure triangle by default in every engine that
     supports the element, and this is a button, not a list item.
     `min-height: 44px` for the same reason every other mobile control has
     one. */
  .nav-disclosure > summary {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 44px;
    padding: 0 var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-dim);
    font-size: var(--text-sm);
    cursor: pointer;
    list-style: none;
    user-select: none;
  }

  .nav-disclosure > summary::-webkit-details-marker {
    display: none;
  }

  .nav-disclosure > summary svg {
    width: 16px;
    height: 16px;
    transition: transform 0.18s ease;
  }

  .nav-disclosure[open] > summary svg {
    transform: rotate(180deg);
  }

  /* The open menu is a panel hanging off the header rather than a row inside
     it: pushed into the flow it would move the brand and the page below it
     down by its own height every time it opened. Absolute against `.wrap`,
     which is the element that already knows where the gutters are, so
     `-gutter` on both sides is the full viewport width.

     It needs its own ground. The header's 78% background and blur cover the
     68px bar only, and the panel sits outside that box — over hero copy, at
     78% of nothing. */
  .nav-disclosure[open] {
    position: absolute;
    top: 100%;
    left: calc(var(--gutter) * -1);
    right: calc(var(--gutter) * -1);
    padding: var(--space-2) var(--gutter) var(--space-5);
    background: rgba(var(--bg-rgb), 0.98);
    backdrop-filter: blur(10px);
    border-bottom: 1px solid var(--border);
  }

  /* A column of full-width rows. The desktop rule's wrapping and `flex-end`
     were what kept a row of seven items from overflowing; there is no row to
     keep now, and `flex-end` would right-align a stack. */
  .site-nav {
    flex-direction: column;
    align-items: stretch;
    flex-wrap: nowrap;
    gap: 0;
    margin-left: 0;
  }

  .site-nav a {
    display: flex;
    align-items: center;
    min-height: 44px;
    font-size: var(--text-base);
    color: var(--text);
  }

  .site-nav a.btn {
    justify-content: center;
    margin-top: var(--space-3);
  }

  /* `position: relative` is what the open panel is absolute against. The
     header stops being 68px tall and stops laying out as one rigid row,
     because the brand and the menu control are all that is left in it. */
  .site-header .wrap {
    height: auto;
    padding-top: var(--space-2);
    padding-bottom: var(--space-2);
    flex-wrap: nowrap;
    position: relative;
  }
}

@media (max-width: 760px) {
  .shell,
  .docs {
    grid-template-columns: 1fr;
    gap: var(--space-5);
  }

  .side {
    position: static;
  }

  .side-nav {
    flex-direction: row;
    flex-wrap: wrap;
  }

  .hero {
    padding: 58px 0 44px;
  }

  /* No `font-size` on the headline here any more. This block used to pin it at
     2.05rem, which gave a 760px tablet and a 390px phone the same size — the
     `clamp()` floor on `--text-display` does that job now, and does it per
     viewport instead of in one lump. */
  .section {
    padding: var(--space-9) 0;
  }

  /* Stacked and full-width rather than side by side: two centred buttons on a
     narrow screen are two small targets, and the primary one stops being
     obviously the primary one. */
  .hero-actions .btn {
    width: 100%;
  }

  /* Rotated rather than merely stacked: a `border-left` on a full-width row is
     a short line down one edge, which reads as an indent rather than as a
     divider between the rows. */
  .spec-list li {
    flex-direction: column;
    gap: var(--space-1);
  }

  .spec-list li > strong {
    flex: none;
  }
}

/* Touch targets.

   `pointer: coarse` rather than a width breakpoint, because the question is
   whether the thing is being tapped, not how wide the window is — a 1024px
   tablet is coarse and a 400px desktop window is not. 44px is the figure in
   both the Apple and the WCAG guidance, and these are the four controls small
   enough to miss.

   `inline-flex` is what makes a `min-height` mean anything on an anchor, which
   is inline by default and ignores one. */
@media (pointer: coarse) {
  .btn,
  .btn-sm,
  .side-nav a,
  .switcher a {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  .side-nav a,
  .switcher a {
    justify-content: center;
  }
}

/* ---------------------------------------------------------------------------
   The plan cards on /pricing

   The grid is `auto-fit` with a 240px floor rather than a fixed column count,
   because the number of plans is whatever the operator saved — one is as valid
   as six — and a fixed count would leave a row of empty tracks next to a single
   card. `auto-fit` collapses the empty tracks, so one plan fills the row and
   four sit in a line, both without a media query.
   --------------------------------------------------------------------------- */

/* The pricing header is the one compact hero on the site that is centred.
   `.hero-compact` is deliberately left-aligned — its own comment explains why,
   and the reason is that the five inner pages all start their sections at the
   left edge of `.wrap`. This page is the exception that shows the rule was
   about those five: the row of cards below is a centred, symmetric object, so a
   left-aligned header sitting over it reads as a mistake rather than a decision.

   Declared here, after `.hero-compact`, with the same one-class specificity, so
   source order is the whole of the cascade between them. */
.price-hero {
  text-align: center;
}

.price-hero .lede {
  margin-left: auto;
  margin-right: auto;
}

/* Five across on a desktop — four packages and the custom card, comparable in
   one glance, and the whole point of the row is that it is ONE row. `auto-fit`
   gets there without a single media query: at the 1280px wrap the tracks are
   222px and five of them fit, so the row is five equal fifths. Narrow the
   window and it steps down to four, three, two, then one on its own. A plan
   switched off on /admin/plans makes the remaining cards wider rather than
   leaving a hole.

   THE FLOOR IS 216px AND IT WAS MEASURED, NOT PICKED. It is the number that
   decides which window still gets five in a row, because five tracks need
   `5 * floor + 4 * gap` of space and the wrap stops growing at 1280px. At
   230px that sum is 1230 and the wrap only offers 1192 once the gutters are
   taken, so the fifth card wrapped alone onto a second row — an orphan that
   reads as a broken layout, and one that quietly costs the row its equal
   heights, because the grid then has two rows and stretches each to its own
   tallest card. 216px brings the requirement down to 1160 of available width,
   so every window wide enough to give the wrap 1160px shows the five in one
   line. It is not lower than that because the price inside a card has to sit on
   one line: at 216px of track a card has 176px of content, and the widest price
   on the page, `USD` beside `17.5`, measures about 150px.

   WHAT THIS DOES NOT AVOID, and is worth knowing before somebody reports it:
   with five cards, `auto-fit` lands on four tracks across a band of widths
   just under the five-across threshold, and four into five leaves one card
   alone on the second row. That is the cost of the row adapting to how many
   plans are actually enabled — `/admin/plans` can switch one off, and a fixed
   `repeat(5, 1fr)` would then publish a hole where the fifth card used to be.
   A row that never orphans and a row that never lies about how many packages
   exist cannot both be had from one declaration; this keeps the honest one.

   The floor is wrapped in `min()` because it is a *minimum*: below a 216px
   viewport a bare 216px track would be wider than the space it sits in and the
   page would scroll sideways, which no amount of grid arithmetic elsewhere can
   undo. `min(216px, 100%)` also keeps one long unbroken word in a benefit from
   raising the track's own minimum and knocking the row out of alignment. */
.plan-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(13.5rem, 100%), 1fr));
  gap: var(--space-5);
  /* Room for the badge on the emphasised card, which sits on the card's top
     edge and would otherwise be clipped by the section above it. */
  padding-top: var(--space-5);
  margin-bottom: var(--space-5);
}

/* Built from the same tokens as `.card` rather than by adding `.card` to the
   markup: a plan is a card whose contents are a price and a button, and giving
   the class its own rules keeps the pricing page from inheriting every future
   change to the generic card.

   Equal heights are the grid's own stretch plus the `margin-top: auto` below —
   not a `min-height`. A magic height is a number that has to be re-guessed
   whenever a benefit is reworded; this way the row is as tall as its tallest
   card and every card is that tall. */
.plan-card {
  /* The badge is positioned against this. */
  position: relative;
  display: flex;
  flex-direction: column;
  background-color: var(--surface);
  background-image: var(--card-grad);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-6) var(--space-5) var(--space-5);
  box-shadow: var(--shadow-tight);
  transition:
    transform 180ms ease,
    border-color 180ms ease;
}

/* A lift is a `transform`, so it does not disturb the layout: the card moves
   and its neighbours do not, which is the failure to avoid in a row of five.
   The colour change is written for the non-emphasised cards only, because at
   equal specificity this rule would otherwise outrank `.plan-card-featured`
   below and dim the one card that is supposed to be lit. */
.plan-card:hover {
  transform: translateY(-4px);
}

.plan-card:not(.plan-card-featured):hover {
  border-color: var(--border-bright);
}

.plan-name {
  margin: 0 0 var(--space-1);
  font-size: var(--text-lg);
  font-weight: 700;
  letter-spacing: var(--tracking-tight);
}

/* The subtitle is the operator's own `description` from /admin/plans, so it is
   allowed to be a full sentence — hence a measure and a line-height rather than
   a single-line label.

   The `min-height` is two lines, so the price beneath it starts at the same
   height across the row when one plan is described in a sentence and another in
   a phrase. It cannot align a three-line description with a one-line one, and
   it is not meant to: the real fix for that is short subtitles, which is a
   decision for /admin/plans rather than for this stylesheet. */
.plan-sub {
  margin: 0 0 var(--space-5);
  min-height: calc(1.45em * 2);
  font-size: var(--text-sm);
  color: var(--text-dim);
  line-height: 1.45;
}

/* The one figure a buyer scans the row for, so it gets the largest step in the
   card and the code beside it does not compete: the two parts are laid on one
   baseline at different sizes rather than set as a single run of text.

   The parts are inline children of a flex row, which is what makes the baseline
   alignment work across two font sizes — the number stays on the line while the
   code is set down against it. `--text-2xl` is the top of the existing ramp and
   fits: at the five-across breakpoint the widest pairing on the page, `USD`
   beside `17.5`, needs about 150px of the 200px available. */
.plan-price {
  display: flex;
  align-items: baseline;
  gap: 0.28em;
  margin-bottom: var(--space-1);
  font-size: var(--text-2xl);
  font-weight: 600;
  color: var(--text);
  line-height: 1.05;
  letter-spacing: var(--tracking-tight);
  white-space: nowrap;
}

.plan-currency {
  font-size: var(--text-xl);
  font-weight: 500;
  letter-spacing: 0;
}

/* What you get, in the same near-white as the price rather than dimmed: the
   token count is half of what the card is selling, and a buyer comparing four
   packages is reading this line, not the descriptive one above it. */
.plan-tokens {
  margin-bottom: var(--space-5);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text);
}

/* The rule above the benefits is the card's one internal division: everything
   above it is what you are buying, everything below it is what it does. */
.plan-benefits {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin: 0 0 var(--space-5);
  padding: var(--space-5) 0 0;
  border-top: 1px solid var(--border);
}

/* A grid rather than a flex row, so a benefit that wraps to a second line
   hangs under its own first word instead of under the tick. With flex the
   text is one anonymous item and its second line starts at the tick's edge. */
.plan-benefits li {
  display: grid;
  grid-template-columns: 15px 1fr;
  gap: var(--space-3);
  font-size: var(--text-xs);
  color: var(--text-dim);
  line-height: 1.45;
}

/* The tick is a mask rather than an inline <svg> per item: one rule instead of
   forty lines of repeated markup, and it takes `--accent` rather than a baked-in
   fill, so changing the brand colour carries the ticks with it. The mask uses
   the glyph's alpha, which is why the SVG's own stroke colour is irrelevant.

   A pseudo-element is a grid item like any other, so it takes the first column
   the `li` declares and the text takes the second. */
.plan-benefits li::before {
  content: "";
  width: 15px;
  height: 15px;
  margin-top: 0.3em;
  background-color: var(--accent);
  -webkit-mask: var(--icon-check) center / contain no-repeat;
  mask: var(--icon-check) center / contain no-repeat;
}

/* The emphasised card. `--surface-2` lifts it one step off the page, the accent
   border draws it, and the glow is deliberately weak — a coloured shadow that
   is legible as a colour reads as decoration, and this row has to read as five
   comparable packages with one recommendation rather than as one lit product
   and four also-rans. */
.plan-card-featured {
  border-color: var(--accent);
  background-color: var(--surface-2);
  box-shadow:
    0 0 0 1px rgba(var(--accent-rgb), 0.12),
    0 12px 55px rgba(var(--accent-rgb), 0.1);
}

/* On the card's top edge rather than above it, so the card it marks and the
   card it outranks still start on the same line.

   The fill is OPAQUE and darker than the card, not a translucent surface: the
   badge straddles the card's own border, and anything see-through would let
   that 1px line run straight through the label. */
.plan-badge {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  padding: 4px var(--space-4);
  border-radius: var(--radius-pill);
  border: 1px solid var(--accent);
  background-color: var(--card-bg);
  color: var(--accent);
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  white-space: nowrap;
  box-shadow: 0 0 25px rgba(var(--accent-rgb), 0.13);
}

/* `margin-top: auto` is what puts every card's button on the same line when the
   descriptions above are different lengths. Without it the shortest card has
   its button floating mid-card, which reads as a mistake rather than a layout.
   The gap above the button is the benefits list's own bottom margin, so there
   is no padding here to double it. */
.plan-card .card-actions {
  margin-top: auto;
  padding-top: 0;
}

/* `.btn` is `inline-block`, so a full-width button needs its own centring —
   `width` alone would leave the label pinned to the left edge. */
.plan-cta {
  display: block;
  width: 100%;
  text-align: center;
}

/* The custom card's button asks for a conversation rather than an account, so
   it is the quiet one in the row: outlined, not filled. That is now the only
   thing that distinguishes the custom card — it is a real thing to buy, not the
   odd one out, and the row should read as five options rather than four
   packages and an apology. */
.plan-cta-outline {
  background: transparent;
  border-color: var(--border-bright);
  color: var(--text);
}

.plan-cta-outline:hover {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent);
}

/* The billing note under the row: the last thing read before deciding, so it
   carries the three facts that make the decision safe — what the figure counts,
   that nothing is charged until it is funded, and that it does not lapse. */
.price-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4) var(--space-6);
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background-color: var(--surface);
}

/* The note takes whatever the bar has left once the tag has its width, and
   wraps only when that is genuinely not enough room for the sentence.

   THERE WAS A `max-width: 80ch` HERE AND IT WAS THE THING FORCING THE WRAP.
   80ch is about 620px at this size, and the note is a single sentence that
   needs roughly 1070px — so on the wide bar the cap cut the available width
   almost in half and broke the line after "funded, and" while ~400px of bar
   sat unused beside it. A measure cap earns its place on running prose, where
   a long line is tiring to read; this is one sentence on one line, and there
   is no measure to protect. The container is capped at 1700px, so the note
   cannot run away on a large screen even without it.

   `flex: 1` IS `flex-basis: 0`, AND THAT BASIS IS THE POINT. A wrapping flex
   row decides which line each item goes on using its base size, and the base
   size of an `auto`-basis note is its whole unwrapped sentence. At any width
   where the note plus the tag did not fit, that would have pushed the TAG onto
   a second line — leaving it stranded at the left under a full-width note,
   which is not the bar's shape. With a zero basis the line decision is made on
   the tag alone, so the note is the thing that wraps, exactly as it does now.

   `min-width: 0` is what lets it: a flex item will not shrink below its
   content width without it, so the note would overflow the bar instead of
   wrapping inside it. */
.price-bar-note {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  margin: 0;
  flex: 1;
  min-width: 0;
  font-size: var(--text-xs);
  color: var(--text-dim);
  line-height: 1.5;
}

/* The icon is the accent, which is the one piece of colour in the bar and what
   makes it read as an information mark rather than as a stray glyph. */
.price-bar-note svg {
  flex: none;
  width: 19px;
  height: 19px;
  margin-top: 1px;
  color: var(--accent);
}

/* The right-hand tag is a different kind of statement from the note beside it,
   so it is separated by a rule rather than by whitespace alone. */
.price-bar-tag {
  margin: 0;
  padding-left: var(--space-6);
  border-left: 1px solid var(--border);
  font-size: var(--text-xs);
  color: var(--text-faint);
  white-space: nowrap;
}

/* Stacked, the rule moves to the top: left as it is, it would be a stray
   vertical line beside the first word of the tag rather than a divider between
   two rows. 900px is where the note and the tag stop fitting on one line
   without the note being squeezed to a two-word measure. */
@media (max-width: 900px) {
  .price-bar {
    flex-direction: column;
    align-items: flex-start;
  }

  /* The note's `flex: 1` above is undone here, and not for tidiness. That
     shorthand carries `flex-basis: 0`, which in a COLUMN container is a
     statement about the note's HEIGHT rather than its width — the note would
     be asked to start at zero height in a bar whose height is decided by its
     own contents, which is how a note ends up collapsed or clipped instead of
     simply stacked under the cards. `none` returns it to its natural size, so
     the stacked bar is measured by the same rule as before. */
  .price-bar-note {
    flex: none;
  }

  .price-bar-tag {
    width: 100%;
    padding-left: 0;
    padding-top: var(--space-3);
    border-left: 0;
    border-top: 1px solid var(--border);
  }
}

/* --------------------------------------------------------------------------
   The wide row
   --------------------------------------------------------------------------

   Everything above this line describes the card row at the size it was
   measured at, and none of it changes here. This block is what the extra width
   from `.wrap-wide` is spent on, and it is a media query rather than a fluid
   value for one specific reason: BELOW 1345px IT MUST NOT APPLY AT ALL.

   1345 is not a taste threshold, it is arithmetic. `.wrap-wide` and `.wrap`
   both resolve to `100% - gutter * 2` until the available width passes 1280px,
   and `.wrap-wide`'s ceiling is only reached once the gutters clear the old cap
   — 1280 + 32 + 32 = 1344. Under that viewport the two containers are the same
   width, so there is no extra room to spend and widening the card's own padding
   would take width away from the content instead of giving it any.

   That matters for the floor above, not just for looks. `.plan-grid` sits at
   216px tracks on a small desktop and the widest price needs ~150px of the
   176px that leaves. A horizontal padding of 24px would cut that to 168px and
   the price would start to clip. Confining the increase to viewports where a
   track is at least 240px wide keeps the measured numbers true exactly where
   they were measured.

   The padding is grown on the left and right only. The request was more
   horizontal room, and the cards are already as tall as their tallest
   neighbour; adding vertical padding here would make the row taller without
   making it fit anything better. */
@media (min-width: 1345px) {
  /* 20px -> 24px of side padding. On a 1700px container the five tracks are
     324px wide, so this is 276px of content in each — wider than the 200px the
     same card had at the old 1280px ceiling, with the padding grown to match so
     the text does not sit against the edge. */
  .plan-card {
    padding-left: var(--space-6);
    padding-right: var(--space-6);
  }

  /* The tick column gains a little air from the benefit it labels. Four pixels,
     but it is the difference between the tick reading as attached to its own
     line and as a separate margin mark. */
  .plan-benefits li {
    gap: var(--space-4);
  }

  /* 12px -> 16px of block padding: a taller target, on the full width of the
     card. The label keeps its size — this is a bigger button, not bigger type. */
  .plan-cta {
    padding-block: var(--space-4);
  }
}

/* An animation preference is an accessibility requirement, so this is not
   merely "less motion" — it is none, including the entrance transition, and
   the reveal elements are forced back to visible so nothing can be left
   hidden by a transition that never runs. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  .js-reveal .reveal,
  .js-reveal .reveal.is-in {
    opacity: 1;
    transform: none;
  }

  /* There is no `.feature:hover { transform: none }` here any more, and its
     absence is deliberate: features no longer lift on hover at all, so the rule
     would be a guard over nothing. The blanket `transition-duration` above
     already stops the border-colour change animating. */

  /* The scrub value arrives as an inline custom property, and an inline style
     outranks every selector in this file — `!important` is the only thing that
     beats it. So the parallax is stopped here as well as in the script, which
     declines to run under this preference: if it ran anyway, this rule would
     still hold every layer still. The read line above is not covered, on
     purpose and for the reason given at `.scroll-progress`. */
  [data-parallax] {
    --scrub: 0 !important;
  }
}
