/* ~~ codemap ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 * @title       Console Styles
 * @category    ui
 * @plane       control
 * @description The console's entire presentation layer: three deploy knobs
 *              that every surface, ink and line is derived from, the Material
 *              3 token set every screen reads off them, 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, docs/CONSOLE-VOCABULARY.md
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

/* ============================================================================
   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.

     KNOBS     --ds-ground --ds-tint        the three numbers a deploy sets.
               --ds-lines                   Every surface, ink and line below
                                            is derived from them, so the
                                            ground moves without anything
                                            being re-picked by eye
     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)
     SHELL     --shell-ink                  grey text on a shell surface,
                                            where a tinted grey would read
                                            as a colour. The rail and the
                                            sub-nav only
     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;

  /* ============================== THE THREE DEPLOY KNOBS ==================
     A deploy does not pick colours. It picks three numbers, and every
     surface, ink and line below re-derives from them:

       --ds-ground   0 near-black          1 graphite
       --ds-tint     0 true neutral grey   1 tinted towards the primary
       --ds-lines    0 barely there        1 assertive borders

     This replaces a palette of fifty-odd hand-picked hexes, and the reason
     is not tidiness. The hexes could not be moved: darkening the ground
     meant re-picking every line and every grey against it by eye, so in
     practice nobody did, and the shell's neutral axis had to be bolted on
     as four separate tokens (`--neutral-deep`, `--neutral`, `--neutral-high`,
     `--neutral-ink`) rather than turned down out of the palette that was
     already there. Those four are gone; `--ds-tint` is what they were
     asking for, and it reaches the whole file instead of two columns.

     A deploy does not edit this file to change them. The knobs, the tint
     hue and the two accents below are a deployment's own setting, chosen on
     the console's Appearance section or in the MCP_BRAND_* variables, and
     `/dashboard/colours.css` (app/colours.py) repaints them over the values
     written here. What is written here is the default a deployment gets when
     it has chosen nothing: a teal accent on true grey, which is nobody's
     brand. `test_framework.py` §19g fails if these defaults and
     app/colours.py's stop agreeing. Nothing ever writes a component rule.

     The rule that makes the colour pop, and the one thing not to undo: the
     shell is neutral and the content is not. The rail and the sub-nav -- the
     two columns that hold no content, only the way to it -- carry almost no
     chroma at any tint (`--c-shell`), so colour sits inside a frame that has
     none. That is what makes a coloured switch read as coloured rather than
     as a slightly brighter piece of the surface behind it. */
  --ds-ground: 0.25;
  --ds-tint: 0;
  --ds-lines: 0.5;

  /* The hue everything tinted leans on: the primary accent's own hue, which
     app/colours.py reads off whatever primary the deployment chose. Named by
     its job rather than by whose it is. */
  --tint-h: 185.9;
  /* How strongly the codebase chart's brand-family colours carry that hue:
     the primary's chroma against the old violet's. Grey brand, grey chart. */
  --brand-sat: 0.751;

  /* ------------------------------------------------------------ surfaces --
     Derived lightnesses, darkest on the outside. The content pane sits one
     step up from the sub-nav so the page beside the nav reads as lit rather
     than as another column of chrome.

     Five content steps rather than the three the pre-M3 theme had, because
     the layout stacks a rail on a page on a card on a row and three greys
     cannot keep those apart. */
  --l-rail:    calc(0.098 + 0.052 * var(--ds-ground));
  --l-nav:     calc(0.132 + 0.052 * var(--ds-ground));
  --l-pane:    calc(0.146 + 0.050 * var(--ds-ground));
  --l-surface: calc(0.162 + 0.050 * var(--ds-ground));
  --l-low:     calc(0.202 + 0.050 * var(--ds-ground));
  --l-cont:    calc(0.242 + 0.048 * var(--ds-ground));
  --l-high:    calc(0.300 + 0.046 * var(--ds-ground));
  --l-est:     calc(0.368 + 0.044 * var(--ds-ground));

  /* Chroma, in three strengths. The shell carries almost none of it whatever
     the tint, because the shell is the thing an accent is read against. */
  --c-shell:   calc(0.006 * var(--ds-tint));
  --c-surface: calc(0.020 * var(--ds-tint));
  --c-ink:     calc(0.016 * var(--ds-tint));

  --surface:       oklch(var(--l-surface) var(--c-surface) var(--tint-h));
  --surface-low:   oklch(var(--l-low)     var(--c-surface) var(--tint-h));
  --surface-cont:  oklch(var(--l-cont)    var(--c-surface) var(--tint-h));
  --surface-high:  oklch(var(--l-high)    var(--c-surface) var(--tint-h));
  --surface-est:   oklch(var(--l-est)     var(--c-surface) var(--tint-h));

  /* ------------------------------------------------------ shell surfaces --
     The four columns of every screen. The rail and the sub-nav are on
     `--c-shell` and are therefore neutral by construction rather than by a
     separate set of tokens somebody has to remember to keep in step.

     The content pane is one surface at every depth. It carried a lighter
     step 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:  oklch(var(--l-rail) var(--c-shell)   var(--tint-h));
  --surface-nav:   oklch(var(--l-nav)  var(--c-shell)   var(--tint-h));
  --surface-pane:  oklch(var(--l-pane) var(--c-surface) var(--tint-h));

  /* Grey text on a shell surface, where a violet-tinted grey reads as a faint
     *colour* against a neutral ground rather than as quiet text. The shell's
     own ink, and nothing outside it may ask: a component outside the rail or
     the sub-nav reaching for this is claiming to be part of the shell, which
     is the same rule `--hairline` already carries.

     Deliberately not a second ink ramp. `--dim`, `--faint` and `--fainter`
     stay the console's greys everywhere content is. */
  --shell-ink: oklch(calc(0.60 + 0.04 * var(--ds-ground)) var(--c-shell) var(--tint-h));

  /* ---------------------------------------------------------------- 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.
                     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. */
  --l-line: calc(0.198 + 0.072 * var(--ds-lines));
  --hairline: oklch(calc(var(--l-line) - 0.022) var(--c-shell)   var(--tint-h));
  --rule:     oklch(var(--l-line)               var(--c-surface) var(--tint-h));
  --edge:     oklch(calc(var(--l-line) + 0.048) var(--c-surface) var(--tint-h));

  /* --------------------------------------------------------------- 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:              oklch(calc(var(--l-surface) - 0.018) var(--c-surface) var(--tint-h));
  --field-border:       oklch(calc(0.330 + 0.03 * var(--ds-ground)) calc(0.014 * var(--ds-tint)) var(--tint-h));
  --field-border-hover: oklch(calc(0.430 + 0.03 * var(--ds-ground)) calc(0.020 * var(--ds-tint)) var(--tint-h));
  --field-y: 7px;
  --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   (the deployment's text colour)
       `--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.

     Every rung here is a rung something is *read* at, so every rung clears
     4.5:1 on the surfaces it is painted on -- `--surface` and `--surface-low`
     both, since the quiet rungs get used inside cards. That is a repair, not
     a restatement: `--fainter` was #6f6183 and measured 3.48:1 on `--surface`
     and 3.3:1 on `--surface-low`, which made the quietest step decorative in
     fact while this list called it text -- and it carries the section counts
     and the sentence under every settings label. It measures 4.9 and 4.53 now.

     Measured, not eyeballed, and the tightest pair is `--fainter` on
     `--surface-low` at 4.53:1: moving `--ds-ground` up lightens both and
     moving it down darkens both, so the ramp holds across the knob's range,
     but a rung edited by hand needs re-measuring against both grounds. The
     top of the ramp moved with the bottom to keep six distinct rungs; the
     bottom two are 0.02 apart, which is the least that still reads as two. */
  /* The top two rungs are a deployment's setting (Text colour), derived by
     app/colours.py like the accents. The console launched with a cream here,
     which was one agency's brand; the default is a plain off-white. */
  --on: #f2f2f2;
  --on-variant: #e6e6e6;
  --outline:    oklch(0.820 var(--c-ink) var(--tint-h));
  --dim:        oklch(0.705 var(--c-ink) var(--tint-h));
  --faint:      oklch(0.625 var(--c-ink) var(--tint-h));
  --fainter:    oklch(0.605 var(--c-ink) var(--tint-h));

  /* ------------------------------------------------------------- primary --
     The deployment's accent. `--primary` is the switch, the active rail
     item, the focus ring, the page's own action. Every screen follows these
     six lines, which is the one styling rule stated as a test.

     Only `--primary` is chosen. The other five are derived from it by
     app/colours.py, each at a fixed lightness, which is what keeps every
     pairing clear of 4.5:1 whatever hue a deployment picks. The lightnesses
     are the ones the hand-picked violet palette used, where
     `--primary-cont` was a brand guide's deep purple at 5.98:1 under
     `--on-primary-cont`. It lands on the rail's active pill and the agency
     badge. */
  --primary: #7fc8bf;
  --on-primary: #042320;
  --primary-cont: #00625b;
  --on-primary-cont: #cae7e3;
  --secondary-cont: #003b36;
  --on-secondary-cont: #a9dad3;

  /* ------------------------------------------------------------- accent-2
     A second hue, because one is not a palette. It has to sit far enough
     round the wheel from the primary to read as a different *kind* of thing
     rather than a lighter shade of it. The default is sage. Like the
     primary it is a deployment's choice, and its container and ink are
     derived from it.

     It carries one meaning, consistently: **this is the thing you are
     looking at or editing** -- the selected row, the last crumb, a field
     being edited, a section head about what a thing *is*. Violet stays "this
     is on, this is the action". Keeping the two jobs apart is the whole
     reason for a second hue; a third accent, or either of these used because
     a section looked plain, spends the only two meanings the console has. */
  --accent-2: #babf95;
  --accent-2-cont: #393b28;
  --on-accent-2-cont: #dee2c2;

  /* --------------------------------------------------------------- state --
     Three, not four: there is no "success" here. A thing that worked says so
     by looking ordinary. Where "this is live" genuinely must be stated, sage
     says it. */
  --info: #9dc3de;
  --error: #f0a8ac;
  --warn: #e2b67f;

  /* ------------------------------------------------------------- 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.

     Pulled to the density the Modules list already read at, rather than the
     Modules list being loosened to match the settings pages: the list is the
     thing this console is mostly looking at. */
  --row-y: 8px;
  --row-x: 13px;
  --row-y-block: 11px;

  /* ------------------------------------------------------------ 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: 2px;
  --chip-x: 9px;
  --chip-y-sm: 1px;
  --chip-x-sm: 7px;

  /* ---------------------------------------------------------- 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: 18px;
  --pane-y: 14px;
  --pane-x-side: 15px;

  --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;

  /* ----------------------------------------------------------------- type --
     Two faces, both self-hosted above, and the reason is in that @font-face
     block: a console that reaches fonts.googleapis.com renders in a fallback
     on a locked-down network.

     `--font-display` is the seam for the brand's titular face. The design
     system names Ubuntu for anything titular and Noto Sans for prose; both
     arrive over Google Fonts, and a face that resolves only by luck of a
     local install is worse than one face everywhere -- two admins would see
     two consoles. So the seam exists and points at the face we actually
     ship. Landing Ubuntu is three things: a woff2 beside the other two, an
     @font-face above, and its name at the front of this line. */
  --font-display: "DM Sans", ui-sans-serif, system-ui, sans-serif;
  --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.

     That family used to be one agency's violet, written as hexes. It is the
     deployment's primary hue now (`--tint-h`), each colour keeping the
     lightness and chroma it had, so the graph wears whichever brand the
     console does. The literals left are the fixed meanings -- blue, amber,
     pink, magenta, green -- which are the same on every deployment and are
     listed in test_framework.py §19g. */
  --cat-entrypoint: var(--on);
  --cat-core: oklch(0.750 calc(0.099 * var(--brand-sat)) var(--tint-h));
  --cat-module: oklch(0.709 calc(0.159 * var(--brand-sat)) var(--tint-h));
  --cat-tool: #9dc3de;
  --cat-support: #e2b67f;
  --cat-ui: #e08fc0;
  --cat-table: oklch(0.624 calc(0.063 * var(--brand-sat)) var(--tint-h));
  --cat-tooling: oklch(0.812 calc(0.081 * var(--brand-sat)) var(--tint-h));
  --cat-config: oklch(0.663 calc(0.056 * var(--brand-sat)) var(--tint-h));
  --cat-docs: oklch(0.573 calc(0.052 * var(--brand-sat)) var(--tint-h));
  --cat-legacy: oklch(0.518 calc(0.048 * var(--brand-sat)) var(--tint-h));
  --cat-unmapped: oklch(0.409 calc(0.041 * var(--brand-sat)) var(--tint-h));
  --cat-schema: oklch(0.733 calc(0.090 * var(--brand-sat)) var(--tint-h));
  --cat-database: oklch(0.778 calc(0.104 * var(--brand-sat)) var(--tint-h));
  --cat-service: #f0a8ac;

  /* 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: #9dc3de;
  --plane-data: #a8c7b5;
  --plane-shared: oklch(0.718 calc(0.046 * var(--brand-sat)) var(--tint-h));

  /* Direction colours for the satellites in services mode: who calls whom. */
  --dir-inbound: #9dc3de;
  --dir-outbound: oklch(0.750 calc(0.099 * var(--brand-sat)) var(--tint-h));
  --dir-bidirectional: #e2b67f;

  /* ---------------------------------------------------------------- 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: #e2b67f;
  --tag-destructive: #f0a8ac;
  --tag-pii: #9dc3de;
  --tag-beta: #e08fc0;
  --tag-bulk: oklch(0.718 calc(0.046 * var(--brand-sat)) var(--tint-h));

  /* 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.5;
  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: 9px; height: 9px; }
::-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; }

/* 500 rather than 600, which is what every *named* title in this console
   already uses -- `.level-title`, `.side-title`, `.pane-bar-title` and the
   topbar's own h1, which overrode this base rule to get there. The base was
   the outlier, and the two bare <h3>s left in the markup were the only things
   reading heavier than the headings beside them. */
h1, h2, h3 { margin: 0; font-family: var(--font-display); font-weight: 500; 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 var(--space-2); }
code { font-family: var(--mono); font-size: 0.92em; color: var(--outline); }
.muted { color: var(--dim); }
.warn-ink { color: var(--warn); }
.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: 68px 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;
  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: 36px;
  height: 36px;
  flex: 0 0 auto;
  margin-bottom: 13px;
  border-radius: var(--radius);
  background: var(--primary-cont);
  color: var(--on-primary-cont);
}
.rail-brand-mark { display: grid; place-items: center; }

.rail-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  width: 58px;
  flex: 0 0 auto;
  padding: 4px 0 2px;
  background: transparent;
  border: 0;
  /* Grey, on a ground that is now actually black. `--dim` is a violet grey
     and reads as a faint colour against a neutral surface rather than as
     quiet text -- which is the same swap the sub-nav's labels make, and the
     whole reason the neutral axis carries an ink of its own. */
  color: var(--shell-ink);
  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: 50px;
  height: 29px;
  border-radius: var(--radius-pill);
  color: var(--shell-ink);
  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: 38px;
  height: 38px;
  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: 30px;
  height: 30px;
  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 56px;
  padding: 0 var(--pane-x) 0 22px;
  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;
}

/* The save cluster, at the right of every header.

   Three items that were three different heights: a bare span with no control
   height at all, a Discard button at `--control-h`, and Save at
   `--control-h-lg`. Read left to right that is a stepped line rather than a
   cluster. They share the tallest of the three now, so the state reads as
   part of the control group it describes rather than as text that happens to
   sit beside it.

   The other half of the unevenness was motion: Discard is only there while
   something is staged, and it was `hidden`, so the whole cluster changed
   width as you moved between screens and the Save button slid left. Its slot
   is reserved instead -- see `.is-reserved` -- and nothing in this corner
   moves any more. */
.topbar-right {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-left: auto;
  flex: 0 0 auto;
}
.topbar-right .ghost { height: var(--control-h-lg); }

/* Present, and holding its space, but neither seen nor reachable: hidden this
   way an element keeps its box, and `visibility` takes it out of the tab
   order and the accessibility tree exactly as `hidden` would. */
.is-reserved { visibility: hidden; }

/* 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 {
  display: inline-flex;
  align-items: center;
  height: var(--control-h-lg);
  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: .45; 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 216px;
  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) 9px var(--space-4);
}
/* ------------------------------------------------------ the column's foot --
   Where a SLICE'S ACTIONS live. "Enable shown" and "Disable shown" act on
   whatever this column narrowed to, so they sit under the filters that built
   it -- and the count on the group's own label is what says how many the
   gesture will reach.

   Not on the list's caption bar. The caption bar *describes* what is on
   screen, and an action there reads as "enable this list" instead of "enable
   what I just narrowed to", which are different sets the moment a filter is
   on. So no bulk action goes on `.pane-bar`. Other buttons still sit there
   on some screens -- Refresh, Export, the Memories screen's Add -- and none
   of those acts on a filtered set. */
.subnav-foot {
  flex: 0 0 auto;
  padding: var(--space-2) 9px var(--space-3);
  border-top: 1px solid var(--hairline);
}

/* An action row, not a filter: it does something rather than narrowing the
   list. The glyph is what says so, and it is the only thing in this column
   that takes an ink brighter than the shell's. */
.subnav-item.is-act { color: var(--dim); }
.subnav-item.is-act:hover { color: var(--on); }
.subnav-item.is-act > [data-icon] { color: var(--primary); }
.subnav-item.is-act.is-off > [data-icon] { color: var(--faint); }
.subnav-item.is-act > [data-icon] { display: grid; place-items: center; flex: 0 0 auto; }
.subnav-item.is-act > [data-icon] svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }
/* Nothing on screen it could reach, or a token that may not switch things.
   No hover either: a row that lights up under the pointer is offering
   itself. */
.subnav-item.is-act:disabled,
.subnav-item.is-act:disabled:hover { color: var(--faint); background: transparent; cursor: default; }
/* The group's label carries the count the gesture reaches, at the right edge
   where every filter row above it keeps its own count. Full opacity on the
   number: the title already dims to .72, and the rule below would dim the
   count again inside it. */
.subnav-foot .subnav-title {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
}
.subnav-foot .subnav-title .subnav-n { opacity: 1; }

.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: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--shell-ink);
}

.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(--shell-ink);
  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(--shell-ink);
}
.subnav-item.is-on .subnav-n { color: var(--on-secondary-cont); }
/* Scoped to the column, because `.subnav-n` is borrowed by the right pane's
   side labels for the same "quiet count" job -- and the right pane is
   content, which stays violet. The ancestor is what says "this one is in the
   shell". */
.subnav .subnav-title,
.subnav .subnav-n { color: var(--shell-ink); opacity: .72; }

/* 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: 13px;
  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; }
/* Whatever the level carries on its right: a gear, a state chip, the way
   out. Never a bulk action -- those belong under the filters that built
   the slice, in `.subnav-foot`. */
.level-head-aside { display: flex; align-items: center; gap: var(--space-2); flex: 0 0 auto; }
.level-title { font-family: var(--font-display); font-size: var(--text-11); font-weight: 500; }
.level-slug {
  font-family: var(--mono);
  font-size: var(--text-3);
  color: var(--faint);
  margin-top: 2px;
  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 352px;
  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-family: var(--font-display); 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: sage for what this thing *is*, violet for what you can change,
   blue for what the model gets told. */
.side-label {
  padding: var(--space-4) 0 5px;
  font-size: var(--text-2);
  font-weight: 600;
  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: 5px 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;
}

/* A set of names read as a set, under its own `.side-label`: the toolsets a
   module loaded, and nothing else today.

   Not a `.side-stat`. A stat is a key on the left and its value on the right,
   and fifteen identifiers in that right-hand column wrap under the key and
   push it out of line. This has the whole width, and the heading above it
   does the job the key was doing. Monospace like a stat's value, because what
   is in it is identifiers. */
.side-list {
  padding: 5px 0;
  border-bottom: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: var(--text-4);
  line-height: 1.6;
  color: var(--on);
  overflow-wrap: anywhere;
}

/* A name and the sentence about it, stacked. The `.side-param` shape one
   section down, for a different pair: there it is an argument and what it is
   for, here it is something that did not load and the reason given.

   Stacked rather than a `.side-stat`, for the reason that shape exists at
   all: a stat holds its value in the right-hand column on one line, and a
   reason is a sentence. */
.side-entry {
  padding: 5px 0;
  border-bottom: 1px solid var(--rule);
}
.side-entry-k {
  font-family: var(--mono);
  font-size: var(--text-4);
  color: var(--on);
  overflow-wrap: anywhere;
}
.side-entry-v {
  font-size: var(--text-3);
  line-height: 1.45;
  color: var(--dim);
  margin-top: 1px;
  overflow-wrap: anywhere;
}

/* 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: 5px;
}
.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.5;
  color: var(--outline);
  white-space: pre-wrap;
}
.side-quote-foot {
  margin-top: 9px;
  padding-top: 9px;
  border-top: 1px solid var(--rule);
  font-size: var(--text-3);
  color: var(--faint);
}

/* The gestures at the foot of a right pane -- the ones that take the thing
   away. A row rather than a stack, because there are at most two of them and
   the order they read in is the order they have to happen in.

   Each carries a hint marker beside it holding what used to be a paragraph
   under it. They wrap: the pane is narrow, and two buttons and two markers
   do not always fit on one line. */
.side-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-2);
}

/* 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.45;
  color: var(--dim);
  margin-top: 2px;
  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. */

/* ------------------------------------------------- a label with an action --
   The inset moves from the label to the row, so the button sits on the
   label's own line rather than pushing it down. The one place a control is
   allowed in this column, and it is not really a control: it is the way in
   to the briefing, which opens in the *content* pane at the width the prose
   will be read at -- never in a 352px column and never in a dialog. */
.side-label-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-4) 0 5px;
}
.side-label-row > .side-label { flex: 1 1 auto; padding: 0; }

/* ----------------------------------------------------------- description --
   What a thing IS, in the console's own words, for the admin configuring it.
   Unboxed, directly under the overview label, because boxing it would say it
   came from somewhere else -- which is exactly what `.side-quote` says, and
   why the two are not two versions of one thing. The description is part of
   the overview; the briefing is sent. */
.side-desc {
  margin: 0 0 var(--space-3);
  font-size: var(--text-5);
  /* 1.5, with `body` and every other block of prose. This rule was written
     before the density pass moved them and was the one it did not reach. */
  line-height: 1.5;
  color: var(--outline);
}

/* ----------------------------------------------------------------- tiles --
   A FIGURE somebody scans for and compares: "168 write tools", "1 not
   loaded". A tile costs more space than a line of text and earns it by being
   compared with the tile beside it -- so a fact that is read once and never
   compared is a `.side-stat`, not a tile. A tile round a sentence is a card
   with a sentence in it. */
.side-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: var(--space-2);
}
.side-tile {
  padding: 9px 11px;
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  background: var(--surface);
}
.side-tile-v {
  font-family: var(--font-display);
  font-size: var(--text-12);
  font-weight: 500;
  letter-spacing: -.02em;
  color: var(--on);
  /* Figures in a row of tiles are compared down the column, and proportional
     digits make two four-digit numbers different widths. */
  font-variant-numeric: tabular-nums;
}
.side-tile-k { margin-top: 1px; font-size: var(--text-3); color: var(--dim); }
/* Figures, then facts: a stat straight after the tiles carries no top rule
   of its own, so without this its text sits against the tiles' bottom edge.
   A label after them needs nothing -- it brings its own top inset. */
.side-tiles + .side-stat { margin-top: var(--space-2); }

/* The one state a figure carries. Amber is "this is wrong", so it is the
   border and the figure together -- a tile whose number is fine does not
   get a colour for being a number. */
.side-tile.is-warn { border-color: color-mix(in srgb, var(--warn) 34%, transparent); }
.side-tile.is-warn .side-tile-v { color: var(--warn); }

/* ---------------------------------------------------------- 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.5;
  color: var(--outline);
  background: var(--surface-low);
}
.banner strong { color: var(--on-variant); font-weight: 600; }
.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); }

/* --------------------------------------------------------------- snackbar -- */

/* 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; }

.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-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 snackbar:
   a snackbar 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: 200px; }
/* 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);
}

/* A rule at rest: a line per field, the name on the left and what it holds on
   the right. The form that edits all three is in `.sec-edit` and is not on
   screen until the control in the section head is pressed -- see `editor` in
   console-policy.js for why it is one control for the whole section rather
   than one per field. */
.sec-form.is-editing .sec-rest { display: none; }
.sec-form:not(.is-editing) .sec-edit { display: none; }
.sec-rest-v {
  font-family: var(--mono);
  font-size: var(--text-4);
  color: var(--on);
}
.sec-rest-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: 6px;
}
/* The added context, quoted rather than typed into. Same treatment as a side
   quote, because it is the same kind of thing: prose this console did not
   write, shown as the model will read it. */
.sec-rest-quote {
  margin: 0;
  font-size: var(--text-5);
  line-height: 1.5;
  color: var(--outline);
  white-space: pre-wrap;
}
/* A section head that carries a control keeps it at the right, away from the
   label and its count. */
.level-section-head.has-action > :last-child { margin-left: auto; }

.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: 42px;
  height: 24px;
  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 violet from
   grey. */
.switch .slider::before {
  content: "";
  position: absolute;
  width: 13px;
  height: 13px;
  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: 17px;
  height: 17px;
  left: 19px;
  background: var(--on-primary);
}

/* On a row that is selected, the switch takes the sage. Sage 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 violet from olive 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: 33px; height: 19px; }
.switch.switch-sm .slider { border-width: 2px; }
.switch.switch-sm .slider::before { width: 9px; height: 9px; left: 3px; }
.switch.switch-sm input:checked + .slider::before {
  width: 13px; height: 13px; left: 15px;
}

/* ------------------------------------------------------------------ chip --
   One shape, several meanings, separated by a modifier rather than by a set
   of near-identical rules. `.chip-grant` is a token's access; `.chip-env` is a
   variable name; `.chip-facet` is what kind of thing a module is;
   `.chip-warn` is a state worth stopping on. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  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); }
/* A classification, not a state: quieter than `.chip-env`, which names
   something an operator has to go and fix. */
.chip-facet { color: var(--dim); font-style: italic; }
.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. Sage 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); }

/* The agency's badge. Violet-FILLED rather than outlined, which is the one
   place a chip is filled: it marks a row that is a different *kind* of thing
   from the rows under it, and an outline would read as one more tag. */
.chip-agency {
  color: var(--on-primary-cont);
  background: var(--primary-cont);
  border-color: transparent;
  font-weight: 600;
  letter-spacing: .05em;
  text-transform: uppercase;
  font-size: var(--text-1);
  padding: var(--chip-y-sm) var(--chip-x-sm);
}

/* The tiny uppercase mark that says what a row IS without competing with its
   name. `is-write` is the only state it carries, because "this one writes"
   is the only thing about a kind worth stopping on. */
.chip-kind {
  font-size: var(--text-1);
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--dim);
  padding: var(--chip-y-sm) var(--chip-x-sm);
}
.chip-kind.is-write { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 30%, 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: 13px;
  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; }

/* Sage 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: 1px;
  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: 600; 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: 3px; }
.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: 72px;
  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); }

/* ------------------------------------------------------- the pinned row --
   The account above the accounts. An agency is the FIRST ROW OF THE SAME
   LIST, not a card above it -- it is one of these things, and a separate
   card would say it was a different screen with its own rules.

   Four marks, and they are all geometry rather than copy: a violet left edge
   (the list's only one), a filled `.chip-agency`, a rule under it heavy
   enough to say everything below is inside it, and `.row.is-child` inset on
   the rows that belong to it. Its gear opens its configuration exactly as
   theirs do.

   Nothing in `app/` names a module to get this: a collection declares which
   other collection its rows hang off (`registry.Parent`) and the console
   works out which rows end up under which. It was going to be a `pinned`
   boolean, which describes one special row; several parents each with
   children is a relationship, and a boolean cannot hold one. */
.row.is-pinned {
  background: color-mix(in srgb, var(--primary) 6%, transparent);
  box-shadow: inset 3px 0 0 var(--primary);
  border-bottom: 1px solid var(--edge);
  padding-top: var(--row-y-block);
  padding-bottom: var(--row-y-block);
}
.row.is-pinned:hover { background: color-mix(in srgb, var(--primary) 10%, transparent); }
.row.is-pinned .row-mark { color: var(--primary); }
.row.is-pinned .row-name { font-size: var(--text-8); }

/* Its children, inset so the hierarchy is in the geometry and not only in
   the copy. Still an ordinary row in every other respect -- the inset is the
   whole of what makes it a child. */
.row.is-child { padding-left: calc(var(--row-x) + 14px); }
.row.is-child .row-mark { color: var(--dim); }
.row.is-child.is-selected .row-mark { color: var(--accent-2); }

/* --------------------------------------------------------- the add row --
   THE ONE ADD GESTURE, everywhere a list can be added to: sub-accounts,
   tokens, seats, accounts, rules. A quiet trailing row inside the group's
   own border, because it IS the next row of the list -- which is what makes
   "one more of these" read as ordinary rather than as a mode you entered.

   At rest a dashed ghost; clicked, it becomes the field the module asked for,
   in place, and Enter adds. It takes ONE value -- the id the module
   validates -- then goes straight into the new row's own level, because the
   credential is the next thing they were going to type. New rows arrive off.

   What this replaced: a tonal button in a pane bar, a ghost button wired to
   a window.prompt, a ghost button beside a text box inside a level, and on
   the roster nothing at all. Four looks for one act were four things to
   learn, and the fourth taught people the act was impossible. There is no
   composer above the list, no three fields at once, no bulk paste box and no
   dialog. */
.add-row {
  display: flex;
  align-items: center;
  min-height: 38px;
  padding: 6px var(--row-x);
  border-top: 1px dashed var(--edge);
  background: color-mix(in srgb, var(--surface-low) 50%, transparent);
}
.add-row-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: var(--control-h-sm);
  padding: 0 var(--control-x-sm) 0 4px;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--faint);
  font: inherit;
  font-size: var(--text-5);
  cursor: pointer;
  transition: color var(--motion-short) var(--easing);
}
.add-row-trigger .icon { --icon-size: var(--icon-size-sm); }
/* The whole row is the hover target, not just the words in it. */
.add-row:hover .add-row-trigger { color: var(--primary); }

.add-row-form { display: none; align-items: center; gap: var(--space-2); width: 100%; }
.add-row-form input {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--mono);
  font-size: var(--text-5);
}
.add-row-hint { font-size: var(--text-3); color: var(--faint); white-space: nowrap; }

/* Open, the dashed ghost becomes a solid violet edge: the row is now the
   thing being typed into, and violet is what "this is the action" means. */
.add-row.is-open {
  background: var(--surface-low);
  border-top-style: solid;
  border-top-color: var(--primary);
}
.add-row.is-open .add-row-trigger { display: none; }
.add-row.is-open .add-row-form { display: flex; }
.add-row[hidden] { display: none; }
.add-row-trigger:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

/* The row above an add row gives up its own rule: the add row's dashed edge
   is the line between them, and two lines a pixel apart read as a gap. An add
   row with nothing above it is the list's first row, so the box's own border
   is its top edge. Built by `addRow` in console-core.js. */
.row:has(+ .add-row),
.field-row:has(+ .add-row) { border-bottom: 0; }
.add-row:first-child:not(.is-open) { border-top: 0; }

/* ------------------------------------------------------------- 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. */
.snackbar {
  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;
}
.snackbar.is-error { border-color: var(--error); color: var(--error); }
.snackbar-text { flex: 1 1 auto; min-width: 0; }
.snackbar-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;
}
.snackbar-undo:hover { background: color-mix(in srgb, var(--primary) 16%, transparent); }
.snackbar-undo:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.snackbar-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-family: var(--font-display); font-size: var(--text-10); font-weight: 500; }
.pane-bar-sub { font-size: var(--text-4); color: var(--faint); margin-top: 1px; }
/* 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; }

/* ---------------------------------------------------- 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: 18px; }
.cat-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0 2px 7px;
}
.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: 600;
  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); }
/* Between two sections that are both on screen, and nowhere else.

   It used to be `+` with a second rule zeroing the gap after a hidden
   sibling, which gets the common case right and the actual case wrong: a
   module's level has five of these and usually shows two, so the gap was
   being cancelled by the hidden blocks between them and Settings sat flush
   against Tools with nothing to say they were different sections. `~` with
   `:not([hidden])` on both ends asks the question the rule was always trying
   to ask -- is there a visible section above this one -- instead of asking
   about whichever sibling happens to be adjacent in the markup. */
.level-block:not([hidden]) ~ .level-block:not([hidden]) { margin-top: 26px; }
/* ------------------------------------------------------ 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;
}
/* The gap after a section's box, when the next section's head follows it
   inside the same block -- a module's two record collections, where the
   Agencies list is followed straight away by the Sub-accounts head. The three
   boxes above are one vocabulary, so the space after them is one rule too;
   naming only `.field-rows` here let a `.row-list` sit flush against the next
   heading, which read as one long list with a label in the middle of it. */
.tool-rows + .level-section-head,
.row-list + .level-section-head,
.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: 16px;
  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: 1px;
  max-width: 66ch;
}
/* 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; }
/* A block row's contents bring no margin of their own: the row's inset is
   the spacing, and a child with a margin on top of it reads as a second,
   wider row. */
.field-row.is-block > .scope-grid { margin-top: 10px; }

/* ------------------------------------------------------- a grouped row --
   One of exactly two things that earn an edit gesture, and it earns it
   because the act is not "change a value": a sub-account is a name, a token,
   a switch, a note and fifteen grants, which read better on their own than
   scattered among the module's own fields. The row summarises; the gear
   opens its level. The other is a sealed credential (`.sec-field`), where
   the act is *replace* -- the console cannot read it back.

   Everything else on a settings page is live. A folded control is a control
   somebody has to discover, and nothing is folded here to tidy the page. */
.field-row.is-grouped { cursor: pointer; transition: background var(--motion-short) var(--easing); }
.field-row.is-grouped:hover { background: var(--surface-low); }

/* --------------------------------------------------------------- scopes --
   A tick list laid across the width rather than down a column: fifteen
   grants in a column is a scroll, and the thing being read is which of them
   are on, which is a shape rather than a list. The checkbox is real and
   hidden -- the chip is its label, so a click, a tab and a space all work
   without one line of script. */
.scope-grid { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.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);
  color: var(--dim);
  cursor: pointer;
  user-select: none;
  transition: color var(--motion-short) var(--easing),
              border-color var(--motion-short) var(--easing);
}
.scope input { position: absolute; opacity: 0; width: 0; height: 0; }
/* The tick takes its space back rather than appearing beside the word and
   shoving it: the chip's width should not change when it is ticked. */
.scope .icon { --icon-size: 14px; opacity: 0; margin-left: -4px; transition: opacity var(--motion-short) var(--easing); }
.scope:has(input:checked) { border-color: var(--primary); background: var(--secondary-cont); color: var(--on-secondary-cont); }
.scope:has(input:checked) .icon { opacity: 1; margin-left: 0; }
.scope:has(input:focus-visible) { outline: 2px solid var(--primary); outline-offset: 1px; }
/* A grant somebody cannot change here -- a named module while `*` is ticked.
   The same fade every other disabled control in the file takes. */
.scope:has(input:disabled) { opacity: .45; cursor: not-allowed; }

/* ----------------------------------------------------- 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-family: var(--font-display);
  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; }

/* One module's part of the briefing: who it is, whether it is reaching
   anybody, and the box that changes it. A line of chips over a quote over a
   form -- three things stacked inside one row of the group, which is what
   `.field-row.is-block` exists for. */
.dash-brief-head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 8px;
}
.dash-brief-name {
  font-family: var(--mono);
  font-size: var(--text-4);
  color: var(--on-variant);
}
/* The pills and the sentence that says what the chosen one does. Stacked,
   because the sentence changes as the choice changes and a reader who has to
   look sideways for it will not. */
.brief-mode {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: var(--space-3);
}
.brief-mode p { margin: 0; max-width: 62ch; }
/* The quote and the buttons under it are siblings inside one row, so the
   buttons need the air between them that a bordered group would otherwise
   have provided. */
#dash-briefing-parts .sec-actions,
#dash-briefing-orphans .sec-actions { margin-top: var(--space-3); }
/* The line that says a tool name in the box is not a tool here. Amber rather
   than red: it is a warning about somebody's prose, not a refusal of it. */
.brief-warn { color: var(--warn); max-width: 62ch; }

/* ------------------------------------------------------------ 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-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. Sage 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. */
.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) 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 -- sage, 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);
}

/* The label line, and -- for a field that already holds something -- the
   value and its edit control on the same line. Two children of a flex row:
   the head takes what it needs, the resting value takes the rest and pushes
   its button to the right edge.

   `margin-bottom` moved off the head and onto the row, because at rest there
   is nothing under it to be spaced away from. */
.set-field-top {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: 8px;
}
.set-field-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  flex: 0 1 auto;
  min-width: 0;
}
/* The chips stop being pushed to the far right of the card: at rest the far
   right belongs to the edit button, and a row of chips floating between the
   value and that button reads as a caption for the wrong thing. */
.set-field-top .set-field-tags { margin-left: var(--space-2); }

.set-field-rest {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1 1 auto;
  min-width: 0;
  justify-content: flex-end;
}
.set-field-rest [data-icon], .set-field-rest svg {
  flex: 0 0 auto;
  color: var(--faint);
}
.set-field-now {
  overflow: hidden;
  font-family: var(--mono);
  font-size: var(--text-4);
  color: var(--dim);
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Folded away until the edit control is pressed. The control and the foot
   are built either way -- see `fieldCard` -- so this is the whole of what
   "at rest" means to the page. */
.set-field:not(.is-editing) .set-field-control,
.set-field:not(.is-editing) .set-field-foot { display: none; }
.set-field.is-editing .set-field-rest { display: none; }
.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; }

/* The console's colours (console-colours.js): a native picker beside the hex
   box, and the colour itself on the resting line. The swatch's colour is the
   one value a script hands in, through `--swatch`, so the rule is still here. */
.set-field-control:has(.colour-pick) {
  display: flex;
  gap: var(--space-2);
  align-items: stretch;
}
.set-field-control .colour-pick {
  flex: 0 0 auto;
  width: var(--control-h);
  height: var(--control-h);
  padding: 2px;
  border: 1px solid var(--field-border);
  border-radius: var(--radius-sm);
  background: var(--field);
  cursor: pointer;
}
.set-field-control .colour-pick + input { flex: 1 1 auto; font-family: var(--mono); }
/* codeweb.js resolves a colour token to a real colour on this, because a
   canvas cannot draw `var(...)` or read a custom property's computed value.
   Never seen. */
.colour-probe {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
  pointer-events: none;
  color: var(--probe);
}
.colour-swatch {
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  border-radius: 4px;
  border: 1px solid var(--edge);
  background: var(--swatch);
}
.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. */
/* A multiselect: "everything, including anything added later" on its own
   line, then the choices as a scope grid. The one option the others are
   exceptions to sits above them rather than among them. */
.set-scopes-all { margin-bottom: var(--space-3); }

/* What is already stored, under the control that would replace it. Shown by
   default and hidden at rest by the `:not(.is-editing)` rule above. The design
   system writes the same behaviour the other way round -- base `none` plus an
   `.is-editing` rule -- so its `display` value reads as a difference and is
   not one. Taking it without its partner rule hides this line for good. */
.set-field-foot {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-top: 8px;
}
.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. */
/* What a module's record collection says about itself, and any warning about
   where it is stored, above its list of rows. The banners bring their own
   bottom margin for where they stand alone; in here the stack's gap spaces
   them, so the space above the list is the same with one banner or three. */
.records-intro {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.records-intro > .banner { margin-bottom: 0; }
.set-intro {
  margin: 0;
  font-size: var(--text-5);
  line-height: 1.5;
  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 marker is styled here. The bubble is one node at the end of <body> --
   a popover, in the top layer -- and everything about where it goes is
   decided by `placeHint` in console-core.js, including the two cases that
   used to be extra rules down here: opening downward when there is no room
   above, and pulling back from the right edge. Nothing below sets a
   position, and nothing below should: a second opinion about where the
   bubble goes is a second thing to keep in step. */
.hint {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  margin-left: 5px;
  border-radius: 50%;
  border: 1px solid var(--edge);
  background: transparent;
  color: var(--dim);
  font-size: var(--text-1);
  font-weight: 700;
  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 {
  /* Fixed, at coordinates JavaScript writes. `inset` and `margin` are reset
     because the UA stylesheet gives every `[popover]` `inset: 0; margin:
     auto`, which would centre it in the viewport and ignore both. */
  position: fixed;
  inset: auto;
  /* Written by `placeHint`, which is the only thing that knows where the
     anchor is. Defaulted here so the element is never positioned by nothing. */
  left: var(--hint-x, 0);
  top: var(--hint-y, 0);
  margin: 0;
  z-index: 60;
  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);
  /* The accessible copy of this text is the marker's own `aria-label`, so
     nothing is lost by the bubble being invisible -- or, where popovers are
     supported, not rendered at all until it is shown. */
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .12s ease;
}
/* Added only after the bubble has been measured and placed. Everything
   before that happens while it is transparent. */
.hint-bubble.is-open { opacity: 1; visibility: visible; }

/* --------------------------------------------------------- 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: 600;
  cursor: pointer;
  transition: background 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: .45; 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. `.scope input` is excluded for
   the same reason: the chip round it is the thing you see. */
input[type="checkbox"]:not(.switch input):not(.scope input) {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 17px;
  height: 17px;
  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):not(.scope input):hover { border-color: var(--primary); }
input[type="checkbox"]:not(.switch input):not(.scope input):focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
input[type="checkbox"]:not(.switch input):not(.scope 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):not(.scope input):checked::after {
  content: "";
  position: absolute;
  left: 4px;
  top: 1px;
  width: 4px;
  height: 8px;
  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):not(.scope input):indeterminate {
  background: var(--surface-est);
  border-color: var(--outline);
}
input[type="checkbox"]:not(.switch input):not(.scope 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):not(.scope 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: 17px;
  height: 17px;
  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: 24px;
  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: 600;
}
.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; }

/* The briefing editor's field, opened in the CONTENT pane at the width the
   prose will be read at -- never in the 352px right pane and never in a
   dialog. Monospaced and tall because what is typed here is read by a
   model rather than laid out for a person, and because the thing being
   judged while writing it is its length.

   Written `textarea.editor-area` because `.sec-field textarea`, earlier in
   the file, sets a 60px floor, and a bare class loses to it -- which left
   the one tall field in the console three lines high. */
textarea.editor-area {
  min-height: 264px;
  font-family: var(--mono);
  font-size: var(--text-5);
  line-height: 1.6;
}
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; }
