/* ==========================================================================
   WDOS Studio — K1 chrome stylesheet
   --------------------------------------------------------------------------
   2026-09-11 REDESIGN (K3 lane): the workbench chrome is rebuilt to
   design-tool quality — an amber honesty chip in a sticky branded header
   with pill navigation, display page headings with eyebrows, card panels
   with a consistent heading tick, a dotted preview stage that frames the kit
   render like an artboard, and first-class status chips.
   2026-09-11 R2 (owner feedback): the Build surface becomes a full-bleed
   canvas layout — a fixed header bar (brand + honesty strip + nav), a left
   sidebar rail (kit, viewport, brief, entries, composition, validation,
   export/import) that scrolls independently, and the preview canvas taking
   ALL remaining space edge-to-edge; the stage pans horizontally so any exact
   child width (incl. 1280) is fully viewable and the chrome itself never
   horizontally overflows.    Below the declared 1024px breakpoint the surface
   keeps the single-column stacking (controls above, canvas below) with a
   tall stacked canvas.
   2026-09-11 R3 (owner feedback): canvas fit + in-canvas zoom + the
   1920 · Full HD preset. The app shell is sized from 100dvh and
   minmax(0, 1fr) grid/flex chains only — never from raw vh on the frame —
   so browser page zoom cannot distort the geometry. The canvas panel is a
   three-row grid (toolbar / stage / status bar) so the faces-disclosure
   status bar is always fully visible and wraps gracefully. The JS zoom
   layer scales a wrapper around the exact-width frame with
   transform: scale() driven by typed attr() (data-zoom/data-h attributes —
   the CSP style-src 'self' forbids inline styles, and content attributes
   are not styles), so the child layout width stays EXACTLY the selected
   preset at every zoom level; the no-JS default stays the static
   exact-width canvas with pan. Every behavior contract is unchanged: no-JS
   static completeness, inert-by-default controls, exact 390/768/1280
   policy child viewports (1920 is an additive Studio preset — the frozen
   policy tuple is untouched), single-property transitions, runtime
   reduced-motion collapse.
   2026-09-11 R4 (owner feedback): sidebar control + canvas declutter.
   (1) The sidebar rail is drag-resizable (min --sidebar-w-min, max
   --sidebar-w-max-pct of the shell, double-click resets) and collapsible;
   the width travels via the data-sidebar-w CONTENT ATTRIBUTE (typed attr()
   in the grid track — inline styles are CSP-forbidden), and both features
   are JS enhancements: the no-JS default keeps the rail at its default
   width with the usable static canvas. Width/collapse persist in memory
   for the page session only (a reload restores the default rail). (2) The
   3840 · 4K preset joins 1920 · Full HD as an additive Studio preset — the
   frozen policy tuple stays [390, 768, 1280]. (3) Switching any preset
   re-fits automatically. (4) Viewport buttons carry inline SVG device
   icons (CSP-safe, no external assets). (5) The canvas top toolbar is
   removed: the "Preview" heading is visually hidden, the rendering-kit
   line moved into the sidebar Kit panel, and the zoom controls float as a
   translucent (backdrop-blur where supported) overlay at the canvas
   top-right whose wrapper is pointer-events:none — only its own buttons
   intercept clicks. The canvas panel is now a two-row grid (stage /
   status bar); the faces-disclosure bar stays always visible.
   2026-09-11 R5 (owner feedback): sidebar floor + control language.
   (1) The sidebar minimum width rises to --sidebar-w-min 340px so every
   rail panel stays usable at the floor. (2) The workbench's interactive
   controls are restyled as one coherent set — buttons, chips and toggles
   share the 44px touch token, radius-md, control-identification borders,
   shadow-subtle elevation and consistent hover/active/focus/pressed
   states. (3) The large desktop presets are fixed-aspect device frames:
   1920 renders 1920×1080 and 3840 renders 3840×2160 (the JS sets the
   explicit frame height via the height content attribute; taller content
   scrolls INSIDE the frame like a real screen); the policy-tuple presets
   stay content-height. (4) The viewport selector is redesigned as two
   labelled device rows (policy tuple / Studio presets) of icon+label
   chips that wrap at the rail floor. (5) A pan tool (hand toggle,
   grab/grabbing cursors, Esc exit and Space-hold) pans the stage, and the
   zoom overlay chip is draggable by its move handle — clamped inside the
   stage, positioned via the data-x/data-y content attributes (typed
   attr(), CSP-safe), in-memory for the session, double-click resets to
   the canvas top-right.
   2026-09-11 catalogue-v2 r4 (owner feedback): Components matches the
   Build app-shell — kit/viewport/chips/provenance move into the left
   sidebar so the preview canvas is edge-to-edge (stage + status only).
   Empty canvas is an intentional empty state (no device frame) until a
   component is selected. The zoom chip is ~40% smaller on both pages,
   never wraps or squishes (intrinsic single-row; icons-only when the
   stage is narrower than the chip), and mouse-wheel over the canvas
   zooms toward the pointer (JS enhancement; reduced-motion and
   Ctrl+wheel leave page scroll alone).

   2026-09-11 catalogue-v2 r5 (owner feedback): Components rail is the
   same expand/collapse/resize sidebar as Build (shared JS path). Kit /
   viewport / chips sit above the category list. Functional taxonomy
   replaces era groups. Pan is always-on when zoomed past fit (threshold
   so clicks still click). Zoom chip grows ~15% to ~37px.

   2026-09-12 catalogue-v2 r8 (owner iPad): the desktop app-shell used to
   engage at 900px, so iPad portrait/landscape and split-view laptops
   (768–1023) got a sidebar+canvas split with almost no usable canvas.
   Stacked/scrolling layout (header, capped rail, kit/viewport band,
   generous ~65dvh canvas with pan/zoom) now covers every width below
   1024px; the app-shell engages at --bp-chrome 1024px. Touch-coarse
   hit plates on the zoom chip and sidebar handle are ≥44px.

   TOKEN RULE (plan §4.2, STUDIO-DESIGN-SPEC.md §12, unified E1–E5 list):
   literal values appear ONLY in the token layer (:root block below).
   All component rules reference var(--*) exclusively. Permitted literals in
   component CSS: E1 unitless zero; E2 intrinsic keywords; E3 the chrome
   breakpoint restated in @media as 1024px (shell) / 1023px (stacked
   ceiling) — a MANUAL restatement of --bp-chrome; there is NO breakpoint
   generator); E4 the .sr-only utility; E5 percentage lengths and fr grid tracks.

   Chrome = presentation-dark-axel at all times, consumed from the REAL
   pipeline outputs: kits.css ([data-kit="presentation-dark-axel"] on <html>,
   mirrored from vault @cbb3dc7 via D1 — kits/SOURCE-PIN.json) supplies every
   color below through --kit-* aliases, and tokens.css supplies the static
   spacing/type/radius/motion/touch scale. Nothing here
   restates a kit palette value by hand. Preview = selected kit, rendered
   inside a sandboxed scriptless child document (preview.css), never here.
   Fonts: system fallback stacks only; no font fetch (plan §7, M5).
   ========================================================================== */

/* --- Chrome token layer ---------------------------------------------------
   Colors: aliases of the generated --kit-* layer (presentation-dark-axel).
   Scale/layout/policy: named Studio declarations (spec §12 token layer). */
:root {
  /* surfaces */
  --chrome-surface-base: var(--kit-surface-base);
  --chrome-surface-raised: var(--kit-surface-raised);
  --chrome-surface-overlay: var(--kit-surface-overlay);
  --chrome-surface-sunken: var(--kit-surface-sunken);
  --chrome-surface-raised-2: var(--kit-surface-raised-2);
  /* borders */
  --chrome-border-subtle: var(--kit-border-subtle);     /* decorative only */
  --chrome-border-default: var(--kit-border-default);   /* decorative only (1.29–1.39:1) */
  --chrome-border-strong: var(--kit-border-strong);
  /* Control-identification borders must reach 3:1 (spec §5 amendment 7):
     the adequate EXISTING token is text-secondary. */
  --chrome-border-control: var(--kit-text-secondary);
  /* text */
  --chrome-text-primary: var(--kit-text-primary);
  --chrome-text-secondary: var(--kit-text-secondary);  /* ≥4.5:1 on base/raised/overlay — the only small-text token */
  --chrome-text-inverse: var(--kit-text-inverse);
  /* accent */
  --chrome-accent-primary: var(--kit-accent-primary);
  --chrome-accent-primary-hover: var(--kit-accent-primary-hover);
  --chrome-accent-primary-muted: var(--kit-accent-primary-muted);
  --chrome-accent-text: var(--kit-accent-text);
  --chrome-link: var(--kit-link-color);
  --chrome-accent-secondary: var(--kit-accent-secondary);
  /* status */
  --chrome-status-success: var(--kit-status-success);
  --chrome-status-success-muted: var(--kit-status-success-muted);
  --chrome-status-warning: var(--kit-status-warning);
  --chrome-status-warning-muted: var(--kit-status-warning-muted);
  --chrome-status-error: var(--kit-status-error);
  --chrome-status-error-muted: var(--kit-status-error-muted);
  --chrome-status-info: var(--kit-status-info);
  --chrome-status-info-muted: var(--kit-status-info-muted);
  /* focus — meets 3:1 against every chrome surface (spec §5) */
  --chrome-focus-ring: var(--kit-status-info);
  /* decorative composites (kit-token colors only; literals live here) */
  --chrome-body-ambient: radial-gradient(90% 420px at 50% -140px, var(--chrome-accent-primary-muted), transparent 70%) no-repeat, var(--chrome-surface-base);
  --chrome-banner-sheen: linear-gradient(90deg, var(--chrome-status-warning-muted), transparent 60%);
  --chrome-brandmark: linear-gradient(135deg, var(--chrome-accent-primary), var(--chrome-accent-secondary));
  --chrome-stage-grid: radial-gradient(circle, var(--chrome-border-subtle) 1px, transparent 1.5px);
  --chrome-stage-grid-size: 22px 22px;
  /* type — chrome uses system stacks (spec §12); brand faces never render */
  --chrome-font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
  --chrome-font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --chrome-text-display: 30px;
  --chrome-text-eyebrow: 11px;
  --chrome-tracking-display: -0.01em;
  --chrome-tracking-eyebrow: 0.14em;
  --line-height-body: 1.5;
  --line-height-heading: 1.25;
  --line-height-display: 1.15;
  --tracking-caps: 0.04em;
  /* borders/focus dimensions */
  --border-w: 1px;
  --focus-w: 2px;
  --focus-offset: 2px;
  --brandmark-size: 14px;
  --brandmark-radius: 4px;
  --panel-tick-size: 8px;
  --panel-tick-radius: 2px;
  --eyebrow-rule-w: 24px;
  /* layout policy */
  --layout-max: 1600px;
  --controls-min: 320px;
  --controls-max: 420px;
  /* R4/R5 sidebar drag-resize bounds (the JS clamps to the same values):
     min rail width (R5 floor: every rail panel stays usable at 340px),
     max as a share of the app-shell width. */
  --sidebar-w-min: 340px;
  --sidebar-w-max-pct: 30%;
  --sidebar-handle-w: 8px;
  --zoom-overlay-blur: 12px;
  /* Catalogue-v2 r4: compact zoom chip (~40% smaller than the R5 44px
     touch chip). Visual button height is --zoom-btn-h; an absolutely
     positioned hit plate expands each control to --zoom-hit (≥40px)
     without growing the painted chip. Catalogue-v2 r5: grow ~15%
     (chip ~37px) — hit plates stay --zoom-hit 40px. */
  --zoom-chip-pad: 2px;
  --zoom-btn-h: 31px;
  --zoom-btn-pad: 2px;
  --zoom-hit: 40px;
  --zoom-hit-coarse: 44px;     /* r8: ≥44px plates at pointer:coarse (iPad); painted chip stays --zoom-btn-h */
  --zoom-icon: 14px;
  --bp-chrome: 1024px;             /* restated literally in the @media rule (exemption E3) */
  --preview-h: 620px;
  --preview-h-stacked: 65vh;   /* stacked (<1024px) canvas height: 60–70dvh tablet/phone band */
  --rail-stacked-max: 20vh;    /* stacked rail pane — keeps the canvas on the first screen */
  --cat-stage-h: 65vh;         /* catalogue-v2 Components-page preview stage height (window-relative, container is not the app shell) */
  --app-h: 100vh;              /* full-bleed app-shell height (Build surface, ≥1024px) */
  /* R3: dynamic-viewport variants where the engine supports them — the shell
     and the stacked stage track the ACTUAL visible viewport under browser
     chrome/zoom instead of the layout viewport. Custom property values are
     not unit-validated until use, so the dvh override lives behind
     @supports; engines without dvh keep the vh declarations above. */
  --zoom-min: 10;              /* in-canvas zoom bounds (percent, unitless) */
  --zoom-max: 400;
  --prose-max: 64ch;
  --inspector-max-h: 460px;
  --z-header: 90;
  --z-overlay: 40;
  --z-sticky: 1;
  /* The responsive policy tuple {viewports:[390,768,1280]} (spec §4.2) as
     named frame widths — the CSP (style-src 'self', no 'unsafe-inline')
     forbids runtime style attributes, so the selected policy width is applied
     by attribute selector below, still from policy values, not ad-hoc sizes. */
  --viewport-w-390: 390px;
  --viewport-w-768: 768px;
  --viewport-w-1280: 1280px;
  /* R3 additive Studio preset (NOT part of the frozen policy tuple above):
     1920 · Full HD, exact child width like the other presets. */
  --viewport-w-1920: 1920px;
  /* R4 additive Studio preset (likewise NOT a policy entry): 3840 · 4K. */
  --viewport-w-3840: 3840px;
  /* motion: T1 ceiling for chrome transitions, single property only (§7) */
  --motion-zero: 0ms;
  --textarea-min-h: calc(var(--touch-min) * 2);
  --opacity-disabled: 0.45;
}

/* R3: dvh upgrades (see --app-h / --preview-h-stacked above). Browser page
   zoom changes CSS-pixel geometry uniformly; sizing the shell and the stage
   from the dynamic viewport + grid chains keeps presets exact at any zoom. */
@supports (height: 100dvh) {
  :root {
    --app-h: 100dvh;
    --preview-h-stacked: 65dvh;
    --rail-stacked-max: 20dvh;
    --cat-stage-h: 65dvh;
  }
}

/* --- Base -------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html { color-scheme: dark; }
body {
  margin: 0;
  background: var(--chrome-body-ambient);
  color: var(--chrome-text-primary);
  font-family: var(--chrome-font-sans);
  font-size: var(--text-body);
  line-height: var(--line-height-body);
}
h1, h2, h3 { margin: 0; font-weight: var(--font-semibold); line-height: var(--line-height-heading); }
h1 { font-size: var(--text-h1); }
h2 { font-size: var(--text-h2); }
h3 { font-size: var(--text-h3); }
p { margin: 0; }
button, input, select, textarea { font: inherit; color: inherit; }
a { color: var(--chrome-link); }
code, .mono { font-family: var(--chrome-font-mono); font-size: var(--text-sm); }

/* Visible focus everywhere (keyboard operability, plan §7) */
:focus-visible {
  outline: var(--focus-w) solid var(--chrome-focus-ring);
  outline-offset: var(--focus-offset);
  border-radius: var(--radius-sm);
}

/* Reduced motion: chrome collapses to static (T0). The media query responds
   to RUNTIME reduced-motion changes by itself (spec §7). */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: var(--motion-zero) !important; animation: none !important; }
}

/* --- Fixture banner (persistent, never dismissible — plan §4.1) -------------
   R2: the honesty strip content lives inside the fixed header bar as a solid
   amber chip (warning wash + directional sheen) — first-class, always on
   screen because the header itself is sticky/pinned. */
.fixture-banner {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-2) var(--sp-4);
  background: var(--chrome-status-warning-muted);
  background-image: var(--chrome-banner-sheen);
  border: var(--border-w) solid var(--chrome-status-warning);
  border-radius: var(--radius-full);
  color: var(--chrome-status-warning);
  font-family: var(--chrome-font-mono);
  font-weight: var(--font-bold);
  font-size: var(--text-xs);
  letter-spacing: var(--chrome-tracking-eyebrow);
  text-transform: uppercase;
  text-align: center;
}

/* --- App header / pill nav ------------------------------------------------
   R2: one fixed bar — brand, honesty chip, nav. Sticky at every width so the
   honesty strip and nav never scroll away (on the Build app-shell the body
   does not scroll at all, so the bar is effectively fixed there). */
.studio-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3) var(--sp-4);
  padding: var(--sp-3) var(--sp-6);
  background: var(--chrome-surface-raised);
  border-bottom: var(--border-w) solid var(--chrome-border-default);
}
.studio-header .fixture-banner { margin-inline-start: auto; }
.studio-header .brand { display: flex; flex-direction: column; gap: var(--sp-1); }
.studio-header .brand .title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--text-h2);
  font-weight: var(--font-bold);
  letter-spacing: var(--chrome-tracking-display);
}
.studio-header .brand .title::before {
  content: "";
  inline-size: var(--brandmark-size);
  block-size: var(--brandmark-size);
  border-radius: var(--brandmark-radius);
  background: var(--chrome-brandmark);
  flex: none;
}
.studio-header .brand .meta { color: var(--chrome-text-secondary); font-size: var(--text-xs); }
.studio-nav {
  display: flex;
  gap: var(--sp-1);
  margin-inline-start: auto;
  padding: var(--sp-1);
  background: var(--chrome-surface-sunken);
  border: var(--border-w) solid var(--chrome-border-default);
  border-radius: var(--radius-full);
}
.studio-nav a {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-min);
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--radius-full);
  color: var(--chrome-text-secondary);
  text-decoration: none;
  font-weight: var(--font-medium);
  /* single-property transition rule (amendment 9): color only */
  transition: color var(--motion-dur) var(--motion-ease);
}
.studio-nav a:hover { color: var(--chrome-text-primary); }
.studio-nav a[aria-current="page"] {
  color: var(--chrome-accent-primary);
  background: var(--chrome-accent-primary-muted);
  font-weight: var(--font-semibold);
}

/* --- Layout -------------------------------------------------------------- */
.studio-main { padding: var(--sp-6); max-width: var(--layout-max); margin-inline: auto; }

/* Page heading block: eyebrow + display title + lede (spec §4 amendment 6:
   one real <h1> per Studio view). */
.page-head { display: flex; flex-direction: column; gap: var(--sp-2); margin-bottom: var(--sp-6); }
.page-eyebrow {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--chrome-font-mono);
  font-size: var(--chrome-text-eyebrow);
  letter-spacing: var(--chrome-tracking-eyebrow);
  text-transform: uppercase;
  color: var(--chrome-accent-primary);
}
.page-eyebrow::before {
  content: "";
  inline-size: var(--eyebrow-rule-w);
  block-size: var(--border-w);
  background: var(--chrome-accent-primary);
  flex: none;
}
h1.page-title {
  font-size: var(--chrome-text-display);
  line-height: var(--line-height-display);
  letter-spacing: var(--chrome-tracking-display);
  font-weight: var(--font-bold);
  overflow-wrap: anywhere;
}
.page-lede { color: var(--chrome-text-secondary); max-width: var(--prose-max); overflow-wrap: anywhere; }

.build-grid,
.catalogue-grid {
  display: grid;
  grid-template-columns: minmax(var(--controls-min), var(--controls-max)) minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: start;
}

/* E3: literal restates --bp-chrome (custom properties invalid in media
   queries). Single-column below the shell: the sidebar rail stacks
   above the canvas, and the stacked canvas gets a tall window-relative
   height. The header pins (sticky, above) so nav + honesty strip are always
   reachable on the single-column layouts. 1023px is the stacked ceiling
   (1024px is the shell floor) so the two queries never overlap. */
@media (max-width: 1023px) {
  .build-grid,
  .catalogue-grid {
    display: flex;
    flex-direction: column;
    min-width: 0;
    max-width: 100%;
  }
  .studio-main { padding: var(--sp-4); min-width: 0; max-width: 100%; }
  /* r8 tablet: sidebar children participate in the page flex so the canvas
     can sit directly under kit/viewport (Build) or under a capped rail
     (Components) instead of after the entire rail. */
  .build-app .build-sidebar,
  .catalogue-app .catalogue-sidebar { display: contents; }
  .build-app .page-head,
  .catalogue-app .page-head { order: 0; margin-bottom: var(--sp-2); gap: var(--sp-1); }
  .build-app .page-eyebrow,
  .catalogue-app .page-eyebrow,
  .build-app .page-lede,
  .catalogue-app .page-lede { display: none; }
  .build-app h1.page-title,
  .catalogue-app h1.page-title { font-size: var(--text-h2); }
  .build-sidebar .chrome-preview-band {
    order: 1;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(var(--controls-min), 100%), 1fr));
    gap: var(--sp-3);
  }
  .build-app .canvas-panel,
  .catalogue-app .catalogue-preview {
    order: 2;
    width: 100%;
    max-width: 100%;
    min-width: 0;
    overflow: hidden;
  }
  .build-sidebar #authoring {
    order: 3;
    max-height: var(--rail-stacked-max);
    overflow-y: auto;
    min-height: 0;
  }
  .chrome-preview-band .hint:not(.preview-kit-line) { display: none; }
  .chrome-preview-band .panel,
  #cat-preview-controls { padding: var(--sp-3); gap: var(--sp-2); }
  .chrome-preview-band .label-text,
  #cat-preview-controls .label-text,
  #cat-kit-reason { display: none; }
  .catalogue-sidebar .filter-bar { order: 1; }
  .catalogue-sidebar .catalogue-rail {
    order: 2;
    max-height: var(--rail-stacked-max);
    overflow-y: auto;
    min-height: 0;
  }
  .catalogue-sidebar #cat-preview-controls { order: 3; }
  .catalogue-app .catalogue-preview { order: 4; }
  #cat-preview-controls .hint { display: none; }
  .viewport-row {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
  }
  /* R3: the window-relative height lives on the STAGE (the container), never
     on the frame — the no-JS frame fills it, and the JS zoom layer measures
     the stage in container px. */
  .build-app .preview-stage { height: var(--preview-h-stacked); max-width: 100%; }
  .build-app .preview-stage > .preview-frame { height: 100%; }
  .catalogue-app .preview-stage { height: var(--preview-h-stacked); max-width: 100%; }
  .zoom-controls .btn::before {
    inset: calc((var(--zoom-btn-h) - var(--zoom-hit-coarse)) / 2);
  }
}

.panel {
  background: var(--chrome-surface-raised);
  border: var(--border-w) solid var(--chrome-border-default);
  border-radius: var(--radius-lg);
  padding: var(--sp-6);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  box-shadow: var(--shadow-subtle);
}
.panel > h2 {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--text-h3);
  padding-bottom: var(--sp-3);
  border-bottom: var(--border-w) solid var(--chrome-border-subtle);
}
.panel > h2::before {
  content: "";
  inline-size: var(--panel-tick-size);
  block-size: var(--panel-tick-size);
  border-radius: var(--panel-tick-radius);
  background: var(--chrome-accent-primary);
  flex: none;
}
.panel .hint { color: var(--chrome-text-secondary); font-size: var(--text-xs); max-width: var(--prose-max); }
.stack { display: flex; flex-direction: column; gap: var(--sp-3); }
.row { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.chrome-preview-band,
.chrome-authoring-rest {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  min-width: 0;
}

/* --- Form controls -------------------------------------------------------- */
label.field { display: flex; flex-direction: column; gap: var(--sp-1); font-size: var(--text-sm); font-weight: var(--font-medium); }
label.field .label-text { color: var(--chrome-text-secondary); }
select, input[type="text"], textarea {
  background: var(--chrome-surface-sunken);
  border: var(--border-w) solid var(--chrome-border-control);
  border-radius: var(--radius-md);
  padding: var(--sp-2) var(--sp-3);
  min-height: var(--touch-min);
  color: var(--chrome-text-primary);
  width: 100%;
  /* single-property transition rule (amendment 9): border-color only */
  transition: border-color var(--motion-dur) var(--motion-ease);
}
textarea { resize: vertical; min-height: var(--textarea-min-h); }
select:hover, input[type="text"]:hover, textarea:hover { border-color: var(--chrome-text-primary); }
[aria-invalid="true"] { border-color: var(--chrome-status-error) !important; }
.char-count { color: var(--chrome-text-secondary); font-size: var(--text-xxs); text-align: end; }
.char-count.over { color: var(--chrome-status-error); font-weight: var(--font-bold); }
.field-error {
  color: var(--chrome-status-error);
  font-size: var(--text-xs);
  font-weight: var(--font-medium);
}

/* --- Buttons (states: default/hover/focus/active/disabled) ----------------
   R5 control language: one coherent set — the 44px touch token, radius-md,
   a control-identification border, a whisper of elevation, a quiet hover
   lift (overlay surface + stronger border) and a pressed-in active state. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--touch-min);
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--radius-md);
  border: var(--border-w) solid var(--chrome-border-control);
  background: var(--chrome-surface-raised-2);
  box-shadow: var(--shadow-subtle);
  color: var(--chrome-text-primary);
  font-weight: var(--font-medium);
  font-size: var(--text-sm);
  cursor: pointer;
  /* single-property transition rule (amendment 9): background only */
  transition: background var(--motion-dur) var(--motion-ease);
}
.btn:hover:not(:disabled) { background: var(--chrome-surface-overlay); border-color: var(--chrome-text-primary); }
.btn:active:not(:disabled) { background: var(--chrome-surface-sunken); box-shadow: none; }
.btn:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; box-shadow: none; }
.btn-primary {
  background: var(--chrome-accent-primary);
  border-color: var(--chrome-accent-primary);
  color: var(--chrome-accent-text);
  font-weight: var(--font-semibold);
}
.btn-primary:hover:not(:disabled) {
  background: var(--chrome-accent-primary-hover);
  border-color: var(--chrome-accent-primary-hover);
  box-shadow: var(--glow-primary);
}
/* The inherited active rule (sunken fill + dark label) measured 1.03:1 in K0R;
   the primary active pair uses existing tokens only: hover-accent label on
   the sunken surface — computed 8.6:1 (spec §5/K0Q §A7). */
.btn-primary:active:not(:disabled) {
  background: var(--chrome-surface-sunken);
  color: var(--chrome-accent-primary-hover);
  border-color: var(--chrome-accent-primary-hover);
}
.btn-danger { color: var(--chrome-status-error); }
/* The error-muted hover wash measured 4.401:1 (below 4.5); the hover pair
   uses the existing sunken surface (5.6:1) — spec §5/K0Q §A7. */
.btn-danger:hover:not(:disabled) { border-color: var(--chrome-status-error); background: var(--chrome-surface-sunken); }
.btn-sm { min-height: var(--touch-min); min-width: var(--touch-min); padding: var(--sp-1) var(--sp-3); font-size: var(--text-xs); }

/* Filter bar pressed state (components surface): same measured accent pair
   as the nav current pill. */
.filter-bar .btn[aria-pressed="true"] {
  background: var(--chrome-accent-primary-muted);
  color: var(--chrome-accent-primary);
  border-color: var(--chrome-accent-primary);
  font-weight: var(--font-semibold);
}

/* Import trigger: the native (visually hidden) file input keeps keyboard
   focus; the visible ring is forwarded to its visible label (spec §3). The
   input PRECEDES the label in DOM order so the sibling selector can match. */
#import-input:focus-visible + label[for="import-input"] {
  outline: var(--focus-w) solid var(--chrome-focus-ring);
  outline-offset: var(--focus-offset);
}

/* --- Badges ---------------------------------------------------------------- */
.badge {
  display: inline-flex;
  align-items: center;
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--radius-full);
  font-size: var(--text-xxs);
  font-weight: var(--font-semibold);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  border: var(--border-w) solid var(--chrome-border-default);
  color: var(--chrome-text-secondary);
  white-space: nowrap;
}
.badge-success { color: var(--chrome-status-success); border-color: var(--chrome-status-success); background: var(--chrome-status-success-muted); }
.badge-warning { color: var(--chrome-status-warning); border-color: var(--chrome-status-warning); background: var(--chrome-status-warning-muted); }
.badge-error   { color: var(--chrome-status-error);   border-color: var(--chrome-status-error);   background: var(--chrome-status-error-muted); }
.badge-info    { color: var(--chrome-status-info);    border-color: var(--chrome-status-info);    background: var(--chrome-status-info-muted); }
.badge-fixture { color: var(--chrome-accent-secondary); border-color: var(--chrome-accent-secondary); }
/* The validation-stub badge carries a full sentence, not a short label: it
   must wrap instead of forcing horizontal overflow at the 390 policy width. */
.badge-stub { white-space: normal; }

/* --- Composition list -------------------------------------------------------- */
.composition-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.composition-item {
  border: var(--border-w) solid var(--chrome-border-default);
  border-radius: var(--radius-md);
  background: var(--chrome-surface-overlay);
  padding: var(--sp-3);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}
.composition-item.has-error { border-color: var(--chrome-status-error); background: var(--chrome-status-error-muted); }
/* Missing-section gap annotation (spec §9): visible marker inserted into the
   composition list at the missing entry's deterministic gap index. Blocking
   gaps use error tokens; the legal editable blocked state (missing primary
   action) uses warning tokens with an adjacent reason. */
.composition-gap {
  border: var(--border-w) dashed var(--chrome-status-error);
  border-radius: var(--radius-md);
  background: var(--chrome-status-error-muted);
  color: var(--chrome-status-error);
  padding: var(--sp-3);
  font-size: var(--text-xs);
  font-weight: var(--font-medium);
}
.composition-gap.blocked {
  border-color: var(--chrome-status-warning);
  background: var(--chrome-status-warning-muted);
  color: var(--chrome-status-warning);
}
/* Visible boundary-arrow disabled reason (spec §3). */
.move-note { color: var(--chrome-text-secondary); font-size: var(--text-xxs); align-self: center; }
.composition-item .item-head { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.composition-item .item-title { font-weight: var(--font-semibold); font-size: var(--text-sm); }
.composition-item .item-id { color: var(--chrome-text-secondary); font-size: var(--text-xs); }
.composition-item .item-controls { display: flex; gap: var(--sp-1); margin-inline-start: auto; }
.composition-item .slots { display: flex; flex-direction: column; gap: var(--sp-2); }

/* --- Validation panel ---------------------------------------------------------- */
.validation-panel { border: var(--border-w) solid var(--chrome-border-default); border-radius: var(--radius-md); padding: var(--sp-3); background: var(--chrome-surface-sunken); }
.validation-panel.valid { border-color: var(--chrome-status-success); }
.validation-panel.invalid { border-color: var(--chrome-status-error); }
.validation-panel h3 { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.validation-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.validation-list li { display: flex; gap: var(--sp-2); align-items: baseline; font-size: var(--text-sm); }
.validation-list .rule { color: var(--chrome-text-secondary); font-size: var(--text-xs); }
.validation-list .err { color: var(--chrome-status-error); }
.validation-list .warn { color: var(--chrome-status-warning); }
.validation-list .ok { color: var(--chrome-status-success); }

/* --- Preview (sandboxed scriptless child document — spec §10) ---------------
   The stage frames the kit render like an artboard: dotted grid, generous
   padding, the exact-width frame floating on a prominent shadow. */
/* R4: the rendering-kit line lives in the sidebar Kit panel (the canvas top
   toolbar is gone). */
.preview-kit-line code { color: var(--chrome-accent-primary); }
/* R5 selector redesign: two labelled device rows — the frozen responsive
   policy tuple and the additive Studio presets — each a flex-wrap group of
   icon+label chips in the R5 control language (44px touch token, radius-md,
   control-identification border, subtle elevation, accent pressed state).
   The rows wrap gracefully at the sidebar floor width; no clipping wrap, so
   the chips' focus rings always paint fully. */
.viewport-group { display: flex; flex-direction: column; gap: var(--sp-3); }
.viewport-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.viewport-row-label {
  flex: 1 1 100%;
  font-family: var(--chrome-font-mono);
  font-size: var(--text-xxs);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--chrome-text-secondary);
}
.viewport-group button {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--touch-min);
  padding: var(--sp-2) var(--sp-3);
  background: var(--chrome-surface-raised-2);
  border: var(--border-w) solid var(--chrome-border-control);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-subtle);
  color: var(--chrome-text-secondary);
  cursor: pointer;
  font-size: var(--text-sm);
  font-family: var(--chrome-font-mono);
  /* single-property transition rule (amendment 9): color only */
  transition: color var(--motion-dur) var(--motion-ease);
}
.viewport-group button:hover { color: var(--chrome-text-primary); border-color: var(--chrome-text-primary); background: var(--chrome-surface-overlay); }
.viewport-group button[aria-pressed="true"] {
  background: var(--chrome-accent-primary-muted);
  color: var(--chrome-accent-primary);
  border-color: var(--chrome-accent-primary);
  font-weight: var(--font-semibold);
  box-shadow: none;
}
.preview-stage {
  background: var(--chrome-surface-sunken);
  background-image: var(--chrome-stage-grid);
  background-size: var(--chrome-stage-grid-size);
  border: var(--border-w) solid var(--chrome-border-default);
  border-radius: var(--radius-lg);
  padding: var(--sp-6);
  /* explicitly scrollable outer stage (spec §6 amendment 5): the child
     viewport keeps its exact policy width; the stage scrolls instead of
     clamping. Keyboard-scrollable via tabindex=0 + role=region. R3: block
     layout + margin-inline:auto centering (safe at any overflow: a too-wide
     payload starts at the inline start edge and pans to the far end). */
  overflow: auto;
  display: block;
}
.preview-frame {
  /* The exact child viewport width comes from the named policy tuple tokens
     via the data-viewport attribute rules below — NO max-width clamp, no
     runtime style attribute (CSP style-src 'self'): the child viewport is
     exactly the selected policy width; clipping is not an overflow pass.
     box-sizing:content-box makes the declared width the frame's CONTENT box,
     so childWindow.innerWidth equals the selected width exactly (the two
     border widths add outside; the global border-box rule is overridden). */
  box-sizing: content-box;
  display: block;
  border: var(--border-w) solid var(--chrome-border-control);
  border-radius: var(--radius-md);
  margin-inline: auto;
  background: var(--chrome-surface-base);
  box-shadow: var(--shadow-prominent);
}
/* No-JS / unwrapped frame (direct stage child): a static usable canvas —
   nominal height below, stage-filling in the app shell, stage-filling in the
   stacked layout. With JS the frame moves inside .preview-zoom, where its
   height is set as the height content attribute (a presentational hint, not
   an inline style — CSP-safe): the MEASURED child document height for the
   policy-tuple presets, the explicit device height (1920×1080 / 3840×2160)
   for the fixed-aspect Studio presets (R5) — so no CSS height rule may
   match the wrapped frame. */
.preview-stage > .preview-frame { height: var(--preview-h); }
.preview-frame[data-viewport="390"] { width: var(--viewport-w-390); }
.preview-frame[data-viewport="768"] { width: var(--viewport-w-768); }
.preview-frame[data-viewport="1280"] { width: var(--viewport-w-1280); }
.preview-frame[data-viewport="1920"] { width: var(--viewport-w-1920); }
.preview-frame[data-viewport="3840"] { width: var(--viewport-w-3840); }

/* --- R3 in-canvas zoom (JS enhancement; absent without JS) -----------------
   Zoom is transform: scale() on the frame inside a sized wrapper, driven by
   TYPED attr() on data-zoom (unitless percent) and data-h (the measured
   child document height as a <length>) — content attributes, never inline
   styles (CSP style-src 'self'). The frame's own width stays the exact
   preset token at every zoom level: the child's layout width never changes,
   only the canvas magnification. The wrapper carries the scaled layout size
   so the stage's normal overflow provides panning exactly when the zoomed
   preview exceeds the stage. Engines without typed attr() (older Firefox)
   drop these declarations and render the frame at 100% with pan — the
   no-JS-equivalent fallback. */
.preview-zoom {
  margin-inline: auto;
  width: calc(var(--zoom-w, var(--viewport-w-1280)) * attr(data-zoom type(<number>), 100) / 100);
  height: calc(attr(data-h type(<length>), var(--preview-h)) * attr(data-zoom type(<number>), 100) / 100);
}
.preview-zoom[data-viewport="390"] { --zoom-w: calc(var(--viewport-w-390) + var(--border-w) * 2); }
.preview-zoom[data-viewport="768"] { --zoom-w: calc(var(--viewport-w-768) + var(--border-w) * 2); }
.preview-zoom[data-viewport="1280"] { --zoom-w: calc(var(--viewport-w-1280) + var(--border-w) * 2); }
.preview-zoom[data-viewport="1920"] { --zoom-w: calc(var(--viewport-w-1920) + var(--border-w) * 2); }
.preview-zoom[data-viewport="3840"] { --zoom-w: calc(var(--viewport-w-3840) + var(--border-w) * 2); }
.preview-zoom .preview-frame {
  transform: scale(calc(attr(data-zoom type(<number>), 100) / 100));
  transform-origin: 0 0;
}
/* Measurement twin: a hidden, inert, scriptless (sandbox without
   allow-scripts + the child's own CSP meta) same-origin copy of the preview
   child document at the exact preset width. Studio reads its document
   height to size the zoom layer; it never displays, never receives focus
   and never runs script. position:fixed keeps it out of scrollable overflow
   at the widest preset. */
.preview-measure {
  position: fixed;
  inset-inline-start: 0;
  inset-block-start: 0;
  visibility: hidden;
  pointer-events: none;
}
/* Canvas zoom controls (JS-generated): out / in / Fit / 100% / Pan + live
   readout + the move handle. R5: the chip is one segmented control — quiet
   ghost segments inside a bordered pill; the move handle is divided off at
   the far right (the classic 4-way move affordance, cursor: move).
   Catalogue-v2 r4: intrinsic single-row size (never wrap, never shrink);
   visual controls are --zoom-btn-h with a transparent --zoom-hit plate. */
.zoom-controls {
  display: inline-flex;
  flex-wrap: nowrap;
  flex-shrink: 0;
  align-items: center;
  gap: 0;
  padding: var(--zoom-chip-pad);
  width: max-content;
  max-width: none;
  white-space: nowrap;
  background: var(--chrome-surface-sunken);
  border: var(--border-w) solid var(--chrome-border-control);
  border-radius: var(--radius-md);
}
.zoom-controls .btn {
  position: relative;
  border-color: transparent;
  background: transparent;
  box-shadow: none;
  min-height: var(--zoom-btn-h);
  min-width: var(--zoom-btn-h);
  height: var(--zoom-btn-h);
  padding: var(--zoom-btn-pad);
  font-size: var(--text-xxs);
  gap: var(--zoom-chip-pad);
}
.zoom-controls .btn svg {
  width: var(--zoom-icon);
  height: var(--zoom-icon);
  flex: none;
}
/* Transparent hit plate: visual control stays --zoom-btn-h; the clickable
   area expands to --zoom-hit without growing the painted chip. */
.zoom-controls .btn::before {
  content: "";
  position: absolute;
  inset: calc((var(--zoom-btn-h) - var(--zoom-hit)) / 2);
}
/* r8: iPad / coarse pointers get a 44px plate; the painted chip stays
   --zoom-btn-h. Fine pointers keep the r5 40px plate. */
@media (pointer: coarse) {
  .zoom-controls .btn::before {
    inset: calc((var(--zoom-btn-h) - var(--zoom-hit-coarse)) / 2);
  }
}
.zoom-controls .btn:hover:not(:disabled) { background: var(--chrome-surface-overlay); border-color: transparent; color: var(--chrome-text-primary); }
.zoom-controls .btn:active:not(:disabled) { background: var(--chrome-surface-sunken); }
.zoom-controls button[aria-pressed="true"] {
  background: var(--chrome-accent-primary-muted);
  color: var(--chrome-accent-primary);
  border-color: var(--chrome-accent-primary);
  font-weight: var(--font-semibold);
}
.zoom-controls .zoom-move {
  cursor: move;
  touch-action: none;
  color: var(--chrome-text-secondary);
  margin-inline-start: var(--zoom-chip-pad);
  padding-inline-start: var(--sp-1);
  border-inline-start: var(--border-w) solid var(--chrome-border-subtle);
  border-radius: var(--radius-sm);
}
.zoom-controls .zoom-move:hover:not(:disabled) { color: var(--chrome-accent-primary); }
.zoom-controls[data-compact="true"] .zoom-label { display: none; }
.zoom-readout {
  min-width: var(--sp-6);
  padding-inline: var(--sp-1);
  text-align: center;
  font-family: var(--chrome-font-mono);
  font-size: var(--text-xxs);
  color: var(--chrome-text-secondary);
}

/* --- R4 floating zoom overlay (JS enhancement; absent without JS) ---------
   The zoom controls moved OUT of the removed canvas top toolbar into a
   compact chip floating at the canvas top-right. The wrapper is
   pointer-events:none — only the chip's own buttons intercept clicks, and
   nothing here paints interactivity over the child document (GAP-20/21
   honesty: the translucent chip never hides the render). The backdrop is a
   high-opacity mix of the raised chrome surface (the same token pairing
   that already passes contrast on panels) plus backdrop blur where the
   engine supports it, so the chip stays readable over light AND dark kits. */
.canvas-panel { position: relative; }
.zoom-overlay {
  position: absolute;
  inset-block-start: var(--sp-3);
  inset-inline-end: var(--sp-3);
  z-index: var(--z-overlay);
  pointer-events: none;
  width: max-content;
  max-width: none;
}
.zoom-overlay[hidden] { display: none; }
.zoom-overlay .zoom-controls {
  pointer-events: auto;
  background: color-mix(in srgb, var(--chrome-surface-raised) 86%, transparent);
  border: var(--border-w) solid var(--chrome-border-control);
  box-shadow: var(--shadow-prominent);
  /* engines without backdrop-filter drop this and keep the 86% chip */
  backdrop-filter: blur(var(--zoom-overlay-blur));
}
/* R5 dragged position: the move handle's drag stores the chip position in
   the data-x/data-y content attributes (typed attr() — inline styles are
   CSP-forbidden), clamped to the stage bounds by the JS. The attribute's
   presence re-anchors the chip to the inline start; engines without typed
   attr() drop the length declarations and the chip keeps its corner. */
.zoom-overlay[data-x] {
  inset-inline-end: auto;
  inset-block-start: attr(data-y type(<length>), var(--sp-3));
  inset-inline-start: attr(data-x type(<length>), var(--sp-3));
}
/* R5 pan tool: while armed, the stage advertises the hand and the child
   render is pointer-transparent, so a drag pans the stage (scrollLeft/Top)
   and can never select or activate content inside the preview. */
.preview-stage[data-pan="true"] { cursor: grab; }
.preview-stage[data-pan-overflow="true"] { cursor: grab; }
.preview-stage[data-pan-dragging="true"] { cursor: grabbing; }
.preview-stage[data-pan="true"] .preview-zoom { pointer-events: none; }
/* Wheel zoom is a JS enhancement on the stage. Contain overscroll so a
   wheel over the canvas cannot chain-scroll the sidebar or the page.
   Build's fixture child is scriptless and inert — pointer-events:none
   lets the stage receive wheel (cross-origin sandbox="" cannot).
   Catalogue entry frames stay same-origin and attach the same handler
   on the child document, so they keep pointer-events for clicks. */
.preview-stage { overscroll-behavior: contain; }
.build-app .preview-zoom .preview-frame { pointer-events: none; }

/* --- R4 sidebar resize handle + collapse controls (JS enhancements;
       absent without JS — the no-JS default keeps the rail at its default
       width). Desktop-shell only: hidden in the stacked (<1024px) layout,
       where the collapse/resize state has no effect by design. */
.sidebar-bar, .sidebar-resizer, .sidebar-reopen { display: none; }
.sidebar-reopen[hidden] { display: none; }

/* --- R2 full-bleed canvas layout (Build surface) ----------------------------
   Sidebar rail + canvas grid cells. Defaults here apply at every width; the
   app-shell (viewport-locked, edge-to-edge canvas) engages at and above the
   declared chrome breakpoint in the min-width block below (E3: 1024px is the
   same manually restated --bp-chrome value as the stacked ceiling above). */
.build-sidebar { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }
.build-sidebar .page-head { margin-bottom: 0; }
.canvas-panel { min-width: 0; min-height: 0; }
/* the faces-disclosure status line keeps the full canvas width (the
   .panel .hint 64ch prose clamp is for rail prose, not for status bars) */
.canvas-status { max-width: none; }
/* No-JS: the viewport buttons are JS-generated; an empty group renders
   nothing instead of a bare empty segmented control (the canvas noscript
   explains the static default). */
.viewport-group:empty { display: none; }

@media (min-width: 1024px) {
  /* App shell: the window is the workbench. The body locks to the dynamic
     window height (100dvh) and never scrolls; the header bar stays fixed on
     top, the sidebar rail scrolls its own overflow, and the canvas takes ALL
     space remaining — edge-to-edge, no card chrome. Every track on the chain
     is minmax(0, …)/min-height:0 so the regions are sized FROM THE GRID,
     never from raw vh on the frame: browser page zoom re-derives the same
     proportions in CSS px and cannot distort the geometry. The stage pans
     (overflow:auto) so any exact child width at any canvas zoom stays fully
     reachable while the page chrome itself can never overflow. */
  html:has(> body.build-app) { overflow: hidden; }
  .build-app {
    height: var(--app-h);
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  .build-app .studio-main {
    flex: 1;
    min-height: 0;
    max-width: none;
    margin-inline: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
  }
  .build-app .build-grid {
    flex: 1;
    min-height: 0;
    gap: 0;
    align-items: stretch;
    /* R4: the sidebar track is drag-resizable via the data-sidebar-w CONTENT
       ATTRIBUTE (typed attr() — inline styles are CSP-forbidden), clamped to
       [--sidebar-w-min, --sidebar-w-max-pct of the shell]. Without the
       attribute (no-JS default, or after a double-click reset) the track is
       the R2 default minmax(--controls-min, --controls-max). Engines without
       typed attr() drop the second declaration at parse time and keep the
       fallback — the R3-established pattern. */
    grid-template-columns: minmax(var(--controls-min), var(--controls-max)) minmax(0, 1fr);
    grid-template-columns: minmax(var(--sidebar-w-min), min(attr(data-sidebar-w type(<length>), var(--controls-max)), var(--sidebar-w-max-pct))) minmax(0, 1fr);
    /* one explicit row, exactly the grid height — never content-sized, so a
       tall rail can never stretch the canvas region past the window. */
    grid-template-rows: minmax(0, 1fr);
  }
  /* R4 collapsed rail: the canvas takes the full width; the rail is clipped
     away (its controls leave the tab order with visibility:hidden) and the
     floating re-open tab stays reachable at the canvas top-left. */
  .build-app .build-grid[data-sidebar-collapsed="true"],
  html.has-js .catalogue-app .catalogue-grid[data-sidebar-collapsed="true"] { grid-template-columns: 0 minmax(0, 1fr); }
  .build-app .build-grid[data-sidebar-collapsed="true"] .build-sidebar,
  html.has-js .catalogue-app .catalogue-grid[data-sidebar-collapsed="true"] .catalogue-sidebar {
    visibility: hidden;
    overflow: hidden;
    padding: 0;
    border: none;
  }
  .build-app .build-grid[data-sidebar-collapsed="true"] .sidebar-resizer,
  html.has-js .catalogue-app .catalogue-grid[data-sidebar-collapsed="true"] .sidebar-resizer { display: none; }
  /* R4 rail header: the caption + collapse button row (JS-generated) at the
     top of the sidebar. */
  .build-app .sidebar-bar,
  html.has-js .catalogue-app .sidebar-bar { display: flex; justify-content: space-between; align-items: center; gap: var(--sp-2); }
  .sidebar-bar-title {
    font-family: var(--chrome-font-mono);
    font-size: var(--text-xxs);
    letter-spacing: var(--tracking-caps);
    text-transform: uppercase;
    color: var(--chrome-text-secondary);
  }
  /* R4 drag handle on the rail's inner edge: a slim strip at the canvas
     panel's inline start (it overlays only the stage's padding, never the
     child), keyboard-operable (role=separator + arrow keys in studio.js)
     with a visible grip line. */
  .build-app .sidebar-resizer,
  html.has-js .catalogue-app .sidebar-resizer {
    display: block;
    position: absolute;
    inset-block: 0;
    /* r8: 44px (--touch-min) hit plate, visually still the 8px grip via
       ::before/::after centered on the rail/canvas seam. */
    width: var(--touch-min);
    inset-inline-start: calc((var(--sidebar-handle-w) - var(--touch-min)) / 2);
    cursor: col-resize;
    z-index: var(--z-overlay);
    touch-action: none;
  }
  .build-app .sidebar-resizer::after,
  html.has-js .catalogue-app .sidebar-resizer::after {
    content: "";
    position: absolute;
    inset-block: 0;
    inset-inline-start: calc(50% - var(--border-w));
    width: calc(var(--border-w) * 2);
    background: var(--chrome-border-control);
    /* single-property transition rule (amendment 9): background only */
    transition: background var(--motion-dur) var(--motion-ease);
  }
  .build-app .sidebar-resizer:hover::after, .build-app .sidebar-resizer:focus-visible::after,
  html.has-js .catalogue-app .sidebar-resizer:hover::after, html.has-js .catalogue-app .sidebar-resizer:focus-visible::after {
    background: var(--chrome-accent-primary);
  }
  /* R5 grip affordance: a centered pill marks the handle as draggable (the
     line alone read as a static divider). */
  .build-app .sidebar-resizer::before,
  html.has-js .catalogue-app .sidebar-resizer::before {
    content: "";
    position: absolute;
    inset-block-start: 50%;
    inset-inline-start: 50%;
    transform: translate(-50%, -50%);
    width: calc(var(--border-w) * 4);
    height: var(--sp-8);
    border-radius: var(--radius-full);
    background: var(--chrome-border-control);
    /* single-property transition rule (amendment 9): background only */
    transition: background var(--motion-dur) var(--motion-ease);
  }
  .build-app .sidebar-resizer:hover::before, .build-app .sidebar-resizer:focus-visible::before,
  html.has-js .catalogue-app .sidebar-resizer:hover::before, html.has-js .catalogue-app .sidebar-resizer:focus-visible::before {
    background: var(--chrome-accent-primary);
  }
  /* R4 floating re-open tab (JS-generated): visible only while collapsed. */
  .build-app .sidebar-reopen:not([hidden]),
  html.has-js .catalogue-app .sidebar-reopen:not([hidden]) {
    display: inline-flex;
    position: absolute;
    inset-block-start: var(--sp-3);
    inset-inline-start: var(--sp-3);
    z-index: var(--z-overlay);
    box-shadow: var(--shadow-prominent);
  }
  .build-app .build-sidebar {
    overflow-y: auto;
    min-height: 0;
    padding: var(--sp-4);
    border-inline-end: var(--border-w) solid var(--chrome-border-default);
    background: var(--chrome-surface-raised);
  }
  .build-app .build-sidebar .panel { background: var(--chrome-surface-overlay); box-shadow: none; }
  .build-app .canvas-panel {
    padding: 0;
    gap: 0;
    border: none;
    border-radius: 0;
    box-shadow: none;
    background: var(--chrome-surface-base);
    overflow: hidden;
    /* R4: the top toolbar is removed — the canvas runs edge-to-edge. Two
       explicit rows — stage / status bar — keep the faces-disclosure status
       bar a first-class, always-visible row of the canvas region: it wraps
       to as many lines as it needs (row auto), the stage absorbs the
       remainder (minmax(0, 1fr)), and nothing can ever clip or overlap it. */
    display: grid;
    grid-template-rows: minmax(0, 1fr) auto;
  }
  .build-app .preview-stage {
    min-height: 0;
    border: none;
    border-radius: 0;
    overflow: auto;
  }
  /* No-JS: the unwrapped frame fills the canvas stage vertically; the exact
     policy width still comes from the data-viewport rules above. With JS the
     wrapped frame's height is the measured child document height (content
     attribute), so this rule must stay scoped to the direct stage child. */
  .build-app .preview-stage > .preview-frame { height: 100%; }
  .build-app .canvas-status {
    /* the .panel .hint prose clamp (64ch) must not narrow the status bar */
    max-width: none;
    padding: var(--sp-2) var(--sp-4);
    border-top: var(--border-w) solid var(--chrome-border-default);
    background: var(--chrome-surface-raised);
    overflow-wrap: anywhere;
  }

  /* Catalogue v2 r3: Components matches the Build app-shell — left rail of
     categories → components, preview canvas always visible on the right.
     Overflow lock is JS-only (html.has-js): without scripts the static table
     must still scroll. */
  html.has-js:has(> body.catalogue-app) { overflow: hidden; }
  html.has-js .catalogue-app {
    height: var(--app-h);
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  html.has-js .catalogue-app .studio-main {
    flex: 1;
    min-height: 0;
    max-width: none;
    margin-inline: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
  }
  html.has-js .catalogue-app .catalogue-grid {
    flex: 1;
    min-height: 0;
    gap: 0;
    align-items: stretch;
    grid-template-columns: minmax(var(--controls-min), var(--controls-max)) minmax(0, 1fr);
    grid-template-columns: minmax(var(--sidebar-w-min), min(attr(data-sidebar-w type(<length>), var(--controls-max)), var(--sidebar-w-max-pct))) minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
  }
  html.has-js .catalogue-app .catalogue-sidebar {
    overflow-y: auto;
    min-height: 0;
    padding: var(--sp-4);
    border-inline-end: var(--border-w) solid var(--chrome-border-default);
    background: var(--chrome-surface-raised);
  }
  html.has-js .catalogue-app .catalogue-sidebar .page-head { margin-bottom: 0; }
  html.has-js .catalogue-app .catalogue-sidebar .panel { background: var(--chrome-surface-overlay); box-shadow: none; }
  html.has-js .catalogue-app .catalogue-preview {
    margin-top: 0;
    padding: 0;
    gap: 0;
    border: none;
    border-radius: 0;
    box-shadow: none;
    background: var(--chrome-surface-base);
    overflow: hidden;
    /* r4: same two-row grid as Build — stage / status. Controls live in
       the sidebar; the canvas is edge-to-edge. */
    display: grid;
    grid-template-rows: minmax(0, 1fr) auto;
    min-height: 0;
  }
  html.has-js .catalogue-app .catalogue-preview .preview-stage {
    min-height: 0;
    height: auto;
    border: none;
    border-radius: 0;
    overflow: auto;
  }
  html.has-js .catalogue-app .catalogue-preview .canvas-status {
    flex: none;
    max-width: none;
    padding: var(--sp-2) var(--sp-4);
    border-top: var(--border-w) solid var(--chrome-border-default);
    background: var(--chrome-surface-raised);
    overflow-wrap: anywhere;
  }
}

/* Authoring fieldset: inert-by-default without JS (amendment 9) — JS removes
   the disabled attribute; without JS no fake working controls are shown. */
fieldset.stack, fieldset.filter-bar { border: none; margin: 0; padding: 0; min-width: 0; }

/* --- No-JS explanation (amendment 9) -------------------------------------------- */
.no-js-note {
  border: var(--border-w) solid var(--chrome-status-warning);
  background: var(--chrome-status-warning-muted);
  color: var(--chrome-text-primary);
  border-radius: var(--radius-md);
  padding: var(--sp-3);
  font-size: var(--text-sm);
}
main > .no-js-note, main > noscript > .no-js-note { display: block; margin-bottom: var(--sp-4); }

/* --- Catalogue table (components.html) ---------------------------------------------- */
.filter-bar { display: flex; gap: var(--sp-2); flex-wrap: wrap; align-items: center; margin-bottom: var(--sp-4); }
.catalogue-wrap { overflow-x: auto; border: var(--border-w) solid var(--chrome-border-default); border-radius: var(--radius-lg); box-shadow: var(--shadow-subtle); }
table.catalogue { border-collapse: collapse; width: 100%; font-size: var(--text-sm); background: var(--chrome-surface-raised); }
table.catalogue caption { text-align: start; padding: var(--sp-3) var(--sp-4); color: var(--chrome-text-secondary); font-size: var(--text-xs); background: var(--chrome-surface-raised); border-bottom: var(--border-w) solid var(--chrome-border-subtle); }
table.catalogue th, table.catalogue td { padding: var(--sp-3) var(--sp-3); text-align: start; border-bottom: var(--border-w) solid var(--chrome-border-subtle); vertical-align: top; }
table.catalogue thead th {
  position: sticky; top: 0;
  background: var(--chrome-surface-overlay);
  color: var(--chrome-text-secondary);
  font-family: var(--chrome-font-mono);
  font-size: var(--text-xxs); text-transform: uppercase; letter-spacing: var(--tracking-caps);
  z-index: var(--z-sticky);
}
table.catalogue tbody tr:hover { background: var(--chrome-surface-overlay); }
table.catalogue tr.historical-rb { background: var(--chrome-status-warning-muted); }
table.catalogue tr.historical-rb:hover { background: var(--chrome-surface-overlay); }
table.catalogue .record-id { display: block; font-family: var(--chrome-font-mono); font-size: var(--text-xs); color: var(--chrome-text-secondary); margin-top: var(--sp-1); }
.reference-only-note {
  display: block;
  color: var(--chrome-status-warning);
  font-size: var(--text-xxs);
  font-weight: var(--font-semibold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  margin-top: var(--sp-1);
}
.add-reason { display: block; color: var(--chrome-text-secondary); font-size: var(--text-xxs); margin-top: var(--sp-1); max-width: var(--controls-min); }

/* --- Evidence page ---------------------------------------------------------------------
   Label above full-width value (spec §2.4 amendment 5): no two-column
   squeeze; labels wrap; values keep full width. Hashes/paths wrap at any
   character (overflow-wrap), ordinary prose is never break-all. The
   Inspector is the widest payload on the surface, so it spans the grid. */
.evidence-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(var(--controls-min), 1fr)); gap: var(--sp-4); }
.evidence-grid section[aria-labelledby="ev-inspector"] { grid-column: 1 / -1; }
dl.facts { margin: 0; display: flex; flex-direction: column; gap: var(--sp-2); font-size: var(--text-sm); }
dl.facts dt { color: var(--chrome-text-secondary); font-family: var(--chrome-font-mono); font-size: var(--text-xxs); text-transform: uppercase; letter-spacing: var(--tracking-caps); overflow-wrap: break-word; }
dl.facts dd { margin: 0 0 var(--sp-3); font-family: var(--chrome-font-mono); font-size: var(--text-xs); overflow-wrap: anywhere; color: var(--chrome-text-primary); }
.json-view {
  background: var(--chrome-surface-sunken);
  border: var(--border-w) solid var(--chrome-border-default);
  border-radius: var(--radius-md);
  padding: var(--sp-4);
  font-family: var(--chrome-font-mono);
  font-size: var(--text-xs);
  color: var(--chrome-text-secondary);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: var(--inspector-max-h);
  overflow: auto;
}

/* --- Live region (polite status announcements) ------------------------------------------ */
/* E4: fixed accessibility utility, no design value */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.status-line { color: var(--chrome-text-secondary); font-size: var(--text-xs); min-height: var(--sp-5); }
.status-line.ok { color: var(--chrome-status-success); }
.status-line.err { color: var(--chrome-status-error); }

/* --- Catalogue v2: harvested rows + entry preview (Components surface) -----
   Additive only; every value resolves to the shared token layers. The
   preview reuses the Build canvas machinery verbatim (.preview-stage /
   .preview-frame / .preview-zoom / .zoom-overlay / .viewport-row). r3: the
   Components page is an app-shell with a category rail (JS) and a static
   table noscript fallback; the canvas is always on-screen once scripts run. */
html:not(.has-js) .catalogue-preview { display: none; }
html:not(.has-js) .catalogue-grid { grid-template-columns: minmax(0, 1fr); }
html.has-js .catalogue-static { display: none; }

table.catalogue .provenance-chip,
.catalogue-preview-chips .provenance-chip {
  display: block;
  font-family: var(--chrome-font-mono);
  font-size: var(--text-xs);
  color: var(--chrome-text-secondary);
  margin-top: var(--sp-1);
}
table.catalogue tr.harvested-vpa { background: var(--chrome-status-info-muted); }
table.catalogue tr.harvested-vpa:hover { background: var(--chrome-surface-overlay); }
table.catalogue tr.harvested-aod { background: var(--chrome-accent-primary-muted); }
table.catalogue tr.harvested-aod:hover { background: var(--chrome-surface-overlay); }
.catalogue-preview { margin-top: var(--sp-6); }
.catalogue-preview[hidden] { display: none; }
.catalogue-preview-chips { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.catalogue-preview .preview-stage { height: var(--cat-stage-h); }
#cat-kit-reason { max-width: 100%; overflow-wrap: anywhere; }
.catalogue-grid, .catalogue-sidebar, .catalogue-preview { min-width: 0; }
#cat-preview-controls select { max-width: 100%; }
#cat-preview-id:empty { display: none; }
.catalogue-empty {
  margin: auto;
  text-align: center;
  color: var(--chrome-text-secondary);
  font-size: var(--text-sm);
  max-width: var(--prose-max);
  padding: var(--sp-6);
}
.catalogue-empty[hidden] { display: none; }
.preview-stage:not([data-has-entry]) {
  display: grid;
  place-items: center;
}

.catalogue-rail {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  min-height: 0;
  margin-top: 0;
}
.catalogue-rail[hidden] { display: none; }
.catalogue-rail-group[hidden] { display: none; }
.catalogue-rail-heading {
  font-family: var(--chrome-font-mono);
  font-size: var(--text-xxs);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--chrome-text-secondary);
  margin: 0 0 var(--sp-2);
}
.catalogue-rail-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}
.catalogue-rail-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-1);
  width: 100%;
  text-align: start;
  padding: var(--sp-2) var(--sp-3);
  border: var(--border-w) solid transparent;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--chrome-text-primary);
  cursor: pointer;
}
.catalogue-rail-item:hover {
  background: var(--chrome-surface-overlay);
}
.catalogue-rail-item[aria-pressed="true"] {
  background: var(--chrome-accent-primary-muted);
  border-color: var(--chrome-border-control);
}
.catalogue-rail-name {
  font-size: var(--text-sm);
  font-weight: var(--font-medium);
}
.catalogue-rail-id {
  font-family: var(--chrome-font-mono);
  font-size: var(--text-xxs);
  color: var(--chrome-text-secondary);
}
.catalogue-sidebar .page-lede { max-width: none; font-size: var(--text-xs); }
.catalogue-sidebar .filter-bar {
  margin-bottom: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-2);
  flex-wrap: nowrap;
}
.catalogue-sidebar .filter-bar .btn {
  min-width: 0;
  width: 100%;
  justify-content: center;
  white-space: nowrap;
  padding-inline: var(--sp-2);
  font-size: var(--text-xs);
}
