/* @bydefaultstudio/design-system v4.8.0 */
/* ─────────────────────────────
   DESIGN SYSTEM FRAMEWORK
   ───────────────────────────── */


/* ------ 1. BRAND TOKENS ------ */

/*
 * Neutral engine defaults. A brand expresses itself by overriding these
 * primitives in its theme.css (cms/brands/<brand>/assets/theme.css);
 * the §2 semantic layer resolves through them, so a theme never restates
 * semantics it can reach through a primitive. Instance 0 (By Default)
 * loads cms/brands/bydefault/assets/theme.css like any other brand.
 * Neutralize values here, never delete token names: core components and
 * docs-site.css consume them by name.
 */

:root {
  /* -- Typography (neutral system stacks) -- */
  --font-primary: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-secondary: Georgia, "Times New Roman", serif;
  --font-tertiary: system-ui, sans-serif;
  --font-quaternary: ui-monospace, "SF Mono", Menlo, monospace;

  /* Accent + semantic aliases. Editorial + code alias the existing slots
     so call sites can stay role-named even if the underlying face is
     swapped later. */
  --font-display:   system-ui, sans-serif;
  --font-playful:   system-ui, sans-serif;
  --font-script:    cursive;
  --font-editorial: var(--font-secondary);
  --font-code:      var(--font-quaternary);

  /* -- Palette -- */
  --off-white: var(--neutral-50);
  --off-black: var(--neutral-900);
  /* Brown is the warm palette ramp. It defaults through the neutral scale so a
     brand that re-points only the neutrals still carries the toast, tooltip and
     mark surfaces with it; a brand wanting a true brown sets these directly. */
  --brown-lighter: var(--neutral-100);
  --brown-light: var(--neutral-300);
  --brown: var(--neutral-500);
  --brown-dark: var(--neutral-950);

  /* -- Accent colours (generic) -- */
  --red-lighter: #FEE2E2;
  --red-light: #FECACA;
  --red: #DC2626;
  --red-dark: #7F1D1D;
  --blue-lighter: #DBEAFE;
  --blue-light: #BFDBFE;
  --blue: #2563EB;
  --blue-dark: #1E3A8A;
  --yellow-lighter: #FEF3C7;
  --yellow-light: #FDE68A;
  --yellow: #D97706;
  --yellow-dark: #78350F;
  --green-lighter: #DCFCE7;
  --green-light: #BBF7D0;
  --green: #16A34A;
  --green-dark: #14532D;
  --purple-lighter: #F3E8FF;
  --purple-light: #E9D5FF;
  --purple: #9333EA;
  --purple-dark: #581C87;
  /* Aqua, pink and peach are the bright accents: their base step sits a shade
     lighter than the five signal hues above, so they read as a highlight on a
     light surface rather than as a status colour. */
  --aqua-lighter: #CCFBF1;
  --aqua-light: #99F6E4;
  --aqua: #5EEAD4;
  --aqua-dark: #134E4A;
  --pink-lighter: #FCE7F3;
  --pink-light: #FBCFE8;
  --pink: #F9A8D4;
  --pink-dark: #831843;
  --peach-lighter: #FFEDD5;
  --peach-light: #FED7AA;
  --peach: #FDBA74;
  --peach-dark: #7C2D12;
  /* -- Base font sizes (rem-based, progressive step scale: +2px body, +4px heading, +8px display) -- */
  --font-3xs: 0.625rem;  /* 10px */
  --font-2xs: 0.75rem;   /* 12px */
  --font-xs: 0.875rem;   /* 14px */
  --font-s: 1rem;        /* 16px */
  --font-m: 1.125rem;    /* 18px */
  --font-l: 1.25rem;     /* 20px */
  --font-xl: 1.375rem;   /* 22px */
  --font-2xl: 1.5rem;    /* 24px */
  --font-3xl: 1.75rem;   /* 28px */
  --font-4xl: 2rem;      /* 32px */
  --font-5xl: 2.25rem;   /* 36px */
  --font-6xl: 2.5rem;    /* 40px */
  --font-7xl: 3rem;      /* 48px */
  --font-8xl: 3.5rem;    /* 56px */
  --font-9xl: 4rem;      /* 64px */
  --font-10xl: 4.5rem;   /* 72px */


  /* -- UI font sizes -- */

  /* -- Line height tokens -- */
  --line-height-xs: 0.7;
  --line-height-s: 1;
  --line-height-m: 1.2;
  --line-height-l: 1.4;
  --line-height-xl: 1.6;
  --line-height-2xl: 1.8;

  /* -- Font weight tokens -- */
  --font-weight-light: 300;
  --font-weight-regular: 400;
  --font-weight-book: 450;
  --font-weight-medium: 500;
  --font-weight-semi-bold: 600;
  --font-weight-bold: 700;
  --font-weight-extra-bold: 800;
  --font-weight-black: 900;

  /* -- Font width tokens (wdth axis, applied via font-stretch) -- */
  --font-condensed: 75%;
  --font-narrow:    87.5%;
  --font-normal:    100%;
  --font-wide:      112.5%;
  --font-expanded:  125%;

  /* -- Letter spacing tokens (em-based for proportional scaling) -- */
  --letter-spacing-xs: -0.03em;
  --letter-spacing-s: 0.03em;
  --letter-spacing-m: 0.06em;
  --letter-spacing-l: 0.12em;
  --letter-spacing-xl: 0.24em;

  /* -- Type roles — semantic tokens composing the primitives into four
     role-based building blocks (headline, title, label, body). Each role
     covers four dimensions: size, weight, leading (line-height), and
     tracking (letter-spacing).

     Sizes are stepped rem, never clamp() with a vw term: a vw component in
     font-size doesn't track browser zoom or user font-size preferences
     (WCAG 1.4.4 Resize Text). Each role steps down through the width tiers
     below this block. -- */

  /* Headline — the biggest thing on a page; one per page (h1). */
  --headline-size:     var(--font-8xl);  /* 56px · 48px ≤1439 · 28px ≤959 */
  --headline-weight:   400;
  --headline-leading:  var(--line-height-s);
  --headline-tracking: -0.04em;
  --headline-width: var(--font-condensed);

  /* Subline — sub-heading scale, sits between title and body. Independent
     of --text-size-l so it can be tuned without dragging modifier text. */
  --subline-size:    var(--font-2xl);    /* 24px · 20px ≤1439 */
  --subline-weight:  300;


  /* Title — section heads and recurring labels within a page (h2). */
  --title-size:     var(--font-6xl);     /* 40px · 36px ≤1439 · 24px ≤959 */
  --title-weight:   400;
  --title-leading:  var(--line-height-m);
  --title-tracking: -0.04em;
  --title-family: var(--font-primary);


  /* Label — meta rows, eyebrows, captions */
  --label-size:     var(--font-2xs);
  --label-weight:   500;
  --label-leading:  var(--line-height-s);
  --label-tracking: var(--letter-spacing-m);

  /* Body — running text. One size at every width: the old fluid ceiling
     (18px) only engaged beyond ~1900px viewports, so 16px is the honest
     static value. */
  --body-size:     var(--font-s);
  --body-weight:   var(--font-weight-regular);
  --body-leading:  var(--line-height-xl);
  --body-tracking: 0;

  /* Text size utility scale — stepped tokens consumed by .text-size-* utilities.
     Use these on any selector when you want a utility-style size without the class. */
  --text-size-xl: var(--font-3xl);       /* 28px · 24px ≤959 */
  --text-size-l:  var(--font-l);         /* 20px */
  --text-size-m:  var(--body-size);
  --text-size-s:  var(--font-xs);        /* 14px — the old fluid ceiling (16px)
                                            only engaged beyond ~1400px */
  --text-size-xs: var(--font-xs);

  /* Icon Size */
  --icon-size: 1.3rem;

}

/* Laptop typography tier — the top-of-scale sizes are reserved for wide
   screens; below 1440 the three heading roles step down one rung. */
@media (max-width: 1439px) {
  :root {
    --headline-size:  var(--font-7xl);   /* 48px */
    --title-size:     var(--font-5xl);   /* 36px */
    --subline-size:   var(--font-l);     /* 20px */
  }
}

/* Mobile typography tier — static rem values for the single-column range.
   Tablet uses mobile typography because the layout is also single-column
   at that width (see [data-grid] collapse at the same breakpoint).

   --label-size is bumped from --font-2xs (12px) to --font-xs (14px) on
   mobile — 12px is below the practitioner floor for body-adjacent text
   and tap-target-adjacent labels. */
@media (max-width: 959px) {
  :root {
    --headline-size:  var(--font-3xl);   /* 28px */
    --title-size:     var(--font-2xl);   /* 24px */
    --subline-size:   var(--font-l);     /* 20px */
    --label-size:     var(--font-xs);    /* 14px (a11y floor) */

    --text-size-xl:   var(--font-2xl);   /* 24px */
    --text-size-l:    var(--font-l);     /* 20px */
    /* --body-size/--text-size-m (16px) and --text-size-s/-xs (14px)
       hold their size at every width */
  }
}

/* ------ 2. SYSTEM TOKENS - COLORS ------ */

:root {
  /* -- Neutrals -- */
  --neutral-50:  #fafafa;
  --neutral-100: #e5e5e5;
  --neutral-150: #d4d4d4;
  --neutral-200: #c4c4c4;
  --neutral-300: #a3a3a3;
  --neutral-400: #8a8a8a;
  --neutral-500: #737373;
  --neutral-600: #5c5c5c;
  --neutral-700: #474747;
  --neutral-800: #333333;
  --neutral-900: #1f1f1f;
  --neutral-950: #141414;
  --neutral-990: #0a0a0a;

  /* -- Black & White Alpha Tokens -- */
  --black: #000000;
  --black-alpha-3: #00000008;
  --black-alpha-5: #0000000d;
  --black-alpha-7: #00000012;
  --black-alpha-10: #0000001a;
  --black-alpha-15: #00000026;
  --black-alpha-20: #00000033;
  --black-alpha-30: #0000004d;
  --black-alpha-40: #00000066;
  --black-alpha-50: #00000080;
  --black-alpha-60: #00000099;
  --black-alpha-70: #000000b3;
  --black-alpha-80: #000000cc;
  --black-alpha-90: #000000e6;
  --black-alpha-95: #000000f2;

  --white: #ffffff;
  --white-alpha-3: #ffffff08;
  --white-alpha-5: #ffffff0d;
  --white-alpha-7: #ffffff12;
  --white-alpha-10: #ffffff1a;
  --white-alpha-15: #ffffff26;
  --white-alpha-20: #ffffff33;
  --white-alpha-30: #ffffff4d;
  --white-alpha-40: #ffffff66;
  --white-alpha-50: #ffffff80;
  --white-alpha-60: #ffffff99;
  --white-alpha-70: #ffffffb3;
  --white-alpha-80: #ffffffcc;
  --white-alpha-90: #ffffffe6;
  --white-alpha-95: #fffffff2;

  --transparent: transparent;

  /* -- Color-Mix Alpha Scale -- */
  --alpha-5: transparent 95%;
  --alpha-10: transparent 90%;
  --alpha-15: transparent 85%;
  --alpha-20: transparent 80%;
  --alpha-25: transparent 75%;
  --alpha-30: transparent 70%;
  --alpha-35: transparent 65%;
  --alpha-40: transparent 60%;
  --alpha-45: transparent 55%;
  --alpha-50: transparent 50%;
  --alpha-55: transparent 45%;
  --alpha-60: transparent 40%;
  --alpha-65: transparent 35%;
  --alpha-70: transparent 30%;
  --alpha-75: transparent 25%;
  --alpha-80: transparent 20%;
  --alpha-85: transparent 15%;
  --alpha-90: transparent 10%;
  --alpha-95: transparent 5%;

  /* -- Semantic colours -- */
  --text-primary: var(--off-black);
  --text-secondary: var(--neutral-700);
  --text-plain: var(--black);
  /* a11y: alpha-60 → 5.7:1 on white; alpha-50 computed to 3.9:1, below the
     4.5:1 WCAG AA minimum for the body copy this token is used on */
  --text-faded: var(--black-alpha-60);
  --text-accent: var(--blue);
  --text-link: var(--text-accent);
  --text-inverted: var(--off-white);
  --text-sidebar: var(--text-primary);
  --text-top-nav: var(--text-primary);

    /* -- Text with Opacity -- */
  --text-alpha-5: color-mix(in srgb, var(--text-primary), var(--alpha-5));
  --text-alpha-10: color-mix(in srgb, var(--text-primary), var(--alpha-10));
  --text-alpha-15: color-mix(in srgb, var(--text-primary), var(--alpha-15));
  --text-alpha-20: color-mix(in srgb, var(--text-primary), var(--alpha-20));
  --text-alpha-30: color-mix(in srgb, var(--text-primary), var(--alpha-30));
  --text-alpha-40: color-mix(in srgb, var(--text-primary), var(--alpha-40));
  --text-alpha-50: color-mix(in srgb, var(--text-primary), var(--alpha-50));



  --background-primary: var(--white);
  --background-secondary: var(--neutral-100);
  --background-plain: var(--white);
  --background-faded: var(--black-alpha-5);
  --background-darker: var(--black-alpha-10);
  --background-lighter: var(--white-alpha-10);
  --bg-faded-3: var(--black-alpha-3);
  --bg-faded-5: var(--black-alpha-5);
  --bg-faded-7: var(--black-alpha-7);
  --bg-faded-10: var(--black-alpha-10);
  --bg-faded-15: var(--black-alpha-15);
  --background-modal: rgba(0, 0, 0, 0.75);
  --background-sidebar: var(--background-primary);
  --background-top-nav: var(--background-primary);

  /* -- Solid colour backgrounds — for hero blocks, callouts, ad units, etc. -- */
  --background-accent: var(--text-accent);
  --background-black: var(--black);
  --background-white: var(--white);
  --background-blue: var(--blue);
  --background-red: var(--red);
  --background-green: var(--green);

  --border-primary: var(--text-primary);
  --border-secondary: var(--neutral-300);
  --border-faded: var(--black-alpha-15);
  /* The boundary of an interactive control whose surface is transparent — the
     border is then the only thing saying "this is pressable", so it carries the
     3:1 that 1.4.11 asks of a control boundary. --border-faded is decorative
     (1.4:1) and must not be used for one. 4.74:1 on --background-primary. */
  --border-control: var(--neutral-500);


  /* -- Status colours (from brand book or defaults) --
     Text and surface are a pair: -dark text on a -lighter surface. The mid
     primitives only reach 3.0-4.2:1 on their own pastels — under AA for small
     text — so every hue's text takes the -dark step, as warning always has.
     Buttons consume the text half alone and follow it deliberately. */
  --status-info: var(--blue-dark);
  --status-info-bg: var(--blue-lighter);
  --status-success: var(--green-dark);
  --status-success-bg: var(--green-lighter);
  --status-warning: var(--yellow-dark);
  --status-warning-bg: var(--yellow-lighter);
  --status-danger: var(--red-dark);
  --status-danger-bg: var(--red-lighter);
  /* -- Form semantic tokens -- */
  --input-border: var(--border-secondary);
  --input-background: var(--black-alpha-5);
  --input-text: var(--text-plain);
  --input-placeholder: var(--text-faded);
  --input-focus: var(--text-accent);
  --input-disabled-bg: var(--background-faded);
  --input-disabled-text: var(--text-faded);
  --checkbox-background: var(--neutral-100);
  --checkbox-selected: var(--text-primary);
  --checkbox-border: var(--border-faded);
  --checkbox-checkmark: var(--off-white);
  /* -- Toggle color tokens removed — uses values directly on component -- */

  /* -- Selection -- */
  /* Two neighbours one letter apart, doing different jobs. --selection-* is the
     browser's text highlight (::selection). --selected-* is the state a chosen
     control wears: a checked chip, a segmented control's active option. They
     happen to resolve to the same inverted neutral today; they are separate
     names because a brand can move one without the other. */
  --selection-text: var(--background-primary);
  --selection-background: var(--text-primary);

  /* The system's selected pair. A wash cannot carry state — the --bg-faded-*
     scale tops out near 1.4:1 where WCAG wants 3:1 of a state indicator — so
     selection is an inverted surface, never a tint. Shared by both segmented
     forms, the chip group and the docs A-Z rail. */
  --selected-background: var(--text-primary);
  --selected-text: var(--background-primary);

  /* -- Card tokens removed — uses semantic tokens directly -- */

  /* -- Tooltip tokens removed — uses values directly on component -- */

  /* -- Toast tokens removed — uses values directly on component -- */

  /* -- Tab tokens removed — uses semantic tokens directly -- */

  /* -- Progress tokens removed — uses semantic tokens directly -- */

  /* -- Divider tokens -- */
  --divider-color: var(--border-faded);
  --divider-thickness: var(--border-s);
  --divider-spacing: var(--space-xl);

  /* -- Dropdown tokens -- */
  --dropdown-background: var(--background-primary);
  --dropdown-border: var(--border-faded);
  --dropdown-item-hover: var(--background-faded);
  /* A floor, not a width — menus grow past it on their content. It is a knob
     because it is the one thing that overflows a 375px screen, so a narrow
     context needs to drop it rather than override the rule. */
  --dropdown-menu-min-width: 250px;

  /* -- Select picker tokens -- */
  /* The customizable-select popup reuses the dropdown surface tokens above so
     a native select and a .dropdown read as the same object. Only the scroll
     ceiling is its own, because an option list has no item count limit. */
  --select-picker-max-height: 18rem;

  /* -- Tag tokens removed — uses semantic tokens directly -- */

  /* -- Dialog tokens -- */
  --dialog-max-width: 560px;

  /* -- Slider tokens removed — uses semantic tokens directly -- */

  /* -- Rating tokens -- */
  --rating-size: 1.5rem;
  /* -- Rating color tokens removed — uses values directly on component -- */

  /* -- Mark tokens removed — uses values directly on component -- */

  color-scheme: light;
}

/* ------ 2b. THEME TOKENS - DARK MODE ------ */
/* Set data-theme="dark" on <html> for page-level, or on any element for scoped theming */

[data-theme="dark"] {
  /* -- Text -- */
  --text-primary: #e8e6e3;
  --text-secondary: #a8a5a2;
  --text-plain: #f0eeeb;
  /* a11y: 0.5 → ~5.0:1 on --neutral-900; 0.45 was 4.4:1, marginal for AA */
  --text-faded: rgba(255, 255, 255, 0.5);
  --text-accent: var(--yellow);
  --text-link: var(--text-accent);
  --text-inverted: #1a1a1a;
  --text-sidebar: var(--text-primary);
  --text-top-nav: var(--text-primary);

  /* -- Background -- */
  --background-primary: var(--neutral-900);
  --background-secondary: #222222;
  --background-plain: #2a2a2a;
  --background-faded: rgba(255, 255, 255, 0.06);
  --background-darker: rgba(255, 255, 255, 0.12);
  --background-modal: var(--black-alpha-50);
  --background-sidebar: var(--background-primary);
  --background-top-nav: var(--background-primary);
  --bg-faded-3: var(--white-alpha-3);
  --bg-faded-5: var(--white-alpha-5);
  --bg-faded-7: var(--white-alpha-7);
  --bg-faded-10: var(--white-alpha-10);
  --bg-faded-15: var(--white-alpha-15);

  /* -- Border -- */
  --border-primary: #e8e6e3;
  --border-secondary: #3a3a3a;
  --border-faded: var(--white-alpha-20);
  /* Lighter than the light-theme value, not darker — contrast runs the other
     way against a dark ground. 4.77:1 on --background-primary. */
  --border-control: var(--neutral-400);

  /* -- Status -- */
  --status-info: var(--blue-light);
  --status-success: var(--green-light);
  --status-warning: var(--yellow-light);
  --status-danger: var(--red-light);
  /* The pair flips as a pair: light text over the page ground pulled 15%
     toward the hue. Anchored to --background-primary rather than --brown-dark
     (a toast-local colour), so a rethemed dark ground carries these surfaces
     with it. Holds every variant above 6.6:1 (danger is the floor, since
     --red-light carries more chroma than the other three). */
  --status-info-bg: color-mix(in srgb, var(--status-info), var(--background-primary) 85%);
  --status-success-bg: color-mix(in srgb, var(--status-success), var(--background-primary) 85%);
  --status-warning-bg: color-mix(in srgb, var(--status-warning), var(--background-primary) 85%);
  --status-danger-bg: color-mix(in srgb, var(--status-danger), var(--background-primary) 85%);

  /* -- Form -- */
  --input-border: #3a3a3a;
  --input-background: rgba(255, 255, 255, 0.08);
  --input-text: #f0eeeb;
  --input-placeholder: rgba(255, 255, 255, 0.45);
  --input-focus: var(--text-accent);
  --input-disabled-bg: rgba(255, 255, 255, 0.06);
  --input-disabled-text: rgba(255, 255, 255, 0.3);
  --checkbox-background: #3a3a3a;
  --checkbox-selected: #e8e6e3;
  --checkbox-border: #555;
  --checkbox-checkmark: #1a1a1a;
  /* -- Toggle/Card/Tooltip/Toast/Dialog/Mark color tokens removed — dark mode handled on components -- */

  /* -- Dropdown -- */
  --dropdown-background: var(--background-plain);

  color-scheme: dark;
}

/* ------ 2c. SYSTEM PREFERENCE FALLBACK (no-JS) ------ */
/* Applies dark tokens when the user's OS prefers dark and no data-theme is set */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --text-primary: #e8e6e3;
    --text-secondary: #a8a5a2;
    --text-plain: #f0eeeb;
    --text-faded: rgba(255, 255, 255, 0.5); /* AA — see [data-theme="dark"] above */
    --text-accent: var(--yellow);
    --text-link: var(--text-accent);
    --text-inverted: #1a1a1a;
    --text-sidebar: var(--text-primary);
    --text-top-nav: var(--text-primary);

    --background-primary: var(--neutral-900);
    --background-secondary: #222222;
    --background-plain: #2a2a2a;
    --background-faded: rgba(255, 255, 255, 0.06);
    --background-darker: rgba(255, 255, 255, 0.12);
    --background-modal: var(--black-alpha-50);
    --background-sidebar: var(--background-primary);
    --background-top-nav: var(--background-primary);
    --bg-faded-3: var(--white-alpha-3);
    --bg-faded-5: var(--white-alpha-5);
    --bg-faded-7: var(--white-alpha-7);
    --bg-faded-10: var(--white-alpha-10);
    --bg-faded-15: var(--white-alpha-15);

    --border-primary: #e8e6e3;
    --border-secondary: #3a3a3a;
    --border-faded: var(--white-alpha-20);
    --border-control: var(--neutral-400);

    --status-info: var(--blue-light);
    --status-success: var(--green-light);
    --status-warning: var(--yellow-light);
    --status-danger: var(--red-light);
    /* Mirror of the [data-theme="dark"] status surfaces above */
    --status-info-bg: color-mix(in srgb, var(--status-info), var(--background-primary) 85%);
    --status-success-bg: color-mix(in srgb, var(--status-success), var(--background-primary) 85%);
    --status-warning-bg: color-mix(in srgb, var(--status-warning), var(--background-primary) 85%);
    --status-danger-bg: color-mix(in srgb, var(--status-danger), var(--background-primary) 85%);

    --input-border: #3a3a3a;
    --input-background: rgba(255, 255, 255, 0.08);
    --input-text: #f0eeeb;
    --input-placeholder: rgba(255, 255, 255, 0.45);
    --input-focus: var(--text-accent);
    --input-disabled-bg: rgba(255, 255, 255, 0.06);
    --input-disabled-text: rgba(255, 255, 255, 0.3);
    --checkbox-background: #3a3a3a;
    --checkbox-selected: #e8e6e3;
    --checkbox-border: #555;
    --checkbox-checkmark: #1a1a1a;
    /* -- Toggle/Card/Tooltip/Toast/Dialog/Mark color tokens removed — dark mode handled on components -- */

    /* -- Dropdown -- */
    --dropdown-background: var(--background-plain);

    color-scheme: dark;
  }
}

/* -- Selection highlight -- */
::selection {
  color: var(--selection-text);
  background-color: var(--selection-background);
}

/* ------ 2d. COLOUR STEPS (data-color + data-step) ------ */
/*
 * One attribute picks a colour family, a second picks which of its three
 * stages becomes the ground. data-color="purple" data-step="shade".
 *
 *   tint   the pale stage  — the default, written or omitted
 *   base   the pure hue    — the only stage that does NOT follow the theme
 *   shade  the deep stage
 *
 * tint and shade swap under [data-theme="dark"], so a tint band stays quiet
 * against the page and a shade band stays loud, in either theme. base holds
 * its colour in both, which is what it is for.
 *
 * NOTHING HERE PAINTS. These four layers only declare custom properties, so
 * a stray data-color on a div changes no pixels. Painting happens in the
 * per-component assignment rules, each next to its own component below.
 * That is deliberate: .badge[data-color="danger"] and .button[data-color="red"]
 * keep their status colours, because no rule here selects them and --status-*
 * is never redefined.
 */

/* -- 1. Steps. Three stages, named for what they are: tint, base, shade. ---
   These are ROLE names, not claims about mixing. The house ramps are
   --<hue>-lighter / --<hue> / --<hue>-dark, and this layer translates them, so
   the attribute value, the step token and the step all read the same word:
   data-step="shade" selects --color-shade. Naming the middle --color-base
   rather than --color-hue matters, because "hue" is already this project's word
   for the colour family — the thing data-color selects — and one word cannot
   carry two axes on the same element.

   The house ramp's fourth step, --<hue>-light, has no stage in a three-stage
   map and nothing ever read it, so it is gone rather than carried unnamed.

   --color-on-base is not a step. It is which of black or white sits ON the base
   colour. It is never a free choice: take the better of the two and the answer
   is forced by the hue. Today that is white on blue, purple, green and red, and
   black on yellow, aqua, pink and peach — every one of them the higher figure,
   with no compromise anywhere. Section 1 prints the measurement next to each
   base step rather than asserting it here, so this comment cannot go stale.

   Purple is worth remembering. It was the one exception: on the old --purple
   (#AA4FE3) white measured 4.25:1 against black's 4.94:1, and white was chosen
   anyway, knowingly below AA for body copy. The ramp has since moved to
   #881BDC, where white is 6.49:1 and black only 3.24:1 — so white is now both
   the chosen ink and the correct one, and the exception is gone. Nothing in the
   mode changed to achieve that.

   --color-off-base is simply the other one, and it is what gives the base
   step a raised surface — see section 4. */

[data-color="blue"]   { --color-tint: var(--blue-lighter);   --color-base: var(--blue);   --color-shade: var(--blue-dark);   --color-on-base: var(--white); --color-off-base: var(--black); }
[data-color="purple"] { --color-tint: var(--purple-lighter); --color-base: var(--purple); --color-shade: var(--purple-dark); --color-on-base: var(--white); --color-off-base: var(--black); }
[data-color="green"]  { --color-tint: var(--green-lighter);  --color-base: var(--green);  --color-shade: var(--green-dark);  --color-on-base: var(--white); --color-off-base: var(--black); }
[data-color="yellow"] { --color-tint: var(--yellow-lighter); --color-base: var(--yellow); --color-shade: var(--yellow-dark); --color-on-base: var(--black); --color-off-base: var(--white); }
[data-color="red"]    { --color-tint: var(--red-lighter);    --color-base: var(--red);    --color-shade: var(--red-dark);    --color-on-base: var(--white); --color-off-base: var(--black); }
[data-color="aqua"]   { --color-tint: var(--aqua-lighter);   --color-base: var(--aqua);   --color-shade: var(--aqua-dark);   --color-on-base: var(--black); --color-off-base: var(--white); }
[data-color="pink"]   { --color-tint: var(--pink-lighter);   --color-base: var(--pink);   --color-shade: var(--pink-dark);   --color-on-base: var(--black); --color-off-base: var(--white); }
[data-color="peach"]  { --color-tint: var(--peach-lighter);  --color-base: var(--peach);  --color-shade: var(--peach-dark);  --color-on-base: var(--black); --color-off-base: var(--white); }

/* -- 2. Roles. Step picks which end of the ramp is the ground. --------- */

[data-color] {
  --color-background: var(--color-tint);
  --color-text: var(--color-shade);
}

[data-color][data-step="shade"] {
  --color-background: var(--color-shade);
  --color-text: var(--color-tint);
}

/* The pure hue, fixed in both themes. */
[data-color][data-step="base"] {
  --color-background: var(--color-base);
  --color-text: var(--color-on-base);
}

/* -- 3. Dark swaps the two ends. base does not move. ------------------
   :where() zeroes the theme part of each selector so all three land at the same
   specificity and source order decides, nearest-written-last. Without it the
   media form is (0,4,0) and the light form (0,3,0), so on an OS-dark machine
   with no attribute on <html> a scoped light region would still get the dark
   mapping. Order is deliberate: media, then light, then dark last — which is
   the only order that keeps dark-inside-light working, the one direction the
   system itself supports (there is no [data-theme="light"] block in
   design-system.css, so light-inside-dark is unsupported there too).

   The tint arm is written as "not shade and not base" rather than by
   listing :not([data-step]) and ="tint". Enumerating the known values means
   a typo — data-step="Tint", or an empty value — matches no theme arm at
   all and silently pins that region to the light mapping in both themes. */

@media (prefers-color-scheme: dark) {
  :where(:root:not([data-theme])) [data-color]:where(:not([data-step="shade"]):not([data-step="base"])) {
    --color-background: var(--color-shade);
    --color-text: var(--color-tint);
  }
  :where(:root:not([data-theme])) [data-color][data-step="shade"] {
    --color-background: var(--color-tint);
    --color-text: var(--color-shade);
  }
}
:where([data-theme="light"]) [data-color]:where(:not([data-step="shade"]):not([data-step="base"])) {
  --color-background: var(--color-tint);
  --color-text: var(--color-shade);
}
:where([data-theme="light"]) [data-color][data-step="shade"] {
  --color-background: var(--color-shade);
  --color-text: var(--color-tint);
}
:where([data-theme="dark"]) [data-color]:where(:not([data-step="shade"]):not([data-step="base"])) {
  --color-background: var(--color-shade);
  --color-text: var(--color-tint);
}
:where([data-theme="dark"]) [data-color][data-step="shade"] {
  --color-background: var(--color-tint);
  --color-text: var(--color-shade);
}

/* -- 4. Two derived roles. ------------------------------------------------
   raised — a nested surface sitting on the ground. A step will not do the job:
   in tint the next step along works, but in shade nothing sits between shade
   and base, and in base nothing sits beside base at all. One mix adapts to
   all three and to both themes.

   muted — a second, quieter text colour, for a subtitle or a caption.

   On the base step the raise changes DIRECTION, and that is the whole
   trick. The base step is the tightest ground in the set — white on --red is 4.87:1, the lowest
   of the eight — so moving the surface toward the text costs contrast it cannot
   spare, measured 4.33:1 on red at a 10% raise toward the ink.
   Moving it AWAY instead spends nothing: it widens the gap. Because the ink on the base
   step is always literally black or white, "away" has an exact name, which
   is --color-off-base, and mixing 15% of it lands text at 5.9-12.7:1 — better
   than the header ground manages on its own — with a surface shift of 1.09-1.30
   against the 1.18-1.31 that tint and shade get.

   So the rule is: raised moves toward the ink where there is room, and away
   from it where there is not. Two directions, one reason.

   muted has no such escape. It IS the text, so moving it away from the ground
   is not an option — there is nowhere for it to go that is not the text itself.
   On base it stays at full strength.

   muted is 90%, not the 80% this started at. 80% was calibrated against five
   ramps whose text-on-ground sits at 9.5-13.5:1, and it had 4.65:1 of headroom
   at its tightest. aqua and pink are shallower — 7.03:1 and 7.90:1 — because
   their dark steps (#15605A, #7F2B56) are lighter than the other five
   (#001F64, #3A005F, #003E37, #583800, #4E0100). At 80% that put muted at
   4.37:1 on aqua and muted-on-raised at 3.76:1, both under AA. 90% clears every
   hue in both themes and costs the deep ramps nothing they need: blue's muted
   moves 7.59 -> 10.22 against a 13.11 ground, still visibly quieter than the
   text. The alternative fix is deeper dark steps on the two new ramps, which is
   a change to theme.css rather than to the mode.

   border-faded — a decorative boundary, not a control boundary. The system's own
   --border-faded is an alpha wash of black (--black-alpha-15) or, in dark, of
   white. That polarity is chosen for the page, and a moded region is light
   inside dark or dark inside light, so the root's choice is wrong for it: black
   at 15% is invisible on a shade ground. Mixing toward --color-text instead
   keeps the wash but takes its polarity from the mode. 25% was picked to sit on
   the system's own band — the root token measures 1.41:1 in light and 1.89:1 in
   dark, and 25% spans 1.40-2.02 across all thirty hue x step x theme
   combinations. It is deliberately below the 3:1 of WCAG 1.4.11: this is the
   decorative border, and 1.4.11 governs control boundaries, which take
   --border-control. */

[data-color] {
  --color-raised: color-mix(in srgb, var(--color-text) 10%, var(--color-background));
  --color-text-muted: color-mix(in srgb, var(--color-text) 90%, var(--color-background));
  --color-border-faded: color-mix(in srgb, var(--color-text) 25%, var(--color-background));
  --color-hover-mix: 75%;
}
[data-color][data-step="base"] {
  --color-raised: color-mix(in srgb, var(--color-off-base) 15%, var(--color-background));
  --color-text-muted: var(--color-text);
  --color-hover-mix: 95%;
}

/* ------ 3. SYSTEM TOKENS - SPACING & TYPOGRAPHY ------ */


:root {
  /* -- Unit tokens -- */
  --none: 0;
  --2xs: 0.125rem;
  /* 2px */
  --xs: 0.25rem;
  /* 4px */
  --s: 0.5rem;
  /* 8px */
  --m: 0.75rem;
  /* 12px */
  --l: 1rem;
  /* 16px */
  --xl: 1.5rem;
  /* 24px */
  --2xl: 2rem;
  /* 32px */
  --3xl: 2.5rem;
  /* 40px */
  --4xl: 3rem;
  /* 48px */
  --5xl: 3.5rem;
  /* 56px */
  --6xl: 4rem;
  /* 64px */
  --7xl: 4.5rem;
  /* 72px */
  --8xl: 5rem;
  /* 80px */
  --9xl: 5.5rem;
  /* 88px */
  --10xl: 6rem;
  /* 96px */
  --11xl: 6.5rem;
  /* 104px */
  --12xl: 7rem;
  /* 112px */
  --13xl: 7.5rem;
  /* 120px */
  --14xl: 10rem;
  /* 160px */

  /* -- Spacing scale -- */
  --space-none: var(--none); /* 0 */
  --space-2xs: var(--2xs); /* 2px */
  --space-xs: var(--xs); /* 4px */
  --space-s: var(--s); /* 8px */
  --space-m: var(--m); /* 12px */
  --space-l: var(--l); /* 16px */
  --space-xl: var(--xl); /* 24px */
  --space-2xl: var(--2xl); /* 32px */
  --space-3xl: var(--3xl); /* 40px */
  --space-4xl: var(--4xl); /* 48px */
  --space-5xl: var(--5xl); /* 56px */
  --space-6xl: var(--6xl); /* 64px */
  --space-7xl: var(--7xl); /* 72px */
  --space-8xl: var(--8xl); /* 80px */
  --space-9xl: var(--9xl); /* 88px */
  --space-10xl: var(--10xl); /* 96px */
  --space-11xl: var(--11xl); /* 104px */
  --space-12xl: var(--12xl); /* 112px */
  --space-13xl: var(--13xl); /* 120px */
  --space-14xl: var(--14xl); /* 160px */

  /* -- Container widths -- */
  --container-xs: 480px;
  --container-s: 640px;
  --container-m: 960px;
  --container-l: 1200px;
  --container-xl: 1440px;

  /* -- Max-widths -- */
  --max-width-xs: 480px;
  --max-width-s: 640px;
  --max-width-m: 960px;
  --max-width-l: 1200px;
  --max-width-xl: 1440px;

  /* -- Line length (typographic readability, 45–75ch range) -- */
  --line-length-headline: 22ch;
  --line-length-body: 55ch;

  --line-length-xsmall: 35ch;
  --line-length-small: 45ch;
  --line-length-medium: 65ch;
  --line-length-wide: 75ch;

  /* -- Section spacing variables -- */
  --section-xs: var(--space-xl);
  --section-s: var(--space-2xl);
  --section-m: var(--space-6xl);
  --section-l: var(--space-10xl);
  --section-xl: var(--space-14xl);


  /* -- Accordion spacing variables -- */
  --accordion-padding-x: var(--space-l);
  --accordion-padding-y: var(--space-l);

  /* -- Border width tokens -- */
  --border-s: 0.0625rem;   /* 1px */
  --border-m: 0.09375rem;  /* 1.5px */
  --border-l: 0.1875rem;   /* 3px */

  /* -- Border composition variables -- */
  --border-width: var(--border-s);
  --border-style: solid;
  --border-color: var(--border-primary);

  /* -- Border radius tokens -- */
  --radius-2xs: 2px;
  --radius-xs: 4px;
  --radius-s: 6px;
  --radius-m: 10px;
  --radius-l: 16px;
  --radius-xl: 24px;
  --radius-pill: 999px;

  /* -- Interaction sizing -- */
  /* Minimum hit area for standalone pointer targets: WCAG 2.5.5 Target Size
     (Enhanced, Level AAA) and the Apple HIG target. The AA floor is 2.5.8's
     24px (WCAG 2.2). Applied as a min size on small controls — the visual
     size stays, the hit area pads out. Not for dense chrome: bar controls
     hold their own --bar-control-height and answer 2.5.8 instead. Not a
     per-theme override surface. */
  --target-min: 44px;

  /* -- Safe-area insets -- */
  /* Device cutouts (notch, home indicator) in installed-PWA / standalone
     contexts. 0px in a normal browser tab, so composing them is always safe:
     padding-bottom: calc(var(--space-m) + var(--safe-area-bottom)).
     Non-zero only when the page's viewport meta carries viewport-fit=cover. */
  --safe-area-top: env(safe-area-inset-top, 0px);
  --safe-area-right: env(safe-area-inset-right, 0px);
  --safe-area-bottom: env(safe-area-inset-bottom, 0px);
  --safe-area-left: env(safe-area-inset-left, 0px);
}

/* ------ 4. SYSTEM TOKENS - MOTION ------ */
/*
 * Motion design tokens. Two layers, same model as color and spacing:
 *
 *   1. Primitives (--ease-*, --duration-*) — raw building blocks. Same
 *      t-shirt scale as the rest of the design system. Easing names match
 *      the standard CSS keywords (in / out / in-out) so anyone with web
 *      experience already knows what shape they have; the cubic-bezier
 *      values are our refined replacements for the defaults.
 *
 *   2. Semantic tokens (--motion-*) — named by intent ("page-open",
 *      "page-close"). These compose primitives. Consumers (CSS components,
 *      JS code) only ever read the semantic layer.
 *
 * Naming convention for new semantic tokens:
 *   --motion-{scope}-{event}-{property}
 *   scope    = page | surface | element | feedback
 *   event    = open | close | swap | enter | exit | fade | hover | press
 *   property = duration | easing
 *
 * See cms/motion.md for the full reference and demos.
 */

:root {
  /* -- Easing primitives -- */
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
  /* fast start, slow end. for exits. */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  /* slow start, slow end with long tail. for entrances. */
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  /* symmetric. for swaps and continuous motion. */
  --ease-emphasized: cubic-bezier(0.86, 0, 0.07, 1);
  /* steeper symmetric curve — near-still start and end, fast middle.
     for large layout shifts that should feel decisive (panel collapse,
     sidebar toggle, stage transforms). */

  /* -- Duration primitives -- */
  --duration-2xs: 100ms;
  --duration-xs: 200ms;
  --duration-s: 400ms;
  --duration-m: 600ms;
  --duration-l: 800ms;
  --duration-xl: 1200ms;
  --duration-2xl: 1500ms;
  --duration-3xl: 1800ms;
  --duration-4xl: 2000ms;
  --duration-5xl: 2500ms;
  --duration-6xl: 3000ms;
  --duration-7xl: 4000ms;
  --duration-8xl: 6000ms;

  /* -- Stagger offset — per-item delay step for sequenced groups (list
     items, grid cards). A delay increment, not an animation duration. -- */
  --duration-stagger: 30ms;


  /* -- Page-level semantic motion -- */
  --motion-page-open-duration: var(--duration-xl);
  /* 1200ms */
  --motion-page-open-easing: var(--ease-in-out);

  --motion-page-close-duration: var(--duration-xl);
  /* 1200ms */
  --motion-page-close-easing: var(--ease-in-out);

  --motion-page-swap-duration: var(--duration-xl);
  /* 1200ms */
  --motion-page-swap-easing: var(--ease-in-out);

  --motion-page-fade-duration: var(--duration-m);
  /* 600ms */
  --motion-page-fade-easing: var(--ease-in-out);
}

/* ------ 5. LAYOUT PRIMITIVES ------ */

/* -- Global horizontal padding -- */
.padding-global {
  padding-left: var(--space-xl);
  padding-right: var(--space-xl);
}

/* -- Padding utilities -- */
.padding-s {
  padding: var(--space-s);
}

.padding-m {
  padding: var(--space-m);
}

.padding-l {
  padding: var(--space-l);
}

.padding-xl {
  padding: var(--space-xl);
}

.padding-2xl {
  padding: var(--space-2xl);
}

.padding-3xl {
  padding: var(--space-3xl);
}

/* -- Containers -- */
.container-xs {
  display: block;
  max-width: var(--container-xs);
  width: 100%;
  margin-left: auto;
  margin-right: auto;
}

.container-s {
  display: block;
  max-width: var(--container-s);
  width: 100%;
  margin-left: auto;
  margin-right: auto;
}

.container-m {
  display: block;
  max-width: var(--container-m);
  width: 100%;
  margin-left: auto;
  margin-right: auto;
}

.container-l {
  display: block;
  max-width: var(--container-l);
  width: 100%;
  margin-left: auto;
  margin-right: auto;
}

.container-xl {
  display: block;
  max-width: var(--container-xl);
  width: 100%;
  margin-left: auto;
  margin-right: auto;
}

/* -- Max-width utilities -- */
.max-width-xs {
  max-width: var(--max-width-xs);
}

.max-width-s {
  max-width: var(--max-width-s);
}

.max-width-m {
  max-width: var(--max-width-m);
}

.max-width-l {
  max-width: var(--max-width-l);
}

.max-width-xl {
  max-width: var(--max-width-xl);
}

.max-width-full {
  max-width: 100%;
}

/* -- Section spacing -- */

section {
  display: block;
}

.padding-section {
  display: block;
}

/* -- Top spacing combo classes -- */
.top-small {
  padding-top: var(--section-s);
}

.top-medium {
  padding-top: var(--section-m);
}

.top-large {
  padding-top: var(--section-l);
}

.top-xl {
  padding-top: var(--section-xl);
}

/* -- Bottom spacing combo classes -- */
.bottom-small {
  padding-bottom: var(--section-s);
}

.bottom-medium {
  padding-bottom: var(--section-m);
}

.bottom-large {
  padding-bottom: var(--section-l);
}

.bottom-xl {
  padding-bottom: var(--section-xl);
}

/* ------ 6. BLOCKS ------ */

.block {
  display: flex;
  flex-direction: column;
  gap: var(--space-m);
}

/* -- Child margin reset --
   gap-* is the sole source of vertical spacing inside a block: element
   defaults (headings, p, lists, figure…) are zeroed so gap and margin never
   compound — flex gap does not collapse with margins, it adds to them.
   margin-block only: margin-inline auto must survive for data-align="center".
   Component classes that set their own root margins (e.g. .callout) are
   declared later in the cascade and keep them inside blocks. */
.block > * {
  margin-block: 0;
}

/* -- Gap modifiers -- */
.gap-none {
  gap: var(--space-none);
}

.gap-xs {
  gap: var(--space-xs) !important;
}

.gap-s {
  gap: var(--space-s) !important;
}

.gap-m {
  gap: var(--space-m) !important;
}

.gap-l {
  gap: var(--space-l) !important;
}

.gap-xl {
  gap: var(--space-xl) !important;
}

.gap-2xl {
  gap: var(--space-2xl) !important;
}

.gap-3xl {
  gap: var(--space-3xl) !important;
}


/* -- Layout modifiers -- */
.row {
  flex-direction: row;
}

.row-reverse {
  flex-direction: row-reverse;
}

.align-start {
  align-items: flex-start;
}

.align-center {
  align-items: center;
}

.align-end {
  align-items: flex-end;
}

.align-stretch {
  align-items: stretch;
}

.justify-center {
  justify-content: center;
}

.justify-end {
  justify-content: flex-end;
}

.overflow-hidden {
  overflow: hidden;
}


/* -- Self alignment -- */
[data-align="center"] {
  margin-inline: auto;
  justify-self: center;
}


/* ------ 7. GRID ------ */

/* -- Grid -- */
.grid {
  display: grid;
  gap: var(--space-m);
  grid-template-columns: 1fr 1fr;
  grid-auto-columns: 1fr;
}

/* -- Grid column modifiers -- */
.cols-2 {
  grid-template-columns: 1fr 1fr;
}

.cols-3 {
  grid-template-columns: 1fr 1fr 1fr;
}

.cols-4 {
  grid-template-columns: 1fr 1fr 1fr 1fr;
}

/* -- Responsive collapse -- */
@media (max-width: 768px) {
  .grid {
    grid-template-columns: 1fr;
  }
}

.token-col {
  grid-template-columns: 1fr auto;
  padding: var(--space-m);
}

/* -- Grid item width modifiers -- */
.fit-content {
  width: fit-content;
}

/* ------ 8. BORDER UTILITIES ------ */

/* -- Structural classes -- */
.border {
  border: var(--border-width) var(--border-style) var(--border-color);
}

.border-top {
  border-top: var(--border-width) var(--border-style) var(--border-color);
}

.border-bottom {
  border-bottom: var(--border-width) var(--border-style) var(--border-color);
}

.border-left {
  border-left: var(--border-width) var(--border-style) var(--border-color);
}

.border-right {
  border-right: var(--border-width) var(--border-style) var(--border-color);
}

/* -- Width combo classes -- */
.border-s {
  --border-width: var(--border-s);
}

.border-m {
  --border-width: var(--border-m);
}

.border-l {
  --border-width: var(--border-l);
}

/* -- Style combo classes -- */
.border-solid {
  --border-style: solid;
}

.border-dashed {
  --border-style: dashed;
}

.border-dotted {
  --border-style: dotted;
}

/* -- Color combo classes -- */
.border-primary {
  --border-color: var(--border-primary);
}

.border-secondary {
  --border-color: var(--border-secondary);
}

.border-faded {
  --border-color: var(--border-faded);
}

/* ------ 9. TYPOGRAPHY UTILITIES ------ */

/* -- Font size utilities -- */

.font-primary {
font-family: var(--font-primary);
}

.font-secondary {
font-family: var(--font-secondary);
}

/* -- Accent font treatments --
   Apply on spans inside prose, or to retreat a heading into a different
   voice. Each sets family + sensible companion properties (weight,
   tracking, leading where the face needs it). Size stays whatever the
   host element controls. */

.accent-display {
  font-family: var(--font-display);
  letter-spacing: 0;
  text-transform: uppercase;
}

.accent-playful {
  font-family: var(--font-playful);
  font-weight: 700;
  font-style: normal;
}

.accent-script {
  font-family: var(--font-script);
  font-weight: 400;
  line-height: 1.1;
}

.accent-editorial {
  font-family: var(--font-editorial);
  font-weight: 400;
  font-style: italic;
}

.accent-code {
  font-family: var(--font-code);
  font-weight: 400;
}

.font-2xs {
  font-size: var(--font-2xs);
}

.font-xs {
  font-size: var(--font-xs);
}

.font-s {
  font-size: var(--font-s);
}

.font-m {
  font-size: var(--font-m);
}

.font-l {
  font-size: var(--font-l);
}

.font-xl {
  font-size: var(--font-xl);
}

.font-2xl {
  font-size: var(--font-2xl);
}

.font-3xl {
  font-size: var(--font-3xl);
}

.font-4xl {
  font-size: var(--font-4xl);
}

.font-5xl {
  font-size: var(--font-5xl);
}

.font-6xl {
  font-size: var(--font-6xl);
}

.font-7xl {
  font-size: var(--font-7xl);
}

.font-8xl {
  font-size: var(--font-8xl);
}

.font-9xl {
  font-size: var(--font-9xl);
}

.font-10xl {
  font-size: var(--font-10xl);
}

/* -- Text size utilities -- */
/* Each class consumes its matching --text-size-* token (defined in :root). */
/* xs stays fixed at the a11y floor; m reuses --body-size for clean reset.  */

/* Two spellings, one scale. The long forms were the published API in
   `classes.md` and were never defined here, so every one of them — 80 across
   the repo, including generated pages and the docs' own demos — silently did
   nothing. Aliasing them onto the short forms is what makes the documented
   names true without minting a second scale that would differ from this one at
   three of its five steps. The short forms stay: they have 129 working
   consumers and are what the CSS has always meant. */
.text-size-xl,
.text-size-xlarge {
  font-size: var(--text-size-xl);
  line-height: var(--line-height-m);
}

.text-size-l,
.text-size-large {
  font-size: var(--text-size-l);
  line-height: var(--line-height-l);
}

.text-size-m,
.text-size-medium {
  font-size: var(--text-size-m);
  line-height: var(--line-height-l);
}

.text-size-s,
.text-size-small {
  font-size: var(--text-size-s);
  line-height: var(--line-height-xl);
}

.text-size-xs,
.text-size-xsmall {
  font-size: var(--text-size-xs);
  line-height: var(--line-height-xl);
}

/* -- Text wrap utilities -- */
[data-text-wrap="balance"] {
  text-wrap: balance;
}

[data-text-wrap="pretty"] {
  text-wrap: pretty;
}

/* -- Font width utilities -- */
[data-font-width="condensed"] { font-stretch: var(--font-condensed); }
[data-font-width="narrow"]    { font-stretch: var(--font-narrow); }
[data-font-width="normal"]    { font-stretch: var(--font-normal); }
[data-font-width="wide"]      { font-stretch: var(--font-wide); }
[data-font-width="expanded"]  { font-stretch: var(--font-expanded); }

/* -- Text alignment utilities -- */
.text-left   { text-align: left; }
.text-center { text-align: center; }
.text-right  { text-align: right; }

/* -- Eyebrow text -- */
.eyebrow {
  font-size: var(--font-xs);
  line-height: var(--line-height-m);
  font-weight: var(--font-weight-regular);
  letter-spacing: var(--letter-spacing-l);
  text-transform: uppercase;
}

/* -- Label text --
 * The form label's look as a class, for the places something should read as
 * a label but is not a <label> — a heading over a column of links, a caption
 * above a control group. Quieter than .eyebrow, which shouts in uppercase
 * and belongs above a section heading.
 *
 * It resets more than it sets, and that is the point: the element this lands
 * on is usually a heading, and h1–h6 carry a title family, -0.04em tracking
 * and 24px/16px margins that would otherwise survive underneath. A class
 * that says "this reads as a label" has to actually win, or it only changes
 * the size and leaves the rest of the heading showing through.
 *
 * `display` is deliberately not set — that positions a label inside a form
 * row, and a class applied to an arbitrary element should not decide its
 * layout. Margin is different: it is not this class's margin, it is
 * inherited debris this class has to clear. .bar-label (section 41)
 * makes the same call.
 */
.label {
  margin: 0;
  font-family: var(--font-primary);
  font-size: var(--font-s);
  font-weight: var(--font-weight-medium);
  line-height: var(--line-height-m);
  letter-spacing: normal;
  color: var(--text-primary);
}

/* -- Button reset (bare element) -- */
button {
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
  cursor: pointer;
  border: none;
  background: none;
  padding: 0;
  color: inherit;
  display: inline-flex;
  align-items: center;
  gap: var(--space-s);
  transition: transform var(--duration-2xs) var(--ease-out);
}

/* -- Button component (CUBE pattern) -- */
/* All tokens live at component level. data-* exceptions override them. */

.button,
button.button {
  /* -- Component tokens (overridable by exceptions + role classes) -- */
  --button-color: var(--text-primary);
  --button-faded: color-mix(in srgb, var(--button-color), var(--alpha-10));
  --button-bg: var(--button-color);
  --button-border: var(--button-color);
  --button-text-color: var(--text-inverted);
  --button-font: var(--font-primary);
  --button-text-size: var(--font-m);
  --button-padding-y: var(--space-l);
  --button-padding-x: var(--space-xl);
  --button-gap: var(--space-m);

  /* -- Layout -- */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--button-gap);

  /* -- Box model -- */
  padding: var(--button-padding-y) var(--button-padding-x);

  /* -- Visual -- */
  background-color: var(--button-bg);
  border: var(--border-m) solid var(--button-border);
  border-radius: var(--radius-s);
  

  /* -- Typography -- */
  font-family: var(--button-font);
  font-size: var(--button-text-size);
  font-weight: var(--font-weight-regular);
  line-height: var(--line-height-xs);
  /* letter-spacing: var(--letter-spacing-s); */
  text-transform: none;
  text-decoration: none;
  color: var(--button-text-color);

  /* -- Icon -- */
  --icon-size: 0.9em;


  /* -- Other -- */
  cursor: pointer;
  transition: background-color var(--duration-m) var(--ease-out), color var(--duration-2xs) var(--ease-out), border-color var(--duration-2xs) var(--ease-out), opacity var(--duration-2xs) var(--ease-out), transform var(--duration-2xs) var(--ease-out);
}

/* -- Hover -- */
.button:hover {
  --button-color: color-mix(in srgb, var(--text-primary), var(--background-primary) 10%)
}

/* -- Active (press) -- */
.button:active {
  transform: scale(0.98);
}

/* -- Variant exceptions -- */

.button[data-variant="outline"] {
  --button-bg: transparent;
  --button-text-color: var(--button-color);
}

.button[data-variant="faded"] {
  --button-bg: var(--button-faded);
  --button-border: transparent;
  --button-text-color: var(--button-color);
}

.button[data-variant="outline-faded"] {
  --button-bg: transparent;
  --button-border: var(--button-faded);
  --button-text-color: var(--button-color);
}

.button[data-variant="transparent"] {
  --button-bg: transparent;
  --button-border: transparent;
  --button-text-color: var(--button-color);
}

.button[data-variant="text"] {
  --button-bg: transparent;
  --button-border: transparent;
  --button-text-color: var(--button-color);
  --button-padding-x: 0;
  --button-padding-y: 0;
}
.button[data-variant="text"]:hover {
  text-decoration: underline;
  text-underline-offset: 2.5px;
}
.button[data-variant="text"]:active {
  transform: none;
}

/* Unfilled variants: subtle fill on hover instead of darkening transparent */
.button[data-variant="outline"]:hover,
.button[data-variant="faded"]:hover,
.button[data-variant="outline-faded"]:hover,
.button[data-variant="transparent"]:hover {
  background-color: color-mix(in srgb, var(--button-color), var(--alpha-5));
}

/* -- Size exceptions -- */

.button[data-size="small"] {
  --button-padding-y: var(--space-s);
  --button-padding-x: var(--space-m);
  --button-text-size: var(--font-xs);
}

.button[data-size="xsmall"] {
  --button-padding-y: var(--space-xs);
  --button-padding-x: var(--space-s);
  --button-text-size: var(--font-2xs);
}

/* -- Boolean modifiers -- */

.button[data-icon-only] {
  --button-padding-x: var(--button-padding-y);
  --icon-size: 1.3rem;
}

.button[data-full-width] {
  width: 100%;
}

/* -- Colour exceptions -- */
/* Semantic names and brand aliases produce identical output.
   --button-color drives bg, border, and faded automatically.

   A colour sets --button-color and nothing else: the text colour belongs to
   the variant. The base already declares --button-text-color: var(--text-inverted),
   which is what a filled button wants, and every unfilled variant re-points it to
   var(--button-color). Restating it here tied those variant rules at (0,2,0) and
   won on source order, so an unfilled button was painted with text meant to sit
   on a fill it does not have — inverted text on a transparent surface, i.e.
   invisible. The role classes below are unaffected either way: .button.close-btn
   re-points --button-text-color itself, and does so later still. */

.button[data-color="danger"],
.button[data-color="red"] {
  --button-color: var(--status-danger);
}

/* Hover lightens toward the ground, matching .button:hover, rather than
   darkening toward --text-primary as it once did. The status colours take the
   -dark step, which is near-black; mixing near-black into near-black gave a
   1.03:1 rest-to-hover step, i.e. no perceptible feedback. Lightening gives
   1.39:1, in line with the base button's own 1.31:1, and self-corrects in dark
   mode because --background-primary flips with the theme. */
.button[data-color="danger"]:hover,
.button[data-color="red"]:hover {
  --button-color: color-mix(in srgb, var(--status-danger), var(--background-primary) 15%);
}

.button[data-color="success"],
.button[data-color="green"] {
  --button-color: var(--status-success);
}

.button[data-color="success"]:hover,
.button[data-color="green"]:hover {
  --button-color: color-mix(in srgb, var(--status-success), var(--background-primary) 15%);
}

/* -- Role classes -- */
/* Role classes override component tokens only. They never redeclare the base
   structure, so padding, focus ring, sizes and states are inherited intact.
   Must come after the base :hover rule to win on equal specificity.
   Qualified with .button because the base also matches as `button.button`
   (0,1,1) — a bare `.close-btn` (0,1,0) would lose to it on <button>. */

.button.close-btn {
  --button-bg: transparent;
  --button-border: transparent;
  --button-color: var(--text-faded);
  --button-text-color: var(--button-color);
  flex-shrink: 0;
}

.button.close-btn:hover {
  --button-color: var(--text-primary);
  background-color: color-mix(in srgb, var(--button-color), var(--alpha-5));
}

/* data-color has to be re-stated here: .button[data-color] and .button.close-btn
   tie at (0,2,0), and the role class is declared later, so it would win. */
.button.close-btn[data-color="danger"],
.button.close-btn[data-color="red"] {
  --button-color: var(--status-danger);
}

.button.close-btn[data-color="danger"]:hover,
.button.close-btn[data-color="red"]:hover {
  --button-color: var(--status-danger);
  background-color: color-mix(in srgb, var(--status-danger), var(--alpha-10));
}

/* -- States -- */

.button:disabled,
.button.is-disabled {
  opacity: 0.4;
  pointer-events: none;
  cursor: not-allowed;
}

.button.is-loading {
  pointer-events: none;
  opacity: 0.6;
}

/* -- Reduced motion -- */
@media (prefers-reduced-motion: reduce) {
  .button:active {
    transform: none;
  }
}


.svg-icn {
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: var(--icon-size);
  height: var(--icon-size);
  display: flex;
  aspect-ratio: 1 / 1;
  transition: color var(--duration-m) var(--ease-out);
  margin: -0.175em
}

.svg-icn svg,
.svg-icn path {
  transition: fill var(--duration-m) var(--ease-out);
}

.svg-icn.is-preview {
  width: 2.5rem;
  height: 2.5rem;
}

.svg-logo {
  width: 100%;
  max-width: 300px;
  aspect-ratio: 210 / 101;
}

.svg-logo {
  transition: color var(--duration-m) var(--ease-out);
}

.svg-logo svg,
.svg-logo path {
  transition: fill var(--duration-m) var(--ease-out);
}

.button-group {
  grid-column-gap: var(--space-m);
  grid-row-gap: var(--space-m);
  flex-wrap: wrap;
  align-items: center;
  display: flex;
}

/* ------ 10. BACKGROUND UTILITIES ------ */

/* -- Background utilities -- */
.bg-primary {
  background-color: var(--background-primary);
}

.bg-secondary {
  background-color: var(--background-secondary);
}

.bg-plain {
  background-color: var(--background-plain);
}

.bg-faded {
  background-color: var(--background-faded);
}

/* -- Text color utilities -- */
.text-primary {
  color: var(--text-primary);
}

.text-secondary {
  color: var(--text-secondary);
}

.text-plain {
  color: var(--text-plain);
}

.text-faded {
  color: var(--text-faded);
}

.text-accent {
  color: var(--text-accent);
}

.text-link {
  color: var(--text-link);
}


/* -- Global state utilities -- */
.is-hidden {
  display: none !important;
}

/* -- Visually hidden --
 * Removes something from the page without removing it from the accessibility
 * tree. .is-hidden is display:none, which hides it from everyone — use this
 * instead for text that a screen reader still needs: the page heading of a
 * full-space tool, a live region, an instruction that the visual design makes
 * obvious and a screen reader does not.
 *
 * clip-path over the old clip/1px-height recipe, and white-space: nowrap so a
 * long string is not collapsed into a single wrapped column before clipping.
 * Promoted from three private copies (logo tool, email signature, ad preview)
 * once the third consumer arrived — CLAUDE.md section 17, rule 7. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ------ BASE ELEMENTS ------ */

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

html {
  font-size: 16px;
}

body {
  margin: 0;
  font-family: var(--font-primary);
  color: var(--text-primary);
  font-size: var(--body-size);
  font-weight: var(--body-weight);
  line-height: var(--body-leading);
  letter-spacing: var(--body-tracking);
  background-color: var(--background-primary);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* -- Headings --
   Shared base style for h1–h6: weight, tracking, margins, title family.
   Sizes follow in the editorial scale below. */
h1,
h2,
h3,
h4,
h5,
h6 {
  font-weight: 400;
  letter-spacing: -0.03em;
  margin-top: var(--space-xl);
  margin-bottom: var(--space-l);
  font-family: var(--title-family);
  line-height: var(--line-height-m);

}

/* Progressive editorial scale. Each level is a foundation --font-*
   primitive, one step up from the next level, stepped at the 959px
   breakpoint rather than clamp() so type tracks browser zoom and user
   font-size (WCAG 1.4.4). For hero-scale display type, set font-size to
   --headline-size / --title-size on your own display class instead of
   restyling the elements. */
h1 {
  font-size: var(--font-7xl);  /* 48px · 36px ≤959 */
  line-height: var(--line-height-m);
}

h2 {
  font-size: var(--font-6xl);  /* 40px · 32px ≤959 */
}

h3 {
  font-size: var(--font-5xl);  /* 36px · 28px ≤959 */
}

h4 {
  font-size: var(--font-4xl);  /* 32px · 24px ≤959 */
}

h5 {
  font-size: var(--font-3xl);  /* 28px · 22px ≤959 */
}

h6 {
  font-size: var(--font-2xl);  /* 24px · 20px ≤959 */
  line-height: var(--line-height-l);
}

@media (max-width: 959px) {
  h1 { font-size: var(--font-5xl); }
  h2 { font-size: var(--font-4xl); }
  h3 { font-size: var(--font-3xl); }
  h4 { font-size: var(--font-2xl); }
  h5 { font-size: var(--font-xl); }
  h6 { font-size: var(--font-l); }
}

/* -- Paragraphs -- */
p {
  margin-top: 0;
  margin-bottom: var(--space-m);
}

/* -- Links -- */
a {
  color: var(--text-plain);
  /* explicit length, not auto — the offset transition can't interpolate from auto */
  text-underline-offset: 0.15em;
  text-decoration: underline;
  -webkit-text-decoration-color: var(--text-link);
  text-decoration-color: var(--text-link);
  text-decoration-thickness: 0.06em;
  transition: color var(--duration-m) var(--ease-out),
              text-decoration-color var(--duration-m) var(--ease-out),
              text-underline-offset var(--duration-m) var(--ease-out);
}

a:not(.button):hover {
  color: var(--text-link);
  text-underline-offset: 0.25em;
}

/* The glyph warns that an underlined run of text leaves the site. A button is
   already a distinct affordance and carries its own icon slot, so it is excluded
   — on an icon-only button the mark would trail the icon as a second glyph. */
a[target="_blank"]:not(.button)::after {
  content: "";
  display: inline-block;
  width: 0.75em;
  height: 0.75em;
  margin-left: 0.2em;
  vertical-align: baseline;
  background-color: currentColor;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 21V3H9V5H7C5.89543 5 5 5.89543 5 7V17C5 18.1046 5.89543 19 7 19H17C18.1046 19 19 18.1046 19 17V15H21V21H3ZM9.7 15.7L8.3 14.3L15.8929 6.70711C16.5229 6.07714 16.0767 5 15.1858 5H12V3H21V12H19V8.81421C19 7.92331 17.9229 7.47714 17.2929 8.10711L9.7 15.7Z' fill='black'/%3E%3C/svg%3E");
  -webkit-mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 21V3H9V5H7C5.89543 5 5 5.89543 5 7V17C5 18.1046 5.89543 19 7 19H17C18.1046 19 19 18.1046 19 17V15H21V21H3ZM9.7 15.7L8.3 14.3L15.8929 6.70711C16.5229 6.07714 16.0767 5 15.1858 5H12V3H21V12H19V8.81421C19 7.92331 17.9229 7.47714 17.2929 8.10711L9.7 15.7Z' fill='black'/%3E%3C/svg%3E");
  mask-size: contain;
  mask-repeat: no-repeat;
}

/* -- Lists -- */
ul,
ol {
  margin-top: 0;
  margin-bottom: var(--space-m);
  padding-left: var(--space-2xl);
}

li {
  margin-top: var(--space-s);
  margin-bottom: 0;
}

li:first-child {
  margin-top: 0;
}

/* Nested lists */
ul ul,
ul ol,
ol ul,
ol ol {
  margin-top: var(--space-s);
  margin-bottom: 0;
}

ol ol,
ul ol {
  list-style-type: lower-roman;
}

ul ul ol,
ul ol ol,
ol ul ol,
ol ol ol {
  list-style-type: lower-alpha;
}

/* GFM task lists (- [ ]) render a checkbox inside the list item. Drop the
   disc bullet so the checkbox is the only marker, and pull the list flush
   since there's no bullet to indent against. */
li:has(> input[type="checkbox"]) {
  list-style: none;
}

ul:has(> li > input[type="checkbox"]) {
  padding-left: 0;
}

li > input[type="checkbox"] {
  margin-right: var(--space-xs);
}

/* List items with paragraphs */
li > p {
  margin-top: var(--space-l);
  margin-bottom: 0;
}

li > p:first-child {
  margin-top: 0;
}

/* -- Blockquotes -- */
blockquote {
  position: relative;
  margin: var(--space-xl) 0;
  padding-left: var(--space-xl);
  /* border-left: var(--border-m) solid var(--border-secondary); */
  font-family: var(--font-secondary);
  color: var(--text-primary);
  font-size: calc(var(--body-size) + 0.5rem);
  font-style: italic;
  hanging-punctuation: first;
  text-wrap: balance;
}

blockquote::before {
  content: "\201C";          /* “ — left double quotation mark */
  position: absolute;
  left: var(--space-l);      /* line up with the text edge... */
  top: 0;
  transform: translateX(-100%);
  font-size: 2em;            
  line-height: 1;
  font-family: var(--font-secondary);
}

blockquote p {
  margin-top: 0;
  margin-bottom: var(--space-m);
}

blockquote p:last-child {
  margin-bottom: 0;
}

/* -- Bold -- */
b,
strong {
  font-weight: var(--font-weight-semi-bold);
}

/* -- Images -- */
img {
  border-style: none;
  max-width: 100%;
  box-sizing: content-box;
}

/* -- Definition lists -- */
dl {
  padding: 0;
}

dl dt {
  padding: 0;
  font-size: var(--font-s);
  font-style: italic;
  font-weight: var(--font-weight-semi-bold);
}

dl dd {
  padding: 0 var(--space-l);
}

dd {
  margin-left: 0;
}

/* -- Focus styles -- */
/* `button` is included so .button and every bare button share one ring
   instead of falling back to the browser default.

   The widest ring in this section paints 4px outside the border box
   (2px outline + 2px offset on zero-padding buttons). .dialog-body and
   .drawer-body size their clip-box padding to exactly that — widen an
   outline or offset here and those two rules must grow with it, or
   focus rings crop again at the body edges. */
a:focus,
button:focus,
[role=button]:focus,
input[type=radio]:focus,
input[type=checkbox]:focus {
  outline: 2px solid var(--status-info);
  outline-offset: -2px;
  box-shadow: none;
}

a:focus:not(:focus-visible),
button:focus:not(:focus-visible),
[role=button]:focus:not(:focus-visible),
input[type=radio]:focus:not(:focus-visible),
input[type=checkbox]:focus:not(:focus-visible) {
  outline: solid 1px transparent;
}

a:focus-visible,
button:focus-visible,
[role=button]:focus-visible,
input[type=radio]:focus-visible,
input[type=checkbox]:focus-visible {
  outline: 2px solid var(--status-info);
  outline-offset: -2px;
  box-shadow: none;
}

/* The inset offset above assumes the control has padding to draw into.
   Zero-padding buttons need the ring outside, or it strikes through the label. */
.button[data-variant="text"]:focus-visible,
.dialog-close:focus-visible,
.toast-close:focus-visible,
.footer-link:focus-visible {
  outline-offset: 2px;
}

/* A filled button surrounds the inset ring with its own fill, where the
   status hue can vanish — --status-info measured 1.03:1 inside a filled
   danger button. The text colour is the one value guaranteed to clear its
   own fill, so the ring borrows it. Unfilled variants re-point
   --button-text-color at --button-color, which sits on the page ground. */
.button:focus,
.button:focus-visible {
  outline-color: var(--button-text-color);
}

a:not([href]) {
  color: inherit;
  text-decoration: none;
}





/* ------ 11. CALLOUTS & ALERTS ------ */

/* -- Base callout -- */
.callout {
  padding: var(--space-m) var(--space-l);
  margin-top: var(--space-l);
  margin-bottom: var(--space-l);
  background-color: var(--background-faded);
  border: var(--border-s) solid var(--border-faded);
}

.callout > *:first-child {
  margin-top: 0;
}

.callout > *:last-child {
  margin-bottom: 0;
}

/* -- Callout title -- */
.callout-title {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  font-weight: var(--font-weight-semi-bold);
  font-size: var(--font-m);
  line-height: var(--line-height-s);
}

/* -- Callout icon layout -- */
.callout[data-icon] {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: var(--space-m);
}

.callout[data-icon] > .svg-icn {
  grid-column: 1;
  grid-row: 1;
  width: 1.25rem;
  height: 1.25rem;
  align-self: center;
}

.callout[data-icon] > :not(.svg-icn) {
  grid-column: 2;
}

/* -- Callout description -- */
.callout-description {
  margin-top: var(--space-xs);
  font-size: var(--font-s);
}

/* -- Callout type variants -- */
.callout[data-type="info"] {
  background-color: var(--status-info-bg);
}

.callout[data-type="info"] .callout-title,
.callout[data-type="info"] > .svg-icn {
  color: var(--status-info);
}

.callout[data-type="success"] {
  background-color: var(--status-success-bg);
}

.callout[data-type="success"] .callout-title,
.callout[data-type="success"] > .svg-icn {
  color: var(--status-success);
}

.callout[data-type="warning"] {
  background-color: var(--status-warning-bg);
}

.callout[data-type="warning"] .callout-title,
.callout[data-type="warning"] > .svg-icn {
  color: var(--status-warning);
}

.callout[data-type="danger"] {
  background-color: var(--status-danger-bg);
}

.callout[data-type="danger"] .callout-title,
.callout[data-type="danger"] > .svg-icn {
  color: var(--status-danger);
}

/* ------ 12. FORM ELEMENTS ------ */

/* -- Labels -- */
label {
  display: block;
  font-size: var(--font-s);
  font-weight: var(--font-weight-medium);
  color: var(--text-primary);
  margin-bottom: var(--space-xs);
}

/* -- Text inputs & textarea -- */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="search"],
input[type="url"],
input[type="tel"],
textarea,
select {
  font-family: var(--font-primary);
  font-size: var(--body-size);
  line-height: var(--line-height-l);
  color: var(--input-text);
  background-color: var(--input-background);
  border: var(--border-s) solid var(--input-border);
  padding: var(--space-m) var(--space-l);
  min-height: var(--target-min);
  width: 100%;
  display: block;
  transition: border-color 0.2s ease;
}

input[type="text"]:focus,
input[type="email"]:focus,
input[type="password"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="url"]:focus,
input[type="tel"]:focus,
textarea:focus,
select:focus {
  outline: 1px solid var(--input-focus);
  outline-offset: 2px;
  box-shadow: none;
}

/* -- Input error state -- */
input.is-error,
textarea.is-error,
select.is-error {
  border-color: var(--status-danger);
}

input.is-error:focus,
textarea.is-error:focus,
select.is-error:focus {
  outline-color: var(--status-danger);
}

/* -- Disabled inputs -- */
input:disabled,
textarea:disabled,
select:disabled {
  background-color: var(--input-disabled-bg);
  color: var(--input-disabled-text);
  cursor: not-allowed;
}

input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

input[type="number"] {
  -moz-appearance: textfield;
  appearance: textfield;
}

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

/* -- Textarea -- */
textarea {
  min-height: 120px;
  resize: vertical;
}

/* -- Colour input -- */
input[type="color"] {
  appearance: none;
  -webkit-appearance: none;
  border: var(--border-s) solid var(--input-border);
  background-color: var(--input-background);
  padding: 0;
  width: auto;
  aspect-ratio: 1 / 1;
  align-self: stretch;
  cursor: pointer;
  transition: border-color 0.2s ease;
}

input[type="color"]::-webkit-color-swatch-wrapper {
  padding: 0;
}

input[type="color"]::-webkit-color-swatch {
  border: none;
}

input[type="color"]::-moz-color-swatch {
  border: none;
}

input[type="color"]:focus {
  outline: none;
  border-color: var(--input-focus);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus), var(--alpha-75));
}

/* -- Select -- */
select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23666' d='M1.41 0L6 4.58 10.59 0 12 1.41l-6 6-6-6z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-m) center;
  padding-right: var(--space-2xl);
  cursor: pointer;
  border-radius: 0;
}

/* -- Select: customizable picker -- */
/* `appearance: base-select` takes the option list out of the OS popup and into
   a CSS box, so the list matches the input instead of the operating system.
   Chromium-only at time of writing (not Baseline). Everything lives inside the
   @supports block, so a browser without it silently keeps the appearance:none
   input above and its native list — the fallback is the previous behaviour,
   not a broken one.

   Both the select and its ::picker() must opt in. Setting it on the select
   alone restyles the input and leaves the popup native.

   Scoped to drop-down selects. `base-select` also applies to listboxes
   (`[multiple]`, `[size]`), which have no picker and no room for these row
   metrics — those keep the native rendering above. A `size="1"` select is a
   drop-down but is excluded too, since it cannot be told apart by selector;
   it degrades to the same native fallback Safari and Firefox get. */
@supports (appearance: base-select) {
  select:not([multiple]):not([size]),
  select:not([multiple]):not([size])::picker(select) {
    appearance: base-select;
  }

  /* The base-appearance select is a real box again: the background-image caret
     and the padding reserved for it are replaced by a laid-out ::picker-icon. */
  select:not([multiple]):not([size]) {
    display: flex;
    align-items: center;
    gap: var(--space-m);
    background-image: none;
    padding-right: var(--space-m);
    text-align: left;
  }

  /* Sized from the artwork's own 12x8 ratio, not --icon-size — the caret is a
     wide glyph and a square box would letterbox it. */
  select::picker-icon {
    content: "";
    width: 0.75rem;
    height: 0.5rem;
    flex: none;
    margin-inline-start: auto;
    background-color: currentColor;
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1.41 0L6 4.58 10.59 0 12 1.41l-6 6-6-6z'/%3E%3C/svg%3E");
    mask-repeat: no-repeat;
    mask-position: center;
    mask-size: contain;
    transition: transform var(--duration-xs) var(--ease-out);
  }

  select:open::picker-icon {
    transform: rotate(180deg);
  }

  /* Mirrors .dropdown-menu — same surface tokens, same square edge. */
  select::picker(select) {
    border: var(--border-s) solid var(--dropdown-border);
    background-color: var(--dropdown-background);
    padding: 0;
    max-height: var(--select-picker-max-height);
    overflow-y: auto;
    opacity: 1;
    transition:
      opacity var(--duration-2xs) var(--ease-out),
      display var(--duration-2xs) allow-discrete,
      overlay var(--duration-2xs) allow-discrete;
  }

  /* The picker is a popover: it is display:none until open, so the entry
     transition needs both allow-discrete above and a @starting-style origin. */
  select:not(:open)::picker(select) {
    opacity: 0;
  }

  @starting-style {
    select:open::picker(select) {
      opacity: 0;
    }
  }

  /* Row metrics match .dropdown-item, including the leading checkmark — an
     option row and a checkable dropdown row are the same row, so they align
     on the same two columns. Nothing here claims the trailing edge, which
     leaves the auto margin free for the row's own content. */
  select:not([multiple]):not([size]) option {
    display: flex;
    align-items: center;
    gap: var(--space-s);
    padding: var(--space-l) var(--space-m);
    font-size: var(--font-s);
    line-height: var(--line-height-m);
    color: var(--text-primary);
    background-color: transparent;
    cursor: pointer;
    transition: background-color var(--duration-2xs) var(--ease-out);
  }

  select:not([multiple]):not([size]) option:hover {
    background-color: var(--dropdown-item-hover);
    color: var(--text-plain);
  }

  /* An option is in none of the element lists the global focus ring covers, so
     the ring is drawn here. The row tint alone is a ~5% luminance shift, far
     under the 3:1 non-text floor (1.4.11). Inset, because rows sit flush to
     the picker border. */
  select:not([multiple]):not([size]) option:focus-visible {
    background-color: var(--dropdown-item-hover);
    color: var(--text-plain);
    outline: 1px solid var(--input-focus);
    outline-offset: -2px;
  }

  select:not([multiple]):not([size]) option:disabled {
    color: var(--input-disabled-text);
    cursor: not-allowed;
  }

  select:not([multiple]):not([size]) option:disabled:hover {
    background-color: transparent;
  }

  /* An option can hold an icon in base appearance. Sized exactly as
     .dropdown-item .svg-icn, so an option row and a dropdown row line up. */
  select:not([multiple]):not([size]) option .svg-icn {
    width: 1.25rem;
    height: 1.25rem;
    flex-shrink: 0;
  }

  /* Markup order decides the side: an icon written before the label leads,
     one written after it trails. No class either way — the row is a flex
     line and this is nearly all the trailing case needs.
     :not(:only-child) excludes an option holding nothing but an icon, which
     would otherwise be pushed right for no reason (and has no accessible
     name to begin with). Structural pseudo-classes count element children
     only: the ::checkmark is a pseudo-element and cannot satisfy either half
     of this, but a bare text label cannot satisfy them either — which is why
     the docs require the label in its own <span>.
     The end margin is zeroed because .svg-icn carries a -0.175em optical trim
     on all four sides; left alone it would hang the icon into the row's right
     padding and miss the edge the label column is measured to. */
  select:not([multiple]):not([size]) option .svg-icn:not(:only-child):last-child {
    margin-inline-start: auto;
    margin-inline-end: 0;
  }

  /* <selectedcontent> clones the selected option's child nodes into the closed
     button, so those clones have no `option` ancestor and every rule above
     misses them — without this the icon would jump from row size to the base
     --icon-size the moment the picker closed. The start margin is reset for
     the same reason in reverse: a cloned trailing icon must not keep an auto
     margin that would compete with ::picker-icon's for the button's free
     space and stop the caret sitting flush right. */
  select:not([multiple]):not([size]) selectedcontent .svg-icn {
    width: 1.25rem;
    height: 1.25rem;
    flex-shrink: 0;
    margin-inline-start: 0;
  }

  /* A group label is a heading over rows, not a row: tighter and quieter than
     the options under it. The UA's own optgroup chrome is neutralised first,
     or the label keeps a native font and sits flush to the picker border. */
  select:not([multiple]):not([size]) optgroup {
    border: none;
    padding: 0;
  }

  select:not([multiple]):not([size]) optgroup legend {
    padding: var(--space-s) var(--space-m);
    font-size: var(--font-xs);
    font-weight: var(--font-weight-medium);
    color: var(--text-secondary);
  }

  /* The UA already hides the mark on unselected options; the explicit pair
     below states it in the sheet rather than leaving it implied. Sized to
     match .dropdown-checkmark. Colour alone would not carry selection
     (1.4.1) — the mark is the signal.

     It leads the row, where .dropdown-checkmark sits. `order` is declared
     rather than left to the pseudo-element's generated position, so the rule
     states where the mark goes instead of inheriting it from a detail of the
     UA's box generation. Hidden with visibility, matching .dropdown-checkmark
     exactly: the box is still generated, so its column is reserved on every
     row and selecting one cannot shunt the labels sideways. That reservation
     was invisible while the mark sat behind an auto margin at the trailing
     edge; leading the row, it is what holds the two columns straight. */
  option::checkmark {
    content: "";
    order: -1;
    width: 1.25rem;
    height: 1.25rem;
    flex: none;
    visibility: hidden;
    background-color: currentColor;
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9.54998 18L3.84998 12.3L5.27498 10.875L8.13576 13.7358C8.91681 14.5168 10.1831 14.5168 10.9642 13.7358L18.725 5.97501L20.15 7.40001L9.54998 18Z'/%3E%3C/svg%3E");
    mask-repeat: no-repeat;
    mask-position: center;
    mask-size: contain;
  }

  option:checked::checkmark {
    visibility: visible;
  }

  /* forced-colors flattens background-color to a system colour, which erases a
     masked shape entirely — the caret and, worse, the only selection indicator
     would both vanish. Hand both back to a real glyph, with empty alt text so
     the mark is not announced twice on top of the option's selected state. */
  @media (forced-colors: active) {
    select::picker-icon {
      content: "\25BE" / "";
      width: auto;
      height: auto;
      background-color: transparent;
      mask-image: none;
    }

    /* min-width rather than auto: the glyph is narrower than the 1.25rem
       column, and now that the mark leads the row that difference would move
       every label instead of hiding at the trailing edge. The floor keeps an
       option row on the same column as a .dropdown-item row here too. */
    option::checkmark {
      content: "\2713" / "";
      width: auto;
      min-width: 1.25rem;
      height: auto;
      background-color: transparent;
      mask-image: none;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    select::picker-icon,
    select::picker(select),
    select:not([multiple]):not([size]) option {
      transition: none;
    }
  }
}

/* -- Disabled states -- */
input:disabled,
textarea:disabled,
select:disabled {
  background-color: var(--input-disabled-bg);
  color: var(--input-disabled-text);
  cursor: not-allowed;
  border: none;
}

/* -- Fieldset & legend -- */
fieldset {
  border: var(--border-s) solid var(--border-secondary);
  padding: var(--space-l);
  margin: 0 0 var(--space-l) 0;
}

legend {
  font-size: var(--font-s);
  font-weight: var(--font-weight-semi-bold);
  padding: 0 var(--space-s);
  color: var(--text-primary);
}

/* -- Checkbox & radio -- */
input[type="checkbox"],
input[type="radio"] {
  -webkit-appearance: none;
  appearance: none;
  width: var(--space-xl);
  height: var(--space-xl);
  margin: 0;
  cursor: pointer;
  background-color: var(--checkbox-background);
  border: var(--border-s) solid var(--checkbox-border);
  transition: background-color 0.15s ease, border-color 0.15s ease;
  flex-shrink: 0;
  position: relative;
}

input[type="radio"] {
  border-radius: 50%;
}

input[type="checkbox"]:checked {
  background-color: var(--checkbox-selected);
  border-color: var(--checkbox-selected);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 12 10 18 20 6'/%3E%3C/svg%3E");
  background-size: 14px 14px;
  background-position: center;
  background-repeat: no-repeat;
}

input[type="radio"]:checked {
  background-color: var(--checkbox-selected);
  border-color: var(--checkbox-selected);
}

input[type="radio"]:checked::after {
  content: "";
  position: absolute;
  inset: 5px;
  border-radius: 50%;
  background-color: var(--checkbox-checkmark);
}

input[type="checkbox"]:focus-visible,
input[type="radio"]:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus), var(--alpha-75));
}

input[type="checkbox"]:disabled,
input[type="radio"]:disabled {
  background-color: var(--input-disabled-bg);
  border-color: var(--input-disabled-bg);
  cursor: not-allowed;
  opacity: 0.5;
}

input[type="checkbox"]:disabled:checked {
  background-color: var(--input-disabled-text);
  border-color: var(--input-disabled-text);
}

input[type="radio"]:disabled:checked {
  background-color: var(--input-disabled-text);
  border-color: var(--input-disabled-text);
}

/* Dark mode checkmark */
[data-theme="dark"] input[type="checkbox"]:checked {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%231a1a1a' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 12 10 18 20 6'/%3E%3C/svg%3E");
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) input[type="checkbox"]:checked {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%231a1a1a' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 12 10 18 20 6'/%3E%3C/svg%3E");
  }
}

/* -- Inline label + checkbox/radio pattern -- */
.form-check {
  display: flex;
  align-items: center;
  gap: var(--space-s);
}

.form-check label {
  display: inline;
  margin-bottom: 0;
  font-weight: var(--font-weight-regular);
  cursor: pointer;
}

/* -- Segmented control (single-select button group) --
 * Nothing is drawn until something is selected: no track, no outer border, no
 * rules between segments.
 *
 * Selection is deliberately NOT an accent colour. A status hue in the middle
 * of a control would spend a colour the system reserves for meaning. Instead
 * it is the system's selected grammar — the inverted neutral the toggle's
 * checked track uses — via the shared pair --segmented-selected-background /
 * --segmented-selected-text (declared with the thumb variant tokens below).
 *
 * Hover previews engagement with the faded wash; selection inverts. They
 * used to share the same 5%-black fill, argued as hover-previews-selection —
 * but that fill measured 1.13:1 against the page, under the 3:1 non-text
 * minimum a state indicator needs (1.4.11, measured at the 2026-08-25
 * audit), so the wash now belongs to hover alone and selection is
 * unmistakable. aria-pressed still carries the state for the accessibility
 * tree; keep it in step with .is-active. */
.segmented-control {
  display: inline-flex;
  gap: var(--space-2xs);
}

.segmented-control-btn {
  padding: var(--space-s) var(--space-m);
  font-size: var(--font-s);
  font-weight: var(--font-weight-regular);
  border-radius: var(--radius-s);
  border: none;
  background: transparent;
  color: var(--text-faded);
  cursor: pointer;
  transition: background-color var(--duration-2xs) var(--ease-out),
              color var(--duration-2xs) var(--ease-out);
  white-space: nowrap;
  font-family: inherit;
}

.segmented-control-btn:hover {
  background-color: var(--background-faded);
  color: var(--text-primary);
}

/* Declared after :hover so a hovered selected segment stays inverted. */
.segmented-control-btn.is-active {
  background-color: var(--segmented-selected-background);
  color: var(--segmented-selected-text);
}

.segmented-control-btn:focus-visible {
  outline: 2px solid var(--text-primary);
  outline-offset: 2px;
}

.segmented-control-btn.is-icon {
  padding: var(--space-s);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Backgrounds flatten under forced colours, so the fill that carries selection
   disappears. The system's own selected pair replaces it. forced-color-adjust:
   none also stops the mode forcing the focus outline on the selected segment,
   so its colour is declared explicitly. */
@media (forced-colors: active) {
  .segmented-control-btn.is-active {
    forced-color-adjust: none;
    background-color: Highlight;
    color: HighlightText;
  }

  .segmented-control-btn.is-active:focus-visible {
    outline-color: CanvasText;
  }
}

/* -- Segmented control, thumb variant (radio-backed) --
 * data-variant="thumb" draws what the flat form deliberately omits: a track,
 * and a thumb that slides to the selected segment. The flat form's argument
 * — hover previews the selected state — assumes a pointer that can hover.
 * On a touch surface there is no hover, and on a dense app screen an
 * unfilled group reads as unrelated buttons, so the app form trades the
 * ghost group for a visible boundary. The flat form stays the default and
 * the right choice inside bars and toolbars.
 *
 * The markup is different on purpose: native radios inside labels, not
 * buttons. The browser supplies the radiogroup semantics — arrow keys move
 * selection, Tab enters and leaves the group as one stop, the value posts
 * with a form when there is one — so there is no script and no aria-pressed
 * to keep in step. Segments are equal width (the thumb is sized by count,
 * not measured), and the position selectors below enumerate up to five
 * segments — a sixth option was a dropdown's job anyway.
 *
 * Selection is still not an accent colour — it is the system's selected
 * grammar: the inverted neutral the toggle's checked track already uses.
 * The thumb takes --text-primary, the checked label inverts, and the pair
 * clears non-text contrast by an order of magnitude in both themes
 * (13.1:1 light / 11.9:1 dark against the track) with no border and no
 * dark-mode override — the tokens flip themselves. The selected pair is
 * shared with the flat form's .is-active above, so both forms re-theme
 * from one place. The border stays as an aesthetic knob, transparent by
 * default; the 1.4.11 pass lives in the surface, where re-pointing an
 * optional border can never undo it. */
:root {
  --segmented-track-background: var(--background-faded);
  --segmented-track-padding: 3px; /* visual inset, same register as --toggle-knob-offset */
  --segmented-track-radius: var(--radius-m);
  /* Selection now speaks the system-wide pair (see System Tokens). These two
     names stay as published aliases so a brand that already re-points them
     keeps working; new components read --selected-* directly. */
  --segmented-selected-background: var(--selected-background);
  --segmented-selected-text: var(--selected-text);
  --segmented-thumb-border: transparent;
  /* Inner radius follows the outer one minus the inset, so the thumb's
     corners stay concentric with the track's when a brand re-points the
     track radius. */
  --segmented-thumb-radius: calc(var(--segmented-track-radius) - var(--segmented-track-padding));
  --segmented-thumb-duration: var(--duration-xs);
}

.segmented-control[data-variant="thumb"] {
  --segmented-count: 2;
  --segmented-index: 0;
  --segmented-direction: 1;

  position: relative;
  isolation: isolate;
  display: inline-grid; /* shrink-to-fit, the same box level as the flat form */
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 0; /* the base gap would break the thumb's count arithmetic */
  padding: var(--segmented-track-padding);
  background-color: var(--segmented-track-background);
  border-radius: var(--segmented-track-radius);
}

/* Every option rule is scoped to the variant: an option dropped into a flat
   control would render styled segments with no track and no thumb, carrying
   selection by text colour alone — the 1.4.1 failure the variant exists to
   avoid. */
.segmented-control[data-variant="thumb"] .segmented-control-option {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-s) var(--space-m);
  margin-bottom: 0; /* this is a segment, not a field label — undo the form label margin */
  font-size: var(--font-s);
  font-weight: var(--font-weight-regular);
  color: var(--text-faded);
  border-radius: var(--segmented-thumb-radius);
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--duration-2xs) var(--ease-out);
}

/* Hover previews engagement on unselected segments only — a hover rule that
   also matched the checked option would out-rank it on specificity and
   flip the inverted label back to dark-on-dark. */
.segmented-control[data-variant="thumb"] .segmented-control-option:hover:not(:has(input:checked)):not(:has(input:disabled)) {
  color: var(--text-primary);
}

.segmented-control[data-variant="thumb"] .segmented-control-option:has(input:checked) {
  color: var(--segmented-selected-text);
}

/* The radio is the hit area, stretched invisibly over its whole label —
   not display:none, which would forfeit focus and the arrow keys. */
.segmented-control[data-variant="thumb"] .segmented-control-option input[type="radio"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  border: none;
  border-radius: inherit;
  opacity: 0;
  cursor: pointer;
}

.segmented-control[data-variant="thumb"] .segmented-control-option:has(input[type="radio"]:focus-visible) {
  outline: 2px solid var(--text-primary);
  outline-offset: 2px;
}

/* The radio's own disabled styling is invisible at opacity 0, so the label
   carries it — the same colour-plus-half-opacity pair the bare radio uses,
   because --input-disabled-text alone matches the resting label exactly. */
.segmented-control[data-variant="thumb"] .segmented-control-option:has(input:disabled) {
  color: var(--input-disabled-text);
  opacity: 0.5;
  cursor: not-allowed;
}

/* Checked AND disabled (a whole group disabled is the natural case): the
   disabled rule above would win the label back to faded-on-dark — illegible
   on the thumb. Keep the inverted pairing and dim thumb and label together
   instead; :has(input:checked:disabled) out-ranks both single-state rules. */
.segmented-control[data-variant="thumb"] .segmented-control-option:has(input:checked:disabled) {
  color: var(--segmented-selected-text);
}

.segmented-control[data-variant="thumb"]:has(input:checked:disabled)::before {
  opacity: 0.5;
}

.segmented-control[data-variant="thumb"] .segmented-control-option:has(input:disabled) input[type="radio"] {
  cursor: not-allowed;
}

/* The thumb: one element for the whole group, sized by segment count and
   moved by checked index. Equal columns mean translate needs no measuring,
   and translate composites without layout. */
.segmented-control[data-variant="thumb"]::before {
  content: "";
  position: absolute;
  z-index: 0;
  inset-block: var(--segmented-track-padding);
  inset-inline-start: var(--segmented-track-padding);
  width: calc((100% - var(--segmented-track-padding) * 2) / var(--segmented-count));
  background-color: var(--segmented-selected-background);
  border: var(--border-s) solid var(--segmented-thumb-border);
  border-radius: var(--segmented-thumb-radius);
  translate: calc(var(--segmented-index) * 100% * var(--segmented-direction)) 0;
  transition: translate var(--segmented-thumb-duration) var(--ease-out);
}

/* Nothing checked yet: no thumb. The flat form's "nothing is drawn until
   something is selected" survives here, scoped to the selection. */
.segmented-control[data-variant="thumb"]:not(:has(input:checked))::before {
  display: none;
}

/* Segment count — equal columns mean the thumb only needs to know how many. */
.segmented-control[data-variant="thumb"]:has(> :nth-child(3):last-child) { --segmented-count: 3; }
.segmented-control[data-variant="thumb"]:has(> :nth-child(4):last-child) { --segmented-count: 4; }
.segmented-control[data-variant="thumb"]:has(> :nth-child(5):last-child) { --segmented-count: 5; }

/* Past the documented five the enumeration above runs out, which would pin a
   half-width thumb to the first segment while selection sits elsewhere.
   Failing safe means failing flat: drop the thumb, and hand the checked
   label back its resting-surface colour — the selected text would be
   invisible on the bare track. A sixth option was a dropdown's job anyway. */
.segmented-control[data-variant="thumb"]:has(> :nth-child(6))::before {
  display: none;
}

.segmented-control[data-variant="thumb"]:has(> :nth-child(6)) .segmented-control-option:has(input:checked) {
  color: var(--text-primary);
}

/* Checked position. */
.segmented-control[data-variant="thumb"]:has(> :nth-child(2) input:checked) { --segmented-index: 1; }
.segmented-control[data-variant="thumb"]:has(> :nth-child(3) input:checked) { --segmented-index: 2; }
.segmented-control[data-variant="thumb"]:has(> :nth-child(4) input:checked) { --segmented-index: 3; }
.segmented-control[data-variant="thumb"]:has(> :nth-child(5) input:checked) { --segmented-index: 4; }

/* RTL: the first segment sits at the inline start, so travel flips sign.
   A browser without :dir() keeps LTR travel — the thumb lands on the wrong
   segment visually, but selection is never wrong, because the checked label
   carries its own state. */
.segmented-control[data-variant="thumb"]:dir(rtl) {
  --segmented-direction: -1;
}

/* Re-points the token rather than disabling the transition, matching
   .form-toggle, .dialog, .drawer and .book-cover. The thumb still lands on
   the selected segment — it just arrives immediately. */
@media (prefers-reduced-motion: reduce) {
  .segmented-control[data-variant="thumb"] {
    --segmented-thumb-duration: var(--duration-2xs);
  }
}

/* Both fills flatten under forced colours: the track keeps a drawn boundary,
   the thumb is dropped, and the checked label takes the system's selected
   pair — the same answer as the flat form. forced-color-adjust: none also
   stops the mode forcing the focus outline, so its colour is declared
   explicitly. */
@media (forced-colors: active) {
  .segmented-control[data-variant="thumb"] {
    border: 1px solid CanvasText;
  }

  .segmented-control[data-variant="thumb"]::before {
    display: none;
  }

  .segmented-control[data-variant="thumb"] .segmented-control-option:has(input:checked) {
    forced-color-adjust: none;
    background-color: Highlight;
    color: HighlightText;
  }

  .segmented-control[data-variant="thumb"] .segmented-control-option:has(input[type="radio"]:focus-visible) {
    outline-color: CanvasText;
  }
}

/* -- Toggle / switch -- */
.form-toggle {
  --toggle-width: 44px; /* visual width that happens to equal 44 — not --target-min */
  --toggle-height: var(--space-xl);
  --toggle-knob-size: 18px;
  --toggle-knob-offset: 3px;
  --toggle-transition-duration: var(--duration-xs);

  display: flex;
  align-items: center;
  gap: var(--space-s);
}

.form-toggle label {
  display: inline;
  margin-bottom: 0;
  font-weight: var(--font-weight-regular);
  cursor: pointer;
  order: -1;
  white-space: nowrap;
}

.form-toggle.is-label-right label {
  order: 1;
}

.form-toggle input[type="checkbox"] {
  width: var(--toggle-width);
  height: var(--toggle-height);
  background-color: var(--black-alpha-3);
  border: var(--border-s) solid var(--border-faded);
  border-radius: var(--radius-pill);
  position: relative;
  transition: background-color var(--toggle-transition-duration) ease;
}

.form-toggle input[type="checkbox"]::after {
  content: "";
  position: absolute;
  top: 50%;
  left: var(--toggle-knob-offset);
  transform: translateY(-50%);
  width: var(--toggle-knob-size);
  height: var(--toggle-knob-size);
  border-radius: 50%;
  background-color: var(--neutral-500);
  transition:
    transform var(--toggle-transition-duration) ease,
    background-color var(--toggle-transition-duration) ease;
}

.form-toggle input[type="checkbox"]:checked {
  background-color: var(--text-accent);
  background-image: none;
}

.form-toggle input[type="checkbox"]:checked::after {
  transform: translateY(-50%) translateX(calc(var(--toggle-width) - var(--toggle-knob-size) - var(--toggle-knob-offset) * 2 - var(--border-s) * 2));
  background-color: var(--off-white);
}

.form-toggle input[type="checkbox"]:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus), var(--alpha-75));
}

.form-toggle input[type="checkbox"]:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.form-toggle input[type="checkbox"]:disabled + label {
  cursor: not-allowed;
}

/* -- Reduced motion -- */
/* Re-points the token rather than disabling the transition, matching .dialog,
   .drawer and .book-cover. The knob still moves to its new position — it just
   arrives immediately, so the checked state stays legible as a change. */
@media (prefers-reduced-motion: reduce) {
  .form-toggle {
    --toggle-transition-duration: var(--duration-2xs);
  }
}

/* -- Toggle dark mode -- */
[data-theme="dark"] .form-toggle input[type="checkbox"] {
  background-color: #444;
}

[data-theme="dark"] .form-toggle input[type="checkbox"]::after {
  background-color: #888;
}

[data-theme="dark"] .form-toggle input[type="checkbox"]:checked {
  background-color: #e8e6e3;
}

[data-theme="dark"] .form-toggle input[type="checkbox"]:checked::after {
  background-color: #1a1a1a;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .form-toggle input[type="checkbox"] {
    background-color: #444;
  }

  :root:not([data-theme]) .form-toggle input[type="checkbox"]::after {
    background-color: #888;
  }

  :root:not([data-theme]) .form-toggle input[type="checkbox"]:checked {
    background-color: #e8e6e3;
  }

  :root:not([data-theme]) .form-toggle input[type="checkbox"]:checked::after {
    background-color: #1a1a1a;
  }
}

/* -- Form group (label + input pair) -- */
.form-group {
  margin-bottom: var(--space-l);
}

/* -- Form hint text -- */
.form-hint {
  font-size: var(--font-s);
  color: var(--text-secondary);
  margin-top: var(--space-xs);
}

/* -- Form error message -- */
.form-error {
  font-size: var(--font-s);
  color: var(--status-danger);
  margin-top: var(--space-xs);
}

/* -- Access support form states -- */
.access-support-success {
  text-align: center;
  padding: var(--space-3xl) 0;
}

.access-support-success p {
  font-size: var(--body-size);
  color: var(--text-secondary);
}

.access-support-error {
  font-size: var(--font-s);
  color: var(--status-danger);
  margin-top: var(--space-s);
}

/* -- Password field -- */
/* Behaviour ships as password-toggle.js; these styles ship with it so a
   package consumer gets a working control, not a bare button. */
.password-field {
  position: relative;
}

.password-field input {
  padding-right: var(--space-4xl);
}

.password-toggle {
  position: absolute;
  right: var(--space-m);
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-faded);
  padding: var(--space-xs);
  min-width: var(--target-min);
  min-height: var(--target-min);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: color var(--duration-xs) var(--ease-out);
}

.password-toggle:hover {
  color: var(--text-primary);
}

.password-toggle .svg-icn {
  width: 1.25rem;
  height: 1.25rem;
}

/* Icon state: eye-off while the password is hidden, eye once revealed.
   Keyed on aria-pressed — the attribute the script already flips — so the
   swap is pure CSS and the markup carries both icons. Scoped with :has()
   to dual-icon toggles: legacy markup with a single eye keeps its static
   icon rather than ending up with an empty button. Two rules, not one
   selector list — a browser without :has() drops only the rule it cannot
   parse, keeping the pressed-state half working. */
.password-toggle:has(.svg-icn[data-icon="eye-off"]):not([aria-pressed="true"]) .svg-icn[data-icon="eye"] {
  display: none;
}

.password-toggle[aria-pressed="true"] .svg-icn[data-icon="eye-off"] {
  display: none;
}

/* ------ 13. DISCLOSURE (details / summary) ------ */

/* -- Base disclosure -- */


summary {
  display: flex;
  align-items: center;
  gap: var(--space-m);
  font-family: var(--font-primary);
  font-size: var(--body-size);
  color: var(--text-primary);
  padding: var(--space-s) var(--space-m);
  cursor: pointer;
  user-select: none;
  list-style: none;
}

summary::-webkit-details-marker {
  display: none;
}

summary::before {
  content: "";
  width: 0.5em;
  height: 0.5em;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 0.2s ease;
  flex-shrink: 0;
}

details[open] > summary::before {
  transform: rotate(45deg);
}

summary:hover {
  color: var(--text-secondary);
}

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

/* -- Disclosure content -- */
details .disclosure-content {
  padding: var(--space-s) var(--space-m) var(--space-m) calc(0.5em + var(--space-s) + var(--space-m));
  font-family: var(--font-primary);
  font-size: var(--body-size);
  line-height: var(--line-height-xl);
  color: var(--text-secondary);
}

/* -- Disclosure table (key-value pairs) -- */
.disclosure-table {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-2xs) var(--space-l);
  align-items: baseline;
  margin: 0;
}

.disclosure-table dt {
  color: var(--text-faded);
  white-space: nowrap;
}

.disclosure-table dd {
  color: var(--text-primary);
  margin: 0;
}


/* ------ 16. BADGE ------ */

.badge {
  --badge-case: uppercase;
  --badge-tracking: var(--letter-spacing-l);

  display: inline-flex;
  align-items: center;
  font-family: var(--font-tertiary);
  font-size: var(--font-2xs);
  font-weight: var(--font-weight-bold);
  text-transform: var(--badge-case);
  letter-spacing: var(--badge-tracking);
  padding: var(--space-2xs) var(--space-s);
  border-radius: 0;
  background-color: var(--background-darker);
  color: var(--text-primary);
  white-space: nowrap;
  line-height: var(--line-height-m);
}

/* Sentence-case variant — customize case via the tokens above or this
   attribute, never by overriding the class (Layer Rule 3) */
.badge[data-case="none"] {
  --badge-case: none;
  --badge-tracking: normal;
}

.badge[data-color="success"],
.badge[data-color="green"] {
  background-color: var(--status-success-bg);
  color: var(--status-success);
}

.badge[data-color="warning"],
.badge[data-color="yellow"] {
  background-color: var(--status-warning-bg);
  color: var(--status-warning);
}

.badge[data-color="danger"],
.badge[data-color="red"] {
  background-color: var(--status-danger-bg);
  color: var(--status-danger);
}

.badge[data-color="info"],
.badge[data-color="blue"] {
  background-color: var(--status-info-bg);
  color: var(--status-info);
}


/* ------ 17. CARD ------ */

.card {
  background-color: var(--background-primary);
  border: var(--border-s) solid var(--border-faded);
  padding: var(--space-xl);
  transition: border 0.3s, background-color 0.3s, box-shadow 0.15s;
}

.card--flush {
  padding: 0;
}

.card--interactive {
  display: block;
  cursor: pointer;
  text-decoration: none;
  color: inherit;
  transition: border 0.3s, background-color 0.3s, box-shadow 0.15s;
}

.card--interactive:hover {
  background-color: var(--background-faded);
  box-shadow: 0 4px 12px var(--black-alpha-10);
  /* border-color: var(--border-primary); */
}

.card--interactive:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus), var(--alpha-75));
}
/* -- Card inner elements -- */
.card-title {
  margin-top: 0;
  margin-bottom: var(--space-s);
  font-size: var(--font-3xl);
  line-height: var(--line-height-m);
}

.card-description {
  color: var(--text-secondary);
  font-size: var(--body-size);
  margin-bottom: 0;

}

.card-image {
  border-radius: var(--radius-m) var(--radius-m) 0 0;
}

/* -- Card dark mode -- */
[data-theme="dark"] .card {
  background-color: var(--background-secondary);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .card {
    background-color: var(--background-secondary);
  }
}



/* ------ 18. BREADCRUMB ------ */

.breadcrumb {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-xs);
  font-size: var(--font-xs);
  color: var(--text-faded);
}

.breadcrumb a {
  color: var(--text-faded);
  text-decoration: none;
  transition: color 0.15s ease-in-out;
}

.breadcrumb a:hover {
  color: var(--text-primary);
  text-decoration: underline;
}

.breadcrumb [aria-current="page"] {
  color: var(--text-primary);
  font-weight: var(--font-weight-medium);
}

.breadcrumb-separator {
  color: var(--text-faded);
  user-select: none;
}


/* ------ 14. ACCORDION ------ */

.accordion {
  border-top: var(--border-s) solid var(--border-faded);
}

.accordion-item {
  border-bottom: var(--border-s) solid var(--border-faded);
}



.accordion-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  text-align: left;
  gap: var(--space-m);
  padding: var(--accordion-padding-y) var(--accordion-padding-x);
  color: var(--text-primary);
  z-index: 10;
  position: relative;
}

.accordion-header .svg-icn {
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  transition: transform var(--duration-m) var(--ease-out);
}

.accordion-item.is-open > .accordion-header .svg-icn {
  transform: rotate(180deg);
}

.accordion-content {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--duration-m) var(--ease-out);
  z-index: 5;
  position: relative;
}

.accordion-item.is-open > .accordion-content {
  grid-template-rows: 1fr;
}

.accordion-header:focus:not(:focus-visible) {
  outline: solid 1px transparent;
}

.accordion-header:focus-visible {
  outline: 2px solid var(--status-info);
  outline-offset: -2px;
  box-shadow: none;
}

.accordion-inner {
  overflow: hidden;
  min-height: 0;
}

.accordion-body {
  padding:  0 var(--accordion-padding-x) var(--accordion-padding-y) var(--accordion-padding-x);
}


/* ------ 19. TABS ------ */

.tabs {
  display: flex;
  border-bottom: var(--border-s) solid var(--border-faded);
  gap: var(--space-none);
}

.tab {
  position: relative;
  background: none;
  border: none;
  padding: var(--space-m) var(--space-l);
  color: var(--text-faded);
  font-weight: var(--font-weight-medium);
  font-size: var(--font-s);
  cursor: pointer;
  transition: color 0.15s ease-in-out;
}

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

.tab.is-active {
  color: var(--text-primary);
}

.tab.is-active::after {
  content: "";
  position: absolute;
  bottom: -1px;
  left: 0;
  right: 0;
  height: 2px;
  background-color: var(--text-primary);
}

.tab:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus), var(--alpha-75));
}

.tab-panel {
  padding-top: var(--space-m);
}

a.tab {
    text-decoration: none;
}

a.tab:hover {
  background-color: var(--background-faded);
}


/* ------ 20. PROGRESS ------ */

/* Shared palette for both shapes, plus the ring's geometry. The ring's
   geometry tokens are viewBox units (the canonical viewBox is 0 0 48 48):
   rendered at --progress-ring-size the whole drawing scales together, so a
   bigger ring is a bigger drawing, not a thinner one. */
:root {
  --progress-track: var(--background-darker);
  --progress-fill: var(--text-primary);
  --progress-height: var(--space-s);
  --progress-radius: 0px;
  --progress-duration: var(--duration-s); /* one knob times both shapes */
  --progress-ring-size: 48px;
  --progress-ring-thickness: 4px;
}

progress.progress-bar {
  width: 100%;
  height: var(--progress-height);
  border-radius: var(--progress-radius);
  border: none;
  overflow: hidden;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
}

progress.progress-bar::-webkit-progress-bar {
  background-color: var(--progress-track);
  border-radius: var(--progress-radius);
}

progress.progress-bar::-webkit-progress-value {
  background-color: var(--progress-fill);
  border-radius: var(--progress-radius);
  transition: width var(--progress-duration) var(--ease-in-out);
}

progress.progress-bar::-moz-progress-bar {
  background-color: var(--progress-fill);
  border-radius: var(--progress-radius);
}

/* -- Progress status variants -- */
progress.progress-bar--success::-webkit-progress-value { background: var(--status-success); }
progress.progress-bar--success::-moz-progress-bar { background: var(--status-success); }

progress.progress-bar--warning::-webkit-progress-value { background: var(--status-warning); }
progress.progress-bar--warning::-moz-progress-bar { background: var(--status-warning); }

progress.progress-bar--danger::-webkit-progress-value { background: var(--status-danger); }
progress.progress-bar--danger::-moz-progress-bar { background: var(--status-danger); }

/* -- Progress ring (radial) --
 * The bar's job in a compact, centre-labelled circle, for app surfaces
 * where a full-width bar has no width to fill. Determinate only, like the
 * bar — indeterminate loading is a skeleton's job.
 *
 * The circles carry pathLength="100", so the dash arithmetic is percentage
 * arithmetic: the value arrives as a unitless 0–100 custom property
 * (--progress-ring-value) set on the element from script, and no
 * circumference ever enters the CSS. Keep aria-valuenow and the property
 * in step — the same pairing rule as .is-active and aria-pressed on the
 * segmented control. */
.progress-ring {
  --progress-ring-value: 0;

  position: relative;
  display: inline-grid;
  place-items: center;
  width: var(--progress-ring-size);
  aspect-ratio: 1;
}

.progress-ring svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transform: rotate(-90deg); /* progress runs from 12 o'clock */
}

.progress-ring circle {
  fill: none;
  stroke-width: var(--progress-ring-thickness);
}

.progress-ring-track {
  stroke: var(--progress-track);
}

.progress-ring-fill {
  stroke: var(--progress-fill);
  stroke-linecap: round;
  stroke-dasharray: 100;
  stroke-dashoffset: calc(100 - var(--progress-ring-value));
  /* A rounded cap paints a dot even at zero length; gate visibility on the
     value so an empty ring is empty. Any value above 0 saturates to 1. */
  opacity: clamp(0, var(--progress-ring-value) * 999, 1);
  transition: stroke-dashoffset var(--progress-duration) var(--ease-in-out);
}

.progress-ring-label {
  position: relative;
  /* Sized to clear the stroke at the default 48px ring with a three-digit
     value; a larger ring can re-point it alongside --progress-ring-size. */
  font-size: var(--font-2xs);
  font-weight: var(--font-weight-medium);
  color: var(--text-primary);
}

/* -- Ring status variants, same grammar as the bar -- */
.progress-ring--success .progress-ring-fill { stroke: var(--status-success); }
.progress-ring--warning .progress-ring-fill { stroke: var(--status-warning); }
.progress-ring--danger .progress-ring-fill { stroke: var(--status-danger); }

/* Re-points the shared knob rather than disabling transitions, matching
   .form-toggle: bar and ring both still move to the new value — they just
   arrive immediately. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --progress-duration: var(--duration-2xs);
  }
}

/* Forced colours would flatten the token strokes, but author-declared system
   colour keywords are honoured — so declare the pair: track recedes, fill
   carries the reading. Written at (0,2,0) so the status variants above
   cannot out-rank it. */
@media (forced-colors: active) {
  .progress-ring .progress-ring-track { stroke: GrayText; }
  .progress-ring .progress-ring-fill { stroke: Highlight; }
}

/* -- Progress wrapper with tooltip -- */
.progress-wrapper {
  position: relative;
  width: 100%;
}

.progress-tooltip {
  position: absolute;
  bottom: calc(100% + var(--space-xs));
  background-color: var(--brown-dark);
  color: var(--off-white);
  border-radius: var(--radius-s);
  padding: var(--space-xs) var(--space-s);
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transform: translateX(-50%);
  transition: opacity 0.15s ease-in-out;
  z-index: 800;
}

.progress-wrapper:hover .progress-tooltip {
  opacity: 1;
}


/* ------ 21. TOOLTIP ------ */

[data-tooltip] {
  position: relative;
  cursor: help;
}

[data-tooltip]::before {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + var(--space-xs));
  left: 50%;
  transform: translateX(-50%);
  background-color: var(--brown-dark);
  color: var(--off-white);
  border-radius: var(--radius-s);
  padding: var(--space-xs) var(--space-s);
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s ease-in-out;
  z-index: 800;
}

/* Bubble typography pinned in full. A pseudo-element inherits from its
   trigger and .progress-tooltip from its wrapper, so an unpinned bubble
   renders per-host: a .button trigger collapses leading to 0.7, a .badge
   would uppercase and letterspace it, a condensed ancestor narrows the face. */
[data-tooltip]::before,
.progress-tooltip {
  font-family: var(--font-primary);
  font-size: var(--font-xs);
  font-weight: var(--font-weight-regular);
  line-height: var(--line-height-m);
  letter-spacing: 0;
  text-transform: none;
  font-style: normal;
  font-stretch: var(--font-normal);
}

[data-tooltip]:hover::before,
[data-tooltip]:focus-visible::before {
  opacity: 1;
}

/* -- Tooltip positions -- */
[data-tooltip-position="bottom"]::before {
  bottom: auto;
  top: calc(100% + var(--space-xs));
}

[data-tooltip-position="left"]::before {
  bottom: auto;
  top: 50%;
  left: auto;
  right: calc(100% + var(--space-xs));
  transform: translateY(-50%);
}

[data-tooltip-position="right"]::before {
  bottom: auto;
  top: 50%;
  left: calc(100% + var(--space-xs));
  transform: translateY(-50%);
}

/* -- Interactive elements override cursor -- */
button[data-tooltip],
a[data-tooltip],
.button[data-tooltip] {
  cursor: pointer;
}

/* -- Tooltip dark mode -- */
[data-theme="dark"] [data-tooltip]::before,
[data-theme="dark"] .progress-tooltip {
  background-color: var(--neutral-100);
  color: var(--brown-dark);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) [data-tooltip]::before,
  :root:not([data-theme]) .progress-tooltip {
    background-color: var(--neutral-100);
    color: var(--brown-dark);
  }
}


/* ------ 22. TOAST ------ */

.toast-container {
  /* An app shell running a bottom .app-nav (section 48) re-points this so
     toasts stack above the tab bar instead of covering it. */
  --toast-offset-bottom: var(--space-xl);

  position: fixed;
  bottom: var(--toast-offset-bottom);
  right: var(--space-xl);
  z-index: 900;
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
  pointer-events: none;
}

.toast {
  --toast-exit-duration: var(--duration-xs);
  pointer-events: all;
  background-color: var(--brown-dark);
  color: var(--off-white);
  padding: var(--space-m) var(--space-l);
  border: var(--border-s) solid var(--border-faded);
  display: flex;
  align-items: center;
  gap: var(--space-m);
  max-width: 360px;
  font-size: var(--font-s);
  /* toast.js reads the computed duration before removing the node, so the
     fade length lives here only. */
  transition: opacity var(--toast-exit-duration) var(--ease-in-out);
}

.toast[data-type="success"],
.toast[data-type="green"] { background: var(--status-success-bg); color: var(--status-success); }

.toast[data-type="warning"],
.toast[data-type="yellow"] { background: var(--status-warning-bg); color: var(--status-warning); }

.toast[data-type="danger"],
.toast[data-type="red"] { background: var(--status-danger-bg); color: var(--status-danger); }

.toast[data-type="info"],
.toast[data-type="blue"] { background: var(--status-info-bg); color: var(--status-info); }

.toast-message {
  flex: 1;
}

/* The toast paints its own surface colour, so the close button follows
   currentColor instead of the page-level text tokens. No opacity dimming:
   at 0.7 the icon fails 1.4.11 non-text contrast on the success, danger
   and info surfaces. */
.toast .button.close-btn {
  --button-text-color: currentColor;
}

.toast .button.close-btn:hover {
  background-color: color-mix(in srgb, currentColor, var(--alpha-5));
}

/* Deprecated — use <button class="button close-btn" data-icon-only data-size="small">.
   Kept so existing markup keeps its styling; remove at the next major. */
.toast-close {
  color: inherit;
  line-height: 1;
  opacity: 0.7;
  transition: opacity var(--duration-2xs) var(--ease-out);
}

.toast-close:hover {
  opacity: 1;
}

/* -- Reduced motion -- */
/* Re-points the exit token so dismissal is effectively immediate. */
@media (prefers-reduced-motion: reduce) {
  .toast {
    --toast-exit-duration: var(--duration-2xs);
  }
}

/* -- Toast dark mode -- */
/* Only the neutral toast inverts its surface. A typed toast keeps its status
   colour as text in both themes, and its surface now comes straight from
   --status-*-bg — the dark theme derives those from the page ground, so the
   local re-derivation that used to live here is retired.

   Scoped on the absence of data-type, which matches what toast.js emits: it
   sets dataset.type only when the type is not 'default' (toast.js:65), so a
   neutral toast carries no attribute at all. Selecting on that makes these
   rules mutually exclusive with the .toast[data-type] rules above rather than
   competing with them on specificity and source order. Previously they matched
   every .toast and won — (0,2,0) tying with the typed rules and beating them on
   source order, (0,3,0) beating them outright — which flattened success,
   warning, danger and info to the same grey the moment dark mode was on. */
[data-theme="dark"] .toast:not([data-type]) {
  background-color: var(--neutral-100);
  color: var(--brown-dark);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .toast:not([data-type]) {
    background-color: var(--neutral-100);
    color: var(--brown-dark);
  }
}


/* ------ 23. CODE ------ */

/* -- Inline code -- */
code {
  font-family: var(--font-quaternary);
  font-size: calc(var(--body-size) - .2em);
  background-color: color-mix(in srgb, var(--text-primary), var(--alpha-5));
  padding: var(--space-2xs) var(--space-xs);
  border-radius: var(--radius-xs);
  color: var(--text-primary);
  white-space: nowrap;
}

/* -- Code block -- */
pre {
  background-color: var(--background-secondary);
  border: var(--border-s) solid var(--border-faded);
  padding: var(--space-l);
  overflow-x: auto;
  font-family: var(--font-quaternary);
  font-size: calc(var(--body-size) - 2px);
  line-height: var(--line-height-xl);
}

pre code {
  background: none;
  padding: 0;
  border-radius: 0;
  font-size: inherit;
  word-break: normal;
}

/* -- Keyboard shortcut -- */
kbd {
  display: inline-block;
  padding: var(--space-xs) var(--space-s);
  font-family: var(--font-quaternary);
  font-size: var(--font-2xs);
  line-height: var(--line-height-m);
  color: var(--text-secondary);
  vertical-align: middle;
  background-color: var(--background-faded);
  border: var(--border-s) solid var(--border-secondary);
  border-radius: var(--radius-xs);
  box-shadow: 0 1px 0 var(--border-secondary);
  white-space: nowrap;
}

/* -- Highlighted text -- */
mark {
  background-color: var(--yellow-light);
  color: var(--brown-dark);
  padding: 0 var(--space-2xs);
}

/* -- Mark dark mode -- */
[data-theme="dark"] mark {
  background-color: color-mix(in srgb, var(--yellow), var(--alpha-60));
  color: var(--off-white);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) mark {
    background-color: color-mix(in srgb, var(--yellow), var(--alpha-60));
    color: var(--off-white);
  }
}

/* -- Figure / Figcaption -- */
figure {
  margin: var(--space-xl) 0;
  padding: 0;
}

figcaption {
  font-size: var(--font-xs);
  color: var(--text-faded);
  margin-top: var(--space-s);
  line-height: var(--line-height-xl);
  font-style: italic;
}

/* -- Abbreviation -- */
abbr[title] {
  text-decoration: underline dotted var(--text-faded);
  text-underline-offset: 2px;
  cursor: help;
}




/* ------ 24. IMAGE ------ */

/* -- Base image -- */
.img {
  display: block;
  width: 100%;
  height: auto;
  object-fit: cover;
}

/* -- Aspect ratio combos -- */
.img-1x1  { aspect-ratio: 1 / 1; }
.img-3x2  { aspect-ratio: 3 / 2; }
.img-4x3  { aspect-ratio: 4 / 3; }
.img-16x9 { aspect-ratio: 16 / 9; }
.img-21x9 { aspect-ratio: 21 / 9; }


/* NOTE: .bar is section 41, near the end of this file. It sits beside
   Site Header (section 40) because the two are the same bar shape and were
   drifting apart while they lived in different files. */


/* ------ 25. TABLE ------ */

/* -- Table scroll wrapper -- */
.table-scroll {
  position: relative;
  width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* -- Scroll hint: fade on right edge when overflowing -- */
.table-scroll.is-overflowing {
  display: flex;
}

.table-scroll.is-overflowing::after {
  content: "";
  position: sticky;
  right: 0;
  flex-shrink: 0;
  width: var(--space-2xl);
  margin-left: calc(-1 * var(--space-2xl));
  background: linear-gradient(to right, transparent, var(--background-primary));
  pointer-events: none;
}

.table-scroll.is-scrolled-end::after {
  display: none;
}

/* -- Base table -- */
.table {
  border-spacing: 0;
  border-collapse: collapse;
  width: fit-content;
  max-width: 100%;
  /* font-variant: tabular-nums; */
  font-size: var(--body-size);
  margin-bottom: var(--space-2xl);
}

.table th {
  font-weight: var(--font-weight-bold);
  text-align: left;
  min-width: 130px;
}

.table th,
.table td {
  padding: var(--space-m) var(--space-l);
  border: none;
}

.table thead {
  background-color: transparent;
}

.table thead tr {
  border-bottom: var(--border-s) solid var(--border-secondary);
}

.table tbody tr {
  border-bottom: var(--border-s) solid var(--border-faded);
}

/* -- Table modifier: full width -- */
.table-full {
  width: 100%;
}

/* -- Table modifier: hoverable rows -- */
.table-hover tbody tr:hover {
  background-color: var(--background-faded);
}

/* -- Table modifier: header background -- */
.table-header-filled th {
  background-color: var(--background-faded);
  font-weight: var(--font-weight-semi-bold);
  color: var(--text-primary);
}

/* -- Table footer -- */
.table tfoot tr {
  border-top: var(--border-m) solid var(--border-primary);
}

.table tfoot th {
  font-weight: var(--font-weight-bold);
}

.table tfoot td {
  font-weight: var(--font-weight-semi-bold);
}

td .svg-icn {
  display: inline-flex;
}

/* -- Condensed table on mobile -- */
@media (max-width: 768px) {
  .table th,
  .table td {
    padding: var(--space-s) var(--space-m);
  }
}


/* ------ 26. DIVIDER ------ */

.divider {
  border: none;
  border-top: var(--divider-thickness) solid var(--divider-color);
  margin: var(--divider-spacing) 0;
}

.divider--vertical {
  border-top: none;
  border-left: var(--divider-thickness) solid var(--divider-color);
  height: auto;
  align-self: stretch;
  margin: 0 var(--divider-spacing);
  display: inline-block;
}

.divider--subtle {
  --divider-color: var(--black-alpha-10);
}

.divider--strong {
  --divider-color: var(--border-primary);
  border-top-width: var(--border-m);
}

/* -- Labelled divider --
   A rule with a word in it ("or"). Not an <hr> — the label is content —
   so this variant rides a <div>. Lines are pseudo-elements drawn from the
   same divider tokens. No role="separator": the label should be read. */
.divider--labelled {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  border-top: none;
  font-size: var(--font-s);
  color: var(--text-secondary);
}

.divider--labelled::before,
.divider--labelled::after {
  content: "";
  flex: 1;
  border-top: var(--divider-thickness) solid var(--divider-color);
}

hr {
  border: none;
  border-top: var(--divider-thickness) solid var(--divider-color, var(--border-faded));
  margin: var(--divider-spacing) 0;
}

hr.xl {
  border: none;
  margin: var(--space-2xl) 0;
}


/* ------ 27. DROPDOWN ------ */

/* inline-flex, not flex: this element is the menu's containing block, so a
   block-level box would stretch to its parent's full width and `right: 0`
   would pin the menu to the far side of the page rather than to the trigger.
   The wider the window, the further it flew — which is why it looked correct
   in a narrow preview pane and broke when maximised.
   In a flex or grid parent (a bar, a header) display is
   blockified back to flex, so those layouts are unaffected. */
.dropdown {
  position: relative;
  display: inline-flex;
  vertical-align: middle;
  align-items: stretch;
}

/* -- Trigger -- */
/* Padding is tokenised because bars re-point it rather than overriding the
   rule: a trigger inside a header or bar takes its vertical rhythm from
   the bar's height, not from its own padding. */
.dropdown-trigger {
  --dropdown-trigger-padding-y: var(--space-l);
  --dropdown-trigger-padding-x: var(--space-m);
  display: flex;
  align-items: center;
  align-self: stretch;
  gap: var(--space-s);
  padding: var(--dropdown-trigger-padding-y) var(--dropdown-trigger-padding-x);
  background: none;
  border: none;
  color: var(--text-primary);
  cursor: pointer;
  white-space: nowrap;
  font-family: var(--font-primary);
  font-size: var(--font-s);
  line-height: var(--line-height-m);
  text-decoration: none;
  transition: color var(--duration-2xs) var(--ease-out),
              background-color var(--duration-2xs) var(--ease-out);
}

.dropdown-trigger:hover {
  background-color: var(--dropdown-item-hover);
  color: var(--text-plain);
}

.dropdown-trigger .svg-icn {
  width: 1.25rem;
  height: 1.25rem;
  flex-shrink: 0;
}

.dropdown.is-open > .dropdown-trigger {
  background-color: var(--background-faded);
}

/* -- Chevron (rotates on open) -- */
.dropdown-chevron {
  width: 1rem;
  height: 1rem;
  color: var(--text-faded);
  transition: transform var(--duration-xs) var(--ease-out);
  flex-shrink: 0;
}

/* Child combinator, load-bearing: a dropdown can contain another dropdown
   (the bar's overflow panel holds demoted ones), and a descendant selector
   here made opening the outer panel rotate the nested chevrons and paint the
   nested menus as if the user had opened them all. Same discipline on every
   .is-open and placement rule below. */
.dropdown.is-open > .dropdown-trigger .dropdown-chevron {
  transform: rotate(180deg);
}

/* -- Menu -- */
.dropdown-menu {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  min-width: var(--dropdown-menu-min-width);
  background-color: var(--dropdown-background);
  border: var(--border-s) solid var(--dropdown-border);
  z-index: 1000;
  flex-direction: column;
}

/* -- Placement --
   Authored intent goes on the dropdown as data-placement; dropdown.js measures
   on open and writes the placement it actually resolved to into
   data-resolved-placement, flipping whichever axis would overflow. Styling the
   resolved value rather than the requested one keeps one source of truth — the
   menu cannot be told to open upward and then paint downward.
   .is-right predates this and still works; the script reads it as bottom-end. */
.dropdown-menu.is-right,
.dropdown[data-resolved-placement$="-end"] > .dropdown-menu {
  left: auto;
  right: 0;
}

.dropdown[data-resolved-placement^="top"] > .dropdown-menu {
  top: auto;
  bottom: 100%;
}

/* A flip beats the legacy class, or a right-aligned menu near the right edge
   could never be pushed back inward. */
.dropdown[data-resolved-placement$="-start"] > .dropdown-menu.is-right {
  left: 0;
  right: auto;
}

.dropdown.is-open > .dropdown-menu {
  display: flex;
}

/* -- Items -- */
.dropdown-item {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  padding: var(--space-l) var(--space-m);
  font-size: var(--font-s);
  color: var(--text-primary);
  text-decoration: none;
  background: none;
  border: none;
  width: 100%;
  cursor: pointer;
  transition: background-color var(--duration-2xs) var(--ease-out);
  font-family: var(--font-primary);
  line-height: var(--line-height-m);
  text-align: left;
}

/* The flex display above beats the UA's [hidden] rule, so without this an
   item hidden by a consumer (per-mode options, a per-variant allow-list)
   stays visible, focusable and clickable. */
.dropdown-item[hidden] {
  display: none;
}

.dropdown-item:hover {
  background-color: var(--dropdown-item-hover);
  color: var(--text-plain);
}

/* Only the row tint belongs here. The ring itself comes from the global
   button/link rule — this used to override it with `outline: none` plus an
   inset box-shadow, which forced-colors modes strip entirely (leaving no
   focus indicator at all) and which drew in --input-focus, an amber in dark
   mode that fell below the 3:1 non-text contrast floor against this tint. */
.dropdown-item:focus-visible {
  background-color: var(--dropdown-item-hover);
}

.dropdown-item .svg-icn {
  width: 1.25rem;
  height: 1.25rem;
}

/* aria-disabled is the supported form — it keeps the item focusable so a
   keyboard user finds the same options a mouse user can see, and dropdown.js
   refuses the activation. `pointer-events: none` used to sit here, which both
   cancelled the not-allowed cursor on the line above and left keyboard
   activation fully live while only blocking the mouse. Opacity is raised off
   0.4, which put item text at 2.45:1. */
.dropdown-item.is-disabled,
.dropdown-item[aria-disabled="true"] {
  color: var(--text-faded);
  cursor: not-allowed;
}

/* Dimmed with colour, not opacity. `opacity` renders the focus outline into
   the same group and drops it to 2.34:1 — and these items stay focusable by
   design, so their focus indicator still has to clear 3:1. Scoping the fade
   to the icon keeps the visual weight without touching anything focusable. */
.dropdown-item.is-disabled .svg-icn,
.dropdown-item[aria-disabled="true"] .svg-icn {
  opacity: 0.55;
}

.dropdown-item--danger {
  color: var(--status-danger);
}

/* --alpha-10 is `transparent 90%`, giving a 10% tint. The rule previously
   read --alpha-90, which is `transparent 10%` — a 90% opaque red fill that
   put the label at 1.49:1 in dark mode. Every other status tint in this file
   uses --alpha-10; this was the only --alpha-90 in the stylesheet. */
.dropdown-item--danger:hover {
  background-color: color-mix(in srgb, var(--status-danger), var(--alpha-10));
}

/* -- Divider -- */
.dropdown-divider {
  border: none;
  border-top: var(--border-s) solid var(--dropdown-border);
  margin: 0;
}

/* -- Label (section header) -- */
.dropdown-label {
  padding: var(--space-s) var(--space-m);
  /* Neutralised so the class renders the same on a <p> as on a <div> — the
     base paragraph margin (section 8) otherwise doubles the panel's own
     spacing. Same reasoning as .bar-label. */
  margin-bottom: 0;
  font-size: var(--font-2xs);
  font-weight: var(--font-weight-semi-bold);
  color: var(--text-faded);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* -- Description (under item text) -- */
.dropdown-desc {
  font-size: var(--font-2xs);
  color: var(--text-faded);
  white-space: normal;
}

/* -- Trailing content (pushes to right edge) -- */
.dropdown-item-end {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  color: var(--text-faded);
  flex-shrink: 0;
  font-size: var(--font-2xs);
}

/* -- Group (role="group") --
   role="menu" may only own menuitems, groups and separators, so a section
   label has to sit inside a group rather than loose in the menu. The group
   re-declares the column layout because it becomes a single flex child of
   .dropdown-menu and would otherwise lay its items out in a row.
   `display: contents` would avoid that but has a history of dropping
   elements from the accessibility tree — not a risk worth taking on the
   element carrying the role. */
.dropdown-group {
  display: flex;
  flex-direction: column;
}

/* -- Header (non-interactive identity row) --
   For content that belongs in the panel but not in the menu, such as the
   signed-in user. It sits OUTSIDE the role="menu" element; it is not an item
   and must never be given a menuitem role. */
.dropdown-header {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  padding: var(--space-l) var(--space-m);
}

/* -- Checkable items --
   The checkmark is what conveys selection: colour alone would fail 1.4.1.
   Its space is reserved in both states so toggling cannot reflow the row,
   which is why this hides with visibility rather than display. */
.dropdown-checkmark {
  width: 1.25rem;
  height: 1.25rem;
  flex-shrink: 0;
  visibility: hidden;
}

/* Keyed on aria-checked alone, deliberately. Accepting .is-selected here too
   would let class-only markup show a checkmark that assistive tech never
   reports — a visible state with no announced counterpart. The class is still
   applied alongside for consistency with the shared state-class convention,
   but the ARIA state is the single source of truth. */
.dropdown-item[aria-checked="true"] .dropdown-checkmark {
  visibility: visible;
}


/* ------ 28. TAG ------ */

.tag {
  --tag-case: uppercase;
  --tag-tracking: var(--letter-spacing-m);

  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-family: var(--font-primary);
  font-size: var(--font-2xs);
  font-weight: var(--font-weight-regular);
  letter-spacing: var(--tag-tracking);
  text-transform: var(--tag-case);
  padding: var(--space-s) var(--space-s);
  background-color: var(--bg-faded-5);
  color: var(--text-secondary);
  white-space: nowrap;
  line-height: var(--line-height-xs);
  border-radius: 3px;
}

/* Sentence-case variant — customize case via the tokens above or this
   attribute, never by overriding the class (Layer Rule 3). Required whenever a
   tag carries content the consumer supplies rather than a fixed label: a
   case-sensitive identifier, a proper noun, or a full sentence, none of which
   survive being uppercased. */
.tag[data-case="none"] {
  --tag-case: none;
  --tag-tracking: normal;
}

/* Border colour rides the status text token at reduced strength instead of a
   raw -light primitive, so it follows the pair through both themes. It only
   paints when a consumer composes the tag with the border classes. */
.tag[data-color="success"],
.tag[data-color="green"] {
  background-color: var(--status-success-bg);
  border-color: color-mix(in srgb, var(--status-success), transparent 60%);
  color: var(--status-success);
}

.tag[data-color="warning"],
.tag[data-color="yellow"] {
  background-color: var(--status-warning-bg);
  border-color: color-mix(in srgb, var(--status-warning), transparent 60%);
  color: var(--status-warning);
}

.tag[data-color="danger"],
.tag[data-color="red"] {
  background-color: var(--status-danger-bg);
  border-color: color-mix(in srgb, var(--status-danger), transparent 60%);
  color: var(--status-danger);
}

.tag[data-color="info"],
.tag[data-color="blue"] {
  background-color: var(--status-info-bg);
  border-color: color-mix(in srgb, var(--status-info), transparent 60%);
  color: var(--status-info);
}

.tag-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  cursor: pointer;
  color: inherit;
  opacity: 0.6;
  padding: var(--space-2xs);
  transition: opacity 0.15s ease, background-color 0.15s ease;
  flex-shrink: 0;
  border-radius: var(--radius-pill);
}

.tag-remove .svg-icn {
  width: 0.625rem;
  height: 0.625rem;
}

.tag-remove:hover {
  opacity: 1;
  background-color: var(--black-alpha-10);
}

.tag-remove:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus), var(--alpha-75));
}

.tag-group {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  align-items: center;
}

/* ------ 29. DIALOG ------ */

.dialog {
  /* -- Component tokens (re-point these to re-skin; never override the rules) -- */
  --dialog-background: var(--background-primary);
  --dialog-shadow: 0 8px 32px var(--black-alpha-20);
  --dialog-radius: 0;
  --dialog-padding: var(--space-xl);
  --dialog-backdrop: var(--background-modal);

  /* -- Motion -- */
  /* Enters from below and settles upward; exits back down. Entrance is the
     slower half, which is why the two directions have their own tokens. */
  --dialog-offset: var(--space-l);
  --dialog-enter-duration: var(--duration-m);
  --dialog-enter-easing: var(--ease-out);
  --dialog-exit-duration: var(--duration-s);
  --dialog-exit-easing: var(--ease-in);

  background-color: var(--dialog-background);
  border: none;
  border-radius: var(--dialog-radius);
  box-shadow: var(--dialog-shadow);
  padding: var(--dialog-padding);
  max-width: var(--dialog-max-width);
  width: min(var(--dialog-max-width), calc(100vw - var(--space-2xl) * 2));
  max-height: 85vh;
  max-height: 85dvh; /* dvh accounts for mobile browser chrome; vh is the fallback */
  margin: auto;

  /* Layout lives here, not on [open]. Only `display` is discretely transitioned
     on exit, so any layout property left on [open] would snap back the instant
     the attribute is removed — reflowing the dialog mid-animation. */
  flex-direction: column;
  gap: var(--space-l);

  /* Closed / exit state. See the entrance-exit note below. */
  opacity: 0;
  translate: 0 var(--dialog-offset);
  transition:
    opacity var(--dialog-exit-duration) var(--dialog-exit-easing),
    translate var(--dialog-exit-duration) var(--dialog-exit-easing),
    display var(--dialog-exit-duration) allow-discrete,
    overlay var(--dialog-exit-duration) allow-discrete;
}

.dialog::backdrop {
  /* ::backdrop inherits from the dialog in current engines, which is what makes
     a scoped [data-theme] region theme its own backdrop. Engines predating that
     inherit from nothing, so --dialog-backdrop is unset there and the fallback
     picks up the root-level token instead. */
  background-color: var(--dialog-backdrop, var(--background-modal));

  opacity: 0;
  transition:
    opacity var(--dialog-exit-duration) var(--dialog-exit-easing),
    display var(--dialog-exit-duration) allow-discrete,
    overlay var(--dialog-exit-duration) allow-discrete;
}

/* -- Entrance / exit --
   A top-layer element can only transition out of display:none with
   allow-discrete, and can only transition *in* with @starting-style. The
   `overlay` property is what keeps the dialog painted in the top layer for
   the duration of the exit — without it the close is instant.
   Engines missing either feature show and hide the dialog with no animation,
   which is a clean degradation rather than a broken state. */

.dialog[open] {
  display: flex;

  opacity: 1;
  translate: 0 0;
  transition:
    opacity var(--dialog-enter-duration) var(--dialog-enter-easing),
    translate var(--dialog-enter-duration) var(--dialog-enter-easing),
    display var(--dialog-enter-duration) allow-discrete,
    overlay var(--dialog-enter-duration) allow-discrete;
}

.dialog[open]::backdrop {
  opacity: 1;
  transition:
    opacity var(--dialog-enter-duration) var(--dialog-enter-easing),
    display var(--dialog-enter-duration) allow-discrete,
    overlay var(--dialog-enter-duration) allow-discrete;
}

/* Must follow the [open] rules — this supplies the "before open" state only. */
@starting-style {
  .dialog[open] {
    opacity: 0;
    translate: 0 var(--dialog-offset);
  }

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

/* -- Reduced motion -- */
/* Drops the travel and shortens to a plain fade. Re-points tokens rather than
   disabling the transition, so the discrete display/overlay steps still run
   and the dialog can't get stuck mid-exit. */
@media (prefers-reduced-motion: reduce) {
  .dialog {
    --dialog-offset: 0px;
    --dialog-enter-duration: var(--duration-2xs);
    --dialog-exit-duration: var(--duration-2xs);
  }
}

.dialog-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-l);
}

.dialog-title {
  font-size: var(--font-3xl);
  font-weight: var(--font-weight-medium);
  line-height: var(--line-height-m);
  color: var(--text-primary);
  margin: 0;
}

/* Deprecated — use <button class="button close-btn" data-icon-only data-size="small">.
   Kept so existing markup keeps its styling; remove at the next major. */
.dialog-close {
  flex-shrink: 0;
  color: var(--text-faded);
  transition: color var(--duration-2xs) var(--ease-out);
}

.dialog-close:hover {
  color: var(--text-primary);
}

.dialog-body {
  overflow-y: auto;
  flex: 1;
  /* Column flex items default to min-height: auto, which stops the body
     shrinking and pushes the footer actions out of the dialog. */
  min-height: 0;
  overscroll-behavior: contain;
  /* overflow-y turns the body into a clip box on both axes, and focus
     outlines paint outside the border box — up to 4px (2px outline +
     2px offset). Full-width fields touch the body's edges, so their
     outlines were cropped. The padding keeps them inside the clip box;
     the matching negative margin cancels it so nothing visibly moves. */
  padding: 4px;
  margin: -4px;
}

.dialog-body > *:first-child {
  margin-top: 0;
}

.dialog-body > *:last-child {
  margin-bottom: 0;
}

.dialog-footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-m);
  flex-wrap: wrap;
}

/* -- Dialog dark mode -- */
/* Re-points tokens rather than redeclaring rules, so a brand override of
   --dialog-background survives the theme switch. */
[data-theme="dark"] .dialog {
  --dialog-background: var(--background-secondary);
  --dialog-shadow: 0 8px 40px var(--black-alpha-60);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .dialog {
    --dialog-background: var(--background-secondary);
    --dialog-shadow: 0 8px 40px var(--black-alpha-60);
  }
}


/* ------ 30. DRAWER ------ */

/* A drawer is a dialog docked to an edge of the viewport. It shares the native
   <dialog> element, the modal machinery in dialog.js (showModal, native Escape,
   native focus trapping) and the same top-layer entrance/exit technique as
   section 29 — @starting-style to transition in, allow-discrete + overlay to
   transition out. Only the docking geometry and the slide axis differ, so read
   the transition comments in section 29 first; they are not repeated here.

   Nothing in this section duplicates .dialog: a drawer is <dialog class="drawer">
   and never carries both classes. */

.drawer {
  /* -- Component tokens (re-point these to re-skin; never override the rules) -- */
  --drawer-background: var(--background-primary);
  --drawer-shadow: 0 8px 32px var(--black-alpha-20);
  --drawer-radius: 0;
  --drawer-padding: var(--space-xl);
  --drawer-backdrop: var(--background-modal);
  /* Width when docked to an inline edge, height when docked to a block edge.
     One token covers both because only one axis is ever free. */
  --drawer-size: 25rem;

  /* -- Drag handle --
     Declared here rather than on .drawer-handle itself: a custom property set
     on the element that consumes it shadows every inherited value whatever the
     ancestor's specificity, so a brand's theme.css (which only ever opens
     :root) and the per-instance style="--drawer-size: …" idiom would both
     silently fail to re-point them. */
  /* --text-faded, not a border token: a handle is a control, so non-text
     contrast (WCAG 1.4.11) applies, and both --border-secondary steps land
     under 3:1 against the panel they sit on — #ffffff light, and
     --background-secondary #222222 dark, which is where --drawer-background
     re-points in dark mode. */
  --drawer-handle-color: var(--text-faded);
  --drawer-handle-length: 2.25rem; /* 36px */
  --drawer-handle-thickness: 0.25rem; /* 4px */
  /* Grab-strip thickness AND the padding the drawer reserves for it — one
     token so the two can never disagree, because the moment they do the strip
     overlays content. --target-min is a floor, not a suggestion: below it the
     strip stops clearing the hit-area rule (section 3). */
  --drawer-handle-target: var(--target-min);

  /* -- Motion -- */
  /* Faster than a dialog in both directions: a drawer travels the full width of
     its own box, so the same duration would read as sluggish. */
  --drawer-enter-duration: var(--duration-s);
  --drawer-enter-easing: var(--ease-out);
  --drawer-exit-duration: var(--duration-xs);
  --drawer-exit-easing: var(--ease-in);
  /* How far the drawer travels when closed. Percentages resolve against its
     own border box, so 100% parks it exactly off-screen whatever
     --drawer-size is.

     Magnitude and direction are deliberately split. The placement rules below
     are more specific than `.drawer`, so if they carried the magnitude too,
     the reduced-motion block — which can only reach `.drawer` — would lose the
     cascade and leave reduced-motion users with the full slide crammed into a
     shorter duration. Placement sets direction only; magnitude stays here,
     declared exactly once, where the media query can reach it. */
  --drawer-travel: 100%;
  --drawer-offset: 0 0;

  background-color: var(--drawer-background);
  border: none;
  border-radius: var(--drawer-radius);
  box-shadow: var(--drawer-shadow);
  padding: var(--drawer-padding);
  /* The UA stylesheet centres a dialog with margin:auto — that would undo the
     docking insets below. */
  margin: 0;

  /* Layout lives here, not on [open] — see the note in section 29. */
  flex-direction: column;
  gap: var(--space-l);

  /* Closed / exit state. */
  opacity: 0;
  translate: var(--drawer-offset);
  transition:
    opacity var(--drawer-exit-duration) var(--drawer-exit-easing),
    translate var(--drawer-exit-duration) var(--drawer-exit-easing),
    display var(--drawer-exit-duration) allow-discrete,
    overlay var(--drawer-exit-duration) allow-discrete;
}

.drawer::backdrop {
  /* Fallback rationale as section 29. */
  background-color: var(--drawer-backdrop, var(--background-modal));

  opacity: 0;
  transition:
    opacity var(--drawer-exit-duration) var(--drawer-exit-easing),
    display var(--drawer-exit-duration) allow-discrete,
    overlay var(--drawer-exit-duration) allow-discrete;
}

/* -- Placement --
   Two placements dock to the inline edges and take a width; two dock to the
   block edges and take a height. In each pair the free axis is stretched with
   a paired inset and the docked axis is pinned, with the opposite inset reset
   to auto so the UA's `inset: 0` cannot stretch the drawer across the viewport.
   `end` is the default, so an omitted attribute matches it. */

/* Inline-edge sizing. Written as "anything that is not an explicit block-edge
   placement" so a typo (data-placement="left") degrades to a working end-docked
   drawer. Matching only the known values would leave an unrecognised one with
   no placement rule at all — the UA's `inset: 0` would then stretch it edge to
   edge with no slide and no warning. */
.drawer:not([data-placement="top"]):not([data-placement="bottom"]) {
  inset-block: 0;
  block-size: auto;
  max-block-size: none;
  inline-size: var(--drawer-size);
  max-inline-size: 100%;
}

.drawer[data-placement="start"] {
  inset-inline-start: 0;
  inset-inline-end: auto;
  --drawer-offset: calc(var(--drawer-travel) * -1) 0;
}

/* `end` is the default, and the landing place for an unrecognised value. */
.drawer:not([data-placement="start"]):not([data-placement="top"]):not([data-placement="bottom"]) {
  inset-inline-end: 0;
  inset-inline-start: auto;
  --drawer-offset: var(--drawer-travel) 0;
}

.drawer[data-placement="top"],
.drawer[data-placement="bottom"] {
  inset-inline: 0;
  inline-size: auto;
  max-inline-size: none;
  block-size: var(--drawer-size);
  max-block-size: 100%;
}

.drawer[data-placement="top"] {
  inset-block-start: 0;
  inset-block-end: auto;
  --drawer-offset: 0 calc(var(--drawer-travel) * -1);
}

.drawer[data-placement="bottom"] {
  inset-block-end: 0;
  inset-block-start: auto;
  --drawer-offset: 0 var(--drawer-travel);
}

/* The insets above are logical and follow the element's own direction, but
   `translate` has no logical form — its first value is always physical X. In
   RTL the inline placements dock to the opposite side, so the slide direction
   must flip with them or the drawer animates away from its own edge.
   `:dir()` rather than `[dir="rtl"] &` so that dir set on the drawer itself
   counts, not only dir on an ancestor. */
.drawer[data-placement="start"]:dir(rtl) {
  --drawer-offset: var(--drawer-travel) 0;
}

.drawer:not([data-placement="start"]):not([data-placement="top"]):not([data-placement="bottom"]):dir(rtl) {
  --drawer-offset: calc(var(--drawer-travel) * -1) 0;
}

/* -- Entrance / exit -- */

.drawer[open] {
  display: flex;

  opacity: 1;
  translate: 0 0;
  transition:
    opacity var(--drawer-enter-duration) var(--drawer-enter-easing),
    translate var(--drawer-enter-duration) var(--drawer-enter-easing),
    display var(--drawer-enter-duration) allow-discrete,
    overlay var(--drawer-enter-duration) allow-discrete;
}

.drawer[open]::backdrop {
  opacity: 1;
  transition:
    opacity var(--drawer-enter-duration) var(--drawer-enter-easing),
    display var(--drawer-enter-duration) allow-discrete,
    overlay var(--drawer-enter-duration) allow-discrete;
}

/* Must follow the [open] rules — supplies the "before open" state only. */
@starting-style {
  .drawer[open] {
    opacity: 0;
    translate: var(--drawer-offset);
  }

  .drawer[open]::backdrop {
    opacity: 0;
  }
}

/* showModal() makes the rest of the page inert but does not stop it scrolling.
   Behind a full-height drawer that reads as the drawer itself scrolling, so
   pin the page while one is open. Scoped to .drawer deliberately — .dialog has
   never locked scroll and changing that belongs with a dialog change.

   Both elements are targeted because which one scrolls depends on the document.
   This only reaches a page whose scroller IS the root: an app that scrolls a
   container div, or drives scrolling through a library, has to lock its own
   scroller (noted in cms/drawer.md). Pages without a stable scrollbar gutter
   will shift by the scrollbar width on open — set `scrollbar-gutter: stable`
   on :root in the consuming product if that matters there. */
html:has(.drawer[open]),
body:has(.drawer[open]) {
  overflow: hidden;
}

/* -- Drag to dismiss --
   A drawer that contains a .drawer-handle can be dragged off its own edge.
   The handle's presence IS the opt-in: there is no attribute, for the same
   reason .drawer-header has none — the affordance is markup you write, so a
   drawer can never advertise a gesture it does not have, nor hide one it
   does. Pointer tracking lives in dialog.js.

   The handle docks to the drawer's FREE edge — the one facing the page,
   opposite the edge it is docked to — because that is the edge the gesture
   pulls away from. One rule, all four placements.

   The strip is --target-min thick so the grab area clears the hit-area floor
   (section 3) while the visible pill stays slim, and the drawer reserves that
   thickness as extra padding on the same side rather than letting the strip
   overlay content: an invisible 44px band swallowing clicks on the panel's
   inner edge is exactly the kind of bug nobody attributes to the handle. */

/* Tokens live on .drawer (see the block above). Every use carries the same
   fallback shape --drawer-backdrop already uses, so a missing token falls to
   its default rather than to nothing. (A handle outside a drawer still
   collapses — all its geometry is scoped under .drawer — the fallbacks only
   guarantee the values, not the placement.) */
.drawer-handle {
  position: absolute;
  display: grid;
  place-items: center;
  user-select: none;
  -webkit-user-select: none;
  cursor: grab;
}

.drawer-handle::before {
  content: "";
  display: block;
  border-radius: var(--radius-pill);
  background-color: var(--drawer-handle-color, var(--text-faded));
}

/* Block placements: the strip spans the inline axis, the pill lies along it.
   touch-action gives the browser back the axis the gesture does NOT use. A
   blanket `none` would make the reserved strip a dead zone the user cannot
   see — a finger planted in what looks like ordinary panel padding would
   refuse to scroll, with no feedback and no visible cause. Surrendering the
   cross axis also routes browser-claimed diagonal swipes through
   pointercancel, which dialog.js already resolves by returning the drawer
   home rather than half-committing. */
.drawer[data-placement="top"] .drawer-handle,
.drawer[data-placement="bottom"] .drawer-handle {
  inset-inline: 0;
  block-size: var(--drawer-handle-target, var(--target-min));
  touch-action: pan-x;
}

.drawer[data-placement="top"] .drawer-handle::before,
.drawer[data-placement="bottom"] .drawer-handle::before {
  inline-size: var(--drawer-handle-length, 2.25rem);
  block-size: var(--drawer-handle-thickness, 0.25rem);
}

/* A top-docked drawer's free edge is the bottom, and the reverse. */
.drawer[data-placement="top"] .drawer-handle { inset-block-end: 0; }
.drawer[data-placement="bottom"] .drawer-handle { inset-block-start: 0; }

.drawer[data-placement="top"]:has(.drawer-handle) {
  padding-block-end: calc(var(--drawer-padding) + var(--drawer-handle-target, var(--target-min)));
}

.drawer[data-placement="bottom"]:has(.drawer-handle) {
  padding-block-start: calc(var(--drawer-padding) + var(--drawer-handle-target, var(--target-min)));
}

/* Inline placements: the strip spans the block axis, the pill stands upright.
   Written as negations of the block-edge values for the same reason the
   placement rules are — an unrecognised data-placement degrades to a working
   end-docked drawer, handle included, rather than to no handle rule at all.

   On an inline drawer the strip runs the full height beside the header, where
   the close button also sits. They are adjacent rather than overlapping only
   because the padding reservation below insets the header by the strip's own
   width — re-point --drawer-padding toward zero and that clearance narrows. */
.drawer:not([data-placement="top"]):not([data-placement="bottom"]) .drawer-handle {
  inset-block: 0;
  inline-size: var(--drawer-handle-target, var(--target-min));
  touch-action: pan-y;
}

.drawer:not([data-placement="top"]):not([data-placement="bottom"]) .drawer-handle::before {
  inline-size: var(--drawer-handle-thickness, 0.25rem);
  block-size: var(--drawer-handle-length, 2.25rem);
}

.drawer[data-placement="start"] .drawer-handle { inset-inline-end: 0; }

.drawer[data-placement="start"]:has(.drawer-handle) {
  padding-inline-end: calc(var(--drawer-padding) + var(--drawer-handle-target, var(--target-min)));
}

.drawer:not([data-placement="start"]):not([data-placement="top"]):not([data-placement="bottom"]) .drawer-handle {
  inset-inline-start: 0;
}

.drawer:not([data-placement="start"]):not([data-placement="top"]):not([data-placement="bottom"]):has(.drawer-handle) {
  padding-inline-start: calc(var(--drawer-padding) + var(--drawer-handle-target, var(--target-min)));
}

/* While the finger is down the drawer must track it exactly, so the entrance
   transition is suspended for the duration of the gesture. dialog.js clears
   the inline translate on release and the stylesheet takes the drawer home
   again — or off its edge — on the tokens above. */
.drawer.is-dragging {
  transition: none;
}

.drawer.is-dragging .drawer-handle {
  cursor: grabbing;
}

/* The pill is a filled shape, which forced-colors would repaint with the
   system background — an invisible handle on the one surface where the
   gesture is least discoverable. Repaint the fill in the system text colour
   and opt out of further forcing; a system keyword (never a literal hex) is
   what keeps it appropriate to whatever palette the user chose. Same pattern
   as the segmented control's active state (section 12). */
@media (forced-colors: active) {
  .drawer-handle::before {
    background-color: CanvasText;
    forced-color-adjust: none;
  }
}

/* -- Blocked-close feedback --
   A cancelled close (see drawer-hide / dialog-hide in dialog.js) leaves the
   surface open with nothing to show for the click. The pulse says "heard you,
   not closing" — without it a guarded surface reads as a broken close button.
   Axis-agnostic, so one animation serves all four drawer placements and the
   centred dialog. Shared because dialog.js pulses BOTH: styling only .drawer
   would leave a guarded dialog with a state class and no feedback. */
.dialog.is-pulsing,
.drawer.is-pulsing {
  animation: surface-pulse var(--duration-xs) var(--ease-in-out);
}

@keyframes surface-pulse {
  50% {
    scale: 1.008;
  }
}

/* -- Reduced motion -- */
/* Drops the travel and shortens to a plain fade, re-pointing tokens rather than
   disabling the transition so the discrete display/overlay steps still run and
   the drawer cannot get stuck mid-exit (same reasoning as section 29). The
   pulse becomes a backdrop flash: scale is exactly the kind of motion the
   preference asks us to drop, but silent failure is worse than no feedback. */
@media (prefers-reduced-motion: reduce) {
  /* Drag to dismiss is deliberately NOT disabled here. The drawer following a
     finger is direct manipulation, not the decorative motion the preference
     asks us to drop, and removing it would take away a dismissal route rather
     than calm one down. Only the release animation changes. */
  .drawer {
    /* Zeroing the magnitude is what actually drops the travel — the placement
       rules only carry direction, so they cannot override this. */
    --drawer-travel: 0%;
    --drawer-enter-duration: var(--duration-2xs);
    --drawer-exit-duration: var(--duration-2xs);

    /* A drag leaves an inline translate of at least half the drawer's extent.
       With travel zeroed, keeping translate in the exit would tween the
       drawer all the way back toward its edge while it fades — a large, fast
       slide opposite the gesture, delivered to exactly the users who asked
       for less motion. Take position out of the exit: dialog.js clears the
       inline translate on release, so the position CUTS home in a single
       frame — a cut, not motion — and the drawer fades out from there.

       The whole shorthand is re-declared, never just transition-property:
       the base rule's behavior list is positionally bound to its four
       properties, so shortening the property list alone would move
       allow-discrete off `display` and strand the drawer mid-exit — the
       failure the section 29 comments exist to prevent. The [open] entrance
       rule still carries translate at higher specificity, so a CANCELLED
       drag still visibly returns home; that is feedback, not decoration. */
    transition:
      opacity var(--drawer-exit-duration) var(--drawer-exit-easing),
      display var(--drawer-exit-duration) allow-discrete,
      overlay var(--drawer-exit-duration) allow-discrete;
  }

  .dialog.is-pulsing,
  .drawer.is-pulsing {
    animation: none;
  }

  .dialog.is-pulsing::backdrop,
  .drawer.is-pulsing::backdrop {
    animation: surface-backdrop-flash var(--duration-xs) var(--ease-in-out);
  }

  @keyframes surface-backdrop-flash {
    50% {
      opacity: 0.7;
    }
  }
}

/* -- Structure -- */

.drawer-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-l);
}

.drawer-title {
  font-size: var(--font-2xl);
  font-weight: var(--font-weight-regular);
  line-height: var(--line-height-m);
  color: var(--text-primary);
  margin: 0;
}

/* Supplementary controls sitting beside the close button. */
.drawer-header-actions {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  flex-shrink: 0;
}

.drawer-body {
  overflow-y: auto;
  flex: 1;
  /* Column flex items default to min-height:auto, which stops the body
     shrinking and pushes the footer out of the drawer. */
  min-height: 0;
  overscroll-behavior: contain;
  /* Same clip-box fix as .dialog-body: focus outlines paint up to 4px
     outside the border box and were cropped at the body's edges. */
  padding: 4px;
  margin: -4px;
}

.drawer-body > *:first-child {
  margin-top: 0;
}

.drawer-body > *:last-child {
  margin-bottom: 0;
}

.drawer-footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-m);
  flex-wrap: wrap;
}

/* -- Drawer dark mode -- */
/* Re-points tokens rather than redeclaring rules, so a brand override of
   --drawer-background survives the theme switch. */
[data-theme="dark"] .drawer {
  --drawer-background: var(--background-secondary);
  --drawer-shadow: 0 8px 40px var(--black-alpha-60);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .drawer {
    --drawer-background: var(--background-secondary);
    --drawer-shadow: 0 8px 40px var(--black-alpha-60);
  }
}


/* ------ 31. SLIDER ------ */

input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 4px;
  background-color: var(--background-darker);
  border-radius: var(--radius-pill);
  outline: none;
  cursor: pointer;
  border: none;
  padding: 0;
  display: block;
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background-color: var(--text-primary);
  border: 2px solid var(--background-primary);
  cursor: pointer;
  box-shadow: 0 1px 4px var(--black-alpha-20);
  transition: transform 0.1s ease, box-shadow 0.1s ease;
}

input[type="range"]::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background-color: var(--text-primary);
  border: 2px solid var(--background-primary);
  cursor: pointer;
  box-shadow: 0 1px 4px var(--black-alpha-20);
}

input[type="range"]:hover::-webkit-slider-thumb {
  transform: scale(1.1);
  box-shadow: 0 2px 8px var(--black-alpha-20);
}

input[type="range"]:focus-visible {
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus), var(--alpha-75));
  border-radius: var(--radius-pill);
}

input[type="range"]:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

input[type="range"]:disabled::-webkit-slider-thumb {
  cursor: not-allowed;
}

.slider-wrapper {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.slider-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: var(--font-s);
  color: var(--text-primary);
}

.slider-value {
  font-family: var(--font-quaternary);
  font-size: var(--font-xs);
  color: var(--text-faded);
}


/* ------ 32. RATING ------ */

.rating {
  display: inline-flex;
  gap: var(--space-xs);
  align-items: center;
}

.rating-star {
  width: var(--rating-size);
  height: var(--rating-size);
  cursor: pointer;
  color: var(--background-faded);
  transition: color 0.1s ease, transform 0.1s ease;
  flex-shrink: 0;
}

.rating-star svg path {
  stroke: var(--text-faded);
  stroke-width: var(--border-m);
}

.rating-star.is-filled,
.rating-star.is-hovered {
  color: var(--yellow);
}


.rating-star.is-filled svg path,
.rating-star.is-hovered svg path {
  stroke: var(--text-primary);
}


.rating-star:hover {
  transform: scale(1.1);
}

.rating-star:focus-visible {
  outline: none;
  border-radius: var(--radius-xs);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus), var(--alpha-75));
}

.rating.is-readonly .rating-star {
  cursor: default;
  pointer-events: none;
}

.rating.is-readonly .rating-star:hover {
  transform: none;
}

.rating--sm { --rating-size: 1rem; }
.rating--lg { --rating-size: 2rem; }


/* ------ 33. COPY BUTTON ------ */

.copy-btn {
  position: relative;
}

/* -- State: default -- */
.copy-btn .copy-btn-default { display: inline-flex; align-items: center; gap: var(--space-s); }
.copy-btn .copy-btn-copied  { display: none; }

/* -- State: copied -- */
.copy-btn.is-copied .copy-btn-default { display: none; }
.copy-btn.is-copied .copy-btn-copied  { display: inline-flex; align-items: center; gap: var(--space-s); }

.copy-btn.is-copied {
  color: var(--status-success);
  border-color: var(--status-success);
  background-color: color-mix(in srgb, var(--status-success), var(--alpha-10));
}

/* -- Variant: icon only -- */
.copy-btn.is-icon-only {
  padding: var(--space-s);
}

/* -- Variant: ghost (no background, no border) -- */
.copy-btn.is-ghost {
  background: none;
  border: none;
  padding: var(--space-xs);
  color: var(--text-primary);
}

.copy-btn.is-ghost:hover {
  color: var(--text-faded);
}

.copy-btn.is-ghost.is-copied {
  background: none;
  border: none;
  color: var(--status-success);
}

/* -- Tooltip: force visible on copied state -- */
.copy-btn.is-copied[data-tooltip]::before {
  opacity: 1;
}


/* ------ 34. NUMBER INPUT ------ */

/* One field with two steppers on it, not three controls in a row. The border and
   radius belong to the whole thing; the steppers are transparent until pointed
   at, so the field reads as the subject and the buttons as affordances on it. */
.number-input {
  display: inline-flex;
  align-items: stretch;
  width: fit-content;
  border: var(--border-s) solid var(--input-border);
  border-radius: var(--radius-s);
  overflow: hidden;
  background-color: var(--input-background);
}

.number-input:focus-within {
  border-color: var(--input-focus);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus), var(--alpha-75));
}

/* tabular-nums so the field does not reflow as the value steps through digits
   of different widths — the whole control would shift on every click. */
.number-input input[type="number"] {
  border: none;
  background: transparent;
  text-align: center;
  width: 4rem;
  min-height: 0;
  padding: var(--space-m) var(--space-xs);
  font-variant-numeric: tabular-nums;
  box-shadow: none;
  -moz-appearance: textfield;
  appearance: textfield;
}

.number-input input[type="number"]:focus {
  outline: none;
  border: none;
  box-shadow: none;
}

.number-input input[type="number"]::-webkit-outer-spin-button,
.number-input input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
}

/* Transparent, not filled: a permanently shaded stepper reads as two buttons
   bolted either side of a field. The glyph sits at body size — at --font-xl it
   was the loudest thing in the control and set its height. */
.number-input-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  background-color: transparent;
  border: none;
  border-radius: 0;
  color: var(--text-primary);
  cursor: pointer;
  font-size: var(--font-s);
  line-height: 1;
  transition: background-color var(--duration-2xs) var(--ease-out);
  flex-shrink: 0;
}

.number-input-btn:hover {
  background-color: var(--background-faded);
}

.number-input-btn:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--input-focus), var(--alpha-75));
}

.number-input-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}


/* ------ 35. RADIO GROUP ------ */

.radio-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
}

.radio-group.is-horizontal {
  flex-direction: row;
  flex-wrap: wrap;
  gap: var(--space-m) var(--space-xl);
}

.radio-group-label {
  font-size: var(--font-s);
  font-weight: var(--font-weight-medium);
  color: var(--text-primary);
  margin-bottom: var(--space-xs);
  display: block;
}

.radio-group-hint {
  font-size: var(--font-xs);
  color: var(--text-faded);
  margin-top: var(--space-2xs);
  display: block;
}


/* ------ 36. DROP CAP ------ */

/*
 * Apply .drop-cap or data-drop-cap to a <p> element to style its first
 * letter as a large decorative initial. Use on long paragraphs only —
 * the text must wrap alongside the letter for at least 3 lines.
 */

.drop-cap::first-letter,
[data-drop-cap]::first-letter {
  float: left;
  font-family: var(--font-secondary);
  font-size: 3.2rem;
  font-weight: var(--font-weight-regular);
  line-height: 0.94;
  color: var(--text-primary);
  padding-right: var(--space-s);
  margin-top: 0.06em;
  text-transform: uppercase;
}

.drop-cap::after,
[data-drop-cap]::after {
  content: "";
  display: table;
  clear: both;
}


/* NOTE: .token-copy, .token-swatch, .asset-card, .dont-card moved to docs-site.css
 * — see CLAUDE.md §17 (Layer Discipline) */


/* -- 5. Assignment. The only layer that paints. ---------------------------
   Scoped to the component, so the attribute alone colours nothing — a stray
   data-color on a div does not repaint the page, and .badge[data-color="danger"]
   is untouched because --status-* is not in this list.

   TWO groups, and the split is forced by where each token is declared.

   A var() inside a custom property is substituted when THAT property computes,
   on the element it is declared on. --page-header-background is declared in
   :root as var(--background-secondary), so it resolved to a literal colour at
   :root and inherits down already-resolved. Re-pointing --background-secondary
   here cannot reach it. This is not a quirk of this experiment: it is why
   design-system.css carries ten component-level dark rules
   ([data-theme="dark"] .card, .dialog, .drawer, .toast …) rather than relying
   on the token block alone. Scoped theming has always had this limit.

   So: tokens a component declares on ITS OWN class resolve below this element
   and follow the semantics for free — .button declares --button-color as
   var(--text-primary) on .button, so buttons need nothing here, and
   .button:hover, which derives from --text-primary and --background-primary,
   re-colours itself in the right hue. Two rules gone from the old build. Its
   MAGNITUDE still needs a rule though — see the hover note below.

   Tokens declared in :root have to be restated. For the page header that is
   four lines. */

/* :not(:has(.page-header-media)) because design-system.css:7315-7320 re-points
   the same three tokens for a header carrying a picture, at the same (0,2,0),
   and this rule would win on source order — painting the title in --color-text
   over a 45% black scrim. A photo header already has a colour treatment; this
   one stands aside rather than fighting it. */
.page-header[data-color]:not(:has(.page-header-media)) {
  /* Semantics — reach anything that declares its own tokens below this point. */
  --background-primary: var(--color-background);
  --background-secondary: var(--color-background);
  --background-faded: var(--color-raised);
  --text-primary: var(--color-text);
  --text-secondary: var(--color-text-muted);
  --text-inverted: var(--color-background);
  --border-faded: var(--color-border-faded);
  /* Prose colours, easy to miss: a { color: var(--text-plain) } renders black
     inside a shade header without these, and --text-faded is near-invisible on
     any deep ground. */
  --text-plain: var(--color-text);
  --text-link: var(--color-text);
  --text-faded: var(--color-text-muted);

  /* Declared in :root, so they must be restated. No bottom rule: the component
     draws one by default (design-system.css:7217/7256) to separate a header
     whose ground is a 1.1:1 wash from the page behind it. A moded header does
     not have that problem — the colour is the separation — so the rule is
     switched off rather than re-coloured. --border-faded above carries the mode's
     own faded border, so anything INSIDE the header that draws one follows the
     mode instead of washing out. */
  --page-header-background: var(--color-background);
  --page-header-color: var(--color-text);
  --page-header-color-muted: var(--color-text-muted);
  --page-header-border: none;
}

/* Hover has to be restated after all, and on this one point the old build was
   right to carry a rule. .button:hover mixes 10% of --background-primary into
   --text-primary, calibrated for a neutral page ground; on a coloured one it
   lands at 1.18-1.32 against the system's own 1.31 benchmark
   (design-system.css:1496). 75% is the settled value: worst step 1.55, worst
   label 4.79, and it has to hold for BOTH mappings, because tint in dark is
   shade in light — a percentage tuned for one is wrong for the other. 55% was
   tried first and failed the label at 2.92 on yellow.

   The base step takes 95% — almost no movement — deliberately. The pure hue has no
   headroom: on green, purple and red NO percentage reaches 1.31 while keeping
   the label readable, so a weak hover is the accepted outcome rather than a
   solved problem. 95% is the most movement red can take before its label drops
   under 4.5; at the inherited 10% it was already failing at 4.18.

   --color-hover-mix is therefore a tuning number, the kind this pass otherwise
   deleted. It earns its place by being the single value both the real rule and
   the measurement probe read, which is what stops the two drifting apart. */
.page-header[data-color] .button:not([data-color]):not([data-variant]):hover {
  --button-color: color-mix(in srgb, var(--color-text) var(--color-hover-mix), var(--color-background));
}

/* The book cover, and the two further traps it exposes. Neither is visible
   from the page header, and both produced silently wrong colour.

   1. `color: inherit` ignores the tokens. .book-cover sets it
      (design-system.css:5596), and `inherit` takes the PARENT element's
      colour — it never consults a custom property on this element. Redefining
      --text-primary here does nothing for anything that inherits, so the title
      read body-black on every hue while the description and byline, which read
      var(--text-secondary) directly, followed the mode correctly. `color` has
      to be set outright.

   2. A dark-mode rule can re-point the component token to a DIFFERENT
      semantic. --book-cover-background is var(--background-primary) normally,
      but [data-theme="dark"] .book-cover re-points it to
      var(--background-secondary) (design-system.css:5735). Redefining only
      --background-primary left every book at #222 in dark — right hue in
      light, no hue at all in dark. The mode has to cover every semantic the
      component can point at, not just the one it points at today.

   Three rules, then, for adopting the mode on a component: restate whatever it
   keeps in :root, set `color` outright wherever it inherits, and read its
   dark-mode block for tokens it re-points there. */
.book-cover[data-color] {
  color: var(--color-text);
  --background-primary: var(--color-background);
  --background-secondary: var(--color-background);
  --background-faded: var(--color-raised);
  --text-primary: var(--color-text);
  --text-secondary: var(--color-text-muted);
  --text-inverted: var(--color-background);
  --border-faded: var(--color-border-faded);
  --text-plain: var(--color-text);
  --text-link: var(--color-text);
  --text-faded: var(--color-text-muted);
  --book-cover-background-hover: var(--color-raised);
}

/* --text-link is not optional on any of these three. Every one of them is an
   <a>, and a:not(.button):hover sets color: var(--text-link)
   (design-system.css:1820), which resolves to --text-accent: --blue in light,
   --yellow in dark. Measured on a purple contents row before the fix: rest
   #35005D, hover #1A54D6 — the row turned blue under the cursor, and yellow in
   dark. The book cover escaped it only by accident, because
   a.book-cover:hover sets color: var(--text-primary) and ties the generic rule
   on specificity, winning on source order. Fixing the token rather than the
   hover rule covers all four consumers of --text-link, including the underline
   colour and any link nested inside the component.

   --text-accent is deliberately NOT redefined. It also drives a checked
   .form-toggle's background (design-system.css:2974), which is a control state
   rather than surface text — the same reason --status-* is left alone. The cost
   is that a .text-accent utility inside a moded card stays blue, which is an
   explicit author opt-in rather than something the mode applies for you.

   The contents list, and the container question it raises. Its own tokens all
   sit on its own classes, so nothing has to be restated — but it has
   `color: inherit` (design-system.css:5817), so `color` must be set outright,
   the same trap the book cover sprang.

   The list can take the mode at either level, and they are not equivalent:
   --book-contents-border is declared on .book-contents, the CONTAINER, so a
   per-item mode leaves the outer frame on the page's --border-faded while the
   rows inside change. Moding the container brings the frame with it. Both are
   below so the difference is visible rather than argued. */
.book-contents[data-color],
.book-contents-item[data-color] {
  color: var(--color-text);
  --background-primary: var(--color-background);
  --background-secondary: var(--color-background);
  --background-faded: var(--color-raised);
  --text-primary: var(--color-text);
  --text-secondary: var(--color-text-muted);
  --text-inverted: var(--color-background);
  --border-faded: var(--color-border-faded);
  --text-plain: var(--color-text);
  --text-link: var(--color-text);
  --text-faded: var(--color-text-muted);
}

/* The row paints its own ground only when the mode is on the row. On the
   container the ground comes from .book-contents itself, so the rows stay
   transparent and the divider between them is what carries the hue. */
.book-contents-item[data-color] { background: var(--color-background); }
.book-contents[data-color] { background: var(--color-background); }

/* The row-moded ground has to yield on hover. .book-contents-item[data-color]
   and .book-contents-item:hover are both (0,2,0), and this stylesheet loads
   after design-system.css, so the flat ground was winning and the row had a
   1.00 hover shift — no feedback under the cursor at all. Restated one level
   up so hover outranks it. The container-moded list never had this: its rows
   are transparent at rest, so the hover wash paints over the container ground
   normally. */
.book-contents-item[data-color]:hover {
  background: var(--book-contents-item-background-hover);
}

/* Focus rings are fixed --status-info, which is not redefined here on purpose —
   it is a status token. On a blue shade header in light that is --blue-dark on
   --blue-dark: a 1.00:1 ring. Buttons already re-point their own outline to
   --button-text-color, so they are excluded rather than overridden. */
.page-header[data-color] :focus-visible:not(.button) {
  outline-color: var(--color-text);
}

/* ------ 37. BOOK ------
 * The navigation surfaces of a documentation space, modelled as a book:
 *
 *   .book-shelf          L0 — a grid of books to choose from
 *   .book-cover          L0 — one closed book
 *   .book-contents       L1 — the table of contents of an opened book
 *   .book-contents-item  L1 — one line in that contents list
 *
 * The row is a contents *item*, not a page. In a book the thing you click
 * in the contents is an entry; the thing you land on is the page. Moving
 * sideways between pages is .page-nav (section 38); moving back up is
 * .breadcrumb (section 18).
 *
 * TWO KINDS OF SLOT, named on two different principles:
 *
 *   leading / trailing  — position. They frame the surface and hold an icon.
 *                         Generic on purpose: another product's trailing icon
 *                         might be a chevron, an arrow, or a download glyph.
 *   byline / meta       — content type. They carry text about the item, so
 *                         naming them by position would say nothing useful.
 *
 * On a cover, leading/trailing are the header's two ends; the footer's two
 * ends are byline and meta, because what belongs there is text rather than
 * icons. On a row there is only one line, so leading and trailing are
 * unambiguous and meta moves inside the body, under the description.
 *
 * Neither slot may hold an interactive control. The surface is already a
 * link, so a nested button is invalid HTML and unreachable in several
 * screen-reader modes. `trailing` holds an icon that *hints* at what
 * clicking does — it is not an action control.
 *
 *   <div class="book-shelf">
 *     <a href="/brand/" class="book-cover">
 *       <header class="book-cover-header">
 *         <div class="book-cover-leading">…</div>    ← optional, varies
 *         <div class="book-cover-trailing">…</div>   ← open-full
 *       </header>
 *       <div class="book-cover-body">
 *         <h3 class="book-cover-title">Brand Book</h3>
 *         <p class="book-cover-description">Brand identity, values…</p>
 *       </div>
 *       <footer class="book-cover-footer">
 *         <span class="book-cover-byline"><em>by</em> Studio</span>
 *         <span class="book-cover-meta">…</span>     ← optional metadata
 *       </footer>
 *     </a>
 *   </div>
 *
 * ENGINE vs INSTANCE: the component guarantees the slots. Which icon goes in
 * `trailing`, and whether it stays the same across a shelf, is an instance
 * decision. By Default keeps it constant, because constancy is what signals
 * "this opens" — but that is a usage rule, not a property of the component.
 *
 * Re-skin by re-pointing the component tokens. Never override the rules.
 */

/* -- Book shelf (L0 index) --
 * Owns its grid rather than composing .grid.cols-2.gap-xl: a component that
 * depends on the consumer applying the right utility combo is not portable.
 * It carries its own breakpoint for the same reason — .grid's collapse would
 * not follow it into another codebase.
 */
.book-shelf {
  /* -- Component tokens (re-point these to re-skin; never override the rules) -- */
  --book-shelf-columns: 2;
  --book-shelf-gap: var(--space-xl);

  display: grid;
  grid-template-columns: repeat(var(--book-shelf-columns), minmax(0, 1fr));
  gap: var(--book-shelf-gap);
}

.book-shelf[data-cols="3"] {
  --book-shelf-columns: 3;
}

/* Both selectors are listed because a media query adds no specificity: the
   (0,2,0) attribute rule above would otherwise keep three columns on mobile. */
@media (max-width: 768px) {
  .book-shelf,
  .book-shelf[data-cols="3"] {
    --book-shelf-columns: 1;
  }
}

/* Group headings above a shelf or a contents list. Same treatment as
   .eyebrow, declared rather than composed, so the heading stays restylable
   independently of the utility. */
.book-shelf-title,
.book-contents-title {
  font-size: var(--font-xs);
  line-height: var(--line-height-m);
  font-weight: var(--font-weight-regular);
  letter-spacing: var(--letter-spacing-l);
  text-transform: uppercase;
}


/* -- Book cover (L0 tile) -- */
.book-cover {
  /* -- Component tokens (re-point these to re-skin; never override the rules) -- */
  --book-cover-min-height: 250px;
  --book-cover-title-size: var(--font-5xl);
  --book-cover-background: var(--background-primary);
  --book-cover-background-hover: var(--background-faded);
  --book-cover-border: var(--border-s) solid var(--border-faded);
  --book-cover-bar-padding: var(--space-m) var(--space-l);
  --book-cover-body-padding: var(--space-2xl);
  --book-cover-lift: -2px;
  --book-cover-shadow-hover: 0 7px 1px -3px var(--black-alpha-10);

  display: flex;
  flex-direction: column;
  min-height: var(--book-cover-min-height);
  background: var(--book-cover-background);
  border: var(--book-cover-border);
  color: inherit;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  text-wrap: pretty;
  transition:
    border var(--duration-xs) var(--ease-out),
    background-color var(--duration-xs) var(--ease-out),
    box-shadow var(--duration-xs) var(--ease-out),
    transform var(--duration-xs) var(--ease-out);
}

/* Size re-points height, title and inset together — a short cover with a
   full-size title reads as a mistake rather than a variant. */
.book-cover[data-size="small"] {
  --book-cover-min-height: 170px;
  --book-cover-title-size: var(--font-3xl);
  --book-cover-body-padding: var(--space-l);
}

.book-cover[data-size="large"] {
  --book-cover-min-height: 340px;
  --book-cover-title-size: var(--font-6xl);
  --book-cover-body-padding: var(--space-3xl);
}

/* The heading scale steps by restating font-size on the element, not by
   re-pointing a token — so a class that sets its own size has to carry the
   step itself, or it stays at desktop size on every phone. Every step drops
   two rungs, matching h1-h6.

   This block must stay BELOW the size variants: the bare .book-cover rule
   here is (0,1,0) against their (0,2,0), so the variants' own steps only win
   on source order. Move it above them and both sizes silently revert to
   desktop type. */
@media (max-width: 959px) {
  .book-cover {
    --book-cover-title-size: var(--font-3xl);
  }

  .book-cover[data-size="small"] {
    --book-cover-title-size: var(--font-2xl);
  }

  .book-cover[data-size="large"] {
    --book-cover-title-size: var(--font-4xl);
  }
}

/* Header and footer are the same shape: a full-width strip carrying a start
   slot and an end slot. Empty slots are simply absent; the end slot's auto
   margin holds its side either way, so no spacer elements are needed. */
.book-cover-header,
.book-cover-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-s);
  flex-shrink: 0;
  padding: var(--book-cover-bar-padding);
}

.book-cover-leading,
.book-cover-trailing,
.book-cover-meta {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  flex-shrink: 0;
}

/* End-of-strip slots hold their side whether or not a start slot sits
   beside them. Action lives in the header, meta in the footer, so the two
   auto margins never compete inside one strip. */
.book-cover-trailing,
.book-cover-meta {
  margin-inline-start: auto;
}

.book-cover-trailing .svg-icn {
  opacity: 0;
  transition: opacity var(--duration-xs) var(--ease-out);
}

/* Focus reveals the affordance too — a keyboard user tabbing a shelf should
   see what a mouse user sees. */
a.book-cover:hover .book-cover-trailing .svg-icn,
a.book-cover:focus-visible .book-cover-trailing .svg-icn {
  opacity: 1;
}

.book-cover-byline,
.book-cover-meta {
  font-size: var(--font-s);
  color: var(--text-secondary);
}

/* A byline is the general case of "by X" — an author, a maintainer, whoever
   last touched it. Naming it for the concept rather than for `author` keeps
   it usable where the thing has no author. */
.book-cover-byline em,
.book-contents-item-byline em {
  font-style: italic;
}

.book-cover-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  padding: var(--book-cover-body-padding);
}

.book-cover-title {
  margin-top: 0;
  margin-bottom: var(--space-s);
  font-size: var(--book-cover-title-size);
  line-height: var(--title-leading);
  font-weight: var(--font-weight-regular);
}

.book-cover-description {
  color: var(--text-secondary);
  font-size: var(--body-size);
  margin-bottom: 0;
}

a.book-cover:hover {
  color: var(--text-primary);
  background: var(--book-cover-background-hover);
  transform: translateY(var(--book-cover-lift));
  box-shadow: var(--book-cover-shadow-hover);
}

a.book-cover:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus), var(--alpha-75));
}

/* -- Book cover dark mode -- */
[data-theme="dark"] .book-cover {
  --book-cover-background: var(--background-secondary);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .book-cover {
    --book-cover-background: var(--background-secondary);
  }
}

/* Re-point the travel token rather than blanket-disabling the transition,
   matching how .dialog and .drawer handle reduced motion. */
@media (prefers-reduced-motion: reduce) {
  .book-cover {
    --book-cover-lift: 0;
  }
}


/* -- Book contents (L1 index) --
 * Entries stack vertically as wide horizontal rows, rather than the tall
 * centred covers used on the shelf. Same slot vocabulary as the cover:
 * identity leads, action trails.
 *
 *   <div class="book-contents">
 *     <a class="book-contents-item">
 *       <div class="book-contents-item-leading">…</div>   ← optional, varies
 *       <div class="book-contents-item-body">
 *         <h3 class="book-contents-item-title">Colour</h3>
 *         <p class="book-contents-item-description">Palette, tokens…</p>
 *       </div>
 *       <span class="book-contents-item-meta">…</span>     ← optional status
 *       <div class="book-contents-item-trailing">…</div>
 *     </a>
 *   </div>
 */
.book-contents {
  /* -- Component tokens (re-point these to re-skin; never override the rules) -- */
  --book-contents-border: var(--border-s) solid var(--border-faded);

  display: flex;
  flex-direction: column;
  border: var(--book-contents-border);
}

.book-contents-item {
  /* -- Component tokens (re-point these to re-skin; never override the rules) -- */
  --book-contents-item-padding: var(--space-xl);
  --book-contents-item-gap: var(--space-xl);
  --book-contents-item-title-size: var(--font-4xl);
  --book-contents-item-background-hover: var(--background-faded);

  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--book-contents-item-gap);
  padding: var(--book-contents-item-padding);
  border-bottom: var(--border-s) solid var(--border-faded);
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--duration-xs) var(--ease-out);
}

.book-contents-item:hover {
  background: var(--book-contents-item-background-hover);
}

/* The transparent outline is not dead code: forced-colors mode (Windows High
   Contrast) suppresses box-shadow, and an outline drawn transparent here is
   repainted in the user's forced palette there — without it the ring
   vanishes entirely for exactly the users who most need it. */
.book-contents-item:focus-visible {
  outline: 2px solid transparent;
  outline-offset: -1px;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus), var(--alpha-75));
}

/* Content grows so identity stays left and meta/action stay right, whatever
   combination of slots is present. flex-basis stays auto so text wraps
   exactly as it did before the slots existed. */
.book-contents-item-body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-2xs);
  min-width: 0;
}

/* A row is one horizontal line, so it has no header or footer strip to hang
   slots off — identity leads and action trails. That is why the cover's
   separate author and meta slots collapse into meta here: on a row they would
   be two arbitrary adjacent boxes rather than two ends of a strip. */
.book-contents-item-leading,
.book-contents-item-trailing {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  flex-shrink: 0;
}

/* Meta sits INSIDE the content column, under the description, rather than as
   a fourth item on the row. On the cover, meta lives in the footer strip and
   so gets a line of its own; a row has no strip, so trailing meta would be
   the one slot competing for horizontal space with the title. Identity and
   action refuse to shrink, so everything a trailing meta took would come out
   of the title — worst exactly where space is tightest. Under the
   description it wraps instead of squeezing, and reads as belonging to the
   item rather than floating at the far edge. */
.book-contents-item-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-m);
  margin-top: var(--space-2xs);
  font-size: var(--font-s);
  color: var(--text-secondary);
  padding-top: var(--space-s);
}

/* On a row the byline lives inside meta rather than in a slot of its own —
   a row has no footer strip with two ends to separate them. */
.book-contents-item-byline {
  white-space: nowrap;
}

.book-contents-item-title {
  margin: 0;
  font-size: var(--book-contents-item-title-size);
  line-height: var(--title-leading);
  font-weight: var(--title-weight);
}

/* Same reason as the cover title: a class that sets its own size has to
   carry the heading scale's breakpoint step itself. */
@media (max-width: 959px) {
  .book-contents-item {
    --book-contents-item-title-size: var(--font-2xl);
  }
}

.book-contents-item-description {
  margin: 0;
  color: var(--text-secondary);
  font-size: var(--body-size);
}

.book-contents-item-trailing .svg-icn {
  opacity: 0;
  flex-shrink: 0;
  transition: opacity var(--duration-xs) var(--ease-out);
}

a.book-contents-item:hover .book-contents-item-trailing .svg-icn,
a.book-contents-item:focus-visible .book-contents-item-trailing .svg-icn {
  opacity: 1;
}

/* -- Contents row as a disclosure --
 * The same row, opening in place instead of navigating: a <details> wearing
 * .book-contents-item, for lists whose entries are short enough to live
 * inline — a glossary definition, a spec. The summary is the row; the panel
 * is what opening reveals.
 *
 *   <details class="book-contents-item" name="…">   ← same name = single-open
 *     <summary>
 *       <span class="book-contents-item-title">Channel</span>
 *       <div class="book-contents-item-trailing">…chevron…</div>
 *     </summary>
 *     <div class="book-contents-item-panel">
 *       <p class="book-contents-item-description">…</p>
 *       <span class="book-contents-item-meta">…</span>
 *     </div>
 *   </details>
 *
 * The row layout moves from the element to the summary: on the anchor row the
 * element IS the single line, but here the element also contains the open
 * panel, and a flex row on it would sit summary and panel side by side.
 * Same reason the hover moves — a highlight washing over an open panel would
 * read as the whole thing being one button when only the summary is.
 *
 * The trailing icon is visible at rest, unlike the anchor row's
 * hover-revealed arrow: a constant chevron says "opens in place" where the
 * appearing arrow says "navigates". It rotates rather than swaps.
 *
 * No ARIA is added anywhere here on purpose: <details>/<summary> announce
 * the disclosure role and expanded state natively, and duplicating state
 * with aria-expanded double-announces in some screen readers.
 */
details.book-contents-item {
  display: block;
  padding: 0;
  cursor: default;
}

details.book-contents-item:hover {
  background: transparent;
}

/* Restates the row that section 13's bare-summary rule would otherwise
   shrink: that rule is written for the small monospace disclosure and sets
   its own font, padding and gap. */
details.book-contents-item > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--book-contents-item-gap);
  padding: var(--book-contents-item-padding);
  font-size: inherit;
  color: inherit;
  transition: background-color var(--duration-xs) var(--ease-out);
}

/* Section 13 draws its own left-side chevron on every summary; this row
   carries the real icon in the trailing slot, so the pseudo goes. */
details.book-contents-item > summary::before {
  content: none;
}

details.book-contents-item > summary:hover {
  background: var(--book-contents-item-background-hover);
  color: inherit;
}

/* The summary is the focusable element — a :focus-visible on the details
   itself never matches — and it takes the family ring, not section 13's
   inset outline, so the accordion matches its link siblings. The transparent
   outline is the forced-colors fallback — see the family focus rule above. */
details.book-contents-item > summary:focus-visible {
  outline: 2px solid transparent;
  outline-offset: -1px;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus), var(--alpha-75));
}

details.book-contents-item .book-contents-item-trailing .svg-icn {
  opacity: 1;
  transition: transform var(--duration-xs) var(--ease-out);
}

details.book-contents-item[open] > summary .book-contents-item-trailing .svg-icn {
  transform: rotate(180deg);
}

/* --space-m, not the body slot's --space-2xs: the body pairs a title with
   one line, but a panel holds paragraphs — at 2px they read as one blob. */
.book-contents-item-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-m);
  padding: 0 var(--book-contents-item-padding) var(--book-contents-item-padding);
}

/* Rotation is travel; the state change itself is the feedback. */
@media (prefers-reduced-motion: reduce) {
  details.book-contents-item .book-contents-item-trailing .svg-icn {
    transition: none;
  }
}


/* ------ 38. PAGE NAVIGATION (PREV / NEXT) ------
 * Prev/next across any ordered sequence of pages. It knows nothing about
 * what the sequence is — pair it with .book-contents (section 37) for a
 * documentation space, or with anything else that has an order.
 *
 * Both directions render from identical markup and are flipped by
 * data-direction, so this is one component with a variant rather than two
 * that share class names.
 *
 *   <nav class="page-nav" aria-label="Page navigation">
 *     <div class="page-nav-inner">
 *       <a class="page-nav-link" data-direction="prev" rel="prev">
 *         <div class="svg-icn page-nav-arrow">…</div>
 *         <span class="page-nav-text">
 *           <span class="page-nav-label">Previous</span>
 *           <span class="page-nav-section">Brand Book</span>  ← only when crossing
 *           <span class="page-nav-title">4.4 Iconography</span>
 *         </span>
 *       </a>
 *       <a class="page-nav-link" data-direction="next" rel="next">…</a>
 *     </div>
 *   </nav>
 *
 * The section line appears only when the neighbour sits in a different
 * section — it marks leaving one book for the next, so showing it on every
 * page would turn a signpost into chrome.
 *
 * With no neighbour in a direction, nothing renders for that side.
 * grid-column pins each link to its own half, so the survivor keeps its
 * side without a placeholder element to hide.
 *
 * Re-skin by re-pointing the component tokens. Never override the rules.
 */

.page-nav {
  /* -- Component tokens (re-point these to re-skin; never override the rules) -- */
  --page-nav-background: var(--background-primary);
  --page-nav-background-hover: var(--background-faded);
  --page-nav-border: var(--border-s) solid var(--border-faded);
  --page-nav-link-padding: var(--space-xl) var(--space-2xl);
  --page-nav-link-gap: var(--space-m);
  --page-nav-title-size: var(--font-5xl);
  --page-nav-arrow-color: var(--text-secondary);
  --page-nav-arrow-color-hover: var(--text-link);

  border-top: var(--page-nav-border);
}

/* The heading scale steps by restating font-size on the elements rather than
   re-pointing a token, so a component replacing a heading has to carry the
   step itself. Mirrors the h3 step at the same breakpoint. */
@media (max-width: 959px) {
  .page-nav {
    --page-nav-title-size: var(--font-3xl);
  }
}

.page-nav-inner {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0;
}

.page-nav-link {
  display: flex;
  align-items: center;
  gap: var(--page-nav-link-gap);
  padding: var(--page-nav-link-padding);
  border: none;
  background: var(--page-nav-background);
  text-decoration: none;
  color: var(--text-primary);
  transition: background-color var(--duration-xs) var(--ease-out);
}

.page-nav-link[data-direction="prev"] {
  grid-column: 1;
}

/* A reversed row packs to the right and puts the arrow outermost, which is
   what lets both directions share one markup shape. */
.page-nav-link[data-direction="next"] {
  grid-column: 2;
  flex-direction: row-reverse;
  text-align: right;
}

.page-nav-link:hover {
  background-color: var(--page-nav-background-hover);
  color: var(--text-primary);
}

.page-nav-text {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  min-width: 0;
}

.page-nav-label {
  font-size: var(--font-xs);
  font-family: var(--font-primary);
  color: var(--text-secondary);
  font-weight: var(--font-weight-semi-bold);
  text-transform: uppercase;
  letter-spacing: var(--letter-spacing-l);
}

.page-nav-section {
  font-size: var(--font-2xs);
  color: var(--text-faded);
  text-transform: uppercase;
  letter-spacing: var(--letter-spacing-m);
  font-weight: var(--font-weight-medium);
}

/* Sized here rather than inherited from a heading element: the title labels
   a link, it does not open a section, so it renders as a span. */
.page-nav-title {
  margin: 0;
  font-family: var(--font-primary);
  font-size: var(--page-nav-title-size);
  font-weight: var(--font-weight-regular);
  letter-spacing: var(--title-tracking);
  line-height: var(--title-leading);
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
}

.page-nav-arrow {
  flex-shrink: 0;
  color: var(--page-nav-arrow-color);
  transition: color var(--duration-xs) var(--ease-out);
}

.page-nav-link:hover .page-nav-arrow {
  color: var(--page-nav-arrow-color-hover);
}

/* Single column: both directions collapse into column 1 and stack as rows.
   Without resetting grid-column, next would address an implicit second
   column and the collapse would not take. The breakpoint travels with the
   component rather than living in a shared responsive section. */
@media (max-width: 768px) {
  .page-nav-inner {
    grid-template-columns: 1fr;
  }

  .page-nav-link[data-direction="prev"],
  .page-nav-link[data-direction="next"] {
    grid-column: 1;
  }

  .page-nav-link[data-direction="prev"] {
    border-bottom: var(--page-nav-border);
  }

  /* Keep the base row-reverse: the DOM order is [arrow, text], so a plain
     `row` would put the right-chevron on the left. */
  .page-nav-link[data-direction="next"] {
    text-align: left;
  }
}


/* ------ 39. NAV ------
 * A flat row (or column) of navigation links.
 *
 * Deliberately not the docs sidebar's tree nav (.sidebar-nav-*, in
 * docs-site.css) — that one has collapsible sections, sublists and an active
 * trail, and is a different component that happens to also be navigation.
 * This is the link set that appears in a site header, inside a nav drawer,
 * and in a footer.
 *
 * Orientation is the only axis. Everything about width, position and
 * background belongs to whatever contains it, so the same markup works in
 * all three places without knowing which one it is in.
 */

:root {
  --nav-gap: var(--space-xs);
  --nav-link-padding: var(--space-s) var(--space-m);
  --nav-link-color: var(--text-primary);
  --nav-link-color-hover: var(--text-plain);
  --nav-link-color-current: var(--text-plain);
}

.nav {
  display: flex;
  align-items: center;
  gap: var(--nav-gap);
}

.nav[data-orientation="vertical"] {
  flex-direction: column;
  align-items: stretch;
}

.nav-link {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  padding: var(--nav-link-padding);
  color: var(--nav-link-color);
  font-family: var(--font-primary);
  font-size: var(--font-s);
  line-height: 1;
  text-decoration: none;
  transition: color var(--duration-2xs) var(--ease-out);
}

.nav-link:hover {
  color: var(--nav-link-color-hover);
}

/* Current page is styled from the ARIA attribute rather than a class, so the
   accessible state and the visible state cannot drift apart — there is only
   one thing to set. */
.nav-link[aria-current="page"] {
  color: var(--nav-link-color-current);
  font-weight: var(--font-weight-semi-bold);
}

/* Horizontal only: a header has no room to wrap, and a wrapped link there
   changes the bar's height. A vertical nav has the width to wrap freely. */
.nav:not([data-orientation="vertical"]) .nav-link {
  white-space: nowrap;
}


/* ------ 39B. STICKY STACK / SCROLL OFFSET ------
 * Anything docked to the top of the viewport hides whatever an in-page anchor
 * scrolls to. A link in a docked bar that jumps to a heading would otherwise
 * land that heading underneath the bar that contains the link.
 *
 * CSS cannot add up how much is currently stuck there, so the page declares
 * it once: --scroll-offset is the total of every docked bar plus whatever
 * breathing room you want beneath them. The default of 0 means a product that
 * has no sticky chrome inherits no offset and this costs nothing.
 *
 * Raise --site-header-height or --bar-height and the landing position follows,
 * because the shell's --scroll-offset is written in terms of them.
 *
 * The stacking order lives here too, for the same reason: it is a relationship
 * between components, not a property of any one of them. The bar's own --bar-z
 * points at this scale rather than carrying a number. A bar sits under the
 * site header, which outranks everything. A page stacking two bars re-points
 * --bar-z on the upper one locally — there is no second global slot, because
 * which bar wins is that page's decision, not the system's.
 */

:root {
  --scroll-offset: 0px;

  --z-bar: 40;
  --z-app-nav: 90; /* app chrome: above bars, below the site header */
  --z-site-header: 100;
}

/* main[id] matters as much as the headings: the skip link targets it, and
   without this the thing you skipped to lands underneath the header. */
h1[id],
h2[id],
h3[id],
h4[id],
h5[id],
h6[id],
main[id],
section[id] {
  scroll-margin-top: var(--scroll-offset);
}


/* ------ 40. SITE HEADER ------
 * The bar at the top of a page: identity on one side, controls on the other,
 * an optional .nav (section 39) between them.
 *
 * The outer element is full-bleed and owns background, border and sticky;
 * .site-header-inner owns max-width and centring. That split is what lets the
 * border run edge to edge while the content lines up with the page column —
 * the same division .bar (section 41) uses.
 *
 * Slots are optional, and the end slot is pushed over with an auto margin
 * rather than justify-content: space-between. space-between resolves to
 * flex-start with a single child, so a header carrying only an end slot would
 * render on the left. With the auto margin every combination lands correctly
 * and no empty slot has to be emitted just to hold the shape.
 *
 * Placement is the consumer's job — nothing here knows about a page grid.
 */

:root {
  --site-header-height: 80px;
  --site-header-logo-height: 50px;
  /* Full-bleed by default: a header spans its container, and the identity and
     actions sit at the outer edges. Set this to a length to line the contents
     up with a content column instead — worth doing when the page below has a
     narrow measure, but it is a choice, not the norm. */
  --site-header-max-width: none;
  --site-header-padding-inline: var(--space-l);
  --site-header-gap: var(--space-m);
  --site-header-background: var(--background-top-nav);
  --site-header-text: var(--text-top-nav);
  --site-header-border: var(--border-s) solid var(--border-faded);
  --site-header-z: var(--z-site-header);
}

.site-header {
  position: sticky;
  top: 0;
  z-index: var(--site-header-z);
  height: var(--site-header-height);
  background: var(--site-header-background);
  color: var(--site-header-text);
  border-bottom: var(--site-header-border);
}

.site-header[data-sticky="false"] {
  position: static;
}

.site-header-inner {
  display: flex;
  align-items: center;
  gap: var(--site-header-gap);
  height: 100%;
  max-width: var(--site-header-max-width);
  margin-inline: auto;
  padding-inline: var(--site-header-padding-inline);
}

/* True centring needs equal side tracks. Flex with an auto margin cannot do
   it — the centre would drift with whatever the sides weigh.

   Named data-layout, not data-align: `[data-align="center"]` is already a
   foundation utility (section 6) meaning "centre this box in its parent", and
   it sets justify-self, which Chromium now honours in block layout — so
   reusing the name made the header itself shrink to fit. Different axis,
   different attribute. */
.site-header[data-layout="center"] .site-header-inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
}

.site-header[data-layout="center"] .site-header-end {
  justify-self: end;
}

.site-header-start,
.site-header-end {
  display: flex;
  align-items: stretch;
  height: 100%;
}

.site-header-start {
  min-width: 0;
}

/* The auto margin, not space-between — see the section note above. */
.site-header-end {
  margin-inline-start: auto;
}

.site-header-logo {
  display: flex;
  align-items: center;
  color: inherit;
  text-decoration: none;
}

.svg-logo.site-header-logo-image {
  display: block;
  height: var(--site-header-logo-height);
  width: auto;
  color: var(--text-plain);
}

.svg-logo.site-header-logo-image svg {
  height: 100%;
  width: auto;
  object-fit: contain;
}

/* A trigger in the bar takes its vertical rhythm from the bar's height, not
   from its own padding — the same re-point .bar makes in §41. */
.site-header .dropdown-trigger {
  --dropdown-trigger-padding-y: 0;
  --dropdown-trigger-padding-x: var(--space-m);
}

/* -- Header action: a Button role class --
 * Kept here rather than in the Button role-class block because it belongs to
 * this component, the same reasoning that puts `.toast .button.close-btn` in
 * the Toast section.
 *
 * Two rules that are not optional. It must stay written as
 * `.button.header-action` — a bare `.header-action` is (0,1,0) and loses to
 * the base, which also matches as `button.button` (0,1,1). And the :hover
 * rule below is required, not decorative: `.button:hover` and
 * `.button.header-action` both re-point --button-color, and the role class is
 * later in the file, so without an explicit hover it would win *during* hover
 * and the button would never change colour.
 */
.button.header-action {
  --button-bg: transparent;
  --button-border: transparent;
  --button-color: var(--site-header-text);
  --button-text-color: var(--button-color);
  --button-padding-y: 0;
  --button-padding-x: var(--space-m);
  --button-text-size: var(--font-s);
  align-self: stretch;
  flex-shrink: 0;
  min-block-size: var(--target-min);
  border-radius: 0;
}

.button.header-action:hover {
  --button-color: var(--text-plain);
  background-color: var(--background-faded);
}

/* -- Skip link --
 * The first focusable thing on the page. Hidden by clipping rather than
 * display:none or visibility:hidden — both of those remove it from the focus
 * order, which is the one thing it exists to be in.
 *
 * Collapsing the box is load-bearing, not belt-and-braces. clip-path hides
 * the painting but leaves the element its full layout box, and that box still
 * receives pointer events — a clipped-only skip link sits invisibly across
 * the header and swallows clicks meant for the controls beneath it.
 *
 * The padding has to live in the focused state for the same reason: under
 * border-box, padding is a floor that width:1px cannot get under, so a
 * padded hidden link is still a 26x18 click target.
 */
.skip-link {
  position: absolute;
  inset-block-start: var(--space-m);
  inset-inline-start: var(--space-m);
  z-index: calc(var(--site-header-z) + 1);
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  background: var(--background-primary);
  color: var(--text-primary);
  border: var(--site-header-border);
  border-radius: var(--radius-s);
  font-family: var(--font-primary);
  font-size: var(--font-s);
  text-decoration: none;
  white-space: nowrap;
  pointer-events: none;
  clip-path: inset(50%);
}

.skip-link:focus-visible {
  width: auto;
  height: auto;
  padding: var(--space-s) var(--space-m);
  overflow: visible;
  pointer-events: auto;
  clip-path: none;
}

@media (max-width: 768px) {
  /* Flush to the viewport edge, so the first and last actions read as
     full-height tap targets rather than floating inside a gutter. */
  .site-header-inner {
    padding-inline: 0;
  }

  /* Labels collapse to icons. The accessible name lives on the button's
     aria-label, so nothing is lost to a screen reader. */
  .site-header .header-action-label {
    display: none;
  }

  /* Opt in, not automatic. A product with a short header nav and no drawer
     would otherwise lose its entire navigation here, with a class override as
     the only way back — the incomplete-token-coverage case §17 Rule 3 names.
     Set data-nav-collapse="drawer" once you have somewhere for it to go. */
  .site-header[data-nav-collapse="drawer"] .nav {
    display: none;
  }

  :root {
    --site-header-logo-height: 45px;
  }
}


/* ------ 41. BAR ------
 * A horizontal strip at the top of a content region: breadcrumbs, a title,
 * control groups, a trailing action cluster, a close.
 *
 * ONE component, not two. This was `.sticky-bar` and `.toolbar` until
 * 2026-08-14, and they were the same component discovered twice — same box,
 * same regions, same page-column variant. They had already converged from both
 * ends: the sticky bar documented tabs, toggles and segmented controls inside
 * it, and the toolbar grew a sticky variant. What actually differed was
 * configuration, and configuration is now what it is: `data-density` for
 * height, `data-width` for the column, `data-sticky` for docking.
 *
 * Named for its form, not its contents — like .card, .drawer and .dialog. Any
 * content-derived name is wrong somewhere: a doc page's bar holds a breadcrumb,
 * not tools. **The class names the box; the ARIA role names the contents.**
 * Give it role="toolbar" when it holds a set of controls; wrap a <nav> inside
 * it when it holds a breadcrumb. Never role="toolbar" on the latter.
 *
 * Not a Site Header (section 40). A header carries site identity and global
 * navigation; a bar carries what belongs to the page you are on, and sits
 * *below* the header rather than beside it. They share a shape — an outer
 * element owning background, border and sticky, an inner container owning the
 * flex row, an end slot pushed over with an auto margin rather than
 * space-between (which resolves to flex-start with a single child).
 *
 * -- The bar is always ONE row --
 * It neither wraps nor lets its items scroll out of reach. Items that do not
 * fit move into the overflow menu (assets/js/bar.js), keyed on data-priority.
 * This replaced `flex-wrap: wrap`, which was previously justified here as the
 * WCAG 1.4.10 answer. A disclosure satisfies 1.4.10 just as well — reflow asks
 * for content to be *available*, not simultaneously visible — and one row is
 * what makes `align-self: stretch` on the close safe at any density. A wrapping
 * bar turned a stretched close into a tall slab down the trailing edge, which
 * is the bug that started the merge.
 *
 * A single item too wide for the bar is not a collapse case — there is nothing
 * to move — so it handles its own width: the breadcrumb truncates with an
 * ellipsis (docs-site.css), fixed-width inputs stay fixed. .bar-content does
 * not clip — it was a hidden-scrollbar scroll container once, and the clip
 * silently swallowed every dropdown panel opened from inside it.
 *
 * Controls are composed, never re-invented. Everything that goes in a bar is
 * already a component — button, dropdown, segmented control, toggle, number
 * input, divider. The bar adapts them by re-pointing their own custom
 * properties, so nothing here needs a bar-specific class.
 */

:root {
  --bar-background: var(--background-primary);
  --bar-border: var(--border-s) solid var(--border-faded);
  --bar-gap: var(--space-l);
  /* Between items in the actions cluster. The close opts out below: the
     overflow cell and the close share a divider, and a gap there would open
     the divider off its neighbour. */
  --bar-group-gap: var(--space-xs);
  --bar-padding-x: var(--space-xl);
  --bar-padding-y: var(--space-s);
  /* The fixed height of data-density="regular" only. Compact builds its height
     from the control plus --bar-padding-y instead. Both feed --scroll-offset,
     so raising either pushes anchored targets down by the same amount — a link
     inside the bar cannot scroll its target underneath the bar itself. */
  --bar-height: 70px;
  /* Every control in the bar resolves to this. 36px is 8px of air around a
     19px line box at the 16px body size, and clears the 24px WCAG 2.5.8 AA
     target with room. Page forms keep their own 44px — dense chrome and a form
     on a phone are not the same problem. */
  --bar-control-height: 36px;
  /* Wide enough for four digits and a unit without the field resizing the bar. */
  --bar-number-input-width: 4.5rem;
  /* Ceiling for a menu opened from the bar on a narrow screen. */
  --bar-menu-max-width: 20rem;
  --bar-z: var(--z-bar);
  /* Where a sticky bar docks. 0 suits an app shell, where the bar is the top of
     the window; a shell with a header above it re-points this to that header's
     height. A knob rather than a rule to override, because overriding `top`
     means winning a specificity argument against the [data-sticky] selector
     below, while a custom property set on the element simply wins (§17 Rule 3). */
  --bar-top: 0px;
}

.bar {
  background: var(--bar-background);
  border-bottom: var(--bar-border);
  flex-shrink: 0;
}

/* Opt-in, taking the toolbar's polarity rather than the sticky bar's always-on.
   A sticky bar owes --scroll-offset its height. Note the polarity differs from
   Site Header (§40), which sticks by default and opts out — each default
   matches what that component usually does, but the attribute reads the same
   and means the opposite, so check which component you are on. */
.bar[data-sticky="true"] {
  position: sticky;
  top: var(--bar-top);
  z-index: var(--bar-z);
}

/* Default height is built up: one control tall plus the bar's own
   breathing room. Both halves matter — --bar-control-height stops the contents
   disagreeing, and --bar-padding-y keeps them off the bar's edges. An earlier
   toolbar set no vertical padding at all, reasoning that the controls should
   set the height. Right about the cause, wrong about the cure: the controls
   were disagreeing by 27px, and removing the padding only made a bar whose
   contents touched its borders. With the height enforced, padding is a density
   choice again.

   The floor is enforced on the container, not inferred from its contents.
   Note both density presets re-point --bar-padding-y to a *typed* 0px, not a
   bare 0, or this calc is invalid under them — see the density block below. The
   controls carry the 36px, but a control can leave: bar.js moves a group that
   no longer fits into the overflow panel, at whatever width that happens, and
   a bar whose every control has gone is left holding only full-height cells
   (the close, the menu triggers), which stretch to the row but never set it.
   They cannot: the weld margins subtract exactly 2 * --bar-padding-y from what
   a cell contributes to the line, so an icon-only cell contributes nothing and
   the line floors at zero. The row is then the container's own padding, 16px,
   with the icons overflowing it, and the shell's --scroll-offset sum, which
   assumes control plus padding, is 36px wrong. min-height, not height:
   regular's fixed --bar-height must still win, and compact's zeroed padding
   makes the same calc resolve to one control. Border-box, so the padding sits
   inside the value and the inner height is exactly the control. */
.bar-container {
  display: flex;
  align-items: center;
  gap: var(--bar-gap);
  padding: var(--bar-padding-y) var(--bar-padding-x);
  min-height: calc(var(--bar-control-height) + 2 * var(--bar-padding-y));
}

/* -- Density --
 * regular: a fixed 70px, for a bar whose content is uniform — a breadcrumb and
 * a close. The vertical padding goes, because the height now sets itself.
 * compact: the bar collapses to exactly one control tall, for a full-height
 * app shell where every row given to chrome is a row taken from the canvas.
 * Both re-point the token rather than setting padding-block directly, so the
 * close's padding-derived weld margins below resolve to zero with them.
 *
 * The zero is typed. A unitless 0 substitutes into calc() as a <number>, and
 * <length> + <number> is a type error that invalidates the whole declaration
 * at computed-value time — so the container's min-height silently fell back
 * to auto on both presets, and the weld margins' calc(-1 * …) with it.
 * regular hid it behind its explicit height; compact had nothing to hide
 * behind. */
.bar[data-density="regular"] {
  --bar-padding-y: 0px;
}

.bar[data-density="regular"] .bar-container {
  height: var(--bar-height);
}

.bar[data-density="compact"] {
  --bar-padding-y: 0px;
}

/* -- Content: the leading region --
 * Holds a breadcrumb, a title, or the control groups. It does NOT clip. An
 * earlier version set overflow-x: auto here, inherited from the sticky bar —
 * which per spec makes this a scroll container on BOTH axes, and with the
 * scrollbar hidden it silently swallowed any dropdown panel opened from
 * inside: the menu painted below the bar's edge and simply vanished. Menus
 * must be able to cross this edge. A set of items that no longer fits is the
 * overflow menu's job (bar.js reads scrollWidth, which reports the overhang
 * with or without a scroll container); a single too-wide item truncates
 * itself, the way the breadcrumb does in docs-site.css. */
.bar-content {
  display: flex;
  align-items: center;
  align-self: stretch;
  gap: var(--bar-gap);
  min-width: 0;
}

/* -- Group: a label and its control, or a set of related controls --
 * Moves into the overflow menu as one unit. That is deliberate: a group holding
 * four switches captioned "Show" is one idea, and splitting it across the bar
 * and the menu would read as two. */
.bar-group {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  flex-shrink: 0;
}

/* The base label element is display:block with a medium weight and a bottom
   margin (section 8). A caption is allowed to BE a real <label for>, so those
   three are neutralised here or the same class would render two different ways
   depending on the element it is written on — and inside an align-items:center
   row it is the margin box that gets centred, so the margin would push the
   caption off the control's centre line. .form-toggle label does the same. */
.bar-label {
  margin-bottom: 0;
  color: var(--text-faded);
  font-size: var(--font-xs);
  font-weight: var(--font-weight-regular);
  white-space: nowrap;
}

/* Nothing in a bar wraps. The bar is one row by contract (section note), and
   .bar-label has carried nowrap all along — but a button never did, and
   nothing caught it because a one-word label ("Reset", "Copy") cannot break.
   "Export logo" broke the moment a select landed beside it and made the bar
   two rows tall in the one place the component guarantees it will not be.
   .bar-actions has flex-shrink: 0, but that protects the cluster, not the
   text inside it: a flex item's automatic minimum size is its min-content,
   and for two words that is the longer word. Toggle labels and segments are
   listed for the same reason — any text in the row can be two words. */
.bar .button,
.bar .dropdown-trigger,
.bar .form-toggle label,
.bar .segmented-control-btn {
  white-space: nowrap;
}

/* -- Actions: the trailing cluster --
 * The auto margin, not space-between on the container — see the section note.
 * Holds only pinned items, so it never collapses, which is what lets the auto
 * margin keep working while content grows from the left. */
.bar-actions {
  display: flex;
  align-items: center;
  align-self: stretch;
  gap: var(--bar-group-gap);
  flex-shrink: 0;
  margin-inline-start: auto;
}

/* -- Menus in the actions cluster: full-height cells --
 * A dropdown in .bar-actions is chrome, not a captioned control, so it takes
 * the close's shape: a cell the bar's full height, with the menu hanging off
 * the bar's bottom border instead of floating from a mid-bar origin. One
 * stretch produces both, because the menu (top: 100%) is anchored to the
 * wrapper. The sticky bar did exactly this before the merge; the merge lost it
 * when every control was pinned to --bar-control-height.
 *
 * Both boxes stretch together, deliberately. dropdown.js measures the TRIGGER
 * to decide placement while the CSS anchors the menu to the WRAPPER — stretch
 * only one and the flip logic and the paint disagree by the difference, so a
 * menu the script decides "fits below" can overflow the viewport. The negative
 * block margins are the close's weld (see .bar-close), bleeding the cell over
 * --bar-padding-y where a density preset has not already zeroed it.
 *
 * Child combinators, load-bearing: a dropdown that has travelled INTO the
 * overflow panel is a row in a menu, not a cell in the bar, and must keep the
 * control height these rules would otherwise override. */
.bar-actions > .dropdown {
  align-self: stretch;
  margin-block: calc(-1 * var(--bar-padding-y));
}

.bar-actions > .dropdown > .dropdown-trigger {
  --dropdown-trigger-padding-y: 0;
  --dropdown-trigger-padding-x: var(--space-l);
  height: auto;
  align-self: stretch;
  border-radius: 0;
}

/* No border trimming on the attached panel, and that is deliberate. The menu
   (top: 100% of the stretched wrapper) starts on the same pixel row as the
   bar's border-bottom and paints over it, so its own border-top COINCIDES
   with the bar's hairline — one visible line, drawn by overlap. Removing the
   menu's border-top was tried (a commented-out attempt lived in §27 for a
   while) and it cuts a 1px gap in the bar's rule across the panel's width. */

/* -- Overflow: the "more" trigger and its panel --
 * The trigger lives in .bar-actions, never in .bar-content — the universal
 * order is content, actions, overflow, close, and the trigger is chrome, not
 * one of the controls it collects. Hidden until bar.js finds something to put
 * in it, so a bar whose items all fit shows no empty affordance. Open/close
 * belongs to dropdown.js; bar.js only moves items in and out. */
.bar-overflow[hidden] {
  display: none;
}

/* Items keep their bar classes when they move into the panel, so the panel
   restates the layout the row was providing: groups stack, each on its own
   line with the label and control pushed apart. */

/* The panel is a surface of relocated form controls, not a menu of flush
   rows — flush-edge hover is a .dropdown-item affordance and nothing in here
   is one. A gutter gives the rows breathing space instead. Child combinator,
   so a nested dropdown's own static menu is not double-padded. */
.bar-overflow > .dropdown-menu {
  padding: var(--space-s);
}

.bar-overflow .dropdown-menu [data-bar-demoted],
.bar-overflow .dropdown-menu [data-bar-spill] {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

/* Every resident gets the same inline inset, whatever it is. A resident is
   any direct child of .bar-content that bar.js moved here — usually a
   .bar-group, but a tool that folds a multi-row settings panel into the
   overflow (CPM Calculator) sends plain stacked blocks, and their labels sat
   4px left of the groups' until the inset moved from the group to the slot. */
.bar-overflow .dropdown-menu [data-bar-demoted] > *,
.bar-overflow .dropdown-menu [data-bar-spill] > * {
  padding-inline: var(--space-xs);
}

.bar-overflow .dropdown-menu .bar-group {
  justify-content: space-between;
  padding-block: var(--space-2xs);
}

/* The narrow-viewport rule above hides labels to save bar width; in the panel
   there is no width to save, and a control without its caption is a mystery. */
.bar-overflow .dropdown-menu .bar-label {
  display: block;
}

/* Bar geometry must not follow items into the panel: the 36px floor under a
   toggle exists to align it with the bar's other controls, and in a stacked
   panel row it just pads every switch. */
.bar-overflow .dropdown-menu .form-toggle {
  min-height: 0;
}

/* A vertical divider is row punctuation. The panel's own separator does its
   job there, so a divider that travelled with its neighbours stays out of the
   way rather than rendering as a stray tick. */
.bar-overflow .dropdown-menu .divider--vertical {
  display: none;
}

/* A dropdown that travelled into the panel cannot keep a floating menu: its
   menu is positioned against its own wrapper, and inside a floating panel
   those coordinates point at nothing — the menu painted half-detached beside
   the panel. In the panel it becomes a disclosure instead: the menu goes
   static, sheds the floating chrome, and expands in place, pushing the panel
   taller. Same reasoning as the panel's other restatements above — the panel
   provides the room the row could not. */
.bar-overflow .dropdown-menu .dropdown {
  flex-direction: column;
  align-items: stretch;
}

/* In the bar a trigger earns its width from its value; stacked in a panel a
   shrink-wrapped trigger floats in space. It spreads to the row's full width
   instead, chevron pushed to the far edge, so it reads like the select
   beside it. (The group's label keeps its share — flex: 1 grows the dropdown
   into what is left.) */
.bar-overflow .dropdown-menu .bar-group > .dropdown {
  flex: 1;
}

.bar-overflow .dropdown-menu .dropdown .dropdown-trigger {
  width: 100%;
  justify-content: space-between;
}

.bar-overflow .dropdown-menu .dropdown .dropdown-menu {
  position: static;
  min-width: 0;
  border: none;
}

/* -- Close: the exit, at the trailing edge --
 * Always last. It is the terminal cell — a single border-left and a stretched
 * box — so anything placed after it turns it into an interior cell with a
 * divider on one side, which reads as a mistake. The overflow trigger goes
 * BEFORE it. Safe at any density because the bar is always one row.
 *
 * The weld is in two parts. The container drops its inline-end padding when a
 * close is present — the gutter belongs to every control except the exit, and
 * :has() keeps that a property of the bar rather than a per-page override. The
 * block margins bleed the cell over any vertical padding, written in the token
 * so they are exactly as large as what they undo; both density presets zero
 * --bar-padding-y, so they usually resolve to nothing. */
.bar-container:has(.bar-close) {
  padding-inline-end: 0;
}

.bar-close {
  display: flex;
  align-items: center;
  justify-content: center;
  align-self: stretch;
  padding: 0 var(--space-l);
  margin-block: calc(-1 * var(--bar-padding-y));
  /* Same move as margin-block: exactly as large as the gap it undoes, so the
     close sits flush against whatever precedes it — normally the overflow
     cell — while every other pair in the cluster keeps --bar-group-gap. */
  margin-inline-start: calc(-1 * var(--bar-group-gap));
  color: var(--text-primary);
  text-decoration: none;
  border-left: var(--border-s) solid var(--border-faded);
  transition: background-color var(--duration-2xs) var(--ease-out),
              color var(--duration-2xs) var(--ease-out);
}

.bar-close:hover {
  background: var(--background-faded);
  color: var(--text-plain);
}

.bar-close .svg-icn {
  width: var(--icon-size);
  height: var(--icon-size);
  flex-shrink: 0;
}

/* -- One control height --
 * The single most important rule in this section. Left alone, a bar renders a
 * dropdown trigger at 27px, a select at 44px and a default button at 54px,
 * because three unrelated rules set them: the trigger's padding, the base
 * input's min-height, and --button-padding-y. Nothing reconciles the three, and
 * the mismatch is what makes a bar look broken however well its groups are
 * spaced.
 *
 * min-height: 0 is load-bearing. The base input rule sets min-height:
 * var(--target-min) (44px) — the WCAG 2.5.5 AAA touch target, right for a
 * form on a phone — and it beats any padding trim, so trimming padding
 * alone changes nothing. The height here
 * clears 2.5.8 Target Size (Minimum, AA), which asks for 24px.
 *
 * Excluded on purpose: range, checkbox, radio and colour size their own box and
 * would be distorted rather than aligned. The swatch is squared below instead. */
.bar .button,
.bar .dropdown-trigger,
.bar .copy-btn,
/* The thumb variant is excluded like range and checkbox: its labels plus
   track padding need more than the bar's 36px, so clamping the container
   would break the labels out of the track. The flat form is the bar's
   segmented control (cms/form.md). */
.bar .segmented-control:not([data-variant="thumb"]),
.bar .number-input,
.bar select,
.bar input:not([type="range"]):not([type="checkbox"]):not([type="radio"]):not([type="color"]) {
  height: var(--bar-control-height);
  min-height: 0;
}

/* With the height fixed, vertical padding only fights it. */
.bar .button {
  --button-padding-y: 0;
  --button-padding-x: var(--space-m);
}

.bar .dropdown-trigger {
  --dropdown-trigger-padding-y: 0;
  --dropdown-trigger-padding-x: var(--space-s);
}

.bar select,
.bar input:not([type="range"]):not([type="checkbox"]):not([type="radio"]):not([type="color"]) {
  padding-top: 0;
  padding-bottom: 0;
}

/* -- Quiet chrome --
 * A bar is a row of tools, not a row of boxes. Everything that acts is
 * transparent until you point at it; a visible border is reserved for the
 * things you can type into, so an edge in the bar carries meaning instead of
 * being decoration. This is the other half of what makes a dense bar read —
 * matching the heights alone is not enough. */
.bar .dropdown-trigger,
.bar .button[data-variant="transparent"] {
  border-radius: var(--radius-s);
}

.bar .dropdown-trigger:hover {
  background-color: var(--background-faded);
}

/* Fields keep an edge, and re-point the input surface to the bar's own ground
   so a bordered field reads as a well rather than a grey slab. */
.bar select,
.bar input:not([type="range"]):not([type="checkbox"]):not([type="radio"]),
.bar .number-input {
  --input-background: var(--bar-background);
  border-radius: var(--radius-s);
}

/* A toggle is a compound — a 24px switch and its label — so left alone it sits
   12px shorter than every bordered control beside it, and the row reads as
   misaligned even though nothing is misplaced. It cannot join the height rule
   above: that rule sets `height`, and its checkbox exclusion exists so a
   checkbox's own box is never distorted. min-height on the wrapper gives the
   toggle the row's height while the switch keeps the box it drew for itself. */
.bar .form-toggle {
  min-height: var(--bar-control-height);
  align-self: flex-start;
}

/* Square, so an icon button matches the swatch and reads as a tool. Keyed on
   data-icon-only rather than on .copy-btn or any other role class — a copy
   button that carries a label must stay as wide as its label. */
.bar .button[data-icon-only] {
  width: var(--bar-control-height);
  padding: 0;
}

.bar input[type="color"] {
  height: var(--bar-control-height);
  width: var(--bar-control-height);
  align-self: center;
  border-radius: var(--radius-s);
}

/* Fixed rather than fluid: a four-digit value must not resize the whole bar. */
.bar .number-input input[type="number"] {
  width: var(--bar-number-input-width);
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* The divider's own --divider-spacing is page-scale. In a bar it only needs to
   separate two clusters, and the container's gap already provides most of it.
   It already stretches itself, so only the margin is restated. */
.bar .divider--vertical {
  margin: var(--space-s) 0;
}

/* -- Child components --
 * NOTE: there is no .bar-breadcrumbs. Breadcrumb is a core component (§18) and
 * `.bar .breadcrumb` adapts it here. The docs generator used to emit a parallel
 * implementation under `.sticky-bar-breadcrumbs`; it was retired on 2026-08-11
 * and the bar composes the component like any other consumer. All docs-site.css
 * keeps is the truncation a bar needs and a page does not. */
.bar .tabs {
  align-self: stretch;
  flex-shrink: 0;
  border-bottom: none;
}

.bar .tab {
  display: flex;
  align-items: center;
  align-self: stretch;
  padding: 0 var(--space-l);
  font-size: var(--font-s);
}

/* Aligns the indicator with the container's bottom border rather than the
   tab list's own -1px offset. */
.bar .tab.is-active::after {
  bottom: 0;
}

.bar .breadcrumb {
  align-self: center;
  font-size: var(--font-s);
}

.bar .segmented-control {
  align-self: center;
}

/* -- Narrow viewports --
 * The labels go before the controls do. Every group in a bar is either a named
 * control (the label repeats what the trigger already says) or an icon set that
 * carries its own accessible name, so dropping the visible label costs width
 * and no meaning. Menus are clamped so a panel still fits a 375px screen rather
 * than overflowing it. */
@media (max-width: 768px) {
  /* Re-pointed, not overridden: a brand that moves --bar-gap or --bar-padding-x
     must keep it at every width, and writing literals here would silently
     discard the knob above. */
  .bar {
    --bar-gap: var(--space-m);
    --bar-padding-x: var(--space-l);
  }

  .bar-label {
    display: none;
  }

  /* On a narrow screen a menu is often the only route to the controls inside
     it, so the menu's own floor (--dropdown-menu-min-width, section 27) is
     dropped and a ceiling applied instead. The ceiling subtracts the bar's real
     gutter rather than a repeated literal, so moving the padding moves the
     clamp with it. */
  .bar .dropdown-menu {
    --dropdown-menu-min-width: 0;
    max-width: min(var(--bar-menu-max-width), calc(100vw - 2 * var(--bar-padding-x)));
  }
}


/* ------ 42. AVATAR ------
 * A person or organisation reduced to a small circle: an image where there is
 * one, initials where there is not.
 *
 * The initial is decorative, not content. A name rendered as "E" tells a
 * screen reader nothing useful and is usually sitting next to the full name
 * anyway, so the element is hidden from the accessibility tree by default and
 * the surrounding markup carries the identity.
 *
 * Size is one token, because the box is always square and the type scales with
 * it. Everything else re-points a colour.
 *
 * Fallback is a stack, not a swap. Initials sit in the box as text and the
 * photograph covers them from the layer above, so removing a broken image
 * reveals the initials with no JavaScript beyond the removal itself.
 */

:root {
  --avatar-size: 2rem;
  --avatar-radius: var(--radius-pill);
  --avatar-background: var(--background-secondary);
  --avatar-color: var(--text-primary);
  --avatar-icon-scale: 0.55;
  /* A fraction, not a length. A custom property containing var() is resolved
     where it is declared, so calc(var(--avatar-size) * 0.3) here would freeze
     at the :root size and never follow a re-pointed avatar. The multiplication
     happens at the avatar instead. */
  --avatar-group-overlap: 0.3;
  --avatar-group-ring-width: 2px;
  --avatar-group-ring-color: var(--background-primary);
}

.avatar {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  inline-size: var(--avatar-size);
  block-size: var(--avatar-size);
  overflow: hidden;
  border-radius: var(--avatar-radius);
  background: var(--avatar-background);
  color: var(--avatar-color);
  font-family: var(--font-primary);
  /* Tied to the box rather than the type scale, so one token resizes both and
     the initial can never outgrow its circle. */
  font-size: calc(var(--avatar-size) * 0.4);
  font-weight: var(--font-weight-semi-bold);
  line-height: 1;
  text-transform: uppercase;
  user-select: none;
}

/* Out of flow so it can sit over the initials without displacing them. */
.avatar img {
  position: absolute;
  inset: 0;
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

/* The icon fallback. .svg-icn carries a -0.175em optical margin for inline use
   next to text; inside a centred box it just pulls the icon off-centre.
   inline-size and block-size are restated rather than left to .svg-icn's own
   width/height, because context rules like .dropdown-item .svg-icn set a literal
   size at (0,2,0) and would otherwise pin the icon at 20px inside an avatar. */
.avatar > .svg-icn {
  --icon-size: calc(var(--avatar-size) * var(--avatar-icon-scale));
  inline-size: var(--icon-size);
  block-size: var(--icon-size);
  margin: 0;
}

.avatar[data-size="small"] {
  --avatar-size: 1.5rem;
}

.avatar[data-size="large"] {
  --avatar-size: 3rem;
}

.avatar[data-size="xlarge"] {
  --avatar-size: 4.5rem;
}

/* Colour follows the display-component axis (data-color), the same values
   badge and tag use, so a rebrand is a token change rather than an edit here.
   The set stops at two on purpose. Unlike badge and tag, this component paints
   --text-inverted directly onto the saturated fill, so a value only earns its
   place if it clears 4.5:1 in both themes AND without a brand theme loaded.
   Success was dropped when it sat at 3.16:1 unthemed; the -dark lift means it
   would clear now, but a person is not a status anyway: the colour here is
   variety, not meaning. */
.avatar[data-color="accent"] {
  --avatar-background: var(--background-accent);
  --avatar-color: var(--text-inverted);
}

.avatar[data-color="info"] {
  --avatar-background: var(--status-info);
  --avatar-color: var(--text-inverted);
}

/* Shape re-points the radius token rather than setting border-radius, so a
   context that already re-pointed --avatar-radius is not silently overridden. */
.avatar[data-shape="circle"] {
  --avatar-radius: var(--radius-pill);
}

.avatar[data-shape="square"] {
  --avatar-radius: 0;
}

.avatar[data-shape="rounded"] {
  --avatar-radius: var(--radius-s);
}

/* Avatar group — the same component, not a second one. It has no meaning
   without avatars inside it and adds no anatomy of its own: one rule pulls the
   siblings together, one separates them from what they overlap. */
.avatar-group {
  display: flex;
  align-items: center;
}

/* A ring rather than a border: a border would draw inside the box, eating into
   the photograph and sitting under the overflow clip. box-shadow paints outside
   it and touches neither.

   Matched as a descendant, not a child, because the avatars in a real team list
   are each wrapped in the <a> that links to the person — the component's own
   rule is that the avatar itself is never the clickable element. */
.avatar-group .avatar {
  box-shadow: 0 0 0 var(--avatar-group-ring-width) var(--avatar-group-ring-color);
}

/* On the group's direct children, whatever they are, so a wrapped avatar still
   overlaps. --avatar-size resolves on the group here rather than on the avatar,
   so a per-avatar data-size inside a group does not shift the overlap. */
.avatar-group > *:not(:first-child) {
  margin-inline-start: calc(-1 * var(--avatar-size) * var(--avatar-group-overlap));
}

/* Forced colours drops box-shadow and flattens every background to Canvas, so
   an overlapping group would merge into one silhouette of stacked letters. A
   border is not dropped, and it gives every avatar an edge — which is all the
   ring was ever providing. 1px inside the box costs a hairline of the
   photograph and nothing of the overlap, which reads --avatar-size. */
@media (forced-colors: active) {
  .avatar {
    border: 1px solid CanvasText;
  }
}


/* ------ 43. PAGE HEADER ------
 * The opening block of a page: eyebrow, title, subtitle, actions, and an
 * optional background image. One header per page, always first, always full
 * bleed.
 *
 * Not a Site Header (section 40), and the two are stacked on most pages. A
 * site header is chrome — the same identity and navigation on every page, and
 * it does not change when the route does. A page header is content — it names
 * *this* page and offers the next step on it. The tell is that a site header
 * survives navigation and a page header is replaced by it.
 *
 * One place this component breaks the house convention, worth stating plainly
 * rather than discovering later:
 *
 *   There is no implicit medium size. Button, Avatar and Book Cover all treat
 *   the bare class as their middle step, but a height on the bare class here
 *   would silently stretch every header using it — the docs site alone has
 *   122. So the bare class is content-sized — padding plus whatever is in it
 *   — and data-size is the opt-in for a fixed height. This is also the honest
 *   default on its own merits: a header with no media has no reason to
 *   reserve space it will not fill.
 *
 * Heights are clamped rem/vh rather than plain vh. The rem floor is what stops
 * a landscape phone collapsing a large header into a strip, and the rem
 * ceiling stops a 27" display turning it into a full screen of empty ground.
 * A product that genuinely wants a viewport-height hero re-points the token —
 * see --page-header-min-height-large.
 *
 * Media is a child element, not a background-image, so the picture is a real
 * <img>: it gets alt text, srcset, loading and fetchpriority, and it is not
 * dropped when a browser declines background images. The scrim rides on the
 * media wrapper, so a header with no image also has no scrim to unset.
 */

:root {
  /* Opaque, not --background-faded. The faded token is an alpha wash
     (rgba(0,0,0,0.05) light, rgba(255,255,255,0.06) dark), so the header ground
     shifted with whatever sat behind it. --background-secondary is a flat value
     and holds. It is a trade: separation from the page improves in light
     (1.26 vs 1.12) and narrows in dark (1.04 vs 1.18), where #222222 sits close
     to #1f1f1f. In dark the band is then read mostly from the bottom rule
     rather than the fill — and that rule is itself faint, since --border-faded
     is also a wash: it lands at 1.40:1 on the header ground in light and
     1.92:1 in dark, both under the 3:1 of WCAG 1.4.11. Those figures are
     unchanged by this swap (they were 1.41 and 1.91) and are the same defect
     already recorded against .callout and .toast borders. */
  --page-header-background: var(--background-secondary);
  --page-header-border: var(--border-s) solid var(--border-faded);
  --page-header-color: var(--text-primary);
  --page-header-color-muted: var(--text-secondary);
  --page-header-padding-y: var(--section-l);
  --page-header-padding-x: var(--space-xl);
  --page-header-max-width: var(--container-l);
  --page-header-gap: var(--space-m);
  --page-header-actions-gap: var(--space-s);
  /* Bounded on both ends — see the section note. Re-point any of the three to
     get a viewport-height hero (100svh) or a fixed pixel band. */
  --page-header-min-height-small: clamp(12rem, 24vh, 18rem);
  --page-header-min-height-medium: clamp(18rem, 40vh, 28rem);
  --page-header-min-height-large: clamp(24rem, 60vh, 40rem);
  /* Applied to the media wrapper, so it costs nothing on a header without one.
     Set to transparent for a light image carrying its own contrast. */
  --page-header-scrim: rgba(0, 0, 0, 0.45);
  /* Fixed light, not --text-inverted: the scrim is dark in both themes, so a
     token that flips with the theme would fail contrast in one of them. */
  --page-header-color-on-media: var(--off-white);
  /* The ground behind the picture, seen only while it loads or if it fails.
     Fixed dark for the same reason as the colour above. */
  --page-header-background-on-media: var(--off-black);
  /* The subtitle is a line of prose, so it gets a reading measure rather than
     the container's full width. */
  --page-header-subtitle-max-width: var(--max-width-s);
  /* Matches h1 at both steps of the heading scale. Restated rather than
     inherited so the title can be an <h1> on a page and an <h2> in a demo
     without changing size — see the note on .page-header-title. */
  --page-header-title-size: var(--font-7xl);
}

.page-header {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 100%;
  padding-block: var(--page-header-padding-y);
  background: var(--page-header-background);
  border-bottom: var(--page-header-border);
  color: var(--page-header-color);
  text-wrap: balance;
}

/* -- Height --
   Opt-in only. See the section note for why the bare class has none. */
.page-header[data-size="small"] {
  min-height: var(--page-header-min-height-small);
}

.page-header[data-size="medium"] {
  min-height: var(--page-header-min-height-medium);
}

.page-header[data-size="large"] {
  min-height: var(--page-header-min-height-large);
}

/* -- Media --
   Absolutely positioned rather than a background-image, so the <img> keeps its
   alt text and loading hints. Nothing here clips the header itself: an
   object-fit:cover image already fills the box exactly, and an overflow on the
   shell would clip any Flip or scroll animation that leaves it. */
.page-header-media {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
}

.page-header-image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.page-header-media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--page-header-scrim);
}

/* Text over an image goes light automatically. Keyed on the media element
   rather than an attribute so the two cannot disagree — an author who adds a
   picture cannot forget to add the matching theme flag. */
.page-header:has(.page-header-media) {
  --page-header-color: var(--page-header-color-on-media);
  --page-header-color-muted: var(--page-header-color-on-media);
  --page-header-background: var(--page-header-background-on-media);
  border-bottom-color: transparent;
}

/* Buttons over the scrim, and the reason this is not left to the author: the
   text switch above fixes the prose and would leave every button behind. An
   outline button reads --button-color, which is --text-primary — dark type and
   a dark rule on a dark wash. So the colour is re-pointed for the whole
   cluster, and the filled button additionally has its label flipped, because
   the base sets --button-text-color: var(--text-inverted) and that is light in
   light mode: light on a now-light fill.

   Both are held off buttons that state their own colour. A danger button is a
   deliberate choice and the header has no business overruling it — those keep
   their semantic fill, which carries its own contrast. */
.page-header:has(.page-header-media) .button:not([data-color]) {
  --button-color: var(--page-header-color-on-media);
}

.page-header:has(.page-header-media) .button:not([data-color]):not([data-variant]) {
  --button-text-color: var(--page-header-background-on-media);
}

/* -- Container --
   Owns the gutter and the measure. It is also what lifts the content off the
   media: the media sits at z-index 0, this at 1. A header using a .container-*
   utility instead is fine as long as it has no media. */
.page-header-container {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: var(--page-header-gap);
  width: 100%;
  max-width: var(--page-header-max-width);
  margin-inline: auto;
  padding-inline: var(--page-header-padding-x);
}

/* -- Content --
   The gap owns the vertical rhythm, so the elements inside must not add
   margins of their own. The heading scale is restated rather than inherited so
   the title stays restylable without changing the element. */
.page-header-title,
.page-header-subtitle,
.page-header .eyebrow {
  margin: 0;
}

/* The size is set on the class, not taken from the element. A page header's
   title is an <h1> in production and an <h2> in a demo beside one, and it
   should look the same in both — the element is a document-outline decision,
   not a size decision. Same reasoning as .book-cover-title (section 37). */
.page-header-title {
  font-size: var(--page-header-title-size);
  line-height: var(--line-height-m);
}

.page-header-subtitle {
  color: var(--page-header-color-muted);
  font-size: var(--font-m);
  max-width: var(--page-header-subtitle-max-width);
  text-wrap: balance;
}

/* -- Actions --
   A row that wraps. Buttons keep their own sizing; the header only spaces
   them. margin-top adds to the container gap deliberately: an action cluster
   earns more separation than a subtitle does. */
.page-header-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--page-header-actions-gap);
  margin-top: var(--space-s);
}

/* -- Alignment -- */
.page-header[data-align="center"] .page-header-container {
  align-items: center;
  text-align: center;
}

.page-header[data-align="center"] .page-header-subtitle {
  margin-inline: auto;
}

.page-header[data-align="center"] .page-header-actions {
  justify-content: center;
}

/* -- Narrow viewports --
   The title steps with the heading scale at 959px, the same breakpoint h1
   uses; padding steps later, at 767px. The heights are already clamped, so
   shrinking them again here would double-count. */
@media (max-width: 959px) {
  .page-header {
    --page-header-title-size: var(--font-5xl);
  }
}

@media (max-width: 767px) {
  .page-header {
    --page-header-padding-y: var(--section-m);
    --page-header-padding-x: var(--space-l);
  }
}

/* Forced colours drops the scrim and flattens every background, which would
   leave light-on-light text over the image. Hide the picture and hand the
   header back to the system palette. */
@media (forced-colors: active) {
  .page-header-media {
    display: none;
  }

  .page-header:has(.page-header-media) {
    --page-header-color: CanvasText;
    --page-header-color-muted: CanvasText;
    --page-header-background: Canvas;
  }
}


/* ------ 44. FOOTER ------
 * The block at the bottom of a page: an optional top region carrying
 * identity and link columns, and a bottom region carrying the copyright and
 * the legal links. On most sites the bottom region is the whole component.
 *
 * Same outer/inner split as .site-header (section 40) and .bar
 * (section 41). The outer element is full-bleed and owns background and the
 * top border; .footer-inner owns max-width, centring and the inset. That is
 * what lets the border run edge to edge while the contents line up with the
 * page column.
 *
 * The top region is three fixed slots — start, logo, end — rather than a
 * variable column count. The logo is the middle one and the sides are
 * optional, so the arrangement has to hold with one side, the other, or
 * neither.
 *
 * Links here are .footer-link, not .nav-link (section 39). A nav link
 * carries the padding a header bar needs to turn each link into a
 * full-height hit area; a footer column has no bar to fill. Owning the link
 * also means the footer wraps its rows freely without asking Nav to soften
 * its "a header nav that needs to wrap needs a drawer" rule.
 *
 * Placement is the consumer's job. There is deliberately no margin-top: auto
 * — a footer that pins to the bottom of a short page is a property of the
 * page shell, not of the footer. Same rule .site-header follows.
 */

:root {
  /* -- Component tokens (re-point these to re-skin; never override the rules) -- */

  /* Full-bleed by default, matching .site-header. Set a length to line the
     footer up with a content column. */
  --footer-max-width: none;
  /* Matches .padding-global, so the copyright lines up with the paragraph
     above it rather than sitting on its own margin. */
  --footer-padding-inline: var(--space-xl);
  /* A footer has vertical size where a header has a fixed height, so this
     stands in for --site-header-height rather than joining it. */
  --footer-padding-block: var(--space-2xl);
  /* Three jobs, one measure: the grid gap, the space either side of the
     hairline, and the bottom row's gap. Splitting them would only give them
     a way to drift apart. */
  --footer-gap: var(--space-xl);
  /* The tighter rhythm inside a slot — heading to links, and link to link. */
  --footer-slot-gap: var(--space-s);
  /* A floor, not a width. Without it a slot holding one short link collapses
     to that word and drags the centre track off the page centre. */
  --footer-side-min-width: 160px;
  --footer-logo-height: 80px;
  --footer-background: var(--background-primary);
  /* Paired with the background deliberately. A background knob on its own is
     a trap: the first brand to point it at something dark gets a contrast
     bug and no knob to fix it with. Every other colour below exists for the
     same reason — anything the background can break needs its own knob, or
     the two-token re-skin the doc promises is a half-truth. */
  --footer-text: var(--text-primary);
  --footer-border: var(--border-s) solid var(--border-faded);
  --footer-label-color: var(--text-faded);
  --footer-logo-color: var(--text-plain);
  --footer-link-color: var(--text-secondary);
  --footer-link-color-hover: var(--text-primary);
  --footer-link-color-current: var(--text-plain);
  /* The WCAG 2.2 AA 2.5.8 floor, exposed rather than baked in: the rules are
     not to be overridden, so a brand that wants a taller link needs a knob. */
  --footer-link-min-size: 24px;
  --footer-legal-gap: var(--space-m);
}

.footer {
  background: var(--footer-background);
  color: var(--footer-text);
  border-top: var(--footer-border);
  padding-block: var(--footer-padding-block);
}

.footer-inner {
  max-width: var(--footer-max-width);
  margin-inline: auto;
  padding-inline: var(--footer-padding-inline);
}

/* The middle track takes the leftover width so the logo has room to sit at
   the centre of it; the side tracks are content-sized above a floor.

   All three slots are placed by explicit line number rather than left to
   auto-placement. A footer carrying only an end slot would otherwise have it
   auto-placed into the first track and rendered on the left, and an absent
   slot has to keep holding its track open or the logo stops being centred
   the moment one side is dropped. */
.footer-top {
  display: grid;
  grid-template-columns:
    minmax(var(--footer-side-min-width), auto)
    minmax(0, 1fr)
    minmax(var(--footer-side-min-width), auto);
  gap: var(--footer-gap);
  align-items: center;
  margin-block-end: var(--footer-gap);
  padding-block-end: var(--footer-gap);
  border-bottom: var(--footer-border);
}

.footer-top-start,
.footer-top-end {
  display: flex;
  flex-direction: column;
  gap: var(--footer-slot-gap);
}

.footer-top-start {
  grid-column: 1;
}

/* Right-aligned as a slot. align-items is what does the work — text-align
   alone cannot move a stretched flex child, it can only move the text inside
   one, so both are needed for the heading and the links to agree. */
.footer-top-end {
  grid-column: 3;
  align-items: flex-end;
  text-align: right;
}

.footer-logo {
  grid-column: 2;
  justify-self: center;
  display: inline-flex;
  align-items: center;
  color: inherit;
  text-decoration: none;
}

/* .svg-logo is width-driven with a 300px ceiling, which is a poster rather
   than a footer mark. Height-driven instead — the same re-point
   .svg-logo.site-header-logo-image makes in section 40. */
.svg-logo.footer-logo-image {
  display: block;
  height: var(--footer-logo-height);
  width: auto;
  color: var(--footer-logo-color);
}

.svg-logo.footer-logo-image svg {
  height: 100%;
  width: auto;
  object-fit: contain;
}

/* Composes with .label (section 9): the shape comes from the utility and the
   footer only steps the colour down, so the links stay the loudest thing in
   the slot. */
.footer-label {
  color: var(--footer-label-color);
}

.footer-links {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--footer-slot-gap);
}

.footer-top-end .footer-links {
  align-items: flex-end;
}

/* No padding: a footer link has no bar to fill, and an inset would show the
   moment a background were added. The height comes from min-block-size
   instead — --font-s at --line-height-m is 19.2px, and WCAG 2.2 AA 2.5.8
   asks for a 24×24 target. */
.footer-link {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--footer-link-min-size);
  color: var(--footer-link-color);
  font-family: var(--font-primary);
  font-size: var(--font-s);
  line-height: var(--line-height-m);
  text-decoration: none;
  transition: color var(--duration-2xs) var(--ease-out);
}

.footer-link:hover {
  color: var(--footer-link-color-hover);
}

/* Current page is styled from the ARIA attribute rather than a class, so the
   accessible state and the visible state cannot drift apart — there is only
   one thing to set. Same reasoning as .nav-link in section 39. */
.footer-link[aria-current="page"] {
  color: var(--footer-link-color-current);
  font-weight: var(--font-weight-semi-bold);
}

.footer-bottom {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--footer-gap);
}

.footer-bottom[data-layout="center"] {
  justify-content: center;
}

/* The auto margin, not justify-content: space-between — space-between
   resolves to flex-start with a single child, and a lone copyright is the
   common case. Wrapping is the footer's own concern: a legal row has no bar
   height to protect, so it may take a second line rather than overflow. */
.footer-legal {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--footer-legal-gap);
  margin-inline-start: auto;
}

@media (max-width: 768px) {
  .footer-top {
    grid-template-columns: minmax(0, 1fr);
    text-align: center;
  }

  /* Every pinned line number has to be released. A slot left pinned to line
     3 would conjure two implicit columns beside the single explicit one. */
  .footer-top-start,
  .footer-top-end,
  .footer-logo {
    grid-column: auto;
  }

  /* Stacked, everything centres — including the slot that right-aligns when
     there are three columns to align against. */
  .footer-top-start,
  .footer-top-end,
  .footer-links,
  .footer-top-end .footer-links {
    align-items: center;
  }

  .footer-top-end {
    text-align: center;
  }

  .footer-bottom,
  .footer-legal {
    justify-content: center;
  }

  .footer-legal {
    margin-inline-start: 0;
  }
}


/* ------ 45. SECTION HEADER ------
 * A heading row that opens a region of content: a title on the left, an
 * optional action on the right. The top border and block padding mark the
 * boundary with whatever came before — the header owns that seam, so the
 * content below it starts flush.
 *
 * Not a Page Header (that names the whole page, once) and not a Bar (that
 * is persistent chrome with a bottom border). A section header is content:
 * it repeats down a page, one per region, and the heading element chosen
 * (h2/h3/h4) sets the size — the level is a document-outline decision and
 * the scale follows it.
 *
 * There is deliberately no inline padding token: the layout hierarchy
 * (padding-global) owns horizontal space, the header only owns the seam.
 */

:root {
  /* The boundary line above the row — composes from the border system */
  --section-header-border: var(--border-s) solid var(--border-faded);
  /* Space between the line and the row content, mirrored below the row */
  --section-header-padding-block: var(--space-l);
  /* Minimum breathing room between title and actions when they crowd */
  --section-header-gap: var(--space-m);
  /* Space between controls inside the actions slot */
  --section-header-actions-gap: var(--space-s);
}

.section-header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--section-header-gap);
  padding-block: var(--section-header-padding-block);
  border-top: var(--section-header-border);
}

/* The element (h2/h3/h4) supplies the size; the class only removes the
   document-flow margins, because the row's padding owns that rhythm.
   min-width: 0 lets the title shrink as a flex item so one long unbroken
   word wraps inside the row instead of overflowing it. */
.section-header-title {
  margin-block: 0;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* -- Actions --
   margin-inline-start: auto rather than justify-content: space-between on
   the parent — with no actions present, the title still sits flush left
   and gains no stray justification behaviour. When the row wraps, the
   auto margin keeps the actions right-aligned on their own line — that is
   intended, the slot holds trailing controls. */
.section-header-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--section-header-actions-gap);
  margin-inline-start: auto;
}


/* ------ 46. LOGIN ------
   An authentication stack: header, one or more form panes, trailing text
   actions. Thin by design — it composes .card, .form-group, .password-field
   and .button rather than restyling them. Page centering belongs to the
   consuming layout, not the component. Pane switching (sign in vs reset vs
   one-time code) is consumer JavaScript toggling .is-hidden; the component
   ships no script of its own. Password reveal is password-toggle.js. */

:root {
  --login-max-width: 380px;   /* one-column form measure */
  --login-logo-height: 88px;  /* header logo slot */
}

.login {
  width: 100%;
  max-width: var(--login-max-width);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-l);
}

/* -- Header: logo + title + subtitle -- */
.login-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-s);
  text-align: center;
}

.login-logo {
  height: var(--login-logo-height);
  color: var(--text-primary);
}

.login-logo svg {
  height: 100%;
  width: auto;
}

/* Size is fixed by the class; weight stays with the heading element, so
   an h1 and an h2 render the same size here but keep their own weight. */
.login-title {
  margin-block: 0;
  font-size: var(--font-3xl);
}

.login-subtitle {
  margin-block: 0;
  font-size: var(--font-m);
  color: var(--text-secondary);
}

/* Populated by flow JS (invite / recovery instructions); collapses when unused */
.login-subtitle:empty {
  display: none;
}

/* -- Form pane -- */
/* A page may hold several panes and switch them with .is-hidden */
.login-form {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
}

/* Tighter rhythm than the document-flow default: the pane's own gap owns
   the spacing between groups, the group only owns label-to-input. */
.login-form .form-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  margin-bottom: 0;
}

.login-form label {
  color: var(--text-secondary);
}

/* Divider spacing is re-pointed, not overridden — the pane gap already
   separates it from its neighbours. */
.login-form .divider {
  --divider-spacing: var(--space-2xs);
}

/* -- Meta row: remember-me vs forgot link -- */
.login-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-s);
}

/* -- Status: form-level live region -- */
/* Present but empty until consumer JS fills it */
.login-status {
  margin-block: 0;
  font-size: var(--font-s);
  text-align: center;
}

/* Not display: none — that would drop the live region out of the
   accessibility tree while empty, and polite (role="status") regions
   revealed from display:none are not reliably announced. The element
   stays rendered at zero height; the negative margin cancels the extra
   pane gap it would otherwise open. */
.login-status:empty {
  margin-top: calc(-1 * var(--space-s));
}

.login-status[data-type="danger"] {
  color: var(--status-danger);
}

.login-status[data-type="success"] {
  color: var(--status-success);
}

/* -- Footer: trailing text actions -- */
.login-footer {
  display: flex;
  flex-direction: column;
  align-items: center;
}


/* ------ 47. CELL INPUT ------ */

/* Fixed-shape text entry rendered as one cell per character — quiz answers,
   verification codes, PINs, dates, sort codes. One real input carries focus,
   value and the accessible name; the cells are a projection of it, built and
   painted by cell-input.js. A [data-static] instance is cells only — no
   input, no JS — for displaying a code rather than entering one. */

:root {
  /* -- Component tokens (re-point these to re-skin; never override the rules) -- */
  /* Cell geometry is the only value the component owns outright — everything
     else rides the input/status semantic tokens. */
  --cell-input-width: 2rem;
  --cell-input-height: 2.5rem;
  /* Surface defaults ride the input semantics so cells read as text fields;
     re-point these for a brand that wants cells styled apart from its inputs. */
  --cell-input-background: var(--input-background);
  --cell-input-border-color: var(--input-border);
  --cell-input-radius: var(--radius-s);
  /* Separators are punctuation, not entry — narrower so they read as marks
     between cells rather than empty cells. */
  --cell-input-separator-width: 1.25rem;
  /* Letter gap smaller than word gap, so words read as units. */
  --cell-input-gap: var(--space-xs);
  --cell-input-word-gap: var(--space-s);
}

/* The real input is stretched invisibly over the block, not hidden offscreen:
   the OS caret and soft-keyboard geometry then sit exactly where the user is
   looking (iOS scrolls and zooms to the input's rect), and the cells row is a
   label for it, so tapping a cell focuses the input. */
.cell-input {
  position: relative;
  width: fit-content;
  max-width: 100%;
}

.cell-input input {
  position: absolute;
  inset: 0;
  width: 100%;
  min-height: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  pointer-events: none;
  transition: none;
}

/* Invisible, so the caret cell below is the focus indicator — reset the
   global input outline explicitly rather than relying on opacity to hide it */
.cell-input input:focus {
  outline: none;
  border: none;
  box-shadow: none;
}

.cell-input-cells {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--cell-input-word-gap);
  width: fit-content;
  max-width: 100%;
  /* The element is a label — undo the global label margin and type so
     neither leaks into the cells (see .form-check label for the precedent) */
  margin-bottom: 0;
  font-size: inherit;
  border-radius: var(--cell-input-radius);
  cursor: text;
}

.cell-input[data-static] .cell-input-cells {
  cursor: default;
}

/* -- Words and cells -- */

/* Words wrap as units; a word longer than the row wraps within itself */
.cell-input-word {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--cell-input-gap);
}

.cell-input-cell {
  display: grid;
  place-items: center;
  width: var(--cell-input-width);
  height: var(--cell-input-height);
  background-color: var(--cell-input-background);
  border: var(--border-s) solid var(--cell-input-border-color);
  border-radius: var(--cell-input-radius);
  color: var(--input-text);
  font-weight: var(--font-weight-semi-bold);
  text-transform: uppercase;
  transition:
    border-color var(--duration-xs) var(--ease-out),
    background-color var(--duration-xs) var(--ease-out);
}

/* The next empty cell reads as the caret — only while the input has focus —
   and it IS the focus indicator (no ring around the group: the caret says
   "focused, and here", which is strictly more than a ring says, so the ring
   would be redundant ink). The inset shadow doubles the 1px border to a 2px
   accent without shifting layout. cell-input.js keeps the caret on the last
   cell when the row is full, so a complete entry never loses its indicator. */
.cell-input:focus-within .cell-input-cell.is-active {
  border-color: var(--input-focus);
  box-shadow: inset 0 0 0 1px var(--input-focus);
}

/* Pre-revealed punctuation (hyphens, slashes, apostrophes) — shown, not typed */
.cell-input-cell[data-cell="separator"] {
  width: var(--cell-input-separator-width);
  background-color: transparent;
  border-color: transparent;
  color: var(--text-faded);
}

/* -- Reduced motion -- */
@media (prefers-reduced-motion: reduce) {
  .cell-input-cell {
    transition: none;
  }
}

/* forced-colors erases both the caret's border-color and its inset shadow,
   and the focused element itself is invisible — high-contrast mode gets a
   real outline around the row instead */
@media (forced-colors: active) {
  .cell-input:focus-within .cell-input-cells {
    outline: 2px solid Highlight;
    outline-offset: 2px;
  }
}

/* -- Size -- */

/* Long formats get compact cells; the variant re-points the geometry tokens */
.cell-input[data-size="small"] {
  --cell-input-width: 1.5rem;
  --cell-input-height: 2rem;
  --cell-input-separator-width: 1rem;
}

.cell-input[data-size="small"] .cell-input-cell {
  font-size: var(--font-xs);
}

/* -- States -- */

/* Verdict states colour the letter cells; separators stay quiet */
.cell-input[data-type="success"] .cell-input-cell[data-cell="letter"] {
  background: var(--status-success-bg);
  border-color: var(--status-success);
  color: var(--status-success);
}

.cell-input[data-type="danger"] .cell-input-cell[data-cell="letter"],
.cell-input.is-error .cell-input-cell[data-cell="letter"] {
  background: var(--status-danger-bg);
  border-color: var(--status-danger);
  color: var(--status-danger);
}

/* Pair .is-disabled with disabled on the input itself — the class paints,
   the attribute stops focus and input. */
.cell-input.is-disabled .cell-input-cells {
  cursor: not-allowed;
}

.cell-input.is-disabled .cell-input-cell {
  background: var(--input-disabled-bg);
  color: var(--input-disabled-text);
}


/* ------ 48. APP NAVIGATION ------
 * The app's primary navigation: one component, two layouts. Below 960px it
 * is a bottom tab bar; from 960px it is a left icon rail. data-layout pins
 * one layout at every width for a product that wants no switch.
 *
 * Links, not buttons: each destination is an <a class="app-nav-link">, and
 * the current one is declared with aria-current="page" — accessible state
 * and visible state come from the same attribute, so they cannot drift
 * (the same rule as .nav, section 39).
 *
 * Positioning is fixed and the geometry composes the safe-area insets
 * (section 3): on an installed app the bar clears the home indicator and
 * the rail clears a landscape cutout. Content clearance is the app
 * shell's job — pad the scrolling column by --app-nav-height (bar tier)
 * or --app-nav-rail-width (rail tier), because the component cannot know
 * which container scrolls. Full-screen flows hide it with the shared
 * .is-hidden state.
 *
 * The current-state switch has no dedicated animation — platform tab bars
 * snap — but hover feedback transitions on the primitives, the same as
 * .nav-link (section 39). No element-scope semantic motion token exists
 * yet; never hardcode a duration here instead.
 */

:root {
  --app-nav-height: 56px;
  --app-nav-rail-width: 72px;
  --app-nav-z: var(--z-app-nav);
  /* Where the rail's top edge docks. 0 suits an app shell that owns the
     whole viewport; a product with a site header re-points this to the
     header's height instead of fighting the rail selector inside a media
     query (§17 Rule 3 — the same knob-over-override reasoning as
     --bar-top). */
  --app-nav-inset-block-start: 0px;
  --app-nav-gap: var(--space-m);
  --app-nav-link-gap: var(--space-s);
  --app-nav-rail-padding-block: var(--space-l);
  --app-nav-background: var(--background-primary);
  --app-nav-border: var(--border-primary);
  --app-nav-color: var(--text-faded);
  --app-nav-color-hover: var(--text-primary);
  --app-nav-color-current: var(--text-accent);
  --app-nav-icon-size: 1.5rem; /* 24px */
  /* --label-size steps 12px → 14px below 960px, so the touch bar gets the
     tap-adjacent label floor and the desktop rail keeps 12px. */
  --app-nav-label-size: var(--label-size);
  --app-nav-badge-size: 1.125rem; /* 18px — holds one digit round, two snug */
  --app-nav-badge-font-size: var(--font-3xs);
  --app-nav-badge-background: var(--status-danger);
  --app-nav-badge-color: var(--text-inverted);
}

/* Bar is the base layout: bottom-docked, links share the width equally. */
.app-nav {
  position: fixed;
  z-index: var(--app-nav-z);
  inset-inline: 0;
  inset-block-end: 0;
  display: flex;
  align-items: stretch;
  block-size: calc(var(--app-nav-height) + var(--safe-area-bottom));
  padding-block-end: var(--safe-area-bottom);
  background-color: var(--app-nav-background);
  border-block-start: var(--border-s) solid var(--app-nav-border);
}

.app-nav-link {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--app-nav-link-gap);
  min-inline-size: var(--target-min);
  min-block-size: var(--target-min);
  color: var(--app-nav-color);
  text-decoration: none;
  transition: color var(--duration-2xs) var(--ease-out);
}

.app-nav-link:hover {
  color: var(--app-nav-color-hover);
}

.app-nav-link[aria-current="page"] {
  color: var(--app-nav-color-current);
}

.app-nav-link[aria-current="page"] .app-nav-label {
  font-weight: var(--font-weight-semi-bold);
}

/* The icon wrapper sizes the glyph and anchors the badge. */
.app-nav-icon {
  position: relative;
  display: grid;
  place-items: center;
}

.app-nav-icon .svg-icn {
  width: var(--app-nav-icon-size);
  height: var(--app-nav-icon-size);
}

.app-nav-label {
  font-size: var(--app-nav-label-size);
  line-height: var(--line-height-s);
}

/* Count bubble over the icon's top-outside corner. Presentational — the
   count it shows must also be in the link's accessible name (see
   cms/app-nav.md), so the bubble itself is aria-hidden. */
.app-nav-badge {
  position: absolute;
  inset-block-start: calc(var(--space-xs) * -1.4);
  inset-inline-end: calc(var(--space-s) * -1.5);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--app-nav-badge-size);
  block-size: var(--app-nav-badge-size);
  padding-inline: var(--space-2xs);
  border-radius: var(--radius-pill);
  background-color: var(--app-nav-badge-background);
  color: var(--app-nav-badge-color);
  font-size: var(--app-nav-badge-font-size);
  font-weight: var(--font-weight-semi-bold);
  line-height: 1;
}

/* Rail geometry: left-docked column, links full-width. Reached two ways —
   pinned via the attribute, or automatically at the desktop tier. The two
   blocks repeat the same declarations because a media query cannot join a
   selector list; change both together. */
.app-nav[data-layout="rail"] {
  inset-inline: auto;
  inset-inline-start: 0;
  inset-block: var(--app-nav-inset-block-start) 0;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  gap: var(--app-nav-gap);
  block-size: auto;
  inline-size: calc(var(--app-nav-rail-width) + var(--safe-area-left));
  padding-block: var(--app-nav-rail-padding-block);
  padding-inline-start: var(--safe-area-left);
  border-block-start: 0;
  border-inline-end: var(--border-s) solid var(--app-nav-border);
}

.app-nav[data-layout="rail"] .app-nav-link {
  flex: 0 0 auto;
}

@media (min-width: 960px) {
  .app-nav:not([data-layout="bar"]) {
    inset-inline: auto;
    inset-inline-start: 0;
    inset-block: var(--app-nav-inset-block-start) 0;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: var(--app-nav-gap);
    block-size: auto;
    inline-size: calc(var(--app-nav-rail-width) + var(--safe-area-left));
    padding-block: var(--app-nav-rail-padding-block);
    padding-inline-start: var(--safe-area-left);
    border-block-start: 0;
    border-inline-end: var(--border-s) solid var(--app-nav-border);
  }

  .app-nav:not([data-layout="bar"]) .app-nav-link {
    flex: 0 0 auto;
  }
}

/* High-contrast modes erase the colour difference; underline the current
   label so the state survives, and give the badge back the outline its
   forced background erased. */
@media (forced-colors: active) {
  .app-nav-link[aria-current="page"] .app-nav-label {
    text-decoration: underline;
    text-underline-offset: 0.2em;
  }

  .app-nav-badge {
    border: 1px solid CanvasText;
  }
}


/* ------ 49. SKELETON ------
 * .skeleton-line is the base primitive: a pulsing placeholder block sized by
 * the composer, holding a layout while content loads. .skeleton-label and
 * .skeleton-input are presets that compose it
 * (class="skeleton-line skeleton-label") and shape it to the form controls
 * in section 12. See cms/skeleton.md.
 */

:root {
  --skeleton-background: var(--background-faded);
  --skeleton-radius: var(--radius-xs);
  --skeleton-duration: var(--duration-2xl);
}

.skeleton-line {
  background-color: var(--skeleton-background);
  border-radius: var(--skeleton-radius);
  animation: skeleton-pulse var(--skeleton-duration) var(--ease-in-out) infinite;
}

.skeleton-label {
  height: 14px;
  width: 30%;
  margin-bottom: var(--space-xs);
}

.skeleton-input {
  /* Tracks the input's rendered box, not the hit-area floor — a skeleton is
     not a pointer target, so this stays a literal. */
  height: 44px;
  width: 100%;
}

@keyframes skeleton-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

/* A static placeholder still says "loading"; the pulse is decoration. The
   hard floor here is 2.2.2 (Level A): blinking content lasting longer than
   five seconds needs a way to stop, which is why skeletons must resolve —
   to content or to an error — rather than pulse indefinitely. This guard
   covers the users who asked the platform for less motion. */
@media (prefers-reduced-motion: reduce) {
  .skeleton-line {
    animation: none;
  }
}

/* The fill flattens to Canvas under forced colours, which would erase the
   loading shape entirely — keep a drawn boundary, the badge pattern. */
@media (forced-colors: active) {
  .skeleton-line {
    border: 1px solid GrayText;
  }
}

/* ------ 50. CHIP ------
 *
 * A one-of-N control whose options wrap. Native radios inside labels, exactly
 * as the segmented control's thumb variant does it — the browser supplies
 * radiogroup semantics, roving arrow keys and the label association, so there
 * is nothing to keep in step and no script required to select.
 *
 * The line against .tag: a tag labels, a chip chooses. .tag is a <span> and
 * cms/tag.md forbids using one as a control. The line against .segmented-
 * control: that is one unified bar of equal, adjacent, non-wrapping segments,
 * capped at five. A chip group wraps and takes labels of any length.
 *
 * Clearing a chosen answer by re-clicking it is the one thing native radios
 * cannot do; that is chip.js, and only groups marked [data-clearable] load it.
 */
:root {
  --chip-background: transparent;
  --chip-border: var(--border-control);
  --chip-text: var(--text-secondary);
  --chip-hover-background: var(--background-faded);
  --chip-hover-text: var(--text-primary);
  /* Selection is the system's inverted surface, never a wash — see the
     --selected-* note in System Tokens for why. */
  --chip-selected-background: var(--selected-background);
  --chip-selected-text: var(--selected-text);
  --chip-radius: var(--radius-pill);
  --chip-padding-x: var(--space-m);
  --chip-gap: var(--space-xs);
  /* Narrowest a scale stop may be before the row wraps. Sized so five stops
     fit two rows on a 375px viewport at --font-s. */
  --chip-scale-min: 7rem;
  --chip-duration: var(--duration-2xs);
}

.chip-group {
  display: flex;
  flex-wrap: wrap;
  gap: var(--chip-gap);
  align-items: center;
}

/* An ordered scale reads as a ramp rather than ragged text: auto-fit keeps the
   stops even within a row and still wraps. The final row stretches to fill,
   which is the accepted trade for wrapping at all. */
.chip-group[data-layout="scale"] {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--chip-scale-min), 1fr));
}

.chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* A chip is a standalone target, not a segment of a grouped control, so it
     takes the hit-area floor — in the BLOCK axis. There is deliberately no
     min-width: a chip is as wide as its label, and --chip-gap is only 4px, so
     the inline axis leans on padding. A one-character label lands near 35px,
     which clears 2.5.8's 24px but not 44. Size labels accordingly. */
  min-height: var(--target-min);
  padding-inline: var(--chip-padding-x);
  margin-bottom: 0; /* this is a control, not a field label */
  font-family: var(--font-primary);
  font-size: var(--font-s);
  font-weight: var(--font-weight-regular);
  line-height: var(--line-height-s);
  text-align: center;
  color: var(--chip-text);
  background-color: var(--chip-background);
  border: var(--border-s) solid var(--chip-border);
  border-radius: var(--chip-radius);
  cursor: pointer;
  transition: background-color var(--chip-duration) var(--ease-out),
              color var(--chip-duration) var(--ease-out),
              border-color var(--chip-duration) var(--ease-out);
}

/* The radio covers the chip as an invisible hit area. Never display:none —
   that forfeits focus and the arrow keys, which are the whole reason this is
   a radio group and not a row of buttons. */
.chip input[type="radio"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  border: none;
  border-radius: inherit;
  opacity: 0;
  cursor: pointer;
}

/* Excludes checked and disabled explicitly: an equal-specificity hover rule
   that also matched the checked chip would out-rank it and flip the inverted
   label back to dark-on-dark. */
.chip:hover:not(:has(input:checked)):not(:has(input:disabled)) {
  background-color: var(--chip-hover-background);
  color: var(--chip-hover-text);
}

.chip:has(input:checked) {
  background-color: var(--chip-selected-background);
  color: var(--chip-selected-text);
  border-color: var(--chip-selected-background);
}

/* The radio is transparent, so the ring is drawn on the label around it.
   The offset is load-bearing, not cosmetic: on a SELECTED chip the ring colour
   and the fill are both --text-primary, so the page-coloured 2px gap is the
   only thing that makes the ring visible. Do not tidy it to 0. */
.chip:has(input[type="radio"]:focus-visible) {
  outline: 2px solid var(--text-primary);
  outline-offset: 2px;
}

/* No opacity here: it would compound onto an already-faded colour, and on a
   locked-in answer (checked AND disabled) it dragged the label to ~1.9:1 on its
   own fill. --input-disabled-text alone reads as disabled at 5.74:1. */
.chip:has(input:disabled) {
  color: var(--input-disabled-text);
  border-color: var(--border-faded);
  cursor: not-allowed;
}

.chip:has(input:disabled) input[type="radio"] {
  cursor: not-allowed;
}

/* Disabled and checked can co-occur, and two :has() rules at equal specificity
   let source order decide. Name the winner rather than relying on it. */
.chip:has(input:checked:disabled) {
  color: var(--chip-selected-text);
  background-color: var(--chip-selected-background);
  border-color: var(--chip-selected-background);
}

/* The unanswered-group state, set by the consumer on a failed submit. Sits on
   the group rather than each chip, following .cell-input.is-error. */
.chip-group.is-error .chip {
  border-color: var(--status-danger);
}

.chip-group.is-error .chip:has(input:checked) {
  border-color: var(--chip-selected-background);
}

/* Disabled outranks the group error. A disabled option is not part of what the
   user can fix, so painting it as the error misreads the state. Stated rather
   than left to the cascade: .chip-group.is-error .chip is (0,3,0) against
   .chip:has(input:disabled)'s (0,2,1), so error would otherwise win on
   specificity rather than on intent — the same reasoning as the checked rule
   above, which is why that one is written out too. */
.chip-group.is-error .chip:has(input:disabled) {
  border-color: var(--border-faded);
}

/* No prefers-reduced-motion block, deliberately. The chip transitions colour
   and nothing else — no transform, no movement — so there is no motion to
   reduce, and 2.3.3 explicitly excludes colour and opacity changes that do not
   move anything. The safety comes from "colour only", not from the duration
   value: a brand re-pointing --chip-duration to 400ms still only slows a fade.
   The segmented control carries a guard because its thumb translates. */

/* Backgrounds flatten under forced colours, so selection cannot be carried by
   a fill. Keep the drawn boundary, hand selection to the system Highlight
   pair, and re-declare the outline colour — forced-color-adjust: none on the
   selected chip would otherwise take the focus ring down with it. */
@media (forced-colors: active) {
  .chip {
    border: 1px solid CanvasText;
  }

  .chip:has(input:checked) {
    forced-color-adjust: none;
    background-color: Highlight;
    color: HighlightText;
    border-color: Highlight;
  }

  .chip:has(input[type="radio"]:focus-visible) {
    outline-color: CanvasText;
  }

  /* Every border flattens to CanvasText in this mode, so the error border
     would be identical to the rest border — the state would vanish. Mark is
     the system's "something is flagged here" colour. */
  .chip-group.is-error .chip {
    border-color: Mark;
  }

  /* opacity is not honoured here, so disabled needs saying outright. */
  .chip:has(input:disabled) {
    color: GrayText;
    border-color: GrayText;
  }

  /* And it outranks the group error here too, for the same reason and against
     the same specificity gap. GrayText is the only signal disabled has left
     once backgrounds flatten, so Mark must not take it. */
  .chip-group.is-error .chip:has(input:disabled) {
    color: GrayText;
    border-color: GrayText;
  }
}

/* ------ 15. COMPONENTS IN PROGRESS ------ */
/*
 * The following components are actively being built.
 * Remove each item from this list as it is implemented above.
 * See cms/components.md for the full component spec and build rules.
 *
 * REMAINING:
 * (all components implemented)
 *
 * IMPLEMENTED IN THIS FILE:
 * - [x] Button (.button)
 * - [x] Form elements (inputs, select, textarea, checkbox, radio, toggle, segmented control)
 * - [x] Callout (.callout)
 * - [x] Disclosure (details/summary)
 * - [x] Badge (.badge)
 * - [x] Card (.card)
 * - [x] Tabs (.tabs)
 * - [x] Progress (.progress-bar, .progress-ring)
 * - [x] Skeleton (.skeleton-line)
 * - [x] Tooltip (data-tooltip)
 * - [x] Toast (.toast)
 * - [x] Breadcrumb (.breadcrumb)
 * - [x] code / pre / kbd / mark / abbr / figure
 * - [x] Scrollbar
 * - [x] Divider (.divider)
 * - [x] Dropdown (.dropdown)
 * - [x] Tag (.tag)
 * - [x] Chip (.chip, .chip-group)
 * - [x] Dialog (.dialog)
 * - [x] Slider (input[type="range"])
 * - [x] Rating (.rating)
 * - [x] Number Input (.number-input)
 * - [x] Radio Group (.radio-group)
 * - [x] Drop Cap (.drop-cap)
 * - [x] Book (.book-shelf, .book-cover, .book-contents)
 * - [x] Page Navigation (.page-nav)
 * - [x] Nav (.nav)
 * - [x] Site Header (.site-header)
 * - [x] Bar (.bar)
 * - [x] Avatar (.avatar)
 * - [x] Page Header (.page-header)
 * - [x] Footer (.footer)
 * - [x] Section Header (.section-header)
 * - [x] Login (.login)
 * - [x] Cell Input (.cell-input)
 * - [x] App Navigation (.app-nav)
 */

