
@layer normalize, base, components, app, base-theme, theme;
/*
 * The Civility framework stylesheet. Declares the full layer order (so an app's
 * `app.css` and `theme.css` slot in correctly) but bundles only normalize +
 * base + components + the default theme.
 *
 * The order splits at `app`: **everything below it is rules, everything above
 * it is tokens.** That is what makes a drop-in theme possible — an app can't
 * redefine `:root` tokens from its structural layer and quietly out-rank the
 * designer, and a theme can restate a colour without fighting the app's grid.
 *
 * - `normalize`  browser smoothing
 * - `base`       classless semantic styling
 * - `components` `ui-*` / `civ-*` element rules, plus the shared class
 *                vocabulary in `_patterns.css`
 * - `app`        the app's own structural and domain CSS
 * - `base-theme` the framework's default tokens
 * - `theme`      the app's or designer's tokens and skin
 *
 * Unlayered CSS beats all of it, and is the app's deliberate escape hatch.
 * See `packages/ui/README.md` and spec §6.5.2.
 */
@layer normalize {
  /* 1. Use a more-intuitive box-sizing model */
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  /* 2. Remove default margin */
  *:not(dialog) {
    margin: 0;
  }

  /* 3. Enable keyword animations */
  @media (prefers-reduced-motion: no-preference) {
    html {
      interpolate-size: allow-keywords;
    }
  }

  body {
    /* 4. Increase line-height */
    line-height: 1.5;
    /* 5. Improve text rendering */
    -webkit-font-smoothing: antialiased;
  }

  /* 6. Improve media defaults */
  img,
  picture,
  video,
  canvas,
  svg {
    display: block;
    max-width: 100%;
  }

  /* 7. Inherit fonts for form controls */
  input,
  button,
  textarea,
  select {
    font: inherit;
  }

  /* 8. Avoid text overflows */
  p,
  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    overflow-wrap: break-word;
  }

  /* 9. Improve line wrapping */
  p {
    text-wrap: pretty;
  }
  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    text-wrap: balance;
  }

  /*
    10. Create a root stacking context
  */
  #root,
  #__next {
    isolation: isolate;
  }

  /*
    11. iOS touch behavior on controls.

    Safari reads a fast second tap as a double tap: it selects whatever is
    underneath and raises its own "Copy / Look Up" bar over the UI. Scoped to
    controls on purpose — a blanket `user-select: none` would make body text
    unselectable, which is the wrong default for a framework built on semantic
    documents. `touch-action: manipulation` also drops the 300ms click delay.

    This is only the CSS half. Suppressing the callout during rapid tapping
    additionally needs a `touchstart` listener calling `preventDefault()` with
    `{ passive: false }` — `pointerdown` does not suppress it — which has to be
    scoped by the app, since doing it broadly would break scrolling.

    Chrome drops `-webkit-touch-callout` from `cssRules` entirely, so it cannot
    verify this rule; check on a real iPhone.
  */
  button,
  a,
  label,
  summary,
  [role='button'],
  [role='tab'],
  input[type='button'],
  input[type='submit'],
  input[type='checkbox'],
  input[type='radio'] {
    touch-action: manipulation;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -moz-user-select: none;
         user-select: none;
  }

  :root {
    -webkit-tap-highlight-color: transparent;
  }
}
/* Accessibility improvements */
/* A slow-refresh panel — e-ink and e-paper — repaints in 100–800ms and ghosts,
   so an animation is either invisible or a smear. `update: slow` is the media
   feature for exactly this hardware, and it applies whether or not the app has
   linked `themes/eink.css`; the theme handles colour and weight, this handles
   motion. Kept separate from `prefers-reduced-motion` on purpose: that is a
   stated preference, this is a property of the display. */
@media (update: slow) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
@keyframes spin {
  0% {
    transform: rotate(0deg);
  }

  100% {
    transform: rotate(360deg);
  }
}
@layer base {
  :root {
    --black: hsl(var(--blackH), var(--blackS), var(--blackL));
    --gray: hsl(var(--grayH), var(--grayS), var(--grayL));
    --white: hsl(var(--whiteH), var(--whiteS), var(--whiteL));
    --blue: hsl(var(--blueH), var(--blueS), var(--blueL));
    --green: hsl(var(--greenH), var(--greenS), var(--greenL));
    --yellow: hsl(var(--yellowH), var(--yellowS), var(--yellowL));
    --orange: hsl(var(--orangeH), var(--orangeS), var(--orangeL));
    --red: hsl(var(--redH), var(--redS), var(--redL));
    --pink: hsl(var(--pinkH), var(--pinkS), var(--pinkL));
    --purple: hsl(var(--purpleH), var(--purpleS), var(--purpleL));

    --error: hsl(var(--errorH), var(--errorS), var(--errorL));
    --error-dull: hsl(var(--errorH), var(--errorS), var(--errorL), 0.2);

    --warning: hsl(var(--warningH), var(--warningS), var(--warningL));
    --warning-dull: hsl(var(--warningH), var(--warningS), var(--warningL), 0.2);

    --info: hsl(var(--infoH), var(--infoS), var(--infoL));
    --info-dull: hsl(var(--infoH), var(--infoS), var(--infoL), 0.2);

    --success: hsl(var(--successH), var(--successS), var(--successL));
    --success-dull: hsl(var(--successH), var(--successS), var(--successL), 0.2);

    --background: hsl(
      var(--backgroundH),
      var(--backgroundS),
      var(--backgroundL)
    );
    --background-dull: hsl(
      var(--backgroundH),
      var(--backgroundS),
      var(--backgroundL),
      0.2
    );

    /* The ground *behind* `--background`, for apps that float their content as
       cards on a recessed page (the iOS grouped-list idiom). Only the lightness
       is a primitive: hue and saturation track `--background`, so a theme sets
       one number, and setting it equal to `--backgroundL` flattens the effect
       entirely — which is what a theme with no backlight wants. */
    --surface-recessed: hsl(
      var(--backgroundH),
      var(--backgroundS),
      var(--surface-recessedL, var(--backgroundL))
    );

    --body: hsl(var(--bodyH), var(--bodyS), var(--bodyL));
    --body-dull: hsl(var(--bodyH), var(--bodyS), var(--bodyL), 0.2);

    --secondary: hsl(var(--secondaryH), var(--secondaryS), var(--secondaryL));
    --secondary-dull: hsl(
      var(--secondaryH),
      var(--secondaryS),
      var(--secondaryL),
      0.2
    );

    --primary: hsl(var(--primaryH), var(--primaryS), var(--primaryL));
    --primary-dull: hsl(var(--primaryH), var(--primaryS), var(--primaryL), 0.2);

    /* Neutral UI tokens. `--muted` (secondary text) tracks `--secondary`, so it
       adapts in dark mode for free. `--border` (hairline) composes from its own
       primitives, defaulting to `--secondary`'s hue/sat when a theme omits them
       (see `themes/default.css` for the light/dark `--borderL`). */
    --muted: var(--secondary);
    --border: hsl(
      var(--borderH, var(--secondaryH)),
      var(--borderS, var(--secondaryS)),
      var(--borderL, var(--secondaryL))
    );
  }
}
@layer base {
  /* Forms */
  form {
    margin: 0 0 var(--s4) 0;
  }

  fieldset {
    border: var(--border-width) solid currentColor;
    border-radius: var(--br-base);
    padding: var(--s3);
    margin: 0 0 var(--s3) 0;
  }

  legend {
    font-weight: var(--fw-semibold);
    padding: 0 var(--s2);
    opacity: 1;
  }

  label {
    display: block;
    font-weight: var(--fw-medium);
    margin-bottom: var(--s2);
  }

  input[type='text'],
  input[type='email'],
  input[type='password'],
  input[type='number'],
  input[type='search'],
  input[type='url'],
  input[type='tel'],
  textarea,
  select {
    /* Without these two the control keeps the UA's own canvas — white with
       black text — so every text field in a dark theme rendered as a white
       block. `transparent` picks up whatever surface it sits on (page,
       dialog, card) rather than guessing at a token. */
    background-color: transparent;
    color: inherit;
    padding: var(--s3);
    border: var(--border-width) solid currentColor;
    border-radius: var(--br-base);
    font-size: var(--f5);
    line-height: var(--lh-base);
    transition:
      border-color var(--transition-base),
      box-shadow var(--transition-base),
      opacity var(--transition-base);
    margin-bottom: var(--s3);
    box-sizing: border-box;
  }

  input:focus,
  textarea:focus,
  select:focus {
    outline: 2px solid currentColor;
    /* box-shadow: 0 0 0 3px currentColor; */
    outline-offset: 2px;
    opacity: 1;
  }

  /* Checkboxes, radios, range thumbs and progress bars are painted by the UA,
     which uses its own blue unless told otherwise. `color-scheme` alone only
     picks light or dark — it does not carry the theme's hue, so a burgundy or
     purple theme was still getting a browser-blue tick. */
  input[type='checkbox'],
  input[type='radio'],
  input[type='range'],
  progress {
    accent-color: var(--primary);
  }

  button,
  input[type='submit'],
  input[type='button'] {
    display: inline-block;
    padding: var(--s2) var(--s2);
    background: none;
    color: inherit;
    border: var(--border-width) solid currentColor;
    border-radius: var(--br-base);
    font-size: var(--f5);
    font-weight: var(--fw-medium);
    cursor: pointer;
    /* Elevation is a token because it is an *idiom*, not a value: a soft blur
       and a hard offset are different design languages, and an app whose
       buttons are chunky had to write its own `button` rule to say so — which
       a theme from another app then cannot carry. Defaults to `none`, so this
       changes nothing until a theme opts in. */
    box-shadow: var(--button-shadow, none);
    transition:
      box-shadow var(--transition-base),
      opacity var(--transition-base),
      transform var(--transition-fast);
    text-decoration: none;
  }

  button:hover,
  input[type='submit']:hover,
  input[type='button']:hover {
    opacity: 0.8;
    transform: translateY(-1px);
  }

  button:active,
  input[type='submit']:active,
  input[type='button']:active {
    transform: translateY(0);
  }

  button:disabled,
  input[type='submit']:disabled,
  input[type='button']:disabled {
    opacity: 0.3;
    cursor: not-allowed;
    transform: none;
  }
}
@layer base {
  /* ─── CSS Variables ──────────────────────────────────────────────────────── */

  :root {
    --nav-drawer-duration: 250ms;
    --nav-footer-height: 72px;

    /* Display cutouts. 0 in a browser tab and on devices without them, so
       never use one as an entire padding — compose, or wrap in max(). Only
       non-zero when index.html sets `viewport-fit=cover`. */
    --safe-top: env(safe-area-inset-top, 0px);
    --safe-right: env(safe-area-inset-right, 0px);
    --safe-bottom: env(safe-area-inset-bottom, 0px);
    --safe-left: env(safe-area-inset-left, 0px);
  }

  /* ─── Body Grid Layout ──────────────────────────────────────────────────── */

  body {
    margin: 0;
    min-height: 100vh;
    display: grid;
    grid-template-columns: var(--nav-width, auto) 1fr var(--aside-width, 0);
    grid-template-rows: auto auto auto auto 1fr auto auto;
    grid-template-areas:
      'banner  banner  banner'
      'header  header  header'
      'subhead subhead subhead'
      'navhd   mainhd  aside'
      'nav     main    aside'
      'navft   mainft  aside'
      'footer  footer  footer';
    background-color: var(--background);
    color: var(--body);

    /* Space each landmark actually occupies, cutout included — what sticky
       offsets below must clear. */
    --header-block: calc(var(--header-height, 56px) + var(--safe-top));
    --footer-block: calc(var(--footer-height, 56px) + var(--safe-bottom));
  }

  body:has(footer[fixed]) {
    padding-bottom: var(--footer-block);
  }

  /* ─── Grid area assignments ──────────────────────────────────────────────── */
  /* Scoped to direct descendants of body to avoid styling nested landmarks */

  body > header {
    grid-area: header;
  }

  body > nav {
    grid-area: nav;
  }

  body > main {
    grid-area: main;
  }

  body > aside {
    grid-area: aside;
  }

  body > footer {
    grid-area: footer;
  }

  /* ─── Header ─────────────────────────────────────────────────────────────── */

  body > header {
    position: sticky;
    top: var(--banner-height, 0px);
    z-index: var(--z-sticky);
    margin: 0;
    padding: var(--safe-top) var(--safe-right) 0 var(--safe-left);
    /* min-height, not height: the app owns --header-height, and the cutout is
       added on top of it rather than eaten out of it. */
    min-height: var(--header-block);
    box-sizing: border-box;
    background: var(--background);
  }

  /* ─── Nav (vertical sidebar) ─────────────────────────────────────────────── */

  body > nav {
    display: flex;
    flex-direction: column;
    gap: var(--s1);
    padding: var(--s3) var(--s2);
    position: sticky;
    top: calc(var(--banner-height, 0px) + var(--header-block));
    max-height: calc(
      100vh - var(--banner-height, 0px) - var(--header-block)
      - var(--footer-height, 76px) - var(--safe-bottom)
    );
    overflow-y: auto;
    align-self: start;
    padding-bottom: calc(var(--nav-footer-height) + var(--s3));
  }

  /* When no footer exists, remove the extra padding */
  body:not(:has(ui-nav-footer)) > nav {
    padding-bottom: var(--s3);
  }

  body > nav ul {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--s1);
  }

  body > nav li {
    position: relative;
  }

  body > nav a {
    display: block;
    text-decoration: none;
    color: inherit;
    font-weight: var(--fw-medium);
    padding: var(--s2) var(--s3);
    border-radius: var(--br-base);
    transition: opacity var(--transition-base);
  }

  body > nav a:hover {
    opacity: 0.7;
  }

  body > nav a[aria-current='page'] {
    opacity: 0.5;
    cursor: default;
  }

  /* Nested nav lists (sub-sections) */
  body > nav ul ul {
    padding-left: var(--s3);
    border-left: var(--border-width) solid currentColor;
    margin-left: var(--s3);
    margin-top: var(--s1);
  }

  /* ─── Main ───────────────────────────────────────────────────────────────── */

  body > main {
    padding: var(--main-padding, 0);
    max-width: var(--main-max-width, var(--max-width-lg));
    margin: 0 auto;
    width: 100%;
    box-sizing: border-box;
  }

  /* ─── Aside ──────────────────────────────────────────────────────────────── */

  body > aside {
    padding: var(--aside-padding, var(--s3) var(--s2));
    position: sticky;
    top: calc(var(--banner-height, 0px) + var(--header-block));
    max-height: calc(100vh - var(--banner-height, 0px) - var(--header-block));
    overflow-y: auto;
    align-self: start;
  }

  /* ─── Footer ─────────────────────────────────────────────────────────────── */

  body > footer {
    padding: var(--footer-padding, var(--s3) var(--container-padding));
    background: var(--background);
    border-top: var(--border-width) solid currentColor;
  }

  body > footer[fixed] {
    grid-area: unset;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: var(--z-fixed);
    border-top: var(--border-width) solid currentColor;
    /* Override --footer-padding-bottom too if you override --footer-padding. */
    padding-bottom: calc(
      var(--footer-padding-bottom, var(--s3)) + var(--safe-bottom)
    );
    padding-left: max(var(--safe-left), var(--container-padding));
    padding-right: max(var(--safe-right), var(--container-padding));
    min-height: var(--footer-block);
    box-sizing: border-box;
  }

  /* ─── Page Layout Components ──────────────────────────────────────────────────── */

  /* Banner: sits above the header; for announcements, promo bars, env notices.
     Sticky by default at top: 0. Set --banner-height to match its rendered
     height so that header's sticky offset (top: var(--banner-height)) is correct. */
  ui-banner {
    display: block;
    grid-area: banner;
    position: sticky;
    top: 0;
    z-index: var(--z-sticky);
    background: var(--background);
  }

  /* Sub-header: sits below the main header; breadcrumbs, tab bars, filter rows.
     Sticky below both banner and header. Set --banner-height and --header-height
     to match their rendered heights so stacking offsets are correct. */
  ui-sub-header {
    display: block;
    grid-area: subhead;
    position: sticky;
    top: calc(
      var(--banner-height, 0px) + var(--header-height, 0px) + var(--safe-top)
    );
    z-index: calc(var(--z-sticky) - 1);
    background: var(--background);
  }

  /* Nav header/footer: header and footer zones within the nav sidebar column.
     Both are sticky so they remain visible as the nav itself scrolls.
     ui-nav-header pins to the top of the sticky area (below page header);
     ui-nav-footer pins to the bottom of the viewport. */
  ui-nav-header {
    display: block;
    grid-area: navhd;
    padding: var(--s3) var(--s2) var(--s2);
    position: sticky;
    top: calc(var(--banner-height, 0px) + var(--header-block));
    z-index: var(--z-sticky);
    background: var(--background);
    align-self: start;
  }

  ui-nav-footer {
    display: block;
    grid-area: navft;
    padding: var(--s2) var(--s2) var(--s3);
    position: sticky;
    bottom: calc(var(--footer-height, 0px) + var(--safe-bottom));
    z-index: var(--z-sticky);
    background: var(--background);
    align-self: end;
  }

  /* Main header/footer: header and footer zones within the main content column. */
  ui-main-header {
    display: block;
    grid-area: mainhd;
    padding: var(--s3) var(--container-padding) var(--s2);
  }

  ui-main-footer {
    display: block;
    grid-area: mainft;
    padding: var(--s2) var(--container-padding) var(--s3);
  }

  /* ─── Mobile drawer with CSS-only navigation toggle ──────────────────────── */

  /* Hide the checkbox that controls the drawer */
  #nav-toggle {
    display: none;
  }

  /* ui-nav-toggle: Hamburger menu button component (label for checkbox)
     Place this anywhere, typically in your header. Hidden on desktop. */
  ui-nav-toggle {
    display: none;
    /* Hidden by default, shown on mobile */
  }

  /* Style the label inside ui-nav-toggle as a hamburger button */
  ui-nav-toggle label[for='nav-toggle'] {
    display: flex;
    width: 44px;
    height: 44px;
    cursor: pointer;
    background: transparent;
    border: var(--border-width) solid currentColor;
    border-radius: var(--br-base);
    padding: 0;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    gap: 4px;
    transition: opacity var(--transition-base);
  }

  ui-nav-toggle label[for='nav-toggle']:hover {
    opacity: 0.7;
  }

  /* Hamburger icon lines */
  ui-nav-toggle label[for='nav-toggle']::before,
  ui-nav-toggle label[for='nav-toggle']::after,
  ui-nav-toggle label[for='nav-toggle'] span {
    content: '';
    display: block;
    width: 20px;
    height: 2px;
    background: currentColor;
    transition: all 0.3s ease;
  }

  ui-nav-toggle label[for='nav-toggle'] span {
    pointer-events: none;
  }

  /* Transform hamburger to X when drawer is open */
  #nav-toggle:checked ~ header ui-nav-toggle label[for='nav-toggle']::before,
  #nav-toggle:checked ~ * ui-nav-toggle label[for='nav-toggle']::before {
    transform: translateY(6px) rotate(45deg);
  }

  #nav-toggle:checked ~ header ui-nav-toggle label[for='nav-toggle'] span,
  #nav-toggle:checked ~ * ui-nav-toggle label[for='nav-toggle'] span {
    opacity: 0;
  }

  #nav-toggle:checked ~ header ui-nav-toggle label[for='nav-toggle']::after,
  #nav-toggle:checked ~ * ui-nav-toggle label[for='nav-toggle']::after {
    transform: translateY(-6px) rotate(-45deg);
  }

  /* Backdrop overlay when drawer is open - also a label to close on click */
  label.nav-backdrop {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100vw;
    height: 100vh;
    background: var(--backdrop-color);
    z-index: calc(var(--z-fixed) + 1);
    cursor: pointer;
    opacity: 0;
    transition: var(--nav-drawer-duration) allow-discrete;
    transition-property: display, opacity, overlay;
  }

  #nav-toggle:checked ~ label.nav-backdrop {
    display: block;
    opacity: 1;
  }

  @starting-style {
    #nav-toggle:checked ~ label.nav-backdrop {
      opacity: 0;
    }
  }

  /* ─── Mobile: collapse sidebar, show drawer ─────────────────────────────── */

  @media (max-width: 900px) {
    body {
      grid-template-columns: 1fr;
      grid-template-rows: auto auto auto auto 1fr auto auto;
      grid-template-areas:
        'banner'
        'header'
        'subhead'
        'mainhd'
        'main'
        'mainft'
        'footer';
    }

    /* Hide sidebar components by default on mobile */
    body > nav,
    body > aside,
    ui-nav-header,
    ui-nav-footer {
      display: none;
    }

    /* Show nav toggle button on mobile */
    ui-nav-toggle {
      display: block;
    }

    /* Show nav as drawer when checkbox is checked */
    body > nav {
      position: fixed;
      top: 0;
      left: 0;
      bottom: 0;
      height: 100vh;
      width: min(80vw, 320px);
      z-index: calc(var(--z-fixed) + 2);
      background: var(--background);
      box-shadow: 4px 0 12px var(--drawer-shadow-color);
      overflow-y: auto;
      max-height: calc(100vh - var(--nav-header-height, 56px));
      padding: var(--s3);
      padding-top: calc(
        var(--banner-height, 0px) + var(--header-block) + var(--s3)
      );
      padding-bottom: calc(var(--nav-footer-height) + var(--s3));
      transform: translateX(-100%);
      transition: var(--nav-drawer-duration) allow-discrete;
      transition-property: display, transform;
    }

    body:not(:has(ui-nav-footer)) > nav {
      max-height: calc(100vh);
    }

    #nav-toggle:checked ~ nav {
      display: flex;
      transform: translateX(0);
    }

    @starting-style {
      #nav-toggle:checked ~ nav {
        transform: translateX(-100%);
      }
    }

    /* Show ui-nav-header in drawer */
    ui-nav-header {
      position: fixed;
      top: var(--banner-height, 0px);
      left: 0;
      width: min(80vw, 320px);
      height: var(--header-block);
      padding-top: var(--safe-top);
      box-sizing: border-box;
      z-index: calc(var(--z-fixed) + 3);
      background: var(--background);
      border-bottom: var(--border-width) solid currentColor;
      align-items: center;
      transform: translateX(-100%);
      transition: var(--nav-drawer-duration) allow-discrete;
      transition-property: display, transform;
    }

    #nav-toggle:checked ~ ui-nav-header {
      display: flex;
      transform: translateX(0);
    }

    @starting-style {
      #nav-toggle:checked ~ ui-nav-header {
        transform: translateX(-100%);
      }
    }

    /* Show ui-nav-footer in drawer */
    ui-nav-footer {
      position: fixed;
      bottom: 0;
      left: 0;
      width: min(80vw, 320px);
      height: var(--nav-footer-height);
      z-index: calc(var(--z-fixed) + 3);
      background: var(--background);
      border-top: var(--border-width) solid currentColor;
      align-items: center;
      transform: translateX(-100%);
      transition: var(--nav-drawer-duration) allow-discrete;
      transition-property: display, transform;
    }

    #nav-toggle:checked ~ ui-nav-footer {
      display: flex;
      transform: translateX(0);
    }

    @starting-style {
      #nav-toggle:checked ~ ui-nav-footer {
        transform: translateX(-100%);
      }
    }
  }
}
@layer base {
  /* Typography */
  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    margin: 0 0 var(--s3) 0;
    font-weight: var(--fw-semibold);
    line-height: var(--lh-tight);
  }

  h1 {
    font-size: var(--f1);
  }

  h2 {
    font-size: var(--f2);
  }

  h3 {
    font-size: var(--f3);
  }

  h4 {
    font-size: var(--f4);
  }

  h5 {
    font-size: var(--f5);
  }

  h6 {
    font-size: var(--f6);
  }

  /* Links */
  a {
    color: var(--primary);
    text-decoration: underline;
    transition: color var(--transition-base);
  }

  @media (max-width: 768px) {
    h1 {
      font-size: var(--f3);
    }

    h2 {
      font-size: var(--f4);
    }

    h3 {
      font-size: var(--f4);
    }
  }

  @media (max-width: 480px) {
    main {
      padding: var(--s3);
    }

    h1 {
      font-size: var(--f3);
    }

    h2 {
      font-size: var(--f4);
    }
  }
}
@layer base {
  /* Chrome that only makes sense in a browser tab — a link back to a hub, a
     site header, an install hint. `data-display` is set during parse by the
     inline script in `index.html` (see the `civ init` stubs), not from the
     bundle, so this never flashes before it is hidden. */
  [data-display='standalone'] [data-browser-only] {
    display: none;
  }

  /* Document structure */
  html {
    /* Native controls — text fields, selects, checkboxes, scrollbars — render
       from the UA's own palette, which tracks this and nothing else. Without
       it they stay in light colours on a dark page however the tokens read.
       Lives in `base`, not `base-theme`: it is browser behaviour rather than a
       token, and a dark-only app needs to be able to override it.
       `modules/theme.ts` sets an inline `color-scheme` when the user picks a
       theme explicitly; this covers the device-follows case. */
    color-scheme: light dark;
    font-family: var(--font-family-base);
    font-size: var(--f5);
    line-height: var(--lh-base);
    height: 100%;
    color: var(--body);
    background-color: var(--background);
  }

  main {
    margin: auto;
  }

  /* Body grid layout moved to landmarks.css for better organization */

  p {
    margin: 0 0 var(--s3) 0;
    line-height: var(--lh-relaxed);
    text-wrap: balance;
  }

  /* Lists */
  ul,
  ol {
    margin: 0 0 var(--s3) 0;
    padding: var(--s2) 0 var(--s2) var(--s4);
  }

  li {
    margin-bottom: var(--s2);
  }

  /* Tables */
  table {
    width: 100%;
    border-collapse: collapse;
    margin: 0 0 var(--s4) 0;
  }

  th,
  td {
    text-align: left;
    padding: var(--s3) var(--s3);
    border-bottom: var(--border-width) solid currentColor;
    opacity: 0.9;
  }

  th {
    font-weight: var(--fw-semibold);
    opacity: 1;
  }

  /* Code */
  code,
  kbd,
  samp {
    font-family: var(--font-family-mono);
    font-size: 0.9em;
    border: var(--border-width) solid currentColor;
    padding: 0.2em 0.4em;
    border-radius: var(--br-sm);
  }

  pre {
    border: var(--border-width) solid currentColor;
    padding: var(--s3);
    border-radius: var(--br-base);
    overflow-x: auto;
    margin: 0 0 var(--s3) 0;
  }

  pre code {
    border: none;
    opacity: 1;
    padding: 0;
  }

  /* ARIA-aware styling */
  /* Tab interface */
  [role='tablist'] {
    display: flex;
    border-bottom: var(--border-width) solid currentColor;
    opacity: 0.8;
    margin-bottom: var(--s3);
  }

  [role='tab'] {
    padding: var(--s3) var(--s3);
    background: none;
    border: none;
    border-radius: 0;
    border-bottom: 2px solid transparent;
    cursor: pointer;
    font-weight: var(--fw-medium);
    opacity: 1;
    transition: all var(--transition-base);
  }

  [role='tab']:hover {
    opacity: 1;
  }

  [role='tab'][aria-selected='true'] {
    opacity: 1;
    border-bottom-color: currentColor;
  }

  [role='tabpanel'] {
    padding: var(--s3) 0;
  }

  [role='tabpanel'][hidden] {
    display: none;
  }

  /* Details/Summary */
  details {
    margin: 0 0 var(--s2) 0;
    border-radius: var(--br-base);
  }

  summary {
    padding: var(--s2);
    cursor: pointer;
    font-weight: var(--fw-medium);
    border-radius: var(--br-base);
    transition: opacity var(--transition-base);
    opacity: 1;
  }

  summary:hover {
    opacity: 0.7;
  }

  details[open] summary {
    border-radius: var(--br-base) var(--br-base) 0 0;
  }

  details > *:not(summary):not(ul) {
    padding: var(--s2);
  }

  /* Focus styles */
  :focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 2px;
  }

  /* Skip to main content link */
  .skip-to-main {
    position: absolute;
    top: -40px;
    left: 6px;
    background: currentColor;
    color: var(--primary-contrast);
    padding: 8px;
    text-decoration: none;
    border-radius: var(--br-base);
    z-index: var(--z-tooltip);
  }

  .skip-to-main:focus {
    top: 6px;
  }
}
/**
 * Basic Theme Variables
 * Defines foundational design tokens for the styles framework
 */
@layer base-theme {
  :root {
    /* Typography */
    --font-family-base:
      system-ui,
      -apple-system,
      BlinkMacSystemFont,
      'Segoe UI',
      Roboto,
      Helvetica,
      Arial,
      sans-serif;
    --font-family-mono:
      ui-monospace,
      SFMono-Regular,
      Consolas,
      'Liberation Mono',
      Menlo,
      monospace;

    /* FONT SIZE */
    --f-headline: 6rem;
    --f-subheadline: 5rem;
    --f1: 3rem;
    --f2: 2.25rem;
    --f3: 1.5rem;
    --f4: 1.25rem;
    --f5: 1rem;
    --f6: 0.875rem;
    --f7: 0.75rem;

    /* SPACING */
    --s0: 0;
    --s1: 0.25rem;
    --s2: 0.5rem;
    --s3: 1rem;
    --s4: 2rem;
    --s5: 3rem;
    --s6: 4rem;
    --s7: 6rem;

    /* Layout */
    --max-width-sm: 640px;
    --max-width-md: 768px;
    --max-width-lg: 1024px;
    --max-width-xl: 1280px;
    --max-width-2xl: 1536px;

    /* Sticky offset helpers — update to match your actual rendered heights.
       These drive the top: calc(...) offsets for nav, aside, and ui-sub-header. */
    --banner-height: 0px;
    --header-height: 56px;
    --sub-header-height: 0px;

    /* FONT WEIGHT */
    --fw-normal: 400;
    --fw-medium: 500;
    --fw-semibold: 600;
    --fw-bold: 700;

    /* LINE HEIGHT */
    --lh-tight: 1.2;
    --lh-base: 1.5;
    --lh-relaxed: 1.7;

    /* BORDER RADIUS */
    /* Hairline weight. A token because a reflective or high-density display
       needs a thicker line to register at all — see themes/eink.css. */
    --border-width: 1px;

    /* The wash behind a modal. Was a literal rgba(0,0,0,.5), which no theme
       could reach. */
    --backdrop-color: rgb(0 0 0 / 0.5);

    /* Drawers cast sideways, so they can't use the downward `--shadow-*`
       scale. Separate token so a flat theme can switch it off. */
    --drawer-shadow-color: rgb(0 0 0 / 0.15);

    /* Text on a filled `--primary` surface. Was `#fff`, which is wrong for any
       light-primary theme and unreachable from one. */
    --primary-contrast: hsl(var(--whiteH), var(--whiteS), var(--whiteL));

    /* Focus ring. Was a hardcoded blue — unreachable, and invisible in any
       greyscale theme. */
    --focus-ring-color: var(--primary-dull);
    --focus-ring-width: 3px;

    --br-none: 0;
    --br-sm: 0.25rem;
    --br-base: 0.5rem;
    --br-lg: 0.75rem;
    --br-xl: 1rem;
    --br-full: 9999px;

    /* Shadows */
    /* Buttons are flat by default. A theme that wants elevation sets this —
       `--shadow-sm` for a soft lift, or its own hard-offset stack. */
    --button-shadow: none;

    --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
    --shadow-base:
      0 1px 3px 0 rgba(0, 0, 0, 0.1),
      0 1px 2px 0 rgba(0, 0, 0, 0.06);
    --shadow-md:
      0 4px 6px -1px rgba(0, 0, 0, 0.1),
      0 2px 4px -1px rgba(0, 0, 0, 0.06);
    --shadow-lg:
      0 10px 15px -3px rgba(0, 0, 0, 0.1),
      0 4px 6px -2px rgba(0, 0, 0, 0.05);
    --shadow-xl:
      0 20px 25px -5px rgba(0, 0, 0, 0.1),
      0 10px 10px -5px rgba(0, 0, 0, 0.04);

    /* Transitions */
    --transition-fast: 0.15s ease-in-out;
    --transition-base: 0.2s ease-in-out;
    --transition-slow: 0.3s ease-in-out;

    /* Z-index scale */
    --z-dropdown: 1000;
    --z-sticky: 1020;
    --z-fixed: 1030;
    --z-modal-backdrop: 1040;
    --z-modal: 1050;
    --z-popover: 1060;
    --z-tooltip: 1070;

    /* Grid and breakpoints */
    --grid-columns: 12;
    --container-padding: var(--s3);

    /* Animation curves */
    --ease-in: cubic-bezier(0.4, 0, 1, 1);
    --ease-out: cubic-bezier(0, 0, 0.2, 1);
    --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);

    /* Neutral colors */
    --blackH: 0;
    --blackS: 0%;
    --blackL: 0%;

    --grayH: 220;
    --grayS: 14%;
    --grayL: 41%;

    --whiteH: 0;
    --whiteS: 0%;
    --whiteL: 100%;

    /* Brand colors */
    --blueH: 217;
    --blueS: 91%;
    --blueL: 60%;

    --greenH: 142;
    --greenS: 71%;
    --greenL: 45%;

    --yellowH: 45;
    --yellowS: 93%;
    --yellowL: 47%;

    --orangeH: 25;
    --orangeS: 95%;
    --orangeL: 53%;

    --redH: 0;
    --redS: 84%;
    --redL: 60%;

    --purpleH: 262;
    --purpleS: 83%;
    --purpleL: 58%;

    --pinkH: 330;
    --pinkS: 81%;
    --pinkL: 60%;

    /* Semantic colors */
    --errorH: var(--redH);
    --errorS: var(--redS);
    --errorL: var(--redL);

    --warningH: var(--orangeH);
    --warningS: var(--orangeS);
    --warningL: var(--orangeL);

    --infoH: var(--blueH);
    --infoS: var(--blueS);
    --infoL: var(--blueL);

    --successH: var(--greenH);
    --successS: var(--greenS);
    --successL: var(--greenL);

    /* Neutral application colors */
    --backgroundH: var(--whiteH);
    --backgroundS: var(--whiteS);
    --backgroundL: var(--whiteL);

    --bodyH: var(--blackH);
    --bodyS: var(--blackS);
    --bodyL: 13%;

    --secondaryH: var(--grayH);
    --secondaryS: var(--grayS);
    --secondaryL: var(--grayL);

    --primaryH: var(--blueH);
    --primaryS: var(--blueS);
    --primaryL: var(--blueL);

    /* Hairline border — a light neutral in light mode, darkened in dark mode.
       Muted text tracks `--secondary` (composed in base/colors.css). */
    --borderH: var(--grayH);
    --borderS: var(--grayS);
    --borderL: 85%;

    /* Recessed page ground; see `--surface-recessed` in base/colors.css. */
    --surface-recessedL: 96%;
  }

  /*
   * Dark mode. Overrides only the neutral application colors (background /
   * body text / secondary / border) and nudges the primary lighter for
   * contrast — the brand ramp (blue/green/red/…) is unchanged. An app that
   * wants a different dark palette redefines these in `@layer theme`, which
   * wins the cascade (see the layer order in `civility.css`).
   *
   * Applied from two places so an app can offer a light/dark/system control
   * without restating the palette:
   *
   *   - the media query, which follows the device but yields to an explicit
   *     choice via `:not([data-theme])`;
   *   - `[data-theme="dark"]`, set on `<html>` by the app.
   *
   * Setting `data-theme="light"` therefore forces light on a dark device with
   * no further CSS: the media query simply stops matching. Removing the
   * attribute hands control back to the device. See `modules/theme.ts`.
   */
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme]) {
      --backgroundH: var(--grayH);
      --backgroundS: 14%;
      --backgroundL: 11%;

      --bodyH: 0;
      --bodyS: 0%;
      --bodyL: 92%;

      --secondaryH: var(--grayH);
      --secondaryS: 14%;
      --secondaryL: 65%;

      --primaryL: 66%;

      --borderL: 28%;

      --surface-recessedL: 7%;
    }
  }

  :root[data-theme='dark'] {
    --backgroundH: var(--grayH);
    --backgroundS: 14%;
    --backgroundL: 11%;

    --bodyH: 0;
    --bodyS: 0%;
    --bodyL: 92%;

    --secondaryH: var(--grayH);
    --secondaryS: 14%;
    --secondaryL: 65%;

    --primaryL: 66%;

    --borderL: 28%;

    --surface-recessedL: 7%;
  }
}
/**
 * Custom Components CSS
 *
 * Components are elements that HTML doesn't have a concept for.
 * We define these as HTML custom components with `ui-` prefix.
 *
 * These components are aware of theme variables but base.css has no awareness of them.
 */
@layer components {
  /* UI Card Component */
  ui-card {
    display: block;
    border: var(--border-width) solid currentColor;
    border-radius: var(--br-lg);
    box-shadow: var(--shadow-sm);
    transition: all var(--transition-base);
    overflow: hidden;
  }

  ui-card:hover {
    box-shadow: var(--shadow-md);
    transform: translateY(-2px);
  }

  ui-card[clickable] {
    cursor: pointer;
  }

  ui-card[clickable]:active {
    transform: translateY(0);
  }

  /* UI Badge Component */
  ui-badge {
    display: inline-block;
    padding: var(--s1) var(--s2);
    font-size: var(--f7);
    font-weight: var(--fw-medium);
    border: var(--border-width) solid currentColor;
    border-radius: var(--br-full);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }

  ui-badge[variant='success'] {
    background-color: var(--success-dull);
    color: var(--success);
  }

  ui-badge[variant='warning'] {
    background-color: var(--warning-dull);
    color: var(--warning);
  }

  ui-badge[variant='error'] {
    background-color: var(--error-dull);
    color: var(--error);
  }

  ui-badge[variant='info'] {
    background-color: var(--info-dull);
    color: var(--info);
  }

  /* UI Button Group */
  ui-button-group {
    display: flex;
    gap: var(--s2);
    flex-wrap: wrap;
  }

  ui-button-group[orientation='vertical'] {
    flex-direction: column;
  }

  ui-button-group[attached] {
    gap: 0;
  }

  ui-button-group[attached] > button:not(:first-child),
  ui-button-group[attached] > input:not(:first-child) {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
    border-left: none;
  }

  ui-button-group[attached] > button:not(:last-child),
  ui-button-group[attached] > input:not(:last-child) {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
  }

  /* UI Grid */
  ui-grid {
    display: grid;
    gap: var(--s3);
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  }

  ui-grid[cols='1'] {
    grid-template-columns: 1fr;
  }

  ui-grid[cols='2'] {
    grid-template-columns: repeat(2, 1fr);
  }

  ui-grid[cols='3'] {
    grid-template-columns: repeat(3, 1fr);
  }

  ui-grid[cols='4'] {
    grid-template-columns: repeat(4, 1fr);
  }

  @media (max-width: 768px) {
    ui-grid[cols='2'],
    ui-grid[cols='3'],
    ui-grid[cols='4'] {
      grid-template-columns: 1fr;
    }
  }

  /* UI Stack */
  ui-stack {
    display: flex;
    flex-direction: column;
    gap: var(--s3);
  }

  ui-stack[spacing='xs'] {
    gap: var(--s1);
  }

  ui-stack[spacing='sm'] {
    gap: var(--s2);
  }

  ui-stack[spacing='md'] {
    gap: var(--s3);
  }

  ui-stack[spacing='lg'] {
    gap: var(--s4);
  }

  ui-stack[spacing='xl'] {
    gap: var(--s5);
  }

  ui-stack[direction='row'] {
    flex-direction: row;
    align-items: center;
  }

  /* UI Progress */
  ui-progress {
    display: block;
    width: 100%;
    height: 8px;
    border: var(--border-width) solid currentColor;
    border-radius: var(--br-full);
    overflow: hidden;
  }

  ui-progress::before {
    content: '';
    display: block;
    height: 100%;
    background-color: currentColor;
    border-radius: var(--br-full);
    width: var(--progress-value, 0%);
    transition: width var(--transition-base);
  }

  ui-progress[variant='success']::before {
    background-color: var(--success);
  }

  ui-progress[variant='warning']::before {
    background-color: var(--warning);
  }

  ui-progress[variant='error']::before {
    background-color: var(--error);
  }

  /* UI Notifications */
  ui-notifications {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
  }

  ui-notifications:empty {
    display: none;
  }

  ui-notifications > div {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
  }

  ui-notifications [data-row] {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
  }

  ui-notifications [data-row] > button {
    flex-shrink: 0;
  }

  /* UI Alert
     ─────────────────────────────────────────────────────────────────────────
     Each variant carries a **glyph as well as a colour**. Colour alone fails
     WCAG 1.4.1 for anyone who can't distinguish the hues, and it fails
     outright on a greyscale display — `themes/eink.css` rendered `error` and
     `success` as the same grey, which is what surfaced this. The glyph is a
     pseudo-element, so no component renders a node for it and a theme can
     replace or drop it via `--alert-glyph`. */
  ui-alert {
    display: flex;
    align-items: baseline;
    gap: var(--s2);
    padding: var(--s3);
    border-radius: var(--br-base);
    border: var(--border-width) solid currentColor;
  }

  ui-alert[variant]::before {
    content: var(--alert-glyph, '');
    flex: none;
    font-weight: var(--fw-bold);
    line-height: 1;
  }

  ui-alert[variant='success'] {
    --alert-glyph: '✓';

    background-color: var(--success-dull);
    border-color: var(--success);
    color: var(--success);
  }

  ui-alert[variant='warning'] {
    --alert-glyph: '!';

    background-color: var(--warning-dull);
    border-color: var(--warning);
    color: var(--warning);
  }

  ui-alert[variant='error'] {
    --alert-glyph: '✕';

    background-color: var(--error-dull);
    border-color: var(--error);
    color: var(--error);
  }

  ui-alert[variant='info'] {
    --alert-glyph: 'i';

    background-color: var(--info-dull);
    border-color: var(--info);
    color: var(--info);
  }

  /* UI Divider */
  ui-divider {
    display: block;
    height: 1px;
    border-top: var(--border-width) solid currentColor;
    margin: var(--s4) 0;
    border-left: none;
    border-right: none;
    border-bottom: none;
  }

  ui-divider[orientation='vertical'] {
    width: 1px;
    height: auto;
    margin: 0 var(--s3);
  }

  /* UI Loading Spinner */
  ui-spinner {
    display: inline-block;
    width: 20px;
    height: 20px;
    border: 2px solid;
    border-color: currentColor transparent currentColor transparent;
    border-radius: 50%;
    animation: spin 1s linear infinite;
  }

  ui-spinner[size='sm'] {
    width: 16px;
    height: 16px;
  }

  ui-spinner[size='lg'] {
    width: 32px;
    height: 32px;
    border-width: 3px;
  }

  /* UI Breadcrumb */
  ui-breadcrumb {
    display: flex;
    align-items: center;
    gap: var(--s2);
    font-size: var(--f5);
  }

  ui-breadcrumb a {
    color: var(--primary);
    text-decoration: none;
    padding: var(--s1) var(--s2);
    border-radius: var(--br-sm);
    transition: opacity var(--transition-base);
  }

  ui-breadcrumb a:hover {
    opacity: 0.7;
  }

  ui-breadcrumb::before {
    content: attr(separator);
    opacity: 0.5;
    margin: 0 var(--s1);
  }

  ui-breadcrumb:not([separator])::before {
    display: none;
  }

  /* UI Avatar */
  ui-avatar {
    display: inline-block;
    width: 40px;
    height: 40px;
    border-radius: var(--br-full);
    border: var(--border-width) solid currentColor;
    font-weight: var(--fw-medium);
    text-align: center;
    line-height: 40px;
    font-size: var(--f5);
    overflow: hidden;
  }

  ui-avatar[size='sm'] {
    width: 32px;
    height: 32px;
    line-height: 32px;
    font-size: var(--f7);
  }

  ui-avatar[size='lg'] {
    width: 56px;
    height: 56px;
    line-height: 56px;
    font-size: var(--f4);
  }

  ui-avatar img {
    width: 100%;
    height: 100%;
    -o-object-fit: cover;
       object-fit: cover;
  }

  /* UI Data Actions */
  ui-data-actions {
    display: block;
  }

  ui-data-actions > div {
    display: flex;
    gap: var(--s2);
    margin-bottom: var(--s2);
  }

  /* Status line. Generalised from `ui-data-actions > p[data-status]`: every
     component that reports the outcome of an action uses the same two
     attributes, so the styling belongs to the attributes, not to one parent. */
  p[data-status] {
    margin-top: var(--s2);
    font-size: var(--f5);
  }

  /* Same reasoning as `ui-alert`: an error line that differs from a success
     line only by hue disappears on a greyscale display, and is invisible to a
     good share of readers on any display. */
  p[data-status][data-error] {
    color: var(--error);
  }

  p[data-status][data-error]::before {
    content: '✕ ';
    font-weight: var(--fw-bold);
  }

  /* UI Dropdown */
  ui-dropdown {
    position: relative;
    display: inline-block;
  }

  ui-dropdown-content {
    position: absolute;
    top: 100%;
    left: 0;
    background: var(--background);
    border: var(--border-width) solid currentColor;
    border-radius: var(--br-base);
    box-shadow: var(--shadow-lg);
    min-width: 200px;
    z-index: var(--z-dropdown);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-8px);
    transition: all var(--transition-base);
  }

  ui-dropdown[open] ui-dropdown-content {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
  }

  ui-dropdown-item {
    display: block;
    padding: var(--s3);
    text-decoration: none;
    transition: opacity var(--transition-base);
    cursor: pointer;
  }

  ui-dropdown-item:hover {
    opacity: 0.7;
  }

  ui-dropdown-item[active] {
    opacity: 1;
    font-weight: var(--fw-medium);
  }

  @media (max-width: 768px) {
    ui-dropdown-content {
      left: auto;
      right: 0;
      min-width: 150px;
    }
  }

  /* ─── UI Drawer Component ──────────────────────────────────────────────── */

  /* Generic drawer component - can be triggered from anywhere in UI */
  /* Usage:
     <input type="checkbox" id="my-drawer" class="drawer-toggle">
     <label for="my-drawer" class="drawer-backdrop"></label>
     <ui-drawer drawer-id="my-drawer" position="left">
       Drawer content here
     </ui-drawer>
     <button><label for="my-drawer">Open Drawer</label></button>
  */

  .drawer-toggle {
    display: none;
  }

  .drawer-backdrop {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100vw;
    height: 100vh;
    background: var(--backdrop-color);
    z-index: calc(var(--z-fixed) + 1);
    cursor: pointer;
    opacity: 0;
    transition: 250ms allow-discrete;
    transition-property: display, opacity, overlay;
  }

  .drawer-toggle:checked ~ .drawer-backdrop {
    display: block;
    opacity: 1;
  }

  @starting-style {
    .drawer-toggle:checked ~ .drawer-backdrop {
      opacity: 0;
    }
  }

  ui-drawer {
    position: fixed;
    z-index: calc(var(--z-fixed) + 2);
    background: var(--background);
    box-shadow: 4px 0 12px var(--drawer-shadow-color);
    overflow-y: auto;
    padding: var(--s3);
    display: none;
    transition: 250ms allow-discrete;
    transition-property: display, transform;
  }

  /* Left drawer (default) */
  ui-drawer,
  ui-drawer[position='left'] {
    top: 0;
    left: 0;
    bottom: 0;
    width: min(80vw, 320px);
    height: 100vh;
    transform: translateX(-100%);
  }

  .drawer-toggle:checked ~ ui-drawer[position='left'],
  .drawer-toggle:checked ~ * ui-drawer[position='left'] {
    display: block;
    transform: translateX(0);
  }

  @starting-style {
    .drawer-toggle:checked ~ ui-drawer[position='left'],
    .drawer-toggle:checked ~ * ui-drawer[position='left'] {
      transform: translateX(-100%);
    }
  }

  /* Right drawer */
  ui-drawer[position='right'] {
    top: 0;
    right: 0;
    bottom: 0;
    left: auto;
    width: min(80vw, 320px);
    height: 100vh;
    transform: translateX(100%);
    box-shadow: -4px 0 12px var(--drawer-shadow-color);
  }

  .drawer-toggle:checked ~ ui-drawer[position='right'],
  .drawer-toggle:checked ~ * ui-drawer[position='right'] {
    display: block;
    transform: translateX(0);
  }

  @starting-style {
    .drawer-toggle:checked ~ ui-drawer[position='right'],
    .drawer-toggle:checked ~ * ui-drawer[position='right'] {
      transform: translateX(100%);
    }
  }

  /* Top drawer */
  ui-drawer[position='top'] {
    top: 0;
    left: 0;
    right: 0;
    bottom: auto;
    width: 100vw;
    height: min(80vh, 400px);
    transform: translateY(-100%);
    box-shadow: 0 4px 12px var(--drawer-shadow-color);
  }

  .drawer-toggle:checked ~ ui-drawer[position='top'],
  .drawer-toggle:checked ~ * ui-drawer[position='top'] {
    display: block;
    transform: translateY(0);
  }

  @starting-style {
    .drawer-toggle:checked ~ ui-drawer[position='top'],
    .drawer-toggle:checked ~ * ui-drawer[position='top'] {
      transform: translateY(-100%);
    }
  }

  /* Bottom drawer */
  ui-drawer[position='bottom'] {
    bottom: 0;
    left: 0;
    right: 0;
    top: auto;
    width: 100vw;
    height: min(80vh, 400px);
    transform: translateY(100%);
    box-shadow: 0 -4px 12px var(--drawer-shadow-color);
  }

  .drawer-toggle:checked ~ ui-drawer[position='bottom'],
  .drawer-toggle:checked ~ * ui-drawer[position='bottom'] {
    display: block;
    transform: translateY(0);
  }

  @starting-style {
    .drawer-toggle:checked ~ ui-drawer[position='bottom'],
    .drawer-toggle:checked ~ * ui-drawer[position='bottom'] {
      transform: translateY(100%);
    }
  }

  /* ─── UI Bottom Bar Component ───────────────────────────────────────────── */

  /* Mobile-style bottom navigation bar
     Usage:
     <footer fixed>
       <ui-bottom-bar>
         <button>
           <i class="icon" style="--icon:url(/static/icons/home.svg)"></i>
           <span>Home</span>
         </button>
         <button>
           <i class="icon" style="--icon:url(/static/icons/search.svg)"></i>
           <span>Search</span>
         </button>
       </ui-bottom-bar>
     </footer>
  */

  ui-bottom-bar {
    display: flex;
    flex-direction: row;
    align-items: stretch;
    width: 100%;
    height: 100%;
    padding: 0;
    margin: 0;
    gap: 0;
  }

  ui-bottom-bar > button,
  ui-bottom-bar > a {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    padding: var(--s1) var(--s2);
    font-size: var(--f7);
    text-decoration: none;
    color: inherit;
    font-weight: var(--fw-medium);
    background: transparent;
    border: none;
    cursor: pointer;
    opacity: 1;
    transition: opacity var(--transition-base);
  }

  ui-bottom-bar > button:hover,
  ui-bottom-bar > a:hover {
    opacity: 0.7;
  }

  ui-bottom-bar > button:disabled {
    opacity: 0.3;
    cursor: not-allowed;
  }

  ui-bottom-bar > button:not([aria-current='page']),
  ui-bottom-bar > a:not([aria-current='page']) {
    opacity: 0.5;
  }

  /* Icon sizing within bottom bar */
  ui-bottom-bar .icon {
    font-size: var(--f4);
    line-height: 1;
  }

  /* Hide text variant */
  ui-bottom-bar[hide-text] > button > span:not(.icon),
  ui-bottom-bar[hide-text] > a > span:not(.icon) {
    display: none;
  }

  ui-bottom-bar[hide-text] > button,
  ui-bottom-bar[hide-text] > a {
    gap: 0;
  }

  /* Horizontal variant (icons + text side-by-side) */
  ui-bottom-bar[orientation='horizontal'] > button,
  ui-bottom-bar[orientation='horizontal'] > a {
    flex-direction: row;
    gap: var(--s2);
  }

  /* ─── Button intent ───────────────────────────────────────────────────────
     `base/forms.css` already gives every `<button>` its border, radius,
     weight, hover and disabled state, so a framework button needs no class —
     only a way to say what *kind* of action it is. Two attributes cover every
     case the components have: destructive, and dismissive.

     `data-*` rather than a bare attribute because these sit on native
     elements, matching the `data-status` / `data-error` convention already in
     use here. Both maps and music had independently arrived at exactly these
     colours for `.action--danger`. */
  button[data-variant='danger'],
  input[type='button'][data-variant='danger'] {
    color: var(--error);
    border-color: var(--error);
  }

  /* The "never mind" button beside a real action: present, reachable, and
     visibly not the thing you came to press. */
  button[data-variant='quiet'] {
    border-color: transparent;
    color: var(--muted);
  }

  button[data-variant='quiet']:hover {
    color: var(--body);
  }

  /* ─── Civ Store Import ────────────────────────────────────────────────── */

  /* `stretch` (the flex default) rather than `flex-start`: these are the
     page's data controls, sitting under a paragraph that spans the column, and
     every app that styled them at all made them full width. */
  civ-store-import {
    display: flex;
    flex-direction: column;
    gap: var(--s3);
  }

  /* Import and Export share a row; Delete All Data owns one. */
  civ-store-import ui-button-group > button {
    flex: 1;
  }

  civ-store-import > p {
    margin: 0;
  }

  /* The radio group in the import-mode dialog: `<label>` is a block by
     default, which would put the control on its own line away from its text. */
  civ-store-import fieldset {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
    margin: 0;
  }

  civ-store-import fieldset label {
    display: flex;
    align-items: baseline;
    gap: var(--s2);
    margin: 0;
    font-weight: var(--fw-normal);
  }

  civ-store-import fieldset label small {
    display: block;
    color: var(--muted);
  }

  /* ─── Search ──────────────────────────────────────────────────────────── */

  ui-search-field {
    display: block;
  }

  /* A row, not a column: the field takes the space and the action hugs its
     label. Every app that had a search page had written this. */
  ui-search-field form {
    display: flex;
    align-items: flex-end;
    gap: var(--s2);
  }

  ui-search-field label {
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: var(--s1);
    min-width: 0;
  }

  ui-search-field label > span {
    font-size: var(--f6);
    color: var(--muted);
  }

  /* `hide-label` keeps the text for assistive tech and takes it out of the
     layout — `display: none` would remove it from the accessibility tree too,
     leaving the input named only by its placeholder, which is not a name. */
  ui-search-field[hide-label] label > span {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  ui-search-field input {
    width: 100%;
    box-sizing: border-box;
    margin-bottom: 0;
  }

  ui-search-field form > button {
    flex: none;
    margin-bottom: 0;
  }

  ui-search-field [data-recent] {
    margin-top: var(--s2);
  }

  /* ─── Sync surfaces ───────────────────────────────────────────────────── */

  civ-sync-input form {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
  }

  /* Authorize is the only thing to do on this form, and it sits directly under
     a full-width field — a hugging button reads as secondary to the input it
     submits. Full width was what three apps restated by hand before this rule
     existed, so it is the default now rather than the thing they undo. */
  civ-sync-input form > button {
    align-self: stretch;
  }

  civ-sync-input label {
    display: flex;
    flex-direction: column;
    gap: var(--s1);
  }

  civ-sync-input label > span {
    font-size: var(--f6);
    color: var(--muted);
  }

  civ-sync-input [data-status-row] {
    display: flex;
    align-items: center;
    gap: var(--s2);
    flex-wrap: wrap;
    margin-bottom: var(--s3);
  }

  civ-sync-input [data-status-row] small {
    color: var(--muted);
  }

  /* The status dot is a pseudo-element, so it is never a node an app has to
     know the name of — and a theme can restyle or remove the indicator with
     no markup change. One custom property carries the colour so the inline
     and detailed variants can't drift apart. */
  civ-sync-state {
    --civ-sync-dot: var(--muted);

    display: inline-flex;
    align-items: center;
    gap: var(--s2);
  }

  civ-sync-state[status='ok'] {
    --civ-sync-dot: var(--success);
  }

  civ-sync-state[status='syncing'] {
    --civ-sync-dot: var(--info);
  }

  civ-sync-state[status='error'] {
    --civ-sync-dot: var(--error);
  }

  civ-sync-state::before,
  civ-sync-state[variant='detailed'] > strong::before {
    content: '';
    flex: none;
    width: 0.6em;
    height: 0.6em;
    border-radius: var(--br-full);
    background: var(--civ-sync-dot);
  }

  civ-sync-state[variant='badge'] {
    gap: 0;
  }

  civ-sync-state[variant='detailed'] {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s1);
  }

  /* Stacked, the dot belongs beside the label rather than above it. */
  civ-sync-state[variant='detailed']::before {
    display: none;
  }

  civ-sync-state[variant='detailed'] > strong::before {
    display: inline-block;
    margin-right: var(--s2);
  }

  civ-sync-state small {
    color: var(--muted);
  }

  /* ─── Civ Usage ───────────────────────────────────────────────────────── */

  civ-usage {
    display: flex;
    flex-direction: column;
    gap: var(--s3);
  }

  civ-usage > div > div:first-child {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s2);
    margin-bottom: var(--s1);
  }

  civ-usage p[data-tier] {
    margin: 0;
    color: var(--muted);
  }

  /* ─── Civ Devices ─────────────────────────────────────────────────────── */

  civ-devices ul {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--s2);
  }

  civ-devices li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    flex-wrap: wrap;
    padding: var(--s3);
    border: var(--border-width) solid var(--border);
    border-radius: var(--br-base);
  }

  civ-devices li[data-current] {
    border-color: currentColor;
  }

  civ-devices li > div {
    display: flex;
    align-items: center;
    gap: var(--s2);
    flex-wrap: wrap;
  }

  civ-devices small,
  civ-shares-list small {
    color: var(--muted);
  }

  /* ─── Share surfaces ──────────────────────────────────────────────────── */

  civ-share-link {
    display: flex;
    align-items: center;
    gap: var(--s2);
    min-width: 0;
  }

  civ-share-link code {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  civ-shares-list ul {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--s3);
  }

  civ-shares-list li {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
    padding: var(--s3);
    border: var(--border-width) solid var(--border);
    border-radius: var(--br-base);
  }

  civ-shares-list li > div {
    display: flex;
    align-items: center;
    gap: var(--s2);
    flex-wrap: wrap;
  }

  /* ─── Comments ────────────────────────────────────────────────────────── */

  civ-comments ul {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--s4);
  }

  civ-comments li > header {
    display: flex;
    align-items: baseline;
    gap: var(--s2);
    flex-wrap: wrap;
  }

  civ-comments time,
  civ-comments small,
  civ-comments [data-verified='false'] {
    font-size: var(--f6);
    color: var(--muted);
  }

  civ-comments [data-verified]:not([data-verified='false']) {
    color: var(--success);
  }

  civ-comments p[data-editing] {
    font-size: var(--f6);
    color: var(--muted);
  }

  civ-comment-box form {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
  }

  civ-comment-box footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2);
    flex-wrap: wrap;
  }

  /* ─── Service AI input ────────────────────────────────────────────────── */

  civ-service-ai-input form {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
    align-items: flex-start;
  }

  civ-service-ai-input label {
    display: flex;
    flex-direction: column;
    gap: var(--s1);
    width: 100%;
  }

  civ-service-ai-input small {
    color: var(--muted);
  }

  /* ─── UI Error Display ────────────────────────────────────────────────── */

  ui-error-display {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
  }

  ui-error-display ui-alert {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--s3);
    flex-wrap: wrap;
  }

  ui-error-display p {
    margin: var(--s1) 0 0;
  }

  /* ─── Empty states ────────────────────────────────────────────────────── */

  /* One convention across every component that can have nothing to show. */
  p[data-empty] {
    margin: 0;
    color: var(--muted);
  }

  /* ─── Civ Locked Screen ─────────────────────────────────────────────────── */

  civ-locked-screen {
    display: contents;
  }

  civ-locked-screen > [data-backdrop] {
    position: fixed;
    inset: 0;
    z-index: var(--z-fixed, 1000);
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--backdrop-color);
    backdrop-filter: blur(4px);
  }

  civ-locked-screen article {
    background: var(--background, #fff);
    border-radius: var(--br-lg, 12px);
    box-shadow: var(--shadow-lg, 0 8px 32px rgba(0, 0, 0, 0.2));
    padding: var(--s5, 2rem);
    max-width: min(90vw, 420px);
    width: 100%;
    text-align: center;
  }

  civ-locked-screen [data-icon] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 64px;
    height: 64px;
    border-radius: var(--br-full, 50%);
    background: var(--info-dull, #e0f2fe);
    color: var(--primary, #2563eb);
    margin-bottom: var(--s3, 1rem);
  }

  civ-locked-screen article > h2 {
    font-size: var(--f3, 1.25rem);
    font-weight: var(--fw-semibold, 600);
    margin: 0 0 var(--s2, 0.5rem);
  }

  civ-locked-screen article > p {
    color: var(--muted, #6b7280);
    font-size: var(--f5, 0.875rem);
    margin: 0 0 var(--s3, 1rem);
  }

  civ-locked-screen p[data-error] {
    color: var(--error, #dc2626);
    font-size: var(--f5, 0.875rem);
    padding: var(--s2, 0.5rem) var(--s3, 1rem);
    background: var(--error-dull, #fef2f2);
    border-radius: var(--br-base, 6px);
    margin-bottom: var(--s3, 1rem);
  }

  civ-locked-screen form {
    display: flex;
    flex-direction: column;
    gap: var(--s3, 1rem);
    text-align: left;
  }

  civ-locked-screen form label {
    display: flex;
    flex-direction: column;
    gap: var(--s1, 0.25rem);
  }

  civ-locked-screen form label span {
    font-size: var(--f6, 0.8rem);
    font-weight: var(--fw-medium, 500);
    color: var(--muted, #6b7280);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }

  civ-locked-screen form input,
  civ-locked-screen form textarea {
    padding: var(--s2, 0.5rem) var(--s3, 1rem);
    border: var(--border-width) solid var(--border, #d1d5db);
    border-radius: var(--br-base, 6px);
    font-size: var(--f5, 0.875rem);
    font-family: inherit;
    transition: border-color var(--transition-base, 150ms);
  }

  civ-locked-screen form input:focus,
  civ-locked-screen form textarea:focus {
    outline: none;
    border-color: var(--primary, #2563eb);
    box-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring-color);
  }

  civ-locked-screen form textarea {
    resize: vertical;
    min-height: 72px;
  }

  civ-locked-screen form button[type='submit'] {
    padding: var(--s2, 0.5rem) var(--s4, 1.5rem);
    background: var(--primary, #2563eb);
    color: var(--primary-contrast);
    border: none;
    border-radius: var(--br-base, 6px);
    font-size: var(--f5, 0.875rem);
    font-weight: var(--fw-medium, 500);
    cursor: pointer;
    transition: opacity var(--transition-base, 150ms);
  }

  civ-locked-screen form button[type='submit']:hover:not(:disabled) {
    opacity: 0.9;
  }

  civ-locked-screen form button[type='submit']:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  civ-locked-screen nav {
    margin-top: var(--s3, 1rem);
  }

  civ-locked-screen nav a {
    color: var(--primary, #2563eb);
    font-size: var(--f6, 0.8rem);
    text-decoration: underline;
    cursor: pointer;
  }

  civ-locked-screen nav a:hover {
    opacity: 0.7;
  }

  civ-locked-screen[dismissed] civ-locked-screen > [data-backdrop] {
    display: none;
  }

  /* ─── Civ Pin Field ─────────────────────────────────────────────────────── */

  civ-pin-field > [role='group'] {
    display: flex;
    gap: var(--s2, 0.5rem);
    justify-content: center;
  }

  civ-pin-field input {
    width: 2.5rem;
    height: 3rem;
    text-align: center;
    font-size: var(--f3, 1.25rem);
    border: var(--border-width) solid var(--border, #d1d5db);
    border-radius: var(--br-base, 6px);
    background: var(--bg, #fff);
    color: var(--fg, #111827);
  }

  civ-pin-field input:focus {
    outline: none;
    border-color: var(--primary, #2563eb);
    box-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring-color);
  }

  /* ─── Civ Phrase Field ──────────────────────────────────────────────────── */

  civ-phrase-field > [role='group'] {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--s2, 0.5rem);
  }

  civ-phrase-field label {
    display: flex;
    align-items: center;
    gap: var(--s1, 0.25rem);
    padding: 0 var(--s2, 0.5rem);
    border: var(--border-width) solid var(--border, #d1d5db);
    border-radius: var(--br-base, 6px);
    background: var(--bg, #fff);
  }

  civ-phrase-field [data-index] {
    color: var(--fg-muted, #9ca3af);
    font-size: var(--f6, 0.8rem);
  }

  civ-phrase-field input {
    width: 100%;
    min-width: 0;
    padding: var(--s2, 0.5rem) 0;
    border: none;
    background: transparent;
    font-family: var(--ff-mono, ui-monospace, monospace);
    font-size: var(--f5, 0.875rem);
    color: var(--fg, #111827);
  }

  civ-phrase-field input:focus {
    outline: none;
  }

  civ-phrase-field label:focus-within {
    border-color: var(--primary, #2563eb);
    box-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring-color);
  }

  civ-phrase-field input[aria-invalid='true'] {
    color: var(--danger, #dc2626);
  }

  /* Civ PWA Version */
  civ-version {
    display: block;
  }

  civ-version p {
    margin: 0 0 var(--s2) 0;
  }

  civ-version button {
    margin-top: var(--s1);
  }

  /* UI Counter */
  ui-counter {
    display: flex;
    align-items: stretch;
    gap: var(--s2);
  }

  ui-counter button {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
  }

  ui-counter input[type='number'] {
    flex: 1;
    text-align: center;
    padding: 0;
    margin: 0;
    -webkit-appearance: textfield;
            appearance: textfield;
    -moz-appearance: textfield;
  }

  ui-counter input[type='number']::-webkit-inner-spin-button,
  ui-counter input[type='number']::-webkit-outer-spin-button {
    -webkit-appearance: none;
  }

  ui-dialog {
    display: contents;
  }

  /* Dialog content shell.
     `dialog` itself is padding-free so a consumer can bleed artwork to its
     edges; an `<article>` child is the ordinary case and gets the padded,
     stacked layout every app was previously re-inventing (`.bm-dialog` in
     maps, and a copy of it wherever else a dialog appeared). */
  dialog > article {
    display: flex;
    flex-direction: column;
    gap: var(--s3);
    margin: 0;
    padding: var(--s4);
    min-width: min(320px, 90vw);
  }

  dialog > article > h2 {
    margin: 0;
    font-size: var(--f4);
    font-weight: var(--fw-semibold);
  }

  /* The action row. `<footer>` rather than a class: it is genuinely the
     article's footer, so the meaning and the styling hook are the same thing. */
  dialog > article > footer {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    margin-top: var(--s1);
  }

  /* A busy dialog centres whatever it is waiting on. */
  dialog > article[aria-busy='true'] > ui-spinner {
    align-self: center;
    margin: var(--s3) 0;
  }

  dialog {
    /* Tokens, not the `Canvas` system colour it used to be: `Canvas` tracks
       `color-scheme` but not the theme, so a designer's `--background` could
       never reach a dialog. */
    background: var(--background);
    color: var(--body);
    border: none;
    border-radius: var(--br-lg);
    box-shadow: var(--shadow-xl);
    max-width: min(90vw, 560px);
    opacity: 0;
    padding: 0;
    transform: scale(0.6);
    transform-origin: top center;
    transition: 0.25s allow-discrete;
    transition-property: display, opacity, overlay, transform;
    width: 100%;
  }

  dialog::backdrop {
    /* The wash is entirely `--backdrop-color`, alpha included. It used to be
       `#000` dimmed by `opacity: 0.5`, which meant a theme could change the
       hue but never how heavy the wash was — on a reflective display a
       half-black overlay hides the page rather than dimming it. */
    background-color: var(--backdrop-color);
    opacity: 0;
    transition: var(--transition-slow) allow-discrete;
    transition-property: display, opacity, overlay;
  }

  dialog[open] {
    opacity: 1;
    transform: scale(1);
  }

  dialog[open]::backdrop {
    opacity: 1;
  }

  @starting-style {
    dialog[open] {
      opacity: 0;
      transform: scale(0.6);
    }

    dialog[open]::backdrop {
      opacity: 0;
    }
  }

  ui-back-header {
    align-items: center;
    border-bottom: none;
    display: flex;
    justify-content: start;
    margin: var(--s0);
    padding: var(--s3);
    width: 100%;
  }

  ui-back-header h1 {
    font-size: var(--f3);
    margin: 0;
  }

  ui-back-header button {
    border: none;
    box-shadow: none;
    margin-right: var(--s3);
    padding: var(--s1);
    width: unset;
  }

  ui-back-header button .icon {
    font-size: var(--f3);
  }

  ui-header-content {
    align-items: center;
    border-bottom: none;
    display: flex;
    justify-content: start;
    margin: var(--s0);
    padding: var(--s3);
    width: 100%;
  }

  ui-header-content h1 {
    flex: 1;
    font-size: var(--f3);
    margin: 0;
  }

  ui-header-content [data-back] {
    background: transparent;
    border: none;
    box-shadow: none;
    margin-right: var(--s3);
    padding: var(--s1);
    width: unset;
    cursor: pointer;
  }

  ui-header-content [data-back] .icon {
    font-size: var(--f3);
  }

  ui-header-content [data-actions] {
    display: flex;
    align-items: center;
    gap: var(--s2);
  }

  /* ── Scenes ──────────────────────────────────────────────────────────────
     Page-sized components. They mostly arrange elements the rules above
     already style; what they add is their own frame. */

  /* The About page's sign-off. Three apps had centred this by hand with an
     inline style, which is exactly the kind of thing a scene should own. */
  ui-settings-about > footer {
    display: block;
    padding-top: var(--s4);
    text-align: center;
  }

  /* A credits section is slotted, so it arrives as a sibling of the App
     section rather than inside one; give it the same rhythm. */
  ui-settings-about > [data-slot='credits'] > section {
    display: block;
    margin-top: var(--s4);
  }
}
/**
 * Shared class vocabulary.
 *
 * Components style themselves (§6.4.5) — this file is for the markup *apps*
 * write. Those are patterns the framework has no element for, but which every
 * app builds anyway: a row in a list, a toolbar, an empty state, a masked
 * icon.
 *
 * Named classes rather than elements, deliberately, and for the opposite
 * reason to §6.4.5. There, the framework owns the markup, so it can dictate
 * the shape. Here the app owns the markup and the framework only offers a
 * name — a class is the right tool, and an element would just be a `<div>`
 * with extra steps. (The elements that *were* only styling hooks —
 * `ui-divider`, `ui-breadcrumb`, `ui-avatar`, `ui-data-actions` — had zero
 * adoption across six apps for exactly this reason.)
 *
 * Everything here is reconciled from what the apps already wrote, not
 * invented. Where four apps had converged on the same shape under different
 * names, that shape is the pattern. Where they had converged on the same
 * *name*, that name is kept.
 *
 * Lives in `@layer components`, below `app` — so an app's own rule always
 * wins, and adopting a pattern is subtractive: delete your rule, keep your
 * markup.
 */
@layer components {
  /* ─── Icon ────────────────────────────────────────────────────────────────
     A mask over a solid fill, so the glyph takes `currentColor` and follows
     the theme like text — and needs no JavaScript, so it renders in a static
     `index.html` shell on first paint.

         <i class="icon" style="--icon: url(/static/icons/home.svg)"
            aria-hidden="true"></i>

     Icons are decorative by default: mark them `aria-hidden` and let the
     surrounding control carry the accessible name (`aria-label` on the button
     or link). An icon that is the *only* content of a control still needs
     that label — nothing here can supply it for you.

     `--icon-url` is accepted alongside `--icon`, because one app named it
     that way. Size with `font-size` on the icon or its parent; the glyph is
     `1em` square. */
  .icon {
    display: inline-block;
    flex: none;
    width: 1em;
    height: 1em;
    background-color: currentColor;
    -webkit-mask: var(--icon, var(--icon-url)) center / contain no-repeat;
    mask: var(--icon, var(--icon-url)) center / contain no-repeat;
  }

  /* An icon with a caption under it — a bottom-bar destination, a transport
     control. This was the one thing the retired `<ui-icon display="both">`
     did that a class could not — six lines of flex, so the class won.

         <span class="icon-label">
           <i class="icon" style="--icon: url(…)" aria-hidden="true"></i>
           <span>Recipes</span>
         </span> */
  .icon-label {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: 0.25em;
  }

  .icon-label > :not(.icon) {
    font-size: 0.5em;
    line-height: 1;
  }

  /* ─── Muted text ──────────────────────────────────────────────────────── */
  .muted {
    color: var(--muted);
  }

  /* ─── Empty state ─────────────────────────────────────────────────────────
     Every app has one and every app named it differently — `.empty-state`,
     `.library-empty`, `.tab-empty`, `.bm-empty`, `.notes-empty`. Same shape
     each time: centred, muted, a bit of room around it. */
  .empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--s2);
    padding: var(--s5) var(--s3);
    color: var(--muted);
    text-align: center;
  }

  /* ─── Rows ────────────────────────────────────────────────────────────────
     The single most duplicated pattern in the codebase: 52 distinct class
     names across four apps for the same flex row.

     The class goes on the **container**, not each row — one class per list
     instead of one per item, and it stops mattering what the children are.
     That matters here because rows are mostly *not* `<li>`: across the four
     apps they are 22 `div`, 20 `button`, 18 `span`, 13 `li`, 8 `label`,
     5 `a`. Styling `li` would miss most of them and hit every prose list.
     `ui-button-group` already works this way.

         <ul class="rows">
           <li>…</li>
         </ul>

     A row that needs a card's border and radius is a `ui-card`, not a variant
     here. */
  .rows > * {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    padding: var(--s3) 0;
    border-bottom: var(--border-width) solid var(--border);
  }

  .rows > :last-child {
    border-bottom: none;
  }

  /* A `<ul class="rows">` shouldn't keep its markers or indent. */
  ul.rows,
  ol.rows {
    list-style: none;
    margin: 0;
    padding: 0;
  }

  /* A row that is itself a link or button keeps the text colour rather than
     turning into a link-blue slab. */
  .rows > a,
  .rows > button {
    width: 100%;
    color: inherit;
    text-align: left;
    text-decoration: none;
    background: none;
    border: none;
    border-bottom: var(--border-width) solid var(--border);
    border-radius: 0;
    cursor: pointer;
    font: inherit;
  }

  /* ─── Toolbar ─────────────────────────────────────────────────────────────
     A heading with controls beside it — 31 names across four apps
     (`.page-header`, `.library-toolbar`, `.recipe-controls`,
     `.collections-header`, …). Wraps rather than squashes on narrow screens. */
  .toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    flex-wrap: wrap;
  }

  .toolbar > :first-child {
    margin: 0;
  }

  /* ─── Field ───────────────────────────────────────────────────────────────
     A label, its control, and an optional hint, stacked. `base/forms.css`
     already styles the control itself; this is only the grouping every app
     rebuilt as `.form-group` / `.form-row` / `.bookmark-page-field`. */
  .field {
    display: flex;
    flex-direction: column;
    gap: var(--s1);
    margin-bottom: var(--s3);
  }

  /* The control carries the field's own bottom margin, so it doesn't stack. */
  .field input,
  .field textarea,
  .field select {
    margin-bottom: 0;
  }

  .field-hint {
    font-size: var(--f6);
    color: var(--muted);
  }

  /* ─── Stat ────────────────────────────────────────────────────────────────
     A number and what it counts. Label under value, because the number is the
     thing being scanned. */
  .stat {
    display: flex;
    flex-direction: column;
    gap: var(--s1);
  }

  .stat-value {
    font-size: var(--f3);
    font-weight: var(--fw-semibold);
    line-height: var(--lh-tight);
    font-variant-numeric: tabular-nums;
  }

  .stat-label {
    font-size: var(--f6);
    color: var(--muted);
  }

  /* ─── Settings ────────────────────────────────────────────────────────────
     These names are kept exactly as three apps independently arrived at them.
     The implementations had drifted — block vs flex, `--border` vs
     `currentColor`, the meta line at three different sizes — so the shape
     below is reconciled, but nothing needs renaming to adopt it. */
  .settings-nav-list {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
  }

  .settings-nav-link {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    padding: var(--s3);
    border: var(--border-width) solid var(--border);
    border-radius: var(--br-base);
    color: inherit;
    text-decoration: none;
    font-weight: var(--fw-medium);
    transition: opacity var(--transition-base);
  }

  .settings-nav-link:hover {
    opacity: 0.7;
  }

  .settings-nav-link-meta {
    display: block;
    font-size: var(--f6);
    font-weight: var(--fw-normal);
    color: var(--muted);
  }

  .settings-toggle-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    padding: var(--s3) 0;
    border-bottom: var(--border-width) solid var(--border);
  }

  .settings-toggle-row:last-child {
    border-bottom: none;
  }

  /* The hint under a row's title. `.settings-nav-link-meta` is the same thing
     for a link row; inside a `label` the element's position already says it,
     so a settings row needs no class here either. */
  .settings-toggle-row label > span {
    display: block;
    font-size: var(--f6);
    font-weight: var(--fw-normal);
    color: var(--muted);
  }

  /* The control half of the row. A `select` defaults to shrink-to-fit, but
     apps commonly stretch form controls to full width, which in a
     space-between row leaves no room for the label. Sizing it here means a
     settings row needs no class on the control itself. */
  .settings-toggle-row select {
    width: auto;
    min-width: 8rem;
    margin: 0;
  }
}
