/* Single CSS entry for login.html — same reasoning as entry-main.css's header comment.
   Matches login.html's original two <link> tags, same order. */
/* Business OS — base tokens + theme overrides (Prompt 24.8's CSS split of style.css).
   MUST load first — every other sheet inherits the --ds-* tokens from :root/[data-theme].
   No @media (prefers-color-scheme) here by design — theme.js resolves 'system' to a
   concrete data-theme value itself; this stays the one mechanism deciding colors. Also
   carries the page-wide reset (box-sizing, [hidden], body, .layout) since those depend on
   the tokens defined immediately above them. */

* { box-sizing: border-box; }
:root {
  --surface-hover: #242938;
  --border-strong: #3a3f52;
  --danger-bg: #3a1e22;
  --danger-text: #ff9a9a;

  /* ---- Design System tokens (Business OS — Design System + Environment Intelligence
     Redesign, V1). Forward-only: only new components (below) and Environment's rebuild
     consume these — the rest of the app's ~400 existing hardcoded hex occurrences are
     deliberately NOT retrofitted onto this scale (see docs/BUSINESS_OS.md's Design System
     section for the scoping rationale). See /styleguide for a live token+component
     showcase. ---- */

  /* Background layers */
  --ds-bg-page: #0f1115;
  --ds-bg-card: #171a21;
  --ds-bg-inset: #1d212b;
  --ds-border: #262b36;
  --ds-border-strong: #343b4a;

  /* Sidebar shell (.primary-nav and friends — Commit 5, dark/light/system theme).
     No existing --ds-* background/border token was a close semantic match for the
     sidebar's own slightly-lighter-than-page chrome, so these are new rather than a
     forced reuse of --ds-bg-card/--ds-border. */
  --ds-nav-bg: #14161c;
  --ds-nav-border: #23262f;
  --ds-nav-active-bg: #23283a;

  /* Text */
  --ds-text-primary: #e8e8ec;
  --ds-text-secondary: #aab0c0;
  /* WCAG AA: measured against --ds-bg-card (5.47:1) and --ds-bg-page (5.93:1), because
     .ds-kpi-sub / .ds-loading / .ds-empty render on the card, not the page. The previous
     #6b7180 was 3.56:1 on the card — the whole secondary text tier failed AA in dark mode.
     test/design-token-contrast.test.js holds this to 4.5:1; do not darken without re-running it. */
  --ds-text-muted: #8b90a0;

  /* Focus ring. Reuses --ds-info-text's hue rather than introducing a new accent, so focus
     reads as the same "interactive blue" the rest of the system already uses. WCAG 2.4.11
     wants a focus indicator distinguishable from its surroundings; #60a5fa is 3.4:1 against
     --ds-bg-inset, the darkest ground any focusable control sits on. */
  --ds-focus-ring: #60a5fa;

  /* THE INTERACTIVE COLOUR — the one an inline link or an active tab is drawn in. Same hue as
     --ds-focus-ring above and --ds-info-text below, deliberately: a system with one interactive
     blue should say so once, and focus should look like the thing it is focusing.

     ADDED BECAUSE IT WAS ALREADY BEING USED AND DID NOT EXIST. Three files referenced
     var(--ds-accent) while nothing defined it anywhere in the repo. Two carried a fallback and
     silently rendered that instead (builder.css's #58a6ff, accounting.css's currentColor); the
     third, css/auditLog.css's `.audit-entity`, had NO fallback — so an unresolvable var() made
     the declaration invalid at computed-value time, `color` fell back to inherited, and every
     audit-log entity link rendered as plain body text with no underline. The only clickable
     element in each row was indistinguishable from the text beside it (WCAG 1.4.1).

     Contrast, computed against all three grounds rather than assumed, and held by
     test/design-token-contrast.test.js from here on: 7.43:1 on --ds-bg-page, 6.85:1 on
     --ds-bg-card, 6.33:1 on --ds-bg-inset. */
  --ds-accent: #60a5fa;

  /* The accent under a pointer. Deeper, not lighter: --ds-accent is already the bright end
     of the ramp in dark, so a "brighter on hover" step has nowhere to go and reads as a
     wash. Added with the builder's colour sweep, which had three separate literals doing
     this by hand (#3a5ce0, #a9c6ff, #2c3350) and no shared answer. */
  --ds-accent-hover: #3a5ce0;

  /* Text ON a fill that does NOT invert between palettes — a fixed semantic red badge, an
     overlay scrim. Distinct from --ds-btn-primary's use of --ds-bg-page, which is the
     answer when the FILL itself inverts (see components.css's note there): if the ground
     flips light-to-dark with the theme, the text has to flip with it, and if the ground is
     fixed the text must NOT. Deliberately the same value in both palettes — a role that
     stays put is a decision, not an oversight. */
  --ds-text-on-fill: #ffffff;

  /* The INK half of that pair. A fill light enough to need dark text needs a token for it too,
     and until now there was none — which is why .ds-badge-force and .ds-badge-scope simply used
     white on all 16 categorical fills and FAILED WCAG AA on 15 of them in dark, worst 1.86:1 on
     the amber. Whether a fill takes ink or paper is decided per fill by measured contrast, not
     by palette (see components.css's .ds-badge-force-* block); both values are fixed in both
     palettes for the same reason --ds-text-on-fill is. */
  --ds-ink-on-fill: #14161c;

  /* The other half of that role: a fixed dark ground for a floating overlay that must read
     the same in both palettes — the [data-tip] tooltip, an image scrim. A tooltip is not a
     surface of the page, it is a label over it, so it does not take the page's palette.
     Same value in both blocks, for the same deliberate reason as --ds-text-on-fill. */
  --ds-scrim: #000000;

  /* Driving-force accents (PESTTG) */
  --ds-force-political: #ef4444;
  --ds-force-economic: #22c55e;
  --ds-force-societal: #f0b429;
  --ds-force-technological: #3b82f6;
  --ds-force-mega_trends: #a855f7;
  --ds-force-globalisation: #14b8a6;

  /* Idea scope accents (Ideas Hub Redesign — Intrapreneurship V1) — org_wide/new_venture
     plus the 8 lib/businessOrgSlots.ts FUNCTION_SLOTS keys, same "one accent per enum
     value" shape as --ds-force-* above. */
  --ds-scope-org_wide: #6b7280;
  --ds-scope-new_venture: #ec4899;
  --ds-scope-supply_chain: #f0b429;
  --ds-scope-operations: #3b82f6;
  --ds-scope-marketing_sales: #22c55e;
  --ds-scope-service_warranty: #14b8a6;
  --ds-scope-infrastructure: #a855f7;
  --ds-scope-human_resources: #ef4444;
  --ds-scope-technology: #06b6d4;
  --ds-scope-procurement: #f97316;

  /* Semantic */
  --ds-success-bg: #14301f;
  --ds-success-text: #4ade80;
  --ds-warning-bg: #3a2e12;
  --ds-warning-text: #f0b429;
  --ds-danger-bg: #3a1e22;
  --ds-danger-text: #ff9a9a;
  --ds-info-bg: #14243a;
  --ds-info-text: #60a5fa;
  --ds-neutral-bg: #262b36;
  --ds-neutral-text: #aab0c0;

  /* The hairline on a semantic fill. The bg/text pair above is the codebase's usual
     callout shape (see components.css's .ds-badge-* block), but a BORDERED status strip
     needs a third value, and builder.css's .build-status.* variants had been carrying it
     as literals — which is exactly why that whole file had no light palette. Each sits
     between its own -bg and -text so the edge reads without competing with the label. */
  --ds-success-border: #1e4029;
  --ds-warning-border: #4d3f1a;
  --ds-danger-border: #5a2a30;

  /* Impact scale (low -> very_high) */
  --ds-impact-low: #6b7180;
  --ds-impact-medium: #60a5fa;
  --ds-impact-high: #f0b429;
  --ds-impact-very_high: #ef4444;

  /* Spacing scale */
  --ds-space-1: 4px;
  --ds-space-2: 8px;
  --ds-space-3: 12px;
  --ds-space-4: 16px;
  --ds-space-5: 24px;
  --ds-space-6: 32px;

  /* Radius scale */
  --ds-radius-sm: 6px;
  --ds-radius-md: 10px;
  --ds-radius-lg: 14px;

  /* ---- Type scale ------------------------------------------------------------------------
     Nine steps, each with one job. A size that is not one of these is drift, and
     test/type-scale.test.js fails on it rather than trusting the rule to be read.

       --ds-font-glyph-xs   9px  a symbol in a small fixed box: avatar initials, SVG chart labels
       --ds-font-glyph-sm  10px  a symbol in a fixed box: the info tip's "i", the nav caret
       --ds-font-xs        11px  THE FLOOR FOR TEXT — badges, chips, meta, timestamps
       --ds-font-sm        12px  labels, control text, table headers
       --ds-font-md        13px  body
       --ds-font-lg        16px  card and section titles
       --ds-font-title     18px  panel titles — the login card, a popover heading, a tile value
       --ds-font-xl        22px  a KPI value: the largest thing ON a card
       --ds-font-2xl       28px  a PAGE title: the only step that outranks a KPI value

     THE GLYPH TIER IS A TOKEN, NOT A SENTENCE. Sub-11px sizes exist because a "9px" that is
     one letter centred in a 20px circle is not small TEXT, it is a symbol scaled to its box —
     but stated as prose that is a judgement call every reviewer re-litigates. As tokens it
     becomes mechanical: sub-floor is legitimate iff it uses a glyph token, and the guard can
     assert exactly that. THE TEST A READER CAN APPLY ALONE: a symbol occupying a fixed box, not
     text a user reads as a line. Initials in an avatar, a caret, an "i", an axis label — glyph.
     A tag, a business name, a section title — text, and it takes --ds-font-xs or larger.

     WHY THIS DOES NOT FOLD INTO THE CHART EXCEPTION, since both look like carve-outs for
     non-chrome content: that rule (docs/BUSINESS_OS.md, AUDIT.md AUD-24) exempts chart COLOUR
     from the token system — it says stay OUT. This says the opposite: sub-floor sizes come INTO
     the token set under a named tier. Opposite directions, so folding them would blur the one
     thing each is for. SVG chart labels therefore take a glyph token for SIZE while their
     strokes and fills stay literal under the Chart Exception, which is not an inconsistency:
     size is chrome-like and bounded, palette is data.

     HOW A STEP GETS ADDED, so the next drift has a procedure rather than a precedent:
       1. Show the sites that need it. Two or more real, shipped sites that no existing step
          serves — not one, and not a hypothetical.
       2. Name the ROLE it fills, in one line, distinct from its neighbours' roles above.
       3. Add it here, in size order, with that line.
       4. Convert every site in step 1 in the same commit, so the step ships with its readers
          (a token nothing reads is the SR-1 defect).
       5. Update DESIGN.md's step count and role table.
     --ds-font-title and --ds-font-2xl were both added this way: eleven titles at 15/18/20px had
     no step between 16 and 22, and the Dashboard's page title could not outrank the KPI values
     it labels because 22px was the ceiling (docs/ROADMAP.md's [Dashboard] entry). */
  --ds-font-glyph-xs: 9px;
  --ds-font-glyph-sm: 10px;
  --ds-font-xs: 11px;
  --ds-font-sm: 12px;
  --ds-font-md: 13px;
  --ds-font-lg: 16px;
  --ds-font-title: 18px;
  --ds-font-xl: 22px;
  --ds-font-2xl: 28px;

  /* Monospace family. ADDED BECAUSE IT WAS ALREADY BEING USED AND DID NOT EXIST — the same
     defect --ds-accent was added to fix (see its comment above), found again by an audit and
     fixed together with seven more of the same shape. css/content.css referenced
     var(--ds-font-mono, monospace) in four places while nothing defined it anywhere in the
     repo, so all four silently rendered the bare `monospace` fallback — the browser default,
     not this stack — and css/builder.css and css/automationsCanvas.css each carried their own
     hand-written copy of the family instead. The value is the stack builder.css already used
     (the most complete of the three), so nothing changes visually where a stack was declared;
     content.css's four sites gain the stack they were asking for.

     test/design-token-defined.test.js is the permanent guard: it fails on any var(--ds-*)
     reference this file does not define, with or without a fallback. A fallback does not make
     an undefined token safe — it silently substitutes an off-system value, which is how six of
     the eight went unnoticed; the two without one (css/media.css, css/messages.css) made the
     whole declaration invalid at computed-value time and fell back to inherited colour. */
  --ds-font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Z-index scale (P0-A — Z-Index Scale Consolidation). One ordered scale for the whole
     app shell's stacking, replacing scattered literals (including several max-int hacks
     that lived in the preview-iframe VE overlay scripts — see public/veZIndex.js, a
     separate parallel scale since that iframe document never loads this stylesheet).
     Each tier below lists every current consumer so the next addition has an obvious
     home; --ds-z-messages is a deliberate reserved gap for the upcoming shell messages
     overlay (must sit above all module chrome/popovers/drawers/modals, below toasts). */
  --ds-z-inline-suggest: 10;  /* .ds-mention-autocomplete, .ds-reference-search */
  --ds-z-float-control: 20;   /* .ds-info-tip::after, .ve-toolbar, .global-footer-bar, .docs-search-results */
  --ds-z-tooltip: 30;         /* [data-tip]:hover::after, #pesttgOverflowMenu */
  --ds-z-mobile-toggle: 50;   /* .ds-messages-rail-toggle (mobile) */
  --ds-z-modal-lite: 100;     /* .ds-messages-rail (mobile), .attachment-lightbox */
  --ds-z-menu: 200;           /* .ds-board-menu-popover, .ds-reaction-picker-popover */
  --ds-z-popover: 500;        /* .new-project-popover, .notifications-popover */
  --ds-z-drawer: 600;         /* .drawer-backdrop, .ds-search-modal-backdrop */
  --ds-z-drawer-top: 601;     /* .drawer-panel */
  --ds-z-modal: 700;          /* .ds-lightbox-backdrop */
  --ds-z-messages: 1000;      /* #messagesOverlayRoot — see public/messagesOverlay.js (P0-B) */
  --ds-z-toast: 2000;         /* .feedback-toast, .global-toast-stack */
}

/* ---- Light theme (Business OS — Notifications Bell + Settings, V1, Commit 5). ----
   Overrides every color token declared in :root above for document.documentElement
   ([data-theme="light"] on <html>, set by public/theme.js / index.html's inline
   flash-of-wrong-theme guard — see theme.js's header comment). Purely attribute-driven:
   no `@media (prefers-color-scheme)` anywhere in this file — theme.js resolves 'system'
   to a concrete 'dark'/'light' data-theme value itself via matchMedia, so there is exactly
   one mechanism deciding colors.

   Scope of the retrofit that makes this override actually work app-wide, beyond the
   token layer itself: body{} (below) and the .primary-nav sidebar shell (further down —
   search ".primary-nav {") now read these tokens instead of hardcoded hex, since the
   sidebar wraps every page including /styleguide and /settings. Everything inside
   #bizosMain's per-module CSS (~400 pre-existing hardcoded hex values, out of scope for
   this commit) is NOT retrofitted and stays dark-only when data-theme="light" — logged
   page-by-page to docs/ROADMAP.md.

   Non-color tokens (spacing/radius/type scale) are identical in both modes and aren't
   repeated here. The 6 PESTTG force-accent colors and the 4 impact-scale colors are
   categorical, not background/foreground, so they're only lightly adjusted for
   contrast-on-white rather than fully re-paired like the bg/text tokens. */
[data-theme="light"] {
  --surface-hover: #e9ebef;
  --border-strong: #c7ccd6;
  --danger-bg: #fee2e2;
  --danger-text: #b91c1c;

  /* Background layers */
  --ds-bg-page: #f5f6f8;
  --ds-bg-card: #ffffff;
  --ds-bg-inset: #eef0f3;
  --ds-border: #dfe2e8;
  --ds-border-strong: #c7ccd6;

  /* Sidebar shell */
  --ds-nav-bg: #eef0f3;
  --ds-nav-border: #dde1e7;
  --ds-nav-active-bg: #e2e6ee;

  /* Text */
  --ds-text-primary: #14161c;
  --ds-text-secondary: #4b5563;
  /* Was #6b7280: 4.83:1 on --ds-bg-card #ffffff (pass) but 4.47:1 on --ds-bg-page #f5f6f8
     (FAIL). Light mode was not "one adjustment from failing" — it was already failing on the
     page background, which is where .ds-empty and .ds-loading render. #666c7a is 5.26:1 on
     card and 4.87:1 on page. Both are enforced by test/design-token-contrast.test.js. */
  --ds-text-muted: #666c7a;

  /* Focus ring, light palette — the dark #60a5fa is too pale on white. Matches this
     palette's own --ds-info-text hue for the same reason. */
  --ds-focus-ring: #1d4ed8;

  /* The interactive colour, light palette — see the dark declaration for why this token exists.
     Same value as this palette's focus ring, same reasoning. Measured: 6.20:1 on --ds-bg-page,
     6.70:1 on --ds-bg-card, 5.87:1 on --ds-bg-inset. */
  --ds-accent: #1d4ed8;

  /* Accent-under-pointer, light palette. Deeper again, for the same reason as the dark
     declaration: the hover step moves AWAY from the page, and on white that means darker. */
  --ds-accent-hover: #1e40af;

  /* Text on a non-inverting fill — see the dark declaration. Same value on purpose: the
     grounds this sits on (a fixed red badge, a black scrim) do not change with the theme,
     so neither may their text. */
  --ds-text-on-fill: #ffffff;

  /* Overlay ground — see the dark declaration. Same value on purpose. */
  --ds-scrim: #000000;

  /* Ink on a fill — see the dark declaration. Same value on purpose; what changes between
     palettes is WHICH fills choose it, since the fills themselves shift. */
  --ds-ink-on-fill: #14161c;

  /* Driving-force accents (PESTTG) — darkened slightly from the dark-mode values for
     contrast against light backgrounds, otherwise the same recognizable hues. */
  --ds-force-political: #dc2626;
  --ds-force-economic: #16a34a;
  --ds-force-societal: #b45309;
  --ds-force-technological: #2563eb;
  --ds-force-mega_trends: #9333ea;
  --ds-force-globalisation: #0d9488;

  /* Idea scope accents — darkened for light-background contrast, same hues as the dark
     theme's --ds-scope-* above. */
  --ds-scope-org_wide: #4b5563;
  --ds-scope-new_venture: #db2777;
  --ds-scope-supply_chain: #b45309;
  --ds-scope-operations: #2563eb;
  --ds-scope-marketing_sales: #16a34a;
  --ds-scope-service_warranty: #0d9488;
  --ds-scope-infrastructure: #9333ea;
  --ds-scope-human_resources: #dc2626;
  --ds-scope-technology: #0891b2;
  --ds-scope-procurement: #c2410c;

  /* Semantic */
  --ds-success-bg: #dcfce7;
  --ds-success-text: #15803d;
  --ds-warning-bg: #fef3c7;
  --ds-warning-text: #92400e;
  --ds-danger-bg: #fee2e2;
  --ds-danger-text: #b91c1c;
  --ds-info-bg: #dbeafe;
  --ds-info-text: #1d4ed8;
  --ds-neutral-bg: #e5e7eb;
  --ds-neutral-text: #4b5563;

  /* Semantic hairlines, light palette — see the dark declarations. Each sits between its
     own -bg and -text here too, which on this palette means a tint darker than the fill. */
  --ds-success-border: #86d5a4;
  --ds-warning-border: #e5c169;
  --ds-danger-border: #f0a9a9;

  /* Impact scale (low -> very_high) */
  --ds-impact-low: #6b7280;
  --ds-impact-medium: #2563eb;
  --ds-impact-high: #b45309;
  --ds-impact-very_high: #dc2626;
}

/* The `hidden` attribute must win even on flex/grid elements (otherwise a
   display:flex rule overrides it and the element stays visible). */
[hidden] { display: none !important; }
/* The clipping that makes the app shell a fixed, non-scrolling frame lives on .layout, NOT on
   body. It used to sit on body, and that was a plain bug rather than a device policy: index.html
   hosts three FULL-PAGE surfaces that sit OUTSIDE .layout — the QR wants survey
   (#wantsSurveyMain), the accept-invite card, and the workspace chooser — and `overflow: hidden`
   on body clipped every one of them at exactly one viewport height with no way to reach the rest.
   The survey is the case that proves it: a respondent scanning a printed QR code gets a
   multi-question form on a phone, and anything past the first screenful was unreachable.

   .layout keeps the identical behaviour it always had (its own panes scroll internally via
   .bizos-main's overflow-y), so the shell is visually and behaviourally unchanged. Nothing
   depends on body's height: the two `height: 100%` shells in the app (.docs-shell,
   .settings-shell) chain off .bizos-main, not off body.

   dvh with a vh fallback: on mobile browsers 100vh is the viewport with the chrome HIDDEN, so a
   100vh frame is taller than what you can actually see and the bottom sits under the toolbar.
   Fixing this is not a device commitment either way — it is what vh has always meant. */
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: var(--ds-bg-page);
  color: var(--ds-text-primary);
  min-height: 100vh;
  min-height: 100dvh;
}
.layout {
  display: flex;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}
/* Business OS — the AI App Builder's own shell (Prompt 24.8's CSS split of style.css).
   Predates Business OS entirely: sidebar/header/section cards/build progress/panels/
   checklist/login/visual-editing toolbar/attachments/voice input/brand kits/content (CMS)/
   publish. Not tied to any single os/<area>.js module — this is the chat-panel + preview-
   panel builder tool, not a Business OS analysis page. */

/* ---- Left sidebar (collapsible) ---- */
.chat-panel {
  width: 360px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--ds-nav-border);
  background: var(--ds-nav-bg);
  padding: 14px;
  transition: width 0.18s ease, padding 0.18s ease;
}
.sidebar-inner {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1;
  gap: 10px;
}
.chat-panel.collapsed {
  width: 60px;
  padding: 12px 8px;
}

/* ---- Header ---- */
.sidebar-header {
  display: flex;
  align-items: center;
  gap: 8px;
}
.chat-panel h1 {
  font-size: var(--ds-font-lg);
  margin: 0;
  white-space: nowrap;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.header-right {
  display: flex;
  align-items: center;
  gap: 6px;
}
.chat-panel.collapsed .sidebar-header {
  flex-direction: column;
}
.chat-panel.collapsed h1,
.chat-panel.collapsed #provider {
  display: none;
}

select {
  /* Widened slightly beyond the plan's named sidebar selector list (Commit 5) — this bare
     `select` rule is what paints every plain <select> in the app (e.g. #provider,
     #projectSelect) consistently. Token values match the existing dark hex closely enough
     that dark mode is visually unchanged. */
  background: var(--ds-bg-inset);
  color: var(--ds-text-primary);
  border: 1px solid var(--ds-border-strong);
  border-radius: 6px;
  padding: 4px 8px;
  font-size: var(--ds-font-sm);
}

/* ---- Section cards (Project / Configuration) — grouped, visually distinct rows
   instead of a flat stack of same-weight controls. ---- */
.sidebar-section {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 8px 9px;
  background: var(--ds-bg-card);
  border: 1px solid var(--ds-nav-border);
  border-radius: 8px;
}
.section-title {
  margin: 0;
  font-size: var(--ds-font-xs);
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ds-text-muted);
}
.section-row {
  display: flex;
  align-items: center;
  gap: 6px;
}
.section-row select {
  flex: 1;
  min-width: 0;
}
.section-row select:disabled {
  opacity: 0.7;
  cursor: default;
}
/* ---- Project actions: labelled two-row grid (icon above, sentence-case label below).
   4 columns so 7 buttons wrap to two rows (4 + 3) rather than one. No hover-tooltip
   mechanism here on purpose (see index.html's comment above this markup) — the label is
   a permanent child of the button, not a `[data-tip]:hover::after` reveal. ---- */
.project-actions-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 4px;
}
.icon-grid-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 6px 2px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  color: var(--ds-text-secondary);
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.icon-grid-btn:hover,
.icon-grid-btn.is-active {
  background: var(--surface-hover);
  border-color: var(--border-strong);
  color: var(--ds-text-primary);
}
.icon-grid-btn.danger:hover {
  background: var(--danger-bg);
  color: var(--danger-text);
}
.icon-grid-btn .icon-grid-btn-icon .icon {
  width: 16px;
  height: 16px;
}
.icon-grid-btn-label {
  font-size: var(--ds-font-glyph-sm);
  line-height: 1.15;
  text-align: center;
  white-space: nowrap;
}
.icon-grid-btn:disabled {
  opacity: 0.5;
  cursor: default;
}
.icon-grid-btn:disabled:hover {
  background: transparent;
  border-color: transparent;
  color: var(--ds-text-secondary);
}
.field-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  color: var(--ds-text-muted);
}

/* This project's Brand Kit picker, rendered at the top of the Brand Kit panel (opened from
   the project row's icon — see brandKits.js's renderProjectBrandKitPicker). Not a permanent
   sidebar row: only present while the panel is open. */
.brandkit-project-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border-strong);
}
.brandkit-project-row label {
  font-size: var(--ds-font-sm);
  color: var(--ds-text-secondary);
  flex-shrink: 0;
}
.brandkit-project-row select {
  flex: 1;
  min-width: 0;
}

/* ---- Collapsed sidebar: section cards shrink to icon-only columns. Field icons (e.g. the
   Project row's folder glyph) still carry a data-tip hover reveal at this width — there's no
   room for a permanent label at 60px. The project-actions-grid buttons drop their permanent
   label here too for the same space reason, falling back to icon-only + aria-label; this is
   the one place they lose the always-visible label the rest of this file's sizing keeps. ---- */
.chat-panel.collapsed .section-title,
.chat-panel.collapsed .section-row select {
  display: none;
}
.chat-panel.collapsed .sidebar-section {
  align-items: center;
  padding: 6px 0;
  background: transparent;
  border-color: transparent;
}
.chat-panel.collapsed .section-row {
  flex-direction: column;
}
.chat-panel.collapsed .project-actions-grid {
  grid-template-columns: 1fr;
}
.chat-panel.collapsed .icon-grid-btn-label {
  display: none;
}
/* Nothing below the cost row (activity log, history/brand-kit panels, the composer)
   fits meaningfully in an icon-only column — hide it while collapsed, same as the
   old rail design did, just scoped per-element instead of one big wrapper. */
.chat-panel.collapsed .messages,
.chat-panel.collapsed .history-panel,
.chat-panel.collapsed .build-status,
.chat-panel.collapsed .prompt-form,
.chat-panel.collapsed .hint {
  display: none;
}

/* ---- Icon buttons: icon-only by default, label on hover via data-tip (no JS). ---- */
.icon-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  flex-shrink: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  /* Widened slightly beyond the plan's named sidebar selector list (Commit 5) — .icon-btn
     is what #primaryNavToggle/#bellBtn/#docsBtn/#settingsBtn/#logoutBtn (all in
     .primary-nav-header/#globalFooterBar, explicitly in scope) actually use for color. */
  color: var(--ds-text-secondary);
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.icon-btn:hover,
.icon-btn.is-active {
  background: var(--surface-hover);
  border-color: var(--border-strong);
  color: var(--ds-text-primary);
}
.icon-btn.danger:hover {
  background: var(--danger-bg);
  color: var(--danger-text);
}
/* "New project" as the primary action — toggled by projects.js only while there is no
   current project (see loadProjects/deleteCurrentProject's empty-state branches). Once a
   project exists, New Project reverts to an ordinary icon button; it's no longer the one
   obvious first thing to do. Same treatment components.css's .ds-btn-primary uses
   elsewhere, applied to this sidebar's own icon-button shape. */
.icon-btn.primary {
  background: var(--ds-info-text);
  border-color: var(--ds-info-text);
  color: var(--ds-bg-page);
}
.icon-btn.primary:hover {
  background: var(--ds-info-text);
  opacity: 0.85;
}
/* Notifications Bell's unread-count badge (see #bellBtn/#bellBadge in index.html) —
   .icon-btn's own position:relative anchors it. Hidden via the [hidden] attribute
   (public/notifications.js toggles it), not display:none in CSS, so JS stays the single
   source of truth for visibility. */
.icon-btn-badge {
  position: absolute;
  top: 2px;
  right: 2px;
  min-width: 15px;
  height: 15px;
  padding: 0 3px;
  border-radius: 999px;
  background: var(--ds-force-political);
  color: var(--ds-text-on-fill);
  font-size: var(--ds-font-glyph-sm);
  font-weight: 700;
  line-height: 15px;
  text-align: center;
}
[data-tip] {
  position: relative;
}
/* :focus-visible is not decoration here. The OS shell's footer rail is six icon-only
   buttons whose only visible label is this tooltip — one of them is log out. With a
   hover-only reveal, a keyboard or magnifier user can focus all six and never see a name
   for any. The aria-labels in index.html already serve screen-reader users; this serves
   the sighted-keyboard user they don't cover. */
[data-tip]:hover::after,
[data-tip]:focus-visible::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--ds-scrim);
  color: var(--ds-text-on-fill);
  font-size: var(--ds-font-xs);
  padding: 3px 7px;
  border-radius: 4px;
  white-space: nowrap;
  pointer-events: none;
  z-index: var(--ds-z-tooltip);
}
/* The top-bar toolbar sits right at the top of the viewport — a tooltip opening
   upward from there gets clipped by the browser chrome, so flip it downward. */
.toolbar-actions [data-tip]:hover::after,
.toolbar-actions [data-tip]:focus-visible::after {
  bottom: auto;
  top: calc(100% + 6px);
}

/* Sidebar collapse/expand toggle icon rotates to hint direction: pointing left = will
   collapse, pointing right = will expand. */
#sidebarToggle .icon {
  transition: transform 180ms ease;
  transform: rotate(90deg);
}
/* The chat panel and this toggle are shell furniture — they sit in the document on every OS
   route, not just in the builder — so they were two of the five selectors still animating
   under a real reduced-motion context. Killed rather than substituted for the same reason as
   businessOS-shell.css's block: the panel's width and the icon's angle are end states that
   still read instantly, and only the tween between them is being removed. */
@media (prefers-reduced-motion: reduce) {
  .chat-panel,
  #sidebarToggle .icon {
    transition: none;
  }
}
.chat-panel.collapsed #sidebarToggle .icon {
  transform: rotate(-90deg);
}

/* ---- Cost tracker: persistent metadata row, pulled out of the activity log. ---- */
.cost-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 9px;
  background: var(--ds-nav-bg);
  border: 1px solid var(--ds-nav-border);
  border-radius: 6px;
}
.cost-row:has(.cost-text:empty) {
  display: none;
}
.cost-text {
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
  margin-left: auto; /* figure right-aligned, label left — so the number is scannable */
  font-variant-numeric: tabular-nums;
}
/* Follows .cost-text's own visibility rules exactly: the row hides itself when there is no figure
 * (`:has(.cost-text:empty)` above), and a label with no number beside it would be a lie about
 * there being something to label. */
.cost-label {
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
}
.chat-panel.collapsed .cost-text,
.chat-panel.collapsed .cost-label {
  display: none;
}
.chat-panel.collapsed .cost-row {
  justify-content: center;
  background: transparent;
  border-color: transparent;
  padding: 6px 0;
}
.messages {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 12px;
}
.history-panel {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 12px;
}
.history-empty {
  font-size: var(--ds-font-sm);
  /* Was a hardcoded #6b7080 — a near-copy of the old --ds-text-muted that bypassed the
     token, so the palette fix could not reach it. os/dashboard.js uses this class 8 times. */
  color: var(--ds-text-muted);
  margin: 0;
}
.history-item {
  background: var(--ds-bg-card);
  border: 1px solid var(--ds-nav-border);
  border-radius: 8px;
  padding: 10px;
}
.history-meta {
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
  margin-bottom: 4px;
}
.history-prompt {
  font-size: var(--ds-font-md);
  line-height: 1.35;
  color: var(--ds-text-primary);
  white-space: pre-wrap;
  word-break: break-word;
  margin-bottom: 8px;
}
.history-actions {
  display: flex;
  gap: 6px;
}
.history-actions button {
  background: var(--ds-nav-active-bg);
  font-size: var(--ds-font-xs);
  padding: 4px 10px;
}
/* Elements panel (public/elementsPanel.js) — uses --ds-* tokens (both color modes), unlike
   this file's older .history-item etc. rules above, which predate the design system's token
   adoption and stay dark-only (see css/base.css's own header on that scope). */
.elements-target-status {
  margin-bottom: 10px;
}
.elements-target-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--ds-font-sm);
  color: var(--ds-text-primary);
  background: var(--ds-bg-inset);
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-sm, 6px);
  padding: 8px 10px;
}
.elements-target-empty {
  color: var(--ds-text-secondary);
}
.elements-position-toggle {
  display: flex;
  gap: 6px;
  margin-top: 6px;
}
.elements-position-toggle button {
  flex: 1;
  font-size: var(--ds-font-xs);
  padding: 5px 8px;
  background: var(--ds-bg-card);
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-sm, 6px);
  color: var(--ds-text-secondary);
  cursor: pointer;
}
.elements-position-toggle button.is-active {
  background: var(--ds-nav-active-bg);
  border-color: var(--ds-border-strong);
  color: var(--ds-text-primary);
}
.elements-shared-warning {
  margin-top: 6px;
  font-size: var(--ds-font-xs);
  background: var(--ds-warning-bg);
  color: var(--ds-warning-text);
  border-radius: var(--ds-radius-sm, 6px);
  padding: 6px 8px;
}
/* A REFUSAL IS NOT AN EMPTY STATE. /api/ve/insert-resolve answers a protected library component
   with a 409, and the panel rendered that refusal through .elements-target-empty — the same grey
   row as "Select an element on the canvas first." So a correct, deliberate refusal was painted in
   the one style that means "nothing has happened yet", which is how a live 409 was seen in the
   console and not in the UI (2026-09-19). */
/* A PAGE AT SEVERAL URLS — information, deliberately NOT the warning palette. It sits where the
   shared-component caution would, so if it were painted the same way it would read as one
   (lib/componentUsage.ts). */
.ve-shared-note {
  margin: 0 0 8px;
  font-size: var(--ds-font-xs);
  color: var(--ds-text-secondary);
  background: var(--ds-bg-inset);
  border-radius: var(--ds-radius-sm, 6px);
  padding: 6px 8px;
}
.elements-target-refused {
  background: var(--ds-danger-bg);
  color: var(--ds-danger-text);
  border-radius: var(--ds-radius-sm, 6px);
  padding: 6px 8px;
}
.elements-search {
  width: 100%;
  box-sizing: border-box;
  margin-bottom: 10px;
  padding: 6px 10px;
  font-size: var(--ds-font-sm);
  background: var(--ds-bg-card);
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-sm, 6px);
  color: var(--ds-text-primary);
}
.elements-category-header {
  font-size: var(--ds-font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ds-text-muted);
  margin: 10px 0 6px;
}
.elements-category-header:first-child {
  margin-top: 0;
}
.elements-category-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
}
.elements-item-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 8px 4px;
  background: var(--ds-bg-card);
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-sm, 6px);
  color: var(--ds-text-secondary);
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.elements-item-btn:hover:not(:disabled) {
  background: var(--surface-hover);
  border-color: var(--border-strong);
  color: var(--ds-text-primary);
}
.elements-item-btn:disabled {
  opacity: 0.45;
  cursor: default;
}
.elements-item-icon .icon {
  width: 16px;
  height: 16px;
}
.elements-item-label {
  font-size: var(--ds-font-glyph-sm);
  line-height: 1.15;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* ---- Round 4 drawer editor (public/veDrawer.js) — same panel shell as .history-panel,
   reuses .elements-target-row/.elements-target-empty/.elements-shared-warning verbatim for
   the "select an element" empty state and the shared-component warning. ---- */
/* ---- ancestor breadcrumb -------------------------------------------------------------------
   The path from the selection outward. WRAPS rather than scrolls: a horizontal scroller in a
   460px drawer hides the outermost levels behind a gesture, and those are exactly the ones
   someone reaching for "the section around this" needs. */
/* SAYS WHAT IT IS. The row was read as decoration by the first person to use it who had not built
   it — "it works, it's just not obvious what it is" — so the block leads with its own name and the
   gesture, and the levels below now carry a border rather than reading as plain text. */
.ve-breadcrumb-block {
  margin: 0 0 12px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--ds-border);
}
.ve-breadcrumb-label {
  margin: 0 0 4px;
  font-size: var(--ds-font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ds-text-muted);
}
.ve-breadcrumb-label span {
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}
.ve-breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px;
  margin: 0;
  font-size: var(--ds-font-xs);
}
.ve-crumb {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  max-width: 100%;
  padding: 2px 6px;
  /* A VISIBLE EDGE, not a transparent one. These are the drawer's only controls that looked like
     running text, which is most of why the row did not read as clickable. */
  border: 1px solid var(--ds-border);
  border-radius: 4px;
  background: none;
  color: var(--ds-text-secondary);
  font: inherit;
  cursor: pointer;
}
.ve-crumb:hover {
  background: var(--ds-bg-inset);
  border-color: var(--ds-text-muted);
  color: var(--ds-text-primary);
}
.ve-crumb:focus-visible { outline: 2px solid var(--ds-focus-ring); outline-offset: 1px; }
/* The selected level is not a link to where you already are — it reads as a position, and it is
   the anchor the rest of the row is relative to, so it is the one thing here with a filled edge. */
.ve-crumb.is-selected {
  color: var(--ds-text-primary);
  font-weight: 600;
  background: var(--ds-bg-inset);
  border-color: var(--ds-text-primary);
  cursor: default;
}
/* THE TRAIL BELOW THE SELECTION. Clicking inside a library component selects the COMPONENT, so the
   path keeps the levels down to whatever was actually clicked. They stay reachable — that is the
   whole point of showing them — but they are not where the tenant is, so they recede. */
.ve-crumb.is-downstream { opacity: 0.55; }
.ve-crumb.is-downstream:hover { opacity: 1; }
/* A component is a different KIND of level, not a styled tag: it names a usage, and selecting it
   lands somewhere structurally different from its own root element. */
.ve-crumb.is-component { color: var(--ds-accent); }
.ve-crumb-hint { color: var(--ds-text-muted); font-weight: 400; opacity: 0.8; }
.ve-crumb-sep { color: var(--ds-text-muted); opacity: 0.5; }
/* A failed label lookup is a TRANSPORT failure wearing a product face, so it is painted as the
   drawer's other transport errors are rather than as a quiet caption. */
.ve-crumb-note {
  margin: -6px 0 12px;
  color: var(--ds-text-muted);
  font-size: var(--ds-font-xs);
}
.ve-crumb-gap { padding: 0 2px; color: var(--ds-text-muted); cursor: default; }

.ve-section-title {
  font-size: var(--ds-font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ds-text-muted);
  margin: 14px 0 6px;
}
.ve-section-title:first-child {
  margin-top: 0;
}
.ve-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 8px;
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-sm, 6px);
  margin-bottom: 6px;
  font-size: var(--ds-font-sm);
}
.ve-field label {
  font-weight: 500;
  color: var(--ds-text-secondary);
}
.ve-field-row {
  flex-direction: row;
  align-items: center;
}
.ve-field-greyed {
  opacity: 0.65;
  background: var(--ds-bg-inset);
}
/* An UNSET-but-settable prop (Round 5 (a)) — deliberately NOT dimmed like .ve-field-greyed:
   its control is live, and dimming it is exactly what made "not set here" read as a refusal.
   The dashed border is the only thing marking it as not-yet-written. */
.ve-field-unset {
  border-style: dashed;
}
/* The repeatable array-of-media control (Carousel's images) — every row is local, unsaved
   state until "Save images", so nothing here implies a write has happened. */
.ve-media-rows {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.ve-media-row {
  display: flex;
  align-items: center;
  gap: 6px;
}
.ve-media-thumb {
  width: 40px;
  height: 30px;
  object-fit: cover;
  border-radius: var(--ds-radius-sm, 6px);
  border: 1px solid var(--ds-border);
  flex-shrink: 0;
  background: var(--ds-bg-inset);
}
.ve-media-row .ve-media-alt {
  flex: 1 1 auto;
  min-width: 0;
}
/* An alt field still holding the text a picker suggested from the file name (public/altFromFilename.js).
   Said twice — a warning-toned border and a sentence — not by colour alone. For a carousel image the
   sentence sits under the row rather than inside it: wrapping the row put the thumbnail, the field
   and the buttons on three lines (seen by rendering it, 2026-10-06). */
.ds-input.is-suggested {
  border-color: var(--ds-warning-border);
  background: var(--ds-warning-bg);
}
.ve-alt-suggested {
  margin: 4px 0 0;
  font-size: var(--ds-font-xs);
  color: var(--ds-warning-text);
}
.ve-media-row-actions {
  display: flex;
  gap: 2px;
  flex-shrink: 0;
}
/* The per-item array-of-objects control (Timeline's entries) — one card per entry, its fields
   stacked, its move/remove actions top-right. Same local-until-Save rule as the media rows. */
.ve-oa-items {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.ve-oa-item {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: 0;
  padding: 8px;
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-sm, 6px);
  min-width: 0;
}
.ve-oa-item legend {
  font-size: var(--ds-font-sm);
  font-weight: 600;
  padding: 0 4px;
}
.ve-oa-item > .ve-media-row-actions {
  align-self: flex-end;
}
.ve-oa-media {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.ve-oa-media .ve-media-thumb {
  width: 96px;
  height: 54px;
}
.media-picker-multi-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--ds-border);
  font-size: var(--ds-font-sm);
  color: var(--ds-text-secondary);
}
.ve-prop-add-check {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1 1 auto;
  font-weight: 400;
  color: var(--ds-text-secondary);
}

/* A spacing side that cannot receive a value, because the element has no class attribute to append
   to. Dimmed like .ve-field-greyed and for the same reason — this is the drawer's existing "can't,
   and here is why" vocabulary, with the why stated once under the row rather than four times
   inside it. It stays IN the grid rather than moving to its own greyed row: all four sides must
   remain visible (the Round 4 (2) decision), and the reason is the group's, not the side's. */
.ve-spacing-cell-off {
  opacity: 0.55;
}
.ve-spacing-cell-off select {
  cursor: not-allowed;
}

/* ---- The FOURTH STATE: a prop whose absence is a working setting, not a gap (2026-09-01) -----
   Deliberately NOT .ve-field-unset's dashed border when the Business OS side is chosen — dashed
   means "nothing written here yet", and for these props nothing written here IS the recommended
   configuration. The field only goes dashed when it is genuinely unset AND the tenant has not
   said which side they want, which is the same state every other unset prop is in. */
.ve-bos-choice {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: 2px 0;
}
.ve-bos-option {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  font-weight: 400;
  color: var(--ds-text-secondary);
  line-height: 1.35;
}
.ve-bos-option input {
  margin-top: 2px;
  flex-shrink: 0;
}
/* The area's name, so the option reads as a place and not only as a sentence. */
.ve-bos-where {
  display: inline-block;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--ds-bg-inset);
  border: 1px solid var(--ds-border);
  font-size: var(--ds-font-xs, 11px);
  color: var(--ds-text-muted);
  white-space: nowrap;
}
.ve-bos-literal[hidden] {
  display: none;
}
/* A COMPONENT-LEVEL source (ScheduleDisplay's sessions) — a note at the top of the props list
   rather than a field, because there is no prop to switch and offering a control would imply one. */
.ve-bos-note {
  margin: 0 0 6px;
  padding: 8px;
  border: 1px solid var(--ds-border);
  border-left: 3px solid var(--ds-accent);
  border-radius: var(--ds-radius-sm, 6px);
  background: var(--ds-bg-inset);
  font-size: var(--ds-font-xs, 11px);
  color: var(--ds-text-secondary);
  line-height: 1.45;
}
.ve-bos-link {
  white-space: nowrap;
  color: var(--ds-accent);
}
.ve-greyed-reason {
  margin: 0;
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
}

/* ---- The Edit drawer's one-Save header, role chips and icon controls (2026-08-28) ----------- */
.ve-drawer-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
}
/* The Save is in the drawer HEADER, which openDrawer renders sticky, because it serves every
   control in the scrolling list below — a per-control button has to sit beside its control, so ten
   controls make ten buttons however they are styled. Disabled with nothing staged: a Save that is
   pressable over no changes is a control whose effect the tenant cannot predict. */
.ve-drawer-save:disabled { opacity: 0.45; cursor: default; }

.ve-field-note {
  margin: 4px 0 0;
  font-size: var(--ds-font-xs, 11px);
  color: var(--ds-text-secondary);
  line-height: 1.45;
}
.ve-field-note code {
  font-family: var(--ds-font-mono);
}

/* Brand roles: two columns of three, so each surface/ink PAIR reads as a row. The pairing is the
   palette's actual contract (theme.css), not a layout convenience. */
.ve-role-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px;
}
.ve-role-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 7px;
  min-width: 0;
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-sm, 6px);
  background: var(--ds-bg-card);
  color: var(--ds-text-primary);
  font-size: var(--ds-font-xs, 11px);
  cursor: pointer;
  text-align: left;
}
.ve-role-chip:hover { border-color: var(--ds-border-strong, var(--ds-accent)); }
.ve-role-chip.is-selected { border-color: var(--ds-accent); box-shadow: inset 0 0 0 1px var(--ds-accent); }
.ve-role-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A role with no hex in theme.css gets a HATCHED chip rather than a blank one — "we could not read
   this colour" and "this colour is white" must not look the same. */
.ve-role-swatch {
  flex: none;
  width: 14px;
  height: 14px;
  border-radius: 3px;
  border: 1px solid var(--ds-border);
  background: repeating-linear-gradient(45deg, var(--ds-bg-inset) 0 3px, var(--ds-bg-card) 3px 6px);
}

/* Visual enums: one chip per value, showing the effect AND naming it.
   The glyph states what the value does in a glance, which is what a word cannot do for
   `align-items: baseline`; the word names it, which is what a glyph cannot do without a hover. The
   chip stacks them because there are up to six values on a ~400px row and side-by-side wraps. */
.ve-enum-row { display: flex; flex-wrap: wrap; gap: 4px; }
.ve-enum-chip {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 4px 6px;
  min-width: 44px;
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-sm, 6px);
  background: var(--ds-bg-card);
  color: var(--ds-text-primary);
  cursor: pointer;
}
.ve-enum-chip:hover { border-color: var(--ds-border-strong, var(--ds-accent)); }
.ve-enum-chip.is-selected { border-color: var(--ds-accent); box-shadow: inset 0 0 0 1px var(--ds-accent); }
/* nowrap: the words are single short tokens ("between", "baseline"), and one breaking across two
   lines is the exact SR-14 symptom — a label that no longer reads as belonging to its own glyph. */
.ve-enum-word { font-size: var(--ds-font-xs, 11px); line-height: 1.2; white-space: nowrap; }

/* ---- A STAGED EDIT: the count on the Save button, said again on the control it came from -------
   These marks and the button's number are rendered by one function over one map (veDrawer.js's
   `syncPendingUi`) because they were previously two computations of two different facts and could
   disagree — the button counted `pending`, while the "highlight" a tenant saw was `is-selected`
   (which marks the CURRENT value, staged or not) plus a <select>'s native focus ring, which is not
   state and cleared on the next click.
   Said TWICE — a border and a word — rather than by colour alone, which is not a signal for every
   reader. */
.ve-field.is-staged {
  border-color: var(--ds-accent);
  box-shadow: inset 0 0 0 1px var(--ds-accent);
}
/* A SPACING CELL IS MARKED ON ITS SELECT, NOT ON THE CELL. The cell has no border and no padding —
   it is a bare flex column in a four-column grid — so an inset ring drawn on it hugs the label and
   the control with nothing between, and the marked column stops matching the three beside it. Seen
   by looking at a render of the group with one side staged: the row read as three cells and one
   box. Marking the control that actually changed is both tidier and more precise, and the dot
   beside the side's own name is the second signal. */
.ve-spacing-cell.is-staged select {
  border-color: var(--ds-accent);
  box-shadow: inset 0 0 0 1px var(--ds-accent);
}
.ve-staged-badge {
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: 999px;
  /* --ds-bg-inset, not an invented `--ds-accent-soft`: there is no soft-accent token in the design
     system, and referencing one would have meant this badge was painted by a fallback for the rest
     of its life while reading as though it used a colour that exists. */
  background: var(--ds-bg-inset);
  color: var(--ds-accent);
  font-size: var(--ds-font-xs, 11px);
  font-weight: 600;
  white-space: nowrap;
}
/* A spacing cell is ~90px wide — too narrow for the badge's words, and truncating them would make
   "Changed — not saved" read as something else entirely. A dot beside the side's own name carries
   the same fact in the space available; the cell border says it a second time. */
.ve-staged-dot {
  width: 6px;
  height: 6px;
  margin-left: auto;
  border-radius: 50%;
  background: var(--ds-accent);
  flex: none;
}
/* `hidden` is what syncPendingUi toggles, and a flex/inline-flex display would override it. */
.ve-staged-badge[hidden],
.ve-staged-dot[hidden] { display: none; }

.ve-spacing-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.ve-spacing-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  /* 4px vertical, not 2px: at --ds-font-xs/1.4 plus a 1px border that is 25.4px painted, clearing
     WCAG 2.5.8's 24px floor. `min-height` was the first attempt and is NOT used — test/
     target-size.test.js computes the painted box from font-size, line-height, padding and border,
     so a min-height it cannot see would have satisfied the browser and not the guard, which is the
     wrong way round for a rule about whether a finger can hit this. */
  padding: 4px 8px;
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-sm, 6px);
  background: var(--ds-bg-card);
  color: var(--ds-text-secondary);
  font-size: var(--ds-font-xs, 11px);
  cursor: pointer;
}
.ve-spacing-link.is-selected {
  border-color: var(--ds-accent);
  color: var(--ds-text-primary);
  box-shadow: inset 0 0 0 1px var(--ds-accent);
}
/* The icon carries the side; the word beside it is the redundant half, kept because an icon alone
   is a guess for anyone meeting the control for the first time. */
.ve-spacing-icon { display: inline-flex; align-items: center; }
.ve-spacing-side {
  display: flex;
  align-items: center;
  gap: 3px;
}

/* ---- Margin/Padding as one group of four sides (2026-08-28) --------------------------------
   Replaces eight stacked dropdowns, each with its own Add button. All four sides are still
   present and independently controlled — that part is a decision (see veDrawer.js's
   spacingGroupHtml) — only the arrangement moved from vertical to horizontal.

   THE GRID USES minmax(0, 1fr), NOT 1fr. A track's implicit minimum is min-content, so a <select>
   whose widest option is a long word ("extrabold") would push the row past the drawer's edge
   rather than shrink. DEFENSIVE: not observed at this drawer's width with this scale — the values
   here are short ("0", "px", "2.5") — and kept because the scale is data, and a longer value is a
   one-line change away from being in it. */
.ve-spacing-row {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 6px;
}
.ve-spacing-cell {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
.ve-spacing-cell select {
  width: 100%;
  min-width: 0;
}
/* The side name sits directly above its own control. In a four-column row the nearest label is
   what a reader binds a control to, so the vertical stack is doing the work the old per-row
   label did. */
.ve-spacing-side {
  font-size: var(--ds-font-xs, 11px);
  font-weight: 500;
  color: var(--ds-text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.ve-spacing-save {
  width: 100%;
  padding-inline: 4px;
  font-size: var(--ds-font-xs, 11px);
}

.ve-color-row {
  display: flex;
  gap: 6px;
  align-items: center;
}
.ve-color-row select,
.ve-color-row input {
  flex: 1 1 auto;
  min-width: 0;
}
.ve-color-swatch {
  display: inline-block;
  width: 20px;
  height: 20px;
  border-radius: 4px;
  border: 1px solid var(--ds-border-strong);
  flex-shrink: 0;
}
.ve-color-subfield {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1 1 auto;
  min-width: 0;
  font-weight: 400;
}
.ve-color-subfield span {
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
}
.ve-current-src {
  margin: 0;
  font-size: var(--ds-font-xs);
  color: var(--ds-text-secondary);
  word-break: break-all;
}
.ve-location-row {
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
  margin-bottom: 8px;
  overflow-wrap: anywhere;
}
.ve-delete-row {
  margin-bottom: 10px;
}
.ve-delete-row .ds-btn {
  width: 100%;
}
.ve-shared-warning {
  margin-bottom: 8px;
  font-size: var(--ds-font-xs);
  background: var(--ds-warning-bg);
  color: var(--ds-warning-text);
  border-radius: var(--ds-radius-sm, 6px);
  padding: 6px 8px;
}
.ve-status-row {
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
  margin-bottom: 8px;
}

/* ONE ROW PER CLASS THAT PRODUCES NO CSS (AUDIT.md AUD-117). Written because the markup shipped
   with no rule at all and rendered as three left-aligned lines per class at body size, with the
   button on its own line against the panel edge — every scene check passed and it read as raw
   output rather than a row. The class name is the identifier a tenant will search their file for,
   so it stays monospace and keeps its size; the branch and the action sit beside it. */
.ve-dead-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 6px;
  padding: 6px 8px;
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-sm, 6px);
  font-size: var(--ds-font-xs);
}
.ve-dead-row code {
  /* It can be long (`bg-[var(--bxb-ink)]/80`), and truncating an identifier somebody has to find
     in their own file would be the one thing this row must not do — so it wraps instead. */
  word-break: break-all;
  min-width: 0;
}
.ve-dead-branch {
  color: var(--ds-text-muted);
}
.ve-dead-row .ds-btn {
  margin-left: auto;
}

.msg {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 10px 12px;
  border-radius: 8px;
  font-size: var(--ds-font-md);
  line-height: 1.4;
}
.msg-text {
  min-width: 0;
}
/* Activity-log status dot: gray for routine narration, green once a system message's
   phrasing reads as a completed/ready outcome (see messages.js's READY_RE). */
.msg-dot {
  flex-shrink: 0;
  width: 6px;
  height: 6px;
  margin-top: 5px;
  border-radius: 50%;
  background: var(--ds-text-muted);
}
.msg-dot.ready {
  background: var(--ds-success-text);
}
.msg.user {
  background: var(--ds-nav-active-bg);
  align-self: flex-end;
}
.msg.system {
  background: var(--ds-bg-card);
  color: var(--ds-text-secondary);
}
.msg.error {
  background: var(--ds-danger-bg);
  color: var(--ds-danger-text);
}
.msg.error .msg-dot {
  background: var(--ds-danger-text);
}
.prompt-form {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
/* ---- The builder's own bare-element defaults --------------------------------------------
   These two are ELEMENT selectors, and this sheet loads on every page — so until this scope
   existed they were the app-wide default for every <button> and <textarea> in the Business
   OS as well, which is not what a builder-era rule should ever have been.

   What that cost, measured across 27 routes in a rendered browser: five OS buttons carrying
   no class of their own were painted a solid accent fill with `color: white` — a literal, so
   it never inverted — at 2.54:1 in dark. Four were first-run CTAs ("Start positioning map",
   "Start marketing mix", "+ Submit an idea", "+ New Brand Kit"); the fifth was a POS location
   tile. Nothing about those call sites asked for an accent fill; the cascade volunteered it.

   IT IS ALSO A TRAP THE ACCESSIBILITY WORK WALKS INTO, which is the real reason to fix the
   rule rather than the five call sites. Converting a click-handling <div> into a real
   <button> is the standard remedy for WCAG 2.1.1 — this codebase did exactly that to all 26
   primary navigation rows — and under the old rule every such conversion also silently
   restyled the element. A remedy that changes how something looks is a remedy people avoid.

   :where() rather than :not(.bizos-main button): :where() contributes ZERO specificity, so
   this stays (0,0,1) exactly as the bare `button` selector was. Written the obvious way the
   selector would score (0,1,1) and start beating .ds-btn, .ds-btn-primary and every other
   design-system class it is supposed to lose to — a fix that breaks more than it repairs.

   The three regions named are the OS shell's content roots (index.html). Drawers and
   popovers are deliberately NOT excluded: they are shared furniture, both realms open them,
   and inside one a design-system class is what decides the look.

   `textarea` below is deliberately left global: it is the same shape of rule, but it paints
   entirely in tokens and matches .ds-textarea, so nothing measurable rides on it. Scoping it
   would restyle every OS textarea to prove a point rather than to fix a defect. */
textarea {
  background: var(--ds-bg-inset);
  color: var(--ds-text-primary);
  border: 1px solid var(--ds-border-strong);
  border-radius: 8px;
  padding: 10px;
  resize: vertical;
  font-family: inherit;
  font-size: var(--ds-font-md);
}
button:not(:where(.bizos-main, .docs-main, .settings-main) button) {
  background: var(--ds-accent);
  color: var(--ds-bg-page);
  border: none;
  border-radius: 6px;
  padding: 8px 14px;
  font-size: var(--ds-font-md);
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
}
button:disabled {
  opacity: 0.6;
  cursor: default;
}
.hint {
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
  margin: 8px 0 0;
}

/* ---- Build progress indicator (see buildStatus.js) ------------------------ */
.build-status {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 7px 10px;
  background: var(--ds-nav-bg);
  border: 1px solid var(--ds-nav-border);
  border-radius: 8px;
  font-size: var(--ds-font-sm);
  color: var(--ds-text-secondary);
}
.build-status-header {
  display: flex;
  align-items: center;
  gap: 8px;
}
.build-status-trail {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
}
/* Scrolling step-by-step log — plain lines, not styling-polished yet (first pass). */
.build-status-log {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 90px;
  overflow-y: auto;
}
.build-step {
  display: flex;
  align-items: baseline;
  gap: 6px;
}
.build-step__icon {
  flex-shrink: 0;
  width: 10px;
  text-align: center;
  color: var(--ds-text-muted);
}
.build-step[data-status="active"] .build-step__icon {
  color: var(--ds-accent);
}
.build-step[data-status="done"] {
  color: var(--ds-text-muted);
}
.build-step[data-status="error"] .build-step__icon {
  color: var(--ds-danger-text);
}
.build-status-footer {
  display: flex;
  justify-content: space-between;
  padding-top: 4px;
  border-top: 1px solid var(--ds-nav-border);
}
.build-status-cancel.icon-btn {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
}
.build-stage-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--border-strong);
  flex-shrink: 0;
}
.build-stage-dot.done {
  background: var(--ds-text-muted);
}
.build-stage-dot.active {
  background: var(--ds-accent);
  animation: build-stage-pulse 1s ease-in-out infinite;
}
@keyframes build-stage-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.45; transform: scale(1.35); }
}
/* This pulse is STATUS, not decoration — it is the only thing saying "this stage is running
   right now" — so reduced motion gets a substitute rather than `animation: none` alone, which
   would leave the active dot identical in size to the two it has to be told apart from. Frozen
   at the keyframe's own peak scale: still colour-distinct from pending (--ds-border-strong) and
   done (--ds-text-muted), and now size-distinct too, with nothing moving. WCAG 2.2.2 wants the motion
   stoppable; it does not want the information deleted with it. */
@media (prefers-reduced-motion: reduce) {
  .build-stage-dot.active {
    animation: none;
    transform: scale(1.35);
  }
}
.build-status-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.build-status-dismiss {
  background: transparent;
  border: none;
  color: inherit;
  font-size: var(--ds-font-lg);
  line-height: 1;
  padding: 0 2px;
  cursor: pointer;
  flex-shrink: 0;
}
/* Clean success — green, auto-fades (see buildStatus.js AUTO_HIDE_MS). */
.build-status.success {
  background: var(--ds-success-bg);
  border-color: var(--ds-success-border);
  color: var(--ds-success-text);
}
.build-status.success .build-stage-dot.done,
.build-status.success .build-stage-dot.active {
  background: var(--ds-success-text);
  animation: none;
}
/* Success, but truncated or with warnings — amber, not green; stays until dismissed. */
.build-status.success-warn {
  background: var(--ds-warning-bg);
  border-color: var(--ds-warning-border);
  color: var(--ds-warning-text);
}
.build-status.success-warn .build-stage-dot.done {
  background: var(--ds-warning-text);
}
/* Informational terminal state (e.g. "full-stack detected but not buildable yet") —
   neutral, matching the muted .msg.system tone, not styled as an error. */
.build-status.info {
  background: var(--ds-bg-card);
  border-color: var(--ds-border-strong);
  color: var(--ds-text-secondary);
}
/* Build failed — red, matching .msg.error's palette; stays until dismissed. */
.build-status.error {
  background: var(--ds-danger-bg);
  border-color: var(--ds-danger-border);
  color: var(--ds-danger-text);
}
.build-status.error .build-stage-dot.done {
  background: var(--ds-danger-text);
}
/* Stream dropped before a terminal event arrived — the build may well have completed
   server-side, so this is deliberately NOT styled as a hard failure. */
.build-status.lost {
  background: var(--ds-warning-bg);
  border-color: var(--ds-warning-text);
  color: var(--ds-warning-text);
}

/* ---- Right side: resizable / collapsible panels ---- */
.preview-panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
}
.panel {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}
/* Top bar: view toggles on the left, device-size toggle truly centered (equal 1fr
   columns on either side keep it centered regardless of how wide those groups are),
   global actions on the right. */
.top-bar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  border-bottom: 1px solid var(--ds-nav-border);
  background: var(--ds-nav-bg);
  flex-shrink: 0;
}
/* Edit now sits INSIDE this group, between Preview and Code, so the wrapper that briefly held the
   two side by side is gone and .view-toggle is the grid item again. */
.top-bar .view-toggle {
  justify-self: start;
  min-width: 0;
}
.top-bar .device-toggle {
  justify-self: center;
}
.top-bar .toolbar-actions {
  justify-self: end;
}
.view-toggle {
  display: flex;
  gap: 4px;
  background: var(--ds-nav-bg);
  border: 1px solid var(--ds-nav-border);
  border-radius: 8px;
  padding: 3px;
}
.view-btn {
  background: transparent;
  color: var(--ds-text-secondary);
  border: none;
  border-radius: 6px;
  padding: 6px 10px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
}
/* THE TABS CARRY VISIBLE LABELS, not just `title`. Four unlabelled glyphs (eye / <> / page /
 * upload) are four guesses — the page icon opens Notes, which nobody works out from the icon, and
 * one of the four is the only way to reach a whole view. `title` is hover-only, and hover does not
 * exist on touch, which is the same ruling the project-actions grid already applies
 * (public/index.html's icon-grid comment). Labels beat tooltips for the same reason here.
 *
 * Hidden below 640px rather than wrapped: the tab strip shares the top bar with the device toggle
 * and the toolbar actions, and four labels there would push those off. `title`/`aria-label` remain
 * on every button, so the narrow case is no worse than it is today and the wide case is better. */
.view-btn-label {
  font-size: var(--ds-font-sm);
  line-height: 1;
  white-space: nowrap;
}
@media (max-width: 640px) {
  .view-btn-label { display: none; }
  .view-btn { padding: 6px 12px; }
}
.view-btn:hover {
  background: var(--ds-bg-card);
  color: var(--ds-text-primary);
}
.view-btn.active,
/* panelButtons.js drives the Edit toggle with `.is-active` (the class every panel button in this
   shell uses), while the view tablist uses `.active`. Both get the same treatment rather than
   renaming one: the panel-button class is derived in ONE place for five buttons, and changing it
   for this button alone would put a second convention in that map. */
.view-btn.is-active {
  background: var(--ds-accent);
  color: var(--ds-bg-page);
}
.icon {
  width: 16px;
  height: 16px;
  display: block;
  stroke: currentColor;
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Tabbed views: only the active one is shown. */
.view {
  display: none;
  flex: 1;
  min-width: 0;
  min-height: 0;
}
.view.active {
  display: flex;
  flex-direction: column;
}
#pane-notes {
  flex: 1;
}

/* Preview sub-toolbar: page picker (static multi-page projects only — hidden
   entirely for full-stack, see updatePreviewSurface). */
.preview-subbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 12px;
  border-bottom: 1px solid var(--ds-nav-border);
  background: var(--ds-bg-page);
  flex-shrink: 0;
}
.device-toggle {
  display: flex;
  gap: 4px;
  background: var(--ds-nav-bg);
  border: 1px solid var(--ds-nav-border);
  border-radius: 8px;
  padding: 3px;
}
.device-btn {
  background: transparent;
  color: var(--ds-text-secondary);
  border: none;
  border-radius: 6px;
  padding: 5px 9px;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
}
.device-btn:hover {
  background: var(--ds-bg-card);
  color: var(--ds-text-primary);
}
.device-btn.active {
  background: var(--ds-accent);
  color: var(--ds-bg-page);
}
.panel-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  border-bottom: 1px solid var(--ds-nav-border);
  font-size: var(--ds-font-md);
  background: var(--ds-nav-bg);
  flex-shrink: 0;
}
.panel-title {
  font-weight: 500;
  white-space: nowrap;
}
.toolbar-left {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
#pageSelect {
  max-width: 200px;
}
.toolbar-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
  justify-content: flex-end;
}
/* Icon-only buttons (see .icon-btn) — same pattern as the sidebar's icon buttons. */
.min-btn {
  background: var(--ds-nav-active-bg);
  font-size: var(--ds-font-md);
  line-height: 1;
  padding: 4px 9px;
  font-weight: 700;
}
.min-btn:hover {
  background: var(--ds-nav-active-bg);
}
.panel-body {
  flex: 1;
  min-height: 0;
  min-width: 0;
  display: flex;
  overflow: hidden;
}
.preview-body {
  flex: 1;
  min-height: 0;
  display: flex;
  justify-content: center;
  align-items: stretch;
  overflow: auto;
  background: var(--ds-bg-page);
  position: relative; /* anchor for the floating visual-editing toolbar */
}
/* Device-size wrapper: full-width desktop, or a centered framed viewport. */
.device-frame {
  display: flex;
  width: 100%;
  background: white;
}
.device-frame.dev-tablet,
.device-frame.dev-mobile {
  margin: 16px;
  max-width: 100%;
  border: 1px solid var(--ds-border-strong);
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 0 12px 44px rgba(0, 0, 0, 0.5);
}
.device-frame.dev-tablet {
  width: 768px;
}
.device-frame.dev-mobile {
  width: 375px;
}
#previewFrame {
  flex: 1;
  width: 100%;
  border: none;
  background: white;
}
.preview-idle {
  flex: 1;
  display: flex;
  /* COLUMN, because this panel now holds a sentence, a button and a footnote rather than one
     line. The button is here rather than only in the toolbar: the sentence asks someone to start
     the preview, and the control it means has to be the thing directly under it. */
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 24px;
  color: var(--ds-text-secondary);
  text-align: center;
  line-height: 1.6;
}
.preview-idle strong {
  color: var(--ds-text-primary);
}
.preview-idle-start {
  /* ds-btn supplies the shape and the colour; this only says how big it is in this one place,
     where it is the single thing being asked for. */
  padding: 8px 20px;
  font-size: var(--ds-font-sm);
}
.preview-idle-note {
  font-size: var(--ds-font-xs);
  max-width: 44ch;
  /* --ds-text-muted, not a made-up --ds-text-tertiary: the token guard caught the invention, and a
     var() fallback would have hidden it behind something that renders correctly. */
  color: var(--ds-text-muted);
}
/* Notes tab: the checklist (when there's anything to show) stacks above the permanent
   reference content below, instead of the row layout .panel-body uses everywhere else. */
#pane-notes .panel-body {
  flex-direction: column;
  overflow-y: auto;
}
/* NOTHING IN THIS COLUMN MAY SHRINK BELOW ITS CONTENT.
 *
 * Reported from use: the Design & Readability checkboxes overlapped the "npm install" setup
 * instructions below them. The mechanism this targets is the flex default — a flex item is
 * `flex-shrink: 1`, so in a column both the checklist and the note compress when their combined
 * content exceeds the panel, and `.preview-note`'s own `align-items/justify-content: center` then
 * paints its content out of the shrunk box and over the neighbour above it.
 *
 * The parent already scrolls, so shrinking buys nothing here: the honest layout is each child at
 * its content height with the column scrolling past. Written as a child selector rather than two
 * rules so a third section added to this panel (the queued Brief/URL/media ones) inherits it
 * instead of arriving with the same defect. `flex-shrink` only — `.preview-note` keeps its
 * `flex: 1` growth, which is what centres it when the checklist is empty. */
#pane-notes .panel-body > * {
  flex-shrink: 0;
}
.preview-note {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  color: var(--ds-text-secondary);
  overflow: auto;
}
.preview-note .note-inner {
  max-width: 420px;
}
.preview-note ol {
  margin: 8px 0 0;
  padding-left: 18px;
  line-height: 1.7;
}
.preview-note h3 {
  margin: 0 0 8px;
  color: var(--ds-text-primary);
  font-size: var(--ds-font-lg);
}
.preview-note ul {
  margin: 8px 0 0;
  padding-left: 18px;
  line-height: 1.6;
}
.preview-note code {
  background: var(--ds-bg-card);
  padding: 1px 5px;
  border-radius: 4px;
  font-size: var(--ds-font-sm);
}

/* ---- Build checklist (Notes tab, see checklist.js) ------------------------ */
.checklist-container:empty {
  display: none;
}
.checklist {
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
  padding: 20px 24px 0;
  text-align: left;
  flex-shrink: 0;
}

/* The "this project is behind the template" notice (Notes tab). Tokens only, so both colour
 * modes follow the theme rather than this block carrying its own palette.
 * Deliberately NOT the danger palette: being behind is not an error, it is something you can
 * choose to act on, and painting it red would make every older project look broken. (An accepted
 * artifact painted in the danger palette is one of the three defects that motivated the
 * browser-scene harness — the same mistake on a different surface.) */
.checklist-template-notice {
  width: 100%;
  max-width: 560px;
  margin: 20px auto 0;
  padding: 14px 16px;
  text-align: left;
  border: 1px solid var(--ds-border-strong);
  border-radius: var(--ds-radius-md);
  background: var(--ds-bg-card);
  flex-shrink: 0;
}
.checklist-template-headline {
  color: var(--ds-text-primary);
  font-size: var(--ds-font-md);
  font-weight: 600;
  margin-bottom: 6px;
}
.checklist-template-body {
  color: var(--ds-text-muted);
  font-size: var(--ds-font-sm);
  line-height: 1.5;
}
.checklist-template-actions {
  margin-top: 12px;
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.checklist-template-result {
  color: var(--ds-text-primary);
  font-size: var(--ds-font-sm);
  line-height: 1.5;
}
.checklist-template-result:empty {
  display: none;
}
.checklist-template-result p {
  margin: 12px 0 0;
}
/* The file list is the point of this surface, so it SCROLLS rather than truncating — a refresh
 * that names 41 files and shows 5 is the "counts over vibes" rule broken in miniature. */
.checklist-template-files {
  margin: 8px 0 0;
  padding: 8px 10px;
  max-height: 220px;
  overflow-y: auto;
  list-style: none;
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-sm);
  background: var(--ds-bg-inset);
  color: var(--ds-text-muted);
  font-family: var(--ds-font-mono);
  font-size: var(--ds-font-xs);
}
.checklist-template-files li {
  padding: 1px 0;
  word-break: break-all;
}
.checklist-template-error {
  color: var(--ds-danger-text);
}

.checklist-heading {
  margin: 0 0 12px;
  color: var(--ds-text-primary);
  font-size: var(--ds-font-lg);
}
.checklist-category {
  margin-bottom: 16px;
}
.checklist-category-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 6px;
  font-size: var(--ds-font-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ds-text-muted);
}
/* A derived row: computed from live state on every read, no database row behind it, so no
 * checkbox. The dot sits where the checkbox would, keeping the two kinds of row on one text
 * baseline — they read as one list, which is the point, while staying visibly different in the
 * one way that matters (you cannot tick this one off; it ticks itself). */
.checklist-derived-dot {
  width: 8px;
  height: 8px;
  margin: 5px 3px 0;
  border-radius: 50%;
  background: var(--ds-text-muted);
  flex: none;
}
.checklist-item.is-derived {
  border-style: dashed;
}
.checklist-item {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px;
  background: var(--ds-bg-card);
  border: 1px solid var(--ds-nav-border);
  border-radius: 8px;
  margin-bottom: 6px;
}
.checklist-item.is-done {
  opacity: 0.6;
}
.checklist-item.is-done .checklist-item-title {
  text-decoration: line-through;
}
.checklist-checkbox {
  margin-top: 2px;
  flex-shrink: 0;
  cursor: pointer;
}
.checklist-item-body {
  flex: 1;
  min-width: 0;
}
.checklist-item-title {
  font-size: var(--ds-font-md);
  color: var(--ds-text-primary);
  line-height: 1.4;
}
.checklist-item-desc {
  font-size: var(--ds-font-sm);
  color: var(--ds-text-secondary);
  margin-top: 2px;
  line-height: 1.4;
}
.checklist-item-ref {
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
  margin-top: 4px;
  font-family: var(--ds-font-mono);
}
.checklist-dismiss {
  background: transparent;
  border: none;
  color: var(--ds-text-muted);
  font-size: var(--ds-font-lg);
  line-height: 1;
  padding: 2px 4px;
  cursor: pointer;
  flex-shrink: 0;
}
.checklist-dismiss:hover {
  color: var(--ds-danger-text);
}

/* ---- The dev-server startup surface (2026-08-28) -------------------------------------------
   Replaces the raw scrolling npm log as the thing a tenant meets while their site starts. The
   log itself is unchanged and still here, one disclosure away — it is the only diagnostic when
   a run fails, so it is never removed, only stopped from being the front door. */
.run-progress {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 28px 24px;
  overflow: auto;
  background: var(--ds-bg-page);
}
.run-progress-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}
.run-progress-spinner {
  flex: none;
  width: 16px;
  height: 16px;
  margin-top: 2px;
  border: 2px solid var(--ds-border);
  border-top-color: var(--ds-accent);
  border-radius: 50%;
  animation: run-progress-spin 0.8s linear infinite;
}
/* An error is not in progress, and a spinner that keeps turning over a failure is a lie the
   eye reads before the words. run.js sets this state. */
.run-progress[data-phase='error'] .run-progress-spinner {
  animation: none;
  border-color: var(--ds-danger-text, var(--ds-text-secondary));
}
@keyframes run-progress-spin {
  to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .run-progress-spinner { animation: none; }
  .run-progress-bar span { animation: none; width: 100%; opacity: 0.4; }
}
.run-progress-phase {
  margin: 0;
  font-size: var(--ds-font-md);
  color: var(--ds-text-primary);
}
.run-progress-detail {
  margin: 4px 0 0;
  font-size: var(--ds-font-sm);
  color: var(--ds-text-secondary);
}
.run-progress-detail:empty { display: none; }
.run-progress-bar {
  height: 3px;
  border-radius: 3px;
  background: var(--ds-border);
  overflow: hidden;
}
/* INDETERMINATE ON PURPOSE. npm install and vite boot report no percentage anybody could read,
   and a fake percentage that stalls at 80% is worse than a bar that only says "working". */
.run-progress-bar span {
  display: block;
  width: 35%;
  height: 100%;
  background: var(--ds-accent);
  animation: run-progress-slide 1.4s ease-in-out infinite;
}
.run-progress[data-phase='error'] .run-progress-bar span { animation: none; width: 100%; background: var(--ds-danger-text, var(--ds-border)); }
@keyframes run-progress-slide {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(340%); }
}
.run-log-disclosure {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.run-log-disclosure > summary {
  cursor: pointer;
  font-size: var(--ds-font-sm);
  color: var(--ds-text-secondary);
  padding: 4px 0;
}
.run-log-disclosure[open] > summary { margin-bottom: 6px; }
.run-log-disclosure .run-log {
  border: 1px solid var(--ds-border);
  border-radius: 6px;
}

.run-log {
  flex: 1;
  margin: 0;
  padding: 12px 16px;
  overflow: auto;
  background: var(--ds-bg-page);
  color: var(--ds-text-secondary);
  font-family: var(--ds-font-mono);
  font-size: var(--ds-font-sm);
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}

/* Lower row: file tree | code, side by side */
.code-cols {
  flex: 1;
  display: flex;
  flex-direction: row;
  min-height: 0;
  min-width: 0;
}
#pane-filetree {
  flex: var(--tree-grow, 24) 1 0;
}
#pane-editor {
  flex: var(--editor-grow, 76) 1 0;
}
.file-tree {
  flex: 1;
  overflow: auto;
  padding: 8px 0;
  background: var(--ds-bg-page);
}
.tree-folder {
  font-size: var(--ds-font-sm);
  color: var(--ds-text-secondary);
  padding: 4px 8px;
  white-space: nowrap;
}
.tree-file {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  border-radius: 0;
  color: var(--ds-text-primary);
  font-size: var(--ds-font-sm);
  padding: 4px 8px;
  cursor: pointer;
  white-space: nowrap;
}
.tree-file:hover {
  background: var(--ds-bg-card);
}
.tree-file.active {
  background: var(--ds-nav-active-bg);
  color: var(--ds-text-primary);
}
.tree-empty {
  font-size: var(--ds-font-sm);
  color: var(--ds-text-muted);
  padding: 8px 12px;
  margin: 0;
}
.editor {
  flex: 1;
  min-width: 0;
  margin: 0;
  padding: 16px;
  overflow: auto;
  background: #0b0d12;
  color: #c9d1d9;
  font-size: var(--ds-font-sm);
  white-space: pre-wrap;
  word-break: break-word;
}

/* Prism token colours for the read-only Code panel (public/codeHighlight.js). Hand-written
   against this panel's existing palette (bg #0b0d12 / fg #c9d1d9) rather than importing one of
   prismjs's own themes: a vendored theme would ship its own background, font stack and padding
   and then have to be fought back out again to match the panel it sits in. Only the token
   classes the six registered grammars actually emit are styled; anything unstyled simply
   inherits the panel's foreground, which is the pre-highlighting appearance and always legible. */
.editor .token.comment,
.editor .token.prolog,
.editor .token.doctype,
.editor .token.cdata {
  color: #6a737d;
  font-style: italic;
}
.editor .token.punctuation {
  color: #8b949e;
}
.editor .token.keyword,
.editor .token.rule,
.editor .token.important,
.editor .token.atrule {
  color: #ff7b72;
}
.editor .token.string,
.editor .token.char,
.editor .token.attr-value,
.editor .token.regex {
  color: #a5d6ff;
}
.editor .token.number,
.editor .token.boolean,
.editor .token.constant,
.editor .token.symbol {
  color: #79c0ff;
}
.editor .token.function,
.editor .token.class-name,
.editor .token.builtin {
  color: #d2a8ff;
}
.editor .token.tag,
.editor .token.selector {
  color: #7ee787;
}
.editor .token.attr-name,
.editor .token.property {
  color: #ffa657;
}
.editor .token.operator,
.editor .token.entity,
.editor .token.url {
  color: #a5d6ff;
}
.editor .token.deleted {
  color: #ffa198;
}
.editor .token.inserted {
  color: #7ee787;
}
.editor .token.namespace {
  opacity: 0.75;
}

/* Find-in-file matches (public/codeSearchMatch.js's markHtml). A match crossing Prism token
   spans is emitted as SEVERAL <mark> elements, so these rules must look continuous when two sit
   side by side — hence a flat background and no border/radius/padding, which would otherwise
   draw visible seams through the middle of one logical match. `color: inherit` keeps the
   token's own syntax colour underneath the highlight instead of flattening it. */
.editor mark.code-match {
  background: #3b3d1a;
  color: inherit;
  border-radius: 0;
  padding: 0;
}
.editor mark.code-match.is-active {
  background: #9e6a03;
  color: #fff5d6;
}

.code-search {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-left: auto;
}
.code-search-input {
  width: 130px;
  background: var(--ds-bg-inset);
  border: 1px solid var(--ds-border-strong);
  border-radius: 5px;
  color: var(--ds-text-primary);
  font-size: var(--ds-font-xs);
  padding: 2px 6px;
}
/* A border-colour shift alone was the whole focus state here. It keeps its border shift (the
   input sits on a busy code surface, where the colour change is the legible part) and gains
   the design system's ring so the indicator meets WCAG 2.4.11 rather than relying on a
   1px hue change. --ds-accent is now a real token, so the fallback is gone. */
.code-search-input:focus-visible {
  outline: 2px solid var(--ds-focus-ring);
  outline-offset: 0;
  border-color: var(--ds-accent);
}
.code-search-count {
  font-size: var(--ds-font-glyph-sm);
  color: var(--ds-text-muted);
  min-width: 46px;
  text-align: right;
  white-space: nowrap;
}
.code-search-nav {
  background: transparent;
  border: 1px solid var(--ds-border-strong);
  border-radius: 5px;
  color: var(--ds-text-secondary);
  cursor: pointer;
  font-size: var(--ds-font-glyph-sm);
  line-height: 1;
  padding: 3px 5px;
}
.code-search-nav:hover:not(:disabled) {
  color: var(--ds-text-primary);
}
.code-search-nav:disabled {
  opacity: 0.4;
  cursor: default;
}

/* Drag handles */
.resizer {
  flex-shrink: 0;
  background: var(--ds-bg-page);
  transition: background 0.12s ease;
}
.resizer-h {
  width: 6px;
  cursor: col-resize;
}
.resizer:hover,
.resizer.dragging {
  background: var(--ds-accent);
}

/* Minimized panels collapse to a labeled strip; siblings grow to fill. */
.panel.minimized {
  flex: 0 0 auto;
}
.panel.minimized .panel-body {
  display: none;
}
.panel.minimized .panel-head {
  cursor: pointer;
}
/* File tree / code minimized: a narrow vertical strip with a rotated label. */
.subpanel.minimized {
  flex: 0 0 38px;
}
.subpanel.minimized .panel-head {
  flex-direction: column;
  align-items: center;
  gap: 10px;
  height: 100%;
  padding: 10px 4px;
  border-bottom: none;
  border-right: 1px solid var(--ds-nav-border);
}
.subpanel.minimized .panel-title {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
}

/* ---- Login / signup screen ---- */
/* min-height, not height — same plain bug as base.css's old `body { overflow: hidden }`, which
   used to mask it: a fixed 100vh screen clips its own card on any viewport shorter than the
   card (a phone in landscape, a short desktop window), and now that body scrolls there is
   something to scroll TO. dvh so the mobile browser's chrome is accounted for. */
.auth-screen {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.auth-card {
  width: 320px;
  max-width: 100%;
  background: var(--ds-nav-bg);
  border: 1px solid var(--ds-nav-border);
  border-radius: 12px;
  padding: 28px 24px;
  display: flex;
  flex-direction: column;
}
.auth-card h1 {
  font-size: var(--ds-font-title);
  margin: 0 0 4px;
}
.auth-card h1.auth-brand {
  display: flex;
  flex-direction: column;
  line-height: 1.2;
}
.auth-brand-letter {
  color: var(--ds-accent);
}
.auth-sub {
  font-size: var(--ds-font-md);
  color: var(--ds-text-muted);
  margin: 0 0 18px;
}
.auth-label {
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
  margin-bottom: 4px;
}
.auth-card input {
  background: var(--ds-bg-inset);
  color: var(--ds-text-primary);
  border: 1px solid var(--ds-border-strong);
  border-radius: 8px;
  padding: 9px 10px;
  font-size: var(--ds-font-md);
  font-family: inherit;
  margin-bottom: 14px;
}
/* The login wall — the first control every user of this product ever touches, and it had no
   visible focus indicator, only a border shift to the builder's untokenised indigo. Ring from
   the design system; border shift moved onto --ds-accent, the system's one interactive colour
   (DESIGN.md, The One Signal Rule). The literal var(--ds-accent) it replaced is the same legacy indigo
   the bare `button` rule further down this file still uses. */
.auth-card input:focus-visible {
  outline: 2px solid var(--ds-focus-ring);
  outline-offset: 0;
  border-color: var(--ds-accent);
}
.auth-card #authSubmit {
  margin-top: 2px;
}
.auth-error {
  color: var(--ds-danger-text);
  background: var(--ds-danger-bg);
  border-radius: 6px;
  font-size: var(--ds-font-sm);
  padding: 8px 10px;
  margin: 14px 0 0;
}
.auth-toggle {
  font-size: var(--ds-font-sm);
  color: var(--ds-text-muted);
  margin: 16px 0 0;
  text-align: center;
}
.link-btn {
  background: none;
  border: none;
  color: var(--ds-accent);
  cursor: pointer;
  font-size: var(--ds-font-sm);
  padding: 0 0 0 4px;
  text-decoration: underline;
}
/* ---- Visual-editing toolbar + composer chips ------------------------------ */
/* Floating mini toolbar, bottom-center over the live preview. */
.ve-toolbar {
  position: absolute;
  bottom: 16px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px;
  background: var(--ds-nav-bg);
  border: 1px solid var(--ds-border-strong);
  border-radius: 10px;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
  z-index: var(--ds-z-float-control);
}
.ve-toolbar button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  background: var(--ds-nav-active-bg);
  color: var(--ds-text-primary);
  border: none;
  border-radius: 6px;
  padding: 0;
  cursor: pointer;
}
.ve-toolbar button:hover {
  background: var(--ds-nav-active-bg);
}
.ve-toolbar .ve-tool.is-unavailable {
  /* Over a past version (public/previewWriteGate.js). Still clickable on purpose: the click says why. */
  opacity: 0.4;
}
.ve-toolbar .ve-tool.active {
  background: var(--ds-accent);
  color: var(--ds-bg-page);
}
.ve-toolbar .ve-send {
  background: var(--ds-accent);
  color: var(--ds-bg-page);
}
.ve-toolbar .ve-send:hover {
  background: var(--ds-accent-hover);
}
.ve-sep {
  width: 1px;
  align-self: stretch;
  background: var(--ds-border-strong);
  margin: 2px 2px;
}

/* Text mode's Update / Clear bar (public/inlineTextEditor.js). In the BOS frame, just above the
   toolbar, because Update is the only thing that writes and the preview must not be able to draw or
   click it (AUDIT.md AUD-123). Shows the proposed text, so what Update writes is visible first. */
.ve-draft-bar {
  position: absolute;
  bottom: 64px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: calc(100% - 32px);
  padding: 6px 6px 6px 12px;
  background: var(--ds-nav-bg);
  border: 1px solid var(--ds-border-strong);
  border-radius: var(--ds-radius-md);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
  color: var(--ds-text-secondary);
  font-size: var(--ds-font-sm);
  z-index: var(--ds-z-float-control);
}
.ve-draft-bar .ve-draft-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ve-draft-bar .ve-draft-label [data-vc="proposed"] {
  color: var(--ds-text-primary);
}
.ve-draft-bar button {
  flex: none;
}
.ve-draft-bar .ve-draft-hint {
  flex: none;
  color: var(--ds-accent);
  font-weight: 600;
  white-space: nowrap;
}

/* Comment composer / note (public/commentPins.js) — BOS-frame popovers at the clicked point. */
.ve-note-popover {
  position: fixed;
  width: 240px;
  padding: 10px;
  background: var(--ds-nav-bg);
  color: var(--ds-text-primary);
  border: 1px solid var(--ds-border-strong);
  border-radius: var(--ds-radius-md);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
  z-index: var(--ds-z-popover);
}
.ve-note-popover .ve-note-title {
  font-weight: 600;
  margin-bottom: 6px;
}
.ve-note-popover .ve-note-meta {
  color: var(--ds-text-muted);
  font-size: var(--ds-font-sm);
  margin-bottom: 4px;
}
.ve-note-popover .ve-note-text {
  margin-bottom: 8px;
  white-space: pre-wrap;
}
.ve-note-popover textarea {
  width: 100%;
  box-sizing: border-box;
  resize: vertical;
}
.ve-note-popover .ve-note-actions {
  display: flex;
  gap: 6px;
  justify-content: flex-end;
  margin-top: 6px;
}

/* Attached-context chips shown above the prompt textarea. */
.vc-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 6px;
}
.vc-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--ds-bg-inset);
  border: 1px solid var(--ds-border-strong);
  color: var(--ds-text-primary);
  border-radius: 999px;
  padding: 3px 8px;
  font-size: var(--ds-font-sm);
}
.vc-chip-x {
  background: transparent;
  border: none;
  color: var(--ds-text-muted);
  cursor: pointer;
  font-size: var(--ds-font-md);
  line-height: 1;
  padding: 0;
}
.vc-chip-x:hover {
  color: var(--ds-danger-text);
}

/* ---- Prompt Attachments (chat-panel image attachments) --------------------------- */
.attachments-preview {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 6px;
}
.attachment-thumb {
  position: relative;
  width: 48px;
  height: 48px;
  border-radius: 6px;
  overflow: hidden;
  border: 1px solid var(--ds-border-strong);
  background: var(--ds-bg-inset);
  cursor: zoom-in;
}
.attachment-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.attachment-thumb-x {
  position: absolute;
  top: 1px;
  right: 1px;
  width: 16px;
  height: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.65);
  color: var(--ds-text-on-fill);
  border: none;
  border-radius: 50%;
  font-size: var(--ds-font-xs);
  line-height: 1;
  cursor: pointer;
  padding: 0;
}
.attachment-thumb-x:hover {
  background: var(--ds-danger-text);
}
.attachment-drop-zone {
  position: relative;
  width: 100%;
}
/* A bare <textarea> sizes itself from its `cols` attribute, not its container, so without
   this it renders at roughly half the sidebar's width instead of filling it — the visible
   margin then comes entirely from .chat-panel's own 14px padding, not the textarea itself. */
#promptInput {
  width: 100%;
  box-sizing: border-box;
}
.attachment-drop-overlay {
  display: none;
  position: absolute;
  inset: 0;
  align-items: center;
  justify-content: center;
  background: rgba(74, 108, 247, 0.15);
  border: 2px dashed var(--ds-accent);
  border-radius: 8px;
  color: var(--ds-text-primary);
  font-size: var(--ds-font-sm);
  pointer-events: none;
}
.attachment-drop-zone.dragover .attachment-drop-overlay {
  display: flex;
}
.attachment-error {
  font-size: var(--ds-font-xs);
  color: var(--ds-danger-text);
  margin: -2px 0 2px;
}

/* Lightbox for a clicked attachment thumbnail — thumbnails/message images both use it. */
.attachment-lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--ds-z-modal-lite);
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.8);
  cursor: zoom-out;
}
.attachment-lightbox img {
  max-width: 90vw;
  max-height: 90vh;
  border-radius: 8px;
}

/* Sent-message thumbnails in the chat log (see messages.js). */
.msg-attachments {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 6px;
}
.msg-attachments .attachment-thumb {
  cursor: zoom-in;
}

.prompt-form-actions {
  display: flex;
  align-items: center;
  gap: 6px;
}
.prompt-form-actions #sendBtn {
  margin-left: auto;
}

/* ---- Voice input (recording / transcribing status) -------------------------------- */
.voice-status {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--ds-font-sm);
  color: var(--ds-text-secondary);
  background: var(--ds-bg-card);
  border: 1px solid var(--ds-nav-border);
  border-radius: 6px;
  padding: 6px 10px;
}
.voice-status.recording {
  color: var(--ds-danger-text);
}
.voice-status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--ds-danger-text);
  animation: voice-pulse 1.2s ease-in-out infinite;
}
@keyframes voice-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}
/* Stopping this one loses nothing, unlike the build-stage dot above: the recording state is
   already carried without motion by .voice-status.recording, which tints the accompanying
   label. So the dot simply rests solid at full opacity. Written out rather than left to a
   blanket rule so the difference between the two cases is on the record. */
@media (prefers-reduced-motion: reduce) {
  .voice-status-dot {
    animation: none;
    opacity: 1;
  }
}
.voice-level {
  flex: 1;
  height: 3px;
  border-radius: 2px;
  background: var(--ds-border-strong);
  overflow: hidden;
  max-width: 80px;
}
.voice-level-bar {
  height: 100%;
  width: 0%;
  background: var(--ds-danger-text);
  transition: width 80ms linear;
}

/* ---- Brand Kits (management panel + project-bar selector + new-project popover) --- */
.brandkit-manage-btn {
  background: var(--ds-nav-active-bg);
  font-size: var(--ds-font-xs);
  padding: 4px 10px;
  white-space: nowrap;
}

.brandkit-item {
  background: var(--ds-bg-card);
  border: 1px solid var(--ds-nav-border);
  border-radius: 8px;
  padding: 10px;
  margin-bottom: 8px;
}
.brandkit-item-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.brandkit-item-name {
  font-size: var(--ds-font-md);
  font-weight: 500;
}
.brandkit-swatches {
  display: flex;
  gap: 4px;
  margin: 6px 0;
}
.brandkit-swatch {
  width: 18px;
  height: 18px;
  border-radius: 4px;
  border: 1px solid rgba(255, 255, 255, 0.15);
}
.brandkit-item-actions {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}
.brandkit-item-actions button {
  background: var(--ds-nav-active-bg);
  font-size: var(--ds-font-xs);
  padding: 4px 8px;
}
.brandkit-item-actions button.danger {
  background: var(--ds-danger-bg);
}

/* Kit detail/edit view */
.brandkit-detail-back {
  background: var(--ds-nav-active-bg);
  font-size: var(--ds-font-sm);
  padding: 4px 10px;
  margin-bottom: 10px;
}
.brandkit-field {
  margin-bottom: 14px;
}
.brandkit-field label {
  display: block;
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
  margin-bottom: 4px;
}
.brandkit-field input[type="text"] {
  width: 100%;
  background: var(--ds-bg-inset);
  color: var(--ds-text-primary);
  border: 1px solid var(--ds-border-strong);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: var(--ds-font-md);
}
.contrast-badges {
  display: inline-flex;
  gap: 4px;
  flex-wrap: wrap;
}
.contrast-badge {
  display: inline-block;
  min-width: 34px;
  text-align: center;
  font-size: var(--ds-font-glyph-sm);
  font-weight: 700;
  padding: 2px 5px;
  border-radius: 4px;
  background: var(--ds-danger-bg);
  color: var(--ds-danger-text);
}
.contrast-badge.AA,
.contrast-badge.AAA {
  background: var(--ds-success-bg);
  color: var(--ds-success-text);
}
.brandkit-font-row {
  margin-bottom: 10px;
}
.brandkit-font-row .role-label {
  font-size: var(--ds-font-sm);
  color: var(--ds-text-muted);
  margin-bottom: 4px;
  display: block;
}
.brandkit-assets-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-bottom: 10px;
}
.brandkit-asset-tile {
  background: var(--ds-bg-card);
  border: 1px solid var(--ds-nav-border);
  border-radius: 8px;
  padding: 8px;
  text-align: center;
}
.brandkit-asset-tile img {
  max-width: 100%;
  max-height: 60px;
  margin-bottom: 6px;
}
.brandkit-asset-tile .label {
  font-size: var(--ds-font-glyph-sm);
  color: var(--ds-text-muted);
  margin-bottom: 6px;
}
.brandkit-asset-tile .actions {
  display: flex;
  gap: 4px;
  justify-content: center;
}
.brandkit-asset-tile button {
  background: var(--ds-nav-active-bg);
  font-size: var(--ds-font-glyph-sm);
  padding: 3px 6px;
}
.brandkit-history-list {
  font-size: var(--ds-font-sm);
  color: var(--ds-text-secondary);
}
.brandkit-history-entry {
  padding: 6px 0;
  border-bottom: 1px solid var(--ds-nav-border);
}
.brandkit-history-entry:last-child {
  border-bottom: none;
}

/* New Project popover (floating, anchored near the New button) */
.new-project-popover {
  position: fixed;
  z-index: var(--ds-z-popover);
  width: 300px;
  background: var(--ds-nav-bg);
  border: 1px solid var(--ds-border-strong);
  border-radius: 10px;
  padding: 14px;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
  font-size: var(--ds-font-md);
}
.new-project-popover h3 {
  margin: 0 0 10px;
  font-size: var(--ds-font-lg);
}
.new-project-popover input[type="text"] {
  width: 100%;
  box-sizing: border-box;
  background: var(--ds-bg-inset);
  color: var(--ds-text-primary);
  border: 1px solid var(--ds-border-strong);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: var(--ds-font-md);
  margin-bottom: 10px;
}
.new-project-popover .brandkit-option {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
  font-size: var(--ds-font-sm);
}
.new-project-popover .np-section-label {
  margin: 12px 0 6px;
  padding-top: 10px;
  border-top: 1px solid var(--ds-nav-border);
  font-size: var(--ds-font-xs);
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--ds-text-muted);
}
.new-project-popover .brandkit-option.is-disabled {
  color: var(--ds-text-muted);
  cursor: default;
}
.new-project-popover .np-coming-soon {
  margin-left: auto;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--ds-nav-border);
  color: var(--ds-text-muted);
  font-size: var(--ds-font-glyph-sm);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.new-project-popover select {
  width: 100%;
  margin: 4px 0 10px;
}
.new-project-popover .actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  margin-top: 8px;
}
.new-project-popover .actions button.cancel {
  background: var(--ds-nav-active-bg);
}

/* Notifications Bell popover (Business OS — Notifications Bell + Settings, V1) — same
   anchored-popover mechanics as .new-project-popover above (see public/notifications.js),
   styled with the --ds-* Design System tokens (see :root) since this is new UI. */
.notifications-popover {
  position: fixed;
  z-index: var(--ds-z-popover);
  width: 360px;
  max-height: 70vh;
  display: flex;
  flex-direction: column;
  background: var(--ds-bg-card);
  border: 1px solid var(--ds-border-strong);
  border-radius: var(--ds-radius-lg);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
  font-size: var(--ds-font-md);
  color: var(--ds-text-primary);
  overflow: hidden;
}
.notifications-popover-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ds-space-2);
  padding: var(--ds-space-3) var(--ds-space-4);
  border-bottom: 1px solid var(--ds-border);
  flex-shrink: 0;
}
.notifications-popover-header h3 {
  margin: 0;
  font-size: var(--ds-font-lg);
}
.notifications-popover-body {
  overflow-y: auto;
  padding: var(--ds-space-2) var(--ds-space-4);
}
.notifications-popover-footer {
  padding: var(--ds-space-3) var(--ds-space-4);
  border-top: 1px solid var(--ds-border);
  text-align: center;
  flex-shrink: 0;
}
.notifications-popover-footer a {
  color: var(--ds-info-text);
  text-decoration: none;
  font-size: var(--ds-font-sm);
}
.notifications-popover-footer a:hover {
  text-decoration: underline;
}
.notif-group {
  padding: var(--ds-space-2) 0;
}
.notif-group + .notif-group {
  border-top: 1px solid var(--ds-border);
}
.notif-group-title {
  font-size: var(--ds-font-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ds-text-muted);
  margin-bottom: var(--ds-space-1);
}
.notif-row {
  cursor: pointer;
}
.notif-row:hover {
  background: var(--ds-bg-inset);
}
.ds-source-area-icon {
  flex-shrink: 0;
  color: var(--ds-text-secondary);
}

/* ---- Content (CMS) panel: reuses .brandkit-item/.brandkit-field/.brandkit-detail-back
   for the type list + field-editor form (see content.js), plus its own badge styling below.
   This is the OLD, now-read-only builder-panel (public/content.js) — the editable field-row
   UI lives in public/os/content.js, styled in css/content.css, not here. ---- */
.content-type-fields-summary {
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
  margin: 4px 0 8px;
}
.content-field-type-tag {
  display: inline-block;
  font-size: var(--ds-font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--ds-text-muted);
  background: var(--ds-bg-inset);
  border-radius: 3px;
  padding: 1px 4px;
  margin-right: 6px;
}

/* CMS Round 3a — this panel became read-only; the link out to the canonical, writable OS
   surface (public/os/content.js). Persistent across every view in this panel (see
   content.js's manageLinkEl). */
.content-manage-link-wrap {
  margin-bottom: 10px;
}
.content-manage-link {
  background: none;
  border: none;
  color: var(--ds-accent);
  font-size: var(--ds-font-sm);
  cursor: pointer;
  padding: 0;
  text-decoration: underline;
}
.content-manage-link:hover {
  color: var(--ds-accent-hover);
}

.content-entries-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}
.content-entry-item .brandkit-item-head {
  justify-content: flex-start;
  gap: 8px;
}
.content-entry-updated {
  font-size: var(--ds-font-glyph-sm);
  color: var(--ds-text-muted);
}
.content-entry-preview {
  font-size: var(--ds-font-sm);
  color: var(--ds-text-secondary);
  margin: 6px 0 8px;
  word-break: break-word;
}

/* Draft/Published badge — same visual language as .contrast-badge (fail vs AA/AAA). */
.content-badge {
  display: inline-block;
  font-size: var(--ds-font-glyph-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 2px 6px;
  border-radius: 4px;
}
.content-badge.draft {
  background: var(--ds-warning-bg);
  color: var(--ds-warning-text);
}
.content-badge.published {
  background: var(--ds-success-bg);
  color: var(--ds-success-text);
}

/* Generic right-side drawer — a fixed panel over the whole app, since the sidebar itself
   is too narrow for a multi-field form (unlike the popover's small, anchored-near-a-
   button footprint). Shared by the Content entry editor (content.js) and Publish
   (publish.js) — kept generic on purpose rather than per-feature-prefixed. */
.drawer-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  z-index: var(--ds-z-drawer);
}
.drawer-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 360px;
  max-width: 90vw;
  background: var(--ds-bg-card);
  border-left: 1px solid var(--ds-border-strong);
  box-shadow: -8px 0 30px rgba(0, 0, 0, 0.5);
  z-index: var(--ds-z-drawer-top);
  display: flex;
  flex-direction: column;
}
/* Drawer Containment — designSystem.js's openDrawer(opts.container) modifier: a drawer
   opened from WITHIN another floating panel (the messages overlay's own drawers — New
   conversation/Create channel/AI summary/Forward/Pinned messages/Create task from message/
   Conversation settings) docks INSIDE that panel's own bounds instead of the true viewport
   edge. Requires the container to be `position: relative` + `overflow: hidden` (already
   true of .messages-overlay-panel) so `position: absolute` here is actually clipped to it —
   every other rule above (colors/borders/width/z-index) is unchanged, only the positioning
   scheme flips from viewport-relative to container-relative. */
.drawer-backdrop.contained,
.drawer-panel.contained {
  position: absolute;
}
/* Drawer Width — designSystem.js's openDrawer(opts.wide). The 360px default is exactly the
   width of .chat-panel (the sidebar), so a dense control surface that moved OUT of the sidebar
   into a drawer would otherwise gain no room at all. max-width keeps the same 90vw ceiling the
   base rule sets, so this can never crowd out the preview on a narrow viewport. */
.drawer-panel-wide {
  width: 460px;
}
/* THE EDIT DRAWER, and only it — see public/veDrawer.js's applyVeDrawerWidth for why this is not
   a change to .drawer-panel-wide above. Declared after it so it wins regardless of class order. */
.drawer-panel.ve-drawer-width {
  width: 400px;
}
/* Drawer Modality — designSystem.js's openDrawer(opts.modal === false). The backdrop element
   still exists (the drawer stack indexes on it), but is transparent and click-through, so the
   page behind stays fully interactive. Built for the visual editor's element-edit drawer,
   which stays open while the user keeps clicking elements on the preview canvas behind it —
   a normal modal backdrop would swallow exactly those clicks. Every other drawer is modal. */
.drawer-backdrop-nonmodal {
  background: transparent;
  pointer-events: none;
}
/* Drawer Stacking — designSystem.js's openDrawer(opts.stack): a picker drawer
   (openBusinessMediaPicker/openPartyPicker) opened from WITHIN an already-open drawer stacks
   on top of it instead of replacing it. The layer below gets this modifier: shifted further
   from the edge so a sliver stays visible, dimmed, and inert — so the caller sees the context
   they're picking for (e.g. the product they're adding an image to) without being able to
   interact with it until the drawer on top closes. */
.drawer-panel-stacked-back {
  right: 28px;
  opacity: 0.55;
  pointer-events: none;
}
.drawer-backdrop-stacked-back {
  pointer-events: none;
}
.drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 14px;
  border-bottom: 1px solid var(--ds-border);
  font-size: var(--ds-font-md);
  font-weight: 500;
}
/* The title inside .drawer-head is an <h2> as of the heading fix (designSystem.js's openDrawer
   — see the note there). These four declarations exist ONLY to neutralise the UA h2 defaults so
   the promotion is semantics-only and the drawer looks byte-identical: the UA margin would
   otherwise push the close button out of line, and the UA size would double it. Same pattern
   and same reason as components.css's .ds-section-title, which carries `margin: 0` for exactly
   this. */
.ds-drawer-title {
  margin: 0;
  font-size: inherit;
  font-weight: inherit;
  min-width: 0;
}
/* Measured at 9.3x16px — a bare glyph with `padding: 0`, and the smallest target in the app
   (WCAG 2.5.8 wants 24x24). It matters more than its size suggests: this is the dismiss on
   every drawer opened from the POS register, and POS order-taking is the one admin surface
   PRODUCT.md declares phone-supported, so it is hit with a thumb. A real box rather than the
   ::before overlay used elsewhere in components.css — the element is free to grow here, and
   an overlay is only worth its complexity when the painted size is load-bearing. */
.drawer-close {
  background: transparent;
  border: none;
  color: var(--ds-text-secondary);
  cursor: pointer;
  font-size: var(--ds-font-lg);
  line-height: 1;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  min-height: 24px;
  flex-shrink: 0;
}
.drawer-body {
  flex: 1;
  overflow-y: auto;
  padding: 14px;
  color: var(--ds-text-primary);
}
.drawer-body textarea {
  width: 100%;
  box-sizing: border-box;
  background: var(--ds-bg-inset);
  color: var(--ds-text-primary);
  border: 1px solid var(--ds-border-strong);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: var(--ds-font-md);
  font-family: inherit;
  resize: vertical;
}
.drawer-body input[type="text"],
.drawer-body input[type="url"],
.drawer-body input[type="password"] {
  width: 100%;
  box-sizing: border-box;
  background: var(--ds-bg-inset);
  color: var(--ds-text-primary);
  border: 1px solid var(--ds-border-strong);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: var(--ds-font-md);
}
.drawer-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 14px;
  border-top: 1px solid var(--ds-border);
}

/* Feedback Button (V1) — the one local toast in the main document (see public/feedback.js's
   own header for why every OTHER toast in this codebase is a small local one-off rather than
   a shared component). Bottom-right, success-tinted, uses the same design tokens the rest of
   the app already themes with, so it tracks light/dark automatically. */
.feedback-toast {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: var(--ds-z-toast);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border-radius: 8px;
  background: var(--ds-success-bg);
  color: var(--ds-success-text);
  font-size: var(--ds-font-md);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}

.tasks-feedback-filter {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 8px 0;
  font-size: var(--ds-font-md);
  color: var(--ds-text-secondary);
}

.feedback-toast-link {
  background: none;
  border: none;
  color: inherit;
  text-decoration: underline;
  cursor: pointer;
  font: inherit;
  padding: 0;
}

.content-media-current {
  margin-bottom: 6px;
}
.content-media-thumb {
  width: 52px;
  height: 52px;
  object-fit: cover;
  border-radius: 4px;
  display: block;
}
.content-media-thumb-pick {
  cursor: pointer;
  border: 2px solid transparent;
}
.content-media-thumb-pick:hover {
  border-color: var(--ds-accent);
}
.content-media-grid {
  display: grid;
  grid-template-columns: repeat(4, 52px);
  gap: 6px;
  max-height: 180px;
  overflow-y: auto;
  margin-top: 8px;
}

/* ---- Content Round 2: read-only block-document preview (contentBlocksPreview.js) — no
   editing controls, see that file's header. Reuses the surrounding drawer's own type styles
   for headings/paragraphs/etc.; these three classes are only for the non-content notes
   (component/unresolved-reference/unhandled) that have no natural HTML element of their own. */
.content-blocks-preview {
  max-height: 320px;
  overflow-y: auto;
  border: 1px solid var(--ds-border-strong);
  border-radius: 6px;
  padding: 10px 12px;
  font-size: var(--ds-font-md);
  color: var(--ds-text-secondary);
}
.content-blocks-preview img.content-media-thumb {
  width: auto;
  max-width: 100%;
  height: auto;
  max-height: 160px;
}
.content-block-note {
  font-size: var(--ds-font-xs);
  font-style: italic;
  color: var(--ds-text-muted);
  border-left: 2px solid var(--ds-border-strong);
  padding-left: 8px;
  margin: 6px 0;
}
.content-block-unresolved {
  color: var(--ds-warning-text);
  border-left-color: var(--ds-warning-text);
}
.content-block-unhandled {
  color: var(--ds-danger-text);
  border-left-color: var(--ds-danger-text);
}

/* ---- Publish (publish.js): deploy status badge — distinct color mapping from the
   content-entry Draft/Published badge (pending/building/failed all have real meaning
   here, not just a binary state). ---- */
.deploy-status-badge {
  display: inline-block;
  font-size: var(--ds-font-glyph-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 2px 6px;
  border-radius: 4px;
}
.deploy-status-badge.live {
  background: var(--ds-success-bg);
  color: var(--ds-success-text);
}
.deploy-status-badge.pending,
.deploy-status-badge.building {
  background: var(--ds-warning-bg);
  color: var(--ds-warning-text);
}
.deploy-status-badge.failed {
  background: var(--ds-danger-bg);
  color: var(--ds-danger-text);
}


/* ---- The site planner (public/sitePlanner.js) — the PRE-BUILD state of the preview surface,
   a sibling of .device-frame inside .preview-body. That parent is `display:flex; overflow:auto`
   and centres its children, so this claims the row with flex:1 and does its own scrolling: the
   pane is the widest space in the product and the whole point of the move was to stop a
   questionnaire scrolling in a 300px column.

   Design-system tokens only, both colour modes by construction (no literal colours here). ---- */
.preview-planner {
  flex: 1;
  min-width: 0;
  overflow-y: auto;
  padding: var(--ds-space-6) var(--ds-space-5);
  background: var(--ds-bg-page);
}
/* Centred and capped: a form whose inputs run the full width of a 1600px monitor is harder to
   read, not easier. The cap is generous enough for the two-column split below to survive. */
.planner-shell {
  max-width: 1080px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-5);
}

.planner-head {
  border-bottom: 1px solid var(--ds-border);
  padding-bottom: var(--ds-space-4);
}
.planner-title {
  margin: 0;
  font-size: var(--ds-font-2xl);
  color: var(--ds-text-primary);
}
.planner-lede {
  margin: var(--ds-space-2) 0 0;
  max-width: 68ch;
  font-size: var(--ds-font-md);
  line-height: 1.55;
  color: var(--ds-text-secondary);
}

/* The questions lead; what BOS already knows sits beside them rather than above, so it reads as
   context rather than as something to get through first. Collapses to one column on a narrow
   pane — the builder's sidebar can be wide, and this pane is what is left. */
.planner-columns {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: var(--ds-space-5);
  align-items: start;
}
@media (max-width: 1100px) {
  .planner-columns { grid-template-columns: minmax(0, 1fr); }
}
.planner-main {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-5);
  min-width: 0;
}
.planner-aside {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-4);
  position: sticky;
  top: 0;
}

.planner-block {
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-lg);
  background: var(--ds-bg-card);
  padding: var(--ds-space-5);
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-5);
}
.planner-block-head,
.planner-aside-head {
  margin: 0;
  font-size: var(--ds-font-sm);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ds-text-muted);
}
/* A heading followed immediately by its help line should not carry the block's full gap. */
.planner-block-head + .planner-help { margin-top: calc(-1 * var(--ds-space-4)); }

.planner-field {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-2);
  min-width: 0;
}
/* Two questions that are one decision (primary action, then the fallback) sit side by side. */
.planner-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--ds-space-5);
}
.planner-label {
  font-size: var(--ds-font-md);
  font-weight: 600;
  color: var(--ds-text-primary);
}
.planner-help {
  margin: 0;
  max-width: 72ch;
  font-size: var(--ds-font-sm);
  line-height: 1.5;
  color: var(--ds-text-muted);
}
.planner-note {
  margin: var(--ds-space-2) 0 0;
  font-size: var(--ds-font-xs);
  line-height: 1.5;
  color: var(--ds-text-muted);
}
.planner-input {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  font-size: var(--ds-font-md);
  color: var(--ds-text-primary);
  background: var(--ds-bg-page);
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-md);
  padding: var(--ds-space-3);
}
.planner-input:focus-visible {
  outline: 2px solid var(--ds-focus-ring);
  outline-offset: 1px;
}

/* The multi-site model, stated where it can matter. Deliberately an INFO surface and not a
   warning: nothing is wrong, the product is explaining what it believes. */
.planner-model-note {
  margin: var(--ds-space-2) 0 0;
  padding: var(--ds-space-3) var(--ds-space-4);
  border-radius: var(--ds-radius-md);
  background: var(--ds-info-bg);
  color: var(--ds-info-text);
  font-size: var(--ds-font-sm);
  line-height: 1.55;
  max-width: 72ch;
}

/* Pages as tiles, so the whole set AND the fact that ticked each one read at a glance — the
   thing a scrolling checkbox list in a sidebar could not do. */
.planner-pages {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--ds-space-3);
}
.planner-page {
  display: flex;
  align-items: flex-start;
  gap: var(--ds-space-3);
  padding: var(--ds-space-3);
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-md);
  background: var(--ds-bg-page);
  cursor: pointer;
}
.planner-page:hover { border-color: var(--ds-border-strong); }
/* Unticked reads as OFF rather than as missing: the reason it was offered stays legible, because
   a tenant who unticks Shop should still be able to see why it was suggested. */
.planner-page.is-off { opacity: 0.55; }
.planner-page-main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.planner-page-label {
  font-size: var(--ds-font-md);
  color: var(--ds-text-primary);
}
.planner-page-why {
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
  line-height: 1.4;
}
.planner-page-new {
  font-size: var(--ds-font-xs);
  color: var(--ds-info-text);
}
.planner-page-add,
.planner-ref-add {
  display: flex;
  gap: var(--ds-space-2);
  align-items: center;
}
.planner-page-add .planner-input,
.planner-ref-add .planner-input { flex: 1; }
.planner-ref-add select.planner-input { flex: 0 0 auto; width: auto; }

.planner-refs {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-2);
}
.planner-ref {
  display: flex;
  align-items: center;
  gap: var(--ds-space-3);
  padding: var(--ds-space-2) var(--ds-space-3);
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-md);
  background: var(--ds-bg-page);
  font-size: var(--ds-font-sm);
}
.planner-ref-kind {
  color: var(--ds-text-muted);
  font-size: var(--ds-font-xs);
  white-space: nowrap;
}
.planner-ref-url {
  flex: 1;
  min-width: 0;
  color: var(--ds-text-primary);
  overflow-wrap: anywhere;
}

/* The aside panels: quieter than the question blocks (inset rather than card) so the eye lands
   on what is being ASKED, not on what is already answered. */
.planner-known,
.planner-gaps {
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-lg);
  background: var(--ds-bg-inset);
  padding: var(--ds-space-4);
}
.planner-facts {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--ds-space-3);
  margin: var(--ds-space-3) 0 0;
  font-size: var(--ds-font-sm);
}
.planner-facts dt {
  color: var(--ds-text-muted);
  font-size: var(--ds-font-xs);
}
.planner-facts dd {
  margin: 0;
  color: var(--ds-text-primary);
  overflow-wrap: anywhere;
}
/* The linked Brand Kit's colours, beside its name. A strip rather than a grid: three chips at
 * text size, aria-hidden because the kit's NAME is the accessible fact and the swatches repeat
 * nothing a screen reader can use. Tokened border so the chips stay visible against a near-white
 * kit colour in both modes. */
.planner-swatches {
  display: inline-flex;
  gap: 4px;
  margin-left: var(--ds-space-2);
  vertical-align: middle;
}
.planner-swatch {
  width: 12px;
  height: 12px;
  border-radius: 3px;
  border: 1px solid var(--ds-border);
}
.planner-gap-list {
  list-style: none;
  margin: var(--ds-space-3) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-2);
  font-size: var(--ds-font-sm);
}
.planner-gap-list li {
  display: flex;
  justify-content: space-between;
  gap: var(--ds-space-2);
}
.planner-gap-label { color: var(--ds-text-secondary); }
.planner-gap-where {
  color: var(--ds-text-muted);
  font-size: var(--ds-font-xs);
  white-space: nowrap;
}

.planner-footer {
  display: flex;
  align-items: center;
  gap: var(--ds-space-3);
  flex-wrap: wrap;
  border-top: 1px solid var(--ds-border);
  padding-top: var(--ds-space-4);
}
.planner-status {
  flex: 1;
  min-width: 240px;
  margin: 0;
  font-size: var(--ds-font-sm);
  color: var(--ds-text-muted);
}

/* ---- The direction stage (public/siteDirections.js) — three boards in the same pre-build surface
   the planner uses.

   TWO COLOUR WORLDS, AND THE BOUNDARY BETWEEN THEM IS THE POINT. Everything outside
   `.direction-frame` is BOS chrome on --ds-* tokens; everything inside is painted with the
   DIRECTION's own six theme.css variables, set as inline custom properties by the renderer. That is
   what makes a board a preview of the palette rather than an illustration of it — and it is the
   same scoping decision docs/BUSINESS_OS.md records for chart SVG palettes (AUD-24): a foreign
   palette lives inside its frame and never touches --ds-*. ---- */
.directions-shell { max-width: 1400px; }

.direction-boards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--ds-space-5);
  align-items: start;
}
.direction-board {
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-lg);
  background: var(--ds-bg-card);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.direction-board.is-chosen {
  border-color: var(--ds-accent);
  box-shadow: 0 0 0 1px var(--ds-accent);
}
.direction-head { padding: var(--ds-space-4); }
.direction-name {
  margin: 0;
  font-size: var(--ds-font-lg);
  color: var(--ds-text-primary);
}
.direction-thesis {
  margin: var(--ds-space-2) 0 0;
  font-size: var(--ds-font-sm);
  line-height: 1.5;
  color: var(--ds-text-secondary);
}
.direction-refuses {
  margin: var(--ds-space-3) 0 0;
  display: flex;
  gap: var(--ds-space-2);
  font-size: var(--ds-font-xs);
  line-height: 1.5;
  color: var(--ds-text-muted);
}
.direction-refuses-label {
  flex: 0 0 auto;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: 600;
}

/* ---- Inside the frame: the direction's world ---- */
.direction-frame {
  background: var(--brand-surface);
  color: var(--brand-surface-ink);
  font-family: var(--board-body-font);
  padding: var(--ds-space-4);
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-4);
  border-top: 1px solid var(--ds-border);
  border-bottom: 1px solid var(--ds-border);
}
.board-hero { display: grid; gap: var(--ds-space-3); }
.board-hero-words { display: flex; flex-direction: column; gap: var(--ds-space-2); align-items: flex-start; }
.board-display {
  margin: 0;
  font-family: var(--board-heading-font);
  /* The display step's own size comes from the specimen below; the hero uses a bounded clamp so
     three boards' heroes stay comparable even when one direction's display step is enormous. */
  font-size: clamp(1.25rem, 3.2vw, 1.9rem);
  line-height: 1.1;
  color: var(--brand-surface-ink);
}
.board-sub { margin: 0; font-size: var(--ds-font-sm); opacity: 0.8; }
.board-btn {
  display: inline-block;
  background: var(--brand);
  color: var(--brand-ink);
  padding: var(--board-button-padding);
  border-radius: var(--board-radius);
  border: var(--board-border) solid var(--brand);
  box-shadow: var(--board-shadow);
  font-family: var(--board-heading-font);
  font-size: var(--ds-font-sm);
}
.board-btn-accent {
  background: var(--brand-accent);
  color: var(--brand-accent-ink);
  border-color: var(--brand-accent);
}
/* Arrangement variants — the one thing that makes two boards structurally different at a glance. */
.board-hero-stacked { text-align: center; }
.board-hero-stacked .board-hero-words { align-items: center; }
.board-hero-split { grid-template-columns: 1fr 1fr; align-items: center; }
.board-hero-bleed { position: relative; }
.board-hero-bleed .board-hero-image { position: absolute; inset: 0; opacity: 0.25; }
.board-hero-bleed .board-hero-words { position: relative; padding: var(--ds-space-4); }
.board-hero-type .board-display { font-size: clamp(1.6rem, 4.4vw, 2.6rem); }
.board-hero-panel .board-hero-words {
  background: var(--brand);
  color: var(--brand-ink);
  padding: var(--ds-space-4);
  border-radius: var(--board-radius);
}
.board-hero-panel .board-display { color: var(--brand-ink); }
.board-hero-panel .board-btn { background: var(--brand-accent); color: var(--brand-accent-ink); border-color: var(--brand-accent); }
.board-hero-image {
  background: var(--brand-accent);
  border-radius: var(--board-radius);
  min-height: 90px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.board-hero-image-note {
  font-size: var(--ds-font-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--brand-accent-ink);
  opacity: 0.7;
}

.board-components { display: flex; flex-direction: column; gap: var(--ds-space-3); }
.board-card {
  background: var(--brand-surface);
  color: var(--brand-surface-ink);
  border: var(--board-border) solid var(--brand);
  border-radius: var(--board-radius);
  box-shadow: var(--board-shadow);
  padding: var(--ds-space-3);
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-1);
}
.board-card-title { margin: 0; font-family: var(--board-heading-font); font-size: var(--ds-font-md); }
.board-card-body { margin: 0; font-size: var(--ds-font-sm); line-height: 1.5; opacity: 0.85; }
/* The pair lib/directionSpec.ts checks that no theme pair covers: brand as TEXT on the ground. */
.board-link { color: var(--brand); font-size: var(--ds-font-sm); text-decoration: underline; }
.board-controls { display: flex; gap: var(--ds-space-2); align-items: center; }
.board-input {
  flex: 1;
  border: var(--board-border) solid var(--brand);
  border-radius: var(--board-radius);
  padding: var(--ds-space-2) var(--ds-space-3);
  font-size: var(--ds-font-sm);
  opacity: 0.6;
}

.board-specimen { display: flex; flex-direction: column; gap: var(--ds-space-2); }
.board-specimen-row { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.board-specimen-text { overflow-wrap: anywhere; }
.board-specimen-label {
  font-size: var(--ds-font-glyph-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  opacity: 0.5;
  font-family: var(--ds-font-mono);
}

/* ---- Back outside the frame ---- */
.direction-pairs { display: flex; gap: var(--ds-space-2); padding: var(--ds-space-4) var(--ds-space-4) 0; }
.direction-pair {
  flex: 1;
  text-align: center;
  padding: var(--ds-space-2);
  border-radius: var(--ds-radius-sm);
  font-size: var(--ds-font-xs);
  border: 1px solid var(--ds-border);
}
.direction-facts {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--ds-space-1) var(--ds-space-3);
  margin: 0;
  padding: var(--ds-space-4);
  font-size: var(--ds-font-xs);
}
.direction-facts dt { color: var(--ds-text-muted); }
.direction-facts dd { margin: 0; color: var(--ds-text-primary); font-family: var(--ds-font-mono); overflow-wrap: anywhere; }
.direction-rules {
  padding: 0 var(--ds-space-4) var(--ds-space-4);
  font-size: var(--ds-font-xs);
  line-height: 1.5;
  color: var(--ds-text-muted);
}
.direction-rules p { margin: 0 0 var(--ds-space-2); }
.direction-unknown-arrangement { color: var(--ds-warning-text); }
.direction-foot {
  margin-top: auto;
  padding: var(--ds-space-4);
  border-top: 1px solid var(--ds-border);
}

/* A round that failed its own gate. WARNING, never danger: nothing is broken and the tenant did
   nothing wrong — the checks did their job, which is the opposite of an error. */
.direction-refusals {
  border: 1px solid var(--ds-warning-border);
  background: var(--ds-warning-bg);
  color: var(--ds-warning-text);
  border-radius: var(--ds-radius-lg);
  padding: var(--ds-space-4);
}
.direction-refusals h4 { margin: 0 0 var(--ds-space-2); font-size: var(--ds-font-md); }
.direction-refusal-list {
  margin: var(--ds-space-2) 0 0;
  padding-left: var(--ds-space-5);
  font-size: var(--ds-font-sm);
  line-height: 1.6;
}

/* The MOVES a failed round offers. Buttons, not list items: each one does something, and a thing
   that looks like prose but responds to a click is a thing nobody clicks. */
.direction-moves {
  display: grid;
  gap: var(--ds-space-2);
  margin: var(--ds-space-3) 0 var(--ds-space-2);
}
.direction-move {
  display: grid;
  gap: 2px;
  text-align: left;
  padding: var(--ds-space-3);
  border: 1px solid var(--ds-warning-border);
  border-radius: var(--ds-radius-md);
  background: var(--ds-bg-card);
  color: inherit;
  cursor: pointer;
  font: inherit;
}
.direction-move:hover,
.direction-move:focus-visible { border-color: var(--ds-focus-ring); }
.direction-move strong { font-size: var(--ds-font-sm); }
.direction-refusal-detail { margin-top: var(--ds-space-3); font-size: var(--ds-font-sm); }
.direction-refusal-detail summary { cursor: pointer; }

/* A round that offered SOME boards. Informational, never warning: two directions a tenant can
   choose between is a result, not a failure — the warning palette would say otherwise louder than
   any words. */
.direction-shortfall {
  border: 1px solid var(--ds-border);
  background: var(--ds-bg-inset);
  border-radius: var(--ds-radius-lg);
  padding: var(--ds-space-3) var(--ds-space-4);
  margin: var(--ds-space-3) 0;
  font-size: var(--ds-font-sm);
}
.direction-shortfall strong { display: block; margin-bottom: var(--ds-space-2); }

/* ---- The live progress line -------------------------------------------------------------------
   Replaces a muted "Working…" on a step that takes ~20s and spends real money. It sits in the
   normal text colour on purpose: a progress signal in muted grey is the thing a tenant squints at
   to decide whether the page has died. */
.direction-progress {
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-lg);
  padding: var(--ds-space-3) var(--ds-space-4);
  margin: var(--ds-space-3) 0;
  background: var(--ds-bg-inset);
}
.direction-progress-line { margin: 0; display: flex; align-items: baseline; gap: var(--ds-space-2); flex-wrap: wrap; }
.direction-progress-timer { font-variant-numeric: tabular-nums; color: var(--ds-text-muted); }

/* ---- A board BOS adjusted ---------------------------------------------------------------------
   Sits above the palette facts and inside the card's own chrome, never inside the painted frame:
   this is BOS talking about the direction, not part of it. The two chips ARE the tenant's colours
   and are set inline from the adjustment, which is why nothing here sets a background on them. */
.direction-adjusted {
  border: 1px solid var(--ds-warning-border);
  background: var(--ds-warning-bg);
  color: var(--ds-warning-text);
  border-radius: var(--ds-radius-md);
  padding: var(--ds-space-3);
  margin-top: var(--ds-space-3);
  font-size: var(--ds-font-sm);
}
.direction-adjusted-swatches { display: flex; align-items: center; gap: var(--ds-space-2); margin: var(--ds-space-2) 0; flex-wrap: wrap; }
.direction-adjusted-chip {
  padding: 4px 10px;
  border-radius: var(--ds-radius-sm);
  border: 1px solid var(--ds-border);
  font-family: var(--ds-font-mono);
  font-size: var(--ds-font-xs);
  /* NO COLOUR HERE, DELIBERATELY. The chip's background is a TENANT colour of unknown lightness, so
     its text has to be derived from that colour rather than fixed — public/siteDirectionBoard.js's
     boardAdjustments() runs the same deriveInk() the palette itself uses and the renderer sets it
     inline. The first version hardcoded #111 with a white text-shadow, which is a guess that fails
     on exactly the dark grounds this stage exists to make possible. */
}
.direction-adjusted-arrow { color: var(--ds-text-muted); }
.direction-adjusted p { margin: 0; }

/* ---- The exact-colours control -----------------------------------------------------------------
   Hidden until asked for. A tenant who wanted to pick hexes would not have asked for three
   directions, so this is never the first offer — see public/siteDirectionMoves.js. */
.direction-exact {
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-lg);
  padding: var(--ds-space-4);
  margin-top: var(--ds-space-3);
  display: grid;
  gap: var(--ds-space-3);
  justify-items: start;
}
.direction-exact-fields { display: flex; gap: var(--ds-space-4); flex-wrap: wrap; }
.direction-exact-field { display: grid; gap: 4px; font-size: var(--ds-font-sm); }
.direction-exact-label { display: flex; align-items: center; gap: 6px; }
/* A colour input for a role nobody is pinning does nothing, and looks like it does nothing. */
.direction-exact-input:disabled { opacity: 0.4; cursor: not-allowed; }
.direction-exact-input { width: 64px; height: 36px; padding: 2px; border: 1px solid var(--ds-border); border-radius: var(--ds-radius-sm); background: none; }

/* ---- Brand Kit palette editor (the pairs, the cross-check, the support rows) ------------------
   The three ground/ink cards and the fourth contract pair. Layout only: every colour painted here
   is a TENANT's colour arriving inline from public/paletteEditorModel.js, never a design token, so
   nothing in this block may set a background or a foreground on .palette-sample. */
.palette-pairs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 10px;
  margin: 8px 0 12px;
}
.palette-pair,
.palette-cross {
  border: 1px solid var(--ds-border-strong);
  border-radius: 8px;
  padding: 10px;
  background: var(--ds-bg-inset);
}
.palette-cross {
  margin-bottom: 12px;
}
/* The sample is the whole point of a pair card: the ink, at readable size, ON its ground. Its two
   colours are set inline from the live palette. */
.palette-sample {
  border-radius: 6px;
  padding: 14px 12px;
  font-size: var(--ds-font-md);
  font-weight: 600;
  text-align: center;
  border: 1px solid var(--ds-border-strong);
}
.palette-pair-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 8px 0;
}
.palette-where {
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
}
.palette-short {
  font-size: var(--ds-font-xs);
  color: var(--ds-danger-text);
  font-weight: 600;
}
.palette-cross-hint {
  margin: 0;
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
}
.palette-support-heading {
  margin: 14px 0 2px;
  font-size: var(--ds-font-sm);
  color: var(--ds-text-primary);
}
.palette-support {
  display: grid;
  /* 330px, not 240: a support row carries a swatch, a label, a hex, a provenance chip, a "follows
     X" phrase and up to two buttons. At 240 it packed four columns and every row wrapped into
     three lines with its label broken mid-phrase — measured by looking at it, which is the only
     way this kind of defect shows up. */
  grid-template-columns: repeat(auto-fit, minmax(330px, 1fr));
  gap: 2px 16px;
  margin-top: 6px;
}
.palette-support .palette-slot {
  border-top: 1px solid var(--ds-border-strong);
}
.palette-slot {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 6px 0;
}
.palette-slot input[type="color"] {
  width: 32px;
  height: 32px;
  border: 1px solid var(--ds-border-strong);
  border-radius: 6px;
  background: none;
  padding: 0;
  cursor: pointer;
  flex-shrink: 0;
}
/* A derived swatch is not editable in place — Set records it first. Dimmed rather than hidden, so
   the tenant can see the colour the site is actually using. */
.palette-slot input[type="color"]:disabled {
  cursor: default;
  opacity: 0.75;
}
.palette-slot-body {
  display: flex;
  align-items: baseline;
  gap: 6px;
  flex-wrap: wrap;
  min-width: 0;
  flex: 1;
}
.palette-slot-label {
  font-size: var(--ds-font-sm);
  color: var(--ds-text-primary);
  /* A role's name is a term, not a sentence: "Muted text" breaking across two lines above its own
     swatch reads as two rows. */
  white-space: nowrap;
}
.palette-slot-hex {
  font-family: var(--ds-font-mono);
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
  white-space: nowrap;
}
.palette-prov,
.palette-follows {
  white-space: nowrap;
}
/* Provenance. THREE-VALUED and visually distinct, because "why did that change?" is the support
   conversation this exists to prevent: Recorded is pinned, Derived moves with the palette, Default
   is a value BOS picked because nothing in the palette implies one. */
.palette-prov {
  font-size: var(--ds-font-glyph-sm);
  font-weight: 700;
  letter-spacing: 0.02em;
  padding: 1px 6px;
  border-radius: 999px;
  border: 1px solid var(--ds-border-strong);
  color: var(--ds-text-muted);
}
.palette-prov-kit {
  background: var(--ds-nav-active-bg);
  color: var(--ds-text-primary);
}
.palette-prov-default {
  border-style: dashed;
}
.palette-follows {
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
  font-style: italic;
}
.palette-slot-actions {
  display: flex;
  gap: 4px;
  flex-shrink: 0;
}
.palette-slot-actions button {
  background: var(--ds-nav-active-bg);
  font-size: var(--ds-font-xs);
  padding: 3px 8px;
}
.palette-slot-name {
  flex-basis: 100%;
  background: var(--ds-bg-inset);
  color: var(--ds-text-primary);
  border: 1px solid var(--ds-border-strong);
  border-radius: 6px;
  padding: 4px 7px;
  font-size: var(--ds-font-sm);
}

/* ---- The Brief view (0013 §A4, slice 4) -------------------------------------------------------
   A document, so it reads in one column at a comfortable measure rather than filling the canvas.
   Every colour is a design-system token: nothing here paints a tenant's brand. */
#view-brief .panel-body {
  overflow-y: auto;
}
#briefDoc {
  max-width: 780px;
  padding: 4px 2px 24px;
}
.brief-banner {
  border: 1px solid var(--ds-border-strong);
  border-left: 3px solid var(--ds-warning-text, var(--ds-text-primary));
  border-radius: 8px;
  padding: 10px 12px;
  margin-bottom: 16px;
  background: var(--ds-bg-inset);
}
.brief-banner p {
  margin: 6px 0 0;
  font-size: var(--ds-font-sm);
  color: var(--ds-text-muted);
}
.brief-section {
  margin-bottom: 22px;
}
.brief-section h3 {
  margin: 0 0 8px;
  font-size: var(--ds-font-md);
  color: var(--ds-text-primary);
  border-bottom: 1px solid var(--ds-border-strong);
  padding-bottom: 4px;
}
.brief-field {
  margin-bottom: 12px;
}
.brief-field label {
  display: block;
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
  margin-bottom: 3px;
}
.brief-field input[type="text"],
.brief-field select {
  width: 100%;
  background: var(--ds-bg-inset);
  color: var(--ds-text-primary);
  border: 1px solid var(--ds-border-strong);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: var(--ds-font-md);
}
.brief-help {
  margin: 4px 0 0;
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
}
/* A row this panel does not own. Visually quieter than an editable one, on purpose: the difference
   between "you can change this here" and "this lives somewhere else" is the panel's main job. */
.brief-readonly {
  display: grid;
  /* minmax(0, 1fr), NOT 1fr — DEFENSIVE, and it did NOT fix an observed defect. Worth the
     correction because the first version of this comment claimed it did: the clipped "Open X"
     buttons were an artifact of the throwaway screenshot harness, which un-hides #previewPanel
     outside the shell's own layout so the panel has no width constraint. Measured afterwards
     through the browser scene, at a real width and with a long unbreakable URL in the fixture:
     nothing overflows with `1fr` either.
     Kept anyway, because a grid track's implicit minimum IS min-content and this row pairs
     free-form text with a nowrap button — the shape that overflows at a narrower panel than the
     one this renders at today. The scene asserts the property with its own control, so a future
     change that does break it is caught. */
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: 4px 12px;
  padding: 8px 0;
  border-bottom: 1px solid var(--ds-border-strong);
}
.brief-readonly label {
  grid-column: 1;
  margin: 0;
}
.brief-readonly-value,
.brief-readonly-why {
  grid-column: 1;
  margin: 0;
  font-size: var(--ds-font-sm);
  color: var(--ds-text-primary);
  /* A URL with no spaces in it is one long word with no break opportunity. Same posture as the
     column definition above: defensive rather than a fix for something seen. */
  overflow-wrap: anywhere;
}
.brief-readonly-why {
  color: var(--ds-text-muted);
}
/* These read as DISABLED and are not — there is no `disabled` attribute anywhere in
 * briefPanel.js, and every one of them navigates. The cause was a missing `color`: the rule set a
 * tinted background and inherited its text colour from the surrounding muted row, which lands a
 * low-contrast grey on a tinted fill — the exact appearance of a dead control. A control that looks
 * disabled is worse than one that is, because the tenant does not try it and never finds out the
 * area was reachable all along. Colour and cursor stated explicitly so neither is inherited. */
.brief-owner-link {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: center;
  background: var(--ds-nav-active-bg);
  color: var(--ds-text-primary);
  border: 1px solid var(--ds-nav-border);
  border-radius: 6px;
  cursor: pointer;
  font-size: var(--ds-font-xs);
  padding: 4px 8px;
  white-space: nowrap;
}
.brief-owner-link:hover {
  background: var(--ds-bg-card);
  border-color: var(--ds-accent);
}
.brief-unset {
  color: var(--ds-text-muted);
  font-style: italic;
  font-size: var(--ds-font-sm);
}
.brief-pages,
.brief-refs,
.brief-history {
  list-style: none;
  margin: 0;
  padding: 0;
}
.brief-pages li,
.brief-refs li,
.brief-history li {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 5px 0;
  border-bottom: 1px solid var(--ds-border-strong);
  font-size: var(--ds-font-sm);
}
.brief-page-label {
  color: var(--ds-text-primary);
}
/* ASKED vs DERIVED — 175 created the column for this distinction, so it is rendered rather than
   flattened into a list of names. */
.brief-page-source,
.brief-ref-kind {
  font-size: var(--ds-font-glyph-sm);
  font-weight: 700;
  padding: 1px 6px;
  border-radius: 999px;
  border: 1px solid var(--ds-border-strong);
  color: var(--ds-text-muted);
  white-space: nowrap;
}
.brief-page-source[data-source="asked"] {
  background: var(--ds-nav-active-bg);
  color: var(--ds-text-primary);
}
.brief-ref-url {
  font-family: var(--ds-font-mono);
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
  overflow-wrap: anywhere;
}
.brief-history-when {
  font-family: var(--ds-font-mono);
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
  white-space: nowrap;
}
.brief-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: 18px 0;
}
.brief-saved-note {
  font-size: var(--ds-font-xs);
  color: var(--ds-text-muted);
}
