/*
 * This is a manifest file that'll be compiled into application.css.
 *
 * With Propshaft, assets are served efficiently without preprocessing steps. You can still include
 * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
 * cascading order, meaning styles declared later in the document or manifest will override earlier ones,
 * depending on specificity.
 *
 * Consider organizing styles into separate files for maintainability.
 */

/* Project pages render the band-2 context bar inside <nav> (h-11 in
   _project_navbar — border drawn inside the 44px). Every offset that clears
   the app chrome reads --layout-chrome-height so both bands count; pages
   without the bar keep the 0px default. On :root (not body) because
   fullscreen_toggle_controller reads variables off documentElement. */
:root:has([data-context-bar]) {
  --layout-context-bar-height: 44px;
}

/* Band 3 — a page's own control bar inside <nav> (h-14 in the layout, matching
   the 56px in-content band-3 rows the other project views use). Same contract
   as band 2: the height here must match the rendered band exactly, so the band
   never wraps; narrow screens scroll it sideways instead. */
:root:has([data-page-bar]) {
  --layout-page-bar-height: 56px;
}

/* The same band 3, rendered IN THE PAGE instead of inside <nav> — Timeline,
   Story Map, Board, and both estimator boards. Band 3 is owned by the page whose actions it holds
   (Jon 2026-08-13): its controls are Stimulus targets, and a target cannot
   leave its controller's scope the way an action can, so hoisting these bands
   into <nav> would cost them the capability the placement was meant to tidy.

   The layout cannot see them, though — --layout-chrome-height only counts a
   band mounted in <nav>, via :root:has([data-page-bar]) above. So the pages
   declare their own, and every RIGHT-ANCHORED PANEL positions below it: the
   drawer and the chat dock both read this. Nothing in the page flow does —
   the band is already in that flow, and adding it there would push the page
   down by its own control bar.

   56px matches the bands' min-h-[56px]. They wrap on a narrow window and this
   does not follow — at that width the dock overlays instead of insetting and
   the drawer is nearly full-bleed anyway, and a measured value would mean JS
   doing layout on every open. */
:root {
  --inline-control-bar-height: 0px;
  /* Compatibility for the shipped Drawer::Component while its shared inset
     name migrates; both tokens intentionally describe the same page band. */
  --drawer-top-inset: 0px;
}
:root:has([data-inline-control-bar]) {
  /* 56px literal, NOT var(--layout-page-bar-height, 56px): that variable is
     declared 0px on :root and only rises to 56 under [data-page-bar], so on
     exactly these pages it resolves to 0 — a var() fallback answers "unset",
     never "set to zero". */
  --inline-control-bar-height: 56px;
  --drawer-top-inset: 56px;
}

/* And the band takes back the strip the dock insets the page by, so it spans
   the window like a band mounted in <nav> does. Safe only because the dock now
   starts BELOW it: the two no longer share any pixel. Its page pays the inset
   as padding on <main>, so a negative margin is what gives it back. */
[data-inline-control-bar] {
  margin-right: calc(-1 * var(--chat-dock-inset, 0px));
}

/* Client plan workspaces keep band 3 inside the page so its controls remain in
   the same Stimulus scope as the content they affect. Visually it still belongs
   to the chrome: break it out of either the centered or full-bleed <main>, pin
   it directly below band 2, and let one flat page-ground strip replace the
   floating cards the portal used to draw around each group of controls. */
.client-page-control-band {
  position: sticky;
  top: var(--layout-chrome-height);
  z-index: 40;
  box-sizing: border-box;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  background-color: var(--layout-page-bg);
  /* Paint the divider inside the fixed 56px band. A real border adds a 57th
     pixel, so the next sticky stack jumps up by 1px when it reaches its
     --inline-control-bar-height stop. */
  box-shadow: inset 0 -1px 0 var(--layout-border-color);
}

/* Prioritize has a second page-owned control group below its shared filter:
   the allocation ribbon and its optional how-to row. Both must remain inside
   client-priority-board so their Stimulus targets survive results-frame
   replacement. Stack that group beneath the 56px filter band while the
   document scrolls, and give it an opaque page-ground so cards pass behind
   the controls instead of showing through them. */
.client-prioritize-sticky-controls {
  position: sticky;
  top: calc(var(--layout-chrome-height) + var(--inline-control-bar-height));
  z-index: 30;
  background-color: var(--layout-page-bg);
}

/* Classic-scrollbar platforms (Windows, "always show scroll bars" on macOS)
   otherwise nudge centered content and the nav's right cluster by half a
   scrollbar width whenever a page does/doesn't scroll — one more source of
   "the menu moved". Overlay scrollbars (macOS default) reserve nothing, so
   this is a no-op there. */
html {
  scrollbar-gutter: stable;
}

/* Pages that give scrolling to a descendant scrollport instead of the document —
   the staffing timeline, which bounds both axes in .timeline-workspace-scrollport.
   There is no page scrollbar to stabilise, so the reserved gutter is simply a
   band of dead page between the scrollport's own scrollbar and the window edge.
   Declared per page with content_for :document_never_scrolls; see the note on
   <html> in layouts/application.html.erb. */
html.document-never-scrolls {
  scrollbar-gutter: auto;
}

/* The timeline page's RIGHT gutter, which is conditional because what that edge
   borders changes underneath it.

   Dock closed: the edge is the window edge, and the workspace's own scrollbar
   belongs flush against it — that is the whole point of .document-never-scrolls
   above, and a gutter here would put the scrollbar back 20px inboard.
   Dock open: the edge borders the chat panel, and flush means the TOTAL column
   collides with it. The gutter comes back.

   min() against the EXISTING --chat-dock-inset rather than a new open/closed
   class: that variable is 0px closed and at least 360px open, so this resolves to
   exactly 0 or the full gutter with no threshold to tune, it needs no JS, and it
   inherits the below-md fork where the dock stops insetting and overlays instead.

   Phones keep the gutter unconditionally: there is no visible scrollbar to sit
   flush with, and a table against a phone's edge just reads as clipped. Hence the
   base rule plus the sm override, rather than one min() — below sm the inset is
   0px for a reason that does not apply here.

   Published as a custom property, not just applied: .timeline-scroll-band reads it
   back to stand its own inset down by the same amount, which is what stops the two
   from stacking. It inherits, so the bands inside pick it up with no plumbing. */
.timeline-page-right-gutter {
  --timeline-page-gutter: 1.25rem;
  padding-right: var(--timeline-page-gutter);
}

@media (min-width: 40rem) {
  .timeline-page-right-gutter {
    --timeline-page-gutter: min(1.25rem, var(--chat-dock-inset, 0px));
  }

  /* Except in fit, which brings its own gutter. Fit sizes the week column to stop
     Dimensions::BAND_RIGHT_GUTTER short of the scrollport's right edge so the
     tables line up with the cost cards above them — a page gutter on top of that
     is the second of two. Small and stretch draw at a fixed column and routinely
     overrun the scrollport, putting the TOTAL column hard against its edge, which
     is the case that needs this.

     Keyed off the attribute rather than a server-rendered class on the wrapper:
     switching modes replaces the TABLES via Turbo Stream, not show.html.erb, so a
     class on the wrapper would go stale the moment you clicked Small. Stimulus
     reflects modeValue to this attribute immediately, so the gutter also updates
     before the re-render lands. */
  :root:has([data-gantt-stretch-mode-value="fit"]) .timeline-page-right-gutter {
    --timeline-page-gutter: 0px;
  }
}

/* The page ground, painted so it can never collapse to a flat fill.

   Chrome analyzes every raster tile: a tile that comes out one flat color is
   stored as a solid color and drawn by the compositor, which skips the color
   conversion the rastered tiles get. Across a wide empty dark surface — the
   story map's untouched drop cells — those tiles read a few values darker than
   their neighbors, as rectangles with no relation to the layout, until
   something paints into them (hover a cell and its dashed add button draws, the
   tile re-rasters, and that rectangle is gone for good). Light mode hides the
   whole thing because white converts to white.

   The second stop is 0.1% white — one value of green in dark mode, nothing at
   all in light mode, and enough that the tile holds a gradient instead of a
   flat fill. Keep it that small: at 1% the far end lifts two values and the
   board reads as a wash. Reach for this on any large scrolled surface that
   would otherwise be an unbroken sheet of ground. */
.ground-fill {
  background-color: var(--layout-page-bg);
  background-image: linear-gradient(
    to bottom,
    var(--layout-page-bg),
    color-mix(in oklab, var(--layout-page-bg) 99.9%, white)
  );
}

/* Pair with .ground-fill on a sticky strip that hides cards scrolling under it.
   Two things a plain .ground-fill gets wrong there, both of which show up as
   marks in the gutters beside the strip.

   1. THE CARDS' SHADOWS SPILL PAST THE FILL. A card is exactly as wide as the
      strip (both 1024px here) and carries `shadow` — 0 1px 3px plus
      0 1px 2px -1px — which paints OUTSIDE its box. So the couple of pixels of
      shadow either side of the card are not covered by anything, and a card
      sliding under the strip drags two faint vertical smears up through the
      gutters. Widening the fill past the spill and pulling the content back in
      by the same amount fixes it without moving a thing on screen. 1rem
      against a 3px blur is deliberately generous: it costs nothing and it
      covers a future card with a heavier shadow.

   2. THE RAMP IS THE WRONG LENGTH. .ground-fill's gradient runs over the
      ELEMENT, so a ~100px strip travels the whole 0.1% in 100px while the page
      ground travels the same 0.1% over the entire document. By the strip's
      bottom it is a whole value of green light, and its own edges then read as
      a faint rectangle — measured rgb(15,17,19) inside against rgb(15,16,19)
      beside it (screenshots JPEG that away; measure pixels rather than look).
      background-attachment: fixed anchors the ramp to the VIEWPORT, putting a
      short strip on the same gentle ramp as the ground behind it — matched to
      well under one 8-bit value at any scroll position, and still a gradient
      rather than the flat fill .ground-fill exists to avoid. iOS Safari
      ignores fixed attachment and degrades to the old seam.

   Not folded into .ground-fill itself: its other users are tall scroll
   containers that cover nothing and want no bleed. */
.ground-fill-strip {
  --ground-fill-bleed: 1rem;
  background-attachment: fixed;
  margin-inline: calc(-1 * var(--ground-fill-bleed));
  padding-inline: var(--ground-fill-bleed);
  /* Keep the synthetic card edge on the CARD's width, not the bled fill's. */
  --rounded-reveal-inset: var(--ground-fill-bleed);
}

/* A sticky header that paints the ground hides whatever scrolls under it, and
   the card it cuts through comes out with a square top edge — two hard corners
   and no border, against a card whose bottom edge has both. This draws the top
   edge the cut took away: ::before rounds the corners, ::after re-draws the
   border over them. Put it on the sticky element; both hang one radius below
   its bottom edge, which is where the cut is.

   ::before is two tiles, each one radius square. The disc is centred on the
   tile's inner corner — the same point a rounded-lg corner arcs around — so the
   card shows through inside the disc and the ground covers the rest.

   Order matters: ::after paints last, so the border lands on top of the fillets
   rather than under them.

   The edge is only correct while a card is actually under the cut. Drawn
   unconditionally it floats over bare ground — for the 32px between two cards,
   for the whole of a short page's tail, and for every frame of a scroll that
   parks in one of those gaps (Jon 2026-08-11, and it reads as a stray hairline
   with two little hooks on its ends rather than as a card edge). So the
   pseudo-elements are gated on [data-reveal], which the sticky-reveal Stimulus
   controller sets only while a card spans the whole band the edge paints.
   Gated OFF by default on purpose: with no JS the page loses a nicety, which
   is the better failure than painting an edge onto nothing. */
.rounded-reveal {
  --rounded-reveal-radius: 0.5rem; /* rounded-lg, the radius every card uses */
}

/* --rounded-reveal-inset is how far in from the sticky element's own edges the
   synthetic edge is drawn: zero unless the strip has been bled wider than the
   cards it covers, which .ground-fill-strip does — the edge belongs to the
   CARD, so it must not stretch into the bleed along with the fill. The default
   lives in the var() fallback, NOT as a declaration on .rounded-reveal: both
   classes sit on the same element with the same specificity, and .rounded-reveal
   is declared second here, so a default of its own would win and quietly pull
   the edge back out to the fill's width. */
.rounded-reveal[data-reveal]::before,
.rounded-reveal[data-reveal]::after {
  content: "";
  position: absolute;
  bottom: calc(-1 * var(--rounded-reveal-radius));
  height: var(--rounded-reveal-radius);
  pointer-events: none;
}

/* ::after is the 1px border, so it holds the CARD's width. */
.rounded-reveal[data-reveal]::after {
  left: var(--rounded-reveal-inset, 0px);
  right: var(--rounded-reveal-inset, 0px);
}

/* ::before paints ground, so it runs the full BLED width instead. The card's
   shadow is drawn outside the card's own box — measured 3px of it past the
   border — and the fillet used to stop at the card's edge, so in these 8px the
   shadow carried straight on up to the cut while the corner beside it had
   already curved away: a square tab on the corner (Jon 2026-08-11). Taking the
   fillet out to the strip's edges covers the spill in the same pass that
   rounds the corner.
   Each tile grows by the inset for that, and the discs stay put: they are
   centred on the tile's inner corner, which is still exactly one radius in
   from the CARD's edge. Nothing paints between the two tiles, so the card
   itself shows through the middle. */
.rounded-reveal[data-reveal]::before {
  left: 0;
  right: 0;
  background-image:
    radial-gradient(
      circle var(--rounded-reveal-radius) at 100% 100%,
      transparent calc(100% - 0.5px),
      var(--layout-page-bg) 100%
    ),
    radial-gradient(
      circle var(--rounded-reveal-radius) at 0 100%,
      transparent calc(100% - 0.5px),
      var(--layout-page-bg) 100%
    );
  background-size: calc(var(--rounded-reveal-inset, 0px) + var(--rounded-reveal-radius)) 100%;
  background-position: left bottom, right bottom;
  background-repeat: no-repeat;
}

.rounded-reveal[data-reveal]::after {
  border: 1px solid var(--layout-border-color);
  border-bottom: 0;
  border-radius: var(--rounded-reveal-radius) var(--rounded-reveal-radius) 0 0;
}

/* Layout constants for staffing timeline */
:root {
  --layout-inset-width: 20px;
  --layout-nav-height: 64px;
  --layout-context-bar-height: 0px;
  --layout-page-bar-height: 0px;
  --layout-chrome-height: calc(var(--layout-nav-height) + var(--layout-context-bar-height) + var(--layout-page-bar-height));
  /* Width of the AI chat dock, the right-hand rail every project page renders.
     0px when closed, and 0px everywhere else — the layout only mounts the dock
     on project pages, so nothing else ever sees a non-zero value. Same idea as
     --layout-chrome-height above: one number the whole page reads, so opening
     the dock moves every edge at once instead of each view knowing about it. */
  --chat-dock-width: 0px;
  --layout-first-column-width: 175px;
  --layout-total-column-width: 204px;
  --layout-week-width-collapsed: 120px;
  --layout-week-width-stretched: 200px;
  /* Where an in-page anchor has to stop on a CLIENT PORTAL page. The shared
     chrome token counts the account row and plan-tab band; the inline token
     adds a page-owned sticky control band when one is present. A release jump
     that clears only the nav lands its heading underneath the Features bar.
     The extra 1rem makes the target read as arrived-at rather than flush
     against the chrome. */
  --portal-anchor-offset: calc(var(--layout-chrome-height) + var(--inline-control-bar-height, 0px) + 1rem);
  --layout-page-bg: rgb(249, 250, 251);
  --layout-border-color: rgb(229, 231, 235);
  /* Weeks containing a holiday. Barely-there warm white — the ⚠️ in the week
     header and the orange date numbers are the primary cue, this is a hint.
     Its own variable rather than yellow-50 so the dark value can stay as
     understated as this one is, instead of inheriting a tint scaled for badges. */
  --layout-holiday-bg: rgb(255, 254, 240);
  /* The gantt's day/week ruler lines. Their own variable rather than
     --color-gray-200 because that token is a structural border used app-wide:
     a line that reads as a hairline over white reads much stronger over the
     dark page, and only the ruler wants softening. */
  --gantt-grid-line: var(--color-gray-200);
  --layout-box-shadow-offset: -26px;

  /* Z-index layers - modals must be above nav (10002) and escape transform stacking contexts */
  --z-modal: 10010;
  --z-modal-backdrop: 10010;
}

/* Dashboard pipeline board: a card dragged into an empty stage column hides the
   "Empty" placeholder immediately; the morph refresh that follows removes it. */
[data-pipeline-board-target="column"]:has([data-pipeline-card]) [data-empty-placeholder] {
  display: none;
}

/* While a card is in flight, collapse the Won/Lost rail lists into two compact,
   equal drop targets — with long lists the Lost card would otherwise sit below
   the fold. min-height keeps each collapsed card an easy target. */
[data-pipeline-dragging] [data-rail-rows] {
  display: none;
}
[data-pipeline-dragging] [data-pipeline-board-target="wonZone"],
[data-pipeline-dragging] [data-pipeline-board-target="lostZone"] {
  min-height: 6rem;
}

/* Reset default browser styles on <dialog> elements.
   Must be in @layer base so Tailwind utilities (max-w-sm, etc.) can override. */
@layer base {
  dialog {
    padding: 0;
    border: none;
    max-width: none;
    max-height: none;
  }
}

/* Pointer-modality focus rings: clicking a control must not paint a focus
   outline, but Chrome's :focus-visible heuristic paints one anyway whenever a
   modifier rides along (Option+click) or a keystroke came just before the
   click (hotkeys, arrow-key nav) — how buttons kept lighting up blue.
   application.js (mirrored inline in the public deck viewer, which loads no
   app JS) stamps html.pointer-mode on pointerdown and lifts it on Tab, so
   keyboard users keep their rings. Only outlines clear: box-shadow/border
   focus styles (Tailwind ring/border utilities) and typing surfaces — where
   a click SHOULD show where you type — are untouched. Unlayered on purpose:
   it must beat UA defaults and layered utilities on buttons. */
html.pointer-mode :focus:not(input, textarea, [contenteditable]) {
  outline: none;
}

/* Presentation mode (bd sales-estimates-d974): the screen-share curtain.
   html.presenting is server-rendered from users.presentation_mode and flipped
   live by presentation_mode_controller. data-sensitive marks what a client
   must not see over a shared screen — money, AI deliberation, other clients'
   deals. A curtain for casual viewing, NOT a boundary: values stay in the DOM
   (and in a few data-* attributes), which is accepted — the real boundary is
   the client portal. Hidden elements reflow — hide whole tiles/cards/rows,
   never leave labeled shells (Jon, 2026-08-10); the one shape rule is that
   table cells hide as td+th PAIRS or the columns shear. Unlayered like
   pointer-mode above, and a plain class on <html> rather than :root:has() —
   see the chat-dock activity note further down for why root-anchored :has()
   is banned for mode flags. */
html.presenting [data-sensitive] {
  display: none !important;
}
/* Grids reshape to their remaining tiles while presenting: the project
   overview loses Avg Weekly Cost + Estimate and the dashboard loses Pipeline
   value + Won this quarter, so three tiles spread where five sat. */
@media (min-width: 1024px) {
  html.presenting [data-overview-stats],
  html.presenting [data-dashboard-stats-grid] {
    grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
  }
}
/* The bot's voice on the execution board already carries selectors of its
   own: comments stamp data-comment-bot, rendered @fulcrum mentions carry
   .mention-tag-bot. Zero extra markup needed. */
html.presenting [data-comment-bot="true"],
html.presenting .mention-tag-bot {
  display: none !important;
}
/* A pending-AI execution card signals through border/ring color, which hiding
   its badge cannot reach; the card stamps data-pending-card while pending. */
html.presenting [data-pending-card] {
  border-color: #e5e7eb !important;
  box-shadow: none !important;
}
/* The on-state chrome: a pulsing amber dot beside the org menu (the loud
   reminder that the curtain is up, and where to lift it) and the switch face
   on the org-menu row. Both derive purely from html.presenting, so a
   Turbo-cached page can never show a stale state. */
.presenting-dot {
  display: none;
}
html.presenting .presenting-dot {
  display: inline-block;
}
.presenting-switch {
  background-color: #d1d5db;
  transition: background-color 150ms;
}
html.presenting .presenting-switch {
  background-color: #f59e0b;
}
.presenting-switch-knob {
  transition: transform 150ms;
}
html.presenting .presenting-switch-knob {
  transform: translateX(1rem);
}

/* Disable vertical bounce on staffing timeline pages (expanded mode) */
body[data-expanded-mode] {
  overscroll-behavior-y: none;
}

/* On smaller screens in horizontal mode, kanban takes full height (chat/files hidden) */
/* Hide chat/files when viewport is below 1024px wide (matches Tailwind lg: breakpoint) */
/* This ensures the bottom panel only shows when there's enough width for chat (420px) + files (420px) + gaps */
@media (max-width: 1023px) {
  body[data-expanded-mode] [data-resizable-horizontal-split-target="top"] {
    height: 100% !important;
    flex: 1 !important;
    max-height: none !important;
  }
  body[data-expanded-mode]
    [data-resizable-horizontal-split-target="kanbanInner"] {
    padding-bottom: 1rem;
  }
  body[data-expanded-mode] [data-resizable-horizontal-split-target="handle"] {
    display: none !important;
  }
  body[data-expanded-mode] [data-resizable-horizontal-split-target="bottom"] {
    display: none !important;
  }
  /* Reset all kanban columns to equal flex widths on tablet/mobile */
  /* Desktop saved widths don't make sense at narrow viewport sizes */
  body[data-expanded-mode] [data-resizable-columns-target="column"] {
    flex: 1 1 0% !important;
  }
}

/* On large screens, apply max-height constraint to kanban so bottom panel has room */
@media (min-width: 1024px) {
  body[data-expanded-mode]
    [data-resizable-horizontal-split-target="top"]:not(
      [data-horizontal-split-both-hidden="true"]
    ) {
    max-height: calc(100% - 282px);
    min-height: 175px;
  }
}

/* Hide header buttons and column resize handles on narrow screens */
@media (max-width: 439px) {
  body[data-expanded-mode] [data-horizontal-header-hide] {
    display: none !important;
  }
  body[data-expanded-mode] [data-resizable-columns-target="handle"] {
    display: none !important;
  }
  /* Also hide responsive spacer handles added dynamically */
  body[data-expanded-mode] [data-responsive-spacer="true"] {
    display: none !important;
  }

  /* Make kanban columns full width for horizontal scrolling */
  body[data-expanded-mode] #kanban-board > .flex {
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
  }
  body[data-expanded-mode] [data-resizable-columns-target="column"] {
    flex: 0 0 calc(100vw - 2rem) !important;
    min-width: calc(100vw - 2rem) !important;
    max-width: calc(100vw - 2rem) !important;
    scroll-snap-align: start;
  }

  /* Chat slot in kanban: show on portrait, hide on landscape (below lg breakpoint) */
  body[data-expanded-mode] #kanban-chat-slot {
    flex: 0 0 calc(100vw - 2rem) !important;
    min-width: calc(100vw - 2rem) !important;
    max-width: calc(100vw - 2rem) !important;
    scroll-snap-align: start;
  }
}

/* Hide chat on mobile landscape orientation (horizontal phone layout is too cramped) */
/* Also hide Chat/Files badges in hidden indicator (can't show those panels on mobile landscape) */
/* Hide the entire hidden indicator if it only contains Chat/Files badges (no column badges) */
@media (max-width: 1023px) and (orientation: landscape) {
  #kanban-chat-slot {
    display: none !important;
  }
  /* Also hide the chat slot handle when chat slot is hidden */
  #kanban-chat-slot-handle {
    display: none !important;
  }
  [data-hidden-badge-chat],
  [data-hidden-badge-files] {
    display: none !important;
  }
  /* Hide indicator if it has no column priority badges (only Chat/Files which are hidden) */
  [data-resizable-horizontal-split-target="hiddenIndicator"]:not(
    :has([data-column-priority])
  ) {
    display: none !important;
  }
}

/* Hide chat hide button on mobile portrait (chat is forced visible there) */
@media (max-width: 1023px) and (orientation: portrait) {
  [data-chat-hide-button] {
    display: none !important;
  }
}

/* Nav collapse button - visible on mobile (both orientations) */
[data-nav-collapse-button] {
  display: none;
}
@media (max-width: 1023px) {
  [data-nav-collapse-button] {
    display: block;
  }
}

/* Hide files panel in horizontal mode based on visible panel count */
/* Users can still access files via the +file icon in chat header */
/* 3 panels visible (chat + files + popped): hide files below 1280px */
@media (max-width: 1279px) {
  body[data-expanded-mode]
    [data-bottom-panel-count="3"]
    [data-files-panel-split-target="chat"] {
    min-width: 0 !important;
    flex: 1 !important;
  }
  body[data-expanded-mode]
    [data-bottom-panel-count="3"]
    [data-files-panel-split-target="files"] {
    display: none !important;
  }
  body[data-expanded-mode]
    [data-bottom-panel-count="3"]
    [data-files-panel-split-target="handle"] {
    display: none !important;
  }
}

/* 2 panels visible (chat + files): hide files below 1024px (min widths: 420+420=840px + padding) */
@media (max-width: 1023px) {
  body[data-expanded-mode]
    [data-bottom-panel-count="2"]
    [data-files-panel-split-target="chat"] {
    min-width: 0 !important;
    flex: 1 !important;
  }
  body[data-expanded-mode]
    [data-bottom-panel-count="2"]
    [data-files-panel-split-target="files"] {
    display: none !important;
  }
  body[data-expanded-mode]
    [data-bottom-panel-count="2"]
    [data-files-panel-split-target="handle"] {
    display: none !important;
  }
}

/* 1 panel visible (files only): never hide via media query, only via explicit user action */
/* No CSS rule needed - files stays visible at all viewport widths */

/* Vertical layout below 768px: one pane at a time. The estimator's split
   root carries data-mobile-pane ("chat" | "board", switched by
   mobile_pane_toggle_controller); pages without the attribute keep the
   legacy chat-only behavior. The handle never shows on small screens. */
@media (max-width: 767px) {
  /* Left panel (chat+files) takes full width on small screens */
  [data-vertical-left-panel] {
    flex: 1 !important;
    min-width: 0 !important;
  }
  /* Add right padding to match left padding on small screens */
  [data-vertical-left-panel] [data-left-panel-inner] {
    padding-right: 1rem;
  }
  [data-vertical-right-panel] {
    display: none !important;
  }
  [data-vertical-handle] {
    display: none !important;
  }

  [data-mobile-pane="board"] [data-vertical-right-panel] {
    display: block !important;
  }
  [data-mobile-pane="board"] [data-vertical-left-panel] {
    display: none !important;
  }
}

/* The one nav wrapper every page shares: full width, constant gutters (px-5
   in the layout). Chrome aligns to the VIEWPORT; content aligns to the page —
   the old per-page fork (centered container vs full width vs a timeline-synced
   --nav-content-width) made the menu items jump horizontally on navigation. */
.nav-content-wrapper {
  width: 100%;
}

/* Fullscreen mode for wide tables - scrolls horizontally when wider than viewport.
   Horizontal padding is on the content div (px-5), not main, so the padding is part
   of the element's box and included in the scroll area for breathing room. */
main.fullscreen-mode {
  padding-left: 0;
  padding-right: 0;
  padding-bottom: 0;
}

/* Center content when viewport is wider than tables */
main.fullscreen-mode > div {
  margin-left: auto;
  margin-right: auto;
}

/* The freeze-container FOUC pre-positioning that used to live here is gone with
   the machinery it served. It only ever matched the staffing timeline, which now
   bounds its own overflow in .timeline-workspace-scrollport, and no other
   fullscreen page attaches that controller — the nav takes it only when NOT in
   expanded mode, and the nav is outside <main> regardless.

   The timeline's dock inset comes from body > main.fullscreen-mode's
   padding-right below, like the story map's: the scrollport is max-width: 100%
   of a w-full container inside that padded box, so it ends where the dock
   begins with no rule of its own. */

/* ── AI chat dock ───────────────────────────────────────────────────────────
   The dock INSETS the page rather than covering it: you open the chat to make
   a change and watch it land, and the right edge of the gantt, the board and
   the story map is exactly where new work appears.

   Two variables, not one:

     --chat-dock-width   the panel's own width, written by the Stimulus
                         controller. 0px when closed.
     --chat-dock-inset   how much the PAGE gives up. Equal to the width on
                         desktop; pinned to 0px below md, where the dock stops
                         insetting and becomes a full-width overlay. Every rule
                         below reads the inset, so that fork is one media query
                         and no JS — which also makes it correct on rotation,
                         where a JS breakpoint check is not.

   --chat-dock-width is set on <body>, never on :root. The server has to emit an
   initial value or every project page paints full-width and then jumps by up to
   900px on Stimulus connect. <html> can't carry it (Turbo takes only <head> and
   <body> from a response, so an inline style there goes stale after the first
   visit) and a <style> in <head> can't either (Turbo's head merge only ADDS
   stylesheet elements, so the value would leak onto the dashboard). <body> is
   replaced wholesale on every visit: the value arrives fresh with each render
   and disappears the moment you leave a project page, with nothing to undo in
   disconnect(). */
body {
  --chat-dock-inset: var(--chat-dock-width, 0px);
}

@media (max-width: 767px) {
  body {
    --chat-dock-inset: 0px;
  }
}

/* Presenting: the dock panel itself is curtained (its wrapper is
   data-sensitive), so the page must take the space back — a hidden dock that
   still insets the page reads as content shoved left into a phantom gutter.
   Pinning the INSET rather than the width keeps the dock's own open state
   intact: end the presentation and the panel returns exactly as it was. */
html.presenting body {
  --chat-dock-inset: 0px;
}

/* The timeline-change modals (the busy overlay and the staffing job/error
   modals) center on the CONTENT, not the viewport: with the dock open, a card
   centered on the whole window sits visibly right-of-center in the region the
   user is actually watching. The scrim still covers everything — only the
   centering math gives up the dock's slice. Reading the INSET makes this a
   no-op below md, where the dock overlays instead of insetting, and whenever
   the dock is closed. Fullscreen hides the dock entirely — it sits outside
   the fullscreen element — so there the full width is the content. */
.timeline-modal-inset {
  padding-right: var(--chat-dock-inset, 0px);
}

*:fullscreen .timeline-modal-inset {
  padding-right: 0px;
}

/* The release-notes review modal pays the dock the same way, in the one form a
   native <dialog> allows. It is centered by the UA's own
   `inset-inline: 0; margin: auto`, so moving the RIGHT EDGE in shrinks the box
   the margins center inside and the panel re-centers on the content. A
   transform would do it in one line and must not be used: a transformed element
   becomes the containing block for its position:fixed descendants, and this
   modal contains a Lexxy editor whose slash menu and @-mention dropdown are
   both fixed.

   It stays a descendant of <body> while it paints in the top layer, which is
   how it sees the variable at all — --chat-dock-inset is set on <body>, never
   on :root. */
.release-notes-review-dialog {
  right: var(--chat-dock-inset, 0px);
}

*:fullscreen .release-notes-review-dialog {
  right: 0px;
}

/* A MultiEditor told to FILL its box has to scroll inside it.
   Lexxy sizes .lexxy-editor__content with min-block-size and leaves the editor
   `overflow: visible`, which is right where the page scrolls around a
   fixed-height editor — and wrong inside a flex column that can shrink. A long
   draft in the review modal spilled straight over the Save button beneath it,
   far enough that a click on Save landed on the prose instead.

   Making the editor a column with a scrolling content area keeps the toolbar
   where it is and gives the words their own scrollport. Only for the fill
   shape: with `rows` the editor has a minimum height on purpose and the panel
   around it scrolls. */
[data-multi-editor-fill="true"] lexxy-editor {
  display: flex;
  flex-direction: column;
}

[data-multi-editor-fill="true"] lexxy-editor .lexxy-editor__content {
  flex: 1 1 0;
  min-block-size: 0;
  overflow-y: auto;
}

/* Hide diff hands the editor the whole modal. An attribute plus two rules
   rather than class bookkeeping in JavaScript: the toggle writes one word and
   the layout answers. Only above xl, where the two panes are side by side at
   all — stacked, the diff is simply the section above and hiding it needs no
   column arithmetic. */
[data-diff-hidden] [data-review-pane="diff"] {
  display: none;
}

@media (min-width: 80rem) {
  [data-diff-hidden] [data-review-pane="editor"] {
    grid-column: 1 / -1;
  }
}

/* Tailwind's .container cap, mirrored as a number the rule below can do
   arithmetic on — the utility only ever emits it as a max-width, and there is
   no reading a used max-width back out in CSS. Same breakpoint ladder the
   utility ships. 100% is the uncapped base, where calc(100% + inset) can never
   bite because .container is already width: 100%. */
:root {
  --page-column-cap: 100%;
}
@media (min-width: 40rem) { :root { --page-column-cap: 40rem; } }
@media (min-width: 48rem) { :root { --page-column-cap: 48rem; } }
@media (min-width: 64rem) { :root { --page-column-cap: 64rem; } }
@media (min-width: 80rem) { :root { --page-column-cap: 80rem; } }
@media (min-width: 96rem) { :root { --page-column-cap: 96rem; } }

/* Contained pages — the window scrolls and <main> is a centered .container.
   Padding rather than margin: mx-auto owns both margins, and overriding
   margin-right would let margin-left absorb the slack and push the column INTO
   the dock. Stacks with the layout's own px-4 instead of replacing it.

   The cap grows by that same inset, so the page's SLACK MARGIN pays for the
   dock before the column does. A centered container on a wide screen is
   already carrying empty margin either side; padding alone spends the column
   and leaves that margin sitting there — at 2009px the overview went from a
   1504px column to a 1084px one with 245px of nothing on both sides of it.
   Growing the cap keeps the padded content box exactly as wide as it was and
   slides it left instead, so opening the chat does not reflow the page
   underneath it. .container's width: 100% is the clamp that ends this: once
   the viewport can no longer afford cap + dock, the box stops growing and the
   column starts narrowing, which is the right order.

   Scoped to main.container — the contained case in both layouts — rather
   than main:not(.fullscreen-mode): the client layout's full-bleed pages
   (plan timeline) render <main class="w-full">, and the old negative guard
   swept them back under the container cap the moment the viewport passed
   40rem, quietly re-containing a page that had opted out. */
body > main.container {
  max-width: calc(var(--page-column-cap) + var(--chat-dock-inset));
  padding-right: calc(1rem + var(--chat-dock-inset));
}

/* Expanded shells — fixed inset-0 top-[var(--layout-chrome-height)]. inset-0
   already sets right: 0; this moves that one edge in. */
body [data-expanded-shell] {
  right: var(--chat-dock-inset);
}

/* Fullscreen mode covers the two remaining scroll models with one rule.
   The timeline pans the BODY sideways behind a fixed dock, and padding on
   <main> is part of its box, so it joins the horizontal scroll area — the last
   gantt week can always be scrolled clear of the dock rather than living under
   it. The story map is body { overflow: hidden } plus a w-full flex column, and
   w-full is 100% of main's CONTENT box, so the same padding shrinks it with no
   per-page rule. Higher specificity than main.fullscreen-mode above, which
   zeroes padding-right. */
body > main.fullscreen-mode {
  padding-right: var(--chat-dock-inset);
}

/* Right-anchored PAGE panels inset rather than fight the dock for the same
   strip. The drawer sits above the dock, so without this its backdrop dims the
   chat and its panel covers it; the board's slide-over sits below, so without
   this its right edge simply disappears behind the dock. Both are the page
   talking about the page — the dock is chrome that outlives them, and
   side-by-side is the honest arrangement. */
body [data-drawer-target="root"] {
  right: var(--chat-dock-inset);
}

/* Modals centre on the CONTENT area, not the window (Jon 2026-08-11 — this
   reverses the earlier "true modals stay full-bleed above the dock" call).

   A <dialog> opens in the top layer, whose containing block is the viewport,
   so the UA's inset: 0 + margin: auto centres it across the whole window
   including the strip the dock occupies — with the dock open a modal sits
   visibly off-centre and drifts under it. Moving the right edge in is the
   whole fix: margin: auto then re-centres inside what is left.

   The right edge and NOTHING else. A max-width guard here looked tempting —
   it would stop a viewport-sized dialog growing back over the dock — but this
   file is unlayered, so it beats Tailwind's utilities and would REPLACE each
   modal's own max-w-* rather than clamp it. It widened the decision composer
   from 64rem to the full content area. The two dialogs actually measured in
   viewport units subtract the inset in their own markup instead.

   By offset and NEVER by transform/translate. A transformed element becomes
   the containing block for its position:fixed descendants, and these modals
   carry Lexxy editors whose slash menu and @-mention dropdown are both fixed
   — the same trap the rail below, drawer_controller.js and
   execution_board_controller.js each document.

   The backdrop still covers the window on purpose. The dock is inert while a
   modal is open, so dimming it is the truth about what is clickable.

   Anchored dialogs are excluded by construction rather than by exception: the
   drawer's root is a <div> (see the rule above), and the board's slide-over is
   a panel, so neither is a :modal. */
dialog:modal {
  right: var(--chat-dock-inset);
}

/* The dialogs measured in viewport units, re-measured against the content
   area. 95vw of the WINDOW is wider than the window minus the dock, so these
   would keep their old overlap and — now that the box they centre in is
   narrower than they are — give up centring too and sit flush left. Every
   percentage here is the one the markup already asked for; only what it is a
   percentage OF has changed.

   Expressed in CSS rather than as Tailwind arbitrary values on purpose:
   `w-[calc(95vw-var(--chat-dock-inset,0px))]` puts a custom property whose own
   name is full of hyphens inside a calc that Tailwind rewrites hyphens in. */
dialog[data-file-preview-target="dialog"] {
  width: calc(100vw - var(--chat-dock-inset));
}

@media (min-width: 768px) {
  dialog[data-file-preview-target="dialog"] {
    width: calc((100vw - var(--chat-dock-inset)) * 0.95);
  }
}

dialog[data-estimate-modal-target="dialog"] {
  max-width: calc((100vw - var(--chat-dock-inset)) * 0.9);
}

dialog[data-activity-log-target="dialog"] {
  width: calc((100vw - var(--chat-dock-inset)) * 0.66);
}

/* The rail itself.

   NO transform, filter, backdrop-filter, perspective, contain or will-change on
   this element or its panel — any one of them makes it the containing block for
   its position:fixed descendants, which is precisely how the Lexxy slash menu
   and the @-mention dropdown break. drawer_controller.js:6-10 and
   execution_board_controller.js:352-354 both went out of their way to leave an
   OPEN panel with no transform for the same reason. That is why this opens by
   WIDTH and never translates. spec/assets/chat_dock_inset_spec.rb asserts it.

   z-index 10000 is the floor of the app's overlay range: above every page-level
   panel (board slide-over z-50, PRD sidebar z-30, freeze-container z-100),
   below the whole 10000-series that should win over it — nav 10010, drawers
   10002, modals, flash. The nav's own org dropdown is z-[10001] but sits inside
   <nav>, which is positioned at 10010 and therefore forms a stacking context,
   so the entire nav subtree still paints above this. */
[data-chat-dock] {
  position: fixed;
  /* Below band 3 whichever side of the <nav> boundary it lives on: the nav one
     is already inside --layout-chrome-height, the in-page one is not. Without
     this the dock opened level with the story map's band and its page inset
     squeezed that band sideways (Jon 2026-08-13). */
  top: calc(var(--layout-chrome-height) + var(--inline-control-bar-height, 0px));
  right: 0;
  bottom: 0;
  width: var(--chat-dock-width);
  z-index: 10000;
  overflow: hidden;
}

/* The panel holds its full width while the rail clips it, so a composer laid
   out for 420px does not reflow down to nothing as the dock closes.

   The TOP-LEFT corner is rounded because that is the only corner of the dock
   that meets the page: the other three run into the viewport edges, where a
   radius would just leak the ground through. On the boards it lands directly
   under band 3's control row, and a square corner there read as a seam in the
   chrome rather than as a panel sitting beside the work (Jon 2026-08-18).

   The drag handle takes the same radius. It is an absolutely positioned strip
   on that very edge and paints a hover fill, so left square it would square the
   corner back off again the moment the pointer arrived — and only then, which
   is the kind of bug that gets reported as "sometimes". Rounding the handle
   rather than clipping the panel: [data-chat-dock-panel] holds popovers and
   dropdowns that must be free to overflow it. */
[data-chat-dock-panel] {
  position: absolute;
  inset: 0 0 0 auto;
  width: max(var(--chat-dock-width), 360px);
  border-top-left-radius: 0.5rem;
}

[data-chat-dock-handle] {
  border-top-left-radius: 0.5rem;
}

/* ...unless a side panel is standing against it. Every drawer root ends at
   --chat-dock-inset, so an open panel's right edge lands exactly on the dock's
   left edge and the two are married — the panel takes the rounded corner (it
   is the one meeting the page now) and the dock goes square, or the seam
   between them carries a notch of page showing through.

   html[data-panel-docked] rather than :root:has(): a root-anchored :has() with
   a descendant argument is re-checked on every insertion anywhere in the
   document, and this dock streams — during a response that is every chunk,
   each restyling the whole page. See the chat-dock activity note below, which
   is the same trap. utils/docked_panels.js stamps the attribute; both panel
   systems report through it, because neither can see the other. */
html[data-panel-docked] [data-chat-dock-panel],
html[data-panel-docked] [data-chat-dock-handle] {
  border-top-left-radius: 0;
}

/* ONE hairline between two panels, not two. The dock draws its own border-l
   against the page, and the panel beside it is lifted (shadow-xl) and paints
   ABOVE the dock — z 10002+ over 10000 — so its shadow lands on that border
   and the seam reads as a doubled line. While they are married the panel's own
   edge is the divider and the dock's border stands down.

   Transparent rather than zero-width: the border still occupies its pixel, so
   the dock's contents do not shift sideways as a drawer opens and closes. */
html[data-panel-docked] [data-chat-dock-panel] {
  border-left-color: transparent;
}

/* Below md the dock stops insetting (see --chat-dock-inset above) and covers
   instead, so it needs its own width back. */
@media (max-width: 767px) {
  [data-chat-dock][data-open] {
    left: 0;
    width: auto;
  }

  /* Full-bleed below md, so there is no page beside it to round against. */
  [data-chat-dock][data-open] [data-chat-dock-panel],
  [data-chat-dock][data-open] [data-chat-dock-handle] {
    border-top-left-radius: 0;
  }

  [data-chat-dock][data-open] [data-chat-dock-panel] {
    inset: 0;
    width: auto;
  }
}

/* Animate every edge the dock moves — but not mid-drag, where a transition
   makes the panel lag the pointer. The inline page band lives INSIDE the box
   being narrowed and takes the dock strip back with a negative right margin.
   That margin must run on the exact same clock: applying the full negative
   margin while right/padding-right is still moving briefly makes the band wider
   than the viewport, then snaps its right-side controls back into place. */
body:not([data-chat-dock-dragging]) [data-chat-dock],
body:not([data-chat-dock-dragging]) > main,
body:not([data-chat-dock-dragging]) [data-expanded-shell],
body:not([data-chat-dock-dragging]) [data-inline-control-bar] {
  transition: width 180ms ease, right 180ms ease, padding-right 180ms ease,
    max-width 180ms ease, margin-right 180ms ease;
}

@media (prefers-reduced-motion: reduce) {
  [data-chat-dock],
  body > main,
  [data-expanded-shell],
  [data-inline-control-bar] {
    transition: none;
  }
}

/* The band-2 toggle's activity dot, drawn from two sources the button itself
   cannot observe. Both signals are mirrored onto <html> as plain attributes by
   the controllers that own them — NOT read from inside the dock with
   :root:has(): a :has() anchored at the root with a descendant argument had to
   be re-checked on every insertion under [data-chat-dock], which during a
   streaming response is every chunk, and each flip restyled the whole
   document. A plain attribute costs one scoped recalc when it toggles.

   Unread — a broadcast landed in the dock while it was closed. Only the dock
   can see that; chat_dock_controller#flagActivity stamps the attribute. */
html[data-chat-dock-activity="unread"] [data-chat-dock-indicator] {
  opacity: 1;
}

/* Streaming — cancel-streaming is mounted for exactly as long as a response is
   in flight (it is the Stop button's controller), so its connect/disconnect
   toggles this attribute (dock-scoped, refcounted in the controller). */
html[data-chat-dock-streaming] [data-chat-dock-indicator] {
  opacity: 1;
  animation: chat-dock-pulse 1.4s ease-in-out infinite;
}

@keyframes chat-dock-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

@media (prefers-reduced-motion: reduce) {
  html[data-chat-dock-streaming] [data-chat-dock-indicator] {
    animation: none;
  }
}

/* The board's proposals Inbox is rendered even when empty, so a proposal
   arriving from the chat rail on this page has a container to land in — a
   broadcast cannot fill something that isn't in the DOM. It costs no space
   until it holds a card, which is what this rule buys: the "an always-present
   empty column would cost 280px" concern the partial used to solve by
   omitting itself entirely. */
[data-proposals-column]:not(:has([data-proposal-card], [data-inbox-changed-row])) {
  display: none;
}

[data-chat-dock-handle] {
  cursor: col-resize;
}

body[data-chat-dock-dragging] {
  cursor: col-resize;
  user-select: none;
}

/* Sortable drag-and-drop placeholder (shows where item will drop) */
.sortable-ghost {
  opacity: 0.8;
  background: var(--color-blue-100) !important;
  border: 2px dashed #3b82f6 !important;
  border-radius: 0.5rem;
}

.sortable-ghost * {
  opacity: 0.7;
}

/* Source card that stays in place while dragging (faded) */
.sortable-source {
  opacity: 0.3 !important;
}

/* ── Drag collapse: questions ── */
/* Hide everything inside question cards except the question text */
[data-dragging="questions"] [data-question-id] [data-question-edit],
[data-dragging="questions"] [data-question-id] [data-options-display],
[data-dragging="questions"] [data-question-id] [data-question-badges],
[data-dragging="questions"] [data-question-id] details,
[data-dragging="questions"] [data-question-id] hr {
  display: none !important;
}

/* Compact card padding and spacing */
[data-dragging="questions"] [data-sortable-group] [data-question-id] {
  padding: 0.25rem 0 !important;
  margin-bottom: 0 !important;
  border: none !important;
  border-radius: 0 !important;
}

/* Borders: every parent container gets a bottom border except the last */
[data-dragging="questions"] [data-sortable-group] > [data-parent-container] {
  border-bottom: 1px solid var(--color-gray-200) !important;
}

[data-dragging="questions"]
  [data-sortable-group]
  > [data-parent-container]:last-child {
  border-bottom: none !important;
}

/* Follow-ups: full-width rows, indented via handle padding, separated by top borders */
[data-dragging="questions"] [data-follow-ups-container] {
  margin-left: 0 !important;
}

/* First follow-up: top border separates from parent question */
[data-dragging="questions"]
  [data-follow-ups-container]
  > [data-question-id]:first-child {
  border-top: 1px solid var(--color-gray-200) !important;
}

/* Between sibling follow-ups: single separator */
[data-dragging="questions"]
  [data-follow-ups-container]
  > [data-question-id]
  + [data-question-id] {
  border-top: 1px solid var(--color-gray-200) !important;
}

[data-dragging="questions"] [data-follow-ups-container] [data-drag-handle] {
  padding-left: 1.25rem !important;
}

/* Compact the group content container */
[data-dragging="questions"] [data-sortable-group] {
  padding: 0 !important;
  overflow: hidden !important;
  gap: 0 !important;
  --tw-space-y-reverse: 0 !important;
}

[data-dragging="questions"] [data-sortable-group] > * {
  margin-top: 0 !important;
  margin-bottom: 0 !important;
}

/* Center-align drag handle + text when collapsed to single line */
[data-dragging="questions"] [data-question-id] [data-question-row] {
  align-items: center !important;
  padding-left: 0.75rem !important;
  padding-right: 0.75rem !important;
}

[data-dragging="questions"] [data-question-id] [data-drag-handle] {
  padding-top: 0 !important;
}

/* Truncate long question text to one line */
[data-dragging="questions"] [data-question-id] [data-question-display] {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* In reorder mode, use background highlight instead of blue ring for kbd focus */
[data-dragging] .kbd-focus {
  box-shadow: none !important;
  background-color: var(--color-blue-50) !important; /* blue-50 */
}
[data-dragging] .kbd-focus[data-reorder-expanded] {
  background-color: transparent !important;
}
[data-dragging] .kbd-focus-hidden {
  box-shadow: none !important;
}

/* ── Reorder mode: expanded card overrides ── */
/* Undo collapse for the one card the user clicked to inspect */
[data-dragging="questions"]
  [data-question-id][data-reorder-expanded]
  [data-options-display],
[data-dragging="questions"]
  [data-question-id][data-reorder-expanded]
  [data-question-badges],
[data-dragging="questions"]
  [data-question-id][data-reorder-expanded]
  [data-question-actions],
[data-dragging="questions"] [data-question-id][data-reorder-expanded] details,
[data-dragging="questions"] [data-question-id][data-reorder-expanded] hr {
  display: revert !important;
}

[data-dragging="questions"] [data-question-id][data-reorder-expanded] {
  padding: 1rem !important;
  border: none !important;
  border-radius: 0 !important;
  margin: 0 !important;
}

[data-dragging="questions"]
  [data-question-id][data-reorder-expanded]
  [data-question-row] {
  align-items: start !important;
  padding-left: 0 !important;
  padding-right: 0 !important;
}

[data-dragging="questions"]
  [data-question-id][data-reorder-expanded]
  [data-drag-handle] {
  padding-top: 0.125rem !important;
}

[data-dragging="questions"]
  [data-question-id][data-reorder-expanded]
  [data-question-display] {
  overflow: visible !important;
  white-space: normal !important;
  text-overflow: unset !important;
}

/* Allow question editor to show when JS removes .hidden on expanded card */
[data-dragging="questions"]
  [data-question-id][data-reorder-expanded]
  [data-question-edit]:not(.hidden) {
  display: block !important;
}

/* Remove inherited border-bottom from nested elements that have data-question-id */
[data-dragging="questions"]
  [data-question-id][data-reorder-expanded]
  [data-question-id] {
  border: none !important;
}
/* Restore add-option input styling in expanded card */
[data-dragging="questions"]
  [data-question-id][data-reorder-expanded]
  [data-add-option-input] {
  border: 1px solid var(--color-gray-300) !important;
  border-radius: 0.375rem !important;
  padding: 0.5rem !important;
}
[data-dragging="questions"]
  [data-question-id][data-reorder-expanded]
  [data-add-option-input]:hover {
  border-color: var(--color-gray-300) !important;
}
[data-dragging="questions"]
  [data-question-id][data-reorder-expanded]
  [data-add-option-input]:focus {
  border-color: #60a5fa !important;
}

/* Option editor border in expanded reorder card — match Lexxy editor border */
[data-dragging="questions"]
  [data-question-id][data-reorder-expanded]
  .option-editor-wrapper:not(.hidden) {
  border: 1px solid var(--lexxy-color-ink-lighter, oklch(85% 0 0)) !important;
  border-radius: 0.5rem !important;
  overflow: hidden !important;
  padding: 0 !important;
}
[data-dragging="questions"]
  [data-question-id][data-reorder-expanded]
  .option-lexxy-editor {
  padding: 0 !important;
}

/* Override SortableJS inline height on the fallback clone */
[data-dragging="questions"] .sortable-fallback[data-question-id] {
  height: auto !important;
  padding: 0.25rem 0 !important;
}

/* ── Drag collapse: groups ── */
/* Hide group content areas (questions inside) */
[data-dragging="groups"] [data-question-group-item] [data-sortable-group] {
  display: none !important;
}

/* Round header fully when content is hidden */
[data-dragging="groups"] [data-question-group-item] [data-group-header] {
  border-radius: 0.5rem !important;
  margin-top: 0 !important;
}

/* Override SortableJS inline height on the fallback clone */
[data-dragging="groups"] .sortable-fallback[data-question-group-item] {
  height: auto !important;
}

/* Reduce spacing between collapsed groups (override space-y-6 margins) */
[data-dragging="groups"] [data-groups-container] {
  --tw-space-y-reverse: 0 !important;
}

[data-dragging="groups"] [data-groups-container] > * {
  margin-top: 0 !important;
  margin-bottom: 0.25rem !important;
}

/* The fallback clone that follows the cursor (created by forceFallback) */
.sortable-fallback {
  cursor: grabbing !important;
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.25) !important;
  z-index: 10000 !important;
  background: var(--color-white) !important;
  opacity: 0.95 !important;
  pointer-events: none !important;
  /* CRITICAL: Disable transitions so the fallback snaps to cursor position instantly.
     Without this, cards with transition-all will animate their transform property,
     causing the dragged card to lag behind the cursor instead of following it. */
  transition: none !important;
}

/* Add a slight tilt for kanban cards when dragging */
.sortable-fallback.kanban-card {
  rotate: 2deg;
}

/* The item being dragged */
.sortable-drag {
  cursor: grabbing !important;
}

/* Selected item before drag starts — keep pointer until actual drag */
.sortable-chosen {
  cursor: pointer !important;
}

/* Grab cursor for draggable items */
.sortable-item,
[data-sortable-item] {
  cursor: grab;
}

.sortable-item:active,
[data-sortable-item]:active {
  cursor: grabbing;
}

/* Bulk drag: Clear fallback styling so our custom stack shows */
.bulk-drag-fallback {
  background: transparent !important;
  background-color: transparent !important;
  border: none !important;
  box-shadow: none !important;
  padding: 0 !important;
  margin: 0 !important;
  outline: none !important;
}

/* Bulk drag: Clear ghost styling for custom drop placeholder */
.bulk-drag-ghost {
  background: transparent !important;
  background-color: transparent !important;
  border: none !important;
  box-shadow: none !important;
  padding: 0 !important;
  outline: none !important;
  /* Reset height to fit content (ghost inherits original card dimensions) */
  height: auto !important;
  min-height: 0 !important;
  /* Width is set dynamically by JS to match cards in target column */
  /* Fallback min-width if JS can't measure (empty column) */
  min-width: 200px;
  display: block !important;
  opacity: 1 !important;
  box-sizing: border-box !important;
  overflow: hidden !important;
  /* Prevent ghost from growing beyond its set width in flex containers */
  flex-shrink: 0 !important;
  flex-grow: 0 !important;
}

/* Bulk drag: Drop placeholder content styling */
.bulk-drop-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  max-width: 100%;
  min-height: 80px;
  border: 2px dashed #60a5fa;
  border-radius: 0.5rem;
  background-color: var(--color-blue-50);
  color: var(--color-blue-600);
  font-size: 0.875rem;
  font-weight: 500;
  padding: 1rem;
  gap: 0.25rem;
  box-sizing: border-box;
}

/* Bulk drag: Stacked cards container */
.bulk-drag-stack {
  position: relative;
  background: transparent;
}

/* Bulk drag: Count badge */
.bulk-drag-badge {
  position: absolute;
  top: -10px;
  right: -12px;
  width: 24px;
  height: 24px;
  background: #3b82f6;
  color: white;
  font-size: 12px;
  font-weight: bold;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  z-index: 10;
}

/* Bulk drag: Hide OTHER selected cards during drag (not the dragged card).
   display:none removes them from layout flow entirely so remaining cards
   reflow naturally and calculateLayout() measures accurate heights.
   The dragged card (SortableJS ghost) is never hidden — only other
   selected cards get this class, so SortableJS always has a valid ghost. */
.bulk-drag-hidden {
  display: none !important;
}

/* Bulk drag: Cloned card in stack */
.bulk-drag-clone {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  opacity: 1;
  pointer-events: none;
  background-color: var(--color-white) !important;
  overflow: hidden;
}

/* Dark mode: a gray drop shadow can't separate a dark card from the dark card
   fanned behind it. Each clone instead gets a lit rim hugging its edge (the
   visible outline), a near-black ring outside it (the cut that keeps card
   bottoms readable against the clone behind), and a heavier depth shadow.
   Borders themselves are left alone so priority-tint edges keep their color. */
html[data-theme="dark"] .bulk-drag-clone {
  box-shadow:
    0 0 0 1px color-mix(in oklab, var(--color-gray-400) 45%, transparent),
    0 0 0 2px oklch(0% 0 0 / 0.8),
    0 6px 18px oklch(0% 0 0 / 0.55);
}

/* Bulk drag: Hide individual card hours in stack (total is shown below) */
.bulk-drag-clone [data-card-hours] {
  display: none;
}

/* Bulk drag: Hours indicator below the dragging stack */
.bulk-drag-hours {
  position: absolute;
  bottom: -28px;
  left: 50%;
  transform: translateX(-50%);
  background: #3b82f6;
  color: white;
  font-size: 12px;
  font-weight: 600;
  padding: 2px 10px;
  border-radius: 9999px;
  white-space: nowrap;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

/* Bulk drag: All columns and dropzones get 2px dashed borders during drag */
.bulk-drag-column > div:first-child {
  /* Column header */
  border-width: 2px !important;
  border-style: dashed !important;
}

.bulk-drag-dropzone {
  border-width: 0 2px 2px 2px !important;
  border-style: dashed !important;
  overflow-x: hidden !important;
}

/* Bulk drag: Active column header gets thicker (3px) dashed border */
.bulk-drag-active-column > div:first-child {
  border-width: 3px !important;
  border-style: dashed !important;
}

/* Bulk drag: Active dropzone gets thicker (3px) dashed border, no top border (header has bottom) */
.bulk-drag-active-dropzone {
  border-width: 0 3px 3px 3px !important;
  border-style: dashed !important;
}

/* Bulk drag: Instant height changes during drag (no animation jitter).
   overflow:visible prevents clipping of dashed bottom borders when
   subpixel rounding makes the dropzone slightly taller than the column. */
[data-bulk-dragging] [data-kanban-rows-layout-target="column"] {
  transition: none !important;
  overflow: visible !important;
}

/* Bulk select: Use outline to avoid layout shift */
.kanban-card.bulk-selected {
  outline: 2px solid rgb(59 130 246); /* blue-500 */
  outline-offset: -2px; /* inset to cover the existing border */
  background-color: var(--color-blue-50); /* bg-blue-50 */
}

/* Kanban cards: Pointer cursor on hover (clickable to open details) */
.kanban-card:hover {
  cursor: pointer;
}

/* Bulk select: Selected cards show grab cursor (drag to move) */
.kanban-card.bulk-selected:hover {
  cursor: grab;
}

/* Grabbing cursor while actually dragging (sortable-fallback is the dragged ghost element) */
.sortable-fallback {
  cursor: grabbing !important;
}

/* During any kanban drag, force grabbing cursor on entire page.
   :has(> …) — child combinator, deliberately not descendant: every drag that
   needs the page-wide cursor appends its clone to <body> directly
   (fallbackOnBody), and the child form confines :has() invalidation to body's
   direct children. The descendant form was re-checked on every class mutation
   anywhere in the document, and its `*` subject kept an invalidation entry
   alive for every element. Drags whose clone stays inside the list
   (mvp-groups, deck editor) still get the cursor from .sortable-fallback
   itself above. */
[data-bulk-dragging="true"],
[data-bulk-dragging="true"] *,
body:has(> .sortable-fallback),
body:has(> .sortable-fallback) * {
  cursor: grabbing !important;
}

/* ===================== */
/* Story Map: Select & Reorder */
/* ===================== */

/* Story map card selection (reuses kanban bulk-selected visual) */
[data-story-map-card].bulk-selected {
  outline: 2px solid rgb(59 130 246);
  outline-offset: -2px;
  background-color: var(--color-blue-50) !important;
}

/* Story map feature cards: sizing and cursor */
[data-story-map-card] {
  cursor: pointer;
  min-height: 80px;
  max-height: 200px;
  overflow: hidden;
}

/* Grabbing cursor while dragging any story map element */
[data-dragging-features] *,
[data-dragging-columns] * {
  cursor: grabbing !important;
}

/* Hide empty feature placeholders while dragging */
[data-dragging-features] [data-empty-feature],
[data-dragging-columns] [data-empty-feature] {
  display: none !important;
}

/* Hide add-feature buttons while dragging */
[data-dragging-features] [data-add-feature],
[data-dragging-columns] [data-add-feature] {
  visibility: hidden !important;
}

/* Story map filter section: suppress input's own ring, show ring on parent filter section */
.story-map-search-input:focus {
  box-shadow: none !important;
  outline: none !important;
}

[data-story-map-filters-target="filterSection"]:focus-within,
[data-story-map-filters-target="filterSection"].story-map-filter-focused {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
  border-radius: 0.375rem;
}

/* Keyboard help bar kbd styling */
.story-map-kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5rem;
  padding: 0.125rem 0.375rem;
  font-size: 0.75rem;
  font-weight: 600;
  background: rgba(255, 255, 255, 0.2);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 0.25rem;
  line-height: 1.25rem;
}

/* Step containers maintain minimum height */
[data-story-map-target="skeletonContainer"] {
  min-height: 36px;
}

/* In reorder mode, full card is draggable (except add-activity button) */
[data-reorder-mode] [data-story-map-target="backboneCard"],
[data-reorder-mode] [data-story-map-target="backboneCard"] * {
  cursor: grab !important;
}

[data-reorder-mode] [data-story-map-target="backboneCard"] .group\/addAct,
[data-reorder-mode] [data-story-map-target="backboneCard"] .group\/addAct * {
  cursor: pointer !important;
}

[data-reorder-mode] [data-story-map-target="backboneCard"]:active,
[data-reorder-mode] [data-story-map-target="backboneCard"]:active * {
  cursor: grabbing !important;
}

/* Activity reorder: Ghost placeholder (drop target) — inset blue dashed card with text */
.activity-reorder-ghost {
  background: transparent !important;
  border: none !important;
  box-shadow: none !important;
}

.activity-reorder-ghost > [data-story-map-target="backboneLabel"] {
  background: var(--color-blue-50) !important; /* blue-50 */
  border: 2px dashed #93c5fd !important; /* blue-300 */
  box-sizing: border-box !important;
  box-shadow: none !important;
  color: #93c5fd !important; /* blue-300 */
  padding-left: 10px !important; /* px-3 minus 2px border */
  padding-right: 10px !important;
}

.activity-reorder-ghost > .group\/addAct {
  display: none !important;
}

/* Step reorder: Ghost placeholder (drop target) — inset yellow dashed card with text */
.step-reorder-ghost {
  position: relative;
  background: transparent !important;
  border: none !important;
  box-shadow: none !important;
}

.step-reorder-ghost > * {
  background: var(--color-yellow-50) !important; /* yellow-50 */
  border: none !important;
  outline: 2px dashed var(--color-amber-300) !important;
  outline-offset: -2px !important;
  box-shadow: none !important;
  color: var(--color-amber-300) !important;
}

.step-reorder-ghost button {
  display: none !important;
}

/* Unassigned column: sticky to right edge of scroll container.
   The gutter is the same column continued through each lane's NAME BAR, which
   is a separate row above the cells and carried no background — so the column
   was interrupted by ~25px of page ground once per lane. It pins identically or
   it would slide out from under the cells the moment the map is scrolled
   sideways. Decorative only; see the partial for why it is a separate
   attribute rather than another [data-unassigned-cell]. */
[data-unassigned-cell],
[data-unassigned-gutter] {
  position: sticky;
  right: 0;
  padding-left: 8px;
}

/* Keep add-feature buttons at the bottom of their flex container always */
[data-add-feature] {
  order: 9999;
}

/* Feature card: Ghost placeholder (drop target) — dashed outline in priority color */
[data-story-map-card].feature-reorder-ghost {
  background-color: var(--ghost-bg, var(--color-gray-100)) !important;
  border: 2px dashed var(--ghost-color, #9ca3af) !important;
  border-left-width: 2px !important;
  box-shadow: none !important;
  outline: none !important;
  border-radius: 0.25rem;
  color: var(--ghost-color, #9ca3af) !important;
  opacity: 0.5 !important;
}

/* Activity reorder: Fallback (dragged clone) — show only the blue card */
.activity-reorder-fallback {
  width: 168px !important;
  padding: 0 !important;
  background: transparent !important;
  box-shadow: none !important;
  border: none !important;
  opacity: 1 !important;
  transition: none !important;
}

.activity-reorder-fallback > [data-story-map-target="backboneLabel"] {
  width: 100% !important;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}

/* Hide add-activity button and drag handle in fallback */
.activity-reorder-fallback > .group\/addAct {
  display: none !important;
}

/* Step reorder: Fallback (dragged clone) — show only the yellow card */
.step-reorder-fallback {
  background: transparent !important;
  box-shadow: none !important;
  border: none !important;
  opacity: 1 !important;
  transition: none !important;
}

.step-reorder-fallback [data-story-map-target="skeletonCard"] {
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}

/* In reorder mode, steps and features are NOT draggable */
[data-reorder-mode] [data-story-map-target="skeletonContainer"] {
  cursor: default;
}

[data-reorder-mode] [data-story-map-card] {
  cursor: default;
}

/* Reorder mode: collapse backbone cards to single-step width + add-activity button */
[data-reorder-mode] [data-story-map-target="backboneCard"] {
  width: 196px !important;
}

/* Hide extra skeleton containers (keep first per step group) */
[data-reorder-mode]
  [data-story-map-target="skeletonContainer"]
  ~ [data-story-map-target="skeletonContainer"] {
  display: none !important;
}

/* Hide add-step buttons in reorder mode */
[data-reorder-mode] .group\/addStep {
  visibility: hidden !important;
  pointer-events: none !important;
}

/* Hide step spacers in reorder mode */
[data-reorder-mode] [data-story-map-target="releaseRow"] > .w-\[20px\],
[data-reorder-mode] [data-story-map-target="moscowRow"] > .w-\[20px\] {
  visibility: hidden !important;
}

/* Hide extra drop cells per column (keep first per column-id) — handled by JS for now */

/* Hide unassigned column in reorder mode */
/* The gutter goes wherever the cells go: left behind in reorder mode it would
   be a grey strip in every name bar with no column under it. */
[data-reorder-mode] [data-unassigned-cell],
[data-reorder-mode] [data-unassigned-gutter] {
  display: none !important;
}

/* Collapsed activity columns: hide card content in the first (kept) cell */
[data-collapse-hidden] > * {
  visibility: hidden !important;
}

/* 4px edge buffer so cards have 8px space at row edges */
[data-story-map-target="backboneRow"]
  > :nth-child(2):not([data-unassigned-cell]),
[data-story-map-target="skeletonRow"]
  > :nth-child(2):not([data-unassigned-cell]),
[data-story-map-target="releaseRow"]
  > :nth-child(2):not([data-unassigned-cell]),
[data-story-map-target="moscowRow"]
  > :nth-child(2):not([data-unassigned-cell]) {
  margin-left: 4px;
}

/* 4px right margin on the last element before each unassigned cell,
   so combined with existing padding it creates an 8px visual gap */
:has(+ [data-unassigned-cell]) {
  margin-right: 4px;
}

[data-unassigned-cell] {
  margin-left: auto;
  padding-right: 8px;
}

/* Prevent turbo-frame from expanding parent in flexbox layouts */
/* This allows wide gantt charts to scroll without forcing parent containers to expand */

/* Force overflow visible on all gantt cell right elements to allow text labels to overflow */
[data-gantt-cell-right] {
  overflow: visible !important;
  overflow-x: visible !important;
  overflow-y: visible !important;
}
turbo-frame {
  min-width: 0;
}

/* Global button cursor - excludes buttons with explicit cursor classes */
button:not([class*="cursor-"]) {
  cursor: pointer;
}

/* Force cursor during drag/resize operations - body class takes precedence */
body.cursor-ew-resize * {
  cursor: ew-resize !important;
}
body.cursor-grabbing * {
  cursor: grabbing !important;
}
body.cursor-crosshair * {
  cursor: crosshair !important;
}

/* Make drag handle lighter and non-interactive when feature is expanded (either disclosure or estimates panel) */
[data-controller*="feature-toggle"]:has(
    [data-feature-estimates-inline-target="expanded"]:not([hidden])
  )
  .drag-handle,
[data-controller*="feature-toggle"]:has(
    [data-feature-toggle-target="disclosure"]:not([hidden])
  )
  .drag-handle {
  color: var(--color-gray-300) !important; /* gray-300 - two shades lighter than gray-500 */
  pointer-events: none;
  cursor: default !important;
}

[data-controller*="feature-toggle"]:has(
    [data-feature-estimates-inline-target="expanded"]:not([hidden])
  )
  .drag-handle:hover,
[data-controller*="feature-toggle"]:has(
    [data-feature-toggle-target="disclosure"]:not([hidden])
  )
  .drag-handle:hover {
  color: var(--color-gray-300) !important; /* Keep same color on hover */
}

/* Show drag handle in collapsed mode (minimal mode) */
.feature-collapsed .drag-handle {
  /* Visible in minimal mode */
}

/* Remove negative margin from drag handle SVG in minimal mode */
.feature-collapsed .drag-handle svg {
  margin-left: 0 !important;
}

/* ===========================================
   Generic keyboard navigation focus indicators
   Used by all navigable nodes and containers
   =========================================== */

/* Primary focus state - bright blue ring */
.kbd-focus {
  outline: none !important;
  --tw-ring-color: #3b82f6; /* blue-500 */
  box-shadow: 0 0 0 2px var(--tw-ring-color) !important;
  border-color: transparent !important;
}

/* Hidden focus state - subtle blue ring indicating position is preserved */
.kbd-focus-hidden {
  outline: none !important;
  --tw-ring-color: var(--color-blue-200);
  box-shadow: 0 0 0 2px var(--tw-ring-color) !important;
  border-color: transparent !important;
}

/* Search-hidden features */
.search-hidden {
  display: none !important;
}

/* Feature-specific scroll margins (features need extra space for expanded panels) */
.feature-focused {
  scroll-margin-top: 80px;
  scroll-margin-bottom: 175px;
}

.feature-focus-hidden {
  scroll-margin-top: 80px;
  scroll-margin-bottom: 175px;
}

/* Base padding for all role rows to prevent shifting */
[data-role-buttons] {
  padding: 0.25rem;
  margin-left: -0.25rem;
  margin-right: -0.25rem;
  margin-top: -0.25rem;
  margin-bottom: -0.25rem;
}

.role-focused {
  background-color: color-mix(in oklab, var(--color-gray-900) 6%, transparent);
  border-radius: 0.375rem;
}

.role-focused .button-focused {
  outline: 2px solid var(--color-blue-600) !important;
  outline-offset: 2px;
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-blue-600) 20%, transparent) !important;
  scroll-margin-top: 80px;
  scroll-margin-bottom: 175px;
}

/* Shortcut key indicators - only show when scope is active */
.shortcut-key {
  text-decoration: none;
  font-weight: inherit;
}

/* Show underlines when the shortcut is active */
.shortcut-key[data-shortcut-active="true"],
[data-shortcut][data-shortcut-active="true"] .shortcut-key {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

/* Consistent underline styling on shortcut indicators */
.shortcut-key,
u {
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}

/* Sidebar toggle: underline the right half of the chevron icon (the shortcut key half) */
.sidebar-shortcut-icon {
  position: relative;
  display: inline-flex;
}

[data-shortcut][data-shortcut-active="true"] .sidebar-shortcut-icon::after {
  content: "";
  position: absolute;
  bottom: -2px;
  right: 0;
  width: 50%;
  border-bottom: 1px solid currentColor;
}

/* Hide estimate button popovers when feature is expanded */
[data-feature-estimates-inline-target="collapsed"]:has(
    + [data-feature-estimates-inline-target="expanded"]:not([hidden])
  )
  [popover] {
  display: none !important;
}

/* Collapsed feature view - show only essentials */
.feature-collapsed {
  /* Fixed height to match role focus bar for popout alignment */
  height: 42px !important;
  /* Horizontal padding only - vertical centering via flexbox */
  padding: 0 0.5rem !important;
  /* Reduce bottom margin from 0.75rem/12px to 0.375rem/6px */
  margin-bottom: 0.375rem !important;
}

/* Ensure collapsed feature content is vertically centered */
.feature-collapsed > [data-feature-content-wrapper] {
  display: flex !important;
  align-items: center !important;
  height: 100% !important;
  width: 100% !important;
}

/* Make the first row (containing name and action links) fill the width */
.feature-collapsed > [data-feature-content-wrapper] > div:first-child {
  width: 100% !important;
}

/* Remove bottom margin from last feature */
.feature-collapsed:last-child {
  margin-bottom: 0 !important;
}

[id^="feature_"]:not(.feature-collapsed):last-child {
  margin-bottom: 0 !important;
}

.feature-collapsed [data-collapsible-content] {
  display: none !important;
}

/* Show only action links when feature is focused (keyboard navigation) */
.feature-collapsed.feature-focused [data-feature-action-links],
.feature-collapsed.kbd-focus [data-feature-action-links] {
  display: flex !important;
}

.feature-collapsed [data-feature-toggle-target="disclosure"] {
  display: none !important;
}

.feature-collapsed [data-collapsed-only] {
  display: flex !important;
}

/* Global form element styling */
select,
input[type="text"],
input[type="email"],
input[type="tel"],
input[type="number"],
input[type="password"],
input[type="url"],
textarea {
  border: 1px solid var(--color-gray-400);
  border-radius: 0.375rem; /* rounded-md */
  padding: 0.5rem 0.75rem; /* py-2 px-3 */
  box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); /* shadow-sm */
}

select:focus,
input[type="text"]:focus,
input[type="email"]:focus,
input[type="tel"]:focus,
input[type="number"]:focus,
input[type="password"]:focus,
input[type="url"]:focus,
textarea:focus {
  outline: none;
  box-shadow: 0 0 0 2px var(--color-blue-600); /* blue-600 ring that follows border-radius */
}

/* Exemption for fields whose WRAPPER draws the border and the focus edge — the
   gantt popover's hours field is one. Both rules above are UNLAYERED, so they
   beat any Tailwind utility on the element regardless of specificity: `border-0
   p-0` does nothing, and the field renders a second inset border plus a hard
   blue focus shadow inside its wrapper. Selector is element+attribute+class so
   it outranks them without !important; it must stay AFTER them. */
input[type="text"].gantt-hours-input,
input[type="text"].gantt-hours-input:focus {
  border: none;
  border-radius: 0;
  padding: 0;
  box-shadow: none;
  outline: none;
}

/* Same exemption, same reason: the branding page renames a deck theme IN its
   picker chip, and the chip is what draws the border and the selected edge. The
   global rules above put a second bordered, shadowed, py-2 box inside it — a
   chip within a chip. Tailwind utilities cannot undo that (unlayered CSS wins),
   so the escape has to be here, after them.

   The affordance moves to the text itself: nothing until you reach for it, then
   a dotted underline saying this word is editable. */
input[type="text"].deck-theme-name,
input[type="text"].deck-theme-name:focus {
  border: none;
  border-radius: 0;
  padding: 0;
  background: transparent;
  box-shadow: none;
  outline: none;
}

input[type="text"].deck-theme-name:hover,
input[type="text"].deck-theme-name:focus {
  text-decoration: underline dotted;
  text-underline-offset: 3px;
}

/* A third exemption lived here for a per-task role picker
   (sales-estimates-7wm8) and is gone with it (sales-estimates-zlhq). Worth the
   note, because it failed in a way the other two do not: squeezing a select
   into a 30x14px chip won the size fight and lost the legibility one — the
   glyphs rendered clipped top and bottom. The lesson is not "escape harder",
   it is that a select does not belong in a column that narrow. Each role
   has its own add row now, so nothing has to be chosen after the fact. */

/* Remove focus outline from popovers.
   .tooltip-overlay is in here as a belt to application.js's brace: showing a
   popover runs the UA's focusing steps, so the tooltip briefly holds focus
   before the handler hands it back — and it must never paint a ring while it
   does. It is decorative and nothing should ever look focused on it. */
el-popover:focus,
[popovertarget]:focus,
.tooltip-overlay:focus,
.tooltip-overlay:focus-visible {
  outline: none;
}

/* Override browser UA styles on popovers — default background:canvas leaks
   behind the inner rounded-lg div, and default overflow:auto clips shadows */
el-popover[popover] {
  background: transparent;
  overflow: visible;
  border: none;
}

/* Restore explicit bg-white on popovers that set it directly (not via inner div).
   The transparent reset above is for HoverPopover which uses an inner content div. */
el-popover[popover].bg-white {
  background: var(--color-white);
}

/* Deck-overview engagement popover: a native [popover] positioned as fixed by
   deck_engagement_controller. Reset the UA [popover] chrome (opaque Canvas bg,
   border, centering margin, clipping overflow) — the look lives on the inner
   styled div, same split HoverPopover uses. position:fixed is the UA default. */
.deck-engagement-popover[popover] {
  background: transparent;
  border: none;
  overflow: visible;
  margin: 0;
  padding: 0;
  max-width: none;
  max-height: none;
}

/* ===========================================
   Team Assignment keyboard navigation
   =========================================== */

/* Container hidden focus - shown when navigating inside team assignments */
[data-has-subnav].subnav-active [data-card-root] {
  --tw-ring-color: var(--color-blue-200);
  box-shadow: 0 0 0 2px var(--tw-ring-color) !important;
}

/* Allocation matrix cell - remove native focus styling */
[data-half-day-cell] {
  outline: none !important;
}

[data-half-day-cell]:focus {
  outline: none !important;
}

[data-half-day-cell]:focus-visible {
  outline: none !important;
}

/* Allocation matrix cell focus - keyboard navigation */
[data-half-day-cell].allocation-cell-focused {
  outline: 2px solid var(--color-blue-600) !important;
  outline-offset: 1px;
  z-index: 10;
  position: relative;
}

/* ===========================================
   Timeline Workspace Scrollport
   ===========================================

   The Staffing Timeline draws a timeline that is wider than the screen at most
   week widths. It used to handle that by making the DOCUMENT that wide and
   panning the body sideways, which meant nothing could stay put with
   position: sticky — sticky pins against the scrollport, and the scrollport was
   the window. Hence freeze-container: a scroll handler that re-wrote `top` on
   position: fixed elements every frame and left placeholder divs behind.

   Now the overflow lives in ONE bounded box instead, and every pinned thing is
   native sticky against it. No scroll handlers, no placeholders, no synthetic
   resize events, and the header/column/corner cannot lag the content because
   the browser lays them out in the same pass.

   The staffing table and the gantt share this ONE scrollport deliberately: it
   gives them a single horizontal coordinate system, so their week columns line
   up at every scroll offset with no JS scrollLeft mirroring between them. */
.timeline-workspace-scrollport {
  position: relative;
  /* Sticky descendants and the dependency SVG both paint into this box; own the
     stacking context so their z-indexes are local and cannot reach app chrome. */
  isolation: isolate;
  max-width: 100%;
  /* max-height, not height: a two-row plan should not reserve a screen of empty
     scrollport. The page keeps scrolling normally around this box. */
  /* The compatibility fallback keeps this bound correct while the shared
     drawer-offset token is renamed from --drawer-top-inset. */
  max-height: calc(100dvh - var(--layout-chrome-height) - var(--inline-control-bar-height, var(--drawer-top-inset, 0px)) - 2rem);
  overflow: auto;
  /* Contain only the INLINE axis. Reaching the right edge should not start
     panning whatever is behind this; reaching the bottom SHOULD hand off to the
     page, which is how the user gets to the unscheduled list below. */
  overscroll-behavior-inline: contain;
  /* Reserve the vertical scrollbar's width at all times. Without this, a plan
     that grows tall enough to need one narrows clientWidth mid-life, and Fit —
     which sizes the week column to exactly that width — would start overflowing
     the axis it is supposed to fit. */
  scrollbar-gutter: stable;
}

/* Section titles and the timeline's own control headers: they ride the
   workspace's vertical scroll but stay put horizontally.

   Both halves of this are load-bearing and neither is obvious.

   `sticky` can only travel inside its CONTAINING BLOCK. A normal block child of
   the scrollport is only clientWidth wide, so a band in it has exactly zero room
   to slide and scrolls away as if it were static — which is what happened when
   this was `width: 100%`. `.timeline-band-host` widens the parent to the full
   scroll width to give it that room.

   The band's own width then has to be the scrollport's VISIBLE width, which is
   the one value CSS cannot reach from here. `100%` resolves against that
   max-content host (thousands of px), and container query units would need
   `container-type` on the scrollport, whose implied `contain: layout` would make
   it the containing block for every `position: fixed` descendant and clip the
   gantt's overlay tooltips. So timeline_workspace_controller.js publishes it as
   --timeline-visible-width on a resize observer — no scroll-time work.

   `max-content` is NOT usable as the fallback: .timeline-controls-container is a
   container-query container, and a narrow band trips
   `@container timeline-controls (max-width: 1000px)` into the stacked layout,
   which narrows it further — an intrinsic-sizing feedback loop that collapses the
   controls into four rows. The band needs a DEFINITE width.

   Nor is `100%`, which resolves against that max-content host: the band would be
   the whole timeline wide and `justify-between` would put its right-hand controls
   thousands of pixels off-screen until the observer corrected it. Stimulus
   controllers are lazy-loaded, so on a heavy plan that wrong layout was on screen
   for seconds.

   So the value arrives three ways, best first: the server seeds the custom
   property from the last reported width (show.html.erb), this viewport-derived
   calc covers a first-ever visit with no cookie yet, and the observer corrects
   both to the exact pixel. Only the third involves JavaScript, and it is never
   on the critical path for first paint.

   The calc deliberately errs NARROW. It mirrors the real chain — page px-5
   either side, minus the chat dock, minus the scrollbar gutter reserved above —
   but 100dvw counts the page's OWN scrollbar, which clientWidth does not, so an
   exact-looking subtraction would come out ~15px too wide wherever scrollbars
   take space and put a sliver of phantom horizontal scroll in the workspace. A
   band a few px too narrow just leaves a little gap at the right, for one
   render, on a first-ever visit. */
/* Centring the cost-summary band, which is a DIRECT child of the scrollport rather
   than living inside a host. Scoped to direct children on purpose: a band nested in
   a host must stay pinned to that host's left edge, and when the plan overflows the
   host is the whole scroll width — auto margins there would park the band's controls
   in the middle of the scroll area instead of at the visible left edge. */
.timeline-workspace-scrollport > .timeline-scroll-band {
  /* 100%, not the published --timeline-visible-width. A direct child's containing
     block IS the visible box, so this is live — where the published value lags by a
     resize-observer tick. That lag used to be harmless (a band 20px narrow, still
     pinned left); with the auto margins below it became a 10px SIDEWAYS SHIFT, which
     is how the cost cards ended up overhanging the tables toward the chat dock.
     Bands nested in a host still need the published value: their containing block is
     the max-content host, so 100% there is the whole scroll width. */
  width: min(100%, var(--timeline-content-width, 100%));
  margin-inline: auto;
}

.timeline-band-host {
  width: max-content;
  /* Normally the visible box, so a band inside has room to slide (see below). But
     when the server says the stack is narrower than the box, this floors at the
     STACK's width instead, which lets max-content win and the auto margins centre
     the whole thing. min() of the two rather than a second selector so a window
     resize re-centres with no round-trip. */
  min-width: min(100%, var(--timeline-content-width, 100%));
  margin-inline: auto;
}

.timeline-scroll-band {
  position: sticky;
  left: 0;
  z-index: 120;
  /* The visible box, or the stack's own width when that is narrower — which is what
     brings the cost cards and the section headings in with the tables instead of
     leaving them stretched across a box the plan does not fill. Centred by the auto
     margins; a no-op in the common case where this resolves to the full box. */
  width: min(
    var(--timeline-visible-width, calc(100dvw - 2.5rem - var(--chat-dock-inset, 0px) - 2rem)),
    var(--timeline-content-width, 100%)
  );
  /* The band is exactly the visible width, so without this its right-hand
     controls sit flush against the scrollbar gutter. Left stays 0 so the
     headings keep aligning with the table's left edge below them.

     It STANDS DOWN by whatever the page gutter is already providing, so the two
     never stack. They clear different things — this one the scrollbar inside the
     scrollport, the page gutter the chat dock outside it — but they are both empty
     space at the same edge, and 16 + 20 reads as two gutters: the cost cards end
     36px off the dock while an overflowing table ends 20px off it.

     max()/calc() rather than a second selector because the page gutter is itself
     conditional (dock, mode, breakpoint); subtracting the value means this follows
     all three without restating any of them. --timeline-band-inset is the number
     Gantt::Dimensions::BAND_RIGHT_GUTTER mirrors, and fit lines the tables up on
     it — which stays true because the page gutter is 0 in fit.

     1.25rem, not 1rem: it is the page's own left gutter (pl-5), so the timeline is
     inset the same on both sides and the right edge measures the same in every
     mode. At 1rem this edge came out 16px in fit — where the band provides it —
     and 20px in stretch, where the page gutter does.

     And it stands down again for centring, for the same no-stacking reason: once the
     column is centred there are hundreds of px either side, and keeping the inset
     would only make the band's content 20px narrower than the table directly beneath
     it. Zero when there is no room to centre in — fit, or a plan that overflows. */
  --timeline-band-inset: 1.25rem;
  --timeline-centering-room: max(0px, calc(
    var(--timeline-visible-width, 0px) -
    var(--timeline-content-width, var(--timeline-visible-width, 0px))
  ));
  padding-right: max(0px, calc(
    var(--timeline-band-inset) - var(--timeline-page-gutter, 0px) - var(--timeline-centering-room)
  ));
}

/* Header rows pin to the workspace's own top edge — 0, not the app chrome
   height. The chrome is outside this box and no longer overlaps it. */
.timeline-sticky-header {
  position: sticky;
  top: 0;
  z-index: 100;
}

/* The task/member column. `left: 0` because the column sits flush against the
   scrollport edge; the old `left: 20px` plus a -26px page-coloured box-shadow
   existed only to fake the page's 20px inset while the BODY was the scroller. */
.timeline-sticky-column {
  position: sticky;
  left: 0;
  z-index: 60;
}

/* Above both the header row it sits in and the body column it sits above, or
   week cells scroll out from under it on one axis or the other. */
.timeline-sticky-corner {
  position: sticky;
  top: 0;
  left: 0;
  z-index: 110;
}

/* Depth cue at the boundary between the pinned column and the content sliding
   under it. Replaces the fake page-inset mask; the cells keep their own
   border-r, this only adds the falloff. */
.timeline-sticky-column,
.timeline-sticky-corner {
  box-shadow: 4px 0 6px -4px rgb(0 0 0 / 0.15);
}

/* The pinned cell has to be OPAQUE — week cells scroll underneath it — so it
   carries its own ground rather than inheriting the row's. That ground used to
   be an inline style, which beats the row's `hover:bg-gray-50` class, so the
   hover was painted by onmouseover/onmouseout handlers instead: script doing
   what a stylesheet does, on every row of a plan (bd sales-estimates-jrqy).
   Here it is a rule, so the row's hover reaches the pinned cell like any other.
   The cells keep an inline z-index, which genuinely varies per row. */
[data-sticky-column-cell] {
  background-color: var(--color-white);
}

tr:hover > [data-sticky-column-cell] {
  background-color: var(--color-gray-50);
}

/* A rounded corner on the sticky header cuts a wedge out of it, and the body rows
   scrolling up pass BEHIND that wedge — so the timeline shows through its own top
   corner as a small pointed sliver, changing colour with whatever row is under it.
   This paints the page into the wedge instead.
   It has to be a child of the header cell. Nothing at row level can do it: row and
   row-group backgrounds paint BELOW every cell in CSS table order, so colouring the
   header's row or group changes nothing — measured, not assumed. A child of the cell
   is inside the sticky header's own stacking context, which is above the body cells.
   z-index: -1 puts it behind the cell's rounded fill, so the fill covers all of the
   square but the wedge. The cell must not carry overflow: hidden, which would clip
   this away — and which never did anything for the corner anyway, since overflow does
   not clip a table-cell's background. */
.timeline-header-corner {
  position: relative;
}

.timeline-header-corner::after {
  content: "";
  position: absolute;
  top: -1px;
  right: -1px;
  width: 10px;
  height: 10px;
  z-index: -1;
  background-color: var(--layout-page-bg);
}

/* The gantt's scrollable canvas. The dependency SVG and [data-gantt-table] both
   live in here so they share one coordinate system and one width.

   The SVG must NOT be hoisted to a direct child of the scrollport: an
   `absolute; inset: 0` layer there would size to the VISIBLE box and clip every
   dependency arrow outside it. min-width: 100% keeps the canvas full-bleed when
   the timeline is narrower than the workspace (Fit mode, short plans). */
.gantt-canvas {
  position: relative;
  width: max-content;
  min-width: 100%;
}

/* Each row's timeline area — the one heavy box in the row; every bar, label,
   stripe and popover trigger lives inside it — skips layout and paint entirely
   while the workspace is scrolled far away from it. The area carries explicit
   inline width and height, so skipping its contents moves nothing: the row
   keeps its exact size and the week-grid background (a sibling layer) stays
   drawn. Controllers that MEASURE inside these areas cope explicitly: skipped
   bars read 0×0, so gantt-label-fit refits and gantt-dependencies redraws on
   contentvisibilityautostatechange, and a draw pass leaves 0×0 bars out rather
   than drawing arrows to the canvas origin. */
[data-timeline-area] {
  content-visibility: auto;
}

/* The shared week grid. Each row's single .gantt-week-grid div paints the
   whole background — week color bands, day + week hairlines, outside-project
   shading — from gradient layers declared once on [data-gantt-table] by
   gantt_week_grid_css_variables. This replaced ~7 DOM nodes per week per row,
   which at a row per estimate was most of the document. Layer order (top
   first) mirrors the old z-indexes: hairlines above the bounds shading above
   the bands. */
.gantt-week-grid {
  background-image: var(--gantt-grid-lines), var(--gantt-grid-bounds, none), var(--gantt-grid-bands);
  background-repeat: no-repeat;
  background-size: 100% 100%;
}

/* Drag state: gantt_interaction toggles this class on the canvas during a
   resize/reschedule instead of building per-week AM/PM overlay DOM. Hairlines
   get heavier and each day splits into a blue AM half and a light PM half. */
.gantt-grid-ampm-active .gantt-week-grid {
  background-image: var(--gantt-grid-lines-strong), var(--gantt-grid-ampm), var(--gantt-grid-bounds, none), var(--gantt-grid-bands);
}

/* ===========================================
   Responsive Team Member Column
   Narrow column with vertical text on mobile
   =========================================== */

/* Team member column - narrow with vertical text on mobile, wide on sm+ */
/* Width is dynamically calculated by mobile_column_sync_controller.js below sm breakpoint */
.team-member-column {
  width: auto;
  min-width: 40px;
}

@media (min-width: 640px) {
  .team-member-column {
    width: 200px;
    min-width: 200px;
    max-width: 200px;
  }
}

/* Vertical text container - shown only on mobile */
.team-member-vertical {
  writing-mode: vertical-rl;
  text-orientation: mixed;
  transform: rotate(180deg);
  /* vertical-rl + rotate(180deg) = text reads bottom-to-top, lines wrap left-to-right (correct order) */
  white-space: normal; /* Allow wrapping */
  padding: 4px;
  font-size: 0.5rem; /* Smaller font (8px) on mobile for better fit */
  line-height: 0.625rem; /* Tight line height */
  /* Center horizontally within the column */
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  width: 100%;
  /* Constrain height to fit within row - max 60px to fit in 68px row with padding */
  max-height: 60px;
  overflow: hidden;
}

/* Hide vertical content on sm+ screens */
@media (min-width: 640px) {
  .team-member-vertical {
    display: none;
  }
}

/* Full content container - hidden on mobile, shown on sm+ */
.team-member-full {
  display: none;
}

@media (min-width: 640px) {
  .team-member-full {
    display: block;
  }
}

/* ===========================================
   Container Queries for Timeline Controls
   Responsive layout based on container width
   =========================================== */

/* Define timeline controls as a container for container queries */
.timeline-controls-container {
  container-type: inline-size;
  container-name: timeline-controls;
}

/* Force stacked/wrapped layout when container is narrow */
/* This overrides the xl: breakpoint behavior based on container width */

/* At narrow widths, force the main flex container to column layout */
@container timeline-controls (max-width: 1000px) {
  .timeline-controls-row {
    flex-direction: column !important;
  }
}

/* At narrow widths, force buttons grid to single column */
@container timeline-controls (max-width: 700px) {
  .timeline-controls-buttons {
    display: grid !important;
    grid-template-columns: 1fr !important;
  }

  .timeline-controls-buttons > * {
    width: 100% !important;
  }

  /* Make direct child buttons fill their container, but not nested links in the view toggle */
  .timeline-controls-buttons > div > button,
  .timeline-controls-buttons > div > form > button {
    width: 100% !important;
  }
}

/* At medium widths, allow 2 columns for buttons */
@container timeline-controls (min-width: 701px) and (max-width: 1000px) {
  .timeline-controls-buttons {
    display: grid !important;
    grid-template-columns: repeat(2, 1fr) !important;
  }

  .timeline-controls-buttons > * {
    width: 100% !important;
  }

  /* Make direct child buttons fill their container, but not nested links in the view toggle */
  .timeline-controls-buttons > div > button,
  .timeline-controls-buttons > div > form > button {
    width: 100% !important;
  }
}

/* Split the Team/Features toggle down the middle once it goes full width.

   `> *`, NOT `a`: the ACTIVE side renders as a <span> rather than a link, so
   an a-only rule stretched whichever side you were NOT on and left the side
   you were on at its label width. It also flipped which half was wide when you
   switched views. The segments carry flex-1 themselves below xl — this rule
   is what re-establishes it above xl, where they take xl:flex-none to hug
   their labels in the roomy layout this container query says we are not in.
   Opening the chat dock is the everyday way to get here: a viewport past xl
   with the controls container under 1000px. */
@container timeline-controls (max-width: 1000px) {
  .timeline-view-toggle {
    width: 100% !important;
  }

  .timeline-view-toggle > * {
    flex: 1 !important;
  }
}

/* Force heading section to wrap when narrow */
@container timeline-controls (max-width: 500px) {
  .timeline-controls-heading {
    flex-direction: column !important;
    align-items: flex-start !important;
  }
}

/* ===========================================
   Container Queries for Staffing Table Footer
   Responsive layout based on container width
   =========================================== */

/* Define staffing table card as a container for container queries */
.staffing-table-container {
  container-type: inline-size;
  container-name: staffing-table;
}

/* At narrow widths, stack footer vertically with controls on top, instructions below */
@container staffing-table (max-width: 700px) {
  /* Stack footer vertically */
  .staffing-table-footer {
    flex-direction: column-reverse !important;
    align-items: stretch !important;
  }

  /* Instructions take full width */
  .staffing-table-footer > .text-sm.text-gray-600 {
    width: 100% !important;
  }

  /* Stack controls vertically */
  .staffing-table-controls {
    flex-direction: column !important;
    align-items: stretch !important;
    width: 100% !important;
  }

  .staffing-table-controls > * {
    width: 100% !important;
  }

  /* Make the save button full width */
  .staffing-table-controls input[type="submit"],
  .staffing-table-controls button[type="submit"] {
    width: 100% !important;
  }
}

/* At medium widths, show controls on top, instructions below */
@container staffing-table (min-width: 701px) and (max-width: 1000px) {
  .staffing-table-footer {
    flex-direction: column-reverse !important;
    align-items: stretch !important;
  }

  /* Instructions take full width */
  .staffing-table-footer > .text-sm.text-gray-600 {
    width: 100% !important;
  }

  .staffing-table-controls {
    flex-direction: row !important;
    align-items: center !important;
    justify-content: flex-end !important;
  }
}

/* At wide widths, show row layout with instructions left, controls right */
@container staffing-table (min-width: 1001px) {
  .staffing-table-footer {
    flex-direction: row !important;
    justify-content: space-between !important;
    align-items: flex-start !important;
  }

  .staffing-table-controls {
    flex-direction: row !important;
    align-items: center !important;
  }
}

/* Staffing table header - force column layout at narrow widths */
@container staffing-table (max-width: 500px) {
  .staffing-table-header {
    flex-direction: column !important;
    align-items: flex-start !important;
  }

  .staffing-table-header-buttons {
    width: 100% !important;
    flex-direction: column !important;
  }

  .staffing-table-header-buttons > form,
  .staffing-table-header-buttons > button {
    width: 100% !important;
  }

  .staffing-table-header-buttons > form > button,
  .staffing-table-header-buttons > form > input[type="submit"] {
    width: 100% !important;
  }
}

/* Staffing table header - row layout at medium widths */
@container staffing-table (min-width: 501px) {
  .staffing-table-header {
    flex-direction: row !important;
    align-items: center !important;
    justify-content: space-between !important;
  }

  .staffing-table-header-buttons {
    flex-direction: row !important;
  }
}

/* ── Comment thread cards (Linear-style) ── */

/* Thread card — wraps an entire comment thread (top-level + replies + reply input) */
.comment-thread-card {
  border: 1px solid var(--color-gray-200); /* gray-200 */
  border-radius: 0.5rem;
  background: var(--color-white);
}

/* Bump toolbar dropdown chevrons to 5px — upstream's 0.3ch (≈3px) is too small
   at our UI density */
.lexxy-editor__toolbar-dropdown--chevron summary::after {
  block-size: 5px !important;
  inline-size: 5px !important;
  margin-block-start: -1px;
}

/* Rendered lexxy content outside the editor — slightly smaller than default 16px */
.lexxy-content:not(.lexxy-editor__content) {
  font-size: 14px;
}

/* Land the placeholder where the first typed character will actually appear.
   (Jon 2026-08-11 — the caret sat a few px above its own placeholder, so
   typing did not replace the text it was standing on.)

   lexxy-editor.css pulls the first block up to seat the first glyph a fixed
   distance below the editor's top border — for a paragraph, and an empty
   editor's first block is always a paragraph:

     > :is(:first-child, .hidden + *):is(p:not(.hidden)…) { margin-top: -5.5px }

   The placeholder does not get that. It is an absolutely positioned ::before
   with no `top`, so it sits at its static position — the content box's padding
   edge — and misses the compensation the real paragraph receives. The caret
   ends up above its own placeholder by exactly that offset.

   Correcting the PLACEHOLDER rather than the paragraph on purpose: the -5.5px
   is Lexxy's deliberate first-glyph rhythm, so the typed text is where it
   belongs and the placeholder is the outlier. Moving the paragraph instead
   would drag every editor's first line out of that rhythm to meet a hint that
   disappears on the first keystroke.

   Not for compact editors: block-handles="false" resets those first-child
   margins to 0, so their caret and placeholder already agree.

   The literal is Lexxy's number, and spec/assets/lexxy_placeholder_spec.rb
   reads BOTH files and fails if they ever stop matching. */
lexxy-editor:not([block-handles="false"]) .lexxy-editor__content::before {
  margin-top: -5.5px;
}

/* Shared rich-text styles for Lexxy editor, rendered content, and preview.
   Tables: borders, padding, alternating rows.
   Strikethrough: <s>, <del>, <strike> elements. */
lexxy-editor table,
.lexxy-content table,
.rich-text-preview table {
  border-collapse: collapse;
  width: 100%;
  margin: 1rem 0;
  font-size: 0.875rem;
}

lexxy-editor th,
.lexxy-content th,
.rich-text-preview th {
  border: 1px solid var(--color-gray-300);
  padding: 0.5rem 0.75rem;
  text-align: left;
  font-weight: 600;
  background-color: var(--color-gray-50);
}

lexxy-editor td,
.lexxy-content td,
.rich-text-preview td {
  border: 1px solid var(--color-gray-300);
  padding: 0.5rem 0.75rem;
}

lexxy-editor s,
lexxy-editor del,
lexxy-editor strike,
lexxy-editor .lexxy-content__strikethrough,
.lexxy-content s,
.lexxy-content del,
.lexxy-content strike,
.lexxy-content .lexxy-content__strikethrough,
.rich-text-preview s,
.rich-text-preview del,
.rich-text-preview strike,
.rich-text-preview .lexxy-content__strikethrough {
  text-decoration: line-through;
}

lexxy-editor u,
lexxy-editor .lexxy-content__underline,
.lexxy-content u,
.lexxy-content .lexxy-content__underline,
.rich-text-preview u,
.rich-text-preview .lexxy-content__underline {
  text-decoration: underline;
}

/* Increase line-height only in paragraphs containing mention pills */
.comment-thread-card .prose p:has([class^="mention-tag"], mark),
.lexxy-content p:has([class^="mention-tag"], mark),
.markdown-content p:has([class^="mention-tag"], mark) {
  line-height: 1.75;
}

/* Code blocks in rendered markdown (chat messages, streaming content) */
.markdown-content pre {
  background: rgb(30 41 59);
  color: rgb(226 232 240);
  padding: 0.75rem 1rem;
  border-radius: 0.375rem;
  overflow-x: auto;
  margin: 0.5rem 0;
  font-size: 0.8125rem;
  line-height: 1.5;
}
.markdown-content pre code {
  background: none;
  padding: 0;
  color: inherit;
}
.markdown-content code {
  background: var(--color-gray-100);
  padding: 0.125rem 0.25rem;
  border-radius: 0.25rem;
  font-size: 0.8125rem;
}
.markdown-content pre code {
  background: none;
  padding: 0;
}

/* Mermaid diagram blocks — ```mermaid fences upgraded into a white card with a
   Diagram/Code tab header by mermaid_diagram_controller.js. The original dark
   <pre> becomes the Code tab, so it drops its margin/rounding inside the card. */
.mermaid-block pre {
  margin: 0;
  border-radius: 0;
}
.mermaid-diagram-pane svg {
  display: block;
  margin: 0 auto;
  max-width: 100%;
}

/* Blockquotes in rendered markdown (e.g. "Chat about this" quoted questions).
   currentColor keeps the bar readable on both gray (assistant) and blue (user) bubbles. */
.markdown-content blockquote {
  border-left: 3px solid currentColor;
  padding-left: 0.75rem;
  margin: 0.375rem 0;
  opacity: 0.85;
}

/* Links in rendered markdown (chat messages). Inherit the bubble's text color
   (white on the blue user bubble, dark on the gray assistant bubble) and lean on
   an underline + weight so a URL reads as clickable on either background without
   a hardcoded color that fails on one. Long URLs wrap instead of overflowing. */
.markdown-content a {
  color: inherit;
  font-weight: 500;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
  overflow-wrap: anywhere;
}
.markdown-content a:hover {
  text-decoration-thickness: 2px;
}

/* Flash-highlight a question card targeted via a #question-<id> anchor
   (deep links from the questionnaire pending-changes banner) */
[data-question-id][id^="question-"] {
  scroll-margin-top: 0.75rem;
}
[data-question-id][id^="question-"]:target,
.question-flash {
  animation: question-target-flash 2.5s ease-out 1;
}
@keyframes question-target-flash {
  0%, 50% {
    background-color: var(--color-purple-100);
    border-color: rgb(192 132 252);
  }
}

/* ── Compact comment inputs (Linear-style) ── */

/* Shared wrapper — single border around everything */
.comment-input-wrapper {
  position: relative;
  border: 1px solid var(--color-gray-200); /* gray-200 */
  border-radius: 0.5rem;
  background: var(--color-white);
  transition: border-color 0.15s;
}

.comment-input-wrapper:focus-within {
  border-color: var(--color-gray-400);
}

/* Borderless variant — used inside thread cards where the card provides the border */
.comment-input-wrapper--borderless {
  border: none;
  border-radius: 0;
  background: transparent;
}

.comment-input-wrapper--borderless:focus-within {
  border-color: transparent;
}

/* Borderless inline — container handles all alignment */
.comment-input-wrapper--borderless.comment-input-wrapper--inline {
  padding-left: 0;
  padding-bottom: 0;
}

/* Make the content div the containing block for the placeholder ::before.
   Without this, the placeholder is positioned relative to lexxy-editor while
   the cursor is inside .lexxy-editor__content (which has padding), causing
   a vertical misalignment. */
.lexxy-editor__content {
  position: relative;
  min-height: 100%;
}

/* Make the chat <lexxy-editor> actually scroll instead of spilling.

   Tailwind v4 compiles its utilities into `@layer utilities`, but Lexxy ships
   its editor CSS UNLAYERED. Unlayered rules beat any @layer regardless of
   specificity, so Lexxy's `:where(lexxy-editor){ overflow: visible }` silently
   wins over the host's `overflow-y-auto` utility. The result: a tall paste caps
   the box at `max-h-40` but paints the overflowing text outside it rather than
   scrolling inside. Re-assert overflow here as an unlayered rule — `lexxy-editor.overflow-y-auto`
   (0,1,1) beats Lexxy's `:where()` (0,0,0), and being unlayered it also beats the
   Tailwind utility layer. overflow-x is clipped so long/unbreakable lines never
   add a horizontal scrollbar to the whole editor. */
lexxy-editor.overflow-y-auto {
  overflow-y: auto;
  overflow-x: clip;
}

/* Lexxy's .lexxy-content class applies margin-block: 0 1rem on p. The first
   paragraph's bottom margin misaligns the placeholder ::before. */
lexxy-editor .lexxy-editor__content p:first-child {
  margin-top: 0;
  margin-block-start: 0;
}

/* Zero bottom margin on the only/last paragraph inside comment wrappers
   so the placeholder text aligns with the cursor vertically. */
.comment-input-wrapper lexxy-editor .lexxy-editor__content p:last-child {
  margin-bottom: 0;
  margin-block-end: 0;
}

/* Editor inside wrapper — no extra borders/background, wrapper provides those */
.comment-input-wrapper lexxy-editor {
  border: none;
  border-radius: 0;
  background: transparent;
  --lexxy-editor-rows: auto;
}

.comment-input-wrapper lexxy-editor .lexxy-editor__content {
  padding: 0.75rem 0.875rem; /* 12px vertical, 14px horizontal */
  min-height: 1.25rem;
  min-block-size: auto;
}

/* Hold the composer's height until <lexxy-editor> upgrades. An undefined
   custom element is display:inline with no box, so every composer paints at
   zero height and then JUMPS when Lexxy defines itself — worst in the plan
   feature modal, which is server-rendered open and resizes under the reader.
   45px is the defined editor's settled height (measured: 12px padding block
   + one line). The inline variant is a flex row with its own padding and a
   shorter settled height, so it is left alone rather than mis-reserved. */
.comment-input-wrapper:not(.comment-input-wrapper--inline) lexxy-editor:not(:defined) {
  display: block;
  min-height: 45px;
}

/* Tall variant — a form FIELD rather than a composer. A one-line box reads as
   an afterthought next to a field meant to hold a paragraph or two, so this
   hands sizing back to Lexxy's own knob: --lexxy-editor-rows feeds both the
   content's min-block-size and the pre-upgrade reservation above, which is
   what keeps the server-rendered-open plan feature modal from jumping when
   Lexxy defines itself. The two rules below re-assert what the compact
   composer rules zeroed out; both are equal specificity to those, so they
   must stay AFTER them. */
.comment-input-wrapper--tall lexxy-editor {
  --lexxy-editor-rows: 6lh;
}

.comment-input-wrapper--tall lexxy-editor .lexxy-editor__content {
  min-height: var(--lexxy-editor-rows);
  min-block-size: var(--lexxy-editor-rows);
}

/* Same shape as Lexxy's own :not(:defined) reservation, but with the 0.75rem
   block padding the wrapper rule above actually applies rather than Lexxy's
   --lexxy-editor-padding, which that rule overrode. */
.comment-input-wrapper--tall lexxy-editor:not(:defined) {
  min-height: calc(var(--lexxy-toolbar-height) + var(--lexxy-editor-rows) + 1.5rem);
}

/* Mention pill line-height — applies to all lexxy editors */
lexxy-editor .lexxy-editor__content p:has(mark) {
  line-height: 1.75;
}

/* Completed tasks: inherit line-through and dim mention pills.
   Exclude del elements — their red bg already signals deletion, and
   opacity traps tooltip pseudo-elements in a dim stacking context. */
.line-through:not(del) [class^="mention-tag-"] {
  text-decoration: line-through;
  opacity: 0.5;
}
del [class^="mention-tag-"] {
  text-decoration: line-through;
}

/* Task display + edit: vertically center pills within the text line so
   the checkbox aligns consistently whether mentions are present or not.
   (Tasks don't have the line-height: 1.75 that lexxy content paragraphs get.) */
[data-task-edit-target="display"] [class^="mention-tag-"],
.task-mention-editor [class^="mention-tag-"] {
  vertical-align: middle;
}

/* ── Task mention editor (contenteditable, wraps) ── */
.task-mention-editor {
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-700); /* gray-700 */
  padding: 0;
  border-bottom: 1px solid transparent;
  outline: none;
  overflow-wrap: break-word;
  word-break: break-word;
  min-width: 0;
}

.task-mention-editor:focus {
  border-bottom-color: var(--color-gray-300);
}

.task-mention-placeholder {
  position: absolute;
  left: 1.5rem; /* after the + icon + gap */
  top: 0.375rem;
  font-size: 0.875rem;
  color: var(--color-gray-400);
  pointer-events: none;
}

/* Actions bar — sits inside the wrapper border, pulled up with negative margin */
.comment-input-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.25rem;
  padding: 0 1rem 1rem 0;
}

/* Icon button (paperclip, etc.) */
.comment-input-action {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 0.375rem;
  color: var(--color-gray-400);
  transition:
    color 0.15s,
    background-color 0.15s;
  cursor: pointer;
  background: none;
  border: none;
  padding: 0;
}

.comment-input-action:hover {
  color: var(--color-gray-600); /* gray-600 */
  background-color: var(--color-gray-100); /* gray-100 */
}

/* Submit arrow button — matches avatar size (24px / 1.5rem) */
.comment-input-submit {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 0.375rem;
  color: white;
  background-color: var(--color-blue-600);
  transition: background-color 0.15s;
  cursor: pointer;
  border: none;
  padding: 0;
}

.comment-input-submit:hover {
  background-color: var(--color-blue-700);
}

/* ── Inline reply variant ── */
/* Avatar + editor + buttons all inside one bordered pill on one row */
.comment-input-wrapper--inline {
  display: flex;
  align-items: stretch;
  padding-left: 0.5rem;
}

.comment-input-wrapper--inline lexxy-editor {
  flex: 1;
  min-width: 0;
}

.comment-input-wrapper--inline lexxy-editor .lexxy-editor__content,
.reply-editor .lexxy-editor__content {
  padding-top: 1px;
  padding-bottom: 0;
}

.comment-input-wrapper--inline .comment-input-actions {
  flex-shrink: 0;
  padding: 0;
  display: flex;
  align-items: flex-end;
}

/* ── Internal-lane marking (bd sales-estimates-f574) ── */
/* Server-rendered tints: an internal thread card, and an internal reply row
   sitting inside an open thread. Amber custom properties invert for dark
   mode in theme.css. */
.comment-thread-card--internal {
  background: var(--color-amber-50);
  border-color: var(--color-amber-100);
}

.comment-reply--internal {
  background: var(--color-amber-50);
}

/* Decision venue row: the free-form input exists only while "Other" is the
   picked venue — pure :has(), no Stimulus state (same doctrine as the lock
   toggle below). */
[data-decision-venue] [data-decision-venue-other] {
  display: none;
}

[data-decision-venue]:has(select option[value="Other"]:checked) [data-decision-venue-other] {
  display: block;
}

/* Lock toggle pressed state — a native checkbox styled through :has(),
   no Stimulus state involved */
.comment-internal-toggle {
  cursor: pointer;
}

.comment-internal-toggle:has(input:checked) {
  color: var(--color-amber-600);
  background-color: var(--color-amber-100);
}

.comment-internal-toggle:has(input:focus-visible) {
  outline: 2px solid var(--color-amber-400);
  outline-offset: 1px;
}

/* Live tint while composing or editing an internal note. Out-specifies the
   --borderless variant's transparent background. */
.comment-input-wrapper:has(.comment-internal-toggle input:checked) {
  background: var(--color-amber-50);
}

/* Forced-internal indicator — replies under an internal parent. An
   indicator, not a control: the model seals the lane server-side. */
.comment-internal-forced {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  color: var(--color-amber-600);
}

/* Avatar inside the inline wrapper — anchored to top */
.comment-input-avatar {
  flex-shrink: 0;
  align-self: flex-start;
  position: relative;
  top: -3px;
}

/* ── Comment delete icon ── */
.comment-delete-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  padding: 0.25rem;
  color: var(--color-gray-400);
  border-radius: 0.25rem;
  cursor: pointer;
  transition:
    color 0.15s,
    background-color 0.15s;
}

.comment-delete-btn:hover {
  color: var(--color-red-500); /* red-500 */
  background-color: var(--color-red-50); /* red-50 */
}

/* Gray out the edit pencil button when another user is editing (presence span has content).
   Pure CSS — reacts instantly to Turbo Stream updates, no JS needed. */
:has(> [id^="editing-presence-"]:not(:empty)) > button[data-action*="edit"] {
  opacity: 0.3;
  pointer-events: none;
}

/* Lexxy editor body text uses 14px (0.875rem). Scoped to the content area
   so the toolbar inherits the page's base font-size (chevrons use ch units).
   Mention pills inside editors use 13px via the [class^="mention-tag-"] rule. */
lexxy-editor .lexxy-editor__content {
  font-size: 0.875rem;
}

/* Fallback mention pill style for bold text in comment editors.
   Inline styles from the JS controller provide per-type colors,
   but this catches any bold text as a baseline. */
.comment-editor b,
.comment-editor strong,
.reply-editor b,
.reply-editor strong {
  background-color: var(--color-blue-100); /* blue-100 */
  color: var(--color-blue-800); /* blue-800 */
  padding: 2px 4px;
  border-radius: 6px;
  font-weight: 500;
}

/* Mention pills rendered as <mark> by Lexical — universal styling
   so they look the same in comments, descriptions, PRDs, etc. */
lexxy-editor mark,
.lexxy-content mark,
.comment-thread-card mark,
.prose mark,
.rich-text-preview mark {
  background-color: transparent;
  border-radius: 6px;
  font-weight: 500;
}

/* Vertical padding + horizontal padding at word boundaries only for background-color marks */
lexxy-editor mark[style*="background-color"],
.lexxy-content mark[style*="background-color"],
.rich-text-preview mark[style*="background-color"] {
  padding: 2px 0;
}

lexxy-editor mark[style*="background-color"][data-pad-start],
.lexxy-content mark[style*="background-color"][data-pad-start],
.rich-text-preview mark[style*="background-color"][data-pad-start] {
  padding-left: 4px;
}

lexxy-editor mark[style*="background-color"][data-pad-end],
.lexxy-content mark[style*="background-color"][data-pad-end],
.rich-text-preview mark[style*="background-color"][data-pad-end] {
  padding-right: 4px;
}

/* Mention marks — 13px in both rendered content and inside editors.
   Exclude highlight marks (.lexxy-content__highlight) which should
   inherit font-size from their parent (e.g., headings). */
.lexxy-content mark:not(:has(.lexxy-content__highlight)),
.comment-thread-card mark:not(:has(.lexxy-content__highlight)),
.prose mark:not(:has(.lexxy-content__highlight)),
lexxy-editor mark:not(:has(.lexxy-content__highlight)),
.rich-text-preview mark:not(:has(.lexxy-content__highlight)) {
  font-size: 13px;
}

/*
 * Mention tag styles — keep colors in sync with:
 *   JS: MENTION_INLINE_STYLES in app/javascript/utils/lexxy_helpers.js
 *   JS: _inlineStyleForType in app/javascript/controllers/lexxy_mention_controller.js
 *   Ruby: MentionHtmlEnricher::MENTION_CSS in app/services/mention_html_enricher.rb
 */
/* Shared base — inline-block so the pill is treated as a single unit for
   line breaking: short mentions move to the next line as a whole if they
   don't fit. max-width + overflow-wrap let long mentions wrap internally
   when wider than the container. No white-space:nowrap — inline-block
   already handles the "stay together" behavior. */
[class^="mention-tag-"] {
  display: inline-block;
  max-width: 100%;
  overflow-wrap: break-word;
  vertical-align: baseline;
  padding: 0.075em 0.35em;
  border-radius: 0.25rem;
  font-weight: 500;
  font-size: 13px;
  line-height: 1.3;
  margin: 1px 0;
}

.mention-tag-user {
  background-color: var(--color-blue-100);
  color: var(--color-blue-800);
} /* blue */

/* Inside an editor a mention is not a .mention-tag-* span yet — Lexical keeps
   only color and background-color off the inserted span (see CLAUDE.md) and
   re-emits it as a bare <mark>, so it loses the "one unit" line breaking the
   rendered pill gets above. A person's name breaks at its space like ordinary
   prose: "@Eric" at the end of one line, "Verboncouer" at the start of the
   next.

   Only in a mentions-only field. There a background-color mark can only be a
   mention — the field ships no toolbar, so nobody can fill text — while in a
   prose editor the same selector would also catch a highlighted phrase, and
   holding THAT off a line break would push the paragraph sideways. */
.mentions-only lexxy-editor mark[style*="background-color"] {
  white-space: nowrap;
}

/* Bump line-height in ins blocks so unmarked lines match the height of prd-word-added marks */
.prd-annotated-content ins {
  line-height: 1.55;
}

/*
 * === PRD Diff @-mention styling ===
 *
 * Three contexts for @-mentions in the annotated PRD diff view (.prd-annotated-content):
 *
 * 1. UNCHANGED TEXT (outside <ins>/<del>):
 *    Mentions keep their normal clickable pill styling — no overrides needed.
 *    Controlled by the base [class^="mention-tag-"] rules above.
 *
 * 2. DELETED TEXT (<del> blocks):
 *    Currently: all: unset (plain text, no styling).
 *    To show dimmed pills instead: replace `all: unset` with `opacity: 0.5`.
 *    To show strikethrough pills: add `text-decoration: line-through`.
 *
 * 3. INSERTED TEXT (<ins> blocks):
 *    Currently: all: unset first (reset), then restore full pill styling.
 *    To show as plain text: remove the restore rules below.
 *    To only style genuinely NEW mentions (not carried-over): add `:has(> .prd-word-added)`
 *    to each restore selector (the <mark class="prd-word-added"> wrapper distinguishes new text).
 *    To add a green outline on new mentions: add `outline: 2px solid var(--color-green-300)`.
 *
 * The <mark class="prd-word-added"> inside mention spans is preserved by MentionResolver
 * and can be used as a CSS hook to distinguish genuinely new mentions from carried-over ones.
 */

/* Step 1: Reset mentions inside diff blocks to plain text */
.prd-annotated-content ins [class*="mention-tag"],
.prd-annotated-content del [class*="mention-tag"] {
  all: unset !important;
}

/* Step 2: Restore pill styling for mentions inside <ins> blocks */
.prd-annotated-content ins [class*="mention-tag"] {
  display: inline-block !important;
  padding: 0.075em 0.35em !important;
  border-radius: 0.25rem !important;
  font-weight: 500 !important;
  font-size: 13px !important;
  line-height: 1.3 !important;
  margin: 1px 0 !important;
  cursor: pointer !important;
}
/* Step 2b: Style the <mark> inside genuinely new mention spans to blend with prd-word-added.
   Targets the mark directly since :has() may not work reliably across all nesting depths. */
.prd-annotated-content [class*="mention-tag"] > mark.prd-word-added {
  background: transparent !important;
  font-size: 12px !important;
}
/* The mention span containing a new-content mark: green border, no rounding, flush height */
/* Outer span: solid green background matching prd-word-added, no rounding.
   Negative vertical margin pulls the box flush with surrounding text height. */
/* Outer: green background flush with adjacent prd-word-added marks */
.prd-annotated-content [class*="mention-tag"]:has(> mark.prd-word-added) {
  background: var(--color-green-300) !important;
  border-radius: 0 !important;
  border: none !important;
  display: inline !important;
  padding: 2px 0.5px 2.5px !important;
  margin: 0 -4px 0 0 !important;
  font-size: inherit !important;
}
/* The mark wrapper inside: shift up slightly to vertically center */
.prd-annotated-content [class*="mention-tag"] > mark.prd-word-added {
  background: transparent !important;
  position: relative;
  top: -0.5px;
}
/* Inner span: inset rounded pill */
.prd-annotated-content .mention-pill-inner {
  display: inline;
  padding: 0 3px;
  border-radius: 3px;
  font-weight: 500;
  font-size: 11px;
  box-decoration-break: clone;
}
.prd-annotated-content .mention-tag-user .mention-pill-inner {
  background-color: var(--color-blue-100);
  color: var(--color-blue-800);
}
.prd-annotated-content .mention-tag-feature .mention-pill-inner {
  background-color: var(--color-teal-100);
  color: var(--color-teal-900);
}
.prd-annotated-content .mention-tag-release .mention-pill-inner {
  background-color: var(--color-amber-100);
  color: var(--color-amber-800);
}
.prd-annotated-content .mention-tag-document .mention-pill-inner {
  background-color: var(--color-gray-100);
  color: var(--color-gray-700);
}
.prd-annotated-content .mention-tag-role .mention-pill-inner {
  background-color: var(--color-rose-100);
  color: var(--color-rose-800);
}
/* Remove rounding on adjacent prd-word-added marks (mention-tag or strong wrappers) */
.prd-annotated-content
  [class*="mention-tag"]:has(> mark.prd-word-added)
  + mark.prd-word-added,
.prd-annotated-content strong:has(> mark.prd-word-added) + mark.prd-word-added {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}
.prd-annotated-content
  mark.prd-word-added:has(+ [class*="mention-tag"] > mark.prd-word-added),
.prd-annotated-content strong:has(+ mark.prd-word-added) > mark.prd-word-added {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}
.prd-annotated-content ins .mention-tag-user {
  background-color: var(--color-blue-100) !important;
  color: var(--color-blue-800) !important;
}
.prd-annotated-content ins .mention-tag-feature {
  background-color: var(--color-teal-100) !important;
  color: var(--color-teal-900) !important;
}
.prd-annotated-content ins .mention-tag-release {
  background-color: var(--color-amber-100) !important;
  color: var(--color-amber-800) !important;
}
.prd-annotated-content ins .mention-tag-document {
  background-color: var(--color-gray-100) !important;
  color: var(--color-gray-700) !important;
}
.prd-annotated-content ins .mention-tag-role {
  background-color: var(--color-rose-100) !important;
  color: var(--color-rose-800) !important;
}

/* Clear the <mark> background inside mention pills so it doesn't layer with the pill bg */
.prd-annotated-content [class*="mention-tag"] > .prd-word-added {
  background: transparent !important;
}
.mention-tag-bot {
  background-color: var(--color-violet-100);
  color: var(--color-violet-700);
} /* violet */
.mention-tag-feature {
  background-color: var(--color-teal-100);
  color: var(--color-teal-900);
} /* teal */
.mention-tag-release {
  background-color: var(--color-amber-100);
  color: var(--color-amber-800);
} /* amber */
.mention-tag-document {
  background-color: var(--color-gray-100);
  color: var(--color-gray-700);
} /* slate */
.mention-tag-role {
  background-color: var(--color-rose-100);
  color: var(--color-rose-800);
} /* rose */

/* Clickable mentions - subtle hover, no vertical shift */
.mention-clickable {
  cursor: pointer;
}

.mention-clickable:hover {
  filter: brightness(0.95) saturate(1.5);
}

/* Lexxy slash-command prompt menu — override inline position:absolute set by
   Lexxy's JS so the viewport coordinates it calculates actually work correctly.
   position:fixed also prevents the menu from extending the editor's height and
   causing page scroll. */
.lexxy-prompt-menu {
  position: fixed !important;
}

/* Mention autocomplete dropdown */
.mention-autocomplete {
  position: fixed;
  z-index: 19999;
  max-height: 16rem;
  overflow-y: auto;
  background-color: var(--color-white);
  border: 1px solid var(--color-gray-200);
  border-radius: 0.5rem;
  box-shadow:
    0 10px 15px -3px rgb(0 0 0 / 0.1),
    0 4px 6px -4px rgb(0 0 0 / 0.1);
  width: 18rem;
}

.mention-autocomplete__category {
  padding: 0.375rem 0.75rem;
  font-size: 0.6875rem;
  color: var(--color-gray-400);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.mention-autocomplete__item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  width: 100%;
  text-align: left;
  cursor: pointer;
  border: none;
  background: none;
}

.mention-autocomplete__item:hover {
  background-color: var(--color-gray-100);
}

.mention-autocomplete__item--selected {
  background-color: var(--color-blue-100);
  color: var(--color-blue-900);
}

.mention-autocomplete__more {
  padding: 0.375rem 0.75rem;
  font-size: 0.75rem;
  color: var(--color-gray-400);
  border-top: 1px solid var(--color-gray-100);
}

.mention-clickable:active {
  transform: translateY(0);
}

/* Highlight animation for kanban cards when mention is clicked */
.mention-highlight {
  animation: mention-pulse 2s ease-out;
}

@keyframes mention-pulse {
  0% {
    box-shadow: 0 0 0 0 rgb(59 130 246 / 0.7); /* blue-500 */
    background-color: var(--color-blue-100); /* bg-blue-100 */
  }
  25% {
    box-shadow: 0 0 0 8px rgb(59 130 246 / 0.4);
    background-color: var(--color-blue-200);
  }
  50% {
    box-shadow: 0 0 0 12px rgb(59 130 246 / 0.2);
    background-color: var(--color-blue-100); /* bg-blue-100 */
  }
  100% {
    box-shadow: 0 0 0 0 rgb(59 130 246 / 0);
    background-color: var(--color-white);
  }
}

/* Mention popover (interactive tooltip in Lexxy editors) */
.mention-popover {
  position: fixed;
  z-index: 20000;
  background: var(--color-white);
  border: 1px solid var(--color-gray-200);
  border-radius: 8px;
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
  padding: 8px 12px;
  min-width: 120px;
  pointer-events: auto;
}

.mention-popover__label {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-gray-400);
  margin-bottom: 2px;
}

.mention-popover__name {
  font-size: 13px;
  font-weight: 500;
  color: var(--color-gray-900);
}

.mention-popover__link {
  display: inline-block;
  margin-top: 6px;
  font-size: 12px;
  font-weight: 500;
  color: var(--color-blue-600);
  text-decoration: none;
  cursor: pointer;
}

.mention-popover__link:hover {
  text-decoration: underline;
}

.mention-popover__actions {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 4px;
}

.mention-popover__actions .mention-popover__link {
  margin-top: 0;
}

/* File type icon colors for attachment cards in previews */
.lexxy-content .attachment--pdf {
  --lexxy-attachment-icon-bg: white;
  --lexxy-attachment-icon-border: oklch(55% 0.2 27);
  --lexxy-attachment-icon-text: oklch(30% 0.05 27);
}
.lexxy-content .attachment--md {
  --lexxy-attachment-icon-bg: white;
  --lexxy-attachment-icon-border: oklch(40% 0 0);
  --lexxy-attachment-icon-text: oklch(30% 0 0);
}
.lexxy-content .attachment--csv,
.lexxy-content .attachment--numbers {
  --lexxy-attachment-icon-bg: white;
  --lexxy-attachment-icon-border: oklch(55% 0.15 160);
  --lexxy-attachment-icon-text: oklch(45% 0.15 160);
}
.lexxy-content .attachment--xls,
.lexxy-content .attachment--xlsx {
  --lexxy-attachment-icon-bg: oklch(55% 0.15 160);
  --lexxy-attachment-icon-border: oklch(55% 0.15 160);
  --lexxy-attachment-icon-text: white;
}
.lexxy-content .attachment--txt,
.lexxy-content .attachment--rtf {
  --lexxy-attachment-icon-bg: oklch(55% 0 0);
  --lexxy-attachment-icon-border: oklch(55% 0 0);
  --lexxy-attachment-icon-text: white;
}
.lexxy-content .attachment--doc,
.lexxy-content .attachment--docx,
.lexxy-content .attachment--pages {
  --lexxy-attachment-icon-bg: white;
  --lexxy-attachment-icon-border: oklch(55% 0.196 258);
  --lexxy-attachment-icon-text: oklch(45% 0.196 258);
}
.lexxy-content .attachment--png,
.lexxy-content .attachment--jpg,
.lexxy-content .attachment--jpeg,
.lexxy-content .attachment--gif,
.lexxy-content .attachment--webp {
  --lexxy-attachment-icon-bg: oklch(55% 0.196 258);
  --lexxy-attachment-icon-border: oklch(55% 0.196 258);
  --lexxy-attachment-icon-text: white;
}
.lexxy-content .attachment--psd,
.lexxy-content .attachment--key,
.lexxy-content .attachment--sketch,
.lexxy-content .attachment--ai,
.lexxy-content .attachment--eps,
.lexxy-content .attachment--indd,
.lexxy-content .attachment--svg,
.lexxy-content .attachment--ppt,
.lexxy-content .attachment--pptx {
  --lexxy-attachment-icon-bg: white;
  --lexxy-attachment-icon-border: oklch(55% 0.2 27);
  --lexxy-attachment-icon-text: oklch(40% 0.15 27);
}
.lexxy-content .attachment--css,
.lexxy-content .attachment--php,
.lexxy-content .attachment--json,
.lexxy-content .attachment--htm,
.lexxy-content .attachment--html,
.lexxy-content .attachment--rb,
.lexxy-content .attachment--erb,
.lexxy-content .attachment--ts,
.lexxy-content .attachment--js {
  --lexxy-attachment-icon-bg: white;
  --lexxy-attachment-icon-border: oklch(55% 0.15 305);
  --lexxy-attachment-icon-text: oklch(40% 0.15 305);
}

/* Floating controls for attachment cards in document previews */
.rich-text-preview .lexxy-floating-controls {
  background: transparent;
  display: none;
  position: absolute;
  z-index: 1;
}

.rich-text-preview .attachment:hover .lexxy-floating-controls {
  display: block;
}

.rich-text-preview .lexxy-floating-controls__group {
  background: #171717;
  border-radius: 6px;
  display: flex;
  gap: 0;
  padding: 2px;
}

.rich-text-preview .lexxy-node-action {
  align-items: center;
  aspect-ratio: 1;
  background: transparent;
  block-size: 28px;
  border: none;
  border-radius: 6px;
  color: #d3d3d3 !important;
  cursor: pointer;
  display: flex;
  justify-content: center;
  min-block-size: 28px;
  min-inline-size: 28px;
  text-decoration: none;
}

.rich-text-preview .lexxy-node-action svg {
  block-size: 16px;
  inline-size: 16px;
  fill: currentColor;
  opacity: 0.8;
}

.rich-text-preview .lexxy-node-action:hover {
  background: #474747;
  color: white !important;
}

.rich-text-preview .lexxy-node-action:hover svg {
  opacity: 1;
}

/* Match editor card sizing and spacing */
.rich-text-preview .attachment--file .attachment__caption {
  padding: 0;
}

.rich-text-preview .attachment {
  margin-block-end: 8px;
}

lexxy-editor .attachment.attachment--file,
lexxy-editor .attachment.attachment--preview {
  margin-block: 0 4px;
}

.rich-text-preview .attachment--file .lexxy-floating-controls {
  inset-block-start: 50%;
  inset-inline-end: 1ch;
  inset-inline-start: unset;
  transform: translate(0, -50%);
}

.rich-text-preview .attachment--preview .lexxy-floating-controls {
  inset-block-start: 1.5ch;
  inset-inline-end: 1.5ch;
  inset-inline-start: unset;
}

/* ===========================================
   MoSCoW filter button hover effect
   Increases background opacity from 25% to 50% on hover
   =========================================== */

.moscow-filter-btn:hover {
  background-color: var(--hover-bg) !important;
}

/* ===========================================
   MoSCoW priority pill hover effect
   Increases background opacity from 25% to 50% on hover
   =========================================== */

.moscow-pill-btn:hover {
  background-color: var(--hover-bg) !important;
}

/* ===========================================
   Gantt estimate highlight animation
   Used when scrolling to newly scheduled estimate
   Applied to estimate container for visible bounding box effect
   =========================================== */

.estimate-highlight {
  z-index: 20 !important; /* Bring to front during highlight */
  animation: estimate-highlight-box 2s ease-out;
  pointer-events: none;
}

@keyframes estimate-highlight-box {
  0% {
    box-shadow:
      0 0 0 2px rgb(34 197 94 / 0.9),
      0 0 12px 4px rgb(34 197 94 / 0.4); /* green-500 */
    background-color: rgb(34 197 94 / 0.08);
  }
  50% {
    box-shadow:
      0 0 0 3px rgb(34 197 94 / 0.7),
      0 0 20px 8px rgb(34 197 94 / 0.3);
    background-color: rgb(34 197 94 / 0.12);
  }
  100% {
    box-shadow:
      0 0 0 0 rgb(34 197 94 / 0),
      0 0 0 0 rgb(34 197 94 / 0);
    background-color: rgb(34 197 94 / 0);
  }
}

/* ===========================================
   Drag-to-reschedule drop flash — briefly highlight the just-dropped feature (~6s) so the
   user can see where it landed. Only the dragged estimate flashes; cascaded/shifted bars do
   not. Applied by gantt-interaction's shared scheduled-work focus; the class is removed on
   animationend, reverting each element to its own border/background.

   The hour bar segment and release pill have no border of their own, so they get a yellow
   border; the overflow indicator's red border is swapped to yellow (border-color only). The
   name label has no background of its own, so it gets a yellow highlighter background.
   =========================================== */
/* Single outer bar outline (synthesized full-bar element, so a multi-day bar gets one ring
   instead of a line between each segment), release pill, and features-view aggregate bar: a
   yellow ring drawn with CSS `outline`. Outline is painted outside the box and never reflows, so
   applying/removing it can't shift the element's size or position (a `border` grows an auto-sized
   pill). outline-offset: -1px keeps it tight on the element edge, and outline follows
   border-radius so the rounding still matches the bar / pill. No z-index bump — the pill (wrapper
   z-index 11) and the outline/overflow indicator (z-index 10) already stack above the segments. */
[data-drop-flash-outline].gantt-drop-flash,
[data-release-badge].gantt-drop-flash,
[data-feature-aggregate-bar].gantt-drop-flash,
[data-feature-focus-target].gantt-drop-flash {
  outline: 1px solid rgb(250 204 21); /* yellow-400 */
  outline-offset: -1px;
  animation: gantt-drop-flash-border 4s ease-out;
}

/* When the person is overloaded the bar already has a full-width red overflow border — reuse it,
   swapping only the color to yellow (no width change → no reflow). */
[data-overflow-border].gantt-drop-flash {
  border-color: rgb(250 204 21) !important;
  animation: gantt-drop-flash-border 4s ease-out;
}

/* No `forwards`: after the animation the color reverts to the element's own value, and the JS
   removes the class on animationend (dropping the outline and the synthesized element). */
@keyframes gantt-drop-flash-border {
  0%,
  65% {
    border-color: rgb(250 204 21);
    outline-color: rgb(250 204 21);
  }
  100% {
    border-color: rgb(250 204 21 / 0.2);
    outline-color: rgb(250 204 21 / 0.2);
  }
}

/* Name label: yellow highlighter background with a little horizontal breathing room. The
   negative margins cancel the padding so the text doesn't move when the highlight is applied or
   removed — only the background box grows. */
[data-feature-label].gantt-drop-flash,
[data-feature-row-label].gantt-drop-flash {
  padding-left: 6px;
  padding-right: 6px;
  margin-left: -6px;
  margin-right: -6px;
  border-radius: 4px;
  animation: gantt-drop-flash-bg 4s ease-out forwards;
}

/* The flash background is a literal yellow in BOTH themes, so its text has to be
   dark in both — the label's own `text-gray-700` re-points to near-white in dark
   mode and left the name unreadable on the highlighter. Color is animated
   alongside the background rather than set outright: it starts dark while the
   yellow is opaque and lands on the label's own token as the yellow fades, so
   the handoff is invisible in light mode and correct in dark. */
@keyframes gantt-drop-flash-bg {
  0%,
  65% {
    background-color: rgb(250 204 21 / 0.9);
    color: rgb(24 24 27);
  }
  100% {
    background-color: rgb(250 204 21 / 0);
    color: var(--color-gray-700);
  }
}

/* ===========================================
   Rich text preview — shared styles for markdown-preview
   and rich-text-preview (Lexxy HTML document preview).
   Matches Lexxy editor appearance for consistency.
   =========================================== */

.markdown-preview h1,
.rich-text-preview h1 {
  font-size: 1.5rem;
  font-weight: 700;
  margin: 1.25rem 0 0.75rem;
  line-height: 1.3;
}
.markdown-preview h2,
.rich-text-preview h2 {
  font-size: 1.25rem;
  font-weight: 600;
  margin: 1rem 0 0.5rem;
  line-height: 1.35;
}
.markdown-preview h3,
.rich-text-preview h3 {
  font-size: 1.1rem;
  font-weight: 600;
  margin: 0.75rem 0 0.5rem;
  line-height: 1.4;
}
.markdown-preview h4,
.markdown-preview h5,
.markdown-preview h6,
.rich-text-preview h4,
.rich-text-preview h5,
.rich-text-preview h6 {
  font-size: 1rem;
  font-weight: 600;
  margin: 0.75rem 0 0.5rem;
}
.markdown-preview p {
  margin: 0.5rem 0;
  line-height: 1.6;
}
.rich-text-preview p {
  margin: 0 0 14px;
  line-height: 1.6;
}
.markdown-preview ul,
.rich-text-preview ul {
  list-style: disc;
  padding-left: 30px;
  margin: 0.5rem 0;
}
.markdown-preview ol,
.rich-text-preview ol {
  list-style: decimal;
  padding-left: 30px;
  margin: 0.5rem 0;
}
.markdown-preview li,
.rich-text-preview li {
  margin: 0.25rem 0;
  line-height: 1.6;
}
.markdown-preview code,
.rich-text-preview code {
  background: var(--color-gray-100);
  padding: 0.125rem 0.375rem;
  border-radius: 0.25rem;
  font-size: 0.875em;
}
.markdown-preview pre,
.rich-text-preview pre {
  background: var(--syntax-bg, rgb(30 41 59));
  color: var(--syntax-text, rgb(226 232 240));
  padding: 0.75rem 1rem;
  border-radius: 0.5rem;
  overflow-x: auto;
  margin: 0.75rem 0;
  font-size: 0.875rem;
  line-height: 1.6;
}
.markdown-preview pre code,
.rich-text-preview pre code {
  background: none;
  padding: 0;
  color: inherit;
}
.markdown-preview blockquote,
.rich-text-preview blockquote {
  border-left: 3px solid var(--color-gray-300);
  padding-left: 1rem;
  margin-left: 18px;
  color: var(--color-gray-500);
  margin-top: 0.75rem;
  margin-bottom: 0.75rem;
}
.markdown-preview a,
.rich-text-preview a {
  color: var(--color-blue-600);
  text-decoration: underline;
}
.markdown-preview a:hover,
.rich-text-preview a:hover {
  color: var(--color-blue-700);
}
.markdown-preview hr,
.rich-text-preview hr {
  border: none;
  border-top: 1px solid var(--color-gray-200);
  margin: 1rem 0;
}
.markdown-preview table,
.rich-text-preview table {
  border-collapse: collapse;
  width: 100%;
  margin: 0.75rem 0;
}
.markdown-preview th,
.markdown-preview td,
.rich-text-preview th,
.rich-text-preview td {
  border: 1px solid var(--color-gray-200);
  padding: 0.5rem 0.75rem;
  text-align: left;
}
.markdown-preview th,
.rich-text-preview th {
  background: var(--color-gray-50);
  font-weight: 600;
}

/* Lexxy wraps table cell content in <p> tags — tighten the margin */
lexxy-editor th p,
lexxy-editor td p,
.lexxy-content th p,
.lexxy-content td p,
.rich-text-preview th p,
.rich-text-preview td p {
  margin: 0;
}

.markdown-preview img,
.rich-text-preview img {
  max-width: 100%;
  border-radius: 0.375rem;
}
/* GFM strikethrough */
.markdown-preview del,
.markdown-preview s {
  text-decoration: line-through;
  color: var(--color-gray-500);
}
/* GFM task list checkboxes */
.markdown-preview ul:has(> li > input[type="checkbox"]) {
  list-style: none;
  padding-left: 0.25rem;
}
.markdown-preview li > input[type="checkbox"] {
  margin-right: 0.5rem;
  accent-color: var(--color-blue-600);
  vertical-align: middle;
  pointer-events: none;
}
/* GFM alternating table rows */
.markdown-preview tbody tr:nth-child(even) {
  background: var(--color-gray-50);
}

/* =======================================
   PRD inline suggestion styles (Google Docs-inspired)
   Block-level: entire paragraphs/headings are wrapped
   =======================================
*/

/* Block-level suggestion container — shows old crossed out, new highlighted */
.prd-suggestion-block {
  border-left: 3px solid rgb(59 130 246); /* blue-500 */
  padding-left: 12px;
  margin: 8px 0;
  cursor: pointer;
}

/* Deleted block content — struck through, dimmed */
del[data-suggestion-id] {
  display: block;
  background-color: var(--color-red-100); /* red-100 */
  text-decoration: line-through;
  color: var(--color-gray-500); /* gray-500 */
  padding: 4px 8px;
  border-radius: 4px;
  margin-bottom: 4px;
  cursor: pointer;
}

/* Inserted block content — green highlight */
ins[data-suggestion-id] {
  display: block;
  background-color: var(--color-green-100); /* green-100 */
  text-decoration: none;
  padding: 4px 8px;
  border-radius: 4px;
  cursor: pointer;
}

/* Word-level diff highlights within del/ins blocks.
   Consecutive changed words are merged into a single <mark> server-side. */
mark.prd-word-removed {
  background-color: var(--color-red-300); /* stands out against the red-100 parent */
  border-radius: 3px;
  padding: 1px 2px;
  color: inherit;
}

mark.prd-word-added {
  background-color: var(--color-green-300); /* stands out against the green-100 parent */
  border-radius: 3px;
  padding: 3px 2px;
  color: inherit;
}

/* Mentions inside ins blocks: keep their pill colors, add green outline */
ins[data-suggestion-id] [class^="mention-tag-"] {
  outline: 1.5px solid var(--color-green-300); /* green-300 — matches word-added bg */
  outline-offset: -1px;
}

ins[data-suggestion-id] [class^="mention-tag-"] mark.prd-word-added {
  background-color: transparent;
}

/* Added-only blocks (no old content) */
.prd-suggestion-block-add {
  border-left: 3px solid rgb(34 197 94); /* green-500 */
  padding-left: 12px;
  margin: 8px 0;
}

/* Preserve inner HTML structure within del/ins blocks */
del[data-suggestion-id] h2,
del[data-suggestion-id] h3,
del[data-suggestion-id] p,
del[data-suggestion-id] ul,
del[data-suggestion-id] ol,
del[data-suggestion-id] li,
ins[data-suggestion-id] h2,
ins[data-suggestion-id] h3,
ins[data-suggestion-id] p,
ins[data-suggestion-id] ul,
ins[data-suggestion-id] ol,
ins[data-suggestion-id] li {
  text-decoration: inherit;
  color: inherit;
}

/* Inline accept/reject buttons on diff blocks */
.prd-inline-suggestion-actions {
  float: right;
  position: relative;
  z-index: 1;
  display: flex;
  gap: 2px;
  margin: 0 0 4px 8px;
}

.prd-inline-suggestion-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 4px;
  border: 1px solid var(--color-gray-200);
  background: var(--color-white);
  cursor: pointer;
  opacity: 0.6;
  transition:
    opacity 0.15s,
    background-color 0.15s;
}

.prd-suggestion-block:hover .prd-inline-suggestion-btn,
.prd-suggestion-block-add:hover .prd-inline-suggestion-btn,
del.prd-suggestion-block:hover .prd-inline-suggestion-btn {
  opacity: 1;
}

.prd-inline-accept {
  color: var(--color-green-600);
}
.prd-inline-accept:hover {
  background: var(--color-green-100);
  border-color: rgb(22 163 74);
}
.prd-inline-reject {
  color: var(--color-red-500);
}
.prd-inline-reject:hover {
  background: var(--color-red-100);
  border-color: rgb(220 38 38);
}

/* Highlight pulse when clicking a suggestion card */
.prd-suggestion-highlight {
  outline: 2px solid rgb(59 130 246); /* blue-500 */
  outline-offset: 2px;
  border-radius: 4px;
  animation: suggestion-pulse 1.5s ease-out;
}

@keyframes suggestion-pulse {
  0% {
    outline-color: rgb(59 130 246);
  }
  100% {
    outline-color: transparent;
  }
}

/* Suggestion card active state — fades out to match inline highlight */
.prd-suggestion-card-active {
  animation: card-highlight-fade 1.5s ease-out forwards;
}

@keyframes card-highlight-fade {
  0%,
  60% {
    border-color: rgb(59 130 246);
    box-shadow: 0 0 0 1px rgb(59 130 246);
  }
  100% {
    border-color: var(--color-gray-200);
    box-shadow: none;
  }
}

/* Suggestion reply editor (contenteditable with @-mentions) */
.suggestion-reply-editor {
  outline: none;
  min-height: 1.5em;
  word-break: break-word;
}

.suggestion-reply-editor:empty + .suggestion-reply-placeholder {
  display: block;
}

.suggestion-reply-placeholder {
  display: none;
  position: absolute;
  left: 0.625rem;
  top: 50%;
  transform: translateY(-50%);
  font-size: 0.75rem;
  color: var(--color-gray-400);
  pointer-events: none;
}

/* Streaming overlay while AI is computing changes */
.prd-computing-overlay {
  position: absolute;
  inset: 0;
  background: color-mix(in oklab, var(--color-white) 85%, transparent);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10;
  border-radius: 4px;
}

@keyframes prd-spinner {
  to {
    transform: rotate(360deg);
  }
}
.prd-computing-spinner {
  width: 24px;
  height: 24px;
  border: 3px solid var(--color-gray-200);
  border-top-color: rgb(99 102 241);
  border-radius: 50%;
  animation: prd-spinner 0.8s linear infinite;
}

/* =======================================
   CSS-only tooltips via data-tooltip attr
   =======================================
   Usage: set data-tooltip="text" on any element (the local-time Stimulus
   controller does this automatically for <time> elements).
*/
[data-tooltip] {
  position: relative;
  cursor: pointer;
}
/* Tooltip body — always present, fades in/out */
[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  white-space: normal;
  text-align: left;
  max-width: 20rem;
  width: max-content;
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1rem;
  color: var(--color-gray-900);
  background: var(--color-white);
  border: 1px solid var(--color-gray-200);
  border-radius: 0.5rem;
  box-shadow:
    0 10px 15px -3px rgba(0, 0, 0, 0.1),
    0 4px 6px -4px rgba(0, 0, 0, 0.1);
  padding: 0.375rem 0.75rem;
  pointer-events: none;
  z-index: 19999;
  opacity: 0;
  transition: opacity 0.15s ease-in;
}
/* Arrow — always present, fades in/out */
[data-tooltip]::before {
  content: "";
  position: absolute;
  bottom: calc(100% + 2px);
  left: 50%;
  transform: translateX(-50%);
  border: 4px solid transparent;
  border-top-color: var(--color-gray-200);
  pointer-events: none;
  z-index: 19999;
  opacity: 0;
  transition: opacity 0.15s ease-in;
}
/* Show on hover with slight delay for JS auto-alignment */
[data-tooltip]:hover::after,
[data-tooltip]:hover::before {
  opacity: 1;
  transition-delay: 0.1s;
}
/* Below variant: data-tooltip-position="below" */
[data-tooltip][data-tooltip-position="below"]::after {
  bottom: auto;
  top: calc(100% + 6px);
}
[data-tooltip][data-tooltip-position="below"]::before {
  bottom: auto;
  top: calc(100% + 2px);
  border-top-color: transparent;
  border-bottom-color: var(--color-gray-200);
}

/* Right variant: data-tooltip-position="right" */
[data-tooltip][data-tooltip-position="right"]::after {
  bottom: auto;
  left: calc(100% + 6px);
  right: auto;
  top: 50%;
  transform: translateY(-50%);
  white-space: nowrap;
}
[data-tooltip][data-tooltip-position="right"]::before {
  bottom: auto;
  left: calc(100% + 2px);
  right: auto;
  top: 50%;
  transform: translateY(-50%);
  border-top-color: transparent;
  border-right-color: var(--color-gray-200);
  border-bottom-color: transparent;
  border-left-color: transparent;
}

/* Start-aligned variant: tooltip left edge aligns with trigger left edge.
   No :hover qualifier so position holds during fade-out. */
[data-tooltip][data-tooltip-align="start"]::after {
  left: 0;
  right: auto;
  transform: none;
}
[data-tooltip][data-tooltip-align="start"]::before {
  left: 6px;
  right: auto;
  transform: none;
}

/* End-aligned variant: tooltip right edge aligns with trigger right edge */
[data-tooltip][data-tooltip-align="end"]::after {
  left: auto;
  right: 0;
  transform: none;
}
[data-tooltip][data-tooltip-align="end"]::before {
  left: auto;
  right: 6px;
  transform: none;
}

/* Overlay variant: data-tooltip-overlay renders the SAME tooltip through one
   shared top-layer popover element (built lazily by application.js), escaping
   overflow-hidden ancestors that clip the pseudo-element tooltips — e.g. the
   rounded-corner group boxes inside the scrolling Resolve Overlaps modal.
   Authoring stays data-tooltip="text"; just add the data-tooltip-overlay
   attribute. Pseudo-element rendering is suppressed on these elements. */
[data-tooltip][data-tooltip-overlay]::after,
[data-tooltip][data-tooltip-overlay]::before {
  content: none;
}
/* Visual twin of [data-tooltip]::after. [popover] UA styles are overridden:
   inset/margin (UA centers popovers) and background (UA paints canvas). */
.tooltip-overlay[popover] {
  position: fixed;
  inset: auto;
  margin: 0;
  white-space: normal;
  text-align: left;
  /* Wider than the pseudo-element tooltips (20rem): overlay consumers carry
     date-range diffs ("Closes a 3.0h gap · 5.0h · Jul 21 – Jul 22 → ...")
     that read best on one line. Wrapping stays as the fallback so a long
     string can never run off-viewport. */
  max-width: 30rem;
  width: max-content;
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1rem;
  color: var(--color-gray-900);
  background: var(--color-white);
  border: 1px solid var(--color-gray-200);
  border-radius: 0.5rem;
  box-shadow:
    0 10px 15px -3px rgba(0, 0, 0, 0.1),
    0 4px 6px -4px rgba(0, 0, 0, 0.1);
  padding: 0.375rem 0.75rem;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s ease-in;
}
.tooltip-overlay[popover].tooltip-overlay-visible {
  opacity: 1;
  transition-delay: 0.1s;
}

/* Fulcrum PRD CTAs — toggle based on whether PRD has content */
/* .lexxy-content is added/removed by InlineEditorBaseController._updateDisplay() */
[data-prd-tab-content]:has([data-description-text].lexxy-content)
  [data-prd-empty-cta] {
  display: none;
}
[data-prd-tab-content]:not(:has([data-description-text].lexxy-content))
  [data-prd-filled-cta] {
  display: none;
}

/* PRD authoring page — chat empty-state CTA toggles with PRD content */
/* .lexxy-content is added/removed by InlineEditorBaseController._updateDisplay() */
[data-prd-authoring]:has([data-description-text].lexxy-content)
  [data-prd-chat-empty-cta] {
  display: none;
}
[data-prd-authoring]:not(:has([data-description-text].lexxy-content))
  [data-prd-chat-filled-cta] {
  display: none;
}

/* Hide chat empty-state CTA once any message has been appended to the list */
#prd-messages-list:has([id^="prd-message-"]) [data-prd-chat-no-messages] {
  display: none;
}

/* Estimate modal — prevent complexity button rows from wrapping so the modal sizes to content */
.estimates-matrix-nowrap [data-role-buttons] {
  flex-wrap: nowrap;
}

/* Estimate modal dialog positioning — width: fit-content prevents the dialog
   from stretching to fill the block container; centering is via auto margins */
dialog[id^="estimates-modal-"] {
  width: fit-content;
  margin-top: 10rem;
  margin-bottom: auto;
  margin-left: auto;
  margin-right: auto;
}

/* Modal scroll containment for EVERY app modal. Lives here, not in a feature
   stylesheet: Propshaft globs app/assets/**/*.css onto every layout, so these
   already govern the estimate, activity-log and AI-preference modals — putting
   them where an unrelated feature's scope audit could silently wrap or drop
   them was a regression waiting to happen. showModal() blocks clicks and focus
   but NOT touch pans — on iOS the page kept scrolling behind an open dialog.
   Pure CSS so every current and future showModal call is covered with no
   open/close lifecycle to leak:
   - body lock while any modal dialog is open (overflow:hidden preserves the
     scroll position, unlike the position:fixed trick);
   - pans that START on the backdrop die there;
   - a dialog's own internal scrolling never chains out to the page. */
body:has(dialog:modal) {
  overflow: hidden;
}
dialog:modal {
  overscroll-behavior: contain;
}
dialog:modal::backdrop {
  touch-action: none;
}

/* The plan feature dialog BEFORE it upgrades. A direct load of a feature URL
   renders this dialog `open` from the server, so first paint is already the
   modal instead of flashing it in on connect — but an open dialog is not
   :modal until showModal() runs: no top layer, no ::backdrop, no scroll lock.
   These rules stand in for all three, and every value has to match what the
   real modal produces, because anything that differs IS the flash:
   - z-index clears the sticky nav (z-50). At z-40 the backdrop stand-in
     tinted the page but not the nav bands, so the bars changed shade the
     moment the real backdrop took over.
   - box-shadow is ONE property, so the backdrop stand-in would REPLACE the
     panel's own shadow-xl and the drop shadow would pop in on upgrade. Rather
     than restate a value, this appends the backdrop layer to Tailwind's own
     five shadow slots — the exact composition .shadow-xl emits — so the panel
     keeps whatever shadow the utility and theme.css agree on, including dark
     mode's stronger --tw-shadow-color and its --tw-inset-ring-shadow rim
     light (see the elevation section in theme.css). Hardcoding the light
     value here left the shadow visibly weaker until the upgrade.
   - the tint reads the same token as backdrop:bg-gray-900/40, which theme.css
     repaints per mode (in dark it resolves LIGHT — the backdrop lightens). */
.plan-feature-dialog[open]:not(:modal) {
  position: fixed;
  inset: 0;
  z-index: 60;
  box-shadow:
    var(--tw-inset-shadow),
    var(--tw-inset-ring-shadow),
    var(--tw-ring-offset-shadow),
    var(--tw-ring-shadow),
    var(--tw-shadow),
    0 0 0 100vmax color-mix(in oklab, var(--color-gray-900) 40%, transparent);
}

/* The body lock above keys on :modal, which the server-rendered open state is
   not yet — same shape as the file modal's auto-open guard below. */
body:has(.plan-feature-dialog[open]) {
  overflow: hidden;
}

/* The client prioritize board's "Only movable" filter: one attribute on the
   board root hides every card the client cannot reprioritize, so the filtering
   costs no per-card JavaScript and survives cards arriving from a drop. The
   same two markers already drive SortableJS's drag filter, and the controller
   reuses this selector when it re-derives the column counts — one definition
   of "immovable", three consumers. Plain CSS rather than a `hidden` utility on
   the card: display utilities lose to whatever sorts after them (see the
   inline-flex trap), and this has to win outright. */
[data-movable-only="true"] [data-client-priority-board-target="card"][data-locked="true"],
[data-movable-only="true"] [data-client-priority-board-target="card"][data-scheduled="true"] {
  display: none;
}

/* ── Client prioritize board: select mode ─────────────────────────────────
   One attribute on the board root drives the whole affordance, the same shape
   as the "only movable" filter above: cards become pickable, the ones the
   board refuses to bulk-move say so by staying un-pickable, and a picked card
   wears a ring. No per-card JavaScript, and it survives a card arriving from
   a broadcast.

   .bulk-selected is the internal kanban's own class name, kept so the mark on
   a picked card means the same thing on both boards — see
   app/javascript/utils/bulk_selection.js, which is what sets it. */
[data-select-mode="true"] [data-client-priority-board-target="card"] {
  cursor: pointer;
}

/* The multi-select verbs grow leftward into the page-controls slot. Hide the
   ordinary view toggles while that mode is active so the canonical right-edge
   search keeps its space; Select remains visible as the mode's exit. */
[data-select-mode="true"] [data-prioritize-default-control] {
  display: none;
}

/* In this mode the card is a selection surface, not a drawer link. Taking the
   link out of pointer hit-testing makes every point on the card resolve to the
   wrapper the board selects; keyboard-generated link clicks still bubble to
   the controller and are intercepted there. */
[data-select-mode="true"] [data-client-priority-board-target="card"] > [data-feature-card] {
  pointer-events: none;
}

/* Archived, scheduled, and locked cards cannot join the next move. Their card
   component removes the body tint but preserves the full-strength priority
   stripe in normal browsing. Select mode dims them decisively so the cards
   that can join the pending move remain the visual foreground. */
[data-select-mode="true"] [data-priority="archived"] [data-client-priority-board-target="card"],
[data-select-mode="true"] [data-client-priority-board-target="card"][data-immovable="true"] {
  cursor: not-allowed;
  opacity: 0.25;
}

[data-client-priority-board-target="card"].bulk-selected [data-feature-card] {
  outline: 2px solid var(--color-indigo-500);
  outline-offset: 1px;
}

[data-select-mode="true"] [data-client-priority-board-target="card"].bulk-selected:hover {
  cursor: grab;
}

/* ── Client prioritize board: the allocation ribbon ───────────────────────
   Hours by priority as one bar, which is also the board's colour legend and
   its column show/hide control. Three things the utilities cannot express:

   1. A segment's labels have to drop out when THAT SEGMENT is narrow, and a
      segment's width comes from its flex-grow against the other four, not
      from the viewport — Must Have at 706h and Won't Have at 1h are the same
      breakpoint and wildly different widths. So each segment is its own
      container and the labels answer to it.
   2. inline-size containment zeroes a flex item's min-content contribution,
      which is what lets a 1h bucket shrink to its floor instead of being
      propped open by the word inside it. The floor is a min-width utility on
      the element; without it these collapse to nothing.
   3. The fill is an inline style (the priority's own hex, the same value the
      cards and column dots are painted from), so the hidden state has to be
      able to beat it — the controller clears the inline colour when it adds
      this class, exactly as recolorCard rewrites a card's inline style
      rather than keeping a second copy of the server's choice over in JS. */
.ribbon-seg {
  container-type: inline-size;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0.4375rem;
  height: 1.5rem;
  padding: 0 0.5rem;
  border: 1px solid transparent;
  border-radius: 0.3125rem;
  overflow: hidden;
  white-space: nowrap;
  cursor: pointer;
  /* The segments are painted in the MoSCoW hexes, which are chosen to read
     against a light card and stay put in dark mode — so the text on top of
     them is a fixed near-black in BOTH themes rather than a theme token that
     would turn white on yellow. */
  color: #0d1014;
  font-size: 0.6875rem;
  font-weight: 600;
  line-height: 1;
  transition: flex-grow 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

.ribbon-seg-hours {
  font-weight: 500;
  opacity: 0.72;
  font-variant-numeric: tabular-nums;
}

.ribbon-seg-off {
  border-color: var(--color-gray-300);
  border-style: dashed;
  color: var(--color-gray-400);
}

.ribbon-seg-off .ribbon-seg-hours {
  opacity: 1;
}

/* Narrow enough that the hours would crowd the label, then narrow enough
   that neither fits. Both live in the tooltip regardless, which is why a
   segment can afford to go wordless.

   Measured against the longest run each has to hold, and measured against
   the CONTENT box — a container query resolves to the container's content
   box, so the segment's 1rem of horizontal padding is already subtracted
   before these are compared. Setting them at border-box widths is what left
   real 5h buckets rendering as unlabelled colour chips: a 69px segment has
   only about 51px to put a word in, and the threshold was reading as though
   it had 69. A segment should go wordless only when the word truly will not
   fit — the numbers are in the tooltip either way. */
@container (max-width: 6rem) {
  .ribbon-seg-hours {
    display: none;
  }
}

@container (max-width: 2.5rem) {
  .ribbon-seg-label {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ribbon-seg {
    transition: none;
  }
}

/* Prevent page scroll flash when file modal will auto-open on page load.
   The JS connect() handler opens the modal and adds overflow-hidden to body
   a frame later — this CSS rule prevents the visible scroll during that frame. */
body:has([data-files-dropzone-auto-open-file-value]) {
  overflow: hidden;
}

/* ── Questionnaire: the All | Unanswered | Answered filter ────────────────
   questionnaire_filter_controller puts one of these classes on the page root
   (All puts none). Each question declares its own state — data-question-answered
   or data-question-unanswered, emitted by the answer form where there is one and
   by the thread itself on a read-only render. We hide any top-level thread — and
   any whole section — left with nothing of the kind being shown.
   Nested follow-up threads are deliberately not tagged as roots, so they stay
   with their parent thread rather than collapsing independently: a thread whose
   parent is answered and whose follow-up is not shows under BOTH filters. */
.questionnaire-hide-answered [data-question-thread-root]:not(:has([data-question-unanswered])) {
  display: none;
}
.questionnaire-hide-answered [data-question-section]:not(:has([data-question-unanswered])) {
  display: none;
}
.questionnaire-hide-unanswered [data-question-thread-root]:not(:has([data-question-answered])) {
  display: none;
}
.questionnaire-hide-unanswered [data-question-section]:not(:has([data-question-answered])) {
  display: none;
}

/* The question text is clickable only while there is a collapsed answer to
   open — which is to say, while the form under it says data-question-answered
   (bd sales-estimates-8bxs). CSS rather than a class the server writes: the
   toggle is bound whenever the viewer CAN answer, and the ANSWERED half
   changes under them when they save, because a save re-renders the form. A
   server-written class would have gone stale exactly then, which is the bug
   this replaces — an inline onclick rendered only for people who had already
   answered when the page loaded. */
[data-controller~="question-answer-toggle"]:has([data-question-answered]) [data-question-toggle] {
  cursor: pointer;
  transition: color 150ms;
}
[data-controller~="question-answer-toggle"]:has([data-question-answered]) [data-question-toggle]:hover {
  color: var(--color-blue-600);
}

/* Per-row staffing lock (staffing_lock_controller + row_lock_control partial).
   A locked row's edit affordances (allocation half-day buttons, fill/settings/
   assign/remove) are disabled — everything EXCEPT the row's own lock toggle,
   which must stay clickable so anyone can unlock. The lock toggle carries
   data-action="staffing-lock#toggle", hence the :not() exclusion. */
tr[data-pra-locked="true"] button:not([data-action*="staffing-lock"]),
tr[data-pra-locked="true"] a,
tr[data-pra-locked="true"] input:not([type="hidden"]) {
  pointer-events: none;
  opacity: 0.45;
}

/* Amber tint the locked row's week cells. Uses !important to beat the per-cell
   inline background-color set in the staffing table; the sticky name column is
   excluded so its lock icon and name stay legible. */
tr[data-pra-locked="true"] td:not(.team-member-column) {
  background-color: rgba(245, 158, 11, 0.08) !important;
}


/* ————— Color picker compact (short-viewport) layout —————
   The stacked ~360×520 panel cannot fit a landscape phone band (or any
   keyboard-squeezed viewport). cp-compact re-flows it into a wide 3-zone
   grid: header across the top, presets+wheel left, hex row + Apply stacked
   right, wheel square capped. The class is toggled by
   color_picker_controller#repositionPopover off visualViewport HEIGHT —
   media queries can't see the iOS keyboard (it never changes the layout
   viewport), so a height media query would miss the squeezed state.
   Unlayered on purpose: these override the component's Tailwind utilities. */
/* Belt for the cascade trap: this block's unlayered display:grid would beat
   Tailwind's LAYERED .hidden (display:none), making a compact panel
   unclosable. close() also removes cp-compact; this guarantees it. */
.cp-compact.hidden {
  display: none;
}
/* Rotation curtain for the picker: it fades out fast at the first rotation
   signal and back in after its re-fit passes settle — the reposition churn
   happens behind the veil (the slides stay visible; they anchor smoothly). */
[data-color-picker-target="popover"] {
  transition: opacity 170ms ease;
}
[data-color-picker-target="popover"].cp-rotating {
  opacity: 0;
  transition-duration: 60ms;
}
.cp-compact {
  width: 34rem;
  max-width: calc(100vw - 16px);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) 13rem;
  /* The two 1fr filler rows center the slider pair between the hex row and
     Apply, so the right column reads balanced instead of top-heavy. */
  grid-template-rows: auto auto minmax(0, 1fr) auto auto minmax(0, 1fr) auto;
  grid-template-areas:
    "header header header"
    "presets gradient hex"
    "presets gradient ."
    "presets gradient hue"
    "presets gradient alpha"
    "presets gradient ."
    "presets gradient apply";
  column-gap: 0.75rem;
  row-gap: 0.6rem;
}
/* Dissolve the stacked wrappers so their children seat directly into the
   grid — the right column becomes a real control stack (hex, hue, alpha,
   Apply) instead of leaving a void, and the gradient square gets the full
   center. Safe: wheelContainer's box is never measured (class-toggles only;
   the :not(.hidden) keeps its hide toggle working). */
.cp-compact > [data-cp="main"],
.cp-compact [data-color-picker-target="wheelContainer"]:not(.hidden) {
  display: contents;
}
.cp-compact > [data-cp="header"] {
  grid-area: header;
  margin-bottom: 0;
  /* The close X must stay tappable even if a mis-flowed pick surface (all
     touch-action:none + preventDefault) transiently overlaps the header. */
  position: relative;
  z-index: 1;
}
.cp-compact [data-cp="presets"] {
  grid-area: presets;
  align-content: start;
}
.cp-compact [data-color-picker-target="gradientPicker"] {
  grid-area: gradient;
  aspect-ratio: auto;
  height: auto;
  min-height: 11rem;
  margin-bottom: 0;
}
.cp-compact [data-color-picker-target="hueSlider"] {
  grid-area: hue;
  align-self: center;
  margin-bottom: 0;
}
.cp-compact [data-color-picker-target="alphaSlider"] {
  grid-area: alpha;
  align-self: center;
  margin-bottom: 0;
}
.cp-compact > [data-cp="hex"] {
  grid-area: hex;
  margin-bottom: 0;
  align-self: start;
}
.cp-compact > [data-cp="apply"] {
  grid-area: apply;
  align-self: end;
}

/* ————— Color picker touch layout (phones — ONE design, both orientations) ————— */
@media (pointer: coarse) {
  /* Finger-size pick surfaces in EVERY layout (stacked, phone grid,
     cp-compact): the 12px hue/alpha strips were sub-finger. rounded-full
     keeps the pill look at any height; the handles are centered on the
     bar's midline, so taller bars need no handle changes. */
  [data-color-picker-target="popover"] [data-color-picker-target="hueSlider"],
  [data-color-picker-target="popover"] [data-color-picker-target="alphaSlider"] {
    height: 1.25rem;
  }

  /* One compact grid for portrait AND landscape: a slim top row holds the
     label (the drag handle), the 8 preset swatches as a dot strip, and the
     X; below it the gradient fills the left zone beside a shrunk right
     stack (preview + hex, hue, alpha, Apply). Same DOM, CSS-only — the
     header and stacked wrappers dissolve (display: contents) so their
     children seat directly into the grid, which is what lets the presets
     sit BETWEEN the label and the X without moving markup.
     These rules deliberately DROP the :not(.cp-compact) guard the old
     portrait grid had: the controller still toggles cp-compact on
     height-squeezed viewports, but every rule here out-specifies the
     cp-compact block (attr + :not beats its lone class at equal-or-later
     source order), so a phone renders THIS one layout in any orientation or
     keyboard state while desktop's fine-pointer cp-compact stays intact.
     The :not(.hidden) keeps Tailwind's layered .hidden working — no
     unlayered display:grid zombie (the cp-compact belt above documents
     that trap). */
  [data-color-picker-target="popover"]:not(.hidden) {
    display: grid;
    width: 22rem;
    max-width: calc(100vw - 16px);
    padding: 0.75rem;
    /* EQUAL fixed outer columns (label / X): gradient spans cols 1-2 and the
       control stack cols 3-4, so symmetric outers are what make that split
       an exact 50/50 — auto-sized outers skewed it by the label-vs-X width
       difference. 2.6rem is the tightest that fits "Color" (longer labels
       ellipsize) — every reclaimed px goes to the preset circles. */
    grid-template-columns: 2.6rem minmax(0, 1fr) minmax(0, 1fr) 2.6rem;
    grid-template-rows: auto auto auto auto minmax(0, 1fr);
    grid-template-areas:
      "label presets presets close"
      "gradient gradient hex hex"
      "gradient gradient hue hue"
      "gradient gradient alpha alpha"
      "gradient gradient apply apply";
    column-gap: 0.5rem;
    row-gap: 0.5rem;
  }
  [data-color-picker-target="popover"]:not(.hidden) > [data-cp="main"],
  [data-color-picker-target="popover"]:not(.hidden) [data-color-picker-target="wheelContainer"]:not(.hidden) {
    display: contents;
  }
  /* Dissolve the header too: its label and X become grid items flanking the
     preset strip. The label is the remaining drag surface — startPanelDrag
     listens on the (boxless) header, and pointerdowns on the label still
     bubble through it; touch-action: none is what keeps the drag's
     pointermoves firing on touch (the old header box carried it). */
  [data-color-picker-target="popover"]:not(.hidden) > [data-cp="header"] {
    display: contents;
  }
  [data-color-picker-target="popover"]:not(.hidden) > [data-cp="header"] h3 {
    grid-area: label;
    align-self: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    cursor: move;
    touch-action: none;
    -webkit-user-select: none;
    user-select: none;
    padding-block: 0.2rem;
  }
  [data-color-picker-target="popover"]:not(.hidden) > [data-cp="header"] button {
    grid-area: close;
    align-self: center;
    position: relative;
    z-index: 1;
    display: grid;
    place-items: center;
    /* Fill the whole outer column — no dead gutter between the last circle
       and the X, and the tap target grows for free (icon stays centered). */
    width: 100%;
    height: 1.75rem;
  }
  /* The presets: a single strip between the label and the X, FLUID — each
     circle grows equally (flex: 1 + aspect-ratio) so the 8 of them + fixed
     gaps span the strip's full width exactly, and the row's height follows
     the grown circle size. The halo pads the tap target a little further
     out (same trick as the deck's jump tab). */
  [data-color-picker-target="popover"]:not(.hidden) [data-cp="presets"] {
    grid-area: presets;
    display: flex;
    align-self: center;
    gap: 0.3rem;
    min-width: 0;
  }
  [data-color-picker-target="popover"]:not(.hidden) [data-cp="presets"] > button {
    flex: 1 1 0;
    width: auto;
    height: auto;
    aspect-ratio: 1 / 1;
    min-width: 0;
    position: relative;
  }
  [data-color-picker-target="popover"]:not(.hidden) [data-cp="presets"] > button::before {
    content: "";
    position: absolute;
    inset: -0.2rem;
  }
  /* The strip stays 8 circles: the component renders all 12 presets (desktop
     shows every one) and marks the 4 phone-dropped extras data-cp-extra —
     they leave the strip here so each survivor keeps a finger-sized size. */
  [data-color-picker-target="popover"]:not(.hidden) [data-cp="presets"] > button[data-cp-extra] {
    display: none;
  }
  /* The gradient fills the whole left zone beside the right stack — pick
     math is offsetWidth/Height-relative, so a non-square surface is fine. */
  [data-color-picker-target="popover"]:not(.hidden) [data-color-picker-target="gradientPicker"] {
    grid-area: gradient;
    align-self: stretch;
    height: auto;
    min-height: 0;
    aspect-ratio: auto;
    margin-bottom: 0;
  }
  [data-color-picker-target="popover"]:not(.hidden) [data-color-picker-target="hueSlider"] {
    grid-area: hue;
    align-self: center;
    margin-bottom: 0;
  }
  [data-color-picker-target="popover"]:not(.hidden) [data-color-picker-target="alphaSlider"] {
    grid-area: alpha;
    align-self: center;
    margin-bottom: 0;
  }
  /* Preview + hex, shrunk ~1/3. 13px mono keeps #RRGGBBAA inside the narrow
     column; the deck editor's viewport meta already blocks iOS's sub-16px
     focus zoom on the surface where the phone picker actually lives. */
  [data-color-picker-target="popover"]:not(.hidden) > [data-cp="hex"] {
    grid-area: hex;
    align-self: start;
    margin-bottom: 0;
    min-width: 0;
    gap: 0.375rem;
  }
  [data-color-picker-target="popover"]:not(.hidden) > [data-cp="hex"] > button {
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 0.5rem;
  }
  [data-color-picker-target="popover"]:not(.hidden) > [data-cp="hex"] > div,
  [data-color-picker-target="popover"]:not(.hidden) > [data-cp="hex"] input {
    min-width: 0;
  }
  [data-color-picker-target="popover"]:not(.hidden) > [data-cp="hex"] input {
    height: 1.75rem;
    font-size: 13px;
    padding-inline: 0.4rem;
  }
  [data-color-picker-target="popover"]:not(.hidden) > [data-cp="apply"] {
    grid-area: apply;
    align-self: end;
    padding-block: 0.35rem;
    font-size: 13px;
  }
}

/* ————— Logo drop targets —————
   Every place the organization's or the client's mark renders is a slot you can
   drop a file on (shared/_logo_dropzone). The drag feedback lives here rather
   than in Tailwind utilities because the slots sit on wildly different grounds —
   a white settings card, a dark deck slide, a light deck slide — and the cue has
   to read on all of them. Literal colors for the same reason the ground chips
   use them: app dark mode redefines the color tokens, and this cue must not
   invert with it.

   The active attribute is set on the CONTROLLER element while a file is over the
   zone; the slot inside it is what carries the box. */
[data-logo-dropzone] {
  cursor: pointer;
  transition: border-color 0.12s ease, background-color 0.12s ease;
  /* The ground chip's colors, supplied per slot as custom properties
     (ClientLogoHelper::LOGO_CHIP). Read here rather than declared inline so the
     drag cue below can override them through the normal cascade. Deck slots set
     neither and fall through to their own Tailwind classes. */
  background-color: var(--logo-chip-bg, transparent);
  border-color: var(--logo-chip-border, currentColor);
}

[data-logo-dropzone-active] [data-logo-dropzone] {
  border-color: #60a5fa;
  background-color: rgb(96 165 250 / 0.14);
}

/* An empty slot's "+" brightens with the outline it sits in, so the whole target
   reacts as one thing rather than a box with an inert glyph in it. */
[data-logo-dropzone-active] [data-logo-dropzone-target="empty"] {
  color: #60a5fa;
  opacity: 1;
}

/* While an upload is in flight the slot stops inviting another one. */
[data-logo-dropzone-busy] [data-logo-dropzone] {
  cursor: progress;
  opacity: 0.6;
}

/* Electron desktop shell -----------------------------------------------------
   bridge.js stamps [data-desktop] on <html> inside the desktop app only; a
   browser never sees these rules.

   The window reserves a 28px title bar strip (TITLEBAR_H in the Electron app's
   src/shared/layout.ts) because macOS gives no drag region otherwise. Left
   alone that stacks a second gap above the nav, so the nav row gives back the
   same 28px and the strip becomes the nav's top padding instead of an extra
   bar. Total chrome height is unchanged from the browser. */
/* Somebody running the desktop menubar already has a timer on screen, and two
   of them is one too many. bridge.js stamps this when the app's "Hide the web
   timer" preference is on; the web has no opinion of its own about it. */
[data-hide-web-timer] [data-web-timer] {
  display: none;
}

/* The favorite star. Filled and colored from data-starred, which the timer
   controller flips on press — the state used to be baked into class names and
   the svg's own fill attribute, so pressing it changed nothing until reload. */
[data-timer-star] svg { fill: none; }
[data-timer-star] { color: var(--color-gray-300); }
[data-timer-star]:hover { color: var(--color-gray-500); }

[data-timer-star][data-starred="true"] { color: var(--color-amber-500); }
[data-timer-star][data-starred="true"] svg { fill: currentColor; }

/* The nav timer's glyph half.
 *
 * Driven entirely by the Stimulus running value, which Stimulus reflects back
 * onto the element as an attribute — so pressing the button repaints instantly
 * with no JS in the path, and the Turbo broadcast that follows simply confirms
 * what is already on screen. Toggling Tailwind classes from JS would have done
 * the same job while depending on those class names surviving elsewhere in the
 * app for the scanner to emit them.
 *
 * The colors are the Tailwind tokens, so they follow theme.css's dark-mode
 * inversion. Teal is deliberately not inverted, so white on it reads either way. */
/* Amber while PAUSED: that is where time sits on the clock, neither being
   worked nor yet recorded, and the only state anybody needs prompting out of.
   Grey said "nothing to see"; running is teal and needs no prompting at all. */
[data-timer-glyph] {
  background: var(--color-amber-500);
  color: #fff;
}

[data-effort-timer-running-value="true"] [data-timer-glyph] {
  background: var(--color-teal-500);
  color: #fff;
}

/* One cell for both glyphs, so the block is the same size whichever is showing
   and grid auto-placement can never put them in two rows. WHICH one shows is
   the `hidden` attribute the controller flips, not a rule here. */
[data-timer-glyph] > svg,
[data-timer-glyph] > [data-effort-timer-target] {
  grid-area: 1 / 1;
}

/* The desktop shell's terminal button, in the nav row left of the timer.
   Hidden until the shell's bridge stamps data-desktop on <html> — in a plain
   browser there is no terminal for it to open. */
.desktop-terminal-button {
  display: none;
}

[data-desktop] .desktop-terminal-button {
  display: inline-flex;
}

[data-desktop] [data-app-nav-row] {
  /* The window's own title bar strip already supplies the top breathing room,
     so the row drops its own. The bottom is matched to the space ABOVE the
     controls (27px) rather than left at the 12px that fell out of centring —
     unequal padding reads as the bar sitting slightly too high.

     height:auto rather than a shrunken height: the row centres its content, so
     reducing the height took the space off both ends and pressed the links
     against the page beneath them. */
  height: auto;
  padding-top: 0;
  padding-bottom: 27px;
}
