/* ==========================================================================================
   MATERIAL 3 DESIGN TOKENS
   ==========================================================================================
   The preview server's chrome is a Material 3 (Material You) surface. This block is the design
   system's *token* layer — colour roles, shape scale, elevation, state layers, motion and the
   type scale — expressed as the same `--md-sys-*` custom properties Material Web (`@material/md`)
   emits, so a reader who knows M3 already knows what every value here means. Nothing is imported:
   the server is self-contained (a strict CSP, no CDN, no web font fetch), so the baseline scheme
   below is inlined rather than pulled from a package.

   Two families, one source of truth:
   - `--md-sys-color-*` are the **roles** — the full M3 scheme (primary/secondary/tertiary/error
     families, the surface-container ladder, outline pair, inverse trio).
   - `--cp-*` are the **chrome aliases** this sheet was already written against. They are now
     defined *in terms of* the roles, so a component can be written either way and both stay in
     step.

   That indirection is what keeps the catalog palette sync working. A served design system
   publishes its resolved `MaterialTheme.colorScheme` as `tokens.dtcg.json`, and `ServeThemeCss`
   emits an inline `:root` override — of BOTH families — after this sheet, so browsing `/wear-m3/`
   paints the page in Wear M3's own M3 roles. Semantic colours (trusted/unverified badges,
   good/warn/bad scores, the code/design parity lanes) stay literal: they mean the same thing in
   every system and must not drift with a catalog's palette.

   LIGHT AND DARK ARE ONE DECLARATION, not two blocks. Every mode-dependent value is written as
   `light-dark(<light>, <dark>)`, which resolves against the element's used `color-scheme` — so the
   whole sheet (and the catalog palette `ServeThemeCss` emits over it) has exactly one place per
   colour, and switching modes is one property rather than a duplicated stylesheet. That is what
   makes the page-theme setting possible: `.cp-scheme-light` / `.cp-scheme-dark` on <html> pin
   `color-scheme`, so the chrome can be made to follow the *selected preview theme* instead of the
   visitor's OS (see `page-theme.js`). With no class the declared `light dark` pair defers to
   `prefers-color-scheme`, which is the default and the no-JS behaviour. */
:root {
  color-scheme: light dark;
  --site-header-height: 56px;

  /* --- M3 baseline scheme. The reference palette Material ships when a product has not supplied a
     source colour, so an unthemed preview server looks like Material rather than like an arbitrary
     indigo — light half first, dark half second. */
  --md-sys-color-primary: light-dark(#6750a4, #d0bcff);
  --md-sys-color-on-primary: light-dark(#ffffff, #381e72);
  --md-sys-color-primary-container: light-dark(#eaddff, #4f378b);
  --md-sys-color-on-primary-container: light-dark(#4f378a, #eaddff);
  --md-sys-color-secondary: light-dark(#625b71, #ccc2dc);
  --md-sys-color-on-secondary: light-dark(#ffffff, #332d41);
  --md-sys-color-secondary-container: light-dark(#e8def8, #4a4458);
  --md-sys-color-on-secondary-container: light-dark(#4a4458, #e8def8);
  --md-sys-color-tertiary: light-dark(#7d5260, #efb8c8);
  --md-sys-color-on-tertiary: light-dark(#ffffff, #492532);
  --md-sys-color-tertiary-container: light-dark(#ffd8e4, #633b48);
  --md-sys-color-on-tertiary-container: light-dark(#633b48, #ffd8e4);
  --md-sys-color-error: light-dark(#b3261e, #f2b8b5);
  --md-sys-color-on-error: light-dark(#ffffff, #601410);
  --md-sys-color-error-container: light-dark(#f9dedc, #8c1d18);
  --md-sys-color-on-error-container: light-dark(#8c1d18, #f9dedc);
  --md-sys-color-surface: light-dark(#fef7ff, #141218);
  --md-sys-color-on-surface: light-dark(#1d1b20, #e6e0e9);
  --md-sys-color-surface-variant: light-dark(#e7e0ec, #49454f);
  --md-sys-color-on-surface-variant: light-dark(#49454f, #cac4d0);
  --md-sys-color-surface-dim: light-dark(#ded8e1, #141218);
  --md-sys-color-surface-bright: light-dark(#fef7ff, #3b383e);
  --md-sys-color-surface-container-lowest: light-dark(#ffffff, #0f0d13);
  --md-sys-color-surface-container-low: light-dark(#f7f2fa, #1d1b20);
  --md-sys-color-surface-container: light-dark(#f3edf7, #211f26);
  --md-sys-color-surface-container-high: light-dark(#ece6f0, #2b2930);
  --md-sys-color-surface-container-highest: light-dark(#e6e0e9, #36343b);
  --md-sys-color-outline: light-dark(#79747e, #938f99);
  --md-sys-color-outline-variant: light-dark(#cac4d0, #49454f);
  --md-sys-color-inverse-surface: light-dark(#322f35, #e6e0e9);
  --md-sys-color-inverse-on-surface: light-dark(#f5eff7, #322f35);
  --md-sys-color-inverse-primary: light-dark(#d0bcff, #6750a4);
  /* `scrim` and `shadow` are deliberately absent. Every role declared here must also be emitted by
     `ServeThemeCss` (its test asserts exactly that), or a catalog would re-theme the page while one
     role quietly stayed on the baseline scheme. Those two are pure black in every M3 scheme and
     nothing on this page paints from them, so they are left out rather than themed to no effect —
     the scrim below uses its own literal black, as M3 specifies. */

  /* --- Shape scale. M3 names five steps plus `full`; components pick a step rather than a pixel
     value, which is why a shape change (say, a more expressive corner) is one edit here. */
  --md-sys-shape-corner-extra-small: 4px;
  --md-sys-shape-corner-small: 8px;
  --md-sys-shape-corner-medium: 12px;
  --md-sys-shape-corner-large: 16px;
  --md-sys-shape-corner-extra-large: 28px;
  --md-sys-shape-corner-full: 999px;

  /* --- Elevation. M3's six levels are a *pair* of shadows (a tight key light and a soft ambient
     one); the umbra/penumbra ratios below are the spec's, with the two shadow colours carried in
     `--md-sys-elevation-key` / `--md-sys-elevation-ambient` so the dark scheme deepens every level
     at once — M3's alphas are tuned for a light surface and all but vanish on a dark one. */
  --md-sys-elevation-key: light-dark(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.6));
  --md-sys-elevation-ambient: light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.45));
  --md-sys-elevation-level0: none;
  --md-sys-elevation-level1: 0 1px 2px 0 var(--md-sys-elevation-key),
    0 1px 3px 1px var(--md-sys-elevation-ambient);
  --md-sys-elevation-level2: 0 1px 2px 0 var(--md-sys-elevation-key),
    0 2px 6px 2px var(--md-sys-elevation-ambient);
  --md-sys-elevation-level3: 0 1px 3px 0 var(--md-sys-elevation-key),
    0 4px 8px 3px var(--md-sys-elevation-ambient);
  --md-sys-elevation-level4: 0 2px 3px 0 var(--md-sys-elevation-key),
    0 6px 10px 4px var(--md-sys-elevation-ambient);
  --md-sys-elevation-level5: 0 4px 4px 0 var(--md-sys-elevation-key),
    0 8px 12px 6px var(--md-sys-elevation-ambient);

  /* --- State layers. In M3 an interactive surface signals hover/focus/press by compositing its
     *content* colour over itself at a fixed opacity, rather than by swapping to a different fill.
     That is why these are opacities, not colours: the same three numbers apply to a filled button,
     a tonal chip, and a list row. */
  --md-sys-state-hover-opacity: 0.08;
  --md-sys-state-focus-opacity: 0.1;
  --md-sys-state-pressed-opacity: 0.1;

  /* --- Motion. M3 separates *standard* movement (things entering/leaving their own lane) from
     *emphasized* movement (a surface the eye should follow). */
  --md-sys-motion-easing-standard: cubic-bezier(0.2, 0, 0, 1);
  --md-sys-motion-easing-emphasized: cubic-bezier(0.2, 0, 0, 1);
  --md-sys-motion-easing-emphasized-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1);
  --md-sys-motion-duration-short2: 100ms;
  --md-sys-motion-duration-short4: 200ms;
  --md-sys-motion-duration-medium2: 300ms;

  /* --- Type scale. Roboto is Material's typeface; it is *not* fetched (no web font on a
     self-contained server), so the stack degrades to the platform UI face where Roboto is absent.
     Only the roles this chrome actually uses are declared — display/headline for page titles,
     title for section and card headings, body for prose, label for controls and metadata. */
  --md-sys-typescale-font: Roboto, "Roboto Flex", system-ui, -apple-system, "Segoe UI", sans-serif;
  --md-sys-typescale-mono-font: "Roboto Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --md-sys-typescale-headline-small-size: 1.5rem;
  --md-sys-typescale-headline-small-line: 2rem;
  --md-sys-typescale-title-large-size: 1.375rem;
  --md-sys-typescale-title-large-line: 1.75rem;
  --md-sys-typescale-title-medium-size: 1rem;
  --md-sys-typescale-title-medium-line: 1.5rem;
  --md-sys-typescale-title-medium-tracking: 0.009375em;
  --md-sys-typescale-title-small-size: 0.875rem;
  --md-sys-typescale-title-small-line: 1.25rem;
  --md-sys-typescale-title-small-tracking: 0.00714em;
  --md-sys-typescale-body-medium-size: 0.875rem;
  --md-sys-typescale-body-medium-line: 1.25rem;
  --md-sys-typescale-body-medium-tracking: 0.01786em;
  --md-sys-typescale-body-small-size: 0.75rem;
  --md-sys-typescale-body-small-line: 1rem;
  --md-sys-typescale-body-small-tracking: 0.03333em;
  --md-sys-typescale-label-large-size: 0.875rem;
  --md-sys-typescale-label-large-line: 1.25rem;
  --md-sys-typescale-label-large-tracking: 0.00714em;
  --md-sys-typescale-label-medium-size: 0.75rem;
  --md-sys-typescale-label-medium-line: 1rem;
  --md-sys-typescale-label-medium-tracking: 0.04167em;
  --md-sys-typescale-label-small-size: 0.6875rem;
  --md-sys-typescale-label-small-line: 1rem;
  --md-sys-typescale-label-small-tracking: 0.04545em;

  /* --- Chrome aliases. Every rule in this sheet that predates the token layer reads these; they
     are now a *view* of the M3 roles above rather than a second palette, so there is exactly one
     place a colour is decided. The three-step text ramp maps onto M3's own hierarchy
     (on-surface → on-surface-variant → outline) rather than being mixed by hand. */
  --cp-bg: var(--md-sys-color-surface);
  --cp-surface: var(--md-sys-color-surface-container-low);
  --cp-surface-2: var(--md-sys-color-surface-container-high);
  --cp-fg: var(--md-sys-color-on-surface);
  --cp-fg-soft: var(--md-sys-color-on-surface);
  --cp-fg-muted: var(--md-sys-color-on-surface-variant);
  --cp-fg-faint: var(--md-sys-color-outline);
  --cp-border: var(--md-sys-color-outline-variant);
  --cp-border-strong: var(--md-sys-color-outline);
  --cp-accent: var(--md-sys-color-primary);
  --cp-accent-strong: var(--md-sys-color-on-primary-container);
  --cp-accent-soft: var(--md-sys-color-secondary-container);
  --cp-accent-ring: var(--md-sys-color-primary);
  --cp-on-accent: var(--md-sys-color-on-primary);
  --cp-on-accent-soft: var(--md-sys-color-on-secondary-container);
}
/* The page-theme setting ("Page theme" in the header's Settings menu, wired by `page-theme.js`):
   when the chrome is set to follow the SELECTED PREVIEW THEME, the resolved mode is pinned here
   instead of being left to `prefers-color-scheme`. Every mode-dependent value in this sheet — and
   in the catalog palette `ServeThemeCss` inlines after it — is a `light-dark()` pair resolved
   against `color-scheme`, so these two declarations re-paint the whole page. Absent both classes
   the `light dark` pair above defers to the OS, which is what a visitor who turned the setting off
   (and every no-JS client that wasn't served an explicit `?theme=`) gets. */
:root.cp-scheme-light { color-scheme: light; }
:root.cp-scheme-dark { color-scheme: dark; }
* { box-sizing: border-box; }
body { margin: 0; padding: 28px; font-family: var(--md-sys-typescale-font);
  font-size: var(--md-sys-typescale-body-medium-size);
  line-height: var(--md-sys-typescale-body-medium-line);
  color: var(--cp-fg); background: var(--cp-bg);
  -webkit-font-smoothing: antialiased; }
a { color: var(--cp-accent); text-decoration: none; }
a:hover { text-decoration: underline; }
/* M3 focus indicator: a 3dp ring in `secondary`, offset clear of the component's own outline, on
   every focusable thing. Declared once so no control has to remember to grow its own. */
:where(a, button, summary, input, select, textarea, [tabindex]):focus-visible {
  outline: 3px solid var(--md-sys-color-secondary); outline-offset: 2px; border-radius: inherit; }
/* Selection controls (checkbox, radio, range) are painted by the UA; `accent-color` is the one
   hook it gives us, and pointing it at `primary` is what makes them read as Material. */
input, select, textarea, progress { accent-color: var(--md-sys-color-primary); }
.cp-main, .cp-site-header, .cp-site-footer { width: 100%; max-width: 1440px;
  margin-left: auto; margin-right: auto; }
/* The page's top app bar. M3's small top app bar is a plain surface with a title on the left and
   trailing actions on the right — no rule beneath it; separation comes from the surface, not from
   a line, so the keyline is gone and the bar simply sits on the page's own colour. */
/* Three fixed slots — brand | live status | nav — rather than a flex row that redistributes.
   Both the render-server badge and the GitHub session control are conditional, and under
   `space-between` their absence slid the nav between centred and hard-right from page to page.
   A `1fr auto 1fr` grid pins the brand left and the nav right whatever the middle holds. */
.cp-site-header { min-height: 56px; margin-bottom: 14px; padding: 4px 0 8px;
  position: sticky; top: 0; z-index: 80; background: var(--cp-bg);
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: center; gap: 18px; }
/* The lead slot: the brand, then this page's breadcrumb / back link. Navigation belongs in the
   navigation bar — as the body's first line it spent a whole row (plus its margin) restating
   where you are, and on the viewer that row is the difference between the render being on screen
   and being cut off by the fold. min-width:0 so a long catalog name ellipsises here instead of
   shoving the centred status badge off its column. */
.cp-site-lead { justify-self: start; min-width: 0; display: flex; align-items: center;
  gap: 12px; }
.cp-site-nav { justify-self: end; }
.cp-interface-mode { justify-self: end; display: inline-flex; align-items: center; gap: 2px;
  padding: 3px; border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-surface-container-low); white-space: nowrap; }
.cp-interface-mode-label { margin: 0 7px 0 8px; color: var(--md-sys-color-on-surface-variant);
  font-size: var(--md-sys-typescale-label-small-size); }
.cp-interface-mode button { min-height: 30px; padding: 4px 11px; border: 0;
  border-radius: var(--md-sys-shape-corner-full); background: transparent;
  color: var(--md-sys-color-on-surface-variant); box-shadow: none; }
.cp-interface-mode button[aria-pressed="true"] { background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container); }
/* In the header the trail is a quiet continuation of the brand, separated by a hairline rather
   than by another chip: the brand IS the first crumb, so a keyline reads as the first `/`. */
.cp-site-lead .cp-breadcrumb { margin: 0; min-width: 0; display: flex; align-items: center;
  gap: 6px; padding-left: 12px; border-left: 1px solid var(--cp-border);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cp-site-lead .cp-breadcrumb > a { color: var(--md-sys-color-on-surface-variant);
  overflow: hidden; text-overflow: ellipsis; }
/* The catalog this page belongs to, named in the bar. Styled like the breadcrumb it sits beside —
   a hairline continuation of the brand rather than a second chip — because it answers the same
   "where am I" question and must not compete with the brand for weight. It carries the catalog's
   own primary colour, so on a themed system the bar picks up the palette the page already wears. */
.cp-site-catalog { min-width: 0; padding-left: 12px; border-left: 1px solid var(--cp-border);
  color: var(--md-sys-color-primary);
  font-size: var(--md-sys-typescale-title-small-size);
  line-height: var(--md-sys-typescale-title-small-line);
  font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Beside a breadcrumb the name is the parent, so the trail drops its own keyline (one separator
   between any two items, never two). */
.cp-site-catalog + .cp-breadcrumb { padding-left: 0; border-left: 0; }
.cp-crumb-sep { color: var(--cp-fg-faint); }
.cp-crumb-current { overflow: hidden; text-overflow: ellipsis; }
/* A catalog landing's back button keeps its outlined-button shape but drops to the bar's own
   height and loses the body margin it needed when it was the page's first element. */
.cp-site-lead .cp-back { margin: 0; min-height: 32px; padding: 5px 14px 5px 12px;
  font-size: var(--md-sys-typescale-label-medium-size); white-space: nowrap; }
.cp-site-brand { display: inline-flex; align-items: center; gap: 10px; flex: none;
  color: var(--cp-fg);
  font-size: var(--md-sys-typescale-title-medium-size);
  line-height: var(--md-sys-typescale-title-medium-line);
  letter-spacing: var(--md-sys-typescale-title-medium-tracking); font-weight: 500; }
.cp-site-brand:hover { text-decoration: none; color: var(--md-sys-color-primary); }
/* The product mark is an M3 avatar/monogram: a full-round tonal container, not a rounded square. */
.cp-site-mark { display: grid; place-items: center; width: 32px; height: 32px;
  border-radius: var(--md-sys-shape-corner-full);
  color: var(--md-sys-color-on-primary-container); background: var(--md-sys-color-primary-container);
  font-size: 0.9rem; }
/* Trailing actions read as M3 text buttons: full-round, a state layer on hover, no underline. */
.cp-site-nav { display: flex; align-items: center; gap: 4px;
  font-size: var(--md-sys-typescale-label-large-size);
  line-height: var(--md-sys-typescale-label-large-line);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500; }
/* The nav is a `<details>` holding nothing but its button, and the panel of links BESIDE it — the
   phone bar below opens the one with the other (`[open] + panel`). Here there is nothing to open:
   the disclosure is removed and the panel is the row of actions it has always been. One copy of
   each link, two shapes, and no script in either — which is the whole reason the panel is the
   disclosure's sibling rather than its child. A `<details>` hides its own content when closed, so
   a panel inside it would have to be served `open`, closed again before first paint on a phone,
   and re-resolved on every breakpoint crossing: three moving parts, each able to strand the
   navigation behind a button, to arrive at what one selector says for free. */
.cp-site-menu { display: none; }
.cp-site-menu-panel { display: flex; align-items: center; gap: 4px; }
.cp-site-nav a { display: inline-flex; align-items: center; gap: 6px; padding: 8px 12px;
  border-radius: var(--md-sys-shape-corner-full); color: var(--md-sys-color-on-surface-variant);
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-site-nav a:hover { text-decoration: none; color: var(--md-sys-color-primary);
  background: color-mix(in srgb, var(--md-sys-color-primary) 8%, transparent); }
.cp-site-nav .cp-gh-auth { color: var(--cp-fg-soft); }
/* The Settings menu (ServeWeb.settingsMenuHtml): standing per-visitor preferences, as opposed to
   the controls that describe what is on screen. A `<details>` so it works with no JavaScript; its
   summary is styled as one more nav action, and the panel is an M3 menu surface anchored under it.
   `list-style: none` + the `::-webkit-details-marker` reset drop the disclosure triangle — the gear
   is the affordance. */
.cp-settings { position: relative; }
.cp-settings-btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 12px;
  border-radius: var(--md-sys-shape-corner-full); color: var(--md-sys-color-on-surface-variant);
  cursor: pointer; list-style: none; user-select: none;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-settings-btn::-webkit-details-marker { display: none; }
.cp-settings-btn:hover, .cp-settings[open] > .cp-settings-btn {
  color: var(--md-sys-color-primary);
  background: color-mix(in srgb, var(--md-sys-color-primary) 8%, transparent); }
.cp-settings-panel { position: absolute; right: 0; top: calc(100% + 6px); z-index: 50;
  width: max-content; max-width: min(320px, calc(100vw - 32px)); padding: 8px 4px;
  border: 0; border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-high);
  box-shadow: var(--md-sys-elevation-level2); }
.cp-settings-group { margin: 0; padding: 0; border: 0; }
.cp-settings-legend { padding: 4px 12px; color: var(--cp-fg-faint);
  font-size: var(--md-sys-typescale-label-medium-size);
  letter-spacing: var(--md-sys-typescale-label-medium-tracking); text-transform: uppercase; }
/* Menu items at M3's 48dp list-item rhythm, with the radio as the leading control. */
.cp-settings-option { display: flex; align-items: center; gap: 10px; padding: 10px 12px;
  border-radius: var(--md-sys-shape-corner-small); cursor: pointer; font-weight: 400;
  color: var(--cp-fg); }
.cp-settings-option:hover {
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-settings-option input { margin: 0; }
.cp-settings-hint { margin: 4px 12px 6px; max-width: 280px; color: var(--cp-fg-muted);
  font-size: var(--md-sys-typescale-body-small-size);
  line-height: var(--md-sys-typescale-body-small-line);
  letter-spacing: var(--md-sys-typescale-body-small-tracking); font-weight: 400; }
.cp-settings-keyboard { margin-top: 6px; padding-top: 6px;
  border-top: 1px solid var(--md-sys-color-outline-variant); }
.cp-settings-tour { margin: 0 12px 8px; padding: 4px 0; border: 0; background: none;
  color: var(--md-sys-color-primary); font: inherit;
  font-size: var(--md-sys-typescale-label-medium-size); cursor: pointer; }
.cp-settings-tour:hover { text-decoration: underline; }

/* Opt-in keyboard layer: a quiet persistent hint rail, command palette and first-run tour. */
.cp-keyboard-modal-open { overflow: hidden; }
.cp-keyboard-hints { position: fixed; left: 50%; bottom: 14px; z-index: 44;
  transform: translateX(-50%); display: flex; align-items: center; gap: 2px; padding: 5px;
  max-width: calc(100vw - 24px); overflow-x: auto;
  border-radius: var(--md-sys-shape-corner-full);
  background: color-mix(in srgb, var(--md-sys-color-inverse-surface) 94%, transparent);
  color: var(--md-sys-color-inverse-on-surface); box-shadow: var(--md-sys-elevation-level3);
  backdrop-filter: blur(12px); }
.cp-keyboard-hints button { display: inline-flex; align-items: center; gap: 6px; flex: none;
  padding: 6px 9px; border: 0; border-radius: var(--md-sys-shape-corner-full); background: none;
  color: inherit; font: inherit; font-size: var(--md-sys-typescale-label-small-size); cursor: pointer; }
.cp-keyboard-hints button:hover, .cp-keyboard-hints button:focus-visible {
  background: color-mix(in srgb, var(--md-sys-color-inverse-on-surface) 14%, transparent); }
.cp-keyboard-hints kbd, .cp-keyboard-dialog kbd { min-width: 20px; padding: 2px 5px;
  border: 1px solid color-mix(in srgb, currentColor 28%, transparent); border-radius: 5px;
  background: color-mix(in srgb, currentColor 10%, transparent); color: inherit;
  font: 600 0.72rem/1.25 ui-monospace, SFMono-Regular, Menlo, monospace; text-align: center; }
.cp-keyboard-overlay { position: fixed; inset: 0; z-index: 100; display: grid;
  place-items: start center; padding: min(14vh, 112px) 16px 24px;
  background: color-mix(in srgb, #000 42%, transparent); }
.cp-keyboard-dialog { width: min(640px, 100%); max-height: min(72vh, 700px); overflow: hidden;
  border-radius: var(--md-sys-shape-corner-extra-large);
  background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface);
  box-shadow: var(--md-sys-elevation-level5); }
.cp-keyboard-dialog > header { display: flex; align-items: center; justify-content: space-between;
  padding: 20px 22px 12px; }
.cp-keyboard-dialog h2 { margin: 2px 0 0; font-size: var(--md-sys-typescale-headline-small-size);
  line-height: var(--md-sys-typescale-headline-small-line); font-weight: 500; }
.cp-keyboard-eyebrow { color: var(--md-sys-color-primary);
  font-size: var(--md-sys-typescale-label-medium-size);
  letter-spacing: var(--md-sys-typescale-label-medium-tracking); text-transform: uppercase;
  font-weight: 600; }
.cp-keyboard-close { width: 36px; height: 36px; border: 0; border-radius: 50%; background: none;
  color: inherit; font: 1.5rem/1 inherit; cursor: pointer; }
.cp-keyboard-close:hover {
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-command-search { display: flex; align-items: center; gap: 10px; margin: 0 16px 8px;
  padding: 0 14px; min-height: 48px; border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-surface-container-highest); }
.cp-command-search > span { color: var(--md-sys-color-primary); font-size: 1.3rem; }
.cp-command-search input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent;
  color: inherit; font: inherit; }
.cp-command-list { max-height: min(48vh, 430px); overflow-y: auto; padding: 4px 8px 12px; }
.cp-command-section { padding: 12px 14px 4px; color: var(--md-sys-color-on-surface-variant);
  font-size: var(--md-sys-typescale-label-small-size); text-transform: uppercase;
  letter-spacing: .08em; }
.cp-command-item { width: 100%; display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: 4px 18px; align-items: center; padding: 10px 14px; border: 0;
  border-radius: var(--md-sys-shape-corner-medium); background: transparent; color: inherit;
  font: inherit; text-align: left; cursor: pointer; }
.cp-command-item[aria-selected="true"] { background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container); }
.cp-command-item > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-weight: 500; }
.cp-command-item small { color: var(--md-sys-color-on-surface-variant); }
.cp-command-empty { padding: 32px 16px; color: var(--md-sys-color-on-surface-variant);
  text-align: center; }
.cp-command-palette > footer { display: flex; gap: 18px; padding: 10px 22px 14px;
  border-top: 1px solid var(--md-sys-color-outline-variant);
  color: var(--md-sys-color-on-surface-variant);
  font-size: var(--md-sys-typescale-label-small-size); }
.cp-command-palette > footer span { display: inline-flex; align-items: center; gap: 4px; }
.cp-shortcut-grid { display: grid; grid-template-columns: auto 1fr auto 1fr; gap: 14px 12px;
  align-items: center; padding: 12px 22px 20px; }
.cp-keyboard-note { margin: 0; padding: 14px 22px 20px;
  border-top: 1px solid var(--md-sys-color-outline-variant);
  color: var(--md-sys-color-on-surface-variant); }
.cp-keyboard-onboarding { padding: 28px; }
.cp-keyboard-onboarding h2 { max-width: 480px; margin: 8px 0 12px; }
.cp-keyboard-onboarding p { color: var(--md-sys-color-on-surface-variant); line-height: 1.55; }
.cp-onboarding-demo { display: flex; flex-wrap: wrap; gap: 8px; margin: 22px 0; }
.cp-onboarding-demo span { display: inline-flex; align-items: center; gap: 8px; padding: 9px 12px;
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-highest); }
.cp-keyboard-onboarding > footer { display: flex; justify-content: flex-end; gap: 10px;
  margin-top: 22px; }
.cp-keyboard-onboarding > footer button { padding: 9px 16px;
  border-radius: var(--md-sys-shape-corner-full); border: 0;
  color: var(--md-sys-color-primary); background: transparent; font: inherit; font-weight: 600;
  cursor: pointer; }
.cp-keyboard-onboarding > footer .cp-onboarding-start { color: var(--md-sys-color-on-primary);
  background: var(--md-sys-color-primary); }
@media (max-width: 640px) {
  .cp-keyboard-hints { left: 12px; right: 12px; transform: none; }
  .cp-keyboard-hints button span { display: none; }
  .cp-keyboard-overlay { padding-top: 7vh; }
  .cp-shortcut-grid { grid-template-columns: auto 1fr; }
  .cp-command-palette > footer { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .cp-keyboard-overlay *, .cp-keyboard-hints * { scroll-behavior: auto !important; }
}
.cp-site-footer { margin-top: 40px; padding-top: 16px; border-top: 1px solid var(--cp-border);
  color: var(--cp-fg-muted); font-size: var(--md-sys-typescale-body-small-size);
  line-height: var(--md-sys-typescale-body-small-line);
  letter-spacing: var(--md-sys-typescale-body-small-tracking);
  display: flex; flex-direction: column; align-items: stretch; gap: 14px; }
/* The links row keeps the footer's original single-line shape; the footer itself is now a column
   so a page can put its own block (the catalog-provenance disclosure) above it. */
.cp-site-footer-links { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.cp-site-footer a { display: inline-flex; align-items: center; gap: 4px; }
/* "report a bug" sits in the footer's links row and must read as one of them, but it is a GET form
   (see ServeWeb.reportBugFormHtml for why it cannot be an anchor) — so the form is unwrapped to a
   plain inline box and its submit button is stripped of button chrome, the same treatment the
   per-preview .cp-report-link gets. */
.cp-report-bug { display: inline-flex; margin: 0; }
.cp-report-bug-link { display: inline-flex; align-items: center; gap: 4px; background: none;
  border: 0; padding: 0; font: inherit; color: var(--cp-accent); cursor: pointer; }
.cp-report-bug-link:hover { text-decoration: underline; }
/* Page + section headings ride the M3 type scale rather than ad-hoc rem sizes. */
.cp-head { margin: 0 0 4px; font-size: var(--md-sys-typescale-title-large-size);
  line-height: var(--md-sys-typescale-title-large-line); font-weight: 400; }
.cp-sub { margin: 0 0 20px; font-size: var(--md-sys-typescale-body-medium-size);
  line-height: var(--md-sys-typescale-body-medium-line);
  letter-spacing: var(--md-sys-typescale-body-medium-tracking); color: var(--cp-fg-muted); }
.cp-breadcrumb { margin: 0 0 8px; color: var(--cp-fg-faint);
  font-size: var(--md-sys-typescale-label-medium-size);
  letter-spacing: var(--md-sys-typescale-label-medium-tracking); }
/* Front-door section rhythm. The page is a run of (title, grid) pairs with no wrapper element, so
   the gap BETWEEN sections is expressed on the title that follows a grid — one section reads as one
   band instead of the whole page reading as an undifferentiated column of cards. */
.cp-section-title { margin: 0 0 16px; display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px 12px; }
.cp-section-title .cp-head { margin: 0; font-size: var(--md-sys-typescale-headline-small-size);
  line-height: var(--md-sys-typescale-headline-small-line); letter-spacing: 0; font-weight: 400; }
.cp-grid + .cp-section-title { margin-top: 44px; }
.cp-section-count, .cp-catalog-id { color: var(--cp-fg-faint);
  font-size: var(--md-sys-typescale-label-medium-size);
  letter-spacing: var(--md-sys-typescale-label-medium-tracking); }
/* The count reads as a quiet tally beside its heading, not as a second title — an M3 suggestion
   chip's shape in the neutral container role. */
.cp-section-count { padding: 4px 10px; border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface-variant); font-weight: 500; }
/* The landing's identity line, built like the viewer's `.cp-preview-head`: the catalog's name and
   trust verdict, its id, and the preview/view tally on ONE baseline. They answer the same question
   ("what am I looking at"), and as a title block with a summary paragraph under it they answered it
   twice over a row the fold could be spending on previews. `.cp-catalog-title` stays its own box
   inside the row because the phone rule below hides IT — the bar already names the catalog there —
   while the tally moves below the grid instead. */
.cp-catalog-head-row { display: flex; align-items: baseline; flex-wrap: wrap; gap: 2px 14px;
  margin: 0 0 10px; }
.cp-catalog-head-row .cp-sub { margin: 0; }
.cp-catalog-title { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px 14px;
  margin: 0; }
.cp-catalog-title .cp-head, .cp-catalog-title .cp-catalog-id { margin: 0; }
.cp-catalog-id { font-family: var(--md-sys-typescale-mono-font); }
.cp-catalog-head { font-size: var(--md-sys-typescale-headline-small-size);
  line-height: var(--md-sys-typescale-headline-small-line); }
/* Per-preview GitHub links under the viewer title: source file, and "report an issue". One flex
   row so they read as a single line whether one or both are present. */
.cp-preview-links { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px;
  margin: 0 0 8px; }
.cp-source, .cp-report, .cp-figma { margin: 0; font-size: 0.8rem; }
/* The report affordance is a GET form (its action is the GitHub issue form, its title/body are
   hidden inputs) so no page state ever reaches a navigation sink — but it reads as a link, so the
   submit button is stripped of its button chrome and styled as one. */
.cp-source-link, .cp-report-link, .cp-figma-link { display: inline-flex; align-items: center; gap: 5px;
  color: var(--cp-fg-muted); text-decoration: none; }
.cp-report-link { background: none; border: 0; padding: 0; font: inherit; cursor: pointer; }
.cp-source-link:hover, .cp-report-link:hover, .cp-figma-link:hover { text-decoration: underline; }
/* The Figma mark is 2:3, not square like the GitHub one — size it by height so it isn't squashed. */
.cp-figma-mark { width: auto; height: 14px; }
/* M3 *filled* card: a container-role fill with no outline and no shadow. The border this used to
   carry is what made every panel on the page read as a box; M3 separates panels from the page by
   tone instead, which is why the surface-container ladder exists. */
/* The about disclosure closes the page: it sits under the content it explains and above the site
   footer, so its separation is above it, not below. */
.cp-about { margin: 34px 0 0; padding: 16px 20px; border: 0;
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-low); max-width: 720px; }
.cp-about-title { font-size: var(--md-sys-typescale-title-small-size);
  line-height: var(--md-sys-typescale-title-small-line);
  letter-spacing: var(--md-sys-typescale-title-small-tracking); font-weight: 500; }
.cp-about-body { margin: 0 0 8px; font-size: var(--md-sys-typescale-body-medium-size);
  line-height: 1.5; letter-spacing: var(--md-sys-typescale-body-medium-tracking);
  color: var(--md-sys-color-on-surface-variant); }
.cp-about-body code { font-family: var(--md-sys-typescale-mono-font); font-size: 0.8rem;
  padding: 1px 5px; border-radius: var(--md-sys-shape-corner-extra-small);
  background: var(--md-sys-color-surface-container-high); }
.cp-about-links { margin: 0; font-size: 0.8rem; color: var(--cp-fg-muted); display: flex; flex-wrap: wrap;
  align-items: center; gap: 6px; }
.cp-about-links a, .cp-gh-auth { display: inline-flex; align-items: center; gap: 4px; }
.cp-gh { width: 15px; height: 15px; vertical-align: text-bottom; }
/* An M3 assist chip carrying the build id. */
.cp-about-ver { font-family: var(--md-sys-typescale-mono-font);
  font-size: var(--md-sys-typescale-label-small-size);
  letter-spacing: var(--md-sys-typescale-label-small-tracking);
  padding: 3px 10px; border-radius: var(--md-sys-shape-corner-full);
  border: 1px solid var(--md-sys-color-outline-variant);
  background: transparent; color: var(--md-sys-color-on-surface-variant); }
.cp-gh-auth--signed { color: var(--cp-fg-soft); }
.cp-disclosure > summary { cursor: pointer; list-style: none; display: flex; align-items: center;
  justify-content: space-between; gap: 12px; }
.cp-disclosure > summary::-webkit-details-marker { display: none; }
/* The Show/Hide affordance is an M3 text button: label-large in `primary`, pill state layer. */
.cp-disclosure > summary::after { content: "Show"; color: var(--md-sys-color-primary);
  font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  padding: 6px 12px; border-radius: var(--md-sys-shape-corner-full);
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-disclosure[open] > summary::after { content: "Hide"; }
.cp-disclosure > summary:hover::after {
  background: color-mix(in srgb, var(--md-sys-color-primary) 8%, transparent); }
.cp-disclosure-hint { flex: 1; color: var(--cp-fg-faint);
  font-size: var(--md-sys-typescale-body-small-size);
  letter-spacing: var(--md-sys-typescale-body-small-tracking); font-weight: 400; }
.cp-disclosure-body { margin-top: 12px; }
/* Catalog "back to home" — an M3 outlined button: 40dp tall, fully rounded, an `outline` keyline,
   and a `primary` state layer under the pointer rather than a surface swap. */
.cp-back { display: inline-flex; align-items: center; gap: 8px; margin: 0 0 16px;
  min-height: 40px; font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  padding: 8px 20px 8px 16px; border-radius: var(--md-sys-shape-corner-full);
  border: 1px solid var(--md-sys-color-outline);
  background: transparent; color: var(--md-sys-color-primary); }
.cp-back { transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard),
  border-color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-back:hover { background: color-mix(in srgb, var(--md-sys-color-primary) 8%, transparent);
  border-color: var(--md-sys-color-outline); color: var(--md-sys-color-primary);
  text-decoration: none; }
/* Provenance strip: how/when/from-what this catalog was generated (branch, date, versions,
   a link to re-run the generating workflow). It rides in the site footer, expanded, beside the
   build and source links. */
.cp-prov { margin: 18px 0 0; padding: 14px 18px; border: 0;
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-low);
  font-size: var(--md-sys-typescale-body-small-size);
  line-height: var(--md-sys-typescale-body-small-line);
  letter-spacing: var(--md-sys-typescale-body-small-tracking);
  color: var(--cp-fg-muted); max-width: 720px; }
.cp-prov-title { color: var(--cp-fg-soft); font-weight: 500;
  font-size: var(--md-sys-typescale-title-small-size);
  letter-spacing: var(--md-sys-typescale-title-small-tracking); }
.cp-prov-body { display: flex; flex-wrap: wrap; gap: 8px 16px; }
.cp-prov-item { display: inline-flex; align-items: center; gap: 5px; }
.cp-prov-item .cp-prov-key { color: var(--cp-fg-faint); }
.cp-prov-item code { font-family: var(--md-sys-typescale-mono-font); font-size: 0.74rem;
  padding: 1px 5px; border-radius: var(--md-sys-shape-corner-extra-small);
  background: var(--md-sys-color-surface-container-high); }
.cp-prov-refresh { appearance: none; border: 0; padding: 0; background: none; color: var(--cp-accent);
  font: inherit; cursor: pointer; }
.cp-prov-refresh:hover { text-decoration: underline; }
.cp-prov-refresh:disabled { color: var(--cp-fg-faint); cursor: wait; text-decoration: none; }
.cp-prov-refresh-status { color: var(--cp-fg-faint); }
/* In the footer the strip is the first block, so it drops the leading margin it needed when it
   trailed the catalog grid. */
.cp-site-footer .cp-prov { margin: 0; }
.cp-systems { margin: 0 0 20px; display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  font-size: 0.85rem; }
.cp-systems-label { font-weight: 600; color: var(--cp-fg-muted); }
/* M3 filter chips: 32dp, fully rounded, outlined when unselected and a `secondary-container`
   tonal fill when selected — the same selected treatment every chip row on this page now uses. */
.cp-systems a, .cp-systems-cur { display: inline-flex; align-items: center; min-height: 32px;
  padding: 6px 16px; border-radius: var(--md-sys-shape-corner-small);
  border: 1px solid var(--md-sys-color-outline-variant);
  font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500; }
.cp-systems a { background: transparent; color: var(--md-sys-color-on-surface-variant); }
.cp-systems a:hover { text-decoration: none;
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-systems-cur { background: var(--md-sys-color-secondary-container); border-color: transparent;
  color: var(--md-sys-color-on-secondary-container); font-weight: 500; }
.cp-toolbar { margin: 0; }
.cp-searchbar { margin: 0; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; flex: 1 1 440px; }
.cp-catalog-tools { position: sticky; top: var(--site-header-height); z-index: 18;
  margin: 0 0 20px; padding: 10px 0;
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px 16px; background: var(--cp-bg); }
/* The catalog's own actions — the comparison views, the parity view, the playground, Transparent.
   M3 *assist chips*: outlined, 32dp, label-large, one per destination — inside a MENU, at every
   width, the way the Theme chips beside them now are. They spent years as a row of their own above
   the toolbar, which put two chip rows and a tally between a catalog's heading and its first card;
   they are destinations a visitor goes looking for, not text read on the way past, so they live
   behind the `⋯` pill and the fold they were costing goes back to the previews.
   `position: relative` is load-bearing in both shapes this row takes — nested in the sticky toolbar
   beside the Theme pill, and standing alone when a catalog publishes no Theme control at all —
   because it is what the `⋯` panel measures from. */
.cp-catalog-actions { position: relative; margin: 0; display: flex; align-items: center;
  flex-wrap: wrap; gap: 8px; }
.cp-actions-menu > summary { display: inline-flex; list-style: none; }
.cp-actions-menu > summary::-webkit-details-marker { display: none; }
.cp-actions-menu[open] > summary { background: var(--md-sys-color-secondary-container);
  border-color: transparent; color: var(--md-sys-color-on-secondary-container); }
/* The `⋯` is the trailing control on its row, so its panel hangs off the right edge — the same
   M3 menu surface, offset and elevation `.cp-theme-menu-panel` uses for the Theme pill next to it.
   A menu is a column of full-width rows, not a wrapping chip row: each destination gets its own
   line and its own hit target. */
.cp-actions-panel { display: none; position: absolute; right: 0; top: calc(100% + 6px); z-index: 50;
  width: max-content; max-width: min(320px, calc(100vw - 28px)); padding: 8px;
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-high);
  box-shadow: var(--md-sys-elevation-level2); }
.cp-actions-menu[open] + .cp-actions-panel { display: flex; flex-direction: column;
  align-items: stretch; gap: 2px; }
.cp-actions-panel .cp-action-chip, .cp-actions-panel .cp-bg-btn {
  width: 100%; min-height: 40px; justify-content: flex-start; text-align: left; }
.cp-catalog-download { margin: 30px 0 0; }
.cp-action-chip { display: inline-flex; align-items: center; min-height: 32px; padding: 6px 16px;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-small);
  background: transparent; color: var(--md-sys-color-on-surface-variant);
  font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  text-decoration: none; white-space: nowrap;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-action-chip:hover, .cp-action-chip:focus-visible { text-decoration: none;
  color: var(--md-sys-color-on-surface);
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
/* M3 search bar: a filled `surface-container-high` pill with NO outline — the fill is the
   affordance. Focus is the shared 3dp `secondary` ring rather than a border colour change. */
.cp-search { flex: 1 1 260px; max-width: 420px; min-height: 44px; padding: 10px 18px; font: inherit;
  font-size: var(--md-sys-typescale-body-medium-size);
  letter-spacing: var(--md-sys-typescale-body-medium-tracking);
  border: 0; border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface); }
.cp-search::placeholder { color: var(--md-sys-color-on-surface-variant); }
.cp-search:focus { outline: 3px solid var(--md-sys-color-secondary); outline-offset: 2px; }
.cp-count { font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); color: var(--cp-fg-muted); }
.cp-empty { margin: 12px 0 0; font-size: var(--md-sys-typescale-body-medium-size);
  color: var(--cp-fg-muted); }
.cp-empty[hidden] { display: none; }
/* Sectioned catalog: a navigation TREE beside the grid (ServeWeb.catalogTreeHtml) rather than a
   row of tabs above it — sections as rows, each opening onto its named sub-groups, so both levels
   of a catalog's structure are visible at once instead of only its top one. Progressive
   enhancement — with no JS every section shows under its own <h2>, nothing collapses, and every
   row is a working in-page anchor; the filter script turns the rows into real section switching
   and adds `cp-js` to <html>, which collapses the unselected branches and hides the then-redundant
   per-section <h2> (the tree already names the section). */
.cp-catalog-body { display: block; }
/* The sidebar only appears where there is room for one beside a grid of 200px cards. Below that
   the tree stays a full-width block above the panels — the same reading order, stacked.

   `--cp-sticky-tools` is how much sticky chrome sits above everything: `.cp-catalog-tools` pins
   itself at `top: 0` with a higher z-index, so a second sticky element parked at 16px would spend
   its life underneath the search field, and a `scrollIntoView` that lands an element at viewport
   zero puts it under there too. Both clear it by that height instead. It is set by the tree script
   from the toolbar's measured box — it wraps on a narrow viewport and grows a row with the
   declared-theme chips — and, like every other script-supplied property in this sheet, it is read
   ONLY with a literal fallback, so the no-JS page still clears an unwrapped toolbar. */
@media (min-width: 960px) {
  .cp-catalog-body { display: grid; grid-template-columns: minmax(168px, 216px) minmax(0, 1fr);
    gap: 32px; align-items: start; }
  .cp-catalog-menu { position: sticky;
    top: calc(var(--site-header-height) + var(--cp-sticky-tools, 0px) + 12px);
    max-height: calc(100vh - var(--site-header-height) - var(--cp-sticky-tools, 0px) - 28px);
    overflow-y: auto; overscroll-behavior: contain; }
}
.cp-catalog-menu { margin: 0 0 22px; }
/* Room for a FOCUS RING to be drawn — after the margin shorthand above, which would otherwise
   reset the inline margins this needs.
   Every focusable thing in this column is ringed 5px outside its own box (`outline: 3px` +
   `outline-offset: 2px`), and the column scrolls at this width — which clips on BOTH axes, not
   just the one named, because a scroll container's other axis computes to `auto` rather than
   staying `visible`. The filter field is the first row AND full-width, so it lost the top and both
   sides of its ring: three of its four edges, on the one control a keyboard visitor lands in
   first. The padding is the ring's reach; the negative inline margin gives the column back the
   width it just spent, so the ring gains somewhere to land and nothing moves. */
@media (min-width: 960px) {
  .cp-catalog-menu { padding: 6px; margin-inline: -6px; }
}
/* The Components / Pages switch at the head of the sidebar (ServeWeb.paneTabsHtml). Two peers, so
   an M3 segmented button rather than a nav row: one shape, split in two, with the selected half
   filled. The strip leads the column and the filter sits under it, which is what makes the filter
   read as belonging to whichever list is showing. */
.cp-panes { display: grid; grid-template-columns: 1fr 1fr; gap: 0; margin: 0 0 14px;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-full); overflow: hidden; }
.cp-pane-tab { display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  min-height: 36px; padding: 7px 10px;
  font-size: var(--md-sys-typescale-label-large-size);
  line-height: var(--md-sys-typescale-label-large-line);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  color: var(--md-sys-color-on-surface-variant); border: 0; background: transparent;
  cursor: pointer; text-align: center;
  transition: color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard),
    background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-pane-tab + .cp-pane-tab { border-left: 1px solid var(--md-sys-color-outline-variant); }
.cp-pane-tab:hover { color: var(--md-sys-color-on-surface);
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-pane-tab[aria-selected="true"] { color: var(--md-sys-color-on-secondary-container);
  background: var(--md-sys-color-secondary-container); }
/* The count rides the tab the way it rides a tree row. The column is 168–216px whatever the
   viewport, so this is not a viewport question and there is no breakpoint to hang it on: the label
   holds its size and the count is what shrinks, down to nothing, when the two will not fit. */
.cp-pane-tab .cp-tree-count { flex: 0 1 auto; min-width: 0; overflow: hidden; }
.cp-pane[hidden] { display: none; }
.cp-page-list { list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 2px; }
/* A page WITH sections is a branch. It reuses the component tree's twisty and child indent rather
   than inventing a second visual language for the same idea one pane over — the only difference is
   that this row is a real link to the whole sheet as well as a fold, so the arrow is the part that
   folds and the label is the part that navigates. */
.cp-page-branch > .cp-tree-page { justify-content: flex-start; }
.cp-page-branch > .cp-tree-page .cp-tree-count { margin-left: auto; }
.cp-page-sections { margin: 2px 0 6px 12px;
  border-left: 1px solid var(--md-sys-color-outline-variant); padding-left: 8px; }
html.cp-js .cp-page-branch > .cp-tree-page[aria-expanded="false"] + .cp-page-sections {
  display: none; }
.cp-page-sections li[hidden] { display: none; }
.cp-page-list li[hidden] { display: none; }
.cp-pane-empty { margin: 10px 2px; color: var(--md-sys-color-on-surface-variant);
  font-size: var(--md-sys-typescale-body-small-size); }
/* The way out to the pages index, kept after the list rather than as a row inside it: it is not
   one of the pages, and a filter that empties the list must not take the escape hatch with it. */
.cp-pane-all { display: inline-block; margin: 12px 2px 0;
  font-size: var(--md-sys-typescale-label-medium-size);
  color: var(--md-sys-color-primary); }
.cp-catalog-menu .cp-searchbar { display: block; margin: 0 0 18px; }
.cp-catalog-menu .cp-search { width: 100%; min-width: 0; padding-inline: 14px; }
.cp-catalog-menu .cp-tree { margin: 0; }
.cp-tree { margin: 0 0 22px; }
.cp-tree-list, .cp-tree-children { list-style: none; margin: 0; padding: 0; }
.cp-tree-node[hidden], .cp-tree-children li[hidden] { display: none; }
/* A section row. Still `.cp-tab` — the section-switching script and the e2e harness both key off
   that class and its `data-tab` — but drawn as an M3 navigation item: a full-width 40dp row whose
   selected state is a `secondary-container` pill rather than an underline. */
.cp-tab { position: relative; display: flex; align-items: center; gap: 8px; width: 100%;
  min-height: 40px; padding: 8px 14px;
  font-size: var(--md-sys-typescale-label-large-size);
  line-height: var(--md-sys-typescale-label-large-line);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  color: var(--md-sys-color-on-surface-variant); border: 0;
  border-radius: var(--md-sys-shape-corner-full);
  background: transparent; cursor: pointer; text-align: left;
  transition: color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard),
    background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-tab:hover { color: var(--md-sys-color-on-surface); text-decoration: none;
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-tab[aria-selected="true"] { color: var(--md-sys-color-on-secondary-container);
  background: var(--md-sys-color-secondary-container); }
/* The expand/collapse twisty, drawn only for a section that HAS sub-groups (the markup omits
   `aria-expanded` on a leaf), so a leaf row's label lines up with the others' labels. */
.cp-tab[aria-expanded]::before { content: ""; flex: none; width: 0; height: 0;
  margin-left: -4px; border: 5px solid transparent;
  border-left-color: currentColor; border-right-width: 0;
  transition: transform var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-emphasized); }
.cp-tab[aria-expanded="true"]::before { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .cp-tab[aria-expanded]::before { transition-duration: 0.01ms; } }
/* Collapse is a JS-only state: a no-JS client shows every panel, so it must show every branch —
   which is also the only way its rows stay reachable, since the arrow keys never bind there. One
   rule for all four levels: a collapsed row hides the list it owns. */
html.cp-js .cp-tab[aria-expanded="false"] + .cp-tree-children,
html.cp-js .cp-tree-link[aria-expanded="false"] + .cp-tree-children { display: none; }
/* The twisty, drawn for any row that can expand — group and component alike. */
.cp-tree-link[aria-expanded]::before { content: ""; flex: none; width: 0; height: 0;
  margin-left: -6px; border: 4px solid transparent;
  border-left-color: currentColor; border-right-width: 0;
  transition: transform var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-emphasized); }
.cp-tree-link[aria-expanded="true"]::before { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) {
  .cp-tree-link[aria-expanded]::before { transition-duration: 0.01ms; }
}
/* Level three and four. Each nests one step further in and loses a little weight, so depth reads
   at a glance rather than having to be counted: group → component → variant. */
/* A component row jumps to a card, so a card needs the same clearance under the sticky toolbar
   that sections and sub-groups already get — without it every component jump parks the preview's
   top edge behind the toolbar. */
.cp-card { scroll-margin-top: calc(var(--cp-sticky-tools, 64px) + 12px); }
.cp-tree-components { margin: 2px 0 4px 12px;
  border-left: 1px solid var(--md-sys-color-outline-variant); }
.cp-tree-component, .cp-tree-page { display: flex; align-items: center; gap: 6px; min-height: 30px;
  padding: 4px 10px; border-radius: 0 var(--md-sys-shape-corner-full)
    var(--md-sys-shape-corner-full) 0;
  font-size: var(--md-sys-typescale-label-medium-size);
  color: var(--cp-fg-muted); text-decoration: none;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-tree-component:hover, .cp-tree-page:hover { color: var(--md-sys-color-on-surface);
  text-decoration: none;
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-tree-component[aria-expanded="true"] { color: var(--md-sys-color-on-surface);
  font-weight: 500; }
.cp-tree-variants { margin: 0 0 4px 12px;
  border-left: 1px dashed var(--md-sys-color-outline-variant); }
/* A variant row leaves the page — it is the one row that navigates to the viewer, because the
   grid folds these renders out and there is nothing here to jump to. */
.cp-tree-variant { display: block; min-height: 26px; padding: 3px 10px;
  font-size: var(--md-sys-typescale-label-small-size);
  color: var(--cp-fg-faint); text-decoration: none; }
.cp-tree-variant:hover { color: var(--md-sys-color-primary); text-decoration: underline; }
.cp-tree-component:focus-visible, .cp-tree-variant:focus-visible,
.cp-tree-page:focus-visible, .cp-tree-pages-row:focus-visible {
  outline: 3px solid var(--md-sys-color-secondary); outline-offset: -2px; }
/* The design file's own PAGES, at the foot of the tree. A page is a view of the specification
   rather than a part of this catalog's inventory, and its rows leave the page instead of jumping
   within it — so the branch sits below the components behind a hairline, wearing the group row's
   shape so it still reads as one of the tree's top-level destinations. */
.cp-tree-node.cp-tree-pages { margin-top: 10px; padding-top: 8px;
  border-top: 1px solid var(--md-sys-color-outline-variant); }
/* A section's sub-groups, indented under one guide line. */
.cp-tree-children { margin: 2px 0 8px 22px;
  border-left: 1px solid var(--md-sys-color-outline-variant); }
.cp-tree-group, .cp-tree-pages-row { display: flex; align-items: center; gap: 8px; min-height: 32px;
  padding: 5px 12px; border-radius: 0 var(--md-sys-shape-corner-full)
    var(--md-sys-shape-corner-full) 0;
  font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking);
  color: var(--cp-fg-muted); text-decoration: none;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-tree-group:hover, .cp-tree-pages-row:hover { color: var(--md-sys-color-on-surface);
  text-decoration: none;
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
/* Where you ARE — set by the scroll-spy as the grid moves under you, and on click. */
.cp-tree-group[aria-current="true"] { color: var(--md-sys-color-primary); font-weight: 500;
  box-shadow: inset 2px 0 0 var(--md-sys-color-primary); }
.cp-tab:focus-visible, .cp-tree-group:focus-visible {
  outline: 3px solid var(--md-sys-color-secondary); outline-offset: -2px; }
/* A section row's count is an M3 badge — a small tonal pill riding at the end of the row. */
.cp-tab-count { margin-left: auto; font-size: var(--md-sys-typescale-label-small-size);
  letter-spacing: var(--md-sys-typescale-label-small-tracking); font-weight: 500;
  padding: 2px 8px; border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container); }
.cp-tab[aria-selected="true"] .cp-tab-count { background: var(--md-sys-color-primary);
  color: var(--md-sys-color-on-primary); }
/* A group row's count is plain text: two badge styles down one column would compete. */
.cp-tree-count { margin-left: auto; font-size: var(--md-sys-typescale-label-small-size);
  font-weight: 400; font-variant-numeric: tabular-nums; color: var(--cp-fg-faint); }
.cp-section { margin: 0; scroll-margin-top: calc(var(--cp-sticky-tools, 64px) + 16px); }
.cp-section[hidden] { display: none; }
.cp-section-head { margin: 0 0 16px; font-size: var(--md-sys-typescale-title-medium-size);
  line-height: var(--md-sys-typescale-title-medium-line);
  letter-spacing: var(--md-sys-typescale-title-medium-tracking); font-weight: 500; }
html.cp-js .cp-section-head { display: none; }
/* `scroll-margin-top` because both are jump targets — a tree row scrolls to a `.cp-subgroup`, and
   a no-JS client follows `#cp-panel-<slug>` to a `.cp-section`. Without it the heading they were
   asked for lands at viewport zero, under the sticky toolbar. */
.cp-subgroup { margin: 0 0 30px; scroll-margin-top: calc(var(--cp-sticky-tools, 64px) + 16px); }
.cp-subgroup[hidden] { display: none; }
.cp-group-head { margin: 26px 0 12px; font-size: var(--md-sys-typescale-label-medium-size);
  line-height: var(--md-sys-typescale-label-medium-line); font-weight: 500; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--cp-fg-faint); }
.cp-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 18px; }
/* The Theme picker carries one chip per configured theme (the baked light/dark pair plus any
   app-declared @ThemeCatalog theme), so it wraps rather than overflowing a narrow viewport.
   That is exactly why it is NOT an M3 segmented button: a segmented track is specified for a
   small, fixed set of connected options, and a connected track that wraps draws one keyline
   around two or three ragged rows. A catalog with a handful of declared themes lands there
   immediately, so the picker is a flow row of M3 *filter chips* instead — the component M3
   specifies for a wrapping, variable-length set — sharing .cp-state-btn's shape and the same
   `secondary-container` selected fill the segmented track used. */
.cp-theme { display: inline-flex; flex-wrap: wrap; gap: 8px; max-width: 100%; }
.cp-states { display: inline-flex; flex-wrap: wrap; gap: 8px; margin: 0 0 10px; }
.cp-axis-label { align-self: center; margin-right: 2px; color: var(--cp-fg-faint);
  font-size: var(--md-sys-typescale-label-small-size); font-weight: 500; letter-spacing: 0.06em;
  text-transform: uppercase; }
/* M3 filter chip. */
.cp-state-btn { display: inline-flex; align-items: center; min-height: 32px;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-small); background: transparent;
  color: var(--md-sys-color-on-surface-variant); font: inherit;
  font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  padding: 6px 16px; cursor: pointer; text-decoration: none;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-state-btn:hover { color: var(--md-sys-color-on-surface);
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-state-btn[aria-current="page"] { background: var(--md-sys-color-secondary-container);
  border-color: transparent; color: var(--md-sys-color-on-secondary-container); font-weight: 500; }
/* `<cp-bg-toggle>` (serve-components.js) renders the Transparent button into its own light DOM.
   `display: contents` drops the wrapper out of layout entirely, so the .cp-bg-btn inside stays the
   toolbar's own flex item and the upgraded control lays out exactly as the bare button did. */
cp-bg-toggle { display: contents; }
/* The controller elements (serve-components.js): each only wires behaviour onto markup the server
   already rendered — remembered fold state, the phone's toolbar reflow, the viewer's drawers — and
   none of them has anything to draw. An autonomous custom element defaults to `display: inline`,
   so without this each would sit in its page's flow as an empty inline box. */
cp-group-memory, cp-catalog-toolbar, cp-viewer-drawers, cp-parity-lanes,
cp-rc-lanes, cp-spec-compare, cp-inspect-layers, cp-catalog-live,
cp-design-page, cp-compare-wall, cp-reference-compare { display: none; }
/* `<cp-parity-scores>` DOES render — the whole "Visual differences" band — so it drops out
   of layout rather than out of sight, leaving the `<section>` it renders as the flow's own
   child exactly as the server-emitted one was. */
cp-parity-scores { display: contents; }
/* …and the same filter chip for the theme chips and for the two standalone toggles that used to
   be segmented pairs — Transparent (<cp-bg-toggle>) and the viewer's Fit width (.cp-zoom-toggle).
   One rule so everything on the viewer bar cannot drift apart. `aria-pressed` is a toggle chip's
   selected state, exactly as `aria-current` is a navigation chip's. A disabled chip keeps M3's 38%
   on-surface treatment rather than vanishing — a theme this lane cannot render is information. */
.cp-theme-btn, .cp-bg-btn { display: inline-flex; align-items: center; min-height: 32px;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-small); background: transparent;
  color: var(--md-sys-color-on-surface-variant); font: inherit;
  font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  padding: 6px 16px; cursor: pointer;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-theme-btn:hover:not(:disabled), .cp-bg-btn:hover:not(:disabled) {
  color: var(--md-sys-color-on-surface);
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-theme-btn:disabled, .cp-bg-btn:disabled {
  color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
  border-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
  cursor: default; }
.cp-theme-btn[aria-pressed="true"], .cp-bg-btn[aria-pressed="true"] {
  background: var(--md-sys-color-secondary-container); border-color: transparent;
  color: var(--md-sys-color-on-secondary-container); font-weight: 500; }
/* --- A Light / Dark chip is a TASTER of the theme it selects.
   The chips used to be identical outlined pills that differed only in the word on them, so the
   answer to "what does Dark actually look like here?" was a click and a wait. Each of these now
   pins its own `color-scheme`, which re-resolves every `light-dark()` pair in the token layer —
   including a served catalog's own palette — in THAT chip's mode. So on `/wear-m3/` the Dark chip
   is painted in wear-m3's own near-black-and-cyan and the Light chip in its light projection,
   whichever way round the page itself is; the pair reads as two swatches of one system rather than
   two labels. Costs nothing: no extra data, no render, one property per chip.
   The baked axis and any declared theme with an unambiguous light/dark mode are tasted. `Default`
   and an unqualified `theme:<providerFqn>` keep the plain outlined pill: the page cannot resolve a
   mode for those without guessing. */
.cp-theme-btn:is([data-theme-choice="light"], [data-theme-mode="light"],
    [data-compare-theme="light"]) { color-scheme: light; }
.cp-theme-btn:is([data-theme-choice="dark"], [data-theme-mode="dark"],
    [data-compare-theme="dark"]) { color-scheme: dark; }
.cp-theme-btn:is([data-theme-choice="light"], [data-theme-choice="dark"], [data-theme-mode],
    [data-compare-theme="light"], [data-compare-theme="dark"]) {
  background: var(--md-sys-color-surface-container-low);
  color: var(--md-sys-color-on-surface);
  border-color: var(--md-sys-color-outline-variant); }
.cp-theme-btn:is([data-theme-choice="light"], [data-theme-choice="dark"], [data-theme-mode],
    [data-compare-theme="light"], [data-compare-theme="dark"]):hover:not(:disabled) {
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%,
    var(--md-sys-color-surface-container-low)); }
/* A DISABLED chip — the viewer greys the Day/Night pair on a fixed lane (the spec view, an `.rc`
   canvas) — keeps its swatch and dims its LABEL, rather than falling back to the shared
   transparent treatment. Both halves of that matter. The shared treatment fades the label to 38%
   of `on-surface`, and on a chip pinned to the opposite scheme that is near-white text on a light
   page (or near-black on a dark one): an unavailable option that has vanished rather than one that
   reads as unavailable. Dimming *within the chip's own scheme* keeps the same 38% relationship
   against the surface it is actually painted on. Everything here therefore stays inside one
   `color-scheme`, which is also why the pin above is unconditional: a rule that dropped the pin on
   a state change would leave the chip's colours resolved in one mode and its label in another. */
.cp-theme-btn:is([data-theme-choice="light"], [data-theme-choice="dark"], [data-theme-mode],
    [data-compare-theme="light"], [data-compare-theme="dark"]):disabled {
  background: var(--md-sys-color-surface-container-low);
  color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
  border-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
  opacity: 0.7; }
/* Selection is a RING, not a fill swap: the fill is the swatch — the whole point of the chip — so
   replacing it to say "selected" would hide the theme at exactly the moment you picked it. The ring
   is drawn in the chip's own accent, as a `box-shadow` rather than an `outline` so it doesn't
   collide with the focus ring every control in this sheet shares, and **inset** so it stays inside
   the button's own box. That last part is not a style choice: the viewer's `.cp-theme-bar` is a
   horizontal scroller (`overflow-x: auto; overflow-y: hidden`) with padding on the bottom edge
   only, so an outward ring is clipped flat along the top — and along the outer edge of the first
   and last chip — leaving a "selected" marker with two sides missing. An inset ring is clip-proof
   in any container, and on a filled swatch it reads as the chip's own keyline anyway. */
.cp-theme-btn:is([data-theme-choice="light"], [data-theme-choice="dark"], [data-theme-mode],
    [data-compare-theme="light"], [data-compare-theme="dark"])[aria-pressed="true"] {
  background: var(--md-sys-color-surface-container-low);
  color: var(--md-sys-color-on-surface);
  border-color: transparent; font-weight: 600;
  box-shadow: inset 0 0 0 2px var(--md-sys-color-primary); }
.cp-theme-btn:is([data-theme-choice="light"], [data-theme-choice="dark"], [data-theme-mode],
    [data-compare-theme="light"], [data-compare-theme="dark"])[aria-pressed="true"]:disabled {
  color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--md-sys-color-primary) 38%, transparent); }
/* Trust badges. Their COLOURS stay literal (a verdict must read the same in every design system),
   but the shape is M3's: a fully-rounded tonal container, no keyline. */
.cp-badge { display: inline-block; margin-left: 8px; padding: 4px 10px;
  border-radius: var(--md-sys-shape-corner-full);
  font-size: var(--md-sys-typescale-label-medium-size);
  letter-spacing: var(--md-sys-typescale-label-medium-tracking); font-weight: 500;
  vertical-align: middle; white-space: nowrap; }
.cp-badge--trusted { background: light-dark(#d7f0dd, #0d3a1c); color: light-dark(#10521f, #86dc9d); border: 0; }
.cp-badge--unverified { background: light-dark(#ffdfb0, #4a3000); color: light-dark(#573100, #ffd8a3); border: 0; }
/* The degradation banner takes M3's *error/warning container* shape: a tonal fill at the large
   corner with no outline, so it reads as a surface rather than as a boxed alert. */
.cp-degrade { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px;
  margin: 0 0 12px; padding: 12px 16px; border-radius: var(--md-sys-shape-corner-medium);
  font-size: var(--md-sys-typescale-body-medium-size); line-height: 1.4;
  background: light-dark(#ffdfb0, #4a3000); color: light-dark(#573100, #ffd8a3); border: 0; }
.cp-degrade-icon { font-weight: 700; }
.cp-degrade-item { display: inline; }
/* Historical permalinks. The pin banner takes the same tonal-container shape as the degradation
   banner above — it is the same kind of statement, "what you are looking at is not the default" —
   but in the primary family rather than the warning one: a pinned page is a deliberate choice, not
   a degraded state. */
.cp-pinned { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px;
  margin: 0 0 10px; padding: 12px 16px; border-radius: var(--md-sys-shape-corner-medium);
  font-size: var(--md-sys-typescale-body-medium-size); line-height: 1.4;
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container); }
.cp-pinned code { font-size: 0.86em; }
.cp-pinned-current { font-weight: 650; }
/* The revision control is a menu, not a wall: an outlined trigger that names the revision the page
   is on, and a floating list of the others — the same `<details>`-as-menu shape as the settings
   button above. A dozen publishes as chips took four lines of page before the preview and made the
   reader hunt for the highlighted one; closed, this is a single control that already says which
   version you are looking at. */
.cp-revisions { position: relative; display: inline-block; margin: 0 0 12px;
  font-size: 0.82rem; color: var(--cp-fg-muted); }
.cp-revisions-btn { display: inline-flex; align-items: baseline; gap: 8px; padding: 7px 14px;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-full);
  color: var(--md-sys-color-on-surface-variant); cursor: pointer; list-style: none;
  user-select: none;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-revisions-btn::-webkit-details-marker { display: none; }
.cp-revisions-btn:hover, .cp-revisions[open] > .cp-revisions-btn {
  color: var(--md-sys-color-on-surface);
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-revisions-key { font-weight: 650; }
.cp-revisions-caret { font-size: 0.9em; opacity: 0.7; }
.cp-revisions[open] .cp-revisions-caret { transform: rotate(180deg); }
/* Menu surface: M3 elevated container, capped so a long branch history scrolls inside the popup
   instead of pushing the page around. */
.cp-revisions-menu { position: absolute; left: 0; top: calc(100% + 6px); z-index: 50;
  width: max-content; max-width: min(420px, calc(100vw - 32px));
  max-height: min(60vh, 420px); overflow-y: auto; padding: 8px 4px;
  border: 0; border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-high);
  box-shadow: var(--md-sys-elevation-level2); }
.cp-revision-list { display: flex; flex-direction: column; gap: 2px; margin: 0; }
.cp-revision { display: flex; align-items: baseline; gap: 10px; padding: 9px 12px;
  border: 0; border-radius: var(--md-sys-shape-corner-small);
  color: var(--cp-fg); text-decoration: none; white-space: nowrap; }
.cp-revision:hover {
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-revision[aria-current="true"] { background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container); }
.cp-revision-date { font-variant-numeric: tabular-nums; }
.cp-revision-sha { font-size: 0.92em; opacity: 0.85; margin-left: auto; }
.cp-revision-tag { font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.06em;
  opacity: 0.8; }
.cp-revision-tag--pinned { color: var(--md-sys-color-primary); opacity: 1; }
.cp-revision-note { margin: 8px 12px 4px; max-width: 44ch; color: var(--cp-fg-muted);
  font-size: var(--md-sys-typescale-body-small-size);
  line-height: var(--md-sys-typescale-body-small-line); }
/* Status page (GET /status): stat tiles, config grid, and the catalog/daemon/failure tables. */
.cp-status-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 10px; margin: 0 0 22px; max-width: 860px; }
/* Stat tiles are M3 filled cards at the medium corner: tonal fill, no keyline. */
.cp-stat { border: 0; border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-low); padding: 14px 16px; }
.cp-stat-key { font-size: var(--md-sys-typescale-label-medium-size);
  line-height: var(--md-sys-typescale-label-medium-line); text-transform: uppercase;
  letter-spacing: 0.06em; font-weight: 500; color: var(--cp-fg-faint); }
.cp-stat-val { margin-top: 4px; font-size: var(--md-sys-typescale-title-large-size);
  line-height: var(--md-sys-typescale-title-large-line); font-weight: 400; color: var(--cp-fg); }
.cp-status-sec { margin: 26px 0 12px; font-size: var(--md-sys-typescale-title-medium-size);
  line-height: var(--md-sys-typescale-title-medium-line);
  letter-spacing: var(--md-sys-typescale-title-medium-tracking); font-weight: 500; }
.cp-status-scroll { overflow-x: auto; max-width: 100%; margin: 0 0 8px; }
.cp-table { border-collapse: collapse; font-size: var(--md-sys-typescale-body-medium-size);
  letter-spacing: var(--md-sys-typescale-body-medium-tracking); max-width: 860px; min-width: 100%; }
.cp-table th { text-align: left; font-weight: 500; color: var(--cp-fg-muted);
  font-size: var(--md-sys-typescale-label-medium-size);
  text-transform: uppercase; letter-spacing: 0.06em; padding: 10px 12px 10px 0;
  border-bottom: 1px solid var(--md-sys-color-outline-variant); white-space: nowrap; }
.cp-table td { padding: 10px 12px 10px 0;
  border-bottom: 1px solid var(--md-sys-color-outline-variant); vertical-align: top; }
.cp-table tr:last-child td { border-bottom: none; }
.cp-table code { font-family: var(--md-sys-typescale-mono-font); font-size: 0.76rem; padding: 1px 5px;
  border-radius: var(--md-sys-shape-corner-extra-small);
  background: var(--md-sys-color-surface-container-high); }
.cp-ok { color: #1e7a34; font-weight: 600; }
.cp-muted { color: var(--cp-fg-faint); font-size: 0.76rem; }
/* Design parity page (GET /<system>/parity): the coverage meter, the merged two-lane activity
   feed, and the mapping-gap tables. The lane colours are SEMANTIC (code vs. design), so — like the
   trusted/unverified badges — they stay literal rather than following a catalog's palette: a row
   must mean the same thing on /wear-m3 as on /m3-catalog. Everything else here is drawn from the
   chrome tokens and needs no dark counterpart. */
/* M3 linear progress indicator: a 4dp full-round track in `secondary-container` with a `primary`
   active bar — the shape Material uses for a determinate meter. */
.cp-parity-meter { max-width: 860px; height: 4px; border-radius: var(--md-sys-shape-corner-full);
  overflow: hidden; background: var(--md-sys-color-secondary-container); margin: 0 0 8px; }
.cp-parity-meter-fill { height: 100%; background: var(--md-sys-color-primary);
  border-radius: var(--md-sys-shape-corner-full); }
.cp-parity-sub { margin: 18px 0 6px; font-size: var(--md-sys-typescale-title-medium-size);
  line-height: var(--md-sys-typescale-title-medium-line);
  letter-spacing: var(--md-sys-typescale-title-medium-tracking); font-weight: 500;
  color: var(--cp-fg-soft); }
.cp-parity-lane { display: inline-block; padding: 3px 10px;
  border-radius: var(--md-sys-shape-corner-full);
  font-size: var(--md-sys-typescale-label-small-size); font-weight: 500; text-transform: uppercase;
  letter-spacing: 0.06em; white-space: nowrap; }
.cp-parity-lane--code { background: light-dark(#d7f0dd, #0d3a1c); color: light-dark(#10521f, #86dc9d); border: 0; }
.cp-parity-lane--figma { background: light-dark(#e5deff, #372b70); color: light-dark(#3c2b80, #c9c0ff); border: 0; }
.cp-parity-feed { list-style: none; margin: 0; padding: 0; max-width: 860px; }
.cp-parity-entry { display: flex; gap: 16px; padding: 12px 0;
  border-bottom: 1px solid var(--cp-surface-2); }
.cp-parity-entry:last-child { border-bottom: none; }
.cp-parity-entry[hidden] { display: none; }
.cp-parity-entry--resolved { opacity: 0.55; }
.cp-parity-when { flex: 0 0 128px; font-size: 0.74rem; color: var(--cp-fg-faint);
  font-variant-numeric: tabular-nums; padding-top: 2px; }
.cp-parity-body { flex: 1 1 auto; min-width: 0; }
.cp-parity-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; }
.cp-parity-title { font-size: 0.86rem; color: var(--cp-fg); overflow-wrap: anywhere; }
.cp-parity-meta { margin-top: 3px; display: flex; flex-wrap: wrap; gap: 10px;
  font-size: 0.74rem; color: var(--cp-fg-faint); }
.cp-parity-who { color: var(--cp-fg-muted); }
.cp-parity-targets { margin-top: 5px; font-size: 0.76rem; }
.cp-parity-chips { list-style: none; margin: 0 0 8px; padding: 0;
  display: flex; flex-wrap: wrap; gap: 6px; }
.cp-parity-comparisons { max-width: 860px; margin-top: 30px; padding-top: 18px;
  border-top: 1px solid var(--md-sys-color-outline-variant); }
.cp-parity-comparisons-title { font-size: var(--md-sys-typescale-title-medium-size);
  line-height: var(--md-sys-typescale-title-medium-line); font-weight: 500;
  color: var(--cp-fg); }
.cp-parity-comparisons .cp-table { min-width: 620px; }
.cp-parity-missing { color: var(--md-sys-color-error); font-weight: 500; }
@media (max-width: 640px) {
  .cp-parity-entry { flex-direction: column; gap: 4px; }
  .cp-parity-when { flex: none; }
}
.cp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 18px; }
.cp-syslist { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 22px; }
/* 220px hero + a meta row that takes the rest, so the meta block can bottom-align its count
   line (`margin-top: auto`) and a row of cards agrees on where that line sits. Selected on
   BOTH classes deliberately: `.cp-card { display: block }` is declared later in this sheet and
   at equal specificity it won, which silently made the row template inert. */
.cp-card.cp-sys { display: grid; grid-template-rows: 220px 1fr; }
.cp-syslist .cp-imgwrap { min-height: 0; height: 220px; overflow: hidden; }
.cp-syslist .cp-imgwrap > img { width: 100%; height: 100%; max-height: none; object-fit: contain; }
/* A front-door card is a poster, not a table row: the meta block gets its own padding, the title
   and the library coordinate sit on separate rhythms, and the count line is divided off so a
   three-line coordinate can't run straight into it. */
/* The meta block fills the card's second row so the count line can be pushed to the bottom: a
   one-line coordinate and a three-line one then still land their footers on the same baseline
   across a row, which is what makes the grid read as a grid rather than as ragged tiles. */
.cp-sys .cp-meta { display: flex; flex-direction: column; padding: 14px 16px 15px; }
.cp-sys-title { font-size: var(--md-sys-typescale-title-medium-size);
  line-height: var(--md-sys-typescale-title-medium-line);
  letter-spacing: var(--md-sys-typescale-title-medium-tracking); font-weight: 500; }
.cp-sys .cp-id { margin-top: 5px; font-size: var(--md-sys-typescale-body-small-size);
  letter-spacing: var(--md-sys-typescale-body-small-tracking); }
.cp-sys-desc { margin-top: 7px; font-size: var(--md-sys-typescale-body-small-size);
  letter-spacing: var(--md-sys-typescale-body-small-tracking); color: var(--cp-fg-muted);
  line-height: 1.5; }
/* `auto` collapses to zero once the metadata fills the card, so the padding — not the margin — is
   what guarantees the count never crowds the line above it. */
.cp-sys-foot { margin-top: auto; padding-top: 12px;
  border-top: 1px solid var(--md-sys-color-outline-variant);
  font-size: var(--md-sys-typescale-label-medium-size);
  letter-spacing: var(--md-sys-typescale-label-medium-tracking); color: var(--cp-fg-muted); }
.cp-sys-noimg { font-size: var(--md-sys-typescale-body-small-size); color: var(--cp-fg-faint); }
/* M3 *elevated card*: `surface-container-low` at the medium corner, resting at elevation level 1,
   with no outline — a card is separated from the page by tone and shadow, not by a keyline. */
.cp-card { position: relative; isolation: isolate; border: 0;
  border-radius: var(--md-sys-shape-corner-medium);
  overflow: hidden; background: var(--md-sys-color-surface-container-low); display: block;
  color: inherit; box-shadow: var(--md-sys-elevation-level1);
  transition: transform var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-emphasized),
    box-shadow var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard); }
.cp-card[hidden] { display: none; }
/* Card hover — the whole tile is the affordance, so it must NOT read as a hyperlink. The sheet's
   global `a:hover { text-decoration: underline }` otherwise underlined every line of a card's meta
   at once (title, id, coordinate, count), which looked like four separate links rather than one
   target; the card answers the pointer as an object instead: it lifts off the page, casts a
   shadow, takes an accent rim, and wipes an accent bar along its top edge while its artwork eases
   in a little. Focus-visible gets the identical treatment so keyboard navigation is not a
   second-class path. Everything here is transform/opacity-only, so it costs no layout. */
/* Hover/focus is M3's card interaction: rise to elevation level 3 and take a `primary` STATE
   LAYER over the whole tile (the ::after below), rather than swapping to another fill or growing
   an accent rim. The small lift is kept — it is what tells a pointer the tile itself is the
   target — but it is now the shadow, not a border colour, that carries the change. */
.cp-card:hover, .cp-card:focus-visible {
  transform: translateY(-2px); text-decoration: none;
  box-shadow: var(--md-sys-elevation-level3); }
.cp-card:focus-visible { outline: 3px solid var(--md-sys-color-secondary); outline-offset: 2px; }
.cp-card::after { content: ""; position: absolute; inset: 0; z-index: 3;
  background: var(--md-sys-color-primary); opacity: 0;
  transition: opacity var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
  pointer-events: none; }
.cp-card:hover::after { opacity: var(--md-sys-state-hover-opacity); }
.cp-card:focus-visible::after { opacity: var(--md-sys-state-focus-opacity); }
.cp-card:hover .cp-sys-title, .cp-card:focus-visible .cp-sys-title,
.cp-card:hover .cp-label, .cp-card:focus-visible .cp-label { color: var(--md-sys-color-primary); }
.cp-card .cp-imgwrap img { transition: transform 0.3s cubic-bezier(0.2, 0.7, 0.3, 1); }
.cp-card:hover .cp-imgwrap img, .cp-card:focus-visible .cp-imgwrap img { transform: scale(1.035); }
/* A card mid-render is already animating (fade + spinner); leave its artwork alone so the two
   effects don't fight, and never zoom for a visitor who asked for less motion. */
.cp-card.cp-reloading:hover .cp-imgwrap img { transform: none; }
@media (prefers-reduced-motion: reduce) {
  .cp-card, .cp-card::after, .cp-card .cp-imgwrap img { transition-duration: 0.01ms; }
  .cp-card:hover, .cp-card:focus-visible { transform: none; }
  .cp-card:hover .cp-imgwrap img, .cp-card:focus-visible .cp-imgwrap img { transform: none; }
}
.cp-card--render-failed { border: 1px solid color-mix(in srgb, var(--md-sys-color-error) 55%, transparent); }
.cp-card--render-failed > summary { list-style: none; cursor: pointer; }
.cp-card--render-failed > summary::-webkit-details-marker { display: none; }
.cp-render-failure { flex-direction: column; gap: 7px; padding: 18px; text-align: center;
  color: var(--md-sys-color-error); background: color-mix(in srgb, var(--md-sys-color-error-container) 55%, var(--md-sys-color-surface)); }
.cp-render-failure > span:last-child { color: var(--md-sys-color-on-error-container); font-size: 12px; max-width: 34ch; }
.cp-render-failure-mark { display: grid; place-items: center; width: 32px; height: 32px; border-radius: 50%;
  border: 1px solid currentColor; font-size: 19px; font-weight: 700; }
.cp-render-failure-detail { position: relative; z-index: 4; padding: 12px 14px 16px; border-top: 1px solid var(--md-sys-color-outline-variant);
  color: var(--md-sys-color-on-surface); overflow-wrap: anywhere; }
.cp-render-failure-detail p, .cp-render-failure-frame { color: var(--md-sys-color-on-surface-variant); font-size: 12px; }
.cp-render-stack pre { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 280px; overflow: auto;
  padding: 10px; border-radius: 8px; background: var(--md-sys-color-surface-container-lowest); font-size: 11px; }
.cp-render-failure-summary { margin: 12px 0 18px; padding: 12px 14px; border: 1px solid color-mix(in srgb, var(--md-sys-color-error) 45%, transparent);
  border-radius: 12px; color: var(--md-sys-color-on-error-container); background: color-mix(in srgb, var(--md-sys-color-error-container) 45%, var(--md-sys-color-surface)); }
.cp-render-failure-summary > strong { color: var(--md-sys-color-error); }
.cp-render-failure-summary ul { display: grid; gap: 4px; margin: 8px 0 0; padding: 0; list-style: none; }
.cp-render-failure-summary li { display: flex; justify-content: space-between; gap: 16px; font-size: 12px; }
.cp-render-failure-summary li span { overflow-wrap: anywhere; }
.cp-imgwrap { position: relative; display: flex; align-items: center; justify-content: center; min-height: 88px;
  background: var(--cp-surface); padding: 12px; }
/* `width`/`height` stay auto so the max-* pair alone decides the used size and the image can
   never be stretched. The front door's prebaked heroes DO carry width/height attributes — they
   give the browser the aspect ratio up front (no layout shift) — but those must not become a
   definite width, or a max-height clamp (the narrow-viewport rule below drops it to 200px)
   would squash the height without narrowing the width. The heroes are rasterised at 2x this
   box, so clamping here is what makes them crisp rather than merely small. */
.cp-imgwrap img { max-width: 100%; max-height: 240px; width: auto; height: auto; display: block; }
/* A framed thumbnail: a clip window whose aspect-ratio is the component box, that crops away a
   sticker's empty watch canvas. The window's natural width is the box (so it never upscales past
   1x), but `max-width: 100%` lets it shrink to a narrow grid card instead of overflowing it —
   the render <img> inside is absolutely positioned and sized + offset in PERCENTAGES of the box,
   so the whole frame scales as one when the card is narrower than the box. Overrides fit-to-box. */
.cp-crop { position: relative; overflow: hidden; display: block; max-width: 100%; }
.cp-imgwrap .cp-crop img { position: absolute; max-width: none; max-height: none; margin: 0; }
/* Sticker backing: the preview server shows components on a solid surface by DEFAULT, so a
   transparent sticker reads like a real component instead of washing out. The header's
   Transparent toggle flips the whole page to a checkerboard (html.cp-bg-transparent)
   to inspect the raw transparent PNG; the choice persists per-visitor in localStorage['cp-bg']. */
html.cp-bg-transparent .cp-imgwrap, html.cp-bg-transparent .cp-stage {
  background: repeating-conic-gradient(var(--cp-surface-2) 0% 25%, var(--cp-surface) 0% 50%) 50% / 16px 16px; }
/* Back each card by its OWN render theme: a light-rendered sticker always sits on a light
   surface and a dark-rendered one on a dark surface, independent of the browser's color scheme
   or the explicitly-selected catalog theme. Without this a transparent light sticker (dark
   strokes) shown on the dark browser backing — or dark on light — washes to near-invisible.
   Solid mode only; the Transparent toggle's checkerboard still wins via cp-bg-transparent.
   These two backdrops are deliberately LITERAL, not chrome custom properties: they are pinned to
   the render's own theme (that is the whole point), so neither the browser's colour scheme nor a
   catalog's palette may move them. The un-tagged `.cp-imgwrap`/`.cp-stage` default above does
   follow the palette — an unthemed sticker has no theme of its own to be backed by. */
html:not(.cp-bg-transparent) .cp-card[data-bg-theme="light"] .cp-imgwrap { background: #fff; }
html:not(.cp-bg-transparent) .cp-card[data-bg-theme="dark"] .cp-imgwrap { background: #1d1d20; }
/* The single-preview viewer's stage follows the preview's background theme the same way the grid
   thumbnails do (data-bg-theme on .cp-viewer, kept SEPARATE from the explicit-only
   data-card-theme filter axis): a dark variant — or any preview in a dark-first system like Wear
   — sits on a dark stage so a light-on-transparent render stays readable. The viewer JS keeps
   data-bg-theme in step with the chosen Theme (uiMode) so it doesn't clash after a re-render.
   Solid mode only; the Transparent checkerboard still wins via cp-bg-transparent. */
html:not(.cp-bg-transparent) .cp-viewer[data-bg-theme="dark"] .cp-stage { background: #1d1d20; }
html:not(.cp-bg-transparent) .cp-viewer[data-bg-theme="light"] .cp-stage { background: #fff; }
.cp-meta { padding: 12px 16px 14px; font-size: var(--md-sys-typescale-body-medium-size); }
.cp-label { font-size: var(--md-sys-typescale-title-small-size);
  line-height: var(--md-sys-typescale-title-small-line);
  letter-spacing: var(--md-sys-typescale-title-small-tracking); font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cp-id { color: var(--cp-fg-muted); font-size: var(--md-sys-typescale-body-small-size);
  letter-spacing: var(--md-sys-typescale-body-small-tracking);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cp-engage { margin-top: 3px; color: var(--cp-fg-faint);
  font-size: var(--md-sys-typescale-label-medium-size); }
.cp-viewer-engage { color: var(--cp-fg-faint);
  font-size: var(--md-sys-typescale-body-small-size); }
/* The viewer's identity line: name, trust verdict, id and view tally on ONE baseline. They answer
   the same question ("what am I looking at"), and as three stacked blocks they answered it three
   times over ~90px of the fold — on a laptop that was the difference between seeing the render and
   scrolling to it. They still wrap on a narrow viewport; they just don't insist on wrapping. */
.cp-preview-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 2px 12px;
  margin: 0 0 8px; }
.cp-preview-title { margin: 0; font-size: var(--md-sys-typescale-headline-small-size);
  line-height: var(--md-sys-typescale-headline-small-line); }
.cp-preview-id { font-family: var(--md-sys-typescale-mono-font); padding: 2px 6px;
  border-radius: var(--md-sys-shape-corner-extra-small);
  background: var(--md-sys-color-surface-container-high); color: var(--cp-fg-muted);
  font-size: var(--md-sys-typescale-body-small-size);
  letter-spacing: var(--md-sys-typescale-body-small-tracking);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The four disclosures — Components, State/Variant, Theme, Overrides — ride at the end of that
   same identity row, pushed to the trailing edge by the auto margin. They are the page's controls
   over what is *shown*, so grouping them buys one place to look; keeping them on the title row
   rather than a strip of their own is what makes the fold they buy back a net gain. `align-self`
   overrides the row's baseline alignment: a 34px pill sitting on a headline's baseline hangs below
   the row it belongs to. */
.cp-head-toggles { display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin-left: auto; align-self: center; }
.cp-head-toggles .cp-revisions { margin: 0; font-size: inherit; }
.cp-head-toggles .cp-revisions-btn { min-height: 34px; padding: 6px 14px; }
/* The history menu fills itself in from a manifest, so it takes the row's sizing the same way its
   neighbour does rather than carrying a second set of numbers. `<cp-history-menu>` is the tag the
   server declares in this row; `display: contents` drops it out of layout so the `<details>` it
   renders is the flex item, exactly as the old script's free-standing `<details>` was — otherwise
   the row's `gap` would be spent on a wrapper that is empty whenever there is no timeline. */
cp-history-menu { display: contents; }
.cp-head-toggles .cp-history-menu { font-size: inherit; }
.cp-head-toggles .cp-history-btn { min-height: 34px; padding: 6px 14px; }
.cp-theme-menu { position: relative; }
.cp-theme-menu > summary { list-style: none; }
.cp-theme-menu > summary::-webkit-details-marker { display: none; }
.cp-theme-menu[open] > summary { background: var(--md-sys-color-secondary-container);
  border-color: transparent; color: var(--md-sys-color-on-secondary-container); }
.cp-theme-caret { font-size: 0.85em; opacity: 0.7; }
.cp-theme-menu[open] .cp-theme-caret { transform: rotate(180deg); }
.cp-theme-menu-panel { position: absolute; right: 0; top: calc(100% + 6px); z-index: 50;
  width: max-content; max-width: min(360px, calc(100vw - 32px)); padding: 8px;
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-high);
  box-shadow: var(--md-sys-elevation-level2); }
.cp-theme-menu-panel .cp-theme-bar { display: flex; flex-direction: column; align-items: stretch;
  gap: 2px; overflow: visible; padding: 0; }
.cp-theme-menu-panel .cp-theme-btn { min-width: 12em; width: 100%; text-align: left; }
/* The axis toggles are the drawer toggle's compact sibling: same pill, same selected language,
   but two-part — the axis it folds, then the value that axis currently holds. Four full-size
   drawer pills on the title row of a 1100px laptop wrap; at this size they don't. */
/* Both classes on one button, and `.cp-drawer-toggle` is declared further down this sheet — at
   equal specificity the later rule wins, so a bare `.cp-axis-toggle` here set `gap`/`min-height`/
   `padding` and then had all three taken straight back off it. Qualifying with both classes is what
   actually makes these compact, which is what keeps four controls beside the title instead of
   wrapping them onto a row of their own. */
.cp-drawer-toggle.cp-axis-toggle { gap: 6px; min-height: 34px; padding: 6px 14px; }
.cp-toggle-label { color: var(--cp-fg-faint);
  font-size: var(--md-sys-typescale-label-small-size); font-weight: 500; letter-spacing: 0.06em;
  text-transform: uppercase; }
.cp-axis-toggle[aria-expanded="true"] .cp-toggle-label {
  color: var(--md-sys-color-on-secondary-container); }
/* The value is the fact the folded row would otherwise have carried, so it is the emphasised half
   and it never ellipsises away to nothing. */
.cp-toggle-value { max-width: 14em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The folded component subtree. A plain block wrapper so `hidden` is all the toggle has to set;
   the `<nav class="cp-tree">` inside keeps the tree's own layout. */
.cp-axes[hidden] { display: none; }
/* The viewer's subtree is the catalog tree's markup, so it inherits the rails, the twisty and the
   row shapes for free — but NOT the sidebar geometry. `.cp-tree` is a sticky 216px column inside
   `.cp-catalog-body` above 960px; here it is a block above the stage, on a page that has no such
   grid, so both the stickiness and the outer margin are undone. Overriding rather than minting a
   parallel class is the point: a change to how a tree row looks has to reach both places. */
.cp-axes-tree { position: static; max-height: none; overflow: visible; margin: 0 0 10px;
  max-width: 760px; }
/* A long axis FLOWS into columns rather than running down the page. On the landing the tree lives
   in a 216px sidebar, where one column is the only option; here it sits full-width above the stage
   with the whole right-hand side empty, and `iconbutton-outlined`'s 22 size × width × shape renders
   became a 550px ladder of two-word labels — half the fold, to say what fits in four short columns.
   `column-width` rather than a column count so the browser picks the number that fits, and the
   rows stay one-per-line at any width. */
.cp-axes-tree .cp-tree-variants { columns: 11em; column-gap: 20px; }
.cp-axes-tree .cp-tree-variants li { break-inside: avoid; }
/* The subtree's ROOT is the component itself, so it hangs off nothing and needs no rail or indent
   of its own; the variants keep theirs, which is what still reads as a tree rather than a list. */
.cp-axes-tree > .cp-tree-list > li > .cp-tree-component { padding-left: 0; }
.cp-axes-tree .cp-tree-variants { margin-left: 10px; }
/* Where you ARE. The landing tree marks a row it scroll-spied to with `aria-current="true"`; here
   every row is a real navigation, so the render on screen is `aria-current="page"` — the same
   distinction the component nav drawer already draws, and the same tonal pill it uses. */
.cp-axes-tree .cp-tree-link[aria-current="page"] {
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container); font-weight: 500;
  border-radius: var(--md-sys-shape-corner-full); }
.cp-axes-tree .cp-tree-variant[aria-current="page"] { text-decoration: none; }
/* `inline-block`, not the landing tree's `block`: on the landing a variant row is one line in a
   216px column, but here the marker is a filled pill in a 420px block, and a block-level pill
   stretches the full width — a bar the length of the panel to say "this one", which reads as a
   heading rather than a selection. Shrunk to its label it reads as the chip it replaced. */
.cp-axes-tree .cp-tree-variant { display: inline-block; }
.cp-preview-primary { margin: 0 0 8px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.cp-viewer { display: flex; gap: 24px; flex-wrap: wrap; align-items: flex-start; }
/* The stage is the viewer's hero surface — an M3 elevated card at the large corner. */
.cp-stage { position: relative; flex: 1 1 360px; min-width: 280px; border: 0;
  border-radius: var(--md-sys-shape-corner-large);
  background: var(--md-sys-color-surface-container-low); padding: 16px;
  box-shadow: var(--md-sys-elevation-level1);
  display: flex; align-items: center; justify-content: center; min-height: 220px; }
.cp-stage img, .cp-stage canvas { max-width: 100%; height: auto; }
/* Override/theme renders can take a moment. Preserve the previous pixels as context, fade them
   just enough to signal that they are stale, and layer a compact spinner over the affected
   preview until its replacement image has loaded. */
.cp-card.cp-reloading .cp-imgwrap img,
.cp-viewer[data-reloading="true"] .cp-stage > img {
  opacity: 0.55; transition: opacity 0.15s ease;
}
.cp-card.cp-reloading .cp-imgwrap::after,
.cp-viewer[data-reloading="true"] .cp-stage::after {
  content: ""; position: absolute; left: 50%; top: 50%; z-index: 2;
  width: 24px; height: 24px; margin: -15px 0 0 -15px; border-radius: 50%;
  border: 3px solid rgba(91, 91, 207, 0.25); border-top-color: var(--cp-accent);
  animation: cp-reload-spin 0.75s linear infinite; pointer-events: none;
}
@keyframes cp-reload-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .cp-card.cp-reloading .cp-imgwrap::after,
  .cp-viewer[data-reloading="true"] .cp-stage::after { animation-duration: 1.5s; }
}
/* --- Long-press live: a daemon session started from the grid, in the card.
   The canvas is mounted as an absolute overlay on the thumbnail's slot (the img stays in flow
   with visibility:hidden), so the card keeps its exact geometry whether it is showing baked
   pixels or a live frame — a stream whose dimensions differ from the thumbnail scales into the
   same box instead of resizing the grid under the visitor. */
.cp-card-canvas { position: absolute; left: 12px; top: 12px; right: 12px; bottom: 12px;
  width: calc(100% - 24px); height: calc(100% - 24px); object-fit: contain; z-index: 2;
  touch-action: none; cursor: default; }
.cp-card.cp-card-live { outline: 2px solid var(--cp-accent); outline-offset: 1px; }
/* The card is a link, and the press that goes live must not also read as a text selection or a
   drag of the thumbnail. */
.cp-card-livable { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
.cp-card-livable img { -webkit-user-drag: none; }
.cp-card.cp-card-pressing { transform: scale(0.985); }
/* The affordance. Invisible until the card is hovered or focused — a grid of 80 cards must not
   grow 80 permanent badges — and always visible once the card is actually live, where it is the
   readout ("connecting…" / "live") rather than an invitation. */
.cp-live-hint, .cp-live-chip {
  position: absolute; left: 50%; bottom: 8px; transform: translateX(-50%); z-index: 3;
  padding: 2px 8px; border-radius: 999px; font-size: 0.62rem; font-weight: 600;
  letter-spacing: 0.02em; background: rgba(20, 20, 22, 0.72); color: #fff; pointer-events: none;
  white-space: nowrap;
}
.cp-live-hint { opacity: 0; transition: opacity 0.15s ease; }
.cp-card-livable:hover .cp-live-hint, .cp-card-livable:focus-visible .cp-live-hint { opacity: 1; }
.cp-card.cp-card-live .cp-live-hint { display: none; }
.cp-live-chip { background: var(--cp-accent); }
.cp-live-note { color: var(--cp-fg-muted); }
.cp-live-error {
  position: absolute; left: 50%; top: 50%; z-index: 4; transform: translate(-50%, -50%);
  max-width: calc(100% - 24px); padding: 5px 9px; border-radius: 6px;
  background: rgba(126, 24, 24, 0.92); color: #fff; font-size: 0.7rem; font-weight: 600;
  line-height: 1.2; text-align: center; pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .cp-live-hint { transition-duration: 0.01ms; }
  .cp-card.cp-card-pressing { transform: none; }
}
.cp-card.cp-theme-render-error .cp-imgwrap img { opacity: 0.55; }
.cp-theme-error {
  position: absolute; left: 50%; top: 50%; z-index: 2; transform: translate(-50%, -50%);
  max-width: calc(100% - 24px); padding: 5px 9px; border-radius: 6px;
  background: rgba(126, 24, 24, 0.92); color: #fff; font-size: 0.7rem; font-weight: 600;
  line-height: 1.2; text-align: center; pointer-events: none;
}
.cp-backend { position: absolute; top: 8px; right: 8px; font-size: 0.62rem; font-weight: 600;
  letter-spacing: 0.02em; padding: 2px 8px; border-radius: 999px; background: rgba(20,20,22,0.72);
  color: #fff; pointer-events: none; }
.cp-stage iframe { position: absolute; border: 0; background: transparent; opacity: 0;
  transition: opacity 0.15s ease; pointer-events: none; }
.cp-stage iframe.cp-wasm-live { opacity: 1; pointer-events: auto; }
/* Live daemon frames paint into the SAME absolute overlay box the Wasm iframe uses, pinned to
   the snapshot img's slot — so the stage is one fixed box every transport fills and toggling a
   mode never resizes or shifts it (the img keeps its layout slot underneath via visibility). */
.cp-stage canvas.cp-canvas-live { position: absolute; max-width: none; }

/* ---- The spec lane's comparison views ------------------------------------------------------
   The lane's default (`spec`) is untouched: the imported raster `<img>` is the whole stage, exactly
   as before this surface existed. Picking Diff / Triptych / Slider takes that `<img>` out of flow
   and gives the stage to `#cp-spec-compare`, whose panels are pre-normalised canvases (see
   spec-compare.js) so the three views are all in one pixel space. Only the panels the chosen view
   needs are in flow — one container reshaped by `data-view`, not four containers. */
.cp-viewer[data-mode="spec"][data-spec-view="diff"] .cp-spec-img,
.cp-viewer[data-mode="spec"][data-spec-view="triptych"] .cp-spec-img,
.cp-viewer[data-mode="spec"][data-spec-view="slider"] .cp-spec-img { display: none; }
.cp-spec-compare { display: flex; align-items: flex-start; justify-content: center; gap: 16px;
  flex-wrap: wrap; width: 100%; }
.cp-spec-compare[hidden] { display: none; }
.cp-spec-panel { margin: 0; display: flex; flex-direction: column; align-items: center; gap: 6px;
  min-width: 0; flex: 0 1 auto; position: relative; }
.cp-spec-annotation-layer { position: absolute; pointer-events: none; z-index: 2; }
.cp-spec-type-box { border-width: 2px; background: rgba(37, 99, 235, 0.18); }
.cp-spec-type-diff { align-items: flex-start; }
.cp-spec-type-field { display: block; font-size: 0.68rem; line-height: 1.35;
  font-variant-numeric: tabular-nums; }
/* `max-height` as well as `max-width`, both against auto dimensions, so an app-screen render
   (tall, and often 1000+ px of raster) is bounded by the viewport rather than pushing the stage
   past it — the aspect ratio rides along because the canvas is a replaced element. */
.cp-spec-panel canvas { display: block; max-width: 100%; max-height: 70vh; width: auto;
  height: auto; border-radius: var(--md-sys-shape-corner-small); }
/* Three-up: equal columns (`min-width: 0` so a wide frame shrinks rather than overflowing). */
.cp-spec-compare[data-view="triptych"] .cp-spec-panel { flex: 1 1 0; }
.cp-spec-compare[data-view="triptych"] .cp-spec-panel canvas { max-height: 52vh; }
/* The diff map is magenta-on-transparent, so it needs a ground of its own to read against —
   without one it inherits whatever the stage (or the Transparent checkerboard) is showing. */
.cp-spec-panel[data-cp-spec-panel="diff"] canvas {
  background: var(--md-sys-color-surface-container-highest); }
.cp-spec-panel figcaption { font-size: 0.7rem; font-weight: 600; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--cp-fg-muted); }
/* Hugs the frame (`fit-content`) so the seam control is as wide as the thing it moves the seam
   across — a fixed-width slider under a 200px component reads as belonging to the stage rather
   than to the frame. `min-width` keeps it draggable under a very narrow preview. */
.cp-spec-wipe { display: flex; flex-direction: column; align-items: center; gap: 10px;
  width: fit-content; min-width: min(260px, 100%); max-width: 100%; }
.cp-spec-wipe canvas { display: block; max-width: 100%; max-height: 70vh; width: auto;
  height: auto; cursor: ew-resize; border-radius: var(--md-sys-shape-corner-small);
  touch-action: none; }
.cp-spec-wipe-control { display: flex; align-items: center; gap: 10px; width: 100%;
  font-size: 0.7rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--cp-fg-muted); }
.cp-spec-wipe-control input { flex: 1; min-width: 0; }
/* `diff` shows the delta map alone; `triptych` shows all three panels; `slider` shows only the
   wipe. Everything else in the container stays out of flow so the stage sizes to what's shown. */
.cp-spec-compare[data-view="diff"] .cp-spec-panel:not([data-cp-spec-panel="diff"]),
.cp-spec-compare[data-view="diff"] .cp-spec-wipe,
.cp-spec-compare[data-view="triptych"] .cp-spec-wipe,
.cp-spec-compare[data-view="slider"] .cp-spec-panel { display: none; }
/* A three-up triptych of app screens is unreadable on a phone; stack it instead, which is what
   the focused comparison page does at the same breakpoint. */
@media (max-width: 720px) {
  .cp-spec-compare[data-view="triptych"] { flex-direction: column; align-items: center; }
}

.cp-live-row { flex-direction: row !important; align-items: center; gap: 6px !important; }
.cp-modes { display: flex; flex-direction: row; flex-wrap: wrap; gap: 6px 14px;
  font-size: 0.8rem; }
.cp-controls { position: sticky; top: 16px; flex: 0 0 240px; max-height: calc(100vh - 32px);
  overflow: auto; display: flex; flex-direction: column; gap: 14px; }
.cp-controls label { display: flex; flex-direction: column; gap: 4px; font-size: 0.8rem; }
.cp-controls input, .cp-controls select { padding: 5px 6px; font-size: 0.85rem; }
.cp-status { font-size: 0.75rem; color: var(--cp-fg-muted); min-height: 1em; }
/* Visible mode-activation error overlay: shown when a lane (Live stream, Wasm
   app, or a /render) fails to activate, so a failed mode reads as an error
   instead of a silent stale frame. Centered over the stage; [hidden] hides it. */
/* M3 error container: the `error-container` / `on-error-container` role pair at the medium
   corner, lifted on elevation 2 because it floats over the stage. */
.cp-error { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  max-width: 80%; z-index: 3; padding: 12px 16px; border-radius: var(--md-sys-shape-corner-medium);
  font-size: var(--md-sys-typescale-body-medium-size); line-height: 1.4; text-align: center;
  color: var(--md-sys-color-on-error-container); background: var(--md-sys-color-error-container);
  border: 0; box-shadow: var(--md-sys-elevation-level2); }
.cp-error[hidden] { display: none; }
.cp-note { font-size: var(--md-sys-typescale-body-small-size);
  letter-spacing: var(--md-sys-typescale-body-small-tracking);
  color: var(--md-sys-color-on-surface-variant); line-height: 1.45; padding: 12px 16px;
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-high); }
.cp-controls label:has(:disabled) { opacity: 0.55; }
.cp-size { display: flex; flex-direction: column; gap: 8px; }
/* :not([hidden]) so the mode toggle's `hidden` attribute still wins over this display rule. */
.cp-size-row:not([hidden]) { display: flex; gap: 8px; }
.cp-size-row label { flex: 1; }
.cp-size-row input { width: 100%; box-sizing: border-box; }
.cp-knobs { border-top: 1px solid var(--cp-border); padding-top: 12px; display: flex; flex-direction: column; gap: 8px; }
.cp-knobs-head { font-size: 0.72rem; color: var(--cp-fg-muted); }
.cp-overlays { border-top: 1px solid var(--cp-border); padding-top: 12px; display: flex; flex-direction: column; gap: 8px; }
.cp-overlays-head { font-size: 0.72rem; color: var(--cp-fg-muted); }
.cp-knobs input:disabled { opacity: 0.7; }
/* The Exploded 3D group's camera axes. A range + a live readout on one row: the drawer column is
   240px, and the stacked `label { flex-direction: column }` default would give each of four
   sliders three lines. `font-variant-numeric: tabular-nums` on the readout stops the row twitching
   sideways as the value crosses a digit boundary mid-drag. */
.cp-explode-row { flex-direction: row !important; align-items: center; gap: 8px !important;
  justify-content: space-between; }
.cp-explode-row input[type="range"] { flex: 1 1 90px; min-width: 0; padding: 0; }
.cp-explode-value { flex: 0 0 auto; min-width: 3.2em; text-align: right; font-size: 0.75rem;
  font-variant-numeric: tabular-nums; color: var(--cp-fg-muted); }
.cp-links { border-top: 1px solid var(--cp-border); padding-top: 12px; display: flex; flex-direction: column; gap: 8px; }
/* The under-stage column: the static-snapshot note and the export bar. Page-level, not part of
   the collapsible overrides drawer, so grabbing the /render URLs stays visible with the drawer
   closed. (Lane selection lives above the stage, beside the preview heading.) */
.cp-below { margin: 18px 0 0; max-width: 720px; display: flex; flex-direction: column; gap: 12px; }
/* Export bar: ONE always-visible line — "Export  PNG Copy link · Copy PNG · Download  SVG …" —
   rather than a disclosure hiding a stack of full-width URL fields. Wraps as a flow row on narrow
   screens; each format's actions stay glued together as a group so a wrap never splits "SVG" from
   its own buttons. */
.cp-export { margin: 12px 0 0; padding: 8px 16px; border: 0;
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-low);
  display: flex; align-items: center; flex-wrap: wrap; gap: 2px 12px; }
.cp-export-head { color: var(--cp-fg-soft);
  font-size: var(--md-sys-typescale-title-small-size);
  letter-spacing: var(--md-sys-typescale-title-small-tracking); font-weight: 500; }
.cp-link-group { display: inline-flex; align-items: center; gap: 2px; }
.cp-link-kind { font-size: var(--md-sys-typescale-label-medium-size);
  letter-spacing: var(--md-sys-typescale-label-medium-tracking); font-weight: 500;
  color: var(--cp-fg-muted); flex: none; margin-right: 4px; }
/* The URL itself: a real input (refreshLinks writes it, both copy buttons and the lane e2e read
   it) that nobody needs to look at, so it is taken out of the flow rather than given a third of
   the line. Not `hidden` / `display:none` — `field.select()` in the copy fallback needs a laid-out
   control. */
.cp-url { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
/* M3 text buttons on the export line. */
.cp-copyurl, .cp-copyimg, .cp-dl { font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  padding: 8px 10px; border-radius: var(--md-sys-shape-corner-full); border: 0;
  background: transparent; color: var(--md-sys-color-primary); cursor: pointer;
  text-decoration: none; flex: none; white-space: nowrap; font-family: inherit;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-copyurl:hover, .cp-copyimg:hover, .cp-dl:hover {
  background: color-mix(in srgb, var(--md-sys-color-primary) 8%, transparent); }
/* Drawer toggles + the two collapsible drawers (left component nav, right overrides). The open
   state is a class on .cp-viewer — cp-controls-open (default on) and cp-nav-open (default off) —
   so a drawer hides purely in CSS and the toggle JS just flips the class + aria-expanded. */
/* ONE row, not a block that grows a line per theme. A catalog with seven declared themes wrapped
   this bar onto three lines and pushed the stage most of a screen down — the render is what the
   page is for, so the bar is capped at a single row and the theme group is the only elastic member:
   everything else is flex:none, the chips shrink (ellipsising their labels) to take the squeeze,
   and past the point where shrinking would make them unreadable the group scrolls within itself.
   The bar wraps again below the mobile breakpoint, where the fixed controls alone fill the width. */
.cp-viewer-bar { display: flex; flex-wrap: nowrap; align-items: center; gap: 8px;
  margin: 0 0 12px; min-width: 0; }
.cp-viewer-bar > * { flex: none; }
.cp-theme-bar { flex: 0 1 auto; flex-wrap: nowrap; min-width: 0;
  overflow-x: auto; overflow-y: hidden; scrollbar-width: thin;
  /* Room for the thin scrollbar so it sits under the chips rather than clipping them, and a
     scroll padding so keyboard-focusing a chip doesn't leave it flush against the clipped edge. */
  padding-bottom: 4px; scroll-padding-inline: 8px; }
/* inline-block, not the chips' usual inline-flex: `text-overflow` needs a block container of
   inline content, and an ellipsised "Light Medium Cont…" still says which chip it is. The floor
   matters as much as the shrink — without it eight chips on a 1024px bar collapse to "D…" / "N."
   / "B…", which is a row of identical stubs, so shrinking stops at ~7em and the group scrolls
   from there. */
.cp-theme-bar .cp-theme-btn { display: inline-block; flex: 0 1 auto; min-width: 7em;
  padding: 6px 12px; line-height: 20px; text-align: center;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The two drawer toggles are M3 *toggle buttons*: outlined while off, tonal (`secondary-container`)
   while the drawer they own is open — the same selected language as the chips above. */
.cp-drawer-toggle { display: inline-flex; align-items: center; gap: 8px; font: inherit;
  min-height: 40px; font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  padding: 8px 16px; border-radius: var(--md-sys-shape-corner-full);
  border: 1px solid var(--md-sys-color-outline);
  background: transparent; color: var(--md-sys-color-on-surface-variant); cursor: pointer;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-drawer-toggle:hover {
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-drawer-toggle[aria-expanded="true"] { background: var(--md-sys-color-secondary-container);
  border-color: transparent; color: var(--md-sys-color-on-secondary-container); font-weight: 500; }
/* The component list is an M3 *navigation drawer*: a container-role surface at the extra-large
   corner whose rows are full-round pills. */
.cp-nav { flex: 0 0 240px; align-self: stretch; max-height: 72vh; overflow: auto;
  border: 0; border-radius: var(--md-sys-shape-corner-large);
  background: var(--md-sys-color-surface-container-low); padding: 12px;
  display: flex; flex-direction: column; gap: 10px; }
.cp-nav-head { display: flex; align-items: center; justify-content: space-between;
  padding: 4px 8px; font-size: var(--md-sys-typescale-title-small-size);
  letter-spacing: var(--md-sys-typescale-title-small-tracking); font-weight: 500;
  color: var(--cp-fg-soft); }
.cp-nav-close { border: 0; background: none; font: inherit; font-size: 1.1rem; line-height: 1;
  color: var(--cp-fg-muted); cursor: pointer; padding: 0 2px; }
.cp-nav-search { padding: 8px 14px; font: inherit;
  font-size: var(--md-sys-typescale-body-medium-size); border: 0;
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-surface-container-high); color: inherit; }
.cp-nav-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.cp-nav-current { padding-bottom: 8px; border-bottom: 1px solid var(--md-sys-color-outline-variant); }
.cp-nav-current .cp-axes-tree { margin: 0; max-width: none; }
.cp-nav-current .cp-axes-tree .cp-tree-variants { columns: auto; }
/* M3 navigation-drawer item: 56dp tall, full-round, `secondary-container` when it is the page. */
.cp-nav-item { display: flex; align-items: center; gap: 12px; min-height: 44px; padding: 6px 14px;
  border-radius: var(--md-sys-shape-corner-full);
  font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  color: var(--md-sys-color-on-surface-variant);
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-nav-thumb { flex: none; width: 28px; height: 28px; object-fit: contain;
  border-radius: var(--md-sys-shape-corner-extra-small);
  background: repeating-conic-gradient(var(--cp-surface-2) 0% 25%, var(--cp-surface) 0% 50%) 50% / 8px 8px; }
.cp-nav-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cp-nav-item:hover { background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
  color: var(--md-sys-color-on-surface); text-decoration: none; }
.cp-nav-item[aria-current="page"] { background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container); font-weight: 500; }
.cp-nav-empty { font-size: var(--md-sys-typescale-body-medium-size); color: var(--cp-fg-muted); }
.cp-nav-empty[hidden] { display: none; }
.cp-viewer:not(.cp-nav-open) .cp-nav { display: none; }
.cp-viewer:not(.cp-controls-open) .cp-controls { display: none; }
/* `.cp-theme` is inline-flex, which would out-specify the UA's `[hidden]` rule, so the folded
   theme bar needs saying explicitly. */
.cp-theme-bar[hidden] { display: none; }
/* Mobile drawer backdrop — hidden by default; shown (below) only while a drawer is open on a
   narrow viewport, where the drawers render as bottom sheets. */
.cp-scrim { display: none; }
/* Collapsible override groups: each axis-set is a <details class="cp-group"> the viewer
   expands/collapses on its own. The summary is the group title with a rotating disclosure
   caret; the body holds the labelled controls. Replaces the old flat control stack so the
   overrides panel reads as a small set of named, foldable groups. */
/* Each override group is an M3 filled card at the medium corner. */
.cp-group { border: 0; border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-low); }
.cp-group > summary { cursor: pointer; list-style: none; display: flex; align-items: center;
  gap: 8px; min-height: 44px; padding: 10px 14px;
  font-size: var(--md-sys-typescale-title-small-size);
  letter-spacing: var(--md-sys-typescale-title-small-tracking); font-weight: 500;
  color: var(--cp-fg-soft); border-radius: var(--md-sys-shape-corner-medium);
  user-select: none; }
.cp-group > summary::-webkit-details-marker { display: none; }
.cp-group > summary::before { content: "\25B8"; font-size: 0.72rem; color: var(--cp-fg-faint);
  transition: transform 0.12s ease; }
.cp-group[open] > summary::before { transform: rotate(90deg); }
.cp-group > summary:hover { color: var(--md-sys-color-primary);
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-group-body { display: flex; flex-direction: column; gap: 12px; padding: 0 14px 14px; }
/* Override inputs read as M3 *outlined text fields*: an `outline` keyline at the extra-small
   corner, on the group's own surface. The shared focus ring above supplies the focused state. */
.cp-controls input:not([type="range"]):not([type="checkbox"]):not([type="radio"]),
.cp-controls select { padding: 8px 10px; border: 1px solid var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-extra-small);
  background: var(--md-sys-color-surface-container-low); color: var(--md-sys-color-on-surface);
  font: inherit; font-size: var(--md-sys-typescale-body-medium-size); }
.cp-controls label { color: var(--md-sys-color-on-surface-variant); }
/* The single Static⇄Live preview toggle (replaces the old PNG/SVG/Live/Wasm radio row). Off =
   the baked / on-demand snapshot; on = the interactive lane (daemon stream, or the in-browser
   Wasm app). The hidden mode radios behind it are what the transport JS actually reads. */
.cp-preview-mode { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* M3 toggle button, 40dp: outlined while the lane is off, and — because "live" is a status, not a
   selection — a literal green tonal container while it is on. That green is deliberately NOT a
   palette role: it pairs with the status dot below and must mean the same on every catalog. */
.cp-live-toggle { display: inline-flex; align-items: center; gap: 8px; font: inherit;
  min-height: 40px; font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  padding: 8px 20px; border-radius: var(--md-sys-shape-corner-full);
  border: 1px solid var(--md-sys-color-outline); background: transparent;
  color: var(--md-sys-color-on-surface-variant); cursor: pointer;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-live-toggle:hover:not(:disabled) {
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-live-toggle:disabled { opacity: 0.38; cursor: default; }
.cp-live-toggle[aria-pressed="true"] { background: light-dark(#d7f0dd, #0d3a1c); border-color: transparent;
  color: light-dark(#10521f, #86dc9d); }
/* The sign-in affordance shown in place of the Live toggle when auth is the only thing blocking
   the daemon lane. Deliberately NOT dimmed like `:disabled` — the lane is available, it just needs
   one click, and a greyed chip says the opposite. Full contrast and a dashed keyline mark it as an
   action to take rather than a state to read, so it can't be mistaken for the lit `aria-pressed`
   green either. `<a>` inherits none of the button reset, hence the explicit text-decoration. */
.cp-live-signin { border-style: dashed; text-decoration: none; color: var(--cp-fg); }
.cp-live-signin:hover, .cp-live-signin:focus-visible { background: var(--cp-surface-2);
  border-style: solid; text-decoration: none; }
/* The status light. It is the whole answer to "is what I'm looking at live?", so the two states
   are made to differ in SHAPE as well as colour: idle is a hollow grey ring (a light that is off),
   lit is a filled green disc inside a halo. Literal colours by design — the pair reads as one
   indicator and must not drift apart with the palette. */
.cp-live-dot { width: 9px; height: 9px; border-radius: 50%; background: transparent;
  border: 2px solid #b9b9c6; box-sizing: border-box; flex: none; }
.cp-live-toggle[aria-pressed="true"] .cp-live-dot { background: #1e9c3f; border-color: #1e9c3f;
  box-shadow: 0 0 0 3px rgba(30, 156, 63, 0.2); }
/* The renderer combo box: every OTHER lane this preview can be drawn by, beside the chip that
   names the current one. An M3 outlined field at the same 40dp height as the chip and the SVG
   toggle, so the three read as one row of peers rather than a control that fell out of a form. */
.cp-lane-select { min-height: 40px; font: inherit;
  font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  padding: 8px 12px; border-radius: var(--md-sys-shape-corner-full);
  border: 1px solid var(--md-sys-color-outline);
  background: transparent; color: var(--md-sys-color-on-surface-variant); cursor: pointer;
  max-width: 200px; }
.cp-lane-select:hover { background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
/* The spec lane's carrier — the comparison-view group, its score readout, and the "spec diff →"
   step out to the focused comparison page. */
.cp-spec-lane { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
/* How the render/spec pair is drawn: Spec | Diff | Triptych | Slider. A segmented button group
   (one shared outline, dividers between the segments) rather than four loose chips, because the
   four are one mutually-exclusive choice about one pair — the same reason the renderer picker is
   one combo instead of a chip per lane. Revealed by spec-compare.js only while the lane is up. */
.cp-spec-views { display: inline-flex; align-items: stretch;
  border: 1px solid var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-full); overflow: hidden; }
.cp-spec-views[hidden] { display: none; }
.cp-spec-view { font: inherit; min-height: 32px; padding: 5px 12px; border: 0;
  border-left: 1px solid var(--md-sys-color-outline-variant);
  font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  background: transparent; color: var(--md-sys-color-on-surface-variant); cursor: pointer;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-spec-view:first-child { border-left: 0; }
.cp-spec-view:hover { background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-spec-view[aria-pressed="true"] { background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container); }
/* "93.4% match · 2.11% pixels differ" — the number the comparison exists to produce, beside the
   control that produced it rather than buried under the stage. */
.cp-spec-score { font-size: 0.72rem; color: var(--cp-fg-muted); white-space: nowrap; }
.cp-spec-score[hidden] { display: none; }
/* The motion lane's carrier — the per-capture picker and the caption readout — shown only while a
   recording is actually on the stage. `[hidden]` needs its own rule here because the `inline-flex`
   above would otherwise win over the attribute's UA `display: none`. */
.cp-motion-lane { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.cp-motion-lane[hidden] { display: none; }
/* Which recording, when a preview published more than one. The same segmented group the spec
   comparison views use, for the same reason: one mutually-exclusive choice about one component
   reads as one control, not as N loose chips. Hidden — not omitted — for the single-capture case,
   because that button is still where the lane reads its source from. */
.cp-motion-views { display: inline-flex; align-items: stretch;
  border: 1px solid var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-full); overflow: hidden; }
.cp-motion-views[hidden] { display: none; }
.cp-motion-view { font: inherit; min-height: 32px; padding: 5px 12px; border: 0;
  border-left: 1px solid var(--md-sys-color-outline-variant);
  font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  background: transparent; color: var(--md-sys-color-on-surface-variant); cursor: pointer;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-motion-view:first-child { border-left: 0; }
.cp-motion-view:hover { background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-motion-view[aria-pressed="true"] { background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container); }
/* What the recording is showing ("Pressed \2192 checked"). A capture without its caption tells the
   reader only that *something* moved, so the annotation's own words ride beside the frames. */
.cp-motion-caption { font-size: 0.72rem; color: var(--cp-fg-muted); }
/* The inspection layers describe the SNAPSHOT — every box is placed from that image's natural size
   and offset (see inspect.js `place()`), and nothing re-places them when a lane takes the snapshot
   out of flow. Left alone they would keep their last geometry and hang over the recording,
   reporting a focus map and resolved type for a frame that is no longer on screen. Withdrawn by
   mode rather than unticked by script, so returning to the render restores exactly what the
   visitor had ticked instead of silently clearing their choice. */
.cp-viewer[data-mode="motion"] .cp-inspect-layer,
.cp-viewer[data-mode="motion"] .cp-inspect-legend { display: none; }
/* A capture is a still's worth of subject in many frames, so it obeys exactly the geometry the
   snapshot does — `.cp-stage img` already caps its width, and viewer.js's zoom lane writes the same
   `max-height` onto it that the render and the spec raster get. */
.cp-motion-chip[aria-pressed="true"]::before { content: "\25B6"; margin-right: 6px;
  font-size: 0.7em; line-height: 1; }
.cp-mode-hint { font-size: 0.72rem; color: var(--cp-fg-muted); }
/* The SVG format toggle sits beside the Live toggle (only when the session can export SVG).
   It swaps the static snapshot between the raster PNG and the resolution-independent vector
   render; pressing it while Live is on drops back to the static SVG. */
/* The Source panel: the usage code, on the stage in place of the render.

   Scrolls within itself rather than growing the stage, so entering and leaving the lane does not
   resize the page around the visitor — and `max-width` keeps a long line from stretching the stage
   wider than the render it replaced. The surface is the container colour rather than the stage's
   own backing: this is a document, not a render, and it should not look like one that failed to
   load. */
.cp-source-panel { display: none; box-sizing: border-box; width: 100%; max-width: 72ch;
  max-height: min(70vh, 640px); overflow: auto; text-align: left;
  padding: 16px; border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container);
  color: var(--md-sys-color-on-surface); }
.cp-viewer[data-mode="source"] .cp-source-panel { display: block; }
/* The stage's own backing exists to show a RENDER on a neutral field, and the backend badge names
   the engine that drew one. Neither means anything over a code panel: on a dark page the backing
   framed the panel in a white card, and the badge sat on top of the first line of source. Both are
   suppressed for the duration of the lane rather than restyled — the panel brings its own
   surface. */
.cp-viewer[data-mode="source"] .cp-stage { box-shadow: none; padding: 0; }
/* Written at the same specificity as the Day/Night stage-backing rules above, and after them, or
   they win: those paint the stage `#fff` / `#1d1d20` to give a RENDER a known field to sit on, and
   on a dark page that framed the code panel in a white card. */
html:not(.cp-bg-transparent) .cp-viewer[data-mode="source"] .cp-stage,
html.cp-bg-transparent .cp-viewer[data-mode="source"] .cp-stage { background: transparent; }
.cp-viewer[data-mode="source"] .cp-backend { display: none; }
.cp-source-panel pre { margin: 0; white-space: pre; overflow-x: auto;
  font-family: var(--cp-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 0.8125rem; line-height: 1.55; }
/* CodeMirror is an optional, read-only upgrade over the <pre> above. Let the panel own both axes
   rather than nesting a second scroll box, and paint its gutter from the same surface roles as the
   rest of the viewer. `viewportMargin: Infinity` in viewer.js makes this auto-height safe. */
.cp-source-panel .CodeMirror { height: auto; background: transparent;
  color: var(--md-sys-color-on-surface);
  font-family: var(--cp-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 0.8125rem; line-height: 1.55; }
.cp-source-panel .CodeMirror-scroll { overflow: visible !important; }
.cp-source-panel .CodeMirror-lines { padding: 0; }
.cp-source-panel .CodeMirror-gutters { background: var(--md-sys-color-surface-container);
  border-right: 1px solid var(--md-sys-color-outline-variant); }
.cp-source-panel .CodeMirror-linenumber { color: var(--md-sys-color-outline); }
.cp-source-panel .CodeMirror-selected { background: var(--md-sys-color-secondary-container); }
/* Same Kotlin token palette as playground.css. It follows the page's pinned `color-scheme` through
   light-dark(), rather than following the OS after the visitor has explicitly chosen a scheme. */
.cp-source-panel .cm-keyword { color: light-dark(#770088, #c792ea); }
.cp-source-panel .cm-def { color: light-dark(#0000ff, #82aaff); }
.cp-source-panel .cm-variable-3, .cp-source-panel .cm-type {
  color: light-dark(#008855, #82aaff); }
.cp-source-panel .cm-string { color: light-dark(#aa1111, #c3e88d); }
.cp-source-panel .cm-string-2 { color: light-dark(#ff5500, #c3e88d); }
.cp-source-panel .cm-number { color: light-dark(#116644, #f78c6c); }
.cp-source-panel .cm-comment { color: light-dark(#aa5500, #7d8799); }
.cp-source-panel .cm-meta { color: light-dark(#555555, #ffcb6b); }
.cp-source-panel .cm-atom { color: light-dark(#221199, #ffcb6b); }
.cp-source-panel .cm-operator, .cp-source-panel .cm-punctuation {
  color: light-dark(currentcolor, #89ddff); }
.cp-source-actions { display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
  margin-top: 12px; }
.cp-source-note { margin: 0 0 12px; color: var(--cp-fg-muted); font-size: 0.75rem;
  line-height: 1.5; }
.cp-source-note--warn { color: light-dark(#573100, #ffd8a3); }

/* `.cp-spec-chip` shares this shape on purpose: the design-spec lane is now a top-level toggle on
   the viewer bar (not an <option> buried in the renderer combo), and it is the same *kind* of
   control as the SVG format toggle — "swap what is on the stage" — so it reads as one family with
   it and with the Live chip. */
.cp-fmt-toggle, .cp-spec-chip { display: inline-flex; align-items: center; font: inherit;
  min-height: 40px;
  font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  padding: 8px 20px; border-radius: var(--md-sys-shape-corner-full);
  border: 1px solid var(--md-sys-color-outline); background: transparent;
  color: var(--md-sys-color-on-surface-variant); cursor: pointer;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
/* `:not([aria-pressed="true"])` on BOTH, so hover cannot outrank the pressed fill below. It
   already could on `.cp-spec-chip`: `:hover:not(:disabled)` is (0,3,0) against the pressed rule's
   (0,2,0), so resting the pointer on the active Source or Figma chip replaced its
   secondary-container fill with the neutral hover wash — the chip stopped looking selected while
   you were pointing at the thing that selected it. `.cp-fmt-toggle` was only accidentally safe,
   both rules being (0,2,0) and pressed coming second; stating it makes the two agree rather than
   leaving one of them right by declaration order. */
.cp-fmt-toggle:hover:not([aria-pressed="true"]),
.cp-spec-chip:hover:not(:disabled):not([aria-pressed="true"]) {
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-fmt-toggle[aria-pressed="true"], .cp-spec-chip[aria-pressed="true"] {
  background: var(--md-sys-color-secondary-container);
  border-color: transparent; color: var(--md-sys-color-on-secondary-container); }
.cp-spec-chip:disabled { opacity: 0.38; cursor: default; }
/* The design-spec chip's **verdict dot**. The chip states the match score at rest ("Figma 96.3%"),
   and the dot is what makes the number scannable without reading it — one glance says whether this
   page is worth opening. Colour only: the number is always printed, so a reader who cannot separate
   the hues (or a band that drifts against the producer's) loses nothing but the shortcut.

   Bands come from the distribution a real catalog produces, not from round numbers — see
   `ServeWeb.specMatchBand`. Rendered as a pseudo-element rather than a nested span so the chip's
   accessible name stays the plain label text. */
.cp-spec-chip[data-spec-match]::before { content: ""; width: 8px; height: 8px; margin-right: 8px;
  border-radius: 50%; flex: none; }
/* Painted straight onto the dot rather than through a `--cp-` variable: that namespace is the
   THEME PROJECTION's, and `ServeThemeCssTest` holds it to "every var(--cp-…) the sheet uses is one
   the projection emits". A band is a fixed semantic colour, not a themed role, so it has no
   business in that palette — and the indirection bought nothing here anyway. */
.cp-spec-chip[data-spec-match="match"]::before { background: light-dark(#146c2e, #6dd58c); }
.cp-spec-chip[data-spec-match="close"]::before { background: light-dark(#8f5000, #ffb787); }
.cp-spec-chip[data-spec-match="off"]::before { background: var(--md-sys-color-error); }
.cp-format-link { color: var(--cp-fg-muted); font-size: 0.75rem; white-space: nowrap; }
.cp-format-link:hover { color: var(--cp-accent-strong); }
/* Score chips: M3 assist-chip shape, literal semantic colours. */
.cp-match { display: inline-flex; align-items: center; min-height: 32px; padding: 6px 12px;
  border-radius: var(--md-sys-shape-corner-small);
  font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  color: var(--cp-fg-muted); background: var(--md-sys-color-surface-container-high); border: 0; }
.cp-match[hidden] { display: none; }
.cp-match--good { color: light-dark(#10521f, #86dc9d); background: light-dark(#d7f0dd, #0d3a1c);
  border-color: transparent; }
.cp-match--warn { color: light-dark(#573100, #ffd8a3); background: light-dark(#ffdfb0, #4a3000);
  border-color: transparent; }
.cp-match--bad { color: light-dark(#6e1414, #ffb4ab); background: light-dark(#ffdad6, #601410);
  border-color: transparent; }
.cp-match--na { color: var(--cp-fg-muted); }
/* Native format-fidelity gallery. The site chrome follows the page's resolved colour scheme; the
   Theme control selects compared artefacts and never changes the fixed scoring backdrop. */
.cp-compare-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px;
  margin: 0 0 14px; }
.cp-compare-control-label { color: var(--cp-fg-muted); font-size: 0.75rem; }
.cp-compare-searchbar { max-width: 720px; }
.cp-compare-table-wrap { overflow-x: auto; margin-top: 14px; }
.cp-compare-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
.cp-compare-table th, .cp-compare-table td { padding: 10px 12px; text-align: left;
  vertical-align: middle; border-bottom: 1px solid var(--cp-border); }
.cp-compare-table thead th { position: sticky; top: 0; z-index: 1; color: var(--cp-fg-muted);
  font-size: 0.72rem; background: light-dark(var(--cp-bg), var(--cp-surface)); }
.cp-compare-table tbody th { min-width: 150px; font-weight: 600; word-break: break-word; }
/* The variant under the component name. A component that publishes a reference per state gets a
   row per state, and without this they are a run of identical labels — so it reads as secondary to
   the component name, but on its own line where it cannot be mistaken for part of it. */
.cp-compare-variant { display: block; font-weight: 400; font-size: 0.78rem;
  color: var(--cp-fg-muted); }
.cp-compare-shot { display: grid; place-items: center; width: min(260px, 30vw); min-width: 150px;
  min-height: 92px; padding: 8px; border-radius: 8px; overflow: hidden;
  background: repeating-conic-gradient(var(--cp-surface-2) 0% 25%, var(--cp-surface) 0% 50%) 50% / 14px 14px; }
.cp-compare-row[data-bg-theme="dark"] .cp-compare-shot { background: #111113; }
.cp-compare-shot img, .cp-compare-shot canvas { display: block; max-width: 100%; max-height: 220px;
  width: auto; height: auto; object-fit: contain; }
.cp-compare-shot [hidden] { display: none; }
.cp-compare-score { min-width: 88px; text-align: right; white-space: nowrap; color: var(--cp-fg-muted);
  font-variant-numeric: tabular-nums; }
.cp-compare-score--good { color: light-dark(#1e7a34, #6cd98a); }
.cp-compare-score--warn { color: light-dark(#8a5300, #e6b067); }
.cp-compare-score--bad { color: light-dark(#8a1c1c, #ef9292); }
.cp-compare-score--na { color: var(--cp-fg-faint); }
#cp-compare .cp-compare-target-head { font-size: 0; }
#cp-compare .cp-compare-target-head::after { font-size: 0.72rem; }
#cp-compare[data-format="svg"] .cp-compare-target-head::after { content: "SVG"; }
#cp-compare[data-format="rc"] .cp-compare-target-head::after { content: "Remote Compose"; }
#cp-compare[data-format="reference"] .cp-compare-target-head::after { content: "Design reference"; }
#cp-compare[data-format="reference"] .cp-compare-vector { cursor: pointer; }

/* The Remote Compose player wall: one column per player, every render published by the offline
   rc-compare run. A diff appears *inside* a player's column once a reference column is picked. */
#cp-compare[data-rc-lanes="1"][data-format="rc"] .cp-compare-theme-controls { display: none; }
/* The page's one-line summary belongs to whichever pane is showing: the format table scores
   structural similarity, the player wall replays published pixel diffs. */
.cp-sub-rc { display: none; }
#cp-compare[data-rc-lanes="1"][data-format="rc"] .cp-sub-rc { display: inline; }
#cp-compare[data-rc-lanes="1"][data-format="rc"] .cp-sub-formats { display: none; }
.cp-rc-table td { vertical-align: top; }
.cp-rc-table tbody th { vertical-align: top; min-width: 180px; }
.cp-rc-status { color: var(--cp-fg-muted); font-size: 0.75rem; }
.cp-rc-dims, .cp-rc-blank { color: var(--cp-fg-muted); font-size: 0.72rem; margin-top: 4px; }
.cp-rc-cell { margin: 0; width: min(220px, 26vw); min-width: 130px; }
.cp-rc-cell figcaption { color: var(--cp-fg-muted); font-size: 0.7rem; margin-bottom: 4px; }
/* Capped like the format table's shots: a player render is whatever size the preview was captured
   at, and a tall one must not push the next row a screen away. */
.cp-rc-cell img { display: block; width: auto; height: auto; max-width: 100%; max-height: 220px;
  object-fit: contain; border: 1px solid var(--cp-border); border-radius: 8px;
  background: repeating-conic-gradient(var(--cp-surface-2) 0% 25%, var(--cp-surface) 0% 50%) 50% / 14px 14px; }
.cp-rc-refbadge { display: none; margin-left: 6px; padding: 1px 6px; border-radius: 999px;
  background: var(--cp-accent, #0969da); color: #fff; font-size: 0.62rem; text-transform: uppercase; }
.cp-rc-cell.is-reference .cp-rc-refbadge { display: inline-block; }
.cp-rc-cell.is-reference img { outline: 2px solid var(--cp-accent, #0969da); outline-offset: 1px; }
.cp-rc-missing { display: grid; place-items: center; text-align: center; min-height: 80px;
  padding: 8px; border: 1px dashed var(--cp-border); border-radius: 8px;
  color: var(--cp-fg-muted); font-size: 0.7rem; }
.cp-rc-diffslot { margin-top: 6px; }
.cp-rc-difflabel { color: var(--cp-fg-muted); font-size: 0.7rem; margin-bottom: 4px; }
.cp-rc-diffslot img { display: block; width: auto; height: auto; max-width: 100%;
  max-height: 220px; object-fit: contain; border: 1px solid var(--cp-border);
  border-radius: 8px; background: #000; }
.cp-rc-scoreline { display: flex; align-items: flex-start; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.cp-rc-scorelabel { color: var(--cp-fg-muted); font-size: 0.68rem; text-transform: uppercase;
  min-width: 54px; padding-top: 2px; }
.cp-rc-score { display: inline-block; padding: 2px 8px; border-radius: 999px; font-weight: 600;
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.cp-rc-score--good { background: #1a7f37; color: #fff; }
.cp-rc-score--ok { background: #9a6700; color: #fff; }
.cp-rc-score--bad { background: #b32424; color: #fff; }
/* A reason ("player could not decode the document") is prose, not a measurement — it must not
   wear a score pill's weight, and it must wrap rather than burst out of it. */
.cp-rc-score--na { background: #555; color: #fff; font-weight: 400; font-size: 0.68rem;
  border-radius: 6px; }
.cp-rc-px { color: var(--cp-fg-muted); font-size: 0.72rem; font-variant-numeric: tabular-nums; }
.cp-reference-meta { margin: 12px 0 20px; color: var(--cp-fg-muted); font-size: 0.82rem; }
.cp-parity-issues { margin: 12px 0; padding: 12px 14px; border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: 12px; background: var(--md-sys-color-surface-container); }
.cp-parity-issues ul { list-style: none; margin: 7px 0 0; padding: 0; display: grid; gap: 6px; }
.cp-parity-issue { display: flex; gap: 10px; align-items: baseline; justify-content: space-between; }
.cp-parity-issue > span { color: var(--cp-fg-muted); font-size: .75rem; white-space: nowrap; }
.cp-parity-issue.closed { opacity: .58; }
.cp-issue-badge { display: inline-flex; width: fit-content; margin-top: 5px; padding: 2px 7px;
  border-radius: 999px; background: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container);
  font-size: .68rem; font-weight: 650; }
.cp-parity-issue-group h3 { margin-bottom: 0; }
.cp-reference-picker { display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin: 14px 0 10px; color: var(--cp-fg-muted); font-size: 0.8rem; }
.cp-reference-picker > span { margin-right: 2px; font-weight: 650; }
.cp-reference-choice { display: inline-flex; align-items: center; min-height: 32px; padding: 6px 16px;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-small);
  background: transparent; color: var(--md-sys-color-on-surface-variant); text-decoration: none;
  font-weight: 500; }
.cp-reference-choice:hover { color: var(--md-sys-color-on-surface);
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-reference-choice[aria-current="page"] { background: var(--md-sys-color-secondary-container);
  border-color: transparent; color: var(--md-sys-color-on-secondary-container); font-weight: 500; }
.cp-reference-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; }
.cp-reference-grid section h2 { margin: 0 0 8px; font-size: 0.8rem; color: var(--cp-fg-muted);
  text-transform: uppercase; letter-spacing: 0.06em; }
.cp-reference-grid .cp-compare-shot { width: 100%; min-height: 180px; box-sizing: border-box; }
.cp-reference-result { font-weight: 650; font-variant-numeric: tabular-nums; }
/* Annotation layers. The layer is absolutely positioned over its panel's image and sized in JS from
   the image's rendered box, because annotation bounds arrive in the source frame's pixel space and
   the reference and actual frames are routinely different sizes. */
.cp-reference-grid .cp-compare-shot[data-cp-annotated] { position: relative; }
.cp-annotation-layer { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  pointer-events: none; }
.cp-annotation { position: absolute; box-sizing: border-box; display: none; }
/* Labels sit *inside* their box. Annotated regions are routinely flush with the top of the frame,
   and a label hung above the box is clipped by the panel exactly when the region matters most. */
/* Only an index on the box — the spec text lives in the legend below the panel. See the note in
   format-compare.js: captions this size truncate each other wherever annotations nest. */
.cp-annotation-badge { display: inline-flex; align-items: center; justify-content: center;
  min-width: 14px; height: 14px; padding: 0 3px; border-radius: 7px; font-size: 0.6rem;
  font-weight: 700; line-height: 1; color: #fff; }
.cp-annotation .cp-annotation-badge { position: absolute; top: -7px; left: -7px; }
/* The badge is coloured by kind wherever it appears — on the overlay box AND in the legend row.
   Only the box was covered before, so a legend ordinal was `color: #fff` over no background: the
   number was invisible and the row could not be matched back to its box, which is the legend's one
   job. Fixed for all three kinds rather than only the new one, since layout and typography had it
   too. */
.cp-annotation--layout .cp-annotation-badge,
.cp-annotation-entry--layout .cp-annotation-badge { background: rgba(214, 51, 108, 0.95); }
.cp-annotation--typography .cp-annotation-badge,
.cp-annotation-entry--typography .cp-annotation-badge { background: rgba(37, 99, 235, 0.95); }
.cp-annotation--theme .cp-annotation-badge,
.cp-annotation-entry--theme .cp-annotation-badge { background: rgba(13, 148, 136, 0.95); }
.cp-annotation-legend { display: none; margin: 8px 0 0; padding: 0; list-style: none;
  font-size: 0.72rem; color: var(--cp-fg-muted); }
.cp-annotation-entry { display: flex; align-items: center; gap: 6px; padding: 2px 0; }
.cp-annotation-role { font-weight: 650; color: var(--cp-fg); }
.cp-annotation-spec { font-variant-numeric: tabular-nums; }
#cp-reference-compare[data-annotate-layout="on"] .cp-annotation-legend,
#cp-reference-compare[data-annotate-theme="on"] .cp-annotation-legend,
#cp-reference-compare[data-annotate-typography="on"] .cp-annotation-legend { display: block; }
.cp-annotation-entry { display: none; }
#cp-reference-compare[data-annotate-layout="on"] .cp-annotation-entry--layout,
#cp-reference-compare[data-annotate-theme="on"] .cp-annotation-entry--theme,
#cp-reference-compare[data-annotate-typography="on"] .cp-annotation-entry--typography
  { display: flex; }
/* Layout is the redline layer, typography the type spec — distinct hues so both can be on at once
   and still be told apart at a glance. */
.cp-annotation--layout { border: 1px solid rgba(214, 51, 108, 0.9); }
.cp-annotation--layout .cp-annotation-label { background: rgba(214, 51, 108, 0.92); }
.cp-annotation--typography { border: 1px dashed rgba(37, 99, 235, 0.9); }
.cp-annotation--typography .cp-annotation-label { background: rgba(37, 99, 235, 0.92); }
/* Theme is the resolved-container layer — fill, border, radius. A third hue AND a third border
   style, because hue alone is not enough here: a container's redline and its resolved tokens
   describe the SAME box in the ordinary case, so with Layout and Theme both on, the later-appended
   theme box sits exactly on the layout box. Dotted against layout's solid and typography's dashed
   keeps two coincident boxes legible. */
.cp-annotation--theme { border: 1px dotted rgba(13, 148, 136, 0.9); }
.cp-annotation--theme .cp-annotation-label { background: rgba(13, 148, 136, 0.92); }
/* Same collision, worse: every box badge is pinned to the top-left corner, so a theme badge over a
   layout box hid the layout ordinal completely and the legend's ① pointed at nothing visible. Park
   the theme badge on the opposite corner — the two can then never occupy one spot. */
.cp-annotation--theme .cp-annotation-badge { left: auto; right: -7px; }
.cp-annotation--typography-hit { border: 0; background: transparent; }
.cp-annotation--typography-cluster.cp-annotation-active { border-width: 2px;
  background: rgba(37, 99, 235, 0.08); }
.cp-annotation--typography-hit.cp-annotation-active { background: rgba(37, 99, 235, 0.28);
  outline: 1px solid rgba(37, 99, 235, 0.95); }
#cp-reference-compare[data-annotate-layout="on"] .cp-annotation--layout,
#cp-reference-compare[data-annotate-theme="on"] .cp-annotation--theme,
#cp-reference-compare[data-annotate-typography="on"] .cp-annotation--typography { display: block; }
.cp-typography-summary { display: none; margin: 14px 0 0; }
#cp-reference-compare[data-annotate-typography="on"] .cp-typography-summary { display: block; }
.cp-typography-summary h2 { margin: 0 0 8px; font-size: 0.8rem; color: var(--cp-fg-muted);
  text-transform: uppercase; letter-spacing: 0.06em; }
.cp-typography-groups { display: grid; gap: 6px; overflow-x: auto; }
.cp-typography-group { display: flex; align-items: center; gap: 9px; min-width: max-content;
  padding: 7px 10px;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-low); }
.cp-typography-group:hover, .cp-typography-group:focus-visible {
  border-color: rgba(37, 99, 235, 0.75); background: rgba(37, 99, 235, 0.06); outline: none; }
.cp-typography-marker { flex: none; background: rgba(37, 99, 235, 0.95); }
.cp-typography-inline { color: var(--cp-fg); font-size: 0.74rem;
  font-variant-numeric: tabular-nums; white-space: nowrap; }
.cp-typography-side { color: var(--cp-fg-muted); font-size: 0.68rem; font-weight: 700;
  letter-spacing: 0.04em; text-transform: uppercase; }
.cp-typography-changed { color: var(--md-sys-color-error); font-weight: 650; }
.cp-typography-override { text-decoration: underline 2px rgba(214, 51, 108, 0.55);
  text-underline-offset: 2px; }
.cp-typography-count { color: var(--cp-fg-faint); }
.cp-typography-arrow { flex: none; color: var(--cp-fg-muted); }
.cp-annotation-controls { display: flex; align-items: center; flex-wrap: wrap; gap: 14px;
  margin: 14px 0 0; color: var(--cp-fg-muted); font-size: 0.8rem; }
.cp-annotation-toggle { display: inline-flex; align-items: center; gap: 6px; }
/* ---------------------------------------------------------------------------
   Viewer inspection layers (see inspect.js): accessibility focus map, resolved
   typography, resolved theme attributes. The layer is absolutely positioned over
   the stage's <img> and sized in JS from that image's rendered box, because every
   source reports bounds in the render's own pixel space.
   --------------------------------------------------------------------------- */
.cp-inspect-layer { position: absolute; pointer-events: none; }
.cp-viewer[data-inspect="on"] .cp-inspect-layer { pointer-events: auto; }
.cp-inspect-box { position: absolute; box-sizing: border-box; border-radius: 2px;
  border: 1px solid var(--cp-inspect-color, rgba(37, 99, 235, 0.9));
  background: color-mix(in srgb, var(--cp-inspect-color, rgba(37, 99, 235, 1)) 12%, transparent); }
.cp-inspect-box[data-cp-kind="typography"] { border-style: dashed;
  --cp-inspect-color: rgba(37, 99, 235, 0.9); }
.cp-inspect-box[data-cp-kind="theme"] { --cp-inspect-color: rgba(214, 51, 108, 0.9); }
.cp-inspect-box[data-level="warning"] { --cp-inspect-color: rgba(217, 119, 6, 0.95); }
.cp-inspect-box[data-level="error"] { --cp-inspect-color: rgba(220, 38, 38, 0.95); }
.cp-inspect-box-active { border-width: 2px;
  background: color-mix(in srgb, var(--cp-inspect-color, rgba(37, 99, 235, 1)) 28%, transparent); }
/* Only an index on the box — the readable spec lives in the legend beside the stage. Same reasoning
   as the compare page's annotation layers: a label wide enough to read truncates its neighbours
   exactly where the boxes nest most densely. */
.cp-inspect-badge { display: inline-flex; align-items: center; justify-content: center;
  min-width: 14px; height: 14px; padding: 0 3px; border-radius: 7px; font-size: 0.6rem;
  font-weight: 700; line-height: 1; color: #fff;
  background: var(--cp-inspect-color, rgba(37, 99, 235, 0.95)); }
.cp-inspect-box .cp-inspect-badge { position: absolute; top: -7px; left: -7px; }
.cp-inspect-legend { flex: 0 0 232px; min-width: 200px; align-self: stretch;
  position: sticky; top: 16px; max-height: calc(100vh - 32px); overflow: auto;
  border-radius: var(--md-sys-shape-corner-large); padding: 12px 14px;
  background: var(--md-sys-color-surface-container-low); font-size: 0.75rem; }
.cp-inspect-legend[hidden] { display: none; }
.cp-inspect-legend-head { display: flex; align-items: center; justify-content: space-between;
  gap: 8px; font-weight: 650; color: var(--cp-fg); margin: 0 0 8px; }
.cp-inspect-legend-count { color: var(--cp-fg-faint); font-variant-numeric: tabular-nums; }
.cp-inspect-section + .cp-inspect-section { margin-top: 10px; }
.cp-inspect-section-head { color: var(--cp-fg-muted); font-weight: 650; margin: 0 0 4px; }
.cp-inspect-list { margin: 0; padding: 0; list-style: none; }
.cp-inspect-entry { display: flex; align-items: flex-start; gap: 6px; padding: 3px 4px;
  border-radius: 6px; cursor: default; }
.cp-inspect-entry[data-cp-kind="typography"] { padding-right: 0;
  --cp-inspect-color: rgba(37, 99, 235, 0.9); }
.cp-inspect-entry[data-cp-kind="theme"] { --cp-inspect-color: rgba(214, 51, 108, 0.9); }
.cp-inspect-entry[data-level="warning"] { --cp-inspect-color: rgba(217, 119, 6, 0.95); }
.cp-inspect-entry[data-level="error"] { --cp-inspect-color: rgba(220, 38, 38, 0.95); }
.cp-inspect-entry-active { background: var(--md-sys-color-surface-container-high); }
.cp-inspect-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.cp-inspect-text strong { font-weight: 650; color: var(--cp-fg); overflow-wrap: anywhere; }
.cp-inspect-detail { color: var(--cp-fg-muted); overflow-wrap: anywhere;
  font-variant-numeric: tabular-nums; }
.cp-inspect-entry[data-cp-kind="typography"] .cp-inspect-detail { white-space: nowrap;
  font-size: 0.62rem; letter-spacing: -0.02em; }
.cp-overlay-control { display: flex; align-items: center; gap: 10px; max-width: 420px;
  margin: 22px 0 8px; color: var(--cp-fg-muted); font-size: 0.8rem; }
.cp-overlay-control input { flex: 1; }
/* Literal, like the scoring backdrop above it: the reference/actual overlay is a fixed light
   checkerboard in every colour scheme and under every catalog palette, because what is being
   judged is the pixels on it. */
.cp-reference-overlay { position: relative; display: grid; place-items: center; width: min(720px, 100%);
  min-height: 180px; overflow: hidden; border-radius: 8px;
  background: repeating-conic-gradient(#f4f4f6 0% 25%, #fff 0% 50%) 50% / 14px 14px; }
.cp-reference-overlay img { grid-area: 1 / 1; max-width: 100%; max-height: 520px; object-fit: contain; }
@media (max-width: 760px) {
  .cp-reference-grid { grid-template-columns: 1fr; }
}
/* The mode radios are kept in the DOM (the transport JS drives them) but visually removed —
   the single toggle above is the only visible mode control. */
.cp-modes-inputs { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
/* Backend badge flips its accent green when a live lane drives the stage (`<cp-backend-badge>`,
   cli/serve-web/src/components/BackendBadge.ts). */
.cp-backend[data-live="true"] { background: rgba(30, 122, 52, 0.82); }
/* …and amber while that lane is still activating ("connecting…"), so the wait reads on the
   preview itself rather than in the controls footer. Declared after the green rule so it wins
   at equal specificity (a connecting live lane carries both attributes). */
.cp-backend[data-pending="true"] { background: rgba(138, 83, 0, 0.86); }
/* Shared-document surfaces (POST /docs → GET /d/<id>): the drop zone on the upload page and the
   played-back document + its facts on the permalink page. Both reuse the card/stage chrome. */
.cp-drop { display: flex; flex-direction: column; align-items: center; gap: 8px; max-width: 720px;
  padding: 32px 24px; border: 2px dashed var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-large);
  background: var(--md-sys-color-surface-container-low);
  text-align: center; cursor: pointer;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-drop:hover, .cp-drop.cp-drop-over { border-color: var(--md-sys-color-primary);
  background: color-mix(in srgb, var(--md-sys-color-primary) 8%,
    var(--md-sys-color-surface-container-low)); }
.cp-drop-title { font-size: var(--md-sys-typescale-title-medium-size);
  line-height: var(--md-sys-typescale-title-medium-line);
  letter-spacing: var(--md-sys-typescale-title-medium-tracking); font-weight: 500; }
.cp-drop-hint { font-size: var(--md-sys-typescale-body-medium-size); color: var(--cp-fg-muted);
  line-height: 1.45; }
.cp-doc-form { max-width: 720px; margin: 16px 0 0; display: flex; flex-wrap: wrap; gap: 8px; }
.cp-doc-url { flex: 1 1 320px; min-height: 44px; padding: 10px 18px; font: inherit;
  font-size: var(--md-sys-typescale-body-medium-size);
  border: 0; border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-surface-container-high); color: inherit; }
/* The one true "commit" action on the page — an M3 *filled button*. */
.cp-doc-btn, .cp-bug-submit { font: inherit; font-size: var(--md-sys-typescale-label-large-size);
  letter-spacing: var(--md-sys-typescale-label-large-tracking); font-weight: 500;
  min-height: 40px; padding: 10px 24px; border-radius: var(--md-sys-shape-corner-full);
  border: 0; background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary);
  cursor: pointer;
  transition: box-shadow var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-doc-btn:hover, .cp-bug-submit:hover { background: var(--md-sys-color-primary);
  box-shadow: var(--md-sys-elevation-level1); }
/* The bug-report page. The submit row keeps the button and its "nothing is sent from here" caveat
   on one line, because the caveat is the answer to the question the button raises. */
.cp-report-bug-form { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 16px;
  margin: 0 0 8px; }
.cp-bug-submit { display: inline-flex; align-items: center; gap: 8px; }
/* The facts table is a two-column key/value sheet rather than the status page's headed grid: it has
   no column meanings to declare, so the row headers carry the whole structure. */
.cp-report-facts th { width: 220px; text-align: left; font-weight: 500;
  color: var(--cp-fg-muted); white-space: nowrap; }
.cp-report-facts td { word-break: break-word; }
/* Deliberately small. It is evidence attached to a report, not the subject of the page — a full
   viewer-sized render here would push the thing the visitor came to press below the fold. */
.cp-report-shot { display: block; max-width: min(100%, 320px); height: auto; margin: 4px 0 0;
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-low); }
.cp-report-preview { margin: 0; padding: 14px 16px; overflow-x: auto; white-space: pre-wrap;
  word-break: break-word; border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-low);
  font-size: var(--md-sys-typescale-body-small-size); }
.cp-doc-result { max-width: 720px; margin: 18px 0 0; padding: 16px 20px;
  border-radius: var(--md-sys-shape-corner-medium); border: 0;
  background: var(--md-sys-color-surface-container-low);
  font-size: var(--md-sys-typescale-body-medium-size); }
.cp-doc-result[hidden] { display: none; }
.cp-doc-result.cp-doc-error { background: var(--md-sys-color-error-container);
  color: var(--md-sys-color-on-error-container); border-color: transparent; }
.cp-doc-stage { max-width: 720px; border: 0; border-radius: var(--md-sys-shape-corner-large);
  background: var(--md-sys-color-surface-container-low); box-shadow: var(--md-sys-elevation-level1);
  padding: 16px; display: flex; align-items: center; justify-content: center; min-height: 260px; }
.cp-doc-stage canvas, .cp-doc-stage svg, .cp-doc-stage img { max-width: 100%; height: auto; }
.cp-doc-status { margin: 8px 0 0; font-size: var(--md-sys-typescale-body-small-size);
  color: var(--cp-fg-muted); min-height: 1.2em; }
.cp-doc-facts { max-width: 720px; margin: 16px 0 0; display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; }
.cp-doc-expiry { display: inline-block; padding: 4px 10px;
  border-radius: var(--md-sys-shape-corner-full);
  font-size: var(--md-sys-typescale-label-medium-size); font-weight: 500;
  background: light-dark(#ffdfb0, #4a3000); color: light-dark(#573100, #ffd8a3); border: 0; }

@media (min-width: 1100px) {
  /* Component discovery is part of the desktop workspace, so the list still *opens* by default
     here — but it is no longer nailed open. `cp-nav-closed` is the explicit "the reader put this
     away" class the drawer script sets, and it is what lets a 240px column be reclaimed for the
     render on a desktop too; without it the toggle in the title bar would be a control that did
     nothing at exactly the width where the column costs the most. Three states, not two: no class
     (open here, closed below), `cp-nav-open` (open everywhere), `cp-nav-closed` (closed
     everywhere), so a no-JS desktop keeps today's list. */
  .cp-viewer:not(.cp-nav-open):not(.cp-nav-closed) .cp-nav { display: flex; }
}
/* Mobile / narrow viewports. The viewer is a flex row of stage + overrides (+ optional nav);
   on a phone we collapse it to a single stacked column and — crucially for usability on a tall
   preview — turn the two drawers into bottom sheets reachable from a sticky toggle bar, so the
   overrides and the component list are one tap away instead of a long scroll below the preview.
   We also drop the flex items' min-width so nothing overflows the viewport, and reflow the
   grids, size rows and copyable link rows to fit a ~320px screen without horizontal scrolling. */
@media (max-width: 640px) {
  body { padding: 14px; }
  /* ── ONE bar ────────────────────────────────────────────────────────────────────────────────
     A phone gets: the bar, the title, the thing the page is for. The bar used to be three stacked
     rows — brand, status badge, nav — which is ~120px of every screen spent saying where you are
     before the page starts, and on the viewer a fourth went to the breadcrumb.

     So it becomes an app bar: mark, where-you-are, and one `⋮` that holds the navigation. Nothing
     is dropped — the wordmark goes because the mark beside it is the same link and the page title
     names the catalog; `Catalogs` / `Status` / `GitHub` / the page's own action / Settings all move
     into the menu, which is where a phone expects to find them. The row scrolls nothing and wraps
     nothing: the lead is the only elastic slot and it ellipsises. */
  .cp-site-header { min-height: 48px; margin-bottom: 12px; padding: 2px 0 6px;
    grid-template-columns: minmax(0, 1fr) auto; column-gap: 8px; row-gap: 0; }
  .cp-site-lead { min-width: 0; flex-wrap: nowrap; gap: 8px; overflow: hidden; }
  .cp-site-mark { width: 26px; height: 26px; }
  /* The mark alone is the home link. Two words of product name is the single widest thing in the
     bar and the least useful of them on a phone — you know what site you are on. */
  .cp-site-wordmark { display: none; }
  .cp-interface-mode-label { display: none; }
  .cp-interface-mode button { padding-inline: 9px; }
  /* The bar names the catalog AND a sub-page's breadcrumb opens with the same name. Inline on a
     laptop they read as one line; in a 200px slot they are the same word twice with no room for
     the half that navigates, so the plain copy goes and the crumb stays. */
  .cp-site-lead:has(.cp-breadcrumb) .cp-site-catalog { display: none; }
  /* Likewise the landing's "← All design systems": the menu's `Catalogs` is that link. */
  .cp-site-lead .cp-back { display: none; }
  .cp-site-lead .cp-breadcrumb, .cp-site-catalog { padding-left: 0; border-left: 0;
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .cp-site-lead .cp-breadcrumb > a { min-width: 0; }
  /* …and here the disclosure appears: a 40px target, and the panel of links it opens on an M3 menu
     surface. `position: relative` on the nav rather than on the details, so the panel hangs off the
     bar's trailing edge instead of the button's. Everything below is `[open] +` — no script decides
     any of it, so the menu behaves the same whether the bundle loaded or not. */
  .cp-site-nav { justify-self: end; position: relative; }
  .cp-site-menu { display: block; }
  .cp-site-menu-btn { display: inline-flex; align-items: center;
    justify-content: center; width: 40px; height: 40px;
    border-radius: var(--md-sys-shape-corner-full);
    color: var(--md-sys-color-on-surface-variant); font-size: 1.25rem; line-height: 1;
    cursor: pointer; list-style: none; user-select: none; }
  .cp-site-menu-btn::-webkit-details-marker { display: none; }
  .cp-site-menu[open] > .cp-site-menu-btn { color: var(--md-sys-color-primary);
    background: color-mix(in srgb, var(--md-sys-color-primary) 8%, transparent); }
  .cp-site-menu-panel { display: none; position: absolute; right: 0; top: calc(100% + 4px);
    z-index: 60; min-width: 11em;
    max-width: min(280px, calc(100vw - 28px)); padding: 8px;
    border-radius: var(--md-sys-shape-corner-medium);
    background: var(--md-sys-color-surface-container-high);
    box-shadow: var(--md-sys-elevation-level2); }
  .cp-site-menu[open] + .cp-site-menu-panel { display: flex; flex-direction: column;
    align-items: stretch; gap: 2px; }
  /* GitHub is in the menu now, where a row of its own costs nothing, so it stops being the link a
     narrow bar drops. */
  .cp-site-menu-panel > a,
  .cp-site-menu-panel .cp-settings-btn { padding: 10px 12px;
    border-radius: var(--md-sys-shape-corner-small); }
  /* The settings menu nests inside it: its own panel opens in flow under its row rather than
     floating out of the menu it is already in. */
  .cp-site-menu-panel .cp-settings-panel { position: static; width: auto;
    max-width: none; padding: 0; box-shadow: none; background: transparent; }
  .cp-settings-btn-label { display: inline; }
  .cp-disclosure > summary { align-items: flex-start; }
  .cp-disclosure-hint { display: none; }
  .cp-section-title { margin-bottom: 10px; }
  .cp-catalog-tools { margin-left: -14px; margin-right: -14px; margin-bottom: 12px;
    padding: 8px 14px; }
  /* The trust badge can carry a long branch string; constrain it to the viewport and let it
     wrap instead of forcing a horizontal scroll. */
  .cp-badge { max-width: 100%; white-space: normal; overflow-wrap: anywhere; }
  /* ── A catalog leads with its components ────────────────────────────────────────────────────
     Everything between the heading and the first card is chrome, and on a phone every row of it is
     bought out of the ~800px a visitor can actually see. There were four such blocks, each a row or
     more: the catalog's actions, the Theme group (one chip per declared theme — five rows on a
     design system that publishes a dozen), the filter field, and on a sectioned catalog the whole
     navigation tree. Together they put the first preview 500–700px down a 800px screen.

     They become ONE row: `[Filter previews…] [Theme ▾] [⋯]`. The two chip groups are menus at every
     width now (see `.cp-theme-menu` and `.cp-actions-panel` above), so all that is left here is the
     filter field — which belongs to the tree's sidebar whenever there is a tree —
     which `<cp-catalog-toolbar>` moves into the sticky toolbar, in the DOM, so tab order still
     matches what is on screen. */
  .cp-catalog-tools, .cp-catalog-actions { flex-wrap: nowrap; gap: 8px; }
  .cp-catalog-tools .cp-toolbar { flex: none; min-width: 0; }
  .cp-catalog-tools .cp-head-toggles { flex: none; }
  .cp-catalog-tools .cp-searchbar, .cp-catalog-actions .cp-searchbar {
    flex: 1 1 auto; min-width: 0; }
  /* `min-width: 0` on the FIELD as well as its wrapper, and a zero basis. A flex item's automatic
     minimum size is its content's, and an `<input>`'s content width is its `size` attribute — 20
     characters — so on a 320px screen, where the pill and the `⋯` leave the wrapper about 100px,
     the field kept its 260px basis, overflowed and put a horizontal scrollbar under the whole
     page. The wrapper shrinking does not make its contents shrink. */
  .cp-search { min-height: 40px; padding: 8px 14px; min-width: 0; flex-basis: 0; }
  .cp-catalog-tools .cp-catalog-actions { flex: none; margin: 0; }
  /* Both of the toolbar's panels are capped narrower than the 320/360px they take on a laptop:
     `max-width` caps how wide a panel is and says nothing about where it starts, so an uncapped one
     grows past the screen and takes the first characters of every label with it. Scoped to the
     landing's two — the viewer's own Theme menu opens on a page with different neighbours and keeps
     its own cap. */
  .cp-catalog-theme .cp-theme-menu-panel, .cp-actions-panel {
    max-width: min(280px, calc(100vw - 28px)); }
  /* …and the cap alone is not enough for the THEME panel, because it is not the last control on the
     row: the `⋯` and the gap put its anchor ~48px inside the right edge, so a 280px panel
     right-anchored to the pill starts at -14px on a 320px screen and the first characters of a
     qualified `Group · Name` fall off the left of the display. (Measured: `left: -14`, panel 278px
     wide, viewport 320.) The `⋯` panel has no such problem — it IS the last control, so its right
     edge is the row's.
     Dropping `position: relative` here hands the panel to the sticky toolbar, which spans the whole
     row, and `left: 0` then anchors it to the row's left edge instead of the pill's. The cap still
     keeps the right edge on screen, so the panel sits inside the viewport at both ends whatever a
     catalog calls its themes. */
  .cp-catalog-theme { position: static; }
  .cp-catalog-theme .cp-theme-menu-panel { left: 0; right: auto; }
  /* The heading is the catalog's name and the bar three rows above it is already the catalog's
     name. Hidden rather than removed — `clip` keeps it in the accessibility tree, so the document
     still has its `<h1>` and the page still announces what it is. Only when the bar really is
     carrying that name: a plain module names itself nowhere else. */
  html:has(.cp-site-catalog) .cp-catalog-title { position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  /* The clipped title leaves the flow, and `<cp-catalog-toolbar>` takes the tally below the grid,
     so the row that holds them both is left with nothing to show and no reason to keep its own
     bottom margin above the toolbar. */
  html:has(.cp-site-catalog) .cp-catalog-head-row { margin: 0; }
  .cp-sub { margin-bottom: 10px; }
  .cp-catalog-head-row .cp-sub { margin-bottom: 10px; }
  .cp-catalog-menu { margin-bottom: 14px; }
  /* ── The navigation tree, as one scrolling strip ─────────────────────────────────────────────
     Above 960px the tree is a sidebar standing beside the grid; below it, it is a full-width
     outline ABOVE the grid, and on a phone that outline is the single biggest thing between the
     filter and the previews — a sectioned catalog put its first card 526px down, a grouped one
     709px. Its top level becomes a row of chips that scrolls sideways, which is how a phone shows
     a set of sections, and the levels below it fold away: a four-deep outline over a two-up card
     grid is desktop furniture, and everything it reaches is reachable by picking a section and by
     the filter field now sitting directly above it. */
  .cp-catalog-menu .cp-tree-list { display: flex; flex-wrap: nowrap; gap: 6px;
    overflow-x: auto; overflow-y: hidden; scrollbar-width: thin;
    padding-bottom: 4px; scroll-padding-inline: 8px; }
  .cp-catalog-menu .cp-tree-list > li { flex: none; }
  .cp-catalog-menu .cp-tree-list > li > .cp-tree-children { display: none; }
  .cp-catalog-menu .cp-tree-list > li > .cp-tree-link,
  .cp-catalog-menu .cp-tree-list > li > .cp-tab { width: auto; white-space: nowrap; }
  /* The twisty says a branch opens, and here no branch does. */
  .cp-catalog-menu .cp-tree-list > li > .cp-tree-link[aria-expanded]::before,
  .cp-catalog-menu .cp-tree-list > li > .cp-tab[aria-expanded]::before { display: none; }
  /* The Pages row is divided off from the components above it by a rule and a margin, which in a
     column reads as a separator and in a row reads as a stray line that also drops the chip below
     its neighbours' baseline. A row divides the same thing the other way. */
  .cp-catalog-menu .cp-tree-list > li.cp-tree-pages { margin: 0 0 0 4px; padding: 0 0 0 10px;
    border-top: 0; border-left: 1px solid var(--md-sys-color-outline-variant); }
  /* The title row, and by now it is only the title: `viewer-drawers.js` has moved the disclosure
     pills below the stage, and the opaque preview id goes because it is the one thing here a phone
     was already ellipsising into a stub (it is in the URL, the export links and the title
     attribute regardless). Still sticky — a preview can be several screens tall, and which
     component you are looking at is the fact worth pinning. */
  .cp-preview-head { position: sticky; top: var(--site-header-height); z-index: 21;
    margin: 0 0 8px; padding: 6px 0;
    background: var(--cp-bg); row-gap: 6px; }
  .cp-preview-head .cp-preview-id { display: none; }
  /* The name is NOT clamped to one line here, though it would buy a row back: the trust verdict is
     a `<span>` inside the `<h1>`, so an ellipsis on the headline eats the badge — and "is this
     render trustworthy" is not a fact to drop for 24px of a sticky row. */
  /* The two rows below the stage. `.cp-viewer +` targets them only where the reflow has actually
     put them, so a no-JS phone — which keeps them above, in source order — is unaffected. */
  .cp-viewer + .cp-preview-primary { margin: 14px 0 0; }
  .cp-preview-primary + .cp-head-toggles { margin: 10px 0 0; }
  /* Three pills on ONE line rather than two: every control the viewer has that is not the render
     lives here, and at desktop padding they come to ~396px against a phone's ~384px of content
     width — twelve pixels short, paid for with a whole 42px row. The labels are untouched, so
     nothing is abbreviated to get there. */
  .cp-head-toggles { margin-left: 0; gap: 6px; }
  .cp-head-toggles .cp-drawer-toggle { padding: 8px 12px; }
  .cp-head-toggles .cp-drawer-toggle.cp-axis-toggle { padding: 6px 10px; }
  /* The renderer row — the lane toggle, the format chips a catalog offers (Snapshot · Figma ·
     Source, SVG, 3D), Transparent, Fit width, and the hint that names the current lane — wrapped
     to three lines. One scrolling row, same rule as the two chip groups on the landing. */
  .cp-preview-primary { flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden;
    scrollbar-width: thin; padding-bottom: 4px; scroll-padding-inline: 8px; }
  .cp-preview-primary > * { flex: none; }
  /* The hint is prose, not a chip: left to wrap inside a nowrap row it would size to its own
     max-content and push everything after it off the row's scrollable width for no gain. */
  .cp-preview-primary .cp-mode-hint { white-space: nowrap; }
  .cp-viewer { gap: 16px; }
  /* Stage, overrides and nav each stack full-width; min-width:0 lets the flex items (and their
     selects/inputs) shrink below their content's intrinsic width instead of overflowing. */
  .cp-stage, .cp-controls, .cp-nav { flex: 1 1 100%; min-width: 0; }
  .cp-controls { position: static; max-height: none; }
  .cp-controls label, .cp-group, .cp-group-body { min-width: 0; }
  .cp-controls select, .cp-controls input { max-width: 100%; }
  /* Open drawers overlay the preview as bottom sheets (with the scrim below), so overrides and
     the component list are reachable without scrolling past a tall preview. */
  .cp-viewer.cp-controls-open .cp-controls,
  .cp-viewer.cp-nav-open .cp-nav {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 40; margin: 0;
    max-height: 78vh; overflow: auto;
    padding: 12px 14px calc(14px + env(safe-area-inset-bottom, 0px));
    border: 0; border-top: 1px solid var(--cp-border); border-radius: 14px 14px 0 0;
    background: var(--cp-bg);
    box-shadow: 0 -8px 24px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.5)); }
  /* Grabber affordance at the top of a sheet. */
  .cp-viewer.cp-controls-open .cp-controls::before,
  .cp-viewer.cp-nav-open .cp-nav::before {
    content: ""; display: block; width: 36px; height: 4px; margin: 0 auto 10px;
    border-radius: 999px; background: var(--cp-border-strong); }
  .cp-scrim.cp-scrim-on { display: block; position: fixed; inset: 0; z-index: 35;
    background: rgba(0, 0, 0, 0.38); }
  .cp-grid, .cp-cards { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
  .cp-syslist, .cp-syslist.cp-grid { grid-template-columns: 1fr; }
  .cp-card.cp-sys { grid-template-rows: 220px 1fr; }
  .cp-imgwrap img { max-height: 200px; }
  .cp-syslist .cp-imgwrap > img { max-height: none; }
  /* The export line wraps to one group per row rather than squeezing six buttons across. */
  .cp-export { gap: 2px 10px; }
  .cp-link-group { flex: 1 1 100%; }
  /* The paired Min/Max (and Fixed W/H) size inputs stack rather than sit side by side. */
  .cp-size-row:not([hidden]) { flex-direction: column; gap: 6px; }
  .cp-compare-table th, .cp-compare-table td { padding: 8px; }
  .cp-compare-table tbody th { min-width: 110px; }
  .cp-compare-shot { width: 160px; min-width: 130px; min-height: 82px; }
}

/* Render-server badge (see ServeWeb.presenceScript). Catalogs open their daemon on first real use,
   so "not running" is a normal resting state — styled as neutral information, not a fault. */
.cp-daemon-status {
  display: inline-flex; align-items: center; gap: 5px;
  min-width: 38px; justify-content: center;
  padding: 3px 8px;
  border-radius: var(--md-sys-shape-corner-full);
  font-size: var(--md-sys-typescale-label-medium-size);
  letter-spacing: var(--md-sys-typescale-label-medium-tracking);
  line-height: 1.6;
  border: 0;
  background: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface-variant);
}
/* The badge rides INSIDE the header's Status link, so it is a count on a label rather than a
   free-floating pill: `Status ●5`. The nav link already spaces its children (`gap: 6px`), and the
   pill keeps its own colours so the running/idle answer survives the link's hover state. Tightened
   from the standalone pill it used to be — beside a word it no longer has to reserve a minimum
   width to stop the header jittering as the count changes digits, because the link's own text is
   what holds the slot. */
.cp-site-nav .cp-daemon-status { min-width: 0; padding: 2px 7px; }
.cp-daemon-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.cp-daemon-status[data-cp-daemon-running="1"] {
  background: light-dark(#d7f0dd, #0d3a1c); color: light-dark(#10521f, #86dc9d);
}
/* The `display` above outranks the UA's `[hidden] { display: none }`, so the badge has to opt back
   out explicitly — otherwise the reserved slot paints an empty pill before the first poll answers
   (and on every page that never polls a daemon at all). */
.cp-daemon-status[hidden] {
  display: none;
}

/* Render-history menu (viewer-history.js) — the versions THESE pixels have had, on the same
   `<details>` shape as the Revision and Theme controls it sits beside in the title row.
   It was a strip of dated chips under the viewer bar, which is the pattern the revision list had
   already shed: a wall of chips spends the width above the render on a list nobody reads most of
   the time. Closed, this is one control in a row that already exists — so it costs nothing at any
   width, and nothing stands between the title and the render on a phone. */
.cp-history-menu { position: relative; display: inline-block;
  font-size: 0.82rem; color: var(--cp-fg-muted); }
.cp-history-btn { display: inline-flex; align-items: baseline; gap: 8px; padding: 7px 14px;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-full);
  color: var(--md-sys-color-on-surface-variant); cursor: pointer; list-style: none;
  user-select: none;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard); }
.cp-history-btn::-webkit-details-marker { display: none; }
.cp-history-btn:hover, .cp-history-menu[open] > .cp-history-btn {
  color: var(--md-sys-color-on-surface);
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
.cp-history-key { font-weight: 650; }
.cp-history-caret { font-size: 0.9em; opacity: 0.7; }
.cp-history-menu[open] .cp-history-caret { transform: rotate(180deg); }
/* The flapping flag rides on the TRIGGER, so a closed menu still admits that its list is trimmed. */
.cp-history-unstable { padding: 1px 7px; border-radius: var(--md-sys-shape-corner-full);
  background: light-dark(#fff4e5, #40300f); color: light-dark(#8a5300, #f0c07a);
  font-size: 0.92em; cursor: help; }
/* Menu surface: the same M3 elevated container as the revision menu, capped so a long history
   scrolls inside the popup instead of pushing the page around. Right-anchored — this control sits
   near the end of the toggle row, and a left-anchored popup there runs off the viewport. */
.cp-history-panel { position: absolute; right: 0; top: calc(100% + 6px); z-index: 50;
  width: max-content; max-width: min(420px, calc(100vw - 32px));
  max-height: min(60vh, 420px); overflow-y: auto; padding: 8px 4px;
  border: 0; border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-high);
  box-shadow: var(--md-sys-elevation-level2); }
.cp-history-list { display: flex; flex-direction: column; gap: 2px; margin: 0; }
.cp-history-item { display: flex; align-items: baseline; gap: 10px; padding: 9px 12px;
  border: 0; border-radius: var(--md-sys-shape-corner-small);
  color: var(--cp-fg); text-decoration: none; white-space: nowrap; }
.cp-history-item:hover { text-decoration: none;
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent); }
/* The render on the stage right now, marked the way the revision menu marks the publish you are
   pinned to — so the two menus answer "which one am I on?" the same way. */
.cp-history-item[data-current="1"] { background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container); }
.cp-history-date { font-variant-numeric: tabular-nums; }
.cp-history-meta { font-size: 0.92em; opacity: 0.85; margin-left: auto;
  font-family: var(--cp-mono, ui-monospace, SFMono-Regular, Menlo, monospace); }
.cp-history-span { font-size: 0.85em; opacity: 0.7; }
.cp-history-note { margin: 8px 12px 4px; max-width: 34ch;
  color: var(--md-sys-color-on-surface-variant); font-size: 0.92em; line-height: 1.45; }

/* ── Design page ──────────────────────────────────────────────────────────────────────────────
   One whole page of the design file, inlined as SVG, with an outline over every component node on
   it and — behind a toggle — this catalog's own renders standing in for the design's own drawing.

   Unlike the screen backdrop this replaced, the geometry is NOT in percentages: the SVG is the
   source of truth for where a node is, so `<cp-design-page>` measures each `[data-node-id]` element
   and writes the four offsets. Everything here is therefore paint and state, not layout — which is
   also why the stage stacks above the list rather than beside it: a specimen sheet is wider than
   it is tall, the opposite of the phone screens the two-column version was shaped for. */
.cp-page-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1.4rem;
  align-items: start;
}
.cp-page-stage {
  position: relative;
  width: 100%;
  aspect-ratio: var(--cp-page-aspect, 1.2);
  border: 1px solid var(--cp-border);
  border-radius: var(--md-sys-shape-corner-medium, 12px);
  overflow: hidden;
  /* The design's own pixels are the ground truth here, so the stage never tints them: a light
     sheet stays light in dark mode rather than being composited onto a dark surface. */
  background: #fff;
}
/* Everything that ZOOMS, in one layer: the export, the overlays over it, the renders inside those.
   One transform on this element moves the sheet and its whole instrumentation together, so a slot
   cannot drift off the shape it stands in no matter how far in the reader is — which is the whole
   reason the transform is here rather than on the SVG.

   Filling the stage exactly (`inset: 0`) is load-bearing twice over: it is what makes the overlays'
   percentage placement mean "percent of the stage", and `transform` on it makes it their containing
   block, so that stays true once a zoom is applied. The tip and the zoom bar are deliberately NOT
   in here — a tooltip that scaled 12x would be unreadable, and a control that panned away with the
   sheet could not be reached to undo the pan. */
.cp-page-canvas {
  position: absolute;
  inset: 0;
  transform-origin: 0 0;
  /* Eased, so a double-click READS as travel into the sheet rather than as a jump cut to somewhere
     else. Suppressed while a wheel-zoom or a drag is in flight (`.cp-page-canvas-live`), where the
     gesture is already continuous and a transition would lag the pointer. */
  transition: transform 170ms ease-out;
}
.cp-page-canvas-live { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .cp-page-canvas { transition: none; }
}
/* The inlined export. `width/height: 100%` on the element and the frame's own aspect on the stage
   between them make the SVG's user units and the stage's pixels a fixed ratio, which is what lets
   a measured node box stay correct through any resize. */
.cp-page-canvas > svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
/* Zoomed in, the sheet is bigger than its window, so the stage becomes something you drag. Grab
   rather than `move`: the hand says "the surface comes with you", which is what a pan does.

   `touch-action: none` is what makes that drag work with a FINGER. Without it the browser is free
   to claim the gesture for its own scroll or pinch, and the moment it does it fires `pointercancel`
   — the pan stops receiving moves a few pixels in, so the advertised interaction half-works.
   Scoped to the zoomed state, so at 1:1 the stage is an ordinary part of a scrollable page. */
.cp-page-zoomed {
  cursor: grab;
  touch-action: none;
}
.cp-page-zoomed.cp-page-panning { cursor: grabbing; }
/* A pan must not select the sheet's text on the way past, and an overlay is an `<a>` — which a
   browser will happily start dragging as a link instead. */
.cp-page-zoomed .cp-page-canvas {
  user-select: none;
  -webkit-user-drag: none;
}
/* The design's own drawing of a node we have a render for, hidden so ours can stand in its place.
   `visibility` rather than `display`: a hidden SVG element keeps its box, so the measurement that
   positioned our render stays valid while it is hidden, and toggling back is free. */
.cp-page-hide-design .cp-page-replaced { visibility: hidden; }
/* A hit area, drawn as nothing. The sheet is the content, so the resting state of every node is
   invisible — a mark appears where the reader is pointing, or across the board once they ask for
   the layer.

   An INSET OUTLINE and not a border, because this element is also the slot our render stands in.
   A border is layout: it shrinks the padding box, which is what `inset: 0` on the render resolves
   against, so a 2px transparent border drew every render 4px narrower and shorter than the node
   the design drew — a uniform ~4% shrink on this catalog's Shape page, and worse the smaller the
   node. An outline is paint only, so the padding box IS the measured node box and the render fills
   it exactly; `outline-offset: -2px` puts the mark back where the inner border drew it, and an
   outline still doesn't move anything when it goes from transparent to coloured. */
.cp-page-node {
  position: absolute;
  display: block;
  padding: 0;
  border: 0;
  outline: 2px solid transparent;
  outline-offset: -2px;
  border-radius: 3px;
  background: transparent;
  text-decoration: none;
  cursor: pointer;
  transition: background 120ms ease, box-shadow 120ms ease, outline-color 120ms ease;
}
/* Colour by *how* the placement was linked, so the weak links read as weak at a glance. An
   unlinked rectangle — a part of the sheet no code implements — is the dashed red one, and is the
   single most interesting mark on the page once the layer is on. */
.cp-page-node[data-link="code-connect"] { --cp-page-node-color: #2da44e; }
.cp-page-node[data-link="manifest"] { --cp-page-node-color: #0969da; }
.cp-page-node[data-link="convention"] { --cp-page-node-color: #bf8700; }
/* Red is reserved for a real gap. An unlinked node that is furniture — a private component, a
   variant-set container — is structure rather than missing work, so it takes the neutral grey and
   stops competing for the eye with the thing the reader is hunting for. */
.cp-page-node[data-link="unlinked"] { --cp-page-node-color: #6e7781; }
.cp-page-node[data-cp-gap] { --cp-page-node-color: #cf222e; }
/* The opt-in layer: every node marked at once, which is the coverage read rather than the reading
   view. */
.cp-page-outlines-on .cp-page-node {
  outline-color: var(--cp-page-node-color, #2da44e);
  background: color-mix(in srgb, var(--cp-page-node-color, #2da44e) 10%, transparent);
}
.cp-page-outlines-on .cp-page-node[data-link="unlinked"] { outline-style: dashed; }
.cp-page-outlines-on .cp-page-node[data-cp-gap] {
  background: color-mix(in srgb, #cf222e 8%, transparent);
}
/* Pointing, keyboard focus, and selection all mark ONE node — and do it whether or not the resting
   layer is on. This is the page's primary affordance now: an unmarked sheet that answers where you
   point. Selection is the loudest of the three because it persists after the pointer leaves, and
   it must stay legible against both a light sheet and our own render sitting in the slot. */
.cp-page-node:hover,
.cp-page-node:focus-visible,
.cp-page-node.cp-page-active {
  outline-color: var(--cp-page-node-color, #2da44e);
  background: color-mix(in srgb, var(--cp-page-node-color, #2da44e) 18%, transparent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--cp-page-node-color, #2da44e) 45%, transparent);
}
.cp-page-node.cp-page-selected {
  outline-color: var(--cp-page-node-color, #2da44e);
  background: color-mix(in srgb, var(--cp-page-node-color, #2da44e) 12%, transparent);
  box-shadow:
    0 0 0 2px #fff,
    0 0 0 4px var(--cp-page-node-color, #2da44e);
}
/* "Only what we don't implement" — keeps the components with no code behind them and mutes the
   rest, which is the fastest read of a page's coverage.

   Keyed on `data-cp-gap`, NOT on `data-link="unlinked"`. Unlinked also catches the sheet's own
   furniture: a private component (Figma's leading-dot convention) and the `COMPONENT_SET` container
   whose variants ARE the components listed beside it. Filtering on unlinked made a Shape page whose
   35 shapes are all implemented light up `.Header`, `.Header` and `Shape Set` as work to do — three
   marks that no amount of code would ever clear. */
.cp-page-unlinked-only .cp-page-node:not([data-cp-gap]) {
  opacity: 0.12;
  pointer-events: none;
}
/* A render fills the slot the design left, scaled UNIFORMLY and centred — never stretched. A
   component that renders taller than the design drew it is a real finding, and stretching to the
   box would hide exactly that.

   What gets fitted is the render's DRAWN PIXELS, not its canvas. `<cp-design-page>` measures the
   image's ink box once it loads and writes `left`/`top`/`width`/`height` here, because the two
   sides of this swap are boxes of different kinds: the design node's box is the tight bounds of
   the shape Figma drew, while a render is a fixed canvas with the component sitting inside it and
   transparent margin around. Fitting canvas-to-ink letterboxes our half against the design's — on
   this catalog's Shape page the semicircle landed at 58% of the size the design drew it, which is
   the "everything scales when you flip the lane" the two lanes exist to rule out.

   The `inset: 0` + `object-fit: contain` below is the FALLBACK, and the whole rule while the image
   is still loading: an ink box needs a decoded image and a canvas read, and neither is guaranteed
   (a cross-origin render taints the canvas, a fully transparent one has no ink). It is exactly the
   behaviour this lane had before ink-fitting, so the degraded case is the old case. */
.cp-page-render {
  position: absolute;
  inset: 0;
  display: none;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
}
.cp-page-swap-on .cp-page-render { display: block; }
/* A render the server could not produce. `<cp-design-page>` restores the design's own drawing in that
   slot and hides the failed image — but the rule above is a two-class selector and would beat the
   UA stylesheet's `[hidden]`, leaving the browser's broken-image glyph sitting on top of the
   drawing just restored. Specificity has to be answered explicitly. */
.cp-page-swap-on .cp-page-render[hidden] { display: none; }
/* A node the manifest named but the export doesn't carry — a layer the design tool flattened on
   the way out. Its row stays (the mapping is still true); its outline can't, because there is
   nothing to put it around. */
.cp-page-node[data-cp-missing] { display: none; }
/* The diff lane's readout: how far our render is from the design's drawing of that same node, as
   one number per slot. Hidden outside the lane rather than not built, so flipping back into it is
   free and the scoring runs once.

   A number and not a diff map, deliberately — thirty-eight magenta thumbnails at slot size is the
   annotated sheet this page was rescued from. The map is one click away at a readable size. */
.cp-page-score { display: none; }
.cp-page-diff-on .cp-page-score {
  position: absolute;
  right: 2px;
  bottom: 2px;
  display: block;
  padding: 0.05rem 0.3rem;
  border-radius: 999px;
  background: var(--cp-score-bg, #6e7781);
  color: #fff;
  font-size: 0.68rem;
  font-variant-numeric: tabular-nums;
  line-height: 1.5;
  pointer-events: none;
}
/* Three bands, not a gradient: the reader is triaging, and "does this need looking at" is a
   decision rather than a measurement. */
.cp-page-score[data-cp-score="close"] { --cp-score-bg: #1a7f37; }
.cp-page-score[data-cp-score="drifting"] { --cp-score-bg: #9a6700; }
.cp-page-score[data-cp-score="far"] { --cp-score-bg: #cf222e; }
.cp-page-score[data-cp-score="none"] { --cp-score-bg: #6e7781; }
/* In the diff lane every scoreable node is a way out to its full comparison, so the whole slot
   reads as a link rather than as something to select in place. */
.cp-page-diff-on .cp-page-node { cursor: zoom-in; }
.cp-page-diff-link { display: none; }
/* ── Zoom ──────────────────────────────────────────────────────────────────────────────────────
   A specimen sheet is 6263 px wide in the design and lands in a column a sixth of that, so its
   labels are sub-pixel and its swatch numbers are gone. Zoom is what makes the surface readable at
   all — and it is a TRANSFORM on `.cp-page-canvas`, not a re-render, so the export stays vector
   sharp all the way in and the overlays stay welded to the shapes they mark.

   Everything the reader sees ON TOP of the sheet is counter-scaled by `--cp-page-zoom` so it keeps
   its size in screen pixels: a mark drawn 2 px at 1x would be a 24 px slab at 12x, and a score badge
   would be a billboard. The instrumentation should not grow just because the drawing did.

   The OUTLINE and its offset, not `border-width` — a node's mark is an inset outline precisely so
   the box it paints on stays the measured node box for the render inside it (see `.cp-page-node`
   above), and its `border` is 0. Counter-scaling the border is a no-op that looks like it works. */
.cp-page-node {
  outline-width: calc(2px / var(--cp-page-zoom, 1));
  outline-offset: calc(-2px / var(--cp-page-zoom, 1));
}
.cp-page-node:hover,
.cp-page-node:focus-visible,
.cp-page-node.cp-page-active {
  box-shadow: 0 0 0 calc(2px / var(--cp-page-zoom, 1))
    color-mix(in srgb, var(--cp-page-node-color, #2da44e) 45%, transparent);
}
.cp-page-node.cp-page-selected {
  box-shadow:
    0 0 0 calc(2px / var(--cp-page-zoom, 1)) #fff,
    0 0 0 calc(4px / var(--cp-page-zoom, 1)) var(--cp-page-node-color, #2da44e);
}
/* The badge's SIZE and its INSET both, or the score walks away from the corner it marks: `right`
   and `bottom` resolve inside the transformed layer, so a 2 px inset paints 24 px in at 12x and 48
   at the cap — far enough to sit over the middle of the component it is scoring. */
.cp-page-diff-on .cp-page-score {
  right: calc(2px / var(--cp-page-zoom, 1));
  bottom: calc(2px / var(--cp-page-zoom, 1));
  transform: scale(calc(1 / var(--cp-page-zoom, 1)));
  transform-origin: 100% 100%;
}
/* The way back out, and the readout that says how far in you are — `<cp-page-zoom>`, which renders
   its own buttons into this pill (light DOM, so these rules reach them). Subtle on purpose: it is
   parked in the stage's bottom-right corner, it is only there while the sheet IS zoomed (at 1:1 there
   is nothing to reset, and a permanent control would be chrome over the drawing), and it sits at
   two-thirds opacity until pointed at.

   `display: flex` beats the `hidden` attribute on its own — the same trap the legend fell into — so
   the tag has to be told to respect it, or an unzoomed sheet would carry a bar offering to reset a
   zoom nobody applied. The server ships it `hidden` for that reason too: the element only unhides
   itself once there is a zoom. */
cp-page-zoom {
  position: absolute;
  right: 0.5rem;
  bottom: 0.5rem;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 0.15rem;
  padding: 0.15rem;
  border: 1px solid var(--cp-border);
  border-radius: 999px;
  background: color-mix(in srgb, var(--cp-surface, #fff) 88%, transparent);
  backdrop-filter: blur(6px);
  box-shadow: 0 1px 4px rgb(0 0 0 / 12%);
  color: var(--cp-fg-muted);
  font-size: 0.72rem;
  opacity: 0.62;
  transition: opacity 120ms ease;
}
cp-page-zoom[hidden] { display: none; }
cp-page-zoom:hover,
cp-page-zoom:focus-within { opacity: 1; }
cp-page-zoom button {
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: inherit;
  font: inherit;
  line-height: 1;
  cursor: pointer;
}
.cp-page-zoom-step {
  width: 1.5rem;
  height: 1.5rem;
  font-size: 0.95rem;
}
.cp-page-zoom-reset { padding: 0.3rem 0.5rem; }
cp-page-zoom button:hover {
  background: color-mix(in srgb, var(--cp-accent, #6750a4) 14%, transparent);
  color: var(--cp-fg);
}
cp-page-zoom button:focus-visible {
  outline: 2px solid var(--cp-accent, #6750a4);
  outline-offset: -1px;
}
.cp-page-zoom-level {
  min-width: 3rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
/* How the sheet is driven, said once next to the controls rather than discovered by accident.
   Double-click is not a gesture anyone tries on a picture unprompted, and the modifier on the wheel
   is the half nobody guesses. */
.cp-page-hint {
  color: var(--cp-fg-muted);
  font-size: 0.72rem;
}
.cp-page-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.9rem;
  margin: 0.8rem 0;
  font-size: var(--md-sys-typescale-body-small-size, 0.82rem);
}
.cp-page-controls label { display: inline-flex; align-items: center; gap: 0.35rem; }
/* The flip, as a segmented pair rather than two loose radios: the two lanes are one question with
   one answer, and it is the control the page is built around, so it reads as a switch you throw
   rather than a preference you set. The radios stay real (and stay reachable) — they are visually
   removed, not `display: none`, so arrow keys still move between the lanes. */
.cp-page-lane {
  display: inline-flex;
  border: 1px solid var(--cp-border);
  border-radius: 999px;
  overflow: hidden;
}
.cp-page-lane label { gap: 0; }
.cp-page-lane input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.cp-page-lane span {
  display: block;
  padding: 0.32rem 0.85rem;
  color: var(--cp-fg-muted);
  cursor: pointer;
}
.cp-page-lane input:checked + span {
  background: var(--cp-accent, #6750a4);
  color: #fff;
  font-weight: 600;
}
.cp-page-lane input:focus-visible + span {
  outline: 2px solid var(--cp-accent, #6750a4);
  outline-offset: -2px;
}
.cp-page-opt { color: var(--cp-fg-muted); }
.cp-page-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: 0.5rem 0 1rem;
  color: var(--cp-fg-muted);
  font-size: 0.75rem;
}
.cp-page-legend span { display: inline-flex; align-items: center; gap: 0.3rem; }
/* `display: flex` above beats the `hidden` attribute on its own, so the legend has to be told to
   respect it — otherwise it stands over an unmarked sheet naming four colours nothing is wearing,
   which is most of the noise this page was carrying. */
.cp-page-legend[hidden] { display: none; }
.cp-page-swatch {
  width: 0.75rem;
  height: 0.75rem;
  border: 2px solid currentColor;
  border-radius: 2px;
}
/* The pointer tooltip: what the node under the cursor is, where the eye already is.
   
   It replaced a strip under the sheet, which on a specimen sheet taller than the fold put the
   answer somewhere the reader could not see while pointing at the question. Inert, so it can never
   eat the click meant for the node beneath it, and positioned by script against the stage. */
.cp-page-tip {
  position: absolute;
  z-index: 2;
  max-width: min(360px, 80%);
  padding: 0.15rem;
  border: 1px solid var(--cp-border);
  border-radius: var(--md-sys-shape-corner-medium, 12px);
  background: var(--cp-surface);
  box-shadow: 0 6px 20px rgb(0 0 0 / 18%);
  pointer-events: none;
}
.cp-page-tip[hidden] { display: none; }
.cp-page-tip-card { padding: 0.4rem 0.6rem; font-size: 0.85rem; }
.cp-page-tip-card .cp-page-row-code { font-size: 0.74rem; }

/* The audit list, collapsed. It is a coverage inventory — every node, linked or not — which is a
   thing to go and check rather than the first thing to read, so it ships closed and says what it
   holds. */
.cp-page-nodes > summary {
  cursor: pointer;
  padding: 0.35rem 0;
  color: var(--cp-fg-muted);
  font-size: 0.82rem;
}
.cp-page-nodes[open] > summary { margin-bottom: 0.4rem; }
/* A definition sheet carries tens of nodes where a screen carried a dozen, so the list wraps into
   columns instead of running off the bottom of the page. */
.cp-page-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 0.2rem;
}
.cp-page-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.5rem;
  align-items: baseline;
  padding: 0.4rem 0.55rem;
  border: 1px solid transparent;
  border-radius: var(--md-sys-shape-corner-small, 8px);
  color: inherit;
  text-decoration: none;
}
.cp-page-row:hover,
.cp-page-row.cp-page-active {
  border-color: var(--cp-border);
  background: var(--cp-surface);
}
.cp-page-row-name { font-weight: 600; }
.cp-page-row-code {
  grid-column: 2;
  color: var(--cp-fg-muted);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.72rem;
  overflow-wrap: anywhere;
}
.cp-page-dot {
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 50%;
  background: var(--cp-page-node-color, #2da44e);
}
.cp-page-row[data-link="code-connect"] { --cp-page-node-color: #2da44e; }
.cp-page-row[data-link="manifest"] { --cp-page-node-color: #0969da; }
.cp-page-row[data-link="convention"] { --cp-page-node-color: #bf8700; }
.cp-page-row[data-link="unlinked"] { --cp-page-node-color: #6e7781; }
.cp-page-row[data-cp-gap] { --cp-page-node-color: #cf222e; }
.cp-page-count { color: var(--cp-fg-muted); font-size: 0.8rem; }
.cp-page-card {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.9rem;
  border: 1px solid var(--cp-border);
  border-radius: var(--md-sys-shape-corner-medium, 12px);
  color: inherit;
  text-decoration: none;
}
.cp-page-card:hover { border-color: var(--cp-border-strong); background: var(--cp-surface); }
.cp-page-card img {
  width: 100%;
  max-height: 260px;
  object-fit: contain;
  object-position: top;
  background: #fff;
  border-radius: var(--md-sys-shape-corner-small, 8px);
}
.cp-page-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 1rem;
}
/* A hotspot is a bare rectangle, so its accessible name lives in a visually-hidden span rather
   than in the (decorative) outline itself. */
.cp-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* Streamlined, Storybook-like component browser (`serve --component-browser`). Full serve mode
   keeps the richer authoring/diagnostic chrome above; these rules only arrange the smaller set of
   controls the server emits in browser mode. */
.cp-component-browser .cp-site-header { grid-template-columns: minmax(0, 1fr) auto; }
.cp-component-browser .cp-site-lead { min-width: 0; }
.cp-component-browser .cp-main { max-width: 1680px; }
.cp-browser-home-tools { position: sticky; top: var(--site-header-height); z-index: 20;
  margin: 0 0 24px;
  padding: 8px 0; background: var(--cp-bg); }
.cp-browser-home-tools .cp-search { display: flex; max-width: 520px; }
.cp-browser-provenance { margin-top: 7px; color: var(--md-sys-color-on-surface-variant);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--md-sys-typescale-body-small-size); overflow-wrap: anywhere; }
.cp-browser-breadcrumb { display: flex; align-items: center; flex-wrap: wrap; gap: 7px;
  margin: 0 0 12px; color: var(--md-sys-color-on-surface-variant);
  font-size: var(--md-sys-typescale-body-medium-size); }
.cp-browser-breadcrumb a { color: var(--md-sys-color-primary); text-decoration: none; }
.cp-browser-breadcrumb a:hover { text-decoration: underline; }
.cp-browser-separator { color: var(--md-sys-color-outline); }
.cp-browser-variant { margin: -7px 0 12px; color: var(--md-sys-color-on-surface-variant); }
.cp-component-browser .cp-preview-primary { border-bottom: 1px solid var(--md-sys-color-outline-variant);
  gap: 4px; padding-bottom: 0; }
.cp-component-browser .cp-browser-tab { border: 0; border-radius: 0; padding: 10px 16px;
  background: transparent; color: var(--md-sys-color-on-surface-variant); }
.cp-component-browser .cp-browser-tab[aria-selected="true"] { color: var(--md-sys-color-primary);
  box-shadow: inset 0 -3px var(--md-sys-color-primary); }
.cp-component-browser cp-backend-badge { display: none; }
.cp-browser-siblings { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px;
  margin: 20px 0 4px; }
.cp-browser-sibling { display: flex; align-items: center; gap: 9px; min-width: 0; padding: 12px 16px;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-medium); color: var(--md-sys-color-primary);
  text-decoration: none; }
.cp-browser-sibling span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cp-browser-sibling-next { grid-column: 2; justify-content: flex-end; flex-direction: row-reverse; }
.cp-browser-sibling:hover { background: var(--md-sys-color-surface-container-low); }
@media (min-width: 1100px) {
  .cp-component-browser .cp-viewer:not(.cp-nav-closed) .cp-nav { display: flex; }
  .cp-component-browser .cp-nav-close { display: none; }
}
@media (max-width: 640px) {
  .cp-browser-home-tools { top: var(--site-header-height); }
  .cp-browser-breadcrumb { white-space: normal; }
  .cp-browser-siblings { grid-template-columns: 1fr; }
  .cp-browser-sibling-next { grid-column: 1; }
}
