/*
 * Application-wide styles.
 */

/* Formula intellisense — the CM6 wrapper masquerades as a form-control.
   `:focus-within` lights up the wrapper when the embedded editor takes
   focus, so it gets the same blue ring a plain <input> would. The editor
   itself strips its outline (see `formulaTheme` in intellisense.js). */
.formula-cm-wrapper {
  cursor: text;
  background-color: var(--bs-body-bg, #fff);
}
.formula-cm-wrapper:focus-within {
  border-color: #86b7fe;
  box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
  outline: 0;
}
.formula-status {
  user-select: none;
}

/* Chat sidebar: viewport-bounded card with an internally-scrolling message
   panel and a sticky input footer. Without this the chat card grows with
   content, the page (not the panel) scrolls, and the input ends up below
   the visible viewport. */
.chat-card {
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - 90px);
}
.chat-card .chat-panel {
  flex: 1 1 auto;
  overflow-y: auto;
  min-height: 200px;
}
.chat-card .card-header,
.chat-card .card-footer {
  flex: 0 0 auto;
}
.chat-message + .chat-message {
  margin-top: 0.75rem;
}
.chat-message {
  border-left: 3px solid transparent;
  padding-left: 0.5rem;
}
.chat-message.user {
  border-left-color: #6c757d;
}
.chat-message.assistant {
  border-left-color: #0d6efd;
}

/* Markdown widgets edit in place (ux_mini_todo #41): the textarea should
   read like the card it replaces, not like a form control bolted on — no
   heavy border, grows with the text, monospace for raw markdown. */
.widget-markdown-edit {
  field-sizing: content;
  min-height: 4em;
  border-color: transparent;
  background: rgba(0, 0, 0, .02);
  resize: vertical;
  font-size: 12px;
}
.widget-markdown-edit:hover { border-color: #dee2e6; }
.widget-markdown-edit:focus { border-color: #86b7fe; background: #fff; }
@media screen {
  .mode-hack .widget-markdown-edit {
    background: rgba(255, 255, 255, .03);
    color: var(--hack-green);
    border-color: transparent;
  }
  .mode-hack .widget-markdown-edit:hover  { border-color: var(--hack-line); }
  .mode-hack .widget-markdown-edit:focus  { border-color: var(--hack-green-dim); background: var(--hack-bg); }
}

/* Widget drag-and-drop layout */
.widget-slot {
  cursor: grab;
  /* The resize handle inside is absolutely positioned; this is its
     containing block. */
  position: relative;
}
.widget-slot.dragging { opacity: 0.4; }

/* Drag handle on the slot's right edge for column-span resize. Subtle
   by default (so it doesn't visually fight the chart), full-opacity on
   hover. The whole rule set is hidden on print below via the same
   media query that already drops chat/nav/buttons. */
.widget-resize-handle {
  position: absolute;
  top: 0;
  right: -2px;        /* sit half-on, half-off the gutter */
  width: 6px;
  height: 100%;
  cursor: ew-resize;
  background-color: #0d6efd;
  opacity: 0.25;
  border-radius: 3px;
  z-index: 5;
  transition: opacity 0.15s;
}
.widget-resize-handle:hover { opacity: 1; }
/* While a resize drag is in flight, override the body cursor so the
   ew-resize cursor sticks even when the pointer drifts off the handle. */
body.widget-resizing,
body.widget-resizing * { cursor: ew-resize !important; user-select: none; }
.widget-row.drop-target,
.widget-row-new.drop-target {
  outline: 2px dashed #0d6efd;
  outline-offset: 4px;
  border-radius: 6px;
}
.widget-row-new {
  opacity: 0.55;
  transition: opacity 0.15s;
}
.widget-row-new:hover { opacity: 1; }
/* Inline new-row zones sit BETWEEN every existing row, so there can be
   many on the page. Tone them down so they don't overwhelm the layout —
   they still snap to full opacity on hover or when a drag is in progress
   (a sibling .dragging triggers the body-level affordance). */
.widget-row-new.inline {
  opacity: 0.2;
  font-size: 0.75rem;
}
.widget-row-new.inline:hover,
.widget-row-new.inline.drop-target { opacity: 1; }
.border-dashed { border-style: dashed !important; }

/* Turbo Frames are custom elements whose default display is `inline`.
   That means any widget rendered inside one — and especially Vega
   charts with `width: "container"` — measure their parent at width=0
   and render to nothing. Forcing block display gives the frame the
   surrounding col's width as it should have all along. */
turbo-frame { display: block; }

/* Markdown widgets — redcarpet-rendered narrative inside a card.
   Minimal: snug list/heading margins so the body reads as a single
   block. Inherits Bootstrap typography otherwise. */
.widget-markdown {
  padding: 0.25rem 0.5rem;
}
.widget-markdown > :first-child { margin-top: 0; }
.widget-markdown > :last-child  { margin-bottom: 0; }
.widget-markdown h1,
.widget-markdown h2,
.widget-markdown h3,
.widget-markdown h4 { margin-top: 1rem; margin-bottom: 0.5rem; }
.widget-markdown ul,
.widget-markdown ol { padding-left: 1.5rem; }
.widget-markdown pre {
  background: #f5f5f5;
  padding: 0.5rem 0.75rem;
  border-radius: 4px;
  overflow-x: auto;
}
.widget-markdown code {
  background: #f5f5f5;
  padding: 0.05rem 0.25rem;
  border-radius: 3px;
  font-size: 0.9em;
}
.widget-markdown pre code { background: transparent; padding: 0; }
/* MARKDOWN tables are PROSE tables — the hypothesis table is three columns
   of sentences — so they WRAP. That is the opposite of the widget-rendered
   tables below, which must never wrap because their rows have to align with
   sibling tables; here each cell is a paragraph and a nowrap cell pushes the
   table off the page. Erik 2026-10-21, on the hypothesis table scrolling
   sideways: "line breaks is better for free text".
   `white-space: normal` breaks at SPACES only, so a long sentence wraps and a
   krona figure (no spaces inside) still cannot be split across lines.
   `overflow-x: auto` on the block is the fallback for a table that is wide
   even when wrapped (a 15-column pivot): it gets its own scrollbar instead of
   making the whole report scroll. */
.widget-markdown { overflow-x: auto; }
.widget-markdown table {
  border-collapse: collapse;
  margin: 0.5rem 0;
  width: 100%;
  max-width: 100%;
}
.widget-markdown th,
.widget-markdown td {
  border: 1px solid #dee2e6;
  padding: 0.25rem 0.5rem;
  white-space: normal;
  vertical-align: top;
}

/* Vega-embed default styling is `display: inline-block` with width
   set by `.vega-embed .chart-wrapper.fit-x` — a DESCENDANT selector
   that only matches a nested .chart-wrapper. Single-view charts
   (which is everything we author) don't have that nested wrapper, so
   the outer `.vega-embed.fit-x` element gets no width rule and
   collapses to inline-block's content width = 0. Override to block +
   full width so Vega can measure the container properly. */
.vega-embed { display: block; width: 100%; }
.vega-embed > svg { width: 100%; max-width: 100%; }

/* Hide the horizontal scrollbar on the dashboard tab strip — the strip
   still scrolls via touch/wheel; the visible bar was ugly when present.
   Add a soft fade on the right edge so overflow is discoverable. */
#dashboard-page-tabs::-webkit-scrollbar { display: none; }
#dashboard-page-tabs { scrollbar-width: none; -ms-overflow-style: none; }

/* Widget-rendered tables (output of DeclarativeRenderer.render_table)
   should never wrap. A wrapped cell shoves the row down and breaks
   alignment with sibling tables on the same page. Long text overflows
   horizontally; the parent `.table-responsive` wrapper already
   provides horizontal scroll so wide tables stay reachable. Long-text
   columns that NEED wrapping (e.g. review comment bodies) can opt back
   in by setting `white-space: normal` on the specific <td>. */
.table-responsive .table td,
.table-responsive .table th { white-space: nowrap; }

/* …and this is that opt-back-in, applied where it is always needed: TEXT
   columns. `cell_align_class` gives every NUMERIC cell `text-end` and leaves
   text cells unclassed, so `:not(.text-end)` is exactly "this column holds
   words" — no renderer change needed to know which is which.
   Why it matters (report 88, 2026-10-21): the table-extraction step LIFTS a
   prose markdown table into a rendered widget, which is what we want, but the
   nowrap rule above then clipped it. The hypothesis table lost its entire
   third column ("Hur vi prövar den") behind a scrollbar, and the day table cut
   its Kommentar mid-sentence. Numbers still never wrap, so rows stay aligned
   across sibling tables — that is what the rule above is protecting, and a
   krona figure has no spaces to break at anyway.
   `word-break: keep-all` because the first pass broke the Datum column at its
   hyphen ("2026-09-" / "05") — a date is a left-aligned TEXT cell, so it got
   the wrap, and a hyphen is a line-break opportunity. keep-all suppresses
   breaks INSIDE a value while leaving spaces between words breakable, which is
   exactly the distinction wanted: sentences wrap, dates and ids do not. */
.table-responsive .table td:not(.text-end) { white-space: normal; word-break: keep-all; }

/* Dashboard pages: rendered as Bootstrap tabs on screen; expanded into
   page-breaking sections on print. */

@page {
  /* Landscape suits the 12-col widget grid; portrait squeezes wide
     tables. 1cm margin balances ink-on-page against printer-safe
     gutters across the common printers. Bottom margin slightly
     wider (1.4cm) to reserve room for the @bottom-* footer boxes
     (page numbers + vibeinsights.io attribution). */
  size: A4 landscape;
  margin: 1cm 1.2cm 1.4cm 1.2cm;

  @bottom-left {
    content: "Generated by vibeinsights.io";
    font-family: system-ui, sans-serif;
    font-size: 8pt;
    color: #888;
  }
  @bottom-right {
    content: "Sida " counter(page) " av " counter(pages);
    font-family: system-ui, sans-serif;
    font-size: 8pt;
    color: #888;
  }
}

/* Named pages for per-DashboardPage orientation overrides. Pages that
   set `dashboard_page.print_orientation` get the matching class on
   their .dashboard-page section, and CSS named-page support pulls the
   right `@page` rule. Modern browsers (Chromium / Firefox) honor this;
   safe degradation = the dashboard-wide default above. Footer boxes
   redeclared per named-page rule — @page properties don't cascade
   through named-page inheritance. */
@page portrait_page {
  size: A4 portrait;
  margin: 1cm 1.2cm 1.4cm 1.2cm;
  @bottom-left {
    content: "Generated by vibeinsights.io";
    font-family: system-ui, sans-serif;
    font-size: 8pt;
    color: #888;
  }
  @bottom-right {
    content: "Sida " counter(page) " av " counter(pages);
    font-family: system-ui, sans-serif;
    font-size: 8pt;
    color: #888;
  }
}
@page landscape_page {
  size: A4 landscape;
  margin: 1cm 1.2cm 1.4cm 1.2cm;
  @bottom-left {
    content: "Generated by vibeinsights.io";
    font-family: system-ui, sans-serif;
    font-size: 8pt;
    color: #888;
  }
  @bottom-right {
    content: "Sida " counter(page) " av " counter(pages);
    font-family: system-ui, sans-serif;
    font-size: 8pt;
    color: #888;
  }
}
.dashboard-page.print-portrait  { page: portrait_page; }
.dashboard-page.print-landscape { page: landscape_page; }

/* ── Table templates ───────────────────────────────────────────────
   Three named visual templates a widget's TablePresentation#template
   picks from. `normal` is the default Bootstrap-stripe look (no rule
   block — the base .table.table-striped already covers it).

   Per-widget row_color_indexes / column_color_indexes / Metric#color
   paint INLINE styles in the renderer, which beat these CSS rules —
   author colors still win on top of the template.

   NOTE: the accent hexes here (e.g. #c3d598, #D8E4BC for tsb_green)
   ALSO live in `TEMPLATE_PALETTES` in
   `app/pipelines/model/declarative_widget_spec.rb`. This CSS handles
   the static chrome (header bg, banner accent); the Ruby palette
   handles per-cell tints. Same hex family, different roles. If you
   retune a template's color story, touch BOTH places. */

/* business: P&L-report aesthetic. White everywhere, no fills, no
   stripes — just horizontal rules and typography. Header is bold
   text under a top + bottom rule. Section banners read as bold
   typography, not coloured bars. Use for board-deck pages and
   month-end reports where stripes / fills read as decoration noise.
   NOTE: no `!important` on the stripe rule — inline styles painted by
   the renderer's column_color_indexes resolution must win over the
   template stripe. Specificity alone is enough to beat Bootstrap's
   .table-striped default. */
.table-template-business table {
  background: transparent;
}
.table-template-business table.table-striped > tbody > tr:nth-of-type(odd) > * {
  background: transparent;
}
.table-template-business table thead th {
  background: transparent;
  color: #212529;
  border-top: 1px solid #212529;
  border-bottom: 1px solid #212529;
  font-weight: 600;
}
/* Section banner row: bold black text on white, single top rule —
   the typographic equivalent of an accounting page's section header. */
.table-template-business table.table-secondary,
.table-template-business table tr.table-secondary,
.table-template-business table tr.table-secondary > * {
  background: transparent !important;
  color: #212529 !important;
  border-top: 1px solid #adb5bd !important;
  font-weight: 600;
}
/* Totals footer: single rule on top, bold — accounting "underline the
   total" convention. */
.table-template-business table tfoot tr {
  border-top: 1px solid #212529;
}
.table-template-business table tfoot td {
  background: transparent !important;
  font-weight: 700;
}

/* tsb_green: sage / olive green branded look. Palette lifted from
   widget 14.1's column_colors (#D8E4BC, #c3d598) — subtle Excel-era
   corporate greens, not Pantone forest. Header carries the deeper
   shade; section banners and column tint use the lighter one. */
/* No `!important` on the stripe — inline column_color_indexes
   resolution must win on cells that carry a column tint. Higher
   specificity than Bootstrap's .table-striped alone is enough. */
.table-template-tsb_green table.table-striped > tbody > tr:nth-of-type(odd) > * {
  background: #f5f8ee;
}
.table-template-tsb_green table thead th {
  background: #c3d598;
  color: #2a3d10;
  border-bottom: 2px solid #9bb86c;
}
.table-template-tsb_green table.table-secondary,
.table-template-tsb_green table tr.table-secondary,
.table-template-tsb_green table tr.table-secondary > * {
  background: #D8E4BC !important;
  color: #2a3d10 !important;
}

/* Copy-table-to-clipboard button: float top-right of the table widget's
   card-body. Visible in both edit and view modes so Erik can grab the
   rendered table for pasting into chat. */
.widget-copy-table-btn {
  position: absolute;
  top: 4px;
  right: 8px;
  z-index: 2;
  font-size: 0.85rem;
  line-height: 1;
  opacity: 0.5;
}
.widget-copy-table-btn:hover {
  opacity: 1;
}

/* Loading indication for a widget frame (Erik 2026-10-28). Turbo marks the
   frame aria-busy while its GET is in flight — a picker click, a twisty, the
   lazy first load — and the morph keeps the old table on screen meanwhile,
   so without this nothing moves for the whole request. The table dims and a
   small spinner sits top-right of the card body; the picker stays crisp so
   a second quick click still reads as live. */
turbo-frame[aria-busy="true"] .widget-card .card-body table {
  opacity: 0.45;
  transition: opacity 0.15s ease-in 0.1s;
}
turbo-frame[aria-busy="true"] .widget-card .card-body::after {
  content: "";
  position: absolute;
  top: 0.5rem;
  right: 2rem;
  width: 1rem;
  height: 1rem;
  border: 2px solid #adb5bd;
  border-right-color: transparent;
  border-radius: 50%;
  animation: widget-busy-spin 0.75s linear infinite;
}
@keyframes widget-busy-spin { to { transform: rotate(360deg); } }

/* Pivot groups: the first column of each pivot value (and of the trailing
   Totalt group) carries a left rule, so a unit's metrics read as one block.
   See HtmlTableRenderer#pivot_group_start. */
.widget-card table .pivot-group-start {
  border-left: 2px solid #ced4da;
  padding-left: 0.6rem;
}

@media print {
  .widget-copy-table-btn { display: none !important; }

  /* Flat background, no shadows — printers / PDFs render shadows as
     grey halos and waste ink on card backgrounds. `body.bg-light` is
     applied in the application layout for the on-screen dashboard
     look; its own `!important` rule has class-level specificity and
     beats a plain `body` selector, so the print override has to match
     the same specificity to win the cascade. */
  body, body.bg-light { background: #fff !important; }

  /* (Removed) earlier `.print-logo { position: fixed }` rule. Logo
     now flows inline in the page-heading row inside `.dashboard-
     page-content` (see print.html.erb) — Erik's preference: no
     position:fixed / no @page margin reservation / no body
     padding-top. Logo appears on the first sheet of each .dashboard-
     page; continuation sheets (when a table flows) don't repeat. */
  .card, .widget-card {
    box-shadow: none !important;
    border-color: #dee2e6 !important;
    background: #fff !important;
  }

  /* When the author opted out of the widget title (show_title=false),
     strip the card chrome entirely in print — no border, no padding,
     no background. The chart renders naked under the page heading,
     which is already naming the section. */
  .widget-card.widget-no-title {
    border: none !important;
    background: transparent !important;
  }
  .widget-card.widget-no-title .card-body {
    padding: 0 !important;
  }

  /* App chrome the document shouldn't carry onto paper. The sidebar
     and chat dock already opt out via `.d-print-none`; the top navbar
     had no such opt-out, so it printed as a black band above reports
     (reports render through the normal application layout — unlike
     dashboards, which have their own print.html.erb). */
  .navbar { display: none !important; }

  /* …but hiding those elements is NOT enough. The offsets they impose
     live on `body` / `.container-fluid` through `:has()` selectors
     (see the L2-sidebar and chat-dock blocks further down), and a
     `display: none` element STILL MATCHES `:has()` — it stays in the
     DOM and the selector never stops applying. So print inherited the
     240px sidebar gutter and the ~100px dock reservation while the
     sheet width stayed fixed, pushing the document off the right edge:
     text and tables clipped mid-word (Erik, 2026-08-11, /reports/2).
     Reset them here at matching specificity. */
  body:has(.l2-sidebar[open]) .container-fluid,
  body:has(.l2-sidebar:not([open])) .container-fluid {
    padding-left: 0 !important;
    padding-right: 0 !important;
  }
  body:has(.chat-dock),
  body:has(.chat-dock-details[open]) { padding-bottom: 0 !important; }

  /* The report column is pinned to 900px on screen for line-length.
     On paper the sheet is the measure, and 900px (≈238mm) overflows
     A4 portrait's 186mm printable width — so let it fill whatever
     page it lands on instead of clipping. */
  .report-document { max-width: 100% !important; }

  /* Bootstrap `.row` uses negative side margins to create gutters,
     and `.col-*` uses positive padding to cancel them. On screen
     that's fine — the container's own padding accommodates. In print
     the body is pinned at the @page-inner margin, so .row's negative
     margin pushes children outside the printable area and they get
     clipped on the right. Strip both for print so widget rows sit
     exactly at the body edges. */
  .row { margin-left: 0 !important; margin-right: 0 !important; }
  .col, [class^="col-"], [class*=" col-"] {
    padding-left: 0 !important;
    padding-right: 0 !important;
  }
  /* Bootstrap's `.col-md-N` widths only apply at md (≥768px). Headless
     Chrome's print viewport sits at the page-area width (~1032px in
     landscape) but in some setups print-media evaluates layout BEFORE
     the print page dimensions are known. Force the widget-grid widths
     explicitly so a col-md-8 + col-md-4 row stays side-by-side instead
     of stacking vertically — the latter wastes a printed sheet per
     row. Same for the col-md-6 + col-md-6 pattern (two equal charts). */
  .col-md-8  { flex: 0 0 66.66666667% !important; max-width: 66.66666667% !important; }
  .col-md-6  { flex: 0 0 50%           !important; max-width: 50%           !important; }
  .col-md-4  { flex: 0 0 33.33333333% !important; max-width: 33.33333333% !important; }
  .col-md-12 { flex: 0 0 100%          !important; max-width: 100%          !important; }

  /* Force every tab pane visible regardless of .active / .show. */
  .tab-pane.dashboard-page {
    display: block !important;
    opacity: 1 !important;
    page-break-after: always;
    break-after: page;
    /* Fixed printable area per orientation. The inner
       .dashboard-page-content is what JS scales down to fit when the
       natural content height exceeds these bounds — see the
       beforeprint hook in print.html.erb. Width/height match the @page
       size minus the @page margins (1cm × 1.2cm = 297mm × 210mm −
       2cm − 2.4cm landscape; − 2.4cm − 2cm portrait). */
    width: calc(297mm - 2.4cm);
    height: calc(210mm - 2cm);
    overflow: hidden;
  }
  .tab-pane.dashboard-page.print-portrait {
    width: calc(210mm - 2.4cm);
    height: calc(297mm - 2cm);
  }
  /* When the page contains a `.widget-slot.widget-table`, the fit-JS
     skips fit-scaling so the table can flow over multiple sheets with
     <thead> repeating. The fixed height + overflow:hidden above would
     CLIP that flow on the very first sheet — which is the dashboard-
     view vs dashboard-print divergence Erik saw on Produkter (widget
     74): Aktiviteter rows missing from the PDF because they sat below
     the 190mm cutoff. Release both so the natural flow can do its
     work. */
  .tab-pane.dashboard-page:has(.widget-slot.widget-table) {
    height: auto;
    overflow: visible;
  }
  .tab-pane.dashboard-page:last-child {
    page-break-after: auto;
    break-after: auto;
  }

  /* Page heading: bigger + hairline rule below, more like a printed
     report's section header. Default Bootstrap h4 sits at ~17px regular
     weight — in print it reads as a small subheading instead of a
     section title. Bump to 16pt bold; bottom rule + padding live on
     the .dashboard-page-heading flex row (not the h4) so the rule
     spans the full content width including the logo's right edge.
     Selector is `.dashboard-page >`, NOT `.dashboard-page-content >` —
     the heading moved OUT of .dashboard-page-content (print.html.erb)
     so the auto-fit zoom on the content box never shrinks it. */
  .dashboard-page > .dashboard-page-heading {
    border-bottom: 1px solid #000 !important;
    padding-bottom: 4px !important;
    margin-bottom: 16px !important;
  }
  .dashboard-page-heading .dashboard-page-title {
    font-size: 16pt !important;
    font-weight: 600 !important;
    line-height: 1.1 !important;
    margin-bottom: 0 !important;
  }

  /* Inner content gets transform-origin so JS scaling pivots from the
     top-left (the natural reading anchor). Scale factor itself is set
     inline by the beforeprint hook. */
  .dashboard-page-content {
    transform-origin: top left;
    width: 100%;
  }

  /* `.table-responsive` adds its own horizontal scroll wrapper so a
     wide table on a narrow page scrolls inside the wrapper. That hides
     the overflow from the auto-fit measurement (the wrapper's own
     scrollWidth stays at its container width). For print we want the
     table's natural width to bubble UP so the fit can zoom the whole
     dashboard-page-content to fit, leaving no clipped scroll-only
     content. Disable the inner scrolling for print only — the parent
     dashboard-page still has overflow: hidden as a safety net. */
  .table-responsive {
    overflow-x: visible !important;
  }

  /* Compact table typography at print: 35-row Resultat fits one A4
     portrait sheet instead of overflowing to a continuation. Body
     cells go to 8pt + 1px 4px padding (the Qlik-PDF density the
     dashboard is benchmarked against). Header stays at 9pt bold so
     the column titles still read at a glance. Applies to every
     `.widget-card table` in print regardless of template — the
     Resultat / Rullande 12 widgets all use the same row count
     threshold for spillover.

     `width: 100%` is defensive, not the fix for the width bug it was
     first suspected in: a bare <table> has no CSS width, so
     table-layout:auto sizes it to its OWN content instead of
     stretching to fill its .widget-slot. The actual Resultat-page
     width bug (2026-08-06) turned out to be unrelated — an unrelated
     widget's unconstrained long-text column overflowing was
     corrupting Chromium's print-layout pass for OTHER, earlier pages
     in the same PDF; see widget_executor.rb#build_display_columns and
     declarative_renderer.rb#wrap_style. Keeping width:100% here
     anyway since a table that doesn't fill its container on its own
     is still a latent version of the same class of bug. */
  .widget-card table { font-size: 8pt; width: 100%; }
  .widget-card table thead th { font-size: 9pt; padding: 2px 4px !important; }
  .widget-card table tbody td,
  .widget-card table tfoot td { padding: 1px 4px !important; }

  /* Truncate long names in the first column so the table's natural
     width stays bounded — otherwise wide-table portrait pages (e.g.
     Kontrollerbara kostnader with 45-char Swedish account names)
     force the auto-fit to scale everything down too aggressively,
     shrinking the numeric columns we actually want to read. 50mm
     gives ~22 chars before ellipsis at the print font size; numbers
     in subsequent columns then render at their natural size. */
  .widget-card table td:first-child,
  .widget-card table th:first-child {
    max-width: 50mm;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Don't split a single widget across two pages — a half-chart on
     each side of a page break is unreadable. Bootstrap row needs to
     stay flexible (the cell still has to fit), so the avoid lives on
     the slot / card. Widget-rows also avoid break-inside when none
     of the row's widgets is a long-table opt-out: the row's natural
     height fits one sheet, and splitting (84 on sheet N, 85 on sheet
     N+1) wastes a sheet for the trailing widget. Rows that contain a
     `.widget-table` slot DO allow break-inside so the long table can
     flow naturally — the sibling chart still pages together with it. */
  /* widget-row deliberately NOT in this set — adding break-inside:
     avoid on the row pushed the entire row to the next page when
     it + the page heading didn't fit on one sheet, orphaning the
     heading. The slot/card protection is enough to keep a single
     widget intact; flex-wrap handles multi-row layouts naturally. */
  .widget-slot,
  .widget-card { break-inside: avoid; page-break-inside: avoid; }

  /* (Removed) earlier rule forced every widget in a row containing
     a `.widget-slot.widget-table` to 100% width to avoid the "ghost
     column" next to a tall table flowing across sheets. In practice
     tables are often short (Google reviews comments = 4 rows): the
     forced split wasted a sheet for the sibling widget. The natural
     Bootstrap flex layout fits both at their authored col-widths.
     If a future case has a genuinely tall table, address it by
     splitting onto its own dashboard page rather than re-introducing
     this catch-all rule. */

  /* Table widgets are the exception: long tables MUST split across
     pages, with <thead> repeating on each page (browser does that
     automatically for proper <thead>). The slot gets `.widget-table`
     from the dashboard view when the widget's presentation kind is
     "table" — opt back into normal splitting all the way down. */
  .widget-slot.widget-table,
  .widget-slot.widget-table .widget-card,
  .widget-slot.widget-table .table-responsive,
  .widget-slot.widget-table table {
    break-inside: auto;
    page-break-inside: auto;
  }
  /* Don't split an individual row across a page break — splits land
     between rows only. */
  .widget-slot.widget-table tr {
    break-inside: avoid;
    page-break-inside: avoid;
  }
  /* Belt-and-suspenders: ensure <thead> is recognised as a header
     group so the browser repeats it on every page the table spans. */
  .widget-slot.widget-table thead {
    display: table-header-group;
  }
  /* Don't orphan the widget title at the bottom of the previous
     page when the table that follows it splits. */
  .widget-slot.widget-table .card-header {
    break-after: avoid;
    page-break-after: avoid;
  }

  /* ── Compact print sizing ────────────────────────────────────────
     Trim widget chrome so the natural-layout height is closer to the
     printable area — the auto-fit zoom on .dashboard-page-content
     still kicks in for genuine overflow, but pages that fit at a
     more generous scale now print bigger instead of being squeezed
     to ~66%. */

  /* Charts default to 280px min-height in the renderer. Print sees a
     narrower printable area than the screen viewport, so the chart
     gets less vertical real estate anyway — pull the min down so the
     declarative height stops dominating page layout. Charts that
     declared a larger explicit height still get it from `style=`. */
  [data-vega-payload] {
    min-height: 180px !important;
  }

  /* Card chrome shrinks aggressively for print. Header / body / row
     margins all set for screen reading; for print, every mm of
     vertical space we save is one more widget that fits at natural
     scale instead of being shrunk by the zoom hook. */
  .widget-card .card-header { padding: 0.15rem 0.4rem !important; }
  .widget-card .card-body   { padding: 0.25rem 0.4rem !important; }
  .widget-row { margin-bottom: 0.25rem !important; }
  .dashboard-page h4        { font-size: 1rem !important; margin: 0.25rem 0 !important; }
  .dashboard-page .small    { font-size: 0.75rem; }

  /* Hide editing / interactive chrome. The dashboard filter form is
     hidden too; the printable summary above (.d-print-block on
     .dashboard-filter-summary) carries the applied filter values
     instead so the printed page is self-explanatory. */
  .nav-tabs,
  .dashboard-page-actions,
  .widget-row-new,
  .widget-resize-handle,
  .chat-card,
  .dashboard-filter-form,
  nav,
  .btn { display: none !important; }
}

/* ===========================================================================
   Auth / sign-in — clean boss/Looker surface. `.btn-provider` is the TEMPLATE
   for OAuth buttons (icon left, label, full width, uniform chrome); reuse it
   for every provider and, later, the source-picker rows.
   =========================================================================== */
.auth-wrap {
  min-height: 82vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem 1rem;
}
.auth-card {
  width: 100%;
  max-width: 400px;
  background: #fff;
  border: 1px solid #e8eaed;
  border-radius: 14px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .04), 0 10px 30px rgba(0, 0, 0, .06);
  padding: 2.5rem 2rem 2rem;
  text-align: center;
}
.auth-title { font-size: 1.5rem; font-weight: 600; margin: 0 0 .35rem; color: #202124; }
.auth-sub   { color: #5f6368; font-size: .95rem; margin: 0 0 1.5rem; }

/* Slogan swaps with the boss key. Boss (corporate) shown by default; the hack
   variant is revealed under .mode-hack. Both live in the DOM. */
.auth-slogan { font-size: 1.15rem; font-weight: 600; color: #202124; line-height: 1.35; margin: 0 0 1.5rem; }
.slogan-hack { display: none; }
.auth-legal  { color: #80868b; font-size: .78rem; margin: 1.25rem 0 0; }
.auth-legal a { color: #5f6368; text-decoration: underline; }

/* Connect BigQuery (S3) — wider card, why-list, tucked advanced. */
.auth-card-wide { max-width: 560px; }
.connect-why { list-style: none; padding: 0; margin: 0 0 1.5rem; text-align: left; font-size: .9rem; }
.connect-why li { padding: .15rem 0; }
.auth-advanced { margin-top: 1.5rem; text-align: left; }
.auth-advanced > summary { cursor: pointer; color: #5f6368; font-size: .85rem; margin-bottom: .5rem; }

/* Source-type picker (S2) — big tap targets, icon left + title/subtitle. */
.source-picker { display: flex; flex-direction: column; gap: .6rem; }
.source-tile {
  display: flex; align-items: center; gap: .9rem;
  padding: 1rem; border: 1px solid #dadce0; border-radius: 8px;
  text-decoration: none; color: #3c4043; text-align: left;
  transition: background .15s, box-shadow .15s, border-color .15s;
}
.source-tile:hover { background: #f8f9fa; border-color: #d2d5d9; box-shadow: 0 1px 3px rgba(0, 0, 0, .08); }
.source-tile-icon { flex: 0 0 auto; font-size: 1.25rem; display: inline-flex; }
.source-tile-body { display: flex; flex-direction: column; }
.source-tile-body strong { font-weight: 600; }
.source-tile-body small { color: #5f6368; }
.source-coming { color: #80868b; font-size: .8rem; margin-top: 1rem; }

/* Radio pick-list (S4/S5/S6) — a single-select list of projects/datasets/etc. */
.pick-list {
  display: flex; flex-direction: column;
  border: 1px solid #dadce0; border-radius: 8px; overflow: hidden;
  margin-bottom: 1.25rem; text-align: left;
}
.pick-row { display: flex; align-items: center; gap: .75rem; padding: .75rem 1rem; margin: 0; cursor: pointer; }
.pick-row + .pick-row { border-top: 1px solid #eef0f2; }
.pick-row:hover { background: #f8f9fa; }
.pick-body { flex: 1 1 auto; display: flex; flex-direction: column; }
.pick-body small { color: #5f6368; }
.pick-tag { font-size: .72rem; color: #5f6368; background: #f1f3f4; padding: .1rem .5rem; border-radius: 10px; }
.pick-actions { display: flex; gap: .5rem; justify-content: center; flex-wrap: wrap; }

/* Dataset accordion (S6 import tables) — expandable datasets, lazy tables. */
.ds-list { border: 1px solid #dadce0; border-radius: 8px; overflow: hidden; margin-bottom: 1.25rem; text-align: left; }
.ds-group + .ds-group { border-top: 1px solid #eef0f2; }
.ds-summary { cursor: pointer; padding: .6rem 1rem; font-weight: 600; font-family: ui-monospace, monospace; }
.ds-loading { padding: .5rem 1rem; color: #5f6368; font-size: .85rem; }

/* Analysis results (S8) — expandable per-table findings. */
.res-body { padding: .25rem 1rem 1rem; }
.res-meta { color: #80868b; font-size: .78rem; font-weight: 400; margin-left: .5rem; }
.res-note { color: #3c4043; font-size: .88rem; margin: 0 0 .75rem; padding-left: .5rem; border-left: 2px solid #dadce0; }
.res-col { display: flex; flex-direction: column; padding: .4rem 0; border-top: 1px solid #f1f3f4; }
.res-col-name { font-family: ui-monospace, monospace; font-size: .85rem; font-weight: 600; }
.res-col-type { color: #5f6368; font-size: .78rem; }
.res-col-note { color: #5f6368; font-size: .8rem; padding-left: .75rem; }
.cost-note { font-size: .82rem; color: #7a5b00; background: #fff8e1; border: 1px solid #ffe8a3; border-radius: 8px; padding: .5rem .75rem; margin: 0 0 1.25rem; text-align: left; }

/* Discovery progress rows (S7) — live-updated via Turbo Streams. */
.disc-list { border: 1px solid #dadce0; border-radius: 8px; overflow: hidden; margin-bottom: 1.25rem; text-align: left; }
.disc-row { display: flex; align-items: flex-start; gap: .75rem; padding: .5rem 1rem; }
.disc-row + .disc-row { border-top: 1px solid #eef0f2; }
.disc-icon { flex: 0 0 1.1rem; text-align: center; line-height: 1.4; }
.disc-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.disc-name { font-family: ui-monospace, monospace; font-size: .9rem; }
.disc-state { color: #5f6368; font-size: .8rem; }   /* wraps below the name — room for step / error text */
.disc-completed .disc-icon { color: #188038; }
.disc-failed .disc-icon, .disc-failed .disc-state { color: #d93025; }

/* Small spinner (in-progress rows / lazy loads). */
.spinner { display: inline-block; width: .85rem; height: .85rem; border: 2px solid #d2d5d9; border-top-color: #5f6368; border-radius: 50%; animation: spin .7s linear infinite; vertical-align: -1px; }
@keyframes spin { to { transform: rotate(360deg); } }

/* The reusable provider button. */
.provider-form { margin: 0; }
.provider-form + .provider-form { margin-top: .5rem; }
.btn-provider {
  display: flex;
  align-items: center;
  gap: .75rem;
  width: 100%;
  padding: .625rem 1rem;
  background: #fff;
  border: 1px solid #dadce0;
  border-radius: 8px;
  font-size: .95rem;
  font-weight: 500;
  color: #3c4043;
  cursor: pointer;
  transition: background .15s, box-shadow .15s, border-color .15s;
}
.btn-provider:hover {
  background: #f8f9fa;
  border-color: #d2d5d9;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .08);
}
.btn-provider-icon  { display: inline-flex; flex: 0 0 auto; }
.btn-provider-label { flex: 1 1 auto; text-align: center; }

/* Dev shortcut — de-emphasised. */
.auth-dev {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid #eef0f2;
  text-align: left;
}
.auth-dev-label {
  display: block;
  font-size: .7rem;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: #9aa0a6;
  margin-bottom: .5rem;
}

/* ===========================================================================
   Product logo — the "vibe insights" mark + wordmark (shared/_logo partial).
   Boss look here (base); the pixelated hacker variant lives in the .mode-hack
   block below. One SVG source: the boss key flips it round↔pixel.
   --vi-brand is the boss-mode brand green; the mark uses it via currentColor,
   the wordmark's "vibe" inherits the surface colour (light on the dark navbar,
   dark on the white auth card) while "insights" takes the brand green.
   =========================================================================== */
:root { --vi-brand: #1f9d47; }
.logo { display: inline-flex; align-items: center; gap: .5rem; line-height: 1; }
.logo-mark { width: 1.75rem; height: 1.75rem; flex: 0 0 auto; display: block; color: var(--vi-brand); }
/* Boss disc shown by default (so print/PDF is always the round badge); the
   framed hacker SVG is revealed only under .mode-hack, inside @media screen.
   The hacker mark is wider than tall (64×40) → size by height, width auto. */
.logo-mark-hack { display: none; height: 1.75rem; width: auto; flex: 0 0 auto; color: var(--hack-green, #33ff66); }
.logo-word { display: inline-flex; flex-direction: row; gap: .3em; font-weight: 700; letter-spacing: -.01em; font-size: 1.15rem; }
.logo-word-b { color: var(--vi-brand); }
/* Stacked lockup for the sign-in card: bigger mark, name on two lines. */
.logo--stacked { gap: .7rem; }
.logo--stacked .logo-mark { width: 3.25rem; height: 3.25rem; }
.logo--stacked .logo-mark-hack { height: 3.25rem; }
.logo--stacked .logo-word { flex-direction: column; gap: .02em; font-size: 1.65rem; letter-spacing: -.02em; }
.auth-card .logo { margin-bottom: 1.5rem; }

/* Everything below is wrapped in @media screen so the hack skin NEVER
   applies in print/PDF — the monthly-report export stays clean boss/Looker
   regardless of the persisted mode. The Vega controller keys off the
   `--hack-green` custom property (defined only inside this block), so charts
   go clean in print on the exact same gate as the tables. */
@media screen {

/* ===========================================================================
   HACK SKIN (boss-key: hacker mode)
   A pure-CSS re-skin layered over the stock Bootstrap table markup that
   DeclarativeRenderer emits (.card.widget-card > .table.table-sm.table-striped,
   section banners as .table-secondary). No markup changes required — add the
   `mode-hack` class to any container (e.g. <body> or a dashboard wrapper) and
   every table widget inside flips to green-on-black, square corners, monospace.
   Boss mode = simply omit the class (default Bootstrap/Looker look).

   All colours live in the custom properties below — tweak here to play.
   =========================================================================== */
.mode-hack {
  --hack-bg:        #05080a;   /* card / page base */
  --hack-surface:   #0a1410;   /* table body */
  --hack-stripe:    #0f2016;   /* every other row */
  --hack-line:      #1e6b3a;   /* hard grid borders */
  --hack-green:     #33ff66;   /* body text */
  --hack-green-dim: #1f9d47;   /* muted / labels */
  --hack-green-hot: #7dffb0;   /* headers / emphasis */
  --hack-amber:     #ffb000;   /* positive delta */
  --hack-accent:    #d63384;   /* secondary accent — Bootstrap's <code>
                                  pink, the de-facto metric color on the
                                  analyze page (Erik 2026-08-05) */
  --hack-red:       #ff5555;   /* negative delta */
  /* Text face — readable body/tables. */
  --hack-font: "IBM Plex Mono", ui-monospace, "SF Mono", "Cascadia Code",
               "JetBrains Mono", Menlo, Consolas, monospace;
  /* Display face — pixel bitmap for the logo + headers only (short strings). */
  --hack-display: "Silkscreen", var(--hack-font);
}

/* Page background — beat Bootstrap's `.bg-light` (!important) so the dark
   cards sit on black rather than a light page. */
body.mode-hack.bg-light { background-color: #000 !important; }

/* Card chrome — square corners, hard green border, faint phosphor glow. */
.mode-hack .widget-card {
  background: var(--hack-bg);
  border: 1px solid var(--hack-line);
  border-radius: 0;
  box-shadow: 0 0 0 1px #000, 0 0 24px rgba(51, 255, 102, .06);
  color: var(--hack-green);
  font-family: var(--hack-font);
}
.mode-hack .widget-card > .card-header {
  background: #071009;
  border-bottom: 1px solid var(--hack-line);
  border-radius: 0;
  color: var(--hack-green-hot);
  font-family: var(--hack-display);   /* Silkscreen — pixel header face */
  letter-spacing: .02em;
  text-transform: uppercase;
  font-size: .74rem;
}
.mode-hack .widget-card > .card-header .text-muted,
.mode-hack .widget-card > .card-header .btn { color: var(--hack-green-dim) !important; }

/* The table itself — drive Bootstrap's own table CSS vars so striping,
   borders and colours all follow without fighting specificity. */
.mode-hack .table {
  --bs-table-bg: var(--hack-surface);
  --bs-table-color: var(--hack-green);
  --bs-table-striped-bg: var(--hack-stripe);
  --bs-table-striped-color: var(--hack-green);
  --bs-table-border-color: var(--hack-line);
  margin: 0;
  border: 1px solid var(--hack-line);
  font-family: var(--hack-font);
  text-shadow: 0 0 2px rgba(51, 255, 102, .35);   /* phosphor bleed */
}
.mode-hack .table > :not(caption) > * > * {
  border-color: var(--hack-line);                 /* hard grid on every cell */
}
.mode-hack .table thead th {
  background: #08160d;
  color: var(--hack-green-hot);
  border-bottom: 1px solid var(--hack-line) !important;
  text-transform: uppercase;
  letter-spacing: .05em;
  font-weight: 600;
  font-size: .72rem;
}
/* PRIMARY actions carry the secondary accent — systematically, via the
   Bootstrap class itself, so every .btn-primary (Refresh, Send, Add
   filter, form saves…) inherits it with no per-button classes. Outline
   at rest, filled on hover: pink = "acts on data", green stays
   navigation/structure. */
.mode-hack .btn-primary {
  background: transparent; border: 1px solid var(--hack-accent);
  color: var(--hack-accent); border-radius: 0;
  font-family: var(--hack-font);
}
.mode-hack .btn-primary:hover, .mode-hack .btn-primary:active,
.mode-hack .btn-primary:focus-visible {
  background: var(--hack-accent); border-color: var(--hack-accent); color: var(--hack-bg);
}
.mode-hack .btn-primary:disabled {
  background: transparent; border-color: var(--hack-line); color: var(--hack-green-dim);
}

/* Palette cell fills (row_color_indexes / column_color_indexes) — the
   authored color is inlined for the light canvas, and the resolver hands
   over its dark-canvas equivalent as --cell-bg-dark (Runtime::Shade). A
   palette slot means "this much accent, away from the surface": on white
   that is darker, on this near-black table it is lighter, so keeping the
   literal hex made the WEAKEST slot the brightest thing on the page.
   !important because the light value is an inline style and would
   otherwise outrank any rule here. */
.mode-hack td.cell-shade { background-color: var(--cell-bg-dark) !important; }

/* Section-banner rows (.table-secondary) → inverted green bar. */
.mode-hack .table .table-secondary {
  --bs-table-bg: var(--hack-green-dim);
  --bs-table-color: #04120a;
  color: #04120a;
  text-shadow: none;
  font-weight: 700;
}

/* Navbar in BOSS mode — Bootstrap's .bg-dark is #212529, near-black, which
   read as heavier than everything under it (ux_mini_todo #21). A slate that
   still carries light text, plus a hairline so the header ENDS rather than
   bleeding into the page. Hack mode keeps its own black below. */
body:not(.mode-hack) .navbar {
  background-color: #37414f !important;
  border-bottom: 1px solid #4a5666;
}
/* The buttons rode on near-black: a hard white outline was the only way to
   see them. On the lighter header that outline shouts, so they become quiet
   translucent fills that lift on hover. */
body:not(.mode-hack) .navbar .btn-outline-light {
  border-color: transparent;
  background: rgba(255, 255, 255, .1);
  color: #f1f3f5;
}
body:not(.mode-hack) .navbar .btn-outline-light:hover,
body:not(.mode-hack) .navbar .btn-outline-light:focus {
  background: rgba(255, 255, 255, .22);
  border-color: rgba(255, 255, 255, .35);
  color: #fff;
}
body:not(.mode-hack) .navbar .btn-group .btn.active {
  background: rgba(255, 255, 255, .28);
  border-color: rgba(255, 255, 255, .4);
}

/* Navbar in hack mode — black, green accents, square buttons. */
.mode-hack .navbar {
  background: #000 !important;
  border-bottom: 1px solid var(--hack-line);
}
.mode-hack .navbar .btn-outline-light {
  color: var(--hack-green);
  border-color: var(--hack-line);
  border-radius: 0;
  font-family: var(--hack-font);
}
.mode-hack .navbar .btn-outline-light:hover,
.mode-hack .navbar .btn-group .btn.active {
  background: #0f2016;
  border-color: var(--hack-green-dim);
  color: var(--hack-green-hot);
}
.mode-hack .navbar .text-light { color: var(--hack-green-dim) !important; }

/* Auth / sign-in surface in hack mode — black card, green border + text,
   dark square provider buttons. Brand SVG logos keep their own colours. */
.mode-hack .auth-card {
  background: var(--hack-bg);
  border-color: var(--hack-line);
  border-radius: 0;
  box-shadow: 0 0 0 1px #000, 0 0 30px rgba(51, 255, 102, .06);
  color: var(--hack-green);
  font-family: var(--hack-font);
}
.mode-hack .auth-title {
  color: var(--hack-green-hot);
  font-family: var(--hack-display);   /* Silkscreen — page-title display face */
  font-size: 1.15rem;                 /* Silkscreen runs large; dial down vs the 1.5rem sans */
  letter-spacing: .02em;
  font-style: italic;                 /* synthetic slant — "speed run" motion */
}
.mode-hack .auth-sub   { color: var(--hack-green-dim); }
/* Slogan swap: hack shows the plain line, hides the corporate one. */
.mode-hack .slogan-boss { display: none; }
.mode-hack .slogan-hack { display: inline; }
.mode-hack .auth-slogan { color: var(--hack-green-hot); }
.mode-hack .auth-legal  { color: var(--hack-green-dim); }
.mode-hack .auth-legal a { color: var(--hack-green); }
/* Connect BigQuery (S3) — green why-list, advanced, and wizard steps. */
.mode-hack .connect-why { color: var(--hack-green); }
.mode-hack .auth-advanced > summary { color: var(--hack-green-dim); }
.mode-hack .auth-card .badge.bg-primary   { background: var(--hack-green) !important; color: #04120a !important; }
.mode-hack .auth-card .badge.bg-success   { background: var(--hack-green-dim) !important; color: #04120a !important; }
.mode-hack .auth-card .badge.bg-secondary { background: #14211a !important; color: var(--hack-green-dim) !important; }
.mode-hack .auth-card .text-primary { color: var(--hack-green-hot) !important; }
.mode-hack .auth-card .text-success { color: var(--hack-green-dim) !important; }
.mode-hack .auth-card .text-muted   { color: var(--hack-green-dim) !important; }
/* Source-type picker tiles in hack mode. */
.mode-hack .source-tile { background: #0a1410; border-color: var(--hack-line); border-radius: 0; color: var(--hack-green); font-family: var(--hack-font); }
.mode-hack .source-tile:hover { background: #0f2016; border-color: var(--hack-green-dim); box-shadow: 0 0 12px rgba(51, 255, 102, .12); }
.mode-hack .source-tile-body small { color: var(--hack-green-dim); }
.mode-hack .source-coming { color: var(--hack-green-dim); }
/* Pick-list + card buttons in hack mode. */
.mode-hack .pick-list { border-color: var(--hack-line); border-radius: 0; }
.mode-hack .pick-row + .pick-row { border-top-color: var(--hack-line); }
.mode-hack .pick-row:hover { background: #0f2016; }
.mode-hack .pick-body small { color: var(--hack-green-dim); }
.mode-hack .pick-tag { background: #14211a; color: var(--hack-green-dim); }
.mode-hack .ds-list { border-color: var(--hack-line); border-radius: 0; }
.mode-hack .ds-group + .ds-group { border-top-color: var(--hack-line); }
.mode-hack .ds-summary { color: var(--hack-green-hot); }
.mode-hack .ds-loading { color: var(--hack-green-dim); }
.mode-hack .res-meta { color: var(--hack-green-dim); }
.mode-hack .res-note { color: var(--hack-green); border-left-color: var(--hack-line); }
.mode-hack .res-col { border-top-color: var(--hack-line); }
.mode-hack .res-col-name { color: var(--hack-green-hot); }
.mode-hack .res-col-type,
.mode-hack .res-col-note { color: var(--hack-green-dim); }
.mode-hack .cost-note { color: var(--hack-amber); background: #1a1400; border-color: #3a2e00; border-radius: 0; }
.mode-hack .disc-list { border-color: var(--hack-line); border-radius: 0; }
.mode-hack .disc-row + .disc-row { border-top-color: var(--hack-line); }
.mode-hack .disc-name { color: var(--hack-green); }
.mode-hack .spinner { border-color: #1e3a2a; border-top-color: var(--hack-green); }
.mode-hack .disc-state { color: var(--hack-green-dim); }
.mode-hack .disc-completed .disc-icon { color: var(--hack-green-hot); }
.mode-hack .disc-failed .disc-icon,
.mode-hack .disc-failed .disc-state { color: var(--hack-red); }
.mode-hack .auth-card .btn-primary {
  background: var(--hack-green-dim); border-color: var(--hack-green-dim);
  color: #04120a; border-radius: 0; font-family: var(--hack-font);
}
.mode-hack .auth-card .btn-outline-secondary {
  color: var(--hack-green-dim); border-color: var(--hack-line);
  border-radius: 0; font-family: var(--hack-font);
}
.mode-hack .btn-provider {
  background: #0a1410;
  border-color: var(--hack-line);
  border-radius: 0;
  color: var(--hack-green);
  font-family: var(--hack-font);
}
.mode-hack .btn-provider:hover {
  background: #0f2016;
  border-color: var(--hack-green-dim);
  box-shadow: 0 0 12px rgba(51, 255, 102, .12);
}
.mode-hack .auth-dev { border-top-color: var(--hack-line); }
.mode-hack .auth-dev-label { color: var(--hack-green-dim); }

/* Logo in hacker mode — the SAME mark, pixelated via #hack-pixelate, plus a
   phosphor glow; wordmark switches to the DOS font and green. Because this is
   inside @media screen, print/export keeps the round boss mark. */
.mode-hack .logo-mark { display: none; }              /* hide the round boss disc */
.mode-hack .logo-mark-hack {
  display: inline-block;                              /* show the framed SVG block */
  filter: drop-shadow(0 0 2px rgba(51, 255, 102, .45));  /* phosphor halo */
}
.mode-hack .logo-word {
  font-family: var(--hack-display);   /* Silkscreen — pixel display face */
  font-style: italic;                 /* speed-run slant, matching the titles */
  text-shadow: 0 0 6px rgba(51, 255, 102, .55);
}
.mode-hack .logo-word-a { color: var(--hack-green); }
.mode-hack .logo-word-b { color: var(--hack-green-hot); }

} /* end @media screen — hack skin */


/* ── S11a spec-edit panel (prototype) ──────────────────────────────
   Docked right property pane for in-place table editing. Base look =
   boss/clean; the .mode-hack block below re-skins it from the same
   --hack-* tokens the tables use (single colour source). */
.spec-edit-pane {
  position: fixed; top: 0; right: 0; bottom: 0; width: 320px;
  overflow-y: auto; z-index: 1040;
  background: #fff; border-left: 1px solid #d5d9dd;
  box-shadow: -6px 0 18px rgba(0, 0, 0, .06);
  padding: 14px 16px; font-size: 13px;
}
body.se-open { padding-right: 320px; transition: padding-right .15s ease; }

/* The REPORT TEMPLATE pane. Same pane, different mount: the dashboard's is a
   fixed right drawer over a full-width canvas, this one lives inside the
   editor's own grid column (col-lg-4). Reusing .spec-edit-pane and overriding
   only the positioning is what makes every se-* rule below apply to it — it
   previously used a bare .se-pane class that NOTHING styled, which is why it
   rendered short and chrome-less while the dashboard's looked finished.
   Sticky + its own scroll so it runs the height of the viewport instead of
   ending wherever its content happens to stop. */
.spec-edit-pane.se-inline {
  position: sticky; top: 8px; right: auto; bottom: auto;
  width: auto; max-height: calc(100vh - 24px);
  z-index: auto; box-shadow: none;
  border: 1px solid #d5d9dd; border-radius: 4px;
}
.spec-edit-pane .se-crumb {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: 12px; color: #6c757d; margin-bottom: 12px; gap: 8px;
}
.spec-edit-pane .se-crumb a[data-se-root] {
  color: inherit; cursor: pointer; text-decoration: none;
  border-bottom: 1px dotted currentColor;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 55%;
}
.spec-edit-pane .se-crumb .se-node { color: #212529; font-weight: 600; }
.spec-edit-pane label.form-label {
  text-transform: uppercase; font-size: 10.5px; letter-spacing: .06em;
  color: #6c757d; margin-bottom: 2px;
}
.spec-edit-pane .se-sep { border-top: 1px solid #e9ecef; margin: 12px -16px; }
.spec-edit-pane .se-status { font-size: 12px; color: #6c757d; }
.spec-edit-pane .se-error { white-space: pre-wrap; font-size: 11.5px; margin: 8px 0 0; }
.se-selected { box-shadow: 0 0 0 2px #0d6efd; }

/* View mode: widgets are chat-context targets — hover hints the click,
   the ring marks the attached one (twin of the input-row chip). */
.widget-card.chat-focusable { cursor: pointer; }
.widget-card.chat-focusable:hover { box-shadow: 0 0 0 1px rgba(13, 110, 253, .4); }
.widget-card.chat-context-selected { box-shadow: 0 0 0 2px #0d6efd; }
.chat-dock-widget-chip { font-weight: 600; }
.chat-dock-chip-x {
  border: 0; background: none; padding: 0 .15rem; cursor: pointer;
  color: inherit; opacity: .6; font-size: inherit; line-height: 1;
}
.chat-dock-chip-x:hover { opacity: 1; }
.se-gear {
  position: absolute; top: 4px; right: 28px; z-index: 2;
  font-size: 0.85rem; line-height: 1; opacity: 0.5;
  background: none; border: 0; padding: 0; cursor: pointer;
}
.se-gear:hover { opacity: 1; }
@media print { .se-gear, .spec-edit-pane { display: none !important; } }

@media screen {
  .mode-hack .spec-edit-pane {
    background: var(--hack-bg); color: var(--hack-green);
    border-left: 1px solid var(--hack-line); border-radius: 0;
    box-shadow: -6px 0 24px rgba(0, 0, 0, .6);
    font-family: var(--hack-font);
  }
  .mode-hack .spec-edit-pane .se-crumb { color: var(--hack-green-dim); }
  .mode-hack .spec-edit-pane .se-crumb .se-node { color: var(--hack-green-hot); }
  .mode-hack .spec-edit-pane label.form-label,
  .mode-hack .spec-edit-pane .form-check-label,
  .mode-hack .spec-edit-pane .text-muted { color: var(--hack-green-dim) !important; }
  .mode-hack .spec-edit-pane .se-sep { border-color: var(--hack-line); }
  .mode-hack .spec-edit-pane .form-control,
  .mode-hack .spec-edit-pane .form-select {
    background: var(--hack-surface); color: var(--hack-green);
    border: 1px solid var(--hack-line); border-radius: 0;
    font-family: var(--hack-font);
  }
  .mode-hack .spec-edit-pane .form-control:focus,
  .mode-hack .spec-edit-pane .form-select:focus {
    border-color: var(--hack-green); box-shadow: none; outline: none;
  }
  .mode-hack .spec-edit-pane .btn-primary {
    background: transparent; color: var(--hack-green-hot);
    border: 1px solid var(--hack-green); border-radius: 0;
  }
  .mode-hack .spec-edit-pane .btn-primary:hover { background: var(--hack-stripe); }
  .mode-hack .spec-edit-pane .btn-outline-secondary {
    color: var(--hack-green-dim); border-color: var(--hack-line); border-radius: 0;
  }
  .mode-hack .spec-edit-pane .btn-outline-secondary:hover {
    background: var(--hack-stripe); color: var(--hack-green);
  }
  .mode-hack .spec-edit-pane .form-check-input {
    background-color: var(--hack-surface); border-color: var(--hack-line);
  }
  .mode-hack .spec-edit-pane .form-check-input:checked {
    background-color: var(--hack-green-dim); border-color: var(--hack-green);
  }
  .mode-hack .spec-edit-pane .btn-close {
    filter: invert(1) sepia(1) saturate(3) hue-rotate(80deg);
  }
  .mode-hack .spec-edit-pane .se-error { color: var(--hack-red) !important; }
  .mode-hack .se-selected { box-shadow: 0 0 0 2px var(--hack-green-hot); }
  .mode-hack .widget-card.chat-focusable:hover { box-shadow: 0 0 0 1px var(--hack-green-dim, var(--hack-green-hot)); }
  .mode-hack .widget-card.chat-context-selected { box-shadow: 0 0 0 2px var(--hack-green-hot); }
  .mode-hack .se-gear { color: var(--hack-green-dim); }
}

/* S11a pane — config-tree accordion */
.spec-edit-pane .se-tree { margin: 0 -16px; }
.spec-edit-pane .se-sec { border-top: 1px solid #e9ecef; }
.spec-edit-pane .se-sec:last-child { border-bottom: 1px solid #e9ecef; }
.spec-edit-pane .se-sec-h {
  display: flex; align-items: baseline; gap: 5px;
  padding: 4px 12px; cursor: pointer; user-select: none;
}
.spec-edit-pane .se-sec-h:hover { background: #f8f9fa; }
.spec-edit-pane .se-chev { font-size: 10px; color: #6c757d; width: 12px; flex: 0 0 auto; }
.spec-edit-pane .se-sec-t {
  font-size: 12.5px; font-weight: 600; color: #212529;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.spec-edit-pane .se-sec-badge { margin-left: auto; font-size: 10.5px; color: #6c757d; flex: 0 0 auto; }
.spec-edit-pane .se-sec-b { display: none; padding: 4px 12px 10px 20px; }
.spec-edit-pane .se-sec-b.open { display: block; }
.spec-edit-pane .se-open-sec { background: #fcfcfd; }

@media screen {
  .mode-hack .spec-edit-pane .se-sec,
  .mode-hack .spec-edit-pane .se-sec:last-child { border-color: var(--hack-line); }
  .mode-hack .spec-edit-pane .se-sec-h:hover { background: var(--hack-stripe); }
  .mode-hack .spec-edit-pane .se-chev,
  .mode-hack .spec-edit-pane .se-sec-badge { color: var(--hack-green-dim); }
  .mode-hack .spec-edit-pane .se-sec-t { color: var(--hack-green-hot); font-weight: 400; }
  .mode-hack .spec-edit-pane .se-open-sec { background: var(--hack-surface); }
}

/* S11a pane — hidden dims are plumbing: keep them in the tree, dimmed */
.spec-edit-pane .se-sec-muted .se-sec-h { opacity: .55; }

/* S11a pane — server-rendered (turbo frame) variant */
body:has(#spec_edit_pane .spec-edit-pane) { padding-right: 320px; transition: padding-right .15s ease; }
.spec-edit-pane a.se-sec-h { display: flex; align-items: baseline; gap: 6px; text-decoration: none; color: inherit; }
.spec-edit-pane .se-close { text-decoration: none; color: #6c757d; font-size: 14px; }
.spec-edit-pane .se-close:hover { color: #212529; }
@media screen {
  .mode-hack .spec-edit-pane .se-close { color: var(--hack-green-dim); }
  .mode-hack .spec-edit-pane .se-close:hover { color: var(--hack-green-hot); }
}

/* S11a dashboard pane — widget index links */
.spec-edit-pane .se-widget-link {
  display: block; padding: 2px 6px; font-size: 12.5px;
  color: #212529; text-decoration: none; border-radius: 3px;
}
.spec-edit-pane .se-widget-link:hover { background: #f0f2f4; }
.spec-edit-pane .se-widget-other { color: #6c757d; font-style: italic; }
@media screen {
  .mode-hack .spec-edit-pane .se-widget-link { color: var(--hack-green); border-radius: 0; }
  .mode-hack .spec-edit-pane .se-widget-link:hover { background: var(--hack-stripe); color: var(--hack-green-hot); }
  .mode-hack .spec-edit-pane .se-widget-other { color: var(--hack-green-dim); }
}

/* S11a pane — unified dashboard tree */
.spec-edit-pane .se-group {
  padding: 6px 12px 1px; font-size: 10px; text-transform: uppercase;
  letter-spacing: .07em; color: #6c757d;
}
.spec-edit-pane .se-group-sub { padding-left: 16px; }
.spec-edit-pane .se-widget-current .se-sec-t { font-weight: 700; }
.spec-edit-pane .se-sub .se-sec-h { padding-left: 22px; }
.spec-edit-pane .se-sub .se-sec-b { padding: 4px 12px 10px 26px; }
@media screen {
  .mode-hack .spec-edit-pane .se-group { color: var(--hack-green-dim); }
}

/* S11a tree — native <details> nodes */
.spec-edit-pane details.se-sec > summary { list-style: none; }
.spec-edit-pane details.se-sec > summary::-webkit-details-marker { display: none; }
.spec-edit-pane details.se-sec > summary.se-sec-h { display: flex; align-items: baseline; gap: 6px; }
.spec-edit-pane .se-chev { transition: transform .1s ease; display: inline-block; }
.spec-edit-pane details[open] > summary .se-chev { transform: rotate(90deg); }
.spec-edit-pane details.se-sec .se-sec-b { display: block; }

/* ── Hack-skin coverage: dashboard EDIT chrome (Erik 2026-07-27) ─────
   Filter form, page tabs and drag-drop zones were still boss/Bootstrap
   inside .mode-hack. Same tokens as everything else. */
@media screen {
  .mode-hack .dashboard-filter-form {
    background: var(--hack-bg); border: 1px solid var(--hack-line);
    border-radius: 0; color: var(--hack-green);
  }
  .mode-hack .dashboard-filter-form .form-control,
  .mode-hack .dashboard-filter-form .form-select,
  .mode-hack .dashboard-filter-form .input-group-text {
    background: var(--hack-surface); color: var(--hack-green);
    border: 1px solid var(--hack-line); border-radius: 0;
    font-family: var(--hack-font);
  }
  .mode-hack .dashboard-filter-form .form-label,
  .mode-hack .dashboard-filter-form label { color: var(--hack-green-dim); }

  .mode-hack .nav-tabs { border-bottom-color: var(--hack-line); }
  .mode-hack .nav-tabs .nav-link {
    background: transparent; color: var(--hack-green-dim);
    border: 1px solid transparent; border-radius: 0;
    font-family: var(--hack-font);
  }
  .mode-hack .nav-tabs .nav-link:hover { color: var(--hack-green); border-color: var(--hack-line); }
  .mode-hack .nav-tabs .nav-link.active {
    background: var(--hack-surface); color: var(--hack-green-hot);
    border-color: var(--hack-line) var(--hack-line) var(--hack-surface);
  }

  .mode-hack .widget-row-new .col {
    background: var(--hack-surface); color: var(--hack-green-dim) !important;
    border-color: var(--hack-line) !important; border-radius: 0;
  }
}

/* ── S12a viewer home v1: dashboard cards + (dormant) report feed ── */
.dash-card-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 14px;
}
.dash-card {
  display: block; padding: 16px; text-decoration: none;
  background: #fff; border: 1px solid #dde1e5; border-radius: 6px;
  color: #212529; transition: border-color .1s ease, box-shadow .1s ease;
}
.dash-card:hover { border-color: #8a939b; box-shadow: 0 2px 10px rgba(0,0,0,.06); color: #212529; }
.dash-card-logo { height: 22px; width: auto; margin-bottom: 10px; display: block; }
.dash-card-name { font-weight: 600; font-size: 15px; margin-bottom: 4px; }
.dash-card-meta { font-size: 12px; color: #6c757d; }
.dash-report-feed-title {
  font-size: 10.5px; text-transform: uppercase; letter-spacing: .07em;
  color: #6c757d; margin-bottom: 8px;
}
@media screen {
  .mode-hack .dash-card {
    background: var(--hack-bg); border: 1px solid var(--hack-line);
    border-radius: 0; color: var(--hack-green);
  }
  .mode-hack .dash-card:hover {
    border-color: var(--hack-green); background: var(--hack-surface);
    color: var(--hack-green-hot); box-shadow: none;
  }
  .mode-hack .dash-card-name { color: var(--hack-green-hot); font-family: var(--hack-font); }
  .mode-hack .dash-card-meta,
  .mode-hack .dash-report-feed-title { color: var(--hack-green-dim); }
}

/* ── Chat dock (bottom, full width — console_chat_dock.md slice 1) ── */
.chat-dock {
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 1035;
  width: auto; /* Bootstrap .row>* forces width:100% which beats the
                  left/right insets on fixed elements — Send vanished
                  under the pane (found live 2026-08-03) */
  background: #fff; border-top: 1px solid #d5d9dd;
  box-shadow: 0 -4px 14px rgba(0, 0, 0, .06);
}
/* Dock geometry: closed = strip + input row (~84px chrome); open adds
   a FIXED 40vh history. Everything that must clear the dock (page
   scroll, sidebar) derives from these two numbers. */
:root { --dock-chrome: 84px; --dock-history: 40vh; }
body:has(.chat-dock) { padding-bottom: calc(var(--dock-chrome) + 16px); }
body:has(.chat-dock-details[open]) { padding-bottom: calc(var(--dock-history) + var(--dock-chrome) + 16px); }
/* Sidebar ends where the dock begins — one clean band, no L-seam. */
body:has(.chat-dock) .l2-sidebar { bottom: var(--dock-chrome); }
body:has(.chat-dock-details[open]) .l2-sidebar { bottom: calc(var(--dock-history) + var(--dock-chrome)); }
/* The tree's explicit height (Chrome ::details-content workaround)
   must shed the same dock space the sidebar does. */
body:has(.chat-dock) .l2-tree { height: calc(100vh - 132px - var(--dock-chrome)); }
body:has(.chat-dock-details[open]) .l2-tree { height: calc(100vh - 132px - var(--dock-chrome) - var(--dock-history)); }
/* The spec-edit pane owns the right edge; the dock yields to it. */
body:has(#spec_edit_pane .spec-edit-pane) .chat-dock { right: 320px; }
.chat-dock-details > summary { list-style: none; }
.chat-dock-details > summary::-webkit-details-marker { display: none; }
.chat-dock-strip {
  display: flex; align-items: baseline; gap: 8px;
  padding: 5px 14px; cursor: pointer; user-select: none;
  font-size: 12.5px; color: #6c757d;
}
.chat-dock-strip:hover { background: #f8f9fa; }
.chat-dock-title { font-weight: 600; color: #212529; }
.chat-dock-count { margin-left: auto; font-size: 11.5px; }
.chat-dock-ground { font-size: 11.5px; color: #6c757d; }
@media screen { .mode-hack .chat-dock-ground { color: var(--hack-green-dim); } }
.chat-dock-details[open] > .chat-dock-strip { border-bottom: 1px solid #e9ecef; }
.chat-dock-details[open] > summary .se-chev { transform: rotate(90deg); }
.chat-dock-history { height: var(--dock-history); overflow-y: auto; }
.chat-dock-inputrow { padding: 10px 14px 12px; border-top: 1px solid #f1f3f5; }

@media screen {
  .mode-hack .chat-dock {
    background: var(--hack-bg); border-top: 1px solid var(--hack-line);
    box-shadow: 0 -6px 24px rgba(0, 0, 0, .6);
  }
  .mode-hack .chat-dock-strip { color: var(--hack-green-dim); font-family: var(--hack-font); }
  .mode-hack .chat-dock-strip:hover { background: var(--hack-stripe); }
  .mode-hack .chat-dock-title { color: var(--hack-green-hot); }
  .mode-hack .chat-dock-details[open] > .chat-dock-strip { border-bottom-color: var(--hack-line); }
  .mode-hack .chat-dock-inputrow { border-top-color: var(--hack-line); }
  .mode-hack .chat-dock .form-control {
    background: var(--hack-surface); color: var(--hack-green);
    border: 1px solid var(--hack-line); border-radius: 0;
    font-family: var(--hack-font);
  }
  .mode-hack .chat-dock .form-control:focus { border-color: var(--hack-green); box-shadow: none; }
  .mode-hack .chat-dock .btn-primary {
    background: transparent; color: var(--hack-green-hot);
    border: 1px solid var(--hack-green); border-radius: 0;
  }

  /* Page headings were Bootstrap default grey — unreadable on the
     hack background (Erik 2026-07-27, "Dashboards" heading). The
     skin owns headings now. */
  .mode-hack h1, .mode-hack h2, .mode-hack h3,
  .mode-hack h4, .mode-hack h5 {
    color: var(--hack-green-hot); font-family: var(--hack-font);
  }
}

/* ── L2 left sidebar (collapsible resource tree) ──
   BQ-console style: an arrow in the sidebar's top-right corner
   expands/collapses the whole panel; sections collapse individually;
   the tree scrolls. */
.l2-sidebar {
  position: fixed; top: 56px; left: 0; bottom: 0; z-index: 1030;
  width: 208px; background: #fff; border-right: 1px solid #dde1e5;
}
.l2-sidebar:not([open]) { width: 30px; background: #f4f5f7; }
.l2-sidebar > summary.l2-rail {
  list-style: none; cursor: pointer; display: flex;
  justify-content: flex-end; padding: 4px 8px; color: #6c757d;
  flex: 0 0 auto; user-select: none;
}
.l2-sidebar > summary.l2-rail::-webkit-details-marker { display: none; }
.l2-sidebar[open] .l2-arrow::after { content: "«"; }
.l2-sidebar:not([open]) .l2-arrow::after { content: "»"; }
.l2-tree {
  height: calc(100vh - 132px); overflow-y: auto;
  padding: 0 8px 20px; font-size: 12.5px;
}
body:has(.l2-sidebar[open]) .container-fluid { padding-left: 240px; }
body:has(.l2-sidebar:not([open])) .container-fluid { padding-left: 44px; }
.l2-sec { list-style: none; cursor: pointer; font-size: 10.5px; letter-spacing: .07em;
  color: #6c757d; padding: 6px 4px 2px; text-transform: uppercase; }
.l2-sec::-webkit-details-marker { display: none; }
.l2-item { display: block; padding: 2px 6px; color: #212529; text-decoration: none; border-radius: 3px; }
.l2-item:hover { background: #f0f2f4; }
.l2-sub { padding-left: 14px; }
.l2-muted { color: #6c757d; }
.l2-tail { border-top: 1px solid #e9ecef; margin-top: 10px; padding-top: 6px; }
.l2-node > summary { cursor: pointer; user-select: none; }
.l2-node > summary::-webkit-details-marker { display: none; }
.l2-node > summary { list-style: none; }
.l2-chev { display: inline-block; width: 12px; font-size: 10px; color: #6c757d; transition: transform .1s ease; }
.l2-node[open] > summary .l2-chev { transform: rotate(90deg); }
.l2-count { font-size: 10.5px; color: #6c757d; margin-left: 4px; }
/* thin, quiet scrollbar */
.l2-tree { scrollbar-width: thin; scrollbar-color: #c9cfd6 transparent; }
.l2-tree::-webkit-scrollbar { width: 6px; }
.l2-tree::-webkit-scrollbar-track { background: transparent; }
.l2-tree::-webkit-scrollbar-thumb { background: #c9cfd6; border-radius: 3px; }
@media screen {
  /* Bootstrap's .bg-light is a hardcoded near-white (!important) — in
     hack mode every such block (analyze view's filter/SQL/binds pres)
     drops to the dark surface with legible green text. */
  .mode-hack .bg-light { background: var(--hack-surface) !important; color: var(--hack-green); }
  .mode-hack .bg-light code { color: var(--hack-green); }
  .mode-hack .l2-sidebar { background: var(--hack-bg); border-color: var(--hack-line); }
  .mode-hack .l2-sidebar:not([open]) { background: var(--hack-surface); }
  .mode-hack .l2-sidebar > summary.l2-rail { color: var(--hack-green-dim); }
  .mode-hack .l2-sec { color: var(--hack-green-dim); }
  .mode-hack .l2-item { color: var(--hack-green); }
  .mode-hack .l2-item:hover { background: var(--hack-stripe); color: var(--hack-green-hot); }
  .mode-hack .l2-tail { border-color: var(--hack-line); }
  .mode-hack .l2-chev, .mode-hack .l2-count { color: var(--hack-green-dim); }
  .mode-hack .l2-tree { scrollbar-color: var(--hack-line) transparent; }
  .mode-hack .l2-tree::-webkit-scrollbar-thumb { background: var(--hack-line); }
}
@media print { .l2-sidebar { display: none !important; } }

/* ux_mini_todo #1 — filter labels/values were unreadable in hack mode
   (dark on dark): give ALL form labels/inputs a global hack default
   instead of relying on per-component scoping. */
@media screen {
  .mode-hack label, .mode-hack .form-label, .mode-hack .form-check-label { color: var(--hack-green-dim); }
  .mode-hack .form-control, .mode-hack .form-select {
    background: var(--hack-surface); color: var(--hack-green);
    border: 1px solid var(--hack-line); border-radius: 0;
  }
  .mode-hack .form-control:focus, .mode-hack .form-select:focus {
    border-color: var(--hack-green); box-shadow: none;
    background: var(--hack-surface); color: var(--hack-green);
  }
}

/* ux_mini_todo #2 — cards defaulted to Bootstrap white in hack mode
   (/teams et al). Sane global default: every .card inherits the hack
   surface unless a more specific rule (widget-card) overrides. */
@media screen {
  .mode-hack .card {
    background: var(--hack-bg); border: 1px solid var(--hack-line);
    border-radius: 0; color: var(--hack-green);
  }
  .mode-hack .card .card-header {
    background: var(--hack-surface); border-bottom: 1px solid var(--hack-line);
    color: var(--hack-green-hot);
  }
  .mode-hack .list-group-item {
    background: var(--hack-bg); border-color: var(--hack-line); color: var(--hack-green);
  }
}

/* ux_mini_todo (generic): empty states / muted text were Bootstrap
   dark-grey on the hack background — unreadable everywhere ("No
   sources yet…"). The skin owns muted text + body copy globally now. */
@media screen {
  .mode-hack { color: var(--hack-green); }
  .mode-hack .text-muted, .mode-hack .text-secondary { color: var(--hack-green-dim) !important; }
  .mode-hack a:not(.btn) { color: var(--hack-green-hot); }
}

/* Hack mode: assistant-markdown artifacts inside the chat dock
   (Redcarpet output — raw table/pre/code, no Bootstrap classes)
   rendered boss-white on the dark dock. Skin owns them now. */
@media screen {
  .mode-hack .chat-dock table,
  .mode-hack .chat-dock th,
  .mode-hack .chat-dock td {
    background: var(--hack-surface); color: var(--hack-green);
    border: 1px solid var(--hack-line);
  }
  .mode-hack .chat-dock th { color: var(--hack-green-hot); }
  .mode-hack .chat-dock pre,
  .mode-hack .chat-dock code {
    background: var(--hack-surface); color: var(--hack-green-hot);
    border-radius: 0;
  }
  .mode-hack .chat-dock blockquote {
    border-left: 3px solid var(--hack-line); color: var(--hack-green-dim);
  }
  /* belt & suspenders: nothing inside the dock keeps a white bg */
  .mode-hack .chat-dock .bg-white,
  .mode-hack .chat-dock .bg-light { background: var(--hack-surface) !important; }
}

/* Hack mode: Bootstrap alerts were never skinned — flash notices,
   empty states ("No widgets yet…") and error banners rendered as
   boss-white/grey boxes on the dark background. */
@media screen {
  .mode-hack .alert {
    background: var(--hack-surface); color: var(--hack-green);
    border: 1px solid var(--hack-line); border-radius: 0;
  }
  .mode-hack .alert-danger, .mode-hack .alert-warning { color: var(--hack-amber); }
  .mode-hack .alert-danger { border-color: var(--hack-red); color: var(--hack-red); }
  .mode-hack .chat-dock-history, .mode-hack .chat-message { background: transparent; }
}

/* ── chat message bubbles + widget-md tables ─────────────────────────
   Moved from an inline <style> block in the layout head — it bypassed
   the skin entirely (the last white chat bubbles in hack mode). Boss
   keeps the original look; hack owns its own. */
.chat-panel { height: calc(100vh - 80px); overflow-y: auto; }
.chat-message { padding: .5rem .75rem; border-radius: .5rem; margin-bottom: .5rem; }
.chat-message.user { background: #f1f3f5; }
.chat-message.assistant { background: #e7f5ff; }
.chat-message pre { white-space: pre-wrap; }
/* Progress trail (ux_mini_todo #28): ONE collapsed line, expandable, and
   visually unlike an answer — muted, monospace, its own gutter. The box
   stays out of the way until a line actually arrives. */
.chat-progress-box:not(:has(.chat-progress-line)) { display: none; }
.chat-progress-summary {
  list-style: none; cursor: pointer;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11.5px; color: #6c757d;
  padding: 2px 4px 2px 10px; border-left: 2px dotted #adb5bd;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.chat-progress-summary::-webkit-details-marker { display: none; }
.chat-progress-summary:hover { color: #212529; }
.chat-progress-tag { letter-spacing: .04em; text-transform: uppercase; font-size: 10px; margin-right: .4rem; }
.chat-progress-box[open] .chat-progress-summary { margin-bottom: .15rem; }
.chat-progress { border-left: 2px dotted #adb5bd; margin-left: 0; }
.chat-progress-line { opacity: .85; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; }
.chat-progress-line + .chat-progress-line { margin-top: .1rem; }
@media screen {
  .mode-hack .chat-progress-summary,
  .mode-hack .chat-progress { border-left-color: var(--hack-line); }
  .mode-hack .chat-progress-summary { color: var(--hack-green-dim); }
  .mode-hack .chat-progress-summary:hover { color: var(--hack-green); }
}
/* Chat bubbles are conversation, not documents — markdown headers
   render at label scale, body text stays chat-sized, spacing compact. */
.chat-message { font-size: 13px; }
.chat-message h1, .chat-message h2, .chat-message h3,
.chat-message h4, .chat-message h5, .chat-message h6 {
  font-size: 14px; font-weight: 700; margin: .6em 0 .25em;
}
.chat-message h1 { font-size: 16px; }
.chat-message h2 { font-size: 15px; }
.chat-message p, .chat-message ul, .chat-message ol { margin-bottom: .35rem; }
.chat-message ul, .chat-message ol { padding-left: 1.25rem; }
.chat-message table { font-size: 12px; }
.chat-message pre, .chat-message code { font-size: 12px; }
.widget-card { margin-bottom: 1rem; }
.widget-md table { width: 100%; border-collapse: collapse; }
.widget-md table th, .widget-md table td { border: 1px solid #dee2e6; padding: .25rem .5rem; }
.widget-md table th { background: #f8f9fa; }
@media screen {
  .mode-hack .chat-message { border-radius: 0; }
  .mode-hack .chat-message.user {
    background: var(--hack-stripe); color: var(--hack-green);
    border-left: 3px solid var(--hack-green-dim);
  }
  .mode-hack .chat-message.assistant {
    background: var(--hack-surface); color: var(--hack-green);
    border-left: 3px solid var(--hack-green);
  }
  .mode-hack .widget-md table th, .mode-hack .widget-md table td { border-color: var(--hack-line); }
  .mode-hack .widget-md table th { background: var(--hack-surface); color: var(--hack-green-hot); }
}

/* Chat input: auto-grow with content (1 row at rest, cap ~8 lines).
   field-sizing is pure CSS (Chrome 123+); older browsers just keep a
   fixed row — graceful. */
.chat-dock-inputrow textarea {
  field-sizing: content;
  min-height: calc(1.5em + 10px);
  max-height: 12em;
  resize: none;
}

/* Inside the dock the HISTORY is the scroller — neutralize the legacy
   .chat-panel viewport-height so content flows and history scrolls. */
.chat-dock .chat-panel { height: auto; overflow: visible; }

/* Breathing room (ux_mini_todo #27). The dock's chat was flush against the
   edges: the clear-history row sat at px-2, messages hugged the scrollbar,
   and the first message started right under the strip's border. One
   horizontal rhythm — 14px, the input row's — for every child, so the
   column reads as one panel instead of three stacked strips. */
.chat-dock .chat-panel { padding: 10px 14px 14px; }
.chat-dock .chat-dock-history > .d-flex { padding: 6px 14px 0 !important; }
.chat-dock .chat-message { padding: 2px 4px 2px 10px; }
.chat-dock .chat-message + .chat-message { margin-top: 1rem; }
