/* ---------------------------------------------------------------------------
   IBM Plex, self-hosted.
   Drawn for technical documentation, and its mono is metrically related to the
   sans — which is what matters when a part number sits inline with prose.
   Self-hosted rather than pulled from a CDN: the catalogue is used in the EU,
   and a font request to a third party is a data transfer nobody agreed to. It
   also means the type is not one outbound request away from disappearing.
   IBM's own split web files, unmodified, from the @ibm/plex-sans and
   @ibm/plex-mono 1.1.0 releases, with the unicode-ranges copied from IBM's CSS:
   Latin1/2/3 and Pi, so a page fetches only the pieces its text needs. They
   replaced Google Fonts subsets, which count as modified versions of a font
   whose name IBM reserves. Licence: fonts/LICENSE.txt (OFL-1.1). Arabic and
   Chinese fall through to the system stack — Plex Sans does not cover them.
   --------------------------------------------------------------------------- */
@font-face { font-family: 'IBM Plex Mono'; font-style: normal;
  font-weight: 400; font-display: swap; src: url(fonts/IBMPlexMono-Regular-Latin1.woff2) format('woff2');
  unicode-range: U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212, U+FB01-FB02; }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal;
  font-weight: 400; font-display: swap; src: url(fonts/IBMPlexMono-Regular-Latin2.woff2) format('woff2');
  unicode-range: U+0100-0101, U+0104-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+01A0-01A1, U+01AF-01B0, U+01FA-01FF, U+0218-021B, U+0237, U+0259, U+1E80-1E85, U+1E9E, U+20A1, U+20A4, U+20A6, U+20A8-20AA, U+20AD-20AE, U+20B1-20B2, U+20B4-20B5, U+20B8-20BA, U+20BD, U+20BF; }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal;
  font-weight: 400; font-display: swap; src: url(fonts/IBMPlexMono-Regular-Latin3.woff2) format('woff2');
  unicode-range: U+0102-0103, U+01CD-01DC, U+1EA0-1EF9, U+20AB; }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal;
  font-weight: 400; font-display: swap; src: url(fonts/IBMPlexMono-Regular-Pi.woff2) format('woff2');
  unicode-range: U+03C0, U+0E3F, U+2000-200D, U+2028-2029, U+202F, U+2032-2033, U+205F, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+2215, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+2500-259F, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+3000, U+FEFF, U+FFFD; }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal;
  font-weight: 500; font-display: swap; src: url(fonts/IBMPlexMono-Medium-Latin1.woff2) format('woff2');
  unicode-range: U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212, U+FB01-FB02; }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal;
  font-weight: 500; font-display: swap; src: url(fonts/IBMPlexMono-Medium-Latin2.woff2) format('woff2');
  unicode-range: U+0100-0101, U+0104-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+01A0-01A1, U+01AF-01B0, U+01FA-01FF, U+0218-021B, U+0237, U+0259, U+1E80-1E85, U+1E9E, U+20A1, U+20A4, U+20A6, U+20A8-20AA, U+20AD-20AE, U+20B1-20B2, U+20B4-20B5, U+20B8-20BA, U+20BD, U+20BF; }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal;
  font-weight: 500; font-display: swap; src: url(fonts/IBMPlexMono-Medium-Latin3.woff2) format('woff2');
  unicode-range: U+0102-0103, U+01CD-01DC, U+1EA0-1EF9, U+20AB; }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal;
  font-weight: 500; font-display: swap; src: url(fonts/IBMPlexMono-Medium-Pi.woff2) format('woff2');
  unicode-range: U+03C0, U+0E3F, U+2000-200D, U+2028-2029, U+202F, U+2032-2033, U+205F, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+2215, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+2500-259F, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+3000, U+FEFF, U+FFFD; }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal;
  font-weight: 600; font-display: swap; src: url(fonts/IBMPlexMono-SemiBold-Latin1.woff2) format('woff2');
  unicode-range: U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212, U+FB01-FB02; }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal;
  font-weight: 600; font-display: swap; src: url(fonts/IBMPlexMono-SemiBold-Latin2.woff2) format('woff2');
  unicode-range: U+0100-0101, U+0104-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+01A0-01A1, U+01AF-01B0, U+01FA-01FF, U+0218-021B, U+0237, U+0259, U+1E80-1E85, U+1E9E, U+20A1, U+20A4, U+20A6, U+20A8-20AA, U+20AD-20AE, U+20B1-20B2, U+20B4-20B5, U+20B8-20BA, U+20BD, U+20BF; }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal;
  font-weight: 600; font-display: swap; src: url(fonts/IBMPlexMono-SemiBold-Latin3.woff2) format('woff2');
  unicode-range: U+0102-0103, U+01CD-01DC, U+1EA0-1EF9, U+20AB; }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal;
  font-weight: 600; font-display: swap; src: url(fonts/IBMPlexMono-SemiBold-Pi.woff2) format('woff2');
  unicode-range: U+03C0, U+0E3F, U+2000-200D, U+2028-2029, U+202F, U+2032-2033, U+205F, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+2215, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+2500-259F, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+3000, U+FEFF, U+FFFD; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal;
  font-weight: 400; font-display: swap; src: url(fonts/IBMPlexSans-Regular-Latin1.woff2) format('woff2');
  unicode-range: U+0000, U+000D, U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212, U+FB01-FB02; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal;
  font-weight: 400; font-display: swap; src: url(fonts/IBMPlexSans-Regular-Latin2.woff2) format('woff2');
  unicode-range: U+0100-0101, U+0104-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+01A0-01A1, U+01AF-01B0, U+01FA-01FF, U+0218-021B, U+0237, U+0259, U+1E80-1E85, U+1E9E, U+20A1, U+20A4, U+20A6, U+20A8-20AA, U+20AD-20AE, U+20B1-20B2, U+20B4-20B5, U+20B8-20BA, U+20BD, U+20BF; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal;
  font-weight: 400; font-display: swap; src: url(fonts/IBMPlexSans-Regular-Latin3.woff2) format('woff2');
  unicode-range: U+0102-0103, U+01CD-01DC, U+1EA0-1EF9, U+20AB; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal;
  font-weight: 400; font-display: swap; src: url(fonts/IBMPlexSans-Regular-Pi.woff2) format('woff2');
  unicode-range: U+0E3F, U+2000-200D, U+2015, U+2028-2029, U+202F, U+2032-2033, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+2215, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+ECE0, U+EFCC, U+FEFF, U+FFFD; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal;
  font-weight: 500; font-display: swap; src: url(fonts/IBMPlexSans-Medium-Latin1.woff2) format('woff2');
  unicode-range: U+0000, U+000D, U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212, U+FB01-FB02; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal;
  font-weight: 500; font-display: swap; src: url(fonts/IBMPlexSans-Medium-Latin2.woff2) format('woff2');
  unicode-range: U+0100-0101, U+0104-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+01A0-01A1, U+01AF-01B0, U+01FA-01FF, U+0218-021B, U+0237, U+0259, U+1E80-1E85, U+1E9E, U+20A1, U+20A4, U+20A6, U+20A8-20AA, U+20AD-20AE, U+20B1-20B2, U+20B4-20B5, U+20B8-20BA, U+20BD, U+20BF; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal;
  font-weight: 500; font-display: swap; src: url(fonts/IBMPlexSans-Medium-Latin3.woff2) format('woff2');
  unicode-range: U+0102-0103, U+01CD-01DC, U+1EA0-1EF9, U+20AB; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal;
  font-weight: 500; font-display: swap; src: url(fonts/IBMPlexSans-Medium-Pi.woff2) format('woff2');
  unicode-range: U+0E3F, U+2000-200D, U+2015, U+2028-2029, U+202F, U+2032-2033, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+2215, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+ECE0, U+EFCC, U+FEFF, U+FFFD; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal;
  font-weight: 600; font-display: swap; src: url(fonts/IBMPlexSans-SemiBold-Latin1.woff2) format('woff2');
  unicode-range: U+0000, U+000D, U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212, U+FB01-FB02; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal;
  font-weight: 600; font-display: swap; src: url(fonts/IBMPlexSans-SemiBold-Latin2.woff2) format('woff2');
  unicode-range: U+0100-0101, U+0104-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+01A0-01A1, U+01AF-01B0, U+01FA-01FF, U+0218-021B, U+0237, U+0259, U+1E80-1E85, U+1E9E, U+20A1, U+20A4, U+20A6, U+20A8-20AA, U+20AD-20AE, U+20B1-20B2, U+20B4-20B5, U+20B8-20BA, U+20BD, U+20BF; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal;
  font-weight: 600; font-display: swap; src: url(fonts/IBMPlexSans-SemiBold-Latin3.woff2) format('woff2');
  unicode-range: U+0102-0103, U+01CD-01DC, U+1EA0-1EF9, U+20AB; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal;
  font-weight: 600; font-display: swap; src: url(fonts/IBMPlexSans-SemiBold-Pi.woff2) format('woff2');
  unicode-range: U+0E3F, U+2000-200D, U+2015, U+2028-2029, U+202F, U+2032-2033, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+2215, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+ECE0, U+EFCC, U+FEFF, U+FFFD; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal;
  font-weight: 700; font-display: swap; src: url(fonts/IBMPlexSans-Bold-Latin1.woff2) format('woff2');
  unicode-range: U+0000, U+000D, U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212, U+FB01-FB02; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal;
  font-weight: 700; font-display: swap; src: url(fonts/IBMPlexSans-Bold-Latin2.woff2) format('woff2');
  unicode-range: U+0100-0101, U+0104-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+01A0-01A1, U+01AF-01B0, U+01FA-01FF, U+0218-021B, U+0237, U+0259, U+1E80-1E85, U+1E9E, U+20A1, U+20A4, U+20A6, U+20A8-20AA, U+20AD-20AE, U+20B1-20B2, U+20B4-20B5, U+20B8-20BA, U+20BD, U+20BF; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal;
  font-weight: 700; font-display: swap; src: url(fonts/IBMPlexSans-Bold-Latin3.woff2) format('woff2');
  unicode-range: U+0102-0103, U+01CD-01DC, U+1EA0-1EF9, U+20AB; }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal;
  font-weight: 700; font-display: swap; src: url(fonts/IBMPlexSans-Bold-Pi.woff2) format('woff2');
  unicode-range: U+0E3F, U+2000-200D, U+2015, U+2028-2029, U+202F, U+2032-2033, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+2215, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+ECE0, U+EFCC, U+FEFF, U+FFFD; }

/* ============================================================================
   DESIGN SYSTEM
   ----------------------------------------------------------------------------
   Tokens and primitives for the whole application: buttons, inputs, controls,
   tables, links, chips, section headers. Layout lives in app.css; anything that
   looks like a *control* is defined here once and never restyled locally.

   Grounded in how Linear, Vercel and Stripe actually build:
     - 4px spacing base, dense at the small end (2/4/8/12) because reference
       data needs fine control, generous at the large end for reading.
     - Ghost-first buttons: the default state is transparent, and weight is
       added only where an action deserves it. A panel of equally solid buttons
       tells you nothing about which one matters.
     - Density lives in behaviour, not in pixels. Rows stay touchable; what
       makes the interface feel dense is that everything responds.
     - Two densities, and the choice is the reader's, not ours.
   ========================================================================== */

:root {
  /* --- space: 4px base ------------------------------------------------- */
  --s-1: 2px;  --s-2: 4px;  --s-3: 8px;  --s-4: 12px;
  --s-5: 16px; --s-6: 24px; --s-7: 32px; --s-8: 48px;

  /* --- radius ---------------------------------------------------------- */
  /* Squarer than before. A pill-shaped text field reads as a search box; a
     form full of them reads as a toy. Controls and containers stay one step
     apart so a control inside a card never looks rounder than the card. */
  --r-ctl: 6px;      /* buttons, inputs, chips        */
  --r-box: 10px;     /* cards, panels, popovers       */
  --r-pill: 999px;   /* counts and status only        */

  /* --- type ------------------------------------------------------------ */
  --t-display: 600 clamp(26px, 4vw, 40px)/1.1 var(--font);
  --t-title:   600 16px/1.3 var(--font);
  --t-body:    400 14px/1.55 var(--font);
  --t-data:    400 13px/1.45 var(--font);
  --t-small:   400 12px/1.45 var(--font);
  --t-mono:    500 12.5px/1.4 var(--mono);
  --t-label:   650 10px/1 var(--font);
  --label-track: .07em;

  /* --- controls -------------------------------------------------------- */
  --ctl-sm: 28px; --ctl-md: 32px; --ctl-lg: 40px;
  --ring: 0 0 0 3px var(--brand-soft);

  /* --- rows: comfortable by default, compact by choice ----------------- */
  --row-y: 9px;
  --motion-ctl: .16s;
  --motion-box: .24s;
}
:root[data-density="compact"] { --row-y: 5px; }

/* ------------------------------------------------------------------ hidden */
/* `hidden` has to actually hide.

   The browser's own rule for it sets `display`, and so does almost every
   control class in this file — and a class outranks the user agent, so
   `el.hidden = true` on a `.btn` left the button on screen, marked hidden and
   perfectly visible. It had been patched a dozen times at the point of use,
   once per control that happened to hit it, and the comment on those patches
   was already "the same trap". Stated once, with the weight to win, it stops
   being something each new control has to rediscover. */
[hidden] { display: none !important; }

/* ---------------------------------------------------------------- buttons */
/* One class, variants by modifier. Ghost is the default because most actions
   on a reference catalogue are secondary to reading it. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-3);
  height: var(--ctl-md); padding: 0 var(--s-4);
  font: var(--t-data); font-weight: 540;
  color: var(--ink-2); background: transparent;
  border: 1px solid var(--line); border-radius: var(--r-ctl);
  cursor: pointer; white-space: nowrap;
  transition: background var(--motion-ctl) var(--ease),
              border-color var(--motion-ctl) var(--ease),
              color var(--motion-ctl) var(--ease);
}
.btn:hover {
  border-color: var(--brand-line); color: var(--brand-ink);
  background: color-mix(in srgb, var(--brand) 6%, transparent);
}
.btn:focus-visible { outline: none; box-shadow: var(--ring); border-color: var(--brand-line); }
.btn:disabled { opacity: .5; cursor: default; }

.btn--primary {
  background: var(--brand); border-color: var(--brand); color: #fff;
}
.btn--primary:hover { background: color-mix(in srgb, var(--brand) 88%, #000); color: #fff; }
/* The working variant. A proposed change is committed with a blue button, not
   the brand red — red is identity and the one action a view is FOR, and it also
   reads as "danger" next to a field the user has just altered. */
.btn--work {
  background: var(--work); border-color: var(--work); color: var(--work-on, #fff);
}
.btn--work:hover {
  background: color-mix(in srgb, var(--work) 86%, var(--work-on, #000));
  border-color: color-mix(in srgb, var(--work) 86%, var(--work-on, #000));
  color: var(--work-on, #fff);
}
.btn--work:focus-visible { box-shadow: var(--work-ring); }
/* A destructive action that is being CONFIRMED gets a fill: at that point it is
   the one action the bar exists for, and ghost-first is about ranking actions
   that compete, not about the last step of one. */
.btn--danger-solid {
  background: var(--air-ink, var(--air)); border-color: var(--air-ink, var(--air));
  color: var(--air-on, #fff);
}
.btn--danger-solid:hover {
  background: color-mix(in srgb, var(--air-ink, var(--air)) 86%, var(--air-on, #000));
  border-color: color-mix(in srgb, var(--air-ink, var(--air)) 86%, var(--air-on, #000));
  color: var(--air-on, #fff);
}
.btn--quiet { border-color: transparent; }
.btn--quiet:hover { background: var(--bg-sunk); border-color: transparent; }
.btn--danger { color: var(--air-ink, var(--air)); }
.btn--danger:hover { background: color-mix(in srgb, var(--air) 12%, transparent); border-color: transparent; }
.btn--sm { height: var(--ctl-sm); padding: 0 var(--s-3); font: var(--t-small); }
.btn--lg { height: var(--ctl-lg); padding: 0 var(--s-5); }
.btn--icon { width: var(--ctl-md); padding: 0; }
.btn--icon.btn--sm { width: var(--ctl-sm); }
.btn svg { width: 16px; height: 16px; flex: none; }

/* ----------------------------------------------------------------- inputs */
.input, .select, .textarea {
  width: 100%; height: var(--ctl-md); padding: 0 var(--s-4);
  font: var(--t-data); color: var(--ink);
  background: var(--bg-elev); border: 1px solid var(--line);
  border-radius: var(--r-ctl);
  transition: border-color var(--motion-ctl) var(--ease),
              box-shadow var(--motion-ctl) var(--ease);
}
.textarea { height: auto; padding: var(--s-3) var(--s-4); line-height: 1.55; resize: vertical; }
.input::placeholder, .textarea::placeholder { color: var(--ink-3); }
.input:focus, .select:focus, .textarea:focus {
  outline: none; border-color: var(--brand-line); box-shadow: var(--ring);
}
.input:disabled, .select:disabled { opacity: .55; cursor: not-allowed; }

.field { display: block; margin-bottom: var(--s-4); }
.field > span {
  display: block; margin: 0 0 var(--s-2) var(--s-1);
  font: var(--t-small); font-weight: 560; color: var(--ink-2);
}
.hint { font: var(--t-small); color: var(--ink-3); margin: var(--s-2) 0 0; }
.error { font: var(--t-small); color: var(--air-ink, var(--air)); margin: var(--s-2) 0 0; }

/* ------------------------------------------------------- checkbox / radio */
/* Drawn by us, on the real input.
   `appearance: none` on the input itself rather than a visually-hidden input
   plus a decorated span: the control keeps its own focus ring, its own keyboard
   behaviour and its own label association, and there is no second element to
   keep in sync. A native checkbox is drawn by the operating system — it ignores
   the accent, the radius and the theme, and on Windows it is a different size
   again, which is exactly the drift this layer exists to stop. */
input[type="checkbox"], input[type="radio"] {
  appearance: none; -webkit-appearance: none;
  flex: none; width: 18px; height: 18px; margin: 0; padding: 0;
  display: inline-grid; place-content: center;
  border: 1.5px solid var(--line); border-radius: 5px;
  background: var(--bg-elev); cursor: pointer;
  transition: background var(--motion-ctl) var(--ease),
              border-color var(--motion-ctl) var(--ease);
}
input[type="radio"] { border-radius: 50%; }
input[type="checkbox"]:hover, input[type="radio"]:hover { border-color: var(--brand-line); }
input[type="checkbox"]:checked, input[type="radio"]:checked {
  background: var(--brand); border-color: var(--brand);
}
/* Physical `border-right`, not `border-inline-end`. A tick is a glyph, not
   text: under `dir="rtl"` the logical property dropped the other side, and the
   rotation then drew it mirrored — a checked box in Arabic showed a reversed
   check. Nothing about a checkmark belongs to reading order. */
input[type="checkbox"]:checked::after {
  content: ''; width: 9px; height: 5px; margin-top: -2px;
  border: 2px solid #fff; border-top: 0; border-right: 0; rotate: -45deg;
}
input[type="radio"]:checked::after {
  content: ''; width: 7px; height: 7px; border-radius: 50%; background: #fff;
}
input[type="checkbox"]:focus-visible, input[type="radio"]:focus-visible {
  outline: none; box-shadow: var(--ring); border-color: var(--brand-line);
}
input[type="checkbox"]:disabled, input[type="radio"]:disabled {
  opacity: .5; cursor: default;
}

.check, .radio {
  display: inline-flex; align-items: center; gap: var(--s-3);
  font: var(--t-data); color: var(--ink-2); cursor: pointer; padding: var(--s-2) 0;
}
.check:has(input:checked), .radio:has(input:checked) { color: var(--brand-ink); }
.check:hover, .radio:hover { color: var(--ink); }

/* ------------------------------------------------- file, colour, number ---- */
/* A file input renders as an OS button plus "no file chosen" in a font nobody
   chose. Hide the control, keep it in the form, and let its own label be the
   button — `.file-field > input` is still what the browser opens and submits. */
.file-field {
  display: inline-flex; align-items: center; gap: var(--s-3);
  font: var(--t-data); color: var(--ink-3); cursor: pointer;
}
.file-field input[type="file"] {
  position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none;
}
.file-field .file-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-3);
  height: var(--ctl-md); padding: 0 var(--s-4);
  font: var(--t-data); font-weight: 540; color: var(--ink-2);
  background: transparent; border: 1px solid var(--line); border-radius: var(--r-ctl);
  white-space: nowrap;
}
.file-field:hover .file-btn { border-color: var(--brand-line); color: var(--brand-ink); }
.file-field input:focus-visible + .file-btn { box-shadow: var(--ring); border-color: var(--brand-line); }
.file-field .file-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The swatch is ours; the picker it opens is the operating system's colour
   service, like a date picker. The hex field beside it is the one that submits,
   so the swatch is a convenience and never the only way to set a colour. */
input[type="color"] {
  appearance: none; -webkit-appearance: none;
  width: 44px; height: var(--ctl-md); padding: 3px; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--r-ctl); background: var(--bg-elev);
}
input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
input[type="color"]::-webkit-color-swatch { border: 0; border-radius: 4px; }
input[type="color"]::-moz-color-swatch { border: 0; border-radius: 4px; }
input[type="color"]:focus-visible { outline: none; box-shadow: var(--ring); border-color: var(--brand-line); }

/* Spinners are OS chrome and they change the control's width when they appear.
   The field is a number either way; a value this small is typed, not nudged. */
input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* Safari draws its own clear button inside a search field; there is already one
   in the box that matches everything else. */
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button { -webkit-appearance: none; }

/* -------------------------------------------------------------- type pill */
/* The filter type is the first thing a counter hand looks for, so it gets a
   readable pill carrying its NAME rather than a dot to decode against a legend.
   The colour is whatever the admin picked for that type, mixed down: a pill is
   an identity, and identity never tints a surface. */
.tpill {
  display: inline-flex; align-items: center; height: 22px; padding: 0 9px;
  border-radius: var(--r-ctl);
  font: 600 10.5px/1 var(--font); letter-spacing: var(--label-track);
  text-transform: uppercase; white-space: nowrap;
  background: color-mix(in srgb, var(--tc, var(--ink-3)) 15%, transparent);
  /* Half the type colour, half the ink. The type colours are chosen in the
     admin, so this formula has to hold for any of them — at 78% the yellow
     used for oil measured 2.7:1 and was unreadable, while red and blue only
     just passed. Mixed to 50% every current type clears 4.9:1 in both themes
     and stays unmistakably its own colour. */
  color: color-mix(in srgb, var(--tc, var(--ink-3)) 50%, var(--ink));
}
.tpill--lg { height: 26px; padding: 0 11px; font-size: 11.5px; }

/* ------------------------------------------------------------------ links */
.link {
  color: var(--brand-ink); text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color var(--motion-ctl) var(--ease);
}
.link:hover { border-bottom-color: currentColor; }

/* --------------------------------------------------------- section header */
.hd {
  display: flex; align-items: center; gap: var(--s-3);
  margin: 0 0 var(--s-4);
}
.hd h3, .hd .hd-t {
  margin: 0; font: var(--t-label); letter-spacing: var(--label-track);
  text-transform: uppercase; color: var(--ink-3);
}
/* A row count is a fact about the section, not an action and not the product's
   identity — so it is not the brand red. */
.hd .count {
  font: var(--t-label); letter-spacing: .04em; padding: 2px var(--s-2);
  border-radius: var(--r-pill);
  background: var(--bg-sunk); color: var(--ink-3);
}
.hd .hd-end { margin-inline-start: auto; display: flex; gap: var(--s-2); }

/* --------------------------------------------------------------- surfaces */
.chip {
  display: inline-flex; align-items: center; gap: var(--s-2);
  font: var(--t-small); padding: var(--s-1) var(--s-3);
  border-radius: var(--r-pill); background: var(--bg-sunk); color: var(--ink-2);
  white-space: nowrap;
}
.code-chip {
  font: var(--t-mono); padding: 5px var(--s-3); border-radius: 6px;
  background: var(--bg-sunk); color: var(--ink-2); white-space: nowrap;
}

/* ----------------------------------------------------------------- tables */
/* No vertical rules and no zebra: a full grid of lines competes with the data.
   One horizontal separator carries the structure. */
.tbl { width: 100%; border-collapse: collapse; }
.tbl th {
  text-align: start; padding: 0 var(--s-3) var(--s-3) 0;
  font: var(--t-label); letter-spacing: var(--label-track);
  text-transform: uppercase; color: var(--ink-3); white-space: nowrap;
}
.tbl td {
  padding: var(--row-y) var(--s-3) var(--row-y) 0;
  font: var(--t-data); color: var(--ink-2);
  border-top: 1px solid var(--line-soft);
}
.tbl tbody tr:hover td { background: color-mix(in srgb, var(--brand) 4%, transparent); }
.tbl td:first-child { color: var(--ink); font-weight: 520; }
/* numeric right-aligned so digits line up by place value */
.tbl .num, .tbl td.num { text-align: end; font: var(--t-mono); color: var(--ink); }
/* the dash for an empty cell is presentation, never content - in the DOM it
   becomes part of the value and every blank field reads as an edit */
.tbl td:empty::before { content: '—'; color: var(--ink-3); }

/* ---------------------------------------------------------------- loading */
/* A skeleton stands in for the shape that is coming, at the size it will be.
   A spinner says "wait"; a skeleton says "here is what is arriving", which is
   why the layout does not jump when it resolves.

   .sk on any element gives it the shimmer. Reduced motion gets a still block:
   a looping shimmer is exactly the kind of animation that rule exists for. */
.sk {
  position: relative; overflow: hidden; pointer-events: none;
  color: transparent !important;
  background: var(--bg-sunk);
  border-radius: 6px;
}
.sk * { visibility: hidden; }
.sk::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent 0%,
    color-mix(in srgb, var(--ink) 7%, transparent) 50%, transparent 100%);
  transform: translateX(-100%);
  animation: sk-sweep 1.25s var(--ease) infinite;
}
@keyframes sk-sweep { to { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) { .sk::after { animation: none; } }

.sk-line { height: 1em; margin-bottom: var(--s-2); }
.sk-line:last-child { margin-bottom: 0; }
.sk-line--60 { width: 60%; }
.sk-line--40 { width: 40%; }
.sk-line--80 { width: 80%; }
.sk-block { border-radius: var(--r-box); }

/* --------------------------------------------------------------- surfaces */
.card-panel {
  background: var(--bg-elev); border: 1px solid var(--line-soft);
  border-radius: var(--r-box); padding: var(--s-5) var(--s-6);
  margin-bottom: var(--s-5);
}
.card-panel h3 { font: var(--t-title); margin: 0 0 var(--s-2); }
.card-panel .stack > * + * { margin-top: var(--s-4); }

.pop {
  position: absolute; z-index: 70; min-width: 190px; padding: var(--s-1);
  background: var(--bg-elev); border: 1px solid var(--line);
  border-radius: var(--r-box); box-shadow: var(--shadow-3);
  animation: pop .16s var(--ease) both;
}
@keyframes pop { from { opacity: 0; transform: translateY(-4px); } }
@media (prefers-reduced-motion: reduce) { .pop { animation: none; } }
.pop-item {
  display: block; width: 100%; text-align: start; cursor: pointer;
  padding: var(--s-3) var(--s-3); border: 0; border-radius: 7px;
  background: transparent; font: var(--t-data); color: var(--ink-2);
  text-decoration: none;
}
.pop-item:hover {
  background: color-mix(in srgb, var(--brand) 9%, transparent); color: var(--brand-ink);
}
.pop-item[aria-selected="true"] {
  background: var(--brand-soft); color: var(--brand-ink); font-weight: 560;
}
.pop-item--danger { color: var(--air-ink, var(--air)); }
.pop-item--danger:hover { background: color-mix(in srgb, var(--air) 12%, transparent); }
.pop-sep { height: 1px; margin: var(--s-1) 0; background: var(--line-soft); }
.pop-label {
  padding: var(--s-2) var(--s-3); font: var(--t-label);
  letter-spacing: var(--label-track); text-transform: uppercase; color: var(--ink-3);
}

.modal-scrim {
  position: fixed; inset: 0; z-index: 80; display: grid; place-items: center;
  background: rgba(0,0,0,.45); backdrop-filter: blur(3px);
  padding: var(--s-5);
}
.modal {
  width: min(400px, 100%); padding: var(--s-6);
  background: var(--bg-elev); border: 1px solid var(--line);
  border-radius: var(--r-box); box-shadow: var(--shadow-3);
}
.modal h3 { font: var(--t-title); margin: 0 0 var(--s-5); }
.modal-actions { display: flex; gap: var(--s-3); justify-content: flex-end; margin-top: var(--s-5); }

/* ------------------------------------------------------------------ state */
.flash { padding: var(--s-3) var(--s-4); border-radius: var(--r-ctl);
         font: var(--t-data); margin-bottom: var(--s-4); }
.flash.ok  { background: color-mix(in srgb, var(--fuel) 12%, transparent); color: var(--ink); }
.flash.bad { background: color-mix(in srgb, var(--air) 10%, transparent); color: var(--ink); }
.flash a { color: var(--brand-ink); }

.pill {
  font: var(--t-label); letter-spacing: .04em; text-transform: capitalize;
  padding: 3px var(--s-3); border-radius: var(--r-pill);
  background: var(--bg-sunk); color: var(--ink-2);
}
.pill--ok   { background: color-mix(in srgb, var(--fuel) 16%, transparent); }
.pill--warn { background: color-mix(in srgb, var(--oil) 20%, transparent); }
.pill--bad  { background: color-mix(in srgb, var(--air) 14%, transparent); }

.badge {
  display: inline-block; min-width: 16px; padding: 0 5px; margin-inline-start: var(--s-2);
  border-radius: var(--r-pill); background: var(--brand); color: #fff;
  font: 600 10px/16px var(--font); text-align: center;
}

.empty { text-align: center; padding: var(--s-8) var(--s-5); color: var(--ink-3); }
.empty strong { display: block; color: var(--ink-2); font-weight: 560; margin-bottom: var(--s-2); }

/* tabs — used by settings and the review queue */
.tabs { display: flex; gap: var(--s-5); border-bottom: 1px solid var(--line-soft);
        margin-bottom: var(--s-5); }
.tab {
  font: var(--t-data); color: var(--ink-3); text-decoration: none;
  padding: var(--s-3) 0; border-bottom: 2px solid transparent; cursor: pointer;
  display: inline-flex; align-items: center; gap: var(--s-2);
}
.tab:hover { color: var(--ink-2); }
.tab.is-current { color: var(--brand-ink); border-bottom-color: var(--brand); }

/* toolbar — a row of controls that belongs to the thing beneath it */
.toolbar { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap;
           margin-bottom: var(--s-4); }
.toolbar .grow { flex: 1; min-width: 160px; }

/* ----------------------------------------------------------- select (own) */
/* A native <select> cannot be styled to match anything: its menu is drawn by
   the operating system, so it ignores the accent, the radius, the type scale
   and dark mode. Ours is a button that opens the popover primitive, which means
   one menu design across the whole application. Behaviour lives in app.js and
   the native element stays in the form so it still submits. */
.sel { position: relative; }
.sel > select { position: absolute; opacity: 0; pointer-events: none; width: 0; height: 0; }
.sel-btn {
  display: flex; align-items: center; gap: var(--s-3);
  width: 100%; height: var(--ctl-md); padding: 0 var(--s-3) 0 var(--s-4);
  font: var(--t-data); color: var(--ink); text-align: start;
  background: var(--bg-elev); border: 1px solid var(--line);
  border-radius: var(--r-ctl); cursor: pointer;
  transition: border-color var(--motion-ctl) var(--ease),
              box-shadow var(--motion-ctl) var(--ease);
}
.sel-btn:hover { border-color: var(--brand-line); }
.sel-btn:focus-visible, .sel.is-open .sel-btn {
  outline: none; border-color: var(--brand-line); box-shadow: var(--ring);
}
.sel-btn .sel-val { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
                    white-space: nowrap; }
.sel-btn .sel-caret { flex: none; color: var(--ink-3); transition: transform var(--motion-ctl) var(--ease); }
.sel.is-open .sel-caret { transform: rotate(180deg); color: var(--brand-ink); }
.sel .pop { inset-inline-start: 0; inset-inline-end: 0; top: calc(100% + 4px);
            max-height: 280px; overflow: auto; }
.sel-swatch { width: 10px; height: 10px; border-radius: 3px; flex: none;
              background: var(--type-colour, var(--ink-3)); }

/* ================================================================
   RESPONSIVE
   ----------------------------------------------------------------
   Every primitive has to survive an arbitrary width, not three named
   breakpoints. Rules that scroll or wrap beat rules that shrink.
   ================================================================ */

/* Tabs scroll rather than wrapping into a second row that shifts the content
   below them every time the count changes. */
.tabs {
  overflow-x: auto; scrollbar-width: none; flex-wrap: nowrap;
  -webkit-overflow-scrolling: touch;
}
.tabs::-webkit-scrollbar { display: none; }
.tab { white-space: nowrap; flex: none; }

/* A table of reference data cannot be made narrow: it scrolls inside its own
   box so the page never scrolls sideways. */
.tbl-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.tbl-scroll > .tbl { min-width: 420px; }

.toolbar > * { min-width: 0; }
.modal-actions { flex-wrap: wrap; }

@media (max-width: 620px) {
  :root { --s-6: 18px; --s-7: 24px; }      /* less air, same rhythm */
  .card-panel { padding: var(--s-5); }
  .modal { padding: var(--s-5); }
  .btn { padding: 0 var(--s-3); }
  .toolbar { gap: var(--s-2); }
  .toolbar .grow { flex-basis: 100%; }
  /* a full-width primary action is easier to hit than a right-aligned pair */
  .modal-actions .btn { flex: 1; }
}

/* Coarse pointers need a bigger target than a mouse does. */
@media (pointer: coarse) {
  :root { --ctl-sm: 34px; --ctl-md: 40px; --ctl-lg: 46px; --row-y: 11px; }
  .pop-item { padding: var(--s-4) var(--s-3); }
}

/* ------------------------------------------------------------ guided tours */
/* One popover at a time, anchored to a control, the page dimmed around it
   (assets/tour.js). The spotlight is a box whose shadow is the dimming, so
   the control itself stays lit and in place. */
.tour-spot {
  position: fixed; z-index: 90; pointer-events: none;
  border-radius: var(--r-box);
  box-shadow: 0 0 0 2px var(--brand), 0 0 0 9999px rgba(12, 10, 8, .5);
  transition: left var(--motion-box) var(--ease), top var(--motion-box) var(--ease),
              width var(--motion-box) var(--ease), height var(--motion-box) var(--ease);
}
.tour-pop {
  position: fixed; z-index: 91; width: min(340px, calc(100vw - 16px));
  padding: var(--s-5); background: var(--bg-elev); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--r-box); box-shadow: var(--shadow-3);
  transition: left var(--motion-box) var(--ease), top var(--motion-box) var(--ease);
  animation: pop .16s var(--ease) both;
}
.tour-spot.no-motion, .tour-pop.no-motion { transition: none; animation: none; }
.tour-count { font: var(--t-label); letter-spacing: var(--label-track); text-transform: uppercase;
  color: var(--ink-3); margin-bottom: var(--s-3); }
.tour-title { font: var(--t-title); margin: 0 0 var(--s-2); }
.tour-body { font: var(--t-body); color: var(--ink-2); margin: 0 0 var(--s-5); }
.tour-actions { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); }
.tour-nav { display: flex; gap: var(--s-2); }
.tour-skip { margin-inline-start: calc(var(--s-3) * -1); }

/* ================================================================== motion
   One vocabulary for movement, so the whole application moves the same way.
   Motion here says something — this arrived, this is where you are, this
   happened — and is quick: nothing waits for an animation to finish.

     rise    content arriving: a short lift and fade
     pop     something appearing in place: a small scale and fade
     bump    a value that just changed
     flash   "this one": a brief glow that fades

   Everything below is switched off for people who ask their system for
   reduced motion (the block at the end). */
:root { --motion-enter: .32s; --stagger: 45ms; }

@keyframes m-rise  { from { opacity: 0; transform: translateY(8px); } }
@keyframes m-fade  { from { opacity: 0; } }
@keyframes m-pop   { from { opacity: 0; transform: scale(.96); } }
@keyframes m-bump  { 40% { transform: scale(1.06); } }
@keyframes m-flash { from { box-shadow: 0 0 0 3px var(--brand-soft); background-color: var(--brand-soft); } }
@keyframes m-check { from { transform: scale(.4); opacity: 0; } }

/* pages: their sections arrive one after another */
.admin > .meta-row, .admin > .flash, .admin > .card-panel, .admin > .tbl-scroll,
.admin > .settings-form, .admin > .tabs, .admin > .print-shell > *, .admin > form > .card-panel,
.tool-grid > * {
  animation: m-rise var(--motion-enter) var(--ease) backwards;
}
.admin > :nth-child(2), .tool-grid > :nth-child(2), .print-shell > :nth-child(2) { animation-delay: calc(1 * var(--stagger)); }
.admin > :nth-child(3), .tool-grid > :nth-child(3) { animation-delay: calc(2 * var(--stagger)); }
.admin > :nth-child(4), .tool-grid > :nth-child(4) { animation-delay: calc(3 * var(--stagger)); }
.admin > :nth-child(n+5), .tool-grid > :nth-child(n+5) { animation-delay: calc(4 * var(--stagger)); }

/* a button gives under the finger */
.btn { transition: background var(--motion-ctl) var(--ease), border-color var(--motion-ctl) var(--ease),
                   color var(--motion-ctl) var(--ease), transform .12s var(--ease); }
.btn:active:not(:disabled) { transform: scale(.97); }

/* a box being ticked: the tick lands rather than appearing */
input[type="checkbox"]:checked::after, input[type="radio"]:checked::after {
  animation: m-check .18s var(--ease) both;
}

.flash { animation: m-rise var(--motion-enter) var(--ease) backwards; }
.is-bump  { animation: m-bump .36s var(--ease); }
.is-flash-in { animation: m-flash 1.2s var(--ease); }

/* between pages: a quick cross-fade, with the header held still */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
  .admin-bar { view-transition-name: chrome; }
  ::view-transition-old(root) { animation: m-fade .16s var(--ease) reverse both; }
  ::view-transition-new(root) { animation: m-fade .22s var(--ease) both; }
}

@media (prefers-reduced-motion: reduce) {
  .admin > *, .tool-grid > *, .print-shell > *, .admin > form > .card-panel,
  .flash, .is-bump, .is-flash-in,
  input[type="checkbox"]:checked::after, input[type="radio"]:checked::after { animation: none !important; }
  .btn:active:not(:disabled) { transform: none; }
}
.tour-pop.is-step .tour-title, .tour-pop.is-step .tour-body, .tour-pop.is-step .tour-count {
  animation: m-rise .22s var(--ease) backwards; }
.tour-pop.is-step .tour-body { animation-delay: 30ms; }
@media (prefers-reduced-motion: reduce) { .tour-pop.is-step * { animation: none !important; } }
