/* ===========================================================================
   KỆ & GIẤY — "Rack & Paper".  Phase 1: tokens, the width law, focus, hover.

   Phase 1 changes THIS FILE ONLY. No markup moves, so the Vietnamese substring
   translator, the billing engine and every test are untouched by construction.

   The old token names are kept as aliases onto the new ones, so all ~400 rules
   below adopt the new palette without being rewritten. Later phases retire them.
   =========================================================================== */
:root {
  color-scheme: light dark;

  /* — surfaces: cool ground, white cards ————————————————————————————
       Phase 7. The warm paper read as a document; a shop reads as glass. */
  --paper: #F4F6F9;
  --surface: #FFFFFF;
  --surface-2: #F8FAFC;
  --surface-3: #EDF1F6;
  --surface-hover: #F4F7FB;

  /* — ink ————————————————————————————————————————————————————————— */
  --ink: #0F1B2A;     /* 16.8:1 on white */
  --ink-2: #38465A;   /*  9.4:1 — labels, secondary values */
  --ink-3: #5A6A80;   /*  5.4:1 — captions and hints */

  /* — one accent, three money tones ——————————————————————————————— */
  --accent: #0A6FC2;        /* 5.3:1 on white, and 4.6:1 for the white text on it */
  --accent-hover: #08558F;
  --accent-quiet: #E8F1FD;
  --price: #0F1B2A;         /* dong figures only */
  --ok: #0C6A4C;            /* 6.6:1 — paid, available, in stock */
  --ok-quiet: #E1F0E7;
  --warn: #8A5500;          /* 6.2:1 — due today, pending, reserved */
  --warn-quiet: #FAEFD4;
  --bad: #A8221A;           /* 7.2:1 — overdue, destructive */
  --bad-quiet: #FBE9E5;
  --bad-solid: #A8221A;
  --on-solid: #FFFFFF;

  /* — boundaries. --line divides things inside one surface; --line-strong is
       the only border allowed on a control, and clears WCAG 1.4.11 at 3.9:1. */
  --line: #E2E8F0;
  --line-strong: #8595AB;
  --focus: #0A6FC2;
  --slab: #0B1B2B;

  /* — compatibility aliases ————————————————————————————————————————
     Old names, new values. --text and --fg were REFERENCED by three rules and
     never defined, so those declarations were silently dropped; they work now. */
  --bg: var(--paper);
  --panel: var(--surface);
  --muted: var(--ink-3);
  --blue: var(--accent);
  --green: var(--ok);
  --amber: var(--warn);
  --red: var(--bad);
  --soft-blue: var(--accent-quiet);
  --soft-green: var(--ok-quiet);
  --soft-amber: var(--warn-quiet);
  --soft-red: var(--bad-quiet);
  --dash: var(--ink-3);
  --text: var(--ink);
  --fg: var(--ink);

  /* — spacing: eight steps on a 4px grid, replacing 27 ad-hoc values ————— */
  --s-1: 4px;  --s-2: 8px;  --s-3: 12px; --s-4: 16px;
  --s-5: 24px; --s-6: 32px; --s-7: 48px; --s-8: 64px;
  --control-h: 40px;
  --control-chrome: calc(var(--s-3) * 2 + 2px);   /* padding-inline + 1px borders */

  /* — type ———————————————————————————————————————————————————————
     Inter was named first and has never been shipped, so every weight was being
     synthesised — which thickens a Vietnamese tone mark until it merges with the
     glyph above. These faces carry PRECOMPOSED ế ộ ữ ầ ợ at one designed height. */
  --font: "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, Roboto,
          "Noto Sans", "Liberation Sans", Arial, sans-serif;
  --mono: ui-monospace, "Cascadia Mono", "Segoe UI Mono", "SF Mono", Menlo, Consolas, monospace;
  --fs-xs: 13px; --fs-sm: 14px; --fs-base: 15px;
  --fs-md: 17px; --fs-lg: 20px; --fs-xl: 24px;

  /* — shape and elevation ——————————————————————————————————————— */
  --r-sm: 6px;    /* anything you click or type into */
  --r-md: 10px;   /* any container */
  --r-lg: 14px;   /* feature surfaces */
  --r-pill: 999px;
  --bd: 1px solid var(--line);
  --bd-control: 1px solid var(--line-strong);
  --e-1: 0 1px 2px rgba(45,34,24,.06), 0 1px 3px rgba(45,34,24,.05);
  --e-2: 0 2px 4px rgba(45,34,24,.06), 0 8px 20px -6px rgba(45,34,24,.10);
  --e-3: 0 14px 36px -10px rgba(45,34,24,.24);
  --shadow-1: var(--e-1);
  --shadow-2: var(--e-2);
  --shadow-3: var(--e-3);

  /* — elevation: exactly three ————————————————————————————————————
       0 sunken   — sits INSIDE another surface. No shadow; the tint is the signal.
       1 raised   — a container resting on paper. This is most of the application.
       2 floating — over the page: modal sheets, the copy sheet, a sticky bar.
     The hover step is deliberately between 1 and 2, so lifting a card is a move
     along this scale rather than a fourth effect nobody else uses. Containers pick
     a LEVEL; they no longer each invent a shadow. */
  --lift-0: none;
  --lift-1: var(--e-1);
  --lift-2: var(--e-3);
  --lift-hover: var(--e-2);

  /* — density ————————————————————————————————————————————————————
     Four values, and only four. These are the defaults — the public shop, which
     can afford to breathe. The signed-in console overrides all four below. */
  --d-page: var(--s-5);   /* the page's own margin against the viewport */
  --d-pad: var(--s-5);    /* padding inside a raised container */
  --d-gap: var(--s-4);    /* between containers, and between cards in a grid */
  --d-cell: var(--s-3);   /* a table cell's vertical padding */

  --dur-1: 90ms; --dur-2: 160ms; --ease: cubic-bezier(.2,.6,.3,1);
}

@media screen and (prefers-color-scheme: dark) {
  :root {
    --paper: #0B1220; --surface: #131C2B; --surface-2: #172234; --surface-3: #1E2B3F;
    --surface-hover: #1B2739;
    --ink: #F1F5F9; --ink-2: #C7D2E0; --ink-3: #94A3B8;
    --accent: #7CBAF5; --accent-hover: #A3D0FA; --accent-quiet: #16314C;
    --price: #E4EEF9;
    --ok: #58CFA4; --ok-quiet: #10281F;
    --warn: #E6B45C; --warn-quiet: #2C2210;
    --bad: #FF9184; --bad-quiet: #33191A; --bad-solid: #FF9184;
    /* the fill inverts: a dark plate on a dark ground stops reading as hot */
    --on-solid: #0B1220;
    --line: #263449; --line-strong: #64748B; --focus: #7CBAF5;
    --slab: #060C16;
    --e-1: 0 1px 2px rgba(0,0,0,.5);
    --e-2: 0 2px 6px rgba(0,0,0,.55), 0 8px 20px -6px rgba(0,0,0,.45);
    --e-3: 0 18px 44px -10px rgba(0,0,0,.7);
  }
}

/* ── density: the console tightens ──────────────────────────────────────────
   .user-chip is rendered by layout() only when someone is signed in, and
   .site-footer only when nobody is — so this is the shop/console split exactly,
   with no new class and no app.py change. (main.wide, which the proposal named,
   is never emitted: `wide=True` appears nowhere in app.py.)

   Written as an override of the roomy default rather than the other way round, so
   a browser without :has() falls back to comfortable rather than to cramped. */
body:has(.user-chip) {
  --d-page: var(--s-4);
  --d-pad: var(--s-3);
  --d-gap: var(--s-2);
  --d-cell: var(--s-2);
}

/* ── the theme the visitor chose ────────────────────────────────────────────
   data-theme is stamped on <html> by layout(). It has to beat prefers-color-scheme in
   BOTH directions, so light is restated as well as dark — otherwise a visitor on a dark
   phone who picks Light gets the media query back. No attribute at all means "auto",
   and the device decides. color-scheme is set too so form controls and scrollbars follow. */
:root[data-theme="light"] {
  --paper: #F4F6F9; --surface: #FFFFFF; --surface-2: #F8FAFC; --surface-3: #EDF1F6;
  --surface-hover: #F4F7FB;
  --ink: #0F1B2A; --ink-2: #38465A; --ink-3: #5A6A80;
  --accent: #0A6FC2; --accent-hover: #08558F; --accent-quiet: #E8F1FD;
  --price: #0F1B2A;
  --ok: #0C6A4C; --ok-quiet: #E1F0E7;
  --warn: #8A5500; --warn-quiet: #FAEFD4;
  --bad: #A8221A; --bad-quiet: #FBE9E5; --bad-solid: #A8221A;
  --on-solid: #FFFFFF;
  --line: #E2E8F0; --line-strong: #8595AB; --focus: #0A6FC2;
  --slab: #0B1B2B;
  --e-1: 0 1px 2px rgba(45,34,24,.06), 0 1px 3px rgba(45,34,24,.05);
  --e-2: 0 2px 4px rgba(45,34,24,.06), 0 8px 20px -6px rgba(45,34,24,.10);
  --e-3: 0 14px 36px -10px rgba(45,34,24,.24);
  color-scheme: light;
}
@media screen {
/* The dark palette — applied to the whole document when dark is in force, and to a
   slab region (a near-black band on any page) via the same rule, so a light page can
   carry a dark band without a second copy of these values drifting somewhere else. */
:root[data-theme="dark"], .hero {
    --paper: #0B1220; --surface: #131C2B; --surface-2: #172234; --surface-3: #1E2B3F;
    --surface-hover: #1B2739;
    --ink: #F1F5F9; --ink-2: #C7D2E0; --ink-3: #94A3B8;
    --accent: #7CBAF5; --accent-hover: #A3D0FA; --accent-quiet: #16314C;
    --price: #E4EEF9;
    --ok: #58CFA4; --ok-quiet: #10281F;
    --warn: #E6B45C; --warn-quiet: #2C2210;
    --bad: #FF9184; --bad-quiet: #33191A; --bad-solid: #FF9184;
    /* the fill inverts: a dark plate on a dark ground stops reading as hot */
    --on-solid: #0B1220;
    --line: #263449; --line-strong: #64748B; --focus: #7CBAF5;
    --slab: #060C16;
    --e-1: 0 1px 2px rgba(0,0,0,.5);
    --e-2: 0 2px 6px rgba(0,0,0,.55), 0 8px 20px -6px rgba(0,0,0,.45);
    --e-3: 0 18px 44px -10px rgba(0,0,0,.7);
  color-scheme: dark;
}
}

/* The control: a disc that is filled, half-filled or split, drawn in CSS so nothing
   depends on an icon font and the visible name of the mode stays a single translatable
   text node inside .sr-only (and in the title/aria-label, which ARE translated). */
.btn.theme-toggle { min-width: 44px; padding-inline: var(--s-3); }
.theme-toggle::before {
  content: ""; width: 15px; height: 15px; border-radius: var(--r-pill);
  border: 2px solid currentColor; flex: 0 0 auto; box-sizing: border-box;
}
.theme-toggle.theme-light::before { background: currentColor; }
.theme-toggle.theme-dark::before { background: transparent; box-shadow: inset -4px -1px 0 0 currentColor; }
.theme-toggle.theme-auto::before { background: linear-gradient(90deg, currentColor 50%, transparent 50%); }

/* A finger gets a bigger control than a mouse pointer does. */
@media (pointer: coarse) { :root { --control-h: 44px; } }

* { box-sizing: border-box; }
small { font-size: var(--fs-xs); }
body {
  margin: 0;
  font-family: var(--font);
  /* Never let the browser fake a weight: synthesis smears the tone mark on ế ộ ữ. */
  font-synthesis-weight: none;
  font-size: var(--fs-base);
  line-height: 1.6;
  background: var(--bg);
  color: var(--ink);
  -webkit-text-size-adjust: 100%;
}
a { color: inherit; text-decoration: none; }

/* Nothing in 798 lines had a focus style. Keyboard users had no idea where they were. */
:where(a, button, .btn, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

header {
  background: var(--surface);
  border-bottom: var(--bd);
  position: sticky;
  top: 0;
  z-index: 5;
}

/* ── the duty strip ─────────────────────────────────────────────────────────
   One line under the nav answering "what needs me today", on every staff page.

   It sits INSIDE <header>, which is already sticky, so it needs no sticky context
   and — more to the point — no hard-coded top offset. The one place that ever did
   hard-code an offset against the header's height (.due-tab-labels) broke the moment
   the header lost 4px in phase 3, and broke again the moment THIS strip added 49px.
   It no longer holds a constant: see the note on .due-tab-labels below.

   Fixed at one line tall whatever it contains: it is chrome on a phone screen, so
   its cost has to be predictable. Too many items scroll sideways rather than wrap.

   Only what needs attention is rendered — app.py omits a zero entirely — so this is
   never a row of noughts, and "nothing needs you" is a state the strip can be in. */
.duty {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  max-width: 1280px;
  margin: 0 auto;
  padding: var(--s-2) var(--d-page);
  border-top: var(--bd);
  overflow-x: auto;
  scrollbar-width: none;
  white-space: nowrap;
  font-size: var(--fs-xs);
}
body:has(main.wide) .duty { max-width: 1440px; }
.duty::-webkit-scrollbar { display: none; }
.duty-item {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s-2);
  padding: var(--s-1) var(--s-2);
  border-radius: var(--r-sm);
  color: var(--ink-2);
  text-decoration: none;
  font-weight: 600;
}
.duty-item:hover { background: var(--surface-hover); }
.duty-item b {
  font-size: var(--fs-base);
  font-weight: 700;
  font-variant-numeric: tabular-nums slashed-zero;
  color: var(--tone, var(--ink));
}
.duty-item .duty-amt { color: var(--ink-3); font-weight: 600; }
/* the same escalation the chips use, so a number here means what a chip there means */
.duty-late { --tone: var(--bad); }
.duty-now { --tone: var(--warn); }
.duty-new { --tone: var(--accent); }
.duty-clear { color: var(--ok); font-weight: 600; }
@media (max-width: 560px) {
  /* The header is sticky, so on a phone every pixel of it is a pixel of the fleet you
     cannot see. The strip earns its place; the name badge does not — you know who you
     are signed in as, and it is on the account page. Hiding it is display only, so
     body:has(.user-chip) still matches and the density split is unaffected. */
  .user-chip { display: none; }
  /* The amount is the point of this strip — how much, not just how many — so it
     stays on the phone and the row tightens around it instead. Measured in
     Vietnamese (the longer labels) against the live figures. */
  .duty { gap: var(--s-2); padding-block: var(--s-1); }
  .duty-item { gap: var(--s-1); padding-inline: 0; }
  .duty-item b { font-size: var(--fs-xs); }
  /* the digits stay, the unit goes: it is the only figure of that size on the row,
     and it is the difference between fitting on one line and having to swipe */
  .duty-unit { display: none; }
}

.bar {
  max-width: 1280px;
  margin: 0 auto;
  /* the height is fixed — chrome should not change size with page density — but the
     SIDE padding is the page rail, and the nav has to line up with the panels below
     it, so the horizontal axis tracks --d-page exactly as main does. */
  padding: var(--s-3) var(--d-page);
  /* main.wide widens the page to 1440 on staff screens. The header has to widen
     with it or the nav sits 80px inside the panels on any monitor over 1280 —
     which is where this was measured wrong the first time, at exactly 1280. */
  display: flex;
  align-items: center;
  gap: var(--s-4);
  justify-content: space-between;
}
.brand { font-weight: 700; font-size: 18px; }
nav { display: flex; gap: var(--s-2); flex-wrap: wrap; }
main { max-width: 1280px; margin: 0 auto;
  padding: var(--d-page) var(--d-page) var(--s-7); }

/* ── buttons ────────────────────────────────────────────────────────────────
   Nothing responded to the pointer before: there was no :hover rule in the file. */
.btn, button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: var(--control-h);
  border: var(--bd-control);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  padding: var(--s-2) var(--s-4);
  font: 600 var(--fs-sm)/1.4 var(--font);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease),
              border-color var(--dur-1) var(--ease),
              color var(--dur-1) var(--ease),
              box-shadow var(--dur-1) var(--ease);
}
.btn:hover, button:hover { background: var(--surface-2); border-color: var(--ink-3); }
.btn:active, button:active { transform: translateY(1px); }
.btn.primary, button.primary {
  background: var(--accent); color: var(--on-solid); border-color: var(--accent); font-weight: 700;
}
.btn.primary:hover, button.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
/* Never filled: a solid red button invites the click it is warning about. */
.btn.danger, button.danger { color: var(--bad); border: 1.5px solid var(--bad); background: var(--surface); }
.btn.danger:hover, button.danger:hover { background: var(--bad-quiet); }
.btn.sm { min-height: 36px; padding-inline: var(--s-3); font-size: var(--fs-xs); }

/* ── the width law ──────────────────────────────────────────────────────────
   `input, select, textarea { width: 100% }` used to live here. It caused three
   logged bugs, eleven patches and six hand-picked field widths, because of TWO
   causes — and fixing only the famous one leaves the bug alive:

     A. width:100% makes a flex item demand the whole row.
     B. a flex item's default min-width:auto means it REFUSES to shrink below its
        intrinsic width. That is what actually let an input swallow its
        neighbouring button. width:auto alone does not fix it.

   So: controls are intrinsically sized and shrinkable, width is GRANTED by the
   container, and anything that holds data is sized in `ch` — a dong amount is
   eleven characters wide wherever it appears, including in forms not yet written. */
input, select, textarea { font: inherit; }
input, select, textarea, button, .btn { color: inherit; min-width: 0; }

input:where(:not([type=checkbox]):not([type=radio])), select {
  width: auto;
  min-height: var(--control-h);
  padding: var(--s-2) var(--s-3);
  border: var(--bd-control);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  transition: border-color var(--dur-1) var(--ease);
}
textarea {
  width: 100%;
  /* whole line boxes, so the last visible row is never sliced through the tone marks */
  min-height: calc(5 * 1.6em + var(--s-2) * 2 + 2px);
  padding: var(--s-2) var(--s-3);
  border: var(--bd-control);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  resize: vertical;
  line-height: 1.6;
}
input:hover, select:hover, textarea:hover { border-color: var(--ink-3); }
::placeholder { color: var(--ink-3); opacity: 1; }

/* A tick box is a different KIND of thing and opts out of sizing entirely. */
input[type=checkbox], input[type=radio] {
  width: 1.125rem; height: 1.125rem;
  min-width: 0; min-height: 0;
  flex: 0 0 1.125rem; padding: 0; margin: 0;
  accent-color: var(--accent);
}

/* Width is granted by the container, never taken by the control.
   Tick boxes are excluded INSIDE :where(), which costs zero specificity — so the grant can
   never match them however the file is reordered. Excluding them outside :where() would
   raise this selector and start a different cascade fight. */
.form-grid label > :where(input:not([type=checkbox]):not([type=radio]), select, textarea),
.stack label > :where(input:not([type=checkbox]):not([type=radio]), select, textarea),
label.full > :where(input:not([type=checkbox]):not([type=radio]), select, textarea),
.u-fill { width: 100%; }

/* Every flex row of controls declares that its children may shrink. Cause B. */
/* Previous / Next. A pair, so they read as one control and keep their order when the
   row around them wraps; the spent one is a span, not a link, so it cannot be tabbed
   to or followed. */
.step-bar { display: inline-flex; gap: var(--s-1); }
.step-bar .btn { white-space: nowrap; }
.toolbar > *, .inline > *, .actions > *, .due-edit-row > *, .jump-bar > *,
.status-actions > *, .sa-row > *, .step-bar > *, .plans-sort > *, .order-actions > *, .template-actions > *,
.history-pager > *, .refund-bar > * { min-width: 0; }

/* Width is a property of the DATA it holds.
   box-sizing is border-box globally, so a bare `11ch` would be 11ch of BOX — about 7.5
   characters of text. --control-chrome adds the padding and borders back, so `11ch`
   really does hold eleven characters. */
.w-amount { width: calc(11ch + var(--control-chrome)); text-align: right;
            font-variant-numeric: tabular-nums slashed-zero; }
.w-date { width: calc(12ch + var(--control-chrome)); }
.w-code { width: calc(9ch + var(--control-chrome)); }
.w-name { width: calc(24ch + var(--control-chrome)); }
.w-search { flex: 1 1 22ch; min-width: 0; max-width: 42ch; }

/* ── layout primitives that used to sit above `table {` ─────────────────── */
.grid { display: grid; gap: var(--s-3); }
.metrics { grid-template-columns: repeat(5, minmax(0, 1fr)); margin-bottom: var(--s-4); }
.metric {
  background: var(--panel);
  border: var(--bd);
  border-radius: var(--r-md);
  padding: var(--d-pad);
  box-shadow: var(--lift-1);
}
.metric span { display: block; color: var(--ink-3); font-size: var(--fs-xs); }
.metric strong { display: block; margin-top: 7px; font-size: var(--fs-xl); }
a.metric-link { text-decoration: none; color: inherit; cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease),
              box-shadow var(--dur-1) var(--ease); }
a.metric-link:hover { border-color: var(--accent); background: var(--accent-quiet); }
a.metric-link.active { border-color: var(--accent); background: var(--accent-quiet); box-shadow: inset 0 0 0 1px var(--accent); }
a.metric-link.active strong { color: var(--accent); }
/* must clear the STICKY header, which grows as the nav wraps. Measured: 114px at
   >=1100, up to 198.8px between 761 and 1099, and position:static at <=760 where
   84px is already ample. A single constant is what broke this. */
#server-list, #assign-many, #balances { scroll-margin-top: 84px; }
@media (min-width: 761px) { #server-list, #assign-many, #balances { scroll-margin-top: 210px; } }
@media (min-width: 1100px) { #server-list, #assign-many, #balances { scroll-margin-top: 124px; } }
.toolbar {
  background: var(--panel);
  border: var(--bd);
  border-radius: var(--r-md);
  padding: var(--d-pad);
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 14px;
  box-shadow: var(--lift-1);
}
.toolbar input { flex: 1 1 22ch; min-width: 0; max-width: 42ch; }
.toolbar select { width: auto; }

table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  background: var(--panel);
  border: var(--bd);
  border-radius: var(--r-md);
  overflow: hidden;
  box-shadow: var(--lift-1);
}
th, td {
  padding: var(--d-cell) var(--s-3);
  border-bottom: 1px solid var(--line);
  text-align: left;
  vertical-align: top;
  font-size: 14px;
}
th { background: var(--surface-2); color: var(--ink-3); font-size: var(--fs-xs);
  box-shadow: var(--lift-0); }
tr:last-child td { border-bottom: 0; }
h1, h2 { margin: 0 0 14px; }
.muted { color: var(--muted); }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
/* ── status ─────────────────────────────────────────────────────────────────
   These class names arrive from DATA (server status, due state, role, ticket and
   order status) at 19 call sites in app.py and may never be renamed. So each name
   sets two variables and ONE rule draws the chip.

   Two decisions carried here:
   1. `.late` is the only thing in the whole application filled with a status
      colour. Not a button, not a tile, not another chip. A filled red chip
      therefore means exactly one thing at arm's length, on any screen, with no
      reading: this money is late.
   2. `.rented` is neutral. All 137 machines are rented; spending the loudest chip
      in the palette on the state every machine shares carries no information.
      The four due states form a ramp that reads as escalation with no reading at
      all: `.ok` green tint -> `.soon` amber tint -> `.due` red tint -> `.late`
      solid red. They used to collapse into two swatches, which made the only
      distinction that matters on the payments screen invisible. */
.badge {
  display: inline-flex;
  /* not center: a Vietnamese status is often two lines tall, and centring drops the
     tone dot into the gutter between them. The dot is offset onto the first line
     below; on a one-line chip that offset works out identical to centring. */
  align-items: flex-start;
  gap: var(--s-1);
  border-radius: var(--r-pill);
  padding: 3px 10px;
  min-height: 24px;
  font-size: var(--fs-xs);
  font-weight: 600;
  /* not nowrap: a Vietnamese status runs ~30% longer than its English source and
     has to be allowed to wrap rather than push its container open */
  white-space: normal;
  line-height: 1.35;
  max-width: 24ch;
  background: var(--tone-bg, var(--surface-3));
  color: var(--tone-fg, var(--ink-2));
  border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
}
.badge::before {
  content: "";
  width: 6px; height: 6px; border-radius: var(--r-pill);
  background: currentColor; flex: 0 0 auto;
  /* centre the dot in the FIRST line box: (line-height - dot) / 2. The 1.35 must
     track the line-height declared on .badge above. */
  margin-top: calc((1.35em - 6px) / 2);
}
/* informational */
.admin, .pchip, .contacted, .provisioned { --tone-bg: var(--accent-quiet); --tone-fg: var(--accent); }
/* settled / good */
.available, .ok, .approved, .resolved, .customer, .stock-ok { --tone-bg: var(--ok-quiet); --tone-fg: var(--ok); }
/* wants attention soon */
.maintenance, .reserved, .soon, .pending, .open, .waiting, .new { --tone-bg: var(--warn-quiet); --tone-fg: var(--warn); }
/* bad, but tinted — the solid fill is reserved */
.rejected, .owner, .due { --tone-bg: var(--bad-quiet); --tone-fg: var(--bad); }
/* stock is the scarcity signal on the catalog: in stock, but nearly gone */
.stock-low { --tone-bg: var(--warn-quiet); --tone-fg: var(--warn); }
/* neutral: true of everything, so it says nothing */
.rented, .none, .retired, .stock-out, .closed, .cancelled { --tone-bg: var(--surface-3); --tone-fg: var(--ink-2); }
/* THE ONLY SOLID FILL IN THE APPLICATION */
.late { --tone-bg: var(--bad-solid); --tone-fg: var(--on-solid); border-color: var(--bad-solid); }
.two { grid-template-columns: minmax(0, 1fr) minmax(320px, .45fr); align-items: start; }
.panel {
  background: var(--panel);
  border: var(--bd);
  border-radius: var(--r-md);
  padding: var(--d-pad);
  box-shadow: var(--lift-1);
}
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.form-grid.compact { grid-template-columns: 1fr; }
label { display: grid; gap: var(--s-1); min-width: 0; color: var(--ink-2); font-weight: 600; font-size: 13px; }
label.full { grid-column: 1 / -1; }
.actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 14px; }
.spec { white-space: pre-line; min-width: 210px; }
.empty { padding: var(--s-6); text-align: center; color: var(--muted); }
.spaced { margin-top: 14px; }
@media (max-width: 900px) {
  .metrics, .two, .form-grid { grid-template-columns: 1fr; }
  table { display: block; overflow-x: auto; }
  .bar { align-items: flex-start; flex-direction: column; }
}

.login-wrap { min-height: 70vh; display: grid; place-items: center; }
.login-panel { width: min(440px, 100%); }
.stack { display: grid; gap: 14px; }
.hint { margin-top: var(--s-4); padding: var(--d-pad); background: var(--surface-2); border-radius: var(--r-sm); color: var(--muted); line-height: 1.5; }
.user-chip { white-space: nowrap; font-size: 13px; }
.notice { padding: 10px 12px; border-radius: var(--r-sm); margin: 0 0 14px; font-weight: 600; }
.notice.danger { background: var(--soft-red); color: var(--red); }
.feature-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
.feature-grid > div { border: var(--bd); border-radius: var(--r-md); padding: var(--d-pad); background: var(--surface-2); }
.feature-grid ul { margin: 0; padding-left: 18px; line-height: 1.55; }
.section-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 14px; }
.inline { display: inline-flex; gap: 6px; margin: 2px; align-items: center; }
.wide-inline { width: min(520px, 100%); }
.wide-inline input { flex: 1 1 20ch; }
.big-number { font-size: 52px; font-weight: 700; line-height: 1.1; margin: 8px 0; }
.check { display: flex; align-items: center; gap: 8px; }
main.wide { max-width: 1440px; }
body:has(main.wide) .bar { max-width: 1440px; }
@media (max-width: 900px) { .feature-grid { grid-template-columns: 1fr; } .section-head { align-items: flex-start; flex-direction: column; } }

.compact-metrics {
  grid-template-columns: repeat(5, minmax(120px, 1fr));
  gap: 8px;
  margin-bottom: 10px;
}
.compact-metrics .metric { padding: 8px 10px; }
.compact-metrics .metric span { font-size: var(--fs-xs); }
.compact-metrics .metric strong { margin-top: 3px; font-size: 18px; }
.narrow { max-width: 620px; margin-inline: auto; }
.notice.success { background: var(--soft-green); color: var(--green); }
.due-grid { margin-bottom: 14px; }

.overdue-panel { border-color: var(--bad); background: var(--bad-quiet); margin-bottom: 14px; }
.customer-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 14px; margin-top: 14px; }
.customer-card { background: var(--panel); border: var(--bd); border-radius: var(--r-md); padding: var(--d-pad); display: grid; gap: var(--d-gap); box-shadow: var(--lift-1); }
.customer-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.customer-top h2 { margin: 0 0 4px; }
.customer-contact { display: flex; flex-wrap: wrap; gap: 8px; color: var(--muted); font-size: 13px; }
.customer-contact span { border: var(--bd); border-radius: var(--r-pill); padding: 4px 8px; background: var(--surface-2); }
.customer-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(92px, 1fr)); gap: 8px; }
.customer-stats div { border: var(--bd); border-radius: var(--r-sm); padding: 8px; background: var(--surface-2); }
.customer-stats span { display: block; color: var(--muted); font-size: var(--fs-xs); }
.customer-stats strong { display: block; margin-top: 4px; font-size: 16px; }
.assigned-list { display: flex; flex-wrap: wrap; gap: 6px; }
.machine-cell { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.assigned-list.inline { display: inline-flex; }
.mini-link { border: var(--bd); border-radius: var(--r-pill); padding: 5px 8px; background: var(--surface); font-weight: 600; font-size: var(--fs-xs); }
@media (max-width: 520px) { .customer-grid { grid-template-columns: 1fr; } }

.metrics { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.history-pager { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
.btn.disabled { opacity: .48; cursor: not-allowed; background: var(--surface-2); }
.inline button { white-space: nowrap; }

.due-grid { grid-template-columns: 1fr; gap: 10px; }
.due-panel { padding: var(--d-pad); }
.due-panel .section-head { margin-bottom: 8px; }
.due-panel h2 { margin-bottom: 0; font-size: 18px; }
.negative { color: var(--red, #b42318); font-weight: 700; }
.refund-bar { align-items: center; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
/* Explicit offsets and max-width, not inset/min(): older iOS Safari ignores those and the
   sheet then renders off-screen, so the button looks like it does nothing. */
.modal { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 90; display: none; align-items: flex-start; justify-content: center; padding: 24px 14px; background: rgba(30, 26, 23, .45); overflow-y: auto; -webkit-overflow-scrolling: touch; }
.modal.show { display: flex; }
.modal-sheet { width: 100%; max-width: 680px; background: var(--panel, #fff); border: 1px solid var(--line); border-radius: var(--r-md); padding: var(--s-4); box-shadow: var(--lift-2); }
.modal-sheet h2 { margin: 0; }
.modal-close { background: none; border: 0; font-size: 26px; line-height: 1.35; cursor: pointer; color: var(--muted); padding: 0 4px; min-height: auto; }
.modal-close:hover { color: var(--text); }
.ba-cell { line-height: 1.35; white-space: nowrap; }
.ba-cell span { display: block; }
.ba-cell .ba-after { font-weight: 600; }
.ba-cell .ba-arrow { color: var(--muted); font-size: var(--fs-xs); line-height: 1.35; }
.ba-cell .ba-before { color: var(--muted); }
.due-detail { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: 4px; row-gap: 2px; }
.due-detail strong, .due-detail span { overflow-wrap: anywhere; }
.due-detail .due-sep { color: var(--muted); }
.due-detail .due-amount { font-weight: 700; }

/* Changing the amount charged for one due. Folded away by default so a screen full of renters
   stays readable; the input needs an explicit width because the global rule stretches every
   input to 100%, which inside this flex row swallowed the Save button. */
.due-edit { margin-top: 6px; }
.due-edit > summary { cursor: pointer; color: var(--muted); font-size: var(--fs-xs); width: fit-content; }
.due-edit > summary:hover { color: var(--fg); }
.due-edit-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 4px 0; }
.due-edit-what { flex: 1 1 11rem; min-width: 0; overflow-wrap: anywhere; }
.due-edit-row .due-edit-amount { width: calc(11ch + var(--control-chrome)); flex: 0 0 auto;
  text-align: right; font-variant-numeric: tabular-nums slashed-zero; }
.due-edit-row .btn { flex: 0 0 auto; }
.due-edit-row .due-edit-date { width: auto; flex: 0 0 auto; }
/* holds its placeholder whole in Vietnamese ("Số tiền nhận lần này") */
.due-edit-row .due-edit-paid { width: calc(20ch + var(--control-chrome)); max-width: 100%; }
@media (max-width: 560px) { .due-edit-what { flex-basis: 100%; } }
.renter-link { color: var(--blue); font-weight: 700; text-decoration: underline; text-underline-offset: 2px; }

@media (max-width: 560px) {
  /* One rail on a phone too. --d-page IS the rail, so the narrow value is set on the
     token and the header, the duty strip and the page move together — main used to
     override its own padding here and drift 8px inside the nav above it. Restated
     for :has(.user-chip) because the console rule that sets --d-page is (0,1,1) and
     a bare body would lose to it. */
  body, body:has(.user-chip) { --d-page: var(--s-2); }
  main { padding: var(--d-pad) var(--d-page) var(--s-6); }
  /* the toolbar wraps here, so the search field takes the whole line rather than
     sharing it and clipping its own placeholder */
  .toolbar input { flex: 1 1 100%; max-width: none; }
  .due-panel { padding: var(--d-pad); border-radius: var(--r-sm); }
  .due-panel .section-head { flex-direction: row; align-items: center; justify-content: space-between; gap: 8px; }
  .due-panel .section-head h2 { font-size: 16px; }
  .due-panel .section-head .inline { margin: 0; }
}
/* The payments table stacks into cards well before a phone. Three columns of this much
   content — a machine list, a renter with amounts and dates, and three action buttons —
   have no honest arrangement at tablet width: something has to be crushed, and squeezing
   the data columns to make room for the buttons is the worst of the choices. As cards each
   row gets the full width, so nothing is crushed and the action row fits on one line. */
@media (max-width: 1199px) {
    .due-table { display: table; border: 0; border-radius: 0; background: transparent; }
  .due-table thead { display: none; }
  .due-table tbody, .due-table tr, .due-table td { display: block; width: 100%; }
  .due-table tr { border: var(--bd); border-radius: var(--r-sm); background: var(--surface); padding: 8px; margin-bottom: 8px; }
  .due-table tr:last-child { margin-bottom: 0; }
  .due-table td { border: 0; padding: 3px 0; font-size: 13px; }
  .due-table td:nth-child(2)::before { content: ''; }
  .due-table td:nth-child(3) { display: flex; gap: 6px; flex-wrap: wrap; padding-top: 6px; }
  .due-table .inline { margin: 0; }
  .due-table .empty { padding: var(--d-pad); text-align: center; }
}
.lang-toggle { font-weight: 700; min-width: 62px; }

.server-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 10px;
}
/* A colour you can run your eye down a column of 137 without reading a word. The
   states come from due_info(), which reports the NEXT due date — so there is
   deliberately no overdue edge here: arrears live on the payments screen. */
.server-card::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 3px;
  background: var(--edge, transparent);
}
.server-card.state-due { --edge: var(--bad); }
.server-card.state-soon { --edge: var(--warn); }
.server-card.state-ok { --edge: var(--ok); }
.server-card.state-none { --edge: var(--line); }
.server-card {
  position: relative;
  overflow: hidden;
  background: var(--panel);
  border: var(--bd);
  border-radius: var(--r-md);
  padding: var(--d-pad);
  box-shadow: var(--lift-1);
  display: grid;
  gap: 10px;
  min-width: 0;
}
.server-card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}
.server-title {
  display: inline-block;
  font-weight: 700;
  font-size: 16px;
  margin-bottom: 3px;
}
.server-card-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}
.server-field {
  border: var(--bd);
  border-radius: var(--r-sm);
  padding: 8px;
  background: var(--surface-2);
  min-width: 0;
}
.server-field.wide { grid-column: 1 / -1; }
.server-field > span:first-child {
  display: block;
  color: var(--muted);
  font-size: var(--fs-xs);
  margin-bottom: 4px;
}
.server-field strong,
.server-field small {
  display: block;
  overflow-wrap: anywhere;
}
.server-card .spec {
  min-width: 0;
  max-height: 96px;
  overflow: auto;
}

@media (max-width: 560px) {
  .server-grid { grid-template-columns: 1fr; gap: 8px; }
  .server-card { padding: var(--d-pad); }
  .server-card-grid { grid-template-columns: 1fr; }
  .server-card-head { align-items: flex-start; }
}

.server-fast-scroll {
  position: fixed;
  top: 154px;
  right: 8px;
  bottom: 18px;
  width: 32px;
  z-index: 25;
  display: flex;
  justify-content: center;
  touch-action: none;
  cursor: grab;
}
.server-fast-scroll::before {
  content: "";
  display: block;
  width: 7px;
  height: 100%;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
}
.server-fast-scroll.dragging { cursor: grabbing; }
.server-fast-thumb {
  position: absolute;
  left: 50%;
  top: 0;
  width: 24px;
  height: 42px;
  border-radius: var(--r-pill);
  transform: translate(-50%, -50%);
  background: var(--accent);
  border: 2px solid var(--surface);
  box-shadow: var(--lift-hover);
}
.server-fast-label {
  position: fixed;
  right: 48px;
  top: 50%;
  z-index: 26;
  max-width: min(280px, calc(100vw - 82px));
  padding: 9px 11px;
  border-radius: var(--r-sm);
  background: var(--ink);
  color: var(--paper);
  font-weight: 700;
  box-shadow: var(--lift-2);
  opacity: 0;
  pointer-events: none;
  transform: translateY(-50%);
  transition: opacity .12s ease;
  overflow-wrap: anywhere;
}
.server-fast-label.show { opacity: .96; }

@media (max-width: 560px) {
  .server-fast-scroll { top: 132px; right: 3px; bottom: 12px; width: 28px; }
  .server-fast-scroll::before { width: 6px; }
  .server-fast-thumb { width: 22px; height: 38px; }
  .server-fast-label { right: 36px; max-width: calc(100vw - 62px); padding: 8px 10px; }
}

.due-tabs { padding: 10px; }
/* Sizing matters even though it is invisible: the global `input { width: 100% }` stretched this
   absolutely-positioned radio past the viewport and gave the Payments page a permanent 35px
   horizontal scroll. Only its :checked state is used, so keep it 1x1. */
input[type=radio].due-tab-input { position: absolute; opacity: 0; pointer-events: none; width: 1px; height: 1px; min-height: 0; }
/* top: 0, not a constant. A sticky header that is content-sized cannot be tracked by
   a number: it is 114px at 1280 and 198.8px at 800 as the nav wraps. So on the one
   screen with due tabs the header gives up its stickiness and the tabs take it —
   you are already where the duty strip was pointing, and on a 7000px arrears list
   the tab bar is the thing worth pinning. */
body:has(.due-tabs) header { position: static; }
.due-tab-labels { display: flex; flex-wrap: wrap; gap: 6px; border-bottom: 1px solid var(--line); margin-bottom: 10px; position: sticky; top: 0; z-index: 20; background: var(--panel); padding-top: 6px; }
.due-tab-label { cursor: pointer; border: var(--bd); border-bottom: 0; border-radius: var(--r-md) var(--r-md) 0 0; padding: 8px 12px; background: var(--surface-2); font-weight: 700; color: var(--ink-2); }
.due-tab-label span { display: inline-flex; align-items: center; justify-content: center; min-width: 22px; min-height: 20px; margin-left: 6px; border-radius: var(--r-pill); background: var(--surface-3); color: var(--ink); font-size: var(--fs-xs); }
.due-tab-panel { display: none; padding: 4px 2px 2px; }
#due-tab-overdue:checked ~ .due-tab-labels label[for="due-tab-overdue"],
#due-tab-today:checked ~ .due-tab-labels label[for="due-tab-today"],
#due-tab-tomorrow:checked ~ .due-tab-labels label[for="due-tab-tomorrow"] { background: var(--surface); color: var(--ink); }
#due-tab-overdue:checked ~ .due-tab-panel:nth-of-type(1),
#due-tab-today:checked ~ .due-tab-panel:nth-of-type(2),
#due-tab-tomorrow:checked ~ .due-tab-panel:nth-of-type(3) { display: block; }
/* a column of rows, not one wrapping strip: which button lands on which line has to be
   decided here, not by however much room the cell happens to have */
.status-actions { display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--s-2); }
.sa-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
/* Row one is three buttons wide — 412px in English, 358 in Vietnamese. Left to itself
   the column settles around 250px and Return drops onto a line of its own, which is
   the one thing this layout exists to prevent. Applied everywhere the payments table
   is still a TABLE — which is now 1200px and up. Below that it stacks into cards that
   are already full width, and forcing a floor there crushed the data columns instead:
   the table could not scroll (its text simply wrapped), so the 440px came straight out
   of the renter and machine columns. */
@media (min-width: 1200px) {
  .due-table td:nth-child(3) { min-width: 27.5rem; }
}
/* On a phone the card is ~315px, so the same row needs the buttons to give up some
   side padding to stay on one line. The touch target keeps its height. */
@media (max-width: 560px) {
  .status-actions .btn, .status-actions button { padding-inline: var(--s-2); }
}
.reminder-inline, .reminder-inline button { background: var(--warn-quiet); border-color: var(--warn); color: var(--warn); }
.copy-reminder-button, .copy-qr-button { white-space: nowrap; }
.qr-copy-modal { position: fixed; inset: 0; z-index: 80; display: none; align-items: center; justify-content: center; padding: 18px; background: rgba(30, 26, 23, .42); }
.qr-copy-modal.show { display: flex; }
.qr-copy-sheet { position: relative; width: min(360px, 100%); background: var(--surface); border-radius: var(--r-md); padding: var(--s-4); border: var(--bd); box-shadow: var(--lift-2); text-align: center; }
/* A QR code is scanned by a camera, so its backing stays true white in every theme. */
.qr-copy-sheet img { width: min(260px, 78vw); height: auto; border: var(--bd); border-radius: var(--r-sm); background: #fff; padding: 8px; }
.qr-copy-sheet p { color: var(--muted); margin: 12px 0 0; line-height: 1.45; }
.qr-copy-close { position: absolute; top: 8px; right: 8px; min-width: 32px; min-height: 32px; padding: 0; }
.qr-thumb { width: 82px; height: 82px; object-fit: contain; border: var(--bd); border-radius: var(--r-sm); background: #fff; }
/* Stated in lines, not pixels, so it tracks the type scale instead of drifting from
   it: a half-line tall box slices the last row of glyphs, which in Vietnamese is
   exactly where the tone marks live. */
/* min-width is what makes the table scroll instead of crushing once it becomes a block
   scroll container at <=900px; width:100% of a collapsed column is not a floor. */
.template-table textarea { min-height: calc(4 * 1.6em + var(--s-2) * 2 + 2px); min-width: 32ch; }
.template-table input:not([type=checkbox]):not([type=radio]) { width: 100%; min-width: 18ch; }
.template-actions { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.qr-assign-table textarea { min-height: 56px; }
.qr-assign-table input:not([type=checkbox]):not([type=radio]) { width: 100%; min-width: 16ch; }
.compact-save { margin-top: 6px; }
@media (max-width: 760px) {
  header { position: static; }
  .due-tabs { overflow: visible; }
  .due-tab-labels {
    overflow-x: auto;
    flex-wrap: nowrap;
    top: 0;
    z-index: 20;
    margin: -10px -10px 10px;
    padding: 8px 10px 0;
    box-shadow: var(--lift-2);
  }
  .due-tab-label { white-space: nowrap; }
  .status-actions { align-items: flex-start; }
}


/* ---- public homepage ---- */
.hero { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-md); padding: var(--s-6) var(--s-5); margin-bottom: var(--s-4); text-align: center; }
.hero h1 { font-size: clamp(26px, 5vw, 40px); margin: 0 0 10px; }
.hero .lead { max-width: 720px; margin: 0 auto 18px; color: var(--muted); font-size: 16px; line-height: 1.6; }
.hero .actions { justify-content: center; }
.btn.big { padding: 12px 22px; font-size: 16px; }
.hero-stats { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin: 18px 0; }
.hero-stats .stat { border: var(--bd); border-radius: var(--r-sm); padding: 10px 18px; background: var(--surface-2); min-width: 132px; }
.hero-stats .stat strong { display: block; font-size: var(--fs-xl); line-height: 1.35; }
.hero-stats .stat span { color: var(--muted); font-size: var(--fs-xs); }
.gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; margin-bottom: 16px; }
.gallery .shot { margin: 0; border: var(--bd); border-radius: var(--r-md); overflow: hidden; background: var(--slab); }
.gallery .shot img { display: block; width: 100%; height: 260px; object-fit: cover; }
@media (max-width: 560px) {
  .hero { padding: var(--s-5) var(--s-4); }
  .gallery { grid-template-columns: 1fr; }
  .gallery .shot img { height: 220px; }
}
.hero-stats .stat.plain strong { font-size: 17px; letter-spacing: .01em; }

/* ---- brand mark + public header ---- */
.brand {
  display: inline-flex; align-items: center; gap: 10px;
  font-weight: 700; font-size: 16px; letter-spacing: -.01em; color: var(--ink);
  padding: 6px 4px; margin-right: 4px;
}
.brand::before {
  content: "QM"; display: grid; place-items: center;
  width: 30px; height: 30px; border-radius: var(--r-sm);
  background: var(--accent);
  color: var(--on-solid); font-size: var(--fs-xs); font-weight: 700; letter-spacing: .02em;
}
.nav-spacer { flex: 1 1 auto; }
header nav { align-items: center; }

/* ---- hero ---- */
/* A slab region. It takes its palette from the rule above, so every child reads
   correctly on near-black in BOTH themes and no colour is written twice here. */
.hero { padding: var(--s-6) var(--s-5); background: var(--slab);
  /* not border: 0 — the gradient used to be its own boundary. On dark paper the
     slab is DARKER than the ground, so a shadow cannot separate it and only an
     edge can. Written out rather than var(--bd) ON PURPOSE: --bd is declared on
     :root as `1px solid var(--line)`, and a custom property containing var() is
     substituted where it is DECLARED, so --bd would hand the hero the page's line
     colour. Spelled out, --line re-resolves against the slab palette the hero sets
     on itself, and the edge stays inside the band in both themes. */
  border: 1px solid var(--line); color: var(--ink); box-shadow: var(--lift-1); }
.hero h1 { letter-spacing: -.02em; }
/* auto, not 0: the measure stays at 60ch for reading, but the column sits in the middle
   of the band instead of against its left edge. */
.hero .lead { color: var(--ink-2); margin-left: auto; margin-right: auto; max-width: 60ch; }
.hero .actions { justify-content: center; }
.btn.big { min-height: 48px; padding: 12px 22px; font-size: 15px; }
.btn.ghost { background: transparent; border-color: var(--line-strong); color: var(--ink); }
.btn.ghost:hover { background: var(--surface-3); }
.hero-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px;
  justify-content: center; }
.hero-chip { display: inline-flex; align-items: center; gap: 7px; border-radius: var(--r-pill); padding: 6px 13px; font-size: var(--fs-xs); font-weight: 600; background: var(--surface-3); color: var(--ink); }
.hero-chip::before { content: ""; width: 6px; height: 6px; border-radius: var(--r-pill);
  background: var(--ok); }

/* ---- order page ---- */
.field-hint { font-weight: 500; font-size: var(--fs-xs); color: var(--muted); }
.assure { display: flex; align-items: flex-start; gap: 9px; margin: 16px 0 0; padding: 11px 13px; border-radius: var(--r-sm); background: var(--soft-green); color: var(--green); font-size: 13px; font-weight: 600; line-height: 1.45; }
.assure::before { content: "✓"; font-weight: 700; }
.submit-row { align-items: center; }
.notice.amber { background: var(--soft-amber); color: var(--amber); }
.order-aside { display: grid; gap: 12px; align-content: start; }
.order-aside.wait .plan-card { border-color: var(--warn); box-shadow: 0 0 0 1px var(--warn-quiet) inset; }
.summary-help { border: 1px dashed var(--line); border-radius: var(--r-md); padding: var(--d-pad); }
.summary-help p { margin: 0 0 10px; font-size: 13px; }
.summary-help .actions { margin-top: 0; }
/* ---- sign-up / sign-in ---- */
.login-panel h1 { margin-bottom: 10px; }
.login-alt { margin: 16px 0 0; font-size: 14px; text-align: center; }
.login-alt a { color: var(--blue); font-weight: 700; }
.resend-form { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line); }
.resend-form label { font-size: var(--fs-xs); }

.receipt-ok { max-width: 640px; }
.ok-mark { width: 46px; height: 46px; border-radius: var(--r-pill); background: var(--soft-green); display: grid; place-items: center; margin-bottom: 14px; }
.ok-mark::before { content: "✓"; color: var(--green); font-size: 24px; font-weight: 700; }

/* Tap targets come from --control-h, which a coarse pointer raises to 44px. */
@media (max-width: 900px) {
  .hero { padding: var(--s-5) var(--s-4); }
  .hero .actions .btn { width: 100%; }
}

/* ================= Public hosting surface =================
   Design notes that are load-bearing, not decoration:
   - Category headings sit on the page background; only ONE frame per tier (.spec-wrap). The old
     panel > card > table nesting put three borders around every spec value.
   - Mobile spec labels (.k) are real text hidden on desktop, never content:attr(data-label):
     only placeholder|title|aria-label are translated, so a data-* label would stay English.
   - No position:sticky thead: .spec-scroll has overflow-x:auto, which makes it the scrollport,
     so a sticky header would pin inside the table box over its own first row.
*/
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* --- shop bar (catalog intro, deliberately unframed) --- */
.shop-bar { margin-bottom: 18px; }
.shop-bar h1 { font-size: clamp(24px, 3.2vw, 32px); margin: 0 0 8px; letter-spacing: -.01em; }
.shop-bar .lead { max-width: 62ch; margin: 0; color: var(--muted); line-height: 1.6; }
.shop-count { display: inline-block; margin-top: 10px; font-weight: 600; font-size: 13px; color: var(--green); }
.plans-sort select { font-weight: 600; }

/* --- the rack sheet --- */
.rack { margin-bottom: 26px; }
.rack-head { display: flex; align-items: baseline; gap: 12px; margin: 0 0 10px 2px; }
.rack-head h2 { margin: 0; font-size: 17px; letter-spacing: -.01em; }
.spec-wrap { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-md); box-shadow: var(--lift-1); overflow: hidden; }
/* position: relative so the scroller is a containing block. An .sr-only label inside the table
   header is absolutely positioned, and an absolutely positioned descendant is NOT clipped by an
   ancestor's overflow unless that ancestor is its containing block — so it escaped the scroller
   and pushed the whole page 40px wider at the widths where the table is just too big to fit. */
.spec-scroll { overflow-x: auto; position: relative; }
table.spec-table { width: 100%; min-width: 940px; margin: 0; border: 0; border-radius: 0; background: transparent; box-shadow: none; }
.spec-table thead th { background: var(--surface-2); color: var(--ink-3); font-size: var(--fs-xs); font-weight: 700; padding: 11px 14px; border-bottom: 1px solid var(--line); white-space: nowrap; }
.spec-table td { padding: 16px 14px; border-bottom: var(--bd); vertical-align: middle; }
.spec-table tbody tr:last-child td { border-bottom: 0; }
.plan-row { transition: background .12s; }
.plan-row:hover { background: var(--surface-hover); }
/* Column sizing is a TABLE concern only — these class names are reused by the standalone card,
   where an 11% width would collapse the value column to nothing. Keep them scoped. */
.spec-table .col-plan { width: 23%; min-width: 200px; }
.spec-table .col-spec { width: 11%; }
.spec-table .col-price { width: 16%; min-width: 140px; text-align: right; }
.spec-table .col-cta { width: 13%; min-width: 132px; text-align: right; }

.pname { display: block; font-size: var(--fs-base); font-weight: 700; line-height: 1.4; overflow-wrap: anywhere; }
.pmeta { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 7px; }
.pchip { display: inline-flex; align-items: center; border-radius: var(--r-pill);
  padding: 3px 9px; font-size: var(--fs-xs); font-weight: 600; line-height: 1.35;
  background: var(--tone-bg); color: var(--tone-fg); }
.pdesc { display: block; margin-top: 8px; font-size: var(--fs-xs); line-height: 1.5; color: var(--muted); overflow-wrap: anywhere; }
.k { display: none; }
.sv { display: block; font-size: 14px; font-weight: 600; line-height: 1.45; overflow-wrap: anywhere; }
.dash { color: var(--dash); }

.money { display: inline-block; font-size: 20px; font-weight: 700; color: var(--price); letter-spacing: -.02em; font-variant-numeric: tabular-nums; white-space: nowrap; }
.per { display: block; font-size: var(--fs-xs); font-weight: 600; color: var(--muted); }
.money-alt { display: inline-block; margin-top: 6px; font-size: 13px; font-weight: 700; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.money-none { font-size: 13px; color: var(--muted); }
.spec-table .col-cta .btn { width: 100%; white-space: normal; min-height: 44px; text-align: center; }
.plan-row.out .pname, .plan-row.out .sv { color: var(--ink-3); }
.plan-row.out .money { color: var(--ink-3); }
.btn.wait { border-color: var(--warn); background: var(--warn-quiet); color: var(--warn); }

/* --- plan as a standalone card (order page summary) --- */
.plan-card { border: var(--bd); border-radius: var(--r-lg); padding: var(--d-pad); background: var(--surface); box-shadow: var(--lift-1); }
.pc-price { margin: 12px 0; padding: 12px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.pc-price .money { font-size: 26px; }
.pc-specs { display: grid; gap: 10px; margin: 0; }
.pc-specs .col-spec { display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 10px; align-items: baseline; }
.pc-specs .k { display: block; font-size: var(--fs-xs); font-weight: 700; color: var(--muted); }

/* --- homepage teasers --- */
.mini-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 12px; }
.plan-mini { border: var(--bd); border-radius: var(--r-lg); padding: var(--d-pad); background: var(--surface); display: flex; flex-direction: column; gap: 8px; align-items: flex-start; box-shadow: var(--lift-1); transition: transform .12s, box-shadow .12s, border-color .12s; }
.plan-mini:hover { transform: translateY(-2px); box-shadow: var(--lift-hover); border-color: var(--accent); }
.plan-mini .pname { font-size: 14px; }
/* the card's own call to action: full width so the whole foot of the card is the
   target, and 44px so it is one on a phone */
.plan-mini .btn { width: 100%; min-height: 44px; justify-content: center; text-align: center;
  white-space: normal; margin-top: var(--s-2); }
.mini-spec { color: var(--muted); font-size: var(--fs-xs); }
.mini-price { margin-top: auto; }
.mini-price .money { font-size: 18px; }

/* --- the four-step path, shown on home, catalog and success --- */
.steps { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; counter-reset: step; list-style: none; margin: 0 0 22px; padding: 0; }
.step { position: relative; counter-increment: step; padding: 12px 14px 12px 44px; border-radius: var(--r-md); background: var(--surface); border: var(--bd); font-size: 13px; font-weight: 600; line-height: 1.4; }
.step::before {
  content: counter(step); position: absolute; left: 12px; top: 11px;
  width: 22px; height: 22px; border-radius: var(--r-pill); background: var(--soft-blue); color: var(--blue);
  display: grid; place-items: center; font-size: var(--fs-xs); font-weight: 700;
}
.step.done { background: var(--ok-quiet); border-color: var(--ok); color: var(--ok); }
.step.done::before { content: "✓"; background: var(--ok); color: var(--on-solid); }

/* --- generic page rhythm: a band is a section with no box --- */
.band { margin: 0 0 22px; }
.band > h2 { font-size: 17px; margin: 0 0 12px; }
.narrow-read { max-width: 820px; }
/* The configuration editor: four parts, laid out like the form around it rather than as a
   stack of full-width rows, so CPU/RAM/Card/SSD read as one thing. */
/* the four managed lists, side by side while there is room */
/* the running total on the build-your-own form */
.custom-total { display: flex; align-items: baseline; gap: var(--s-2); flex-wrap: wrap;
  margin-top: var(--s-3); min-height: 28px; }
.custom-total .money { font-size: 22px; }
.custom-config .actions { margin-top: var(--s-3); }
.spec-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
  gap: var(--s-4); }
.spec-col h3 { margin: 0 0 var(--s-2); font-size: var(--fs-base); }
.spec-list { list-style: none; margin: 0 0 var(--s-2); padding: 0; display: grid; gap: var(--s-1); }
.spec-list li { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap;
  padding: var(--s-1) 0; border-bottom: 1px solid var(--line); }
.spec-list li form { margin-left: auto; }
.spec-list .tiny { font-size: var(--fs-xs); }

/* The price sits on the same row as the part, so the whole list can be read as a price list.
   Six digits is the widest real figure, but the box is sized to the Vietnamese placeholder
   instead: an empty box saying "chưa có giá" is the one that has to be read, and clipping it
   to "chưa có" turns "no price" into something that could mean anything. */
.spec-price input { width: calc(11ch + var(--control-chrome)); }
.spec-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--s-2) var(--s-3); margin-top: var(--s-1); }
.spec-grid label { display: grid; gap: var(--s-1); font-size: var(--fs-xs); font-weight: 600;
  color: var(--ink-2); }
.spec-grid select { width: 100%; }
/* what is about to change, quoted back before it does */
.spec-what { font-weight: 700; font-size: var(--fs-base); margin: var(--s-2) 0 0; }
.jump-bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 10px; }
/* shrinkable: min-width:0 is inert while flex-shrink is 0, which is how an input can
   still starve the button beside it. */
.jump-bar input { width: calc(12ch + var(--control-chrome)); flex: 0 1 auto; }
/* a renter's name is longer than a machine number, and 12ch clipped the placeholder
   by three pixels — enough to read as a bug rather than a field */
.jump-bar.renter-jump input { width: calc(20ch + var(--control-chrome)); }

/* On a machine's own page the jump box sits in the action row rather than above a list, so it
   loses the bottom margin and is pushed to the far end - away from Edit, which is destructive
   to mis-click. It drops onto its own line when the row runs out of width. */
.detail-actions { flex-wrap: wrap; align-items: center; }
.jump-bar.inline-jump { margin-bottom: 0; margin-left: auto; }
@media (max-width: 560px) { .jump-bar.inline-jump { margin-left: 0; width: 100%; } }
.tag-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 6px; }
.tag-box {
  display: flex; align-items: flex-start; gap: 9px; flex-direction: row;
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 8px 10px; font-weight: 500;
}
/* A checkbox inherits `input { width: 100% }` from the base styles. Inside this flex row that
   let it swallow the whole line and squeezed the text to one character per line. */
.tag-box input { margin-top: 3px; }
.tag-body { display: block; min-width: 0; flex: 1 1 auto; }
.tag-body strong { font-size: 13.5px; }
.tag-spec { display: block; font-size: var(--fs-xs); line-height: 1.4; color: var(--muted); margin-top: 1px; overflow-wrap: anywhere; }
.tag-meta { display: block; font-size: var(--fs-xs); font-weight: 700; color: var(--blue); margin-top: 1px; }
.plan-members { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; padding: 0; list-style: none; }
.plan-members li { font-weight: 600; }
.plan-members .muted { font-weight: 500; font-size: var(--fs-xs); }
a.tag-box:hover { border-color: var(--accent); background: var(--surface-hover); }
label.check { flex-direction: row; align-items: center; gap: 8px; }

/* --- the rack sheet collapses into cards; SAME markup, no duplicate DOM --- */
@media (max-width: 900px) {
  .steps { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .spec-wrap { background: transparent; border: 0; border-radius: 0; box-shadow: none; overflow: visible; }
  .spec-scroll { overflow: visible; }
  /* beat the global `table { display:block; overflow-x:auto }` at this width, which would
     otherwise turn every collapsed row-card into its own clipped scroll container */
  table.spec-table { display: block; min-width: 0; overflow: visible; }
  .spec-table thead { display: none; }
  .spec-table tbody { display: grid; gap: 12px; }
  .spec-table tr { display: block; background: var(--surface); border: var(--bd); border-radius: var(--r-lg); box-shadow: var(--lift-1); padding: var(--d-pad); }
  .spec-table td { display: block; padding: 0; border: 0; }
  /* Percentage column widths are a TABLE concern. Once the cells become blocks in a card they
     must be released: an 11% cell inside a ~330px card leaves the value column no room at all
     and the text renders one character per line. */
  .spec-table .col-plan, .spec-table .col-spec,
  .spec-table .col-price, .spec-table .col-cta { width: auto; min-width: 0; }
  .spec-table .col-plan { margin-bottom: 12px; }
  .spec-table .col-spec {
    display: grid; grid-template-columns: 100px minmax(0, 1fr); gap: 10px;
    align-items: baseline; padding: 7px 0; border-top: var(--bd);
  }
  .spec-table .k { display: block; font-size: var(--fs-xs); font-weight: 700; color: var(--muted); }
  .spec-table .col-price { text-align: left; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); }
  .spec-table .col-cta { text-align: left; margin-top: 12px; }
  .plan-row:hover { background: var(--surface); }
}
@media (max-width: 560px) {
  .spec-table .col-spec { grid-template-columns: 88px minmax(0, 1fr); }
}
/* Phone header and intro must not push the products below the fold. The nav spacer is a
   desktop-only device: inside a wrapping flex row it would eat the rest of line 1 and throw
   every button onto later lines (measured: a 3-row, 175px-tall header). */
@media (max-width: 900px) {
  .nav-spacer { display: none; }
  header nav .btn { padding: 7px 11px; font-size: var(--fs-xs); }
  .brand { font-size: 14px; gap: 8px; padding: 2px; }
  .brand::before { width: 26px; height: 26px; font-size: var(--fs-xs); }
  .steps { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-bottom: 16px; }
  .step { padding: 9px 10px 9px 38px; font-size: var(--fs-xs); }
  .step::before { top: 9px; left: 9px; width: 20px; height: 20px; font-size: var(--fs-xs); }
  .shop-bar { margin-bottom: 14px; }
  .shop-bar .lead { font-size: 14px; }
  .rack { margin-bottom: 20px; }
}

/* ---- Order pipeline ---- */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.radio-row { display: flex; flex-wrap: wrap; gap: 12px; }
label.radio { flex-direction: row; align-items: center; gap: 6px; font-weight: 500; }
.tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.tab { border: var(--bd); border-radius: var(--r-pill); padding: 6px 12px; font-weight: 600; font-size: var(--fs-xs); color: var(--ink-2); background: var(--surface); }
.tab.active { background: var(--accent); color: var(--on-solid); border-color: var(--accent); }
.order-banner { display: block; background: var(--soft-amber); color: var(--amber); text-decoration: none; }
/* The actions sit under their order, not in a column beside it: two rows, the one that
   moves the order forward first. The data row above drops its bottom rule so the pair
   reads as one block rather than two orders. */
.order-row.has-acts > td { border-bottom: 0; padding-bottom: var(--s-1); }
/* the flex lives on a DIV inside the cell, never on the <td>: display:flex on a table
   cell stops it being a table-cell at all, colspan is dropped, and the row that was
   supposed to span the table shrinks to fit its own text instead. */
.order-acts-cell { padding-top: 0; }
.order-actions { display: flex; flex-direction: column; gap: var(--s-2); align-items: flex-start; }
.order-act-row { display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: center; margin: 0; }
.reject-form input { width: calc(12ch + var(--control-chrome)); }

/* ---- Customer portal ---- */
.portal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
.portal-card { border: var(--bd); border-radius: var(--r-md); padding: var(--d-pad); background: var(--surface); box-shadow: var(--lift-1); display: flex; flex-direction: column; gap: 10px; }
.portal-card .section-head { margin-bottom: 0; }
.portal-card h3 { margin: 0; font-size: 16px; }
.portal-fields { display: grid; gap: 8px; }
.portal-field { display: grid; gap: 2px; }
.portal-field span { color: var(--muted); font-size: var(--fs-xs); }
.portal-field strong { font-weight: 600; }
.portal-field .spec { white-space: pre-line; font-weight: 500; }
/* compact plan list — used in the client area, and safe at any width because the specs are
   inline wrapping pairs rather than a two-column grid */
.slim-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 12px; }
.plan-slim {
  border: var(--bd); border-radius: var(--r-lg); background: var(--surface); padding: var(--d-pad);
  box-shadow: var(--lift-1); display: flex; flex-direction: column; gap: 10px;
}
.plan-slim:hover { border-color: var(--accent); box-shadow: var(--lift-hover); }
.slim-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.slim-head .pname { font-size: 15px; }
.plan-chipline { display: flex; flex-wrap: wrap; gap: 5px 14px; }
.kv { display: inline-flex; align-items: baseline; gap: 5px; font-size: 13px; font-weight: 600; min-width: 0; }
.kv b { color: var(--muted); font-size: var(--fs-xs); font-weight: 700; white-space: nowrap; }
.slim-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-top: auto; }
.plan-slim .money { font-size: 17px; }
.plan-slim .per { display: inline; font-size: var(--fs-xs); margin-left: 3px; }
.plan-slim .money-alt { margin-top: 0; margin-left: 8px; font-size: var(--fs-xs); }
.plan-slim .btn { min-height: 40px; }

.portal-empty { grid-column: 1 / -1; text-align: center; padding: 34px 20px; }
.portal-empty h2 { margin: 0 0 8px; }
.portal-empty p { max-width: 52ch; margin: 0 auto; }
.portal-empty .actions { justify-content: center; }
.due-banner { background: var(--bad-quiet); border-color: var(--bad); }
.due-banner h2 { color: var(--red); margin-top: 0; }
.due-list { margin: 0 0 10px; padding-left: 18px; line-height: 1.7; }
.portal-qr-row { display: flex; flex-wrap: wrap; gap: 14px; }
.portal-qr { margin: 0; text-align: center; }
.portal-qr img { width: 180px; height: 180px; object-fit: contain; border: var(--bd); border-radius: var(--r-sm); background: #fff; }
.portal-qr figcaption { font-size: var(--fs-xs); margin-top: 4px; }

/* ---- Payment receipt ---- */
.receipt { max-width: 500px; margin: 0 auto; }
.receipt-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; border-bottom: 2px solid var(--ink); padding-bottom: 12px; margin-bottom: 14px; }
.rc-name { font-weight: 700; font-size: 18px; }
.rc-kind { font-weight: 700; color: var(--muted); }
.rc-table { width: 100%; border-collapse: collapse; }
.rc-table th, .rc-table td { text-align: left; padding: 7px 0; border-bottom: 1px solid var(--line); vertical-align: top; }
.rc-table th { color: var(--muted); font-weight: 600; width: 42%; }
.rc-total { display: flex; justify-content: space-between; align-items: baseline; margin-top: 14px; padding-top: 12px; border-top: 2px solid var(--ink); }
.rc-total span { color: var(--muted); font-size: var(--fs-xs); }
.rc-total strong { font-size: 22px; }
.rc-foot { margin: 16px 0 0; text-align: center; }
@media print {
  /* Paper is light, whatever the screen is. The dark palettes are scoped to
     `screen` above, so the light tokens on :root show through here untouched —
     there is deliberately no fourth copy of them in this block. */
  :root { color-scheme: light; }
  header, .no-print, .order-banner, .site-footer { display: none !important; }
  main { max-width: none; padding: 0; }
  .receipt { border: none; max-width: none; box-shadow: none; }
  .hero { background: none; color: var(--ink); box-shadow: none; border: 0; }
  body { background: #fff; color: var(--ink); }
}

/* ---- schedule adjustments ---- */
.sched-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 8px; margin-bottom: 14px; }
.sched-pick {
  display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 4px 10px;
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 9px 11px; font-weight: 500; cursor: pointer;
}
.sched-pick:hover { border-color: var(--accent); background: var(--surface-hover); }
.sched-pick input { grid-row: 1 / span 3; }
.sched-pick .sched-name { font-weight: 700; }
.sched-pick .muted, .sched-pick .sched-due { font-size: var(--fs-xs); grid-column: 2; }
.sched-pick .badge { grid-column: 2; justify-self: start; margin-top: 4px; }
.warn-panel { border-color: var(--warn); background: var(--warn-quiet); }
.warn-panel h2 { color: var(--amber); margin-top: 0; }
.warn-list { margin: 0 0 12px; padding-left: 20px; line-height: 1.6; color: var(--warn); }
.warn-panel .check { font-weight: 700; color: var(--amber); }

/* ---- FAQ + public footer ---- */
.faq-item { border: var(--bd); border-radius: var(--r-md); padding: 0; margin-top: 10px; background: var(--surface-2); }
.faq-item summary { cursor: pointer; font-weight: 600; padding: 12px 14px; list-style: none; }
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::before { content: "＋"; color: var(--blue); margin-right: 8px; }
.faq-item[open] summary::before { content: "－"; }
.faq-item p { margin: 0; padding: 0 14px 14px; color: var(--muted); line-height: 1.6; }
.site-footer {
  max-width: 1280px; margin: 32px auto 0; padding: var(--s-5) var(--d-page);
  border-top: 1px solid var(--line);
  display: flex; flex-wrap: wrap; gap: 12px; justify-content: space-between; align-items: center;
  color: var(--muted); font-size: 14px;
}
.footer-nav { display: flex; gap: 14px; flex-wrap: wrap; }
.footer-nav a { color: var(--blue); font-weight: 600; }

/* ===========================================================================
   PHASE 6 — the shop window.  Dedicated servers and VPS, on cards that carry
   what a buyer actually compares.

   Everything here is built from the tokens already defined at the top of this
   file, so both themes and the printed sheet keep working without a second
   palette. No image, no font and no gradient from anywhere but CSS: the app has
   to render with the network unplugged.
   =========================================================================== */

/* ---- hero: two columns, and a rack you can see ---- */
/* The hero is a slab region, so it has already set --ink, --line and --surface-3
   to values that read on near-black in BOTH themes. Everything below inherits
   that and names no colour of its own. */
.hero { text-align: left; display: grid; gap: var(--s-5);
        grid-template-columns: minmax(0, 1fr); align-items: center; }
.hero-copy { min-width: 0; }
/* Spaced and heavy rather than uppercased: this app forbids text-transform because
   Vietnamese tone marks sit above and below the letter and uppercase crowds them. */
.hero .eyebrow { display: inline-block; font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: .14em; color: var(--ink-2); margin-bottom: var(--s-3); }
.hero h1 { font-size: clamp(28px, 4.4vw, 46px); line-height: 1.1; text-wrap: balance;
  margin: 0 0 var(--s-3); }
/* Left-aligned now, so the auto margins that used to centre the measure are cleared. */
.hero .lead { margin-left: 0; margin-right: 0; max-width: 56ch; font-size: var(--fs-md); }
.hero .actions { justify-content: flex-start; }
.hero-chips { justify-content: flex-start; }

@media (min-width: 900px) {
  .hero { grid-template-columns: minmax(0, 1.35fr) minmax(0, .65fr); gap: var(--s-6); }
}

/* A rack of 1U machines, drawn rather than photographed. Decorative and
   aria-hidden, so it says nothing a screen reader has to hear. */
.hero-rack { display: grid; gap: 7px; align-content: center; }
.hero-rack i { display: block; height: 26px; border-radius: var(--r-sm);
  background: var(--surface-3); border: 1px solid var(--line); position: relative; }
/* the drive bays: a repeating gradient, so one rule draws every slot */
/* No radius on the bays: they are 3px stripes and a corner would never be seen. */
.hero-rack i::after { content: ""; position: absolute; inset: 6px 34px 6px 12px;
  opacity: .55;
  background: repeating-linear-gradient(90deg, var(--line) 0 3px, transparent 3px 9px); }
/* the status light */
.hero-rack i::before { content: ""; position: absolute; right: 12px; top: 50%;
  width: 6px; height: 6px; margin-top: -3px; border-radius: var(--r-pill);
  background: var(--ok); }
.hero-rack i:nth-child(3)::before { background: var(--warn); }
@media (prefers-reduced-motion: no-preference) {
  .hero-rack i::before { animation: rack-led 3.2s ease-in-out infinite; }
  .hero-rack i:nth-child(2)::before { animation-delay: .5s; }
  .hero-rack i:nth-child(3)::before { animation-delay: 1.1s; }
  .hero-rack i:nth-child(4)::before { animation-delay: 1.7s; }
  .hero-rack i:nth-child(5)::before { animation-delay: 2.3s; }
  .hero-rack i:nth-child(6)::before { animation-delay: 2.9s; }
}
@keyframes rack-led { 0%, 92%, 100% { opacity: 1; } 96% { opacity: .25; } }
@media (max-width: 899px) { .hero-rack { display: none; } }

/* A sub-page hero is quieter than the front door's. */
.hero-sub h1 { font-size: clamp(24px, 3.6vw, 36px); }

/* ---- the two products ---- */
.ptabs { display: flex; gap: var(--s-2); margin: 0 0 var(--d-gap); flex-wrap: wrap; }
.ptab { display: inline-flex; align-items: center; gap: 8px; min-height: 44px;
  padding: 10px 20px; border-radius: var(--r-pill); border: var(--bd);
  background: var(--surface); color: var(--ink-2); font-weight: 650;
  text-decoration: none; transition: background .12s, color .12s, border-color .12s; }
.ptab:hover { border-color: var(--accent); color: var(--ink); }
.ptab.on { background: var(--accent); border-color: var(--accent); color: var(--on-solid); }

/* ---- product cards ---- */
.pcard-grid { display: grid; gap: var(--d-gap);
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); align-items: start; }
.pcard { position: relative; display: flex; flex-direction: column; gap: var(--s-3);
  padding: var(--d-pad); border: var(--bd); border-radius: var(--r-lg);
  background: var(--surface); box-shadow: var(--lift-1);
  transition: transform .12s, box-shadow .12s, border-color .12s; }
.pcard:hover { transform: translateY(-2px); box-shadow: var(--lift-hover);
  border-color: var(--accent); }
.pcard-top { display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-2); flex-wrap: wrap; }
.pcard-top h3 { margin: 0; font-size: var(--fs-lg); line-height: 1.25; }
.pcard-desc { margin: 0; color: var(--ink-3); font-size: var(--fs-sm); line-height: 1.5; }

/* The one card the shop wants read first. An accent rule along the top rather than a
   different background: a second surface colour would fight the elevation scale. */
.pcard.featured { border-color: var(--accent); box-shadow: var(--lift-2); }
.pcard.featured::before { content: ""; position: absolute; left: -1px; right: -1px; top: -1px;
  height: 4px; border-radius: var(--r-lg) var(--r-lg) 0 0; background: var(--accent); }
.card-ribbon { position: absolute; top: -11px; right: var(--s-4); padding: 3px 12px;
  border-radius: var(--r-pill); background: var(--accent); color: var(--on-solid);
  font-size: 11px; font-weight: 700; letter-spacing: .06em; }

/* price */
.pcard-price { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px;
  padding-bottom: var(--s-3); border-bottom: 1px solid var(--line); }
.pcard-price .money { font-size: clamp(24px, 2.6vw, 30px); font-weight: 700;
  color: var(--price); font-variant-numeric: tabular-nums slashed-zero;
  letter-spacing: -.01em; }
.pcard-price .money-alt { font-size: var(--fs-md); font-weight: 650; color: var(--ink-2);
  font-variant-numeric: tabular-nums slashed-zero; }
.pcard-price .per { color: var(--ink-3); font-size: var(--fs-sm); }
.pcard-price .money-none { font-size: var(--fs-md); font-weight: 650; color: var(--ink-2); }

/* specification list — a label and a value per line, like a spec sheet */
.spec-rows { display: grid; gap: 0; }
.spec-row { display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s-3); padding: 7px 0; border-bottom: 1px dashed var(--line); }
.spec-row:last-child { border-bottom: 0; }
.spec-k { color: var(--ink-3); font-size: var(--fs-sm); flex: 0 0 auto; }
.spec-v { color: var(--ink); font-weight: 650; font-size: var(--fs-sm); text-align: right;
  min-width: 0; overflow-wrap: anywhere; }

.pcard-act { margin-top: auto; padding-top: var(--s-2); }
.btn.block, .pcard-act .btn { display: flex; width: 100%; justify-content: center; }

/* ---- the comparison table ---- */
/* Its own scroller: a six-column table must never make the page itself slide sideways. */
.matrix-wrap { overflow-x: auto; border: var(--bd); border-radius: var(--r-md);
  background: var(--surface); box-shadow: var(--lift-1); }
.spec-matrix { width: 100%; border-collapse: collapse; min-width: 640px; }
.spec-matrix th, .spec-matrix td { padding: 12px var(--s-4); text-align: center;
  border-bottom: 1px solid var(--line); font-size: var(--fs-sm); }
.spec-matrix thead th { background: var(--surface-2); font-size: var(--fs-base);
  position: sticky; top: 0; }
/* The row label is the anchor while the eye travels sideways, so it stays put. */
.spec-matrix th[scope="row"] { text-align: left; color: var(--ink-3); font-weight: 600;
  background: var(--surface); position: sticky; left: 0; white-space: nowrap; }
.spec-matrix tbody tr:nth-child(even) th[scope="row"],
.spec-matrix tbody tr:nth-child(even) td { background: var(--surface-2); }
.spec-matrix td { color: var(--ink); font-weight: 650;
  font-variant-numeric: tabular-nums slashed-zero; }
.spec-matrix .matrix-price td { padding-top: var(--s-4); }
.spec-matrix .matrix-price .money { font-size: var(--fs-lg); font-weight: 700;
  color: var(--price); }
.spec-matrix .matrix-act td, .spec-matrix .matrix-act th { border-bottom: 0; }

/* ---- what it is for ---- */
.use-grid { display: grid; gap: var(--d-gap);
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.use-card { padding: var(--d-pad); border: var(--bd); border-radius: var(--r-md);
  background: var(--surface); box-shadow: var(--lift-1); }
.use-card h3 { margin: 0 0 6px; font-size: var(--fs-md); }
.use-card p { margin: 0; color: var(--ink-3); font-size: var(--fs-sm); line-height: 1.55; }

/* ---- print: the shop window is not what anyone prints ---- */
@media print {
  .hero-rack, .ptabs, .card-ribbon { display: none; }
  .pcard, .use-card, .matrix-wrap { box-shadow: none; }
  /* the accent wash and the grid are painted on ::before/::after, which the print block's
     `background: none` on .hero itself does not reach */
  body[data-surface="public"] .hero::before,
  body[data-surface="public"] .hero::after { display: none; }
}

/* ===========================================================================
   PHASE 7 — the shop window, rebuilt.

   Everything here is scoped to body[data-surface="public"]. The management app
   keeps the Kệ & Giấy palette its own tests describe; only the pages a customer
   lands on are repainted. The look is the hosting-page convention the shop asked
   for: cool greys, one bright accent, white cards, generous air, and the tiers
   as columns of a single table rather than cards floating above one.

   Still no webfont and no image file — the app has to render offline, so the
   type is a system stack and every graphic is drawn in CSS or inline SVG.
   =========================================================================== */

/* One stack for the whole app. No webfont: this has to render with the network unplugged, so
   the face is whatever the machine already has, chosen in the order that looks best. */
:root {
  --ui-font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
             "Noto Sans", Arial, sans-serif;
}
body { font-family: var(--ui-font); -webkit-font-smoothing: antialiased; }

body[data-surface="public"] {
  /* The palette lives at :root now and paints the whole app, so nothing is redefined here.
     What stays public is the shop's own furniture: the hero wash and the tier table. */
  background: var(--paper);
}

/* ---- air ---- */
body[data-surface="public"] main { padding-block: var(--s-6); }
body[data-surface="public"] .band { margin-bottom: var(--s-8); }
body[data-surface="public"] .band > .section-head { margin-bottom: var(--s-5); }
/* Both shapes: some bands wrap the heading in .section-head to sit a button beside it, and
   some are just a heading. Styling only the wrapped ones left half the page's headings a size
   smaller than the other half, which reads as an accident rather than as a hierarchy. */
body[data-surface="public"] .section-head h2,
body[data-surface="public"] .band > h2,
body[data-surface="public"] .band > .panel > h2 {
  font-size: clamp(22px, 2.6vw, 32px); letter-spacing: -.02em; line-height: 1.15;
  text-wrap: balance; }
body[data-surface="public"] .section-head .muted { max-width: 62ch; line-height: 1.6;
  margin-top: 6px; }

/* ---- header ---- */
body[data-surface="public"] header .bar { background: var(--surface);
  border-bottom: var(--bd); box-shadow: none; }
body[data-surface="public"] header .btn { border-color: transparent; background: transparent;
  color: var(--ink-2); font-weight: 600; }
body[data-surface="public"] header .btn:hover { background: var(--surface-3); color: var(--ink); }
body[data-surface="public"] header .btn.primary { background: var(--accent);
  border-color: var(--accent); color: #fff; }
body[data-surface="public"] header .btn.primary:hover { background: var(--accent-hover);
  border-color: var(--accent-hover); }

/* ---- hero ---- */
body[data-surface="public"] .hero {
  background: var(--slab); border: 0; border-radius: var(--r-lg);
  padding: clamp(var(--s-6), 5vw, 72px) clamp(var(--s-5), 4vw, 56px);
  box-shadow: none; overflow: hidden; position: relative; }
/* A wash of the accent across the slab, and a faint grid over it: depth without a photograph. */
body[data-surface="public"] .hero::before { content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(90ch 40ch at 12% -20%, rgba(10,111,194,.55), transparent 60%),
    radial-gradient(70ch 40ch at 100% 120%, rgba(88,166,240,.28), transparent 55%);
  pointer-events: none; }
body[data-surface="public"] .hero::after { content: ""; position: absolute; inset: 0;
  background-image:
    repeating-linear-gradient(90deg, rgba(255,255,255,.05) 0 1px, transparent 1px 72px),
    repeating-linear-gradient(0deg, rgba(255,255,255,.05) 0 1px, transparent 1px 72px);
  -webkit-mask-image: radial-gradient(70% 70% at 60% 30%, #000, transparent);
  mask-image: radial-gradient(70% 70% at 60% 30%, #000, transparent);
  pointer-events: none; }
body[data-surface="public"] .hero > * { position: relative; z-index: 1; }
/* The hero is a slab region, so inside it --ink, --ink-2 and --surface-3 already resolve to
   values that read on near-black — and, because that palette is scoped to `screen`, they
   resolve to the LIGHT ones on paper. Naming the tokens is what makes the same rule correct in
   both places, and it is the only form an automated contrast check can read. */
body[data-surface="public"] .hero h1 { color: var(--ink);
  font-size: clamp(30px, 4.6vw, 52px); letter-spacing: -.03em; line-height: 1.06; }
body[data-surface="public"] .hero .lead { color: var(--ink-2);
  font-size: clamp(15px, 1.4vw, 18px); line-height: 1.6; }
body[data-surface="public"] .hero .eyebrow { color: var(--accent); letter-spacing: .16em; }
body[data-surface="public"] .hero-chip { background: var(--surface-3); color: var(--ink);
  border: 1px solid var(--line); }
body[data-surface="public"] .hero .btn.primary { background: var(--ink); border-color: var(--ink);
  color: var(--slab); font-weight: 700; }
body[data-surface="public"] .hero .btn.primary:hover { background: var(--ink-2);
  border-color: var(--ink-2); }
body[data-surface="public"] .hero .btn.ghost { border-color: var(--line-strong);
  color: var(--ink); }
body[data-surface="public"] .hero .btn.ghost:hover { background: var(--surface-3); }
body[data-surface="public"] .hero-rack i { background: var(--surface-3);
  border-color: var(--line); }
body[data-surface="public"] .hero-rack i::after {
  background: repeating-linear-gradient(90deg, var(--line-strong) 0 3px, transparent 3px 9px); }

/* ---- the product switch ---- */
body[data-surface="public"] .ptabs { background: var(--surface); border: var(--bd);
  border-radius: var(--r-pill); padding: 5px; display: inline-flex; gap: 4px;
  box-shadow: var(--lift-1); }
body[data-surface="public"] .ptab { border: 0; background: transparent; min-height: 40px;
  box-shadow: none; }
body[data-surface="public"] .ptab.on { background: var(--accent); color: #fff;
  box-shadow: none; }

/* ---- the tier table: the cards ARE the columns ---- */
/* Headroom at the top: the flag sits above the card's edge, and overflow-x on a box clips
   vertically too, so without the padding the badge is cut in half by its own scroller. */
.tier-wrap { overflow-x: auto; padding: 22px 2px 4px; margin-inline: -2px; }
/* Transparent, not white: the base `table` rule paints a card, and here the columns are the
   cards — the table's own fill showed through as a stray white block behind the row labels. */
.tier-table { border-collapse: separate; border-spacing: 10px 0; width: 100%;
  min-width: 760px; background: transparent; box-shadow: none; border: 0; }
.tier-table th, .tier-table td { text-align: center; }
.tier-table tbody th[scope="row"] { background: transparent; }
.tier-flag { z-index: 2; }

/* the row labels down the left */
.tier-table tbody th[scope="row"] { text-align: left; white-space: nowrap;
  color: var(--ink-3); font-weight: 600; font-size: var(--fs-sm);
  padding: 14px var(--s-4) 14px 0; width: 1%; }

/* each tier is a column that looks like a card: the head, the cells and the foot each
   paint one slice of the same shape */
/* The corner cell is a spacer above the row labels: no card, no rule, nothing to see. */
.tier-table thead th:first-child { background: transparent; border: 0; }
/* Top-aligned, and every column reserves the same room for the flag, so the tier names sit on
   one line across the range. Bottom-aligning them put the featured name 83px above its
   neighbours, which read as a mistake rather than as emphasis. */
.tier-table thead th:not(:first-child) { background: var(--surface); border: var(--bd);
  border-bottom: 0; border-radius: var(--r-lg) var(--r-lg) 0 0;
  padding: 52px var(--s-4) var(--s-4); vertical-align: top; position: relative; }
.tier-table tbody td { background: var(--surface); border-left: var(--bd);
  border-right: var(--bd); padding: 14px var(--s-3); color: var(--ink);
  font-weight: 650; font-variant-numeric: tabular-nums slashed-zero; }
.tier-table tbody tr:not(.tier-act) td { border-bottom: 1px solid var(--line); }
.tier-table tr.tier-act td { border-bottom: var(--bd); border-radius: 0 0 var(--r-lg) var(--r-lg);
  padding: var(--s-4); }

.tier-name { display: block; font-size: var(--fs-lg); font-weight: 700; color: var(--ink);
  letter-spacing: -.01em; }
/* Lifted out of the flow onto the card's top edge, so adding it does not push the featured
   column's contents down past everything beside it. */
/* Inside the card, not straddling its edge. Sitting on the border put the pill in the same
   few pixels as the accent rule drawn across the top of the featured column, and the two
   overlapped — the tone marks of "Được chọn nhiều nhất" disappeared into it. line-height 1.55
   because Vietnamese stacks its marks ABOVE the letter and needs the room. */
.tier-flag { position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
  white-space: nowrap; padding: 4px 12px; line-height: 1.55;
  border-radius: var(--r-pill); background: var(--accent-quiet); color: var(--accent);
  font-size: 11px; font-weight: 700; letter-spacing: .04em; }
.tier-price { display: block; margin-top: 10px; }
.tier-price .money { font-size: clamp(26px, 2.8vw, 34px); font-weight: 800; color: var(--price);
  letter-spacing: -.02em; font-variant-numeric: tabular-nums slashed-zero; }
.tier-price .per { color: var(--ink-3); font-size: var(--fs-sm); }
.tier-price .money-alt { font-size: var(--fs-md); color: var(--ink-2); font-weight: 650; }
.tier-price .money-none { font-size: var(--fs-base); color: var(--ink-2); font-weight: 650; }

/* the one the shop wants read first: lifted, accented, and taller than its neighbours */
.tier-table thead th.is-featured { border-color: var(--accent); box-shadow: var(--lift-2); }
.tier-table thead th.is-featured::before { content: ""; position: absolute;
  left: -1px; right: -1px; top: -1px; height: 5px;
  border-radius: var(--r-lg) var(--r-lg) 0 0; background: var(--accent); }
.tier-table td.is-featured { border-left-color: var(--accent);
  border-right-color: var(--accent); box-shadow: var(--lift-2); }
.tier-table tr.tier-act td.is-featured { border-bottom-color: var(--accent); }

/* ---- product cards (dedicated servers) ---- */
body[data-surface="public"] .pcard { border-radius: var(--r-lg); box-shadow: var(--lift-1);
  border: var(--bd); }
body[data-surface="public"] .pcard:hover { box-shadow: var(--lift-2);
  border-color: var(--accent); }
body[data-surface="public"] .pcard-top h3 { font-size: var(--fs-lg); letter-spacing: -.01em; }
body[data-surface="public"] .pcard-price .money { font-size: clamp(26px, 2.8vw, 34px);
  font-weight: 800; letter-spacing: -.02em; }
body[data-surface="public"] .spec-row { border-bottom-style: solid; }
body[data-surface="public"] .btn.primary { background: var(--accent); border-color: var(--accent);
  color: #fff; font-weight: 700; }
body[data-surface="public"] .btn.primary:hover { background: var(--accent-hover);
  border-color: var(--accent-hover); }
body[data-surface="public"] .btn { border-radius: var(--r-sm); min-height: 44px; }

/* ---- what it is for: line art, drawn inline ---- */
body[data-surface="public"] .use-card { border-radius: var(--r-lg); box-shadow: var(--lift-1);
  padding: var(--s-5); }
body[data-surface="public"] .use-card h3 { font-size: var(--fs-md); margin-bottom: 8px; }
.use-icon { display: block; width: 40px; height: 40px; margin-bottom: var(--s-3);
  color: var(--accent); }
.use-icon svg { width: 100%; height: 100%; display: block; }

/* ---- steps ---- */
body[data-surface="public"] .steps { gap: var(--s-3); }
body[data-surface="public"] .step { background: var(--surface); border: var(--bd);
  border-radius: var(--r-md); box-shadow: none; }

/* ---- footer ---- */
body[data-surface="public"] footer { background: var(--surface); border-top: var(--bd);
  margin-top: var(--s-8); }

@media (max-width: 720px) {
  body[data-surface="public"] .band { margin-bottom: var(--s-6); }
  .tier-table tbody th[scope="row"] { font-size: var(--fs-xs); }
}

/* ===========================================================================
   PHASE 8 — the three shop pages as one place.

   Same opening on each: hero, switch, content. The switch keeps its position
   between pages, so moving between them shifts a highlight rather than
   rearranging the page.
   =========================================================================== */

/* The front page: neither product chosen, because neither product page is open. Both sides
   read as equally available rather than one looking switched off. */
body[data-surface="public"] .ptabs.idle .ptab { color: var(--ink); }
body[data-surface="public"] .ptabs.idle .ptab:hover { background: var(--accent-quiet);
  color: var(--accent); }

/* The switch sits on the same line of the page on all three, so give it the same air. */
body[data-surface="public"] .ptabs { margin-block: 0 var(--s-5); }
body[data-surface="public"] .hero + .ptabs { margin-top: calc(var(--s-5) * -1 + var(--s-4)); }

/* The catalog's own bar no longer carries the page title, so it collapses to the counts and
   the sort control on one line. */
body[data-surface="public"] .shop-bar { margin-bottom: var(--s-5); }
body[data-surface="public"] .shop-bar .section-head { margin-bottom: 0; align-items: center; }
body[data-surface="public"] .shop-count { color: var(--ink-2); font-weight: 650; }

/* ---- moving between the three ----
   Cross-document view transitions were tried here and taken out again. `@view-transition`
   cannot be scoped to a body attribute — it is a document-level at-rule — so enabling it for
   the shop also enabled it for the management app, where the named ::view-transition-group
   rules crashed the renderer on the plans screen at 1280px wide. Reproduced by bisecting the
   stylesheet: removing these rules alone fixed it.

   What makes the three pages feel like one place is structural, and it survives without them:
   each opens hero -> switch -> content, and the switch lands on the same line of the catalog
   and the VPS page, so using it moves a highlight rather than the page. */


/* ---- hero motion ----
   The canvas sits ON the slab and UNDER the copy. The ::before/::after wash stays exactly where
   it was: it is painted first, so it is what a visitor sees before the script runs and what they
   keep if WebGL is unavailable. Fading in on the first drawn frame avoids a flash of the wash
   being replaced. */
body[data-surface="public"] .hero > canvas.hero-canvas {
  position: absolute; inset: 0; width: 100%; height: 100%; display: block;
  z-index: 0; opacity: 0; transition: opacity .5s ease; pointer-events: none; }
body[data-surface="public"] .hero > canvas.hero-canvas.on { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  body[data-surface="public"] .hero > canvas.hero-canvas { transition: none; } }
@media print { body[data-surface="public"] .hero > canvas.hero-canvas { display: none; } }
