/* ~~ codemap ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 * @title       Console Styles
 * @category    ui
 * @plane       control
 * @description The console's entire presentation layer: the Material 3
 *              token set every screen reads, the two self-hosted faces, the
 *              icon rules, and one block per component. Nothing in `app/`
 *              writes a colour or a size in JS -- the one styling rule,
 *              stated in full below.
 * @links       app/ui/static/dashboard.html
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

/* ============================================================================
   THE SYSTEM, IN ONE PLACE

   Read this before adding a component. Everything below picks from these
   ladders; a new thing that needs a value not on one of them is either
   mis-measured or an argument for adding a rung, and adding a rung is a
   decision made here rather than a literal written at the call site.

   Each ladder is documented where it is declared, in `:root`. This is the
   index.

     TYPE      --text-1 .. --text-13        thirteen steps, 10px to 24px
                                            no `font-size` in this file is
                                            anything else
     INK       --on --on-variant --outline  one ramp, brightest first, and
               --dim --faint --fainter      every glyph is on it
     SURFACE   --surface --surface-low       what a thing sits on, plus the
               --surface-cont --surface-high three shell columns
               --surface-est                 (--surface-rail/-nav/-pane)
     LINES     --hairline --edge --rule      shell columns / round a group /
                                            between rows in a group
     SPACE     --space-1 .. --space-6        gaps between things: 4 8 12 16
                                            24 32
     PANE      --pane-x --pane-y             a shell column's own inset,
               --pane-x-side                 which is not a gap
     ROWS      --row-y --row-x               one inset for every row of every
               --row-y-block                 bordered group
     CONTROLS  --control-h-sm/-h/-h-lg       26 / 32 / 38, with the matching
               --control-x-sm/-x/-x-lg       10 / 14 / 18
     FIELDS    --field --field-border        a control you type into: it has
               --field-border-hover          an inside, so it is brighter
               --field-y --field-x           than any line
     CHIPS     --chip-y/-x                   a word with a box round it,
               --chip-y-sm/-x-sm             ordinary and tiny
     RADIUS    --radius-sm/--radius          8 / 12 / 16 / pill
               --radius-lg/--radius-pill
     MOTION    --motion-short --motion-long  120ms for a thumb, 260ms for
               --easing                      something entering the page

   TWO VOCABULARIES, AND WHICH ONE TO REACH FOR

   The console's layout rule is that **the main column is where a thing is
   changed and the right pane is where it is read**. That is not a
   convention about columns; it is what the two component families mean:

     `.field-row` + `.field-rows`   a settings row: a label, one line on
     `C.sectionHead` `C.fieldRows`  why, and the control that changes it,
                                    inside a bordered group under a head.

     `.side-label` `.side-stat`     the information vocabulary: a small-caps
     `.side-quote`                  heading, a key and its value, and a
     `C.sideLabel` `C.sideStat`     quotation of prose this console did not
     `C.sideQuote`                  write.

   They look like duplicates of each other and are not. `.field-row` holds a
   control; `.side-stat` holds a fact. The names say "side" for historical
   reasons -- the information vocabulary grew in the right pane -- but it is
   the *job* that picks one, not the column: a client's Identity section is
   `sideStat` rows inside `.field-row`s in the main column, because it is
   information, and that is correct.

   The one thing neither family does is put a control in the right pane. A
   switch there is a second copy of one that is already on the row the pane
   is about, and the pane is the half of the screen a reader is not expected
   to be careful in.
   ============================================================================ */

/* Self-hosted, latin subset, one variable file per family. Same origin
   because the alternative degrades badly: a console that reaches
   fonts.googleapis.com renders in a fallback face on a locked-down network,
   and the icon font it used to pull alongside these degraded to the literal
   word `handyman` in place of every glyph. The icons are inline SVG now
   (console-icons.js) and the text is these two files.

   Flat in static/ rather than in a fonts/ subdirectory: router.py serves one
   path segment out of an explicit allowlist, and a subdirectory needs
   {filename:path}, which re-opens the traversal class that allowlist closed.
   Same reasoning the console-*.js files are flat for. */
@font-face {
  font-family: "DM Sans";
  src: url("/dashboard/static/dm-sans.woff2") format("woff2");
  font-weight: 100 1000;   /* one variable file covers the range */
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("/dashboard/static/jetbrains-mono.woff2") format("woff2");
  font-weight: 100 800;
  font-style: normal;
  font-display: swap;
}

:root {
  /* The one line that makes every *native* control agree with the palette: a
     select's dropdown, a date picker, a number spinner, the text caret. It is
     not a token -- it is a declaration about the document -- but it belongs
     with them, because without it the browser draws those parts in its light
     default and the only dark-looking bits of a select are the ones this
     stylesheet reaches. */
  color-scheme: dark;

  /* ------------------------------------------------------------ surfaces --
     Material 3 dark, brown. Five steps rather than the three the old theme
     had, because the redesign stacks a rail on a page on a card on a row and
     three greys cannot keep those apart.

     The ramp is built around one brand colour rather than tinted towards it:
     `--surface-cont` **is** the dark brown, and the four steps either side of
     it are the same hue lifted and dropped. A card is therefore the brand
     colour and everything else is its own shade, which is what stops a warm
     theme reading as a grey theme somebody has held a filter over.

     `--surface-est` is the exception and the only cool value in the ramp: the
     brand's dark teal, used at the top of the stack for hovers, the scrollbar
     thumb and the one-off bordered box. It is a step up in lightness like the
     others, so the ordering still reads correctly, and being the one place the
     ramp turns cool is what makes a hover legible without a second border. */
  --surface: #171614;
  --surface-low: #1d1b19;
  --surface-cont: #242322;
  --surface-high: #2e2c29;
  --surface-est: #2f3f31;

  /* ------------------------------------------------------ shell surfaces --
     The four columns of every screen, darkest on the outside. The rail is
     nearly black so the page beside it reads as lit; the sub-nav sits just
     under the content it filters; the right pane sits just over it, because
     it is the thing being looked at.

     The content pane is one surface at every depth. It carried a lighter
     `--surface-deep` once you had drilled in, on the argument that a settings
     level should say it replaced the list -- but the level's own head, the
     breadcrumb and the sub-nav all say that already, and the tint only made
     two pages of the same screen look like two different screens. */
  --surface-rail: #0e0d0c;
  --surface-nav: #131211;
  --surface-pane: #1a1917;

  /* ---------------------------------------------------------------- lines --
     Three, and nothing else draws a line. All much darker than a border
     usually is: at this brightness a divider separates two surfaces without
     drawing a box round either, which is what lets four columns sit side by
     side without the screen reading as a grid of cards.

     Pick by *what the line separates*, never by how dark it should look:

       `--hairline`  between the columns of the shell -- and nothing else.
                     Six uses, all of them the rail, the sub-nav, the right
                     pane or the header. A component reaching for this is
                     claiming to be part of the shell.
       `--edge`      round a group. A card, a list, a bordered box, a
                     banner, a tile -- anything with an inside.
       `--rule`      between two rows inside one group, and under a group's
                     own head. It never closes; it only divides.

     There were four. `--hairline-soft` sat half a step between `--edge` and
     `--rule` and was used for both jobs, which meant the answer to "which
     line do I draw" depended on which file you had read last. Its uses went
     to whichever of the two they were already doing. */
  --hairline: #201e1b;
  --edge: #322e29;
  --rule: #26231f;

  /* --------------------------------------------------------------- fields --
     A control you can type into has to look like it has an inside, and that
     means a border brighter than any of the three lines above. Its own
     tokens rather than reusing `--edge`: a divider separates two surfaces,
     and a field is a hole in one. `--edge` got darker when the layout became
     four columns, and every text field that had taken it disappeared into
     the pane behind it. */
  --field: #141312;
  --field-border: #4a4339;
  --field-border-hover: #665d4f;
  --field-y: 8px;
  --field-x: 10px;

  /* ----------------------------------------------------------------- ink --
     One ramp, six steps, brightest first. Every colour a glyph is ever drawn
     in is on it, and a component picks a step by how loudly the thing should
     speak rather than by naming a new colour:

       `--on`          the thing you came to read
       `--on-variant`  a label beside it
       `--outline`     quoted prose, a monospaced value, a switch's track
       `--dim`         a hint, a count, an aside -- still meant to be read
       `--faint`       a slug under a name, a unit after a figure
       `--fainter`     the quietest thing on the screen that is still text

     Below `--fainter` is decoration, and decoration takes a line or a
     surface colour, not an ink. */
  --on: #f7f2ea;
  --on-variant: #ece4d8;
  --outline: #cfc4b3;
  --dim: #ad9f8c;
  --faint: #9c8e7b;
  --fainter: #7d7163;

  /* ------------------------------------------------------------- primary --
     `--primary` is the switch, the active rail item, the focus ring. Change
     this one line and every screen follows, which is the one styling rule
     stated as a test.

     Orange, two tones above the brand's own, and that gap is deliberate rather
     than a drift. `--primary` is ink as often as it is a fill -- a live pill,
     a quoted label, the focus ring -- and the brand orange comes to 3.4:1 on
     `--surface-cont`, which is enough for a switch track and not enough for a
     word.

     The surface it has to clear is `--surface-cont` and not `--surface`, and
     that is the whole reason this value is where it is. A card is the busiest
     surface in the console and the lightest of the three a row ever sits on,
     so it is the one that decides: #d0603a passes on the page (4.66:1) and
     fails on a card (4.05:1), which is the kind of near-miss that gets shipped
     because the first place you look at it is fine. #d86a44 clears 4.5:1 on
     all three.

     The brand value itself is not lost: it is `--primary-cont`, the filled
     plate behind the brand mark and the selection highlight, where light text
     sits on top of it -- and `--on-primary-cont` is nearly white rather than a
     tinted cream for the same reason, since #ffdccc on that orange was 3.98:1.

     `--secondary-cont` is the brand's dark green. It is the quieter of the two
     containers -- an on chip, a badge -- and being a different hue rather than
     a darker orange is what keeps "this is on" from reading as "this is the
     primary action, but dimmer". */
  --primary: #d86a44;
  --on-primary: #2a0e04;
  --primary-cont: #b74d2e;
  --on-primary-cont: #fff2ec;
  --secondary-cont: #45451e;
  --on-secondary-cont: #e4e4b6;

  /* ------------------------------------------------------------- accent-2
     A second hue, because one is not a palette. The brand's blue against the
     orange is the far side of the wheel, which is as different as two colours
     get and reads as a different *kind* of thing rather than a cooler orange.

     It carries one meaning, consistently: **this is the thing you are
     looking at or editing.** Orange stays "this is on, this is the primary
     action". Keeping the two jobs apart is the whole reason for adding a
     colour -- a second accent used decoratively would just be more noise.

     Its container is a dark blue and not one of the brand's other values,
     which is the one place a brand colour was passed over on purpose: the
     container is the *thumb* of a switch sitting on an `--accent-2` track, so
     it has to be the same hue several steps down or the control turns to mud.
     The brand's green and teal both have homes above. */
  --accent-2: #7bb1f6;
  --accent-2-cont: #24384f;
  --on-accent-2-cont: #d3e3fb;
  /* --------------------------------------------------------------- state --
     Three, not four: there is no "success" here. A thing that worked says so
     by looking ordinary. */
  --info: #8fb6d8;
  --error: #f0a8a0;
  --warn: #e6b36a;

  /* ------------------------------------------------------------- metrics */
  --radius: 12px;
  --radius-sm: 8px;
  --radius-lg: 16px;
  --radius-pill: 999px;

  /* ------------------------------------------------------------ type scale --
     Thirteen steps, and nothing off them. Every `font-size` in this file is
     one of these, so a new panel picks a step rather than inventing 13.5px
     because it looked right that afternoon -- which is how the console came
     to have seventeen sizes, four of them used once each.

     What lands where, so a new thing can be placed without measuring:

       1  10px    small-caps group labels, the kind mark on a row
       2  10.5px  the quietest real text: section counts, a sub-slug
       3  11px    hints, slugs, chips, counted asides
       4  11.5px  secondary body -- a stat, a quoted paragraph, a row's blurb
       5  12px    controls: buttons, nav items, banners, a tool's name
       6  12.5px  a row's own label, and every form control
       7  13px    names you scan a list by, and text inputs
       8  14px    the document default, and a pane's title
       9  15px    a crumb, a card's heading
      10  16px    the head of a pane or a section of a level
      11  17px    the title of a drilled-in level
      12  19px    the screen's name in the header
      13  24px    a figure on the Dashboard, which is read across the room

     Steps 1-4 are half-point apart on purpose: they are the four weights of
     "quieter than the body text", and the console leans on all four. */
  --text-1: 10px;
  --text-2: 10.5px;
  --text-3: 11px;
  --text-4: 11.5px;
  --text-5: 12px;
  --text-6: 12.5px;
  --text-7: 13px;
  --text-8: 14px;
  --text-9: 15px;
  --text-10: 16px;
  --text-11: 17px;
  --text-12: 19px;
  --text-13: 24px;

  /* --------------------------------------------------------- row metrics --
     One inset for every row of every bordered group, and one for the taller
     rows that hold a block rather than a line. These exist because "a row in
     a list" is the single most repeated thing in this console -- a tool, a
     module, a client, a seat, a setting, a memory, an account -- and they had
     drifted to 9px/10px/11px/12px/16px of vertical inset with four different
     horizontal ones, which is exactly the difference a reader notices
     between two screens without being able to name it. */
  --row-y: 10px;
  --row-x: 14px;
  --row-y-block: 14px;

  /* ------------------------------------------------------------ controls --
     Three sizes of clickable thing, each a height and a horizontal inset,
     and every button in the console is one of them. Heights rather than
     vertical padding, because the defect these fix is two buttons of
     different classes sitting on one bar at different heights -- which is a
     thing you see immediately and cannot fix by choosing a padding, since
     the type inside them differs too.

       sm  26px   an escape hatch beside a control: `Inherit`, `Open these`
       md  32px   the ordinary button, the sub-nav item, the quiet icon
       lg  38px   a page's own action: Save, the right pane's way out

     Six pixels apart, so two rungs are never mistaken for one rung and a
     rendering difference. The type step goes with the size -- --text-3 on
     sm, --text-5 on md, --text-6 on lg -- and is written on each rule
     rather than tokenised, because a button's font is one declaration and a
     token for it would only be read once. */
  --control-h-sm: 26px;
  --control-h: 32px;
  --control-h-lg: 38px;
  --control-x-sm: 10px;
  --control-x: 14px;
  --control-x-lg: 18px;

  /* --------------------------------------------------------------- chips --
     A chip is a word with a box round it and it is never clicked to mean
     something -- a tag, a state, a scope, a kind. Two densities, because the
     console genuinely has two: an ordinary chip beside a name, and the tiny
     uppercase mark that labels a row without competing with it.

     There were seven insets across nine rules that all drew the same
     object. If a tenth is needed it is one of these two. */
  --chip-y: 3px;
  --chip-x: 10px;
  --chip-y-sm: 2px;
  --chip-x-sm: 8px;

  /* ---------------------------------------------------------- pane inset --
     What a column of the shell holds its contents in from its own edges.
     Not a `--space-*` step: those are gaps *between* things and this is the
     margin of the page itself, which is wider than any gap on it and would
     drag the gap scale up with it if they shared a number.

     The right pane is tighter than the content pane because it is narrower
     -- the same inset on a 300px column reads as a wide empty border. */
  --pane-x: 20px;
  --pane-y: 16px;
  --pane-x-side: 16px;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;

  /* M3 motion: the short one is for state a finger is holding (a switch
     thumb), the long one for something entering or leaving the page. */
  --motion-short: 120ms;
  --motion-long: 260ms;
  --easing: cubic-bezier(.2, 0, 0, 1);

  --icon-size: 20px;
  --icon-size-sm: 16px;
  --icon-size-lg: 24px;

  --font: "DM Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", monospace;

  /* ---------------------------------------------------------- categories --
     codeweb.js reads these off the document so the graph and the legend can
     never drift apart. The mockup names seven; the other eight are real
     categories in this repo's codemap that it never drew. They are derived
     in the same hue family rather than left at the old blue-green, because
     half a graph in each palette is worse than either palette.

     Which family that is, is the brand's four: orange for the code that runs,
     blue for what it exposes, green and teal for what it stores, and a warm
     grey for the rest. Fifteen values off four hues rather than fifteen hues,
     so the graph reads as one drawing and a node's colour narrows what it is
     without anybody memorising a legend. */
  --cat-entrypoint: #f7f2ea;
  --cat-core: #d0603a;
  --cat-module: #e08a5e;
  --cat-tool: #7bb1f6;
  --cat-support: #e6b36a;
  --cat-ui: #dd93a4;
  --cat-table: #8fa98f;
  --cat-tooling: #9fc6a8;
  --cat-config: #a89a86;
  --cat-docs: #8c8172;
  --cat-legacy: #6f6558;
  --cat-unmapped: #4a443c;
  --cat-schema: #7fa48a;
  --cat-database: #6fa9a0;
  --cat-service: #f0a8a0;

  /* Plane colours. Deliberately unlike the categories above: a plane is a
     region of the web, not a kind of node, and the two groupings have to stay
     tellable apart on one canvas. Kept cooler and flatter for that reason. */
  --plane-control: #8fb6d8;
  --plane-data: #7fa48a;
  --plane-shared: #ab9d8c;

  /* Direction colours for the satellites in services mode: who calls whom. */
  --dir-inbound: #8fb6d8;
  --dir-outbound: #d0603a;
  --dir-bidirectional: #e6b36a;

  /* ---------------------------------------------------------------- tags --
     A tag's colour is its meaning, so it is named for the meaning. `money`
     and `destructive` are the two worth stopping on. */
  --tag-money: #e6b36a;
  --tag-destructive: #f0a8a0;
  --tag-pii: #7bb1f6;
  --tag-beta: #dd93a4;
  --tag-bulk: #ab9d8c;

  /* Roles, in the ordering the old stylesheet already encoded. */
  --role-admin: var(--warn);
  --role-operator: var(--primary);
  --role-reader: var(--outline);
}

/* --------------------------------------------------------------- icons --
   The complete set the library needs; everything else is per-component.
   `console-icons.js` returns an <svg> with no size, no colour and no style
   attribute, so size comes from here and colour from `currentColor` -- an
   icon follows the text beside it through every hover and state class
   without one extra rule. */
.icon {
  width: var(--icon-size);
  height: var(--icon-size);
  flex: 0 0 auto;          /* never squashed by a flex row */
  display: block;          /* no inline baseline gap */
}

/* The three sizes the console actually uses. A fourth is a smell -- check
   whether the component beside it should be resizing instead. */
.icon-sm { --icon-size: var(--icon-size-sm); }
.icon-lg { --icon-size: var(--icon-size-lg); }

/* Icons carrying a state colour rather than inheriting one. */
.icon-danger { color: var(--error); }
.icon-warn   { color: var(--warn); }
.icon-dim    { color: var(--dim); }

/* Motion is a preference, not a decoration. Honoured globally so no
   component has to remember it. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
}

* { box-sizing: border-box; }

/* `hidden` is semantic, and every `display` in this file outranks it -- which
   is not a nuisance, it is a security bug waiting to happen. The console
   hides what a principal may not use rather than showing a control that
   always 403s, and it does that by setting this attribute; a component rule
   with `display: flex` silently un-hides it.
   It cost an empty snackbar sitting on every screen, and then a reader seeing
   four rail items that would refuse them. `!important` is right here for the
   reason it is usually wrong: nothing should ever out-rank this. */
[hidden] { display: none !important; }

::selection { background: var(--primary-cont); color: var(--on); }

body {
  margin: 0;
  background: var(--surface);
  color: var(--on);
  font-family: var(--font);
  font-size: var(--text-8);
  line-height: 1.55;
  height: 100vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Four columns means up to four scroll containers side by side, and a
   browser's default bar is a bright band down each of the seams. Thin and in
   the palette instead -- still visible, still draggable, no longer the
   loudest vertical line on the screen. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--surface-est) transparent;
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--surface-est);
  border: 3px solid transparent;
  background-clip: content-box;
  border-radius: var(--radius-pill);
}
::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--surface-est) 60%, var(--dim));
  background-clip: content-box;
}
::-webkit-scrollbar-corner { background: transparent; }

h1, h2, h3 { margin: 0; font-weight: 600; letter-spacing: -0.01em; }
h1 { font-size: var(--text-9); }
h2 { font-size: var(--text-11); }
h3 { font-size: var(--text-10); }
p { margin: 0 0 10px; }
code { font-family: var(--mono); font-size: 0.92em; color: var(--outline); }
.muted { color: var(--dim); }
.small { font-size: var(--text-5); }

/* ---------------------------------------------------------------- shell -- */

.shell {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  /* The rail is a fixed column because its contents are fixed: eight screens
     and the two controls under them. Everything that grows lives in the page
     beside it. Narrow, because it holds a glyph and one word -- the column
     that holds this screen's own filters is the next one along, and it
     belongs to the screen rather than to the shell. */
  grid-template-columns: 72px minmax(0, 1fr);
}

.shell-page {
  min-width: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* ----------------------------------------------------------------- rail -- */

.rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: var(--space-3) 0 var(--space-3);
  background: var(--surface-rail);
  border-right: 1px solid var(--hairline);
  overflow-y: auto;
  overflow-x: hidden;
}

/* The mark alone. The deployment's own name used to sit under it, cut to two
   lines; at 72px there is no width for a sentence, and the name is on the
   sign-in page, in the tab title and in the Dashboard's right pane -- three
   places that can hold it whole. The `title` carries it here. */
.rail-brand {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  flex: 0 0 auto;
  margin-bottom: 14px;
  border-radius: var(--radius);
  background: var(--primary-cont);
  color: var(--on-primary-cont);
}
.rail-brand-mark { display: grid; place-items: center; }

/* A deployment's own mark, when it set one. The plate underneath keeps the
   brand orange rather than going transparent: the rail is the darkest column
   on the screen and a logo floating on it reads as a stray image, where the
   same logo on the orange square reads as the thing this server is.

   `object-fit: contain` and not a fixed height, because a logo's aspect ratio
   is the one thing about it this stylesheet cannot know -- a wordmark and a
   monogram both have to sit inside the same square without either being
   cropped or stretched. The inset is what keeps it off the corners. */
.brand-logo {
  display: block;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}
.rail-brand .brand-logo { width: 26px; height: 26px; }

/* Two marks are declared and one is shown. The glyph stays in the markup so
   that a deployment with no logo -- and this repository's own default is
   none -- still has one, and `:has()` is what picks between them without the
   server having to render two different pages. */
.rail-brand:has(.brand-logo) .rail-brand-mark { display: none; }

.rail-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  width: 60px;
  flex: 0 0 auto;
  padding: 4px 0 2px;
  background: transparent;
  border: 0;
  color: var(--dim);
  font: inherit;
  font-size: var(--text-2);
  line-height: 1.2;
  text-align: center;
  cursor: pointer;
  transition: color var(--motion-short) var(--easing);
}
.rail-item:hover { color: var(--on); }
.rail-item:hover .rail-pill { background: var(--surface-cont); color: var(--on-variant); }
.rail-item:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: -2px;
  border-radius: var(--radius);
}

/* The active screen is a filled pill behind the glyph, the way M3 marks a
   navigation destination -- not a border, which would shift the label. */
.rail-pill {
  display: grid;
  place-items: center;
  width: 52px;
  height: 30px;
  border-radius: var(--radius-pill);
  color: var(--dim);
  transition: background var(--motion-short) var(--easing),
              color var(--motion-short) var(--easing);
}
.rail-glyph { display: grid; place-items: center; }
.rail-item.is-active { color: var(--on); font-weight: 500; }
.rail-item.is-active .rail-pill {
  background: var(--primary-cont);
  color: var(--on-primary-cont);
}

.rail-label { display: block; white-space: nowrap; }

.rail-foot {
  margin-top: auto;
  padding-top: var(--space-4);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
}
.rail-action {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--surface-cont);
  color: var(--on-variant);
  cursor: pointer;
  transition: background var(--motion-short) var(--easing);
}
.rail-action:hover { background: var(--surface-high); }
.rail-action:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Who is signed in, as one letter. The whole string -- principal and role --
   is the `title`, and the Dashboard's right pane says it in full. */
.rail-avatar {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--surface-est);
  color: var(--on);
  font: inherit;
  font-size: var(--text-5);
  font-weight: 500;
  cursor: pointer;
  text-transform: uppercase;
}
.rail-avatar.is-static { cursor: default; }
.rail-avatar:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* --------------------------------------------------------------- topbar -- */

/* One row, fixed height, on every screen. The left half says where you are
   and how you got there; the right half is the save cluster, which holds its
   slot whether or not there is anything to save -- so the place a change is
   committed never moves from screen to screen. */
.topbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 0 0 60px;
  padding: 0 var(--pane-x) 0 24px;
  border-bottom: 1px solid var(--hairline);
}

.topbar h1 {
  margin: 0;
  flex: 0 0 auto;
  font-size: var(--text-12);
  font-weight: 500;
  letter-spacing: -.015em;
}

/* The trail into a screen. The screen's own name stays in the <h1> on the
   left and the crumbs append to it, so the way back out is always the
   leftmost thing rather than something that moved. */
.crumbs {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.crumbs:empty { display: none; }
.crumb-sep { display: grid; place-items: center; color: var(--fainter); }
.crumb {
  background: transparent;
  border: 0;
  padding: 0;
  font: inherit;
  font-size: var(--text-9);
  color: var(--dim);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.crumb { transition: color var(--motion-short) var(--easing); }
.crumb:hover { color: var(--on); }
.crumb:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
/* The last crumb is where you are, so it carries the colour that means
   "this is the thing you are looking at" and is not a button. */
.crumb.is-current { color: var(--accent-2); cursor: default; }

.screen-meta {
  flex: 0 1 auto;
  font-family: var(--mono);
  font-size: var(--text-3);
  color: var(--faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.topbar-right {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-left: auto;
  flex: 0 0 auto;
}

/* Quiet when nothing is pending, which is most of the time -- switches apply
   on click with an undo snackbar, and only the forms that stage edits ever
   light this up. */
.save-state {
  font-size: var(--text-4);
  color: var(--faint);
  white-space: nowrap;
}
.save-state.is-dirty {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--warn);
}
.save-state.is-dirty::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--warn);
}

.save-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: var(--control-h-lg);
  padding: 0 var(--control-x-lg);
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--primary);
  color: var(--on-primary);
  font: inherit;
  font-size: var(--text-6);
  font-weight: 500;
  cursor: pointer;
}
.save-btn:disabled {
  background: var(--surface-cont);
  color: var(--fainter);
  cursor: default;
}
.save-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.save-btn [data-icon] { display: grid; place-items: center; }

.pill {
  font-family: var(--mono);
  font-size: var(--text-5);
  color: var(--dim);
  background: var(--surface-cont);
  border: 1px solid var(--edge);
  border-radius: var(--radius-pill);
  padding: var(--chip-y) var(--chip-x);
}

/* Widened from `button.ghost` when the Context screen needed one of these to
   be an anchor: its export endpoint sets its own filename, and an ordinary
   navigation carries the session cookie, so a link is the honest element for
   it. A second near-identical block for `a.ghost` is exactly the duplication
   the one styling rule exists to prevent -- the two would drift, and the one
   that drifted would be the one nobody was looking at. */
.ghost {
  background: transparent;
  /* An anchor is inline and would sit on the text baseline beside the
     buttons; the buttons are already inline-flex boxes. */
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  border: 1px solid var(--edge);
  color: var(--dim);
  border-radius: var(--radius-pill);
  height: var(--control-h);
  padding: 0 var(--control-x);
  font: inherit;
  font-size: var(--text-5);
  cursor: pointer;
  transition: color var(--motion-short) var(--easing),
              border-color var(--motion-short) var(--easing);
}
.ghost:hover { color: var(--on); border-color: var(--surface-est); }
.ghost:disabled { opacity: .5; cursor: default; }
.ghost:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

/* --------------------------------------------------------------- panels -- */

main { flex: 1 1 auto; min-height: 0; display: flex; }

/* Every screen is the same three columns: what to show, the thing itself,
   and the one row you picked. A screen differs from its neighbour in what it
   puts in those columns and never in whether it has them -- which is what
   lets a reader who has learned one screen navigate all eight. */
.panel {
  display: none;
  flex: 1 1 auto;
  min-width: 0;
}
.panel.is-active { display: flex; }

/* ------------------------------------------------------------- sub-nav -- */

/* The permanent second column. On a list screen it holds that list's filters
   and sorting; drilled into a row it holds that row's setting sections with
   the way back at its top. It is never a bar above the list, because a bar
   takes width from the thing it filters and moves every time the filters
   change shape. */
.subnav {
  flex: 0 0 228px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: var(--surface-nav);
  border-right: 1px solid var(--hairline);
}
.subnav:empty { display: none; }

.subnav-head {
  flex: 0 0 auto;
  padding: var(--space-3);
  /* One control's worth, whether or not it holds one -- see `subnav` in
     console-core.js for why an empty head is the right answer on the one
     screen that has nothing to put in it. The `1px` is the border below:
     `box-sizing` is border-box everywhere, so a min-height that left it out
     would come up exactly one pixel short of a head that has a control in
     it, and the column under it would sit a pixel high on that one screen. */
  min-height: calc(var(--control-h) + var(--space-3) * 2 + 1px);
  border-bottom: 1px solid var(--hairline);
}
.subnav-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  padding: var(--space-2) 10px var(--space-4);
}
.subnav-foot { padding: 12px 11px 0; }

.subnav-search {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  height: var(--control-h);
  padding: 0 var(--control-x);
  background: var(--surface-cont);
  border-radius: var(--radius-pill);
  color: var(--faint);
}
.subnav-search [data-icon] { display: grid; place-items: center; flex: 0 0 auto; }
.subnav-search [data-icon] svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }
.subnav-search input {
  flex: 1 1 auto;
  min-width: 0;
  background: transparent;
  border: 0;
  padding: 0;
  font-family: var(--mono);
  font-size: var(--text-3);
  color: var(--on);
}
.subnav-search input::placeholder { color: var(--fainter); }
.subnav-search input:focus { outline: none; }
.subnav-search:focus-within { outline: 2px solid var(--primary); outline-offset: 1px; }

.subnav-title {
  padding: 9px var(--control-x) 5px;
  font-size: var(--text-1);
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fainter);
}

.subnav-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  height: var(--control-h);
  padding: 0 var(--control-x);
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--dim);
  font: inherit;
  font-size: var(--text-5);
  text-align: left;
  cursor: pointer;
  transition: background var(--motion-short) var(--easing),
              color var(--motion-short) var(--easing);
}
.subnav-item > .subnav-label {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.subnav-item:hover { color: var(--on); background: var(--surface-cont); }
/* A row that leaves this screen rather than narrowing it. The chevron is the
   only thing that says so, so it is what moves on hover. */
.subnav-item.is-go > [data-icon] {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  margin-left: 2px;
  margin-right: -2px;
  color: var(--fainter);
  transition: transform var(--motion-short) var(--easing),
              color var(--motion-short) var(--easing);
}
.subnav-item.is-go > [data-icon] svg { width: 14px; height: 14px; }
.subnav-item.is-go:hover > [data-icon] { color: var(--dim); transform: translateX(2px); }
.subnav-item.is-on {
  background: var(--secondary-cont);
  color: var(--on-secondary-cont);
  font-weight: 500;
}
.subnav-item:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.subnav-n {
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: var(--text-1);
  color: var(--fainter);
}
.subnav-item.is-on .subnav-n { color: var(--on-secondary-cont); }

/* A filter row that carries its own control. The row is a <label> rather than
   a button, so the box and its words are one target and the input underneath
   is the thing that is actually focused, checked and announced -- no class
   standing in for state a screen reader could have read directly.

   Checkbox or radio is the caller's choice and it means something: a checkbox
   marks a filter you can turn off and be left with nothing selected, a radio
   marks a group where one is always chosen and picking another replaces it.
   Same rows, same column -- the shape is what says whether "none" is
   reachable, before the label is read.

   The controls are the console's own drawn checkbox and radio, only smaller:
   18px is sized for a form field and would crowd a 31px row of 12px text.
   Re-stated through the same `:not(.switch input)` the base rule uses, which
   is not superstition -- without it the base rule is the more specific one
   and the size below never lands. */
.subnav-item.has-control { cursor: pointer; }
.subnav-item > input[type="checkbox"]:not(.switch input),
.subnav-item > input[type="radio"]:not(.segmented input) {
  width: 15px;
  height: 15px;
  border-width: 1.5px;
}
.subnav-item > input[type="checkbox"]:not(.switch input) { border-radius: 4px; }
.subnav-item > input[type="checkbox"]:not(.switch input):checked::after {
  left: 4px;
  top: 1px;
  width: 3px;
  height: 7px;
}
.subnav-item > input[type="radio"]:not(.segmented input)::after { inset: 2.5px; }

/* On a chosen row the control switches from the accent to the row's own
   foreground: the pill behind it is already the selection, and a primary-
   filled box on a secondary container reads as two different answers to the
   same question. */
.subnav-item.is-on > input[type="checkbox"]:not(.switch input):checked {
  background: var(--on-secondary-cont);
  border-color: var(--on-secondary-cont);
}
.subnav-item.is-on > input[type="checkbox"]:not(.switch input):checked::after {
  border-color: var(--secondary-cont);
}
.subnav-item.is-on > input[type="radio"]:not(.segmented input) {
  border-color: var(--on-secondary-cont);
}
.subnav-item.is-on > input[type="radio"]:not(.segmented input)::after {
  background: var(--on-secondary-cont);
}

/* The ring goes round the whole row, matching the button rows above it, so a
   keyboard walk down the column does not change shape halfway. `:has` rather
   than `:focus-within` so it follows the input's own `:focus-visible` -- a row
   reached by mouse is not a row that needs telling where the keyboard is. */
.subnav-item > input:focus-visible { outline: none; }
.subnav-item.has-control:has(input:focus-visible) {
  outline: 2px solid var(--primary);
  outline-offset: -2px;
}

/* The way back out of a drill-down, above the identity of the thing you
   drilled into. Both live in the sub-nav head, so the breadcrumb in the
   header and the back button here are never the only route -- one is for
   reading where you are, the other for leaving. */
.subnav-back {
  display: flex;
  align-items: center;
  gap: 7px;
  width: 100%;
  height: var(--control-h);
  padding: 0 var(--control-x-sm);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--dim);
  font: inherit;
  font-size: var(--text-4);
  text-align: left;
  cursor: pointer;
}
.subnav-back:hover { color: var(--on); background: var(--surface-cont); }
.subnav-back:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.subnav-back [data-icon] { display: grid; place-items: center; }
.subnav-back [data-icon] svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }
.subnav-back > span:last-child {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.subnav .check {
  padding: var(--space-1) var(--control-x);
  font-size: var(--text-5);
  color: var(--dim);
}

/* ------------------------------------------------------- content column -- */

/* One scroll container per level, not one for the screen: a settings level
   keeps its own heading fixed while its fields scroll, and a list level
   scrolls whole. */
.screen-main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.level {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  padding: var(--space-4) var(--pane-x) var(--space-5);
}
.level[hidden] { display: none; }

/* A level you drilled into. Darker than the list it replaced, so it reads as
   having taken the pane over rather than as floating above it.

   It arrives from the right, which is the direction you went. Toggling
   `hidden` restarts the animation, so this needs no JS and no class to
   clear -- and the global `prefers-reduced-motion` block already turns it
   into an instant swap for anyone who has asked for that. */
/* Drawn on the same surface as the list it replaced -- see the shell surfaces
   above for why. The animation is what says a level arrived; the colour used
   to say it too, and twice was once too many. */
.level-deep {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 0;
  animation: level-in var(--motion-long) var(--easing) both;
}
@keyframes level-in {
  from { opacity: 0; transform: translateX(10px); }
  to   { opacity: 1; transform: none; }
}
.level-head {
  flex: 0 0 auto;
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: var(--space-4) var(--pane-x) var(--space-3);
  border-bottom: 1px solid var(--rule);
}
.level-mark {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  margin-top: 2px;
  color: var(--accent-2);
}
.level-mark svg { width: var(--icon-size-lg); height: var(--icon-size-lg); }
.level-ident { min-width: 0; flex: 1 1 auto; }
.level-title { font-size: var(--text-11); font-weight: 500; }
.level-slug {
  font-family: var(--mono);
  font-size: var(--text-3);
  color: var(--faint);
  margin-top: 3px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.level-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  padding: var(--pane-y) var(--pane-x) var(--space-5);
}

/* Close is the X at the top-right of the pane it closes, everywhere. */
.icon-btn {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: var(--control-h-lg);
  height: var(--control-h-lg);
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--surface-cont);
  color: var(--on-variant);
  cursor: pointer;
  transition: background var(--motion-short) var(--easing);
}
.icon-btn:hover { background: var(--surface-high); }
.icon-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.icon-btn.is-quiet {
  width: var(--control-h);
  height: var(--control-h);
  background: transparent;
  color: var(--dim);
}
.icon-btn.is-quiet:hover { background: var(--surface-cont); color: var(--on); }
.icon-btn.is-small { width: var(--control-h-sm); height: var(--control-h-sm); }
.icon-btn.is-small svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }

/* --------------------------------------------------------- right pane -- */

/* The fourth column, on every screen. It is the selection -- one module, one
   tool, one client, one seat -- or, on the Dashboard, the server itself.
   Fixed width because it holds one thing at a time and a column that
   resized with its contents would move the list beside it. */
.screen-side {
  flex: 0 0 384px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--surface-pane);
  border-left: 1px solid var(--hairline);
}
.screen-side:empty { display: none; }

.side-head-row {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--pane-y) var(--pane-x-side) var(--space-3);
  border-bottom: 1px solid var(--rule);
}
.side-mark { display: grid; place-items: center; flex: 0 0 auto; color: var(--accent-2); }
.side-ident { min-width: 0; flex: 1 1 auto; }
.side-title { font-size: var(--text-8); font-weight: 500; }
.side-sub {
  font-family: var(--mono);
  font-size: var(--text-2);
  color: var(--faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.side-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  padding: 0 var(--pane-x-side) var(--space-5);
}

/* Section headings inside the right pane. Each carries the colour of what it
   is about: blue for what this thing *is*, orange for what you can change,
   and the info tone for what the model gets told. */
.side-label {
  padding: var(--space-4) 0 6px;
  font-size: var(--text-2);
  font-weight: 500;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--accent-2);
}
.side-label-alt { color: var(--primary); }
.side-label-ctx { color: var(--info); }
.side-label-warn { color: var(--warn); }

/* A key and its value on one line, the whole width of the pane. */
.side-stat {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding: 6px 0;
  border-bottom: 1px solid var(--rule);
}
.side-stat-k { flex: 1 1 auto; font-size: var(--text-4); color: var(--dim); }
.side-stat-v {
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: var(--text-4);
  color: var(--on);
  font-variant-numeric: tabular-nums;
}

/* What the model is actually told. Boxed, because it is a quotation of
   something this console did not write. */
.side-quote {
  background: var(--surface);
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  padding: var(--space-3);
}
.side-quote-head {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-2);
  color: var(--faint);
  margin-bottom: 6px;
}
.side-quote-head [data-icon] { display: grid; place-items: center; }
.side-quote-head [data-icon] svg { width: 14px; height: 14px; }
/* A module's or a tool's own description is a docstring: its paragraphs and
   its indented argument list are how somebody wrote it, and collapsing them
   into one slab makes the longest text in the console the hardest to read.
   Rendered as written, still through textContent -- this changes how
   whitespace is drawn, not whether the string is interpreted. Harmless on the
   console's own copy, which is written as one line however it is typed. */
.side-quote p {
  margin: 0;
  font-size: var(--text-4);
  line-height: 1.55;
  color: var(--outline);
  white-space: pre-wrap;
}
.side-quote-foot {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--rule);
  font-size: var(--text-3);
  color: var(--faint);
}

/* One argument of a tool. The name and its type are a key and a value, so
   they are a `.side-stat` and nothing here restyles that; what a stat has no
   room for goes underneath -- whether the argument may be left out, and
   whatever the module wrote about it.

   Divided rather than boxed. The pane is a column of sections and a bordered
   group inside one would read as a card floating in it; every other list in
   this column is hairlines too. */
.side-params { margin-top: 2px; }
.side-param { padding-bottom: var(--space-2); }
.side-param + .side-param {
  padding-top: var(--space-2);
  border-top: 1px solid var(--rule);
}
/* Inside a parameter the stat gives up its own divider -- the block around
   it already carries one -- and its key is a code identifier, so it is
   monospaced and takes the bright ink the value normally has. */
.side-param .side-stat { padding: 0; border-bottom: 0; }
.side-param .side-stat-k {
  font-family: var(--mono);
  font-size: var(--text-3);
  color: var(--on);
  overflow-wrap: anywhere;
}
.side-param .side-stat-v { font-size: var(--text-2); color: var(--faint); }
.side-param-note { font-size: var(--text-2); color: var(--fainter); margin-top: 1px; }
.side-param-note .is-required { color: var(--warn); }
.side-param-hint {
  font-size: var(--text-3);
  line-height: 1.5;
  color: var(--dim);
  margin-top: 3px;
  overflow-wrap: anywhere;
}

/* The pane's own footer, and the one exception to the rule that this column
   holds no controls: a way *out* of it, to a screen the pane can only point
   at. A client's memories are curated on the Memory screen and nowhere else,
   so the pane that counts them has to be able to say where they are.

   Held at the bottom of the column rather than at the bottom of the scroll,
   because a pane whose body is longer than the column would otherwise hide
   its one link below the fold.

   Nothing that opens a *level of this same screen* belongs here. That was
   "Open full settings", and it was a second copy of the gear already on the
   row the pane is about. */
.side-foot {
  flex: 0 0 auto;
  padding: var(--space-3) var(--pane-x-side) var(--space-4);
  background: var(--surface-pane);
  border-top: 1px solid var(--rule);
}
/* The body ends where the footer begins, so the last stat is not sitting
   against the button. */
.side-body:has(+ .side-foot) { padding-bottom: 4px; }
/* And it opens where the head would have been. A drilled-in pane has no head
   -- the level to its left is already titled -- so the top inset the head was
   carrying has to come from somewhere, or the first section sits against the
   top of the column. */
.screen-side > .side-body:first-child { padding-top: var(--pane-y); }

.side-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  width: 100%;
  height: var(--control-h-lg);
  margin-top: var(--space-3);
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--secondary-cont);
  color: var(--on-secondary-cont);
  font: inherit;
  font-size: var(--text-6);
  font-weight: 500;
  cursor: pointer;
}
.side-action { transition: background var(--motion-short) var(--easing); }
.side-action:hover { background: var(--surface-est); }
.side-action:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.side-action [data-icon] { display: grid; place-items: center; }
/* In the footer it is the footer's whole content, so its own top margin is
   the footer's padding twice over. */
.side-foot .side-action { margin-top: 0; }

/* `.side-empty` was here: the grey sentence a pane showed with nothing
   picked. Every pane that had one shows an overview of the slice in view
   instead, built from the same `.side-*` pieces as a selection. A column
   whose empty state is a sentence telling you to click something is a
   column that is blank on the screen the console opens on. */

/* ---------------------------------------------------------- codebase -- */

/* The canvas is the column, edge to edge -- a graph with a margin is a graph
   with less graph in it. The pane beside it is prose and keeps its padding,
   which this rule used to take away: it out-specified `.web-detail` and left
   every line of that explanation against the window's edge. */
.panel-web .screen-main { padding: 0; }

/* ------------------------------------------------------------ narrower -- */

/* Under about 1120px the four columns do not fit at a readable width, so the
   right pane goes under the content instead of beside it. It keeps its
   contents: on Users and Security that pane is the editor, and hiding it
   would leave rows that cannot be opened. */
@media (max-width: 1120px) {
  .panel.is-active {
    display: grid;
    grid-template-columns: 196px minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr) minmax(0, auto);
  }
  .subnav { grid-row: 1 / span 2; flex: 1 1 auto; }
  .screen-main { grid-column: 2; grid-row: 1; }
  .screen-side {
    grid-column: 2;
    grid-row: 2;
    flex: 0 0 auto;
    max-height: 46vh;
    border-left: 0;
    border-top: 1px solid var(--hairline);
  }
}

@media (max-width: 760px) {
  .shell { grid-template-columns: 56px minmax(0, 1fr); }
  .rail-item { width: 48px; }
  .rail-pill { width: 44px; }
  .rail-label { display: none; }
  .panel.is-active { grid-template-columns: 0 minmax(0, 1fr); }
  .subnav { display: none; }
}

/* ---------------------------------------------------------------- tools -- */

/* The pre-redesign Tools styles lived here; WP-6 replaced the screen. */

/* ------------------------------------------------------------- activity -- */

.activity-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 10px;
}
/* The bare .is-warn rule is scoped to .banner, so the source line needs its
   own. Amber rather than --danger: a trail that dies on restart is a caveat
   about what you are reading, not a failure. */
#activity-source.is-warn { color: var(--warn); }

.activity-row {
  display: flex;
  gap: 10px;
  align-items: baseline;
  padding: 7px 0;
  border-top: 1px solid var(--rule);
}

/* On/off carried by colour *and* by the verb in the sentence beside it, so the
   row still reads correctly to someone who cannot separate the two hues. */
.activity-dot {
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--surface-est);
  transform: translateY(-1px);
}
.activity-dot.is-on { background: var(--primary); }
.activity-dot.is-off { background: var(--error); }

.activity-body { flex: 1 1 auto; min-width: 0; }
.activity-line { overflow-wrap: anywhere; }

.activity-actor { color: var(--on); font-weight: 600; }
.activity-verb { color: var(--dim); }
/* Tool and module names are identifiers, so they get the mono face the Tools
   tab gives them -- the same string should not look like two different things
   in two places. */
.activity-target { font-family: var(--mono); font-size: var(--text-5); color: var(--outline); }
.activity-kind {
  margin-left: 6px;
  font-size: var(--text-1);
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--dim);
  border: 1px solid var(--edge);
  border-radius: 5px;
  padding: var(--chip-y-sm) var(--chip-x-sm);
}

/* Fixed width so the timestamps stack into a column instead of ragging along
   with each name's length. */
.activity-when {
  flex: 0 0 auto;
  width: 96px;
  text-align: right;
  font-size: var(--text-5);
  color: var(--dim);
}

/* ------------------------------------------------------------------ web -- */

/* A checkbox and its label as one target. `accent-color` used to be set here
   and reached nothing: these boxes are drawn by the rule above, so the native
   accent has nothing left to tint. */
.check {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-5);
  color: var(--dim);
  cursor: pointer;
  transition: color var(--motion-short) var(--easing);
}
.check:hover { color: var(--on-variant); }
.check:hover input[type="checkbox"] { border-color: var(--primary); }

.web-canvas-wrap {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  background:
    radial-gradient(circle at 50% 45%, var(--surface-cont) 0%, var(--surface) 70%);
}
#web-canvas { display: block; width: 100%; height: 100%; cursor: grab; }
#web-canvas.is-grabbing { cursor: grabbing; }

.web-engine { position: absolute; left: 12px; bottom: 10px; pointer-events: none; }

.web-legend {
  position: absolute;
  top: 12px;
  left: 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  background: rgba(11, 15, 20, .78);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  padding: var(--space-3);
  backdrop-filter: blur(4px);
}
.legend-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--text-5);
  color: var(--dim);
  cursor: pointer;
  user-select: none;
}
.legend-row.is-muted { opacity: .35; }
.legend-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex: 0 0 auto;
  /* Set per element by console-web.js, which knows the graph's colours
     and nothing about how a swatch is drawn. Falls back to the outline,
     so a swatch whose colour never arrived is still a swatch. */
  background: var(--dot, var(--outline));
}
.legend-count { margin-left: auto; font-family: var(--mono); font-size: var(--text-3); }

/* The legend is in two halves: the plane a node belongs to, and the kind of
   thing it is. They sort the same nodes two different ways, so they are
   separated rather than run together into one list. */
.legend-heading {
  font-size: var(--text-1);
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--dim);
  opacity: .65;
}
.legend-heading + .legend-row { margin-top: 1px; }
.legend-group + .legend-group { margin-top: 10px; }
.legend-group { display: flex; flex-direction: column; gap: 4px; }

/* A plane row is the heavier of the two: it is the coarsest thing the map
   sorts by, and its swatch is a bar rather than a dot to set it apart from
   the category dots below it. */
.legend-row.is-plane { font-size: var(--text-5); color: var(--on); }
.legend-row.is-plane .legend-dot {
  width: 11px;
  height: 4px;
  border-radius: 2px;
}

/* A node's shape carries meaning too: a square is a table, a circle a file.
   The legend says so rather than leaving it to be worked out. */
.legend-shape {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--text-3);
  color: var(--dim);
  opacity: .8;
}
.legend-glyph { width: 9px; height: 9px; flex: 0 0 auto; background: var(--cat-table); }
.legend-glyph.is-circle { border-radius: 50%; background: var(--cat-core); }
.legend-glyph.is-square { border-radius: 2px; }
.legend-glyph.is-hex {
  background: var(--cat-service);
  clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
}

/* Services mode: a small ring in place of a dot, echoing the thick border
   drawn on the node itself rather than a flat swatch. */
.legend-row.is-direction .legend-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: transparent;
  border: 2px solid var(--dot-ring, var(--outline));
}

/* The one right pane that is prose rather than sections, so it holds its own
   inset instead of getting it from `.side-body` -- the same inset, because a
   column that starts two pixels lower than the other six is the difference a
   reader notices clicking through the rail without being able to name it. */
.web-detail { padding: var(--pane-y) var(--pane-x-side); overflow: auto; }

.detail-category {
  color: var(--ink, var(--on));
  display: inline-block;
  font-family: var(--mono);
  font-size: var(--text-3);
  text-transform: uppercase;
  letter-spacing: .06em;
  padding: var(--chip-y-sm) var(--chip-x-sm);
  border-radius: 999px;
  border: 1px solid currentColor;
  margin-bottom: 8px;
}

/* The plane sits beside the category, filled rather than outlined so the two
   badges do not read as the same kind of label. */
.detail-plane {
  color: var(--ink, var(--on));
  display: inline-block;
  font-family: var(--mono);
  font-size: var(--text-3);
  text-transform: uppercase;
  letter-spacing: .06em;
  padding: var(--chip-y-sm) var(--chip-x-sm);
  border-radius: 999px;
  background: color-mix(in srgb, currentColor 14%, transparent);
  margin: 0 0 8px 6px;
}

.detail-columns { margin-top: 16px; }
.detail-columns h4 {
  margin: 0 0 8px;
  font-size: var(--text-5);
  color: var(--dim);
  font-weight: 600;
}
.column-list {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  overflow: hidden;
}
.column-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: var(--chip-y) var(--chip-x-sm);
  font-family: var(--mono);
  font-size: var(--text-3);
  background: var(--surface-low);
}
.column-row + .column-row { border-top: 1px solid var(--rule); }
.column-name { color: var(--on); }
/* The primary key is the one column worth picking out of the list. */
.column-name.is-key { color: var(--cat-table); font-weight: 600; }
.column-name.is-key::after { content: " PK"; font-size: var(--text-1); opacity: .7; }
.column-type { margin-left: auto; color: var(--dim); text-align: right; }
.column-ref {
  background: none;
  border: none;
  padding: 0;
  color: var(--cat-table);
  font: inherit;
  cursor: pointer;
  flex: 0 0 auto;
}
.column-ref:hover { text-decoration: underline; }

.detail-path {
  font-family: var(--mono);
  font-size: var(--text-5);
  color: var(--dim);
  word-break: break-all;
  margin-bottom: 12px;
}
.detail-description { font-size: var(--text-7); }

.detail-meta {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px 12px;
  margin: 14px 0;
  font-size: var(--text-5);
}
.detail-meta dt { color: var(--dim); }
.detail-meta dd { margin: 0; font-family: var(--mono); }

.detail-links h4 {
  font-size: var(--text-5);
  color: var(--dim);
  text-transform: uppercase;
  letter-spacing: .06em;
  margin: 16px 0 6px;
  font-weight: 600;
}
.detail-link {
  display: block;
  width: 100%;
  text-align: left;
  background: var(--surface-low);
  border: 1px solid var(--edge);
  border-radius: 7px;
  color: var(--dim);
  font-family: var(--mono);
  font-size: var(--text-3);
  padding: var(--chip-y) var(--chip-x);
  margin-bottom: var(--space-1);
  cursor: pointer;
}
.detail-link:hover { color: var(--on); border-color: var(--surface-est); }
.detail-link .arrow { color: var(--dim); margin-right: 6px; }

/* A standing condition, said once above the thing it is about. An edge and
   a tint rather than a filled amber block: three of these can be true at
   once on a settings level -- the module is dark, no key is configured, and
   nothing survives a restart -- and three filled blocks read as an error
   page rather than as three facts. */
.banner {
  border: 1px solid var(--edge);
  border-left: 3px solid var(--edge);
  border-radius: var(--radius-sm);
  padding: var(--space-3);
  margin-bottom: 10px;
  font-size: var(--text-5);
  line-height: 1.55;
  color: var(--outline);
  background: var(--surface-low);
}
.banner strong { color: var(--on-variant); font-weight: 500; }
.banner code {
  font-family: var(--mono);
  font-size: var(--text-3);
  overflow-wrap: anywhere;
}
.banner.is-warn,
.banner.banner-warn {
  border-color: color-mix(in srgb, var(--warn) 22%, transparent);
  border-left-color: var(--warn);
  background: color-mix(in srgb, var(--warn) 7%, var(--surface));
}
.banner.is-warn strong,
.banner.banner-warn strong { color: var(--warn); }

/* --------------------------------------------------------------- toast -- */

/* snackbar: see primitives */

/* --------------------------------------------------------------- login -- */

/* `place-items: center` on a grid rather than a flex column, because the card
   is a grid *item* here and cannot be stretched by an inherited `flex` the
   way it was as a `<main>`. */
.login-body {
  display: grid;
  place-items: center;
  padding: var(--space-5);
  overflow: auto;
  /* The one gradient in the console. This is the only screen with nothing on
     it, and a flat field of one colour reads as a page that failed to load. */
  background: radial-gradient(120% 90% at 50% 0%,
              var(--surface-high) 0%, var(--surface) 65%);
}

.login-card { width: min(400px, 100%); }

.login-brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-6);
}
.login-brand h1 { margin: 0; font-size: var(--text-11); font-weight: 500; letter-spacing: -.01em; }
.login-brand p { margin: 0; }
.login-mark {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  flex: 0 0 auto;
  border-radius: var(--radius);
  background: var(--primary-cont);
  color: var(--on-primary-cont);
}
.login-mark .icon { --icon-size: 24px; }
/* The same pair as the rail, one size up: this is the first thing anybody
   sees, and it is the only mark on the page. */
.login-mark .brand-logo { width: 30px; height: 30px; }
.login-mark:has(.brand-logo) [data-icon] { display: none; }

.login-card label {
  display: block;
  font-size: var(--text-5);
  color: var(--on-variant);
  margin-bottom: var(--space-2);
}
/* An M3 filled field: no box, a weighted bottom edge that lights up on
   focus. The token is the only thing on this page, so it gets the emphasis. */
.login-card input {
  width: 100%;
  height: 52px;
  background: var(--surface-cont);
  border: 0;
  border-bottom: 2px solid var(--field-border);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  color: var(--on);
  font: inherit;
  font-family: var(--mono);
  font-size: var(--text-8);
  padding: 0 var(--space-4);
  margin-bottom: var(--space-4);
  transition: border-color var(--motion-short) var(--easing);
}
.login-card input:focus-visible {
  outline: none;
  border-bottom-color: var(--primary);
  box-shadow: none;
}
.login-card button { width: 100%; justify-content: center; height: 44px; }

.login-note { margin-top: var(--space-5); }
.error { color: var(--error); font-size: var(--text-7); margin-top: var(--space-3); }

@media (max-width: 900px) {
  .topbar { gap: var(--space-2); }
  .topbar .screen-meta { display: none; }
}

/* ----------------------------------------------------------- write access */

.write-card {
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  padding: var(--space-4);
  margin-bottom: 14px;
  background: var(--surface-cont);
}
.write-card.is-off { opacity: .72; }

.pill.is-live { color: var(--primary); border-color: var(--primary-cont); }
.pill.is-muted { opacity: .6; }

.write-form { display: flex; flex-direction: column; gap: 12px; }

.write-field { display: flex; flex-direction: column; gap: 5px; }
.write-field input { width: 100%; }

.write-field-check { flex-direction: row; align-items: center; gap: 8px; }

.write-scopes { display: flex; flex-wrap: wrap; gap: 8px; }
.write-scope {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--edge);
  border-radius: 999px;
  padding: var(--chip-y) var(--chip-x);
  font-size: var(--text-5);
  cursor: pointer;
}
.write-scope:has(input:checked) { border-color: var(--primary-cont); color: var(--primary); }

.write-limits { display: flex; gap: 12px; flex-wrap: wrap; }
.write-limits .write-field { flex: 1 1 200px; }

.write-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.write-stamp { margin-left: auto; }
.ghost.danger { color: var(--error); }

/* --------------------------------------------------------------- access -- */

/* The bootstrap-token warning. Sits outside `main`, above every tab, because
   it is not about the panel you happen to be looking at -- it is about the
   deployment being one variable deletion away from locking everybody out. */
/* `:not([hidden])` is load-bearing, not tidiness. A bare `display: flex` here
   beats the `hidden` attribute's own `display: none`, so the banner would
   show permanently -- announcing a lockout risk on a server that has none,
   which is the fastest way to teach someone to ignore it. */
#fallback-banner:not([hidden]) {
  display: flex;
  gap: 10px;
  align-items: baseline;
  flex-wrap: wrap;
}
#fallback-banner {
  margin: 14px 22px 0;
  border-color: color-mix(in srgb, var(--warn) 34%, transparent);
  background: color-mix(in srgb, var(--warn) 12%, var(--surface));
}
#fallback-banner strong { color: var(--warn); }
#fallback-banner span { color: var(--dim); }

#access-source.is-warn { color: var(--warn); }

/* Roles carry a colour, and the ordering of those colours is the point:
   admin is the loud one because it is the grant somebody should hesitate
   over, and reader is deliberately quiet. */
.pill.role-admin {
  color: var(--role-admin);
  border-color: color-mix(in srgb, var(--role-admin) 40%, transparent);
}
.pill.role-operator {
  color: var(--primary);
  border-color: color-mix(in srgb, var(--primary) 35%, transparent);
}
.pill.role-reader { color: var(--dim); }

.access-card.is-new { border-color: var(--primary); }
.access-card .role-note {
  margin: -6px 0 4px;
  min-height: 1.4em;
}
.access-card select { width: 100%; }
/* A field that cannot be edited should not invite the click. */
.access-card input.is-fixed {
  color: var(--dim);
  background: transparent;
  border-style: dashed;
}

/* The one-time token reveal.
   Given the accent border and its own block rather than folded into a toast:
   a toast dismisses itself on a timer, and this value does not exist anywhere
   else, before or after. */
.token-reveal {
  border: 1px solid var(--primary);
  border-radius: var(--radius);
  background: var(--surface-low);
  padding: var(--space-4);
  margin-bottom: 16px;
}
.token-reveal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.token-reveal-head h3 { margin: 0; font-size: var(--text-8); }
.token-value {
  display: flex;
  gap: 10px;
  align-items: center;
  margin-top: 10px;
}
.token-value code {
  flex: 1;
  font-family: var(--mono);
  font-size: var(--text-7);
  color: var(--on);
  background: var(--surface-low);
  border: 1px solid var(--surface-est);
  border-radius: var(--radius);
  padding: var(--space-3);
  /* Wraps rather than scrolls. A 64-character token cut off at the edge of a
     box is one somebody copies half of. */
  word-break: break-all;
  user-select: all;
}

/* ----------------------------------------------------- governed fields --
   The three controls a governance rule is edited with, wherever a page
   places them: `console-policy.js` builds them, and a tool's settings page, a
   module's, and a client's overrides section all host the same markup.

   The `sec-` prefix is what those controls have always been called. It is
   kept deliberately -- renaming a class to match where it happens to be
   placed is exactly the coupling that dissolving the Security screen removed.

   Everything that styled that screen's own furniture -- the collapsible
   module cards, the two-column editor/preview grid, the per-tool blocks
   inside a card -- is gone with it. There is no list of tools to make a card
   out of any more: a rule is drawn beside the one thing it governs. */

/* The rule form is a `.field-rows` group, so it stacks nothing itself -- its
   children are rows and the group draws the rules between them. The bare
   `.sec-form` (inside a client's "write a rule for this one") keeps the
   stack, because there it is a form and not a group. */
.sec-form:not(.field-rows) { display: flex; flex-direction: column; gap: 10px; }
.sec-field { display: flex; flex-direction: column; gap: 5px; font-size: var(--text-7); }
.sec-field select,
.sec-field textarea,
.sec-field input[type="text"] { width: 100%; }
.sec-field textarea { resize: vertical; min-height: 60px; }
/* `.sec-field-check`, `.sec-field-group` and `.sec-inherit` were here: a
   checkbox laid out sideways, a radio group given a field's label spacing,
   and the "inherits: ..." note under a control. Each of the three governed
   fields is a `.field-row` now -- name on the left with the fall-through as
   its hint, control on the right -- so all three shapes are that one row. */

.sec-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.sec-stamp { margin-left: auto; }

/* The right-hand half of a settings row: the chips or the control the label
   on the left is about. Fixed rather than growing, so a long hint never
   pushes a switch off the edge. */
.field-row-aside {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
}
/* A picker in a row's right-hand half sizes to the row rather than to its
   longest option, so two of them stacked line up. */
.field-row-aside select { min-width: 220px; }
/* Small enough to sit beside a switch without becoming the loudest thing on
   the row -- it is an escape hatch, not the control. */
.ghost.is-small {
  height: var(--control-h-sm);
  padding: 0 var(--control-x-sm);
  font-size: var(--text-3);
}

.sec-preview-form { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; }

.sec-preview-summary {
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  padding: var(--space-3);
  margin-top: 14px;
  font-size: var(--text-7);
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.sec-preview-desc {
  background: var(--surface-low);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  padding: var(--space-3);
  margin: 0;
  font-family: var(--mono);
  font-size: var(--text-3);
  line-height: 1.5;
  /* The description is the model's whole view of the tool, and a long one is
     normal. Scrolled rather than clipped: a truncated preview of the text
     somebody opened this panel to read would defeat the panel. */
  max-height: 220px;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
}

.sec-layers {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.sec-layer {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  padding: 6px var(--field-x);
  border-radius: 6px;
  background: var(--surface-low);
  font-size: var(--text-5);
}
/* An empty layer is the answer to "why did my override not take", so it is
   shown rather than hidden -- dimmed only enough that the layers holding
   something read first. */
.sec-layer.is-empty { opacity: .5; }
.sec-layer-name { font-family: var(--mono); font-size: var(--text-3); }

/* ----------------------------------------------------------- primitives --
   Each of these is defined once here and referenced by class everywhere. The
   test for the whole file: changing `--primary` in one `:root` line restyles
   every screen, and no pixel value for any of it appears in JS. */

/* ---------------------------------------------------------------- switch --
   A real focusable checkbox under an M3 track. The input covers the whole
   control rather than collapsing to 0x0, so it keeps its own focus, its own
   click target and its own announcement -- a div with a click handler would
   have needed all three rebuilt by hand and would have got one of them wrong.

   The entire on/off geometry is here, reacting to `:checked`. JS toggles
   nothing but the checkbox's own state. */
.switch {
  position: relative;
  display: inline-block;
  width: 44px;
  height: 26px;
  flex: 0 0 auto;
}
.switch input {
  position: absolute;
  inset: 0;
  opacity: 0;
  margin: 0;
  width: 100%;
  height: 100%;
  cursor: pointer;
  z-index: 1;
}
.switch .slider {
  position: absolute;
  inset: 0;
  /* M3 draws the off state as an *outlined* track -- which is what keeps a
     row of switches legible when most of them are off. It said so and then
     filled the track with `--surface-est`, the brightest surface in the
     palette: an off switch read as nearly as lit as an on one, and a row of
     modules waiting on credentials looked like a row of live ones. */
  background: var(--surface-cont);
  border: 2px solid var(--field-border);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: background var(--motion-short) var(--easing),
              border-color var(--motion-short) var(--easing);
}
/* The thumb grows when it turns on -- 16px to 24px -- which is the M3 detail
   that makes the state readable without relying on the track colour alone,
   and so survives being looked at by somebody who cannot tell orange from
   grey. */
.switch .slider::before {
  content: "";
  position: absolute;
  width: 14px;
  height: 14px;
  left: 4px;
  top: 50%;
  border-radius: 50%;
  /* The off thumb is the outline colour, not near-white: on is a filled
     track with a grown thumb, and an off thumb bright enough to be the
     loudest thing in the row was competing with it. */
  background: var(--dim);
  transform: translateY(-50%);
  transition: width var(--motion-short) var(--easing),
              height var(--motion-short) var(--easing),
              left var(--motion-short) var(--easing),
              background var(--motion-short) var(--easing);
}
.switch input:checked + .slider {
  background: var(--primary);
  border-color: var(--primary);
}
.switch input:checked + .slider::before {
  width: 18px;
  height: 18px;
  left: 20px;
  background: var(--on-primary);
}

/* On a row that is selected, the switch takes the blue. Blue means one thing
   everywhere in this console -- this is the thing you are looking at -- and
   the switch is part of that row, not an exception to it. The on/off reading
   does not depend on the hue: the thumb still grows and the track still
   fills, which is what somebody who cannot separate orange from blue is
   reading anyway. */
.row.is-selected .switch input:checked + .slider {
  background: var(--accent-2);
  border-color: var(--accent-2);
}
.row.is-selected .switch input:checked + .slider::before {
  background: var(--accent-2-cont);
}
.switch input:focus-visible + .slider {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
/* Nothing to switch. A module whose credentials are missing, or any switch
   in front of a principal who may not toggle: the control is drawn inert
   rather than dimmed. `opacity` faded the outline out with everything else
   and left a bright thumb floating on a mid-purple pill -- which read as a
   switch somebody had greyed out by accident rather than as one there is
   nothing behind. Explicit colours keep the outline, which is the part that
   says "this is a switch". */
.switch input:disabled { cursor: not-allowed; }
.switch input:disabled + .slider {
  cursor: not-allowed;
  background: var(--surface-low);
  border-color: var(--edge);
}
.switch input:disabled + .slider::before { background: var(--fainter); }
/* On, and not yours to turn off -- a reader looking at a serving module. It
   still has to read as on, so it keeps the fill and takes the container
   tone rather than the full primary. */
.switch input:disabled:checked + .slider {
  background: var(--primary-cont);
  border-color: var(--primary-cont);
}
.switch input:disabled:checked + .slider::before { background: var(--surface-low); }

/* The tool row's switch. Smaller because a tool list is forty rows deep and
   a module list is six -- the density is the difference, not the meaning. */
.switch.switch-sm { width: 34px; height: 20px; }
.switch.switch-sm .slider { border-width: 2px; }
.switch.switch-sm .slider::before { width: 10px; height: 10px; left: 3px; }
.switch.switch-sm input:checked + .slider::before {
  width: 14px; height: 14px; left: 15px;
}

/* ------------------------------------------------------------------ chip --
   One shape, four meanings, separated by a modifier rather than by four
   near-identical rules. `.chip-grant` is a token's access; `.chip-env` is a
   variable name; `.chip-warn` is a state worth stopping on. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  max-width: 100%;
  padding: var(--chip-y) var(--chip-x);
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  background: var(--surface-cont);
  color: var(--outline);
  font-size: var(--text-3);
  line-height: 1.6;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chip .icon { --icon-size: 13px; }
.chip-grant { font-family: var(--mono); color: var(--on-secondary-cont); background: var(--secondary-cont); border-color: transparent; }
.chip-env { font-family: var(--mono); color: var(--dim); }
.chip-warn { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 34%, transparent); background: color-mix(in srgb, var(--warn) 10%, var(--surface)); }
.chip-danger { color: var(--error); border-color: color-mix(in srgb, var(--error) 34%, transparent); background: color-mix(in srgb, var(--error) 10%, var(--surface)); }
/* Not a "success" colour -- there is none in this palette. Blue here means
   the same thing it means everywhere: this is the live, current state. */
.chip-on { color: var(--on-accent-2-cont); background: var(--accent-2-cont); border-color: transparent; }

/* Tag colours are the tag's meaning, so the class carries the meaning rather
   than the colour. An unrecognised tag falls through to the plain chip. */
.tag-money { color: var(--tag-money); border-color: color-mix(in srgb, var(--tag-money) 34%, transparent); }
.tag-destructive { color: var(--tag-destructive); border-color: color-mix(in srgb, var(--tag-destructive) 34%, transparent); }
.tag-pii { color: var(--tag-pii); border-color: color-mix(in srgb, var(--tag-pii) 34%, transparent); }
.tag-beta { color: var(--tag-beta); border-color: color-mix(in srgb, var(--tag-beta) 34%, transparent); }
.tag-bulk { color: var(--tag-bulk); border-color: color-mix(in srgb, var(--tag-bulk) 34%, transparent); }

/* ------------------------------------------------------------------- row --
   The module row, and the shape every list on every screen borrows: a mark,
   a growing middle, and a fixed control on the right.

   Rows live in a `.row-list`, which is one bordered block with the rows ruled
   inside it -- the same treatment `.tool-rows` gives a tool. They used to be
   separately outlined and spaced, which made three screens that hold the same
   kind of list look like three different consoles. A list is read as a block
   and picked from one row at a time, and the block is what says where it
   stops. */
.row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: var(--row-y) var(--row-x);
  border-bottom: 1px solid var(--rule);
  cursor: pointer;
  transition: background var(--motion-short) var(--easing);
}
.row:last-child { border-bottom: 0; }
.row:hover { background: var(--surface-low); }
.row:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

/* Blue means "this is the thing you are looking at", here as everywhere. An
   inset shadow rather than a border-left, so the row does not shift by a
   pixel when it becomes the selected one. */
.row.is-selected {
  background: color-mix(in srgb, var(--accent-2) 7%, transparent);
  box-shadow: inset 3px 0 0 var(--accent-2);
}
.row-compact { padding: var(--row-y) var(--row-x); }

.row-mark {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  color: var(--primary);
}
.row-mark svg { width: 21px; height: 21px; }
.row.is-selected .row-mark { color: var(--accent-2); }
.row.is-off .row-mark, .row.is-unavailable .row-mark { color: var(--fainter); }

/* A client has no glyph of its own to declare, so its mark is the first
   letter of its name -- the one thing that is always different between two
   rows on that screen. */
.row-avatar {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  background: var(--surface-cont);
  color: var(--dim);
  font-size: var(--text-7);
  font-weight: 500;
  text-transform: uppercase;
}
.row.is-selected .row-avatar,
.side-head-row .row-avatar {
  background: var(--accent-2-cont);
  color: var(--on-accent-2-cont);
}
.row-avatar.is-small { width: 30px; height: 30px; font-size: var(--text-5); }

/* One line of what a module says about itself, cut where the row ends. The
   whole paragraph is behind the marker on the name beside it. */
.row-desc {
  margin-top: 2px;
  font-size: var(--text-4);
  color: var(--dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.row-body { flex: 1 1 auto; min-width: 0; }
.row-title {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-bottom: 2px;
}
.row-name { font-size: var(--text-7); font-weight: 500; white-space: nowrap; }
.row-slug { font-family: var(--mono); font-size: var(--text-3); color: var(--faint); }
.row-chips { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: 4px; }
.row-aside {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 0 0 auto;
  margin-left: auto;
}
.row-count {
  text-align: right;
  min-width: 76px;
  font-size: var(--text-2);
  color: var(--faint);
}
.row-count b { display: block; font-size: var(--text-5); font-weight: 400; color: var(--on-variant); }

/* A module that could not load is dimmed rather than hidden: the operator
   needs to see that it exists and what it is waiting for. */
.row.is-unavailable .row-name,
.row.is-unavailable .row-desc { color: var(--dim); }

/* ------------------------------------------------------------- snackbar --
   One element, bottom centre, with an action slot that is only filled when
   there is something honest to put in it -- see §7.4. A snackbar built
   without a revert closure renders no button at all rather than offering an
   undo that does nothing. */
.toast {
  position: fixed;
  bottom: var(--space-5);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: var(--space-4);
  max-width: min(560px, calc(100vw - 2 * var(--space-5)));
  padding: var(--space-3) var(--space-4);
  background: var(--surface-est);
  color: var(--on);
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  font-size: var(--text-7);
  z-index: 20;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .5);
  animation: snack-in var(--motion-long) var(--easing) both;
}
.toast.is-error { border-color: var(--error); color: var(--error); }
.toast-text { flex: 1 1 auto; min-width: 0; }
.toast-undo {
  flex: 0 0 auto;
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
  padding: 4px var(--space-2);
  color: var(--primary);
  font: inherit;
  font-size: var(--text-5);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .04em;
  cursor: pointer;
}
.toast-undo:hover { background: color-mix(in srgb, var(--primary) 16%, transparent); }
.toast-undo:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.toast-undo:disabled { opacity: .5; cursor: default; }

@keyframes snack-in {
  from { opacity: 0; transform: translate(-50%, 8px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

/* ------------------------------------------------------- modules screen -- */

.side-head {
  margin: 0 0 var(--space-2);
  font-size: var(--text-3);
  font-weight: 500;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--dim);
}

/* A definition list, because that is what it is: a label and its value. The
   grid keeps the values in one column without either side being a fixed
   width somebody has to maintain. */
.health {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--space-2) var(--space-4);
  margin: 0;
  font-size: var(--text-5);
}
.health dt { color: var(--dim); }
.health dd {
  margin: 0;
  font-family: var(--mono);
  color: var(--on);
  text-align: right;
}

/* --------------------------------------------------------------- banner -- */

/* The bootstrap-token warning, as an M3 error container. It appears only when
   `server.env_fallback` is true, which on a settled deployment is never --
   and when it is true, the deployment is one variable deletion away from
   locking everybody out, which is worth the loudest thing on the page. */
.banner-error {
  border-color: color-mix(in srgb, var(--error) 26%, transparent);
  border-left-color: var(--error);
  background: color-mix(in srgb, var(--error) 10%, var(--surface));
}
.banner-error strong { color: var(--error); }

/* --------------------------------------------------------- tools screen -- */

.tool-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--row-y) var(--row-x);
  border-bottom: 1px solid var(--rule);
  cursor: pointer;
}
.tool-row:last-child { border-bottom: 0; }
.tool-row:hover { background: var(--surface-low); }
.tool-row:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.tool-row.is-selected {
  background: color-mix(in srgb, var(--accent-2) 7%, transparent);
  box-shadow: inset 3px 0 0 var(--accent-2);
}
.tool-row-body { flex: 1 1 auto; min-width: 0; }
.tool-row-module {
  display: block;
  font-size: var(--text-2);
  color: var(--faint);
  margin-top: 1px;
}
/* Switched on, and still not reaching a caller because the module above it is
   off. The row says so in a chip; this only keeps it from reading as normal. */
.tool-row.is-held { opacity: .7; }
.tool-name {
  display: block;
  min-width: 0;
  font-family: var(--mono);
  font-size: var(--text-5);
  color: var(--on);
  /* Broken mid-token rather than ellipsed. These are snake_case identifiers
     read to tell one from another, and `google_ads_set_campaign_...` tells
     you nothing that `google_ads_set_campaign_network_settings` wrapped over
     two lines does not tell you completely. */
  overflow-wrap: anywhere;
}
.tool-meta {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin-left: auto;
  /* Allowed to shrink, so the name is never the thing that gives way. */
  flex: 0 1 auto;
  flex-wrap: wrap;
  justify-content: flex-end;
}
.tool-row .switch { margin-left: var(--space-2); }

/* --------------------------------------------------- the content's bar --
   A header for the content column itself, on a screen whose list is about a
   slice rather than about one row: what the slice is, how much of it there
   is, and the actions that apply to the whole of it. It sits above the list
   and scrolls with it, because it is a caption for the list rather than a
   toolbar for the screen. */
.pane-bar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-4);
  border-bottom: 1px solid var(--rule);
}
.pane-bar-mark { display: grid; place-items: center; flex: 0 0 auto; color: var(--accent-2); }
.pane-bar-mark svg { width: var(--icon-size-lg); height: var(--icon-size-lg); }
.pane-bar-ident { min-width: 0; flex: 1 1 auto; }
/* One line each, cut where the bar ends. A slice's name is short and the bar
   is the tallest thing on the screen after the header -- so a name that wraps
   because the window is narrow takes the whole bar down with it, and the same
   heading then sits at two different heights on two screens. Better to lose
   the tail of a long name than the rhythm of every screen. */
.pane-bar-title,
.pane-bar-sub {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pane-bar-title { font-size: var(--text-10); font-weight: 500; }
.pane-bar-sub { font-size: var(--text-4); color: var(--faint); margin-top: 2px; }
/* Where the rows come from -- "stored in Postgres", "held in memory". It sits
   between what the slice is and what you can do to it, because it is a caveat
   about everything on both sides of it. Amber when what you are about to type
   will not survive a restart. */
/* Allowed to shrink, and to give up entirely on a narrow bar. It is a caveat
   about the rows below; the slice's own name and the actions are what the bar
   is for, and a long "held in memory" line must not squeeze either. */
.pane-bar-source {
  flex: 0 1 auto;
  min-width: 0;
  font-size: var(--text-3);
  color: var(--faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pane-bar-source.is-warn { color: var(--warn); }

.pane-bar-actions { display: flex; align-items: center; gap: var(--space-2); flex: 0 0 auto; }
.pane-bar-actions .ghost,

/* ---------------------------------------------------- a category's block --
   The Tools screen with no category chosen shows every category as its own
   block rather than one flat alphabet of 365 identifiers. The head names the
   category, says how much of it is on screen, and offers the one thing a
   heading can usefully do -- narrow the screen to it. */
.cat-section + .cat-section { margin-top: 20px; }
.cat-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0 2px 9px;
}
.cat-mark { display: grid; place-items: center; flex: 0 0 auto; color: var(--accent-2); }
.cat-mark svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }
.cat-name {
  font-size: var(--text-2);
  font-weight: 500;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--accent-2);
}
.cat-n { font-family: var(--mono); font-size: var(--text-2); color: var(--fainter); }
.cat-open {
  margin-left: auto;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  border: 0;
  background: transparent;
  height: var(--control-h-sm);
  padding: 0 var(--control-x-sm);
  border-radius: var(--radius-sm);
  color: var(--dim);
  font: inherit;
  font-size: var(--text-3);
  cursor: pointer;
  transition: color var(--motion-short) var(--easing),
              background var(--motion-short) var(--easing);
}
.cat-open:hover { color: var(--on); background: var(--surface-cont); }
.cat-open:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

/* -------------------------------------------------- a level's own head -- */

/* A heading inside a drilled-in level: what this section of it is, and how
   much of it there is. Not the screen's name -- the header says that. */
/* A head and the group under it are one section, so the gap between them is
   smaller than the gap between two sections -- `.level-block + .level-block`
   sets that one. */
.level-section-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}
.level-section-head .side-label { padding: 0; }
.level-section-n {
  font-family: var(--mono);
  font-size: var(--text-2);
  color: var(--fainter);
}

/* Two sections in one level's scroll. The rule between them is what says
   they are siblings rather than one long form -- and `scroll-margin-top`
   is what keeps a jump from the sub-nav landing with the heading against
   the very top edge of the pane. */
/* Sections are separated by space alone. They used to carry a rule between
   them, which was right when each section was loose rows on a bare surface;
   now every section is a bordered group, and a line between two boxes is a
   third edge doing nothing. */
.level-block { scroll-margin-top: var(--space-4); }
.level-block[hidden] + .level-block { margin-top: 0; }
.level-block + .level-block { margin-top: 30px; }
/* ------------------------------------------------------ a settings group --
   Every section of every level is the same two things: a head, and one
   bordered group of rows under it. `.field-rows` is that group, and it draws
   the same box `.tool-rows` and `.row-list` draw -- one edge, hairlines
   between the rows inside, no gaps. Written as one rule for the three of them
   so a list of tools, a list of modules and a list of settings cannot drift
   apart, which is exactly what they had done. */
.tool-rows,
.row-list,
.field-rows {
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  overflow: hidden;
}
.field-rows + .level-section-head { margin-top: 26px; }

/* A row is whatever height its contents need -- a switch and a sentence, a
   chip, a four-line textarea, a whole form. What makes them one group is the
   box round them, not their being the same size. `align-items: center` is
   right for a row that is a label and a control; a row holding a form gets
   `is-block` and stacks instead. */
.field-row {
  display: flex;
  align-items: center;
  gap: 18px;
  padding: var(--row-y) var(--row-x);
  border-bottom: 1px solid var(--rule);
}
/* A row that stacks holds a paragraph, a form or a card rather than a line,
   so it takes the taller inset -- the line-height that keeps a one-line row
   from looking cramped is not enough air round a block. */
.field-row.is-block { padding: var(--row-y-block) var(--row-x); }
.field-row:last-child { border-bottom: 0; }
.field-row.is-block { display: block; }
.field-row-body { min-width: 0; flex: 1 1 auto; }
.field-row-label { font-size: var(--text-6); color: var(--on-variant); }
/* The sentence under a label is not optional -- a switch that does not say
   when it bites is a switch somebody flips twice. */
.field-row-hint {
  font-size: var(--text-3);
  color: var(--faint);
  margin-top: 2px;
  max-width: 62ch;
}
/* A form or a quote fills its row rather than sitting in a column of its
   own, and brings no margin of its own to the row's padding. */
.field-row > .sec-form { margin: 0; width: 100%; }
.field-row > .sec-field { width: 100%; }
/* A quote is already inside the group's box, so it draws none of its own --
   a box in a box reads as two things when it is one. In the right pane, where
   there is no group round it, it keeps its edge. */
.field-row > .side-quote {
  margin: 0;
  width: 100%;
  border: 0;
  border-radius: 0;
  padding: 0;
  background: none;
}
.field-row > p:first-child { margin-top: 0; }
.field-row > :last-child { margin-bottom: 0; }

/* ----------------------------------------------------- dashboard screen -- */

.dash-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}
.dash-tile {
  padding: var(--space-4);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: var(--surface-low);
}
.dash-tile-head {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: var(--text-3);
  color: var(--faint);
}
.dash-tile-head [data-icon] { display: grid; place-items: center; color: var(--primary); }
.dash-tile-head [data-icon] svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }
.dash-tile-value {
  margin-top: 8px;
  font-size: var(--text-13);
  font-weight: 500;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
.dash-tile-sub { margin-top: 2px; font-size: var(--text-3); color: var(--faint); }

.dash-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--space-5);
  align-items: start;
}
.dash-col { min-width: 0; }
/* The heading is the first thing in its column, so it takes the padding the
   same heading has when it follows a section inside a level. */
.dash-col > .level-section-head { margin-top: 0; }
.dash-col > .level-section-head .side-label { padding-top: 0; }

/* Both columns are `.field-rows` groups like every other list; these two
   rules only say what a row of each holds. A summary row is a button because
   it goes somewhere; an activity row is a statement and is not. */
.dash-row {
  width: 100%;
  border: 0;
  border-bottom: 1px solid var(--rule);
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  gap: var(--space-3);
  padding: var(--row-y) var(--row-x);
}
.dash-row:hover { background: var(--surface-low); }
.dash-row:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.dash-row-mark { display: grid; place-items: center; flex: 0 0 auto; color: var(--primary); }
.dash-row-mark svg { width: 18px; height: 18px; }
.dash-row-mark.is-held { color: var(--dim); }
.dash-row-mark.is-off { color: var(--fainter); }
.dash-row-name {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--text-6);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dash-row-n {
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: var(--text-3);
  color: var(--faint);
}
.dash-row-state { flex: 0 0 74px; text-align: right; font-size: var(--text-3); color: var(--faint); }

.dash-act {
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--row-y) var(--row-x);
}
.dash-act-body { flex: 1 1 auto; min-width: 0; }
.dash-act-line { font-size: var(--text-5); }
.dash-act-who { font-size: var(--text-2); color: var(--faint); margin-top: 1px; }

/* ------------------------------------------------------------ palette -- */

.palette-overlay {
  position: fixed;
  inset: 0;
  z-index: 35;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding: 12vh var(--space-5) var(--space-5);
  background: color-mix(in srgb, var(--surface) 72%, transparent);
}
.palette-panel {
  width: min(560px, 100%);
  display: flex;
  flex-direction: column;
  max-height: 64vh;
  border: 1px solid var(--edge);
  border-radius: var(--radius-lg);
  background: var(--surface-cont);
  box-shadow: 0 24px 64px rgba(0, 0, 0, .55);
  overflow: hidden;
}
.palette-input {
  border: 0;
  border-bottom: 1px solid var(--rule);
  background: transparent;
  color: var(--on);
  font: inherit;
  font-size: var(--text-9);
  padding: var(--space-4);
}
.palette-input:focus-visible { outline: none; }

.palette-list { overflow: auto; padding: var(--space-2); }
.palette-empty { margin: var(--space-4); }

.palette-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--on);
  font: inherit;
  font-size: var(--text-7);
  text-align: left;
  cursor: pointer;
}
.palette-row:hover,
.palette-row.is-active { background: var(--secondary-cont); color: var(--on-secondary-cont); }
.palette-kind {
  flex: 0 0 62px;
  font-size: var(--text-1);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--dim);
}
.palette-label {
  font-family: var(--mono);
  font-size: var(--text-5);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.palette-hint {
  margin-left: auto;
  flex: 0 0 auto;
  font-size: var(--text-3);
  color: var(--dim);
}

/* ------------------------------------------------------- review dialog -- */

.review-overlay {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  place-items: center;
  padding: var(--space-5);
  background: color-mix(in srgb, var(--surface) 78%, transparent);
  animation: snack-in var(--motion-long) var(--easing) both;
}
.review-panel {
  width: min(520px, 100%);
  max-height: 80vh;
  overflow: auto;
  padding: var(--space-5);
  border: 1px solid var(--edge);
  border-radius: var(--radius-lg);
  background: var(--surface-cont);
  box-shadow: 0 24px 64px rgba(0, 0, 0, .55);
}
.review-title { margin: 0 0 var(--space-1); font-size: var(--text-10); font-weight: 500; }

.review-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: var(--space-4) 0;
}
.review-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto auto;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--surface-low);
  font-size: var(--text-5);
}
.review-field { color: var(--outline); }
/* The old value is struck through rather than merely dimmed: this dialog is
   read quickly, and "what is being taken away" has to survive a glance. */
.review-from { font-family: var(--mono); color: var(--dim); text-decoration: line-through; }
.review-arrow { color: var(--dim); }
.review-to { font-family: var(--mono); color: var(--on); }

.review-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
}

/* ------------------------------------------------------ activity screen -- */

/* A day heading, sticky so the date stays readable while a long day scrolls
   past it. */
.activity-day {
  position: sticky;
  top: 0;
  z-index: 1;
  margin: var(--space-5) 0 var(--space-2);
  padding: var(--space-1) 0;
  background: var(--surface);
  font-size: var(--text-3);
  font-weight: 500;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--dim);
}
.activity-day:first-child { margin-top: 0; }

/* The resolved role floor, in the ordering the palette already encodes:
   admin is the loud one, reader deliberately quiet. */
.chip-role.role-admin    { color: var(--role-admin); border-color: color-mix(in srgb, var(--role-admin) 34%, transparent); }
.chip-role.role-operator { color: var(--role-operator); border-color: color-mix(in srgb, var(--role-operator) 34%, transparent); }
.chip-role.role-reader   { color: var(--role-reader); }

/* -------------------------------------------------- write access screen -- */

/* One account is a row of the Write access group, not a card floating in it.
   It is a tall row -- a head, scopes, two ceilings and a note -- which is the
   point of a group whose rows are whatever height they need. */
.wa-card { padding: var(--row-y-block) var(--row-x); }
.wa-card.is-off { opacity: .78; }

.wa-head { display: flex; align-items: center; gap: var(--space-4); }
.wa-title {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  flex-wrap: wrap;
  flex: 1 1 auto;
  min-width: 0;
}
.wa-name { font-size: var(--text-9); font-weight: 500; }
.wa-id { font-family: var(--mono); font-size: var(--text-5); color: var(--dim); }
.wa-note { margin: var(--space-1) 0 0; color: var(--outline); font-size: var(--text-7); }

/* Scopes wide on the left, the two numbers that bound spend on the right. */
.wa-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 150px 150px;
  gap: var(--space-5);
  align-items: start;
  margin-top: var(--space-4);
}
@media (max-width: 900px) {
  .wa-body { grid-template-columns: minmax(0, 1fr); }
}
.wa-col { min-width: 0; }
.wa-col .side-head { margin-bottom: var(--space-2); }

.wa-scopes { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.wa-scope {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: var(--chip-y) var(--chip-x);
  border: 1px solid var(--edge);
  border-radius: var(--radius-pill);
  font-size: var(--text-5);
  cursor: pointer;
  user-select: none;
}
.wa-scope input { position: absolute; opacity: 0; width: 0; height: 0; }
/* The tick is present in the markup and revealed by the state, rather than
   added and removed by script -- the chip's width then does not jump as it
   is toggled. */
.wa-scope-tick { opacity: 0; margin-left: -4px; transition: opacity var(--motion-short) var(--easing); }
.wa-scope:has(input:checked) {
  border-color: var(--primary);
  color: var(--on-primary-cont);
  background: var(--secondary-cont);
}
.wa-scope:has(input:checked) .wa-scope-tick { opacity: 1; margin-left: 0; }
.wa-scope:has(input:focus-visible) { outline: 2px solid var(--primary); outline-offset: 1px; }

.wa-num {
  display: flex;
  align-items: baseline;
  gap: var(--space-1);
  padding: 7px var(--space-3);
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  background: var(--surface-low);
}
.wa-num input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: transparent;
  color: var(--on);
  font: inherit;
  font-family: var(--mono);
  font-size: var(--text-8);
}
.wa-num input:focus-visible { outline: none; }
.wa-num:focus-within { border-color: var(--primary); }
.wa-unit { color: var(--dim); font-size: var(--text-5); }
.wa-col-num p { margin: var(--space-1) 0 0; }

.wa-more { margin-top: var(--space-4); }
/* The default marker is an OS triangle that no stylesheet reaches and that
   sits on a different baseline in every browser. Replaced with a chevron
   built from two borders in `currentColor` -- no image, no font, and it
   rotates when the disclosure opens so the control animates rather than
   swapping between two still frames. */
.wa-more summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  font-size: var(--text-5);
  color: var(--dim);
  padding: var(--space-1) 0;
  list-style: none;
  transition: color var(--motion-short) var(--easing);
}
.wa-more summary::-webkit-details-marker { display: none; }
.wa-more summary::marker { content: ""; }
.wa-more summary::before {
  content: "";
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  margin-left: 2px;
  border: solid currentColor;
  border-width: 0 1.5px 1.5px 0;
  transform: rotate(-45deg);
  transition: transform var(--motion-long) var(--easing);
}
.wa-more[open] summary::before { transform: rotate(45deg); }
.wa-more summary:hover { color: var(--on-variant); }
.wa-more summary:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.wa-more .sec-field { margin-top: var(--space-3); }
.wa-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-4);
  flex-wrap: wrap;
}
.wa-stamp { margin-left: auto; }

/* -------------------------------------------------------- access screen -- */

/* The enabled dot on a principal row. Blue when reachable, because that is
   the same "this is the live one" statement the accent makes elsewhere. */
/* The tray lays its chips across the width rather than down a column. It
   used to be in the narrow right pane and stacked; it is a section of the
   seat's own level now, which is the full width of the content column, and a
   column of one-word chips down the middle of it would be a strange shape. */
#tray-modules {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

.tray-chip {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  height: var(--control-h-sm);
  padding: 0 var(--control-x-sm);
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  background: var(--surface-cont);
  color: var(--outline);
  font: inherit;
  font-family: var(--mono);
  font-size: var(--text-3);
  text-align: left;
  cursor: pointer;
}
.tray-chip:hover { color: var(--on); border-color: var(--surface-est); }
.tray-chip:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

.ac-allowed { padding: var(--space-1); border-radius: var(--radius-sm); }
/* A brief mark on the scope the tray just ticked, so a click on the far side
   of the screen is visibly connected to the box that changed. */
.write-scope.is-just-added { outline: 2px solid var(--primary); outline-offset: 2px; }

/* The master/detail (`.md`) family used to live here: a two-thirds grid, a
   compact `.md-list` of `.md-row`s, and an `.md-pane` that was information
   about the selection or an editor for it depending on a class.

   Two screens wore it -- Security and Users -- and both are gone from the
   shape it described. Security is not a screen at all now; Users is the same
   two levels as Modules and Tools, with the list in `.row-list` like every
   other list on every other screen and the editing form on a level rather
   than in the right pane. Which is the point: `.md` existed because those two
   screens were different from the rest, and the fix was to stop them being
   different rather than to keep a second layout vocabulary for them. */

/* ------------------------------------------------ a module's own settings --
   One field per card rather than one field per hairline-separated block. The
   block version read as a stack of unrelated paragraphs -- label, control,
   chips, a Clear button floating under it -- with nothing saying where one
   field ended and the next began. A card draws that boundary once and lets
   everything about the field sit inside it: what it is on the top line, what
   is stored on the bottom one, and the control between them.

   The chips move to the top-right of the card, where they read as adjectives
   on the label rather than as a second sentence under the control. */
/* A field is a row of the settings group, not a card of its own. It was an
   outlined block with a gap either side, which made a module's settings read
   as a stack of unrelated paragraphs rather than one section of one page --
   and made them look unlike the tool list two sections below. */
.set-field { padding: var(--row-y-block) var(--row-x); }
.set-field:focus-within { background: var(--surface-low); }
/* Typed into and not yet saved. The header's save cluster carries the count;
   this is which fields it is counting -- blue, because it is the same
   "you are working on this" the selected row wears. */
.set-field.is-dirty {
  background: var(--surface-low);
  box-shadow: inset 3px 0 0 var(--accent-2);
}

.set-field-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: 8px;
}
.set-field-label {
  display: inline-flex;
  align-items: center;
  min-width: 0;
  font-size: var(--text-6);
  font-weight: 500;
  color: var(--on-variant);
}
.set-field-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-left: auto;
  justify-content: flex-end;
}

/* Every control a field can be, one rule. A module declares the kind; the
   console never styles per module. */
.set-field-control input[type="text"],
.set-field-control input[type="password"],
.set-field-control input[type="number"],
.set-field-control select,
.set-field-control textarea {
  display: block;
  width: 100%;
  font-size: var(--text-6);
}
.set-field-control input[type="password"] { font-family: var(--mono); letter-spacing: .08em; }
.set-field-control textarea {
  min-height: 76px;
  line-height: 1.5;
  resize: vertical;
}
/* A tick list inside a card -- the choices of a multiselect. Boxed off from
   the label above it so a long one does not read as loose text in the card. */
.set-choices {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 6px var(--chip-x-sm);
  background: var(--field);
  border: 1px solid var(--field-border);
  border-radius: var(--radius-sm);
  max-height: 232px;
  overflow: auto;
}
.set-choices .check { padding: 4px 4px; border-radius: var(--radius-sm); }
.set-choices .check:hover { background: var(--surface-cont); }
/* "everything, including anything added later" is the option the others are
   exceptions to, so it is separated from them rather than listed among them. */
.set-choices-wide {
  padding-bottom: 6px;
  margin-bottom: 6px;
  border-bottom: 1px solid var(--rule);
}

/* What is already stored, under the control that would replace it. */
.set-field-foot {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-top: 9px;
}
.set-field-state {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--text-3);
  color: var(--faint);
}
.set-field-state code {
  font-family: var(--mono);
  font-size: var(--text-2);
  color: var(--dim);
}
.set-field-foot .ghost { margin-left: auto; font-size: var(--text-4); padding: 4px 12px; }

/* The line under the level's head: what state this module is in, and what
   that means for anything typed below. */
.set-intro {
  margin: 0 0 var(--space-4);
  font-size: var(--text-5);
  line-height: 1.55;
  color: var(--outline);
}


/* ---------------------------------------------------------------- hints --
   Explanatory prose, folded behind a marker beside the thing it explains.
   Built with `C.hint()` / `C.explain()`; see the comment there for why the
   marker is visible rather than the whole element being hoverable.

   The bubble is a child of the marker rather than a positioned singleton, so
   there is no JavaScript deciding where it goes and nothing to keep in step
   while a pane scrolls. The cost is that it cannot escape an ancestor with
   `overflow: hidden`; the panes it is used in all scroll on the y axis only,
   and the bubble opens upward within them. */
.hint {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  margin-left: 6px;
  border-radius: 50%;
  border: 1px solid var(--edge);
  background: transparent;
  color: var(--dim);
  font-size: var(--text-1);
  font-weight: 600;
  line-height: 1;
  cursor: help;
  vertical-align: middle;
  user-select: none;
}
.hint:hover, .hint:focus-visible {
  border-color: var(--primary);
  color: var(--primary);
  outline: none;
}
.hint-bubble {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 40;
  width: max-content;
  max-width: 320px;
  padding: var(--field-y) var(--field-x);
  border-radius: 8px;
  border: 1px solid var(--edge);
  background: var(--surface-high);
  color: var(--on);
  font-size: var(--text-5);
  font-weight: 400;
  line-height: 1.45;
  text-align: left;
  white-space: normal;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .45);
  /* Not `display: none`: the text stays in the accessibility tree, so a
     screen reader reads it without anyone having to hover. */
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .12s ease;
}
.hint:hover .hint-bubble,
.hint:focus-visible .hint-bubble,
.hint:focus .hint-bubble { opacity: 1; visibility: visible; }

/* Near the top of a scrolling pane there is nothing above to open into, so
   these open downward instead. Set by the caller, not guessed at runtime. */
.hint.hint-below .hint-bubble { bottom: auto; top: calc(100% + 8px); }

/* A marker in the right-hand column has no room to its right, and a centred
   bubble hangs off the edge of the window there. It anchors to the marker's
   right instead. Scoped by where the marker is rather than measured at
   runtime: this layout is fixed, and a JavaScript positioner would be a
   second thing to keep in step with the CSS for no gain. */
.screen-side .hint-bubble { left: auto; right: -4px; transform: none; }

/* --------------------------------------------------------- form controls --
   The base every screen inherits. Before this, `.primary` matched no rule at
   all -- every Save, Copy and Apply in the console was a raw browser button
   sitting in a Material 3 page. */

button.primary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: var(--control-h);
  padding: 0 var(--control-x-lg);
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--primary);
  color: var(--on-primary);
  font: inherit;
  font-size: var(--text-5);
  font-weight: 500;
  cursor: pointer;
  transition: background var(--motion-short) var(--easing),
              opacity var(--motion-short) var(--easing);
}
button.primary:hover { background: var(--on-primary-cont); }
button.primary:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
button.primary:disabled { opacity: .5; cursor: default; }

/* Tonal: the same button at the weight a batch action deserves. `Enable all`
   is affirmative but it is not the page's primary action -- Save is -- so it
   takes the container colour rather than the full primary. */
button.primary.is-tonal { background: var(--secondary-cont); color: var(--on-secondary-cont); }
button.primary.is-tonal:hover { background: var(--surface-est); }

/* Destructive keeps the same shape and changes only the colour, so it reads
   as the same kind of control rather than a different widget. */
button.primary.danger { background: var(--error); color: var(--surface); }
button.primary.danger:hover { background: color-mix(in srgb, var(--error) 80%, white); }

.ghost.danger { color: var(--error); border-color: color-mix(in srgb, var(--error) 40%, transparent); }
.ghost.danger:hover { color: var(--error); border-color: var(--error); }

/* ------------------------------------------------------------- checkbox --
   Drawn rather than accent-coloured, because the native control cannot show
   the third state this console actually uses. `indeterminate` is not
   decoration here: a governance field holding null and one holding false
   resolve differently, and the box has to be able to say which.

   `.switch input` is excluded -- it is a checkbox too, but it is invisible by
   design and its track is the thing you see. */
input[type="checkbox"]:not(.switch input) {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin: 0;
  border: 2px solid var(--outline);
  border-radius: 5px;
  background: transparent;
  cursor: pointer;
  position: relative;
  transition: background var(--motion-short) var(--easing),
              border-color var(--motion-short) var(--easing);
}
input[type="checkbox"]:not(.switch input):hover { border-color: var(--primary); }
input[type="checkbox"]:not(.switch input):focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
input[type="checkbox"]:not(.switch input):checked {
  background: var(--primary);
  border-color: var(--primary);
}
/* The tick: two borders of an empty box, rotated. No image, no font. */
input[type="checkbox"]:not(.switch input):checked::after {
  content: "";
  position: absolute;
  left: 4px;
  top: 1px;
  width: 5px;
  height: 9px;
  border: solid var(--on-primary);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
/* Inherit: a dash, and deliberately not the same colour as checked -- the
   whole point is that it is neither on nor off. */
input[type="checkbox"]:not(.switch input):indeterminate {
  background: var(--surface-est);
  border-color: var(--outline);
}
input[type="checkbox"]:not(.switch input):indeterminate::after {
  content: "";
  position: absolute;
  left: 2px;
  top: 6px;
  width: 10px;
  height: 2px;
  border-radius: 1px;
  background: var(--outline);
}
input[type="checkbox"]:not(.switch input):disabled { opacity: .45; cursor: not-allowed; }

/* ---------------------------------------------------------------- radio --
   Drawn for the same reason the checkbox is: the native one cannot be made to
   agree with this palette, and half a styled form is worse than none. Round
   rather than square, because the difference between "pick one" and "pick any"
   should be legible before the label is read.

   `.segmented` radios are excluded -- they are the same input under a pill,
   and the pill is the thing you see. */
input[type="radio"]:not(.segmented input) {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin: 0;
  border: 2px solid var(--outline);
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  position: relative;
  transition: border-color var(--motion-short) var(--easing);
}
input[type="radio"]:not(.segmented input):hover { border-color: var(--primary); }
input[type="radio"]:not(.segmented input):focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
input[type="radio"]:not(.segmented input):checked { border-color: var(--primary); }
/* The dot scales up from nothing, so turning one on is visibly a change and
   not just a different still frame. */
input[type="radio"]:not(.segmented input)::after {
  content: "";
  position: absolute;
  inset: 2px;
  border-radius: 50%;
  background: var(--primary);
  transform: scale(0);
  transition: transform var(--motion-short) var(--easing);
}
input[type="radio"]:not(.segmented input):checked::after { transform: scale(1); }
input[type="radio"]:not(.segmented input):disabled { opacity: .45; cursor: not-allowed; }

/* ------------------------------------------------------------ segmented --
   Three or four mutually exclusive values, all of them worth reading at once:
   a role floor, a status. A `<select>` hides the alternatives behind a click
   and costs an OS dropdown that no stylesheet reaches; a row of pills shows
   the whole vocabulary and says which one is in force.

   Real radios underneath, one name per group. That is not tidiness -- it is
   what gives the group arrow-key navigation, a single tab stop, and an
   announcement that says "2 of 3" without a line of JavaScript. */
.segmented {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border-radius: var(--radius-pill);
  background: var(--surface-low);
  border: 1px solid var(--edge);
}
.segmented-option { position: relative; display: flex; }
.segmented-option input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  z-index: 1;
}
.segmented-option span {
  display: grid;
  place-items: center;
  height: 26px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-pill);
  font-size: var(--text-3);
  line-height: 1;
  white-space: nowrap;
  color: var(--faint);
  transition: background var(--motion-short) var(--easing),
              color var(--motion-short) var(--easing);
}
.segmented-option:hover span { color: var(--on-variant); }
.segmented-option input:checked + span {
  background: var(--primary-cont);
  color: var(--on-primary-cont);
  font-weight: 500;
}
.segmented-option input:focus-visible + span {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
.segmented-option input:disabled { cursor: not-allowed; }
.segmented-option input:disabled + span { opacity: .45; }
/* The inherit segment is not a value -- it is the absence of one, so it reads
   as quieter than the three real choices even when it is the one in force. */
.segmented-option input:checked + span.is-inherit {
  background: var(--surface-cont);
  color: var(--dim);
}

/* ---------------------------------------------------------- text inputs --
   A base, so a field a screen has not styled still looks like it belongs.
   Per-screen rules override it where they need to. */
/* The `[type=...]` filters go through `:where()` so they add no specificity
   of their own. This is the *default* a field falls back to, and a component
   that has already drawn its own -- the sub-nav's search pill, the palette's
   single underlined line -- has to be able to win with one class.

   It could not. `input[type="search"]` and `.subnav-search input` are both
   (0,1,1), this rule is further down the file, and ties go to the last one:
   every search field in the console was drawing a bordered, 37px-tall box
   *inside* the 32px pill that was supposed to contain it, and the palette's
   underline had a full border round it. Neutralising the attribute is the
   fix that also stops the next component hitting the same wall. */
input:where([type="text"], [type="number"], [type="search"],
            [type="password"], [type="date"]),
select, textarea {
  background: var(--field);
  border: 1px solid var(--field-border);
  border-radius: var(--radius-sm);
  color: var(--on);
  font: inherit;
  font-size: var(--text-7);
  padding: var(--field-y) var(--field-x);
  transition: border-color var(--motion-short) var(--easing),
              box-shadow var(--motion-short) var(--easing);
}
input:where([type="text"], [type="number"], [type="search"],
            [type="password"], [type="date"]):hover,
select:hover, textarea:hover { border-color: var(--field-border-hover); }
select { cursor: pointer; }
textarea { resize: vertical; }
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 1px var(--primary);
}
input::placeholder, textarea::placeholder { color: var(--faint); }
/* `:where()` for the same reason the rule above it uses one: this is the
   default a disabled field falls back to, and it must not out-specify a
   component that has deliberately hidden its own input.

   Two of them do. `.switch input` and `.segmented-option input` are real
   focusable controls stretched over the thing that draws them, held at
   `opacity: 0` -- and `input:disabled` is (0,1,1) like `.switch input`,
   further down the file, so it won the tie and turned the browser's own
   checkbox back on at half opacity. That is the grey square that appeared
   in the middle of the pill on every module waiting on credentials. */
input:where(:disabled), select:where(:disabled), textarea:where(:disabled) {
  opacity: .5;
  cursor: not-allowed;
}

/* A number field beside its own unit -- "$" on the left, "%" on the right --
   has no room for a spinner, and nothing on this screen is nudged one step at
   a time. The keyboard still increments; the arrows just stop taking width
   from a figure that decides how much money a tool can spend. */
.wa-num input[type="number"] { appearance: textfield; -moz-appearance: textfield; }
.wa-num input[type="number"]::-webkit-outer-spin-button,
.wa-num input[type="number"]::-webkit-inner-spin-button {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
}

/* ------------------------------------------------------------- context --
   The curation screen: a review queue of what sessions proposed, then the
   live facts strongest first. One card component serves both, wearing a
   state class for which it is -- `is-proposed`, `is-retired`, `is-editing` --
   rather than three near-identical blocks that drift apart. */

/* The bar above the list is `.pane-bar`, the same one every list screen
   carries. It used to be a bespoke wrapping filter bar, from when the filters
   lived above the list rather than in the sub-nav. */
#context-source.is-warn { color: var(--warn); }

.ctx-add {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 180px auto;
  gap: var(--space-3);
  align-items: end;
  padding: var(--space-4);
  margin-bottom: var(--space-5);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: var(--surface-cont);
}
@media (max-width: 720px) {
  .ctx-add { grid-template-columns: minmax(0, 1fr); }
}

/* ---------------------------------------------------------- the queue -- */

#context-queue-count { color: var(--accent-2); border-color: var(--accent-2-cont); }

/* ---------------------------------------------------------- the cards -- */

/* A memory is a row of the list, not a card floating in it. */
.mem-card { padding: var(--row-y-block) var(--row-x); }
.mem-card + .mem-card { margin-top: var(--space-2); }

/* A proposal is marked on its left edge in the second accent, the same
   colour the queue around it carries -- so a card that has been scrolled away
   from its heading still says which half of the screen it belongs to. */
.mem-card.is-proposed {
  border-color: color-mix(in srgb, var(--accent-2) 40%, transparent);
  box-shadow: inset 3px 0 0 var(--accent-2);
}
/* Retired is dimmed rather than hidden: it is still readable, and the reason
   it was retired is regularly the answer to why something is set up as it is. */
.mem-card.is-retired { opacity: .62; }
.mem-card.is-editing { border-color: var(--primary); }

.mem-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  flex-wrap: wrap;
  font-size: var(--text-3);
  color: var(--dim);
}
.mem-type {
  font-size: var(--text-1);
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--on-secondary-cont);
  background: var(--secondary-cont);
  border-radius: var(--radius-pill);
  padding: var(--chip-y-sm) var(--chip-x-sm);
}
.mem-key, .mem-client { font-family: var(--mono); font-size: var(--text-3); }
.mem-client { color: var(--on-variant); }
/* The weight, in the same quiet monospace as the key and never as stars. It
   counts how many times this has been independently confirmed; the stars are
   what one person thinks of it. Drawing both the same way is how that
   distinction gets lost. */
.mem-weight { font-family: var(--mono); color: var(--on-variant); }
.mem-when { margin-left: auto; }

/* The fact itself, and the largest text on the card. Everything around it is
   metadata about the sentence, so the sentence outranks all of it. */
.mem-body {
  margin: var(--space-2) 0 0;
  font-size: var(--text-8);
  color: var(--on);
  overflow-wrap: anywhere;
}
.mem-retired {
  margin: var(--space-2) 0 0;
  font-size: var(--text-5);
  color: var(--warn);
}

/* What a proposal resembles. Two shades: a plain resemblance is information,
   a clash is somebody being told something that contradicts what is stored. */
.mem-like {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin-top: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--surface-low);
  font-size: var(--text-5);
  color: var(--dim);
  flex-wrap: wrap;
}
.mem-like .icon { --icon-size: var(--icon-size-sm); color: var(--info); }
.mem-like.is-clash { background: color-mix(in srgb, var(--error) 10%, var(--surface-low)); }
.mem-like.is-clash .icon { color: var(--error); }
.mem-like.is-clash .mem-like-why { color: var(--error); }
.mem-like-why { flex: 1 1 200px; }
.mem-like-body { flex: 1 1 100%; color: var(--on-variant); overflow-wrap: anywhere; }

.mem-foot {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-top: var(--space-3);
  flex-wrap: wrap;
}
.mem-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
  flex-wrap: wrap;
}
.mem-actions .ghost, .mem-actions .primary { font-size: var(--text-5); }

.mem-editor {
  display: grid;
  gap: var(--space-3);
  margin-top: var(--space-3);
}
.mem-editor textarea { min-height: 72px; }

/* ----------------------------------------------------------- the stars -- */

/* Five real buttons. The interaction is "click the fourth one", which is
   faster than anything that has to be read first, and each is separately
   focusable so the whole widget is operable from the keyboard.

   The state is in the glyph -- filled against hollow -- and the colour only
   agrees with it. Two purples far enough apart to read at 17px do not exist
   in this palette, and a state carried by hue alone is a state somebody
   cannot see; the activity dot already makes the same argument beside its
   verb. Both colours are text colours rather than one being an absence,
   because an unrated star drawn at the border colour would read as
   decoration rather than as the control it is. */
.mem-stars { display: flex; align-items: center; gap: 2px; }
.mem-star {
  border: 0;
  background: transparent;
  padding: 0 2px;
  font-size: var(--text-11);
  line-height: 1;
  cursor: pointer;
  color: var(--dim);
  transition: color var(--motion-short) var(--easing);
}
.mem-star.is-on { color: var(--primary); }
.mem-star:hover { color: var(--on-primary-cont); }
.mem-star:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.mem-rating-note {
  margin-left: var(--space-2);
  font-size: var(--text-3);
  color: var(--dim);
}

/* ------------------------------------------------------- governance bits --
   The Security screen's controls, now that they are sections of the pages
   they are about rather than a screen of their own. They keep the `sec-`
   prefix: it is what the one implementation of each is called, and renaming
   a class to match where it happens to be placed is exactly the coupling
   dissolving that screen removed. */

/* A rule and its what-if are rows of the group they belong to, so neither
   draws a box of its own -- `.field-rows` already drew one round both. The
   rule's form fills its row; the preview is a `<details>` that is one line
   until somebody wants the layers.

   `.sec-override-new` -- "write a rule for this one", on a client's page --
   is the same disclosure in a section that has no group above it, so it keeps
   an edge. */
.sec-override-new {
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  padding: var(--space-3) var(--space-4);
  margin-top: var(--space-4);
}
.tool-resolve > summary,
.sec-override-new > summary {
  cursor: pointer;
  font-size: var(--text-5);
  color: var(--on-variant);
  list-style: none;
}
.tool-resolve > summary::-webkit-details-marker,
.sec-override-new > summary::-webkit-details-marker { display: none; }
.tool-resolve > summary::before,
.sec-override-new > summary::before {
  content: "▸ ";
  color: var(--faint);
}
.tool-resolve[open] > summary::before,
.sec-override-new[open] > summary::before { content: "▾ "; }
.tool-resolve > summary:focus-visible,
.sec-override-new > summary:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
.tool-resolve[open] > summary,
.sec-override-new[open] > summary { margin-bottom: var(--space-3); }

.sec-preview-result { margin-top: var(--space-3); }
.sec-preview-h { margin: 14px 0 6px; font-size: var(--text-5); color: var(--dim); }

/* An override row is a statement, not a thing to pick: it carries a remove
   button and nothing else responds to a click, so it must not look like the
   tool rows above it that do. */
.tool-row.is-static { cursor: default; }
.tool-row.is-static:hover { background: transparent; }

/* An inline link inside a sentence -- the tools that override a module-wide
   rule, named in the module's own Security section. A button, because it
   navigates within the app rather than to a URL. */
.link-inline {
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  color: var(--accent-2);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.link-inline:hover { color: var(--on); }
.link-inline:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
