/* Study Pill — primitives layer.
 *
 * Reusable components built from tokens.css. Once a screen migrates to these,
 * its bespoke `<style>` block can shrink to layout-only rules. Existing
 * screens keep working unchanged because every primitive uses a unique
 * `.sp-…` prefix that doesn't collide with the legacy class names.
 *
 * SEE THEM ALL AT /ui — every component, every state, both themes.
 * That page is the point. This file's first eight components were documented
 * here in exactly this comment and had ZERO adopters for months, because a
 * list of class names tells you nothing about what they look like or which
 * states exist. Before writing any CSS, open /ui and check whether the thing
 * already exists. (Enable with SP_UI_GALLERY=1; off in production by default.)
 *
 * Chrome
 *   .sp-app-bar           sticky top app bar (back · title · actions)
 *   .sp-bottom-nav        thumb-zone tab bar (safe-area aware)
 * Controls
 *   .sp-btn               primary · ghost · icon · sm · lg · [disabled]
 *   .sp-field             label · control · hint · .is-invalid + error
 *   .sp-tabs / .sp-tab    segmented control
 * Surfaces
 *   .sp-card              elevated surface (--interactive, --flat)
 *   .sp-row / .sp-list    tappable list line (lead · body · meta · trail)
 *   .sp-sheet             bottom sheet on phone, dialog on desktop
 * Status
 *   .sp-chip              neutral · accent · success · warn · danger · mono
 *   .sp-alert             error · warn · success — what happened + what to do
 *   .sp-meter             label · count · bar, with threshold states
 *   .sp-toast             ephemeral notification
 * States
 *   .sp-skeleton          loading placeholder (title/line/block/circle)
 *   .sp-empty             empty state — icon · title · body · actions
 * Type & layout
 *   .sp-kicker            the uppercase line above a title
 *   .sp-stack             vertical spacing scale
 */
@import url("./tokens.css");

/* ---------- Reset patches that the design wants everywhere ---------- */
.sp-app-bar *,
.sp-bottom-nav *,
.sp-card * { -webkit-tap-highlight-color: transparent; }

/* HIDDEN MEANS HIDDEN.

   `[hidden]` is only `display: none` in the user-agent sheet, so ANY `display`
   rule on the element beats it. Every author who has hit this in Study Pill has
   patched it locally - `.sp-ra-launch__btn[hidden]`, `.sp-ra-voice[hidden]`,
   `.sp-ra-btn[hidden]` - three copies of one rule, each written after the same
   surprise, and each covering only the class in front of that author.

   The fourth cost more. `.resume-ask__confirm { display: flex }` beat the
   `hidden` attribute on the pack page, so the two-step confirm behind "Start
   afresh" was never two steps: **"Clear my progress" - which wipes a student's
   answers on every device with no undo - sat permanently on screen, one tap
   away, under a warning explaining a question nobody had been asked.** The JS
   was correct the whole time; it toggled an attribute that could not win.

   One rule, `!important`, because the whole point is that it must beat a
   `display` an author did not know they were fighting. Anything that needs to
   animate out uses a class, not `hidden`. */
[hidden] { display: none !important; }

/* ---------- Focus (WCAG 2.2 SC 2.4.7, 2.4.11, 2.4.13) ----------
 *
 * The entire app had ONE :focus-visible rule before this. A student using a
 * keyboard, a switch, or a Bluetooth keyboard on a tablet had no reliable way
 * to see where they were — on a timed mock exam.
 *
 * :focus-visible, not :focus, so a mouse/touch press doesn't leave a ring
 * behind; the browser decides when the ring is warranted. `outline` rather
 * than box-shadow so it follows the border radius and survives on Windows
 * High Contrast / forced-colors, where box-shadow is dropped entirely.
 */
.sp-btn:focus-visible,
.sp-chip:focus-visible,
.sp-card--interactive:focus-visible,
.sp-app-bar__back:focus-visible,
.sp-bottom-nav__btn:focus-visible,
.theme-toggle:focus-visible {
  outline: var(--sp-focus-width, 3px) solid var(--sp-focus);
  outline-offset: var(--sp-focus-offset, 2px);
}

/* Where a control sits flush against the viewport edge (the tab bar), an
   outward offset would be clipped. Pull it inside instead. */
.sp-bottom-nav__btn:focus-visible { outline-offset: -3px; }

/* ---------- App bar ---------- */
/* The bar used to WRAP: `flex-wrap: wrap` plus a rule giving the actions their
   own full-width row under 500px. Every phone is under 500px, so every phone
   got a two-row bar ~69px tall carrying a page name and a boxed back button,
   and no product name anywhere on it. gluearrow.com's bar is 55px, one row,
   wordmark left and plain icons right. This is that bar. */
.sp-app-bar {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: var(--sp-space-2);
  padding: var(--sp-space-2) var(--sp-space-4);
  padding-top: calc(var(--sp-space-2) + env(safe-area-inset-top, 0));
  min-height: 56px;
  background: color-mix(in oklch, var(--sp-paper) 88%, transparent);
  backdrop-filter: saturate(1.2) blur(16px);
  -webkit-backdrop-filter: saturate(1.2) blur(16px);
  border-bottom: 1px solid var(--sp-rule-soft);
}
/* A chevron, not a button. Boxed, filled and bordered, it read as a control of
   equal weight to the brand beside it, which it is not. The 44px tap target is
   kept — that is a thumb requirement, not a visual one — by padding rather than
   by drawing a box. */
.sp-app-bar__back {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; margin-left: calc(var(--sp-space-3) * -1);
  background: none; color: var(--sp-ink);
  border: 0; border-radius: var(--sp-radius-sm);
  text-decoration: none; cursor: pointer;
  font-size: 20px; line-height: 1;
  transition: background var(--sp-dur-fast) ease;
}
.sp-app-bar__back:hover { background: var(--sp-paper-2); }
.sp-app-bar__back:active { transform: scale(.94); }

.sp-app-bar__titles { flex: 1; min-width: 0; display: flex; flex-direction: column; }

/* The wordmark. Weight 900, accent, tightened tracking — gluearrow.com's own
   treatment of "GlueArrow", so the two read as one company. */
.sp-app-bar__brand {
  font-family: var(--sp-font-display);
  font-weight: 900; font-size: 18px; letter-spacing: -.03em; line-height: 1.15;
  color: var(--sp-coral-display);
  text-decoration: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sp-app-bar__brand:hover { color: var(--sp-coral); }

/* Where you are: quiet, one line, never competing with the wordmark. */
.sp-app-bar__title {
  min-width: 0; margin: 0;
  font-family: var(--sp-font-ui); font-weight: 500; font-size: 13px;
  color: var(--sp-ink-soft); letter-spacing: 0; line-height: 1.3;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sp-app-bar__sub { color: var(--sp-ink-soft); font-weight: 400; }

.sp-app-bar__actions {
  flex: 0 0 auto; display: flex; gap: var(--sp-space-1); align-items: center;
}

/* ---------- Bottom nav ---------- */
.sp-bottom-nav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  display: grid; grid-template-columns: repeat(var(--sp-bnav-count, 5), 1fr);
  padding: 6px 4px;
  padding-bottom: calc(6px + env(safe-area-inset-bottom, 0));
  background: color-mix(in oklch, var(--sp-paper) 92%, transparent);
  backdrop-filter: saturate(1.4) blur(20px);
  -webkit-backdrop-filter: saturate(1.4) blur(20px);
  border-top: 1px solid var(--sp-rule-soft);
  transition: transform var(--sp-dur-base) var(--sp-ease-out), opacity var(--sp-dur-base) ease;
}
.sp-bottom-nav.is-hidden { transform: translateY(120%); opacity: 0; pointer-events: none; }
.sp-bottom-nav__btn {
  background: transparent; border: 0; cursor: pointer;
  min-height: 56px; padding: 6px 4px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; border-radius: var(--sp-radius-sm);
  font-family: var(--sp-font-ui); font-size: 11px; font-weight: 600;
  color: var(--sp-ink-soft);
  transition: color var(--sp-dur-fast) ease, background var(--sp-dur-fast) ease, transform 80ms ease;
  position: relative;
}
.sp-bottom-nav__btn:active { transform: scale(.94); }
.sp-bottom-nav__btn[aria-current="page"], .sp-bottom-nav__btn.is-active { color: var(--sp-coral); }
.sp-bottom-nav__btn .ico { font-size: 20px; line-height: 1; }
.sp-bottom-nav__btn .badge {
  position: absolute; top: 6px; right: 22%;
  background: var(--sp-coral); color: var(--sp-on-coral);
  font-size: 10px; font-weight: 700;
  min-width: 16px; height: 16px; padding: 0 5px;
  border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
}
@media (min-width: 768px) {
  .sp-bottom-nav { max-width: 560px; left: 50%; transform: translateX(-50%); border-radius: 18px 18px 0 0; border: 1px solid var(--sp-rule-soft); border-bottom: 0; }
  .sp-bottom-nav.is-hidden { transform: translate(-50%, 120%); }
}

/* ---------- Buttons ---------- */
.sp-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 12px 20px; border-radius: var(--sp-radius-sm);
  font-family: var(--sp-font-ui); font-size: 14px; font-weight: 600;
  letter-spacing: .005em; line-height: 1;
  border: 1px solid var(--sp-rule); background: var(--sp-paper); color: var(--sp-ink);
  cursor: pointer; text-decoration: none;
  transition: background var(--sp-dur-fast) ease, border-color var(--sp-dur-fast) ease,
              transform 80ms ease, box-shadow var(--sp-dur-fast) ease;
  min-height: 44px;
}
.sp-btn:hover { border-color: var(--sp-ink-soft); }
.sp-btn:active { transform: translateY(1px); }
.sp-btn[disabled], .sp-btn:disabled { opacity: .45; cursor: not-allowed; transform: none; }

/* --sp-on-coral, not #fff. On the dark theme coral is a LIGHT colour, so a
   white label sat at 2.87:1 — a hard-coded #fff cannot flip with the theme,
   which is precisely how that shipped. */
.sp-btn--primary {
  background: var(--sp-coral); color: var(--sp-on-coral); border-color: var(--sp-coral);
  box-shadow: var(--sp-lift-1);
}
.sp-btn--primary:hover { background: var(--sp-coral-2); border-color: var(--sp-coral-2); transform: translateY(-1px); box-shadow: var(--sp-lift-2); }
.sp-btn--primary:active { transform: translateY(0); }

.sp-btn--ghost { background: transparent; }
.sp-btn--ghost:hover { background: var(--sp-paper-2); }

/* Destructive — OUTLINED, deliberately, and never `--primary`.
 *
 * Coral is Study Pill's brand colour AND the only red it has. So a filled
 * coral "Delete pack" is the biggest, warmest, most inviting thing on the
 * screen: the styling says "recommended" while the label says "destroy your
 * notes". DESIGN_STANDARD §10 calls that out — visual weight must match which
 * option is actually better for the reader.
 *
 * Outlined keeps it clearly available and clearly not the default. On a delete
 * confirmation the SAFE choice takes `--primary`; this takes the risk.
 * Caught by opening /ui and looking at the sheet demo, which had it backwards.
 */
.sp-btn--danger {
  background: transparent;
  color: var(--sp-coral);
  border-color: color-mix(in oklch, var(--sp-coral) 45%, var(--sp-rule));
}
.sp-btn--danger:hover {
  background: color-mix(in oklch, var(--sp-coral) 10%, var(--sp-paper));
  border-color: var(--sp-coral);
}

.sp-btn--icon {
  width: 44px; height: 44px; padding: 0;
  border-radius: 50%;
}

.sp-btn--lg { padding: 14px 26px; font-size: 15px; min-height: 52px; border-radius: var(--sp-radius-md); }
.sp-btn--sm { padding: 8px 14px; font-size: 13px; min-height: 36px; border-radius: var(--sp-radius-xs); }

/* ---------- Card ---------- */
.sp-card {
  background: var(--sp-paper); color: var(--sp-ink);
  border: 1px solid var(--sp-rule-soft);
  border-radius: var(--sp-radius-md);
  padding: var(--sp-space-5) var(--sp-space-6);
  box-shadow: var(--sp-lift-1);
  transition: transform var(--sp-dur-base) var(--sp-ease-out),
              box-shadow var(--sp-dur-base) var(--sp-ease-out),
              border-color var(--sp-dur-base) var(--sp-ease-out);
}
.sp-card--interactive { cursor: pointer; }
.sp-card--interactive:hover { transform: translateY(-2px); box-shadow: var(--sp-lift-2); border-color: var(--sp-rule); }
.sp-card--flat { box-shadow: none; }

/* ---------- Chip ---------- */
.sp-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 10px; border-radius: 999px;
  font-family: var(--sp-font-ui); font-size: 12px; font-weight: 500;
  letter-spacing: .01em;
  background: var(--sp-paper-2); color: var(--sp-ink-2);
  border: 1px solid var(--sp-rule-soft);
  white-space: nowrap;
}
.sp-chip--accent  { background: color-mix(in oklch, var(--sp-coral) 12%, var(--sp-paper)); color: var(--sp-coral-deep); border-color: color-mix(in oklch, var(--sp-coral) 30%, var(--sp-rule)); }
/* --sp-pine-text, not --sp-pine: this is TYPE on a tinted surface, and the
   fill-weight green measures 2.96:1 against pine-soft. Same split for warn. */
.sp-chip--success { background: var(--sp-pine-soft); color: var(--sp-pine-text); border-color: color-mix(in oklch, var(--sp-pine) 35%, var(--sp-rule)); }
.sp-chip--warn    { background: var(--sp-coral-soft); color: var(--sp-coral); border-color: color-mix(in oklch, var(--sp-coral) 35%, var(--sp-rule)); }
.sp-chip--danger  { background: color-mix(in oklch, var(--sp-coral) 14%, var(--sp-paper)); color: var(--sp-coral); border-color: color-mix(in oklch, var(--sp-coral) 30%, var(--sp-rule)); }
.sp-chip--mono    { font-family: var(--sp-font-mono); font-size: 11px; letter-spacing: .06em; text-transform: uppercase; }

/* ---------- Toast ---------- */
.sp-toast {
  position: fixed; left: 50%; bottom: calc(20px + env(safe-area-inset-bottom, 0));
  transform: translateX(-50%) translateY(120%);
  background: var(--sp-ink); color: var(--sp-paper);
  padding: 10px 16px; border-radius: 999px;
  font-family: var(--sp-font-ui); font-size: 13px; font-weight: 500;
  box-shadow: var(--sp-lift-3); z-index: 1100;
  opacity: 0;
  transition: transform var(--sp-dur-slow) var(--sp-ease-out), opacity var(--sp-dur-base) ease;
  max-width: calc(100vw - 32px);
}
.sp-toast.is-show { transform: translateX(-50%) translateY(0); opacity: 1; }

/* ---------- Empty state ---------- */
.sp-empty {
  padding: var(--sp-space-9) var(--sp-space-5);
  text-align: center;
  color: var(--sp-ink-soft);
  font-family: var(--sp-font-display);
  font-size: 17px; line-height: 1.45;
}
.sp-empty .sp-empty__hint {
  display: block; margin-top: var(--sp-space-2);
  font-family: var(--sp-font-mono); font-size: 11px;
  letter-spacing: .1em; text-transform: uppercase;
}

/* ---------- Vertical stack helper ---------- */
.sp-stack { display: flex; flex-direction: column; }
.sp-stack--1 > * + * { margin-top: var(--sp-space-1); }
.sp-stack--2 > * + * { margin-top: var(--sp-space-2); }
.sp-stack--3 > * + * { margin-top: var(--sp-space-3); }
.sp-stack--4 > * + * { margin-top: var(--sp-space-4); }
.sp-stack--5 > * + * { margin-top: var(--sp-space-5); }
.sp-stack--6 > * + * { margin-top: var(--sp-space-6); }

/* ---------- Reduced motion respect ---------- */
@media (prefers-reduced-motion: reduce) {
  .sp-app-bar, .sp-bottom-nav, .sp-btn, .sp-card, .sp-chip, .sp-toast {
    transition: opacity .12s ease;
  }
}

/* ---- Inline SVG icons (studypill/templates/partials/_icons.html) ----------
   Study Pill uses real stroke icons, never emoji: emoji render differently on
   every device/OS, can't take brand colour, and look like clip-art next to the
   serif type. These inherit `currentColor` and size in `em`, so an icon always
   matches the text beside it. Class is `sp-ico` (NOT `.ico` — that name is
   already taken by the resume-hero's 50px badge and the drop-zone glyph). */
.sp-ico {
  display: inline-block;
  vertical-align: -0.14em;   /* optical centre against lowercase text */
  flex-shrink: 0;
  stroke: currentColor;
}

/* Theme toggle shows the theme you'd switch TO: a moon while light, a sun while
   dark. Both icons ship in the button and CSS picks — the toggle's JS used to
   swap an emoji via textContent, which would wipe inline SVG children. */
.theme-toggle .sp-ico { display: none; }
html[data-theme="light"] .theme-toggle .sp-ico-moon { display: inline-block; }
html[data-theme="dark"]  .theme-toggle .sp-ico-sun  { display: inline-block; }

/* ═══════════════════════════════════════════════════════════════════════════
   THE SECOND WAVE — the components the pages were already re-implementing.
   ═══════════════════════════════════════════════════════════════════════════

   Not picked off a generic design-system checklist. Every one below was counted
   across the thirteen page <style> blocks first, so this builds what the
   product already needs rather than what a component library usually ships:

     card/panel   10 pages      loading / skeleton   7 pages
     button       10 pages      empty state          7 pages
     list row      9 pages      progress meter       5 pages
     kicker        9 pages      toast                4 pages
     field         9 pages      tabs / segmented     4 pages
     alert         8 pages      sheet / modal        4 pages
     chip          8 pages

   Each carries the states DESIGN_STANDARD §7 asks for — loading, empty, error,
   disabled, dense. That is not thoroughness for its own sake: a missing state
   is precisely why a page gives up and writes its own CSS, which is how the
   first wave of primitives ended up with zero adopters.
   ========================================================================= */

/* ---------- Kicker ----------
   The small uppercase line above a title. Nine pages drew their own, each with
   its own dot, letter-spacing and colour. */
.sp-kicker {
  display: inline-flex; align-items: center; gap: var(--sp-space-2);
  font-family: var(--sp-font-ui); font-size: 11.5px; font-weight: 600;
  letter-spacing: .10em; text-transform: uppercase;
  color: var(--sp-coral);
  margin-bottom: var(--sp-space-2);
}
.sp-kicker::before {
  content: ""; flex: 0 0 auto;
  width: 6px; height: 6px; border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 0 4px color-mix(in oklch, currentColor 18%, transparent);
}
.sp-kicker--quiet { color: var(--sp-ink-soft); }
.sp-kicker--bare::before { display: none; }

/* ---------- Field ----------
   Label, control, hint — and an error state that belongs to the component
   instead of being reinvented by every form. */
.sp-field { display: flex; flex-direction: column; gap: 6px; }
.sp-field__label {
  font-family: var(--sp-font-ui); font-size: 13px; font-weight: 600;
  color: var(--sp-ink-2);
}
.sp-field__control,
.sp-field input[type="text"],
.sp-field input[type="email"],
.sp-field input[type="tel"],
.sp-field input[type="number"],
.sp-field input[type="date"],
.sp-field input[type="search"],
.sp-field select,
.sp-field textarea {
  width: 100%;
  min-height: 44px;
  padding: 11px 14px;
  /* 16px, not 15: iOS zooms the viewport when a field under 16px takes focus,
     which on a phone throws the whole layout sideways mid-form. */
  font-family: var(--sp-font-ui); font-size: 16px;
  color: var(--sp-ink);
  background: var(--sp-paper);
  border: 1px solid var(--sp-rule);
  border-radius: var(--sp-radius-sm);
  transition: border-color var(--sp-dur-fast) ease;
}
.sp-field textarea { min-height: 120px; resize: vertical; line-height: 1.55; }
.sp-field__control:hover,
.sp-field input:hover, .sp-field select:hover, .sp-field textarea:hover {
  border-color: var(--sp-ink-soft);
}
.sp-field__control:focus-visible,
.sp-field input:focus-visible,
.sp-field select:focus-visible,
.sp-field textarea:focus-visible {
  outline: var(--sp-focus-width, 3px) solid var(--sp-focus);
  outline-offset: var(--sp-focus-offset, 2px);
  border-color: var(--sp-coral);
}
.sp-field__hint {
  font-family: var(--sp-font-ui); font-size: 12.5px; line-height: 1.45;
  color: var(--sp-ink-soft);
}
/* Invalid never signals by colour alone (WCAG 1.4.1): the border also
   thickens, and the message says what to do about it. */
.sp-field.is-invalid .sp-field__control,
.sp-field.is-invalid input,
.sp-field.is-invalid select,
.sp-field.is-invalid textarea {
  border-color: var(--sp-coral);
  border-width: 2px;
}
.sp-field__error {
  display: flex; align-items: flex-start; gap: 6px;
  font-family: var(--sp-font-ui); font-size: 12.5px; font-weight: 500;
  color: var(--sp-coral);
}
.sp-field input[disabled], .sp-field select[disabled], .sp-field textarea[disabled] {
  opacity: .55; cursor: not-allowed; background: var(--sp-paper-2);
}

/* ---------- Row ----------
   A tappable list line: lead slot, body, trailing meta. Nine pages have one. */
.sp-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--sp-space-3);
  padding: var(--sp-space-3) var(--sp-space-4);
  background: var(--sp-paper);
  border: 1px solid var(--sp-rule-soft);
  border-radius: var(--sp-radius-md);
  color: inherit; text-decoration: none;
  transition: transform var(--sp-dur-base) var(--sp-ease-out),
              box-shadow var(--sp-dur-base) var(--sp-ease-out),
              border-color var(--sp-dur-base) var(--sp-ease-out);
}
a.sp-row, button.sp-row { cursor: pointer; width: 100%; text-align: left; }
a.sp-row:hover, button.sp-row:hover {
  transform: translateY(-2px); box-shadow: var(--sp-lift-2); border-color: var(--sp-rule);
}
a.sp-row:focus-visible, button.sp-row:focus-visible {
  outline: var(--sp-focus-width, 3px) solid var(--sp-focus);
  outline-offset: var(--sp-focus-offset, 2px);
}
.sp-row__lead {
  width: 40px; height: 40px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--sp-radius-sm);
  background: var(--sp-paper-2);
  border: 1px solid var(--sp-rule-soft);
  color: var(--sp-ink-2);
}
.sp-row__body { min-width: 0; }
.sp-row__title {
  font-family: var(--sp-font-display); font-weight: 500; font-size: 16px;
  line-height: 1.3; color: var(--sp-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* One compact metadata line, middot-separated. Good mobile apps all land on
   this shape, and each page here had invented its own version of it. */
.sp-row__meta {
  display: flex; flex-wrap: wrap; gap: 4px 10px;
  font-family: var(--sp-font-ui); font-size: 12.5px;
  color: var(--sp-ink-soft);
  font-variant-numeric: tabular-nums;
  margin-top: 3px;
}
.sp-row__meta > * + *::before { content: "\00B7"; margin-right: 10px; opacity: .6; }
.sp-row__trail { flex: 0 0 auto; color: var(--sp-ink-soft); }
.sp-row--dense { padding: var(--sp-space-2) var(--sp-space-3); }
.sp-row--dense .sp-row__lead { width: 32px; height: 32px; }
.sp-list { display: flex; flex-direction: column; gap: var(--sp-space-2); }

/* ---------- Alert ----------
   Eight pages roll their own error box. An alert is two parts: what happened,
   and what to do next — see DESIGN_STANDARD §8. */
.sp-alert {
  display: flex; align-items: flex-start; gap: var(--sp-space-3);
  padding: var(--sp-space-3) var(--sp-space-4);
  border-radius: var(--sp-radius-sm);
  border: 1px solid var(--sp-rule);
  background: var(--sp-paper-2);
  color: var(--sp-ink-2);
  font-family: var(--sp-font-ui); font-size: 13.5px; line-height: 1.5;
}
.sp-alert__icon { flex: 0 0 auto; margin-top: 1px; }
.sp-alert__body { min-width: 0; flex: 1; }
.sp-alert__title { font-weight: 600; color: var(--sp-ink); margin-bottom: 2px; }
.sp-alert--error {
  background: color-mix(in oklch, var(--sp-coral) 8%, var(--sp-paper));
  border-color: color-mix(in oklch, var(--sp-coral) 32%, var(--sp-rule));
}
.sp-alert--error .sp-alert__icon { color: var(--sp-coral); }
.sp-alert--warn {
  background: var(--sp-coral-soft);
  border-color: color-mix(in oklch, var(--sp-coral) 35%, var(--sp-rule));
}
.sp-alert--warn .sp-alert__icon { color: var(--sp-coral); }
.sp-alert--success {
  background: var(--sp-pine-soft);
  border-color: color-mix(in oklch, var(--sp-pine) 35%, var(--sp-rule));
}
.sp-alert--success .sp-alert__icon { color: var(--sp-pine-text); }

/* ---------- Skeleton ----------
   Seven pages fake a loading state. A skeleton says "this is arriving, and
   roughly this shape"; a spinner only says "something is happening". */
.sp-skeleton {
  display: block;
  border-radius: var(--sp-radius-xs);
  background: linear-gradient(
    90deg,
    var(--sp-paper-2) 0%,
    color-mix(in oklch, var(--sp-paper-edge) 60%, var(--sp-paper-2)) 50%,
    var(--sp-paper-2) 100%);
  background-size: 200% 100%;
  animation: spShimmer 1.4s linear infinite;
}
@keyframes spShimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
.sp-skeleton--title { height: 1.6em; width: 60%; }
.sp-skeleton--line { height: 1em; width: 100%; margin: .25em 0; }
.sp-skeleton--line-short { height: 1em; width: 40%; margin: .25em 0; }
.sp-skeleton--block { height: 96px; border-radius: var(--sp-radius-md); }
.sp-skeleton--circle { border-radius: 50%; width: 40px; height: 40px; }

/* ---------- Meter ----------
   Label, count, bar. Five pages have one, each with its own thresholds. */
.sp-meter__label {
  display: flex; justify-content: space-between; gap: var(--sp-space-3);
  font-family: var(--sp-font-ui); font-size: 12.5px; font-weight: 600;
  color: var(--sp-ink-soft); margin-bottom: 5px;
}
.sp-meter__count { color: var(--sp-ink); font-variant-numeric: tabular-nums; }
.sp-meter__track {
  height: 7px; border-radius: 999px; overflow: hidden;
  background: var(--sp-paper-2);
  border: 1px solid var(--sp-rule-soft);
}
.sp-meter__fill {
  height: 100%; border-radius: 999px;
  background: var(--sp-pine);
  transition: width var(--sp-dur-slow) var(--sp-ease-out);
}
.sp-meter__fill--warn { background: var(--sp-coral); }
.sp-meter__fill--danger { background: var(--sp-coral); }
/* At the limit the count also changes weight, so "you are out" does not
   depend on seeing the bar's hue (WCAG 1.4.1). */
.sp-meter.is-danger .sp-meter__count { color: var(--sp-coral); font-weight: 700; }

/* ---------- Tabs / segmented control ---------- */
.sp-tabs {
  display: inline-flex; padding: 3px; gap: 2px;
  background: var(--sp-paper-2);
  border: 1px solid var(--sp-rule-soft);
  border-radius: 999px;
}
.sp-tab {
  appearance: none; border: 0; background: none; cursor: pointer;
  min-height: 44px; padding: 0 var(--sp-space-4);
  border-radius: 999px;
  font-family: var(--sp-font-ui); font-size: 13.5px; font-weight: 600;
  color: var(--sp-ink-soft);
  transition: background var(--sp-dur-fast) ease, color var(--sp-dur-fast) ease;
}
.sp-tab:hover { color: var(--sp-ink); }
.sp-tab[aria-selected="true"], .sp-tab.is-active {
  background: var(--sp-coral); color: var(--sp-on-coral);
}
.sp-tab:focus-visible {
  outline: var(--sp-focus-width, 3px) solid var(--sp-focus);
  outline-offset: var(--sp-focus-offset, 2px);
}

/* ---------- Sheet ----------
   A bottom sheet on a phone, a centred dialog on a wide screen. Four pages
   have their own modal and none of them had a grab handle, so the affordance
   that says "this can be dismissed" lives here now. */
.sp-sheet-backdrop {
  position: fixed; inset: 0; z-index: 1000;
  background: color-mix(in oklch, var(--sp-ink) 45%, transparent);
  backdrop-filter: blur(2px);
  opacity: 0; pointer-events: none;
  transition: opacity var(--sp-dur-base) ease;
}
.sp-sheet-backdrop.is-open { opacity: 1; pointer-events: auto; }
.sp-sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 1001;
  max-height: 88dvh; overflow-y: auto;
  background: var(--sp-paper);
  border-top: 1px solid var(--sp-rule);
  border-radius: var(--sp-radius-xl) var(--sp-radius-xl) 0 0;
  padding: var(--sp-space-4) var(--sp-space-5)
           calc(var(--sp-space-6) + env(safe-area-inset-bottom, 0));
  box-shadow: var(--sp-lift-3);
  transform: translateY(100%);
  transition: transform var(--sp-dur-slow) var(--sp-ease-out);
}
.sp-sheet.is-open { transform: translateY(0); }
.sp-sheet__handle {
  width: 40px; height: 4px; border-radius: 999px;
  background: var(--sp-rule);
  margin: 0 auto var(--sp-space-4);
}
.sp-sheet__title {
  font-family: var(--sp-font-display); font-weight: 600; font-size: 20px;
  color: var(--sp-ink); margin-bottom: var(--sp-space-2);
}
@media (min-width: 640px) {
  .sp-sheet {
    left: 50%; bottom: auto; top: 50%;
    max-width: 520px; width: calc(100% - 48px);
    border-radius: var(--sp-radius-lg);
    border: 1px solid var(--sp-rule);
    transform: translate(-50%, -40%);
    opacity: 0;
  }
  .sp-sheet.is-open { transform: translate(-50%, -50%); opacity: 1; }
  .sp-sheet__handle { display: none; }
}

/* ---------- Empty state, with somewhere to go ----------
   `.sp-empty` existed but was type only. Seven pages needed an icon and an
   action, so seven pages wrote their own. An empty state that does not say how
   to put the first thing in is half a screen. */
.sp-empty__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 52px; height: 52px; margin-bottom: var(--sp-space-4);
  border-radius: 50%;
  background: var(--sp-paper-2);
  border: 1px solid var(--sp-rule-soft);
  color: var(--sp-ink-soft);
}
.sp-empty__title {
  font-family: var(--sp-font-display); font-weight: 500;
  font-size: clamp(20px, 3.5vw, 26px); line-height: 1.25;
  color: var(--sp-ink); margin-bottom: var(--sp-space-2);
}
.sp-empty__body {
  font-family: var(--sp-font-ui); font-size: 15px; line-height: 1.55;
  color: var(--sp-ink-2); max-width: 44ch; margin: 0 auto var(--sp-space-5);
}
.sp-empty__actions {
  display: flex; gap: var(--sp-space-3); justify-content: center; flex-wrap: wrap;
}

/* ---------- Reduced motion, for everything added above ---------- */
@media (prefers-reduced-motion: reduce) {
  .sp-row, .sp-sheet, .sp-sheet-backdrop, .sp-meter__fill, .sp-tab,
  .sp-field__control, .sp-field input, .sp-field select, .sp-field textarea {
    transition: opacity .12s ease;
  }
  .sp-skeleton { animation: none; }
  a.sp-row:hover, button.sp-row:hover { transform: none; }
}
