/* BonBonBot Console: THE SHELL, and the furniture no one screen owns.

   WHAT IS LEFT HERE after the console-refresh split: the box-sizing reset and
   the element defaults, the rule that stops the app scrolling sideways, the
   frame that lays `.cb-console` out as a rail column and a page column,
   `.cb-main` and the one measure every screen is poured into, the buttons,
   the settings sub-nav, the quiet destructive variant, the save sign, a link
   inside a sentence, and the visually-hidden helper.

   WHAT MOVED OUT, AND WHY. This was two thousand two hundred lines carrying
   four unrelated surfaces, so four people improving four different parts of
   the console were all editing one file. The rail is now console-rail.css,
   the page header console-header.css, the tooltip console-popup.css. Nothing
   changed except which file the rules are in and the comment at the top of
   each one; all three load immediately after this sheet, in that order, so
   the cascade is what it was.

   THE FRAME IS THE SEAM WORTH KNOWING. The two grid columns here are sized
   from `--cbc-rail`, and the block of colour behind the rail is painted by
   this file's `::before` rather than by the rail itself, because the rail is
   sticky and therefore only ever as tall as the window. So the rail's width
   and its ground are declared here and used there. Change them here, once.

   Companion sheets: console-theme.css (the palette and the motion),
   console-data.css (tables, cards, detail), console-forms.css (fields,
   modals), console-panels.css (settings rows), console-charts.css.

   Colours come from the --cbc-* semantic layer in console-theme.css, which
   maps onto --cb-* brand tokens once. Sizes come from --cb-* directly.
   The exceptions the token scale does not model: hairlines (1px), 100%/0,
   opacity and colour-mix percentages, and breakpoint literals in media
   queries, which CSS cannot read from var(). */

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* ---- THE APP NEVER SCROLLS SIDEWAYS ----

   Down the page, yes: that is what a page does. Across it, never, and this is
   the one place that can say so for all of it. A console this wide has dozens
   of things that can overhang by a few pixels on some window width nobody
   tried, and each one of them produces the same symptom: a horizontal scrollbar
   under the whole app, every fixed element measuring against a viewport that is
   now wider than the screen, and the rail sitting a few pixels off the edge.
   Chasing them one at a time is chasing a symptom.

   `clip` and NOT `hidden`, which is the whole reason this can go on the root at
   all. `overflow: hidden` turns an element into a scroll container, and a
   scroll container on the root is what breaks `position: sticky` inside it and
   what makes `scroll-margin` land in the wrong place. `clip` cuts the overflow
   and creates no scroll container, so everything sticky in this app goes on
   working exactly as it did.

   Anything that genuinely has to be wider than the window scrolls INSIDE its
   own box: that is what the `overflow-x: auto` wrappers on the wide tables and
   the code blocks are for, and they are unaffected by this. */
html {
  overflow-x: clip;
}

body {
  margin: 0;
  /* Said twice on purpose: a `clip` on the root is not inherited, and the two
     elements can be given different scroll behaviour by a stylesheet that
     arrives later. Both, or neither. */
  overflow-x: clip;
  font-family: var(--cb-font-family);
  font-size: var(--cb-type-sm-size);
  line-height: var(--cb-type-sm-line-height);
  color: var(--cbc-ink);
  background: var(--cbc-base);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ---- No scrollbar behind the first run ----

   THE CONSOLE IS BUILT WHILE THE QUESTIONS ARE UP, ON PURPOSE. main.ts does not
   wait for them: they are opaque and pixel-identical at frame zero precisely so
   that buildShell can run underneath, which is why the console is simply THERE
   the moment the last card is answered instead of being assembled in front of
   somebody who has just finished answering.

   What leaked out of that was a scrollbar. A dashboard is several thousand
   pixels tall (measured: 6,847 against a 900 window), so the document behind
   the room was a scroll container and the browser drew a bar down the side of a
   screen with nothing on it to scroll. It appeared at the QUESTIONS and never
   before them, which is the tell: the security check and the settling-in stops
   are awaited, and the questions are the one stop the shell is built under.

   `clip` RATHER THAN `hidden`, for the reason written at the top of this file.
   `hidden` makes the root a scroll container and a scroll container on the root
   breaks every `position: sticky` inside it. The rail is sticky, so locking
   with `hidden` would lay the console out wrong for the whole of the first run
   and then correct it the instant the room went, which is a jump on the one
   frame this entire surface exists to keep still. `clip` cuts the overflow,
   creates no scroll container, and leaves the shell being built underneath
   identical to the one that would have been built with no room over it at all.

   `:has()` RATHER THAN A FLAG ON THE ROOT, and that is the safer half of it. A
   room is put up, handed over, held, parked, dropped and swept from six places
   (onboarding/tenancy.ts); an attribute written in one of them and missed in
   another leaves a console nobody can scroll for the rest of the session, which
   is a worse fault than the bar this removes. This cannot get stuck: the lock
   is exactly as alive as the element, it stays on through the fade, which is
   right, and it goes when the node does.

   `dialog.cb-ask` rather than `dialog`, because the console's ordinary dialogs
   are boxes in the middle of a page still worth scrolling behind them. This is
   the full-bleed room, and there is nothing behind it. */
:root:has(dialog.cb-ask[open]),
:root:has(dialog.cb-ask[open]) body {
  /* Said twice for the reason the `overflow-x` above is: a clip on the root is
     not inherited, and both elements have to agree. */
  overflow-y: clip;
}

h1,
h2,
h3,
p,
figure {
  margin: 0;
}
button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}
/* Numbers on this screen are read in columns, so they line up in columns. */
table,
.cb-num,
.cb-stat-value,
.cb-metric-value {
  font-variant-numeric: tabular-nums;
}

/* ---- The frame ----
   A fixed rail and a scrolling page. The rail is the whole navigation,
   which is what let the settings tabs stop being a second row of pills
   underneath a first row of pills. */
/* THE CONSOLE'S GROUND, and the state before it is drawn wears it too.

   [data-returning] is set in the head of index.html from one stored flag, and
   it means this browser has been inside the console before (src/returning.ts).
   For that visitor the loading ground is the console's own two columns rather
   than the sign-in plane's wall, so the shell arriving fills a shape that is
   already the right shape. Without it a refresh painted the door plane first:
   the brand lockup at forty-eight pixels on a wall half the window wide, then
   thirty-two on a two-hundred-and-sixty pixel rail two frames later, which is
   the one visible jump left on a reload.

   Only while loading. The moment the session turns out to be gone, frame.ts
   sets the state to "signin" and this stops matching, which is what puts the
   door back without anything having to undo it. */
.cb-console[data-state="ready"],
:root[data-returning] .cb-console[data-state="loading"] {
  display: grid;
  grid-template-columns: var(--cbc-rail) minmax(0, 1fr);
  min-height: 100svh;
  position: relative;
  background: var(--cbc-base);
}
/* The plane it opens on instead. Hidden rather than removed from the markup,
   because the markup is what a first-time visitor and an ended session both
   still need, and it is one attribute away from being on screen. */
:root[data-returning] .cb-console[data-state="loading"] > .cb-signin {
  display: none;
}
/* The rail is sticky and therefore only as tall as the viewport, so the colour
   of its column is painted behind it. This used to be a two-stop gradient
   across the whole page, which is a gradient in name only and one more thing
   to reason about; a solid block in the same place says it plainly. */
.cb-console[data-state="ready"]::before,
:root[data-returning] .cb-console[data-state="loading"]::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: var(--cbc-rail);
  background: var(--cbc-sunken);
  border-inline-end: 1px solid var(--cbc-line);
  pointer-events: none;
}

/* ---- Page frame ---- */
.cb-main {
  min-width: 0;
  padding: var(--cb-space-6xl) var(--cb-space-6xl) var(--cb-space-10xl);
}

/* Every screen is a measured column, centred, and they are all the same
   measure. See --cbc-measure for why there is only one now. */
.cb-main > * {
  width: 100%;
  max-width: var(--cbc-measure);
  margin-inline: auto;
}

/* ---- Buttons. Pills, per the brand. ---- */
.cb-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--cb-space-md);
  /* An underline is what tells you a run of text inside a sentence is a link.
     A pill with a fill or a border has already said it is pressable, and the
     line under the words then reads as damage. Buttons that happen to be
     anchors ("See all", "Open the report") were getting the browser's default. */
  text-decoration: none;
  padding: var(--cb-space-md) var(--cb-space-3xl);
  border: 1px solid transparent;
  border-radius: var(--cb-radius-full);
  background: var(--cbc-accent);
  color: var(--cbc-on-accent);
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-semibold);
  white-space: nowrap;
  cursor: pointer;
  transition:
    background var(--cb-motion-fast) var(--cbc-spring),
    transform var(--cb-motion-fast) var(--cbc-spring),
    border-color var(--cb-motion-fast) var(--cbc-spring);
}
/* Every icon is a box of exactly the icon's size, so it centres against text
   without the wrapping span's line-height nudging it a pixel or two off.
   
   The box is the only thing that decides an icon's size, and it centres what is
   in it. Both halves of that matter. A second rule elsewhere used to size the
   glyph inside a button directly, at sixteen pixels, which on a small button
   put a 16px glyph in a 12px box: it overhung the box by four pixels and, with
   nothing centring it, all four hung off the bottom, so the mark sat two pixels
   low against its own label. That was every small button in the console.
   Sizing lives here; anything that wants a different size sets it on .cb-icon. */
.cb-icon {
  display: grid;
  place-items: center;
  flex: none;
  inline-size: var(--cb-space-2xl);
  block-size: var(--cb-space-2xl);
}
.cb-icon svg {
  display: block;
  inline-size: 100%;
  block-size: 100%;
}
/* On a button the glyph reads at the weight of the words beside it. */
.cb-btn .cb-icon {
  inline-size: var(--cb-space-xl);
  block-size: var(--cb-space-xl);
  margin-inline-start: calc(var(--cb-space-xs) * -1);
}
.cb-btn[data-size="sm"] .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
}
.cb-btn svg {
  stroke-width: 2;
}

.cb-btn:hover:not(:disabled) {
  background: var(--cbc-accent-hover);
}
.cb-btn:active:not(:disabled) {
  transform: translateY(1px);
}
/* A disabled primary goes neutral rather than translucent. Fading a scarlet
   fill to half opacity leaves a muddy pink block that still draws the eye
   while doing nothing. */
/* Off, but still able to say why. See enable() in dom.ts: the disabled
   attribute would stop the pointer events its tooltip needs. */
.cb-btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  pointer-events: auto;
}
.cb-btn[aria-disabled="true"]:hover,
.cb-btn[aria-disabled="true"]:active {
  background: inherit;
  transform: none;
}
.cb-btn:disabled {
  background: var(--cbc-sunken);
  color: var(--cbc-ink-faint);
  border-color: var(--cbc-line);
  cursor: not-allowed;
}
.cb-btn[data-variant="ghost"] {
  background: transparent;
  color: var(--cbc-ink-muted);
  border-color: var(--cbc-line-strong);
}
.cb-btn[data-variant="ghost"]:hover:not(:disabled) {
  background: var(--cbc-surface);
  color: var(--cbc-ink);
}
/* Quieter than ghost: words and a glyph, no outline at all.
   Ghost is an outlined pill, which is right for an action somebody came to the
   page to take. It is too much for chrome that only exists because of a state
   you are already in, and giving "Clear filters" the same ring as "Export CSV"
   said the two were equals. This is the tier below: muted ink, nothing drawn
   around it, and it picks up the standard hover wash on approach so it is
   plainly still a button. */
.cb-btn[data-variant="quiet"] {
  background: none;
  border-color: transparent;
  /* Muted, not faint. Faint is for a value nobody is looking for; this is a
     control, and at 48% ink beside a row of fields it read as a caption that
     had drifted in. */
  color: var(--cbc-ink-muted);
  /* A field's own padding and type size, so it stands the same height as the
     inputs and menus it sits beside. It was six pixels shorter and two sizes
     smaller than everything in its row, which is what made it float rather
     than belong: a row of 46px controls with one 34px label in it does not
     read as a row. */
  padding: var(--cb-space-lg) var(--cb-space-xl);
  font-size: var(--cb-type-sm-size);
}
.cb-btn[data-variant="quiet"]:hover:not(:disabled) {
  background: var(--cbc-hover);
  color: var(--cbc-ink);
}

/* Destructive, but quiet until you reach for it.
   The keys screen had five filled scarlet Replace buttons and five filled red
   Remove buttons on one page. When everything shouts, the shouting stops
   meaning anything, and the eye cannot find the one action that matters. A
   destructive action reads as an outline in red ink and only fills in on
   hover, which is the moment it is about to happen. */
.cb-btn[data-variant="danger"] {
  background: transparent;
  color: var(--cbc-poor-text);
  border-color: color-mix(in srgb, var(--cbc-poor) 45%, transparent);
}
.cb-btn[data-variant="danger"]:hover:not(:disabled) {
  background: var(--cbc-poor);
  color: var(--cbc-on-accent);
  border-color: var(--cbc-poor);
}
.cb-btn[data-size="sm"] {
  padding: var(--cb-space-sm) var(--cb-space-xl);
  font-size: var(--cb-type-xs-size);
}

/* ---- Sub navigation (settings) ---- */
.cb-subnav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--cb-space-xs);
  margin-bottom: var(--cb-space-4xl);
  padding: var(--cb-space-xs);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-surface);
  border: 1px solid var(--cbc-line);
  width: fit-content;
  max-width: 100%;
  overflow-x: auto;
}
.cb-subnav a {
  padding: var(--cb-space-md) var(--cb-space-2xl);
  border-radius: var(--cb-radius-full);
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-medium);
  color: var(--cbc-ink-muted);
  text-decoration: none;
  white-space: nowrap;
  transition:
    background var(--cb-motion-fast) var(--cbc-spring),
    color var(--cb-motion-fast) var(--cbc-spring);
}
.cb-subnav a:hover {
  color: var(--cbc-ink);
}
.cb-subnav a[aria-current="page"] {
  background: var(--cbc-accent);
  color: var(--cbc-on-accent);
}

/* Sign in lives in console-signin.css. What was here was the card version: one
   centred box with one paragraph in it that carried the welcome, every refusal
   and the local backend being down, all in the same slot in the same grey. The
   screen is two planes now and the state is in the heading. Deleted rather
   than left to fight the sheet that replaced it, and its
   `.cb-signin-google svg { width }` went with it: the box sizes the mark, and
   a rule reaching past it to the glyph is what put every small button's icon
   two pixels low. */

/* Toasts live in console-toast.css. What was here was the ten-line version:
   one region, one box, five seconds, no way to pause it and no way to answer
   it. Deleted rather than left to fight the sheet that replaced it. */

/* 1024px mirrors tokens.breakpoint.lg */
@media (max-width: 1024px) {
  .cb-console[data-state="ready"] {
    grid-template-columns: var(--cbc-rail-tight) minmax(0, 1fr);
  }
  /* The paint behind the rail has to narrow with the rail. It did not, so
     below this width a hundred and sixty pixels of the rail's colour lay over
     the left of the content column: invisible wherever a card happened to be
     positioned and painting straight over anything that was not. */
  .cb-console[data-state="ready"]::before {
    inline-size: var(--cbc-rail-tight);
  }
  .cb-main {
    padding-inline: var(--cb-space-4xl);
  }
}
/* 768px mirrors tokens.breakpoint.md */
@media (max-width: 768px) {
  .cb-main {
    padding: var(--cb-space-4xl) var(--cb-layout-container-padding-mobile) var(--cb-space-8xl);
  }
}

/* ---- Destructive, but not shouting ----
   Removing something is the action you almost never want, and an outlined red
   button repeated down a list was the loudest thing on the screen: it drew the
   eye ahead of the two actions you actually came for. Quiet until the pointer
   is on it, then unmistakable.

   It carries the same vertical rhythm as a pill so it sits on the line with
   them rather than floating between them looking unfinished, which is exactly
   what it did on the submission header. */
.cb-quiet-danger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--cb-space-md);
  padding: var(--cb-space-md) var(--cb-space-xl);
  border: 1px solid transparent;
  border-radius: var(--cb-radius-full);
  background: none;
  color: var(--cbc-ink-faint);
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-medium);
  white-space: nowrap;
  cursor: pointer;
  transition:
    color var(--cb-motion-fast) var(--cbc-spring),
    background var(--cb-motion-fast) var(--cbc-spring),
    border-color var(--cb-motion-fast) var(--cbc-spring);
}
.cb-quiet-danger .cb-icon {
  inline-size: var(--cb-space-xl);
  block-size: var(--cb-space-xl);
  margin-inline-start: calc(var(--cb-space-xs) * -1);
}
.cb-quiet-danger:hover,
.cb-quiet-danger:focus-visible {
  color: var(--cbc-poor-text);
  background: var(--cbc-accent-wash);
  border-color: color-mix(in srgb, var(--cbc-poor) 35%, transparent);
}
.cb-quiet-danger:disabled,
.cb-quiet-danger[aria-disabled="true"] {
  opacity: 0.4;
  cursor: not-allowed;
}
.cb-quiet-danger[aria-disabled="true"]:hover {
  color: var(--cbc-ink-faint);
  background: none;
}
/* Inside a row it is one small action among several, not a page action. */
.cb-conn-actions .cb-quiet-danger,
.cb-rule-actions .cb-quiet-danger,
.cb-expand-actions .cb-quiet-danger {
  padding: var(--cb-space-sm) var(--cb-space-lg);
  font-size: var(--cb-type-xs-size);
}

/* The panel body of a settings card. No padding of its own: the rows inside it
   carry theirs, and a second set here inset every one of them. */
.cb-panel-body {
  display: grid;
}

/* ---- The save sign ----
   Bottom right, only while something is happening. It fades in as the save
   starts, holds "Saved" long enough to be read, and leaves. A permanent badge
   reading "Saved" is a badge nobody sees after the first day.

   Where a game puts its autosave, for the same reason: it is confirmation of
   something you already did, not a thing to attend to, so it belongs at the
   edge of vision rather than in the middle of the work. */
.cb-saver {
  position: fixed;
  z-index: var(--cbc-z-saver);
  /* Above the launcher rather than on it. The corner belongs to the one thing
     that is always there; see --cbc-dock-lift in console-theme.css. */
  inset-block-end: var(--cbc-dock-lift);
  inset-inline-end: var(--cbc-dock-inset);
  display: flex;
  align-items: center;
  gap: var(--cb-space-md);
  padding: var(--cb-space-md) var(--cb-space-xl);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-raised);
  box-shadow: var(--cbc-shadow);
  color: var(--cbc-ink-muted);
  font-size: var(--cb-type-xs-size);
  white-space: nowrap;
  pointer-events: none;
  animation: cbc-saver-in var(--cb-motion-base) var(--cbc-spring) both;
}
.cb-saver[data-leaving] {
  animation: cbc-saver-out var(--cb-motion-base) var(--cb-motion-ease) both;
}
.cb-saver .cb-icon {
  inline-size: var(--cb-space-xl);
  block-size: var(--cb-space-xl);
}
.cb-saver[data-state="saved"] {
  color: var(--cbc-ink);
  border-color: color-mix(in srgb, var(--cbc-good) 45%, transparent);
}
.cb-saver[data-state="saved"] .cb-icon {
  color: var(--cbc-good-text);
}
/* Saved locally but not to the account. Not an error, and not a success
   either: what is true is that this arrangement is on this machine only. */
.cb-saver[data-state="failed"] {
  color: var(--cbc-ink);
  border-color: color-mix(in srgb, var(--cbc-warn) 50%, transparent);
}
.cb-saver[data-state="failed"] .cb-icon {
  color: var(--cbc-warn-text);
}
/* Lucide's loader, turning. It used to be a ring built out of a border with
   one side tinted, which is a drawn mark by another name (docs/10). */
.cb-saver-spin {
  color: var(--cbc-accent);
  animation: cbc-spin var(--cbc-loop) linear infinite;
}

/* ---- A link inside a sentence ----
   There was no rule for one, so any anchor in prose came out in the browser's
   default blue with its own underline: the one thing on these screens that had
   never been designed. Buttons and navigation set their own colour and drop the
   underline, so they are untouched by this.

   The underline stays, offset off the baseline. Inside a run of text it is what
   says the words are a link; a pill or a rail has already said that by shape. */
a {
  color: var(--cbc-accent);
  text-decoration-color: color-mix(in srgb, var(--cbc-accent) 45%, transparent);
  text-underline-offset: var(--cb-space-xxs);
  transition: text-decoration-color var(--cb-motion-fast) var(--cbc-spring);
}
a:hover {
  text-decoration-color: var(--cbc-accent);
}

/* ---- Said, not shown ----
   For a cell whose meaning is carried by a shape. The segments that stand for
   a model's thinking levels are aria-hidden because three coloured pills are
   nothing to read out; this is the sentence they stand for, present in the
   accessibility tree and absent from the page. */
.cb-sr {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

