/* The portal's only stylesheet.
 *
 * One file, no build step, no framework. What changed since the last revision
 * is scale: this is no longer four pages seen by somebody who came to fetch a
 * file, it is eight screens somebody works in. It is still a document that has
 * to read well at any width.
 *
 * # Where these values come from
 *
 * The token block below is COPIED from kerbrus-site/src/styles/tokens.css -
 * the Kerbrus web design layer - and the marketing site is the authority for
 * every one of them. A customer arrives here from kerbrus.com, and a portal
 * that invents its own palette reads as a different company's software.
 *
 * Copied rather than imported, deliberately. The CSP here is `style-src
 * 'self'`, so a stylesheet fetched from kerbrus.com could not load at all; and
 * a runtime dependency on the marketing site is exactly the shared
 * infrastructure ../CLAUDE.md rules out - the site going down must not take
 * the licence portal's styling with it. Tokens are build-time constants, so a
 * copy is honest. When the site's token layer moves, re-copy this block.
 *
 * THE SUBSET IS WIDER THAN IT WAS. The earlier copy took the handful of values
 * four plain pages needed. These screens need the rest of the ladder the site
 * already ships - --text-31, --space-5/7/9/14/16/28/32, --radius-md/lg/xl,
 * --col-max, the glass pair and the card elevation - so they are copied too,
 * from the same file, at the same values. Nothing here is a value invented for
 * the portal.
 *
 * Two deliberate departures, both because this is not the marketing site:
 *
 *   1. THE ACCENT DOES NOT TRAVEL. On kerbrus.com the accent is a position on
 *      the spectrum rail and advances as the visitor scrolls. There is no long
 *      scroll here, so the portal pins it at the site's own mid-ramp default -
 *      the value tokens.css already ships so the page is never colourless when
 *      the script does not run. It is one value in one place; repoint it here.
 *
 *      It is NOT the cyan the design canvas is drawn in. The canvas fixes one
 *      stop of the ramp as a literal because a .dc.html cannot read a variable;
 *      the ramp is the accent, and its mid-point is what a page with no scroll
 *      gets.
 *
 *   2. SEVERITY COMES FROM APERTURE. The site's layer defines no severity
 *      colours because a marketing page never needs one, and this application
 *      has to say "expired", "awaiting confirmation" and "verified". They are
 *      taken from the product's token layer
 *      (FlowCollector/ds-bundle/tokens/tokens.css), which is where the site's
 *      own neutrals came from, so the two agree by construction.
 *      --severity-success is copied from that same file for the same reason
 *      the other two were: the screens needed a positive state and inventing a
 *      green would have been a third palette.
 *
 * # Three decisions worth stating, because all three could look like drift
 *
 * THE TYPE SCALE IS THE SITE'S, NOT THE CANVAS'S. The canvas is drawn on a
 * denser ladder - 42, 38, 33, 26, 20, 19, 14, 10 - and none of those rungs
 * exists in the design layer, whose scale is 11/12/13/15/18/24/31 plus one
 * hero size. Rather than add eight rungs to a shared layer for one
 * application, every size here snaps to the existing ladder: page titles land
 * on 31 instead of 42, section headings on 18 instead of 20, body and tables
 * on 15 and 13. The pages are a little quieter than the drawing and they are
 * unmistakably the same system as kerbrus.com.
 *
 * THE BODY FACE IS CHAKRA PETCH. The canvas sets body copy in IBM Plex Sans
 * and mono in JetBrains Mono. Neither is self-hosted in either repository, the
 * CSP is `font-src 'self'`, and adding two families is six binaries and a
 * decision about the brand's text face - not something a screen build gets to
 * make on the way past. So --font-ui is the brand face, as on the site, and
 * --font-mono resolves to the platform's monospace. Mono still carries meaning
 * here because it is a DIFFERENT family from the body, which is the whole of
 * what that rule needs; it simply is not yet JetBrains Mono.
 *
 * THE FOCUS RING IS 2px OF --accent-ring. The canvas asks for three pixels of
 * cyan. Cyan is not the accent (see above) and the ring already existed at two
 * pixels, derived from whatever the accent is. A ring that names a colour is a
 * ring that stops matching the moment the accent moves.
 *
 * Dark only, like the site. There is no light counterpart and no
 * `prefers-color-scheme` branch - a second palette is a second thing to drift.
 */

/* -- fonts: self-hosted, the three weights this document uses -----------
   Needs `font-src 'self'` in the CSP; see app/src/lib/views/shell.ts. */
@font-face{font-family:'Chakra Petch';font-weight:400;font-style:normal;font-display:swap;
  src:url('/fonts/chakra-petch-latin-400-normal.woff2') format('woff2')}
@font-face{font-family:'Chakra Petch';font-weight:500;font-style:normal;font-display:swap;
  src:url('/fonts/chakra-petch-latin-500-normal.woff2') format('woff2')}
@font-face{font-family:'Chakra Petch';font-weight:600;font-style:normal;font-display:swap;
  src:url('/fonts/chakra-petch-latin-600-normal.woff2') format('woff2')}

:root {
  color-scheme: dark;

  /* ---------- neutrals (site layer, verbatim) ---------- */
  --void-900: #050a13;
  --void-700: #18202f;
  --void-650: #232c3e;
  --slate-100: #e9eff7;
  --slate-300: #94a6bd;
  --slate-400: #7c8ea6;

  /* ---------- surfaces ---------- */
  --surface-page: var(--void-900);
  --surface-card: rgb(9 15 26 / 62%);
  --surface-raised: rgb(5 9 15 / 58%);
  --surface-hover: rgb(255 255 255 / 6%);
  --glass-lift: rgb(255 255 255 / 10%);
  --glass-cast: rgb(0 0 0 / 46%);

  /* the header's scrim is mixed FROM the ground, so the ground is stated once */
  --scrim-nav: color-mix(in srgb, var(--void-900) 92%, transparent);

  /* ---------- borders ---------- */
  --border-strong: var(--void-650);
  --border-subtle: var(--void-700);

  /* ---------- text ---------- */
  --text-primary: var(--slate-100);
  --text-secondary: var(--slate-300);
  --text-muted: var(--slate-400);

  /* ---------- accent: the site's mid-ramp default, pinned ---------- */
  --accent: #bd53ef;
  --accent-ink: #050a13;
  --accent-soft: color-mix(in srgb, var(--accent) 15%, transparent);
  --accent-ring: color-mix(in srgb, var(--accent) 45%, transparent);

  /* The site layer's one named, static accent tint - "used only where a dimmed
     accent that does not travel is wanted", which is every accent here. Copied
     for the downloads groups: they need a second colour that is on the system,
     and tokens.css forbids naming a ramp stop to get one. */
  --cyan-dim: #12b6bd;

  /* ---------- severity (Aperture, dark) ---------- */
  --severity-success: #3fcf9e;
  --severity-warning: #f2cd5c;
  --severity-critical: #ef5f6b;

  /* ---------- type ---------- */
  --font-ui: 'Chakra Petch', system-ui, 'Segoe UI', sans-serif;
  --font-mono: ui-monospace, 'JetBrains Mono', 'SFMono-Regular', Menlo, Consolas, monospace;
  --text-11: 11px;
  --text-12: 12px;
  --text-13: 13px;
  --text-15: 15px;
  --text-18: 18px;
  --text-24: 24px;
  --text-31: 31px;
  --leading-tight: 1.15;
  --leading-normal: 1.5;
  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --tracking-title: 0.02em;
  --tracking-caps: 0.09em;
  --numeric: tabular-nums;

  /* ---------- spacing ---------- */
  --space-1: 2px;
  --space-2: 4px;
  --space-3: 6px;
  --space-4: 8px;
  --space-5: 10px;
  --space-6: 12px;
  --space-7: 14px;
  --space-8: 16px;
  --space-9: 18px;
  --space-10: 20px;
  --space-12: 24px;
  --space-14: 28px;
  --space-16: 32px;
  --space-20: 40px;
  --space-24: 48px;
  --space-28: 56px;
  --space-32: 64px;

  --col-max: 1200px;

  /* ---------- shape ---------- */
  --radius-xs: 2px;
  --radius-sm: 5px;
  --radius-md: 7px;
  --radius-lg: 8px;
  --radius-xl: 12px;
  --radius-pill: 999px;

  --shadow-frame: 0 24px 60px -12px rgb(0 0 0 / 70%);
  --elevation-card: inset 0 1px 0 var(--glass-lift), 0 12px 34px var(--glass-cast);

  /* ---------- motion ---------- */
  --duration-instant: 90ms;
  --duration-fast: 140ms;
  --ease-standard: cubic-bezier(0.2, 0, 0.2, 1);
  --transition-control: background-color var(--duration-fast) var(--ease-standard),
    border-color var(--duration-fast) var(--ease-standard),
    color var(--duration-fast) var(--ease-standard);

  /* ---------- layout constants, not design tokens ----------
     Two fixed dimensions this application has and the design layer has no
     opinion about: the height of the sticky header, and the width of the rail
     beside a page's main column. They are here so the two places that need
     each are one place; they are not candidates for the shared layer. */
  --chrome-h: 68px;
  --rail-w: 340px;
  /* The wordmark's RENDERED width. The asset is 8:1, so a width here also
     sets the height, and the two variants below override this property
     rather than introducing constants of their own: 104px is the signed-in
     lockup at 13px tall, 128px the entry header at kerbrus.com's own 16px,
     160px the entry card's mark at 20px. Reshaping the asset means revisiting
     all three. */
  --wordmark-w: 104px;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition: none !important; animation: none !important; }
}

/* -- base --------------------------------------------------------------- */

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--surface-page);
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-size: var(--text-15);
  line-height: var(--leading-normal);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { margin: 0; font-weight: var(--weight-semibold); letter-spacing: var(--tracking-title); }
h1 { font-size: var(--text-31); line-height: var(--leading-tight); }
h2 { font-size: var(--text-18); }
h3 { font-size: var(--text-15); }

p { margin: 0; }

a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
a:hover { color: var(--text-primary); }

code, .mono, .key, .id, .num, pre { font-family: var(--font-mono); }

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

.lede { color: var(--text-secondary); }
.fine { color: var(--text-muted); font-size: var(--text-13); }

/* One phrase inside muted text, at full contrast. For the thing in a sentence
   that must not be skimmed past - the first administrator's password, which is
   printed once by install.sh and is the only way into the product afterwards.
   Colour, not weight: bold in fine print reads as a heading for the sentence. */
.lit { color: var(--text-primary); }
.none { color: var(--text-muted); }
.unit { color: var(--text-muted); font-family: var(--font-ui); font-size: var(--text-12); }
.sep { color: var(--border-strong); margin: 0 var(--space-3); }

.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--text-11);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--text-muted);
}

.note, .problem {
  padding: var(--space-6) var(--space-8);
  border-radius: var(--radius-lg);
  background: var(--surface-card);
  border-left: var(--space-3) solid var(--accent);
  font-size: var(--text-13);
}
.problem { border-left-color: var(--severity-critical); color: var(--severity-critical); }

/* `.sent` was here and is gone with its one use. It put a success-coloured
   rail down the left edge of the "check your email" confirmation, which is the
   framework-alert idiom and reads as a component library rather than as this
   product. The page it was on carries one message, and a box around a page's
   only message separates it from nothing. See views/auth.ts forgot(). */

/* The confirmation sentence, now that it is a sentence and not a box. Sized
   above the fine print around it because it is the answer to the question the
   page was opened with. */
.said { font-size: var(--text-15); color: var(--text-primary); }

/* The one value on a page somebody is timing themselves against. Brighter
   than the muted `.fine` it sits inside, which is the whole job. */
.lasts { color: var(--text-primary); font-weight: var(--weight-semibold); }

/* The resend button's row. It is the primary action on a page whose reason to
   exist is that the first attempt did not arrive, so it gets the width. */
.entry-act { margin: var(--space-8) 0 var(--space-6); }
.entry-act .button { display: flex; width: 100%; }
.trial { color: var(--severity-warning); }
.blocked { color: var(--text-muted); }

/* -- the chrome --------------------------------------------------------- */

.chrome {
  position: sticky;
  top: 0;
  z-index: 10;
  height: var(--chrome-h);
  background: var(--scrim-nav);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--border-subtle);
}

.chrome-inner {
  max-width: var(--col-max);
  height: 100%;
  margin: 0 auto;
  padding: 0 var(--space-8);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-12);
}

/* Stacked: the qualifier sits under the wordmark rather than beside it. See
   views/shell.ts for where the tracking below came from. The entry card's
   mark is the same construction at a larger size, so it shares the rule
   rather than restating it. */
.wordmark, .entry-mark {
  display: flex;
  flex-direction: column;
  /* Centres the qualifier under the wordmark: the image is the wider of the
     two, so it sets this column's width and the tag centres inside it. */
  align-items: center;
  gap: var(--space-2);
}
.wordmark { text-decoration: none; flex-shrink: 0; }
.wordmark img, .entry-mark img { display: block; width: var(--wordmark-w); height: auto; }

/* The entry pages' header: the bare wordmark at the size kerbrus.com's nav
   gives it, with no qualifier under it - that has moved into the card. */
.wordmark.bare { --wordmark-w: 128px; }
.wordmark-tag {
  font-family: var(--font-mono);
  font-size: var(--text-11);
  /* Half of what spreading PORTAL across the wordmark's 104px produced: that
     put 13.5px between letters, this puts 6.77px. In em so it tracks the font
     size, and because the figure it replaced depended on whichever monospace
     face the viewer had - this one does not. */
  letter-spacing: 0.615em;
  /* Tracking lands after the LAST letter as well, so the box is one gap wider
     than the word looks. Centring without pulling that back would sit PORTAL
     about 3.4px left of the wordmark's centre - which is precisely the kind of
     off-by-a-hair the eye reads as sloppy rather than as a mistake. */
  margin-right: -0.615em;
  color: var(--accent);
}

.nav { display: flex; align-items: stretch; gap: var(--space-14); height: 100%; }
.nav a {
  display: flex;
  align-items: center;
  height: 100%;
  border-bottom: 2px solid transparent;
  color: var(--text-secondary);
  text-decoration: none;
  transition: var(--transition-control);
}
.nav a:hover { color: var(--text-primary); }
.nav a.on { color: var(--text-primary); border-bottom-color: var(--accent); }

/* The entry pages' nav points at kerbrus.com, so none of its four links can
   ever be the current page and the 2px slot above is reserving room for a
   state that cannot happen. Left in, it pushes those labels one pixel above
   the wordmark and the button they sit between - measured, not guessed. */
body.entry .nav a { border-bottom: 0; }

.chrome-end { display: flex; align-items: center; gap: var(--space-6); }

/* the account chip, which is a <details> so it needs no script
   The summary is the disc and nothing else - no name, no address, no caret.
   A bordered box holding an email address was the widest thing in the header
   and the one part of it that is nobody else's business; the name and the
   address are in the menu below, which is where they are read. */
.chip { position: relative; flex-shrink: 0; }
.chip summary {
  display: flex;
  cursor: pointer;
  list-style: none;
  border-radius: 50%;
  transition: var(--transition-control);
}
.chip summary::-webkit-details-marker { display: none; }
.chip-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  background: var(--accent-soft);
  font-family: var(--font-mono);
  font-size: var(--text-12);
  transition: var(--transition-control);
}
.chip summary:hover .chip-mark { border-color: var(--accent-ring); }
/* The focus ring lands on the summary, which is flush to the disc - so it
   traces the circle rather than a square around it. */
.chip summary:focus-visible { outline-offset: 2px; }
.chip-name { font-size: var(--text-13); }

.chip-menu {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-3));
  min-width: 15rem;
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  background: var(--surface-page);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-frame);
}
.chip-who { font-size: var(--text-12); color: var(--text-muted); overflow-wrap: anywhere; }

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

/* On every page (views/shell.ts FOOTER). Muted and small: it is there to be
   found, not read. On the entry pages it sits at the bottom of the flex column
   body.entry already is, over the scrim that keeps the page's bottom edge
   dark. */
.site-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-4) var(--space-12);
  padding: var(--space-8) var(--space-8) var(--space-12);
  color: var(--text-muted);
  font-size: var(--text-12);
}
.site-foot nav { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-4) var(--space-8); }
.site-foot a { color: var(--text-secondary); }
.site-foot a:hover { color: var(--text-primary); }
body.app .site-foot { border-top: 1px solid var(--border-subtle); }

/* -- the column --------------------------------------------------------- */

.column {
  max-width: var(--col-max);
  margin: 0 auto;
  padding: var(--space-20) var(--space-8) var(--space-32);
  display: flex;
  flex-direction: column;
  gap: var(--space-14);
}

.head { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-12); }
.head-text { display: flex; flex-direction: column; gap: var(--space-4); }
.meta { display: flex; align-items: center; color: var(--text-secondary); font-size: var(--text-13); }

.crumbs {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  font-family: var(--font-mono);
  font-size: var(--text-12);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}
.crumbs a { text-decoration: none; }
.crumbs-here { color: var(--text-secondary); }

.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--rail-w);
  gap: var(--space-12);
  align-items: start;
}
.stack, .rail { display: flex; flex-direction: column; gap: var(--space-12); min-width: 0; }

/* -- cards and tiles ---------------------------------------------------- */

.card {
  padding: var(--space-12);
  background: var(--surface-card);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-xl);
  box-shadow: var(--elevation-card);
  display: flex;
  flex-direction: column;
  gap: var(--space-9);
  min-width: 0;
}
.card.warn { border-color: color-mix(in srgb, var(--severity-warning) 45%, transparent); }

.card-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-8); }
.card-label {
  font-family: var(--font-mono);
  font-size: var(--text-11);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--text-muted);
}

.tiles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-10); }
.tile {
  padding: var(--space-10) var(--space-12);
  background: var(--surface-card);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-xl);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.tile-label {
  font-family: var(--font-mono);
  font-size: var(--text-11);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--text-muted);
}
.tile-value {
  font-size: var(--text-24);
  line-height: var(--leading-tight);
  font-weight: var(--weight-medium);
  color: var(--text-primary);
}
.tile-value .mono { font-variant-numeric: var(--numeric); }
.tile-detail { font-size: var(--text-13); color: var(--text-secondary); }

.banner {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  padding: var(--space-9) var(--space-12);
  background: var(--surface-card);
  border: 1px solid var(--accent-ring);
  border-radius: var(--radius-xl);
  font-size: var(--text-13);
}
.banner-label {
  font-family: var(--font-mono);
  font-size: var(--text-11);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--accent);
  flex-shrink: 0;
}
.banner p:nth-child(2) { flex-grow: 1; }

/* -- state, never colour alone ------------------------------------------ */

.state { display: inline-flex; align-items: center; gap: var(--space-4); white-space: nowrap; }
.state .dot {
  width: 7px;
  height: 7px;
  border-radius: var(--radius-pill);
  background: currentColor;
  /* glow, not shadow: the light comes off the state itself */
  box-shadow: 0 0 8px currentColor;
}
.state-ok { color: var(--severity-success); }
.state-warn { color: var(--severity-warning); }
.state-bad { color: var(--severity-critical); }
.state-idle { color: var(--text-muted); }
.state-idle .dot { box-shadow: none; }

/* -- facts -------------------------------------------------------------- */

.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: var(--space-10);
  padding-top: var(--space-9);
  border-top: 1px solid var(--border-subtle);
}
.fact { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.fact-label {
  font-family: var(--font-mono);
  font-size: var(--text-11);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--text-muted);
}
.fact-value { font-size: var(--text-13); overflow-wrap: anywhere; }

/* -- the licence key ---------------------------------------------------- */

.keyline { display: flex; align-items: stretch; gap: var(--space-6); }
.key {
  flex-grow: 1;
  min-width: 0;
  padding: var(--space-7) var(--space-8);
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  font-size: var(--text-13);
  line-height: var(--leading-normal);
  /* A key is one long unbroken string. It wraps rather than scrolls, because
     somebody reading it off the screen to type elsewhere needs all of it. */
  overflow-wrap: anywhere;
}

.snippet {
  margin: 0;
  padding: var(--space-6) var(--space-7);
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  font-size: var(--text-12);
  overflow-x: auto;
}

/* A snippet sharing a .keyline with its Copy button, which is how the install
   commands render. `.key` above grows by its own rule; a `<pre>` has to be
   told, and needs the `min-width` with it - a flex item's default minimum is
   its content, so without this a long curl line widens the card instead of
   scrolling inside the box that `overflow-x` already gave it. */
.keyline > .snippet { flex-grow: 1; min-width: 0; }

.actions { display: flex; flex-wrap: wrap; gap: var(--space-12); font-size: var(--text-13); }

/* -- forms -------------------------------------------------------------- */

form { display: flex; flex-direction: column; gap: var(--space-8); }

.field { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }
.field.grow { flex: 1 1 14rem; }

/* What a control does, said beside the control. Distinct from `.fine`, which
   is an aside anywhere on a page: a hint belongs to the field above it and is
   read while somebody is deciding what to put in it. Used first by the
   environment select, which asked for a permanent choice and said nothing
   about what it was for - see P-0018. */
.hint { font-size: var(--text-12); color: var(--text-muted); max-width: 42ch; }

/* Fields that are one answer in several boxes - a first and a last name -
   sharing a line, and stacking rather than shrinking once they cannot.
   `auto-fit` does the whole of that with no media query, so the breakpoint is
   the container's own width and the rule keeps working wherever it is used.

   9rem is chosen against the narrowest place it renders: the entry card is
   27rem wide with --space-20 either side, which leaves 22rem of content, so
   two 9rem columns and a --space-6 gap fit with room to spare and two 12rem
   columns would not fit at all. On a phone, where the same card has about
   15rem of content, two columns no longer fit and the row becomes a stack -
   which is the right answer there and not a fallback. */
.field-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--space-6);
}
.field-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-6); }

label {
  font-family: var(--font-mono);
  font-size: var(--text-11);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--text-muted);
}

input, select, button {
  font: inherit;
  padding: var(--space-5) var(--space-7);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  color: var(--text-primary);
  transition: var(--transition-control);
}
input, select { width: 100%; }
input:hover, select:hover { border-color: var(--accent-ring); }
input[type=checkbox] { width: auto; accent-color: var(--accent); }

button {
  cursor: pointer;
  align-self: flex-start;
  border-color: var(--accent);
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: var(--weight-semibold);
}
button:hover { background: var(--text-primary); border-color: var(--text-primary); }
button[disabled] { opacity: 0.55; cursor: default; }
button[disabled]:hover { background: var(--accent); border-color: var(--accent); }
button.wide { align-self: stretch; text-align: center; }

.ghost, a.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-5) var(--space-9);
  border-radius: var(--radius-sm);
  text-decoration: none;
  font-size: var(--text-13);
  font-weight: var(--weight-semibold);
  white-space: nowrap;
}
a.button { background: var(--accent); color: var(--accent-ink); border: 1px solid var(--accent); }
a.button:hover { background: var(--text-primary); border-color: var(--text-primary); color: var(--accent-ink); }
.ghost {
  background: transparent;
  border: 1px solid var(--border-strong);
  color: var(--text-primary);
  font-weight: var(--weight-regular);
}
.ghost:hover { background: var(--surface-hover); border-color: var(--accent-ring); color: var(--text-primary); }

button.link {
  align-self: flex-start;
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  font-weight: var(--weight-regular);
  text-decoration: underline;
}
button.link:hover { background: none; color: var(--text-primary); }

form.inline, form.filters { flex-direction: row; flex-wrap: wrap; align-items: flex-end; gap: var(--space-6); }
form.inline button, form.filters button { align-self: flex-end; }

/* A hint in an inline form belongs to the row, not to one field in it: given
   its own line it leaves every control sharing a baseline, where inside a
   field it made that field taller than its neighbours and `flex-end` pushed
   them all down to meet it.

   The wrapper is what carries the full-width basis, and it is not decoration.
   Flex breaks lines on an item's hypothetical main size, which `.hint`'s own
   42ch max-width clamps - so a bare `flex-basis: 100%` on the paragraph still
   fits beside the button and stays on the row. The wrapper takes the line and
   the paragraph keeps its measure. */
form.inline > .form-note { flex: 1 0 100%; }

/* On its own line a hint has room for a longer measure than it gets under a
   field, and one sentence broken across two lines below a form reads as two
   thoughts. 64ch is still a measure, not the card's full width. */
form.inline > .form-note .hint { max-width: 64ch; }
form.filters { padding-bottom: var(--space-9); border-bottom: 1px solid var(--border-subtle); }
form.filters .ghost { align-self: flex-end; }
form.rename { flex: 0 0 auto; }
form.rename input { min-width: 14rem; }

/* The two optional opt-ins. Tighter than the form's own gap on purpose: they
   are one question with two answers, and spacing them like fields would make
   them read as two more things being asked. */
.check-group { display: flex; flex-direction: column; gap: var(--space-5); }

.check { display: flex; align-items: flex-start; gap: var(--space-5); }
.check label {
  font-family: var(--font-ui);
  font-size: var(--text-13);
  letter-spacing: normal;
  text-transform: none;
  color: var(--text-secondary);
}

.cf-turnstile:not(:empty) { margin-top: var(--space-2); }

/* -- tables ------------------------------------------------------------- */

/* A group of release objects on /downloads, captioned by deployment path.
   The listing used to be one name-ordered table, which told a first-time
   customer nothing about which of eight files they needed. Each group is a
   caption, a sentence and its own table, so the answer is on the page. */
.group + .group { margin-top: var(--space-10); }
.group-head {
  font-family: var(--font-mono);
  font-size: var(--text-11);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--text-muted);
  font-weight: var(--weight-medium);
  margin: 0 0 var(--space-6);
}
.group .fine { margin: 0 0 var(--space-7); }

/* One rule down the left edge per group, in a colour of its own, so the three
   deployment paths separate before the captions are read. The colours are the
   accent, the site layer's static cyan tint and the warning severity - no new
   values, and no ramp stop named, which tokens.css forbids.

   `other` takes the neutral border on purpose: it is the group for a name this
   page has no rule about, and giving it a colour of its own would dress an
   admission up as a category. */
.group {
  border-left: 2px solid var(--border-strong);
  padding-left: var(--space-8);
}
.group.g-software { border-left-color: color-mix(in srgb, var(--accent) 65%, transparent); }
.group.g-software .group-head { color: var(--accent); }
.group.g-cloud { border-left-color: color-mix(in srgb, var(--cyan-dim) 70%, transparent); }
.group.g-cloud .group-head { color: var(--cyan-dim); }

/* The air-gap bundle, and the one group that has to be hard to take by
   accident: it is ~345 MB against a release that is otherwise tens of
   kilobytes, and it is useless to a host that can reach GHCR. The rule is a
   stop sign rather than decoration - people were downloading it because
   nothing on the page said not to. Unchanged by the colours above: the other
   bars distinguish, this one warns. */
.group.warn {
  border-left-color: color-mix(in srgb, var(--severity-warning) 55%, transparent);
}
.group.warn .group-head { color: var(--severity-warning); }

/* Release notes reuse the group rule rather than inventing a second way to
   separate sections, so `### Fixed` in a changelog reads like `Air-gapped
   only` in the table above it: same bar, same uppercase mono caption.

   No colour of its own, the same reasoning as `other`. The bars above mean
   "which deployment path"; a changelog group means "Added" or "Fixed", which
   is a different axis, and colouring it would imply the two scales are one.
   Severity by colour alone is forbidden here anyway - the word is the label. */
.notes-list {
  margin: 0;
  padding-left: var(--space-8);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  color: var(--text-secondary);
  font-size: var(--text-13);
}
.notes-list li { padding-left: var(--space-2); }
/* In the 340px rail a heading and its link do not fit on one line, and a long
   file or flag name in a note is one unbreakable word wider than the card. */
.rail .card-head { flex-wrap: wrap; }
.rail .notes-list { overflow-wrap: anywhere; }
.card > .lede + .group { margin-top: var(--space-10); }

table { width: 100%; border-collapse: collapse; }

th, td {
  text-align: left;
  padding: var(--space-7) var(--space-6) var(--space-7) 0;
  border-bottom: 1px solid var(--border-subtle);
  vertical-align: top;
}
th {
  padding-top: 0;
  font-family: var(--font-mono);
  font-size: var(--text-11);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-muted);
  font-weight: var(--weight-medium);
  border-bottom-color: var(--border-strong);
}
td { font-size: var(--text-13); }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: var(--surface-hover); }

/* "Update to 0.7.0", beside the version an install last reported.
 *
 * Not a severity: an install on an older release is behind, not broken, and
 * the warning colours are spoken for by states that need a human. So it takes
 * the accent, which everywhere else in this stylesheet means "somewhere to
 * go" - and it IS somewhere to go, being a link to the archives.
 *
 * It carries the words and the version, never the colour alone, for the same
 * reason .state does: a reader who cannot separate the hues still gets the
 * whole fact, and support and customer end up with the same name for it.
 */
.behind {
  display: inline-block;
  margin-left: var(--space-4);
  padding: 0 var(--space-3);
  border: 1px solid var(--accent-ring);
  border-radius: var(--radius-pill);
  background: var(--accent-soft);
  color: var(--accent);
  font-family: var(--font-ui);
  font-size: var(--text-12);
  line-height: 1.7;
  white-space: nowrap;
  text-decoration: none;
}
.behind:hover { border-color: var(--accent); color: var(--text-primary); }

/* The same pill when the mark is a STATEMENT rather than somewhere to go.
 *
 * rows.ts § versionMark says three things and only one of them - "Update to
 * X" - is an instruction anybody can act on. The other two report what an
 * install is running: a development build, or a version of a shape we do not
 * recognise. Those are not links, so they must not wear the accent, which
 * means "somewhere to go" everywhere else in this stylesheet.
 *
 * It keeps the pill, the border and the position, because it is still the
 * same fact about the same cell - it just stops inviting a click. */
.behind.is-note {
  border-color: var(--border-strong);
  background: transparent;
  color: var(--text-secondary);
}

.num { text-align: right; white-space: nowrap; font-variant-numeric: var(--numeric); padding-right: 0; }

/* The requirements table. Nothing in it is a link or a row you act on, so the
   hover highlight the installs list wants would be an affordance for nothing.

   Fixed layout because auto-layout hands the leftover width to the first
   column, and a rate label three times the width of the number it labels
   reads as a table with one important column and three afterthoughts. Four
   equal columns is what it is: a rate, and what that rate costs. */
.reqs { table-layout: fixed; }
.reqs tbody tr:hover { background: none; }
.reqs td { white-space: nowrap; }

.id {
  display: block;
  font-size: var(--text-11);
  color: var(--text-muted);
  margin-top: var(--space-1);
  overflow-wrap: anywhere;
}

.kind {
  display: inline-block;
  margin-left: var(--space-4);
  padding: 0 var(--space-4);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  font-family: var(--font-mono);
  font-size: var(--text-11);
  color: var(--text-secondary);
}

ul.plain { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--space-7); font-size: var(--text-13); }

/* -- the empty state ---------------------------------------------------- */

/* The first-run guide was a three-column grid of four paragraphs, which is a
   layout for things you compare and not for things you do in order. It is a
   numbered list now: one column, top to bottom, the step you are on holding
   its own form rather than a link to one.

   `.card.empty`, `.empty-lead`, `.steps` and `.steps-grid` below are what it
   used to be. They are kept because the shape is a reasonable empty state and
   this is a stylesheet, not a view - but nothing renders them today, and the
   next person to delete a rule should start here. */

/* The guided list. `.step-body` exists so the number can sit beside the whole
   step rather than above its heading: a step that holds a form is tall, and a
   number floating at the top of a tall block stops reading as its label. */
.steps-list { list-style: none; display: flex; flex-direction: column; gap: var(--space-14); }
.steps-list .step { flex-direction: row; gap: var(--space-8); align-items: flex-start; }
.steps-list .step-n { padding-top: var(--space-2); min-width: 2ch; }
.step-body { display: flex; flex-direction: column; gap: var(--space-5); min-width: 0; flex: 1 1 auto; }
.step-body h3 { font-size: var(--text-15); }

/* The precondition above the numbers. Same size as a step heading, because it
   is read at the same altitude as one - it is not a step only because it
   produces nothing and cannot be ticked off. */
.prep-head { font-size: var(--text-15); }
.step-body > p { font-size: var(--text-13); color: var(--text-secondary); }
.step-body .fine { color: var(--text-muted); }
.step-body form { padding-top: var(--space-4); }

/* The step you are on, against the ones you are not. The later steps are not
   disabled - they are readable, and a customer deciding whether this is worth
   starting is entitled to see where it ends - only quieter. */
.step.done-next .step-n { color: var(--accent); }
.step.later .step-n { color: var(--text-muted); }
.step.later h3 { color: var(--text-secondary); }

/* The branch, asked once and before the list, instead of narrated inside two
   consecutive steps in nearly the same words. */
.fork {
  padding: var(--space-6) var(--space-8);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  font-size: var(--text-13);
  color: var(--text-secondary);
}
.fork strong { color: var(--text-primary); }

.card.empty { gap: var(--space-24); padding: var(--space-24) var(--space-20); }
.empty-lead {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-8);
  text-align: center;
}
.empty-lead h2 { font-size: var(--text-24); }
.empty-lead p { max-width: 34rem; color: var(--text-secondary); }

.steps { display: flex; flex-direction: column; gap: var(--space-10); padding-top: var(--space-16); border-top: 1px solid var(--border-subtle); }
.steps-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-12); }
.step { display: flex; flex-direction: column; gap: var(--space-5); }
.step-n { font-family: var(--font-mono); font-size: var(--text-12); color: var(--accent); }
.step p:last-child { font-size: var(--text-13); color: var(--text-secondary); }

/* -- entry pages -------------------------------------------------------- */

/* The signed-out pages stand on moving artwork: /assets/entry-bg.mp4, a
 * seven-and-a-half-second loop of the Kerbrus mesh, with
 * /assets/entry-bg-poster.webp as its first frame. Both are copies held in
 * this repo for the same reason the token block above is a copy rather than an
 * import: the CSP here is `img-src 'self'` and `media-src 'self'`, and a
 * portal that fetches artwork from kerbrus.com is the runtime dependency
 * ../CLAUDE.md rules out. It stops at the entry pages; a signed-in page is
 * somewhere the customer works, and artwork under a table of installs is
 * noise.
 *
 * # RE-ENCODING IT
 *
 * The master is ClaudeOS/generations/kerbrus-bg-loop-veo.mp4, which is the
 * ONLY copy of it: everything in this repo is derived from it and lossy, so a
 * re-encode that starts from entry-bg.mp4 rather than from the master is
 * compounding artifacts onto artifacts. If that file ever goes missing, this
 * recipe is not runnable and the loop is frozen at whatever is checked in.
 *
 * From that 1920x1080 master, at its native size - the presentation below
 * fits the loop to the viewport HEIGHT, so 1080 is what a tall window asks
 * for and downscaling first only throws that away:
 *
 *   ffmpeg -i master.mp4 -an -vf "scale=1920:1080:flags=lanczos"
 *     -c:v libx264 -profile:v high -level 4.2 -preset veryslow -crf 35
 *     -pix_fmt yuv420p -movflags +faststart
 *     -x264-params "aq-mode=3:aq-strength=1.1" entry-bg.mp4
 *
 *   ffmpeg -i entry-bg.mp4 -vf "select='eq(n\,0)'" -vsync 0
 *     -c:v libwebp -quality 82 entry-bg-poster.webp
 *
 * aq-mode=3 biases bits toward dark areas, which is nearly the whole frame,
 * and is worth more here than a lower CRF.
 *
 * THE POSTER MUST COME FROM THE ENCODED FILE, never from the master. It is
 * what the page shows until the first frame decodes, so any visible
 * difference between the two is a flicker at the handover on every load. A
 * correct pair measures about 42 dB PSNR against each other; this has
 * already been got wrong once by swapping the video without redoing the
 * still, which scored 34 dB and looked like the picture snapping into
 * focus.
 *
 * CRF 35 SOUNDS HIGH AND IS NOT, because of how this is presented. The
 * loop is never magnified more than about 1.33x (see the ultrawide block
 * below), and at that scale CRF 35 is indistinguishable from CRF 26 on this
 * content while being a third of the weight - 1.4 MB against 5.1 MB, on a
 * page whose entire remaining payload is 56 kB. An earlier pass shipped the
 * expensive encode to survive a 3.2x blow-up that the presentation fix had
 * already removed. If the presentation ever changes so the loop is
 * magnified harder again, this number has to be revisited with it - they
 * are one decision, not two.
 *
 * Brightness is the file's own. An earlier version encoded it at 0.72 to
 * protect the contrast floor; that is gone, and the floor is now held by
 * the capped scrim and the height fit instead, both of which cost nothing
 * and neither of which dulls the artwork. What that leaves is "Forgot it?"
 * at 4.35:1 on a 1440x900 window against a 4.5 floor - accent purple, 15px,
 * at the card's right edge, whose ceiling is 5.64:1 even on a perfectly
 * black card. It is the one measured failure on these pages and it is a
 * known, deliberate exception: the answer is to lighten that one link
 * rather than to dim the whole picture for it. On a window wide enough for
 * the ultrawide block it reads 4.77:1 and passes.
 *
 * The two layers are the Splash layout's, at the same values: the art, then a
 * scrim that is deepest at the centre - which is exactly where the card lands -
 * and closes again at the top and bottom edges so the chrome and the footer of
 * the page do not sit on bright pixels. design-system.md's contrast rule is
 * about this: the text has to clear 4.5:1 against the pixels actually behind
 * it, not against a nominal flat ground. The scrim earns its keep more here
 * than it did under a still, because under a still the worst case is a fixed
 * arrangement of pixels somebody can look at once and sign off. Under a loop
 * it is whatever the brightest 42 milliseconds put behind a given glyph.
 *
 * Fixed rather than scrolling with the page, because the card's
 * backdrop-filter below has to have something to blur - and fixed on the
 * element rather than `background-attachment: fixed` on the body, which is
 * unreliable on iOS and is not available to a <video> in any case. */

/* The video fills the viewport the way `cover` fills a background box, and
 * `object-position` applies to the poster on the same terms, so the still and
 * the moving frame are framed identically and the handover moves nothing.
 *
 * 45% rather than centre lifts the frame slightly: the loop's lower third is
 * the reflective floor, and centring it puts more empty floor on screen than
 * the mesh it reflects. The value is the one the still used before it.
 *
 * The black underneath is the page's own --void-900 rather than the browser's
 * default, so the frames before the poster arrives are the right colour rather
 * than a white flash on a dark page. */
.entry-art {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  pointer-events: none;
  object-fit: cover;
  object-position: center 45%;
  background: var(--void-900);
}

/* ULTRAWIDE: fit the loop to the HEIGHT and let the sides go to void.
 *
 * `cover` fills whichever axis needs the most, which on a 32:9 display is the
 * width - 5120 / 1920 is a 2.7x blow-up of a file that has no more detail to
 * give, and the picture reads as out of focus because it is. Fitting to the
 * height instead puts it at 1.33x, and the difference is not subtle: the beads
 * go back to being points instead of smears.
 *
 * It is not only sharpness. `cover` on a wide viewport also crops away the top
 * and bottom of the frame, and what is left is the middle band - which is
 * exactly where the mesh is brightest. That put bright artwork behind "New to
 * Kerbrus? Create an account", the one line on these pages with no card under
 * it, and dropped it to 2.48:1 against a 4.5 floor. Height-fit shows the whole
 * frame again, the darker floor returns to the area under the card, and that
 * line measures 4.66:1. Overscanning even to 150% puts it back to 3.08:1, so
 * 100% here is load-bearing and not a preference.
 *
 * The mask is what stops the video ending on a visible vertical edge. It is in
 * percentages of the ELEMENT, which is now exactly the video's own box, so it
 * lands in the same place at any size.
 *
 * 2/1 is the trigger because below it there is not enough void at the sides
 * for the fade to live in. At or under 16:9 none of this applies at all:
 * height-fit there is already wider than the viewport, so it crops precisely
 * as `cover` does today and the mask falls off-screen. */
@media (min-aspect-ratio: 2/1) {
  .entry-art {
    inset: auto;
    top: 50%;
    left: 50%;
    width: auto;
    height: 100%;
    transform: translate(-50%, -50%);
    -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 8%, #000 92%, transparent 100%);
            mask-image: linear-gradient(90deg, transparent 0%, #000 8%, #000 92%, transparent 100%);
  }
}

/* THE SCRIM IS SIZED TO THE CARD, NOT TO THE VIEWPORT.
 *
 * It used to be `ellipse at center` with no size, which is farthest-corner:
 * the radius is 70.7% of the box, so it GROWS WITH THE WINDOW. What it exists
 * to protect does not - the card is 27rem wide wherever it is drawn. On a
 * 5120x1440 display that mismatch dimmed four thousand pixels of artwork to
 * keep 432 pixels of text legible, and measured over the mid-height strip it
 * was holding the picture at 18-37% of its own brightness right across the
 * frame. The artwork read as washed out because it was.
 *
 * So the radius is capped at the geometry the design was tuned against, which
 * is the 1440x900 viewport every contrast figure in this file was measured on:
 * 70.7% of 1440 is 1018px, and 64rem is 1024px. Under that size nothing
 * changes - the default below IS farthest-corner, written out - and the cap
 * only engages on a window bigger than the one the tuning assumed. There is no
 * jump at the breakpoint because the two values meet there.
 *
 * Capped in a media query rather than `min(70.7%, 64rem)` because Chrome
 * rejects math functions in a radial-gradient's size position. The custom
 * properties are substituted before the gradient is parsed, so they are fine.
 *
 * The linear gradient is NOT capped. It closes the top and bottom edges so the
 * chrome and the page footer never sit on bright pixels, and those edges are
 * wherever the viewport says they are. */
body.entry::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  /* farthest-corner for a centred gradient, written out so it can be capped */
  --scrim-rx: 70.7%;
  --scrim-ry: 70.7%;
  background:
    radial-gradient(ellipse var(--scrim-rx) var(--scrim-ry) at center,
      color-mix(in srgb, var(--void-900) 78%, transparent) 0%,
      color-mix(in srgb, var(--void-900) 30%, transparent) 55%,
      transparent 80%),
    linear-gradient(180deg,
      color-mix(in srgb, var(--void-900) 60%, transparent) 0%,
      color-mix(in srgb, var(--void-900) 10%, transparent) 30%,
      color-mix(in srgb, var(--void-900) 10%, transparent) 65%,
      color-mix(in srgb, var(--void-900) 80%, transparent) 100%);
}
/* 91rem = 1456px and 57rem = 912px: the widths at which 70.7% overtakes the
   cap, so each axis holds its tuned radius from there on out. */
@media (min-width: 91rem) { body.entry::after { --scrim-rx: 64rem; } }
@media (min-height: 57rem) { body.entry::after { --scrim-ry: 40rem; } }

body.entry { display: flex; flex-direction: column; min-height: 100vh; }
body.entry main {
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-12);
  /* The vertical figure is half what it was. `justify-content: center` has
     already centred the card in whatever is left of the viewport, so this
     padding is not what positions it - it is a floor under how close the
     card may come to the chrome, and at --space-24 it was pushing the tallest
     card (sign-up) and its button below the fold of a 900px viewport for no
     gain. The horizontal figure is untouched: that one is a real gutter. */
  padding: var(--space-12) var(--space-8);
}

.entry-card {
  width: 100%;
  max-width: 27rem;
  /* One rung down from --space-20, and the gap one rung down from
     --space-10. Sign-up is four fields, three checkboxes and a button, and
     the padding and gaps were 232px of its 760 - so the thing that made it
     too tall was the rhythm rather than the content. */
  padding: var(--space-16);
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  background: var(--surface-card);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-frame);
  /* --surface-card is 62% opaque, which is what gives the card its depth on a
     flat ground and what would let a bright streak of the artwork read through
     a label behind it. Blurring the backdrop leaves the card sitting ON the
     artwork rather than cut out of it, and removes the detail that was the
     problem. The same 14px the chrome uses - one blur radius in this file. */
  backdrop-filter: blur(14px);
}
/* The card's masthead. A flex child of .entry-card, which stretches it the
   full content width, and its own align-items does the centring - so there is
   no align-self here and nothing to keep in step if the card's alignment
   changes. The tag steps up one rung with the mark; its tracking is in em, so
   that is the whole of what the larger size needs. */
.entry-mark { --wordmark-w: 160px; }
.entry-mark .wordmark-tag { font-size: var(--text-13); }

/* Centred to sit under the mark. The fields below stay left-aligned: centring
   a form is centring its labels away from the inputs they name. */
/* Tighter than the --space-8 every other form in the application uses, and
   scoped here rather than changed on `form` - the account and install pages
   are not short of room and have no reason to inherit a decision that sign-up
   needed. */
.entry-card form { gap: var(--space-7); }

/* The introduction above the sign-in card (views/auth.ts INTRO). The card's
   width and backing - surface, blur - so its text never sits on bare artwork,
   which is the problem .entry-alt below had to solve for one line. Slimmer
   padding, the quieter border and a smaller radius, so it reads as a note
   above the card rather than a second card. */
.entry-intro {
  width: 100%;
  max-width: 27rem;
  padding: var(--space-8) var(--space-12);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  background: var(--surface-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  backdrop-filter: blur(14px);
  font-size: var(--text-13);
  color: var(--text-secondary);
  text-align: center;
}
.entry-intro strong { color: var(--text-primary); font-weight: var(--weight-semibold); }
.entry-intro-links { display: flex; justify-content: center; gap: var(--space-8); }

.entry-head { display: flex; flex-direction: column; gap: var(--space-4); text-align: center; }
.entry-head h1 { font-size: var(--text-24); }
.entry-alt { font-size: var(--text-13); color: var(--text-secondary); text-align: center; }

/* THE ONE LINE ON THESE PAGES WITH NOTHING BEHIND IT.
 *
 * Sign in and sign up put this line OUTSIDE the card - "New to Kerbrus?
 * Create an account" - so unlike every other piece of text here it sits on
 * bare artwork with no backdrop-filter under it. The other two uses of
 * .entry-alt are inside the card and already covered, which is why this rule
 * is scoped to a direct child of main.
 *
 * It measured 2.39:1 against the 4.5:1 floor for a 1.75-second window of the
 * animation's loop - a cluster of bright beads drifting through the line box,
 * two of them landing on the ink of "account". Either side of that window it
 * reads 7.4:1, which is what makes the failure easy to miss and is exactly
 * the case design-system.md names in advance: "text that passes over the dark
 * ground and fails when a light packet drifts behind it".
 *
 * The card's own answer to this problem is a blur and a scrim, so this is
 * that answer at the scale of one line rather than a new mechanism. It goes
 * on a pseudo-element rather than the <p> because masking the element would
 * fade the glyphs along with the scrim; `z-index: 0` on the parent is what
 * keeps the -1 pseudo inside this element's stacking context instead of
 * dropping it behind the canvas, which also lives at -1.
 *
 * The mask is what stops it reading as a box: the scrim is solid under the
 * text and gone before its own edge, so there is nothing to see but slightly
 * quieter artwork. */
main > .entry-alt { position: relative; z-index: 0; }
main > .entry-alt::before {
  content: '';
  position: absolute;
  /* ON THE LADDER, and in tokens rather than em.
     This was `-0.75em -3em`, which at --text-13 computes to 9.75px and 39px -
     neither a rung, and the only off-ladder spatial number in the stylesheet.
     design-system.md is explicit: "Any margin, padding, or corner on this page
     must be a value from those ladders".
     The vertical figure is also three rungs larger than the 10px it replaces,
     and that is the point rather than a rounding: at 10px the mask had almost
     no room to feather top and bottom, and the blur left a measurable step
     where the flattened gradient inside met the unflattened one outside -
     about 3x the artwork's own row-to-row variation, in one row, 260px wide.
     Invisible today at dL* 0.57, and it scales with whatever the artwork does
     next. Giving the mask room to dissolve the seam is cheaper than
     remembering it later. */
  /* ON THE LADDER, and CLEAR OF THE CARD.
     Two constraints, and the second is the one that is easy to miss.
     design-system.md requires every margin off the ladder, so no `-0.75em`
     (9.75px at --text-13) - that was the only off-ladder spatial value in the
     file. And the top edge has to stop short of the card's bottom border,
     which sits 27px above this line: `backdrop-filter` blurs EVERYTHING
     behind the element, and a 1px UI hairline is far more fragile than the
     artwork it was aimed at. Taken to 32px the region swallowed that border
     and the card rendered as an open-bottomed box with two corner stubs -
     visible at 1:1, with no amplification, and a worse fault than the one
     being fixed, because it removes a shape the design intends rather than
     adding one.

     THE INVARIANT, because this will break again otherwise: the TOP inset
     must stay below `body.entry main`'s `gap`, which is --space-12 (24px).
     That gap is the entire distance between the card's bottom border and
     this line, so a top inset of 32px did not merely come close to the
     border, it reached eight pixels past it. --space-10 (20px) leaves four.
     Anyone who tightens that gap, or loosens this inset, has to re-check the
     card's bottom edge - a diff of the border row with and without the
     protection shows it immediately.
     The vertical figure still matters beyond conformance: at 10px the mask
     had no room to feather, so the blur left a seam where the flattened
     gradient met the unflattened one, and a bead crossing near the cap line
     was suppressed in a single frame rather than easing. 20px doubles that
     room without reaching the border. */
  inset: calc(var(--space-10) * -1) calc(var(--space-20) * -1);
  z-index: -1;
  border-radius: var(--radius-pill);
  /* NO TINT, deliberately. The first version darkened the backdrop 62% as
     well as blurring it, and at the moment the beads crossed - which is the
     only moment any of this matters - that darkening read as a visible pill
     behind the text. A scrim you can see is a worse answer than the problem.
     Measured through the failure window, the blur alone reaches 7.72:1 and
     blur-plus-tint 7.93:1: the blur was doing the work and the darkening was
     paying for a box. A 14px blur spreads a two-pixel bead over fifty times
     its area, and peak luminance is what the contrast rule is about. */
  backdrop-filter: blur(14px);
  -webkit-mask-image: radial-gradient(ellipse 100% 100% at 50% 50%,
    #000 0%, #000 46%, transparent 92%);
          mask-image: radial-gradient(ellipse 100% 100% at 50% 50%,
    #000 0%, #000 46%, transparent 92%);
}

/* -- narrow ------------------------------------------------------------- */

@media (max-width: 60rem) {
  .split, .tiles, .steps-grid { grid-template-columns: minmax(0, 1fr); }
  .head { flex-direction: column; align-items: flex-start; }
  .nav { gap: var(--space-8); font-size: var(--text-13); }
}

@media (max-width: 40rem) {
  .chrome-inner { gap: var(--space-6); }
  /* The nav scrolls rather than disappearing. Hiding it would leave the account
     chip as the only navigation on a phone, and the chip holds two of the five
     destinations - so three pages would be reachable only by typing a URL. */
  .nav { gap: var(--space-8); overflow-x: auto; scrollbar-width: none; }
  .nav::-webkit-scrollbar { display: none; }
  .nav a { font-size: var(--text-13); white-space: nowrap; }
  /* The entry pages take the OPPOSITE decision to the line above, and the
     reason the line above gives is why. Hiding the signed-in nav would strand
     three pages behind a URL bar; hiding this one strands nothing, because
     all four of its links go to kerbrus.com and so does the wordmark sitting
     next to them - which is where the site's own mobile menu lives. Scrolled,
     it reads as two clipped words wedged between the mark and the button. */
  body.entry .nav { display: none; }
  .column { padding: var(--space-12) var(--space-8) var(--space-24); }
  .card.empty { padding: var(--space-16) var(--space-10); }
  .keyline { flex-direction: column; }
  table, thead, tbody, tr, th, td { display: block; }
  thead { display: none; }
  tr { padding: var(--space-8) 0; border-bottom: 1px solid var(--border-subtle); }
  td { border: none; padding: var(--space-2) 0; }

  /* ...except the requirements table, which stays a table. The collapse above
     drops the header row, which the installs list can afford - a licence state
     and a version read as themselves - and this cannot: `12 GB` under nothing
     is a number with no noun. Four short cells fit at this width. */
  .reqs, .reqs tbody { display: table; width: 100%; }
  .reqs thead { display: table-header-group; }
  .reqs tr { display: table-row; padding: 0; border-bottom: none; }
  .reqs th, .reqs td {
    display: table-cell;
    padding: var(--space-5) var(--space-5) var(--space-5) 0;
    border-bottom: 1px solid var(--border-subtle);
  }
  .reqs th { padding-top: 0; border-bottom-color: var(--border-strong); }
  .reqs tbody tr:last-child td { border-bottom: none; }
  .num { text-align: left; }
}

/* -- checkout ------------------------------------------------------------
 *
 * P-0026 § The surfaces. The overlay is Paddle's own window, so almost
 * nothing here styles it - what is left is the card that offers the plans and
 * the line that explains a refusal.
 *
 * The inline build had far more: a summary in our tokens, a framed payment
 * panel, a sticky column. It is gone with the frame it decorated. Paddle's
 * inline mode paints the container behind its fields white whatever `theme`
 * says, and a dark window we do not control beat a white panel we did.
 */

.buy-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  margin-top: var(--space-12);
}

/* `hidden` is a UA rule of `display: none`, and ANY author `display` beats it.
   Nothing toggles this today, but the rule costs one line and the omission
   cost an afternoon: it shipped once with the plan buttons still on screen
   underneath an open checkout, which is how somebody starts a second one. */
.buy-row[hidden] { display: none; }

