/* Triptych shell: CSS grid layout replacing DaisyUI drawer.

   --sidebar-w and --board-w are the single source of truth for pane width —
   every state below (collapsed, no-sidebar, board open/closed, settings,
   user-dragged, narrow-viewport) sets ONE of these two vars rather than
   restating the whole grid-template-columns (Swarf-AI/webapp#2621). The grid itself is
   always 3 tracks; a 0-width track occupies no visual space, so this is
   behaviorally identical to the old 2-column declarations it replaces. */

/* --sidebar-w is typed on purpose (Swarf-AI/webapp#4511): grid-template-columns is
   transitioned, and --board-w reaches --sidebar-w through a two-hop chain
   (--board-w → --board-max → var(--sidebar-w)). With --sidebar-w
   unregistered — token soup substituted at var()-time — that chain breaks
   the transition's endpoint resolution when the sidebar collapses while
   the board is open: the sidebar track sticks at a bogus non-zero width
   (a stable state, not a mid-animation blip). Registering it as a real
   <length> gives both transition endpoints a resolved value and the
   collapse settles at 0 (proven by live A/B). --board-w / --board-min /
   --board-max are NOT registered with '<length>': their formulas carry
   100%-based calc(), which that syntax can't hold — registering them so
   makes them fall back to initial-value and the board collapses to 0
   (proven live, the failed first attempt at this fix). A
   '<length-percentage>' registration is untried; nothing today transitions
   through the board family alone, so it stays unregistered until a real
   case needs it.

   initial-value duplicates .shell's own --sidebar-w: 256px below —
   @property forbids var() here, so keep the two in agreement. */
@property --sidebar-w {
  syntax: "<length>";
  inherits: true;
  initial-value: 256px;
}

.shell {
  display: grid;
  /* minmax(20rem, 1fr) on the TRACK (not min-width on #shell-content, the
     grid item) is deliberate: #shell-content has overflow:hidden, which
     makes its automatic minimum size 0 per the CSS Grid/Flexbox spec — this
     is what lets a wide markdown table clip/scroll internally instead of
     stretching the container (Swarf-AI/webapp#2621 regression: setting an explicit
     min-width on the item itself defeats that and breaks table overflow). */
  grid-template-columns: var(--sidebar-w) minmax(20rem, 1fr) var(--board-w);
  --sidebar-w: 256px; /* keep in agreement with @property initial-value above */
  /* Pane drag ranges — the restore script (app.html) clamps persisted
     widths with these. The live drag clamp in triptych-resize.js keeps its
     own numeric constants (it computes per-mousemove from the shell rect);
     keep the two in agreement. The board max is sidebar-aware: each bound
     alone is legal, but 40vw + 60vw + the center's 20rem floor jointly
     over-ask, overflowing the grid and pushing the board partly
     off-viewport. The board (the junior pane) yields; the sidebar's bound
     must NOT reference --board-w back, or the pair becomes a custom-
     property cycle and both go invalid. The var(--sidebar-w) reference
     here is also why --sidebar-w must stay @property-registered — see the
     rule above .shell (Swarf-AI/webapp#4511). */
  --sidebar-min: 180px;
  --sidebar-max: 40vw;
  --board-w: 0;
  /* The floors yield to --board-max too: clamp() does not validate
     MIN <= MAX (it resolves max(MIN, min(VAL, MAX))), so a fixed floor
     above the available space would silently over-ask — reintroducing
     the overflow in the 901-960px band between the overlay breakpoint
     and where the fixed floors fit. */
  --board-min: min(320px, var(--board-max));
  --board-max: min(60vw, calc(100% - var(--sidebar-w) - 20rem));
  height: 100vh;
  overflow: hidden;
  transition: grid-template-columns 0.2s ease;
  position: relative;
}

/* Explicit column placement — required because <aside> (the sidebar) is
   entirely absent from the DOM for anonymous/no-sidebar users, not just
   hidden. Implicit auto-placement fills tracks by DOM order among
   *participating* (non-display:none) children only, so with <aside> absent
   and both dividers display:none, #shell-content would become the sole
   participating item and land in column 1 (the 0-width sidebar track)
   instead of column 2 — a real regression caught via the
   test_wide_table_scrolls_horizontally E2E test (Swarf-AI/webapp#2621). */
.triptych-sidebar {
  grid-column: 1;
  anchor-name: --sidebar-pane;
}

#shell-content {
  grid-column: 2;
}

/* Popover base — every popover="auto" element in this codebase is a
   flex/column menu (verified by grep, Swarf-AI/webapp#2781). DaisyUI utility classes
   (e.g. .menu) set `display: flex` unconditionally in author CSS, which
   wins the cascade over the [popover] UA stylesheet's default
   `display: none` for a closed popover — so a closed "menu" popover
   renders open and stays hit-testable unless display is explicitly
   re-gated here. A future popover needing a different display value can
   override this rule at its own (higher-specificity) selector. */
[popover] {
  display: none;
}
[popover]:popover-open {
  display: flex;
  flex-direction: column;
}

.triptych-board {
  grid-column: 3;
  anchor-name: --board-pane;
}

/* Chat position: right (Swarf-AI/webapp#2619). The user preference mirrors the chat and
   board columns — tracks become sidebar | board | chat, and the two panes
   swap grid columns. The board divider moves to the board's RIGHT edge
   (sidebar + board tracks from the left); triptych-resize.js reads the
   body class to flip its drag math to match. Settings (.triptych-settings)
   spans 2/4 either way. */
body.chat-right .shell {
  grid-template-columns: var(--sidebar-w) var(--board-w) minmax(20rem, 1fr);
}

body.chat-right #shell-content {
  grid-column: 3;
  /* Explicit row: the board (later in DOM, earlier column) would otherwise
     wrap to row 2 — grid auto-placement's cursor can't backtrack. */
  grid-row: 1;
}

body.chat-right .triptych-board {
  grid-column: 2;
  grid-row: 1;
}

body.chat-right #board-divider {
  right: auto;
  left: calc(var(--sidebar-w) + var(--board-w));
}

/* Settings replaces both the chat AND board columns (it hides both — see
   the display:none rule below), so it spans columns 2-3, not just 3. */
.triptych-settings {
  grid-column: 2 / 4;
}

/* The paywall host is a shell child only for its scrim's stacking context
   (see its comment in allauth/layouts/app.html), not a pane. Out of grid
   flow like the splitters below: in flow, it auto-placed past whichever
   pane filled the last column into an implicit row 2, and the grid split
   the viewport height between the two rows — Settings' sidebar stopped
   ~200px short (#3553). Absolute with z-index auto adds no stacking
   context, so the fixed scrim still layers against the panels. */
#paywall-host {
  position: absolute;
}

/* Draggable splitters (Swarf-AI/webapp#2621): absolutely positioned at the track boundary
   rather than made a grid item, so they don't need their own column.
   Each anchors to its pane's RENDERED edge via anchor() (same mechanism as
   the composer add-menu, Swarf-AI/webapp#2775) — not to the width token. The two diverge
   whenever sidebar + board + the center column's 20rem floor exceed the
   viewport: the grid resolves tracks smaller than the tokens, and a
   token-positioned divider detaches (floats mid-chat after an over-drag;
   lands off-screen after a window downsize). The var() declaration listed
   first in each rule is the graceful fallback for browsers without anchor
   positioning — they keep the old token-derived position.
   triptych-resize.js drags these, writing the vars. */
.triptych-divider {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 6px;
  z-index: 20;
  cursor: col-resize;
  transform: translateX(-3px);
}

.triptych-divider:hover,
.triptych-divider.is-dragging {
  background: var(--color-primary, currentColor);
  opacity: 0.3;
}

#sidebar-divider {
  left: var(--sidebar-w);
  left: anchor(--sidebar-pane right, var(--sidebar-w));
  display: none;
}

.shell:not(.no-sidebar) #sidebar-divider {
  display: block;
}

#board-divider {
  right: var(--board-w);
  left: anchor(--board-pane left, calc(100% - var(--board-w)));
  display: none;
}

.shell:has(#board-panel #tab-content > *) #board-divider {
  display: block;
}

/* Narrow viewport: both side panes leave the grid — the sidebar auto-collapses
   (Swarf-AI/webapp#2621) and opens as a left drawer (Swarf-AI/webapp#2962), the
   board as a right drawer (Swarf-AI/webapp#5002) — so neither has a track
   boundary to drag anymore. */
@media (max-width: 56.25rem) {
  .triptych-divider {
    display: none !important;
  }
}

.shell.sidebar-collapsed {
  --sidebar-w: 0;
}

.shell.no-sidebar {
  --sidebar-w: 0;
}

/* Sidebar must clip its content when the grid column collapses to 0.
   min-width: 0 lets the grid track shrink the item (overrides auto default).
   max-width + transition animate the visual width; overflow: hidden clips content.
   Both grid-template-columns AND max-width drive the collapse — getBoundingClientRect()
   reports the element's own layout box even in a 0px track, so max-width must
   reach 0 to make the element's box actually become 0px when collapsed.

   max-width derives from --sidebar-w — the same variable that sizes the grid
   track and positions the divider — so the element can never disagree with
   its track (Swarf-AI/webapp#2808: a hardcoded 256px cap here let drags past it grow the
   track but not the sidebar, stranding a persistent dead gutter). Collapse
   still animates for free: .sidebar-collapsed sets --sidebar-w to 0 and this
   max-width follows it. */
.triptych-sidebar {
  overflow: hidden;
  min-width: 0;
  max-width: var(--sidebar-w);
  transition: max-width 0.2s ease;
}

.shell.sidebar-collapsed .triptych-sidebar {
  border-right-width: 0;
}

/* Reopen button lives in the workspace, visible only when sidebar is collapsed */
.sidebar-reopen-btn {
  display: none;
}

.shell.sidebar-collapsed .sidebar-reopen-btn {
  display: flex;
}

/* The reopen button's geometry, tokenized once (Swarf-AI/webapp#4471, Swarf-AI/webapp#4996): the two
   primitives mirror the button's own Tailwind utilities (top-2/left-2 =
   0.5rem inset, w-8/h-8 = 2rem box — templates/allauth/layouts/app.html),
   and everything else derives. --sidebar-reopen-clearance is the
   horizontal footprint plus a breathing-room gap; --sidebar-reopen-band
   is the full vertical band, so a row spanning it that centers its
   content lands on the button's axis, and content placed below it can
   never collide. --shell-page-inline-pad is the shell-standard page
   inline padding — a clearance consumer inside a padded page subtracts
   it so the two paddings don't stack into a void beside the button. */
:root {
  --sidebar-reopen-inset: 0.5rem;
  --sidebar-reopen-box: 2rem;
  --sidebar-reopen-clearance: calc(
    var(--sidebar-reopen-inset) + var(--sidebar-reopen-box) + 0.5rem
  );
  --sidebar-reopen-band: calc(
    2 * var(--sidebar-reopen-inset) + var(--sidebar-reopen-box)
  );
  /* The single source of horizontal padding for every shell-standard page
     — #chat-scroll-container, .workspace-detail-page, and .wslist-page
     (Workspaces list + Conversation History) all read this one value
     rather than each declaring their own (#798). */
  --shell-page-inline-pad: 1rem;
}

/* Clearance is band-scoped (Swarf-AI/webapp#4996, replacing Swarf-AI/webapp#4511's shell-wide padding):
   the button only occupies the top band, so reserving a full-height
   #shell-content column for it cost every page ~8% of a phone viewport
   and left content visibly off-center at any width. Instead, the elements
   that actually share the button's corner band take the clearance from
   the shared token — the trade-off vs Swarf-AI/webapp#4511 is a maintained selector
   list: a future page header that hugs the top-left band must join it
   (docs/FRONTEND.md §Shared UI Primitives has the rule). An element
   inside a padded page subtracts --shell-page-inline-pad so every listed
   header lands at the same x regardless of its page's own padding. The
   settings pane still hosts its own copy of the button over a centered
   layout that nothing collides with. */
.shell.sidebar-collapsed #conversation-header {
  padding-left: var(--sidebar-reopen-clearance);
}

/* Margin, not padding: the breadcrumb is a content row, not full-bleed
   chrome like #conversation-header (whose hairline must span the pane),
   so shifting its box keeps the layout-invariant "button overlaps no
   sibling box" guard meaningful instead of leaving a padded box
   underlapping the floating button. */
.shell.sidebar-collapsed .workspace-detail-breadcrumb {
  margin-left: calc(
    var(--sidebar-reopen-clearance) - var(--shell-page-inline-pad)
  );
}

/* Board panel (Swarf-AI/webapp#2562): triptych 3rd column, opened via the composer pill.
   Extends the sidebar-toggle pattern — a class on .shell drives the grid columns.
   Closed by default (--board-w: 0 from the base rule above, grid stays visually
   two columns even though the 0-width third track still exists in the DOM). */

/* The board column opens when a tab is open — content-presence, no JS class
   toggle (Swarf-AI/webapp#2637). #tab-content is empty until the composer pill or a tab link
   populates it. `.shell` _is_ #sse-wrapper, so we never swap it to toggle layout
   — the content presence inside #board-panel drives the grid (the Swarf-AI/webapp#2614
   settings-surface pattern). Default width; user-draggable width (Swarf-AI/webapp#2621) writes
   this same var via JS, overriding the default until the user resizes again. */
.shell:has(#board-panel #tab-content > *) {
  --board-w: clamp(min(24rem, var(--board-max)), 40vw, min(44rem, var(--board-max)));
}

/* Narrow-viewport auto-collapse (Swarf-AI/webapp#2621). The center track's own 20rem floor
   (see grid-template-columns above) already stops sidebar+board from
   crushing it — this rule additionally auto-collapses the sidebar so a
   fresh narrow load is usable without the user needing to find and drag a
   splitter first. */

/* !important: user-dragged/persisted width (Swarf-AI/webapp#2621) is written as an inline
   style via JS, which outranks any class selector regardless of source
   order. Only `!important` in a stylesheet rule beats a non-important inline
   style, so the narrow-width floor stays authoritative even if the user
   dragged the sidebar wide before the viewport shrank (e.g. window resize,
   not just initial load). Board becomes an overlay instead of a grid track
   so it doesn't compete with chat for the same starved width.

   The sidebar-state script in templates/allauth/layouts/app.html mirrors
   this exact breakpoint via matchMedia, keeping the sidebar-collapsed class
   (and the reopen affordance it gates) in sync with this floor (Swarf-AI/webapp#2954) —
   change one, change both. */
@media (max-width: 56.25rem) {
  .shell {
    --sidebar-w: 0 !important;
    --board-w: 0 !important;
  }

  /* Board drawer (Swarf-AI/webapp#5002, replacing the full-bleed takeover) — same shape as
     the sidebar drawer below: --board-w: 0 above frees the grid track since
     an open board leaves normal flow entirely and renders as a
     right-anchored fixed drawer (position: fixed drops it out of grid flow,
     so --board-w's 0 no longer applies to it either way — unlike the
     sidebar, .triptych-board carries no base max-width rule tied to its
     width variable, so nothing needs freeing here). The scrim is a fixed
     full-viewport ::after painted behind the drawer via z-index: -1 — a
     pseudo-element hit-tests as its originating element, so a scrim tap
     arrives with e.target === .triptych-board itself, which the
     narrow-drawer dismiss handler (templates/allauth/layouts/app.html)
     reads as a dismissal. Below the sidebar drawer's z-index (40): per
     Swarf-AI/webapp#2962's recorded intent, if both are open the sidebar wins.

     #board-panel-content — not .triptych-board — carries the opaque
     background (Swarf-AI/webapp#5003's trap, reproduced live in the mock): a negative-z
     ::after cannot paint below its own ancestor's background, only below
     its own content, so the scrim would wash over whatever .triptych-board
     itself painted. #board-panel-content is the board's single child that
     tiles its full area, so unlike the sidebar's two-child case one rule
     covers it. */
  .shell:has(#board-panel #tab-content > *) .triptych-board {
    position: fixed;
    inset: 0 0 0 auto;
    width: min(24rem, 85vw);
    z-index: 30;
  }

  .shell:has(#board-panel #tab-content > *) #board-panel-content {
    background: var(--color-base-100);
  }

  .shell:has(#board-panel #tab-content > *) .triptych-board::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    background: var(--color-scrim);
  }

  /* Sidebar drawer (Swarf-AI/webapp#2962) — the sidebar's version of the board move above.
     The --sidebar-w floor stays authoritative for the GRID (chat keeps full
     width); an open sidebar leaves the grid entirely and renders as a
     left-anchored fixed drawer. Keyed off the same .sidebar-collapsed class
     the toggle handler already flips, so the reopen click needs no new JS
     state — today's dead end is purely that no rule renders the open state
     down here. max-width must be freed explicitly: the base rule ties it to
     var(--sidebar-w), which the floor pins to 0. The scrim is a fixed
     full-viewport ::after (zero extra DOM, same per-theme token as the
     paywall's scrim), painted behind the drawer via z-index: -1 within its
     stacking context. A pseudo-element — unlike a box-shadow — hit-tests as
     its originating element, so scrim taps are consumed here (they target
     the aside itself, never the content beneath) and the toggle handler's
     narrow clause (templates/allauth/layouts/app.html) reads a tap whose
     target IS the aside as a dismissal. Above the board overlay (z-index
     30): if states ever combine, navigation wins.

     The aside itself carries NO background here (Swarf-AI/webapp#5003) — a negative
     z-index descendant paints above its *own* ancestor's background (only
     below the ancestor's content), never below it, no matter how negative.
     So the scrim always washes over whatever the aside itself paints,
     regardless of z-index value. The opaque background lives on the two
     children that together tile the aside's full area instead (below),
     which paint at a normal (non-negative) stack level and so sit above
     the scrim like any other content. */
  .shell:not(.sidebar-collapsed) .triptych-sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    width: min(20rem, 85vw);
    max-width: none;
    z-index: 40;
  }

  .shell:not(.sidebar-collapsed) .triptych-sidebar::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    background: var(--color-scrim);
  }

  /* Every direct child of .triptych-sidebar that can be visible while the
     drawer is open — see the no-background note on .triptych-sidebar
     above. #sidebar-main and #sidebar-settings are alternates (only one
     renders at a time, swapped by the settings :has() rule below), and
     .sidebar-userbar always renders alongside whichever nav is showing
     (#review-pr caught the first version of this fix covering only the
     header + #sidebar-main, missing these two — Swarf-AI/webapp#5003). */
  .shell:not(.sidebar-collapsed) .triptych-sidebar-header,
  .shell:not(.sidebar-collapsed) #sidebar-main,
  .shell:not(.sidebar-collapsed) #sidebar-settings,
  .shell:not(.sidebar-collapsed) .sidebar-userbar {
    background: var(--color-base-100);
  }

  /* At narrow width the list pages' H1 row joins the button's band
     instead of stranding the button on a lonely row above it: the page's
     top padding drops to zero and the row spans the button's full band,
     centering its content on the button's axis by construction — no
     back-computed padding, and it stays aligned if the title's font ever
     changes. Shared by /workspaces/ and /conversations/ (one
     stylesheet). */
  .shell.sidebar-collapsed .wslist-page {
    padding-top: 0;
  }

  .shell.sidebar-collapsed .wslist-header {
    min-height: var(--sidebar-reopen-band);
  }

  /* Clearance rides the TITLE, not the header container: .wslist-header
     also holds the page's "+ New X" button (#798 — same title+button
     shape on both /workspaces/ and /conversations/), and a container
     margin would push that button off its band axis along with the H1.
     Margin (not padding) for the same box-integrity reason as the
     breadcrumb rule above. */
  .shell.sidebar-collapsed .wslist-title {
    margin-left: calc(
      var(--sidebar-reopen-clearance) - var(--shell-page-inline-pad)
    );
  }
}

/* Settings surface (Swarf-AI/webapp#2614): full-area in-app view, mock-faithful, driven purely
   by content presence — no JS, no class toggle. When #settings-page is non-empty
   (content hx-get'd in), `:has()` (1) hides chat + board and shows the surface,
   and (2) swaps the sidebar to the settings sub-nav. The .shell (= #sse-wrapper)
   is never swapped, so the SSE connection survives. Active sub-nav state follows
   the loaded section via `:has([data-settings-section="…"])` — no JS.
   Precedent for :has()-driven layout: :root:has(body.theme-mc) (brand.css).
   The board is NOT hidden from here — see the board-show rule (Swarf-AI/webapp#3027). */
.triptych-settings {
  display: none;
  position: relative;
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
}

#sidebar-settings {
  display: none;
}

.shell:has(#settings-page > *) #shell-content,
.shell:has(#settings-page > *) #sidebar-main {
  display: none;
}

.shell:has(#settings-page > *) .triptych-settings {
  display: block;
}

.shell:has(#settings-page > *) #sidebar-settings {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.6rem 0.5rem;
  flex: 1;
  min-height: 0;
}

/* Settings hides the board entirely (its own show rule excludes settings
   mode, Swarf-AI/webapp#3027), so its grid track must also collapse to 0 — otherwise the
   board-open rule above would still be reserving grid-track width even
   though the panel itself is hidden. Source order: this rule must follow
   the board-open rule above so it wins if both :has() conditions match
   simultaneously. */
.shell:has(#settings-page > *) {
  --board-w: 0;
}

/* Sidebar settings sub-nav — reuses .sidebar-nav-item / .sidebar-nav-label /
   .sidebar-section-label from the main sidebar so the settings nav matches the
   main app sidebar visually. Only settings-specific overrides live here. */

/* Back-to-app button (Swarf-AI/webapp#3029) — the mock's own dedicated .settings-return
   component (not a .sidebar-nav-item variant): a full-width bordered CTA in
   the same visual slot as the main sidebar's "+ New Conversation" item,
   chevron pinned left, label centered. Values transcribed from the mock. */
.settings-nav-back {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 0.8rem 0.6rem;
  padding: 0.55rem 0.8rem;
  /* Mock's font-secondary resolves to mono in theme-mc, but its Matt-mode
     re-pins interactive elements back to Space Grotesk (.settings-return in
     the re-pin list) — so live uses the primary font directly (Swarf-AI/webapp#3400). */
  font-family: var(--brand-font-primary);
  font-weight: 700;
  font-size: 0.78rem;
  letter-spacing: 0.02em;
  color: var(--color-base-content);
  background: color-mix(in srgb, var(--color-base-content) 7%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-base-content) 14%, transparent);
  border-radius: 9px;
  cursor: pointer;
  text-decoration: none;
  transition: border-color 0.15s ease, background 0.15s ease;
}

.settings-nav-back:hover {
  border-color: var(--brand-orange);
  background: color-mix(in srgb, var(--color-base-content) 10%, transparent);
}

.settings-nav-back-chev {
  position: absolute;
  left: 0.8rem;
  font-size: 1.05rem;
  line-height: 1;
  color: var(--brand-orange);
}

/* Structural base: display/radius/cursor not provided by body.theme-mc a.sidebar-nav-item
   (that rule only sets font-size/padding/line-height). */
#sidebar-settings a.sidebar-nav-item {
  display: block;
  border-radius: 7px;
  cursor: pointer;
  text-decoration: none;
}

/* Hover background for settings nav items (theme-mc context only). */
body.theme-mc #sidebar-settings a.sidebar-nav-item:hover {
  background: var(--color-surface-raised);
}


/* Section label in settings nav doesn't carry Tailwind's `uppercase` class like the main
   sidebar does — apply it here so "Account" renders as "ACCOUNT" matching the design. */
#sidebar-settings .sidebar-section-label {
  text-transform: uppercase;
}

.settings-nav-group {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

/* Active section — driven by the loaded body's data-settings-section (no JS).
   Uses the same outlined-border treatment as sidebar conversations li.active. */
.shell:has([data-settings-section="profile"]) [data-settings-nav="profile"],
.shell:has([data-settings-section="preferences"]) [data-settings-nav="preferences"],
.shell:has([data-settings-section="billing"]) [data-settings-nav="billing"],
.shell:has([data-settings-section="referrals"]) [data-settings-nav="referrals"] {
  background: var(--color-surface-raised);
  box-shadow: var(--color-border) 0px 0px 0px 1px inset;
}

/* Must target .sidebar-nav-label directly — body.theme-mc .sidebar-nav-label sets
   color: muted on all nav labels and overrides inherited color from the anchor. */
.shell:has([data-settings-section="profile"]) [data-settings-nav="profile"] .sidebar-nav-label,
.shell:has([data-settings-section="preferences"]) [data-settings-nav="preferences"] .sidebar-nav-label,
.shell:has([data-settings-section="billing"]) [data-settings-nav="billing"] .sidebar-nav-label,
.shell:has([data-settings-section="referrals"]) [data-settings-nav="referrals"] .sidebar-nav-label {
  color: var(--color-base-content);
}

/* Settings surface — centered column of cards (mock .settings-body). */
.settings-surface {
  max-width: 680px;
  margin: 0 auto;
  padding: 2.4rem 2rem 3rem;
}

.settings-body {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

.settings-title {
  font-family: var(--brand-font-primary);
  font-size: 1.45rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--color-base-content);
  margin: 0;
}

.settings-sub {
  font-size: 0.82rem;
  font-weight: 300;
  color: var(--color-muted-dim);
  margin: 0.15rem 0 0.65rem;
}

.acct-card {
  display: flex;
  flex-direction: column;
  background: var(--color-base-100);
  border: 1px solid color-mix(in srgb, var(--color-base-content) 10%, transparent);
  border-radius: 14px;
  padding: 0.9rem 1.1rem;
}

.acct-card + .acct-card {
  margin-top: 0.9rem;
}

/* ── Micro-label archetypes (Swarf-AI/webapp#4571) ─────────────────────────────────────────
   Each archetype is declared ONCE here as a combined selector list (the
   .conv-row-menu shared-item-rules precedent); a member's own rule later in
   the file carries only its real deltas — size bumps, ink, layout — which
   win by source order at equal specificity. Joining the family = adding a
   selector here, never restating the core. Catalog: docs/FRONTEND.md
   §Shared UI Primitives.

   Mono eyebrow — the caps micro-heading over a section/group/key.
   (.board-resources-colhead shares this typography but lives inside a
   @container block — its declarations only apply at wide panel widths, so
   it can't join an unconditional list without changing narrow rendering.) */
.acct-title,
.acct-key,
.espec-key,
.rp-section-title,
.chgl-meta,
.rp-group-head h4,
.rp-table th,
.resource-doc-eyebrow,
.chain-end-label,
.chain-card-type,
.cp-tier {
  font-family: var(--brand-font-mono);
  font-weight: 700;
  font-size: 0.58rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-muted-dim);
}

/* Primary-font label — same role in the primary face (dividers, popover
   section labels). letter-spacing deliberately has no core value: every
   member differs, so each keeps its own below. */
.board-search-divider,
.board-group-divider,
.paywall-cap,
.cp-label,
.cp-section-label,
.board-resources-eyebrow {
  font-family: var(--brand-font-primary);
  font-weight: 700;
  font-size: 0.62rem;
  text-transform: uppercase;
  color: var(--color-muted-dim);
}

/* Mono pill chip — the rounded mono action/filter chip. */
.board-filter-chip,
.resource-doc-subnav-tab,
.rp-desc-more {
  font-family: var(--brand-font-mono);
  font-weight: 700;
  font-size: 0.62rem;
  letter-spacing: 0.06em;
  padding: 0.3rem 0.7rem;
  border-radius: 9999px;
  border: none;
  background: none;
  cursor: pointer;
}

.acct-title {
  font-size: 0.62rem;
  letter-spacing: 0.12em;
  margin-bottom: 0.35rem;
}

.acct-row {
  display: grid;
  grid-template-columns: 170px 1fr;
  gap: 1.2rem;
  padding: 0.55rem 0;
  border-bottom: 1px solid color-mix(in srgb, var(--color-base-content) 10%, transparent);
  font-size: 0.84rem;
  color: var(--color-base-content);
}

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

/* Shared delta with .espec-key — the settings row key and the spec-table
   key are the same 0.64rem key treatment over the archetype. */
.acct-key,
.espec-key {
  font-size: 0.64rem;
  padding-top: 0.15rem;
}

/* Usage history rows — 3-column layout matching mock: date / title / cost */
.cr-row {
  display: grid;
  grid-template-columns: 52px 1fr auto;
  gap: 0.6rem;
  padding: 0.45rem 0;
  border-bottom: 1px solid color-mix(in srgb, var(--color-base-content) 10%, transparent);
  align-items: baseline;
}

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

.cr-d {
  font-family: var(--brand-font-mono);
  font-size: 0.56rem;
  font-weight: 700;
  color: var(--color-muted-dim);
  white-space: nowrap;
}

.cr-t {
  font-family: var(--brand-font-primary);
  font-size: 0.70rem;
  font-weight: 400;
  color: var(--color-base-content);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cr-c {
  font-family: var(--brand-font-mono);
  font-size: 0.64rem;
  font-weight: 700;
  color: var(--color-muted-dim);
  text-align: right;
  white-space: nowrap;
}

.cr-c--free {
  color: var(--color-success-content);
}

/* Preference rows — flex layout with label+description on the left, control right-aligned.
   Mirrors mock .pref-row/.pref-text; separate from .acct-row (used in Profile/Billing). */
.pref-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.55rem 0;
  gap: 1rem;
}

.pref-row + .pref-row {
  border-top: 1px solid color-mix(in srgb, var(--color-base-content) 10%, transparent);
}

/* Language preference dropdown (Swarf-AI/webapp#3269) — the one non-segment control in
   Preferences; token-styled to sit beside the .ap-seg segments.
   appearance:none + a drawn chevron (Swarf-AI/webapp#3842) — a bare native <select> reserves
   its own UA arrow gutter on the right regardless of padding, so equal
   left/right padding still reads visually lopsided. Mirrors DaisyUI's own
   .select chevron technique (two triangular linear-gradients, no icon
   asset) rather than inventing a different-looking one. */
.pref-select {
  appearance: none;
  cursor: pointer;
  background-color: color-mix(in srgb, var(--color-base-content) 6%, transparent);
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    calc(100% - 14px) calc(50% - 1px),
    calc(100% - 10px) calc(50% - 1px);
  background-repeat: no-repeat;
  background-size: 4px 4px;
  border: 1px solid color-mix(in srgb, var(--color-base-content) 14%, transparent);
  border-radius: 0.5rem;
  color: var(--color-base-content);
  font-family: inherit;
  font-size: 0.76rem;
  padding: 0.35rem 1.35rem 0.35rem 0.6rem;
  /* A bare <select> sizes to its widest <option> by default, not just the
     selected value — a locale whose translated option text runs longer than
     English's overflows this control's flex parent (.ap-pref, the
     account-menu popover row) since neither carries min-width: 0 (Swarf-AI/webapp#4243).
     min-width: 0 lets it shrink to the row's available space; max-width caps
     it there instead of at its own content's natural width. text-overflow
     needs its usual overflow:hidden + white-space:nowrap pair (see .cr-t,
     .board-tabs .board-tab-label) or the ellipsis never has anything to
     apply to. */
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  transition: border-color 0.15s ease;
}

.pref-select:hover {
  border-color: color-mix(in srgb, var(--color-base-content) 28%, transparent);
}

.pref-text {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

.pref-label {
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--color-base-content);
}

.pref-soon {
  font-size: 0.58rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-muted-dim);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  padding: 0.1rem 0.45rem;
  vertical-align: middle;
  margin-left: 0.35rem;
}

.pref-desc {
  font-size: 0.74rem;
  font-weight: 300;
  color: var(--color-muted-dim);
}

.acct-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.7rem;
}

.acct-btn {
  font-family: var(--brand-font-mono);
  font-weight: 700;
  font-size: 0.68rem;
  padding: 0.45rem 0.9rem;
  border-radius: 999px;
  color: var(--color-base-content);
  background: transparent;
  border: 1px solid var(--color-border);
  text-decoration: none;
  cursor: pointer;
  transition: border-color 0.15s ease;
}

.acct-btn:hover {
  border-color: var(--brand-orange);
}

.acct-btn--primary {
  background: var(--brand-orange);
  border-color: var(--brand-orange);
  color: rgb(11, 14, 16);
}

.acct-btn--primary:hover {
  background: color-mix(in srgb, var(--brand-orange), white 15%);
  border-color: color-mix(in srgb, var(--brand-orange), white 15%);
}

/* Name display row (Swarf-AI/webapp#2614). Read-only — name is set via the bot. */
.acct-val--muted {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--color-muted);
  font-style: italic;
}

.acct-val-tip {
  font-style: normal;
  font-size: 0.8rem;
  color: var(--color-muted-dim);
  cursor: default;
  line-height: 1;
}

.triptych-board {
  display: none;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
  /* Rows adapt to the board's own width, not the viewport (Swarf-AI/webapp#2584). */
  container-type: inline-size;
}

/* Board shows iff it has content AND settings is closed (Swarf-AI/webapp#3027) — the
   settings condition lives here, not in a competing hide rule, because this
   selector's two-ID :has() out-specifies anything the settings block could
   reasonably write (see the settings-surface comment above). */
.shell:has(#board-panel #tab-content > *):not(:has(#settings-page > *)) .triptych-board {
  display: flex;
}

body.theme-mc .triptych-board {
  border-left: 1px solid var(--color-border-subtle);
}

.board-panel-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* Board tab bar (Swarf-AI/webapp#2637, Swarf-AI/webapp#2638, Swarf-AI/webapp#2860) — Mock 2 tab-cards over a scrolling
   #tab-content. Every tab shares .board-tab-wrap; the card look lives on the
   wrap, the active look on the inner button via .board-doc-tab-active —
   server-rendered on the activating render (Swarf-AI/webapp#2928) and kept in sync across
   swaps by board-panel.js's syncActiveDocTab. One mechanism for all tabs
   (Swarf-AI/webapp#4594) — the Resource Library's former structural `:has()` selectors
   retired when its tab-strip entry moved onto board_dynamic_tab.html.
   Strip scrolls horizontally on overflow with a thin scrollbar (mock
   grammar). */
.board-tabs {
  flex-shrink: 0;
  display: flex;
  align-items: stretch;
  gap: 0.3rem;
  overflow-x: auto;
  scrollbar-width: thin;
  padding: 0.45rem 0.6rem 0;
  min-height: 40px;
  background: var(--color-inset);
  border-bottom: 1px solid var(--color-separator);
}

/* One card per tab — resting state (mock .etab, dark-token translation).
   border-top is always drawn (transparent at rest) so activation doesn't
   shift layout. */
.board-tabs .board-tab-wrap {
  flex: 0 0 auto;
  max-width: 200px;
  border-radius: 10px 10px 0 0;
  border-top: 2px solid transparent;
  color: var(--color-muted-dim);
  white-space: nowrap;
}

.board-tabs .board-tab-wrap:hover {
  background: color-mix(in srgb, var(--color-surface-raised) 60%, transparent);
  color: var(--color-base-content);
}

.board-tabs .tab {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  min-width: 0;
  padding: 0.35rem 0.7rem;
  font-size: 0.76rem;
  color: inherit;
  border: none; /* card border lives on the wrap, not DaisyUI's tab underline */
}

.board-tabs .board-tab-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Active card: base background + orange top accent + heavier label. The mock's
   active tab merges into the panel below it — same base color, the 2px accent
   doing the work — rather than reading as a raised chip (Swarf-AI/webapp#2976). The accent is
   brand orange, not the mock's white: white equals the active text color, so
   it blends and reads as near-invisible. */
.board-tab-wrap:has(.board-doc-tab-active) {
  background: var(--color-base-100);
  border-top-color: var(--brand-orange);
  color: var(--color-base-content);
}

.board-doc-tab.board-doc-tab-active .board-tab-label {
  font-weight: 600;
}

/* Per-tab × close — smaller than the board-close because it sits inline with
   the tab label, not at the far right edge of the bar. Muted-dim matches the
   mock's tab-chrome text scheme (Swarf-AI/webapp#2976); the font-size deliberately stays
   larger than the mock's — live renders × (U+00D7, small in its em box)
   where the mock renders ✕ (U+2715), so equal font sizes would NOT render
   equal glyphs. */
.board-doc-tab-close {
  padding: 0.2rem 0.3rem;
  border: none;
  background: none;
  color: var(--color-muted-dim);
  font-size: 0.85rem;
  line-height: 1;
  cursor: pointer;
  margin-left: -0.4rem; /* tighten the gap after the tab label */
}

.board-doc-tab-close:hover {
  color: var(--color-base-content);
}

/* ── Tab polish (Swarf-AI/webapp#2642) ─────────────────────────────────────────────────────
   Resource Library is a regular tab like any other now (Swarf-AI/webapp#2895) — no more
   "structural, no close" exception; the old bulk "Close workspace" button
   this section used to anchor to the right (.board-tab-close) is gone,
   replaced by every tab closing itself via .board-doc-tab-close.

   Wrapper groups the tab button + close button for shared layout, and is
   the unit drag-to-reorder (below) picks up and moves.                   */

.board-tab-wrap {
  display: flex;
  align-items: center;
  touch-action: none;
}

/* Drag-to-reorder states (Swarf-AI/webapp#2642). */
.board-tab-wrap.board-tab-drag-source {
  opacity: 0.35;
}

.board-tab-ghost {
  position: fixed;
  z-index: 300;
  pointer-events: none;
  display: flex;
  align-items: center;
  padding: 0.35rem 0.7rem;
  font-size: 0.76rem;
  background: var(--color-base-100);
  border-radius: 10px 10px 0 0;
  box-shadow: 0 6px 20px color-mix(in srgb, var(--color-base-content) 20%, transparent);
  opacity: 0.92;
  white-space: nowrap;
  user-select: none;
}

.board-tab-dropbar {
  position: absolute;
  top: 5px;
  bottom: 4px;
  width: 3px;
  border-radius: 3px;
  background: var(--brand-orange);
  pointer-events: none;
}

#tab-content {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 0.5rem;
}

/* Resource profile section nav (Swarf-AI/webapp#2969, mock's rp-nav) — jump chips over one
   continuous page, not tabs. The mock's anchor model: header + nav stay
   pinned, ONLY the section body scrolls. For profile tabs the host stops
   scrolling (:has chain below), the tab wrapper and .detail-view become
   full-height flex columns, and #resource-doc-body is the scroll container
   resource-scrollspy.js binds. The subtab URLs survive as deep links:
   data-active-subtab names the section to scroll into view on load (and
   anchors the ?edit=1 door) — it no longer hides anything.

   The setup instrument panel (Swarf-Inc/webapp#2087) shares the model, so
   its sentinel rides the same selector lists: chrome (header, viewer,
   transport) pins and the active G-code/Program pane is the one scroll
   region — which keeps playback's plain scrollIntoView(block:'nearest')
   inside the pane by construction, and lets the viewer take whatever
   height the board leaves. */
#tab-content:has(#resource-doc-body),
#tab-content:has([data-setup-doc-body]) {
  overflow-y: hidden;
  display: flex;
  flex-direction: column;
}

#tab-content:has(#resource-doc-body) > [data-active-board-tab],
#tab-content:has([data-setup-doc-body]) > [data-active-board-tab] {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.detail-view:has(#resource-doc-body),
.detail-view:has([data-setup-doc-body]) {
  flex: 1;
  min-height: 0;
}

#resource-doc-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* Reserve gutter space even on overlay-scrollbar platforms (macOS) so
     right-aligned content (e.g. .rp-pencil) never sits under the
     scrollbar's hit-test band (Swarf-AI/webapp#4458). */
  scrollbar-gutter: stable;
}

/* DaisyUI's unchecked-tab label is a 50% base-content mix — 3.41:1 in light
   theme, under WCAG AA's 4.5:1 (test_theme_contrast's setup-tab scan). */
[data-setup-doc-body] .tabs > .tab:not(:checked, :hover) {
  color: color-mix(in oklab, var(--color-base-content) 70%, transparent);
}

.resource-doc-subnav {
  display: flex;
  gap: 0.25rem;
  /* Same standard as .rp-section's own top padding and the workspace
     header's bottom padding — breathing room before the divider instead
     of the border sitting flush against the pills. */
  padding-bottom: 0.35rem;
  border-bottom: 1px solid var(--color-border-subtle);
}

/* Pill treatment matches .board-filter-chip (Swarf-AI/webapp#3097) — same mono/uppercase/
   pill archetype the mock uses for both chip rows, reused rather than
   re-derived so the light-theme AA fix below doesn't need re-verifying. */
.resource-doc-subnav-tab {
  text-transform: uppercase;
  line-height: 1;
  color: var(--color-muted);
}

.resource-doc-subnav-tab:hover {
  color: var(--color-base-content);
}

/* display/flex-direction/gap used to live here too, back when [data-subtab]
   was a plain tab-content div, one shown at a time (display:none for the
   rest). Once the config-driven section chrome (.rp-section) put title,
   pencil, and body under the same element that also carries data-subtab,
   this rule's higher specificity (#id + [attr] beats .rp-section's single
   class) started silently overriding .rp-section's own gap: 1.25rem
   instead of the 0.7rem meant for the space between a section's header
   and its body. Scroll anchoring is this rule's only remaining job. */
#resource-doc-body > [data-subtab] {
  scroll-margin-top: 3rem;
}

body.theme-mc .resource-doc-subnav-tab.active {
  color: var(--brand-orange);
  background: color-mix(in srgb, var(--brand-orange) 9%, var(--color-base-100));
}

/* Light theme's tint is the mock's dark-theme formula against a near-white
   base — the two together read below WCAG AA (~4:1) at this chip's font
   size (same failure mode as .board-filter-chip.is-active, Swarf-AI/webapp#3060/Swarf-AI/webapp#3059) —
   solid fill + white text holds ~4.88:1. */
body.theme-mc.theme-light .resource-doc-subnav-tab.active {
  color: var(--brand-white);
  background: var(--brand-orange);
}

/* Read-mode profile body (Swarf-AI/webapp#2804) — Mock 2's rp-group / espec / rp-table shapes
   (dev-mock2.css), translated to the theme-aware tokens: brand-white surfaces →
   --color-surface-raised, charcoal-mix borders → --color-border-subtle,
   brand-slate text → --color-muted. */

/* One wrapper per scroll-spy section (About/Specs/Notes/Files, Swarf-AI/webapp#3006) — the
   top border between sections doubles as the visual anchor the nav chips
   point at. Every section carries .rp-section-title, Specs included (Swarf-AI/webapp#4365
   reverses Swarf-AI/webapp#3006's original headerless call — Base/Controller/Axes/Spindles
   still get their own .rp-group-head h4 sub-headers beneath it). */
.rp-section {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  padding: 0.35rem 0 0.4rem;
}

/* Title + pencil share one row instead of stacking as separate flex
   children — no extra row means no extra gap to manage, and the always-
   reserved .rp-section-tools slot (below) keeps this row's height (and
   thus the gap before the body) identical whether a pencil renders inside
   it or not. */
.rp-section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.rp-section + .rp-section {
  border-top: 1px solid var(--color-border-subtle);
}

.rp-section-title {
  font-size: 0.64rem;
  letter-spacing: 0.14em;
  color: var(--color-muted);
}

/* Files-section upload dropzone (Swarf-AI/webapp#3264, ported from the mock's
   .rp-dropzone) — owner-only affordance below the files list. */
.rp-dropzone {
  font-size: 0.76rem;
  font-weight: 300;
  color: var(--brand-slate);
  background: transparent;
  border: 1.5px dashed color-mix(in srgb, var(--brand-charcoal) 18%, transparent);
  border-radius: 12px;
  padding: 0.8rem;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}

.rp-dropzone:hover,
.rp-dropzone.dragover {
  border-color: var(--brand-orange);
  color: var(--brand-gunmetal);
}

/* Per-file upload status rows (file_result SSE partials) accumulate here. */
.rp-upload-results {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.rp-upload-results:empty {
  display: none;
}

/* Container-owned collapse (Swarf-AI/webapp#4365) — one fixed height for every section's
   readable body, whatever shape that body takes (flowed prose, spec rows,
   a table, entry cards). Collapse is never a per-section opt-in: any
   content wrapped in .rp-desc-clip clamps the same way, full stop — there
   used to be two competing mechanisms here (line-count for prose,
   .rp-rows-clip/.chgl-older-clip's own fixed max-height for everything
   else), which meant a template could only clamp by choosing to carry the
   right co-class. One mechanism now; the fade + Read more signal the rest
   (Swarf-AI/webapp#3006). The toggle only renders (unhidden) when the body actually
   overflows — measured client-side by resource-scrollspy.js. */
.rp-desc {
  position: relative;
}

.rp-desc-clip {
  display: block;
  /* Tall enough to show a real entry's head + a couple of prose lines
     before cutting (Swarf-AI/webapp#4105 — 4.5rem chopped enrichment bodies mid-line),
     with a fade so the truncation reads as deliberate rather than broken. */
  max-height: 11rem;
  overflow: hidden;
}

.rp-desc.clamped:not(.open) .rp-desc-clip::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2.2rem;
  background: linear-gradient(to bottom, transparent, var(--color-base-100));
  pointer-events: none;
}

.rp-desc.open .rp-desc-clip {
  max-height: none;
  overflow: visible;
}

/* Pill chrome diverges from the mock's bare-text treatment deliberately
   (Swarf-AI/webapp#3228): the color measured AAA (7.58:1) yet the toggle was still easy
   to miss scanning past dense prose — the problem is size + zero chrome,
   not color. Tint is a transparent color-mix rather than the subnav
   chip's base-100 mix so it reads the same on any backdrop — the base-100
   formula has a documented light-theme wash-out (see
   .resource-doc-subnav-tab.active). */
.rp-desc-more {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  margin-top: 0.45rem;
  font-size: 0.68rem;
  text-transform: uppercase;
  color: var(--brand-orange);
  background: color-mix(in srgb, var(--brand-orange) 9%, transparent);
}

.rp-desc-more:hover {
  background: color-mix(in srgb, var(--brand-orange) 16%, transparent);
}

/* Chevron rotates open/closed via the ancestor's .open state — the
   .act-caret idiom (chat.css), simpler than swapping SVGs in JS. */
.rp-desc-more svg {
  width: 13px;
  height: 13px;
  transition: transform 0.15s ease;
}

.rp-desc.open .rp-desc-more svg {
  transform: rotate(180deg);
}

/* When clamped, square off the bottom corners/border of whatever's carrying
   the card styling inside the clip — otherwise a border-radius painted
   relative to the CLIPPED (not natural) height reads as a complete, closed
   box ("the table just ends here") instead of content sliced mid-flow
   (found live: both the .espec variant, where .rp-desc-clip IS the bordered
   card directly, and the <table> variant, where .rp-table carries its own
   border/radius/overflow one level in). Changelog needs none of this: its
   .chgl-entry cards carry no border of their own, so the cut lands mid-card,
   never at a card's own edge. */
.rp-desc.clamped:not(.open) .rp-desc-clip,
.rp-desc.clamped:not(.open) .rp-desc-clip .rp-table {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  border-bottom: none;
}

.rp-desc.clamped:not(.open) .rp-desc-clip .rp-table {
  overflow: visible;
}

.chgl-list {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.chgl-entry {
  background: var(--color-base-100);
  border: 1px solid var(--color-border-subtle);
  border-radius: 14px;
  padding: 0.75rem 1rem;
}

.chgl-entry-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.chgl-meta {
  /* Leads the head from the left (Swarf-AI/webapp#4105): with Swarf-AI/webapp#3925's body+created_at
     shape there is no topic, so a right-pushed timestamp read as an
     orphan — the date/time now sits where the mock's topic sat.
     Sub-header treatment (Swarf-AI/webapp#4365): an entry is a group and its date heads
     it exactly the way a spec group's name heads it — both ride the mono
     eyebrow archetype unchanged. */
  white-space: nowrap;
}

.chgl-body {
  margin-top: 0.4rem;
  font-size: 0.85rem;
  color: var(--color-muted);
  line-height: 1.5;
}

.rp-group {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
}

.rp-group-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* .rp-group-head h4's sub-header treatment (Swarf-AI/webapp#4365) IS the mono eyebrow
   archetype's base values exactly — it lives in the archetype list above
   with no delta rule. Its 0-1-1 descendant-combinator specificity means a
   bare .foo challenger can never restyle it; edit the archetype (or add a
   .rp-group-head h4 delta rule here) instead. */

.espec {
  display: flex;
  flex-direction: column;
  background: var(--color-base-100);
  border: 1px solid var(--color-border-subtle);
  border-radius: 14px;
  padding: 0.25rem 1rem;
}

.espec-row {
  display: grid;
  grid-template-columns: 170px 1fr;
  gap: 1.2rem;
  padding: 0.55rem 0;
  border-bottom: 1px solid var(--color-border-subtle);
  font-size: 0.84rem;
}

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

/* ISO-code toggle + definition popovers (Swarf-AI/webapp#3629) — .espec-key-symbol is
   hidden by default; the checkbox lives in .rp-group (the wrapper each
   spec template already shares), several DOM levels above the key spans
   it controls, so a plain sibling combinator (Tailwind's peer-checked:)
   can't reach them — :has() can, at any depth. Scoped to .rp-group so it
   only ever matches where the checkbox markup is actually rendered
   (tool component specs today); the other two spec templates never emit
   the checkbox, so this rule is inert there by construction. */
.espec-key-symbol {
  display: none;
}

.rp-group:has(.iso-toggle:checked) .espec-key-name {
  display: none;
}

.rp-group:has(.iso-toggle:checked) .espec-key-symbol {
  display: inline;
}

.iso-toggle-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0 0.6rem;
  font-size: 0.72rem;
  color: var(--color-muted-dim);
}

/* Axis / spindle tables — genuinely tabular data stays a table */
.rp-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  background: var(--color-base-100);
  border: 1px solid var(--color-border-subtle);
  border-radius: 12px;
  overflow: hidden;
}

.rp-table th {
  text-align: left;
  padding: 0.5rem 0.85rem;
  border-bottom: 1px solid var(--color-border-subtle);
}

.rp-table td {
  font-size: 0.8rem;
  color: var(--color-base-content);
  padding: 0.5rem 0.85rem;
}

.rp-table tbody tr + tr td {
  border-top: 1px solid var(--color-border-subtle);
}

/* Empty-state label — shared with the credits popover's no-usage state
   (Swarf-AI/webapp#3646), the app's one muted-italic "nothing here" treatment. */
.rp-empty,
.cp-usage-empty {
  font-size: 0.78rem;
  font-weight: 300;
  font-style: italic;
  color: var(--color-muted);
}

/* Draft chip (Swarf-AI/webapp#2837) — the create tab's DRAFT state in the header eyebrow.
   Sibling of .resource-doc-vis; dashed orange marks the not-yet-saved state. */
.rp-draft-chip {
  font-size: 0.56rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.1rem 0.5rem;
  border: 1px dashed var(--brand-orange);
  border-radius: 999px;
  color: var(--brand-orange);
  white-space: nowrap;
}

/* Per-section edit pencil (Swarf-AI/webapp#2804) — always rendered (even with no pencil
   inside, for a read-only viewer) so .rp-section-head's row height, and
   thus the gap before the body, never shifts based on edit permission.
   Right-aligned within the shared header row via .rp-section-head's own
   justify-content: space-between. */
.rp-section-tools {
  display: flex;
  justify-content: flex-end;
  min-height: 22px;
}

/* Every control that sits in a .rp-section-tools slot — the edit pencil
   and the CONTENT section's render/source toggle — is the same 22px ghost
   square, so they read as one column rather than as two controls that
   happen to be adjacent. */
.rp-pencil,
.rp-src-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  color: var(--color-muted);
  background: transparent;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition:
    color 0.12s ease,
    background 0.12s ease;
}

.rp-pencil svg,
.rp-src-toggle svg {
  width: 12px;
  height: 12px;
}

.rp-pencil:hover,
.rp-src-toggle:hover {
  color: var(--color-base-content);
  background: var(--color-surface-raised);
}

.rp-notes-prose {
  font-size: 0.84rem;
  line-height: 1.6;
  color: var(--color-base-content);
}

/* Unified Resources view (Swarf-AI/webapp#2584) — one list of toggleable resource rows.
   Mock parity: Space Grotesk, orange-circle in-context toggle, pill chips.
   Columns (type/visibility) reveal via container query as the board widens. */
/* Post-toggle scope hide (Swarf-AI/webapp#4568): under the In Context chip with no term,
   a row whose scope toggle just flipped it out of context (the toggle's OOB
   row swap updates data-in-context in place) vanishes without a re-render —
   the retired client filter's toggle-off-hides behavior (Swarf-AI/webapp#2873), preserved
   without re-rendering the region (which would close open popovers, Swarf-AI/webapp#3944).
   Never applies mid-search: reach-past (Swarf-AI/webapp#2878) deliberately shows
   out-of-context rows, so requestBoardSearch sets data-scope="open" there. */
.board-resources[data-scope="context"] .board-resource-row[data-in-context="false"]:not([data-context-only="true"]) {
  display: none;
}

.board-resources {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.board-resources-header {
  padding: 0 0.2rem;
}

.board-resources-title {
  font-family: var(--brand-font-primary);
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.01em;
}

.board-resources-blurb {
  font-family: var(--brand-font-primary);
  font-size: 0.72rem;
  color: var(--color-muted-dim);
  margin-top: 0.15rem;
}

.board-resources-eyebrow {
  font-size: 0.6rem;
  letter-spacing: 0.12em;
  /* The one non-uppercase member — its label text is authored as-is. */
  text-transform: none;
}

/* Count pill inside a filter chip (mock: "Machines 2"). */
.board-chip-count {
  opacity: 0.7;
  font-variant-numeric: tabular-nums;
}

/* Column headers — aligned to the row grid; only shown once the board is wide
   enough for the table layout (same breakpoint as the row columns). */
.board-resources-colhead {
  display: none;
}

@container (min-width: 32rem) {
  /* Per-type column sets (Swarf-AI/webapp#4063): the container's data-cols (set by
     board-panel.js from the active chip) picks which colhead variant shows
     and which grid the rows use. Generic = the shared Type + Key spec pair
     (All / In Context / Others / Files). */
  .board-resources:not([data-cols]) .colhead-generic,
  .board-resources[data-cols="generic"] .colhead-generic,
  .board-resources[data-cols="machines"] .colhead-machines,
  .board-resources[data-cols="materials"] .colhead-materials,
  .board-resources[data-cols="workholdings"] .colhead-workholdings,
  .board-resources[data-cols="tool_components"] .colhead-tool_components {
    display: grid;
    /* Swarf-AI/webapp#4175: Name and the value columns share extra panel width (Name
       weighted higher, since it's the primary identifying content); Toggle
       and Visibility stay fixed — an icon slot and a 2-value badge don't
       benefit from growing. Each value column's rem floor is its old fixed
       width, so narrow-panel layout is unchanged. */
    grid-template-columns: 1.5rem minmax(0, 3fr) minmax(7rem, 1fr) minmax(9rem, 1fr) 5rem;
    gap: 0.5rem;
    /* No header over the toggle column — labels start at the Name column. */
  }

  .board-resources-colhead > span:first-child {
    grid-column: 2;
  }

  .board-resources-colhead {
    padding: 0 0.55rem 0.25rem;
    border-bottom: 1px solid var(--color-border-subtle);
    font-family: var(--brand-font-mono);
    font-size: 0.58rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    color: var(--color-muted-dim);
  }

  /* Typed grids: three value columns (machines/materials/workholdings), one
     (tools). Rows and colhead share each track list. Floors are per-type,
     not shared (Swarf-AI/webapp#4175 originally gave all three the same floors — sized to
     Materials' longest content, which starved Name on Machines/Workholdings
     even though neither renders anything that long): Machine.Type tops out
     at "Boring Mill" (11 chars), Orientation at "Multi-Axis"/"Horizontal"
     (10), Axes is a single digit or em dash — verified against seeded data,
     not guessed. */
  .board-resources[data-cols="machines"] .colhead-machines,
  .board-resources[data-cols="machines"] .board-resource-row {
    grid-template-columns: 1.5rem minmax(0, 3fr) minmax(6rem, 1fr) minmax(6rem, 1fr) minmax(3rem, 1fr) 5rem;
  }

  /* Workholding.Type tops out at "Locating"/"Clamping" (9 chars); Brand's
     longest seeded value is "Kitagawa" (8) but it's free text; Model is
     unbounded free text (longest seeded: "AMR-C080822-16-50", 18 chars) —
     kept closest to its old floor since it already truncates at the old
     width for outliers like that one. */
  .board-resources[data-cols="workholdings"] .colhead-workholdings,
  .board-resources[data-cols="workholdings"] .board-resource-row {
    grid-template-columns: 1.5rem minmax(0, 3fr) minmax(5.5rem, 1fr) minmax(5rem, 1fr) minmax(6rem, 1fr) 5rem;
  }

  /* Materials genuinely needs more room than the other two types: Subgroup
     names run up to 32 chars ("Nodular Cast Iron (Ductile Iron)") and ISO
     Group renders MaterialSubgroup's full descriptive __str__, not just the
     letter code ("S - Heat Resistant Superalloys & Titanium") — both already
     truncate at these floors for their longest values, same as Name does,
     so these two stay close to their original width. Hardness range
     ("270–302 HB") is the one real gap to close here. */
  .board-resources[data-cols="materials"] .colhead-materials,
  .board-resources[data-cols="materials"] .board-resource-row {
    grid-template-columns: 1.5rem minmax(0, 3fr) minmax(8rem, 1fr) minmax(7rem, 1fr) minmax(5rem, 1fr) 5rem;
  }

  .board-resources[data-cols="tool_components"] .colhead-tool_components,
  .board-resources[data-cols="tool_components"] .board-resource-row {
    grid-template-columns: 1.5rem minmax(0, 3fr) minmax(9rem, 1fr) 5rem;
  }

  /* Under a typed grid: generic pair yields to the typed cells. */
  .board-resources[data-cols="machines"] .board-resource-type,
  .board-resources[data-cols="machines"] .board-resource-spec,
  .board-resources[data-cols="materials"] .board-resource-type,
  .board-resources[data-cols="materials"] .board-resource-spec,
  .board-resources[data-cols="workholdings"] .board-resource-type,
  .board-resources[data-cols="workholdings"] .board-resource-spec,
  .board-resources[data-cols="tool_components"] .board-resource-type,
  .board-resources[data-cols="tool_components"] .board-resource-spec {
    display: none;
  }

  .board-resources[data-cols="machines"] .brc-typed,
  .board-resources[data-cols="materials"] .brc-typed,
  .board-resources[data-cols="workholdings"] .brc-typed,
  .board-resources[data-cols="tool_components"] .brc-typed {
    display: block;
  }

  .board-resources[data-cols] .brc-t1 {
    grid-column: 3;
  }

  .board-resources[data-cols] .brc-t2 {
    grid-column: 4;
  }

  .board-resources[data-cols] .brc-t3 {
    grid-column: 5;
  }

  .board-resources[data-cols="machines"] .board-resource-vis,
  .board-resources[data-cols="materials"] .board-resource-vis,
  .board-resources[data-cols="workholdings"] .board-resource-vis {
    grid-column: 6;
  }

  .board-resources[data-cols="tool_components"] .board-resource-vis {
    grid-column: 4;
  }
}

/* Typed value cells (Swarf-AI/webapp#4063) — same cell treatment as the key-spec cell:
   natural case, primary font, ellipsis-truncated. Hidden until their tab's
   grid activates (rules above). */
.brc-typed {
  display: none;
  font-size: 0.8rem;
  font-family: var(--brand-font-primary);
  color: var(--color-base-content);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.board-resources-search {
  width: 100%;
  padding: 0.6rem 0.9rem;
  font-family: var(--brand-font-primary);
  font-size: 0.84rem;
  line-height: normal;
  background: var(--color-base-100);
  border: 1px solid var(--color-border-subtle);
  border-radius: 0.75rem;
  color: inherit;
}

/* "+ New resource" (Swarf-AI/webapp#2837) — the mock's dashed ghost pill beside the filter box.
   The bar becomes a flex row; the button never shrinks. */
.board-resources-bar {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

.board-new-resource-btn {
  anchor-name: --board-new-resource-anchor;
  flex: 0 0 auto;
  white-space: nowrap;
  font-family: var(--brand-font-mono);
  font-weight: 700;
  font-size: 0.66rem;
  letter-spacing: 0.04em;
  color: var(--color-muted-dim);
  background: transparent;
  border: 1px dashed color-mix(in srgb, var(--color-base-content) 20%, transparent);
  border-radius: 999px;
  padding: 0.35rem 0.85rem;
  cursor: pointer;
  transition:
    border-color 0.15s ease,
    color 0.15s ease;
}

.board-new-resource-btn:hover {
  border-color: var(--brand-orange);
  color: var(--color-base-content);
}

/* Anchored below the trigger via CSS anchor positioning — the same idiom as
   .composer-add-menu. Container chrome and item styling come from the
   canonical theme-mc dropdown-menu rules (which this class joins) — only
   positioning lives here. */
.board-new-resource-menu {
  position-anchor: --board-new-resource-anchor;
  position-area: bottom span-left;
  margin: 0.25rem 0 0;
}

.board-resources-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
}

.board-filter-chip {
  color: var(--color-muted-dim);
}

.board-filter-chip:hover {
  color: var(--color-base-content);
}

body.theme-mc .board-filter-chip.is-active {
  color: var(--brand-orange);
  background: color-mix(in srgb, var(--brand-orange) 9%, var(--color-base-100));
}

/* Light theme's tint is the mock's dark-theme formula against a near-white
   base — the two together read below WCAG AA (~4:1) at this chip's font
   size. The mock was only verified against the dark Matt theme (this
   project's actual target, per docs/RESKIN.md), so rather than invent an
   unverified light-theme tint, keep light's prior solid-fill treatment.
   Charcoal text against light theme's own darkened brand-orange computes
   ~2.66:1 (verified via direct sRGB->linear luminance calculation, Swarf-AI/webapp#3060)
   — white text holds ~4.88:1, the same fix Swarf-AI/webapp#3059 applied to the sibling
   ARM badge bug with the identical pattern. */
body.theme-mc.theme-light .board-filter-chip.is-active {
  color: var(--brand-white);
  background: var(--brand-orange);
}

/* In Context is the mock's lead chip — permanently accented (not gated on
   .is-active) with a trailing divider separating it from the type chips. Its
   active state inverts the generic tint treatment above: solid fill instead
   of a light background, since it reads as a distinct, elevated scope rather
   than one of several equal filters. */
body.theme-mc .board-filter-chip[data-filter-scope="context"] {
  position: relative;
  margin-right: 0.6rem;
  color: var(--brand-orange);
}

body.theme-mc .board-filter-chip[data-filter-scope="context"]::after {
  content: "";
  position: absolute;
  right: -0.45rem;
  top: 22%;
  bottom: 22%;
  width: 1px;
  background: var(--color-border-subtle);
}

body.theme-mc .board-filter-chip[data-filter-scope="context"].is-active {
  color: var(--color-base-100);
  background: var(--brand-orange);
}

/* Only the row list scrolls (Swarf-AI/webapp#4109) — search, chips, and column headers
   stay pinned. Same mechanism as the resource profile's pinned-header
   scroll region (Swarf-AI/webapp#2969's #resource-doc-body chain above): the tab host
   stops scrolling, every rung down to the rows becomes a full-height
   flex column, and the rows container owns the overflow. */
#tab-content:has(.board-resources) {
  overflow-y: hidden;
  display: flex;
  flex-direction: column;
}

#tab-content:has(.board-resources) > #board-resources-list {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

#tab-content:has(.board-resources) .board-resources {
  flex: 1;
  min-height: 0;
}

#tab-content:has(.board-resources) .board-resources-rows {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.board-resources-rows {
  display: flex;
  flex-direction: column;
}

/* Empty-filter state (Swarf-AI/webapp#2873) — when every row is client-side [hidden] (the
   In Context chip with nothing in context, or a search term with no
   matches), show a message instead of a blank list. One flat list now (no
   more per-kind sections, Swarf-AI/webapp#2617), so this collapses to a single ::after on
   the rows container rather than a per-section rule. */
.board-resources-rows:not(:has(.board-resource-row:not([hidden])))::after {
  content: "Nothing matches — try a different filter or search.";
  padding: 1.5rem 0.55rem;
  text-align: center;
  font-size: 0.78rem;
  color: var(--color-muted-dim);
}

/* Library list dividers — search-past-filter (Swarf-AI/webapp#2878, separates in-context
   matches from not-yet-in-context shop resources) and grouped-view (Swarf-AI/webapp#3140,
   mirrors the mock's .lib-divider). One rule over the primary-label
   archetype: same role, same chrome. */
.board-search-divider,
.board-group-divider {
  padding: 0.55rem 0.55rem 0.25rem;
  letter-spacing: 0.08em;
  border-top: 1px solid var(--color-border-subtle);
}

/* The workspace-name link inside the "From [Workspace]" divider — same
   weight/case as the divider text so it reads as one label, not a link
   dropped into a heading (mirrors the mock's .lib-divider.lib-group-ws
   button). */
.board-group-divider a {
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: var(--brand-orange-deep);
  text-decoration: underline;
}

/* List/Grouped view preference (Swarf-AI/webapp#3140) — same pill archetype as
   .board-filter-chip, deliberately smaller/quieter since it's a display
   preference, not a filter. */
.board-view-mode-toggle {
  display: flex;
  gap: 0.3rem;
  padding: 0.1rem 0.1rem 0.6rem;
}

.board-view-mode-btn {
  font-family: var(--brand-font-mono);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--color-muted-dim);
  background: transparent;
  border: 1px solid var(--color-border-subtle);
  border-radius: 9999px;
  padding: 0.16rem 0.55rem;
  cursor: pointer;
}

body.theme-mc .board-view-mode-btn.is-active {
  color: var(--brand-orange);
  background: color-mix(in srgb, var(--brand-orange) 9%, var(--color-base-100));
}

/* Same light-theme AA fix as .board-filter-chip.is-active (Swarf-AI/webapp#3059/Swarf-AI/webapp#3060) —
   the dark-theme tint formula fails WCAG AA against light's paler base. */
body.theme-mc.theme-light .board-view-mode-btn.is-active {
  color: var(--brand-white);
  background: var(--brand-orange);
}

.board-resource-row {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem 0.55rem;
  border-radius: 0.4rem;
}

.board-resource-row:hover {
  background: var(--color-surface-raised);
}

.board-resource-name {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  min-width: 0;
  font-family: var(--brand-font-primary);
  font-size: 0.8rem;
  font-weight: 500;
  color: inherit;
  text-decoration: none;
}

a.board-resource-name:hover {
  color: var(--brand-orange);
}

.board-resource-icon {
  display: inline-flex;
  flex-shrink: 0;
  color: var(--color-muted-dim);
}

/* Per-resource thumbnail (Swarf-AI/webapp#2728) — occupies the glyph slot when a resource has
   an image; falls back to the type glyph otherwise. Populated out-of-band. */
.board-resource-thumb {
  width: 18px;
  height: 18px;
  border-radius: 4px;
  object-fit: cover;
}

/* ─── Resource profile header (Swarf-AI/webapp#2805) — Mock 2's rp-head ──────────────────
   Tile (thumbnail or type-icon fallback) · eyebrow (type + visibility chip)
   · title · subtitle · in-context toggle. Values track the mock's .rp-head /
   .rp-tile / .rp-sub / .ctxv-toggle block (dev-mock2.css). */
.resource-doc-header {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  /* Was carried by .detail-view's own gap-4 Tailwind utility, which applied
     uniformly to every top-level child including #resource-doc-body —
     stacking with .rp-section's own padding-top to make the header-to-
     content gap much bigger than any other section-to-section gap on the
     page. Owning this spacing directly keeps that one relationship (header
     to subnav) while letting the subnav-to-body gap fall through to
     .rp-section's own value, matching workspace-detail's page shape.
     0.35rem is the same standard used for .rp-section's own top padding,
     the workspace header's bottom padding, and .resource-doc-subnav's —
     one number for every header/divider-to-content relationship on the
     page, not a fourth one-off value. */
  margin-bottom: 0.35rem;
}

.resource-doc-tile {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 14px;
}

/* The image tile reuses .board-resource-doc-thumb's object-fit crop below;
   the icon fallback gets the mock's raised plate (Swarf-AI/webapp#2964 parity). */
.resource-doc-tile--icon {
  color: var(--brand-white);
  background: var(--color-surface-raised);
}

/* Title sizing lives here with the rest of the header block — the mock's
   .rp-head h2 (1.1rem / -0.02em), not Tailwind's text-xl (Swarf-AI/webapp#2964 parity). */
.resource-doc-header h2 {
  font-size: 1.1rem;
  letter-spacing: -0.02em;
}

/* Typography for the resource-doc header's inline title editor — sized to
   match .resource-doc-header h2 above. Chrome reset is .inline-title-input
   below, shared with workspace-detail's own inline name editor. */
.resource-doc-title-input {
  font-size: 1.1rem;
  font-weight: 700;
  letter-spacing: -0.02em;
}

/* Inline title editor (Swarf-AI/webapp#4442, extracted from workspace-detail's original
   input.workspace-detail-header-name, Swarf-AI/webapp#3942) — an input that replaces a
   title in place: no border/background until focused, then a brand-orange
   underline. Typography is a separate class layered on top per consumer
   (workspace-detail's h1 and the resource-doc header's h2 differ in size
   on purpose) — only this mechanical reset is shared. flex:1 + min-width:0
   assume the input shares its header's flex row with inline Save/Cancel. */
.inline-title-input {
  flex: 1;
  min-width: 0;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--color-base-content);
}

.inline-title-input:focus {
  outline: none;
  box-shadow: 0 1px 0 0 var(--brand-orange);
}

.resource-doc-eyebrow {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  font-size: 0.62rem;
  letter-spacing: 0.12em;
  color: var(--color-base-content);
}

.resource-doc-vis {
  font-size: 0.56rem;
  letter-spacing: 0.1em;
  padding: 0.1rem 0.5rem;
  border: 1px solid color-mix(in srgb, var(--brand-charcoal) 16%, transparent);
  border-radius: 999px;
}

.resource-doc-sub {
  font-size: 0.78rem;
  font-weight: 300;
  color: var(--color-muted-dim);
}

.resource-doc-fork-link {
  color: var(--brand-orange);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.resource-doc-fork-link:hover {
  color: var(--color-base-content);
}

/* A record named in a resource page's rows — Scene, Recipe (#3603). At rest
   the name read exactly like the plain values beside it, so only the pointer
   and a delayed peek gave the link away. A trailing arrow marks it without
   coloring a dense row list; hover turns name and arrow orange and nudges
   the arrow. The glyph is a pseudo-element so the row's text — what tests
   and the peek read — stays the record's name. */
.record-link::after {
  content: "→";
  display: inline-block;
  margin-left: 0.35em;
  color: var(--color-muted-dim);
  transition:
    transform 0.15s,
    color 0.15s;
}

@media (hover: hover) and (pointer: fine) {
  .record-link:hover,
  .record-link:hover::after {
    color: var(--brand-orange);
  }

  .record-link:hover::after {
    transform: translateX(2px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .record-link::after {
    transition: none;
  }

  .record-link:hover::after {
    transform: none;
  }
}

/* Thumbnail crop for the resource detail header's image tile. The box
   (size, radius) belongs to .resource-doc-tile — restating it here at equal
   specificity later in the file silently outranked the tile's 40px (Swarf-AI/webapp#2964
   review finding); this class owns only the crop. */
.board-resource-doc-thumb {
  object-fit: cover;
}

.board-resource-label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Type / Key spec / Visibility columns — hidden until the board is wide
   enough for the table layout. All three share the primary (Space Grotesk)
   token and natural case, matching the typed value cells (.brc-typed) they
   sit beside on Machines/Materials/Workholdings — Type and Visibility used
   to carry the mono/letter-spaced eyebrow treatment for their all-caps
   text; now that the caps are gone (Resource Library table standardization),
   the mismatched font was the one thing still marking them as different
   from their row siblings. */
.board-resource-type,
.board-resource-spec,
.board-resource-vis {
  display: none;
  font-size: 0.8rem;
  font-family: var(--brand-font-primary);
  color: var(--color-base-content);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Narrow-container counterpart of .board-resource-vis (Swarf-AI/webapp#5006) — visible by
   default (below the container query, where the wide column above is
   display:none), hidden once that column takes over. Reuses
   .resource-doc-vis's pill styling as-is; see board_resource_row.html for
   why this isn't a new treatment. grid-column:2 is load-bearing, not
   decorative: .board-resource-row's narrow grid has exactly two explicit
   tracks (auto 1fr) claimed by the toggle and the name, so an unplaced
   third visible child auto-flows to row 2 col 1 — directly under the
   toggle, not beside the name it labels (measured live, caught in
   review). Same track as the name so the pill sits under it, not beside
   it inline — the row's narrow track is too tight for both on one line. */
.board-resource-vis-narrow {
  grid-column: 2;
  justify-self: start;
}

@container (min-width: 32rem) {
  .board-resource-vis-narrow {
    display: none;
  }

  .board-resource-row {
    /* Swarf-AI/webapp#4175: byte-identical to the generic colhead's grid-template-columns
       above — must stay in lockstep, rows and colhead share one track list. */
    grid-template-columns: 1.5rem minmax(0, 3fr) minmax(7rem, 1fr) minmax(9rem, 1fr) 5rem;
  }

  /* Explicit columns so context-only rows (attachments/artifacts, Swarf-AI/webapp#2877 —
     no .lib-toggle rendered) don't auto-flow one column left (Swarf-AI/webapp#2906). The
     toggle, when present, auto-places into the unclaimed first track. Same
     device as .board-resources-colhead's `grid-column: 2` above. */
  .board-resource-name {
    grid-column: 2;
  }

  .board-resource-type,
  .board-resource-spec,
  .board-resource-vis {
    display: block;
  }

  .board-resource-type {
    grid-column: 3;
  }

  .board-resource-spec {
    grid-column: 4;
  }

  .board-resource-vis {
    grid-column: 5;
  }
}

/* In-context toggle — mock .lib-toggle: 22px circle, orange-filled when on. */
.lib-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  font-size: 0.8rem;
  line-height: 1;
  cursor: pointer;
  border: 1px solid var(--brand-orange);
  background: transparent;
  color: var(--brand-orange);
}

.lib-toggle.on {
  background: var(--brand-orange);
  color: var(--brand-charcoal);
}

.lib-toggle:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Read-only workspace-derived row. The app's own rows render a scope picker
   instead (Swarf-AI/webapp#3944); the only remaining consumers are the dev mock surfaces
   (dev_mock_resource_library_body.html, dev-mock-wstab.js), which load this
   stylesheet via app.html. Muted rather than orange so it never reads as an
   in-context choice the way .on does; a span, not a disabled button, since
   there's nothing to click. */
.lib-toggle.locked {
  cursor: default;
  color: color-mix(in srgb, var(--brand-gunmetal) 70%, transparent);
  background: color-mix(in srgb, var(--brand-charcoal) 6%, transparent);
  border-color: color-mix(in srgb, var(--brand-charcoal) 12%, transparent);
  font-size: 0.62rem;
}

/* Wraps the row's single toggle cell so it occupies .board-resource-row's
   `auto` first grid column as one unit, instead of auto-flowing into an
   unclaimed track. */
.board-resource-toggles {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}

/* Brand-orange send button — uses brand tokens so theme-mc overrides apply. */
.btn-brand-orange {
  background-color: var(--brand-orange);
  color: var(--brand-charcoal);
  border-color: var(--brand-orange);
}

.btn-brand-orange:hover:not(:disabled) {
  background-color: var(--brand-orange-deep);
  border-color: var(--brand-orange-deep);
}

.btn-brand-orange:disabled {
  opacity: 0.35;
}

/* Horizontal padding — the shell-standard token, same one every other
   shell page reads (#798). Vertical padding (pt-4) stays a template
   utility class; only the left/right value is unified here. */
#chat-scroll-container {
  padding-inline: var(--shell-page-inline-pad);
}

/* Landing state: center the input-wrapper card in the chat column.
   Removed by the conversationCreated JS event on first message send.
   padding-bottom matches the container's own pt-4 (conversation.html) so
   the centered flex box isn't asymmetrically padded (#1007). */
#chat-scroll-container.is-landing {
  align-items: center;
  justify-content: center;
  padding-bottom: 1rem;
}

/* In landing state, #input-wrapper is a centered card, not sticky-bottom. */
#chat-scroll-container.is-landing #input-wrapper {
  position: static;
  width: 100%;
  max-width: 48rem;
}

/* The scroll spacers exist for scroll-position math during active
   streaming (streaming.js), which cannot be happening in landing state —
   .is-landing is stripped before the first message can start streaming.
   Left visible, they added invisible height to the centered flex box
   above, biasing the visible content downward (#1007). */
#chat-scroll-container.is-landing #scroll-spacer-fixed,
#chat-scroll-container.is-landing #scroll-spacer-dynamic {
  display: none;
}

/* Dark mode: user message bubble — slightly lighter than base-200 with speech-bubble tail.
   Border ring matches the active sidebar item treatment (--color-border inset ring).
   Padding tightened to match mock (8.8px 15.2px ≈ 0.55rem 0.95rem). */
body.theme-mc .user-message-bubble {
  background-color: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: 1rem 1rem 0.3125rem 1rem;
  padding: 0.55rem 0.95rem;
}

/* Dark mode: .prose text color uses the theme token.
   Font size and line-height match mock (13.44px = 0.84rem, ~1.55 line-height). */
body.theme-mc .prose {
  color: var(--color-base-content);
  font-size: 0.84rem;
  line-height: 1.55;
}

/* Resource notes (_read_notes.html, `.rp-notes-prose.prose`) reuse `.prose`
   for the Typography plugin's table/list rendering, but it's a long-form
   reference read — restore normal paragraph spacing for this consumer.
   Scoped to `.rp-notes-prose.prose` (not the bare `.rp-notes-prose` this
   file's other consumer, _wsdetail_edit_section.html, carries alone) so
   the workspace About/Instructions editor's plain <p> tags stay at
   Tailwind preflight's zero margin instead of inheriting this spacing.

   margin-top is explicit here (not inherited from a broader chat-wide
   reset, which no longer exists — Swarf-AI/webapp#4793 removed it): the first paragraph
   needs to sit flush with its section header (the Swarf-AI/webapp#4691 standard), so it
   can't take the same 0.75rem every other paragraph gets. Adjacent
   margins collapse to their max, not their sum, so margin-bottom alone
   already gives every non-first paragraph the same visible gap a
   symmetric top+bottom pair would — the top half only ever mattered for
   the first paragraph, which this rule now states directly rather than
   relying on a rule elsewhere to imply it. */
body.theme-mc .rp-notes-prose.prose p {
  margin-top: 0;
  margin-bottom: 0.75rem;
}

/* Changelog entries (_chgl_entry_card.html, `.chgl-body.prose`, Swarf-AI/webapp#4805) reuse
   `.prose` for the same paragraph-break rendering Notes gets, but a
   changelog entry is secondary metadata about the record, not the record's
   own content — restore `.chgl-body`'s own muted color/font-size rather
   than letting the bare `.prose` override (above) win on equal specificity
   by appearing later in the cascade. Paragraph spacing needs no rule here:
   the base `.prose p` margin (0.75rem, Swarf-AI/webapp#4793) already gives every consumer
   sensible between-paragraph spacing. */
body.theme-mc .chgl-body.prose {
  color: var(--color-muted);
  font-size: 0.85rem;
}

/* Sidebar nav labels: muted gray in dark mode (not full base-content white). */
body.theme-mc .sidebar-nav-label {
  color: var(--color-muted);
}

/* Hover brightens text to full white (mock .side-item:hover), and the active
   conversation holds that same white so the selected item stays fully lit.

   Brightening is ROW-scoped, so a collapsible row keys off its .group/row
   wrapper rather than the label's own :hover (Swarf-AI/webapp#3923): the collapse toggle is
   an absolutely-positioned SIBLING of the label, so hovering the toggle never
   makes the label itself :hover, and a self-referential trigger would leave
   the text dim while the row background lit up.
   Plain unwrapped nav items have no wrapper, so they keep their own :hover. */
body.theme-mc a.sidebar-nav-item:hover .sidebar-nav-label,
body.theme-mc .group\/row:hover .sidebar-nav-label,
body.theme-mc .sidebar-collapsible-list li.active .sidebar-nav-label {
  color: var(--color-base-content);
}

/* The chevron's box is TOGGLE-scoped, not row-scoped (Swarf-AI/webapp#3923) — hovering the
   label lights the row, but must leave the chevron unlit, since the box is the
   toggle's own affordance. A plain `group-hover/row` variant can't express
   that (it fires from the label too), and no ancestor or sibling combinator
   reaches backwards from the toggle, which is a LATER sibling of the label
   that contains the icon zone — so the question has to be asked of the shared
   wrapper: does it hold a hovered toggle? Tailwind's `group-has-[...]` variant
   could encode the same :has() selector inline, but every other :has()-driven
   rule in this file is hand-written here and the repo has no `has-[` variant
   usage to match, so this stays consistent with the file it lives in. */
body.theme-mc .group\/row:has(.sidebar-collapsible-toggle:hover) .sidebar-nav-icon {
  background: color-mix(in srgb, var(--color-base-content) 10%, transparent);
}

/* Active row (conversation or workspace): opaque surface + inset ring
   matching mock border treatment — shared via .sidebar-collapsible-list,
   the class both #sidebar-conversations and #sidebar-workspaces-list carry
   (Swarf-AI/webapp#4265), so History and Workspaces read identically. */
body.theme-mc .sidebar-collapsible-list li.active a {
  background-color: var(--color-surface-raised);
  box-shadow: var(--color-border) 0px 0px 0px 1px inset;
}

/* Conversation rows read smaller than their "History" parent label,
   reusing the same value .sidebar-workspace-row already uses for
   Workspaces' own child rows — Perplexity's reference steps down one
   size from parent to child in both its Spaces and History sections. */
#sidebar-conversations a.sidebar-nav-item {
  font-size: 0.78rem;
  /* The row's kebab (#conv-menu-btn-*) is an absolutely-positioned sibling
     of this link, not a child of it, so nothing in the link's own layout
     reserves room for it. Clear its ~30px footprint (right: 0.375rem + w-6)
     so a truncated title's ellipsis stops short of the icon instead of
     rendering underneath it (Swarf-AI/webapp#4211). */
  padding-right: 2rem;
}

/* Workspace-membership indicator on a sidebar conversation row (Swarf-AI/webapp#3136,
   redesigned from a repeated text-name chip to an icon in Swarf-AI/webapp#4317 — a
   leading, fixed-size icon can never compete with the title's truncation
   space the way the old chip's margin-left:auto/flex-shrink:0 shape did,
   so the whole class of overlap bug Swarf-AI/webapp#4316 patched is structurally
   impossible here rather than merely fixed (its guard-floor rule and its
   own regression test are removed along with the chip, Swarf-AI/webapp#4317 — a ~20px
   fixed-size leading icon needs no shrink floor to protect). No workspace
   name renders anywhere in this row by design — see the row's own title
   tooltip (data-tip) for the conversation title, not the workspace's. */
.sidebar-ws-icon {
  flex-shrink: 0;
  color: var(--color-muted);
}

/* Sidebar nav item typography and spacing: matches mock (13.44px, 8px 9.6px padding). */
body.theme-mc a.sidebar-nav-item {
  font-size: 0.84rem;
  line-height: normal;
  padding: 0.5rem 0.6rem;
}

/* Dual-affordance sidenav collapsible-row mechanism (Swarf-AI/webapp#3789, generalized to
   N instances Swarf-AI/webapp#3843): the toggle chevron is one element, rotated (not
   swapped between two glyphs) to reflect collapsed/expanded — right when
   collapsed, down when expanded. The button overlays the label's icon-zone
   (left side, where the chevron itself renders on hover) — not the row's
   right edge — so the invisible click target lines up with the visible
   glyph. `.sidebar-collapsible-toggle` carries the shared position/size;
   `.sidebar-workspaces-toggle`/`.sidebar-history-toggle` (co-present on
   each instance's button) exist only so tests and any instance-specific
   overrides can scope to one toggle without touching the others. */
.sidebar-collapsible-toggle {
  position: absolute;
  left: 0;
  top: 0;
  width: 2.6rem;
  height: 100%;
  cursor: pointer;
}

.chevron-icon {
  /* Same micro-transform idiom as .rp-desc-more svg below. */
  transition: transform 0.15s ease;
}

li[data-expanded="false"] .chevron-icon {
  transform: rotate(-90deg);
}

/* An expanded Workspaces/History row reads identical to a collapsed one at
   rest — no persistent background just from being expanded (Swarf-AI/webapp#3905, matching
   the Perplexity reference Swarf-AI/webapp#3789/Swarf-AI/webapp#3843 modeled on). The row's hover and
   active backgrounds are the only ones that apply, same as a collapsed row. */

/* Collapsed by default; JS flips data-expanded to reveal the list. */
li[data-expanded="false"] > .sidebar-collapsible-list {
  display: none;
}

/* Expand motion is deliberately ASYMMETRIC (Swarf-AI/webapp#4071, matching the Perplexity
   reference this row is modeled on — Swarf-AI/webapp#3789/Swarf-AI/webapp#3843): a small unfold on
   expand, instant collapse. Collapse staying display: none is what makes
   the cheap shape work — no height animation means no grid-rows wrapper,
   no min-height: 0 footgun, and collapsed rows stay out of the tab order
   for free. The keyframes run natively each time display flips from none
   (animations restart on display, unlike transitions) — AND at element
   creation, so a section that ships expanded (History) plays the reveal
   once on every full page load. That load-time unfold is accepted behavior,
   QA'd and approved with the rest of the motion; tie the animation to an
   interaction-added class if it ever needs to be click-only. */
@keyframes sidenav-list-reveal {
  from {
    opacity: 0;
    transform: translateY(-0.25rem);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

li[data-expanded="true"] > .sidebar-collapsible-list {
  animation: sidenav-list-reveal 0.15s ease;
}

@media (prefers-reduced-motion: reduce) {
  .chevron-icon {
    transition: none;
  }
  li[data-expanded="true"] > .sidebar-collapsible-list {
    animation: none;
  }
}

/* Child workspace rows read as subdued sub-items, not full nav items. */
.sidebar-workspace-row {
  font-size: 0.78rem;
}

body.theme-mc .sidebar-workspace-row {
  color: var(--color-muted);
}

body.theme-mc .sidebar-workspace-row:hover {
  color: var(--color-base-content);
}

/* Zero-workspaces empty-state row (Swarf-AI/webapp#4234) — same type scale as a real
   workspace row, permanently muted (no hover state) since it's static text. */
.sidebar-workspace-empty {
  font-size: 0.78rem;
}

body.theme-mc .sidebar-workspace-empty {
  color: var(--color-muted);
}

/* Sidebar section labels (Recent, etc.): monospace, smaller, bolder, more tracked. */
body.theme-mc .sidebar-section-label {
  font-family: var(--brand-font-mono);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  color: var(--color-muted-dim);
  /* Match mock .side-label padding so RECENT aligns with the convo titles
     below (0.5rem left inset) instead of inheriting the template's px-3. */
  padding: 0.3rem 0.5rem 0.35rem;
}

/* Conversation top bar: hairline divider + mock's padding (Swarf-AI/webapp#2937 parity). */
body.theme-mc #conversation-header {
  border-bottom: 1px solid color-mix(in srgb, var(--color-base-content) 8%, transparent);
  padding: 0.85rem 1.1rem;
}

/* The hairline marks an active conversation. The new-conversation header
   stays in the DOM as an empty OOB target (see conversation.html), so an
   unconditional border paints an orphan line over nothing (Swarf-AI/webapp#2966). Gate on
   the title: :empty flips off live when streaming.js sets textContent — no
   reload, no JS. Color-only suppression keeps the 1px slot occupied, so the
   title's arrival never shifts layout (the header's no-jump contract).
   Guarded both directions by the Swarf-AI/webapp#2966 stateAgreement pairs in
   scripts/layout-invariants/invariants/app-shell.js. */
body.theme-mc #conversation-header:has(#conversation-title:empty) {
  border-bottom-color: transparent;
}

/* Composer card: hairline border instead of solid base-300. */
body.theme-mc #message-form-card {
  border-color: var(--color-border-subtle);
}

/* Composer textarea font-size matching mock's .cwrap textarea (Swarf-AI/webapp#2937). */
body.theme-mc #message-form textarea {
  font-size: 0.84rem;
}

/* Send icon: dark ink on orange, matching mock's .cwrap-send (Swarf-AI/webapp#2937) —
   base-100 rather than --brand-charcoal, which is shared by unrelated
   light-surface uses this button doesn't have. */
body.theme-mc #submit-btn {
  color: var(--color-base-100);
}

/* Generated-artifact message card: mock's .mfile--gen compact density
   (Swarf-AI/webapp#2937). Scoped to the id prefix, not the shared attachment_card element's
   classes, so setup_card.html's cards (id="setup-card-*")
   are unaffected. Border color/style (orange dashed) is untouched — a
   deliberate "unsaved" signal added in f0765b3ea, not a gap. */
body.theme-mc [id^="artifact-card-"] > div {
  padding: 0.4rem 0.6rem;
}

body.theme-mc [id^="artifact-card-"] .text-sm {
  /* Literal px, not rem: mock's 13.3333px doesn't round cleanly to a rem
     step, and this is compared for exact equality by the parity engine. */
  font-size: 13.3333px;
}

/* Composer focus: orange border accent matching mock's .cwrap:focus-within treatment. */
body.theme-mc #message-form-card:focus-within {
  border-color: var(--brand-orange);
}

/* Composer pill: "N in context" button matching mock .cwrap-ctx pattern. */
body.theme-mc .composer-ctx-pill {
  font-family: var(--brand-font-mono);
  font-weight: 700;
  font-size: 0.58rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-muted-dim);
  background: var(--color-surface-raised);
  border: 1px solid color-mix(in srgb, var(--color-base-content) 12%, transparent);
  border-radius: 999px;
  padding: 0.3rem 0.65rem;
  cursor: pointer;
  white-space: nowrap;
  transition: border-color 0.15s ease;
}

body.theme-mc .composer-ctx-pill .context-count {
  color: var(--brand-orange-deep);
  font-size: 0.68rem;
}

/* Workspace-name suffix (Swarf-AI/webapp#3266) — the mock's "· ST-30 Cell" grammar
   (.cwrap-ws): quieter than the pill's own label so the count stays the
   focal point. Its own link (Swarf-AI/webapp#3719) — sibling of .composer-ctx-pill, not
   nested inside it, since it navigates to the workspace instead of
   opening the Resource Library. */
body.theme-mc .context-ws {
  /* inline-block: max-width/text-overflow are inert on inline elements */
  display: inline-block;
  vertical-align: bottom;
  text-decoration: none;
  font-family: var(--brand-font-mono);
  font-size: 0.58rem;
  letter-spacing: 0.08em;
}

/* Icon-first badge (Swarf-AI/webapp#5007) — icon in front of the name, one flat color
   throughout. Desktop shows both; mobile drops the name to icon-only
   (mirrors the web-search pill's own desktop-label/mobile-icon split). */
body.theme-mc .context-ws-icon {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  max-width: 11rem;
  color: var(--color-muted);
}

/* Ellipsized so a long workspace name can't stretch across the composer —
   nowrap is what makes the ellipsis pair actually fire (Swarf-AI/webapp#4481: without it
   the name letter-wrapped into a tower when a narrow board-panel column
   squeezed the composer), and it makes this element the row's designated
   shrink point: overflow:hidden already zeroes its automatic flex minimum,
   so it truncates progressively — to nothing at extreme squeeze — before
   anything can overflow the card. */
body.theme-mc .context-ws-icon .context-ws-reveal {
  display: inline-block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* @media (hover: hover) and (pointer: fine): touch devices simulate :hover
   on tap with no real "hover" concept behind it, so an unguarded :hover
   rule flashes briefly on every tap before the click fires — this guard
   scopes hover-only affordances to devices that can genuinely sustain a
   hover state (mouse/trackpad), leaving touch as a clean tap-and-go.
   Applied to every :hover rule on this row, mock and pre-existing alike
   (Swarf-AI/webapp#5007 live feedback). */
@media (hover: hover) and (pointer: fine) {
  body.theme-mc .context-ws-icon:hover {
    color: var(--color-base-content);
  }

  body.theme-mc .context-ws-icon:hover .context-ws-reveal {
    text-decoration: underline;
    text-underline-offset: 2px;
  }
}

/* Icon-only, mobile only — see the block comment above. */
@media (max-width: 56.25rem) {
  body.theme-mc .context-ws-icon {
    justify-content: center;
    max-width: none;
  }

  body.theme-mc .context-ws-icon .context-ws-reveal {
    display: none;
  }
}

@media (hover: hover) and (pointer: fine) {
  body.theme-mc .composer-ctx-pill:hover {
    border-color: var(--brand-orange);
  }
}

body.theme-mc .composer-ctx-pill:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Web search pill (Swarf-AI/webapp#3242) — composer-level echo of the toggle's on-state,
   matching ChatGPT's persistent "Search" pill (design validated in the mock,
   Swarf-AI/webapp#2587). Composes .composer-ctx-pill for the shared pill shape (the markup
   stacks both classes); this block carries only the deltas — accent
   color/fill since the pill only renders while on, and the icon-swap-to-"x"
   hover (click-to-remove affordance) via the two-span show/hide below. */
body.theme-mc .web-search-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  color: var(--brand-orange-deep);
  /* 10% read ~4.49:1 against the workspace-detail composer's slightly
     lighter surface (Swarf-AI/webapp#4292) — a marginal sub-AA miss the main conversation
     composer's own background happened not to trip. 6% darkens the
     composited bg enough to clear 4.5:1 on both surfaces. */
  background: color-mix(in srgb, var(--brand-orange) 6%, transparent);
  border-color: color-mix(in srgb, var(--brand-orange) 30%, transparent);
}

/* Light theme: orange-deep text on the 10% tint reads 2.74:1 (sub-AA, caught
   by test_theme_contrast). Same fix as the sibling .board-filter-chip.is-active
   override above (Swarf-AI/webapp#3059/Swarf-AI/webapp#3060's pattern): solid orange fill + white text
   (~4.88:1 against light's darkened --brand-orange). */
body.theme-mc.theme-light .web-search-pill {
  color: var(--brand-white);
  background: var(--brand-orange);
  border-color: var(--brand-orange);
}

.web-search-pill-icon,
.web-search-pill-remove {
  flex: 0 0 auto;
}

.web-search-pill-remove {
  display: none;
}

/* No room for a label at composer width alongside the workspace badge (Swarf-AI/webapp#5007). */
@media (max-width: 56.25rem) {
  .web-search-pill-label {
    display: none;
  }
}

/* /review-pr caught this one: the touch-tap-flash guard above claimed
   "every :hover rule on this row" but missed this pill — same row
   (input_area.html's composer toolbar), same bug shape (a hover-triggered
   display swap, arguably worse than a color change to flash on tap). */
@media (hover: hover) and (pointer: fine) {
  .web-search-pill:hover .web-search-pill-icon {
    display: none;
  }

  .web-search-pill:hover .web-search-pill-remove {
    display: inline-flex;
  }
}

/* The + menu item's on-state checkmark rides at the row's far edge,
   mirroring the mock's .web-search-toggle-check placement */
.web-search-menu-check {
  margin-left: auto;
  color: var(--brand-orange-deep);
}

/* Composer + menu — native Popover API (Swarf-AI/webapp#2775, matching Swarf-AI/webapp#2646's sidebar
   popover pattern). anchor-name/position-anchor/position-area replace
   DaisyUI's dropdown-top positioning: unlike Swarf-AI/webapp#2646's fixed-position sidebar
   trigger, this button's viewport position is dynamic (moves with
   sidebar/board-panel state), confirmed live, so a static position:fixed
   offset (Swarf-AI/webapp#2646's approach) doesn't generalize here.

   Display-gating (display:none / :popover-open{display:flex;flex-direction:
   column}) comes from the shared [popover] base rule near the top of this
   file (Swarf-AI/webapp#2781) — don't redeclare it here. */
.composer-add-btn {
  anchor-name: --composer-add-anchor;
}
.composer-add-menu {
  position-anchor: --composer-add-anchor;
  position-area: top span-right;
  margin: 0;
  /* DaisyUI's .menu utility sets overflow: auto, which clips the
     "Add to workspace" item's .ws-picker-submenu flyout the same way it
     clipped .conv-row-menu's identical submenu before Swarf-AI/webapp#4031's fix below —
     same mechanism, third mount point (Swarf-AI/webapp#4482). See .conv-row-menu's own
     comment for the measured explanation. */
  overflow: visible;
}

/* Artifact viewer header: attach-to-machine popover (Swarf-AI/webapp#3089). Same
   anchor-positioning pattern as composer-add-btn/-menu above — the button's
   position in the header is static within the fragment, but reusing the
   established mechanism keeps behavior (Esc, outside-click, focus) uniform
   across every popover in the app. */
.artifact-attach-btn {
  anchor-name: --artifact-attach-anchor;
}
.artifact-attach-menu {
  position-anchor: --artifact-attach-anchor;
  position-area: bottom span-left;
  margin: 0;
}

/* Artifact viewer header: export menu (Swarf-AI/webapp#4879) — combines PDF + Download
   into one dropdown. Same anchor-positioning pattern as artifact-attach
   above, same toolbar. */
.export-menu-btn {
  anchor-name: --export-menu-anchor;
}
.export-menu {
  position-anchor: --export-menu-anchor;
  position-area: bottom span-left;
  margin: 0;
}

/* Sidebar conversation row menu (Swarf-AI/webapp#3901) — the rename trigger, on the row
   itself. Unlike every popover above, this one is REPEATED: each History row
   renders its own, so no unique `anchor-name` per row is possible. It needs
   none — a popover opened by its `popovertarget` invoker gets that button as
   its IMPLICIT anchor, so this single rule positions every row's menu against
   its own trigger (measured: two rows 238px apart, identical offsets from
   their own buttons).

   That implicit association exists ONLY when the popover is opened via its
   invoker. A programmatic `showPopover()` establishes no invoker and the menu
   collapses to the viewport's top-left corner — if this ever needs to open
   from JS, pass `{ source: btn }`.

   A shown popover promotes to the top layer, which is the whole reason a menu
   works on these rows at all: #sidebar-conversations is `overflow-y-auto` and
   clips ordinary absolutely-positioned children (measured — an absolute box
   and a popover with identical geometry, only the popover hit-tests outside
   the scroll container).

   Display gating comes from the shared [popover] base rule near the top of
   this file (Swarf-AI/webapp#2781) — don't redeclare it here. */

/* ── Row kebab trigger (Swarf-AI/webapp#4573) ──────────────────────────────────────────────
   The one kebab-trigger treatment for every conversation row — the shell
   sidebar's rows and the conversation-list rows (/workspaces/<id>/,
   /conversations/) alike. The row carries .kebab-host; the button carries
   .row-kebab; the menu it opens is .conv-row-menu below.

   Reveal is opacity, not display/visibility: display:none would drop the
   button from the tab order entirely, leaving keyboard users no path to the
   row's actions. The paired pointer-events gate stops the invisible button
   from swallowing clicks meant for the row's link, and :focus-visible brings
   it back for tabbing. Values settled by Swarf-AI/webapp#4573's live measurement
   (scripts/reskin-parity/manifests/row-kebab.js): 24px box on the Tailwind
   scale, the 6px radius and fade reveal from the conversation-list rows'
   deliberate design, --color-muted as the rest ink. */
.row-kebab {
  position: absolute;
  right: 0.375rem;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 6px;
  color: var(--color-muted);
  opacity: 0;
  pointer-events: none;
  cursor: pointer;
  transition:
    opacity 0.15s ease,
    background-color 0.15s ease,
    color 0.15s ease;
}

/* The host supplies the trigger's containing block itself, so a new row
   type adopting the pair can't forget to be positioned. */
.kebab-host {
  position: relative;
}

.row-kebab:focus-visible {
  opacity: 1;
  pointer-events: auto;
}

/* Gated to real-hover devices (Swarf-AI/webapp#5000) — a synthesized :hover from a tap
   never clears via mouseleave, so a touch device would show every row's
   kebab as permanently revealed instead of only on genuine mouse hover. */
@media (hover: hover) and (pointer: fine) {
  .kebab-host:hover .row-kebab {
    opacity: 1;
    pointer-events: auto;
  }

  .row-kebab:hover {
    background: color-mix(in srgb, var(--color-base-content) 10%, transparent);
    color: var(--color-base-content);
  }
}

/* Always visible on mobile (Swarf-AI/webapp#5007) — a touch device has no hover state to
   reveal it on, and losing the Swarf-AI/webapp#5000 fix above meant losing the only path
   to a row's actions (rename/pin/delete/move-workspace) entirely. */
@media (max-width: 56.25rem) {
  .row-kebab {
    opacity: 1;
    pointer-events: auto;
  }
}

.conv-row-menu {
  position-area: bottom span-left;
  /* Flips to top span-left when there's no room below the trigger (Swarf-AI/webapp#4246)
     — a row near the bottom of a long, scrolled History list otherwise
     always renders below, clipping off the viewport's bottom edge. */
  position-try-fallbacks: flip-block;
  margin: 0;
  /* DaisyUI's .menu utility sets overflow: auto (for long menus that need
     to scroll internally) — harmless for this menu's own short, fixed item
     list, but it clips ANY descendant that pokes outside the menu's own box
     via position: absolute, which is exactly what .ws-picker-submenu does
     (Swarf-AI/webapp#4031). Top-layer promotion (this element is a shown [popover]) only
     exempts the popover ITSELF from being clipped by an ANCESTOR's
     overflow — it does nothing for a descendant being clipped by the
     popover's OWN overflow. Measured live: the flyout rendered with a
     correct, unclipped bounding rect only after this override. */
  overflow: visible;
}

/* Delete confirm item divider (Swarf-AI/webapp#4195) — separates the destructive Delete
   item from the rest of the conv-row-menu, matching the reference mock's
   structure. Danger coloring on the item itself rides the text-error
   utility (theme-mc auto-routes it to --color-error-content) rather than
   a bespoke class. */
.conv-row-menu-divider {
  margin: 0.25rem 0;
  border-top: 1px solid var(--color-base-300);
}

/* Workspace picker flyout (Swarf-AI/webapp#4031) — a plain nested submenu, not its own
   popover. It's a descendant of .conv-row-menu, which is already promoted
   to the top layer while shown (escaping the sidebar's overflow clipping),
   so this inherits that promotion for free and needs no anchor-name/
   position-area machinery of its own — just ordinary absolute positioning
   relative to its trigger <li>. Shown on hover OR focus-within (desktop
   pointer and keyboard both work; :focus-within also covers a tap that
   focuses the trigger button on platforms where tap synthesizes focus).

   CSS anchor positioning was tried for viewport-collision handling here
   (Swarf-AI/webapp#4246) and abandoned — confirmed via isolated repro that Chromium
   doesn't support an anchor-positioned descendant inside an
   anchor-positioned ancestor: `.conv-row-menu` is itself anchor-positioned
   (self-anchored to its own popover invoker, needed for its own Swarf-AI/webapp#4246
   fix below), and ANY nested `position-anchor` reference inside it
   resolves incorrectly regardless of anchor-name uniqueness or
   anchor-scope — removing the ancestor's own anchor-positioning restored
   correct nested behavior, isolating the cause. Viewport-aware
   positioning for this submenu instead lives in JS
   (workspace-picker-position.js) via Floating UI — already loaded and
   used for exactly this "flip/shift at viewport edges" need in
   floating-tooltip.js. */
.ws-picker-trigger {
  position: relative;
}

/* Bridges the gap between this trigger and its flyout submenu (Swarf-AI/webapp#4212):
   that gap (~7px, see .ws-picker-submenu's own offset below) is otherwise
   unpainted space, and a diagonal mouse move across it drops out of both
   the trigger's own box and the submenu's — nothing left in the
   .ws-picker-trigger hover chain to keep display:block satisfied mid-
   transit, so the submenu closes before the cursor arrives. A transparent
   pseudo-element spanning that gap is still a descendant of
   .ws-picker-trigger, so hovering it keeps :hover true across the crossing.
   Height is the trigger's own box plus a small margin for a natural
   diagonal approach — the submenu's height is dynamic (row count), so
   matching it would mean guessing a tall value that bleeds over unrelated
   sibling items in the same popover. */
.ws-picker-trigger::after {
  content: "";
  position: absolute;
  left: 100%;
  top: -0.5rem;
  bottom: -0.5rem;
  width: 0.5rem;
}

/* body.theme-mc prefix is load-bearing, not decoration (matches this
   file's own convention elsewhere): DaisyUI's .menu utility also sets
   display: flex at equal (0,1,0) specificity, and whichever rule is LATER
   in the compiled stylesheet order wins a specificity tie — measured live,
   DaisyUI's was winning, so an unprefixed display: none here never took
   effect and the submenu stayed visible even while genuinely un-hovered. */
body.theme-mc .ws-picker-submenu {
  display: none;
  position: absolute;
  /* The containing block is the trigger <li>, whose right edge sits inset
     from .conv-row-menu's OWN right edge by that menu's border (1px) +
     padding (0.25rem) — so a bare left: 100% lands 5px inside the parent
     and the flyout visibly clips into it (Swarf-AI/webapp#4184, measured 5px). Clear that
     shortfall, then add a small deliberate gap so the two surfaces read as
     separate panels. Coupled to the shared menu rule's border/padding
     below — if either changes, re-derive this offset. The noOverlap
     invariant (scripts/layout-invariants/) fails if it regresses.
     top/left are the STATIC default position; workspace-picker-position.js
     overrides both via inline style once shown, computing a viewport-
     aware placement (flip above the trigger when there's no room below,
     Swarf-AI/webapp#4246) — see that file and .ws-picker-trigger's own comment above for
     why this couldn't be done in pure CSS. These values remain as the
     pre-JS/no-JS fallback and the base the JS measures against. */
  left: calc(100% + 1px + 0.25rem + 2px);
  top: -0.25rem;
  margin: 0;
  z-index: 1;
}

body.theme-mc .ws-picker-trigger:hover .ws-picker-submenu,
body.theme-mc .ws-picker-trigger:focus-within .ws-picker-submenu {
  display: block;
}

body.theme-mc .ws-menu-search {
  width: 100%;
  background: transparent;
  border: none;
  outline: none;
  color: var(--color-base-content);
  font-family: var(--brand-font-primary);
  font-size: 0.82rem;
  /* Not redundant: DaisyUI's menu-item hover rule (see .ws-menu-empty
     below) would otherwise put a pointer cursor on this input. */
  cursor: text;
  /* Mirrors the shared menu-item padding below, which only covers
     button/label content — so the input aligns with the items around it. */
  padding: 0.45rem 0.7rem;
}

body.theme-mc .ws-menu-search::placeholder {
  color: var(--color-muted);
}

/* The picker's checked marker trails its label, mirroring the composer
   scope picker's own .resource-scope-check (Swarf-AI/webapp#3944) — one glyph convention
   for "this is the current selection" across both menus. margin-left: auto
   on the trailing check itself (not justify-content: space-between on the
   row) is what that sibling rule actually uses — space-between distributes
   space between EVERY flex child, so it only reads as "trailing" when the
   check is the row's last child; get the order wrong and it shoves the
   check and label to opposite edges of the row instead of keeping them
   snug (found live). */
body.theme-mc .ws-menu-item .ws-menu-check {
  margin-left: auto;
}

body.theme-mc .ws-menu-item--active {
  color: var(--color-base-content);
}

/* DaisyUI's .menu hover rule sets a background tint and cursor: pointer on
   any direct <li> child it doesn't exclude (ul/menu/details/.menu-title).
   A bare <p> isn't excluded, so this non-interactive empty state read as a
   clickable menu item. Declaring both here wins at every hover state: real
   specificity always beats DaisyUI's :where()-wrapped selector. */
body.theme-mc .ws-menu-empty {
  padding: 0.45rem 0.7rem;
  color: var(--color-muted);
  font-size: 0.82rem;
  background: transparent;
  cursor: default;
}

/* Resource row scope picker (Swarf-AI/webapp#3944) — one menu per row, positioned off the
   implicit anchor its popovertarget invoker gives it (frontend.md §Menus);
   without a position-area every instance stacks at the viewport's top-left,
   the unanchored default. span-right: the trigger sits at the row's left
   edge, so the menu has room to open rightward. */
.resource-scope-menu {
  position-area: bottom span-right;
  /* Flips to top span-right when there's no room below the trigger (Swarf-AI/webapp#4260,
     same fix as .conv-row-menu's Swarf-AI/webapp#4246) — a row near the bottom of a
     scrolled Resource Library list otherwise always renders below,
     clipping off the viewport's bottom edge. */
  position-try-fallbacks: flip-block;
  margin: 0;
}

/* Workspace card's own kebab menu (Swarf-AI/webapp#3948) — one per card on the
   /workspaces/ list page, holding a single Pin/Unpin toggle. Same
   implicit-anchor-via-popovertarget shape as .conv-row-menu (see that
   rule's own comment for the mechanism); span-right since the card grid
   has no reliable "room to the left" the way the sidebar's fixed-width
   column does. */
.workspace-row-menu {
  position-area: bottom span-right;
  position-try-fallbacks: flip-block;
  margin: 0;
}

/* Composer + button: same visual weight as the pill — monospace, uppercase character. */
body.theme-mc .composer-add-btn {
  font-family: var(--brand-font-mono);
  font-weight: 700;
  font-size: 1rem;
  color: var(--brand-gunmetal);
}

@media (hover: hover) and (pointer: fine) {
  body.theme-mc .composer-add-btn:hover {
    color: var(--brand-orange);
  }
}

/* Composer controls sized to the mock's 32px send button (DaisyUI btn-square
   defaults to 40px). Keeps the context row compact instead of pushing it taller
   than the textarea row above it. */
body.theme-mc #submit-btn,
body.theme-mc #stop-btn,
body.theme-mc .composer-add-btn {
  width: 2rem;
  height: 2rem;
  min-height: 2rem;
}

/* Canonical dropdown menu — the composer + menu (native popover, Swarf-AI/webapp#2775), the
   board's "+ New resource" menu (Swarf-AI/webapp#2837), the sidebar conversation row's own
   menu (Swarf-AI/webapp#3901), and the artifact viewer's export menu (Swarf-AI/webapp#4879) share one
   dark-surface popover: surface fill, hairline border, corner radius,
   shadow, and font. The sidebar's account-menu/credits popovers have their
   own styling (Swarf-AI/webapp#2646) and don't use this rule. Templates must NOT
   re-declare bg/border/shadow/padding/radius on these menus — this rule
   owns them so they stay consistent. */
body.theme-mc .composer-add-menu,
body.theme-mc .board-new-resource-menu,
body.theme-mc .conv-row-menu,
body.theme-mc .resource-scope-menu,
body.theme-mc .workspace-row-menu,
body.theme-mc .ws-picker-submenu,
body.theme-mc .export-menu {
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-menu);
  /* Ambient shadow for depth + a tight contact shadow for grounding, plus an
     inset hairline highlight on the top edge — three layers reading as one
     "raised panel" edge treatment, closer to claude.ai's menu chrome (Swarf-AI/webapp#4185). */
  box-shadow:
    var(--shadow-menu),
    var(--shadow-menu-contact),
    inset 0 1px 0 var(--color-border-subtle);
  padding: 0.25rem;
  font-family: var(--brand-font-primary);
  font-size: 0.82rem;
  font-weight: 400;
  letter-spacing: normal;
}

/* The picker's checked marker trails its label, mirroring the composer
   menu's web-search item (Swarf-AI/webapp#3242) so the two menus read the same. */
body.theme-mc .resource-scope-menu .resource-scope-check {
  margin-left: auto;
  color: var(--brand-orange);
}

/* Hover quick-look portal (Swarf-AI/webapp#2656/Swarf-AI/webapp#2870) — raised-surface popover matching
   the mock's .ctx-pop (Swarf-AI/webapp#3003). Flex-column (Tailwind classes in
   floating-tooltip.js, not here — see that file's comment on why) so
   header/iframe children fill remaining space after padding, instead of a
   fixed iframe height forcing the container past the Swarf-AI/webapp#2881-matched 500x440
   footprint. */
#floating-preview {
  height: 440px;
  background: var(--color-surface-raised);
  border-color: var(--color-border);
  padding: 0.9rem 1.1rem;
  /* Entry animation (Swarf-AI/webapp#3005) — the mock's .ctx-pop settle: fade + nudge over
     0.15s. floating-tooltip.js adds .peek-enter on show and drops it a frame
     later; transition only (no transition on left/top — those are set by
     Floating UI and must not animate). */
  transform-origin: center left;
  transition: opacity 0.15s ease, transform 0.15s ease;
}

#floating-preview.peek-enter {
  opacity: 0;
  transform: translateX(-6px) scale(0.97);
}

/* Loading state (#2276) — an empty portal means the lazy fragment fetch is
   still in flight (the source div is authored empty and cloned before the
   response lands; the afterSwap re-clone un-empties it), so :empty IS the
   pending signal — no JS. currentColor tracks the portal's themed
   foreground; margin auto centers in the portal's flex column. Local
   keyframes on purpose: Tailwind's `spin` exists only where templates
   happen to use animate-spin. */
#floating-preview:empty::before {
  content: "";
  margin: auto;
  width: 1.75rem;
  height: 1.75rem;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  opacity: 0.6;
  animation: peek-loading-spin 0.6s linear infinite;
}

@keyframes peek-loading-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Compact variant (Swarf-AI/webapp#3409) — the shared 440px footprint above is sized for
   resource-peek content (STEP viewers, full docs); a citation card is a
   title + domain, a few lines tall. floating-tooltip.js applies this class
   when the trigger carries data-preview-size="compact", and separately caps
   its own inline-style width at 320px instead of 500px (the size()
   middleware's apply() always wins over a CSS width — see that file's
   comment) — reverse-engineered against live Perplexity (304px/2-source)
   and ChatGPT (384px/2-source) citation cards. Height stays content-sized
   (no fixed 440px) since padding is JS-agnostic and safe to own here. */
#floating-preview.floating-preview--compact {
  height: auto;
  padding: 0;
}

/* Inline resource peek (Swarf-AI/webapp#3005) — the same profile composition as the board
   tab, scaled-down chrome per the mock's .ctx-pop .rp rules. The full tab's
   header sizing comes from .resource-doc-header rules; these tighten the
   peek copy only. */
.resource-peek .resource-doc-header h2 {
  font-size: 1.1rem;
}

.resource-peek .resource-doc-tile {
  width: 40px;
  height: 40px;
}

/* Read-more's fade-to-background gradient (.rp-desc-clip::after above)
   hardcodes --color-base-100 — correct on the board tab, which sits
   directly on the page background, but the peek's own container is
   --color-surface-raised (a step lighter, #floating-preview below), so
   the fade painted a visibly mismatched band behind the button. Override
   the fade's end color to match the peek's real background. */
.resource-peek .rp-desc.clamped:not(.open) .rp-desc-clip::after {
  background: linear-gradient(to bottom, transparent, var(--color-surface-raised));
}

/* Inline file peek (Swarf-AI/webapp#3021) — fill the stage: _file_content.html's image
   classes never upscale (a 1x1 fixture once drew a 1px dot in the popover),
   so the peek overrides img sizing to fill. Carried as an inline <style> by
   the old iframe document; the fragment shares this stylesheet instead. */
.file-peek img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* 3D viewport chrome (Swarf-AI/webapp#2881, migrated from the deleted iframe doc's inline
   <style> in Swarf-AI/webapp#3022) — matches the mock's .fpop-stage. Dark is the
   absent-class default (matching brand.css's convention); explicit
   body.theme-light overrides it below. */
.stage-3d {
  flex: 1 1 auto;
  display: flex;
  min-height: 0;
  /* The shorthand carries a real background-color, not just the gradient:
     the contrast scanner (and any backgroundColor-compositing reader) sees
     only the color, so a color-less gradient scores overlay text against
     whatever sits behind the stage. */
  background: #2b2c30 linear-gradient(180deg, #3b3c41, #2b2c30);
}

.peek-meta-footer {
  flex: 0 0 auto;
  padding: 0.35rem 0.75rem;
  font-family: var(--brand-font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.04em;
  color: var(--color-muted-dim);
  border-top: 1px solid var(--color-border-subtle);
}

body.theme-light .stage-3d {
  background: #e2e0db linear-gradient(180deg, #f7f6f4, #e2e0db);
}

/* A 3D viewer that has the wheel (#2995). Viewers stay inert until clicked,
   so the page scrolls past them; this is how the user tells the click took
   and that a wheel turn will now move the camera. 3d-viewer.js toggles it on
   the canvas's parent in every host. An inset outline rather than a border,
   so activating shifts no layout. */
.is-viewer-active {
  outline: 2px solid var(--color-primary);
  outline-offset: -2px;
}

/* Inline artifact peek (Swarf-AI/webapp#3023) — docx_to_styled_html already carries its own
   inline style= attributes (font sizes, alignment, cell shading, borders);
   only the container-level polish the old iframe document's <style> block
   provided (font family, line-height, base spacing) survives here. */
.artifact-peek {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  overflow-y: auto;
}

.artifact-peek p {
  margin: 0.5rem 0;
}

/* Task-list checkboxes (markdown task_lists plugin, Swarf-AI/webapp#3159) are real
   <input type="checkbox"> elements. Native form-control chrome is
   theme-dependent on screen and absent entirely in Chromium's print
   pipeline — an unstyled checkbox prints as a solid filled square — so the
   frame owns the drawing: appearance none + an explicit bordered box.
   print-color-adjust keeps the checked fill visible when the browser's
   print dialog strips background graphics (Swarf-AI/webapp#3406). */
.artifact-peek input[type="checkbox"] {
  appearance: none;
  width: 0.85em;
  height: 0.85em;
  margin-right: 0.35em;
  vertical-align: -0.05em;
  border: 1.5px solid color-mix(in srgb, var(--brand-charcoal) 55%, transparent);
  border-radius: 3px;
  background: var(--brand-white);
  print-color-adjust: exact;
}

.artifact-peek input[type="checkbox"]:checked {
  background: var(--brand-charcoal);
  box-shadow: inset 0 0 0 2px var(--brand-white);
}

/* Artifact document frame (Swarf-AI/webapp#2659, mock's .doc-page/.doc-page--mini) — a
   generated document reads as a physical paper deliverable, not app UI: fixed
   light background and dark text regardless of the active app theme, same as
   the mock (confirmed via its own theme-mc/theme-dark overrides, which only
   retint the paper's tone — never switch it dark). --brand-* tokens are
   already global, fixed values (brand.css), so no scoped custom properties
   are needed to hold this look steady across themes. .doc-page--mini is the
   hover-peek's compact variant of the exact same frame (one partial, one
   size modifier — see _artifact_document_frame.html).*/
.doc-page {
  max-width: 800px;
  margin: 0 auto;
  background: var(--brand-white);
  border: 1px solid color-mix(in srgb, var(--brand-charcoal) 10%, transparent);
  border-radius: 4px;
  box-shadow: 0 10px 30px color-mix(in srgb, var(--brand-charcoal) 10%, transparent);
  padding: 2.2rem 2.6rem;
  color: var(--brand-charcoal);
}

.doc-page h1 {
  font-size: 1.15rem;
  font-weight: 700;
  margin: 0 0 0.6rem;
}

.doc-foot {
  margin-top: 1.8rem;
  padding-top: 0.7rem;
  border-top: 1px solid color-mix(in srgb, var(--brand-charcoal) 10%, transparent);
  font-family: var(--brand-font-mono);
  font-size: 0.64rem;
  /* 70% mix against .doc-page's white background reads ~5:1 — clears the
     4.5:1 WCAG AA text threshold with margin (45% read ~2.55:1, Swarf-AI/webapp#4912). */
  color: color-mix(in srgb, var(--brand-charcoal) 70%, transparent);
}

/* Mini: the hover-peek's compact projection of the same frame. */
.doc-page--mini {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  max-width: none;
  margin: 0;
  box-shadow: none;
  border-radius: 8px;
  padding: 1rem 1.2rem;
}

/* .artifact-peek's own overflow-y: auto (triptych.css above) needs a
   bounded height to scroll within — .doc-page--mini's flex participation
   (above) shrinks it to the #floating-preview's fixed 440px column instead
   of growing to the preview_html's natural content height. */
.doc-page--mini .artifact-peek {
  flex: 1 1 auto;
  min-height: 0;
}

.doc-page--mini h1 {
  font-size: 0.92rem;
}

.doc-page--mini .doc-foot {
  margin-top: 1rem;
  font-size: 0.58rem;
}

/* Gated on body:has(.doc-page) — triptych.css loads app-wide (every page
   extends templates/allauth/layouts/app.html), so an unscoped `body *`
   hide-everything rule would blank out printing on every other page too.
   .doc-page only exists when an artifact document frame is actually on the
   page (the opened board tab or the hover peek), so this rule only takes
   effect there. */
@media print {
  body:has(.doc-page) * {
    visibility: hidden;
  }

  body:has(.doc-page) .doc-page,
  body:has(.doc-page) .doc-page * {
    visibility: visible;
  }

  body:has(.doc-page) .doc-page {
    position: absolute;
    inset: 0;
    max-width: none;
    box-shadow: none;
    border: none;
  }
}

/* Scoped to button/label/a — the element types used as real menu-item
   content (Swarf-AI/webapp#4202, Swarf-AI/webapp#4203; `a` added Swarf-AI/webapp#4879 for the export menu's Download/PDF
   rows, which are real anchors, not buttons — inert for every other menu
   listed here since none of them currently have an `<a>` child). An
   `li > *` here also reaches a nested submenu <ul>, a search <input>, and
   empty-state <p>: display:flex then beats the submenu's display:none
   default at the specificity tie (always expanded), and the hover tint
   below lands on non-interactive elements. */
body.theme-mc .composer-add-menu li > :is(button, label, a),
body.theme-mc .board-new-resource-menu li > :is(button, label, a),
body.theme-mc .conv-row-menu li > :is(button, label, a),
body.theme-mc .resource-scope-menu li > :is(button, label, a),
body.theme-mc .workspace-row-menu li > :is(button, label, a),
body.theme-mc .ws-picker-submenu li > :is(button, label, a),
body.theme-mc .export-menu li > :is(button, label, a) {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--color-base-content);
  padding: 0.45rem 0.7rem;
  border-radius: var(--radius-menu-item);
  font-size: 0.82rem;
  font-weight: 400;
}

/* Icons inside menu rows: one consistent size + full inherited color. CSS width
   overrides the SVG's own width/height attribute, so per-template icon sizes
   (size=13 vs size=15) and opacity utilities can't make rows drift. */
body.theme-mc .composer-add-menu li svg,
body.theme-mc .board-new-resource-menu li svg,
body.theme-mc .conv-row-menu li svg,
body.theme-mc .resource-scope-menu li svg,
body.theme-mc .workspace-row-menu li svg,
body.theme-mc .ws-picker-submenu li svg,
body.theme-mc .export-menu li svg {
  width: 14px;
  height: 14px;
  opacity: 1;
}

body.theme-mc .composer-add-menu li > :is(button, label, a):hover,
body.theme-mc .board-new-resource-menu li > :is(button, label, a):hover,
body.theme-mc .conv-row-menu li > :is(button, label, a):hover,
body.theme-mc .resource-scope-menu li > :is(button, label, a):hover,
body.theme-mc .workspace-row-menu li > :is(button, label, a):hover,
body.theme-mc .ws-picker-submenu li > :is(button, label, a):hover,
body.theme-mc .export-menu li > :is(button, label, a):hover {
  background: color-mix(in srgb, var(--brand-orange) 10%, transparent);
  color: var(--color-base-content);
}

/* The scope picker's conversation row renders disabled whenever the workspace
   scope is checked (Swarf-AI/webapp#3944): workspace membership is WHY the resource is in
   context, so the workspace row is the lever and this one must not read as
   clickable. Opting into the shared hover tint above without this would give
   an inert control an interactive affordance. */
body.theme-mc .resource-scope-menu li > :is(button, label):disabled {
  opacity: 0.55;
  cursor: default;
}

body.theme-mc .resource-scope-menu li > :is(button, label):disabled:hover {
  background: transparent;
  color: var(--color-muted);
}

/* Bordered-CTA sidebar rows: New Conversation (mock's primary action) and
   the Talk with the Founders external link (Swarf-AI/webapp#4962) share one treatment —
   border/radius/padding/weight, border lights up brand-orange on hover.
   New Conversation stays selected by its stable `[href="/"]` app route;
   the Slack link gets its own class since an external href isn't a stable
   route to select on. Each keeps its own icon treatment below: New
   Conversation hides `.sidebar-nav-icon` for a CSS `::before` "+", the
   Slack link keeps its icon visible (the Slack mark identifies the
   destination) — that divergence is why the icon/`::before` rules stay
   split rather than joining this combined selector. */
body.theme-mc a.sidebar-nav-item[href="/"],
body.theme-mc .sidebar-community-link {
  border: 1px solid var(--color-border);
  border-radius: 9px;
  padding: 0.65rem 0.9rem;
  font-weight: 700;
}

body.theme-mc a.sidebar-nav-item[href="/"]:hover,
body.theme-mc .sidebar-community-link:hover {
  border-color: var(--brand-orange);
}

body.theme-mc a.sidebar-nav-item[href="/"] .sidebar-nav-icon {
  display: none;
}

body.theme-mc a.sidebar-nav-item[href="/"]::before {
  content: "+";
  color: var(--brand-orange);
  font-weight: 700;
  font-size: 1rem;
  line-height: 1;
}

/* Dark mode tooltips: DaisyUI doesn't chain --tt-bg from --color-neutral, so set it explicitly.
   Covers both .tooltip (icon tooltips) and bare [data-tip] elements (conversation title truncation). */
body.theme-mc .tooltip,
body.theme-mc [data-tip] {
  --tt-bg: var(--color-neutral);
}

/* Starter pills: full-opacity orange border on hover — 30% opacity looks brownish on dark bg. */
body.theme-mc .starter-card:hover {
  border-color: var(--brand-orange);
}

/* === SIDEBAR USER BAR (Swarf-AI/webapp#2585) ===
   Ported from the mock's .sidebar-avatar / .sidebar-name / .sidebar-balance
   (dev-mock2.css). The mock's theme-mc fills (#1a2127 / #2c363d / #e9eef2) map
   to surface-raised / border / base-content tokens — no hard-coded hex.
   Font: the mock's source says var(--brand-font-secondary), but the mock locally
   redefines that token to JetBrains Mono — so the rendered target is monospace.
   In production --brand-font-secondary is Caviar Dreams, so we use --brand-font-mono
   to match the rendered mock (monospace is also right for $ numerals + initials). */

/* Circular initials avatar. */
body.theme-mc .sidebar-avatar {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--brand-font-mono);
  font-weight: 700;
  font-size: 0.6rem;
  letter-spacing: 0.04em;
  color: var(--color-base-content);
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: 50%;
}

/* Name label — grows to fill, ellipsizes long names (built for full names; a
   single-letter email fallback simply under-fills). Button triggers inherit
   text-align:center from UA stylesheet; override here so the name stays left. */
/* #2543 — the bezel. A Pro avatar carries one hairline ring in
   brand orange, pairing with the plan word above so status reads as a
   single signal rather than two competing ones. Deliberately not gold and
   not a gradient: this audience's own instruments signal quality by finish
   and tolerance, not by shine. Held below full strength so it stays a
   status mark rather than competing with the CTA colour at full weight.

   A box-shadow with spread paints OUTSIDE the border box, so this does not
   recolor the `1px solid var(--color-border)` every avatar already carries —
   it adds a second, concentric ring around it. That pairing is the treatment
   that was compared against the alternatives and chosen; a single orange
   ring would be `border-color` on this rule instead, and is a different
   look, not a tidier spelling of this one. */
body.theme-mc .sidebar-avatar.is-pro {
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--brand-orange) 70%, transparent);
}

/* #2543 — the brand-mark pill beside the Swarf logo. Outline
   rather than filled: a filled orange pill would carry the same weight as
   a primary button, and this is a label, not an action. */
body.theme-mc .brand-pro-pill {
  flex: 0 0 auto;
  padding: 0.05rem 0.32rem;
  border: 1px solid color-mix(in srgb, var(--brand-orange) 55%, transparent);
  border-radius: 0.25rem;
  font-family: var(--brand-font-mono);
  font-size: 0.55rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  line-height: 1.5;
  color: color-mix(in srgb, var(--brand-orange) 85%, transparent);
}

/* #2543 — the plan word beside the name in the sidebar identity.
   Separated by a middot rather than boxed as a badge: a badge would read as
   a decoration applied to the user, where this reads as part of their name,
   which is the whole design. Brand orange rather than the name's own muted
   tone, matching the avatar ring above so the two read as one signal. At a
   healthy balance it is the only orange in the sidebar; under 20% the
   credit chip's own warn state joins it, which is accepted — that state is
   uncommon and the chip is the more urgent of the two. */
body.theme-mc .sidebar-plan {
  flex: 0 0 auto;
  font-size: 0.78rem;
  color: color-mix(in srgb, var(--brand-orange) 85%, transparent);
  white-space: nowrap;
  /* Cancel the identity row's own `gap-2` (0.5rem) for this element only.
     Without it the separator sits 0.85rem from the name and 0.3rem from
     the plan word — the gap stacks on top of the dot's left margin, so the
     dot reads as belonging to the plan rather than sitting between the
     two. The margins below are then the only spacing, and symmetric. */
  margin-left: -0.5rem;
}

body.theme-mc .sidebar-plan::before {
  content: "·";
  margin: 0 0.3rem;
  color: color-mix(in srgb, var(--color-base-content) 34%, transparent);
}

body.theme-mc .sidebar-name {
  /* #2543: 0 1 auto, not 1 1 auto — the name shrinks and
     truncates as before, but no longer GROWS to fill the bar. Growing
     pushed the plan word to the far right edge, where it read as a
     right-aligned badge rather than as part of the name. Nothing follows
     the name for a free user, so their bar is unchanged. */
  flex: 0 1 auto;
  min-width: 0;
  font-size: 0.78rem;
  text-align: left;
  color: color-mix(in srgb, var(--color-base-content) 86%, transparent);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Credits chip — rounded-rect (not pill), brand-secondary numerals. State
   classes (.warn / .crit) recolor to brand orange instead of red/yellow.
   Popup is Swarf-AI/webapp#2563 — the chip is non-interactive on its own for now. */
body.theme-mc .sidebar-balance {
  flex: 0 0 auto;
  font-family: var(--brand-font-mono);
  font-weight: 700;
  font-size: 0.7rem;
  color: var(--color-base-content);
  background: color-mix(in srgb, var(--color-base-content) 4%, var(--color-surface-raised));
  border: 1px solid var(--color-border);
  border-radius: 7px;
  padding: 0.25rem 0.55rem;
  cursor: pointer;
  transition: border-color 0.15s ease;
}

body.theme-mc .sidebar-balance:hover {
  border-color: var(--color-border-subtle);
  background: color-mix(in srgb, var(--color-base-content) 8%, var(--color-surface-raised));
}

body.theme-mc .sidebar-balance.warn {
  color: var(--brand-orange);
  border-color: color-mix(in srgb, var(--brand-orange) 55%, transparent);
}

body.theme-mc .sidebar-balance.crit {
  color: var(--brand-orange-deep);
  border-color: var(--brand-orange-deep);
}

/* Sidebar popovers — native Popover API (Swarf-AI/webapp#2646). Both #account-pop and
   #credits-pop render in the top layer on open, which means normal-flow
   position:absolute-from-a-relative-ancestor no longer applies (the
   containing block becomes the viewport, not their DOM parent) — hence the
   shared position:fixed rule below instead of each anchoring to a sibling
   .sidebar-userbar. The trigger (the user bar) is pinned to the viewport's
   bottom-left corner regardless of the sidebar's width (which is
   user-draggable, Swarf-AI/webapp#2621/Swarf-AI/webapp#2808), so a static offset from that corner is
   correct and needs no CSS anchor positioning (anchor-name/position-anchor)
   — the reference point never moves even when the sidebar's right edge does.

   Display-gating (display:none / :popover-open{display:flex}) comes from
   the shared [popover] base rule above (Swarf-AI/webapp#2781) — don't redeclare it here.

   The UA popover stylesheet also sets `inset: 0; margin: auto` (the same
   mechanism that auto-centers an unstyled <dialog>) — setting only
   left/bottom here leaves top/right/margin from that UA default in effect,
   producing a contradictory box (/prove'd: rendered pinned to the viewport's
   top instead of the sidebar's bottom corner). `inset` + `margin: 0` below
   explicitly override all four UA insets, not just the two we care about.

   No z-index needed — a shown [popover] promotes into the top layer, which
   renders above the entire document regardless of z-index value. */
.account-pop,
.credits-pop {
  position: fixed;
  inset: auto auto 4.5rem 0.5rem;
  margin: 0;
  width: 15rem;
}

/* ─── Account menu — Mock 2 .account-pop shell (Swarf-AI/webapp#2613) ──────────────────────
   Base rules use brand tokens; theme-mc overrides carry Matt's palette. */
.account-pop {
  gap: 0.15rem;
  background: var(--brand-white);
  border: 1px solid color-mix(in srgb, var(--brand-charcoal) 12%, transparent);
  border-radius: 14px;
  box-shadow: 0 14px 36px color-mix(in srgb, var(--brand-charcoal) 30%, transparent);
  padding: 0.7rem 0.55rem 0.5rem;
}

.ap-id {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0 0.5rem 0.4rem;
}

.ap-id b {
  font-family: var(--brand-font-primary);
  font-size: 0.84rem;
  font-weight: 700;
  color: var(--brand-charcoal);
}

.ap-id span {
  font-size: 0.72rem;
  font-weight: 300;
  color: var(--brand-gunmetal);
  overflow: hidden;
  text-overflow: ellipsis;
}

.ap-sep {
  height: 1px;
  background: color-mix(in srgb, var(--brand-charcoal) 8%, transparent);
  margin: 0.25rem 0;
}

.ap-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--brand-font-primary);
  font-size: 0.8rem;
  color: var(--brand-charcoal);
  background: transparent;
  border: none;
  border-radius: 8px;
  padding: 0.45rem 0.5rem;
  cursor: pointer;
  text-align: left;
  text-decoration: none;
  transition: background 0.12s ease;
}

.ap-item:not(.ap-item--off):hover {
  background: color-mix(in srgb, var(--brand-charcoal) 5%, var(--brand-white));
}

/* Not-yet-built rows: visible but inert ("coming soon"). */
.ap-item--off,
.sidebar-nav-item--off {
  opacity: 0.5;
  cursor: default;
  pointer-events: none;
}

/* Rows that open a sub-panel instead of navigating (e.g. credits popup). */
.ap-item--static {
  cursor: pointer;
}

/* Trailing indicators sit right-aligned. */
.ap-badge,
.ap-bal {
  margin-left: auto;
}

.ap-badge {
  font-family: var(--brand-font-secondary);
  font-weight: 700;
  font-size: 0.52rem;
  line-height: normal;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--brand-white);
  background: var(--brand-orange);
  border-radius: 999px;
  padding: 0.12rem 0.45rem;
}

.ap-bal {
  font-family: var(--brand-font-mono);
  font-size: 0.68rem;
  font-weight: 700;
  background: var(--brand-charcoal);
  color: var(--brand-white);
  border-radius: 6px;
  padding: 1px 7px;
}

.ap-signout {
  color: var(--brand-orange-deep);
}

.ap-pref {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.3rem 0.5rem;
  font-size: 0.76rem;
  color: var(--brand-charcoal);
}

.ap-seg {
  display: flex;
  background: color-mix(in srgb, var(--brand-charcoal) 5%, var(--brand-white));
  border: 1px solid color-mix(in srgb, var(--brand-charcoal) 10%, transparent);
  border-radius: 999px;
  padding: 0.12rem;
}

.ap-seg button {
  font-family: var(--brand-font-secondary);
  font-weight: 700;
  font-size: 0.58rem;
  letter-spacing: 0.04em;
  color: var(--brand-gunmetal);
  background: transparent;
  border: none;
  border-radius: 999px;
  padding: 0.22rem 0.55rem;
  cursor: pointer;
  transition:
    background 0.12s ease,
    color 0.12s ease;
}

.ap-seg button.active {
  background: var(--brand-white);
  color: var(--brand-charcoal);
  box-shadow: 0 1px 3px color-mix(in srgb, var(--brand-charcoal) 14%, transparent);
}

/* Appearance segment (live Light/Dark/System — see
   templates/partials/appearance_segment.html for the shared-partial contract).
   SVG dimensions come from the {% icon size=13 %} call in the template. */
.ap-seg--appearance button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.28rem 0.55rem;
}


/* Matt palette — theme-aware --color-* tokens (which resolve dark under
   theme-mc), matching the Swarf-AI/webapp#2585 sidebar-bar mapping above. */
body.theme-mc .account-pop {
  background: var(--color-surface-raised);
  border-color: var(--color-border);
  box-shadow: var(--shadow-pop);
}

/* Modal/dialog surfaces (Swarf-AI/webapp#2716): DaisyUI's .modal-box paints base-100 (the page
   color under theme-mc), so a modal has no discernible edge against the page.
   Give it the same elevated-surface + 1px border + shadow recipe as the account
   popover (the reference pattern) so the dialog reads as a distinct surface. */
body.theme-mc .modal-box {
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-pop);
}

body.theme-mc .ap-sep {
  background: var(--color-separator); /* mid-tone separator — between surface-raised and border */
}

body.theme-mc .ap-item {
  color: var(--color-base-content);
}

body.theme-mc .ap-item--off {
  /* opacity: 0.5 on the base dims the badge too — use direct color instead */
  opacity: 1;
  color: var(--color-muted-dim);
}

body.theme-mc .ap-item:not(.ap-item--off):hover {
  background: color-mix(in srgb, var(--color-base-content) 6%, transparent);
}

body.theme-mc .ap-id b {
  color: var(--color-base-content);
}

body.theme-mc .ap-pref > span,
body.theme-mc .ap-id span {
  color: var(--color-muted-dim); /* muted — same value .board-tab uses; --color-muted reads too light */
}

body.theme-mc .ap-badge {
  font-family: var(--brand-font-mono);
  background: var(--brand-orange);
  color: var(--brand-on-accent);
}

body.theme-mc .ap-bal {
  background: color-mix(in srgb, var(--color-base-content) 4%, var(--color-surface-raised));
  border: 1px solid var(--color-border);
  color: var(--color-base-content);
}

body.theme-mc .ap-seg {
  background: var(--color-inset); /* inset darker than the pane — below base-200 */
  border: 1px solid var(--color-separator);
}

body.theme-mc .ap-seg button {
  font-family: var(--brand-font-primary);
  color: var(--color-muted-dim);
}

body.theme-mc .ap-seg button.active {
  background: var(--color-surface-raised);
  color: var(--color-base-content);
  box-shadow: none;
}

/* Exhausted-credits conversion wall (Swarf-AI/webapp#2630, reshaped #2375) — the scrim modal
   raised over an intact page when the credit gate refuses a send. Both CTAs
   are POST forms (billing views are POST-only); forms use display:contents so
   their submit buttons participate directly in the card's flex layout.

   Until #2375 a second layer sat here: a bar that replaced the composer at
   render time. The composer is never replaced now, so those rules are gone. */

/* CTAs are <button> inside <form>; the form is layout-transparent. */
.paywall-card-cta form {
  display: contents;
}

.paywall-cta {
  display: inline-flex;
  align-items: center;
  padding: 0.5rem 1rem;
  border-radius: 9999px;
  background: var(--brand-orange);
  border: none;
  color: var(--brand-charcoal);
  font-family: var(--brand-font-primary);
  font-weight: 600;
  font-size: 0.82rem;
  white-space: nowrap;
  cursor: pointer;
}

.paywall-cta:hover {
  background: var(--brand-orange-deep);
}

.paywall-cta-ghost {
  display: inline-flex;
  align-items: center;
  padding: 0.5rem 1rem;
  border-radius: 9999px;
  background: transparent;
  border: 1px solid var(--color-border-subtle);
  color: var(--color-muted-dim);
  font-family: var(--brand-font-primary);
  font-weight: 600;
  font-size: 0.82rem;
  white-space: nowrap;
  cursor: pointer;
}

.paywall-cta-ghost:hover {
  color: var(--color-base-content);
}

/* The scrim modal — delivered by a refused send, dismissed by paywall.js. */
.paywall-scrim {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-scrim);
  backdrop-filter: blur(2px);
}

.paywall-scrim[hidden] {
  display: none;
}

.paywall-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.6rem;
  max-width: 30rem;
  padding: 2rem;
  border-radius: 0.9rem;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border-subtle);
  box-shadow: var(--shadow-overlay-card);
}

.paywall-cap {
  letter-spacing: 0.1em;
  color: var(--brand-orange);
}

.paywall-title {
  font-family: var(--brand-font-primary);
  font-size: 1.15rem;
  font-weight: 700;
  margin: 0;
}

.paywall-body {
  font-family: var(--brand-font-primary);
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--color-muted-dim);
  margin: 0;
}

.paywall-card-cta {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  width: 100%;
  margin-top: 0.4rem;
}

/* In the scrim card the CTAs stack full-width (mock); the bar keeps them as
   inline pills. */
.paywall-card-cta .paywall-cta,
.paywall-card-cta .paywall-cta-ghost {
  width: 100%;
  justify-content: center;
  border-radius: 0.6rem;
  padding: 0.7rem 1rem;
}

.paywall-cta-note {
  font-weight: 400;
  opacity: 0.7;
  font-size: 0.78rem;
}

.paywall-later {
  background: none;
  border: none;
  color: var(--color-muted-dim);
  font-size: 0.78rem;
  cursor: pointer;
}

.paywall-later:hover {
  color: var(--color-base-content);
}

/* Credits popover (Swarf-AI/webapp#2626) — the balance chip's own popover, sibling to the
   account menu. Matches .account-pop's theme-mc surface; the top-up CTAs reuse
   the conversion wall's .paywall-cta / .paywall-cta-ghost, stacked full-width.
   display/flex-direction/width come from the shared popover rule above —
   redeclaring display here would defeat the :popover-open gating. */
.credits-pop {
  gap: 0.45rem;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: 14px;
  box-shadow: var(--shadow-pop);
  padding: 0.85rem 0.75rem;
}

.cp-head {
  display: flex;
  gap: 0.5rem;
  align-items: baseline;
  justify-content: space-between;
}

.cp-label {
  letter-spacing: 0.06em;
}

.cp-bal {
  font-family: var(--brand-font-mono);
  font-weight: 700;
  font-size: 1.5rem;
  color: var(--color-base-content);
}

/* Low/out-of-credits state carries through from the chip (Swarf-AI/webapp#2626). */
.cp-bal.warn {
  color: var(--brand-orange);
}

.cp-bal.crit {
  color: var(--brand-orange-deep);
}

.cp-reset {
  font-family: var(--brand-font-primary);
  font-size: 0.62rem;
  color: var(--color-muted-dim);
}

/* Tier badge — FREE / PRO */
.cp-tier {
  font-size: 0.56rem;
  border: 1px solid color-mix(in srgb, var(--color-muted-dim) 45%, transparent);
  border-radius: 999px;
  padding: 2px 8px;
}

.cp-tier.paid {
  color: var(--brand-orange);
  border-color: color-mix(in srgb, var(--brand-orange) 45%, transparent);
}

/* Balance meter */
.cp-meter {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.cp-meter-row {
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
}

.cp-meter-grant {
  font-family: var(--brand-font-primary);
  font-size: 0.76rem;
  color: var(--color-muted-dim);
}

.cp-meter-bar {
  height: 15px;
  background: var(--color-base-200);
  border-radius: 6px;
  overflow: hidden;
  position: relative;
}

/* Tick marks overlay — 10 equal segments separated by 1px hairlines */
.cp-meter-ticks {
  position: absolute;
  inset: 0;
  z-index: 1;
  background-image: repeating-linear-gradient(
    90deg,
    transparent 0px,
    transparent calc(10% - 1px),
    color-mix(in srgb, var(--color-base-content) 22%, transparent) calc(10% - 1px),
    color-mix(in srgb, var(--color-base-content) 22%, transparent) 10%
  );
}

.cp-meter-fill {
  height: 13px;
  position: absolute;
  left: 0;
  top: 1px;
  background: linear-gradient(90deg, var(--brand-success-accent), var(--brand-success-accent-light));
  border-radius: 5px 0 0 5px;
  transition: width 0.3s ease;
}

.cp-meter-fill::after {
  content: "";
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: rgba(255, 255, 255, 0.95);
}

.cp-meter-fill.warn {
  background: linear-gradient(90deg, var(--brand-orange), color-mix(in srgb, var(--brand-orange) 75%, #fff));
}

.cp-meter-fill.crit {
  background: var(--brand-orange-deep);
}

/* "Recent usage" section label */
.cp-section-label {
  font-size: 0.58rem;
  letter-spacing: 0.12em;
  padding-top: 0.1rem;
}

/* Recent-usage teaser */
.cp-usage {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding-top: 0.1rem;
}

.cp-usage-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 0.3rem;
  align-items: baseline;
  border-bottom: 1px solid color-mix(in srgb, var(--color-base-content) 7%, transparent);
  padding-bottom: 0.25rem;
}

.cp-usage-row:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

.cp-usage-date {
  font-family: var(--brand-font-mono);
  font-size: 0.56rem;
  font-weight: 700;
  color: var(--color-muted-dim);
  white-space: nowrap;
}

.cp-usage-title {
  font-family: var(--brand-font-primary);
  font-size: 0.7rem;
  color: var(--color-base-content);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cp-usage-cost {
  font-family: var(--brand-font-mono);
  font-size: 0.64rem;
  font-weight: 700;
  white-space: nowrap;
}

/* :not(.text-success) guard: triptych.css carries no @layer of its own, so
   an unlayered rule here always beats a Tailwind utility living in
   @layer utilities regardless of specificity (same reasoning as
   .icon-action-btn below) — without the guard this color would always win
   over .text-success's theme-routed color on a free (cost_usd=0) row, Swarf-AI/webapp#4295. */
.cp-usage-cost:not(.text-success) {
  color: var(--color-muted-dim);
}

/* Footer — link to Billing settings */
.cp-footer {
  padding-top: 0.35rem;
  border-top: 1px solid var(--color-border-subtle);
  margin-top: 0.1rem;
}

.cp-footer-link {
  font-family: var(--brand-font-primary);
  font-size: 0.72rem;
  color: var(--color-muted-dim);
  text-decoration: none;
}

.cp-footer-link:hover {
  color: var(--brand-orange);
}

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

/* Message action icon buttons (Swarf-AI/webapp#3033) — muted at rest since these are
   tertiary, after-the-fact actions; full color on hover restores focus.
   Opaque --color-muted, not a base-content/transparent mix — that formula
   composites to ~3.4:1 over the light base, the same sub-AA anti-pattern
   already fixed for placeholders below (~3.1:1 -> --color-muted's 5:1+).
   :not(.text-success):not(.text-error) matters here: triptych.css carries
   no @layer of its own, so any unlayered rule here would always beat
   Tailwind's .text-success/.text-error (both inside @layer utilities) —
   an unlayered declaration wins over a layered one regardless of
   specificity or source order. Without this guard, a rated feedback
   button would never show its success/error color at rest. */
.icon-action-btn:not(.text-success):not(.text-error) {
  color: var(--color-muted);
}

.icon-action-btn:not(.text-success):not(.text-error):hover {
  color: var(--color-base-content);
}

/* ── Assembly chain (Swarf-AI/webapp#3356) — the UNM assemblies figure as a live surface.
   One card per component on a horizontal axis (workpiece left, machine
   right), each wearing its classification above the photo, with joints
   between neighbors carrying the fitment verdict (unknown/warning is the
   default state — a joint with no verdict data never collapses into
   "doesn't fit"). Verdict colors ride the brand semantic tokens. ── */
.chain-figure {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  border: 1px solid var(--color-border-subtle);
  border-radius: 12px;
  padding: 0.7rem 0.8rem 0.8rem;
  background: var(--color-base-100);
}

.chain-ends {
  display: flex;
  justify-content: space-between;
}

.chain-end-label {
  letter-spacing: 0.12em;
  color: var(--color-muted);
}

.chain-rail-scroll {
  overflow-x: auto;
  position: relative;
}

.chain-rail {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  min-width: max-content;
  padding: 0.3rem 0.4rem 0;
}

.chain-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem;
  border-radius: 8px;
}

.chain-card-type {
  font-size: 0.52rem;
  letter-spacing: 0.12em;
  color: var(--color-muted);
}

.chain-card-photo {
  width: 44px;
  height: 44px;
  border-radius: 8px;
  object-fit: cover;
  background: #fff;
}

.chain-card-photo--empty {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.4rem;
  background: color-mix(in srgb, var(--color-base-content) 7%, transparent);
  font-family: var(--brand-font-mono);
  font-size: 0.42rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-muted);
  text-align: center;
}

.chain-card-pill {
  max-width: 8.5rem;
  padding: 0.15rem 0.55rem;
  border: 1px solid var(--color-border-subtle);
  border-radius: 999px;
  font-size: 0.64rem;
  font-weight: 500;
  color: var(--color-base-content);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-decoration: none;
}

/* A record-backed pill is a real anchor wearing the library row's hover
   contract (a.board-resource-name:hover + .board-resource-row:hover). */
a.chain-card-pill:hover {
  color: var(--brand-orange);
  background: var(--color-surface-raised);
}

/* Link quantity (Swarf-AI/webapp#3772) — a labeled line under the pill (a bare number
   read as ambiguous, and inline-in-the-pill got swallowed by the pill's
   own text-overflow ellipsis on longer labels). Quiet like
   .board-chip-count; only rendered above quantity 1, so the common case
   wears no visual change. */
.chain-card-quantity {
  font-size: 0.6rem;
  font-variant-numeric: tabular-nums;
  color: var(--color-muted-dim);
}

/* Joint — the junction promoted to a first-class element: short line stubs
   with the verdict diamond (.chain-joint-gem, a rotated square) between
   them. Height matches the photo so the joint rides the photo centerline. */
.chain-joint {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  height: 44px;
  flex-shrink: 0;
  cursor: help;
  --chain-verdict-color: var(--color-warning-content); /* unknown default */
}

.chain-joint[data-verdict="fits"] {
  --chain-verdict-color: var(--color-success-content);
}

.chain-joint[data-verdict="no_fit"] {
  --chain-verdict-color: var(--color-error-content);
}

.chain-joint--plain {
  /* No verdict yet (Swarf-AI/webapp#3356) — an adjacency line, not an answer. */
  cursor: default;
}

.chain-joint::before,
.chain-joint::after {
  content: "";
  width: 10px;
  height: 2px;
  background: var(--color-border-subtle);
}

.chain-joint-gem {
  font-weight: 900;
  font-size: 0.7rem;
  line-height: 1;
  color: var(--chain-verdict-color);
}

.chain-joint[data-verdict="fits"] .chain-joint-gem::before {
  content: "✓";
}

.chain-joint[data-verdict="no_fit"] .chain-joint-gem::before {
  content: "✕";
}

.chain-joint[data-verdict="unknown"] .chain-joint-gem::before {
  content: "?";
}

/* Floating "Chat with Swarf" trigger (Swarf-AI/webapp#3861) — shared by landing.html and
   the investor portal so the two pages' identical CTA can't drift apart.
   Shadow values are Tailwind's stock shadow-lg/shadow-xl — this app has no
   themed override for them (unlike --shadow-pop/--shadow-menu, which are
   custom tokens for other surfaces). */
.chat-fab {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  padding: 0.75rem 1rem;
  font-weight: 500;
  border-radius: 9999px;
  border: 1px solid var(--color-base-300);
  box-shadow:
    0 10px 15px -3px rgb(0 0 0 / 0.1),
    0 4px 6px -4px rgb(0 0 0 / 0.1);
  transition: all 0.15s ease-in-out;
  cursor: pointer;
  background-color: var(--color-base-100);
  color: var(--color-base-content);
}

.chat-fab:hover {
  box-shadow:
    0 20px 25px -5px rgb(0 0 0 / 0.1),
    0 8px 10px -6px rgb(0 0 0 / 0.1);
  transform: scale(1.05);
}

/* Dev-viewer toolbar toggle/action button (Swarf-AI/webapp#3862) — the small flat chip
   shared by every control in the 3D dev viewer's toolbar. Each call site
   keeps its own gap-* utility alongside this class since spacing varies
   with content (icon+label vs. text-only). */
.viewer-toolbar-btn {
  display: flex;
  align-items: center;
  padding: 0.375rem 0.75rem;
  font-size: 0.75rem;
  font-weight: 500;
  border-radius: 0.5rem;
  cursor: pointer;
  background-color: color-mix(in srgb, var(--color-base-100) 80%, transparent);
}

/* Markdown render/source toggle (#2271) — a File resource's CONTENT section
   shows a .md file as prose, with the raw source one click away, because
   rendering a document must not cost access to what it is made of. Same
   mechanism as .iso-toggle above and for the same reason: the input lives
   inside its <label>, several levels from the panels it controls, so
   Tailwind's peer-checked: (a sibling combinator) can't reach them and
   :has() can.
   Scoped to the two host wrappers rather than to .md-content, because the
   control sits in each host's chrome row and the panes sit in the content
   pane beneath it (Swarf-Inc/webapp#2359) — no single element inside
   _file_content.html contains both any more. Both selectors are inert
   wherever no .md-src-toggle renders, which is every host but these two:
   the hover peek shows markdown with no source view at all. */
.md-source {
  display: none;
}

.rp-section[data-subtab="content"]:has(.md-src-toggle input:checked) .md-rendered,
[data-active-board-tab]:has(.md-src-toggle input:checked) .md-rendered {
  display: none;
}

.rp-section[data-subtab="content"]:has(.md-src-toggle input:checked) .md-source,
[data-active-board-tab]:has(.md-src-toggle input:checked) .md-source {
  display: block;
}

/* The control shows its DESTINATION, never its current state — the same way
   every other icon button in these rows reads as an action rather than a
   status. Rendered prose offers code-xml; raw source offers the eye back.
   The checkbox itself is the accessible control and stays reachable to
   assistive tech and the keyboard; only its native box is taken out of the
   layout, since the label's icons are what a user sees and clicks — which
   is also why the focus ring has to be drawn on the label, since the input
   that actually receives focus is no longer something anyone can see. */
.md-src-toggle input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.md-src-toggle:has(input:focus-visible) {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

.md-src-toggle .md-src-icon-to-rendered,
.md-src-toggle:has(input:checked) .md-src-icon-to-source {
  display: none;
}

.md-src-toggle:has(input:checked) .md-src-icon-to-rendered {
  display: block;
}
