/* EngineX Panel — the whole design language, hand-written, no framework.
   Rules are grouped by the thing they style, in roughly the order a page
   assembles: tokens, base, shell, page furniture, components, states. */

:root {
  --bg: #0B0C0E;
  --surface: #131417;
  --surface-raised: #191B1F;
  --border: rgba(255, 255, 255, 0.06);
  --border-strong: rgba(255, 255, 255, 0.10);
  /* The edge of a control someone types into. --border-strong composites to
     1.32:1 on --surface, which is right for a hairline that only separates two
     regions and wrong for the boundary that *identifies* a text field — that is
     non-text UI and owes 3:1. Measured 3.56 / 3.35 / 3.14:1 against --bg /
     --surface / --surface-raised, so it clears the floor on all three even
     though a resting field only ever meets the first two. */
  --border-control: #666970;
  --text: #E8E9EB;
  --text-muted: #9A9DA4;
  /* 4.5:1 against the lightest surface it sits on, --surface-raised, not just
     against --bg. .card-game uses it on a card that lightens on hover. */
  --text-faint: #84878F;
  /* Reserved for focus rings, the wordmark, and quiet fills, where it sits on a
     dark surface. Never for white text — see --accent-strong. */
  --accent: #6D7FF0;
  /* 4.5:1 with #fff on top. Button fills use this one. */
  --accent-strong: #4C5FD4;
  --accent-quiet: rgba(109, 127, 240, 0.14);
  /* The active-nav rule and the active-filter outline: a visible edge on a
     quiet fill, which is UI rather than text, so 3:1 is the bar it must clear. */
  --accent-edge: rgba(109, 127, 240, 0.38);

  --ok: #4E9E77;
  --neutral: #8A8F98;
  --warn: #B08843;
  /* Was #C0574F, which read as text at 3.87:1 on --surface-raised and failed the
     floor on every surface a .pill-danger can land on. Raised to clear 4.5:1 on
     all three (5.04:1 on the lightest). Same hue, same restraint. */
  --danger: #CE7068;
  --danger-quiet: rgba(206, 112, 104, 0.10);

  --space-1: 4px;  --space-2: 8px;   --space-3: 12px; --space-4: 16px;
  --space-5: 24px; --space-6: 32px;  --space-7: 48px; --space-8: 64px;

  --radius-sm: 6px; --radius: 10px; --radius-lg: 14px;
  --content-max: 1200px;
  --transition: 140ms cubic-bezier(0.4, 0, 0.2, 1);

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
}

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  /* The shell is a column so a short page still pins the footer to the bottom
     instead of leaving it floating mid-viewport. */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

a { color: inherit; text-decoration: none; }

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

/* For labels that a screen reader needs and a sighted reader gets from layout. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

.container { max-width: var(--content-max); margin: 0 auto; padding: 0 var(--space-5); }

/* The machine register: anything the system measured or assigned — versions,
   dates, byte sizes, digests, status codes — is set in mono with tabular
   figures. Anything a person wrote stays in the sans face. The split is what
   makes a column of values scannable without adding a single rule or fill. */
.value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  letter-spacing: -0.01em;
}

/* Topbar */
.topbar {
  position: sticky; top: 0; z-index: 10;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.topbar-inner {
  display: flex; align-items: center; gap: var(--space-5);
  height: 60px;
  max-width: var(--content-max); margin: 0 auto; padding: 0 var(--space-5);
}
.wordmark {
  font-weight: 600; letter-spacing: -0.02em; font-size: 17px;
  display: flex; align-items: center; height: 100%;
}
.wordmark span { color: var(--accent); }
/* Margin rather than a flex gap on .wordmark: gap would also separate the
   "Engine" text item from the "X" span and split the wordmark in two. The
   asset is shipped at 4x this size so it stays sharp on a Retina panel.

   The mark's cyan is #1FB6CB and the X beside it is --accent, #6D7FF0, so the
   lockup currently carries two unrelated blues. Aligning them is a brand
   decision, not a styling one: --accent also drives focus rings, links, quiet
   fills, and the pressed state of every button, so changing it restyles the
   whole panel. Left as-is deliberately. */
.wordmark-mark { width: 27px; height: 28px; margin-right: var(--space-2); }
.topbar nav { display: flex; gap: var(--space-4); height: 100%; }
.topbar nav a {
  position: relative;
  display: flex; align-items: center; height: 100%;
  color: var(--text-muted); font-size: 14px;
  transition: color var(--transition);
}
.topbar nav a:hover { color: var(--text); }
.topbar nav a[aria-current="page"] { color: var(--text); font-weight: 500; }
/* The active marker is a rule sitting on the topbar's own bottom hairline —
   depth from an edge rather than a fill, and it survives a colour-blind read
   because it is a position, not a hue. */
.topbar nav a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px;
  background: var(--accent); opacity: 0;
  transition: opacity var(--transition);
}
.topbar nav a[aria-current="page"]::after { opacity: 1; }

.topbar-end { margin-left: auto; display: flex; align-items: center; gap: var(--space-3); }

/* Signed in, the identity is one object rather than three loose items, so the
   username, the role, and the way out read as belonging together. */
.account {
  display: flex; align-items: center; gap: var(--space-3);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: var(--space-1) var(--space-2) var(--space-1) var(--space-4);
}
.account-name {
  font-weight: 500; font-size: 14px;
  max-width: 18ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.inline-form { margin: 0; display: inline-flex; }
.link-button {
  background: none; border: none; padding: 0;
  color: var(--text-muted); font: inherit; font-size: 14px; cursor: pointer;
  transition: color var(--transition);
}
.link-button:hover { color: var(--text); }
.account .link-button {
  padding: var(--space-1) var(--space-3);
  border-radius: 999px;
  border: 1px solid transparent;
  transition: color var(--transition), border-color var(--transition);
}
.account .link-button:hover { border-color: var(--border-strong); }

/* Page furniture */
main { padding: var(--space-7) 0 var(--space-8); flex: 1 0 auto; }
.page-head { margin-bottom: var(--space-6); }
/* Clamped so the 34px desktop size does not force a two-line title on a phone. */
.page-title {
  font-size: clamp(27px, 5.2vw, 34px);
  font-weight: 600; letter-spacing: -0.02em; line-height: 1.15;
  margin: 0 0 var(--space-2);
  text-wrap: balance;
  /* Product names are user data and can be a single unbroken token. */
  overflow-wrap: break-word;
}
.page-lede { color: var(--text-muted); margin: 0 0 var(--space-6); max-width: 64ch; }
.page-head .page-lede { margin-bottom: 0; }
.section-title {
  font-size: 20px; font-weight: 600; letter-spacing: -0.01em;
  margin: var(--space-7) 0 var(--space-4);
}
/* A kicker for the one true fact that frames the heading under it — the status
   code on an error, the count on a listing. Never decorative. */
.eyebrow {
  font-family: var(--font-mono);
  font-size: 12px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-faint);
  margin: 0 0 var(--space-3);
}

/* Filters */
.filters {
  display: flex; flex-wrap: wrap; gap: var(--space-2);
  margin-bottom: var(--space-5);
}
.filter {
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border); border-radius: 999px;
  color: var(--text-muted); font-size: 13px; line-height: 1.2;
  transition: color var(--transition), border-color var(--transition), background-color var(--transition);
}
.filter:hover { color: var(--text); border-color: var(--border-strong); }
.filter[aria-current="true"] {
  color: var(--text); background: var(--accent-quiet); border-color: var(--accent-edge);
}
/* The count sits with the filters because it describes what they produced. */
.filters-count {
  margin: 0 0 var(--space-6);
  color: var(--text-faint); font-size: 13px;
  font-variant-numeric: tabular-nums;
}

/* Cards */
/* min() rather than a bare 288px: below a 320px viewport the track would
   otherwise hold its minimum and push the body into a horizontal scroll. */
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(288px, 100%), 1fr)); gap: var(--space-4); }
.card {
  /* A column so every card's footer lands on the same baseline regardless of
     how long its summary runs. Ragged card bottoms are the templated look. */
  display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: var(--space-5);
  transition: background-color var(--transition), border-color var(--transition);
}
.card:hover { background: var(--surface-raised); border-color: var(--border-strong); }
.card-game { font-size: 12px; letter-spacing: 0.04em; color: var(--text-faint); text-transform: uppercase; }
.card-name {
  font-size: 17px; font-weight: 600; letter-spacing: -0.01em;
  margin: var(--space-2) 0 var(--space-1);
  overflow-wrap: break-word;
}
.card-summary {
  color: var(--text-muted); font-size: 14px; margin: 0;
  flex: 1 1 auto;
  transition: color var(--transition);
}
/* Hovering the card brings its summary up to full strength — the affordance is
   a colour change, so it costs nothing on a reduced-motion or greyscale read. */
.card:hover .card-summary { color: var(--text); }
.card-meta {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  margin-top: var(--space-4); padding-top: var(--space-3);
  border-top: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
}
.card-version { font-family: var(--font-mono); font-size: 13px; color: var(--text); letter-spacing: -0.01em; }
.card-updated { font-size: 12px; color: var(--text-faint); }
.card-updated time { font-family: var(--font-mono); letter-spacing: -0.01em; }

/* Pills */
.pill {
  display: inline-flex; align-items: center; padding: 1px var(--space-2);
  border-radius: var(--radius-sm); border: 1px solid var(--border-strong);
  font-size: 12px; font-weight: 500; letter-spacing: 0.02em; line-height: 1.5;
  color: var(--text-muted);
  white-space: nowrap;
}
/* Unfilled on purpose. The language's tie-breaker is the quieter option, and a
   filled status chip glows at a reader who is mostly scanning past it. The tone
   tints the hairline instead; the label is what carries the meaning. */
.pill-ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 38%, transparent); }
.pill-warn { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 38%, transparent); }
.pill-danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 38%, transparent); }
.pill-neutral { color: var(--neutral); border-color: var(--border-strong); }

/* Tables */
.table-wrap {
  overflow-x: auto;
  background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius);
}
table { width: 100%; border-collapse: collapse; font-size: 14px; font-variant-numeric: tabular-nums; }
/* Without a floor the table can never be wider than its wrapper, so overflow-x
   never fires and the prose column is crushed to a two-character ribbon instead
   — which is what "wide tables scroll in their own container" was supposed to
   prevent. The min-width is what makes that rule true rather than nominal. */
.table-wrap table { min-width: 560px; }
th {
  position: sticky; top: 0; background: var(--surface-raised);
  text-align: left; font-weight: 500; color: var(--text-muted);
  font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase;
  padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
td { padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); vertical-align: top; }
tr:last-child td { border-bottom: none; }
tbody tr { transition: background-color var(--transition); }
tbody tr:hover td { background: var(--surface-raised); }
/* Identifier columns stay as narrow as their content so the prose column keeps
   whatever width is left — the only column whose length actually varies. */
.col-tight { width: 1%; white-space: nowrap; }
.num { text-align: right; }
.mono { font-family: var(--font-mono); font-size: 13px; color: var(--text-muted); word-break: break-all; }
.row-lead { display: flex; align-items: center; gap: var(--space-2); }

/* Buttons */
.button {
  display: inline-flex; align-items: center; justify-content: center;
  padding: var(--space-3) var(--space-5);
  background: var(--accent-strong); color: #fff; border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font: inherit; font-weight: 500; text-align: center; cursor: pointer;
  transition: opacity var(--transition), background-color var(--transition), border-color var(--transition);
}
.button:hover { opacity: 0.9; }
.button-sm { padding: var(--space-2) var(--space-4); font-size: 14px; }
.button-block { display: flex; width: 100%; }
.button-secondary { background: var(--surface-raised); color: var(--text); border-color: var(--border-strong); }
.button-secondary:hover { border-color: var(--accent); opacity: 1; }
.button:disabled { opacity: 0.45; cursor: not-allowed; }
.button:disabled:hover { opacity: 0.45; border-color: var(--border-strong); }

/* Auth */
.auth { max-width: 400px; margin: 0 auto; }
/* A single 400px panel pinned to the top of a tall viewport leaves a screenful
   of dead space under it — the one page on the site whose content never grows
   to meet the fold. main is already the shell's growing child at flex: 1 0 auto,
   so centring inside it cannot clip: flex-shrink is 0, so main is never shorter
   than the panel, and the overflow that makes centred flex items unreachable is
   not on the table. Scoped through :has() rather than a class on <main>, which
   keeps the shared layout out of it; where :has() is unsupported the page just
   keeps today's top alignment. */
main:has(.auth) { display: flex; align-items: center; }
main:has(.auth) > .container { width: 100%; }
/* One step down from the catalog's title: this page is a single task, not a
   destination, and a 34px heading over a 400px column reads as shouting. */
.auth .page-title { font-size: 28px; }
/* The form is a panel rather than controls floating on the page background: it
   gives the one thing this page asks for an edge, and a place to end. */
.auth-panel {
  background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: var(--space-6);
}
/* Where signing in will land you, when something bounced you here. A
   --surface-raised strip with a hairline under it, bled out to the panel's
   padding edge — the same step a sticky table header makes over its rows, so
   the panel gains the site's own anatomy rather than a new device. It renders
   only when there is a destination to name, which is what keeps it a fact
   instead of an ornament. */
.auth-dest {
  margin: calc(var(--space-6) * -1) calc(var(--space-6) * -1) var(--space-5);
  padding: var(--space-3) var(--space-6);
  background: var(--surface-raised);
  border-bottom: 1px solid var(--border);
  /* The panel's radius less its 1px border, so the strip's corners sit inside
     the frame instead of cutting across it. */
  border-radius: calc(var(--radius-lg) - 1px) calc(var(--radius-lg) - 1px) 0 0;
  color: var(--text-muted); font-size: 13px;
}
/* Sans label, mono path: the destination is machine-assigned, the words around
   it are not. 6.35:1 and 14.19:1 on --surface-raised. */
.auth-dest .value { color: var(--text); }
.form { display: flex; flex-direction: column; gap: var(--space-4); }
.field { display: flex; flex-direction: column; gap: var(--space-2); }
.field label { font-size: 13px; color: var(--text-muted); }
.field input {
  background: var(--surface); color: var(--text);
  /* Not --border-strong: this hairline is the thing that tells a reader there
     is a box here to type in, so it owes the 3:1 that non-text UI owes. */
  border: 1px solid var(--border-control); border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4);
  font: inherit; font-size: 15px;
  transition: border-color var(--transition), background-color var(--transition);
}
/* Inside a panel the field drops a step *below* its container, so the control
   reads as a well to type into rather than a smudge on the same plane. */
.auth-panel .field input { background: var(--bg); }
.field input:hover { border-color: var(--neutral); }
.field input:focus { border-color: var(--accent); background: var(--surface-raised); }
/* The submit is not a third field, so it does not sit on the field rhythm:
   --space-4 of flex gap plus this makes --space-5 above it, one step out on the
   same scale. Scoped to the auth panel — the account page stacks two of these
   forms side by side and reads better tight. */
.auth-panel .form .button { margin-top: var(--space-2); }
.form-error {
  border: 1px solid var(--danger); border-radius: var(--radius-sm);
  background: var(--danger-quiet);
  padding: var(--space-3) var(--space-4);
  font-size: 14px; margin-bottom: var(--space-5);
}
.form-note { color: var(--text-faint); font-size: 13px; margin: var(--space-3) 0 0; }
.divider {
  display: flex; align-items: center; gap: var(--space-3);
  color: var(--text-faint); font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em;
  margin: var(--space-5) 0;
}
.divider::before, .divider::after {
  content: ""; flex: 1; height: 1px; background: var(--border-strong);
}

/* Product detail */
.back {
  color: var(--text-muted); font-size: 14px;
  display: inline-block; margin-bottom: var(--space-4);
  transition: color var(--transition);
}
.back:hover { color: var(--text); }
.detail-lede { color: var(--text-muted); font-size: 16px; margin: 0 0 var(--space-5); max-width: 68ch; }
.detail-body { max-width: 68ch; color: var(--text); margin: var(--space-6) 0 0; }
/* The action and the facts it acts on share one panel: what you are about to
   install, when it shipped, and how big it is, next to the button that does it. */
.detail-panel {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-5);
  justify-content: space-between;
  background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: var(--space-5);
}
.facts { display: flex; flex-wrap: wrap; gap: var(--space-6); margin: 0; }
.fact { display: flex; flex-direction: column; gap: var(--space-1); }
.fact dt { color: var(--text-faint); font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; }
.fact dd { margin: 0; color: var(--text); }
.detail-panel .button { flex: 0 0 auto; }
.detail-none { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-3); }
.detail-none .form-note { margin: 0; }

/* States */
.empty {
  padding: var(--space-8) var(--space-5); text-align: center;
  color: var(--text-muted);
  border: 1px dashed var(--border-strong); border-radius: var(--radius);
}
/* Both children are <p>, so the title rule is scoped the same way to keep the
   two from fighting over margin on specificity alone. */
.empty p { margin: 0 auto; max-width: 44ch; color: var(--text-muted); }
.empty .empty-title { margin-bottom: var(--space-2); font-size: 16px; font-weight: 600; color: var(--text); }
/* Error pages are a single column of consequence: what happened, then the way
   out. Narrow so the eye does not have to travel to find the action. */
.notice { max-width: 52ch; }
.notice .page-lede { margin-bottom: var(--space-6); }

footer {
  border-top: 1px solid var(--border);
  color: var(--text-faint); font-size: 13px;
  padding: var(--space-6) 0;
  flex-shrink: 0;
}
.footer-inner { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-5); justify-content: space-between; }
.footer-mark { color: var(--text-muted); }

@media (max-width: 640px) {
  .container { padding: 0 var(--space-4); }
  .topbar-inner { gap: var(--space-4); padding: 0 var(--space-4); }
  .account { padding-left: var(--space-3); gap: var(--space-2); }
  .account-name { max-width: 10ch; }
  /* The role pill is dropped from the narrow layout for room, but hidden rather
     than removed — display:none would take the role away from a screen reader
     too, which is the one reader that cannot infer it from anything else. */
  .account .pill {
    position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap; border: 0;
  }
  main { padding: var(--space-6) 0 var(--space-7); }
  .detail-panel { align-items: stretch; }
  .detail-panel .button { width: 100%; }
  .facts { gap: var(--space-5); }
}

@media (prefers-reduced-motion: reduce) {
  * { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}

/* ---------------------------------------------------------------------------
   Append new rules below this line.
   --------------------------------------------------------------------------- */

/* --- product icons ---
   Appended as one block so this branch merges cleanly with the parallel
   styles.css restyle. Only the classes below are new; nothing above is edited.
   App Store artwork is shown unmodified and scaled only — see Catalog and
   Products for the attribution terms. */
.card-head { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); }
.card-head-text { min-width: 0; }
/* The head owns its own rhythm. The stacked margins these two carry for the
   flat card layout would otherwise drag the text out of line with the icon. */
.card-head-text .card-game, .card-head-text .card-name { margin: 0; }

.game-icon {
  flex: none;
  width: 44px; height: 44px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  /* Its own surface, so the tile does not shift tone when .card:hover lifts
     the card behind it. Also the backdrop for artwork with transparency. */
  background: var(--surface-raised);
  object-fit: cover;
}
.game-icon-lg { width: 72px; height: 72px; border-radius: var(--radius); }

/* No cached icon: a quiet monogram of the game code. aria-hidden in the markup,
   because the product name beside it is already the accessible label. */
.game-icon-fallback {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 600; letter-spacing: 0.02em;
  color: var(--text-faint); text-transform: uppercase;
}
.game-icon-lg.game-icon-fallback { font-size: 18px; }

.detail-head { display: flex; align-items: center; gap: var(--space-4); }
.detail-head .page-title { margin: 0; }

/* --- account page ------------------------------------------------------- */
/* Appended as its own block: everything above is owned elsewhere. Reuses the
   existing tokens and .card / .pill / .field / .form-error components rather
   than restating them. */

/* The counterpart to .form-error, for a change that succeeded. Same geometry,
   --ok instead of --danger, and role="status" rather than role="alert" in the
   markup — a confirmation should not interrupt a screen reader. */
.form-ok {
  border: 1px solid var(--ok); border-radius: var(--radius-sm);
  background: rgba(78, 158, 119, 0.10);
  padding: var(--space-3) var(--space-4);
  font-size: 14px; margin-bottom: var(--space-5);
}

/* Two settings panels side by side, stacking below ~700px of column. */
.account-panels {
  /* min(300px, 100%) rather than a bare 300px: inside .container a 320px
     viewport leaves 272px of column, so a fixed 300px track overflows and puts
     the whole body into horizontal scroll. Same hardening as .grid. */
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  gap: var(--space-4); align-items: start;
}

/* .card's hover step is an affordance for a card that is a link. These panels
   are static containers, so the lift is cancelled rather than left to imply
   something is clickable. */
.account-panel:hover { background: var(--surface); border-color: var(--border); }
.account-panel-title { font-size: 17px; font-weight: 600; margin: 0; }
.account-panel .form-note { margin: var(--space-1) 0 var(--space-4); }
.account-panel .form { margin-top: var(--space-4); }
/* --- discord sign-in --- */
/* The Discord option is an <a href="/auth/discord">, not a <button>: the OAuth
   handshake starts with a plain GET navigation and there is no form state to
   submit. These rules only restore the metrics an anchor does not inherit from
   .button — everything else, including the hover treatment, comes from
   .button-secondary above. */
/* Deliberately does NOT set `color`. It used to, back when .button was
   inline-block and an anchor needed the metrics restated — but .button is now
   inline-flex and centred, `a` is already text-decoration:none, and the colour
   comes from whichever variant is applied. Restating `color: var(--text)` here
   would win on source order and silently repaint a *primary* anchor button from
   #fff (5.39:1 on --accent-strong) to --text (4.44:1) — under the floor. The
   fourth instance of the surface bug, avoided by not writing the line. */
.button-link {
  display: inline-flex; align-items: center; justify-content: center;
  text-decoration: none;
}
.button-link.button-block { display: flex; }
.button-link:hover { text-decoration: none; }

/* --- user panel and admin panel ----------------------------------------- */
/* Appended as one block: everything above is owned elsewhere. Reuses the
   existing tokens and the .card / .pill / .table-wrap / .field / .form-error /
   .form-ok / .button* components rather than inventing parallels beside them.

   Contrast was measured against the lightest surface each colour can land on,
   which for anything inside .table-wrap is the --surface-raised of a row
   hover — not the --surface of the container. That is the check the three
   shipped instances of the surface bug all skipped. */

/* Where a column genuinely has no value. --neutral (5.31:1 on --surface-raised)
   rather than --text-faint, which lands exactly on the 4.5:1 floor there and
   leaves no margin for a later surface change. The word is spelled out because
   an em dash is read aloud as nothing at all. */
.none { color: var(--neutral); }

/* A sentence that qualifies the section heading above it, not the page. */
.section-note { margin: calc(var(--space-4) * -1) 0 var(--space-4); max-width: 72ch; }

/* Fact lists — account status, account information.
   A <dl>: these are term/value pairs, and a table of two columns would claim a
   grid relationship between rows that does not exist. */
.info-list {
  margin: 0 0 var(--space-5);
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface);
}
.info-row {
  display: flex; align-items: center; gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
}
.info-row:last-child { border-bottom: none; }
.info-list dt {
  flex: 0 0 12rem;
  color: var(--text-muted); font-size: 13px;
}
.info-list dd { margin: 0; min-width: 0; overflow-wrap: break-word; }

/* The overview's three totals. min() for the same reason .grid carries it:
   below a 320px viewport a fixed track would push the body into horizontal
   scroll. */
.stat-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
  gap: var(--space-4);
}
.stat {
  background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: var(--space-5);
}
.stat-label {
  font-size: 12px; font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text-muted);
}
/* The number is the content of the card, so it carries the machine register the
   rest of the panel uses for anything the system counted. */
.stat-value {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: 34px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.15;
  margin: var(--space-2) 0 var(--space-1);
}
.stat-detail { color: var(--text-faint); font-size: 13px; font-variant-numeric: tabular-nums; }

/* A filter bar is a GET form, not a row of links: three independent controls
   are a combinatorial set of hrefs, and a text search cannot be a link at all.
   align-items:end so the button sits on the inputs' baseline, not the labels'. */
.filter-bar {
  display: flex; flex-wrap: wrap; align-items: end; gap: var(--space-3);
  margin-bottom: var(--space-5);
}
.filter-bar .field { flex: 1 1 12rem; min-width: 0; }
.filter-bar .button { flex: 0 0 auto; }

/* Selects were not in the language yet. Given the same treatment as .field
   input so a form does not read as two different design systems, plus the
   appearance reset an <select> needs to accept any of it. */
.field select, .row-actions select {
  appearance: none;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4);
  font: inherit; font-size: 15px;
  transition: border-color var(--transition), background-color var(--transition);
}
.field select:hover, .row-actions select:hover { border-color: var(--neutral); }
.field select:focus, .row-actions select:focus {
  border-color: var(--accent); background: var(--surface-raised);
}
/* The option list is painted by the OS, which does not inherit the page's
   colours on every platform. Stating both keeps it legible where it does. */
.field option, .row-actions option { background: var(--surface); color: var(--text); }

/* Row controls. The select drops a step below the --surface table around it,
   the same "one step down from whatever is behind it" rule the panel fields
   follow, so it reads as a control rather than a smudge on the row. */
.row-actions { display: flex; align-items: center; gap: var(--space-3); }
.row-actions select {
  background: var(--bg);
  padding: var(--space-1) var(--space-2); font-size: 13px;
}
.row-actions .inline-form { align-items: center; gap: var(--space-2); }
/* Why a row has no controls, stated rather than left as an empty cell. */
.row-note { color: var(--neutral); font-size: 13px; white-space: nowrap; }

/* Per-table floors. Without one a table can never exceed its wrapper, so
   overflow-x never fires and the prose column is crushed instead — the reason
   .table-wrap table already carries 560px. Each is the tight columns' content
   plus a readable measure for the column that holds prose. */
.table-wrap table.table-service   { min-width: 520px; }
.table-wrap table.table-licenses  { min-width: 760px; }
.table-wrap table.table-products  { min-width: 720px; }
/* The widest: seven columns, one of which holds two controls. */
.table-wrap table.table-accounts  { min-width: 940px; }

/* The documented stand-in for licence issuing. A note rather than a disabled
   button: there is no decision for an admin to make here, and a disabled
   control would imply the feature is one permission away. */
.admin-placeholder { margin-top: var(--space-7); }
.admin-placeholder:hover { background: var(--surface); border-color: var(--border); }
.admin-placeholder .form-note { margin-top: var(--space-2); max-width: 72ch; }

@media (max-width: 640px) {
  /* Stacked, because a 12rem term column leaves a phone nothing for the value. */
  .info-row { flex-direction: column; align-items: start; gap: var(--space-1); }
  .info-list dt { flex: none; }
  .filter-bar { flex-direction: column; align-items: stretch; }
  .stat-value { font-size: 28px; }
}

/* --- registration ---
   Appended rather than folded into the Auth block above, which is owned by the
   login page. Everything here leans on the existing field and button rules. */

/* A rule the schema will enforce, said before it is broken rather than after.
   --text-faint on --surface is 5.13:1, clear of the floor. */
.field-note { color: var(--text-faint); font-size: 12px; margin: var(--space-1) 0 0; }

/* The way to the other auth page. Below the panel's own actions and separated
   by a hairline, because it is the path for the minority who came to the wrong
   one — but it is still a control, and it looks like one. */
.auth-alt {
  margin: var(--space-5) 0 0;
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
  text-align: center;
}
.auth-alt-label {
  display: block;
  margin-bottom: var(--space-3);
  color: var(--text-muted); font-size: 14px;
}

/* Third in the hierarchy, and still a full-width target. Rank is carried by the
   fill — transparent against --button-secondary's --surface-raised — rather
   than by shrinking the thing to two words of underlined body copy, which is
   what the first version did and why it did not read as clickable.
   The edge is --border-control, not --border-strong. With a transparent fill
   the border is the only thing saying "this is a control", which is exactly the
   3:1 case that token exists for — 3.35:1 on --surface against --border-strong's
   1.32:1. Getting this wrong is what the first version of this link did. */
.button-quiet {
  background: transparent; color: var(--text);
  border-color: var(--border-control);
}
.button-quiet:hover { background: var(--surface-raised); border-color: var(--accent); opacity: 1; }

/* --- admin product editing --- */

/* The listing's own action row: count on the left, the way to add on the right. */
.admin-listing-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-4); flex-wrap: wrap;
}
.admin-listing-head .filters-count { margin-bottom: var(--space-4); }

/* A link inside a table cell. Underlined rather than coloured, for the same
   reason status never rides on colour alone: the cell beside it is plain text
   and the only difference must survive a greyscale read. */
.text-action { text-decoration: underline; text-underline-offset: 2px; }
.text-action:hover { color: var(--accent); }

/* Forms on an admin page sit in a card rather than the auth panel, and cap
   their measure: a full 1200px text input is unreadable and invites the eye to
   travel further than the content ever does. */
.admin-form-card { max-width: 720px; margin-bottom: var(--space-6); }
/* One row where the control and its button belong together — status is a single
   decision, not a form to fill in. */
.form-row { flex-direction: row; align-items: flex-end; gap: var(--space-4); flex-wrap: wrap; }
.form-row .field { flex: 1 1 200px; }

/* textarea was never styled: .field input covered every control the panel had
   until products gained a description. Same box as an input, same 3:1 edge. */
.field textarea {
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border-control); border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4);
  font: inherit; font-size: 15px;
  width: 100%; resize: vertical; min-height: 96px;
  transition: border-color var(--transition), background-color var(--transition);
}
.field textarea:hover { border-color: var(--neutral); }
.field textarea:focus { border-color: var(--accent); background: var(--surface-raised); }

/* A digest is 64 characters and never needs reading in full — it needs to be
   present, comparable, and out of the way. */
.digest { word-break: break-all; color: var(--text-muted); font-size: 12px; }
