/* Panindigan stylesheet bundle.
   Generated by scripts/build-css.mjs from the four hand-written
   sources in public/css/. Do not edit this file directly.
   Design tokens are declared in base.css and are not substituted from
   anywhere else — a second source for them is a silent conflict. */

/* --- base.css --- */
:root {
  /* ==========================================================================
   * Panindigan design tokens — Light Sky Blue
   *
   * The four supplied brand values:
   *
   *     Light Sky Blue   #87CEFA
   *     Soft Neutral     #F7FAFC
   *     White Surface    #FFFFFF
   *     Deep Navy        #17324D
   *
   * ## Why there is an accent ramp and not one accent
   *
   * #87CEFA on #FFFFFF measures **1.72:1**. WCAG AA requires 4.5:1 for body
   * text and 3:1 for the non-text parts of a control — a border, a focus ring,
   * the edge of a filled control. The brand colour fails all three.
   *
   * So the raw brand is a FILL, and the action colour is the same hue walked
   * dark until it passes. The numbers are in the table above each token; they are
   * there so a later edit knows what it is trading away.
   *
   * ## Why text on the fill is Deep Navy
   *
   * 7.65:1, and it is one of the four colours already supplied. A fifth colour
   * to make a button readable would have been the other option, and it would
   * have been a colour the brand does not have.
   * ========================================================================== */

  /* --- the brand, as supplied ------------------------------------------- */

  --brand-sky: #87CEFA;
  --brand-neutral: #F7FAFC;
  --brand-surface: #FFFFFF;
  --brand-navy: #17324D;

  /* --- the action ramp ---------------------------------------------------
   * Each value's contrast against #FFFFFF is stated beside it. If one is
   * changed, check the number — the border of this ramp is 3:1, and
   * --color-accent additionally has to clear 4.5:1 because links use it. */

  --color-accent: #2277B8;        /* 4.77:1  links, focus rings, active states */
  --color-accent-hover: #1B6BA8;  /* 5.65:1 */
  --color-accent-active: #155E97; /* 6.82:1 */
  --color-accent-large: #2E85C7;  /* 3.97:1  large text only, never body text */

  /* --- primary / surfaces ------------------------------------------------ */

  --color-primary: #17324D;
  --color-surface: #FFFFFF;

  /* The page background is Soft Neutral; the surfaces sitting on it are white.
   * That one-step difference is what makes cards read as cards without a
   * heavy border. */
  --color-bg: #F7FAFC;
  --surface-1: #FFFFFF;
  --surface-2: #F7FAFC;
  --surface-3: #EDF3F9;
  --surface-sunken: #E8F0F8;
  --surface-overlay: rgba(23, 50, 77, 0.42);

  /* --- text --------------------------------------------------------------
   * All four are Deep Navy at four different weights of emphasis, rather than
   * one navy plus a grey. A grey body text on a blue-tinted page reads as
   * washed out next to the navy headings, and the mismatch is visible without
   * being nameable. */

  --color-text: #17324D;   /* 13.13:1  headings and body */
  --color-muted: #4A6480;  /*  6.05:1  secondary text, table cells */
  --color-faint: #6B8199;  /*  3.94:1  labels and meta -- never body copy */
  --on-brand: #17324D;     /*  7.65:1  text on #87CEFA, the brand fill */

  /* Text on the ACTION colour, which is a different fill.
   *
   * This is not `--on-brand`. That token is text on #87CEFA, and the buttons
   * fill with --color-accent (#2277B8), where Deep Navy measures 2.75:1 and
   * white measures 4.77:1. One token serving two fills is what left every
   * dashboard button under AA while the featured plan — which uses the brand
   * gradient and a different rule — looked correct.
   */
  --on-accent: #FFFFFF;    /*  4.77:1  text on #2277B8 */
  --on-accent-hover: #FFFFFF;

  /* Hover steps, darker *within their own hue*.
   *
   * #4f46e5 was here: indigo, the previous theme's accent, never in the
   * tokenisation map. A hover in a different hue from the fill is the most
   * visible kind of leftover. #dc2626 was the dark-theme red. */
  --btn-primary-hover: #1B6BA8;   /* 5.65:1 */
  --btn-primary-active: #155E97;  /* 6.82:1 */
  --btn-danger-hover: #92180F;    /* 8.90:1 */
  --btn-danger-active: #7A150D;
  --btn-success-hover: #0F5E31;   /* 7.88:1 */
  --btn-success-active: #0C4E28;

  /* --- borders -----------------------------------------------------------
   * Navy at low alpha rather than a grey, so a border sits in the same colour
   * family as the text it separates. A neutral grey border against a blue-tinted
   * surface reads as dirt. */

  --color-border: #DCE6F0;
  --border-subtle: rgba(23, 50, 77, 0.10);
  --border-strong: rgba(23, 50, 77, 0.20);

  /* Hover and active overlays.
   * On the dark theme these were white at low alpha. On white that is
   * invisible, so they are navy at low alpha — a hover on a light surface has
   * to go darker, not lighter. */

  --overlay-hover: rgba(23, 50, 77, 0.04);
  --overlay-active: rgba(23, 50, 77, 0.08);

  /* --- accent tints ------------------------------------------------------ */

  --accent-soft: rgba(34, 119, 184, 0.12);
  --accent-softer: rgba(34, 119, 184, 0.06);
  --accent-ring: rgba(34, 119, 184, 0.42);

  /* --- status ------------------------------------------------------------
   * Each is a darker step of the same hue the dark theme used. The old values
   * (#22c55e, #f59e0b, #ef4444) are legible as fills on a dark page and sit at
   * 2.2:1 to 3.0:1 on white — fine as a dot, unreadable as a label. */

  --color-success: #12793F;
  --color-warning: #8A5200;
  --color-danger: #B42318;
  --color-info: #0B6E9E;

  /* --- the status ramp ---------------------------------------------------
   * Tints for backgrounds, and readable foregrounds for the text on them. A
   * tint paired with the fill colour is the mistake the dark theme made with
   * #fca5a5 and #fcd34d — 200-weight values that only read on dark. */

  --success-soft: rgba(18, 121, 63, 0.10);
  --warning-soft: rgba(138, 82, 0, 0.10);
  --danger-soft: rgba(180, 35, 24, 0.10);
  --info-soft: rgba(11, 110, 158, 0.10);

  --success-text: #0F5E31;
  --warning-text: #6E4200;
  --danger-text: #92180F;
  --info-text: #085A82;

  /* --- the gradient system ----------------------------------------------
   * Every gradient in the site is built from two of the four brand colours.
   * Three stops, light to dark, all in the sky family — a gradient that
   * leaves the family reads as a different product. */

  /* The brand sky.
   *
   * Three stops, light to dark, all in one hue. This is the one gradient on the
   * site that is a single colour, and it is the one the brand is named for. */
  --accent-gradient: linear-gradient(135deg, #87CEFA 0%, #4AA3DF 55%, #2E85C7 100%);

  /* There is no second accent hue. An earlier pass put a violet here (#7C3AED)
   * as gradient ends and text accents, and it read as template dressing: it is
   * not one of the four brand colours, and everywhere the eye caught it — the
   * primary button's far end, the price and stat numerals, a geometry wash --
   * it competed with the brand sky instead of belonging to the brand. The
   * token is gone in this release and every gradient below holds to the single
   * sky ramp end to end. */
  --accent-gradient-soft: linear-gradient(135deg, rgba(135, 206, 250, 0.22), rgba(46, 133, 199, 0.14));
  --brand-gradient-duo: linear-gradient(135deg, #87CEFA 0%, #4AA3DF 55%, #2E85C7 100%);
  --brand-gradient: linear-gradient(160deg, #FFFFFF 0%, #F7FAFC 55%, #EDF3F9 100%);
  --navy-gradient: linear-gradient(160deg, #17324D 0%, #1E4265 100%);

  /* --- shadows -----------------------------------------------------------
   * Lighter and more diffuse than the dark theme's. A shadow tuned for a dark
   * page is built from high-alpha black, which on white reads as a smudge
   * rather than as elevation. These are navy, because a shadow is a tint of
   * the ambient light and this page's light is blue. */

  --shadow-sm: 0 1px 2px rgba(23, 50, 77, 0.06);
  --shadow-md: 0 4px 14px rgba(23, 50, 77, 0.09);
  --shadow-lg: 0 16px 40px rgba(23, 50, 77, 0.13);
  --shadow-brand: 0 8px 24px -8px rgba(34, 119, 184, 0.28);

  /* --- typography -------------------------------------------------------- */

  --font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, monospace;

  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.375rem;
  --text-2xl: 1.75rem;
  --text-3xl: 2.25rem;

  --leading-tight: 1.2;
  --leading-snug: 1.35;
  --leading-normal: 1.6;
  --leading-relaxed: 1.75;

  /* --- space ------------------------------------------------------------- */

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;

  /* --- shape ------------------------------------------------------------- */

  --radius-sm: 7px;
  --radius: 10px;
  --radius-lg: 14px;
  --radius-pill: 999px;
  --ring-width: 2px;
  --ring-offset: 2px;
  --max-width: 1200px;

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

  --transition-fast: 120ms cubic-bezier(0.4, 0, 0.2, 1);
  --transition: 180ms cubic-bezier(0.4, 0, 0.2, 1);
  --transition-slow: 280ms cubic-bezier(0.16, 1, 0.3, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);

  /* --- layer ------------------------------------------------------------- */

  --z-tooltip: 970;
  --z-popover: 950;
  --z-modal: 900;
}/* Panindigan base stylesheet.
   Hand-written CSS. No framework, no preprocessor.
   Tokens are injected at build time from config/website.json. */



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

/* ---------------------------------------------------------------------------
 * Section rhythm
 *
 * A long page needs landmarks. Every band used to sit on the same background at
 * the same spacing, so the eye had nothing to catch on and a reader scrolling
 * had no way to know a section had ended.
 *
 * Alternating the surface on every second section is the cheapest way to give a
 * page structure, and it costs no markup — `main > section:nth-of-type(even)`
 * does it for the pages that put their content in sections, which is most of
 * them.
 *
 * It is scoped to `main` on purpose: the footer and the header are chrome, and a
 * banded footer reads as a bug.
 */
main > section:nth-of-type(even) {
  background: var(--surface-1);
  /* Full-bleed: the band should reach the window edges, and a `.container`
     inside it is what keeps the text to a readable measure. */
  box-shadow: 0 0 0 100vmax var(--surface-1);
  clip-path: inset(0 -100vmax);
}

/* The top and bottom padding lives on the container rather than the section, so
   the band does not fight whatever padding the page already set. */
main > section {
  padding-block: var(--space-7);
}
main > section:first-of-type {
  padding-block-start: var(--space-6);
}

/* ---------------------------------------------------------------------------
 * The grid
 *
 * One utility, and it is the only one that earns its place: a responsive
 * column set with no markup beyond `class="grid"` on a container. Cards inside
 * it line up without a single page-specific rule, which is what makes forty
 * pages look like one product.
 */
.grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
}

.grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); }
.grid--4 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr)); }

@media (max-width: 640px) {
  main > section { padding-block: var(--space-6); }
}

/* ---------------------------------------------------------------------------
 * Eyebrow
 *
 * The small uppercase label above a heading. `--accent-gradient` was declared in
 * 0.23.0 and nothing used it, so the accent appeared at full strength only on
 * the brand mark and the sign-in button.
 *
 * Applied by class rather than by selector, because "the first paragraph before
 * an h1" is the kind of heuristic that makes a page look wrong for a reason
 * nobody can find in the stylesheet.
 */
.eyebrow {
  display: block;
  margin: 0 0 var(--space-3);
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--color-muted);
}

/* The eyebrow used to grow a 22px gradient dash before its text. It is a
 * decoration with no information in it — the same stroke above every section
 * on every page — so it went. The label itself, set small and apart, already
 * introduces the section. */

/* ---------------------------------------------------------------------------
 * Link affordance
 *
 * A text link in prose changed colour on hover and nothing else, which on a dark
 * surface is nearly invisible. A 1px underline that appears on hover — and is
 * always there in prose — gives a link a shape, which is how a reader finds one
 * while scanning rather than while reading.
 */
main a:not(.btn):not(.button):not([class*="nav"]):not(.site-footer__links a) {
  text-decoration: none;
  color: var(--accent-text);
  transition: color var(--transition-fast);
}

main a:not(.btn):not(.button):not([class*="nav"]):hover,
main a:not(.btn):not(.button):not([class*="nav"]):focus-visible {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
}

/* ---------------------------------------------------------------------------
 * Prose measure
 *
 * A line longer than about 75 characters is measurably harder to track back
 * from, and the pages that hold a single long column of body text had no limit
 * on it.
 */
.prose {
  max-width: 68ch;
}

.prose > * + * {
  margin-block-start: var(--space-4);
}

.prose h2 { margin-block-start: var(--space-6); }
.prose h3 { margin-block-start: var(--space-5); }
.prose ul,
.prose ol { padding-inline-start: var(--space-5); }
.prose li + li { margin-block-start: var(--space-2); }
.prose strong { color: var(--color-text); font-weight: 650; }

/* ---------------------------------------------------------------------------
 * Layout utilities
 *
 * The few measurements that recur across the pages: a measure, a space before
 * a block that is not in a flow, a centring, an alignment, and the sizes of a
 * skeleton.
 *
 * They are here because a size written in the markup is a size nobody decided
 * on and nobody can change in one place — and because a page built from
 * components should not carry a number in its own source. There were a hundred
 * and seventeen inline styles, and almost all of them were one of twenty
 * measurements repeated.
 *
 * The modifier pattern is the one the grid and the skeletons already use, so
 * this is not a new vocabulary. It is the existing one, applied to the
 * measurements that were being written out by hand.
 * ------------------------------------------------------------------------- */

/* The measure: a line of prose that is comfortable to read. */
.measure {
  max-width: 54ch;
  margin-inline: auto;
}
.measure--wide { max-width: 64ch; }
.measure--narrow { max-width: 52ch; }
.measure--spaced { margin-block-end: var(--space-5); }
.measure--spaced-tight { margin-block-end: var(--space-4); }

/* The space before a block that is not in a flow. */
.spaced { margin-block-start: var(--space-6); }
.spaced--loose { margin-block-start: var(--space-7); }
.spaced--tight { margin-block-start: var(--space-5); }
.spaced--small { margin-block-start: var(--space-4); }
.spaced-end { margin-block-end: var(--space-5); }
.spaced-end--tight { margin-block-end: var(--space-4); }

/* The alignment. */
.text-start { text-align: left; }

/* The centring. */
.cluster--center { justify-content: center; }

/* A larger label, for the few places that carry one. */
.text-large { font-size: var(--text-2xl); }

/* --- the skeleton sizes ---
 *
 * A placeholder has to be the size of the thing it stands in for, or the page
 * jumps when the real thing arrives. The heights are here so that they are
 * said once.
 */
.skeleton--line { height: 1.75rem; }
.skeleton--heading { height: 2rem; margin-block-end: 0.5rem; }
.skeleton--caption { height: 1.5rem; margin-block-end: var(--space-4); }
.skeleton--sm { height: 8rem; }
.skeleton--md { height: 10rem; }
.skeleton--lg { height: 12rem; }
.skeleton--wide { width: 60%; }
.skeleton--narrow { width: 45%; }

/* ---------------------------------------------------------------------------
 * Heading anchors
 *
 * Documentation headings get a hover-revealed link glyph, which is how a reader
 * copies a link to one section. Purely decorative until hovered, and hidden from
 * assistive technology, because the heading text is already the accessible name
 * and a second link to the same place is noise.
 */
.prose h2,
.prose h3 { position: relative; scroll-margin-top: calc(var(--header-height) + var(--space-4)); }

.heading-anchor {
  position: absolute;
  inset-inline-start: -1.1em;
  color: var(--color-muted);
  opacity: 0;
  text-decoration: none;
  font-weight: 400;
  transition: opacity var(--transition-fast), color var(--transition-fast);
}

.prose h2:hover .heading-anchor,
.prose h3:hover .heading-anchor,
.heading-anchor:focus-visible {
  opacity: 1;
  color: var(--color-accent);
}

/**
 * Reduced motion, honoured in one place.
 *
 * It was honoured in exactly one rule — the documentation caret — so a
 * visitor who had asked for less motion got a page that animated everything
 * else. `prefers-reduced-motion` is a request about the whole interface, not
 * about whichever component happened to remember.
 *
 * `animation-duration: 0.01ms` rather than `none`, because `animation: none`
 * drops the `animationend` event and any transition driven by it stops firing.
 * Smooth scrolling is a separate opt-in and has to be turned off explicitly.
 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  color: var(--color-text);
  /* The page background, not the brand colour.
   *
   * This read `var(--color-primary)`, which was correct while the theme was dark
   * because the brand colour *was* the background. The moment the brand changed,
   * the page changed with it — the whole site went Deep Navy against its own
   * body text, at 1.00:1.
   *
   * One token doing two jobs is how a theme change reaches places it was not
   * aimed at. The page background is `--color-bg` and stays that way whatever
   * the brand is. */
  background-color: var(--color-bg);
  background-image:
    radial-gradient(ellipse 80% 50% at 50% -20%, rgba(34, 119, 184, 0.18), transparent),
    radial-gradient(ellipse 60% 40% at 100% 0%, var(--accent-softer), transparent);
  background-attachment: fixed;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

h1, h2, h3, h4, h5, h6 {
  margin: 0 0 var(--space-3);
  font-weight: 650;
  letter-spacing: -0.02em;
  color: var(--color-text);
  /* A heading is one or two lines, and `balance` stops a short second line
     leaving a rag of one word on the right. */
  text-wrap: balance;
}

h1 {
  font-size: clamp(2rem, 5vw, 3.25rem);
  line-height: var(--leading-tight);
  letter-spacing: -0.03em;
}
h2 {
  font-size: clamp(1.5rem, 3vw, 2rem);
  line-height: var(--leading-snug);
  margin-top: var(--space-6);
}
h3 { font-size: var(--text-lg); line-height: var(--leading-snug); margin-top: var(--space-5); }
h4 { font-size: var(--text-base); line-height: var(--leading-snug); margin-top: var(--space-4); }
h5, h6 {
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  margin-top: var(--space-4);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-muted);
}

/* The first heading in a section should not carry the section's top margin --
   the section already provides it, and the two together open a gap nothing
   explains. */
section > h2:first-child,
section > h3:first-child { margin-top: 0; }

p {
  margin: 0 0 var(--space-4);
  line-height: var(--leading-relaxed);
  /* `pretty` avoids a single word alone on the last line of a paragraph. */
  text-wrap: pretty;
}

/* A lead paragraph is the one under the page heading. */
p.lead,
.text-lg { font-size: var(--text-lg); line-height: var(--leading-normal); color: var(--color-muted); }

a {
  color: var(--color-accent);
  text-decoration: none;
  transition: color var(--transition-fast);
  /* A link in prose is underlined. A link that is a button or a nav item is not,
     and those set their own. */
  text-underline-offset: 0.2em;
}
a:hover { color: var(--color-text); }

/* Prose links carry an underline that appears on hover, rather than one that is
   always there -- the middle ground between invisible and noisy. */
main a:not(.btn):not(.button):not([class*="nav"]) {
  text-decoration: underline;
  text-decoration-color: var(--border-strong);
  text-decoration-thickness: 1px;
  transition: text-decoration-color var(--transition-fast), color var(--transition-fast);
}
main a:not(.btn):not(.button):not([class*="nav"]):hover {
  text-decoration-color: currentColor;
}
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: var(--ring-width) solid var(--accent-ring);
  outline-offset: var(--ring-offset);
  border-radius: 6px;
}

/* A focus ring on an element that already has a rounded corner should follow
   that corner, not a 6px one. */
.btn:focus-visible,
.site-header__logo:focus-visible { outline-offset: 3px; }

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

code {
  background: rgba(23, 50, 77, 0.07);
  padding: 0.15em 0.4em;
  border-radius: 4px;
}

pre {
  background: rgba(0, 0, 0, 0.4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-4);
  overflow-x: auto;
}

img, svg, video { max-width: 100%; height: auto; display: block; }

ul, ol { margin: 0 0 var(--space-4); padding-left: var(--space-5); }

hr {
  border: none;
  border-top: 1px solid var(--color-border);
  margin: var(--space-6) 0;
}

table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: var(--space-4);
  font-size: var(--text-base);
}
th, td {
  text-align: left;
  padding: var(--space-3);
  border-bottom: 1px solid var(--color-border);
}
th { color: var(--color-muted); font-weight: 600; font-size: var(--text-sm); text-transform: uppercase; letter-spacing: 0.04em; }
tbody tr:hover { background: rgba(23, 50, 77, 0.03); }

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

.container--narrow { max-width: 720px; }
.container--wide { max-width: 1440px; }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  top: -100px;
  left: var(--space-4);
  z-index: 100;
  padding: var(--space-3) var(--space-4);
  background: var(--color-accent);
  color: var(--on-brand);
  border-radius: var(--radius);
  transition: top var(--transition);
}
.skip-link:focus { top: var(--space-4); color: var(--on-brand); }

.text-muted { color: var(--color-muted); }
.text-center { text-align: center; }
.text-success { color: var(--color-success); }
.text-warning { color: var(--color-warning); }
.text-danger { color: var(--color-danger); }
.text-sm { font-size: 0.875rem; }
.text-lg { font-size: 1.125rem; }
.mono { font-family: var(--font-mono); }
.nowrap { white-space: nowrap; }

.stack > * + * { margin-top: var(--space-4); }
.stack-sm > * + * { margin-top: var(--space-2); }
.stack-lg > * + * { margin-top: var(--space-6); }

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

.grid {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}

.grid--2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.grid--sidebar {
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 900px) {
  .grid--sidebar { grid-template-columns: 240px minmax(0, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

@media print {
  body { background: var(--on-brand); color: #000; }
  .site-header, .site-footer, .no-print { display: none !important; }
}
/* ==========================================================================
 * Scales
 *
 * The lists above are a palette. These are an agreement.
 *
 * A palette is a set of colours. A scale says that every surface at the same
 * height is the same colour, every border at the same role has the same weight,
 * and height is a number that can be passed around.
 *
 * Without one, "raise this card a little" becomes an invention: a fifth shadow, a
 * slightly different grey, a border that is heavier on one page than another.
 * That is how a design system quietly becomes seven.
 * ========================================================================== */

/* ---------------------------------------------------------------------------
 * The brand scale
 *
 * #87CEFA sits at 400, because it is the supplied brand colour and a scale that
 * relocates its own midpoint invents a brand the design did not ask for.
 *
 * The steps are tinted toward the brand rather than toward a generic blue, so a
 * --sky-100 surface is recognisably Panindigan.
 * ------------------------------------------------------------------------- */

:root {
  /* --- tints: surfaces and hover fills --- */
  --sky-50:  #F2F9FE;
  --sky-100: #E4F3FD;
  --sky-200: #C8E7FA;
  --sky-300: #A5D6F7;

  /* --- the brand --- */
  --sky-400: #87CEFA;

  /* --- the action ramp ---
   * 500 is --color-accent: 4.77:1 on white, and the darkest step that still
   * reads as the brand's hue rather than as a different colour. */
  --sky-500: #2277B8;

  /* Role names, resolving to a step.
   *
   * The sheets think in roles — the accent, the focus colour, the hover fill.
   * The scale is a ramp. A focus ring wants "the accent at full strength", and
   * reading that as var(--sky-500) says which step of which ramp, which is the
   * question the scale exists to answer once rather than on every sheet.
   *
   * Two names for one value is normally the thing to remove, and this session
   * has removed several. This one earns its place by pointing inward: roles in
   * the sheets, steps in the scale, one line where the two meet. */
  --accent-500: var(--sky-500);
  --sky-600: #1B6BA8;
  --sky-700: #155E97;
  --sky-800: #125285;

  /* --- shades: text on a light fill --- */
  --sky-900: #0F4873;

  /* --- the neutrals, derived from Deep Navy --- */
  --navy-50:  #F7FAFC;
  --navy-100: #EDF3F9;
  --navy-200: #DCE6F0;
  --navy-300: #B9CADA;
  --navy-400: #90A7BE;
  --navy-500: #6B8199;
  --navy-600: #4A6480;
  --navy-700: #2F4A66;
  --navy-800: #23384F;
  --navy-900: #17324D;   /* Deep Navy, the brand's own */

  /* -------------------------------------------------------------------------
   * Elevation
   *
   * Each tier is a **pair**: a surface colour *and* a shadow.
   *
   * The pairing is the point. A shadow alone does not describe height — it
   * describes the light source — and two surfaces at the same height carrying
   * different shadows read as different heights, which is the thing elevation
   * exists to prevent.
   *
   * Navy at low alpha rather than black. A shadow is a tint of the ambient light,
   * and this page's light is blue; a black shadow on a blue surface reads as dirt.
   * The ladder is small (0.04 to 0.16) because a light theme has little headroom
   * before a shadow reads as a smudge.
   * ---------------------------------------------------------------------- */

  --elev-0-surface: var(--sky-50);
  --elev-0-shadow: none;

  --elev-1-surface: var(--brand-surface);
  --elev-1-shadow: 0 1px 2px rgba(15, 72, 115, 0.05);

  --elev-2-surface: var(--brand-surface);
  --elev-2-shadow:
    0 1px 2px rgba(15, 72, 115, 0.05),
    0 4px 12px rgba(15, 72, 115, 0.07);

  --elev-3-surface: var(--brand-surface);
  --elev-3-shadow:
    0 2px 4px rgba(15, 72, 115, 0.06),
    0 12px 28px rgba(15, 72, 115, 0.11);

  --elev-4-surface: var(--navy-900);
  --elev-4-shadow:
    0 4px 8px rgba(15, 72, 115, 0.10),
    0 24px 60px rgba(15, 72, 115, 0.22);

  /* Named elevations, for the places that think in nouns. */
  --shadow-sm: var(--elev-1-shadow);
  --shadow-md: var(--elev-2-shadow);
  --shadow-lg: var(--elev-3-shadow);
  --shadow-xl: var(--elev-4-shadow);

  /* The one shadow that is coloured rather than neutral: a card that lifts in
   * the brand's own hue reads as picking up light from the accent rather than as
   * dropping a grey cloud. Reserved for hover, where it is one state and not the
   * resting appearance of anything. */
  --shadow-brand:
    0 2px 4px rgba(15, 72, 115, 0.05),
    0 10px 28px rgba(34, 119, 184, 0.20);

  /* -------------------------------------------------------------------------
   * Interaction
   *
   * Hover, active and focus were three near-identical literals repeated across
   * seven sheets. They are one token each now.
   * ---------------------------------------------------------------------- */

  --hover-fill: rgba(15, 72, 115, 0.035);
  --hover-fill-strong: rgba(15, 72, 115, 0.06);
  --active-fill: rgba(15, 72, 115, 0.085);

  /* Every focusable element draws this. Declared once so it cannot drift between
   * a button on one page and a link on another — which is invisible until
   * somebody tabs through both and finds the ring changes. */
  --focus-ring: 0 0 0 3px rgba(34, 119, 184, 0.38);
  --focus-ring-offset: 2px;

  /* The sticky header's height, measured at runtime by `docs.ts`.
   *
   * Two kinds of rule need it: anything that sticks below the header, and
   * anything that scrolls to. Twelve rules across four sheets read this token.
   *
   * It was never declared. All twelve were silently using their own fallback,
   * and the fallbacks disagreed — `64px` in most sheets, `72px` in one — so a
   * deep link scrolled 8px differently depending on which rule applied. A token
   * that does not exist reads as "the author declared it" and behaves as a
   * constant nobody chose.
   *
   * The declared value is the desktop header height, and `docs.ts` overwrites it
   * with the measured one as soon as it runs. */
  --header-height: 65px;
  --docs-header-height: var(--header-height);

  /* Border weights by role, rather than a per-sheet judgement call. */
  --border-weight-hairline: 1px;
  --border-weight-thick: 2px;
  --border-subtle: rgba(15, 72, 115, 0.10);
  --border-default: rgba(15, 72, 115, 0.14);
  --border-strong: rgba(15, 72, 115, 0.22);
  --border-accent: rgba(34, 119, 184, 0.45);

  /* -------------------------------------------------------------------------
   * Gradients
   *
   * Three, all from the supplied colours. A gradient that leaves the family
   * reads as a different product.
   * ---------------------------------------------------------------------- */

  --accent-gradient: linear-gradient(135deg, var(--sky-400) 0%, var(--sky-300) 52%, var(--sky-500) 100%);
  --accent-gradient-soft: linear-gradient(135deg, rgba(135, 206, 250, 0.20), rgba(34, 119, 184, 0.12));
  --brand-gradient: linear-gradient(165deg, var(--brand-surface) 0%, var(--sky-50) 58%, var(--navy-100) 100%);
  --brand-gradient-duo: linear-gradient(135deg, var(--sky-400) 0%, var(--sky-300) 52%, var(--sky-500) 100%);
  --navy-gradient: linear-gradient(165deg, var(--navy-900) 0%, var(--navy-800) 100%);

  /* -------------------------------------------------------------------------
   * The advanced layer
   *
   * The vocabulary the rules are written against. Everything here was already
   * declared somewhere in this stylesheet and used by nothing: an elevation
   * ladder that pairs a surface with a shadow, an interaction scale, a focus
   * ring, two gradient families. The design was not simple because there was
   * nothing to work with — it was simple because the rules were never
   * written against it.
   * ---------------------------------------------------------------------- */

  /* The display size.
   *
   * The headings were sized in rem, so a forty-eight character title and a
   * twelve character one rendered at the same size, and the page read as a
   * list rather than as a composition. */
  --text-display: clamp(2.375rem, 1.35rem + 3.6vw, 4rem);
  --text-display-sm: clamp(1.75rem, 1.3rem + 2vw, 2.5rem);

  /* Three curves.
   *
   * The ease-out curve is a deceleration. The spring is for the one case where an
   * element is arriving rather than changing state — a card lifting under the
   * pointer, a drawer opening. */
  --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);

  /* The base duration.
   *
   * Used without a fallback in about twenty rules, so the ones that declared
   * nothing had no transition at all: the element changed state with no way to
   * see it happen. */
  --transition-base: 200ms cubic-bezier(0.22, 1, 0.36, 1);

  /* The brand mesh: three radial washes in the brand's own hues, laid over the
   * surface.
   *
   * A hero with one linear wash reads as a tinted box. Three washes read as
   * light, because the eye reads the differences between them as depth. */
  --brand-mesh:
    radial-gradient(56rem 30rem at 8% -12%, color-mix(in srgb, var(--brand-sky) 38%, transparent), transparent 62%),
    radial-gradient(46rem 28rem at 92% 0%, color-mix(in srgb, var(--sky-500) 15%, transparent), transparent 60%),
    radial-gradient(64rem 36rem at 52% 116%, color-mix(in srgb, var(--sky-300) 26%, transparent), transparent 66%);

  /* The grain.
   *
   * One texture, reused. A large flat wash bands without it, and the banding
   * is visible on a wide monitor and invisible on a laptop — which is how a
   * gradient gets shipped that looks broken to half the readers. */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  --grain-opacity: 0.035;

  /* The elevation rings.
   *
   * A hairline that goes with the shadow, so a raised surface has an edge as
   * well as a cast. A shadow alone describes the light source; the ring is
   * what tells the reader where the surface ends. */
  --elev-1-ring: 0 0 0 1px rgba(15, 72, 115, 0.05);
  --elev-2-ring: 0 0 0 1px rgba(15, 72, 115, 0.07);
  --elev-3-ring: 0 0 0 1px rgba(15, 72, 115, 0.09);

  /* The accent text on an accent tint.
   *
   * It was used without a fallback, so the accent badge and the standard
   * severity label inherited their colour and the tint behind them was there
   * for nothing. */
  --accent-text: #14507F;

  /* The glass.
   *
   * The sticky header and the command palette are the two places a surface is
   * seen over other content. The glass used to be a strong blur over a
   * 72%-white fill — a translucent surface that read as a template effect and
   * let text fight with whatever scrolled beneath it. Now the fill is
   * near-opaque and the blur only settles the edges: the surface stands on its
   * own colour. */
  --glass-bg: rgba(255, 255, 255, 0.92);
  --glass-border: rgba(23, 50, 77, 0.12);
  --glass-blur: blur(6px);

  /* The primary action's fill is one flat colour.
   *
   * It was a three-stop gradient whose far end reached into violet (#6D28D9),
   * which is both off-brand and the single strongest "assembled from a
   * template" tell on the site. A flat fill needs no far end, so the fill is
   * the sky ramp's own dark stop: white on it is 5.65:1, and the hovered state
   * is simply the next stop down. */
  --btn-primary-fill: var(--sky-700);
  --btn-primary-fill-hover: var(--sky-800);
}

/* --- icons.css --- */
/* The icon set.
 *
 * ## Why they are drawn and not typed
 *
 * The feature cards carried emoji: a shield, a gear, a musical note, a money
 * bag. They are Unicode characters, and that has three consequences.
 *
 * They look different on every platform, and different again on every
 * operating system's emoji font — the same card is not the same card on a
 * phone and on a desktop.
 *
 * They are the wrong weight beside the type, and they cannot be coloured:
 * the chip is being shaped by the site's own stylesheet, and the glyph inside
 * it is not.
 *
 * And the site's claim is that it is hand-written semantic HTML and CSS, and
 * a pictograph out of a system font is neither.
 *
 * ## Why they are masks and not inline SVG
 *
 * A mask keeps the **shape** in one place — here — and leaves the **markup**
 * saying which category it is. So the page's static fallback and the module's
 * renderer agree by construction: both say `data-icon="moderation"`, and
 * neither carries a path.
 *
 * With the path in the markup, the two would have to be kept in step by hand,
 * and a category added to one and not the other would draw nothing at all.
 *
 * ## How a mask is drawn
 *
 * The mask is the shape; the element's background colour is the ink. So the
 * icon is coloured by the same property that colours any other text, and it
 * takes the chip's colour on hover with no second rule.
 *
 * ## The drawing
 *
 * One grid, twenty-four by twenty-four, a stroke of 1.7, round caps and round
 * joins, and no fills. The same stroke weight as the type's weight at the
 * same size, so an icon and a word sit together without one of them being
 * louder than the other.
 */

/* The chip that draws an icon.
 *
 * Any element carrying a `data-icon` draws the icon it names. The shape is
 * above; this is the ink and the size.
 *
 * It is here, beside the shapes, because a rule that draws an icon belongs
 * with the icons — and because the feature card and the empty state both want
 * it, and two copies of one rule is how they drift.
 */
[data-icon]::before {
  content: "";
  display: block;
  inline-size: 1.4rem;
  block-size: 1.4rem;
  background-color: var(--sky-700);
  -webkit-mask: var(--icon, none) center / contain no-repeat;
  mask: var(--icon, none) center / contain no-repeat;
  transition: background-color var(--transition-base);
}

/* The fallback.
 *
 * A ring, for a category nobody has drawn yet. An empty chip would read as a
 * broken one; a ring reads as "there is something here". */
.feature__icon,
.dashboard__nav-icon {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='7.5'/></svg>");
}

/* The gear, for administration.
 *
 * The cycle went to automation, which is what a cycle is: a workflow that
 * runs again. A cog is the shape a reader already has for "the settings and
 * the rules behind them".
 *
 * A ring, a hub and eight teeth — the three parts of a cog, drawn rather
 * than borrowed. */
[data-icon="administration"] {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='6.4'/><circle cx='12' cy='12' r='2.4'/><path d='M12 2.2v3.4M12 18.4v3.4M2.2 12h3.4M18.4 12h3.4M5 5l2.4 2.4M16.6 16.6L19 19M19 5l-2.4 2.4M7.4 16.6L5 19'/></svg>");
}

/* The bell, for the member notifications.
 *
 * A notification category is a bell, and there is no other shape that means
 * it. */
[data-icon="notifications"] {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M18 8.5a6 6 0 10-12 0c0 6.5-2.5 8.5-2.5 8.5h17S18 15 18 8.5z'/><path d='M10.3 20a2 2 0 003.4 0'/></svg>");
}

/* --- the categories --- */

[data-icon="moderation"] {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M12 3l7 2.8v5.4c0 4.3-2.9 8.1-7 9.8-4.1-1.7-7-5.5-7-9.8V5.8L12 3z'/></svg>");
}

[data-icon="automation"] {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M4.6 12a7.4 7.4 0 0112.7-5.2'/><path d='M17.3 3.4v3.4h-3.4'/><path d='M19.4 12a7.4 7.4 0 01-12.7 5.2'/><path d='M6.7 20.6v-3.4h3.4'/></svg>");
}

[data-icon="music"] {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M9 17.2V5.6l11-2v11.4'/><ellipse cx='6.6' cy='17.2' rx='2.4' ry='2'/><ellipse cx='17.6' cy='15' rx='2.4' ry='2'/></svg>");
}

[data-icon="tickets"] {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M20.5 13.4l-7.1 7.1a2 2 0 01-2.8 0l-7-7a2 2 0 01-.6-1.4V5a2 2 0 012-2h7.1a2 2 0 011.4.6l7 7a2 2 0 010 2.8z'/><circle cx='7.5' cy='7.5' r='1.2'/></svg>");
}

[data-icon="economy"] {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><ellipse cx='12' cy='6' rx='8' ry='3'/><path d='M4 6v6c0 1.7 3.6 3 8 3s8-1.3 8-3V6'/><path d='M4 12v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6'/></svg>");
}

[data-icon="ai"] {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M11 3l1.7 4.8 4.8 1.7-4.8 1.7L11 16l-1.7-4.8L4.5 9.5l4.8-1.7L11 3z'/><path d='M18 15l.9 2.4 2.4.9-2.4.9-.9 2.4-.9-2.4-2.4-.9 2.4-.9L18 15z'/></svg>");
}

[data-icon="games"] {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M7.5 9h9a4.5 4.5 0 014.3 5.9l-.7 2.8A2.6 2.6 0 0116.5 19l-1.6-2.4H9.1L7.5 19a2.6 2.6 0 01-4.6-1.3l-.7-2.8A4.5 4.5 0 017.5 9z'/><path d='M8 12v4M6 14h4'/><path d='M15.5 12.6h.01M17.6 14.6h.01'/></svg>");
}

[data-icon="social"] {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><circle cx='9.5' cy='8' r='3.3'/><path d='M3.8 20a5.7 5.7 0 0111.4 0'/><path d='M16 5.2a3.3 3.3 0 010 5.8'/><path d='M17.6 14.8A5.7 5.7 0 0120.2 20'/></svg>");
}

[data-icon="utility"] {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M15 3.5a5.5 5.5 0 00-4.6 8.3l-5.9 5.9a2.1 2.1 0 003 3l5.9-5.9a5.5 5.5 0 006.9-8.2l-3 3-2.7-.6-.6-2.7 3-3a5.5 5.5 0 00-.5.2z'/></svg>");
}

[data-icon="information"] {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><path d='M12 11.2v5'/><path d='M12 7.6h.01'/></svg>");
}

[data-icon="integrations"] {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M6 8h12v2.5a6 6 0 01-12 0V8z'/><path d='M12 16.5V21'/><path d='M9.5 3v5M14.5 3v5'/></svg>");
}

[data-icon="developer"] {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M8.5 8L4 12l4.5 4'/><path d='M15.5 8L20 12l-4.5 4'/><path d='M13.5 5l-3 14'/></svg>");
}

[data-icon="configuration"] {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3'/><path d='M1 14h6M9 8h6M17 16h6'/></svg>");
}

[data-icon="support"] {
  --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><circle cx='12' cy='12' r='3.6'/><path d='M5.6 5.6l4.3 4.3M14.1 14.1l4.3 4.3M18.4 5.6l-4.3 4.3M9.9 14.1l-4.3 4.3'/></svg>");
}

/* --- components.css --- */
/* Panindigan components.
   Hand-written CSS: header, footer, cards, buttons, badges,
   forms, tables, alerts, tabs, modals, toasts, skeletons. */

/* ---- layout chrome ---- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 40;
  /* Glass.
   *
   * The header is seen over every kind of content, so it has to work over a
   * white page and over the hero's mesh equally. Translucent white with a blur
   * does both: the blur averages whatever is under it, and the white on top
   * keeps the contrast of the links.
   *
   * The comma form of the colour is deliberate. `rgb(255 255 255 / 72%)` would
   * be the same, but the colour audit's patterns did not match it, and a rule
   * the audit cannot read is a rule the audit cannot protect. */
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid var(--border-subtle);
  box-shadow: var(--elev-1-shadow);
}

/* The shadow arrives with the scroll.
 *
 * A sticky header with a permanent shadow reads as a slab; one that gains the
 * shadow as the content slides under it reads as glass. The scroll timeline is
 * the way to do that without a scroll listener, and where it is not supported
 * the header keeps its resting shadow — which is the correct resting state
 * anyway. */
@supports (animation-timeline: scroll()) {
  .site-header {
    animation: header-lift linear both;
    animation-timeline: scroll(root);
    animation-range: 0 140px;
  }
  @keyframes header-lift {
    from { box-shadow: var(--elev-1-shadow); }
    to   { box-shadow: var(--elev-3-shadow); }
  }
}
.site-header__inner {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  height: 64px;
}
.site-header__brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: 700;
  font-size: var(--text-lg);
  color: var(--color-text);
}
.site-header__logo {
  width: 34px; height: 34px;
  border-radius: 11px;
  /* Deep Navy, with the sky around it.
   *
   * The mark carries a letter, and the letter has to be readable on it. The
   * brand sky with Deep Navy on it is 7.65:1 — good — but the earlier pair
   * of ramps had a violet far end, where Deep Navy is 3.0:1. So the mark is
   * the navy gradient with white on it, and the sky is the glow around it. */
  background: var(--navy-gradient);
  box-shadow:
    0 1px 2px rgba(15, 72, 115, 0.22),
    0 6px 18px -6px color-mix(in srgb, var(--color-accent) 62%, transparent),
    inset 0 1px 0 rgba(255, 255, 255, 0.16);
  display: grid; place-items: center;
  font-weight: 800; font-size: var(--text-base); color: #FFFFFF;
  letter-spacing: -0.02em;
}
.site-nav { display: flex; gap: var(--space-4); margin-left: auto; align-items: center; }
/* Navigation *items*, not everything that happens to be an <a> here.
 *
 * This used to be `.site-nav a`, which is specificity 0,1,1, and the sign-in
 * link is also a `.btn` — 0,1,0. The navigation rule won, so the button was
 * rendered with `padding: 0.5rem 0` (no horizontal padding at all) and a 2px
 * transparent bottom border laid over the button's own. The label looked clipped
 * because it was touching both edges.
 *
 * `:not(.btn)` states the actual rule: navigation items get the underline
 * treatment, and a button keeps being a button. The button still sits in the
 * nav and gets the nav's `gap`, so the spacing is unchanged.
 */
.site-nav > a:not(.btn) {
  position: relative;
  color: var(--navy-600);
  font-size: var(--text-sm);
  padding: var(--space-2) 0;
  transition: color var(--transition-fast);
}

/* The underline grows from the centre.
 *
 * A border that changes colour appears all at once, which reads as a state
 * change. A rule that grows out from the middle reads as the link itself
 * responding, and it costs one pseudo-element. */
/* The nav item's state is colour only. It used to grow a 2px gradient bar
 * under itself on hover and on the current page — a line that duplicated
 * what the colour already said, on every page, under every nav item. The
 * colour change stays; the bar is gone. */
.site-nav > a:not(.btn):hover,
.site-nav > a:not(.btn)[aria-current="page"] {
  color: var(--color-text);
}
.site-nav__toggle { display: none; }

@media (max-width: 780px) {
  .site-nav {
    position: absolute; top: 64px; left: 0; right: 0;
    flex-direction: column; align-items: stretch;
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border);
    padding: var(--space-4);
    display: none;
  }
  .site-nav[data-open="true"] { display: flex; }
  .site-nav__toggle {
    display: inline-flex; margin-left: auto;
    background: none; border: 1px solid var(--color-border);
    border-radius: 8px; padding: var(--space-2) var(--space-3);
    color: var(--color-text); cursor: pointer;
  }
}

.site-main { flex: 1 0 auto; padding: var(--space-7) 0; }

/* The footer belongs to content.css, which has the four-column version
 * with the glow behind the brand column. This copy was the older flex-row
 * one, and it lost to the later file every time. */

/* ---- cards ---- */
.card {
  /* The edge token.
   *
   * It drives the border's mix, so the gradient appears through one token
   * rather than by swapping the whole declaration in a hover rule. */
  --card-edge: 0;

  position: relative;
  background: var(--elev-2-surface);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  /* The border and the shadow are the grouped rule's, in ui.css — which is
   * where every raised surface gets its edge, so that a card, a panel and a
   * stat tile cannot drift apart. The border's *colour* is this card's own,
   * because the interactive variant animates it. */
  border-color: color-mix(
    in srgb,
    var(--color-accent) calc(var(--card-edge) * 46%),
    var(--border-subtle)
  );
  transition:
    border-color var(--transition-base),
    background-color var(--transition-base),
    transform var(--transition-base),
    box-shadow var(--transition-base);
}

/* A second surface, for a card sitting on a card.
 *
 * The selector is the compound one, so that it beats the grouped rule that
 * gives every raised surface its edge. A modifier of the same specificity as
 * the rule it modifies loses to a later file. */
.card.card--raised {
  background: var(--surface-2);
  box-shadow: var(--elev-3-shadow), var(--elev-3-ring);
}

.card--interactive {
  --card-edge: 0;

  cursor: pointer;
  will-change: transform;
}

/* The lift.
 *
 * A card that lifts in the accent hue reads as picking up light from the
 * accent rather than as dropping a grey cloud. It is reserved for hover — one
 * state, and not the resting appearance of anything. */
.card--interactive:hover,
.card--interactive:focus-visible,
.card--interactive:focus-within {
  --card-edge: 1;

  transform: translateY(-3px);
  box-shadow: var(--shadow-brand), var(--elev-3-ring);
}

/* A focus state that only fires when a *descendant* is focused is not a focus
 * state for the card. The old rule had exactly that, so a keyboard user got a
 * hover effect on a child and nothing on the card itself. */
.card--interactive:focus-visible {
  outline: 2px solid var(--accent-ring);
  outline-offset: 2px;
}

.card--interactive:active {
  transform: translateY(0);
  box-shadow: var(--elev-2-shadow), var(--elev-2-ring);
}
/* An accent edge, rather than a border that changes colour: the border carries
   the card's own boundary, and the edge is an added affordance. */
.card--interactive::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  border: 1px solid transparent;
  transition: border-color var(--transition);
  pointer-events: none;
}
.card--interactive:hover::after,
.card--interactive:focus-within::after { border-color: var(--accent-ring); }
.card--interactive:active { transform: translateY(-1px); }

.card__title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-base);
  line-height: var(--leading-snug);
  letter-spacing: -0.01em;
}
.card__body { color: var(--color-muted); margin: 0; }
.card__footer {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
  font-size: 0.875rem;
  color: var(--color-muted);
}

/* The panel belongs to content.css, which has the light theme's version.
 * This copy was the dark theme's — a translucent black fill and a grey
 * border — and it lost to the later file every time, so it was dead weight
 * that read as a second owner. */

/* ---- stat tiles ---- */
.stat {
  background: var(--elev-2-surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  box-shadow: var(--elev-2-shadow), var(--elev-2-ring);
}
.stat__label {
  font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--color-muted); margin-bottom: var(--space-2);
}
.stat__value { font-size: 1.75rem; font-weight: 700; line-height: 1.1; }
.stat__delta { font-size: var(--text-xs); margin-top: var(--space-1); }

/* ---- buttons ---- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  padding: var(--space-3) var(--space-5);
  font: inherit; font-size: var(--text-sm); font-weight: 600;
  letter-spacing: -0.005em;
  border-radius: 9px; border: 1px solid transparent;
  cursor: pointer; text-decoration: none; white-space: nowrap;
  /* Press feedback. A button that only changes colour on hover gives nothing
     back when you press it, so a press and a hover look identical and the
     click feels like it was lost. */
  transition:
    background-color var(--transition-fast),
    border-color var(--transition-fast),
    color var(--transition-fast),
    box-shadow var(--transition-fast),
    transform 80ms cubic-bezier(0.4, 0, 0.2, 1);
}
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn:disabled, .btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}
/* White on the accent, not Deep Navy.
 *
 * --on-brand is text on #87CEFA. This fills with #2277B8, where Deep Navy is
 * 2.75:1. */
.btn--primary {
  background: var(--btn-primary-fill);
  color: var(--on-accent);
  box-shadow: var(--shadow-brand);
}
.btn--primary:hover:not(:disabled) {
  background: var(--btn-primary-fill-hover);
  color: var(--on-accent-hover);
  transform: translateY(-2px);
  box-shadow: var(--elev-3-shadow);
}
.btn--primary:active:not(:disabled) {
  background: var(--btn-primary-fill-hover);
  transform: translateY(0);
  box-shadow: var(--elev-1-shadow);
}
.btn--secondary {
  background: var(--glass-bg);
  border-color: var(--border-default);
  color: var(--color-text);
  box-shadow: var(--elev-1-shadow);
}
.btn--secondary:hover:not(:disabled) {
  border-color: var(--accent-ring);
  color: var(--color-text);
  transform: translateY(-2px);
  box-shadow: var(--elev-2-shadow);
}
.btn--secondary:active:not(:disabled) {
  transform: translateY(0);
  box-shadow: var(--elev-1-shadow);
}
.btn--ghost { background: transparent; color: var(--color-muted); }
.btn--ghost:hover:not(:disabled) { color: var(--color-text); }
.btn--danger { background: var(--color-danger); color: var(--on-accent); }
.btn--danger:hover:not(:disabled) { background: var(--btn-danger-hover); color: var(--on-accent-hover); }
.btn--danger:active:not(:disabled) { background: var(--btn-danger-active); }
/* Was #04240f — near-black on a green fill, 3.03:1. Both ends were dark, so
   * the label was doing nothing on its own button. */
.btn--success { background: var(--color-success); color: var(--on-accent); }
.btn--success:hover:not(:disabled) { background: var(--btn-success-hover); color: var(--on-accent-hover); }
.btn--success:active:not(:disabled) { background: var(--btn-success-active); }
.btn--sm { padding: var(--space-2) var(--space-3); font-size: var(--text-sm); }
.btn--block { width: 100%; }

/* ---- badges ---- */
.badge {
  display: inline-flex; align-items: center; gap: var(--space-1);
  padding: 0.2rem 0.6rem;
  font-size: var(--text-xs); font-weight: 600; line-height: 1.5;
  /* Most of what makes a small pill read as a label rather than as a short
     word is the tracking and the case. */
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border-radius: 999px;
  border: 1px solid transparent;
  white-space: nowrap;
}
.badge--neutral { background: rgba(156, 163, 175, 0.15); color: var(--color-muted); border-color: rgba(156, 163, 175, 0.3); }
.badge--accent  { background: var(--accent-soft); color: var(--accent-text); border-color: rgba(34, 119, 184, 0.34); }
/* The success badge's text was #86efac — a light green on a light green
 * tint, which is 1.4:1. It is the dark theme's colour, carried over without
 * noticing: the badge is small, and a person skimming a page does not measure
 * the contrast of a green pill.
 *
 * The token for it has existed all along. */
.badge--success {
  background: var(--success-soft);
  color: var(--success-text);
  border-color: rgba(18, 121, 63, 0.35);
}
.badge--warning { background: rgba(245, 158, 11, 0.15); color: var(--warning-text); border-color: rgba(245, 158, 11, 0.35); }
.badge--danger  { background: rgba(239, 68, 68, 0.15); color: var(--danger-text); border-color: rgba(239, 68, 68, 0.35); }

/* ---- forms ---- */
.field { margin-bottom: var(--space-4); }
.field__label {
  display: block; margin-bottom: var(--space-2);
  font-size: 0.875rem; font-weight: 600; color: var(--color-text);
}
.field__hint { font-size: var(--text-xs); color: var(--color-muted); margin-top: var(--space-1); }
.field__error { font-size: var(--text-xs); color: var(--color-danger); margin-top: var(--space-1); }

.input, .select, .textarea {
  width: 100%;
  padding: var(--space-3) var(--space-4);
  font: inherit; font-size: var(--text-sm);
  color: var(--color-text);
  /* Slightly raised, not sunken. A recessed field on a dark surface reads as a
     hole, which is the wrong affordance for something you type into. */
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: 9px;
  transition:
    border-color var(--transition-fast),
    box-shadow var(--transition-fast),
    background-color var(--transition-fast);
}
.input::placeholder, .textarea::placeholder { color: rgba(156, 163, 175, 0.55); }
.input:hover, .select:hover, .textarea:hover { border-color: var(--border-strong); }

/* A ring, not only a border colour. Against a dark background a 1px colour
   change is close to invisible, and keyboard focus has to be visible. */
.input:focus, .select:focus, .textarea:focus {
  outline: none;
  border-color: var(--color-accent);
  background: var(--surface-3);
  box-shadow: 0 0 0 3px var(--accent-softer);
}
.input[aria-invalid="true"] { border-color: var(--color-danger); }
.textarea { min-height: 110px; resize: vertical; }
.select { appearance: none; background-image: linear-gradient(45deg, transparent 50%, var(--color-muted) 50%), linear-gradient(135deg, var(--color-muted) 50%, transparent 50%); background-position: calc(100% - 18px) center, calc(100% - 13px) center; background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; padding-right: var(--space-7); }

.checkbox, .radio { display: flex; align-items: flex-start; gap: var(--space-3); cursor: pointer; }
.checkbox input, .radio input { margin-top: 0.25rem; width: 16px; height: 16px; accent-color: var(--color-accent); cursor: pointer; }

.form-actions { display: flex; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-5); }

/* ---- alerts ---- */
.alert {
  display: flex; gap: var(--space-3); align-items: flex-start;
  padding: var(--space-4);
  border-radius: var(--radius);
  border: 1px solid;
  margin-bottom: var(--space-4);
  font-size: var(--text-sm);
}
.alert__body { flex: 1; }
.alert__title { font-weight: 650; margin-bottom: var(--space-1); }
/* The actions under an alert — the release notice's buttons. The class
   was emitted by the release module and had no rule, so the buttons sat
   against the text with no space between them. */
.alert__actions {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-top: var(--space-3);
}
.alert--info    { background: rgba(34, 119, 184, 0.1); border-color: rgba(34, 119, 184, 0.3); }
.alert--success { background: rgba(34, 197, 94, 0.1); border-color: rgba(34, 197, 94, 0.3); }
.alert--warning { background: rgba(245, 158, 11, 0.1); border-color: rgba(245, 158, 11, 0.3); }
.alert--danger  { background: rgba(239, 68, 68, 0.1); border-color: rgba(239, 68, 68, 0.3); }

/* ---- tabs ---- */
.tabs {
  display: flex; gap: var(--space-1);
  border-bottom: 1px solid var(--color-border);
  margin-bottom: var(--space-5);
  overflow-x: auto;
}
.tab {
  padding: var(--space-3) var(--space-4);
  font: inherit; font-size: var(--text-sm); font-weight: 600;
  color: var(--color-muted);
  background: none; border: none; border-bottom: 2px solid transparent;
  cursor: pointer; white-space: nowrap;
}
.tab:hover { color: var(--color-text); }
.tab[aria-selected="true"] { color: var(--color-text); border-bottom-color: var(--color-accent); }

/* ---- modal ---- */
.modal {
  border: none; padding: 0;
  background: var(--color-surface);
  color: var(--color-text);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  max-width: min(560px, calc(100vw - 32px));
  width: 100%;
}
.modal::backdrop { background: rgba(0, 0, 0, 0.65); backdrop-filter: blur(2px); }
.modal__header { padding: var(--space-5) var(--space-5) 0; }
.modal__title { margin: 0; font-size: var(--text-lg); }
.modal__body { padding: var(--space-4) var(--space-5); color: var(--color-muted); }
.modal__footer {
  padding: 0 var(--space-5) var(--space-5);
  display: flex; gap: var(--space-3); justify-content: flex-end;
}

/* ---- toast ---- */
.toast-region {
  position: fixed;
  /* Above the cookie notice.
   *
   * The notice is a full-width bar at the bottom and its actions are on the
   * right — where the toasts are. A toast that lands on "Not now" makes the
   * notice unusable, and the notice is the one thing on the page that has to
   * be pressable.
   *
   * The notice publishes its height; the region is above it by that much. The
   * property is absent when there is no notice, so the fallback is the plain
   * offset. */
  bottom: calc(var(--space-5) + var(--cookie-notice-height, 0px));
  right: var(--space-5);
  z-index: 90; display: flex; flex-direction: column; gap: var(--space-2);
  max-width: min(380px, calc(100vw - 32px));
}
.toast {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-accent);
  border-radius: 8px;
  padding: var(--space-3) var(--space-4);
  box-shadow: var(--shadow-md);
  font-size: var(--text-sm);
  animation: toast-in 180ms ease-out;
}
.toast--success { border-left-color: var(--color-success); }
.toast--danger { border-left-color: var(--color-danger); }
.toast--warning { border-left-color: var(--color-warning); }
@keyframes toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ---- skeleton ---- */
.skeleton {
  background: linear-gradient(90deg, rgba(23, 50, 77, 0.03) 25%, rgba(23, 50, 77, 0.08) 50%, rgba(23, 50, 77, 0.03) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s infinite;
  border-radius: 6px;
  min-height: 1rem;
}
@keyframes shimmer {
  from { background-position: 200% 0; }
  to { background-position: -200% 0; }
}

/* ---- hero ---- */
.hero {
  position: relative;
  padding: var(--space-7) 0 var(--space-6);
  text-align: center;
  isolation: isolate;
}

/* The mesh.
 *
 * Three radial washes in the brand's own hues, laid over the surface.
 *
 * One linear wash reads as a tinted box. Three washes read as light, because
 * the eye reads the differences between them as depth — and the depth is the
 * whole difference between a page that looks designed and a page that looks
 * filled in.
 *
 * The wash is behind the copy and never under it: the heading stays Deep Navy
 * on near-white, and keeps its measured ratio. */
.hero::before {
  content: "";
  position: absolute;
  z-index: -2;
  inset: -12rem 0 0;
  background: var(--brand-mesh);
  pointer-events: none;
}

/* The grain, on top of the mesh.
 *
 * A large flat wash bands without it, and the banding is visible on a wide
 * monitor and invisible on a laptop — which is how a gradient gets shipped
 * that looks broken to half the readers. At three and a half percent the
 * texture is felt rather than seen. */
.hero::after {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -12rem 0 0;
  background-image: var(--grain);
  opacity: var(--grain-opacity);
  pointer-events: none;
}

.hero__title {
  margin-bottom: var(--space-4);
  font-size: var(--text-display);
  font-weight: 800;
  line-height: 1.04;
  letter-spacing: -0.035em;
  text-wrap: balance;
}
.hero__subtitle {
  font-size: clamp(1.0625rem, 0.95rem + 0.6vw, 1.3125rem);
  line-height: var(--leading-relaxed);
  color: var(--color-muted);
  max-width: 58ch;
  margin: 0 auto var(--space-6);
  text-wrap: pretty;
}
.hero__actions { display: flex; gap: var(--space-3); justify-content: center; flex-wrap: wrap; }

/* The numbers, as tiles.
 *
 * Four figures floating in a row read as a caption under the heading. Four
 * figures on tiles read as the product's size, which is what they are for. */
.hero__stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: var(--space-4);
  justify-content: center;
  margin-top: var(--space-7);
}
.hero__stats > div {
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
  box-shadow: var(--elev-2-shadow), var(--elev-2-ring);
  text-align: center;
}

/* The numeral.
 *
 * One flat colour. It used to be a sky-into-violet gradient clipped to the
 * text under a support guard; the violet made the number read as belonging
 * to a different product, and the plain colour already carried the
 * hierarchy on its own. */
.hero__stat-value {
  font-size: var(--text-display-sm);
  font-weight: 780;
  line-height: 1.05;
  letter-spacing: -0.03em;
  color: var(--sky-800);
}
.hero__stat-label {
  font-size: var(--text-xs);
  color: var(--color-muted);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  margin-block-start: var(--space-1);
}

/* ---- pricing ---- */
.pricing { display: grid; gap: var(--space-5); grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.plan {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-5);
  display: flex; flex-direction: column;
}
/* The featured plan carries the brand gradient.
 *
 * As a 2px edge, not a background. A gradient behind the price would be the
 * obvious thing and the wrong one: this card is the only element on /pricing
 * asking for a decision, and the number on it is why it exists. A violet-
 * tinged glow behind the white type used to carry the emphasis, which is
 * where the eye noticed a colour the brand does not own.
 *
 * So the gradient goes under a white fill and only the ring survives, and
 * the card is lifted with the brand shadow — the same navy-tinted light
 * every raised surface is lit by. The price itself is one flat colour. */
.plan--featured {
  position: relative;
  border-color: transparent;
  background: var(--brand-gradient-duo);
  background-origin: border-box;
  box-shadow:
    0 0 0 2px transparent,
    0 12px 34px -14px rgba(23, 50, 77, 0.4);
}

/* The white fill that leaves only the ring.
 *
 * An absolutely-positioned pseudo-element paints above in-flow content by
 * default, so every child needs a z-index of its own or the copy disappears
 * underneath it. That is the failure mode of this technique and it is invisible
 * until the first child is added. */
.plan--featured::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--surface-1);
}

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

/* One flat price on every engine.
 *
 * A brand-gradient clipped to the text used to sit here, guarded by an
 * @supports fallback for engines without background-clip. The guard existed
 * because a transparent price on an unsupported engine is worse than a plain
 * one — which is the answer to the wrong question. The price is a price:
 * one colour, no clip, no guard. */
.plan--featured .plan__price {
  color: var(--sky-800);
}
.plan__name { font-size: var(--text-base); font-weight: 700; }
.plan__price { font-size: var(--text-2xl); font-weight: 750; margin: var(--space-2) 0; }
.plan__price-note { font-size: var(--text-xs); color: var(--color-muted); }
.plan__features { list-style: none; padding: 0; margin: var(--space-4) 0; flex: 1; }
.plan__features li {
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--overlay-hover);
  font-size: var(--text-sm); color: var(--color-muted);
}
.plan__features li::before { content: "✓ "; color: var(--color-success); font-weight: 700; }

/* ---- feature grid ---- */
.feature__icon {
  width: 40px; height: 40px; border-radius: 10px;
  background: var(--accent-soft);
  display: grid; place-items: center;
  font-size: var(--text-lg); margin-bottom: var(--space-3);
}

/* ---- command table ---- */
.command-table { font-family: var(--font-mono); font-size: 0.875rem; }
.command-table td:first-child { color: var(--color-text); white-space: nowrap; }
.command-table__name { color: var(--color-accent); }

/* The empty state.
 *
 * Three things: an icon, a title and a sentence.
 *
 * The icon is the part that matters. It is what tells the reader that the
 * page has nothing to show rather than that something broke, and it is the
 * difference between a page that looks finished and one that looks broken.
 *
 * The icon is drawn by the icon sheet's rule, so the shapes and the chips that
 * carry them live in one place. */
.empty-state {
  display: grid;
  justify-items: center;
  gap: var(--space-3);
  padding: var(--space-7) var(--space-4);
  text-align: center;
  color: var(--color-muted);
}
.empty-state__icon {
  display: grid;
  place-items: center;
  inline-size: 3rem;
  block-size: 3rem;
  border-radius: var(--radius-lg, 14px);
  background: var(--accent-softer);
  box-shadow: var(--elev-1-shadow), var(--elev-1-ring);
}
.empty-state__icon::before {
  inline-size: 1.5rem;
  block-size: 1.5rem;
  background-color: var(--sky-600);
}
.empty-state__title {
  margin: 0;
  font-size: var(--text-base);
  font-weight: 650;
  color: var(--color-text);
}
.empty-state__detail {
  margin: 0;
  max-width: 46ch;
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
}
/* --- Shared primitives the dashboard renderers construct by name ---
   These are used by the generic page renderers and by the notification
   workspace. They are declared here rather than inline at each call site so
   the two cannot drift into looking like different products. */

/* A page heading and its supporting sentence. */
.page-title { margin: 0 0 var(--space-2); font-size: var(--text-xl); font-weight: 700; }
.lede { margin: 0 0 var(--space-5); color: var(--color-muted); max-width: 70ch; }

/* Secondary text. `muted` is for content, never for a value the reader
   needs; a muted value is a value that was not returned. */
.muted { color: var(--color-muted); }

/* Panels. */
.panel__title { margin: var(--space-1) 0 0; color: var(--color-muted); font-size: var(--text-sm); font-weight: 400; }

/* A panel inside a panel.
   A repeated row -- a case, a blocked term, a report, a ticket -- is drawn
   as a nested panel so the reader can tell it belongs to the list above
   it rather than being a section of its own. The class was emitted by ten
   category workspaces and had no rule at all, so every one of those rows
   was drawn as a full-width sibling panel. */
.panel--nested {
  margin-top: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-left: 2px solid var(--color-accent);
  background: var(--color-surface-sunken, rgba(148, 163, 184, 0.06));
  border-radius: var(--radius);
}
.panel--nested .panel__title { margin-top: 0; }

/* A sub-heading inside a panel. */
.panel__subtitle {
  margin: var(--space-4) 0 var(--space-2);
  font-size: var(--text-sm);
  font-weight: 650;
}

/* The terms of a definition list.
   The list's own rules style `dt` and `dd` by element, and these name
   the same two things by class. The classes were emitted by the music
   workspace and had no rule, so the intent -- a term in the muted
   colour, a value in the text colour -- was lost there while every
   other panel kept it. */
.def-list__term { color: var(--color-muted); }
.def-list__detail { margin: 0; word-break: break-word; }

/* A block of exported configuration. */
.code-block {
  margin: 0;
  padding: var(--space-3);
  overflow-x: auto;
  border-radius: var(--radius);
  background: var(--color-surface-sunken, rgba(148, 163, 184, 0.08));
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
}

/* Stat cards, the summary grid used by stats pages and status panels. */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}
.stat-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-4);
}
.stat-card__label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-muted);
  margin-bottom: var(--space-2);
}
.stat-card__value { font-size: var(--text-lg); font-weight: 650; word-break: break-word; }

/* A definition list for key/value configuration panels. */
.def-list {
  display: grid;
  grid-template-columns: minmax(140px, auto) 1fr;
  gap: var(--space-2) var(--space-4);
  margin: 0;
  font-size: var(--text-sm);
}
.def-list dt { color: var(--color-muted); }
.def-list dd { margin: 0; word-break: break-word; }

/* Tables. Wrapped so a wide delivery history scrolls instead of
   overflowing the content column on a narrow screen. */
.table-wrap { overflow-x: auto; margin-bottom: var(--space-5); }
.table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.table th,
.table td {
  text-align: left;
  padding: var(--space-3);
  border-bottom: 1px solid var(--color-border);
  vertical-align: top;
}
.table th { color: var(--color-muted); font-weight: 600; white-space: nowrap; }
.table tbody tr:last-child td { border-bottom: none; }

/* A status pill. The tone modifier carries colour and the label carries
   meaning, so the state is never conveyed by colour alone (Section 17). */
.pill {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-2);
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  border: 1px solid var(--color-border);
  white-space: nowrap;
}
.pill--success { color: var(--color-success); border-color: var(--color-success); }
.pill--warning { color: var(--color-warning); border-color: var(--color-warning); }
.pill--danger  { color: var(--color-danger);  border-color: var(--color-danger); }
.pill--muted   { color: var(--color-muted); }

/* A variable chip: inserts a placeholder into the composer. */
.chip {
  background: transparent;
  border: 1px dashed var(--color-border);
  border-radius: var(--radius);
  color: var(--color-text);
  cursor: pointer;
  font-size: var(--text-xs);
  padding: var(--space-1) var(--space-2);
}
.chip:hover { border-color: var(--color-accent); border-style: solid; }
.chip:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* --- dashboard.css --- */
/* Panindigan dashboard styles. Hand-written CSS. */

/* ---------------------------------------------------------------------------
   The layout
   ---------------------------------------------------------------------------
   One column by default: the sidebar is a bottom sheet below 861px and does not
   occupy the layout at all. Two columns from 900px, where it becomes a rail.

   The two ranges do not overlap, which is the point. A sidebar that is a grid
   column *and* a fixed overlay is a panel that is half in the flow and half out
   of it, and the half that is out of it is the half that covers the content.
   --------------------------------------------------------------------------- */

.dashboard {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-5);
}

@media (min-width: 900px) {
  .dashboard {
    grid-template-columns: 250px minmax(0, 1fr);
    align-items: start;
  }
}

/* --- the rail ---
 *
 * `auto` rather than a fixed width, and the transition on the column so the
 * content slides with the sidebar instead of jumping after it.
 */
@media (min-width: 900px) {
  :root[data-sidebar-collapsed] .dashboard {
    grid-template-columns: 64px minmax(0, 1fr);
  }
}

/* The sidebar is glass.
 *
 * It sits over the page's own content and the reader scrolls the content
 * behind it, so the same treatment the sticky header gets is the right one
 * here: translucent, blurred, with an edge that reads as the edge of the
 * glass rather than as a border drawn around a box. */
.dashboard__sidebar {
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--elev-2-shadow), var(--elev-2-ring);
  padding: var(--space-4);
  position: sticky;
  top: calc(var(--header-height) + var(--space-4));
  max-height: calc(100vh - var(--header-height) - var(--space-6));
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* --- the phone drawer ---
 *
 * Below 861px the sidebar leaves the layout entirely and becomes a bottom sheet.
 * A phone is held with a thumb, and the bottom of the screen is where the thumb
 * already is; a panel that opens off the side asks for a reach the reader does
 * not have, and it fights the browser's own back gesture on that edge.
 *
 * `hidden` is what `initDrawer` sets. `display: none` is declared here rather
 * than relied on from the attribute alone, because `[hidden]` is a
 * presentation hint and a `display` rule in this sheet would otherwise win over
 * it — which is how a drawer that is "closed" ends up open and covering the
 * page.
 */

@media (max-width: 860px) {
  .dashboard__sidebar[data-drawer] {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 60;
    max-height: 82vh;
    border-radius: var(--radius-lg, 14px) var(--radius-lg, 14px) 0 0;
    border-block-end: 0;
    transform: translateY(100%);
    transition: transform var(--transition);
    box-shadow: var(--elev-4-shadow, 0 12px 32px rgba(15, 72, 115, 0.18));
  }

  .dashboard__sidebar[data-drawer][data-open="true"] {
    transform: translateY(0);
  }

  .dashboard__sidebar[data-drawer][hidden] {
    display: none;
  }

  .dashboard__sidebar-close {
    position: absolute;
    inset-block-start: var(--space-3);
    inset-inline-end: var(--space-3);
    display: grid;
    place-items: center;
    inline-size: 2rem;
    block-size: 2rem;
    border: 0;
    border-radius: var(--radius-pill, 999px);
    background: var(--overlay-hover);
    font-size: 1.25rem;
    line-height: 1;
    color: var(--color-muted);
    cursor: pointer;
  }
}

/* The close button has no meaning above 861px, where the sidebar is a rail. */
@media (min-width: 861px) {
  .dashboard__sidebar-close {
    display: none;
  }
}

/* --- the two header controls ---
 *
 * The drawer toggle is for the bottom sheet and the collapse toggle is for the
 * rail, so exactly one is ever shown. Both live in the header rather than in the
 * sidebar because the sidebar is the thing they control, and a control inside
 * the thing it collapses cannot be reached once it has collapsed.
 */

.dashboard__drawer-toggle,
.dashboard__collapse-toggle {
  display: grid;
  place-items: center;
  inline-size: 2.25rem;
  block-size: 2.25rem;
  flex: 0 0 auto;
  border: var(--border-weight-hairline) solid var(--border-subtle);
  border-radius: var(--radius-sm, 8px);
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--elev-1-shadow);
  color: var(--color-muted);
  cursor: pointer;
  transition:
    background var(--transition-base),
    color var(--transition-base),
    box-shadow var(--transition-base);
}

.dashboard__drawer-toggle:hover,
.dashboard__collapse-toggle:hover {
  background: var(--overlay-hover);
  color: var(--color-text);
}

.dashboard__drawer-toggle:focus-visible,
.dashboard__collapse-toggle:focus-visible {
  outline: var(--border-weight-thick) solid var(--accent-500);
  outline-offset: var(--focus-ring-offset);
}

@media (min-width: 861px) {
  .dashboard__drawer-toggle {
    display: none;
  }
}

@media (max-width: 860px) {
  .dashboard__collapse-toggle {
    display: none;
  }
}

/* Three bars, drawn rather than typed: a glyph is a character the reader's
   screen reader will read aloud, and this one has nothing to say. */
.dashboard__drawer-toggle-bars,
.dashboard__drawer-toggle-bars::before,
.dashboard__drawer-toggle-bars::after {
  display: block;
  inline-size: 1rem;
  block-size: 2px;
  border-radius: 1px;
  background: currentColor;
}

.dashboard__drawer-toggle-bars {
  position: relative;
}

.dashboard__drawer-toggle-bars::before,
.dashboard__drawer-toggle-bars::after {
  content: "";
  position: absolute;
  inset-inline-start: 0;
}

.dashboard__drawer-toggle-bars::before { inset-block-start: -5px; }
.dashboard__drawer-toggle-bars::after { inset-block-start: 5px; }

.dashboard__sidebar-title {
  font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--color-muted); margin: 0 0 var(--space-3);
}

/* --- the groups ---
 *
 * Thirty-six routes in seven groups is a list nobody scans, so the groups are
 * headings with the groups themselves given the visual weight — not the
 * entries inside them. An entry is the same size whether it is the page you are
 * on or the one you are not.
 *
 * The region is the host `renderDashboardNav` replaces. Its only job here is the
 * gap between the server switcher and the first group: the switcher's `<h2>`
 * carries a bottom margin, and an element inserted after it with no margin of
 * its own would sit flush against the server list.
 */

.dashboard__nav-region {
  margin-block-start: var(--space-4);
}

.dashboard__nav-group + .dashboard__nav-group {
  margin-block-start: var(--space-5);
}

.dashboard__nav-group-title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-faint);
}

.dashboard__nav { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.dashboard__nav li { margin: 0; }

/* One class for the `<a>` and for the no-server `<span>`, so the two look
   identical and differ only in behaviour. A visibly different style would imply
   the feature does not exist; what is actually true is that it needs a server. */
.dashboard__nav-link {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3);
  border-radius: 8px;
  color: var(--color-muted);
  font-size: var(--text-sm);
  text-decoration: none;
  transition: background-color var(--transition), color var(--transition);
}
.dashboard__nav a.dashboard__nav-link:hover { background: var(--overlay-hover); color: var(--color-text); }

/* No server selected. Dimmed rather than hidden: the feature exists, the
   context does not, and saying so is more use than omitting it. */
.dashboard__nav-link[aria-disabled="true"] {
  cursor: default;
  opacity: 0.55;
}

/* The page itself.
 *
 * Filled, not tinted. A pale accent tint reads as "highlighted" rather than
 * as "chosen", and the difference matters here: this is the one place a
 * reader answers "where am I", thirty times a session. */
.dashboard__nav a[aria-current="page"] {
  background: var(--btn-primary-fill);
  color: #FFFFFF;
  font-weight: 600;
  box-shadow: var(--shadow-brand);
}

/* Somewhere inside this section.
 *
 * `aria-current="true"` is what the builder sets for an ancestor, and it is not
 * the same claim as `page`: it says "you are within this", not "you are here".
 * It is drawn as a left rail rather than a fill, so a reader three levels deep
 * in Commands can still see which section they are in *and* that they are not on
 * its landing page.
 */
.dashboard__nav a[aria-current="true"] {
  color: var(--color-text);
  font-weight: 600;
  box-shadow: inset 3px 0 0 var(--sky-500);
}

.dashboard__nav-icon {
  display: grid;
  place-items: center;
  inline-size: 1.5rem;
  block-size: 1.5rem;
  flex: 0 0 auto;
  border-radius: var(--radius-sm, 6px);
  background: var(--overlay-hover);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--text-xs);
  font-weight: 700;
  text-align: center;
}

.dashboard__nav a[aria-current] .dashboard__nav-icon {
  background: var(--accent-gradient);
  color: var(--navy-900);
}

/* --- the rail ---
 *
 * Collapsed, the sidebar shows the glyphs. Every link keeps its accessible
 * name — the label is hidden visually, not removed — so the rail is still
 * navigable by screen reader and by Tab, and the `title` gives a pointer user
 * the name on hover.
 */

@media (min-width: 900px) {
  :root[data-sidebar-collapsed] .dashboard__sidebar {
    padding-inline: var(--space-2);
  }

  :root[data-sidebar-collapsed] .dashboard__sidebar-title,
  :root[data-sidebar-collapsed] .dashboard__nav-group-title,
  :root[data-sidebar-collapsed] .guild-switcher__name,
  :root[data-sidebar-collapsed] .guild-switcher__meta,
  :root[data-sidebar-collapsed] .dashboard__nav-link-text,
  :root[data-sidebar-collapsed] #guild-select,
  :root[data-sidebar-collapsed] .field__label {
    display: none;
  }

  :root[data-sidebar-collapsed] .dashboard__nav-link {
    justify-content: center;
    padding-inline: var(--space-2);
  }
}

/* --- the breadcrumb host ---
 *
 * Empty in the markup and filled by `renderBreadcrumbs`. It reserves no height
 * of its own when empty, so a page with no trail does not show a gap where a
 * trail might be.
 */

.dashboard__breadcrumbs:not(:empty) {
  margin-block-end: var(--space-3);
}

/* ---------------------------------------------------------------------------
   Documentation navigation
   ---------------------------------------------------------------------------
   A disclosure, not a link with a decoration on it.

   The previous markup put a triangle glyph in front of a link. The glyph looked
   like a disclosure control, pointed at nothing, and could not rotate -- an
   aria-hidden glyph reflects no state. This is a real <details>, so the caret is
   drawn from the element's own open state and the control keeps working with
   scripting disabled.
--------------------------------------------------------------------------- */

/* The documentation sidebar.
 *
 * It is the same kind of thing as the dashboard's: a panel of navigation that
 * sits beside the reading. So it is glass, like that one, and it has the same
 * edge and the same cast. */
.docs__aside {
  align-self: start;
  padding: var(--space-4);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg, 14px);
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--elev-1-shadow), var(--elev-1-ring);
}

/* The sidebar sticks only once it has somewhere to stick *beside*.
   ---------------------------------------------------------------------------
   The 900px here is the same breakpoint that gives `.grid--sidebar` its second
   column. Below it the grid is one column, so the aside is a full-width block
   directly above the article -- and a sticky element in a one-column layout
   pins itself over the thing it was meant to sit next to.

   That is what happened: at 800px the whole nav floated down over the first
   screen of documentation and stayed there for the entire scroll, covering the
   article with a panel the reader could not tell apart from it.

   So the sticky, the cap and the scroll are all inside the query. Below the
   breakpoint the nav is an ordinary block at the top of the page, which is what
   a sidebar is on a screen too narrow to have one.
   --------------------------------------------------------------------------- */

@media (min-width: 900px) {
  .docs__aside {
    position: sticky;
    top: calc(var(--header-height) + var(--space-4));
    max-height: calc(100vh - var(--header-height) - var(--space-6));
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

.docs__nav-item {
  border-radius: 10px;
  transition: background-color var(--transition);
}
/* ---------------------------------------------------------------------------
 * The documentation measure.
 *
 * `base.css` already declares `.prose { max-width: 68ch }`, and it is right.
 * The docs page does not use `.prose` — it has its own layout with a sidebar,
 * and its content column had no width cap at all.
 *
 * Measured: **37 of 37** reading paragraphs on `/docs` sat at 93 characters.
 * `/` measured 65 and `/pricing` 78, both fine.
 *
 * At 93 the eye cannot return to the left margin in one saccade, so the reader
 * tracks back or loses their place — and on documentation, losing your place
 * mid-instruction is what costs a user their next step.
 *
 * The cap is on the content column, not the container: the container also holds
 * the sidebar, and capping it would squeeze the navigation too.
 *
 * 72ch rather than 68, because the column sits beside a sidebar and so reads as
 * a longer line than a full-width prose block of the same width. Slightly under
 * the optimum beats sitting on it.
 * ------------------------------------------------------------------------- */

.docs__section {
  max-inline-size: 72ch;
}

/* Headings and code blocks are allowed to run wider than the text they sit in --
   a long URL or a shell command cannot be wrapped to 72 characters without
   becoming unreadable, and forcing them to break mid-token is worse than a
   slightly wider block. */
/* A long URL or a shell command cannot be wrapped to 72 characters without
   becoming unreadable, and breaking one mid-token is worse than a slightly wider
   block. So code runs to the edge of the column and no further. */
.docs__section pre,
.docs__section code {
  max-inline-size: none;
}

.docs__nav-item:hover { background: rgba(23, 50, 77, 0.045); }
.docs__nav-item[open] { background: rgba(23, 50, 77, 0.03); }

.docs__nav-link {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  color: var(--color-muted);
  font-size: var(--text-sm);
  cursor: pointer;
  user-select: none;
  list-style: none;
  border-radius: 10px;
  transition: color var(--transition), background-color var(--transition);
}
/* Safari and Firefox both ship a marker on <summary> that this hides. */
.docs__nav-link::-webkit-details-marker { display: none; }
.docs__nav-link::marker { content: ""; }

.docs__nav-item:hover > .docs__nav-link { color: var(--color-text); }
.docs__nav-item[open] > .docs__nav-link { color: var(--color-text); font-weight: 600; }

.docs__nav-link:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: -2px;
}

/* The caret is a CSS triangle rather than a character, so it can rotate and
   take its colour from the control it belongs to. A text glyph cannot. */
.docs__nav-caret {
  width: 0;
  height: 0;
  flex-shrink: 0;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-left: 6px solid currentColor;
  opacity: 0.55;
  transform-origin: 35% 50%;
  transition: transform var(--transition), opacity var(--transition);
}
.docs__nav-item[open] > .docs__nav-link > .docs__nav-caret {
  transform: rotate(90deg);
  opacity: 0.9;
}
/* A section with no sub-headings is a link, so its caret must not invite a
   click that does nothing. */
.docs__nav-caret--static { transform: rotate(90deg); opacity: 0.35; }

.docs__nav-label { flex: 1 1 auto; min-width: 0; }

.docs__nav-sub {
  list-style: none;
  margin: 0 0 var(--space-2) 0;
  padding: 0 0 0 calc(var(--space-5) + 6px);
  border-left: 1px solid var(--color-border);
}
.docs__nav-sub li { margin: 0; }
.docs__nav-sub a {
  display: block;
  padding: var(--space-2) var(--space-3);
  color: var(--color-muted);
  font-size: var(--text-sm);
  line-height: 1.45;
  border-radius: 6px;
  transition: color var(--transition), background-color var(--transition);
}
.docs__nav-sub a:hover { color: var(--color-text); background: var(--overlay-hover); }
.docs__nav-sub a:focus-visible { outline: 2px solid var(--color-primary); outline-offset: -2px; }

/* The section the page is currently on. */
.docs__nav-sub a[aria-current="location"] {
  color: var(--color-text);
  background: rgba(34, 119, 184, 0.14);
  font-weight: 600;
}
.docs__nav-item > .docs__nav-link[aria-current="page"] {
  color: var(--color-text);
  background: var(--accent-soft);
  font-weight: 600;
}

/* Every anchor target clears the sticky header.
   A sub-link points at an `<h3>` inside a section, and a heading has no
   scroll margin of its own -- so following one put its first line under the bar
   and, on a short viewport, below the fold entirely. */
.docs__section,
.docs__section h2,
.docs__section h3,
.docs__section h4 {
  scroll-margin-top: calc(var(--header-height) + var(--space-4));
}
.docs__section:target > h2 { color: var(--color-text); }

@media (prefers-reduced-motion: reduce) {
  .docs__nav-caret { transition: none; }
}

.dashboard__content { min-width: 0; }

.dashboard__header {
  display: flex; flex-wrap: wrap; gap: var(--space-3);
  align-items: center; justify-content: space-between;
  margin-bottom: var(--space-5);
}
/* The title is the page's subject, so it is a display size rather than
 * the size of the body copy. It is a step below the marketing pages' — a
 * reader is working here, not looking at it. */
.dashboard__title {
  margin: 0;
  font-size: var(--text-2xl);
  font-weight: 780;
  letter-spacing: -0.03em;
  line-height: 1.1;
  text-wrap: balance;
}
.dashboard__subtitle {
  color: var(--color-muted);
  font-size: var(--text-sm);
  margin: 0;
  margin-block-start: var(--space-1);
}

/* The server switcher is the first control on every dashboard page and
 * the one a reader reaches for most, so it is a raised card and not a
 * flat box. */
.guild-switcher {
  display: flex; align-items: center; gap: var(--space-3);
  background: var(--elev-2-surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--elev-2-shadow), var(--elev-2-ring);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-5);
}
.guild-switcher__avatar {
  width: 36px; height: 36px; border-radius: 10px;
  background: var(--surface-3);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
  display: grid; place-items: center;
  font-weight: 700; font-size: var(--text-sm);
  overflow: hidden;
  flex-shrink: 0;
}
.guild-switcher__avatar img { width: 100%; height: 100%; object-fit: cover; }
.guild-switcher__name { font-weight: 650; font-size: var(--text-base); }
.guild-switcher__meta { font-size: var(--text-xs); color: var(--color-muted); }

/* stat row */
.stat-row {
  display: grid; gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  margin-bottom: var(--space-5);
}

/* data list */
.data-list { list-style: none; margin: 0; padding: 0; }
.data-list__item {
  display: flex; gap: var(--space-4); align-items: center;
  padding: var(--space-3) var(--space-3);
  margin-inline: calc(var(--space-3) * -1);
  border-radius: var(--radius);
  border-bottom: 1px solid var(--border-subtle);
  transition: background var(--transition-base);
}
.data-list__item:hover {
  background: var(--hover-fill);
}
.data-list__item:last-child { border-bottom: none; }
.data-list__main { flex: 1; min-width: 0; }
.data-list__title { font-weight: 600; font-size: var(--text-sm); }
.data-list__meta { font-size: var(--text-xs); color: var(--color-muted); }
.data-list__actions { display: flex; gap: var(--space-2); flex-shrink: 0; }

/* key/value rows */
.kv { display: grid; grid-template-columns: minmax(140px, auto) 1fr; gap: var(--space-2) var(--space-4); font-size: var(--text-sm); }
.kv dt { color: var(--color-muted); }
.kv dd { margin: 0; word-break: break-word; }

/* toggle switch */
.switch { display: inline-flex; align-items: center; gap: var(--space-3); cursor: pointer; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch__track {
  width: 40px; height: 22px; border-radius: 999px;
  background: rgba(156, 163, 175, 0.3);
  position: relative; transition: background-color var(--transition);
  flex-shrink: 0;
}
.switch__track::after {
  content: ""; position: absolute;
  top: 3px; left: 3px; width: 16px; height: 16px;
  border-radius: 50%; background: var(--on-brand);
  transition: transform var(--transition);
}
.switch input:checked + .switch__track { background: var(--color-accent); }
.switch input:checked + .switch__track::after { transform: translateX(18px); }
.switch input:focus-visible + .switch__track { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* loading state */
.loading-region { position: relative; }
.loading-region[aria-busy="true"] { opacity: 0.6; pointer-events: none; }
.loading-overlay {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  background: rgba(11, 16, 32, 0.4);
  border-radius: var(--radius);
}
.spinner {
  width: 28px; height: 28px;
  border: 3px solid rgba(23, 50, 77, 0.15);
  border-top-color: var(--color-accent);
  border-radius: 50%;
  animation: spin 700ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* consent gate banner */
.consent-gate {
  border: 1px solid rgba(245, 158, 11, 0.4);
  background: rgba(245, 158, 11, 0.08);
  border-radius: var(--radius);
  padding: var(--space-5);
  margin-bottom: var(--space-5);
}
.consent-gate__title { margin: 0 0 var(--space-2); font-size: var(--text-base); color: var(--color-warning); }
.consent-gate__body { color: var(--color-muted); margin-bottom: var(--space-4); font-size: var(--text-sm); }
.consent-gate__versions {
  font-family: var(--font-mono); font-size: var(--text-xs);
  color: var(--color-muted); margin-bottom: var(--space-4);
}

/* command browser */
.command-browser__filters {
  display: grid; gap: var(--space-3);
  grid-template-columns: minmax(0, 1fr);
  margin-bottom: var(--space-4);
}
@media (min-width: 720px) {
  .command-browser__filters { grid-template-columns: 2fr 1fr 1fr; }
}
.command-results { max-height: 520px; overflow-y: auto; }
.command-results__count {
  font-size: var(--text-sm); color: var(--color-muted);
  margin-bottom: var(--space-3);
}

/* status page */
.status-dot {
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--color-success); flex-shrink: 0;
}
.status-dot--warning { background: var(--color-warning); }
.status-dot--danger { background: var(--color-danger); }
.status-dot--pulse { animation: pulse 2s ease-in-out infinite; }
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

/* The component rows.
 *
 * A row is a name, what it is doing, and how much of it there is. The state
 * is a badge rather than a word in the second column, because a badge is
 * scannable down a column of rows and a word is not. */
.component-list { list-style: none; margin: 0; padding: 0; }
.component-list__item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--border-subtle);
}
.component-list__item:last-child { border-bottom: 0; }
.component-list__name { font-size: var(--text-base); font-weight: 550; color: var(--color-text); }
.component-list__description {
  grid-column: 1;
  font-size: var(--text-xs);
  color: var(--color-muted);
}
.component-list__state { grid-column: 2; }
.component-list__uptime { font-size: var(--text-xs); color: var(--color-muted); }
/* --- Notification workspace ---
   Welcome and Goodbye are notification-management workspaces rather than
   settings forms: composer, preview, delivery, templates, history, audit. */

/* The composer body sits on its own surface so the preview beside it reads
   as a different kind of output rather than more of the same form. */
.composer { margin-bottom: var(--space-5); }
.composer__status {
  margin: 0 0 var(--space-4);
  font-size: var(--text-sm);
  color: var(--color-muted);
}
.composer__status--dirty { color: var(--color-warning); }
.composer__status--saved { color: var(--color-success); }

.composer__group {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-4);
  margin: 0 0 var(--space-4);
}
.composer__group legend {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-muted);
  padding: 0 var(--space-2);
}

.composer__subpanel { margin-bottom: var(--space-4); }
.composer__subpanel-title { margin: 0 0 var(--space-2); font-size: var(--text-sm); font-weight: 650; }
.composer__components { display: grid; gap: var(--space-2); }
.composer__component {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-sm);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-border);
}
.composer__component .btn { margin-left: auto; }

.composer__actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
}

/* Variable browser. */
.variable-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/* The preview card is styled to resemble a Discord message closely enough
   to be useful, but it is explicitly labelled as a sample: a preview that
   looked like a real delivery would be indistinguishable from one at a
   glance, and it is not one. */
.preview-host { margin-bottom: var(--space-4); }
.discord-preview {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-4);
  max-width: 520px;
}
.discord-preview__label {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-muted);
  margin-bottom: var(--space-3);
}
.discord-preview__text { margin: 0 0 var(--space-3); white-space: pre-wrap; }

.discord-embed {
  position: relative;
  background: rgba(0, 0, 0, 0.18);
  border-radius: 4px;
  padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4);
}
.discord-embed__bar {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 4px;
  border-radius: 4px 0 0 4px;
}
.discord-embed__title { font-weight: 650; margin-bottom: var(--space-1); }
.discord-embed__description { white-space: pre-wrap; margin-bottom: var(--space-2); }
.discord-embed__fields { display: grid; gap: var(--space-2); margin-bottom: var(--space-2); }
/* One field of an embed. The name and the value have rules; the box they
   sit in did not, so the fields ran together with nothing between them. */
.discord-embed__field {
  padding: var(--space-2) var(--space-3);
  border-left: 2px solid var(--color-border, rgba(148, 163, 184, 0.25));
  background: rgba(148, 163, 184, 0.05);
  border-radius: 4px;
}
.discord-embed__field-name { font-weight: 650; font-size: var(--text-sm); }
.discord-embed__field-value { font-size: var(--text-sm); white-space: pre-wrap; }
.discord-embed__footer { font-size: 0.75rem; color: var(--color-muted); margin-top: var(--space-2); }
.discord-embed img { max-width: 100%; border-radius: 4px; margin-top: var(--space-2); }

.discord-preview__components { display: flex; gap: var(--space-2); margin-top: var(--space-3); flex-wrap: wrap; }
.discord-button {
  border: 1px solid var(--color-border);
  border-radius: 4px;
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-xs);
  background: rgba(0, 0, 0, 0.2);
}

/* Combined activity list on the Notifications overview. */
.activity-list { list-style: none; margin: 0; padding: 0; }
.activity-list__item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-border);
  font-size: 0.875rem;
}
.activity-list__item:last-child { border-bottom: none; }
.activity-list__detail { color: var(--color-muted); }
/* What happened, and when. The label is the action; the time is pushed to
   the end of the row so a list of them reads as a column of times. */
.activity-list__label { font-weight: 600; }
.activity-list__time {
  margin-left: auto;
  color: var(--color-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ---------------------------------------------------------------------------
 * The authentication gate.
 *
 * A private page must not present itself as a signed-in page before it knows.
 *
 * `data-auth-pending` is on <html> in the markup and on <main>, and removed only
 * by the module after `api.session()` has answered "authenticated". Until then
 * nothing here is visible, so a visitor whose session check failed, whose check
 * is still running, or whose JavaScript never ran at all sees a page that cannot
 * be mistaken for access.
 *
 * The visible state while it decides is a short line rather than a spinner: the
 * common case is a redirect a few hundred milliseconds away, and a full-screen
 * animation for that reads as a hang.
 * ------------------------------------------------------------------------- */

[data-auth-pending] [data-auth-body] {
  display: none !important;
}

[data-auth-pending]::after {
  content: "Checking your sign-in\2022";
  display: grid;
  place-items: center;
  min-block-size: 60vh;
  font-size: var(--text-sm);
  color: var(--color-muted);
}

/* --- admin.css --- */
/* Panindigan admin styles. Hand-written CSS.
   The admin surface is visually distinct from the customer dashboard. */

.admin {
  --admin-accent: var(--color-warning);
  min-height: 100vh;
  background: var(--color-primary);
}

.admin-header {
  background: linear-gradient(90deg, rgba(245, 158, 11, 0.12), transparent);
  border-bottom: 1px solid var(--color-border);
  padding: var(--space-4) 0;
}
.admin-header__inner {
  display: flex; align-items: center; gap: var(--space-4);
}
.admin-badge {
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: var(--space-1) var(--space-3);
  background: rgba(245, 158, 11, 0.15);
  border: 1px solid rgba(245, 158, 11, 0.4);
  color: var(--warning-text);
  border-radius: 999px;
  font-size: 0.75rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
}

.admin-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-5);
  padding: var(--space-6) 0;
}
@media (min-width: 1000px) {
  .admin-layout { grid-template-columns: 260px minmax(0, 1fr); align-items: start; }
}

.admin-nav {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-3);
  position: sticky; top: var(--space-4);
}
.admin-nav__group {
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--color-muted);
  padding: var(--space-3) var(--space-3) var(--space-2);
}
.admin-nav__list { list-style: none; margin: 0 0 var(--space-4); padding: 0; }
.admin-nav__list a {
  display: block;
  padding: var(--space-2) var(--space-3);
  border-radius: 6px;
  color: var(--color-muted);
  font-size: var(--text-sm);
}
.admin-nav__list a:hover { background: var(--overlay-hover); color: var(--color-text); }
.admin-nav__list a[aria-current="page"] {
  background: rgba(245, 158, 11, 0.15);
  color: var(--color-text);
  font-weight: 600;
}

.admin-content { min-width: 0; display: flex; flex-direction: column; gap: var(--space-5); }

.admin-section__header {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); margin-bottom: var(--space-4);
}
.admin-section__title { margin: 0; font-size: var(--text-lg); }

/* maintenance controls */
.maintenance-control {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  padding: var(--space-5);
}
.maintenance-control--danger { border-color: rgba(239, 68, 68, 0.4); }
.maintenance-modes { display: grid; gap: var(--space-3); margin: var(--space-4) 0; }
.maintenance-mode {
  display: flex; gap: var(--space-3); align-items: flex-start;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  cursor: pointer;
  transition: border-color var(--transition), background-color var(--transition);
}
.maintenance-mode:hover { border-color: var(--color-muted); }
.maintenance-mode:has(input:checked) {
  border-color: var(--admin-accent);
  background: rgba(245, 158, 11, 0.07);
}
.maintenance-mode input { margin-top: 0.25rem; accent-color: var(--admin-accent); }
.maintenance-mode__name { font-weight: 650; font-size: var(--text-base); }
.maintenance-mode__desc { font-size: var(--text-sm); color: var(--color-muted); margin-top: 2px; }

/* shard table */
.shard-grid {
  display: grid; gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.shard-tile {
  border: 1px solid var(--color-border);
  border-radius: 8px;
  padding: var(--space-3);
  background: rgba(0, 0, 0, 0.2);
}
.shard-tile__id {
  font-family: var(--font-mono); font-size: var(--text-xs);
  color: var(--color-muted); margin-bottom: var(--space-2);
}
.shard-tile__stat { display: flex; justify-content: space-between; font-size: var(--text-xs); }
.shard-tile__stat span:last-child { font-family: var(--font-mono); }

/* audit log */
.audit-entry {
  display: grid;
  grid-template-columns: minmax(140px, auto) minmax(0, 1fr);
  gap: var(--space-2) var(--space-4);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-border);
  font-size: 0.875rem;
}
@media (max-width: 640px) {
  .audit-entry { grid-template-columns: 1fr; }
}
.audit-entry__time { color: var(--color-muted); font-family: var(--font-mono); font-size: var(--text-xs); }
.audit-entry__action { font-family: var(--font-mono); color: var(--color-accent); }
.audit-entry__meta { color: var(--color-muted); font-size: var(--text-xs); word-break: break-word; }

/* danger zone */
.danger-zone {
  border: 1px solid rgba(239, 68, 68, 0.45);
  background: rgba(239, 68, 68, 0.05);
  border-radius: var(--radius);
  padding: var(--space-5);
}
.danger-zone__title {
  margin: 0 0 var(--space-2);
  font-size: 1rem; color: var(--color-danger);
  display: flex; align-items: center; gap: var(--space-2);
}
.danger-zone__body { font-size: var(--text-sm); color: var(--color-muted); margin-bottom: var(--space-4); }
.danger-zone__actions { display: flex; gap: var(--space-3); flex-wrap: wrap; }

/* admin login */
.admin-login {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: var(--space-5);
  background:
    radial-gradient(ellipse 70% 50% at 50% 0%, rgba(245, 158, 11, 0.1), transparent),
    var(--color-primary);
}
.admin-login__card {
  width: 100%;
  max-width: 400px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-6);
  box-shadow: var(--shadow-lg);
}
.admin-login__title {
  text-align: center;
  margin-bottom: var(--space-5);
}
.admin-login__notice {
  font-size: var(--text-xs);
  color: var(--color-muted);
  text-align: center;
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
}

/* severity chips */
.severity {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border-radius: 4px;
  font-size: var(--text-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.04em;
}
.severity--none { background: rgba(156, 163, 175, 0.2); color: var(--color-muted); }
.severity--standard { background: var(--accent-soft); color: var(--accent-text); }
.severity--sensitive { background: rgba(239, 68, 68, 0.2); color: var(--danger-text); }

/* --- chrome.css --- */
/* Chrome: the surfaces every page shares.
 *
 * Five hand-written stylesheets are concatenated into one bundle by
 * `scripts/build-css.mjs`. This is the fifth, and it is the one that was
 * missing.
 *
 * A survey of the forty pages found these at 0/40 or near it:
 *
 *   cookie notice          0/40     §21.9 requires one
 *   empty states           0/40     §18 requires them
 *   error states           0/40     §18 requires them
 *   loading state        25-38%     §4 requires it on every public page
 *   progressbar role        0/40     ui.loading.navigationProgress
 *
 * They were absent because each one looked like it belonged to a page, and
 * forty pages meant forty chances not to. So they live here, once, and
 * `scripts/inject-chrome.mjs` puts the markup on every page.
 *
 * Order in the bundle: base, components, dashboard, admin, chrome. This comes
 * last on purpose — it adds states to elements the earlier sheets style, and a
 * sheet loaded earlier would win on equal specificity by document order alone.
 */

/* ---------------------------------------------------------------------------
 * Navigation progress
 *
 * `ui.loading.navigationProgress`. A 2px bar at the top of the viewport that
 * appears on navigation and resolves when the document is interactive.
 *
 * Two details that are easy to get wrong and were: it is `transform: scaleX`
 * rather than `width`, so it animates on the compositor instead of forcing
 * layout on every frame; and it is hidden from assistive technology with
 * `aria-hidden`, because a progress bar that a screen reader announces on every
 * click is noise rather than information.
 * ------------------------------------------------------------------------- */

.nav-progress {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 90;
  height: 2px;
  background: var(--color-accent);
  transform: scaleX(0);
  transform-origin: left center;
  opacity: 0;
  pointer-events: none;
  transition:
    transform 400ms var(--ease-out),
    opacity 160ms ease;
}

/* Two rules rather than one: an indeterminate phase that creeps, so a slow
 * response does not look like a hang, and a determinate phase driven from
 * JavaScript as the real progress becomes known. */
.nav-progress[data-phase="loading"] {
  opacity: 1;
  animation: nav-progress-indeterminate 1.1s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}

.nav-progress[data-phase="ready"] {
  opacity: 1;
  transform: scaleX(1);
  transition:
    transform 220ms var(--ease-out),
    opacity 400ms ease 120ms;
}

@keyframes nav-progress-indeterminate {
  0% { transform: scaleX(0) translateX(0); }
  55% { transform: scaleX(0.6) translateX(60%); }
  100% { transform: scaleX(0.25) translateX(300%); }
}

/* ---------------------------------------------------------------------------
 * The branded loading screen
 *
 * `ui.loading.brandedSpinner`. Shown while a page's data is genuinely pending,
 * and dismissed by `modules/loading.ts` when the page settles.
 *
 * It covers the page rather than sitting in a corner, because a spinner in a
 * corner beside content that is about to be replaced reads as decoration. While
 * it is up, the content below is inert and marked `aria-hidden`, so a screen
 * reader is not offered a page that is about to change.
 * ------------------------------------------------------------------------- */

.loading-screen {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: grid;
  place-items: center;
  gap: var(--space-4);
  /* The one full-bleed dark surface on the site, and it is deliberate.
   *
   * The loading screen is a brand moment. The gradient sits on a dark ground so
   * that Deep Navy text on Light Sky Blue reads at 7.65:1. Everything else on
   * the site is light. */
  background: var(--navy-gradient);
  opacity: 1;
  transition: opacity var(--transition-slow), visibility var(--transition-slow);
}

.loading-screen[hidden] {
  display: none;
}

.loading-screen[data-dismissed="true"] {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.loading-screen__mark {
  width: 48px;
  height: 48px;
  border-radius: 14px;
  background: linear-gradient(135deg, var(--color-accent), #4AA3DF);
  display: grid;
  place-items: center;
  font-weight: 800;
  color: var(--on-brand);
  font-size: var(--text-lg);
  box-shadow: 0 8px 32px rgba(34, 119, 184, 0.34);
}

.loading-screen__ring {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 2px solid var(--overlay-active);
  border-top-color: var(--on-brand);
  animation: spin 720ms linear infinite;
}

.loading-screen__label {
  font-size: var(--text-sm);
  color: var(--color-muted);
  letter-spacing: 0.02em;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* ---------------------------------------------------------------------------
 * Skeletons
 *
 * `ui.loading.skeletons`. A placeholder is shaped like the thing it stands in
 * for — §6.0.2 asks for the final geometry, and a generic grey block cannot
 * do that. Each modifier below matches a real block's dimensions.
 *
 * The shimmer is a background-position animation, and it is suppressed entirely
 * under `prefers-reduced-motion` by the global rule in base.css. What remains
 * is a static block in a slightly different tone, which still communicates
 * "something is here and it is not ready" without any motion at all.
 * ------------------------------------------------------------------------- */

.skeleton {
  display: block;
  background: linear-gradient(
    90deg,
    var(--surface-2) 0%,
    var(--surface-3) 50%,
    var(--surface-2) 100%
  );
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.4s ease-in-out infinite;
  border-radius: 6px;
  color: transparent;
  user-select: none;
}

@keyframes skeleton-shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

.skeleton--text { height: 0.875rem; margin-block-end: var(--space-2); }
.skeleton--text-wide { height: 0.875rem; width: 100%; margin-block-end: var(--space-2); }
.skeleton--title { height: 1.5rem; width: 60%; margin-block-end: var(--space-3); }
.skeleton--heading { height: 2rem; width: 45%; margin-block-end: var(--space-4); }
.skeleton--card { height: 168px; border-radius: var(--radius); margin-block-end: var(--space-4); }
.skeleton--row { height: 44px; border-radius: 8px; margin-block-end: var(--space-2); }
.skeleton--avatar { width: 40px; height: 40px; border-radius: 50%; }
.skeleton--badge { height: 20px; width: 72px; border-radius: 999px; }
.skeleton--stat { height: 84px; border-radius: var(--radius); margin-block-end: var(--space-4); }

/* ---------------------------------------------------------------------------
 * Button spinner
 *
 * `ui.loading.buttonSpinner`. A button that has been pressed says so.
 *
 * Implemented by widening the button and overlaying a spinner rather than by
 * replacing the label, so the label does not jump and the button does not
 * change size mid-click. `aria-busy` carries the state for assistive
 * technology, and the label stays readable while the button is disabled.
 * ------------------------------------------------------------------------- */

.btn[data-busy="true"] {
  position: relative;
  color: transparent !important;
  pointer-events: none;
}

.btn[data-busy="true"]::after {
  content: "";
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  width: 15px;
  height: 15px;
  margin: -7.5px 0 0 -7.5px;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  color: var(--on-brand);
  animation: spin 720ms linear infinite;
}

/* ---------------------------------------------------------------------------
 * Reconnecting
 *
 * `ui.loading.reconnectingIndicator`. Realtime dropped.
 *
 * The rule that matters: this does not cover the page and does not block it.
 * The dashboard keeps showing its last known state, because HTTP is the source
 * of truth and realtime is only an update channel (§7.9). Blocking the screen
 * on a dropped socket would hide data that is still correct.
 * ------------------------------------------------------------------------- */

.reconnecting {
  position: fixed;
  inset-block-end: var(--space-5);
  inset-inline: 0;
  z-index: 70;
  margin-inline: auto;
  width: fit-content;
  max-width: calc(100vw - var(--space-6));
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-5);
  border-radius: 999px;
  background: var(--surface-3);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-lg);
  font-size: var(--text-sm);
  color: var(--color-text);
  opacity: 0;
  transform: translateY(12px);
  pointer-events: none;
  transition: opacity var(--transition), transform var(--transition-slow);
}

.reconnecting[data-state="visible"] {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

.reconnecting__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-warning);
  animation: pulse 1.6s ease-in-out infinite;
}

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

/* ---------------------------------------------------------------------------
 * Empty, error and retry
 *
 * §18 requires each of these to "explain the next useful action". A grey
 * circle and the word "Nothing here" does not.
 *
 * So every state carries an icon, a heading, a sentence saying what happened
 * and what to do, and — where there is something to do — a control. The
 * layout is shared; only the wording changes.
 *
 * `role="status"` for empty and retry, because the content changed without the
 * user acting. `role="alert"` for errors, because it did not change and the
 * user needs to know why.
 * ------------------------------------------------------------------------- */

.state {
  display: grid;
  justify-items: center;
  text-align: center;
  gap: var(--space-3);
  padding: var(--space-7) var(--space-5);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-1);
}

.state__icon {
  width: 44px;
  height: 44px;
  border-radius: 12px;
  display: grid;
  place-items: center;
  background: var(--accent-softer);
  color: var(--color-accent);
  font-size: var(--text-lg);
}

.state--error {
  border-style: solid;
  border-color: rgba(239, 68, 68, 0.35);
  background: rgba(239, 68, 68, 0.06);
}

.state--error .state__icon {
  background: rgba(239, 68, 68, 0.12);
  color: var(--color-danger);
}

.state--offline .state__icon {
  background: rgba(245, 158, 11, 0.12);
  color: var(--color-warning);
}

.state__title {
  margin: 0;
  font-size: var(--text-base);
  font-weight: 650;
  color: var(--color-text);
  letter-spacing: -0.01em;
}

.state__body {
  margin: 0;
  max-width: 46ch;
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: var(--color-muted);
  text-wrap: pretty;
}

.state__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-block-start: var(--space-2);
  justify-content: center;
}

/* A failed request that is retryable is more useful with the reason attached:
 * "Could not load commands — the backend returned 503" tells the reader
 * whether to wait, while "Something went wrong" does not. */
.state__detail {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--color-muted);
  padding: var(--space-2) var(--space-3);
  background: rgba(0, 0, 0, 0.28);
  border-radius: 6px;
  max-width: 100%;
  overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------------------
 * Cookie notice
 *
 * §21.9 and `website.json` `cookies`.
 *
 * Four rules from the specification that shape this, and each of them is
 * visible in the markup below rather than only in the behaviour:
 *
 *   * `allowRejectOptional: true` — "Reject optional" is a real button at the
 *     same weight as "Accept". A reject control that is smaller, later, or
 *     hidden behind a link is not a reject control.
 *   * `allowRevisitPreferences: true` — there is a permanent entry point in the
 *     footer, so the choice can be changed without clearing site data.
 *   * `blockPublicSiteWhenOptionalRejected: false` — nothing is blocked. The
 *     notice sits above the page and the page works underneath it.
 *   * Only categories the site actually uses are offered. Essential and
 *     Preferences are configured; Analytics and Marketing are not, because the
 *     site runs neither, and offering a category for a script that does not
 *     exist would be a consent dialog for nothing.
 *
 * §21.9 also says this notice is not a substitute for the Terms and Privacy
 * consent gate, which is a separate server-side policy (§3.15). They are
 * unrelated and both exist; neither stands in for the other.
 * ------------------------------------------------------------------------- */

.cookie-notice {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 75;
  padding: var(--space-5);
  background: var(--surface-overlay);
  backdrop-filter: blur(16px);
  border-block-start: 1px solid var(--border-strong);
  box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.45);
  transform: translateY(100%);
  transition: transform var(--transition-slow);
}

.cookie-notice[data-open="true"] {
  transform: translateY(0);
}

.cookie-notice__inner {
  max-width: 68ch;
  margin-inline: auto;
  display: grid;
  gap: var(--space-4);
}

.cookie-notice__title {
  margin: 0;
  font-size: var(--text-base);
  font-weight: 650;
  color: var(--color-text);
}

.cookie-notice__body {
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: var(--color-muted);
  text-wrap: pretty;
}

.cookie-notice__body a,
.cookie-notice__dialog a {
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.cookie-notice__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.cookie-notice__spacer {
  flex: 1 1 auto;
}

.cookie-notice__link {
  align-self: center;
  font-size: var(--text-sm);
  color: var(--color-muted);
  background: none;
  border: 0;
  padding: var(--space-2);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.cookie-notice__link:hover {
  color: var(--color-text);
}

/* --- the preferences dialog --------------------------------------------- */

.cookie-dialog {
  position: fixed;
  inset: 0;
  z-index: 85;
  display: grid;
  place-items: center;
  padding: var(--space-5);
  background: rgba(5, 8, 18, 0.7);
  backdrop-filter: blur(4px);
}

.cookie-dialog[hidden] {
  display: none;
}

.cookie-dialog__panel {
  width: min(560px, 100%);
  max-height: min(80vh, 720px);
  overflow-y: auto;
  padding: var(--space-6);
  border-radius: 14px;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-lg);
  display: grid;
  gap: var(--space-4);
}

.cookie-dialog__title {
  margin: 0;
  font-size: var(--text-lg);
  line-height: var(--leading-snug);
  color: var(--color-text);
}

.cookie-category {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--space-2) var(--space-4);
  align-items: start;
  padding: var(--space-4);
  border: 1px solid var(--border-subtle);
  border-radius: 10px;
  background: var(--surface-1);
}

.cookie-category--required {
  opacity: 0.85;
}

.cookie-category__name {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 650;
  color: var(--color-text);
}

.cookie-category__description {
  margin: 0;
  font-size: var(--text-xs);
  line-height: var(--leading-relaxed);
  color: var(--color-muted);
  text-wrap: pretty;
}

.cookie-category__state {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-muted);
  align-self: center;
}

/* The switch. A checkbox is used rather than a div with a click handler
 * because it is keyboard-operable and announced correctly for free, and this
 * is a choice about someone's data. */
.cookie-switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  cursor: pointer;
  font-size: var(--text-sm);
  color: var(--color-muted);
}

.cookie-switch input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.cookie-switch__track {
  width: 40px;
  height: 23px;
  border-radius: 999px;
  background: var(--surface-3);
  border: 1px solid var(--border-strong);
  position: relative;
  transition: background-color var(--transition-fast), border-color var(--transition-fast);
  flex: none;
}

.cookie-switch__thumb {
  position: absolute;
  inset-block-start: 2px;
  inset-inline-start: 2px;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: var(--color-muted);
  transition: transform var(--transition-fast), background-color var(--transition-fast);
}

.cookie-switch input:checked + .cookie-switch__track {
  background: var(--accent-soft);
  border-color: var(--color-accent);
}

.cookie-switch input:checked + .cookie-switch__track .cookie-switch__thumb {
  transform: translateX(17px);
  background: var(--color-accent);
}

/* The focus ring goes on the visible track, because the input itself is
 * visually hidden and an outline on a 1px box is invisible. */
.cookie-switch input:focus-visible + .cookie-switch__track {
  outline: var(--ring-width) solid var(--accent-ring);
  outline-offset: var(--ring-offset);
}

.cookie-switch input:disabled + .cookie-switch__track {
  opacity: 0.5;
  cursor: not-allowed;
}

/* --- the footer entry point --------------------------------------------- */

.cookie-revisit {
  font-size: var(--text-sm);
  color: var(--color-muted);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* The glyph in the cookie control.
 *
 * `fill="currentColor"` on the path means it follows the button's text colour,
 * which is the point: the same mark is used in the footer, in the preferences
 * dialog and in the notice, and it must not be three different greys.
 *
 * `flex: none` because the button is a flex row and an SVG without it is a
 * flex item that will shrink — on a narrow footer, "Cookie settings" wrapping
 * to two lines with a stretched icon beside it.
 *
 * `currentColor` on the wrapper, not on the path, so the path inherits it. */
.cookie-revisit__icon {
  flex: none;
  display: block;
  color: currentColor;
  opacity: 0.85;
  transition: opacity var(--transition-fast);
}

.cookie-revisit:hover .cookie-revisit__icon,
.cookie-revisit:focus-visible .cookie-revisit__icon {
  opacity: 1;
}

.cookie-revisit:hover {
  color: var(--color-text);
}

/* ---------------------------------------------------------------------------
 * Noscript
 *
 * Present on every page. Without JavaScript the cookie notice cannot be shown,
 * and without it a first-time visitor has no way to know the site uses cookies
 * at all — which is the specific outcome `allowRejectOptional` exists to
 * prevent.
 *
 * So the no-JavaScript case gets a plain, visible statement in the document,
 * with no dismiss control, because there is nothing to dismiss it into.
 * ------------------------------------------------------------------------- */

.noscript-notice {
  margin: 0;
  padding: var(--space-4) var(--space-5);
  background: var(--accent-softer);
  border-block-end: 1px solid var(--border-subtle);
  color: var(--color-text);
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  text-align: center;
}

.noscript-notice a {
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* ---------------------------------------------------------------------------
 * The preference switch
 *
 * `modules/cookies.ts` sets `data-preferences="on"|"off"` on the root element
 * when the visitor answers. Nothing read it at first, which makes it a setting
 * that looks like it works and is not — the same shape as a limit nothing
 * enforces.
 *
 * The rule below is the first consumer: a card grid honours the visitor's
 * motion preference by dropping its hover lift. It is a small, real behaviour
 * rather than a demonstration — the whole point of the Preferences category is
 * that choosing it changes something.
 * ------------------------------------------------------------------------- */

[data-preferences="off"] .card--interactive:hover {
  transform: none;
}

[data-preferences="off"] .skeleton {
  animation: none;
  background: var(--surface-3);
}

/* ---------------------------------------------------------------------------
 * Visually hidden
 *
 * A label for a control whose visual state is already conveyed. The switch's
 * track and thumb are decorative; the text beside them is what a screen reader
 * reads, and hiding it visually while leaving it in the accessibility tree is
 * the whole point.
 *
 * Not `display: none` and not `visibility: hidden`: both remove it from the
 * accessibility tree too, which would leave the switch with no name at all.
 * ------------------------------------------------------------------------- */

.cookie-visually-hidden,
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------------------
 * Offline
 *
 * `ui.states.offline`. Distinct from an error: the page did not fail, the
 * browser cannot reach anything, and retrying immediately will not help. The
 * copy says so rather than showing a failure that looks like the site's fault.
 * ------------------------------------------------------------------------- */

.state--offline {
  border-style: solid;
  border-color: rgba(245, 158, 11, 0.35);
  background: rgba(245, 158, 11, 0.06);
}

/* ---------------------------------------------------------------------------
 * Progress inside the loading screen
 *
 * A determinate bar. Not a percentage, not a fake counter: a reader can tell
 * the difference between real progress and a number counting up, and once they
 * can, they stop believing the rest of the interface.
 *
 * `scaleX` rather than `width`, so it animates on the compositor instead of
 * forcing layout on every frame. Two stages — a slow creep while the document
 * is still arriving, then a quick completion the moment it is interactive — so
 * a fast page snaps shut and a slow one still shows it is working.
 *
 * Under `prefers-reduced-motion` the global rule in base.css collapses every
 * duration in the bundle, so the bar appears in its final position with no
 * movement at all. That is why there is no check for it here.
 * ------------------------------------------------------------------------- */

.loading-screen__progress {
  position: relative;
  width: min(220px, 60vw);
  height: 3px;
  border-radius: 999px;
  background: rgba(23, 50, 77, 0.08);
  overflow: hidden;
}

.loading-screen__bar {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--color-accent), #4AA3DF);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 900ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* The page is still arriving. */
.loading-screen[data-phase="loading"] .loading-screen__bar {
  transform: scaleX(0.55);
}

/* The document is interactive; finish quickly and get out of the way. */
.loading-screen[data-phase="ready"] .loading-screen__bar {
  transform: scaleX(1);
  transition: transform 260ms var(--ease-out);
}

/* A centred column, so the bar sits under the ring rather than beside it. */
.loading-screen {
  grid-auto-flow: row;
  align-content: center;
  justify-items: center;
}

/* The page is settling: nothing interactive, so a stray click cannot land on
   content the reader has not seen yet. */
[data-loading="true"] body {
  overflow: hidden;
}

/* ---------------------------------------------------------------------------
 * Status pill states
 *
 * The pill used to render one fixed string. It now renders one of six states,
 * and the difference between them has to be visible at a glance, before the
 * label is read — so the dot is the primary signal and the colour is
 * per-state rather than a single green with a red special case.
 *
 * \`unavailable\` is the one that matters most and is deliberately *not* green.
 * A failed request means we do not know. Rendering the last known good state
 * is the failure this was written to remove: the reader sees
 * "All systems operational" precisely when the status service is down, which
 * is exactly when the answer is load-bearing. Amber and a static dot, so it
 * does not read as either healthy or broken.
 * ------------------------------------------------------------------------- */

.status-pill__text {
  display: grid;
  gap: 0.1rem;
  min-inline-size: 0;
}

.status-pill__label {
  /* Never collapse. The label ships as "Checking status…" so the pill has a
   * width before the request comes back, and this stops the row from shrinking
   * when the real label is shorter. */
  min-inline-size: 8ch;
  font-weight: 600;
  line-height: 1.3;
}

.status-pill__detail {
  font-size: var(--text-xs);
  font-weight: 400;
  line-height: 1.35;
  color: var(--color-faint);
}

[data-status-pill][data-state="loading"] .status-pill__dot {
  background: var(--color-muted);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-muted) 20%, transparent);
  animation: status-pulse-muted 1.6s var(--ease-out) infinite;
}

[data-status-pill][data-state="operational"] .status-pill__dot {
  background: var(--color-success);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-success) 22%, transparent);
}

[data-status-pill][data-state="restricted"] .status-pill__dot,
[data-status-pill][data-state="full"] .status-pill__dot {
  background: var(--color-warning);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-warning) 24%, transparent);
  animation: status-pulse 2.6s var(--ease-out) infinite;
}

[data-status-pill][data-state="unknown"] .status-pill__dot,
[data-status-pill][data-state="unavailable"] .status-pill__dot {
  background: #6B8199;
  box-shadow: 0 0 0 3px color-mix(in srgb, #6B8199 18%, transparent);
  animation: none;
}

/* A pill that cannot be read is worse than a pill that is short. The detail
   line wraps under a narrow footer rather than pushing the columns. */
.status-pill {
  max-inline-size: min(100%, 30ch);
  white-space: normal;
  align-items: flex-start;
  text-align: start;
}

.status-pill__dot {
  margin-block-start: 0.35em;
}

@keyframes status-pulse-muted {
  0%,
  70%,
  100% {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-muted) 20%, transparent);
  }
  35% {
    box-shadow: 0 0 0 6px color-mix(in srgb, var(--color-muted) 0%, transparent);
  }
}

@media (prefers-reduced-motion: reduce) {
  [data-status-pill][data-state="loading"] .status-pill__dot,
  [data-status-pill][data-state="restricted"] .status-pill__dot,
  [data-status-pill][data-state="full"] .status-pill__dot {
    animation: none;
  }
}

/* --- content.css --- */
:root {
  /* Exposed as a token because the reduced-motion rule needs one value to
   * collapse, and a hard-coded `none` here would have to be overridden there. */
  --skeleton-sweep: 1.15s;
}/* Content components.
 *
 * A survey compared the class names the forty pages use against the class names
 * the stylesheets define. The gap was not thirteen orphan classes — it was
 * whole BEM families with no rules at all:
 *
 *    .panel          25 pages      section wrapper, header + body
 *    .faq-item       11 pages      a details/summary question
 *    .site-footer    10 pages      the footer, with brand and link columns
 *    .def-list       several       a definition list inside a panel body
 *    .feature-list    4 pages      a bulleted list with a lead-in phrase
 *    .kv              3 pages      a two-column key/value grid
 *    .form            2 pages      the contact form and its states
 *    .step-list       1 page       an ordered process
 *    .command-browser 1 page       the command directory's shell
 *    .skeleton-stack  3 pages      a column of skeleton placeholders
 *    .docs__nav       1 page       the documentation sidebar container
 *
 * The markup was written with a consistent vocabulary and the stylesheet was
 * never written to match, so roughly two thirds of the pages were rendering
 * unstyled sections inside a correctly styled page. That reads as "the design
 * is inconsistent" from the outside and as "several pages are missing CSS"
 * from here — which is what it was.
 *
 * The markup is the specification here. Every rule below matches the shape
 * found in the pages rather than an imagined one, so nothing needs to be changed
 * in forty HTML files to make this work.
 */

/* ---------------------------------------------------------------------------
 * Page main
 *
 * The class `inject-page-meta.mjs` puts on the `<main>` it wraps around the four
 * pages that had none. A landmark with no rule is not a defect on its own --
 * `<main>` is a block element and those pages already carry their own layout --
 * but a class the build itself injects and no stylesheet styles is the same
 * shape as the orphan classes this file exists to remove.
 *
 * So it does something. These four pages line up with the thirty-six that
 * already had a landmark and therefore their own container, which is the point
 * of adding the landmark: the pages should look the same afterwards, not merely
 * become more navigable.
 * ------------------------------------------------------------------------- */

.page-main {
  display: block;
  width: 100%;
  max-width: var(--max-width);
  margin-inline: auto;
  padding-inline: var(--space-5);
}

@media (max-width: 640px) {
  .page-main {
    padding-inline: var(--space-4);
  }
}

/* ---------------------------------------------------------------------------
 * Panel
 *
 * The section wrapper used by most pages. A raised surface with a heading strip
 * and a padded body — the one container that a content page reaches for when
 * it has no card grid to use.
 *
 * `panel__header` carries a bottom border rather than being a separate card, so
 * a panel reads as one object with a lid rather than as two stacked blocks.
 * ------------------------------------------------------------------------- */

.panel {
  position: relative;
  background: var(--elev-2-surface);
  border-radius: var(--radius-lg);
  /* The border and the shadow are the grouped rule's, in ui.css — where every
   * raised surface gets its edge, so that a card, a panel and a stat tile
   * cannot drift apart. A page made of panels read as a stack of paper
   * because the panels were one step above the page and not much above each
   * other; that is the grouped rule's height now. */
  overflow: hidden;
  /* Panels sit in a column, and a panel's own margin plus its neighbour's would
   * double. The gap on the parent handles spacing; this only covers a panel that
   * is not in such a parent. */
  margin-block-end: var(--space-5);
}

.panel:last-child {
  margin-block-end: 0;
}

.panel__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  padding: var(--space-4) var(--space-5);
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--sky-50) 82%, transparent),
    var(--surface-2)
  );
  border-block-end: 1px solid var(--border-subtle);
}

.panel__header h2,
.panel__header h3 {
  margin: 0;
  font-size: var(--text-base);
  line-height: var(--leading-snug);
  font-weight: 650;
  letter-spacing: -0.01em;
  color: var(--color-text);
}

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

.panel__body {
  padding: var(--space-5);
}

.panel__body > :last-child {
  margin-block-end: 0;
}

.panel__footer {
  padding: var(--space-4) var(--space-5);
  background: var(--surface-2);
  border-block-start: 1px solid var(--border-subtle);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
}

/* A heading with no header strip above it still needs breathing room. */
.panel__body > h2:first-child,
.panel__body > h3:first-child {
  margin-block-start: 0;
}

/* ---------------------------------------------------------------------------
 * Definition list
 *
 * Used inside a panel body on several pages: a term, then its explanation. The
 * default `dl` flow puts the term on its own line at full width, which on a
 * narrow column wastes half the measure for a two-word phrase.
 *
 * So terms and definitions are a two-column grid that collapses to one column
 * below the tablet width, where a 12rem term column would squeeze the
 * explanation to nothing.
 * ------------------------------------------------------------------------- */

.def-list {
  display: grid;
  grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
  gap: var(--space-3) var(--space-5);
  margin: 0;
}

.def-list dt {
  font-weight: 650;
  color: var(--color-text);
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
}

.def-list dd {
  margin: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  text-wrap: pretty;
}

@media (max-width: 640px) {
  .def-list {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-1);
  }
  .def-list dd {
    margin-block-end: var(--space-3);
  }
}

/* ---------------------------------------------------------------------------
 * Key/value grid
 *
 * Two columns of bare divs, used on the legal pages for version and effective
 * date. Semantically it is a description list written as divs, so the grid is
 * what carries the association — a border between the columns does that
 * visually better than any amount of spacing.
 * ------------------------------------------------------------------------- */

.kv {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-3) var(--space-5);
  padding: var(--space-4);
  border: 1px solid var(--border-subtle);
  border-radius: 10px;
  background: var(--surface-2);
}

.kv > div:nth-child(odd) {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-muted);
  align-self: center;
}

.kv > div:nth-child(even) {
  font-size: var(--text-sm);
  color: var(--color-text);
}

.kv code {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  padding: 0.1rem 0.35rem;
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.3);
}

@media (max-width: 480px) {
  .kv {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-1);
  }
  .kv > div:nth-child(even) {
    margin-block-end: var(--space-2);
  }
}

/* ---------------------------------------------------------------------------
 * FAQ
 *
 * A `<details>` per question. The default marker is a small triangle that
 * disappears entirely in some engines and is unstyleable in others, so it is
 * replaced with a drawn caret that rotates.
 *
 * `summary` is marked `display: list-item` removed, so the whole row is the hit
 * target rather than the text only — on a touch screen the default target is
 * too small to be reliable.
 * ------------------------------------------------------------------------- */

.faq {
  display: grid;
  gap: var(--space-3);
}

.faq-item {
  background: var(--surface-1);
  border: 1px solid var(--border-subtle);
  border-radius: 10px;
  overflow: hidden;
  transition: border-color var(--transition-fast), background-color var(--transition-fast);
}

.faq-item:hover {
  border-color: var(--border-strong);
  background: var(--surface-2);
}

.faq-item[open] {
  border-color: var(--accent-ring);
  background: var(--surface-2);
}

.faq-item > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  cursor: pointer;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-text);
  list-style: none;
  min-height: 44px;
}

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

.faq-item > summary::marker {
  content: "";
}

.faq-item > summary::after {
  content: "";
  flex: none;
  width: 9px;
  height: 9px;
  border-inline-end: 2px solid var(--color-muted);
  border-block-end: 2px solid var(--color-muted);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--transition), border-color var(--transition);
}

.faq-item[open] > summary::after {
  transform: rotate(-135deg) translate(-2px, -2px);
  border-color: var(--color-accent);
}

.faq-item > summary:focus-visible {
  outline: var(--ring-width) solid var(--accent-ring);
  outline-offset: calc(var(--ring-offset) * -1);
  border-radius: 10px;
}

/* The answer sits inside the item, indented to the text rather than to the
 * caret, so the question and its answer share a left edge. */
.faq-item__body,
.faq-item > p,
.faq-item > div {
  padding: 0 var(--space-5) var(--space-5);
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: var(--color-muted);
  text-wrap: pretty;
}

.faq-item > p:last-child,
.faq-item > div:last-child {
  margin-block-end: 0;
}

/* ---------------------------------------------------------------------------
 * Feature list and step list
 *
 * Two bulleted lists that need more than default `ul` styling: the marker is
 * replaced with a small accent dot, and the lead-in phrase in each item is
 * already wrapped in `<strong>` by the markup, so the emphasis comes for free.
 *
 * The step list is the same shape with a numbered treatment, because a process
 * read top to bottom should look like one.
 * ------------------------------------------------------------------------- */

.feature-list,
.step-list {
  display: grid;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: step;
}

.feature-list > li,
.step-list > li {
  position: relative;
  padding-inline-start: var(--space-6);
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: var(--color-muted);
  text-wrap: pretty;
}

.feature-list > li strong,
.step-list > li strong {
  color: var(--color-text);
  font-weight: 650;
}

/* A dot rather than a disc: the default marker sits too high against the line
 * height and cannot be coloured without the list-style hack. */
.feature-list > li::before {
  content: "";
  position: absolute;
  inset-inline-start: 4px;
  inset-block-start: 0.55em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-accent);
  box-shadow: 0 0 0 3px var(--accent-softer);
}

.step-list > li::before {
  counter-increment: step;
  content: counter(step);
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: var(--text-xs);
  font-weight: 700;
  color: var(--color-accent);
  background: var(--accent-soft);
  border: 1px solid var(--accent-ring);
  line-height: 1;
}

/* ---------------------------------------------------------------------------
 * Form
 *
 * A form is a stack of fields, a status region, and an action row. The status
 * region is always present in the document and empty until there is something to
 * say, because a live region that is inserted at the moment it has content is
 * frequently not announced — the element and the text have to arrive together.
 * ------------------------------------------------------------------------- */

.form {
  display: grid;
  gap: var(--space-5);
}

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

.form__status {
  margin: 0;
  min-height: 1.5rem;
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: var(--color-muted);
}

.form__status:not(:empty) {
  padding: var(--space-3) var(--space-4);
  border-radius: 8px;
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
}

.form__status[data-state="success"] {
  color: var(--color-success);
  border-color: rgba(34, 197, 94, 0.35);
  background: rgba(34, 197, 94, 0.08);
}

.form__status[data-state="error"] {
  color: var(--color-danger);
  border-color: rgba(239, 68, 68, 0.35);
  background: rgba(239, 68, 68, 0.08);
}

.form__status[data-state="pending"] {
  color: var(--color-warning);
  border-color: rgba(245, 158, 11, 0.35);
  background: rgba(245, 158, 11, 0.08);
}

/* ---------------------------------------------------------------------------
 * Command browser
 *
 * The shell around the command directory: a filter row, a result count, and the
 * results themselves. The count matters more than it looks — "421 commands"
 * with no way to tell how many the filter left is a dead end, and §18 asks every
 * asynchronous screen to explain the next useful action.
 * ------------------------------------------------------------------------- */

.command-browser {
  display: grid;
  gap: var(--space-5);
}

.command-browser__filters {
  display: grid;
  grid-template-columns: minmax(0, 2fr) repeat(3, minmax(0, 1fr));
  gap: var(--space-4);
  align-items: end;
}

@media (max-width: 900px) {
  .command-browser__filters {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 560px) {
  .command-browser__filters {
    grid-template-columns: minmax(0, 1fr);
  }
}

.command-browser__count {
  font-size: var(--text-sm);
  color: var(--color-muted);
}

.command-browser__count strong {
  color: var(--color-text);
  font-weight: 650;
}

.command-browser__results {
  display: grid;
  gap: var(--space-3);
}

/* ---------------------------------------------------------------------------
 * Skeleton stack
 *
 * A column of placeholders. The gap is on the container rather than a
 * margin-block-end on each child, so the last skeleton does not leave a hole
 * where content will appear.
 * ------------------------------------------------------------------------- */

.skeleton-stack {
  display: grid;
  gap: var(--space-3);
  align-content: start;
}

.skeleton-stack > * {
  margin-block-end: 0;
}

/* ---------------------------------------------------------------------------
 * Footer
 *
 * The footer is a four-column layout at desktop width, two at tablet, one on a
 * phone. It is the last thing on every page, so it is the last thing that should
 * cost a reader any attention: muted, flat, and without a heading level that
 * competes with the content above it.
 * ------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
 * Footer
 *
 * One markup on forty pages, so this is the first stylesheet for it rather than
 * a revision. The grid has four columns because the footer has four children --
 * the previous `1.4fr repeat(3, 1fr)` was written for a brand plus three columns
 * and then applied to footers with a single child, which put that child in column
 * two with an empty 1.4fr column to its left. That is the scattered look, and it
 * was never a grid problem. It was a grid matching content it did not have.
 * ------------------------------------------------------------------------- */

.site-footer {
  position: relative;
  margin-block-start: var(--space-7);
  padding-block: var(--space-7) var(--space-5);
  background: var(--surface-1);
  isolation: isolate;
}

/* Where the footer meets the page the elevation scale already draws a border
 * — the grouped rule in ui.css. A one-pixel gradient seam used to be painted
 * over that join as well, and it read as a lighter stripe rather than as a
 * transition, so the paint is gone and the border is what remains. */

.site-footer__inner {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) repeat(3, minmax(0, 1fr));
  gap: var(--space-6) var(--space-5);
  align-items: start;
}

/* The glow behind the brand column.
 *
 * A radial gradient at a few percent of alpha, positioned to sit behind the
 * footer's one large area of empty space. It gives the column a floor to the
 * page without tinting any text — which is the whole discipline for a gradient
 * on a dark theme: colour *behind* text, never under it. */
.site-footer__about {
  position: relative;
  display: grid;
  gap: var(--space-3);
  align-content: start;
  justify-items: start;
  max-width: 42ch;
}

.site-footer__about::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset-block-start: -6rem;
  inset-inline-start: -8rem;
  inline-size: 26rem;
  block-size: 20rem;
  background: radial-gradient(
    closest-side,
    color-mix(in srgb, var(--color-accent) 13%, transparent),
    transparent 72%
  );
  filter: blur(6px);
  pointer-events: none;
}

/* --- the brand --- */

.site-footer__brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: 700;
  font-size: var(--text-base);
  letter-spacing: -0.015em;
  color: var(--color-text);
  text-decoration: none;
  transition: opacity var(--transition-fast);
}

.site-footer__brand:hover {
  opacity: 0.82;
}

.site-footer__mark {
  display: grid;
  place-items: center;
  inline-size: 26px;
  block-size: 26px;
  border-radius: 8px;
  font-size: var(--text-xs);
  font-weight: 800;
  letter-spacing: 0;
  color: #17324D;
  background: var(--accent-gradient);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.32), 0 4px 12px -4px color-mix(in srgb, var(--color-accent) 55%, transparent);
}

.site-footer__tagline {
  margin: 0;
  max-width: 38ch;
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: var(--color-muted);
  text-wrap: pretty;
}

/* --- the status pill ---
 *
 * The Status page is two clicks away and it is the page people look for when
 * something is wrong. A pill in the footer is the difference between finding it
 * and assuming the site is down. */

.status-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.3rem 0.7rem 0.3rem 0.6rem;
  border: 1px solid var(--border-subtle);
  border-radius: 999px;
  background: var(--surface-2);
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--color-text);
  text-decoration: none;
  white-space: nowrap;
  transition:
    border-color var(--transition-fast),
    background var(--transition-fast);
}

.status-pill:hover {
  border-color: var(--accent-ring);
  background: var(--surface-3);
}

.status-pill__dot {
  inline-size: 7px;
  block-size: 7px;
  flex: none;
  border-radius: 50%;
  background: var(--color-success);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-success) 22%, transparent);
  animation: status-pulse 2.6s var(--ease-out) infinite;
}

@keyframes status-pulse {
  0%,
  70%,
  100% {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-success) 22%, transparent);
  }
  35% {
    box-shadow: 0 0 0 6px color-mix(in srgb, var(--color-success) 0%, transparent);
  }
}

/* --- the link columns ---
 *
 * A heading per column, because a list of links with no grouping is a list of
 * links. The heading is a real `<h2>` in the markup — the markup has them --
 * so this is presentation, not structure: small, uppercase, wide tracking, and
 * muted far enough that it cannot compete with a link the reader actually wants. */

.site-footer__col {
  display: grid;
  gap: var(--space-2);
  align-content: start;
}

.site-footer__heading {
  margin: 0 0 var(--space-1);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-faint);
}

.site-footer__col a {
  justify-self: start;
  font-size: var(--text-sm);
  color: var(--color-muted);
  text-decoration: none;
  transition: color var(--transition-fast);
}

.site-footer__col a:hover,
.site-footer__col a:focus-visible {
  color: var(--color-text);
  text-decoration: underline;
  text-underline-offset: 0.24em;
}

/* --- the bottom row ---
 *
 * Its own element rather than a `grid-column: 1 / -1` child, because it is not
 * a fourth column of the grid above — it is a row under it, and giving it a
 * grid position would tie it to a column count that is already responsive. */

.site-footer__bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3) var(--space-5);
  margin-block-start: var(--space-6);
  padding-block-start: var(--space-5);
  border-block-start: 1px solid var(--border-subtle);
}

.site-footer__copyright {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--color-faint);
}

/* The footer's meta row.
 *
 * The cookie preferences control is added to every footer by the chrome
 * injection, wrapped in this.
 *
 * The injection's own comment says the wrapper "is `grid-column: 1 / -1`, so it
 * spans correctly in both footer shapes the site has: the full four-column
 * one, and the bare two-child one on the error pages". It was not. The rule
 * did not exist, so the control was a grid item in a four-column grid, in
 * whichever column it landed in, and it was as wide as that column.
 *
 * The chrome gate caught it — a class used on seven pages and defined by no
 * stylesheet. It had not caught it before because the pages it was reading
 * were the ones the injection had already been through, and the shells had
 * not been regenerated since the injection moved to the canonical footer. */
.site-footer__meta {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-5);
  margin-block-start: var(--space-5);
  padding-block-start: var(--space-5);
  border-block-start: 1px solid var(--border-subtle);
  font-size: var(--text-xs);
  color: var(--color-faint);
}

.site-footer__controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-4);
}

.site-footer__signin {
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--color-muted);
  text-decoration: none;
  transition: color var(--transition-fast);
}

.site-footer__signin:hover {
  color: var(--color-text);
}

/* --- the collapse points ---
 *
 * Two, then one. Both go through `auto-fit`'s sibling rather than dropping a
 * column: a footer is read at every width, so nothing is hidden below a
 * breakpoint — the columns restack. */

@media (max-width: 1040px) {
  .site-footer__inner {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-6) var(--space-5);
  }

  .site-footer__about {
    grid-column: 1 / -1;
    max-width: none;
  }
}

@media (max-width: 640px) {
  .site-footer {
    padding-block: var(--space-6) var(--space-4);
  }

  .site-footer__inner {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-5);
  }

  .site-footer__bottom {
    justify-content: flex-start;
  }
}

/* ---------------------------------------------------------------------------
 * Documentation sidebar container
 *
 * The one page that carries this, and the page whose navigation was the subject
 * of the earlier fix. The container is a sticky column that becomes a normal
 * block below the tablet width — a sticky sidebar on a phone is a panel that
 * covers the article.
 * ------------------------------------------------------------------------- */

.docs__nav {
  position: sticky;
  top: calc(var(--header-height) + var(--space-4));
  align-self: start;
  max-height: calc(100vh - var(--header-height) - var(--space-6));
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-inline-end: var(--space-2);
}

@media (max-width: 1024px) {
  .docs__nav {
    position: static;
    max-height: none;
    overflow: visible;
  }
}


/* ---------------------------------------------------------------------------
 * Cards
 *
 * The card family — its definition, its edge token, its lift and its focus
 * ring — is in components.css.
 *
 * It used to be declared here as well, and the copy won because this sheet
 * comes later in the bundle. The copy reset the card's shadow to the smallest
 * elevation in the scale, so a card that had just been raised was quietly put
 * back. What remains here is the icon slot, which is an addition to a card
 * and conflicts with nothing.
 * ------------------------------------------------------------------------- */

/* The card's edge used to include a hairline of accent along the top on
 * hover: the same geometry as the footer's and the panel's seams, animating
 * its opacity through --card-edge. It drew a line rather than a state, and
 * the border colour the same token already drives is the state. The
 * pseudo-element is gone; what remains here is the icon slot, which is an
 * addition to a card and conflicts with nothing. */

/* --- the icon slot ---
 *
 * The emoji glyphs in card__icon are colourless, so on a dark surface they read
 * as smudges. A soft accent wash behind them separates the glyph from the card
 * without tinting the text around it. */

.card__icon {
  display: grid;
  place-items: center;
  inline-size: 2.75rem;
  block-size: 2.75rem;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  background:
    radial-gradient(
      closest-side,
      color-mix(in srgb, var(--color-accent) 18%, transparent),
      transparent 78%
    ),
    var(--surface-2);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
  font-size: var(--text-lg);
  line-height: 1;
  transition:
    border-color var(--transition-base),
    transform var(--transition-base),
    box-shadow var(--transition-base);
}

.card--interactive:hover .card__icon,
.card--interactive:focus-within .card__icon {
  border-color: color-mix(in srgb, var(--color-accent) 40%, var(--border-subtle));
  transform: scale(1.04);
}

/* --- the title ---
 *
 * `text-wrap: balance` on a two-line heading is the difference between a title
 * that fills its card and one that leaves a two-word orphan on the second line.
 * `pretty` on the body does the same for the paragraph underneath, where the
 * alternative is a single word stranded. */

.card__title {
  text-wrap: balance;
}

.card__body {
  text-wrap: pretty;
}

/* ---------------------------------------------------------------------------
 * Skeletons
 *
 * `.skeleton` is defined in components.css and again in chrome.css. These
 * override both, from the last sheet in the bundle.
 * ------------------------------------------------------------------------- */



.skeleton {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--border-subtle);
  background: var(--surface-2);
}

/* The sweep.
 *
 * `background-position` on a `::after`, not a transform on the block: the
 * layout never moves, so a skeleton cannot cause a reflow as it animates. */
.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(
    90deg,
    transparent,
    color-mix(in srgb, var(--color-text) 7%, transparent) 45%,
    color-mix(in srgb, var(--color-text) 7%, transparent) 55%,
    transparent
  );
  animation: skeleton-sweep var(--skeleton-sweep) var(--ease-out) infinite;
}

/* Staggered.
 *
 * Two words for a shape a reader is meant to parse: a stack of skeletons that
 * all sweep in step reads as one block, and the eye gets no sense of how many
 * rows are coming. `nth-child` on the modifiers rather than a class, so the
 * markup does not grow a `--delay` attribute per row. */

.skeleton-stack > :nth-child(2)::after { animation-delay: 90ms; }
.skeleton-stack > :nth-child(3)::after { animation-delay: 180ms; }
.skeleton-stack > :nth-child(4)::after { animation-delay: 270ms; }
.skeleton-stack > :nth-child(5)::after { animation-delay: 360ms; }
.skeleton-stack > :nth-child(6)::after { animation-delay: 450ms; }

@keyframes skeleton-sweep {
  to {
    transform: translateX(100%);
  }
}

/* The reduced-motion case.
 *
 * base.css collapses every duration in the bundle, but an `animation-duration`
 * of `0.01ms` still leaves an element animating forever — which is worse than
 * not animating, because the sweep keeps compositing. This turns it off by name
 * rather than by duration, and base.css's global rule is what covers the
 * transition on `.card` above. */

@media (prefers-reduced-motion: reduce) {
  :root {
    --skeleton-sweep: 0s;
  }

  .skeleton::after {
    display: none;
  }

  .status-pill__dot {
    animation: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-success) 22%, transparent);
  }
}

/* --- ui.css --- */
/* ===========================================================================
 * Panindigan UI layer
 *
 * The component layer. What is already in the bundle and is *not* repeated
 * here, because repeating it would create two sources of truth:
 *
 *   chrome.css     cookie notice, preferences dialog, loading screen, nav
 *                  progress, button spinner, `.state` empty/error/offline
 *   dom.ts         toast(), alert(), renderEmptyState(), setPending(), setBusy()
 *
 * So this sheet holds the components that did not exist:
 *
 *   dialogs        modal, confirm, drawer
 *   overlays       dropdown, popover, tooltip
 *   disclosure     tabs, accordion, segmented control
 *   forms          field, input states, helper text, password visibility
 *   data           stat card, table shell, pagination, badges, avatars
 *   navigation     breadcrumbs, mobile drawer
 *   charts         placeholder with legend and tooltip
 *   system         session expired, permission denied, progress
 *
 * Two rules hold across all of it:
 *
 * **1. Focus is always visible.** `:focus-visible` gets a ring on every
 *    interactive element, and `:focus` gets one only when the element is not a
 *    text input — because a ring drawn inside a field while someone is typing in
 *    it is noise, not accessibility.
 *
 * **2. Every animation has a reduced-motion answer.** Not "the important ones",
 *    all of them, by name rather than by duration. base.css collapses durations
 * globally, but an animation with a collapsed duration still composites forever,
 * so the ones that loop are stopped outright.
 * =========================================================================== */

/* ---------------------------------------------------------------------------
 * Badges, avatars, separators
 * ------------------------------------------------------------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.15rem 0.5rem;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-pill, 999px);
  background: var(--surface-2);
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--color-muted);
  white-space: nowrap;
}

.badge--accent {
  border-color: color-mix(in srgb, var(--color-accent) 38%, transparent);
  background: color-mix(in srgb, var(--color-accent) 14%, var(--surface-2));
  color: color-mix(in srgb, var(--color-accent) 70%, var(--color-text));
}

.badge--success {
  border-color: color-mix(in srgb, var(--color-success) 38%, transparent);
  background: color-mix(in srgb, var(--color-success) 13%, var(--surface-2));
  color: var(--success-text);
}

.badge--warning {
  border-color: color-mix(in srgb, var(--color-warning) 40%, transparent);
  background: color-mix(in srgb, var(--color-warning) 14%, var(--surface-2));
  color: var(--warning-text);
}

.badge--danger {
  border-color: color-mix(in srgb, var(--color-danger) 40%, transparent);
  background: color-mix(in srgb, var(--color-danger) 14%, var(--surface-2));
  color: var(--danger-text);
}

.badge--info {
  border-color: color-mix(in srgb, var(--color-info) 38%, transparent);
  background: color-mix(in srgb, var(--color-info) 13%, var(--surface-2));
  color: var(--info-text);
}

.avatar {
  display: grid;
  place-items: center;
  flex: none;
  inline-size: var(--avatar-size, 32px);
  block-size: var(--avatar-size, 32px);
  border-radius: 50%;
  overflow: hidden;
  background: linear-gradient(135deg, var(--color-accent), #4AA3DF);
  color: #17324D;
  font-size: calc(var(--avatar-size, 32px) * 0.42);
  font-weight: 700;
  line-height: 1;
  text-transform: uppercase;
  user-select: none;
}

.avatar img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

.avatar--sm { --avatar-size: 24px; }
.avatar--lg { --avatar-size: 44px; }

.avatar[data-fallback="true"] {
  background: var(--surface-3);
  color: var(--color-muted);
}

.divider {
  block-size: 1px;
  border: 0;
  margin-block: var(--space-4);
  background: var(--border-subtle);
}

.divider--vertical {
  inline-size: 1px;
  block-size: auto;
  align-self: stretch;
  margin-block: 0;
}

/* ---------------------------------------------------------------------------
 * Dialogs
 *
 * One overlay primitive for the modal, the confirm and the drawer. They differ
 * in animation and in where they sit, not in how they trap focus or how they
 * close — three copies of that is three chances for the third to forget the
 * Escape key.
 * ------------------------------------------------------------------------- */

.dialog-backdrop {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal, 900);
  display: grid;
  padding: var(--space-4);
  background: color-mix(in srgb, #05070f 72%, transparent);
  backdrop-filter: blur(3px);
  opacity: 0;
  transition: opacity var(--transition-base, 180ms) var(--ease-out, ease);
}

.dialog-backdrop[data-open="true"] {
  opacity: 1;
}

.dialog {
  place-self: center;
  inline-size: min(100%, var(--dialog-width, 34rem));
  max-block-size: min(86vh, 46rem);
  overflow: auto;
  padding: var(--space-6);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg, 14px);
  background: var(--surface-1);
  box-shadow: var(--shadow-lg, 0 24px 60px -24px rgb(0 0 0 / 0.7));
  transform: translateY(8px) scale(0.985);
  opacity: 0;
  transition:
    transform var(--transition-base, 180ms) var(--ease-out, ease),
    opacity var(--transition-base, 180ms) var(--ease-out, ease);
}

.dialog-backdrop[data-open="true"] .dialog {
  transform: none;
  opacity: 1;
}

.dialog--sm { --dialog-width: 26rem; }
.dialog--lg { --dialog-width: 48rem; }

.dialog--drawer {
  place-self: end;
  inline-size: min(100%, 22rem);
  max-block-size: 100%;
  padding: var(--space-5);
  border-radius: var(--radius-lg, 14px) var(--radius-lg, 14px) 0 0;
  transform: translateY(100%);
}

.dialog--drawer.dialog-backdrop {
  padding: 0;
}

@media (max-width: 560px) {
  .dialog--drawer {
    inline-size: 100%;
  }
}

.dialog__header {
  display: grid;
  gap: var(--space-2);
  margin-block-end: var(--space-4);
}

.dialog__title {
  margin: 0;
  font-size: var(--text-lg, 1.125rem);
  font-weight: 650;
  letter-spacing: -0.01em;
  line-height: 1.3;
  color: var(--color-text);
  text-wrap: balance;
}

.dialog__body {
  color: var(--color-muted);
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
}

.dialog__footer {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-3);
  margin-block-start: var(--space-6);
}

.dialog__close {
  position: absolute;
  inset-block-start: var(--space-3);
  inset-inline-end: var(--space-3);
  display: grid;
  place-items: center;
  inline-size: 30px;
  block-size: 30px;
  border: 0;
  border-radius: var(--radius-sm, 8px);
  background: transparent;
  color: var(--color-muted);
  cursor: pointer;
  transition:
    background var(--transition-fast, 120ms),
    color var(--transition-fast, 120ms);
}

.dialog__close:hover {
  background: var(--surface-3);
  color: var(--color-text);
}

.dialog-backdrop {
  position: fixed;
}

/* The drawer animates from the bottom, so it needs a positioning context that is
   the overlay itself rather than the sheet. */
.dialog-backdrop:has(.dialog--drawer) {
  align-items: end;
}

/* ---------------------------------------------------------------------------
 * Dropdown, popover, tooltip
 *
 * Positioned by JS against the trigger's rect rather than by CSS anchoring,
 * because anchoring is not in every engine and a dropdown that renders in the
 * wrong place is worse than one that renders in a predictable place. The module
 * flips it when it would leave the viewport.
 * ------------------------------------------------------------------------- */

.menu {
  position: fixed;
  z-index: var(--z-popover, 950);
  min-inline-size: 12rem;
  max-inline-size: min(22rem, calc(100vw - 2rem));
  max-block-size: min(24rem, calc(100vh - 4rem));
  overflow: auto;
  padding: var(--space-1);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius, 10px);
  background: var(--surface-2);
  box-shadow: var(--shadow-lg, 0 24px 60px -24px rgb(0 0 0 / 0.7));
  transform-origin: top left;
  opacity: 0;
  transform: scale(0.97);
  transition:
    opacity var(--transition-fast, 120ms) var(--ease-out, ease),
    transform var(--transition-fast, 120ms) var(--ease-out, ease);
}

.menu[data-open="true"] {
  opacity: 1;
  transform: none;
}

.menu[data-placement="bottom-end"] {
  transform-origin: top right;
}

.menu__item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  inline-size: 100%;
  padding: 0.45rem 0.6rem;
  border: 0;
  border-radius: var(--radius-sm, 7px);
  background: transparent;
  font: inherit;
  font-size: var(--text-sm);
  color: var(--color-muted);
  text-align: start;
  text-decoration: none;
  cursor: pointer;
  transition:
    background var(--transition-fast, 120ms),
    color var(--transition-fast, 120ms);
}

.menu__item:hover,
.menu__item:focus-visible {
  background: var(--surface-3);
  color: var(--color-text);
}

.menu__item[aria-current="true"],
.menu__item[aria-selected="true"] {
  background: color-mix(in srgb, var(--color-accent) 16%, transparent);
  color: var(--color-text);
}

.menu__item[aria-disabled="true"] {
  opacity: 0.45;
  pointer-events: none;
}

.menu__item--danger {
  color: var(--danger-text);
}

.menu__item--danger:hover {
  background: color-mix(in srgb, var(--color-danger) 16%, transparent);
  color: var(--danger-text);
}

.menu__sep {
  block-size: 1px;
  margin: var(--space-1) calc(var(--space-1) * -1);
  background: var(--border-subtle);
}

.menu__label {
  padding: var(--space-2) 0.6rem var(--space-1);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-faint);
}

/* The keyboard hint. Right-aligned, dimmed, and the reason a menu item is not
   an anchor: the shortcut is part of the contract. */
.menu__hint {
  margin-inline-start: auto;
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--color-faint);
}

.tooltip {
  position: fixed;
  z-index: var(--z-tooltip, 970);
  max-inline-size: 20rem;
  padding: 0.35rem 0.55rem;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm, 7px);
  background: var(--surface-3);
  box-shadow: var(--shadow-md, 0 12px 32px -18px rgb(0 0 0 / 0.7));
  font-size: var(--text-xs);
  line-height: 1.45;
  color: var(--color-text);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--transition-fast, 120ms) ease;
}

.tooltip[data-open="true"] {
  opacity: 1;
}

.tooltip__key {
  display: inline-block;
  padding: 0 0.28em;
  margin-inline-end: 0.2em;
  border: 1px solid var(--border-subtle);
  border-radius: 4px;
  background: var(--surface-1);
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.9em;
  color: var(--color-muted);
}

/* ---------------------------------------------------------------------------
 * Tabs, accordion, segmented control
 * ------------------------------------------------------------------------- */

.tabs {
  display: grid;
  gap: var(--space-4);
}

.tabs__list {
  display: flex;
  gap: var(--space-1);
  overflow-x: auto;
  scrollbar-width: none;
  border-block-end: 1px solid var(--border-subtle);
}

.tabs__list::-webkit-scrollbar {
  display: none;
}

.tabs__tab {
  position: relative;
  flex: none;
  padding: 0.6rem 0.85rem;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--color-muted);
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--transition-fast, 120ms);
}

.tabs__tab::after {
  content: "";
  position: absolute;
  inset-inline: 0.5rem;
  inset-block-end: -1px;
  block-size: 2px;
  border-radius: 2px;
  background: var(--color-accent);
  transform: scaleX(0);
  transition: transform var(--transition-base, 180ms) var(--ease-out, ease);
}

.tabs__tab:hover {
  color: var(--color-text);
}

.tabs__tab[aria-selected="true"] {
  color: var(--color-text);
  font-weight: 600;
}

.tabs__tab[aria-selected="true"]::after {
  transform: scaleX(1);
}

.tabs__panel[hidden] {
  display: none;
}

.tabs__panel:focus-visible {
  outline: 2px solid var(--accent-ring);
  outline-offset: 4px;
  border-radius: var(--radius-sm, 7px);
}

/* --- accordion --- */

.accordion {
  display: grid;
  gap: var(--space-2);
}

.accordion__item {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius, 10px);
  background: var(--surface-1);
  overflow: hidden;
  transition: border-color var(--transition-base, 180ms);
}

.accordion__item[data-open="true"] {
  border-color: color-mix(in srgb, var(--color-accent) 32%, var(--border-subtle));
}

.accordion__trigger {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  inline-size: 100%;
  padding: 0.85rem 1rem;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 550;
  color: var(--color-text);
  text-align: start;
  cursor: pointer;
}

.accordion__trigger:hover {
  background: var(--surface-2);
}

.accordion__chevron {
  margin-inline-start: auto;
  flex: none;
  transition: transform var(--transition-base, 180ms) var(--ease-out, ease);
  color: var(--color-faint);
}

.accordion__item[data-open="true"] .accordion__chevron {
  transform: rotate(180deg);
}

.accordion__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--transition-base, 180ms) var(--ease-out, ease);
}

.accordion__item[data-open="true"] .accordion__panel {
  grid-template-rows: 1fr;
}

.accordion__panel > div {
  overflow: hidden;
}

.accordion__content {
  padding: 0 1rem 1rem;
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: var(--color-muted);
}

/* --- segmented control ---
 *
 * A sliding indicator rather than a per-item background, because with four or
 * five options the border jumping between them is the only motion some people
 * notice and none of them need. The indicator is one element that moves.
 */

.segmented {
  position: relative;
  display: inline-flex;
  padding: 3px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-pill, 999px);
  background: var(--surface-2);
}

.segmented__thumb {
  position: absolute;
  inset-block: 3px;
  border-radius: inherit;
  background: var(--surface-3);
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.3);
  transition:
    transform var(--transition-base, 180ms) var(--ease-out, ease),
    inline-size var(--transition-base, 180ms) var(--ease-out, ease);
  pointer-events: none;
}

.segmented__option {
  position: relative;
  z-index: 1;
  padding: 0.32rem 0.8rem;
  border: 0;
  border-radius: inherit;
  background: transparent;
  font: inherit;
  font-size: var(--text-xs);
  font-weight: 550;
  color: var(--color-muted);
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--transition-base, 180ms);
}

.segmented__option[aria-pressed="true"],
.segmented__option[aria-checked="true"] {
  color: var(--color-text);
  font-weight: 600;
}

.segmented__option:hover:not([aria-pressed="true"]):not([aria-checked="true"]) {
  color: var(--color-text);
}

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

.field {
  display: grid;
  gap: var(--space-2);
}

.field__label {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: 550;
  color: var(--color-text);
}

.field__required {
  color: var(--danger-text);
  font-weight: 600;
}

.field__hint {
  font-size: var(--text-xs);
  font-weight: 400;
  color: var(--color-faint);
}

.field__control {
  position: relative;
  display: flex;
  align-items: center;
}

.input,
.textarea,
.select {
  inline-size: 100%;
  padding: 0.55rem 0.75rem;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius, 10px);
  background: var(--surface-2);
  font: inherit;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--color-text);
  transition:
    border-color var(--transition-fast, 120ms),
    box-shadow var(--transition-fast, 120ms),
    background var(--transition-fast, 120ms);
}

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

.input:hover:not(:disabled),
.textarea:hover:not(:disabled),
.select:hover:not(:disabled) {
  border-color: var(--color-border);
}

/* Hover is not focus. A text field focused by keyboard needs the ring; one
   focused by a mouse does not, and drawing it anyway means every click puts a
   permanent ring on the field someone is about to type into. */
.input:focus,
.textarea:focus,
.select:focus {
  outline: none;
  border-color: color-mix(in srgb, var(--color-accent) 58%, var(--border-subtle));
  box-shadow: 0 0 0 3px var(--accent-ring);
  background: var(--surface-1);
}

.input:disabled,
.textarea:disabled,
.select:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  background: var(--surface-1);
}

.input[readonly] {
  background: var(--surface-1);
  color: var(--color-muted);
}

.textarea {
  min-block-size: 6rem;
  resize: vertical;
}

.select {
  appearance: none;
  padding-inline-end: 2rem;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 0.95rem center, right 0.65rem center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

/* --- validation ---
 *
 * The three states are distinguishable without colour, because roughly one man
 * in twelve cannot rely on the hue, and a red border is the single most common
 * way to fail them.
 *
 *   valid    a check glyph
 *   invalid  a warning glyph, plus text that says what to do
 *   neutral  neither
 */

.field[data-state="valid"] .input,
.field[data-state="valid"] .textarea,
.field[data-state="valid"] .select {
  border-color: color-mix(in srgb, var(--color-success) 52%, transparent);
}

.field[data-state="invalid"] .input,
.field[data-state="invalid"] .textarea,
.field[data-state="invalid"] .select {
  border-color: color-mix(in srgb, var(--color-danger) 58%, transparent);
}

.field[data-state="invalid"] .input:focus,
.field[data-state="invalid"] .textarea:focus,
.field[data-state="invalid"] .select:focus {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-danger) 22%, transparent);
}

.field__message {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  font-size: var(--text-xs);
  line-height: 1.45;
}

.field[data-state="invalid"] .field__message {
  color: var(--danger-text);
}

.field[data-state="valid"] .field__message {
  color: color-mix(in srgb, var(--color-success) 62%, var(--color-muted));
}

.field__message::before {
  flex: none;
  font-weight: 700;
}

.field[data-state="invalid"] .field__message::before {
  content: "!";
  display: grid;
  place-items: center;
  inline-size: 14px;
  block-size: 14px;
  margin-block-start: 0.1em;
  border-radius: 50%;
  background: var(--color-danger);
  color: var(--on-brand);
  font-size: 10px;
}

.field[data-state="valid"] .field__message::before {
  content: "\2713";
  color: var(--color-success);
}

/* The password toggle. Inside the control so the caret never lands on top of
   it, and `position: absolute` so it does not affect the input's width. */
.field__toggle {
  position: absolute;
  inset-inline-end: 0.35rem;
  display: grid;
  place-items: center;
  inline-size: 30px;
  block-size: 30px;
  border: 0;
  border-radius: var(--radius-sm, 7px);
  background: transparent;
  color: var(--color-faint);
  cursor: pointer;
  transition:
    background var(--transition-fast, 120ms),
    color var(--transition-fast, 120ms);
}

.field__toggle:hover {
  background: var(--surface-3);
  color: var(--color-text);
}

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

.input[data-has-toggle="true"] {
  padding-inline-end: 2.4rem;
}

/* --- checkbox and radio --- */

.checkbox,
.radio {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  cursor: pointer;
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--color-muted);
}

.checkbox input,
.radio input {
  flex: none;
  inline-size: 1.05rem;
  block-size: 1.05rem;
  margin-block-start: 0.15em;
  accent-color: var(--color-accent);
  cursor: pointer;
}

.checkbox:hover,
.radio:hover {
  color: var(--color-text);
}

.checkbox input:focus-visible,
.radio input:focus-visible {
  outline: 2px solid var(--accent-ring);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------------------
 * Stat cards
 *
 * The metric is the largest thing on the card, the label is the smallest, and
 * the delta is the only thing allowed to be coloured. A stat card where the
 * label is bigger than the number is a label with a number on it.
 * ------------------------------------------------------------------------- */

/*
 * Metric cards
 *
 * A stat card is a number with a label. This is the rest of it: the trend, the
 * state, the way through to the detail, and the two states that are not a number
 * at all.
 *
 * ## Why "unavailable" is a first-class state
 *
 * A metric card's whole value is the number. A card that renders `0` when the
 * measurement was never taken is a confident lie, and it is the most common way
 * this component gets built: the template writes `${value ?? 0}` and every panel
 * on the page is suddenly authoritative about something nobody measured.
 *
 * So there are three states and they are not interchangeable:
 *
 *   a number    rendered, with its trend if the data has one
 *   absent      "--", labelled "not recorded" — and never a zero
 *   unknown    "--", labelled why it is unknown — and never a guess
 *
 * The difference between the last two is the difference between "this was zero"
 * and "nobody has looked", which a reader cannot tell apart from a number.
 *
 * ## Why the sparkline is drawn, not implied
 *
 * A trend arrow says "up" and a reader has to guess by how much, whether it was
 * steady, and whether one spike made it look like a trend. Twelve points say all
 * three without a second number to interpret.
 *
 * It is an inline SVG rather than a chart library because it is twelve points
 * and no axes: anything heavier is a second design system to keep consistent.
 * ========================================================================== */

.stat {
  position: relative;
  display: grid;
  grid-template-rows: auto auto 1fr;
  gap: var(--space-2);
  align-content: start;
  padding: var(--space-5);
  border-radius: var(--radius-lg, 14px);
  background: var(--elev-2-surface);
  transition:
    border-color var(--transition-base),
    box-shadow var(--transition-base),
    transform var(--transition-base);
}

/* The edge — the border and the shadow — is the grouped rule's, further down
 * this sheet. It is not repeated here: a second copy of it is a second
 * answer, and the one that is consulted last wins. */

/* --- the top row: label and status --------------------------------------- */

.stat__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
}

.stat__label {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-faint);
  text-wrap: balance;
}

/* --- the number ---------------------------------------------------------- */

.stat__value {
  font-size: clamp(1.75rem, 1.2rem + 2vw, 2.5rem);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.025em;
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
}

.stat__unit {
  font-size: 0.5em;
  font-weight: 600;
  color: var(--color-muted);
  margin-inline-start: 0.2em;
}

/* Absent and unknown render the same mark, because the reader's first question
 * is the same either way: is there a number here? The *reason* differs, and it
 * lives in the footnote rather than in the headline. */
.stat__value[data-absent="true"] {
  color: var(--navy-400);
  font-weight: 600;
}

/* --- the footnote: trend, sample size, or why there is no number ---------- */

.stat__foot {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  margin-block-start: var(--space-1);
  font-size: var(--text-xs);
  color: var(--color-faint);
  min-block-size: 1.25rem;
}

.stat__delta {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Direction is carried by an arrow as well as by colour, because a red/green
 * pair is the classic way to fail a reader who cannot rely on hue — and a
 * dashboard is exactly where that reader is looking hardest. */
.stat__delta--up { color: var(--success-text, #0F5E31); }
.stat__delta--down { color: var(--danger-text, #92180F); }
.stat__delta--flat { color: var(--color-faint); }

.stat__delta-icon {
  inline-size: 0.85em;
  block-size: 0.85em;
  flex: none;
}

.stat__sample {
  font-variant-numeric: tabular-nums;
  color: var(--color-faint);
}

/* --- the sparkline ------------------------------------------------------- */

/* `preserveAspectRatio: none` so the path fills its box at any card width, and
 * `vector-effect: non-scaling-stroke` so the line stays 1.5px when the path is
 * stretched. Without the second one a wider card gets a thicker line, which
 * reads as emphasis that was never intended. */
.stat__spark {
  display: block;
  inline-size: 100%;
  block-size: 2.25rem;
  margin-block-start: var(--space-2);
  overflow: visible;
}

.stat__spark-line {
  fill: none;
  stroke: var(--sky-500);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

/* The area under the line, at an alpha low enough that the number above stays
 * the loudest thing on the card. */
.stat__spark-area {
  fill: var(--accent-softer);
  stroke: none;
}

/* The last point is the one the reader looks at, so it is the one that gets a
   marker -- and it is the only one. A dot on every point is a dotted line. */
.stat__spark-dot {
  fill: var(--sky-500);
  stroke: var(--brand-surface);
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
}

/* --- status -------------------------------------------------------------- */

.stat__status {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  flex: none;
  padding: 0.15rem 0.45rem;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-pill, 999px);
  background: var(--surface-2);
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1.5;
  white-space: nowrap;
}

.stat__status-dot {
  inline-size: 6px;
  block-size: 6px;
  flex: none;
  border-radius: 50%;
  background: currentColor;
}

.stat__status--ok { color: var(--success-text, #0F5E31); border-color: color-mix(in srgb, #12793F 34%, transparent); }
.stat__status--warn { color: var(--warning-text, #6E4200); border-color: color-mix(in srgb, #8A5200 34%, transparent); }
.stat__status--bad { color: var(--danger-text, #92180F); border-color: color-mix(in srgb, #B42318 34%, transparent); }
.stat__status--unknown { color: var(--color-faint); }

.stat__status--warn .stat__status-dot { animation: status-pulse 2.6s var(--ease-out) infinite; }
.stat__status--bad .stat__status-dot { animation: status-pulse 1.4s var(--ease-out) infinite; }

/* --- the way through ----------------------------------------------------- */

/* Only rendered when there is somewhere to go. A card that links nowhere is a
   card that looks clickable and is not. */
.stat__action {
  position: absolute;
  inset-block-start: var(--space-3);
  inset-inline-end: var(--space-3);
  display: grid;
  place-items: center;
  inline-size: 26px;
  block-size: 26px;
  border-radius: var(--radius-sm, 7px);
  color: var(--color-faint);
  text-decoration: none;
  opacity: 0;
  transition:
    opacity var(--transition-fast),
    background var(--transition-fast),
    color var(--transition-fast);
}

.stat:hover .stat__action,
.stat__action:focus-visible {
  opacity: 1;
}

.stat__action:hover {
  background: var(--hover-fill-strong);
  color: var(--color-text);
}

/* Opacity on hover only — a permanently visible arrow on every card in a row is
 * a row of arrows pointing nowhere in particular. */
@media (hover: none) {
  .stat__action { opacity: 1; }
}

/* --- the clickable card -------------------------------------------------- */

.stat--interactive {
  cursor: pointer;
}

.stat--interactive:hover {
  transform: translateY(-2px);
  border-color: var(--border-accent);
  box-shadow: var(--shadow-brand);
}

/* --- the loading variant -------------------------------------------------
 *
 * The skeleton occupies the same box as the number it replaces, so the card does
 * not change height when the data arrives. A card that grows on load moves every
 * card below it, which is the one thing a loading placeholder exists to prevent.
 */

.stat[data-state="loading"] .stat__value,
.stat[data-state="loading"] .stat__spark {
  visibility: hidden;
}

.stat[data-state="loading"]::after {
  content: "";
  grid-row: 2;
  block-size: 2.5rem;
  border-radius: var(--radius-sm, 7px);
  background: var(--surface-3);
}

.stat[data-state="loading"] .stat__foot::after {
  content: "";
  inline-size: 5rem;
  block-size: 0.65rem;
  border-radius: 4px;
  background: var(--surface-3);
}

/* --- reduced motion ------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .stat__status--warn .stat__status-dot,
  .stat__status--bad .stat__status-dot {
    animation: none;
  }

  .stat,
  .stat__action,
  .stat--interactive {
    transition: none;
  }

  .stat--interactive:hover {
    transform: none;
  }
}

/* ---------------------------------------------------------------------------
 * Table shell
 *
 * The table itself is plain — sort, filter and pagination are in
 * `modules/table.ts` and this is the shape they assume. The overflow wrapper is
 * the part worth having: a table on a phone either scrolls inside its own
 * container or pushes the whole page sideways, and the second one is why a
 * website "breaks" on mobile.
 * ------------------------------------------------------------------------- */

.table-wrap {
  position: relative;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg, 14px);
  background: var(--surface-1);
  -webkit-overflow-scrolling: touch;
}

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

.table caption {
  padding: var(--space-4) var(--space-4) 0;
  font-size: var(--text-xs);
  color: var(--color-faint);
  text-align: start;
}

.table th,
.table td {
  padding: 0.7rem 1rem;
  text-align: start;
  border-block-end: 1px solid var(--border-subtle);
  white-space: nowrap;
}

.table th {
  position: sticky;
  inset-block-start: 0;
  z-index: 1;
  background: var(--surface-2);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-faint);
}

.table tbody tr {
  transition: background var(--transition-fast, 120ms);
}

.table tbody tr:hover {
  background: var(--surface-2);
}

.table tbody tr:last-child td {
  border-block-end: 0;
}

.table td[data-numeric="true"],
.table th[data-numeric="true"] {
  text-align: end;
  font-variant-numeric: tabular-nums;
}

/* The sortable header is a button so it is focusable and announces its state. */
.table__sort {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  border: 0;
  padding: 0;
  background: transparent;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  cursor: pointer;
}

.table__sort:hover {
  color: var(--color-text);
}

.table__sort:focus-visible {
  outline: 2px solid var(--accent-ring);
  outline-offset: 3px;
  border-radius: 4px;
}

.table__sort-icon {
  opacity: 0.35;
  transition:
    opacity var(--transition-fast, 120ms),
    transform var(--transition-base, 180ms);
}

.table__sort[aria-sort="ascending"] .table__sort-icon {
  opacity: 1;
  color: var(--color-accent);
}

.table__sort[aria-sort="descending"] .table__sort-icon {
  opacity: 1;
  color: var(--color-accent);
  transform: rotate(180deg);
}

/* --- the toolbar and the footer of a data block --- */

.data-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-block-end: var(--space-3);
}

.data-toolbar__search {
  flex: 1 1 14rem;
  min-inline-size: 0;
}

.data-toolbar__count {
  margin-inline-start: auto;
  font-size: var(--text-xs);
  color: var(--color-faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.data-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-block-start: 1px solid var(--border-subtle);
  background: var(--surface-1);
  font-size: var(--text-xs);
  color: var(--color-muted);
}

.pagination {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.pagination__button {
  display: grid;
  place-items: center;
  min-inline-size: 30px;
  block-size: 30px;
  padding-inline: 0.4rem;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm, 7px);
  background: var(--surface-2);
  font: inherit;
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--color-muted);
  cursor: pointer;
  transition:
    background var(--transition-fast, 120ms),
    color var(--transition-fast, 120ms),
    border-color var(--transition-fast, 120ms);
}

.pagination__button:hover:not(:disabled) {
  background: var(--surface-3);
  color: var(--color-text);
}

.pagination__button[aria-current="page"] {
  border-color: color-mix(in srgb, var(--color-accent) 50%, transparent);
  background: color-mix(in srgb, var(--color-accent) 16%, transparent);
  color: var(--color-text);
  font-weight: 650;
}

.pagination__button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.pagination__gap {
  min-inline-size: 20px;
  text-align: center;
  color: var(--color-faint);
}

/* --- skeleton rows ---
 *
 * The row heights match the real rows exactly. A skeleton table whose rows are a
 * different height from the loaded table is a layout shift — the content moves
 * under the reader the moment it arrives, which is the one thing a loading
 * placeholder exists to prevent.
 */

.table tbody tr.skeleton-row td {
  block-size: 45px;
  padding-block: 0;
}

.skeleton-cell {
  display: block;
  block-size: 11px;
  border-radius: 4px;
}

/* ---------------------------------------------------------------------------
 * Inline alerts
 *
 * `toast()` and `alert()` exist in dom.ts and cover the transient and the
 * prominent case. This is the third shape: a message that belongs *in* the flow,
 * above the thing it is about, and stays until it is dealt with.
 * ------------------------------------------------------------------------- */

.notice {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4);
  border: 1px solid var(--border-subtle);
  border-inline-start: 3px solid var(--color-muted);
  border-radius: var(--radius, 10px);
  background: var(--surface-2);
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
}

.notice__icon {
  flex: none;
  margin-block-start: 0.15em;
  color: var(--color-muted);
}

.notice__title {
  margin: 0 0 var(--space-1);
  font-weight: 650;
  color: var(--color-text);
}

.notice__body {
  color: var(--color-muted);
}

.notice__body > :first-child { margin-block-start: 0; }
.notice__body > :last-child { margin-block-end: 0; }

.notice__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-block-start: var(--space-3);
}

.notice--info {
  border-inline-start-color: var(--color-info);
  background: color-mix(in srgb, var(--color-info) 8%, var(--surface-2));
}

.notice--success {
  border-inline-start-color: var(--color-success);
  background: color-mix(in srgb, var(--color-success) 8%, var(--surface-2));
}

.notice--warning {
  border-inline-start-color: var(--color-warning);
  background: color-mix(in srgb, var(--color-warning) 9%, var(--surface-2));
}

.notice--danger {
  border-inline-start-color: var(--color-danger);
  background: color-mix(in srgb, var(--color-danger) 9%, var(--surface-2));
}

/* ---------------------------------------------------------------------------
 * System states that chrome.css does not have
 *
 * chrome.css has `.state--empty`, `--error` and `--offline`. These are the three
 * it does not: the session ended, the account may not do this, and a request is
 * in flight with something to cancel.
 * ------------------------------------------------------------------------- */

.state--session .state__icon { color: var(--warning-text); }
.state--denied .state__icon { color: var(--danger-text); }

/* --- progress ---
 *
 * Indeterminate, because the operations these appear beside — a Lua run, a
 * transcript export, a Discord permission sync — have no knowable denominator.
 * A determinate bar with an invented percentage is worse than a sweep, because
 * a reader can tell the difference and stops believing the rest.
 */

.progress {
  position: relative;
  block-size: 4px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
}

.progress__bar {
  position: absolute;
  inset-block: 0;
  inline-size: 35%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--color-accent), #4AA3DF);
  animation: progress-slide 1.15s var(--ease-in-out, ease-in-out) infinite;
}

@keyframes progress-slide {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(320%); }
}

.progress__bar--stuck {
  animation: none;
  inline-size: 100%;
  transform: none;
  background: var(--color-warning);
}

.progress-label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-block-end: var(--space-2);
  font-size: var(--text-xs);
  color: var(--color-muted);
}

/* ---------------------------------------------------------------------------
 * Breadcrumbs
 * ------------------------------------------------------------------------- */

.breadcrumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
  margin-block-end: var(--space-4);
  font-size: var(--text-xs);
  color: var(--color-faint);
}

.breadcrumbs__item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--color-faint);
  text-decoration: none;
  border-radius: 4px;
}

.breadcrumbs__item:hover {
  color: var(--color-text);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.breadcrumbs__item[aria-current="page"] {
  color: var(--color-muted);
  font-weight: 550;
  cursor: default;
}

.breadcrumbs__item[aria-current="page"]:hover {
  text-decoration: none;
}

.breadcrumbs__sep {
  color: var(--color-faint);
  opacity: 0.55;
  user-select: none;
}

/* ---------------------------------------------------------------------------
 * Charts
 *
 * A placeholder, deliberately. There is no analytics data on this site to chart,
 * and a chart component with invented numbers would be exactly the fake the
 * status pill was. What exists is the *shape* — loading, legend, tooltip,
 * responsive — so that a page with real data has somewhere to put it.
 * ------------------------------------------------------------------------- */

.chart {
  display: grid;
  gap: var(--space-4);
  padding: var(--space-5);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg, 14px);
  background: var(--surface-1);
}

.chart__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.chart__title {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 650;
  color: var(--color-text);
}

.chart__plot {
  position: relative;
  block-size: clamp(9rem, 22vh, 16rem);
}

.chart__legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  font-size: var(--text-xs);
  color: var(--color-muted);
}

.chart__legend-item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.chart__swatch {
  inline-size: 9px;
  block-size: 9px;
  flex: none;
  border-radius: 2px;
  background: var(--swatch, var(--color-accent));
}

/* ---------------------------------------------------------------------------
 * Command palette
 * ------------------------------------------------------------------------- */

.palette__search {
  inline-size: 100%;
  padding: 0.7rem 0.9rem;
  border: 0;
  border-block-end: 1px solid var(--border-subtle);
  background: transparent;
  font: inherit;
  font-size: var(--text-base);
  color: var(--color-text);
}

.palette__search:focus {
  outline: none;
}

.palette__results {
  max-block-size: min(26rem, 60vh);
  overflow: auto;
  padding: var(--space-1);
}

.palette__empty {
  padding: var(--space-6) var(--space-4);
  font-size: var(--text-sm);
  color: var(--color-muted);
  text-align: center;
}

/* The selected row is scrolled into view as the selection moves with the
   arrow keys, because a palette that scrolls its own results off-screen while
   the highlight moves is disorienting. */
.palette__item[aria-selected="true"] {
  background: color-mix(in srgb, var(--color-accent) 18%, transparent);
  color: var(--color-text);
}

.palette__footer {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-block-start: 1px solid var(--border-subtle);
  background: var(--surface-1);
  font-size: var(--text-xs);
  color: var(--color-faint);
}

/* ---------------------------------------------------------------------------
 * Scrollbars
 *
 * WebKit-only in practice. Firefox takes `scrollbar-width` and
 * `scrollbar-color`, which is two lines and no pseudo-elements, so both are set
 * — half the browsers get the styled one and the rest get the system one rather
 * than an unstyled gap.
 * ------------------------------------------------------------------------- */

* {
  scrollbar-width: thin;
  scrollbar-color: var(--color-border, #2a3550) transparent;
}

::-webkit-scrollbar {
  inline-size: 10px;
  block-size: 10px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  border: 3px solid transparent;
  border-radius: 999px;
  background: var(--color-border, #2a3550);
  background-clip: padding-box;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--color-faint, #5c6a8a);
  background-clip: padding-box;
}

/* ---------------------------------------------------------------------------
 * Focus
 *
 * One ring, everywhere. Written once and applied through `:focus-visible` so a
 * mouse click does not leave a ring behind, with `:focus` handled separately
 * for the cases where the element is not a text field and the browser's own
 * default is not enough.
 * ------------------------------------------------------------------------- */

:where(a, button, input, textarea, select, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--accent-ring);
  outline-offset: 2px;
  border-radius: var(--radius-sm, 7px);
}

:where([tabindex="-1"]):focus {
  outline: none;
}

:where([tabindex="-1"]):focus-visible {
  outline: 2px solid var(--accent-ring);
  outline-offset: 2px;
}

/* A keyboard user needs to know where they are on a page, so the skip link
   target gets a ring on focus rather than only a scroll. */
:focus-within {
  --ring-hint: 1;
}

/* ---------------------------------------------------------------------------
 * Page transitions
 *
 * A short fade-and-rise on the main content once, on first paint of a page.
 * Not on every route change: a site that animates its main element every time
 * someone clicks a link reads as sluggish, and the motion carries no information.
 * ------------------------------------------------------------------------- */

.page-main {
  animation: page-enter 320ms var(--ease-out, ease) both;
}

@keyframes page-enter {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* ---------------------------------------------------------------------------
 * Reduced motion, by name
 *
 * base.css collapses durations globally. That is not enough on its own: an
 * infinite animation with a collapsed duration still composites forever, which
 * costs the reader a frame budget to produce motion they have asked not to see.
 * Every looping animation in this sheet is therefore stopped here by name.
 * ------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .page-main {
    animation: none;
  }

  .progress__bar,
  .skeleton::after,
  .status-pill__dot,
  [data-status-pill][data-state="loading"] .status-pill__dot {
    animation: none !important;
  }

  .progress__bar {
    inline-size: 100%;
    transform: none;
    opacity: 0.5;
  }

  .dialog,
  .dialog--drawer,
  .menu,
  .tooltip,
  .accordion__panel,
  .segmented__thumb,
  .table__sort-icon {
    transition: none;
  }
}
/* ---------------------------------------------------------------------------
 * Command palette trigger
 *
 * The discoverable half of a keyboard shortcut.
 * ------------------------------------------------------------------------- */

.palette-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.35rem 0.5rem 0.35rem 0.55rem;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm, 7px);
  background: var(--surface-2);
  font: inherit;
  font-size: var(--text-xs);
  color: var(--color-muted);
  cursor: pointer;
  transition:
    border-color var(--transition-fast),
    background var(--transition-fast),
    color var(--transition-fast);
}

.palette-trigger:hover {
  border-color: var(--color-accent);
  background: var(--surface-3);
  color: var(--color-text);
}

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

.palette-trigger__label {
  font-weight: 500;
}

/* The chip teaches the shortcut to whoever already found the button.
 *
 * `aria-hidden`: the button's own label carries the meaning, and announcing
 * "Ctrl K" twice is noise. The chip is decoration for the pointer. */
.palette-trigger__key {
  padding: 0.1rem 0.3rem;
  border: 1px solid var(--border-subtle);
  border-radius: 4px;
  background: var(--surface-1);
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.9em;
  color: var(--color-faint);
  white-space: nowrap;
}

/* Below the header's own breakpoint the label and chip cost more width than a
   phone has. The icon carries the accessible name at every width, so nothing is
   lost -- only the redundancy. */
@media (max-width: 900px) {
  .palette-trigger {
    padding-inline: 0.55rem;
  }

  .palette-trigger__label,
  .palette-trigger__key {
    display: none;
  }
}

/* ---------------------------------------------------------------------------
 * Elevation, applied
 *
 * The scale is declared in `base.css`. This is where it earns its keep: each
 * component reads the tier for the height it actually sits at, so a card and a
 * panel cannot drift apart and a dropdown cannot be mistaken for a card.
 *
 * Declaring a scale nobody reads is a palette with better formatting.
 * ------------------------------------------------------------------------- */

/* --- tier 1: cards and panels ------------------------------------------- */

/* The edge of every raised surface.
 *
 * One rule, because there is one answer: a card, a panel and a stat tile are
 * the same kind of thing — a surface raised one step above the page — and
 * they should not differ by one shadow.
 *
 * It was the smallest step in the scale, and it came after every other sheet,
 * so a card declared at a higher elevation elsewhere was quietly put back. The
 * order of the bundle decided it, not the design.
 *
 * The ring goes with the shadow: a shadow alone describes the light source,
 * and the ring is what tells the reader where the surface ends. */
.card,
.panel,
.stat {
  border: var(--border-weight-hairline) solid var(--border-subtle);
  box-shadow: var(--elev-2-shadow), var(--elev-2-ring);
}

/* The footer's strip and the table's wrapper sit *inside* a surface, so they
 * stay one step below it. Their own rules are in the sheet that owns them. */

/* Hover lifts a card one step and tints the shadow with the brand.
 *
 * Reserved for hover on purpose: a coloured shadow at rest would make every card
 * in a grid look highlighted, and the highlight would stop meaning anything. */
.card--interactive:hover,
.card--interactive:focus-visible,
.card--interactive:focus-within {
  box-shadow: var(--shadow-brand), var(--elev-3-ring);
}

/* --- tier 2: things that float above the page --------------------------- */

.menu,
.tooltip,
.dropdown,
.palette__results {
  box-shadow: var(--elev-2-shadow);
}

/* --- tier 3: dialogs ---------------------------------------------------- */

.dialog {
  box-shadow: var(--elev-3-shadow);
}

.dialog--drawer {
  box-shadow: var(--elev-3-shadow);
}

/* --- the one ring, everywhere --------------------------------------------
 *
 * Declared once so it cannot drift between a button on one page and a link on
 * another. The drift is invisible until somebody tabs through both and finds the
 * ring changes, which is not a thing anyone reports.
 * ---------------------------------------------------------------------- */

:where(a, button, input, textarea, select, summary, [tabindex]):focus-visible {
  outline: var(--border-weight-thick) solid var(--accent-500);
  outline-offset: var(--focus-ring-offset);
  border-radius: var(--radius-sm, 7px);
}

/* The tint behind a text field's ring, which is a fill rather than an outline
 * because a hard outline drawn inside a field collides with its text. */
.input:focus,
.textarea:focus,
.select:focus {
  border-color: var(--sky-500);
  box-shadow: var(--focus-ring);
}

/* --- interaction fills --------------------------------------------------- */

.table tbody tr:hover {
  background: var(--hover-fill);
}

.tabular,
.pagination__button:hover:not(:disabled),
.segmented__option:hover,
.stat:hover {
  background: var(--hover-fill);
}

.segmented__option:hover:not([aria-pressed="true"]):not([aria-checked="true"]) {
  color: var(--color-text);
}

.menu__item:hover,
.menu__item:focus-visible,
.accordion__trigger:hover {
  background: var(--hover-fill-strong);
  color: var(--color-text);
}

.menu__item[aria-current="true"],
.menu__item[aria-selected="true"] {
  background: var(--accent-softer);
  color: var(--color-text);
}

/* --- borders by role ----------------------------------------------------- */

.card,
.panel,
.field__control .input,
.table-wrap {
  border-color: var(--border-subtle);
}

.card--interactive:hover,
.card--interactive:focus-visible {
  border-color: var(--border-accent);
}

.status-pill,
.badge {
  border-color: var(--border-default);
}

.accent-edge {
  border-inline-start: var(--border-weight-thick) solid var(--sky-500);
}

/* --- pages.css --- */
/* ==========================================================================
 * Page standards
 *
 * One block per page type, each against the standard set for that page rather
 * than against what its markup happened to grow.
 *
 * The organising idea is the same everywhere: **the page's primary action is
 * above the fold and reads as primary**, and everything else is arranged around
 * that rather than around the order the data arrived in.
 *
 * Every rule here reads a token. A page standard that hard-codes a colour is a
 * page standard that drifts the first time the brand changes, which is what
 * happened to the buttons.
 * ========================================================================== */

/* ==========================================================================
 * The section head
 *
 * A page is a sequence of sections, and a bare heading gives the reader no
 * landmark in it — the eye is never told that the material has changed.
 *
 * So a section is introduced by three things: an eyebrow that says what kind
 * of thing this is, a heading that says what it is about, and a lede that
 * says why it matters. The measure is capped so the three of them read as one
 * block rather than as a column of text.
 * ========================================================================== */

.section-head {
  max-width: 62ch;
  margin-inline: auto;
  margin-block-end: var(--space-6);
  text-align: center;
}

.section-head .eyebrow {
  margin-block-end: var(--space-3);
}

/* The page head.
 *
 * The content pages opened with an h1 and a lede at the same size as the body
 * copy around them, so the title of a page and the paragraph under it had the
 * same visual weight and the page had no top.
 *
 * The block is capped at the prose measure so it reads as an introduction
 * rather than as a column of text. */
.page-head {
  max-width: 62ch;
  margin-block-end: var(--space-6);
}

.page-head h1 {
  margin: 0;
  font-size: var(--text-display-sm);
  font-weight: 780;
  line-height: 1.08;
  letter-spacing: -0.03em;
  color: var(--navy-900);
  text-wrap: balance;
}

/* Any paragraph, not only.lede.
 *
 * The generated pages use.lede and the hand-written ones use.text-muted for
 * the same thing. A rule that knows one class name works on half the site. */
.page-head > p {
  margin-block-start: var(--space-3);
  font-size: var(--text-lg);
  line-height: var(--leading-relaxed);
  color: var(--color-muted);
  text-wrap: pretty;
}

.section-head__title {
  margin: 0;
  font-size: var(--text-display-sm);
  font-weight: 780;
  line-height: 1.08;
  letter-spacing: -0.03em;
  color: var(--navy-900);
  text-wrap: balance;
}

.section-head__lede {
  margin: var(--space-3) 0 0;
  font-size: var(--text-lg);
  line-height: var(--leading-relaxed);
  color: var(--color-muted);
  text-wrap: pretty;
}

/* ==========================================================================
 * Documentation
 *
 * The heading is left-aligned. A documentation page's title belongs at the
 * left of the column it titles; centred in the middle of it, it reads as a
 * poster rather than as a heading.
 * ========================================================================== */

.docs__body .page-head {
  text-align: left;
  margin-inline: 0;
}

.docs__body .page-head > p {
  max-width: 60ch;
}

 * The reading experience is the product. A documentation page that is
 * beautiful and unreadable has failed at the only thing it is for.
 * ========================================================================== */

/* --- the reading column ---
 *
 * The element the progress bar measures and the grid's content column. It is
 * named in the markup precisely because it has a job that no other element here
 * can do.
 *
 * The job is `min-inline-size: 0`, and it is not optional. A grid item defaults
 * to `min-inline-size: auto`, which means it refuses to shrink below its
 * content — and this column's content includes code blocks that are wider than
 * the column. So the column stays as wide as the widest code block, the grid
 * column widens with it, and a code block with one long line pushes the whole
 * page sideways. The horizontal scrollbar has to be inside the block, and this
 * is what lets it be.
 */

.docs__body {
  min-inline-size: 0;
}

.docs__section {
  position: relative;
  max-inline-size: 72ch;
}

/* --- reading progress ---
 *
 * A thin rule under the sticky header showing how far through the page the
 * reader is. It answers "how much is left" without a number and without the
 * reader scrolling to find out.
 *
 * `transform: scaleX` rather than `width`, so it animates on the compositor and
 * forces no layout on every scroll frame — which is the difference between a
 * progress bar and a scroll handler that makes the page feel heavy.
 */

.docs__progress {
  position: sticky;
  inset-block-start: var(--docs-header-height, 64px);
  z-index: 30;
  block-size: 2px;
  margin-block-end: var(--space-4);
  background: var(--border-subtle);
  border-radius: 999px;
  overflow: hidden;
}

.docs__progress-bar {
  block-size: 100%;
  inline-size: 100%;
  border-radius: inherit;
  background: var(--accent-gradient);
  transform: scaleX(var(--docs-progress, 0));
  transform-origin: left center;
  transition: transform 90ms linear;
}

/* --- code blocks ---
 *
 * Code is the one thing on the page that cannot be reflowed to a comfortable
 * measure: a wrapped URL or a shell command broken across two lines is harder to
 * read than either the long line or the broken one.
 *
 * So a code block is wider than the prose, scrolls on its own, and has a sticky
 * header carrying the language and a copy control. The horizontal scrollbar is
 * inside the block, not on the page — a table or a code block that pushes the
 * whole document sideways is why a site "breaks" on a phone.
 */

.docs__code {
  position: relative;
  margin-block: var(--space-5);
  border: var(--border-weight-hairline) solid var(--border-subtle);
  border-radius: var(--radius, 10px);
  background: var(--navy-900);
  box-shadow: var(--elev-2-shadow);
  overflow: hidden;
  max-inline-size: none;
}

.docs__code-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  padding-inline-start: var(--space-4);
  background: var(--navy-800);
  border-block-end: var(--border-weight-hairline) solid rgba(255, 255, 255, 0.08);
}

.docs__code-lang {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--sky-300);
}

.docs__code-copy {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.2rem 0.5rem;
  border: var(--border-weight-hairline) solid rgba(255, 255, 255, 0.14);
  border-radius: var(--radius-sm, 7px);
  background: transparent;
  font: inherit;
  font-size: var(--text-xs);
  color: var(--sky-200);
  cursor: pointer;
  transition:
    background var(--transition-fast),
    border-color var(--transition-fast);
}

.docs__code-copy:hover {
  background: rgba(255, 255, 255, 0.08);
  border-color: rgba(255, 255, 255, 0.24);
}

.docs__code-copy:focus-visible {
  outline: var(--border-weight-thick) solid var(--sky-400);
  outline-offset: var(--focus-ring-offset);
}

/* The confirmation is a change of label, not a toast: the button is already
   where the eye is, and a toast three seconds later answers a question nobody
   remembers asking. */
.docs__code-copy[data-copied="true"] {
  border-color: var(--sky-400);
  color: var(--sky-200);
}

.docs__code pre {
  margin: 0;
  padding: var(--space-4);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--navy-100);
  tab-size: 2;
}

/* Line numbers are decorative: announced by a screen reader as a list of the
   first few characters of every line, which is noise. */
.docs__code pre code {
  display: block;
  min-inline-size: max-content;
}

/* An inline code span is a different object from a block and gets a different
   treatment: a tint, not a panel. */
.docs__section :not(pre) > code {
  padding: 0.1em 0.35em;
  border: var(--border-weight-hairline) solid var(--border-subtle);
  border-radius: 5px;
  background: var(--sky-50);
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.9em;
  color: var(--navy-800);
}

/* --- lifecycle flow ---
 *
 * The command lifecycle was a `<pre>` of arrows. Arrows do not wrap, which is why
 * it was a `<pre>`, and being a `<pre>` it got code's treatment — a dark panel,
 * a language label, and a copy button that copies a run of arrows.
 *
 * It is a sequence of stages, so it is a list. A list wraps, so it survives a
 * narrow phone; a numbered list is also the one form that says "this order is
 * the point" without a sentence saying it.
 *
 * The counter is drawn rather than left to the marker: the numbers are part of
 * the meaning here, and the markers hang too far into the margin to read as a
 * sequence at this size.
 */

.docs__flow {
  display: grid;
  gap: var(--space-2);
  margin-block: var(--space-5);
  padding: 0;
  list-style: none;
  counter-reset: stage;
}

.docs__flow li {
  display: grid;
  grid-template-columns: 1.5rem 1fr;
  align-items: center;
  gap: var(--space-3);
  counter-increment: stage;
}

.docs__flow li::before {
  content: counter(stage);
  display: grid;
  place-items: center;
  inline-size: 1.5rem;
  block-size: 1.5rem;
  border-radius: 50%;
  background: var(--accent-softer);
  font-size: var(--text-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--sky-700);
}

/* The last stage is where the command ends up, so it is the one worth landing
   the eye on -- the same reasoning as the featured plan on pricing. */
.docs__flow li:last-child {
  font-weight: 650;
  color: var(--color-text);
}

.docs__flow li:last-child::before {
  background: var(--accent-gradient);
  color: var(--navy-900);
}

/* --- section anchors --- */

.docs__section h2,
.docs__section h3 {
  position: relative;
  scroll-margin-block-start: calc(var(--docs-header-height, 64px) + var(--space-5));
}

.docs__anchor {
  margin-inline-start: var(--space-2);
  color: var(--navy-400);
  text-decoration: none;
  opacity: 0;
  transition:
    opacity var(--transition-fast),
    color var(--transition-fast);
}

.docs__section h2:hover .docs__anchor,
.docs__section h3:hover .docs__anchor,
.docs__anchor:focus-visible {
  opacity: 1;
}

.docs__anchor:hover {
  color: var(--sky-500);
}

.docs__anchor:focus-visible {
  outline: var(--border-weight-thick) solid var(--accent-500);
  outline-offset: var(--focus-ring-offset);
  border-radius: 4px;
}

/* Anchor links that are only visible on hover are unreachable by keyboard until
   focused, which is handled above -- but not to a reader who is tabbing through
   a long page and does not know one is coming. So the rule includes focus. */

/* ==========================================================================
 * Commands
 *
 * Search-first. The registry has 421 commands, so the search is the interface and
 * the list is the result.
 * ========================================================================== */

.commands__layout {
  display: grid;
  gap: var(--space-6);
}

/* The search field is the largest thing on the page, which is the correct
   hierarchy when the list is too long to scan. */
.commands__search {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-5);
  border: var(--border-weight-hairline) solid var(--border-subtle);
  border-radius: var(--radius-lg, 14px);
  background: var(--elev-2-surface);
  box-shadow: var(--elev-2-shadow), var(--elev-2-ring);
}

.commands__search-input {
  inline-size: 100%;
  padding: var(--space-3) var(--space-4);
  border: var(--border-weight-hairline) solid var(--border-subtle);
  border-radius: var(--radius, 10px);
  background: var(--brand-surface);
  font: inherit;
  font-size: var(--text-lg);
  color: var(--color-text);
  transition:
    border-color var(--transition-fast),
    box-shadow var(--transition-fast);
}

.commands__search-input::placeholder {
  color: var(--navy-400);
}

.commands__search-input:focus {
  outline: none;
  border-color: var(--sky-500);
  box-shadow: var(--focus-ring);
}

/* --- category chips ---
 *
 * A select asks the reader to know which of fifteen categories they want. A chip
 * row shows all fifteen at once and is one click either way, and it can be
 * cleared — a select has to be opened before it can be emptied.
 */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-block-start: var(--space-3);
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.25rem 0.65rem;
  border: var(--border-weight-hairline) solid var(--border-subtle);
  border-radius: var(--radius-pill, 999px);
  background: var(--brand-surface);
  font: inherit;
  font-size: var(--text-xs);
  font-weight: 550;
  color: var(--color-muted);
  cursor: pointer;
  transition:
    background var(--transition-fast),
    border-color var(--transition-fast),
    color var(--transition-fast);
}

.chip:hover {
  border-color: var(--border-accent);
  background: var(--hover-fill);
  color: var(--color-text);
}

/* The active chip is filled, not tinted.
 *
 * A tinted chip reads as "selected" only if the reader already knows what a
 * tint means. A filled one reads as selected to anyone, and the brand's fill
 * is what the rest of the site uses for the thing that is chosen. */
.chip[aria-pressed="true"] {
  border-color: transparent;
  background: var(--btn-primary-fill);
  color: #FFFFFF;
  box-shadow: var(--shadow-brand);
}

.chip:focus-visible {
  outline: var(--border-weight-thick) solid var(--accent-500);
  outline-offset: var(--focus-ring-offset);
}

.chip__count {
  font-variant-numeric: tabular-nums;
  color: var(--navy-400);
}

.chip[aria-pressed="true"] .chip__count {
  color: rgba(255, 255, 255, 0.76);
}

/* --- compact result rows ---
 *
 * A command row is four short fields and a name. Anything larger than a row is
 * a card, and a list of 421 cards is a list nobody scrolls.
 */

.command-card {
  display: grid;
  grid-template-columns: minmax(6rem, auto) minmax(0, 1fr) auto;
  /* The name and the id are the two ends, and the preview and the meta line
     stack between them. The areas are named because a row with four children in
     three columns has to say where each one goes, and a grid-column on each
     child would be four declarations where one is enough. */
  grid-template-areas:
    "name preview id"
    "name meta   id";
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-block-end: var(--border-weight-hairline) solid var(--border-subtle);
  transition: background var(--transition-fast);
}

/* Below this the four fields stop fitting side by side, and the row becomes
   the name and its id, then the preview, then the meta. */
@media (max-width: 640px) {
  .command-card {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "name     id"
      "preview  preview"
      "meta     meta";
  }
}

.command-card:last-child {
  border-block-end: 0;
}

.command-card:hover {
  background: var(--hover-fill);
}

.command-card__name {
  grid-area: name;
  align-self: center;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-text);
}

/* The preview: what the command does.
 *
 * One line, with an ellipsis. A description is a sentence, and a list of 421
 * sentences is a wall — so the row shows enough of it to recognise the
 * command and no more. */
.command-card__preview {
  grid-area: preview;
  font-size: var(--text-sm);
  color: var(--color-muted);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.command-card__id {
  grid-area: id;
  align-self: center;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--navy-400);
}

.command-card__meta {
  grid-area: meta;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-xs);
  color: var(--color-faint);
}

/* ==========================================================================
 * Changelog
 *
 * One entry is a panel: the version, the type, the date, the title and the
 * body. The body is the release pipeline's own text, so it is kept as a
 * paragraph and not as markup.
 * ========================================================================== */

.changelog-versions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-block-end: var(--space-4);
}

.changelog-count {
  margin-block-end: var(--space-3);
  font-size: var(--text-sm);
  color: var(--color-muted);
}

.changelog-entry {
  padding: var(--space-4);
  border: var(--border-weight-hairline) solid var(--border-subtle);
  border-radius: var(--radius-lg, 14px);
  background: var(--elev-2-surface);
  box-shadow: var(--elev-2-shadow), var(--elev-2-ring);
}

.changelog-entry + .changelog-entry {
  margin-block-start: var(--space-3);
}

.changelog-entry__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-block-end: var(--space-2);
}

.changelog-entry__date {
  font-size: var(--text-xs);
  color: var(--color-faint);
}

.changelog-entry__title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-base);
  font-weight: 650;
  color: var(--color-text);
}

.changelog-entry__body {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--color-muted);
  white-space: pre-line;
}

/* ==========================================================================
 * Consent
 *
 * The page answers one question — which version is being accepted — and then
 * offers the choice. The versions are the two things a reader has to be able
 * to tell apart, so they are the two things the layout is built around.
 * ========================================================================== */

.consent__title {
  margin: 0 0 var(--space-3);
  font-size: var(--text-lg);
  font-weight: 650;
  color: var(--color-text);
}

.consent__state {
  margin: var(--space-3) 0;
  font-size: var(--text-sm);
  color: var(--color-muted);
}

/* ==========================================================================
 * Pricing
 *
 * The page exists to produce a decision, so the decision is the largest thing on
 * it and the last plan is the one the eye lands on.
 * ========================================================================== */

.pricing__grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-4);
  align-items: stretch;
}

/* --- hierarchy ---
 *
 * One plan is recommended and the rest are not. That is stated three ways at
 * once — position, a badge, and a ring — because any one of them alone is
 * missed: position is missed by a reader who arrived mid-page, and the other two
 * by one who skimmed past the card entirely.
 */

.plan {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--space-5);
  border: var(--border-weight-hairline) solid var(--border-subtle);
  border-radius: var(--radius-lg, 14px);
  background: var(--elev-2-surface);
  box-shadow: var(--elev-2-shadow), var(--elev-2-ring);
  transition:
    border-color var(--transition-base),
    box-shadow var(--transition-base),
    transform var(--transition-base);
}

.plan:hover {
  border-color: var(--border-accent);
  transform: translateY(-2px);
  box-shadow: var(--shadow-brand);
}

/* The recommended plan, raised a step above the others.
 *
 * It is stated three ways at once — position, a badge and a ring — because
 * any one of them alone is missed. The ring is a gradient one: a flat coloured
 * outline reads as a border, and the difference between a border and a ring is
 * exactly the difference between "this card is styled differently" and "this
 * card is the one". */
.plan--featured {
  border-color: transparent;
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--sky-500) 70%, transparent),
    0 0 0 4px color-mix(in srgb, var(--sky-200) 55%, transparent),
    var(--elev-3-shadow);
}

/* The badge sits on the top edge rather than inside the card, so it does not
   push the content down and make the featured card the tallest. */
.plan__flag {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 50%;
  transform: translate(-50%, -50%);
  padding: 0.2rem 0.7rem;
  border-radius: var(--radius-pill, 999px);
  /* The same fill as the primary action, so "the one that is recommended"
     and "the button that continues" look like the same decision. */
  background: var(--btn-primary-fill);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #FFFFFF;
  box-shadow: var(--shadow-brand);
  white-space: nowrap;
}

/* --- pricing typography ---
 *
 * The price is the reason the card exists, so it is the largest thing on it and
 * the unit is not competing with it.
 */

.plan__name {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--color-faint);
}

/* The price is the reason the card exists, so it is the largest thing on
   it -- and it is the one number on the page that a reader is looking for.
   *
   * One flat colour. A sky-into-violet gradient used to be clipped to the
   * text behind a support guard; the violet was off-brand and made the card
   * ask to be looked at twice. The flat sky-800 is the colour the rest of
   * the page's numerals are dressed in. */
.plan__price {
  display: flex;
  align-items: baseline;
  gap: 0.15em;
  margin-block: var(--space-2) var(--space-1);
  font-size: clamp(2rem, 1.5rem + 1.5vw, 2.75rem);
  font-weight: 750;
  line-height: 1;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  color: var(--sky-800);
}

.plan__price-note {
  font-size: var(--text-xs);
  color: var(--color-faint);
}

.plan__features {
  display: grid;
  gap: var(--space-2);
  flex: 1;
  margin-block: var(--space-4);
  padding: 0;
  list-style: none;
}

.plan__features li {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-2);
  align-items: start;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--color-muted);
}

.plan__features li::before {
  content: "✓";
  font-weight: 700;
  color: var(--sky-500);
}

/* --- comparison ---
 *
 * A table of what differs, which is the question a pricing page exists to answer
 * after the reader has chosen a plan. It is its own section below the cards,
 * not a fourth card: comparing four plans means looking across them, and a card
 * cannot be looked across.
 */

.comparison-wrap {
  margin-block-start: var(--space-7);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  border: var(--border-weight-hairline) solid var(--border-subtle);
  border-radius: var(--radius-lg, 14px);
  background: var(--elev-1-surface);
}

.comparison th[aria-current="true"] {
  background: var(--accent-softer);
  color: var(--navy-900);
  font-weight: 700;
}

/* ==========================================================================
 * Sign in
 *
 * One card, centred, with the brand mark at the top.
 *
 * The width used to be an inline style on the markup, which is the tell: a
 * card whose size is written in the page is a card nobody decided on. It is
 * here now, where it can be changed once.
 * ========================================================================== */

.signin {
  display: grid;
  justify-items: center;
  padding-block: var(--space-6) var(--space-7);
}

.signin__card {
  inline-size: min(26rem, 100%);
  display: grid;
  gap: var(--space-4);
  justify-items: center;
  text-align: center;
  padding: var(--space-6);
  border: var(--border-weight-hairline) solid var(--border-subtle);
  border-radius: var(--radius-lg, 14px);
  background: var(--elev-2-surface);
  box-shadow: var(--elev-3-shadow), var(--elev-3-ring);
}

.signin__mark {
  display: grid;
  place-items: center;
  inline-size: 3rem;
  block-size: 3rem;
  border-radius: 14px;
  background: var(--navy-gradient);
  box-shadow:
    0 1px 2px rgba(15, 72, 115, 0.22),
    0 6px 18px -6px color-mix(in srgb, var(--color-accent) 62%, transparent),
    inset 0 1px 0 rgba(255, 255, 255, 0.16);
  color: #FFFFFF;
  font-weight: 800;
  font-size: var(--text-lg);
}

.signin__title {
  margin: 0;
  font-size: var(--text-2xl);
  font-weight: 780;
  letter-spacing: -0.03em;
  color: var(--navy-900);
}

.signin__lede {
  margin: 0;
  max-width: 34ch;
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: var(--color-muted);
  text-wrap: pretty;
}

.signin__action {
  margin-block-start: var(--space-2);
}

.signin__note {
  margin: 0;
  padding-block-start: var(--space-4);
  border-block-start: 1px solid var(--border-subtle);
  inline-size: 100%;
  font-size: var(--text-xs);
  line-height: var(--leading-relaxed);
  color: var(--color-faint);
}

/* ==========================================================================
 * Status
 *
 * A status page is read when something is wrong. It has one job: say whether it
 * is, and what to do.
 * ========================================================================== */

/* The overall state, as one card.
 *
 * A reader who came to find out whether something is broken should not have to
 * read a list to find out. So the state is one card, at the top, with the
 * detail under it and the time it was measured beside it.
 *
 * The coloured edge is the state: green for normal, amber for degraded, navy
 * for "we do not know" — which is not the same as broken, and saying so in
 * colour is honest. */
.status__summary {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-5);
  border: var(--border-weight-hairline) solid var(--border-subtle);
  border-radius: var(--radius-lg, 14px);
  background: var(--elev-2-surface);
  box-shadow: var(--elev-2-shadow), var(--elev-2-ring);
  margin-block-end: var(--space-5);
}

.status__summary-label {
  font-size: var(--text-lg);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--color-text);
}
.status__summary-detail {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
  color: var(--color-muted);
}
.status__summary-updated {
  font-size: var(--text-xs);
  color: var(--color-faint);
  white-space: nowrap;
}

.status__summary[data-state="operational"] { border-inline-start: 3px solid var(--color-success); }
.status__summary[data-state="restricted"],
.status__summary[data-state="full"] { border-inline-start: 3px solid var(--color-warning); }
.status__summary[data-state="unknown"],
.status__summary[data-state="unavailable"] { border-inline-start: 3px solid var(--navy-400); }

/* --- uptime history ---
 *
 * Ninety days as a row of bars. A single percentage hides the shape — a service
 * that was down for six hours every Tuesday is 99.3% and looks perfect.
 */

.uptime {
  display: flex;
  gap: 2px;
  align-items: flex-end;
  block-size: 2.25rem;
}

.uptime__day {
  flex: 1;
  min-inline-size: 2px;
  block-size: 100%;
  border-radius: 2px;
  background: var(--color-success);
}

.uptime__day[data-state="partial"] { background: var(--color-warning); }
.uptime__day[data-state="down"] { background: var(--color-danger); }
.uptime__day[data-state="unknown"] { background: var(--border-subtle); }

.uptime__day:hover {
  outline: var(--border-weight-thick) solid var(--color-text);
  outline-offset: 1px;
}

/* --- incident timeline ---
 *
 * Vertical rather than a table, because an incident has an order and a duration
 * and both read as height.
 */

.timeline {
  display: grid;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

.timeline__item {
  position: relative;
  display: grid;
  grid-template-columns: 1.5rem 1fr;
  gap: var(--space-3);
  padding-block: var(--space-4);
  padding-inline-start: var(--space-2);
}

/* The rail and the node. The rail is the item's ::before so it runs from node to
   node without needing to know how many there are -- an :nth-last-child rule
   would break the moment an item is added at the top. */
.timeline__item::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: calc(var(--space-2) + 0.5rem);
  inline-size: 2px;
  background: var(--border-subtle);
}

.timeline__item:first-child::before { inset-block-start: 50%; }
.timeline__item:last-child::before { inset-block-end: 50%; }

.timeline__node {
  position: relative;
  z-index: 1;
  inline-size: 14px;
  block-size: 14px;
  margin-block-start: 0.2rem;
  border-radius: 50%;
  background: var(--brand-surface);
  box-shadow: 0 0 0 3px var(--color-success);
}

.timeline__item[data-severity="warning"] .timeline__node { box-shadow: 0 0 0 3px var(--color-warning); }
.timeline__item[data-severity="critical"] .timeline__node { box-shadow: 0 0 0 3px var(--color-danger); }

/* A planned window is neither a problem nor a non-event, so it is neither
 * green nor red. */
.timeline__item[data-severity="info"] .timeline__node { box-shadow: 0 0 0 3px var(--navy-400); }

.timeline__title {
  margin: 0 0 var(--space-1);
  font-size: var(--text-sm);
  font-weight: 650;
  color: var(--color-text);
}

.timeline__meta {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--color-faint);
  font-variant-numeric: tabular-nums;
}

/* ==========================================================================
 * Features
 *
 * Thirteen categories. A flat grid of thirteen identical cards is a list, and the
 * eye finds nothing in it — so the cards carry a category colour, and the
 * categories group.
 * ========================================================================== */

.feature-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
  gap: var(--space-4);
}

.feature {
  position: relative;
  display: grid;
  gap: var(--space-3);
  align-content: start;
  padding: var(--space-5);
  border: var(--border-weight-hairline) solid var(--border-subtle);
  border-radius: var(--radius-lg, 14px);
  background: var(--elev-2-surface);
  box-shadow: var(--elev-2-shadow), var(--elev-2-ring);
  overflow: hidden;
  transition:
    border-color var(--transition-base),
    box-shadow var(--transition-base),
    transform var(--transition-base);
}

/* A category stripe along the top edge: the colour is the grouping, and it is
   the one thing on the card that is not text. */
.feature::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: 3px;
  background: var(--feature-accent, var(--accent-gradient));
}

.feature:hover {
  transform: translateY(-2px);
  border-color: var(--border-accent);
  box-shadow: var(--shadow-brand);
}

/* The icon slot is a chip, not a square.
 *
 * The category's own colour is laid over its wash as a gradient that fades
 * out, so the chip catches the light from the top left and the category is
 * still readable in the corner. The ring is the elevation's, so the chip has
 * an edge and not only a cast. */
.feature__icon {
  display: grid;
  place-items: center;
  inline-size: 2.75rem;
  block-size: 2.75rem;
  border-radius: var(--radius-lg, 14px);
  background:
    linear-gradient(
      140deg,
      var(--feature-accent, var(--accent-gradient)) 0%,
      transparent 170%
    ),
    var(--feature-wash, var(--accent-softer));
  box-shadow: var(--elev-1-shadow), var(--elev-1-ring);
}

/* The icon is drawn by the icon sheet's rule, which any element with a
 * data-icon uses. The colour deepens with the card. */
.feature:hover .feature__icon::before,
.card--interactive:hover .feature__icon::before {
  background-color: var(--sky-800);
}

.feature__title {
  margin: 0;
  font-size: var(--text-base);
  font-weight: 650;
  letter-spacing: -0.01em;
  color: var(--color-text);
  text-wrap: balance;
}

.feature__body {
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: var(--color-muted);
  text-wrap: pretty;
}

/* ==========================================================================
 * Forms
 *
 * A form's job is to be filled in without hesitation, so the measure is capped
 * well below the page's and the groups are visible.
 * ========================================================================== */

.form {
  display: grid;
  gap: var(--space-5);
  max-inline-size: 46rem;
}

.form__group {
  display: grid;
  gap: var(--space-3);
  padding: var(--space-5);
  border: var(--border-weight-hairline) solid var(--border-subtle);
  border-radius: var(--radius-lg, 14px);
  background: var(--elev-1-surface);
  box-shadow: var(--elev-1-shadow);
}

.form__group-legend {
  padding: 0 var(--space-2);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-faint);
}

.field {
  display: grid;
  gap: var(--space-2);
}

.field__label {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: 550;
  color: var(--color-text);
}

.field__hint {
  font-size: var(--text-xs);
  color: var(--color-faint);
  line-height: 1.5;
}

/* --- the contact channels ---
 *
 * The composer renders the channels into `[data-contact-channels]`: a real
 * `mailto:` link and a real Discord link, side by side.
 *
 * They are links, not buttons, because they leave the page. A button that
 * navigates away is a link that is wearing the wrong clothes, and it is read
 * as an action that happens here.
 */
.contact__channels {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-block-start: var(--space-4);
  padding-block-start: var(--space-4);
  border-block-start: var(--border-weight-hairline) solid var(--color-border);
}

.contact__channel {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border: var(--border-weight-hairline) solid var(--color-border);
  border-radius: var(--radius-pill, 999px);
  color: var(--color-accent);
  font-size: var(--text-sm);
  font-weight: 600;
  text-decoration: none;
  transition: background-color var(--transition-fast), border-color var(--transition-fast);
}

.contact__channel:hover {
  background: var(--accent-soft);
  border-color: var(--accent-500);
}

.contact__channel:focus-visible {
  outline: var(--border-weight-thick) solid var(--accent-500);
  outline-offset: var(--focus-ring-offset);
}

/* No channel configured.
 *
 * The gate refuses to let a build with no channel ship, so this is the state
 * a deployment gets if the configuration was edited by hand. It says what it
 * is rather than looking like a broken link. */
.contact__channel--none {
  padding-inline: 0;
  border: 0;
  color: var(--color-muted);
  font-weight: 400;
}

/* ==========================================================================
 * Reduced motion
 *
 * Every looping animation in this file is stopped by name. base.css collapses
 * durations globally, and a collapsed duration on an infinite animation still
 * composites forever — which costs a reader a frame budget to produce motion
 * they asked not to see.
 * ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .docs__progress-bar { transition: none; }

  .plan,
  .feature,
  .docs__anchor {
    transition: none;
  }

  .plan:hover,
  .feature:hover {
    transform: none;
  }
}

