/* Night Ferry console.
   Palette and type from docs/console-design-brief.md: the homepage's ink and
   water-blue, a serif for language and a mono for every figure, so a number is
   always recognisable as a number. No cards and no boxes — hairline rules do
   the separating, the way the printed rate card does.

   Fonts are named, not fetched. The homepage does the same: a signed-in page
   should not depend on a third-party host, and the CSP here is
   `default-src 'none'`.

   ---- the two palettes -------------------------------------------------

   Dark is the default and light is the deviation, the same way round as
   site/index.html since 2026-09-18: a page with no attribute on it paints
   dark with nothing resolved. `[data-theme="light"]` is written by the server
   from the tenant's own cookie; `[data-theme="dark"]` is written too when
   that is what they chose, and selects nothing the resting sheet does not
   already paint. The machine's own colour-scheme preference is not consulted
   -- nothing stored means the night crossing -- which is also what lets the
   toggle be named for the palette it takes you to without the server ever
   guessing which one is on screen.

   Only the palette moves. Every colour below is a token, including the
   ad-hoc washes — `--ink-rgb` is the ink's channel triple on its own so a
   one-off `rgba(var(--ink-rgb), 0.56)` follows the theme without becoming a
   named rung on the ladder it does not belong to. */
:root {
  --ink-rgb: 245, 243, 239;
  --deep: #07090c;
  --bg: #101419;
  --surface: #161b22;
  --ink: #f5f3ef;
  --accent: #437c99;
  /* Not a contrast fix: #437c99 measures 4.03:1 on the sheet and a rule needs
     3:1. A 1px mid-blue hairline under text is simply hard to FIND. 2px at
     6.12:1 makes the current screen locatable at a glance. */
  --accent-lit: #5f9cba;
  --accent-wash: rgba(67, 124, 153, 0.05);
  --warn: #e8b26f;
  --flag: #ff9442;
  --flag-edge: rgba(255, 148, 66, 0.34);
  --flag-wash: rgba(255, 148, 66, 0.05);
  --flag-ink: rgba(255, 168, 98, 0.95);
  /* water.js's own fallback gradient at this page's horizon, so the gate
     is never a blank frame with no script and no WebGL. */
  --scene-fallback: linear-gradient(#080b0f 0%, #191f27 42%, #0d1116 43%, #05070a 100%);
  /* The gate mounts both shaders and only the top one's opacity moves — the
     same arrangement site/index.html has, and the reason the palette switch
     can cross-fade the water instead of reloading into the other one.
     Unmounting the hidden scene would restart its crossing from the left edge
     on every switch, so it stays mounted and stays at the same phase. Dawn
     is the top layer and rests off. */
  --dawn-on: 0;

  /* The gate's card, and the hairline inside a field group or between two
     rows of a list. --rule-soft is quieter than --rule on purpose: --rule
     separates one section from the next, this one separates two fields that
     belong to the same control, and at --rule's weight a joined field group
     reads as four boxes again. */
  --panel: rgba(9, 12, 16, 0.66);
  --panel-edge: rgba(245, 243, 239, 0.13);
  --panel-shade: 0 30px 80px -24px rgba(0, 0, 0, 0.78);
  --rule-soft: rgba(245, 243, 239, 0.15);

  /* --ink-faint was 2.93:1 and was carrying real content — two dark payment
     rows, the inactive rate-card tab, the currency switch. At 0.48 it is
     4.61:1 and is reserved for things that are genuinely off. */
  --ink-strong: rgba(var(--ink-rgb), 0.95);
  --ink-mid: rgba(var(--ink-rgb), 0.82);
  --ink-dim: rgba(var(--ink-rgb), 0.66);
  --ink-faint: rgba(var(--ink-rgb), 0.48);
  --line: rgba(var(--ink-rgb), 0.15);
  --rule: rgba(var(--ink-rgb), 0.26);
  --ink-quiet: rgba(var(--ink-rgb), 0.55);
  --ink-sep: rgba(var(--ink-rgb), 0.35);
  --bar: rgba(var(--ink-rgb), 0.56);
  --bar-today: rgba(var(--ink-rgb), 0.80);
  --tie: rgba(var(--ink-rgb), 0.08);
  /* Warmth thrown by a hovered control: a tight inner rim, then three widening
     falloffs, so the light reads as coming off the scene rather than as a
     border that changed colour. The homepage has carried this since the dark
     page shipped. The design canvas puts it on the console's controls too, and
     the six layers below are the canvas's own -- `style-hover` is an authoring
     attribute the runtime does nothing with, so it only exists here. */
  --glow-control:
    inset 0 0 7px -2px rgba(67, 124, 153, 0.34),
    inset 0 0 22px -8px rgba(67, 124, 153, 0.20),
    0 0 0 1px rgba(67, 124, 153, 0.18),
    0 0 12px -2px rgba(67, 124, 153, 0.30),
    0 0 34px -4px rgba(67, 124, 153, 0.20),
    0 0 82px 6px rgba(67, 124, 153, 0.11);

  /* ---- the ferry's lamp, on the keys ledger ---------------------------

     Opening a key's dossier is the one thing on the console that lights up,
     and --flag is the colour it lights up in: the same warm the homepage
     calls --color-accent-warm, already the console's mark for "this figure
     needs looking at". --flag-lit is its hovered edge; --glow-row is the
     light the open row and its panel throw; --glow-warm is the tighter
     version a hovered control throws, --glow-control's warm twin.

     --glow-row-off is the same six layers at zero alpha. box-shadow
     interpolates layer for layer and cannot interpolate between six layers
     and `none`, so the closed panel carries the shape of the glow with none
     of the light in it, and the transition has somewhere to start.

     Light states its own numbers. #ff9442 on paper is a highlighter, and a
     glow built for a dark sheet reads as a smudge on a pale one. */
  --flag-lit: #ff9e4d;
  --glow-row:
    inset 14px 0 26px -20px rgba(255, 158, 77, 0.55),
    inset 0 0 30px -22px rgba(255, 158, 77, 0.30),
    0 0 0 1px rgba(255, 158, 77, 0.10),
    0 0 20px -4px rgba(255, 148, 66, 0.24),
    0 0 62px 2px rgba(255, 148, 66, 0.13),
    0 0 140px 22px rgba(255, 148, 66, 0.07);
  --glow-row-off:
    inset 14px 0 26px -20px rgba(255, 158, 77, 0),
    inset 0 0 30px -22px rgba(255, 158, 77, 0),
    0 0 0 1px rgba(255, 158, 77, 0),
    0 0 20px -4px rgba(255, 148, 66, 0),
    0 0 62px 2px rgba(255, 148, 66, 0),
    0 0 140px 22px rgba(255, 148, 66, 0);
  --glow-warm:
    inset 0 0 7px -2px rgba(255, 158, 77, 0.50),
    inset 0 0 22px -8px rgba(255, 158, 77, 0.26),
    0 0 0 1px rgba(255, 158, 77, 0.18),
    0 0 12px -2px rgba(255, 158, 77, 0.42),
    0 0 34px -4px rgba(255, 148, 66, 0.28),
    0 0 82px 6px rgba(255, 148, 66, 0.16);

  --serif: "Source Serif 4", "Noto Serif SC", Georgia, "Times New Roman", serif;
  /* Latin: Source Serif 4. CJK: whichever gothic the reader has. Ordinary
     中西混排, and the whole fix — a Song/Ming face draws its horizontals as
     hairlines, which at interface sizes on the dark sheet land under a pixel
     and get antialiased away. Above 28px they survive, which is why the
     display sizes below keep --serif. */
  --sans: "Source Serif 4", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC",
          "Microsoft YaHei", Georgia, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;

  --tag: 500 12px/1 var(--mono);
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --rail-w: 264px;

  /* The gate frame's own inset, named because the sign-in form's placement is
     defined as a fraction of the window and has to subtract it. Two uses, one
     definition: with the value written out twice, editing the padding would
     move the form and nothing would say so. */
  --gate-pad-x: clamp(22px, 3.2vw, 52px);
  --gate-pad-y: clamp(38px, 7.5vh, 92px);
}

/* Colour only. A type or layout token defined here and not above would exist
   in one theme and not the other, and nothing would say so.

   Written once. Until 2026-09-18 the dark palette was written twice -- once
   behind `prefers-color-scheme` for readers who never chose and once for the
   attribute -- because the resting sheet was light and the machine was asked.
   The resting sheet is dark now and the machine is not asked, so there is one
   question left: has the tenant chosen light? `_theme_attr` writes `light`
   for that, and `dark` for the other choice, which changes nothing here. */
:root[data-theme="light"] {
  --ink-rgb: 16, 20, 25;
  --deep: #ebe8e1;   /* outside the sheet */
  --bg: #f5f3ef;     /* the sheet itself */
  --surface: #efece6;
  --ink: #101419;
  --accent: #346177; /* the water */
  --accent-lit: #2a5265;
  --accent-wash: rgba(52, 97, 119, 0.06);
  --warn: #7d5119;
  /* Hotter than --warn, and used nowhere else. --warn marks a note worth
     reading; --flag marks the one figure on the console that is actively
     misleading if read the obvious way. */
  --flag: #c8620f;
  --flag-edge: rgba(200, 98, 15, 0.34);
  --flag-wash: rgba(200, 98, 15, 0.07);
  --flag-ink: #9a4708;
  /* dawn-water.js's own fallback, for the same reason as the dark one above. */
  --scene-fallback: linear-gradient(#8fa4bb 0%, #e2ddd6 42%, #d0cbc5 43%, #9ea8b3 100%);

  /* Dawn on, over the night scene that stays mounted underneath. */
  --dawn-on: 1;

  /* The gate's card, and the hairline inside a field group or between two
     rows of a list. --rule-soft is quieter than --rule on purpose: --rule
     separates one section from the next, this one separates two fields that
     belong to the same control, and at --rule's weight a joined field group
     reads as four boxes again. */
  --panel: rgba(245, 243, 239, 0.74);
  --panel-edge: rgba(16, 20, 25, 0.16);
  --panel-shade: 0 24px 60px -26px rgba(16, 20, 25, 0.30);
  --rule-soft: rgba(16, 20, 25, 0.15);

  /* The ink ladder keeps five rungs. The alphas are not shared with the dark
     palette and must not be: dark ink on paper reaches the same contrast at a
     different number than paper on ink does, so each palette states its own. */
  --ink-strong: rgba(var(--ink-rgb), 0.94);
  --ink-mid: rgba(var(--ink-rgb), 0.80);
  --ink-dim: rgba(var(--ink-rgb), 0.70);
  --ink-faint: rgba(var(--ink-rgb), 0.60);
  --line: rgba(var(--ink-rgb), 0.19);
  --rule: rgba(var(--ink-rgb), 0.32);
  /* Washes the design canvas states separately for each palette, because dark
     ink on paper and paper on ink do not read alike at the same alpha. The
     numbers are the artboards' own, not a guess: light from "Nightferry
     Console Light.dc.html", dark from "Nightferry Console.dc.html".

     --ink-quiet is the part of a figure that is not the figure -- the `claude-`
     that every model id starts with, the leading zeros of a cost, a unit beside
     a number. --ink-sep is the divider between the two currencies on a rate
     card, which carries no word and so is held to matching the other palette's
     weight rather than to the text threshold. --bar and --tie are graphics. */
  --ink-quiet: rgba(var(--ink-rgb), 0.62);
  --ink-sep: rgba(var(--ink-rgb), 0.45);
  /* The canvas says 0.44. That measures 2.85:1 against this sheet, and a bar
     in a chart is a graphical object the reader has to make out -- 3:1. 0.46 is
     the first value that clears it. The one deviation from the artboards'
     numbers in this block, and it is 0.02. */
  --bar: rgba(var(--ink-rgb), 0.46);
  --bar-today: rgba(var(--ink-rgb), 0.72);
  --tie: rgba(var(--ink-rgb), 0.11);
  /* Warmth thrown by a hovered control: a tight inner rim, then three widening
     falloffs, so the light reads as coming off the scene rather than as a
     border that changed colour. The homepage has carried this since the dark
     page shipped. The design canvas puts it on the console's controls too, and
     the six layers below are the canvas's own -- `style-hover` is an authoring
     attribute the runtime does nothing with, so it only exists here. */
  --glow-control:
    inset 0 0 7px -2px rgba(52, 97, 119, 0.34),
    inset 0 0 22px -8px rgba(52, 97, 119, 0.20),
    0 0 0 1px rgba(52, 97, 119, 0.18),
    0 0 12px -2px rgba(52, 97, 119, 0.30),
    0 0 34px -4px rgba(52, 97, 119, 0.20),
    0 0 82px 6px rgba(52, 97, 119, 0.11);

  /* The warm light, restated for paper. Hue from this palette's own --flag
     (#c8620f) with --flag-lit deepened rather than lifted -- on a pale sheet
     the thing that reads as "more" is more ink, not more brightness -- and
     the falloffs carried at the dark sheet's alphas, which on paper land as
     a warm tint rather than as light. See the note in the dark block. */
  --flag-lit: #a8510c;
  --glow-row:
    inset 14px 0 26px -20px rgba(168, 81, 12, 0.55),
    inset 0 0 30px -22px rgba(168, 81, 12, 0.30),
    0 0 0 1px rgba(200, 98, 15, 0.10),
    0 0 20px -4px rgba(200, 98, 15, 0.24),
    0 0 62px 2px rgba(200, 98, 15, 0.13),
    0 0 140px 22px rgba(200, 98, 15, 0.07);
  --glow-row-off:
    inset 14px 0 26px -20px rgba(168, 81, 12, 0),
    inset 0 0 30px -22px rgba(168, 81, 12, 0),
    0 0 0 1px rgba(200, 98, 15, 0),
    0 0 20px -4px rgba(200, 98, 15, 0),
    0 0 62px 2px rgba(200, 98, 15, 0),
    0 0 140px 22px rgba(200, 98, 15, 0);
  --glow-warm:
    inset 0 0 7px -2px rgba(168, 81, 12, 0.50),
    inset 0 0 22px -8px rgba(168, 81, 12, 0.26),
    0 0 0 1px rgba(200, 98, 15, 0.18),
    0 0 12px -2px rgba(200, 98, 15, 0.42),
    0 0 34px -4px rgba(200, 98, 15, 0.28),
    0 0 82px 6px rgba(200, 98, 15, 0.16);
}


* { box-sizing: border-box; }

html { background: var(--deep); }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink-strong);
  font: 400 16px/1.6 var(--sans);
  letter-spacing: 0.004em;
  /* No -webkit-font-smoothing: antialiased. It disables stem darkening, which
     thins light-on-dark text specifically — it was making the problem it
     looked like a fix for. */
}

/* 0.09em, not 0.14em. The tracking was set for a Latin small-caps label and is
   applied to CJK, which is already full-width — 余额 at 0.14em is two
   characters holding hands at arm's length, and the airiness reads as
   faintness. */
.tag, dt, th {
  font: var(--tag);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin: 0;
}

a { color: var(--ink-dim); text-decoration: none; }
a:hover {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-underline-offset: 6px;
}
:focus-visible { outline: 2px solid var(--accent-lit); outline-offset: 3px; }

/* ---- serif, where it still reads ---------------------------------------

   Display sizes only. At 40px a Song face is the brand and is perfectly
   legible; at 16px it is the bug. One rule so the threshold is stated once
   rather than remembered per selector. */
.sheet h1,
.gate .sheet h1,
.gate .gate-form h1,
.gate .handoff-title,
.onetime h1,
.grouphead h2,
.docs .name { font-family: var(--serif); }

/* ---- shell ------------------------------------------------------------ */

.app { display: flex; min-height: 100vh; }

/* The rail stands at viewport height and stays there while the sheet scrolls,
   so the address, the switches and Sign out hold the bottom-left corner of the
   screen. `margin-top: auto` on the foot only reaches the bottom of the rail,
   and the rail was as tall as the sheet — on a long screen that put the way out
   below the fold. `overflow-y: auto` is the escape hatch for a viewport too
   short for the nav; the background is opaque so nothing shows through. */
.rail {
  flex: none;
  width: var(--rail-w);
  padding: 52px 36px 40px;
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  align-self: flex-start;
  height: 100dvh;
  overflow-y: auto;
  background: var(--bg);
}
/* 168px, matching the site's lockup — tests/test_lockup.py holds them equal. */
.brand { display: block; width: 168px; color: var(--ink); opacity: 0.84; }
.brand:hover { opacity: 1; text-decoration: none; }
.mark { display: block; width: 100%; height: auto; font-family: var(--serif); }

.rail nav { margin-top: 56px; display: flex; flex-direction: column; gap: 34px; }
.group ul { margin: 14px 0 0; padding: 0; list-style: none; display: grid; gap: 14px; }
.group li { font-size: 16.5px; }
/* Weight, not just brightness. On a dark sheet a brighter label reads as glow;
   the current screen has to read as heavier. The rule under it is --accent-lit
   at 2px — see the token: #437c99 clears 3:1, it was just hard to find. */
.group strong {
  font-weight: 600;
  color: var(--ink-strong);
  text-decoration: underline;
  text-decoration-color: var(--accent-lit);
  text-decoration-thickness: 2px;
  text-underline-offset: 7px;
}

.rail .foot { margin-top: auto; padding-top: 40px; display: grid; gap: 16px; }
.who {
  margin: 0;
  font: 400 12.5px/1.4 var(--mono);
  letter-spacing: 0.04em;
  color: var(--ink-dim);
  word-break: break-all;
}

.sheet {
  flex: 1;
  min-width: 0;
  padding: 52px 64px 60px;
  display: flex;
  flex-direction: column;
}
.sheet-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 32px;
  flex-wrap: wrap;
}
.sheet h1 {
  margin: 0;
  font: 400 40px/1.22 var(--serif);
  letter-spacing: 0.004em;
  color: var(--ink-strong);
}

/* ---- overview --------------------------------------------------------- */

.headline { margin-top: 76px; display: flex; align-items: flex-end; gap: 56px; flex-wrap: wrap; }

.balance { display: grid; gap: 16px; }
.balance .amount {
  margin: 0;
  font: 400 clamp(40px, 2rem + 3vw, 64px) / 1 var(--mono);
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-strong);
}

.runway { display: grid; gap: 10px; padding-bottom: 6px; max-width: 280px; }
.runway .value {
  margin: 0;
  font: 400 26px/1 var(--mono);
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-mid);
}
.runway .hint { margin: 0; font: 400 14px/1.5 var(--sans); color: var(--ink-dim); }

.stats {
  margin: 64px 0 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border-top: 1px solid var(--rule);
}
.stats > div { padding: 22px 24px; display: grid; gap: 12px; border-left: 1px solid var(--rule); }
.stats > div:first-child { padding-left: 0; border-left: 0; }
.stats > div:last-child { padding-right: 0; }
.stats dd {
  margin: 0;
  font: 400 22px/1 var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink-mid);
}

.history { margin-top: 72px; display: grid; gap: 18px; border-bottom: 1px solid var(--rule); }
.history header { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
.history .span {
  margin: 0;
  font: 400 12px/1 var(--mono);
  letter-spacing: 0.05em;
  color: var(--ink-dim);
}
.history svg { display: block; width: 100%; height: 56px; margin-bottom: 18px; }
/* A bar is a graphic and needs 3:1. 42% / 62% against the raised ladder left
   them the quietest thing on the sheet. */
.bar { fill: var(--bar); }
.bar.today { fill: var(--bar-today); }

.footnote {
  margin: 32px 0 0;
  font: 400 13px/1.7 var(--mono);
  letter-spacing: 0.04em;
  color: var(--ink-dim);
}
/* The last thing on the sheet sits on the bottom edge. On the logs page the
   note belongs under the table it qualifies. */
.sheet > .footnote:last-child { margin-top: auto; padding-top: 48px; }

/* ---- tables, forms, one-off states ------------------------------------ */

.scroll { margin: 40px 0 0; overflow-x: auto; }
table { width: 100%; min-width: 40rem; border-collapse: collapse; font-size: 14px; }
th, td {
  text-align: left;
  padding: 14px 20px 14px 0;
  border-bottom: 1px solid var(--line);
}
th { border-bottom-color: var(--rule); }
td { color: var(--ink-mid); }
td:not(:first-child) {
  font-family: var(--mono);
  font-size: 14.5px;
  font-variant-numeric: tabular-nums;
}

.pager { margin: 24px 0 0; font-size: 14px; }
.error {
  margin: 24px 0 0;
  padding-left: 14px;
  border-left: 1px solid var(--warn);
  font-size: 16.5px;
  font-weight: 500;
  color: var(--warn);
}

form { margin: 40px 0 0; }
.rail form, td form { margin: 0; }
label { display: block; max-width: 22rem; margin-bottom: 20px; font-size: 14px; color: var(--ink-dim); }
input[type="email"], input[type="password"] {
  display: block;
  width: 100%;
  margin-top: 8px;
  padding: 10px 12px;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 2px;
  color: var(--ink-strong);
  font: 400 15.5px/1.4 var(--mono);
}
input:focus { outline: none; border-color: var(--accent); }

button {
  box-shadow: none;
  transition: border-color 260ms var(--ease-out-expo), color 260ms ease,
              box-shadow 380ms var(--ease-out-expo);
  appearance: none;
  padding: 10px 18px;
  background: transparent;
  border: 1px solid rgba(var(--ink-rgb), 0.28);
  border-radius: 2px;
  font: 400 12.5px/1 var(--mono);
  letter-spacing: 0.06em;
  color: var(--ink-mid);
  cursor: pointer;
  transition: border-color 220ms ease, color 220ms ease;
}
button:hover {
  border-color: var(--accent);
  color: var(--ink);
  box-shadow: var(--glow-control);
}
.signout button { justify-self: start; padding: 8px 14px; }
td button { padding: 6px 12px; }

/* The two switches — the palette and the language — on one row.

   They are not the same shape, and the asymmetry is deliberate. The palette is
   an icon because both palettes carry the same words: a reader who mistakes it
   loses nothing and can see what happened. The language stays a word, because
   it is the control a reader reaches for precisely when they cannot read the
   page, and drawing it would replace the only readable thing on the screen with
   a glyph labelled in the language they are trying to leave.

   The rail's own rule still holds: sign-out is the wide bordered thing you can
   do, and there is still only one of those in the foot. */
.switches { display: flex; gap: 14px; align-items: center; }

/* The palette switch. A 36px square, sized off the icon rather than the text
   it does not have.

   It is a <button> because it submits a POST — see `_theme_switch` — so the
   default button chrome has to come back off by hand, the same way the language
   switch below undoes it. */
.theme button {
  appearance: none;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 2px;
  color: var(--ink-mid);
  cursor: pointer;
  transition: border-color 260ms var(--ease-out-expo), color 260ms ease;
}
.theme button:hover {
  border-color: var(--accent);
  color: var(--ink);
  box-shadow: var(--glow-control);
}
.theme svg { display: block; width: 20px; height: 20px; }

/* The disc sets behind a waterline that never moves. Then the stars come up,
   after it has gone — which is why they are delayed on the way into dark and
   are not on the way back out.

   Dark is the resting state and carries no attribute, so it is what these
   rules describe: disc down, stars up. `[data-theme="light"]` is the
   deviation, and lifts the disc as the stars go. */
.theme .disc { transform: translateY(12px); transition: transform 620ms var(--ease-out-expo); }
.theme .stars { opacity: 1; transition: opacity 420ms ease 220ms; }
[data-theme="light"] .theme .disc { transform: translateY(0); }
[data-theme="light"] .theme .stars { opacity: 0; transition: opacity 240ms ease; }

@media (prefers-reduced-motion: reduce) {
  .theme .disc, .theme .stars { transition: none; }
}

/* ---- the palette turning ----------------------------------------------

   Those two transitions above only became reachable on 2026-09-06, when the
   switch moved into the page. While it was a POST and a redirect they could
   never fire: a CSS transition needs a start state in the SAME document, and
   the new page rendered the disc already at its destination.

   The sheet itself still needs help. Custom properties do not transition, so
   `--ink` changing under everything repaints in one frame however long the
   disc takes. This eases the colours with it.

   `*` for the length of the change and no longer. Left on permanently it
   would put 320ms on every hover in the console, so the switch scripts add the
   class, wait out the longest thing it gates, and take it off. `!important`
   because it has to beat the shorter transitions components define for their
   own hovers, and only for that moment.

   `transform` is deliberately not in the list: the disc has its own 620ms and
   should keep it rather than being flattened to the colour timing. Which is
   what `:not()` is for, and the reason it had to be added.

   `transition` is a SHORTHAND, and `!important` on a shorthand replaces the
   whole property. So a rule written to add colour easing was silently deleting
   every other transition on every element for the length of the turn — the
   disc's 620ms set, the stars' 420ms rise and their 220ms delay included. The
   sentence above was true about the list and false about the effect: leaving
   `transform` out did not leave the disc alone, it took the disc's own
   transition away and gave it nothing back. Measured with `getAnimations()`
   during a turn: the disc reported one animation, `color`, and the transform
   it declares three rules above was simply not running.

   So the elements that state their own motion are not asked to take this one.
   They lose the colour easing with it and do not need it: they are
   `fill: currentColor` inside a control whose `color` is still eased here, and
   an inherited value that is animating carries them along. */
.palette-turning *:not(.disc, .stars, .dawn),
.palette-turning *::before,
.palette-turning *::after {
  transition:
    background-color 320ms ease,
    border-color 320ms ease,
    color 320ms ease,
    fill 320ms ease !important;
}

/* The same selector, character for character, and it has to stay that way.
   Both rules are `!important`, so the one that wins is the more specific one —
   and `.palette-turning *:not(.disc, .stars, .dawn)` is more specific than
   `.palette-turning *`. Cancelling a rule with a weaker copy of its own
   selector does not cancel it; it leaves the animation running for exactly
   the readers who asked for none. */
@media (prefers-reduced-motion: reduce) {
  .palette-turning *:not(.disc, .stars, .dawn),
  .palette-turning *::before,
  .palette-turning *::after { transition: none !important; }
}

/* The language switch is a preference, not an action, and the rail's foot items
   are different weights on purpose: the address says who you are, sign-out is
   the bordered thing you can do, and this sits between them reading as a link
   rather than a second button. Two bordered buttons stacked in a 264px rail
   would both look like the way out.

   It is a <button> because it submits a POST — a GET would let any page on the
   internet flip a tenant's console with an <img> tag — so the border, padding
   and mono face have to come back off by hand. Type and hover match `a` above,
   which is what makes it read as one. */
.lang button {
  justify-self: start;
  padding: 0;
  border: 0;
  font: 400 14px/1 var(--sans);
  letter-spacing: 0;
  color: var(--ink-dim);
}
.lang button:hover {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-underline-offset: 6px;
}

/* ---- 文档 --------------------------------------------------------------

   Five rows, no cards. The rule between entries is the same hairline the rate
   card uses, so a list of links reads as part of the same document rather than
   as a menu bolted on. Each row leaves the console, which the lede says once
   and the arrow repeats per row — the underline on hover is the rail's, so a
   row behaves like every other link here. */

.lede {
  margin: 14px 0 0;
  max-width: 520px;
  font-size: 16.5px;
  line-height: 1.6;
  letter-spacing: 0;
  color: var(--ink-dim);
}
.lede code { font: 400 14px/1 var(--mono); letter-spacing: 0.055em; }

.docs {
  margin-top: 68px;
  max-width: 820px;
  border-top: 1px solid var(--rule);
}
.docs .entry {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 32px;
  padding: 26px 2px;
  border-bottom: 1px solid var(--line);
  color: inherit;
}
.docs .entry:hover { text-decoration: none; }
.docs .what { display: grid; gap: 8px; }
.docs .name {
  font: 400 23px/1.2 var(--serif);
  letter-spacing: 0.004em;
  color: var(--ink-strong);
}
.docs .sub { font: 400 15.5px/1.55 var(--sans); letter-spacing: 0; color: rgba(var(--ink-rgb), 0.70); }
.docs .out {
  flex: none;
  font: 400 13px/1 var(--mono);
  letter-spacing: 0.05em;
  color: var(--ink-mid);
}
/* Marks the row as a way out of the console, next to the name rather than
   after the title, where it would read as part of it.

   On the arrow's own element, not on the name beside it: in English there is
   no second name to print — the title is already English — and the row still
   has to say it leaves the console. */
.docs .out::after { content: " ↗"; }

.docs .entry:hover .name, .docs .entry:focus-visible .name {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 7px;
}
.docs .entry:hover .out { color: var(--ink-mid); }

@media (max-width: 900px) {
  .docs { margin-top: 40px; }
  .docs .entry { gap: 16px; padding: 22px 2px; }
  .docs .name { font-size: 19px; }
  .docs .sub { font-size: 14px; }
}

/* ---- 密钥 --------------------------------------------------------------

   An account ledger read per key. The row says what a key is and what it has
   been doing; the dossier that opens beneath it says what it is charged and
   what it can reach. Same figures in both, at two sizes — the row is scanned
   and the panel is read, and a single size is a compromise between them.

   What opens a dossier is the URL. `/keys#web-prod` and a `:target` rule, with
   both the opening and the closing anchor in the markup and CSS showing one of
   them: with no script there is nowhere but the address bar to keep a decision.
   static/keys.js adds the row-wide click target, the measured height that can
   interpolate, and nothing else. */

.keystrip { margin-top: 52px; grid-template-columns: repeat(3, 1fr); }
.keystrip .unit { font-size: 14px; color: rgba(var(--ink-rgb), 0.62); }

.keys { margin-top: 56px; }
.keys > table { min-width: 68rem; }
.keys > table > thead > tr > th { white-space: nowrap; }
.keys > table > thead > tr > th.c-name { padding-left: 18px; }
.keys > table > thead > tr > th.c-num { text-align: right; }
/* Fixed, because both are drawings: a sparkline and a limit bar have no
   content to size themselves from and would collapse to their captions. */
.keys > table > thead > tr > th.c-spark { width: 190px; }
.keys > table > thead > tr > th.c-cap { width: 260px; }
.keys > table > thead > tr > th.c-act { width: 210px; padding-right: 0; text-align: right; }

.keys > table > tbody > tr > td {
  padding: 20px 20px 20px 0;
  vertical-align: top;
  /* The canvas leaves the cells at the sheet's own ink and lets every text
     node inside state its own rung. Two cells there ask for a tighter leading
     and say so; the rest keep 1.6, which is what the figures are set on. */
  color: var(--ink-strong);
}
/* Four cells whose every child states its own face and size, so the cell
   itself carries the sheet's ordinary type rather than the table's mono. The
   canvas leaves them inheriting for the same reason. */
.keys td.used, .keys td.spark, .keys td.cap, .keys td.act {
  font: 400 14px/1.6 var(--sans);
  font-variant-numeric: normal;
}
.keys td.who, .keys td.used { line-height: 1.3; }
.keys td.act { padding-right: 0; }

/* The mark the open row carries, on the cell rather than the row: an inset
   shadow on a <tr> under `border-collapse: collapse` is not reliably painted,
   and this one has to be exactly 2px against the alias. */
.keys td.who {
  /* The rail's address line is also `.who`, and its rule is unscoped: without
     these three the alias cell inherits 12.5px mono at --ink-dim. */
  font: 400 14px/1.3 var(--sans);
  letter-spacing: inherit;
  color: var(--ink-strong);
  padding-left: 18px;
  box-shadow: inset 2px 0 0 transparent;
  transition: box-shadow 420ms var(--ease-out-expo);
}

.keys .who a {
  display: block;
  font-size: 16.5px;
  font-weight: 500;
  line-height: 1.3;
  white-space: nowrap;
  color: inherit;
}
.keys .who a:hover { text-decoration: none; }
.keys .who a:hover .alias { color: var(--ink); }
.keys .caret {
  display: inline-block;
  width: 16px;
  font: 400 11px/1 var(--mono);
  color: var(--ink-quiet);
}
.keys .alias { color: var(--ink-strong); transition: color 260ms ease; }
/* One anchor is the way in and the other is the way out, and which of them is
   showing is the whole state of the screen. */
.keys .who .hide { display: none; }
.keys tr.row:target .show, .keys tr.row.open .show { display: none; }
.keys tr.row:target .hide, .keys tr.row.open .hide { display: block; }

.keys .since {
  display: block;
  margin-top: 5px;
  padding-left: 16px;
  font: 400 12px/1 var(--mono);
  letter-spacing: 0.05em;
  color: rgba(var(--ink-rgb), 0.62);
  white-space: nowrap;
}
.keys .group { font: 400 14.5px/1.4 var(--sans); color: var(--ink-mid); white-space: nowrap; }
.keys .tail {
  font: 400 15px/1.4 var(--mono);
  letter-spacing: 0.04em;
  color: var(--ink-mid);
  white-space: nowrap;
}
.keys .state { font: 400 14.5px/1.4 var(--sans); color: var(--ink-mid); white-space: nowrap; }

.keys .used { white-space: nowrap; }
.keys .when { display: block; font: 400 14.5px/1.3 var(--sans); color: var(--ink-mid); }
.keys .reqs, .keys .capnote {
  display: block;
  font: 400 12px/1 var(--mono);
  letter-spacing: 0.05em;
  color: rgba(var(--ink-rgb), 0.62);
  white-space: nowrap;
}
.keys .reqs { margin-top: 5px; }
.keys .capnote { margin-top: 9px; }

.keys .num {
  text-align: right;
  font-family: var(--mono);
  font-size: 14.5px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-strong);
  white-space: nowrap;
}
/* The leading zeros of a cost, the `claude-` every model id starts with: the
   part of a figure that is not the figure. --ink-quiet is named for it. */
.keys .dim { color: var(--ink-quiet); }

.keys svg.spark { display: block; width: 100%; height: 38px; }
.keys svg.long { display: block; width: 100%; height: 104px; }
.keys svg.gauge { display: block; width: 100%; height: 2px; margin-top: 7px; }
.keys .gauge .track { fill: var(--line); }
.keys .gauge .fill { fill: var(--bar); }

.keys .acts { display: flex; gap: 10px; justify-content: flex-end; align-items: center; }
.keys .acts form { margin: 0; }
/* The row's two controls hold one line; the dossier's have room to wrap. */
.keys td.act .acts button { white-space: nowrap; }

/* Dimmed, not struck through or badged. A stopped key is still a row someone
   reads to decide whether to rotate it — dimmed one rung, not two, and only
   the three things that say what the key IS. The figures, the spark and the
   limit caption stay at full ladder value: a blanket row opacity would take
   every one of them under the contrast floor the token block defends. */
.keys .off .alias { color: rgba(var(--ink-rgb), 0.74); }
.keys .off .tail, .keys .off .state, .keys .off .group { color: rgba(var(--ink-rgb), 0.62); }

/* ---- the row you opened ------------------------------------------------

   Warmth, and only on this one. The hover glow is the same light at the same
   six falloffs, so moving across the table reads as passing a lamp rather
   than as rows changing colour. */
.keys tr.row { transition: box-shadow 420ms var(--ease-out-expo); }
.keys tr.row:hover { box-shadow: var(--glow-row); }
.keys tr.row:target, .keys tr.row.open { background: var(--flag-wash); }
.keys tr.row:target > td.who, .keys tr.row.open > td.who { box-shadow: inset 2px 0 0 var(--flag); }

.keys tr.panel > td { padding: 0; border: 0; }

/* Three nested elements, each one load-bearing. This is the one with a height
   to animate, so it carries no padding: padding on an element collapsing to
   zero makes its collapsed height its padding. */
.keys .dossier {
  /* Declared here so the token exists in the sheet rather than only in the
     script that writes it: a var() nothing defines renders as nothing, and
     nothing says so. static/keys.js overwrites it with a measured height at
     the moment a panel opens. */
  --panel-h: 0px;
  height: 0;
  overflow: hidden;
  opacity: 0;
  /* `height: 0; overflow: hidden` hides a panel from the eye and from nothing
     else: its Copy and Rotate stay in the tab order, and Enter on a Rotate
     nobody can see revokes a live key. `visibility: hidden` is the one
     property that removes an element from painting, from hit-testing AND from
     the tab order -- and, unusually for a discrete property, it transitions:
     the change is applied at the far end of its duration. So the delay below
     is what lets the 560ms close play out in full before the panel stops
     existing, and the open rule sets it to 0s so a panel becomes reachable the
     instant it starts opening. Neither number touches the motion itself. */
  visibility: hidden;
  box-shadow: var(--glow-row-off);
  border-bottom: 1px solid transparent;
  transition: height 560ms var(--ease-out-expo), opacity 400ms var(--ease-out-expo),
              box-shadow 560ms var(--ease-out-expo), border-color 400ms linear,
              visibility 0s linear 560ms;
}
.keys tr.row:target + tr.panel .dossier,
.keys tr.panel.open .dossier {
  opacity: 1;
  visibility: visible;
  box-shadow: var(--glow-row);
  border-bottom-color: var(--rule);
  transition: height 560ms var(--ease-out-expo), opacity 400ms var(--ease-out-expo),
              box-shadow 560ms var(--ease-out-expo), border-color 400ms linear,
              visibility 0s linear 0s;
}
/* With no script — or before it runs — the fragment opens the panel outright.
   `auto` is not a height that interpolates, which is the whole reason the
   script exists; see static/keys.js. */
html:not(.keys-live) .keys tr.row:target + tr.panel .dossier { height: auto; }
.keys-live .keys tr.panel.open .dossier { height: var(--panel-h, auto); }
/* A dossier that arrives open was asked for by name. It does not play its
   opening at a reader who followed a link straight to it. */
.keys-still .dossier, .keys-still .keys tr.row, .keys-still .keys td.who { transition: none; }

/* The mark and the wash ride here, inside the clip, so they arrive with the
   panel instead of being painted under a zero-height element. */
.keys .dossier-mark { box-shadow: inset 2px 0 0 var(--flag); background: var(--flag-wash); }
.keys .dossier-body { padding: 8px 40px 44px 34px; }

.keys .facts { margin: 0; display: grid; grid-template-columns: repeat(3, 1fr); }
.keys .facts > div { padding: 26px 24px; display: grid; gap: 12px; border-left: 1px solid var(--line); }
.keys .facts > div:first-child { padding-left: 0; border-left: 0; }
.keys .facts > div:last-child { padding-right: 0; }
.keys .facts dd {
  margin: 0;
  font: 400 26px/1 var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink-strong);
}
/* Money leads; a count and a remaining balance are context for it. */
.keys .facts > div:nth-child(n + 2) dd { color: var(--ink-mid); }
.keys .facts .unit { font-size: 13px; color: rgba(var(--ink-rgb), 0.62); }

.keys .chart { display: grid; gap: 14px; padding-top: 26px; border-top: 1px solid var(--line); }
.keys .chart header { display: flex; align-items: baseline; justify-content: space-between; gap: 20px; }
.keys .chart .span {
  margin: 0;
  font: 400 12px/1 var(--mono);
  letter-spacing: 0.05em;
  color: rgba(var(--ink-rgb), 0.62);
}

.keys .dossier-foot {
  margin-top: 34px;
  display: grid;
  border-top: 1px solid var(--rule);
}
/* However many there are. The rate card and the model list are separately
   unknowable -- both come from the key's New API token, which may be missing
   -- and an empty track where a heading failed to arrive reads as a column
   that broke rather than a fact we do not have. */
.keys .dossier-foot.cols-3 { grid-template-columns: 1fr 1fr 1fr; }
.keys .dossier-foot.cols-2 { grid-template-columns: 1fr 1fr; }
.keys .dossier-foot.cols-1 { grid-template-columns: 1fr; }
/* One padding, stated once, with the ends taking their own off: whichever
   column happens to be first has no rule to its left and nothing to indent
   past. */
.keys .rates, .keys .reach, .keys .rot {
  padding: 26px 44px 0;
  border-left: 1px solid var(--line);
}
.keys .dossier-foot > *:first-child { padding-left: 0; border-left: 0; }
.keys .dossier-foot > *:last-child { padding-right: 0; }
.keys .rot { display: grid; gap: 26px; align-content: start; }
.keys .dossier-foot .tag { margin-bottom: 18px; }
.keys .rot .tag { margin-bottom: 14px; }

.keys .rates table { min-width: 0; }
/* The column names, for a reader who cannot see which column is which. The
   canvas draws no header row, so this one has none of the things that make a
   row tall -- the names inside are clipped by `.sr`. Not `position: absolute`
   on the thead: that blockifies it, takes it out of the table, and the figures
   under it stop having column headers at all, which is the entire point. */
.keys .rates thead th { padding: 0; border: 0; line-height: 0; }
.keys .rates td {
  padding: 11px 0;
  border-bottom: 1px solid var(--line);
  font: 400 14.5px/1 var(--mono);
  color: var(--ink-mid);
  vertical-align: baseline;
}
.keys .rates td.fig { text-align: right; font-variant-numeric: tabular-nums; }

.keys .reach ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 11px; }
.keys .reach li { font: 400 14.5px/1 var(--mono); color: var(--ink-mid); }

.keys .lines {
  margin: 0;
  font: 400 14.5px/1.7 var(--mono);
  letter-spacing: 0.03em;
  color: rgba(var(--ink-rgb), 0.62);
}
.keys .rot .acts { justify-content: flex-start; gap: 12px; }
.keys .rot .acts button { padding: 10px 18px; }
/* Warm, not water. Inside a panel the row has already lit, a blue hover would
   be a second light source in one place. */
.keys .rot .acts button:hover {
  border-color: var(--flag-lit);
  color: var(--ink);
  box-shadow: var(--glow-warm);
}

.keys + .lede { margin-top: 36px; max-width: 640px; font-size: 15px; }

/* ---- the ledger on a laptop -------------------------------------------

   Ten columns want 1240px and a 1440px window offers the sheet 1048. The
   three fixed drawings give back 90px between them, the cell gutters another
   54, and the 额度 caption stops holding one line — which is what it was
   costing 100px to do. Nothing is dropped and nothing is compressed to
   illegibility; the table simply stops needing to be dragged sideways on the
   width most people are reading it at. */
@media (min-width: 1281px) and (max-width: 1632px) {
  /* The floor comes off: below it the columns are held apart by their own
     nowrap contents, which is a truer minimum than a number picked for the
     widest window. */
  .keys > table { min-width: 0; }
  .keys > table > tbody > tr > td { padding-right: 14px; }
  .keys > table > thead > tr > th { padding-right: 14px; }
  .keys > table > thead > tr > th.c-spark { width: 150px; }
  .keys > table > thead > tr > th.c-cap { width: 190px; }
  .keys > table > thead > tr > th.c-act { width: 180px; padding-right: 0; }
  .keys td.act { padding-right: 0; }
  .keys .capnote { white-space: normal; line-height: 1.5; }
}

/* ---- the narrow ledger -------------------------------------------------

   1280px, not the 900px the rest of the sheet reflows at. Ten columns need
   1240px of their own before anything is squeezed, so the sheet has to be
   about 1630px wide before the table fits without scrolling — the logs table
   next door has seven columns and fits in 704px. Between 1280 and that, the
   `.scroll` wrapper does what it does everywhere else on the console and the
   reader drags the table sideways. Below it, the row becomes a card.

   Same markup at both widths: the cells are re-ordered with flex rather than
   duplicated, so there is one row of HTML and no chance of the two drifting. */
@media (max-width: 1280px) {
  /* Ten columns at 327px is not a table, it is a column of two-character
     stacks. Same answer as the log table and the same markup at both widths:
     the cells are re-ordered with flex rather than duplicated, so there is one
     row of HTML and no chance of the two drifting. */
  .keys { margin-top: 40px; }
  .keys > table { min-width: 0; display: block; }
  .keys > table > thead { display: none; }
  .keys > table > tbody { display: block; }
  .keys > table > tbody > tr.row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: 12px;
    row-gap: 10px;
    padding: 18px 0 18px 18px;
    border-top: 1px solid var(--line);
  }
  .keys > table > tbody > tr.panel { display: block; }
  .keys > table > tbody > tr > td { display: block; padding: 0; border: 0; }
  .keys td.who { order: 1; padding-left: 0; }
  .keys .alias { font-size: 16px; }
  .keys td.state { order: 2; margin-left: auto; }
  .keys tr.row::after { content: ""; order: 3; flex-basis: 100%; height: 0; }
  .keys td.tail { order: 4; }
  .keys td.group { order: 5; }
  .keys td.used { order: 6; flex-basis: 100%; }
  .keys td.num { order: 7; text-align: left; }
  /* The log table's trick, for the same reason. With the `thead` gone, 费用
     and 暂时占用 are two adjacent dollar figures and the spark is a drawing of
     nothing in particular; each one prints the heading it lost, from the
     Strings table, in front of itself. */
  .keys td.group::before,
  .keys td.spark::before,
  .keys td.num::before {
    content: attr(data-k) " ";
    font: 400 12px/1 var(--mono);
    letter-spacing: 0.05em;
    color: rgba(var(--ink-rgb), 0.62);
  }
  .keys td.spark::before { display: block; margin-bottom: 8px; }
  .keys td.spark { order: 8; flex-basis: 100%; }
  .keys td.cap { order: 9; flex-basis: 100%; }
  .keys td.act { order: 10; flex-basis: 100%; }
  .keys .acts { justify-content: flex-start; }
  /* The mark moves to the row, which is the box at this width. */
  .keys tr.row:target, .keys tr.row.open { box-shadow: inset 2px 0 0 var(--flag); }
  .keys tr.row:target > td.who, .keys tr.row.open > td.who { box-shadow: none; }

  .keystrip { margin-top: 36px; grid-template-columns: repeat(2, 1fr); }
  .keystrip > div:nth-child(3) { padding-left: 0; border-left: 0; }
  .keystrip > div:nth-child(2) { padding-right: 0; }

  .keys .dossier-body { padding: 8px 16px 28px 16px; }
  .keys .facts { grid-template-columns: repeat(2, 1fr); }
  .keys .facts > div { padding: 20px 16px; }
  .keys .facts > div:nth-child(3) { padding-left: 0; border-left: 0; }
  .keys .facts > div:nth-child(2) { padding-right: 0; }
  .keys .facts dd { font-size: 21px; }
  .keys svg.long { height: 72px; }
  .keys .dossier-foot,
  .keys .dossier-foot.cols-2,
  .keys .dossier-foot.cols-3 { grid-template-columns: 1fr; }
  .keys .rates, .keys .reach, .keys .rot {
    padding: 26px 0 0;
    border-left: 0;
  }
  .keys .dossier-foot > * + * {
    border-top: 1px solid var(--line);
    margin-top: 26px;
  }
  .keys + .lede { margin-top: 32px; }
}

@media (prefers-reduced-motion: reduce) {
  /* 560ms of height and 420ms of warmth are both motion, and both were asked
     not to be shown. The panel still opens; it simply arrives.

     `transition: none` rather than a duration, because the sheet's blanket
     rule flattens durations and says nothing about DELAYS -- and the closed
     panel's visibility carries a 560ms one. Left alone it would keep a shut
     dossier's controls in the tab order for more than half a second after the
     panel itself had gone. */
  .keys .dossier, .keys tr.row, .keys td.who, .keys .alias { transition: none; }
}

/* ---- 新密钥 ------------------------------------------------------------

   A moment in a flow, not a screen to browse away from: the rail is drawn for
   continuity but dimmed and carries no links, and the single control is the
   one that says the key has been saved. The value is never printed; Copy
   hands it over, here and on every row of the list. */

.reveal .rail { opacity: 0.4; pointer-events: none; }
.reveal .rail .group ul { margin: 56px 0 0; }
.reveal .rail li { color: var(--ink-dim); }
.reveal .sheet { justify-content: center; }

.onetime {
  max-width: 760px;
  padding-left: 28px;
  border-left: 1px solid var(--flag);
  display: grid;
  gap: 36px;
}
.onetime .tag { letter-spacing: 0.09em; color: var(--flag-ink); }
.onetime h1 { margin: 0; }
.onetime > div, .onetime .tag + h1 { margin: 0; }
.onetime .lede { margin: 0; max-width: 520px; font-size: 16px; color: var(--ink-mid); }
.onetime .lede.small { font-size: 14px; color: var(--ink-dim); }

/* user-select: all so one click takes the whole value — a partial selection of
   a credential is the kind of mistake that costs an afternoon. */
.onetime .secret {
  margin: 0;
  padding: 26px 28px;
  border: 1px solid var(--flag-edge);
  border-radius: 2px;
  background: var(--flag-wash);
  font: 400 22px/1.7 var(--mono);
  letter-spacing: 0.075em;
  color: var(--ink);
  word-break: break-all;
  user-select: all;
}

.onetime .done { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.onetime .done a {
  padding: 11px 22px;
  border: 1px solid var(--flag-edge);
  border-radius: 2px;
  font: 400 13px/1 var(--mono);
  letter-spacing: 0.075em;
  color: var(--ink);
  transition: border-color 220ms ease;
}
.onetime .done a:hover { border-color: var(--flag); text-decoration: none; }
.onetime .done .tail {
  font: 400 12px/1 var(--mono);
  letter-spacing: 0.075em;
  color: var(--ink-dim);
}

@media (max-width: 900px) {
  .reveal .rail { display: none; }
  .onetime { gap: 28px; padding-left: 18px; }
  .onetime .secret { padding: 20px; font-size: 17px; line-height: 1.6; }
  .onetime .lede { font-size: 15px; }
}

/* ---- 请求记录 ----------------------------------------------------------

   Seven columns at 1440, two lines per request below 900. The table is the
   same markup at both — cells are re-ordered with flex rather than duplicated,
   so there is one row of HTML and no chance of the two drifting. */

.logs { margin-top: 48px; }
.logs table { min-width: 44rem; }
.logs th:nth-child(4), .logs .num, .logs .cost { text-align: right; }
.logs th:last-child, .logs td:last-child { padding-right: 0; }

.logs td {
  padding: 13px 20px 13px 0;
  font-family: var(--mono);
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  white-space: nowrap;
  color: var(--ink-mid);
}
/* Time over date. The date repeats down a page of one afternoon's traffic, so
   it sits under the time rather than beside it, and quieter. */
.logs .when { line-height: 1.35; }
.logs .when span { display: block; }
.logs .when .on { font-size: 12px; letter-spacing: 0.05em; color: var(--ink-dim); }
.logs .ns { color: var(--ink-quiet); }
.logs .mn { color: rgba(var(--ink-rgb), 0.88); }
.logs .cost .mn { color: var(--ink-strong); }
.logs .quiet { color: var(--ink-dim); }
.logs .num::before { content: attr(data-k) " "; color: var(--ink-dim); }
.logs .request-detail, .logs .request-id {
  display: block;
  margin-top: 6px;
  font-size: 12px;
  color: var(--ink-dim);
  white-space: normal;
}
.logs .request-id { overflow-wrap: anywhere; max-width: 30ch; }
.logs .video-usage { text-align: left; white-space: normal; }
.logs .model { max-width: 32ch; white-space: normal; overflow-wrap: anywhere; }
/* Prose, not a figure: a status is the one thing in the row you read rather
   than compare. It takes the sans with the rest of the prose, and it stops
   being the dimmest cell in the row. */
.logs .state {
  font: 400 14.5px/1.5 var(--sans);
  max-width: 20ch;
  white-space: normal;
  letter-spacing: 0;
  color: var(--ink-mid);
}

.logs .failed { background: var(--flag-wash); }
.logs .failed .state { color: var(--flag-ink); }

.tally { display: grid; gap: 8px; justify-items: end; margin-left: auto; text-align: right; }
.tally .amount {
  margin: 0;
  font: 400 26px/1 var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink-strong);
}
.tally .hint { margin: 0; font: 400 14px/1.4 var(--sans); color: var(--ink-dim); }

.pager { margin: 28px 0 0; display: flex; align-items: center; gap: 20px; }
.pager a {
  padding: 8px 14px;
  border: 1px solid var(--rule);
  border-radius: 2px;
  font: 400 12.5px/1 var(--mono);
  letter-spacing: 0.06em;
  transition: border-color 220ms ease, color 220ms ease;
}
.pager a:hover { border-color: var(--accent); color: var(--ink); text-decoration: none; }

.empty { margin: 48px 0 0; color: var(--ink-dim); }

@media (max-width: 900px) {
  /* Two lines per request: what ran and what it cost, then when and how much of
     it. One row of markup at both widths — the cells are placed, not
     duplicated — so the reading order a screen reader gets is the source order
     either way. */
  .logs { margin-top: 32px; overflow-x: visible; }
  .logs table, .logs tbody, .logs tr, .logs td { display: block; }
  .logs table { min-width: 0; }
  .logs thead { display: none; }

  .logs tr {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: 9px;
    row-gap: 8px;
    padding: 15px 0;
    border-top: 1px solid var(--line);
  }
  .logs td { padding: 0; border: 0; white-space: nowrap; }

  .logs .model { order: 1; }
  .logs .cost { order: 2; margin-left: auto; font-size: 15px; }
  /* The line break. A full-basis item of no height ends the first line, so the
     rest flows on the second and wraps to a third if a request carried six
     figures of context rather than three. A grid cannot wrap, and a wide row
     there spills off the screen instead. */
  .logs tr::after { content: ""; order: 3; flex-basis: 100%; height: 0; }

  /* Date first here, the way a card reads, though the markup keeps time first
     so the desktop column leads with the value that changes every row. */
  .logs .when { order: 4; display: flex; gap: 5px; font-size: 12.5px; }
  .logs .when span { display: inline; letter-spacing: 0.06em; }
  .logs .when .on { order: -1; }
  .logs .when .at { color: var(--ink-dim); }
  .logs .when::after { content: "·"; color: var(--ink-dim); }

  /* The narrow log table prints its own column headings in front of each
     figure, so these two runs together are the smallest type on any screen. */
  .logs .num { font-size: 12.5px; letter-spacing: 0.04em; color: var(--ink-dim); }
  /* The column headings are gone, so every figure carries its own. */
  .logs .num::before { content: attr(data-k) " "; }
  .logs td:nth-child(3) { order: 5; }
  .logs td:nth-child(4) { order: 6; }
  .logs td:nth-child(5) { order: 7; }

  .logs .state { order: 8; margin-left: auto; font-size: 13.5px; }
  .logs .video-usage { flex-basis: 100%; font-size: 12.5px; }
  .logs .model { min-width: 0; overflow-wrap: anywhere; white-space: normal; }
  .logs .cost { white-space: normal; }
  .logs td.cost { order: 2; }
  .logs td.state { order: 8; }

  .logs .failed { padding-left: 10px; padding-right: 10px; margin: 0 -10px; }

  .sheet-head { gap: 16px; }
  .sheet h1 { font-size: 28px; }
  .tally { gap: 6px; }
  .tally .amount { font-size: 18px; }
  .tally .hint { font-size: 12px; }
  .pager { gap: 16px; }
  .pager a { padding: 12px 18px; }
}

/* ---- the one-time key -------------------------------------------------- */

.gate { background: var(--deep); }
.gate .sheet {
  min-height: 100vh;
  max-width: 34rem;
  margin: 0 auto;
  justify-content: center;
  gap: 40px;
  padding: 64px 32px;
}
.gate .sheet h1 { font-size: 32px; }
.gate .sheet > * { margin-top: 0; }

/* ---- login -------------------------------------------------------------

   The homepage's scene, with the form where the tagline was. Arriving by the
   Login button, the crossing's phase comes across in the URL fragment and the
   line dissolves into the form in place.

   The form is visible with no script and no WebGL: the two animations below are
   the only thing that moves it, they carry `both` fill, and nothing here sets a
   resting opacity of 0. A page that renders and then stops still logs you in. */

/* The element writes its own inline cssText on connect — display, size and
   position included — so it must be given a sized positioned parent rather than
   laid out from here. Same arrangement as site/index.html. */
.gate .stage {
  position: relative;
  width: 100%;
  height: 100dvh;
  overflow: hidden;
  /* Same gradient the element gets below. With no script at all the scene is
     an unknown inline element with no height, so this is what shows. */
  background: var(--scene-fallback);
}

/* !important throughout because the element writes `display:block; width:100%;
   height:100%; position:relative; background:<flat colour>` into its own inline
   cssText on connect, and an inline declaration beats a stylesheet rule of
   normal weight. Three things are taken back from it:

   `background`, so the scene's own fallback gradient sits at this page's
   horizon and the step across from the marketing host is never a blank frame.

   `position` and `inset`, which is what actually gives the shader a box. Its
   `height:100%` resolves against `.gate .stage`, and the stage stopped having
   a definite height when it was allowed to grow past the window (below) — a
   percentage against an `auto` parent is `auto`, which for an element whose
   only child is an absolutely positioned canvas is 0. The scene rendered
   1440x0 behind the sign-in form for that reason, and `_resize` returns early
   on a zero-height rect, so the canvas never left its 300x150 default either:
   the page downloaded the shader and drew the fallback gradient. Taken out of
   flow and pinned to the stage's padding box, it is sized by its containing
   block rather than by a percentage, so it covers the stage at any height the
   stage takes — which is what the rule below assumed all along.

   `height` and `width` to `auto`, because `inset` is what sizes it now, and
   leaving the inline percentages in place would over-constrain the box.

   Both elements, because /login mounts both and the signup screens mount
   whichever one the palette calls for, and each writes the same inline cssText
   on connect. Naming only the dark one left the light gate showing a flat
   #dfe3e6 rectangle.

   Out of flow, before `.frame` in the DOM and with neither carrying a z-index,
   so the frame still paints over it — the arrangement site/index.html has, and
   the one `test_console_gate_reach.py` hit-tests. The dawn scene is inside
   `.dawn` on /login, which is positioned, so that is the box it pins to. */
.gate night-water,
.gate dawn-water {
  position: absolute !important;
  inset: 0 !important;
  width: auto !important;
  height: auto !important;
  background: var(--scene-fallback) !important;
}

/* ---- the palette turning, at the gate ---------------------------------

   /login mounts both shaders and cross-fades between them, so the switch
   changes the water the way the rest of the console changes its sheet —
   in the page, over time, rather than as a reload that lands in the other
   palette already finished. site/index.html does exactly this, and the two
   pages are supposed to be one view of one scene.

   Night underneath, dawn over it. Only the top layer's opacity moves, so the
   resting state needs no value to resolve: dawn is simply on top at
   `--dawn-on: 1` and gone at 0. The one underneath is never unmounted —
   restarting its crossing from the left edge is the jump the whole
   arrangement exists to avoid, and it is the same reason the handoff hands
   a phase across from the homepage in the first place.

   620ms to match the disc in the switch itself, which is the longest thing
   `gate-theme.js` waits out. The colours of the card ride the shorter
   `.palette-turning` transition above; the water is the slow half, the way it
   is on the homepage.

   Only /login. The signup screens carry no palette control, so they mount one
   scene and pay for one shader — `pages._scene` against `pages._scenes`.

   The reduced-motion block near the end of this file flattens every
   transition on the page, this one included: the switch then changes the
   water in one frame, which is the honest answer to "no animation". */
.gate .dawn {
  position: absolute;
  inset: 0;
  opacity: var(--dawn-on);
  transition: opacity 620ms ease;
}

/* Inert except for the mark and the form, the way the homepage frame is. */
.gate .frame {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: var(--gate-pad-x) var(--gate-pad-x) var(--gate-pad-y);
  pointer-events: none;
}
/* The mark and the language, at the two ends — the composition the homepage
   masthead has (site/index.html puts Login and Contact opposite its lockup), so
   the crossing does not rearrange the top of the frame on arrival. */
.gate .masthead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px 24px;
}
.gate .brand, .gate .switches { pointer-events: auto; }
/* The frame is inert, so these need their own hit area rather than inheriting
   one. Mono and tracked like the homepage's own Login link, which is what sits
   in this spot a moment earlier. */
.gate .lang {
  pointer-events: auto;
  font: 400 13px/1 var(--mono);
  letter-spacing: 0.06em;
  color: var(--ink-dim);
  padding: 6px 2px;
}
.gate .lang:hover { color: var(--ink); }
/* An anchor here, a form in the rail -- the gate has no CSRF token to bind a
   write to, so its switch only changes what this page renders. Same square
   either way. */
.gate .theme {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--rule);
  border-radius: 2px;
  color: var(--ink-mid);
  transition: border-color 260ms var(--ease-out-expo), color 260ms ease;
}
.gate .theme:hover {
  border-color: var(--accent);
  color: var(--ink);
  text-decoration: none;
  box-shadow: var(--glow-control);
}
/* One grid cell for both: whichever is showing, nothing reflows and nothing
   pushes the other. The cell is bottom-aligned, so the handoff keeps the
   homepage's own baseline exactly — see `.handoff` — and the form takes its
   own resting height above it. */
.gate .copy {
  display: grid;
  grid-template-areas: "c";
  align-items: end;
  justify-items: start;
  width: 100%;
}
.gate .handoff, .gate .gate-form { grid-area: c; }

/* Wider than the form's cell on purpose: this is the homepage's copy block at
   the homepage's measure, so the line it was on there is the line it is on
   here. Constrained to the form's width it would reflow on arrival, which is
   the one thing the handoff exists to avoid. */
.gate .handoff {
  display: none;
  width: min(560px, 72vw);
  pointer-events: none;
}
.gate .handoff p { margin: 0; }
.gate .handoff-title {
  font: 400 clamp(24px, 3vw, 40px) / 1.22 var(--serif);
  letter-spacing: 0.004em;
  color: var(--ink-strong);
  text-wrap: pretty;
}
.gate .handoff-lede {
  margin-top: clamp(10px, 1.1vh, 16px) !important;
  font: 400 clamp(13px, 1.2vw, 16px) / 1.5 var(--serif);
  letter-spacing: 0.008em;
  color: var(--ink-dim);
}

/* Set a quarter in from the left and a quarter up from the floor. At the
   frame's own padding the form sat ~48px from the left edge and ~70px off the
   floor — pinned into the corner, and a long diagonal reach on a laptop.

   Relative to the window on each axis independently, so an unusual aspect
   ratio is simply two independent quarters — ultrawide, portrait tablet, or a
   half-height window all land in the same place proportionally.

   Written as the target minus the frame's own inset, not as a hand-tuned
   remainder. The padding then cancels and the edge is at exactly a quarter at
   every size. Tuned figures do not survive the ends of the range: the padding
   is clamped, so once it caps out a fixed 22vw/18vh drifts inward — measured
   0.234 at 3840px wide and 0.223 at 2160px tall.

   `dvh`, not `vh`, because `.stage` is `100dvh`: on a phone with the address
   bar showing, `vh` describes a taller window than the one the form is
   actually inside. `max(0px, ...)` because the subtraction goes negative on a
   window narrower than the padding itself, which would pull the form back out
   past the margin.

   They are margins on the form and not on the cell it shares, which is the
   whole trick: the handoff keeps the cell's bottom-left anchor, so the line
   still occupies exactly the pixels the homepage headline did and dissolves
   where it was being read. Nothing reflows either way — one grid cell, two
   alignments. What it costs is that the form no longer fades in over the line;
   it surfaces up and to the right of it. */
.gate .gate-form {
  display: grid;
  gap: 28px;
  width: min(376px, 84vw);
  margin-left: max(0px, calc(25vw - var(--gate-pad-x)));
  margin-bottom: max(0px, calc(25dvh - var(--gate-pad-y)));
  pointer-events: auto;
  animation: nfGateIn 700ms var(--ease-out-expo) 120ms both;
}
.gate .gate-form h1 { margin: 0; font-size: clamp(28px, 3vw, 40px); }
.gate .gate-form form { margin: 0; display: grid; gap: 22px; justify-items: start; }

.gate label { display: grid; gap: 8px; max-width: none; margin: 0; }
.gate label .tag {
  font: 500 12px/1 var(--mono);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
/* Underline, not a box: one rule per field is quieter against the water than
   four, and the scene shows through where a filled input would sit. */
.gate input[type="email"], .gate input[type="password"] {
  margin: 0;
  padding: 9px 2px;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--rule);
  border-radius: 0;
  font-size: 15.5px;
  color: var(--ink-strong);
  letter-spacing: 0.04em;
}
.gate input:focus {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-bottom-color: var(--accent);
  color: var(--ink);
}
.gate .gate-form button { padding: 11px 22px; font-size: 13px; color: var(--ink-mid); }

/* Blue, not the amber every other note on the console uses. A wrong password is
   not a fault to be warned about, it is a retry — and amber next to a password
   field reads as "something is broken", which sends people to support instead
   of to the field. Flip it to var(--warn) if that call turns out wrong. */
.gate .error {
  margin: 0;
  padding-left: 14px;
  border-left: 1px solid var(--accent);
  font: 400 15.5px/1.5 var(--sans);
  color: var(--ink-mid);
}

@keyframes nfGateIn {
  from { opacity: 0; transform: translateY(7px); }
  to { opacity: 1; transform: none; }
}
@keyframes nfGateOut {
  from { opacity: 1; }
  to { opacity: 0; }
}

/* Only when a phase came across. Arriving cold there is nothing to be
   continuous with, and the form is what you came for. */
.gate[data-handoff] .handoff {
  display: block;
  animation: nfGateOut 620ms ease 700ms both;
}
.gate[data-handoff] .gate-form { animation-delay: 900ms; }

/* Both insets need somewhere to go. Below these the frame runs out of room and
   the form would run off the right edge or climb into the masthead, and a form
   you cannot see all of is worse than one sitting at the margin. Each axis
   gives way on its own, at the size where it stops fitting — the quarter needs
   less room than the third did, so both thresholds are lower than they were,
   and the vertical one no longer needs an intermediate step. */
/* The card is 448px since the redesign, not 376px, and it keeps a 25vw left
   offset above this breakpoint. At 561px that put its right edge at 566px in a
   561px window -- the sign-in page scrolled sideways. It fits when
   448 + --gate-pad-x <= 75vw, which is ~627px, so the offset now collapses
   below 680px rather than 560px. */
@media (max-width: 680px) {
  .gate .gate-form { margin-left: 0; }
}
@media (max-height: 520px) {
  .gate .gate-form { margin-bottom: 0; }
}

/* The card can be taller than the window, and since the redesign the sign-up
   one is: 677px against a 640px phone, in both languages, because the
   providers stack and the footer bar is new. The frame was absolutely
   positioned inside a stage pinned to 100dvh and clipped, so "Create account"
   and the footer were not merely below the fold -- there was no scrollbar and
   no gesture that could reach them, and signing up on a small phone was
   impossible.

   Letting the frame carry the height and the stage grow with it changes
   nothing wherever the composition already fit (min-height holds the
   masthead-top, form-bottom split that space-between draws), and gives it
   somewhere to go where it did not. The scene still fills the stage, so it
   grows with it rather than tiling.

   That last sentence was not free, and for a day it was not true. `height:
   auto` here is exactly what stopped the scene's inline `height:100%`
   resolving, and the sign-in page lost the animation the rest of the site
   runs while every test still passed. The scene is pinned to this box by
   `inset` rather than by a percentage of it now, above — so keep that rule
   and this one together: anything that puts the scene back in flow needs a
   definite height here again. */
.gate .stage { height: auto; min-height: 100dvh; overflow: visible; }
.gate .frame { position: relative; min-height: 100dvh; }

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

@media (max-width: 900px) {
  .app { flex-direction: column; }
  /* Stacked, the rail is a band across the top, not a column beside the sheet.
     Sticking it to the viewport would pin that band over the content and give
     the foot nowhere to sit, so the corner rule is dropped with the column. */
  .rail {
    width: auto;
    padding: 28px 24px;
    border-right: 0;
    border-bottom: 1px solid var(--line);
    position: static;
    height: auto;
    overflow: visible;
  }
  .rail nav { margin-top: 28px; flex-flow: row wrap; gap: 28px; }
  .rail .foot {
    margin-top: 28px;
    padding-top: 24px;
    border-top: 1px solid var(--line);
    grid-auto-flow: column;
    align-items: center;
    justify-content: space-between;
  }
  .sheet { padding: 40px 24px 48px; }
  .headline { margin-top: 40px; gap: 32px; }
  .stats { margin-top: 40px; grid-template-columns: 1fr; }
  .stats > div { padding: 18px 0; border-left: 0; border-top: 1px solid var(--line); }
  .stats > div:first-child { border-top: 0; }
  .history { margin-top: 48px; }
  .sheet > .footnote:last-child { padding-top: 32px; }
}

@media (prefers-reduced-motion: reduce) {
  /* Duration alone is not enough: an animation with `both` fill and a 900ms
     delay holds its from-state for 900ms, which for the form is invisible. */
  * {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-delay: 0s !important;
  }
  .gate .handoff { display: none !important; }
}

/* ---- 价格 --------------------------------------------------------------

   Two jobs. Print seven rates so they can be scanned down a column, and stop
   the cheapest row being read as the cheapest model — the one it flags is
   metered differently and, on a short prompt, costs more than rows priced four
   times higher. So the caveat sits above the table, not under it, and the
   flagged row is lifted out of the price sort and set below the rest with a
   gap between. The tint is the only saturated colour in the console; it is
   spent here because this is the only place a misread costs the tenant money. */

.caution {
  margin: 56px 0 0;
  padding-left: 24px;
  max-width: 46rem;
  border-left: 1px solid var(--flag);
  display: grid;
  gap: 10px;
}
.caution p { margin: 0; font-size: 16px; line-height: 1.8; color: var(--ink-mid); }
.caution .lead { font-size: 20px; line-height: 1.5; font-weight: 600; color: var(--ink-strong); }
.caution code { font: 400 15px/1 var(--mono); letter-spacing: 0.02em; color: var(--ink-strong); }

@media (max-width: 900px) {
  .caution { margin-top: 36px; padding-left: 18px; }
  .caution .lead { font-size: 17px; }
  .caution p { font-size: 14px; }
}

/* ---- 新建密钥 · minting ------------------------------------------------

   Added 2026-08-31 with the group. Three things this screen has to do that the
   old one-key-per-account console did not:

     * Make the group choosable, and judgeable. It cannot be changed after
       creation, and a tenant has no other way to compare two rate cards, so
       both catalogues are on the form with their prices on them. The choice is
       the screen; the name and the limit are lines.
     * Show two currencies without letting them be read as one number. The
       lead figure carries no symbol and its unit is in the label beside it;
       the line under it carries the other currency's own symbol. Same
       treatment as site/models.html, which has shipped it publicly since
       2026-08-30.
     * Hand over a key without printing it. `.secret` is the mask, and the
       clipboard gets the value from a data attribute. */

/* The off-screen radio pair every switch on these two screens is driven by.
   Not display:none — a hidden input is not focusable, and the labels are the
   only way to operate the switch with a keyboard. */
.sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The link to the form. It sits where the logs page puts its page total — on
   the title's baseline, at the far end of the sheet — rather than as a second
   bordered button beside Rotate, which would read as an action on a row. */
.mint-link {
  font: 400 13.5px/1 var(--mono);
  letter-spacing: 0.06em;
  color: var(--ink-mid);
}
.mint-link:hover {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-underline-offset: 6px;
}

/* Copy confirms in place. The label swaps to 已复制 / Copied for two seconds
   and the border warms, so the confirmation is on the control that was
   pressed — a toast somewhere else is a second thing to find. */
button.copy.done { border-color: var(--accent); color: var(--ink); }

/* ---- the form --------------------------------------------------------- */

.mint { margin-top: 44px; max-width: 980px; display: grid; gap: 30px; }
.mint .field { display: block; }
.email-settings { max-width: 40rem; overflow-wrap: anywhere; }
.account-form { max-width: 40rem; display: flex; flex-direction: column; align-items: flex-start; gap: 24px; }
.account-form > p, .account-form > h2 { margin: 0; }
.account-form .field { width: 100%; max-width: 22rem; }
.account-form label { display: block; }
.account-form select {
  display: block;
  margin-top: 10px;
  padding: 10px 12px;
  color: var(--ink-strong);
  background: var(--deep);
  border: 1px solid var(--line);
  border-radius: 2px;
  font: inherit;
}
.account-form input[aria-invalid="true"], .account-form select[aria-invalid="true"] { border-color: var(--warn); }
.account-form input:focus-visible { outline: 2px solid var(--accent-lit); outline-offset: 3px; }
.account-form input[type="email"], .account-form input[type="password"] {
  display: block;
  width: 100%;
  margin-top: 10px;
  padding: 10px 12px;
  background: transparent;
  border: 1px solid var(--ink-dim);
  border-radius: 2px;
  color: var(--ink-strong);
  font: inherit;
}
.account-form > p { overflow-wrap: anywhere; }
.mint .tag { margin: 0; letter-spacing: 0.09em; text-transform: uppercase; color: var(--ink-dim); }
.mint input[type="text"] {
  display: block;
  width: 100%;
  max-width: 22rem;
  margin-top: 10px;
  padding: 10px 12px;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 2px;
  color: var(--ink-strong);
  font: 400 15.5px/1.4 var(--mono);
}
.mint input[type="text"].wrong { border-color: var(--warn); }
.mint .hint { margin: 8px 0 0; max-width: 24rem; font-size: 14px; line-height: 1.6; color: var(--ink-dim); }

/* The message sits against the field it is about, in the colour that marks a
   note worth reading. Not --flag: that is reserved for a figure which is
   actively misleading, and a name already in use is neither. */
.mint p.wrong {
  margin: 10px 0 0;
  max-width: 24rem;
  padding-left: 14px;
  border-left: 1px solid var(--warn);
  font-size: 15px;
  line-height: 1.5;
  color: var(--warn);
}

/* The amount is in the reader's own currency, so the symbol belongs to the
   control rather than to the value: a tenant types 500, not ¥500. */
.mint .money {
  display: inline-flex;
  align-items: stretch;
  margin-top: 10px;
  max-width: 22rem;
  border: 1px solid var(--line);
  border-radius: 2px;
}
.mint .money:focus-within { border-color: var(--accent); }
.mint .money .sym {
  display: flex;
  align-items: center;
  padding: 0 4px 0 12px;
  font: 400 15px/1 var(--mono);
  color: var(--ink-dim);
}
.mint .money input[type="text"] { margin: 0; border: 0; padding-left: 2px; }
.mint .money input:focus { outline: none; }

.mint .picks { margin-top: 14px; display: flex; gap: 24px; align-items: stretch; }

/* A whole rate card you can click. The border is the selection, not a tick
   next to a word: the thing being chosen is the price list, so the price list
   is what lights up. */
.mint .pick {
  flex: 1;
  min-width: 0;
  display: block;
  max-width: none;
  margin: 0;
  padding: 20px 22px 22px;
  border: 1px solid var(--line);
  border-radius: 2px;
  cursor: pointer;
  transition: border-color 220ms ease, background 220ms ease;
}
.mint .pick:hover { border-color: var(--ink-faint); }
.mint .pick:has(input:checked) { border-color: var(--accent); background: var(--accent-wash); }
.mint .pick:has(input:focus-visible) { outline: 1px solid var(--accent); outline-offset: 3px; }
.mint .pick:has(input[aria-invalid="true"]),
.mint .money:has(input[aria-invalid="true"]) { border-color: var(--warn); }

.mint .pickhead { display: flex; align-items: baseline; gap: 12px; }
.mint .pickhead input { margin: 0; width: 13px; height: 13px; accent-color: var(--accent); }
.mint .pickhead .name { font: 500 20px/1.2 var(--sans); color: var(--ink-strong); }
.mint .pickhead .count {
  margin-left: auto;
  font: 400 12px/1 var(--mono);
  letter-spacing: 0.05em;
  color: var(--ink-dim);
}

.mint .cat { display: block; margin-top: 16px; }
.mint .cat .line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 20px;
  padding: 5px 0;
  border-bottom: 1px solid var(--tie);
  font: 400 13.5px/1.35 var(--mono);
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
}
/* The common prefix recedes so the eye lands on what differs between rows. */
.mint .cat .id { color: var(--ink-mid); }
.mint .cat .stem, .mint .cat .at { color: var(--ink-dim); }
.mint .cat .fig { white-space: nowrap; text-align: right; }
.mint .cat .fig .lead { display: block; color: var(--ink-mid); }

.mint .mult {
  display: block;
  margin-top: 14px;
  font: 400 12px/1.5 var(--mono);
  letter-spacing: 0.055em;
  color: var(--ink-dim);
}
.mint .note { display: block; margin-top: 6px; font-size: 14px; line-height: 1.6; color: var(--ink-dim); }

/* The one thing on this form that cannot be undone, in the colour kept for
   exactly that. */
.mint .fixed { max-width: 46rem; padding-left: 24px; border-left: 1px solid var(--flag); }
.mint .fixed p { margin: 0; font-size: 18px; line-height: 1.55; color: var(--ink-strong); }
.mint .go { margin: 0; }
.mint .go button { padding: 11px 22px; font-size: 13px; color: var(--ink-strong); }

/* ---- the created key -------------------------------------------------- */

.onetime .said { display: grid; gap: 14px; }
.onetime .facts {
  margin: 0;
  font: 400 12px/1.6 var(--mono);
  letter-spacing: 0.075em;
  color: var(--ink-dim);
}
.onetime .handover { display: grid; gap: 16px; justify-items: start; }

/* The mask, not the key. user-select: none, because there is nothing here
   worth selecting and a partial selection of a masked string is a credential
   that will not work with no sign that it is wrong. Copy is the way. */
.onetime .secret { color: var(--ink-mid); user-select: none; }

.onetime .handover .copy.loud {
  padding: 11px 22px;
  border-color: var(--flag);
  font-size: 13px;
  color: var(--ink);
}
.onetime .handover .copy.loud:hover { border-color: var(--flag); color: var(--ink); }

/* ---- 价格 · the rate card ---------------------------------------------

   Two groups, one on screen. The tabs are radios for the same reason the
   currency switch is: the page is a document, and switching which rate card
   you are reading should not be a request.

   A third group needs a rule in the pair below. `tests/test_console_pricing.py`
   fails if a sellable group has no rule here, so adding one cannot silently
   render an empty page. */

.grouptabs { margin-top: 40px; display: flex; gap: 26px; align-items: baseline; }
.grouptabs .tab {
  display: inline;
  max-width: none;
  margin: 0;
  font: 400 16px/1 var(--sans);
  letter-spacing: 0;
  color: var(--ink-dim);
  cursor: pointer;
  padding-bottom: 8px;
  border-bottom: 1px solid transparent;
  transition: color 220ms ease, border-color 220ms ease;
}
.grouptabs .tab:hover { color: var(--ink-mid); }

.panels .group { display: none; }
#grp-community:checked ~ .panels .group[data-group="community"],
#grp-enterprise:checked ~ .panels .group[data-group="enterprise"] { display: block; }
#grp-community:checked ~ * label[for="grp-community"],
#grp-enterprise:checked ~ * label[for="grp-enterprise"] {
  font-weight: 600;
  color: var(--ink-strong);
  border-bottom-color: var(--accent);
}
#grp-community:focus-visible ~ * label[for="grp-community"],
#grp-enterprise:focus-visible ~ * label[for="grp-enterprise"] {
  outline: 1px solid var(--accent);
  outline-offset: 3px;
}

.panels { margin-top: 36px; }
.grouphead { display: flex; align-items: center; gap: 16px; }
.grouphead .mark { flex: none; width: 26px; height: auto; color: var(--ink-dim); }
.grouphead h2 { margin: 0; font: 400 24px/1.2 var(--serif); color: var(--ink-strong); }
.grouphead .rule { flex: 1; height: 1px; background: var(--line); }

.panels table { margin-top: 26px; min-width: 0; border-collapse: separate; border-spacing: 0; }
.panels tbody .row > * {
  padding: 10px 6px;
  border-bottom: 1px solid var(--rule);
  vertical-align: bottom;
  transition: border-color 420ms var(--ease-out-expo), color 420ms var(--ease-out-expo);
}
/* A model id is an identifier a tenant types, not a column heading. The sheet
   uppercases every `th`, which would make this one unusable for the thing it
   is read for. */
.panels tbody .row .id {
  text-transform: none;
  padding-left: 26px;
  border-left: 2px solid transparent;
  text-align: left;
  font: 400 19px/1.2 var(--mono);
  letter-spacing: 0.04em;
  color: var(--ink-strong);
}
.panels tbody .row .fig { width: 250px; text-align: right; }
.panels tbody .row .mult {
  width: 104px;
  padding-left: 18px;
  border-left: 1px solid rgba(var(--ink-rgb), 0.09);
}
.panels .lab {
  display: block;
  margin-bottom: 8px;
  font: 500 12.5px/1 var(--mono);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: rgba(var(--ink-rgb), 0.68);
}
.panels .lab .u { color: var(--ink-quiet); }
.panels .n {
  display: block;
  font: 400 22px/1 var(--mono);
  letter-spacing: 0.03em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-strong);
}
.panels .mult .n { font-size: 16px; color: var(--ink-mid); }

/* Models that cost the same are tied by a softened rule between them, so three
   identical prices read as one fact rather than as three coincidences. */
.panels .tie .row:not(:last-child) > * { border-bottom-color: var(--tie); }
.panels tbody .row:hover > * { border-bottom-color: var(--flag-edge); color: var(--ink); }
.panels tbody .row:hover .id { border-left-color: var(--flag); }

.panels .groupfoot { margin-top: 22px; }
.panels .groupfoot div { display: flex; align-items: baseline; gap: 12px; }
.panels .groupfoot .lab { margin: 0; display: inline; }
.panels .groupfoot .n { display: inline; font-size: 15px; color: var(--ink-mid); }
.panels .groupfoot .of { font-size: 13px; color: var(--ink-dim); }

@media (max-width: 900px) {
  .mint .picks { flex-direction: column; }
  .mint .fixed p { font-size: 16px; }
  .grouptabs { margin-top: 28px; gap: 18px; }
  .panels tbody .row .id { padding-left: 18px; font-size: 16px; }
  .panels tbody .row .fig { width: auto; }
  .panels tbody .row .mult { width: auto; padding-left: 10px; }
  .panels .n { font-size: 18px; }
  .keys .acts { justify-content: flex-start; }

  /* "Of list" over two lines reads as two labels. It is the narrowest column
     and the one with the least to say. */
  .panels .mult .lab { white-space: nowrap; }
}

/* ---- 充值 · topping up --------------------------------------------------
   Seven screens sharing one frame: the rail unchanged, a way back above the
   title, and the balance opposite it. Nothing here is a card and nothing is
   boxed — the same hairlines that separate every other figure on this console
   do the work, which is why the method list is three full-width rows and not
   three tiles.

   The one bordered thing on a screen is the way on. Everything else is type. */

.step {
  display: inline-block;
  padding: 11px 22px;
  border: 1px solid var(--rule);
  border-radius: 2px;
  font: 400 12.5px/1 var(--mono);
  letter-spacing: 0.06em;
  color: var(--ink-strong);
  transition: border-color 220ms ease, color 220ms ease;
}
.step:hover { border-color: var(--accent); color: var(--ink); text-decoration: none; }

/* The entry point sits on the balance's baseline, not in the navigation. The
   nav already carries six, and "should I add money" only ever arises next to
   the number that prompts it. */
.headline .add-funds {
  align-self: flex-end;
  margin-bottom: 5px;
  padding: 10px 18px;
  font-size: 12px;
}

.topup .titling { display: grid; gap: 16px; justify-items: start; }
.topup .back { font: 400 12px/1 var(--mono); letter-spacing: 0.075em; }
.topup-balance {
  display: grid;
  gap: 8px;
  justify-items: end;
  text-align: right;
  margin-left: auto;
}
.topup-balance .amount {
  margin: 0;
  font: 400 26px/1 var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink-strong);
}
.topup .hint { margin: 0; font: 400 14px/1.4 var(--sans); color: var(--ink-dim); }
.topup .hint.wide { max-width: 34rem; text-wrap: pretty; }

/* ---- the method list --------------------------------------------------- */

.methods {
  margin: 56px 0 0;
  padding: 0;
  max-width: 900px;
  list-style: none;
  border-top: 1px solid var(--rule);
}
.method {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 32px;
  padding: 24px 2px;
  border-bottom: 1px solid var(--line);
  color: inherit;
}
.method:hover { text-decoration: none; }
.method .what { display: flex; align-items: center; gap: 22px; }
/* Reserved for the rail's own mark, and empty until we hold the files. A brand
   mark redrawn from memory is wrong in a way that looks deliberate.

   Hidden until then. Empty, at the head of a row, in a list you pick one item
   from, it reads as an unticked checkbox — the screen looked like a form you
   have to complete before the arrow will work. The row leads with its name. */
.method .glyph {
  display: none;
  flex: none;
  width: 30px;
  height: 30px;
  border: 1px solid var(--rule);
  border-radius: 2px;
}
.method .named { display: grid; gap: 8px; }
.method .name { font: 500 22px/1.2 var(--sans); color: var(--ink-strong); }
.method .sub { font: 400 15.5px/1.5 var(--sans); letter-spacing: 0; color: var(--ink-dim); }
.method .how { flex: none; display: flex; align-items: baseline; gap: 20px; }
.method .cur {
  font: 400 13px/1 var(--mono);
  letter-spacing: 0.05em;
  color: var(--ink-mid);
}
.method .go { font: 400 14px/1 var(--mono); color: var(--ink-mid); }

/* Hover lands on the name, in the accent, the same underline the rail's
   current screen carries. The row does not fill or shift: a list you are
   scanning should not move under the cursor. */
.method:hover .name {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 7px;
}
.method:hover .cur, .method:hover .go { color: var(--ink-mid); }

/* A rail with no rate to convert at. Dimmed rather than removed: which methods
   exist is a fact about the product, and a list that silently loses two of
   three reads as a product with one way to pay. */
.method.off { cursor: default; }
/* Unavailable, not absent. At --ink-faint's old 0.34 two of four methods were
   invisible rather than dark, and which methods exist is a fact about the
   product. */
.method.off .name { color: rgba(var(--ink-rgb), 0.60); }
.method.off .sub, .method.off .cur { color: rgba(var(--ink-rgb), 0.62); }
.method.off .glyph { border-color: var(--line); }

.topup .error { max-width: 34rem; }
.methods + .error { margin-top: 34px; font-weight: 500; }

/* ---- the amount -------------------------------------------------------- */

.paying {
  margin-top: 48px;
  max-width: 900px;
  padding: 22px 2px;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 32px;
}
.paying .lhs { display: grid; gap: 8px; }
.paying .name { font: 500 22px/1.2 var(--sans); color: var(--ink-strong); }
.paying .rhs { flex: none; display: flex; align-items: baseline; gap: 24px; }
.paying .cur, .paying .rhs a {
  font: 400 12px/1 var(--mono);
  letter-spacing: 0.075em;
}
.paying .cur { color: var(--ink-dim); }

.amount-form { margin: 34px 0 0; max-width: 900px; }

/* 24px, not the 15px the minting form uses. It is the only input in this flow
   and the only place a typo costs real money; at the minting form's size it
   would be the smallest thing on a screen about a payment.

   The currency prefix is a sibling rather than a placeholder, so the unit
   stays on screen while the tenant types over it. */
.money {
  display: inline-flex;
  align-items: stretch;
  margin-top: 12px;
  width: 26rem;
  max-width: 100%;
  border: 1px solid var(--rule);
  border-radius: 2px;
  transition: border-color 220ms ease;
}
.money:focus-within { border-color: var(--accent); }
.money .sym {
  display: flex;
  align-items: center;
  padding: 0 6px 0 16px;
  font: 400 24px/1 var(--mono);
  color: var(--ink-dim);
}
.money input {
  flex: 1;
  min-width: 0;
  margin: 0;
  padding: 16px 16px 16px 2px;
  background: transparent;
  border: 0;
  outline: none;
  color: var(--ink-strong);
  font: 400 24px/1.2 var(--mono);
  font-variant-numeric: tabular-nums;
}
.money input:focus { border: 0; }

.acts { display: flex; align-items: center; gap: 26px; flex-wrap: wrap; margin: 0; }
.amount-form .acts { margin-top: 34px; }
.topup .acts button { padding: 13px 26px; font-size: 13px; color: var(--ink-strong); }
.topup .acts a:not(.step) { font: 400 13px/1 var(--mono); letter-spacing: 0.075em; }

/* ---- confirming -------------------------------------------------------- */

.confirm { margin-top: 52px; max-width: 760px; display: grid; gap: 38px; }
.leg { display: grid; gap: 14px; }
/* The rule between the two legs is the conversion made visible: what leaves on
   one side of it, what arrives on the other. */
.legs .split { height: 1px; background: var(--rule); margin: 30px 0; }
.leg .figure {
  margin: 0;
  font: 400 clamp(34px, 1.5rem + 2.4vw, 48px) / 1 var(--mono);
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-strong);
}
.leg .under { margin: 0; font: 400 15.5px/1.5 var(--sans); color: var(--ink-mid); }

.topup .rate {
  margin: 0;
  font: 400 13px/1.7 var(--mono);
  letter-spacing: 0.04em;
  color: var(--ink-dim);
}
.topup .says {
  margin: 0;
  max-width: 34rem;
  font: 400 16.5px/1.7 var(--sans);
  letter-spacing: 0;
  color: var(--ink-mid);
  text-wrap: pretty;
}
.confirm .says { font-size: 17px; line-height: 1.6; letter-spacing: 0.004em; }

/* ---- coming back ------------------------------------------------------- */

.outcome { margin-top: 52px; max-width: 760px; display: grid; gap: 30px; }
.headline + .outcome { margin-top: 52px; gap: 28px; }
.outcome .lede {
  margin: 0;
  max-width: 34rem;
  font: 400 19px/1.55 var(--sans);
  color: var(--ink-strong);
  text-wrap: pretty;
}
.receipt { border-top: 1px solid var(--rule); padding-top: 24px; display: grid; gap: 12px; }
.receipt .line, .pending .line {
  margin: 0;
  font: 400 13px/1.7 var(--mono);
  letter-spacing: 0.06em;
  color: var(--ink-mid);
}

/* The one amber rule in the flow, and the reason cancelled has none: a rule
   down the left side reads as a fault, and coming back without paying is not
   one. --warn, not --flag: --flag marks a figure that misleads, and this is a
   sentence about an event. */
.outcome .fault {
  margin: 0;
  max-width: 34rem;
  padding-left: 14px;
  border-left: 1px solid var(--warn);
  font: 400 16px/1.7 var(--sans);
  letter-spacing: 0;
  color: var(--warn);
  text-wrap: pretty;
}

.pending .paid { display: grid; gap: 16px; }
.pending .figure {
  margin: 0;
  font: 400 clamp(34px, 1.5rem + 2.4vw, 48px) / 1 var(--mono);
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-strong);
}
/* One hairline crossing under the figure, at the pace the homepage's own light
   crosses the water. It carries no percentage: we do not have one, and a bar
   that guesses is worse than a sentence that does not. Decorative, so it is
   aria-hidden in the markup and stops entirely under reduced motion — the
   sentence beside it already says everything this says. */
.pending .sweep {
  position: relative;
  width: 100%;
  max-width: 34rem;
  height: 1px;
  background: var(--line);
  overflow: hidden;
}
.pending .sweep span {
  position: absolute;
  inset: 0 auto 0 0;
  width: 20%;
  background: var(--accent);
  animation: nf-cross 6.5s var(--ease-out-expo) infinite;
}
@keyframes nf-cross {
  from { transform: translateX(-100%); }
  55%, 100% { transform: translateX(400%); }
}

/* ---- the history, on the overview -------------------------------------- */

.topups { margin-top: 56px; display: grid; gap: 16px; }
.topups header { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
.topups .span {
  margin: 0;
  font: 400 12px/1 var(--mono);
  letter-spacing: 0.05em;
  color: var(--ink-dim);
}
.topups .scroll { margin: 0; }
.topups table { min-width: 32rem; }
.topups th.added, .topups td.added { text-align: right; }
.topups td:last-child, .topups th:last-child { padding-right: 0; }
.topups td.who, .topups td.state {
  font-family: var(--sans);
  font-size: 15px;
  letter-spacing: 0;
}
.topups td.state { font-size: 14.5px; color: var(--ink-mid); }
.topups td.added { font-size: 15px; letter-spacing: 0.02em; }
.topups td.added .lead { display: block; color: var(--ink-strong); }
/* A row still clearing is not in the balance above it, so its figure is dimmed
   to the weight of a note rather than printed at the weight of a fact. */
.topups tr:has(.moving) td.added .lead { color: var(--ink-dim); }
.topups td.added .also {
  display: block;
  font-size: 12px;
  letter-spacing: 0.05em;
  color: var(--ink-dim);
}
/* The same hairline as the pending screen, so the word and the state read as
   one thing rather than two that happen to agree. */
.topups .moving {
  display: inline-block;
  position: relative;
  padding-bottom: 5px;
  overflow: hidden;
  color: var(--ink-mid);
}
.topups .moving span {
  position: absolute;
  inset: auto auto 0 0;
  width: 20%;
  height: 1px;
  background: var(--accent);
  animation: nf-cross 5.2s var(--ease-out-expo) infinite;
}
.topups .hint { margin: 0; font: 400 14px/1.5 var(--sans); color: var(--ink-dim); }

@media (max-width: 900px) {
  .headline .add-funds { align-self: center; }
  .methods { margin-top: 40px; }
  /* The currency line moves into the row: "Charged in USD" under a stacked
     name is a second line on every option, where the bare code at the right
     end does the same work in the space a phone has. */
  .method { gap: 18px; }
  .method .sub { display: none; }
  .method .go { display: none; }
  .paying { margin-top: 36px; flex-wrap: wrap; gap: 18px; }
  /* Full width, and the padding goes up: both put the field and the button
     over 44px, which a money field on a phone is worth. */
  .money { width: 100%; }
  .money input { padding: 13px 14px 13px 2px; }
  .topup .acts button, .topup .acts .step { width: 100%; text-align: center; }
  .confirm, .outcome { margin-top: 40px; }
  .topups { margin-top: 40px; }
}

/* ===========================================================================
   The user-system screens — /login, /signup, /account and the three account
   sub-screens.

   What the redesign changes is hierarchy, not palette. The gate's four
   floating links were all the same weight, so nothing said which way in was
   the ordinary one; they become a labelled provider pair, a rule, the email
   fallback, and a footer carrying the two ways out. On /account the three bare
   links become a list whose destinations you can see before you click.

   Every colour here is a token. The design canvas is dark-only; light is
   derived from the existing light palette rather than invented, which is why
   nothing below states a hex.
   =========================================================================== */

/* The provider mark we do not hold yet, at the size the real one will take.
   Same reasoning as the top-up rows' empty square (_method_row): a brand
   redrawn from memory is wrong in a way that looks deliberate. Drop the
   official SVG in and this span is what it replaces. */
.mono-mark {
  flex: none;
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  border: 1px solid var(--line);
  border-radius: 2px;
  font: 500 9.5px/1 var(--mono);
  letter-spacing: 0.02em;
  color: var(--ink-dim);
}

/* --- the gate card ------------------------------------------------------ */

/* Was a bare grid of controls over the water. The glass is what lets the
   field group have a box at all: four rules floating on the scene read as
   debris, the same four inside a panel read as one control. */
.gate .gate-form {
  display: block;
  width: min(448px, 88vw);
  padding: 0;
  gap: 0;
  background: var(--panel);
  backdrop-filter: blur(26px) saturate(1.15);
  -webkit-backdrop-filter: blur(26px) saturate(1.15);
  border: 1px solid var(--panel-edge);
  border-radius: 5px;
  box-shadow: var(--panel-shade);
}
.gate .gate-body {
  display: grid;
  gap: 24px;
  padding: 36px 34px 28px;
}
.gate .gate-body > h1 { margin: 0; font-size: clamp(26px, 2.6vw, 38px); }
/* The submit is a bar, not a chip: it is the one thing on the card that
   finishes the job, and at the quiet outline weight it lost to the two
   provider buttons above it. */
.gate .gate-body form { margin: 0; display: grid; gap: 16px; justify-items: stretch; }
.gate .gate-body form > button[type="submit"] {
  width: 100%;
  height: 50px;
  padding: 0;
  background: var(--accent);
  border-color: var(--accent);
  font: 500 13px/1 var(--mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bg);
}
.gate .gate-body form > button[type="submit"]:hover {
  background: var(--accent-lit);
  border-color: var(--accent-lit);
  color: var(--bg);
}
/* The quiet alternative under a primary one — Sign out under Send
   verification email. Same size, no fill, so the pair reads as one choice. */
.gate .gate-body form > button.quiet {
  width: 100%;
  height: 44px;
  padding: 0;
  font: 400 12.5px/1 var(--mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

/* The two ways out of the page, on one rule at the bottom, so neither is
   mistaken for a way in. */
.gate .gate-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px 34px;
  border-top: 1px solid var(--panel-edge);
  background: var(--accent-wash);
  font: 400 13.5px/1.4 var(--sans);
}
.gate .gate-foot form { margin: 0; }
.gate .gate-foot > span { color: var(--ink-faint); }

/* --- providers ---------------------------------------------------------- */

.providers { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.provider {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  height: 50px;
  padding: 0 10px;
  background: var(--accent-wash);
  border: 1px solid var(--line);
  border-radius: 3px;
  font: 400 14.5px/1 var(--sans);
  color: var(--ink-strong);
  text-align: center;
}
.provider:hover {
  border-color: var(--accent-lit);
  color: var(--ink);
  text-decoration: none;
  box-shadow: var(--glow-control);
}

/* "or with email" — the rule says the pair above is the ordinary way in and
   this is the fallback, which four equal links could not say. */
.orline { display: flex; align-items: center; gap: 14px; }
.orline::before, .orline::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--rule-soft);
}
.orline span {
  font: 500 10.5px/1 var(--mono);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* --- the joined field group --------------------------------------------- */

/* One box, one hairline between the two fields. The label is a column rather
   than a line above, which is where the extra characters of room came from:
   38 to 44 without the card getting wider. */
.fieldgroup {
  display: grid;
  background: var(--accent-wash);
  border: 1px solid var(--line);
  border-radius: 3px;
  overflow: hidden;
}
.gate .fieldgroup label {
  display: flex;
  align-items: center;
  gap: 0;
  max-width: none;
  margin: 0;
  border-bottom: 1px solid var(--rule-soft);
}
.gate .fieldgroup label:last-child { border-bottom: 0; }
.gate .fieldgroup label .tag {
  flex: none;
  width: 96px;
  padding-left: 15px;
  font: 500 11px/1 var(--mono);
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.gate .fieldgroup input[type="email"],
.gate .fieldgroup input[type="password"],
.gate .fieldgroup input[type="text"] {
  flex: 1;
  min-width: 0;
  height: 52px;
  margin: 0;
  padding: 0 15px 0 0;
  background: transparent;
  border: 0;
  font: 400 15px/1 var(--mono);
  letter-spacing: 0.01em;
  color: var(--ink-strong);
}
/* The focus ring goes on the row, not the input: an outline on a borderless
   input inside a box draws a second rectangle just inside the first. */
.gate .fieldgroup input:focus { outline: none; border-bottom-color: transparent; }
.gate .fieldgroup label:focus-within { background: var(--accent-wash); }
.gate .fieldgroup label:focus-within .tag { color: var(--ink-mid); }

/* --- the reveal toggle --------------------------------------------------- */

/* Ships with the `hidden` attribute and reveal.js takes it off, so a reader
   with no JavaScript gets an ordinary password field instead of a button that
   does nothing. Named .peek because .reveal is already the one-time key
   screen's body class. */
.peek {
  flex: none;
  margin: 0 9px 0 0;
  height: 30px;
  padding: 0 11px;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 2px;
  font: 400 11px/1 var(--mono);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-dim);
  cursor: pointer;
}
.peek:hover { border-color: var(--accent-lit); color: var(--ink); box-shadow: none; }
/* Stacked fields (signup, change password) wrap the input and the button in a
   span instead of a label row; the button sits over the field's right end. */
.field-peek { position: relative; display: block; }
.field-peek input { width: 100%; padding-right: 78px; }
.field-peek .peek { position: absolute; top: 8px; right: 8px; margin: 0; }

/* --- app screens: sections ---------------------------------------------- */

/* The tag column is what the flat form was missing: with no label beside it,
   a display-name field and a password link were the same kind of thing on the
   same page, and nothing grouped either. */
.section {
  margin-top: 52px;
  display: grid;
  grid-template-columns: 172px minmax(0, 1fr);
  gap: 0 44px;
  border-top: 1px solid var(--rule);
  padding-top: 28px;
}
.section-tag {
  margin: 0;
  font: var(--tag);
  line-height: 1.5;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.section-body { display: flex; flex-direction: column; gap: 26px; max-width: 420px; }
.section-body form { margin: 0; }
.section-body > p { margin: 0; font: 400 14px/1.6 var(--sans); color: var(--ink-dim); }
.section-body .field { margin: 0; }
/* A submit and the link beside it — Save password next to Confirm your
   identity — on one line, because they are alternatives at the same moment. */
.section-acts { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
.section-acts form { margin: 0; }

.crumbs {
  margin: 0 0 22px;
  display: flex;
  align-items: center;
  gap: 9px;
  font: 400 13px/1 var(--sans);
  color: var(--ink-faint);
}
.crumbs > span:last-child { color: var(--ink-mid); }

/* The address and its state, on the title's line. It is who you are looking
   at, which is a different fact from what the screen is called. */
.head-id { display: flex; align-items: center; gap: 12px; margin-left: auto; }
.head-id > .addr {
  font: 400 14px/1 var(--mono);
  letter-spacing: 0.03em;
  color: var(--ink-mid);
  overflow-wrap: anywhere;
}

.pill {
  display: inline-flex;
  align-items: center;
  height: 26px;
  padding: 0 11px;
  border: 1px solid var(--accent);
  border-radius: 13px;
  background: var(--accent-wash);
  font: 500 11px/1 var(--mono);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--accent-lit);
  white-space: nowrap;
}
.pill.warn {
  border-color: var(--flag-edge);
  background: var(--flag-wash);
  color: var(--flag-ink);
}

/* --- app screens: the link list ----------------------------------------- */

/* Three bare links became three rows with a destination you can see. The
   arrow is the only thing that says "this leaves the page", and it is why
   these are not buttons. */
.rows { display: flex; flex-direction: column; max-width: 560px; }
.rows .row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 19px 2px;
  border-bottom: 1px solid var(--rule-soft);
  font: 400 16.5px/1.3 var(--sans);
  color: var(--ink-strong);
}
.rows .row:hover { text-decoration: none; color: var(--ink); border-bottom-color: var(--accent); }
.rows .row .go { font: 400 15px/1 var(--mono); color: var(--ink-faint); }
.rows .row .marks { display: flex; align-items: center; gap: 10px; }
.rows .row .marks .mono-mark { width: 18px; height: 18px; opacity: 0.85; }

/* --- app screens: connected sign-ins ------------------------------------ */

/* Deliberately NOT .method: those are the top-up payment rows and carry a
   phone rule that hides their .sub, which here is the sentence saying what
   disconnecting costs you. */
.signins { margin-top: 0; max-width: 660px; border-top: 1px solid var(--rule); }
.signin {
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 22px 2px;
  border-bottom: 1px solid var(--rule-soft);
}
.signin > .mono-mark {
  width: 38px;
  height: 38px;
  border-radius: 3px;
  font-size: 12px;
}
.signin-what { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.signin-name { display: flex; align-items: center; gap: 10px; }
.signin-what .name { font: 400 16.5px/1.3 var(--sans); color: var(--ink-strong); }
.signin-what .sub { font: 400 13.5px/1.4 var(--sans); color: var(--ink-faint); }
.signin-acts { flex: none; display: flex; gap: 10px; align-items: center; }
.signin-acts form { margin: 0; }
.signin.off > .mono-mark { opacity: 0.6; }
.signin.off .name { color: var(--ink-mid); }

/* One outline control, used by every row that has an action of its own.

   Element-qualified, NOT a bare `.act`: /keys renders `<td class="act">` for
   its Copy and Rotate column (pages.py), and a bare rule turned that cell
   into a 38px bordered grid inside a td that still carries 20px of padding.
   The row list and the keys table are allowed to share a word; they are not
   allowed to share a box. */
a.act, button.act {
  display: grid;
  place-items: center;
  height: 38px;
  padding: 0 18px;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 3px;
  font: 400 12.5px/1 var(--mono);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-mid);
  cursor: pointer;
}
a.act:hover, button.act:hover {
  border-color: var(--accent-lit);
  color: var(--ink);
  text-decoration: none;
  box-shadow: var(--glow-control);
}
/* Amber, because it is the one action on the screen that takes something
   away and signs every session out. */
a.act.warn, button.act.warn { border-color: var(--flag-edge); color: var(--flag-ink); }
a.act.warn:hover, button.act.warn:hover { border-color: var(--flag); color: var(--flag-ink); box-shadow: none; }

/* The address on /signup/pending, with its state beside it. */
.status-row {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: var(--accent-wash);
}
.status-row .dot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--ink-faint); }
.status-row .dot.warn { background: var(--warn); }
.status-row .addr {
  font: 400 14.5px/1 var(--mono);
  letter-spacing: 0.02em;
  color: var(--ink-strong);
  overflow-wrap: anywhere;
}

@media (max-width: 900px) {
  /* The tag stops being a column and becomes a heading. At 172px + 44px the
     grid leaves a phone about nothing for the controls it is labelling. */
  .section {
    grid-template-columns: minmax(0, 1fr);
    gap: 18px;
    margin-top: 40px;
  }
  .section-body { max-width: none; }
  .head-id { margin-left: 0; flex-wrap: wrap; gap: 10px; }
  /* Stacked: the name and its state stay on one line, the actions drop under
     the note rather than squeezing it to nothing. */
  .signin { flex-wrap: wrap; gap: 14px; }
  .signin-what { flex-basis: 100%; order: 2; }
  .signin-acts { order: 3; margin-left: auto; }
  .providers { grid-template-columns: 1fr; }
  .gate .gate-body { padding: 28px 22px 22px; }
  .gate .gate-foot { padding: 14px 22px; }
  .gate .fieldgroup label .tag { width: 78px; padding-left: 12px; }
}
