/* Shared minimal styling across the platform's plain-HTML pages - no build
   toolchain, per "radical simplicity", but still meant to look genuinely
   presentable to a client (the signing page especially), not bare HTML.

   Light/dark mode (2026-09-06, Chris: "make sure there is light and dark
   mode and can match windows setting"): every color used anywhere across
   every page is one of the tokens below, nothing hardcoded per-page - so
   `prefers-color-scheme` here is the ONLY place theme logic lives. Windows
   itself doesn't talk to web pages directly; the browser is what reads the
   OS light/dark setting and exposes it as `prefers-color-scheme`, and every
   current Windows browser (Edge, Chrome, Firefox) follows the system theme
   by default unless a user has overridden it in that browser - so this
   automatically matches Windows for the overwhelming majority of visitors
   with zero extra code. No manual toggle was asked for or added.

   Visual refresh (2026-09-08, Chris: "make it prettier") - every existing
   class name and token name is unchanged so no page's HTML/JS needed to
   touch: only the token VALUES and a handful of purely-additive rules
   (shadows, transitions, a nicer heading face) changed.

   Design-system modernisation (2026-09-10, Chris: "the side menu, sub menus
   and all the buttons look like shit, can you modernise it all?" + "make
   things expand or pane in or whatever, make it modern as, but fast, i like
   how snappy it is right now"). Same rules as the refresh above - not one
   class name changed, not one token name removed, so no page's HTML had to
   move. What changed:

     - one type / spacing / radius / control-height scale (the --text-*,
       --space-*, --radius-*, --control-* tokens) instead of the ad-hoc
       px values that had accumulated per component;
     - one semantic colour set (success, warn, danger, info, neutral - each
       a --x-bg / --x-fg pair) that every status, badge and priority token
       now aliases, so a colour means the same thing on every page and in
       both themes;
     - a real button set (primary / .ghost-button secondary / .subtle-button
       / .danger-button) at one height, with hover, pressed, disabled and
       busy states;
     - the sidebar, its favourites list and the .view-sidebar sub-menus given
       actual hierarchy - a quiet default, a neutral hover, and an active
       item marked by BOTH an accent tint and a left accent bar (never colour
       alone);
     - form controls sized to match the buttons, with an invalid state.

   Speed is a feature here ("i like how snappy it is"): the Google Fonts
   @import that used to sit at the top of this file is gone - it was a
   blocking cross-origin round trip before first paint for two faces that
   most machines here never got anyway. --font-display/--font-sans still name
   Sora/Inter first, so a machine that has them installed still uses them;
   everyone else gets the platform UI face immediately instead of waiting.
   Every transition below is 120-160ms and only ever on transform, opacity or
   colour - never width/height/top or box-shadow spread - so nothing animates
   on the layout path, and prefers-reduced-motion turns the lot off.

   Surface layering (2026-09-10, Chris on the above: "Can you merge that
   stuff together in a more modern looking way, maybe the white behind
   everything is making it look funny"). He was right, and the cause was
   that white was doing three different jobs at once: the sidebar was white,
   the cards were white, and the page behind them was a near-white cream, so
   nothing was clearly in front of anything else and every piece of furniture
   needed its own border to be visible at all. There are now exactly three
   levels, and a rule for which one a thing gets:

     --bg              the app field. The sidebar sits on it too (via
                       --sidebar-bg) instead of being its own white slab, so
                       the whole window is one continuous surface.
     --card            raised: cards, panels, inputs, secondary buttons,
                       menus. The only white in the light theme.
     --surface-sunken  recessed INSIDE a raised surface: table headers,
                       filter rows, read-only wells.

   And one rule for separation, applied consistently instead of
   per-component: a surface that sits in the page flow is separated by a
   1px --line and nothing else; a shadow means the surface is genuinely
   floating above the page (menus, popovers, the slide-over). Cards no
   longer carry both.

   Alignment: --panel-px is the single horizontal inset shared by a data
   table's toolbar, its header and body cells, and its pagination, so those
   three - which are siblings, not one element - line up on the same left
   edge and read as one block rather than three stacked strips. A card whose
   whole content IS a table drops its own padding and border so there is one
   panel edge there, not two.

   Accent: --accent-hover/-active/-soft and the nav's --active-bg are all
   derived from --accent with color-mix rather than hardcoded blues, so
   per-organisation branding can swap the one token and have the hover,
   pressed and selected states follow. --accent-on (the text colour that
   sits ON the accent) is the one that cannot be derived - anything setting
   --accent must set it too. */

:root {
  /* Lets the browser render native controls (select, checkbox, scrollbars)
     in a matching light or dark style automatically, on top of the custom
     tokens above driving everything else. */
  color-scheme: light dark;

  /* Type scale. One ramp, used by every rule below instead of the ad-hoc
     11px/11.5px/12.5px/13px/13.5px/14px/15px values that had accumulated.
     Collapsed to five steps on 2026-09-20 (backlog R10): 11 is only the
     uppercase micro-labels, which letter-space and so still read apart from
     the 12px secondary text, and the body is 14 - two clear pixels above it.
     There is deliberately no 13px step: a 1px difference between body and
     secondary text is noise rather than a hierarchy. */
  --text-xs: 11px;
  --text-sm: 12px;
  --text-base: 14px;
  --text-lg: 16px;
  --text-xl: 20px;

  /* Spacing scale (4px base). */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;

  /* Radii. --radius keeps its existing name and meaning (the card/table
     radius) because dashboard.html's own CSS references it. */
  --radius-xs: 4px;
  --radius-sm: 6px;
  --radius-md: 8px;
  --radius: 10px;
  --radius-lg: 14px;
  --radius-pill: 999px;

  /* Control sizing - the single reason a button, a select and an input now
     line up on the same row on every page. */
  --control-h: 32px;
  --control-h-sm: 26px;
  --control-h-lg: 40px;
  --control-px: 12px;
  --control-fs: var(--text-base);

  /* Motion. Short, and only ever applied to transform/opacity/colour. */
  --motion-fast: 120ms;
  --motion: 150ms;
  --motion-panel: 160ms;
  --ease: cubic-bezier(0.2, 0, 0, 1);

  /* Faces. No @import, no runtime fetch - Sora/Inter first for machines that
     have them, then the platform UI face. */
  --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-display: 'Sora', 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI Variable Display", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "Cascadia Mono", Menlo, Consolas, monospace;

  --ink: #1a1f2e;
  --ink-soft: #5c6577;
  /* Quietest text that still clears 4.5:1 on BOTH --card and the tinted
     --bg (4.6:1 on the field, 5.3:1 on white). It lands close to --ink-soft
     on purpose: past this point a third grey stops being legible, so the
     micro-label hierarchy is carried by size, weight and caps instead. */
  --ink-faint: #5e6775;
  --line: #e0dcd3;
  /* Three weights of rule, so a table's row dividers can be quieter than a
     card's edge without either becoming invisible. */
  --line-soft: #e9e5dd;
  /* Raised from #cdc9be on 2026-09-20 (backlog R10): an input's boundary is a
     control's boundary, not decoration, and it has to reach 3:1 against the
     card it sits on to be visible as a field. This one measures 3.25:1 on
     --card; src/ui/typeScale.test.ts computes it and fails if a later theme
     edit drops it below 3. */
  --line-strong: #948e83;
  /* Level 0 - the app field. Stepped down from the old #f7f5f0 so that white
     reads as genuinely in front of it rather than as almost the same colour. */
  --bg: #efece5;
  /* Level 1 - raised. */
  --card: #ffffff;
  /* The sidebar is part of the field, not a second white slab beside the
     cards. Its own token so per-org branding can tint just the chrome. */
  --sidebar-bg: var(--bg);
  /* Level 2 - recessed, and only ever used inside a raised surface. */
  --surface-sunken: #f5f3ee;
  /* Zebra stripe - deliberately almost imperceptible; the row hover below is
     what actually guides the eye. */
  --row-alt: #faf9f6;
  /* Neutral hover. Everything used to hover to --badge-bg (a pale blue),
     which is why every list, menu and row read as vaguely "selected" all the
     time - accent tint is now reserved for things that really are current. */
  --hover-bg: #e7e3da;
  /* Recessed: a field you type into reads as cut INTO the surface it sits
     on, which also stops a form inside a white card being white on white. */
  --input-bg: #f7f5f1;
  /* Shared horizontal inset for the parts of one panel - see the note at
     the top about the toolbar / table / pagination lining up. */
  --panel-px: var(--space-3);
  --accent: #3454d1;
  --accent-dark: #24399a;
  /* Derived, so swapping --accent for an organisation's brand colour carries
     the hover, pressed, tint and selected states with it. The plain value on
     the first line of each pair is the fallback for an engine without
     color-mix; the platform's own browsers all have it. */
  --accent-hover: #2b47b8;
  --accent-hover: color-mix(in srgb, var(--accent) 86%, #000);
  --accent-active: #23399c;
  --accent-active: color-mix(in srgb, var(--accent) 74%, #000);
  --accent-soft: #e9edfc;
  --accent-soft: color-mix(in srgb, var(--accent) 12%, var(--card));
  --accent-halo: rgba(52, 84, 209, 0.18);
  --accent-halo: color-mix(in srgb, var(--accent) 22%, transparent);
  /* The selected-item tint in the nav and sub-menus, mixed onto the field so
     it stays a tint of the field rather than a separate blue patch. */
  --active-bg: #e5e6ea;
  --active-bg: color-mix(in srgb, var(--accent) 11%, var(--bg));
  --active-bg-strong: #dadce4;
  --active-bg-strong: color-mix(in srgb, var(--accent) 18%, var(--bg));
  /* Text/icon colour that sits ON --accent. Not white unconditionally: the
     dark theme's accent is a light blue, and white on it was ~2.3:1 - every
     primary button in dark mode was failing contrast. This is the one accent
     token that cannot be derived, so anything overriding --accent (per-org
     branding) has to set this alongside it. */
  --accent-on: #ffffff;
  /* Scoped purple accent (2026-09-09, "take style queues and inspiration
     from Guardz") - only the org switcher and its All/Single toggle use
     this; --accent (blue) stays the color for every button/link elsewhere. */
  --accent-purple: #6c4bd6;
  --success: #1f8a4c;
  --shadow: 0 1px 2px rgba(26, 31, 46, 0.04), 0 8px 24px -12px rgba(26, 31, 46, 0.16);
  --shadow-sm: 0 1px 2px rgba(26, 31, 46, 0.05);
  --shadow-panel: -12px 0 32px -12px rgba(26, 31, 46, 0.22);
  --overlay: rgba(20, 24, 32, 0.42);

  /* ---- Semantic colour set -------------------------------------------
     Five meanings, one bg/fg pair each, both themes. Every status box,
     badge and priority token below is an alias onto these, so "danger" is
     the same red on Tickets, Devices, Approvals and Proposals rather than
     five hand-picked reds. Every pair clears 4.5:1 so it is safe for the
     11-12px bold text these get used for; and nothing in this stylesheet
     uses one of them as the ONLY signal - a status always carries its own
     word, an active row always also gets a bar or a weight change. */
  --success-bg: #e6f6ec;
  --success-fg: #14703c;
  --success-solid: #16794a;
  --warn-bg: #fdf3e0;
  --warn-fg: #8a5c00;
  --warn-solid: #a06a06;
  --danger-bg: #fdeceb;
  --danger-fg: #9c2b22;
  --danger-solid: #b3271c;
  --danger-on: #ffffff;
  --info-bg: #e9edfc;
  --info-fg: #2b47b8;
  --neutral-bg: #e4e0d6;
  --neutral-fg: #555e6e;

  --badge-bg: var(--info-bg);
  --disabled-bg: #e1ddd3;
  --disabled-fg: #757a85;
  --star-empty: #c9cdd6;
  /* Darkened again: on the deeper field #bf7d00 fell to 2.9:1, under the
     3:1 a meaningful icon needs. */
  --star-filled: #b07300;

  /* Keyboard focus ring (2026-09-10) - deliberately its own token rather
     than reusing --accent: the ring has to stay legible against a filled
     --accent button as well as the page background, in both themes. The
     focus block above already reads these names with an --accent fallback,
     so defining them here is all that is needed. */
  --focus-ring: #24399a;
  --focus-halo: rgba(52, 84, 209, 0.28);

  /* Kept as aliases, not values - approvals/proposals/devices/patching and
     tickets' own page CSS all reference these names. */
  --status-error-bg: var(--danger-bg);
  --status-error-fg: var(--danger-fg);
  --status-success-bg: var(--success-bg);
  --status-success-fg: var(--success-fg);
  --status-warn-bg: var(--warn-bg);
  --status-warn-fg: var(--warn-fg);
  --status-info-bg: var(--info-bg);
  --status-info-fg: var(--info-fg);
  --status-neutral-bg: var(--neutral-bg);
  --status-neutral-fg: var(--neutral-fg);

  --priority-low-bg: var(--success-bg);
  --priority-low-fg: var(--success-fg);
  --priority-medium-bg: var(--info-bg);
  --priority-medium-fg: var(--info-fg);
  --priority-high-bg: var(--warn-bg);
  --priority-high-fg: var(--warn-fg);
  --priority-urgent-bg: var(--danger-bg);
  --priority-urgent-fg: var(--danger-fg);

  /* Aliases for names other files already reference but that were never
     defined here - tickets.html's attachment thumbnail asks for
     var(--border) and silently got nothing. */
  --border: var(--line);
  --border-color: var(--line);

  --ms-button-bg: #ffffff;
  --ms-button-fg: #5e5e5e;
  --ms-button-border: #8c8c8c;
  --ms-button-hover-bg: #f3f3f3;
}

@media (prefers-color-scheme: dark) {
  :root {
    --ink: #e8ebf0;
    --ink-soft: #9aa5b3;
    --ink-faint: #7c8695;
    --line: #262d37;
    --line-soft: #1f252e;
    /* Same job as the light theme's --line-strong, and the same 3:1 floor
       against this theme's --card (3.32:1). */
    --line-strong: #616f83;
    /* Same three levels as the light theme, same job each. The dark theme's
       version of "everything is white" was "everything is one flat dark",
       so --card is stepped clearly above --bg rather than a shade off it. */
    --bg: #0d1117;
    --card: #171d25;
    --sidebar-bg: var(--bg);
    --surface-sunken: #12171e;
    --row-alt: #1a212a;
    --hover-bg: #1f262f;
    --input-bg: #12171e;
    --accent: #7c9bf0;
    --accent-dark: #9db4f3;
    /* Mixed toward white, not black - on a dark field the brighter step is
       the one that reads as "hovered". */
    --accent-hover: #93aef5;
    --accent-hover: color-mix(in srgb, var(--accent) 82%, #fff);
    --accent-active: #6b8ce8;
    --accent-active: color-mix(in srgb, var(--accent) 82%, #000);
    --accent-soft: #232c45;
    --accent-soft: color-mix(in srgb, var(--accent) 16%, var(--card));
    --accent-halo: rgba(124, 155, 240, 0.26);
    --accent-halo: color-mix(in srgb, var(--accent) 30%, transparent);
    --active-bg: #1c2331;
    --active-bg: color-mix(in srgb, var(--accent) 14%, var(--bg));
    --active-bg-strong: #232c40;
    --active-bg-strong: color-mix(in srgb, var(--accent) 22%, var(--bg));
    /* Dark ink on the light-blue accent - white here was ~2.3:1. */
    --accent-on: #0e1420;
    --accent-purple: #a690f0;
    --success: #4fd996;
    --shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 8px 24px -12px rgba(0, 0, 0, 0.5);
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
    --shadow-panel: -12px 0 32px -12px rgba(0, 0, 0, 0.6);
    --overlay: rgba(0, 0, 0, 0.45);

    --success-bg: #10301f;
    --success-fg: #62e2a0;
    --success-solid: #1c8552;
    --warn-bg: #33260a;
    --warn-fg: #e5b45c;
    --warn-solid: #8a6413;
    --danger-bg: #3a1f1d;
    --danger-fg: #ff9186;
    --danger-solid: #c24137;
    --danger-on: #ffffff;
    --info-bg: #232c45;
    --info-fg: #a8bdf8;
    --neutral-bg: #262d38;
    --neutral-fg: #9aa5b3;

    --disabled-bg: #262d38;
    --disabled-fg: #6d7683;
    --star-empty: #3a4451;
    --star-filled: #f5a623;

    --focus-ring: #a8c4ff;
    --focus-halo: rgba(168, 196, 255, 0.32);

    --ms-button-bg: #262b32;
    --ms-button-fg: #e8ebf0;
    --ms-button-border: #454c55;
    --ms-button-hover-bg: #30363e;
  }
}

* { box-sizing: border-box; }

/* The `hidden` attribute is how nearly every page in here toggles a block,
   but the UA's `display: none` for it is a plain type-selector rule that
   any `display: flex` in this stylesheet outranks - so a hidden .field or
   .toolbar stayed on screen. csat.html had already worked this out and
   patched it locally; it belongs here once. */
[hidden] { display: none !important; }

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-base);
  background: var(--bg);
  color: var(--ink);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Motion budget. Every transition in this file is 120-160ms on transform,
   opacity or colour only - nothing on width/height/top or box-shadow spread,
   so nothing animates on the layout path. This turns the lot off for anyone
   who has asked their OS for less motion. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Focus. Deliberately at bare :focus-visible specificity and using
   --focus-ring/--focus-halo with an --accent fallback, so a definition of
   those tokens elsewhere in the platform wins without this needing to
   change, and a component-level rule can still override it. Nothing in this
   file removes an outline outright any more - see the input rules below,
   which only drop it for :focus:not(:focus-visible) (i.e. a mouse click),
   never for keyboard focus. */
:focus-visible {
  outline: 2px solid var(--focus-ring, var(--accent));
  outline-offset: 2px;
}

.page {
  max-width: 760px;
  margin: 0 auto;
  padding: 40px 20px 80px;
}

.brand {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-base);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: var(--space-6);
}

/* Primary navigation (2026-09-10, Chris: "the side menu, sub menus and all
   the buttons look like shit"). The old version gave all thirteen links the
   same 600 weight and hovered every one of them to the same pale blue the
   active item used, so nothing read as current and the whole column read as
   one undifferentiated block. Now: a quiet 500-weight default, a NEUTRAL
   hover (so hovering doesn't look like selecting), and a current item marked
   three ways at once - accent tint, 600 weight, and a left accent bar - so
   the state never rests on colour alone.

   Deliberately kept at single-class specificity, with no !important
   anywhere, so a richer grouped/collapsible nav layered on top of this
   markup can restyle or extend it without having to fight these rules. */
.nav {
  display: flex;
  gap: 1px;
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: none;
  flex-direction: column;
  flex-wrap: nowrap;
}
.nav a {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 34px;
  color: var(--ink-soft);
  text-decoration: none;
  font-size: var(--text-base);
  font-weight: 500;
  padding: 6px 10px;
  border-radius: var(--radius-md);
  /* Colour only - no transform, nothing that could reflow a 13-item list. */
  transition: background-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.nav a:hover { color: var(--ink); background: var(--hover-bg); }
/* Pressed. A background step rather than a nudge - a menu that moves under
   the cursor reads as slow even when it isn't. */
.nav a:active { background: var(--active-bg-strong); }
.nav a.active,
.nav a[aria-current="page"] {
  color: var(--accent);
  background: var(--active-bg);
  font-weight: 600;
  /* Inset, so the bar costs no layout and follows the rounded corner. */
  box-shadow: inset 3px 0 0 var(--accent);
}
.nav a.active:hover,
.nav a[aria-current="page"]:hover { background: var(--active-bg-strong); color: var(--accent); }

/* Nesting inside the primary nav. Structural only - an indent, a smaller
   type size and a rule down the left so a child list reads as belonging to
   the item above it rather than as thirteen more siblings. Provided as
   plain, low-specificity classes any grouped-nav implementation can adopt or
   override; a disclosure chevron rotates on transform alone.
   The :not(:has(.nav-chev)) guard is a fallback switch: an implementation
   that ships its own chevron element (nav.js does) draws that one instead,
   so the two never stack up as a double arrow. */
.nav-group { display: flex; flex-direction: column; gap: 1px; }
.nav-group-label {
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-faint);
  padding: var(--space-3) 10px var(--space-1);
}
.nav-group-toggle:not(:has(.nav-chev))::after {
  content: "";
  width: 0;
  height: 0;
  margin-left: auto;
  border: 4px solid transparent;
  border-top-color: currentColor;
  transform: translateY(2px);
  transition: transform var(--motion-fast) var(--ease);
}
.nav-group-toggle:not(:has(.nav-chev))[aria-expanded="false"]::after { transform: rotate(-90deg); }
.nav-sublist {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin: 2px 0 var(--space-2) 18px;
  padding-left: var(--space-2);
  border-left: 1px solid var(--line);
}
.nav-sublist a {
  min-height: 30px;
  font-size: var(--text-sm);
  font-weight: 500;
  padding: 4px 10px;
}

/* Sidebar shell (2026-09-09, Chris: "build things into menus on the
   sidebar") - .brand and .nav keep their exact class names/markup shape
   (businessUnitNav.js does `document.querySelector(".nav")` and inserts the
   org switcher as its first child; hideDisabledNavLinks walks its <a> tags)
   - only the surrounding layout changes, from a top bar to a left column.
   Every authenticated page wraps <div class="brand">/<nav class="nav"> and
   its own <div class="page"> content in one <div class="app-shell">; the
   three unauthenticated pages (login/sign/csat) keep the old centered
   single-column .page layout untouched, no .app-shell involved. */
.app-shell {
  display: flex;
  align-items: flex-start;
}

.sidebar {
  width: 220px;
  flex-shrink: 0;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-5) var(--space-3) var(--space-5);
  border-right: 1px solid var(--line);
  /* Not --card. The sidebar used to be a white slab standing beside white
     cards on a cream page; now it is the same field the page sits on, with
     one hairline doing the separating. */
  background: var(--sidebar-bg);
  display: flex;
  flex-direction: column;
  scrollbar-width: thin;
}

/* The brand sits on its own rule above the nav rather than floating in the
   same visual layer as the links - the column now reads header / links /
   favourites instead of one flat list of fourteen things. */
.sidebar-header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 34px;
  margin: 0 0 var(--space-3);
  padding: 0 10px var(--space-3);
  border-bottom: 1px solid var(--line-soft);
}
.sidebar-header .brand { margin: 0; padding: 0; font-size: var(--text-sm); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The current organisation's own logo in the sidebar (D5, 2026-09-19) - the
   product name steps aside for it, and comes back on "All" or for an
   organisation that has not uploaded one. Sized like the text it replaces so
   the header does not move. */
.sidebar-header .brand.brand-has-logo { display: flex; align-items: center; min-height: 24px; }
.sidebar-header .brand .brand-logo { display: block; max-height: 24px; max-width: 100%; object-fit: contain; }

/* Top bar (2026-09-09, Chris: "the org switch should always be at the top
   left" + "take style queues and inspiration from Guardz") - spans the full
   width of the content column (not the sidebar), org switcher pinned left.
   Anything else in here (an "+ Add..." button on a couple of pages) stays
   in normal flow, pinned right. */
.page-topbar { display: flex; align-items: center; gap: var(--space-4); min-height: var(--control-h); margin-bottom: var(--space-4); }
/* margin-left: auto (not justify-content: space-between) so this stays
   pinned right even when the org switcher slot is empty on a page with no
   accessible orgs - space-between alone collapses to the start with only
   one real flex child. */
/* padding-right reserves the corner .profile-menu-fixed occupies (2026-09-10,
   found live, Chris: "add device is loading over my CT button - needs to be
   seperate") - the profile icon is position: fixed to the viewport corner,
   completely outside this row's own flex flow, so without this reservation
   an in-flow "+ Add..." button on a page like Devices/Clients renders right
   underneath it instead of stopping short. 30px icon + 20px viewport offset
   + a small gap. */
.page-topbar-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; margin-left: auto; padding-right: 56px; }

/* Fixed to the top-right of the actual page/viewport (2026-09-09, Chris:
   "put the profile icon on the far right as well at the top, like you did
   with the rocket") - same floating treatment as .sidebar-actions below,
   just anchored to the opposite corner, deliberately its own header-like
   element rather than an in-flow item any page's own buttons can collide
   with. #profileMenu inside keeps its own position: relative so its
   dropdown still anchors to itself, not the viewport. */
.profile-menu-fixed { position: fixed; top: 20px; right: 20px; z-index: 30; }

/* Custom views (2026-09-09, Chris: "Should be able to build them on the
   side panel that will come out after the menu before the customer list") -
   a slide-out drawer anchored to the sidebar's own right edge, overlaying
   the start of the list rather than pushing it over (simpler, and the list
   underneath still shows through once a view's filters are applied). */
.views-panel {
  position: fixed;
  top: 0;
  left: 220px;
  width: 280px;
  height: 100vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--card);
  border-right: 1px solid var(--line);
  box-shadow: var(--shadow);
  z-index: 25;
  padding: var(--space-5);
  scrollbar-width: thin;
}
.views-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--line-soft);
  font-weight: 600;
}
.views-list { display: flex; flex-direction: column; gap: 2px; margin-bottom: var(--space-4); }
.views-list-item { display: flex; align-items: center; gap: var(--space-1); }
.views-list-item .view-apply { flex: 1; text-align: left; justify-content: flex-start; }
.views-list-item .view-delete {
  border: none;
  background: none;
  color: var(--ink-soft);
  font-size: var(--text-lg);
  line-height: 1;
  cursor: pointer;
  padding: var(--space-1) 6px;
  border-radius: var(--radius-sm);
  transition: background-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.views-list-item .view-delete:hover { color: var(--danger-fg); background: var(--danger-bg); }
.views-save-row { display: flex; flex-direction: column; gap: var(--space-2); padding-top: var(--space-3); border-top: 1px solid var(--line); }
@media (max-width: 860px) {
  .views-panel { left: 0; width: 100%; }
}

/* Org switcher pill (Guardz-style segmented All/Single toggle). Purple accent
   scoped to just this component - the platform's own --accent (blue) stays
   the button/link color everywhere else, this is a deliberate visual borrow
   for the one control Chris pointed at, not a full re-theme. */
/* Restyled 2026-09-10 to match the Guardz screenshot Chris pointed at more
   closely - two separate pill buttons with a visible gap between them
   (rather than one shared rounded strip), the active one lifted/white, the
   inactive one flat grey. "Single Org" always carries a dropdown-style
   chevron, whether or not it currently opens a real <select> underneath (a
   lone accessible org has nothing to pick between, but the visual affordance
   stays consistent). */
.org-switcher { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.org-switcher-toggle { display: inline-flex; flex-wrap: wrap; gap: 6px; }
.org-toggle-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Never squeezed onto a second line: with the whole org row on screen
     (2026-09-18) a narrow window wraps the ROW, not the word "All". */
  white-space: nowrap;
  flex: 0 0 auto;
  min-height: var(--control-h);
  border: 1px solid var(--line);
  background: var(--neutral-bg);
  padding: 0 var(--space-3);
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink-soft);
  cursor: pointer;
  font-family: inherit;
  transition: background-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}
.org-toggle-btn:hover:not(:disabled) { background: var(--hover-bg); color: var(--ink); }
.org-toggle-btn[aria-pressed="true"] {
  background: var(--card);
  border-color: var(--accent-purple);
  color: var(--accent-purple);
  box-shadow: var(--shadow-sm);
}
.org-toggle-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* The organisations themselves, on screen (2026-09-18, docs/UI-STANDARD.md
   section 4). This replaced a dropdown menu of the same buttons: Chris, "none
   of this drop down shit. I want to be able to see everything fast." The row
   wraps rather than hiding anything, and the current organisation carries the
   same lifted pill the All control does. */
.org-picker-inline { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.org-chip {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  flex: 0 0 auto;
  min-height: var(--control-h);
  border: 1px solid var(--line);
  background: var(--neutral-bg);
  padding: 0 var(--space-3);
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink-soft);
  cursor: pointer;
  font-family: inherit;
  transition: background-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}
.org-chip:hover { background: var(--hover-bg); color: var(--ink); }
.org-chip.active {
  background: var(--card);
  border-color: var(--accent-purple);
  color: var(--accent-purple);
  box-shadow: var(--shadow-sm);
  cursor: default;
}
/* The organisation's own colour on its own pill (D5, 2026-09-19): a dot on
   every pill that has one, and the current pill carrying its colour on the
   border and a soft tint behind the label. The label stays --ink so a light
   brand colour cannot make the pill's own name unreadable, and a pill with no
   colour set keeps exactly what it had. */
.org-chip-coloured::before {
  content: "";
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-right: 7px;
  background: var(--org-color);
}
.org-chip-coloured.active {
  border-color: var(--org-color);
  background: color-mix(in srgb, var(--org-color) 12%, var(--card));
  color: var(--ink);
}
.org-switcher-all-label {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: var(--control-h);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--accent-purple);
  background: var(--accent-soft);
  padding: 0 var(--space-3);
  border-radius: var(--radius-pill);
}
/* Menus "pane in" rather than snapping (2026-09-10, Chris: "make things
   expand or pane in or whatever, make it modern as, but fast") - a 120ms
   fade and a 4px rise, on opacity and transform only, so it costs a
   composite and nothing else. The org switcher no longer has a menu of its
   own (2026-09-18); the profile and notification dropdowns still use this. */
@keyframes menu-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}
.org-picker-edit-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--control-h);
  padding: var(--space-2) 10px var(--space-1);
  font-size: var(--text-sm);
  color: var(--ink-soft);
  text-decoration: none;
}
.org-picker-edit-link:hover { color: var(--accent); }

/* Profile menu (2026-09-09, Chris: "put a circle with profile settings top
   right like every other PSA" - replacing the one-off Sign out button that
   used to live only on tickets.html, the platform's only sign-out path).
   Initials-avatar button that toggles a small dropdown - filled in by
   businessUnitNav.js from the same /auth/me call it already makes for the
   org switcher, so this doesn't cost a second request. */
.profile-avatar {
  width: 30px;
  height: 30px;
  min-width: 30px;
  border-radius: 999px;
  border: none;
  background: var(--accent);
  color: var(--accent-on);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: 0.02em;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-sm);
  transition: background-color var(--motion-fast) var(--ease);
}
.profile-avatar:hover { background: var(--accent-hover); }
.profile-dropdown {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 20;
  min-width: 200px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--space-1);
  animation: menu-in var(--motion-fast) var(--ease);
}
.profile-dropdown-name { padding: var(--space-2) 10px 2px; font-size: var(--text-base); font-weight: 600; color: var(--ink); }
.profile-dropdown-email { padding: 0 10px var(--space-2); font-size: var(--text-sm); color: var(--ink-soft); border-bottom: 1px solid var(--line-soft); margin-bottom: var(--space-1); word-break: break-all; }
.profile-dropdown a, .profile-dropdown button {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  width: 100%;
  min-height: 32px;
  text-align: left;
  padding: 6px 10px;
  border: none;
  background: none;
  color: var(--ink);
  text-decoration: none;
  font-size: var(--text-base);
  font-weight: 500;
  font-family: inherit;
  border-radius: var(--radius-sm);
  cursor: pointer;
  box-shadow: none;
}
.profile-dropdown a:hover, .profile-dropdown button:hover { background: var(--hover-bg); color: var(--ink); box-shadow: none; }

.sidebar-favorites { margin-top: var(--space-5); padding-top: var(--space-3); border-top: 1px solid var(--line-soft); }
.sidebar-favorites:empty { display: none; }
.sidebar-favorites-title {
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-faint);
  padding: 0 10px;
  margin-bottom: var(--space-1);
}
.sidebar-favorite {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 30px;
  padding: 4px 10px;
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  color: var(--ink-soft);
  text-decoration: none;
  transition: background-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.sidebar-favorite:hover { color: var(--ink); background: var(--hover-bg); }
.sidebar-favorite .star { color: var(--star-filled); flex-shrink: 0; }
.sidebar-favorite .label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Fixed to the bottom-right of the actual page/viewport (2026-09-09, Chris:
   "The Notification rocket and chat buttons should end up bottom left" ->
   "bottom RIGHT sidebar, sorry" -> "I want everything on the actual app
   page bottom right", i.e. floating over the page, not tucked into the
   sidebar's own flow) - a future chat launcher slots in beside it, same
   row. position: fixed means where this div sits in the HTML doesn't
   matter for its on-screen position - it's still marked up inside .sidebar
   purely so businessUnitNav.js/updates.js don't need a markup move to
   match; it renders over the page regardless. */
.sidebar-actions { position: fixed; bottom: 20px; right: 20px; z-index: 30; display: flex; gap: 8px; }

/* Notifications (2026-09-09, Chris: "a little rocket logo for the
   notifications for updates" then "I just want a circle button with a
   rocket in it, you click it pulls up a context menu" + "red number for how
   many unreads"). Unread count (public/updates.js) is tracked per-browser in
   localStorage, not per staff-account server-side - proportionate for a
   hand-maintained list this short, revisit if it needs to be reliable
   across devices. */
.notif-button {
  position: relative;
  width: 34px;
  height: 34px;
  min-width: 34px;
  padding: 0;
  border-radius: var(--radius-pill);
  border: 1px solid var(--line);
  background: var(--card);
  font-size: var(--text-lg);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-sm);
  transition: background-color var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}
.notif-button:hover { background: var(--hover-bg); border-color: var(--line-strong); }
.notif-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 16px;
  height: 16px;
  padding: 0 3px;
  border-radius: 999px;
  /* Solid danger + its own on-colour, not --status-error-fg + white: the
     dark theme's error foreground is a light salmon, so white on it was
     ~2.2:1 - the unread count was effectively unreadable at night. */
  background: var(--danger-solid);
  color: var(--danger-on);
  font-size: var(--text-xs);
  font-weight: 700;
  line-height: 16px;
  text-align: center;
}
/* Opens upward from the sidebar's bottom-right corner (unlike
   .profile-dropdown, which it otherwise shares all styling with) - a
   downward-anchored dropdown would run off the bottom of the sidebar there. */
.notif-dropdown { top: auto; bottom: calc(100% + 6px); right: 0; width: 300px; max-height: 360px; overflow-y: auto; }
.notif-dropdown-title {
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-faint);
  padding: var(--space-2) 10px var(--space-1);
}
.sidebar-update { padding: var(--space-1) 10px var(--space-2); border-radius: var(--radius-sm); }
.sidebar-update + .sidebar-update { border-top: 1px solid var(--line-soft); padding-top: var(--space-2); }
.sidebar-update-date { font-size: var(--text-xs); color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
.sidebar-update-text { font-size: var(--text-sm); color: var(--ink-soft); line-height: 1.45; margin-top: 2px; }

/* Found 2026-09-10 (Chris: "you are only using half or less of the
   available page... it's not populating the available screen space") -
   the bare .page rule above sets max-width: 760px for standalone pages
   (login.html) that center a narrow card. flex: 1 here was meant to let
   every app-shell page (tickets/clients/devices/etc.) fill the remaining
   width next to the sidebar, but flex-grow doesn't override max-width -
   that 760px cap was silently still in effect on every single app-shell
   page, this whole time. max-width: 100% here means "as wide as the flex
   item is allowed to grow", i.e. the actual available space, not a fixed
   pixel ceiling - a real percentage, not another guessed px number. */
.app-shell .page { flex: 1; min-width: 0; max-width: 100%; margin: 0; padding: var(--space-6) var(--space-6) 64px; }

/* Organisation switches re-resolve module access before any page callback
   can fetch the next scope. Hide the old scope meanwhile so restricted
   counts or panes never remain visible during that decision. */
html.module-access-loading .page > :not(.page-topbar),
html.module-access-loading .slide-over-panel,
html.module-access-loading .slide-over-backdrop {
  visibility: hidden !important;
}
.module-access-error {
  position: fixed;
  inset: 76px 24px auto auto;
  z-index: 1200;
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: min(440px, calc(100vw - 48px));
  box-shadow: var(--shadow-md);
}

@media (max-width: 860px) {
  .app-shell { flex-direction: column; }
  .sidebar {
    width: 100%;
    height: auto;
    position: static;
    border-right: none;
    border-bottom: 1px solid var(--line);
    padding: 16px 20px;
  }
  /* One horizontally scrollable strip rather than thirteen links wrapping
     into four stacked rows that used to eat most of a phone screen before
     the page even started. */
  /* The strip itself (flex-direction, nowrap, overflow-x, gap,
     scrollbar-width) is declared once at the bottom of this file by the
     mobile-and-PWA pass, which also adds -webkit-overflow-scrolling. Only
     the full-bleed negative margin is unique to this block. */
  .nav {
    margin: 0 calc(var(--space-5) * -1);
    padding: 0 var(--space-5);
  }
  /* min-height is the phone touch target and is only set here; the
     flex-shrink beside it is restated by the pass at the bottom. */
  .nav a { min-height: 38px; }
  /* The left bar reads as nesting in a column and as noise in a row. */
  .nav a.active, .nav a[aria-current="page"] { box-shadow: inset 0 -2px 0 var(--accent); }
  .nav-sublist { margin-left: 0; border-left: none; padding-left: 0; flex-direction: row; }
  .sidebar .brand { margin: 0 0 10px; }
  .sidebar-header { border-bottom: none; padding-bottom: var(--space-2); }
  .sidebar-favorites { display: flex; flex-wrap: wrap; gap: var(--space-1); border-top: none; margin-top: 10px; padding-top: 0; }
  .sidebar-favorites-title { width: 100%; padding: 0 4px; }
  /* Slightly taller controls where a finger is doing the tapping. */
  :root { --control-h: 36px; --control-h-sm: 30px; }
}

h1 {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.015em;
  margin: 0 0 var(--space-2);
}
/* An app page's h1 is a label on a dense screen, not a headline on a
   marketing page - the standalone pages (login, sign, csat) keep the bigger
   display size above. */
.app-shell h1 { font-size: var(--text-xl); margin-bottom: var(--space-1); }

h2 {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.01em;
  margin: 0 0 var(--space-2);
}
h3 {
  font-size: var(--text-base);
  font-weight: 600;
  line-height: 1.35;
  margin: 0 0 var(--space-2);
}

.subtitle {
  color: var(--ink-soft);
  font-size: var(--text-base);
  line-height: 1.5;
  max-width: 88ch;
  margin: 0 0 var(--space-5);
}

/* One recipe for a raised surface that sits in the page flow: the --card
   value, a 1px --line, a --radius corner, and NO shadow. A shadow now means
   one thing only - the surface is floating above the page (see the menus,
   popovers and slide-over below). Cards used to carry both, which is why
   each one read as a separate object rather than as part of a page. */
.card {
  /* One token for the card's own inset, so anything that has to full-bleed
     to the card's edge from inside it (.card-header below) can cancel
     exactly that inset instead of guessing a matching negative margin. */
  --card-px: var(--space-5);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--card-px);
  margin-bottom: var(--space-4);
}
.card > :first-child { margin-top: 0; }
.card > :last-child { margin-bottom: 0; }
/* A card used as a plain container inside an already-bordered region. */
.card-flush { border: none; background: none; padding: 0; }

/* A card whose entire content is a data table gets out of the way: the
   table panel below already draws exactly this surface, and two of them
   nested was the double border on Tickets. Direct-child selector, so a card
   that merely contains a table among other things keeps its own padding. */
.card:has(> .dt-toolbar) {
  padding: 0;
  border: none;
  background: none;
}

/* ---- Containment patterns -------------------------------------------
   (2026-09-10, Chris: "fix the ui more so its more cohesive, more having
   things in things, and everything looking cool.")

   The surface pass before this one got the LEVELS right - field, raised,
   recessed - but every page was still a flat stack of things on the same
   level: a toolbar floating above a panel it controls, a <strong> acting
   as a heading that scopes nothing, six form fields in a row with no
   grouping, a "child" card sitting beside its parent as a sibling. Nothing
   said what belonged to what.

   Four patterns fix that, and only four, so they get used rather than
   multiplied. Each is a plain class on markup any page already has:

     .card-header   a header that OWNS the block under it - a full-bleed
                    recessed band welded to the top edge of its card, title
                    left, controls right. Everything below it in that card
                    is visibly under it, not next to it.
     .field-group   a labelled group INSIDE a card - the recessed well that
                    a set of related controls lives in, with a micro-label
                    naming the set. (.field-row / .field lay the controls
                    out inside it.)
     .subsection    a nested block inside a card - detail that belongs to
                    the card above it, marked by a hairline and a
                    micro-label rather than escaping into its own card.
     .toolbar       a strip of controls as one object. It was never defined
                    in this stylesheet at all - only tickets.html had a
                    local copy - so on Billing, Clients and Devices
                    `class="toolbar"` styled nothing and its select, input
                    and button stacked as three loose rows on the page.

   Nothing here adds a shadow: a contained thing is contained by its
   parent's edge, not by floating off the page. Nothing here transitions,
   either - these are static containers. */

/* P1 - a header that owns the block beneath it. */
.card-header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  min-height: 44px;
  padding: var(--space-2) var(--space-5);
  background: var(--surface-sunken);
  border-bottom: 1px solid var(--line);
}
/* Full-bleed to the card's edges: the band is the card's top, not a strip
   floating inside its padding. Negative margins rather than stripping the
   card's padding, so a card can adopt a header without every other rule
   that targets .card padding having to know. */
.card > .card-header:first-child {
  margin: calc(var(--card-px) * -1) calc(var(--card-px) * -1) var(--space-4);
  padding-left: var(--card-px);
  padding-right: var(--card-px);
  border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
}
/* Works with the <strong> several pages already use as a pseudo-heading,
   so adopting the pattern is one wrapper element, not a rewrite. */
.card-title,
.card-header > strong,
.card-header > h2,
.card-header > h3 {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.005em;
  color: var(--ink);
  margin: 0;
}
.card-header-meta { color: var(--ink-soft); font-size: var(--text-base); font-variant-numeric: tabular-nums; }
/* Anything that acts on the block below sits at the right of its header
   instead of loose above or below the card. */
.card-header-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-left: auto;
}
.card-header .toolbar { margin: 0; flex: 1 1 auto; }

/* The mirror of the header, for the action that commits whatever the card
   contains. Same full-bleed band, so a card reads header / body / footer
   and the "Add to catalog" button is clearly the end of THAT form rather
   than a button loose on the page under it. */
.card-footer {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-5);
  background: var(--surface-sunken);
  border-top: 1px solid var(--line);
}
.card > .card-footer:last-child {
  margin: var(--space-4) calc(var(--card-px) * -1) calc(var(--card-px) * -1);
  padding-left: var(--card-px);
  padding-right: var(--card-px);
  border-radius: 0 0 calc(var(--radius) - 1px) calc(var(--radius) - 1px);
}
.card-footer .card-footer-note { color: var(--ink-soft); font-size: var(--text-base); margin: 0; }

/* An inline label that wraps its own checkbox or radio, rather than the
   block label the form rules assume. */
.checkbox-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--control-h);
  margin: 0;
  font-weight: 400;
}
.checkbox-label input { width: auto; min-height: 0; }

/* P2 - a labelled group of related controls inside a card. Recessed, so
   the group reads as cut into the card rather than as another card. Works
   on a <div> with a .field-group-title or on a real <fieldset>/<legend>. */
.field-group {
  background: var(--surface-sunken);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4) var(--space-4);
  margin: 0 0 var(--space-3);
  min-width: 0;
}
fieldset.field-group { border-color: var(--line-soft); }
.field-group-title,
.field-group > legend {
  display: block;
  float: none;
  width: auto;
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-faint);
  margin: 0 0 var(--space-3);
  padding: 0;
}
/* An input on the recessed group surface would be --input-bg on
   --surface-sunken - two shades that are within one step of each other.
   Same fix the table's filter row uses: inside a recessed group, controls
   step back UP to the raised surface so their edges stay visible. */
.field-group input,
.field-group textarea,
.field-group select { background: var(--card); }
.field-group > :last-child { margin-bottom: 0; }
.field-group > label:first-of-type,
.field-group > .field-group-title + label,
.field-group > legend + label { margin-top: 0; }

/* Controls laid out inside a group. .field is one label+control pair;
   .field-row puts several on one line and wraps them on a narrow screen
   instead of overflowing. */
.field-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: flex-end;
}
.field-row > * { flex: 1 1 190px; min-width: 0; }
.field { display: flex; flex-direction: column; min-width: 0; }
.field > label { margin: 0 0 var(--space-1); }
.field > .field-hint { margin: 0 0 var(--space-1); }
.field > input, .field > select, .field > textarea { width: 100%; }
.field-wide { flex: 2 1 260px; }
.field-narrow { flex: 0 1 120px; }
/* A control that is an action, not a field - it lines up on the baseline
   of the inputs beside it rather than being pushed down by a label. */
.field-row > button, .field-row > .button { flex: 0 0 auto; }

/* P3 - a subsection nested inside its parent card. The hairline plus the
   micro-label is the whole treatment: no second border, no second
   background, because it is not a second card. */
.subsection {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line-soft);
}
.subsection:first-child,
.card-header + .subsection { margin-top: 0; padding-top: 0; border-top: none; }
/* Same relationship, inside a list row rather than inside a card. A row's
   own separator is already a --line-soft hairline, so a second one across
   the row would read as another item rather than as this item's detail:
   nested-inside is a left rule and an indent here, the same way the nav
   marks a sub-list. */
.subsection-tight {
  margin-top: var(--space-2);
  padding-top: 0;
  padding-left: var(--space-3);
  border-top: none;
  border-left: 2px solid var(--line-soft);
}
.subsection-title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-faint);
  margin: 0 0 var(--space-2);
}
.subsection-title .info-icon { text-transform: none; letter-spacing: normal; }
.subsection-note {
  color: var(--ink-soft);
  font-size: var(--text-base);
  line-height: 1.5;
  max-width: 88ch;
  margin: 0 0 var(--space-3);
}
.subsection > :last-child { margin-bottom: 0; }

/* P4 - a strip of controls as one object rather than a stack of loose
   ones. Wraps, so a five-control filter row on a phone becomes two rows
   instead of a horizontal scroll. */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}
/* The three input types that carry width: 100% globally would each take a
   whole row of a toolbar. In here they share the row and grow instead. */
.toolbar > input[type="text"],
.toolbar > input[type="email"],
.toolbar > input[type="search"],
.toolbar > input[type="number"] { flex: 1 1 180px; width: auto; min-width: 0; }
.toolbar > label { margin: 0; }
.toolbar-spacer { margin-left: auto; }

/* Reusable data table (2026-09-10, dataTable.js) - sortable columns, a
   per-column filter row, a Columns show/hide picker, pagination. */
/* ---- The data-table panel -------------------------------------------
   dataTable.js renders .dt-toolbar, .dt-scroll and .dt-pagination as three
   siblings. 2026-09-11 (Chris, on tickets specifically: "Massive header" -
   then, of the reference he sent: "minimalistic but functional... no
   card-style padding or outer border") - this used to be one bordered,
   rounded-corner panel around all three; that framing is gone. What is left
   is a flat surface (still one shared background, so the three don't read
   as unrelated strips) with a single hairline between each part instead of
   a border, radius and shadow around the outside. Every row still shares
   --panel-px, so the toolbar's buttons, the first column header, the first
   cell and the pager all start on the same left edge. */
.dt-toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  min-height: 48px;
  margin-bottom: 0;
  padding: var(--space-2) var(--panel-px);
  position: relative;
  background: var(--card);
  border-bottom: 1px solid var(--line);
}
/* The page's own heading, folded in here (2026-09-11, "'Tickets' appears
   twice... fold [it] into the same PR" - a page hands its title/tooltip/
   primary action into this one bar via config rather than rendering its
   own heading, card header and toolbar stacked above it). Sized down from
   a standalone page h1 - this is one line in a 48px bar, not a headline -
   and margin: 0 since h1's default bottom margin would throw off the
   toolbar's own vertical centering. Empty and collapses to zero width when
   no title is configured, which is why .dt-toolbar can stay
   justify-content: space-between rather than needing a title-less special
   case: the actions group still lands hard against the right edge. */
.dt-title {
  font-family: var(--font-display);
  /* No font-size of its own (2026-09-20, backlog R10). dataTable.js renders
     this as <h1 class="dt-title">, so a page whose title comes from a data
     table is the SAME title as every other page's - it was the only h1 in the
     app shrunk to 16px, which is why Clients and Devices read as a different
     product from Dashboard and Billing. It inherits h1's one size now. */
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.01em;
  margin: 0;
  white-space: nowrap;
}
/* Now only in .dt-pagination (2026-09-11) - moved out of the toolbar so
   the toolbar holds nothing but the title and the right-aligned button
   group the reference calls for; dropped entirely would also have been
   fine ("or drop it if the sidebar counts already say it" - true on
   Clients/Tickets/Devices, not true of Audit/Compliance, which have no
   sidebar count), so it stays, just out of the way. */
.dt-count { color: var(--ink-soft); font-size: var(--text-base); font-variant-numeric: tabular-nums; }
.dt-toolbar-actions { position: relative; display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); }
.dt-columns-popover {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--space-1);
  min-width: 190px;
  max-height: 320px;
  overflow-y: auto;
  overscroll-behavior: contain;
  z-index: 20;
  animation: menu-in var(--motion-fast) var(--ease);
}
.dt-columns-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 30px;
  font-size: var(--text-base);
  font-weight: 400;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.dt-columns-item:hover { background: var(--hover-bg); }
.dt-columns-item input { width: auto; min-height: 0; }

/* The table scrolls inside its own box, vertically as well as horizontally
   (2026-09-11). dataTable.js offers page sizes up to 250 rows, and until now
   the whole page scrolled instead: on a long list the column headings and the
   filter row went off the top of the document, so re-sorting or re-filtering
   meant scrolling all the way back up. Capping the box is what gives the
   sticky thead below something to stick to - position: sticky pins to the
   nearest scrolling ancestor, and without a max-height here that ancestor is
   the document and nothing pins at all. min() so a short viewport still gets
   a usable table rather than a 720px box it has to scroll the page to see. */
.dt-scroll {
  overflow-x: auto;
  overflow-y: auto;
  max-height: min(70vh, 720px);
  background: var(--card);
}
/* The same table, under the name the expiry and website pages already use
   (2026-09-18, backlog §3 UI-R3). public/expiry.html and public/websites.html
   were built with class="data-table", styles.css only ever defined
   .dt-table, and data-table had ZERO rules - so those two tables rendered as
   unstyled text beside the rest of the app. Both names are kept deliberately:
   renaming the pages' markup is a larger change for no reader-visible
   difference, and the report pane's own table uses data-table too. */
.dt-table, .data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-base);
  /* Every column of ids, counts and money lines up on the decimal without
     each page having to remember to ask. */
  font-variant-numeric: tabular-nums;
}
.dt-table th, .dt-table td, .data-table th, .data-table td {
  text-align: left;
  /* Denser still as of 2026-09-11 (Chris, on the reference he sent:
     "streamline everything with lines, boxes that fit... minimalistic") -
     6px instead of 8px, with the dividers at the quieter rule weight so the
     grid stops shouting. --panel-px, not a local value, so the columns line
     up with the toolbar and pager above and below them. */
  padding: 6px var(--panel-px);
  border-bottom: 1px solid var(--line-soft);
  white-space: nowrap;
}
/* A header that reads as a header: sunken surface, a real rule under it
   rather than the same hairline as every row - and it stays put while the
   rows scroll under it. The background has to be opaque for that: a sticky
   cell is painted over the rows passing beneath it, and a transparent one
   shows them through. The rule under it is an inset shadow rather than
   border-bottom because with border-collapse: collapse the borders belong to
   the table, not to the cell, so a real border stays behind and scrolls away
   while the text it underlines does not. */
.dt-table thead tr:first-child th {
  position: sticky;
  top: 0;
  z-index: 3;
  color: var(--ink-soft);
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  background: var(--surface-sunken);
  border-bottom: none;
  box-shadow: inset 0 -1px 0 var(--line-strong);
  user-select: none;
}
.dt-table th.dt-sortable { cursor: pointer; transition: color var(--motion-fast) var(--ease); }
.dt-table th.dt-sortable:hover { color: var(--accent); }
.dt-sort-arrow { margin-left: var(--space-1); font-size: 9px; }
/* Label + sort arrow on the left, the per-column filter icon pushed to the
   far right via margin-left: auto on the icon itself - however many of the
   two left items are present, the icon still lands at the opposite edge. */
.dt-th-inner { display: flex; align-items: center; gap: var(--space-1); }
/* Filter icon lives in the header cell now, not a funnel in the toolbar
   (2026-09-11, Chris: "it should be on the columns... the filter button
   should be in the header column as well"). Deliberately not built from
   .ghost-button/.icon-button - those are full 32px controls, sized for a
   toolbar, and would make every header row that much taller for no reason;
   this is sized to sit inline with 11px uppercase header text instead. */
.dt-col-filter-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  min-height: 18px;
  padding: 0;
  margin-left: auto;
  flex-shrink: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: inherit;
  box-shadow: none;
  cursor: pointer;
}
.dt-col-filter-toggle:hover { background: var(--hover-bg); color: var(--accent); }
.dt-col-filter-toggle:active { background: var(--active-bg); }
.dt-col-filter-toggle svg { display: block; }
/* Shown whenever this column's filter has a value, independent of whether
   the user has also clicked it open - the one thing standing between a
   user and an invisibly narrowed list. */
.dt-col-filter-toggle.is-active { color: var(--accent); }
.dt-col-filter-toggle.is-active::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent);
  border: 1px solid var(--surface-sunken);
}
.dt-filter-input {
  width: 100%;
  min-width: 90px;
  min-height: var(--control-h-sm);
  margin-top: var(--space-1);
  padding: 2px var(--space-2);
  font-size: var(--text-sm);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  /* --card, not --input-bg: the header is already the sunken band, so the
     input in it steps back up rather than disappearing into it. */
  background: var(--card);
  color: var(--ink);
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
}
.dt-table tbody tr:last-child td, .data-table tbody tr:last-child td { border-bottom: none; }
/* Zebra so the eye can track a wide row, quiet enough that hover still
   wins clearly. No transition on either - a row highlight that fades in is
   the one place "modern" reads as laggy. */
.dt-table tbody tr:nth-child(even) td, .data-table tbody tr:nth-child(even) td { background: var(--row-alt); }
.dt-row { cursor: pointer; }
.dt-table tbody tr:hover td, .dt-row:hover td, .data-table tbody tr:hover td { background: var(--hover-bg); }
.dt-row-at-risk td:first-child { box-shadow: inset 3px 0 0 var(--warn-fg); }
.dt-empty, .data-table__empty { text-align: center; color: var(--ink-soft); padding: var(--space-6) var(--panel-px); white-space: normal; }
.dt-table tbody tr:nth-child(even) .dt-empty { background: none; }

.dt-pagination {
  display: flex;
  justify-content: flex-start;
  align-items: center;
  gap: var(--space-3);
  margin-top: 0;
  padding: var(--space-1) var(--panel-px);
  background: var(--card);
  border-top: 1px solid var(--line);
}
.dt-page-label { color: var(--ink-soft); font-size: var(--text-base); font-variant-numeric: tabular-nums; }
.dt-page-size-label { display: flex; align-items: center; gap: var(--space-2); color: var(--ink-soft); font-size: var(--text-base); margin-left: auto; }
.dt-page-size {
  font-size: var(--text-base);
  min-height: var(--control-h-sm);
  padding: 0 var(--space-1);
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--input-bg);
  color: var(--ink);
}

.line-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--line-soft);
}
.line-item:last-child { border-bottom: none; }

.line-item .desc { flex: 1; min-width: 0; font-weight: 500; }
.line-item .meta { color: var(--ink-soft); font-size: var(--text-sm); font-weight: 400; }
.line-item .amount { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* The client list inside Settings -> Staff access, "which clients' devices a
   person can see" (D13's control, 2026-09-22). A bounded, scrolling list
   because one organisation can hold more than a hundred clients and a card is
   not a page - the same rule the dashboard's rows follow, applied to a list
   somebody has to tick through. */
.device-scope-list { max-height: 320px; overflow-y: auto; overscroll-behavior: contain; border-top: 1px solid var(--line-soft); }
.device-scope-row { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-2) var(--space-2) 0; border-bottom: 1px solid var(--line-soft); cursor: pointer; }
.device-scope-row:last-child { border-bottom: none; }
.device-scope-row input { flex: none; }
.device-scope-row .desc { flex: 1; min-width: 0; font-weight: 500; }
.device-scope-row .meta { color: var(--ink-soft); font-size: var(--text-sm); font-weight: 400; white-space: nowrap; }
/* The Save button sits under a list that can be scrolled to its last row, so it
   needs its own separation rather than leaning on the list's last border -
   which is what the 390px picture showed it doing (2026-09-22). */
.device-scope-actions { margin-top: var(--space-4); align-items: center; }
.line-item .recurring-badge {
  display: inline-flex;
  align-items: center;
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--info-fg);
  background: var(--info-bg);
  border-radius: var(--radius-pill);
  padding: 1px var(--space-2);
  margin-left: var(--space-2);
  white-space: nowrap;
}

.total-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-top: var(--space-4);
  margin-top: var(--space-2);
  /* A single strong rule reads as a total; a 2px slab of --ink read as a
     divider someone forgot to finish. */
  border-top: 1px solid var(--line-strong);
  font-size: var(--text-xl);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.legal {
  white-space: pre-wrap;
  font-size: var(--text-base);
  line-height: 1.55;
  color: var(--ink-soft);
  max-height: 220px;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--surface-sunken);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-md);
  padding: var(--space-4);
}

label {
  display: block;
  font-size: var(--text-base);
  font-weight: 600;
  margin: var(--space-4) 0 var(--space-1);
}
/* Optional hint under a label. */
.field-hint { font-size: var(--text-sm); font-weight: 400; color: var(--ink-soft); margin: 2px 0 var(--space-1); }

/* ---- Form controls ---------------------------------------------------
   The whole point of this block is that a text input, a select, a textarea
   and a button are now the same height off the same --control-h token, so a
   toolbar row lines up on every page without each page guessing its own
   padding. width: 100% stays on exactly the three input types that already
   had it (pages rely on it, and several override it back down) - the other
   types get the same look without having their layout changed underneath
   them. */
input[type="text"], input[type="email"], input[type="password"] {
  width: 100%;
}
input[type="text"], input[type="email"], input[type="password"],
input[type="search"], input[type="number"], input[type="tel"],
input[type="url"], input[type="date"], input[type="datetime-local"],
input[type="time"], input[type="month"], textarea, select {
  min-height: var(--control-h);
  padding: 5px var(--space-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  font-size: var(--control-fs);
  font-family: inherit;
  line-height: 1.4;
  background: var(--input-bg);
  color: var(--ink);
  transition: border-color var(--motion-fast) var(--ease), box-shadow var(--motion-fast) var(--ease);
}
select { padding-right: var(--space-1); cursor: pointer; }
textarea { padding: var(--space-2); line-height: 1.5; resize: vertical; }

::placeholder { color: var(--ink-faint); opacity: 1; }

code, kbd, samp, pre, .mono {
  font-family: var(--font-mono);
  font-size: 0.92em;
}
code, kbd {
  background: var(--surface-sunken);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-xs);
  padding: 1px var(--space-1);
}
pre { background: var(--surface-sunken); border: 1px solid var(--line-soft); border-radius: var(--radius-md); padding: var(--space-3); overflow-x: auto; }

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--accent-hover); }
hr { border: none; border-top: 1px solid var(--line-soft); margin: var(--space-4) 0; }

input:hover:not(:disabled), select:hover:not(:disabled), textarea:hover:not(:disabled) { border-color: var(--line-strong); }

input:focus, textarea:focus, select:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--focus-halo, var(--accent-halo));
}
/* Only a mouse click loses the outline; keyboard focus keeps whatever
   :focus-visible ring the platform defines. */
input:focus:not(:focus-visible), textarea:focus:not(:focus-visible), select:focus:not(:focus-visible) { outline: none; }

/* Invalid. Border + halo + a small glyph, never the red border alone -
   :user-invalid rather than :invalid so an empty required field isn't red
   before it has been touched. */
input:user-invalid, textarea:user-invalid, select:user-invalid,
input[aria-invalid="true"], textarea[aria-invalid="true"], select[aria-invalid="true"] {
  border-color: var(--danger-fg);
  box-shadow: 0 0 0 3px var(--danger-bg);
}
.field-error {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--danger-fg);
  margin-top: var(--space-1);
}
.field-error::before { content: "!"; font-weight: 700; }

input:disabled, textarea:disabled, select:disabled {
  background: var(--disabled-bg);
  color: var(--disabled-fg);
  cursor: not-allowed;
}
input[readonly] { background: var(--surface-sunken); color: var(--ink-soft); }

input[type="checkbox"], input[type="radio"] {
  /* Native controls follow color-scheme; this only sizes them to a
     comfortable target and tints them to the platform accent. */
  width: 15px;
  height: 15px;
  min-height: 0;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}
input[type="file"] { font-size: var(--text-base); color: var(--ink-soft); }

/* ---- Buttons ---------------------------------------------------------
   Four ranks, one geometry (2026-09-10, Chris: "all the buttons look like
   shit"). Before this they were a 48px-tall 15px slab by default and
   whatever each page's own CSS happened to set everywhere else, so a
   toolbar could hold three different button heights at once.

     bare <button>   primary   - the one obvious action
     .ghost-button   secondary - the existing class, 77 uses, unchanged name
     .subtle-button  tertiary  - no border, for repeated/inline actions
     .danger-button  destructive

   All four share --control-h, --control-px, --radius-md and the same
   hover / :active / :disabled / [aria-busy] states. The hover is a colour
   step, not the old translateY lift: a button that jumps toward the cursor
   is exactly the "not snappy" feeling to avoid. */
button, .button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--control-h);
  font-family: inherit;
  font-size: var(--control-fs);
  font-weight: 600;
  line-height: 1.2;
  white-space: nowrap;
  padding: 5px var(--control-px);
  border-radius: var(--radius-md);
  border: 1px solid transparent;
  cursor: pointer;
  text-decoration: none;
  background: var(--accent);
  color: var(--accent-on);
  box-shadow: var(--shadow-sm);
  transition: background-color var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
button:hover, .button:hover { background: var(--accent-hover); color: var(--accent-on); }
button:active, .button:active { background: var(--accent-active); }
button:disabled, button[aria-disabled="true"], .button[aria-disabled="true"] {
  background: var(--disabled-bg);
  color: var(--disabled-fg);
  border-color: transparent;
  cursor: not-allowed;
  box-shadow: none;
}

/* Busy. A button mid-request stops accepting clicks and says so, instead of
   looking identical to an idle one. Set aria-busy="true" (which also tells a
   screen reader) or data-busy. */
@keyframes button-spin { to { transform: rotate(360deg); } }
button[aria-busy="true"], button[data-busy] {
  cursor: progress;
  pointer-events: none;
}
button[aria-busy="true"]::before, button[data-busy]::before {
  content: "";
  width: 12px;
  height: 12px;
  flex-shrink: 0;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  opacity: 0.85;
  animation: button-spin 600ms linear infinite;
}

/* Secondary. Reads as a real button on the page surface now - it used to be
   fully transparent with --ink-soft text, so on a card it looked more like a
   disabled control than a clickable one, and it turned accent-blue on hover
   which made every toolbar flicker between colour schemes. */
.ghost-button {
  background: var(--card);
  color: var(--ink);
  border: 1px solid var(--line);
  box-shadow: none;
}
.ghost-button:hover { background: var(--hover-bg); color: var(--ink); border-color: var(--line-strong); box-shadow: none; }
.ghost-button:active { background: var(--active-bg); border-color: var(--line-strong); }
.ghost-button:disabled, .ghost-button[aria-disabled="true"] {
  background: transparent;
  color: var(--disabled-fg);
  border-color: var(--line);
}

/* Tertiary - repeated row actions, panel close buttons, anything where a
   border per item would turn a list into a grid of boxes. */
.subtle-button {
  background: transparent;
  color: var(--ink-soft);
  border-color: transparent;
  box-shadow: none;
  font-weight: 500;
}
.subtle-button:hover { background: var(--hover-bg); color: var(--ink); }
.subtle-button:active { background: var(--active-bg); }
.subtle-button:disabled { background: transparent; color: var(--disabled-fg); }

/* A control that sits INSIDE a table or list row, where the full-height
   button would make every row taller than the text it belongs to. Three
   pages hand-rolled exactly this as `style="font-size: 12px; padding: 4px
   10px;"` (2026-09-18, backlog §3 UI-R5); it is one class now, so the size is
   decided in one place and no page carries presentation in a style attribute.
   It only sets what the hand-rolled version set, so nothing that already
   looked right moves. */
.row-button {
  font-size: var(--text-sm);
  padding: 4px 10px;
}
.row-button-row { display: flex; gap: 6px; }
/* The same idea for colour: a control whose text says "this is destructive"
   without being a solid red button. */
.danger-text { color: var(--status-error-fg); }

/* Destructive. Solid by default; add .ghost-button for the quieter outline
   version used for a "Remove" sitting inside a list of rows. */
.danger-button {
  background: var(--danger-solid);
  color: var(--danger-on);
  border-color: transparent;
}
.danger-button:hover { background: var(--danger-fg); color: var(--danger-on); }
.danger-button:active { background: var(--danger-solid); }
.ghost-button.danger-button, .subtle-button.danger-button {
  background: transparent;
  color: var(--danger-fg);
  border-color: var(--line);
}
.ghost-button.danger-button:hover, .subtle-button.danger-button:hover {
  background: var(--danger-bg);
  color: var(--danger-fg);
  border-color: var(--danger-fg);
}

/* Sizes and shapes. */
.button-sm { min-height: var(--control-h-sm); font-size: var(--text-sm); padding: 2px var(--space-2); gap: var(--space-1); }
.button-lg { min-height: var(--control-h-lg); font-size: var(--text-lg); padding: var(--space-2) var(--space-5); }
.button-block { width: 100%; }
.icon-button {
  width: var(--control-h);
  min-width: var(--control-h);
  padding: 0;
  border-radius: var(--radius-md);
}
/* A row of related buttons as one unit. */
.button-row { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

/* ---- Status message box ---------------------------------------------
   .status / .status.error / .status.success are used on nearly every page
   (~120 places) so the class names and the two existing modifiers stay
   exactly as they were; .warning, .info and .neutral are new siblings, and
   an empty box no longer holds open 40px of padding waiting for a message.
   Each carries a left bar as well as the tint, so "this went wrong" isn't
   signalled by hue alone. */
.status {
  margin-top: var(--space-4);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  border-left: 3px solid var(--line-strong);
  background: var(--neutral-bg);
  color: var(--ink);
  font-size: var(--text-base);
  line-height: 1.5;
}
.status:empty { display: none; }
.status.error { background: var(--danger-bg); color: var(--danger-fg); border-left-color: var(--danger-fg); }
.status.success { background: var(--success-bg); color: var(--success-fg); border-left-color: var(--success-fg); }
.status.warning { background: var(--warn-bg); color: var(--warn-fg); border-left-color: var(--warn-fg); }
.status.info { background: var(--info-bg); color: var(--info-fg); border-left-color: var(--info-fg); }
.status.neutral { background: var(--neutral-bg); color: var(--neutral-fg); border-left-color: var(--line-strong); }

/* ---- Badges ----------------------------------------------------------
   The design-system badge, plus one semantic set. .status-badge shares this
   base and then overrides most of it a few rules further down - that pill
   was already in use on five pages with its own look, and the two are kept
   apart rather than merged so neither has to change to suit the other. */
.badge, .status-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-xs);
  font-weight: 700;
  line-height: 1.5;
  letter-spacing: 0.02em;
  padding: 1px var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--neutral-bg);
  color: var(--neutral-fg);
  white-space: nowrap;
  vertical-align: middle;
}
/* The status pill itself. Five pages (approvals, clients, devices, patching,
   proposals) each carried their own byte-identical copy of this in their own
   <style> block; this is that copy, moved here once (2026-09-11). It sits
   after the .badge base deliberately - same specificity, later wins - so the
   shape below is what .status-badge actually renders as, and the .badge base
   above still fills in what neither declares (line-height, vertical-align).
   Every value here is what those five blocks already set, so the pages render
   exactly as before; the two places they genuinely disagreed are modifiers
   below rather than a flattening. */
.status-badge {
  display: inline-block;
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--ink-soft);
  background: var(--badge-bg);
  border-radius: 999px;
  padding: 2px 8px;
  margin-left: 8px;
  text-transform: capitalize;
  white-space: nowrap;
}
/* Approvals is the one page whose badge does not trail a label - it sits in
   its own flex cell, where the shared 8px indent reads as a stray gap. */
.status-badge.standalone { margin-left: 0; }

/* Badge states. Same vocabulary on every page, so "failed" is the same red
   in an approval as it would be anywhere else. Devices deliberately tints
   the text only and keeps the neutral pill behind it (2026-09-10: the
   light-only amber/green/red fills went near-invisible on the dark
   --badge-bg), so those three stay text-colour rules, not fills. */
.status-badge.executed,
.status-badge.signed,
.status-badge.active,
.status-badge.resolved { color: var(--status-success-fg); background: var(--status-success-bg); }
.status-badge.failed,
.status-badge.rejected { color: var(--status-error-fg); background: var(--status-error-bg); }
.status-badge.inactive,
.status-badge.closed { color: var(--status-neutral-fg); background: var(--status-neutral-bg); }
.status-badge.approving,
.status-badge.sent { color: var(--accent); }
.status-badge.assigned { color: var(--status-success-fg); }
.status-badge.unassigned { color: var(--status-warn-fg); }
.status-badge.revoked { color: var(--status-error-fg); }
/* Tickets (2026-09-11, Chris: "Status values are rendering raw... in his
   reference, status is a small chip rather than plain text") - open reads
   as new/neutral information, the three "someone or something is holding
   this up" states share one amber, waiting-on-parts included since it's
   the same shape as on_hold from a glance at the list. */
.status-badge.open,
.status-badge.prospect { color: var(--status-info-fg); background: var(--status-info-bg); }
.status-badge.in_progress,
.status-badge.on_hold,
.status-badge.waiting_on_parts,
.status-badge.imported_pending_review { color: var(--status-warn-fg); background: var(--status-warn-bg); }

.badge-success { background: var(--success-bg); color: var(--success-fg); }
.badge-warning { background: var(--warn-bg); color: var(--warn-fg); }
.badge-danger { background: var(--danger-bg); color: var(--danger-fg); }
.badge-info { background: var(--info-bg); color: var(--info-fg); }
.badge-neutral { background: var(--neutral-bg); color: var(--neutral-fg); }
/* A dot for a badge that has to survive being read in greyscale or by
   someone who can't separate the hues - the word is always there too. */
.badge-dot::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}

.signed-banner {
  text-align: center;
  padding: var(--space-8) var(--space-5);
}
.signed-banner .check {
  font-size: 48px;
  line-height: 1;
  color: var(--success);
}

.favorite-star {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  background: transparent;
  border: none;
  box-shadow: none;
  color: var(--star-empty);
  font-size: var(--text-lg);
  line-height: 1;
  padding: 2px var(--space-1);
  cursor: pointer;
  /* Colour only. This one renders once per row in tables hundreds of rows
     long, so the old scale(1.15) was the single most expensive hover on the
     platform for no information gained. */
  transition: color var(--motion-fast) var(--ease);
}
.favorite-star:hover { color: var(--star-filled); background: transparent; }
.favorite-star.favorited { color: var(--star-filled); }

/* Shared permanent left-side "views" sidebar (2026-09-10, Chris: "permanent
   side menus for tickets clients devices" - originally built just for
   Tickets, generalized here so Clients/Devices reuse the same look instead
   of re-styling their own). One page = one dimension of quick filter
   buttons with live counts, not a dropdown. */
/* ---- Sub-menus (the per-page views column) ---------------------------
   Tickets/Clients/Devices/Proposals all render one of these down the left of
   their list. This is the second half of "the side menu, sub menus ... look
   like shit": it used to be a drop-shadowed white card that floated
   mid-page, so it read as a widget that had come loose rather than as a
   sub-level of the nav it sits under. Now it is a flat sunken panel with a
   micro-label header, tighter rows, a neutral hover, and the same
   accent-tint + left-bar treatment as the primary nav's current item - so
   "where am I" looks the same at both levels. */
/* The sub-menu column and the panel beside it were two separate raised
   boxes with a gap between them, so a page's filters read as a widget
   parked next to the list rather than as the list's own rail - and,
   because the bulk-action row sits between them in the markup on Clients
   and Tickets, the two boxes started at different heights the moment
   anything was selected (the one follow-up the surface pass could not do
   in CSS alone). They are one panel now: ONE border, ONE radius, the rail
   recessed inside the left edge of it and running the panel's full height,
   the main column filling the rest. The children below give up their own
   edges so there is exactly one, and align-items: stretch is what makes
   the height question disappear rather than being worked around. */
.view-layout {
  display: flex;
  align-items: stretch;
  gap: 0;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  margin-bottom: var(--space-4);
}
.view-sidebar {
  width: 190px;
  flex-shrink: 0;
  /* Recessed, because it is now INSIDE the panel rather than a second
     panel beside it - the same relationship a table header has to its
     table. One shared edge on the right, no edge anywhere else. */
  background: var(--surface-sunken);
  border: none;
  border-right: 1px solid var(--line);
  border-radius: calc(var(--radius) - 1px) 0 0 calc(var(--radius) - 1px);
  box-shadow: none;
  padding: var(--space-2) var(--space-2) var(--space-4);
}
.view-sidebar-title {
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-faint);
  padding: var(--space-2) 10px var(--space-1);
}
.view-sidebar-items { display: flex; flex-direction: column; gap: 1px; }
.view-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: 30px;
  background: none;
  border: 1px solid transparent;
  box-shadow: none;
  color: var(--ink-soft);
  font-size: var(--text-base);
  font-weight: 500;
  text-align: left;
  padding: var(--space-1) 10px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.view-item:hover { background: var(--hover-bg); color: var(--ink); box-shadow: none; border-color: transparent; }
.view-item:active { background: var(--active-bg-strong); }
.view-item.active, .view-item[aria-selected="true"] {
  background: var(--active-bg);
  color: var(--accent);
  font-weight: 600;
  box-shadow: inset 3px 0 0 var(--accent);
}
/* A long view label has to wrap inside the rail, not spill past its right
   edge (2026-09-11, Chris: "Approvals, stuff doesn't fit in sub menu" -
   Approvals is the page with the longest ones: "Onboarding & offboarding",
   "Billing, licences & email"). The label is the flexible half of the row and
   the count is the fixed half, so the label gets min-width: 0 to let it
   shrink past its own min-content, and overflow-wrap so a single long word
   breaks rather than pushing the count out of the button. Wrapped, not
   truncated with an ellipsis: these labels are how you tell the views
   apart, and half of one is not something to make somebody hover to read.
   The row's min-height still holds the one-line case at exactly the height
   it was. */
.view-item > span:not(.view-item-count) {
  min-width: 0;
  /* white-space explicitly, because .view-item is a <button> and the shared
     `button, .button` rule above sets white-space: nowrap (right for a
     control whose label is two words, wrong for a rail item whose label is
     four). That inherited nowrap is what stopped this wrapping and pushed
     the text out through the rail's right edge. */
  white-space: normal;
  overflow-wrap: anywhere;
}
.view-item-count {
  color: var(--ink-faint);
  font-size: var(--text-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.view-item.active .view-item-count { color: var(--accent); }

@media (max-width: 860px) {
  /* The rail becomes the panel's top band instead of its left one, so the
     shared edge moves with it. (These live here rather than in the shell's
     media query near the top of the file: that block is declared before
     .view-sidebar is, and at equal specificity the later rule wins.) */
  /* Only the edges live here now. flex-direction, the rail's own width and
     padding, and the .view-sidebar-items / .view-item row rules were all
     restated verbatim by the mobile-and-PWA pass at the bottom of this file,
     which also adds align-items: stretch and sizes .view-main - so they are
     declared once, there, rather than twice with the second copy silently
     winning. These three borders are not in that copy, so they stay. */
  .view-sidebar {
    border-right: none;
    border-bottom: 1px solid var(--line);
    border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
  }
  .view-layout > .view-main > .card:first-child > .card-header:first-child { border-radius: 0; }
}
.view-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
/* Inside the merged panel a card is a SECTION of it, not a card: no edge,
   no second background, a hairline where two sections meet. */
.view-layout > .view-main > .card {
  --card-px: var(--space-4);
  border: none;
  background: none;
  border-radius: 0;
  margin: 0;
  padding: var(--space-4);
}
.view-layout > .view-main > .card + .card { border-top: 1px solid var(--line-soft); }
.view-layout > .view-main > .card:first-child > .card-header:first-child { border-radius: 0 calc(var(--radius) - 1px) 0 0; }
.view-layout > .view-main > .card + .card > .card-header:first-child { border-radius: 0; }
.view-layout > .view-main > .card:has(> .dt-toolbar) { padding: 0; }
/* The data-table panel draws no outer border of its own at all now (2026-
   09-11, "no card-style padding or outer border") - just the internal
   hairlines between toolbar / table / pager, which is all that is left to
   keep border-free here too. */
.view-main .dt-toolbar,
.view-main .dt-scroll,
.view-main .dt-pagination {
  border-left: none;
  border-right: none;
}
/* The selection bar Clients and Tickets show when rows are ticked. It was
   a loose strip wedged between the rail and the table; it is a contextual
   band across the top of the panel's main column now, tinted so it reads
   as a temporary state of the list rather than another permanent control
   row. */
.view-layout > .view-main > .toolbar {
  /* !important, used once and deliberately: Clients and Tickets both set
     `style="margin-bottom: 10px"` on this bar inline, from when it was a
     free-floating strip, and an inline declaration beats any selector.
     Left alone it opens a 10px white slot between the band and the table
     header inside the panel. The inline styles should come off those two
     pages (see the PR notes); this stops it looking broken until they do. */
  margin: 0 !important;
  padding: var(--space-2) var(--panel-px);
  gap: var(--space-3);
  background: var(--accent-soft);
  border-bottom: 1px solid var(--line);
}

/* ---- Surface corrections for page-owned components -------------------
   A handful of components are declared in their own page's <style> block
   but are part of this stylesheet's surface layering, and getting the
   layering right needs them to know which level they are on. Rather than
   edit those blocks (several agents are in these files, and a page-local
   rule is the wrong place for a platform-wide decision), the shared sheet
   claims just the surface properties, at one step more specificity. Each
   of these sets ONLY background / border / shadow - never the colours,
   spacing or type the page owns. */

/* Dashboard tiles declare background: var(--card) and sat white-on-white
   inside the white section card that contains them, with a second border
   drawn around each to compensate. Inside a card they are recessed wells
   instead, and the border comes off - except on the alert tiles, whose
   coloured border is declared at the same specificity later in the page
   and so still wins, which is exactly the wanted outcome: the tiles that
   mean something keep an outline, the neutral ones recede. */
.card .widget-tile {
  background: var(--surface-sunken);
  border-color: transparent;
  box-shadow: none;
}
/* Was a box-shadow transition on hover - the one transition the motion
   budget rules out, on a grid of tiles. A surface step costs a repaint. */
.card .widget-tile a:hover { box-shadow: none; }
.card .widget-tile:hover { background: var(--hover-bg); }

/* Any button acting as a menu row rather than an action - see the note
   above. Fills in only what those rules never declared. */
.settings-nav button, .nav-menu-item {
  min-height: 32px;
  box-shadow: none;
  letter-spacing: normal;
}
.settings-nav button:active { background: var(--active-bg-strong); }
.settings-nav button.active { box-shadow: inset 3px 0 0 var(--accent); }
.settings-layout .settings-nav button.active { background: var(--active-bg); }
/* The heading over each group of entries (2026-09-17, docs/UI-STANDARD.md §4:
   "Headers, not a flat strip of sixteen buttons"). Not a control - it is not a
   button and takes no click - so it reads as a label over the entries under
   it and cannot be mistaken for an option that does nothing. */
.settings-nav-group {
  margin: 14px 0 4px;
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted-fg);
}
.settings-nav-group:first-child { margin-top: 0; }

/* Shared right-side slide-over panel (2026-09-10) - originally built just
   for the Clients detail pane ("when we click on a client, I want it to
   pane to the right as if opening an email"), generalized here so any page
   can open the same kind of panel for anything - a detail view, a form,
   whatever - without re-styling its own overlay each time. */
/* ---- Slide-over panel -----------------------------------------------
   The "pane in from the right" pattern (2026-09-10, Chris: "make things
   expand or pane in or whatever, make it modern as, but fast"). Kept
   deliberately cheap: the panel animates transform only and the backdrop
   opacity only, both 160ms, so neither touches layout - and will-change
   hands the panel to the compositor before the first open rather than
   during it. The drop shadow is static; animating shadow spread is what
   makes this pattern feel heavy elsewhere. */
.slide-over-backdrop {
  position: fixed;
  inset: 0;
  background: var(--overlay);
  z-index: 40;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-panel) var(--ease);
}
.slide-over-backdrop.open { opacity: 1; pointer-events: auto; }
.slide-over-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(560px, 92vw);
  background: var(--card);
  border-left: 1px solid var(--line);
  box-shadow: var(--shadow-panel);
  z-index: 41;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-5);
  transform: translateX(100%);
  transition: transform var(--motion-panel) var(--ease);
  will-change: transform;
}
.slide-over-panel.open { transform: translateX(0); }
/* Header row for a panel - title left, close right, holds its place while
   the body scrolls under it. */
.slide-over-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  position: sticky;
  top: calc(var(--space-5) * -1);
  margin: calc(var(--space-5) * -1) calc(var(--space-5) * -1) var(--space-3);
  padding: var(--space-4) var(--space-5);
  background: var(--card);
  border-bottom: 1px solid var(--line-soft);
  z-index: 1;
}
/* A pane header wears the organisation's colour when it has one (D5,
   2026-09-19). Gated on the attribute businessUnitNav sets only for a branded
   organisation, so an unbranded one - and "All", which carries no branding -
   keeps exactly the header it had. */
html[data-org-branded="true"] .slide-over-header { box-shadow: inset 0 -2px 0 0 var(--accent); }
.slide-over-title { font-family: var(--font-display); font-size: var(--text-lg); font-weight: 700; margin: 0; }
.slide-over-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-h);
  min-width: var(--control-h);
  min-height: var(--control-h);
  background: none;
  border: 1px solid transparent;
  box-shadow: none;
  font-size: var(--text-xl);
  line-height: 1;
  cursor: pointer;
  color: var(--ink-soft);
  padding: 0;
  border-radius: var(--radius-md);
  transition: background-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.slide-over-close:hover { color: var(--ink); background: var(--hover-bg); }

/* The full-screen control every pane carries (2026-09-14, the same three
   gestures on tickets, clients, compliance and devices - see
   public/ticketWorkspace.js's PaneRules, which is the only thing that
   touches these two classes). It is a named action rather than a glyph
   because it changes what the pane IS, and it reads the same wherever it
   appears: the pane you are looking at, the size you want it. */
.slide-over-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--control-h);
  padding: 0 var(--space-3);
  background: none;
  border: 1px solid transparent;
  box-shadow: none;
  border-radius: var(--radius-md);
  color: var(--ink-soft);
  font-size: var(--text-sm);
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.slide-over-action:hover { color: var(--ink); background: var(--hover-bg); }
.slide-over-action[aria-pressed="true"] {
  color: var(--ink);
  background: var(--active-bg);
  box-shadow: inset 0 0 0 1px var(--line);
}

/* Full screen is a size and nothing else, one rule per pane shape: a side
   pane takes the window's width, the tickets' bottom pane takes its height.
   Both are additive - the class goes on and comes off, so the width or the
   height a pane already had is what returns. A phone gets the side pane at
   full width already, so this changes nothing there. */
.slide-over-panel.pane-fullscreen { width: 100%; }
.ticket-pane.pane-fullscreen { height: 100vh; }

/* ---- Expand / disclosure --------------------------------------------
   The other half of "expand or pane in": a row that opens in place. Plain
   <details>/<summary>, so it needs no JS and no new markup on any page -
   approvals.html and settings.html already use the element. The chevron
   rotates on transform and the body fades in over 140ms; height itself is
   never animated, which is what keeps this from costing a layout pass per
   frame. */
details.disclosure {
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--card);
  margin-bottom: var(--space-2);
}
details.disclosure > summary,
.disclosure-summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--control-h);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
  list-style: none;
  border-radius: var(--radius-md);
  transition: background-color var(--motion-fast) var(--ease);
}
details.disclosure > summary::-webkit-details-marker,
.disclosure-summary::-webkit-details-marker { display: none; }
details.disclosure > summary:hover, .disclosure-summary:hover { background: var(--hover-bg); }
details.disclosure > summary::after,
.disclosure-summary::after {
  content: "";
  margin-left: auto;
  width: 0;
  height: 0;
  border: 4px solid transparent;
  border-top-color: currentColor;
  transform: translateY(2px);
  transition: transform var(--motion-fast) var(--ease);
}
details.disclosure[open] > summary::after { transform: rotate(-180deg) translateY(2px); }
details.disclosure > summary + * { padding: 0 var(--space-3) var(--space-3); }

@keyframes disclosure-in {
  from { opacity: 0; transform: translateY(-2px); }
  to { opacity: 1; transform: translateY(0); }
}
details[open] > summary ~ * { animation: disclosure-in var(--motion) var(--ease); }

/* Shared detail-tabs strip (2026-09-10) - same tab-strip look as Tickets'
   open-ticket tabs, promoted here since it's now used both in Clients'
   detail pane and Devices' add-device slide-over. */
.detail-tabs {
  display: flex;
  gap: var(--space-4);
  margin: var(--space-3) 0 var(--space-4);
  overflow-x: auto;
  border-bottom: 1px solid var(--line);
  scrollbar-width: none;
}
.detail-tabs::-webkit-scrollbar { display: none; }
.detail-tabs .tab {
  background: none;
  color: var(--ink-soft);
  border: none;
  border-bottom: 2px solid transparent;
  box-shadow: none;
  border-radius: 0;
  min-height: 34px;
  /* gap above already spaces these; the old rule had BOTH a gap and a
     14px right margin, which is why the last tab was cut in half. */
  padding: var(--space-2) 0;
  margin-right: 0;
  font-size: var(--text-base);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}
.detail-tabs .tab:hover { color: var(--ink); background: none; }
.detail-tabs .tab.active, .detail-tabs .tab[aria-selected="true"] { color: var(--accent); border-bottom-color: var(--accent); }

/* Header info-icon tooltip (2026-09-10, Chris: "why is there little
   explanations under each header... put a little i in a circle beside it
   with a tooltip") - replaces a visible <p class="subtitle"> explanation
   directly under a card/page header with a small (i) glyph, the same text
   shown on hover via the native title attribute (no JS, no custom tooltip
   component needed - every browser already does this). Only ever used for
   a STATIC explanation right next to its header; dynamic status/empty-state
   text (e.g. "No devices match these filters") stays a plain .subtitle
   paragraph, unchanged. */
/* A real CSS tooltip (2026-09-10, Chris: "the tooltips for i's still say
   nothing") rather than the native title attribute this started as - title
   tooltips depend on OS/browser hover-delay settings and can be suppressed
   entirely in some environments, which is exactly what was happening here.
   content: attr(data-tooltip) means the text still lives in one HTML
   attribute (data-tooltip instead of title, same escaped value), just
   rendered by this CSS instead of left to the browser's own native popup. */
.info-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--neutral-bg);
  color: var(--ink-soft);
  font-size: var(--text-xs);
  font-weight: 700;
  font-style: normal;
  font-family: Georgia, serif;
  margin-left: 6px;
  cursor: help;
  vertical-align: middle;
  flex-shrink: 0;
}
/* Generic - any element (not just .info-icon) can opt into this by carrying
   position: relative and a data-tooltip attribute. */
[data-tooltip]:hover::after,
[data-tooltip]:focus::after {
  content: attr(data-tooltip);
  position: absolute;
  /* BELOW, not above (2026-09-11, Chris: "tooltips open above not below,
     which can throw them off the page"). This is the no-JS fallback; the
     measured, viewport-aware placement lives in tooltip.js, which suppresses
     this pseudo-element entirely. Downward is the safe default here because
     every (i) in this app sits beside a heading near the top of the window,
     where there is no room above and plenty below. */
  top: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: 280px;
  background: var(--ink);
  color: var(--bg);
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: var(--text-sm);
  line-height: 1.4;
  white-space: normal;
  padding: 8px 10px;
  border-radius: 8px;
  box-shadow: var(--shadow);
  z-index: 50;
  pointer-events: none;
}

/* ---- The JS-placed tooltip (2026-09-11) ---------------------------------
   One shared node, appended to <body> by tooltip.js, positioned at hover
   time against the real viewport. Same look as the pseudo-element above -
   only the placement differs, because only JS can measure whether there is
   room below before choosing a side. When tooltip.js has run it puts
   `has-js-tooltips` on <html> and the pseudo-element stands down, so exactly
   one of the two ever renders.
   Kept as one contiguous block, deliberately: this is the whole of the
   change to this file apart from the `bottom:` -> `top:` line above. */
html.has-js-tooltips [data-tooltip]:hover::after,
html.has-js-tooltips [data-tooltip]:focus::after { content: none; }

.tooltip-pop {
  position: fixed;
  /* top/left are written by tooltip.js. */
  top: 0;
  left: 0;
  width: max-content;
  max-width: 280px;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--ink);
  color: var(--bg);
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: var(--text-sm);
  line-height: 1.4;
  white-space: normal;
  text-align: left;
  text-transform: none;
  letter-spacing: normal;
  padding: 8px 10px;
  border-radius: 8px;
  box-shadow: var(--shadow);
  /* Above the slide-over and the grid's column popover, which is where a
     tooltip most often has to be readable. */
  z-index: 200;
  pointer-events: none;
}

/* Shared label:value detail row + list item (2026-09-10) - promoted from
   Clients' detail pane since Devices' new detail pane reuses both. */
.detail-field-row { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); flex-wrap: wrap; }
.sub-list-item { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); padding: var(--space-2) 0; border-bottom: 1px solid var(--line-soft); font-size: var(--text-base); }
.sub-list-item:hover { color: var(--ink); }
.sub-list-item:last-child { border-bottom: none; }

/* ============================================================
   Phone layout (2026-09-10, Chris: the mobile version should have "just
   about everything from the Web app, but mobile friendly, since everything
   is so simplified").

   Everything phone-specific lives HERE, at the very bottom of the file, on
   purpose. Two of these blocks override rules that are declared much
   further up (.view-* is ~800 lines above the shell's own 860px block, and
   several page-level <style> blocks load after this stylesheet does), and
   CSS resolves ties by document order - an override written above the rule
   it is meant to beat silently does nothing. Keeping the whole mobile pass
   as the last thing in the file makes that impossible to get wrong again.

   Where a rule has to beat a page's own inline <style> (a page's <style>
   element comes after <link href="styles.css">, so at equal specificity the
   page wins) the selector is prefixed with `body` purely to add one point
   of specificity. Those are marked.
   ============================================================ */

@media (max-width: 860px) {
  /* --- The views rail becomes a band of chips above the content ---------
     .view-layout / .view-sidebar / .view-item are declared ~800 lines above
     this point; these overrides have to come after them to win. Written to
     work both against the current markup (title + .view-item buttons as
     direct children of .view-sidebar) and against the .view-sidebar-items
     wrapper the design-system branches introduce. */
  /* align-items: stretch, not the desktop rule's flex-start: in a column
     flex container flex-start sizes each item to fit-content, and the
     content here is a table of nowrap cells, so .view-main sized itself to
     the widest row and pushed the whole document sideways (measured at
     420px: a 1246px-wide page). Stretch pins it to the column's width and
     the table scrolls inside its own .dt-scroll box, as intended. */
  .view-layout { flex-direction: column; align-items: stretch; }
  .view-main { width: 100%; min-width: 0; }
  .view-sidebar {
    width: auto;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    padding: 8px;
  }
  .view-sidebar-title { width: 100%; padding: 2px 6px 4px; }
  .view-sidebar-items { display: flex; flex-direction: row; flex-wrap: wrap; gap: 4px; width: 100%; }
  /* gap, because the desktop rule is justify-content: space-between across
     a full-width row - once the button is only as wide as its own content
     the label and its count end up touching ("All tickets4"). */
  .view-item { width: auto; flex: 0 0 auto; padding: 7px 12px; gap: 6px; }

  /* --- The nav is one scrollable strip, not four wrapped rows -----------
     Thirteen links wrapping at 420px used to fill most of a phone screen
     before the page's own content started. */
  .nav {
    flex-direction: row;
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: 2px;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .nav::-webkit-scrollbar { display: none; }
  /* Fades the last few pixels so it reads as "there is more to the right"
     rather than as a link that happens to be cut off. */
  .nav {
    -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 24px), transparent 100%);
    mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 24px), transparent 100%);
  }
  .nav a { flex-shrink: 0; white-space: nowrap; }
  /* Favourites become chips on the same row rather than one per line. */
  .sidebar-favorite { flex: 0 0 auto; max-width: 100%; }

  /* --- Floating furniture stops covering the content -------------------
     The profile avatar is position: fixed to the viewport's top-right. On a
     phone the sidebar is a top band rather than a left column, so that
     corner is the nav strip's own corner, and once you scroll the avatar
     floats over whatever row happens to be under it. position: absolute
     parks it at the top of the document instead, so it scrolls away like
     everything else; its dropdown still anchors to #profileMenu, which
     carries its own position: relative. */
  .profile-menu-fixed { position: absolute; top: 14px; right: 14px; }
  .sidebar { padding-right: 60px; }
  /* The rocket/notifications cluster stays pinned bottom-right (it is meant
     to float), so the page reserves the height it occupies - otherwise it
     sits on top of the last table row or the pagination controls. */
  .app-shell .page { padding: 20px 16px 88px; }
  .sidebar-actions { bottom: 14px; right: 14px; }
  .notif-dropdown { width: min(300px, calc(100vw - 28px)); }
  .profile-dropdown { max-width: calc(100vw - 28px); }

  /* --- Slide-over panel takes the whole screen ------------------------- */
  .slide-over-panel { width: 100%; padding: 16px; }

  /* --- Toolbars stack instead of squeezing ----------------------------- */
  .dt-toolbar { flex-wrap: wrap; gap: 8px; justify-content: flex-start; }
  .dt-toolbar-actions { margin-left: auto; }
  .dt-pagination { flex-wrap: wrap; gap: 10px; justify-content: flex-start; }
  .dt-page-size-label { margin-left: 0; }
  .page-topbar { flex-wrap: wrap; gap: 10px; }
  /* The 56px reservation exists to clear the fixed avatar. Once the topbar
     wraps onto its own row the avatar is no longer beside it, so the gap is
     just dead space pushing buttons off the edge. */
  .page-topbar-right { padding-right: 0; }

  /* Page-level toolbars/filter rows. `body` prefix: these classes are
     declared in each page's own <style>, which loads after this file. */
  body .toolbar,
  body .filters,
  body .add-form,
  body .add-client-form,
  body .row-actions,
  body .prompt-row { flex-wrap: wrap; }
  body .toolbar > select,
  body .toolbar > input,
  body .filters > select,
  body .filters > input { flex: 1 1 140px; min-width: 0; }

  /* Settings has its own copy of the rail pattern in its page <style>
     (.settings-layout / .settings-nav), with the same align-items:
     flex-start that pushed Tickets sideways - measured at 420px it made
     the page 454px wide. Same fix, and the nav becomes a row of chips
     above the cards like every other rail. `body` prefix because these
     classes are declared in settings.html's own <style> - and it needed the
     whole rule, not the flex direction alone: measured on 2026-09-22, the
     list still drew as sixteen full-width buttons with six headings above
     them, about 1,100px of menu before the section the reader asked for.
     The page's own rule sets `width: 100%` on a nav button, which beats a
     `flex: 0 0 auto` that never touched the width. */
  body .settings-layout { flex-direction: column; align-items: stretch; }
  body .settings-nav { width: 100%; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 4px; }
  body .settings-nav button { width: auto; flex: 0 0 auto; padding: 7px 12px; }
  /* The heading keeps its own line rather than becoming one more chip: it is
     a label over the entries under it (see the rule's comment in
     public/styles.css's desktop block), and a heading in the middle of a
     wrapped row is a heading nobody can see the edges of. */
  body .settings-nav-group { flex: 0 0 100%; margin: 8px 0 2px; }
  body .settings-nav-group:first-child { margin-top: 0; }
  body .tax-line-row { flex-wrap: wrap; }

  /* --- Line items and detail rows read as stacked pairs ---------------- */
  .line-item { flex-wrap: wrap; gap: 4px; }
  .line-item .amount { margin-left: auto; }
  /* The heading above has said "stacked pairs" since 2026-09-10 and the row
     did not stack: .detail-field-row is a wrapping flex row whose label
     carries an inline `min-width: 140px` in the page's own markup, so at 390px
     a long value wrapped inside whatever was left beside it - measured in the
     device pane on 2026-09-22, where "Operating system" and its value sat on
     two lines while "Disk encryption" and its value shared one. The label now
     takes its own line and the value the next one, whatever the label's own
     inline width says. */
  .detail-field-row { align-items: flex-start; }
  .detail-field-row > :first-child { flex: 1 1 100%; min-width: 0; }
}

/* Touch sizing, at a wider breakpoint than the layout rules (2026-09-20,
   backlog R10, closing F19). Safari on iOS zooms the whole page whenever a
   focused field's text is under 16px, and an iPad between 620 and 860 is
   still a touch device - it was getting the zoom because the breakpoint was
   sized for phones. Only the CONTROLS grow here; the phone layout below stays
   at 620, because an iPad is not a phone. */
@media (max-width: 860px) {
  /* --- Controls are finger-sized, and never trigger iOS zoom-on-focus ---
     Safari on iOS zooms the whole page in whenever a focused field's text is
     under 16px, and then does not zoom back out. 16px on every real input is
     the only way to stop it. */
  input[type="text"], input[type="email"], input[type="password"],
  input[type="number"], input[type="search"], input[type="tel"], input[type="date"],
  textarea, select { font-size: 16px; }
  button, .ghost-button { min-height: 42px; padding: 10px 16px; }
  .view-item, .nav a, .profile-dropdown a, .profile-dropdown button { min-height: 38px; }
  /* Small icon-ish buttons keep their own size - a 42px minimum would turn
     these into blocks. */
  .notif-button, .profile-avatar, .slide-over-close, .favorite-star,
  .views-list-item .view-delete { min-height: 0; }

  /* Filter inputs inside a table header stay compact: the header row is
     already horizontally scrollable, and 16px there would double the width
     of every column. Tapping one does zoom, which is the lesser problem. */
  .dt-filter-input { font-size: 12.5px; }
}

@media (max-width: 620px) {
  /* --- Typography and spacing ------------------------------------------ */
  /* No h1 override here (2026-09-20, R10): one page title means one size, and
     a phone that rendered the title larger than the desktop did was the
     ladder arguing with itself. */
  .card { padding: 16px; }
  .page { padding: 20px 16px 40px; }
  .subtitle { font-size: var(--text-base); }

  /* --- Tables scroll horizontally, and say so -------------------------- */
  .dt-scroll { -webkit-overflow-scrolling: touch; }
  .dt-table, .data-table { font-size: var(--text-base); }
  .dt-table th, .dt-table td, .data-table th, .data-table td { padding: 9px 10px; }
  /* A phone has far less height to give away than a laptop, and the page
     around the table is short - let it use most of the viewport. */
  .dt-scroll { max-height: 75vh; }

  /* --- Tooltips can't run off the edge of a phone ----------------------
     The desktop tooltip is centred over its (i) and grows in both
     directions, which at 420px runs off whichever edge the icon is nearest.
     On a phone it becomes a band pinned to the bottom of the viewport
     instead: always fully on screen, whatever it is attached to. */
  [data-tooltip]:hover::after,
  [data-tooltip]:focus::after {
    position: fixed;
    top: auto;
    bottom: 16px;
    left: 16px;
    right: 16px;
    width: auto;
    max-width: none;
    transform: none;
  }

  /* --- Forms: one field per row ---------------------------------------- */
  body .add-form input, body .add-form select,
  body .add-client-form input, body .add-client-form select { flex: 1 1 100%; min-width: 0; }
  body .add-form button, body .add-client-form button { width: 100%; }

  /* --- The AI-quote dictation row (branch claude/ai-drafted-quotes) -----
     The textarea and the microphone button share one row on a desktop. At
     phone width that leaves the textarea about 250px wide, so the button
     moves under it and spans the full width - which is also the easier
     thing to hit while holding the phone up to speak. The button itself is
     the page's own browser-native one; nothing here changes its behaviour,
     and it stays hidden where SpeechRecognition is unsupported. */
  body .prompt-row { flex-direction: column; align-items: stretch; }
  body .prompt-row #micButton { width: 100%; padding: 10px 16px; }
}

/* Focus ring, continued (2026-09-10). The bare `:focus-visible` rule near the
   top of this file draws the outline; these three add the halo and the two
   places where a 2px offset is wrong. Kept here as the component-level
   override that rule's own comment invites, rather than editing it. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  box-shadow: 0 0 0 4px var(--focus-halo, var(--accent-halo));
}
/* The sidebar nav's links are full-width pills right against the sidebar's
   edge - an offset ring would clip against the border, so it sits flush. */
.nav a:focus-visible,
.view-item:focus-visible,
.profile-dropdown a:focus-visible,
.profile-dropdown button:focus-visible {
  outline-offset: 0;
}
/* Checkboxes/radios are small enough that a 4px halo swamps them. */
input[type="checkbox"]:focus-visible,
input[type="radio"]:focus-visible {
  box-shadow: none;
  outline-offset: 1px;
}

/* In-flight affordance (2026-09-10) - paired with withBusyButton() in
   api.js: a button that fired a request is disabled and visibly spinning
   until it resolves, instead of looking like a dead click. currentColor
   keeps the spinner visible on both the filled accent button and the
   greyed-out :disabled state it takes on while busy. */
button.is-busy { cursor: progress; }
button.is-busy::after {
  content: "";
  display: inline-block;
  width: 0.8em;
  height: 0.8em;
  margin-left: 8px;
  vertical-align: -0.1em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: altair-spin 0.65s linear infinite;
}
@keyframes altair-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  button.is-busy::after { animation-duration: 2.4s; }
}

/* Failed-request state (2026-09-10) - what renderRequestError() in api.js
   drops in where a list's "Loading..." used to sit forever when a request
   500'd. */
.request-error {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin-top: 0;
}
.request-error .request-error-retry {
  padding: 6px 12px;
  font-size: var(--text-base);
  flex-shrink: 0;
}

/* Toasts (2026-09-10) - see toast.js. Bottom-right, above every panel and
   the fixed profile menu (z-index 30) and the org picker (45). */
.toast-stack {
  position: fixed;
  bottom: 20px;
  right: 20px;
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: min(360px, calc(100vw - 40px));
  pointer-events: none;
}
.toast-region {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px 14px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink);
  box-shadow: var(--shadow);
  font-size: var(--text-base);
  line-height: 1.45;
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.toast-leaving { opacity: 0; transform: translateY(6px); }
.toast-message { flex: 1; word-break: break-word; }
/* A left edge in the status colour rather than a fully tinted card, so a
   long error message stays as readable as body text in both themes. */
.toast-success { border-left: 4px solid var(--status-success-fg); }
.toast-error { border-left: 4px solid var(--status-error-fg); }
.toast-dismiss {
  flex-shrink: 0;
  background: none;
  border: none;
  box-shadow: none;
  color: var(--ink-soft);
  font-size: 18px;
  line-height: 1;
  padding: 0 2px;
  cursor: pointer;
}
.toast-dismiss:hover { background: none; color: var(--ink); box-shadow: none; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .toast { transition: none; }
}

/* ---- Pipeline board --------------------------------------------------
   (2026-09-11, Chris: "in proposals case, let's make it a column the
   proposal sits in... more of a timeline, but only need draft, sent,
   viewed, signed"). Four chevron headers across the top, each with its
   count and its GST-exclusive value, and the proposals stacked underneath
   the one they are currently in. Lives here rather than in proposals.html's
   own <style> so a second board (a ticket pipeline, a quote pipeline) is a
   markup change, not a second stylesheet.

   Built with tokens only - no literal colours - so the dark theme block
   near the top of this file carries it with no extra rules. */
.pipeline-board {
  display: grid;
  /* Four equal tracks that can actually shrink. minmax(0, 1fr) rather than
     1fr: a long client name in a card otherwise sets the track's minimum
     width and the board overflows the page instead of the name wrapping. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-2);
  align-items: start;
}
.pipeline-column {
  min-width: 0;
  background: var(--surface-sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.pipeline-column.is-empty { background: none; }

/* ---- The chevron header --------------------------------------------
   The arrow is a clip-path on the header itself, so it costs no extra
   markup and no images. The first column is square on its left edge and
   the last is square on its right, which is what makes the four read as
   one run rather than four detached arrows. */
.pipeline-col-header {
  display: block;
  width: 100%;
  text-align: left;
  padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4);
  background: var(--neutral-bg);
  color: var(--ink);
  border: none;
  border-radius: 0;
  box-shadow: none;
  cursor: pointer;
  clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 50%, calc(100% - 12px) 100%, 0 100%, 12px 50%);
  transition: background-color var(--motion-fast) var(--ease);
}
.pipeline-column:first-child .pipeline-col-header {
  clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 50%, calc(100% - 12px) 100%, 0 100%);
  padding-left: var(--space-3);
}
.pipeline-column:last-child .pipeline-col-header {
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 12px 50%);
}
.pipeline-col-header:hover { background: var(--hover-bg); box-shadow: none; transform: none; }
.pipeline-col-header[aria-pressed="true"] { background: var(--active-bg-strong); }
.pipeline-col-header[aria-pressed="true"] .pipeline-col-label { color: var(--accent); }
.pipeline-col-label {
  display: block;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.pipeline-col-figures {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-top: 2px;
  /* A long money figure wraps under the count rather than pushing the
     chevron's point off the end of the header. */
  flex-wrap: wrap;
}
.pipeline-col-count { font-size: var(--text-lg); font-weight: 700; font-variant-numeric: tabular-nums; }
.pipeline-col-value { font-size: var(--text-sm); color: var(--ink-soft); font-variant-numeric: tabular-nums; }

/* ---- The stack of cards ---------------------------------------------
   Capped and scrolled, not unbounded: a Draft column with 200 proposals in
   it must not make the other three columns unreachable by pushing the page
   metres long. The cap is in vh so it is always about a screenful. */
.pipeline-col-cards {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2);
  max-height: min(62vh, 720px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.pipeline-col-empty {
  padding: var(--space-4) var(--space-3);
  color: var(--ink-faint);
  font-size: var(--text-sm);
  text-align: center;
}

.pipeline-card {
  display: block;
  width: 100%;
  text-align: left;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  color: var(--ink);
  transition: border-color var(--motion-fast) var(--ease), background-color var(--motion-fast) var(--ease);
}
.pipeline-card:hover { border-color: var(--accent); background: var(--card); box-shadow: var(--shadow-sm); transform: none; }
.pipeline-card-client {
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  /* Cards are narrow; a long client name truncates rather than reflowing
     the whole column. The title below is allowed two lines instead. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pipeline-card-title {
  font-weight: 600;
  font-size: var(--text-base);
  margin: 2px 0 var(--space-2);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.pipeline-card-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.pipeline-card-amount { font-weight: 700; font-variant-numeric: tabular-nums; font-size: var(--text-base); }
.pipeline-card-amount .suffix { font-weight: 400; font-size: var(--text-xs); color: var(--ink-soft); }
.pipeline-card-note { font-size: var(--text-xs); color: var(--ink-faint); margin-top: var(--space-1); }

/* Expiry badge. Three tones onto the shared semantic set - no new reds. */
.pipeline-expiry {
  display: inline-block;
  font-size: var(--text-xs);
  font-weight: 600;
  border-radius: var(--radius-pill);
  padding: 1px var(--space-2);
  background: var(--neutral-bg);
  color: var(--neutral-fg);
}
.pipeline-expiry.overdue { background: var(--danger-bg); color: var(--danger-fg); }
.pipeline-expiry.soon { background: var(--warn-bg); color: var(--warn-fg); }

/* ---- The "other statuses" group -------------------------------------
   Superseded/declined proposals are not a stage of the pipeline, but they
   must still be reachable - this is where they go, under the board. */
.pipeline-other { margin-top: var(--space-4); }
.pipeline-other > summary {
  cursor: pointer;
  font-size: var(--text-base);
  color: var(--ink-soft);
  padding: var(--space-2) 0;
}
.pipeline-other-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--space-2);
  padding-top: var(--space-2);
}

/* ---- The funnel filter ----------------------------------------------
   Behind a button, not always-on chrome (Chris: "dropdowns on every page").
   Same popover mechanics as the data table's Columns picker - an absolutely
   positioned panel under a ghost button, closed by a click anywhere else. */
.board-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
}
.board-toolbar-actions { position: relative; margin-left: auto; }
.filter-popover {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--space-3);
  width: min(280px, calc(100vw - 32px));
  z-index: 20;
  animation: menu-in var(--motion-fast) var(--ease);
}
.filter-popover label {
  display: block;
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  margin-bottom: var(--space-1);
}
.filter-popover select,
.filter-popover input[type="text"] { width: 100%; }
.filter-popover .filter-field + .filter-field { margin-top: var(--space-3); }
.filter-popover .filter-check {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-base);
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  color: var(--ink);
  margin: var(--space-3) 0 0;
}
.filter-popover .filter-check input { width: auto; min-height: 0; }
.filter-popover-foot { margin-top: var(--space-3); display: flex; justify-content: flex-end; }
/* A count on the funnel button, so an active filter is visible with the
   popover shut - the whole point of hiding the controls. */
.filter-count {
  display: inline-block;
  margin-left: var(--space-1);
  min-width: 16px;
  padding: 0 4px;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--accent-on);
  font-size: var(--text-xs);
  font-weight: 700;
  line-height: 16px;
  text-align: center;
}

@media (max-width: 860px) {
  /* Two-up rather than four squeezed columns - below about 200px a track a
     card's client name and money figure stop fitting on their own lines.
     The chevrons keep pointing right within each row, which still reads as
     a progression. */
  .pipeline-board { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .pipeline-column:nth-child(odd) .pipeline-col-header {
    clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 50%, calc(100% - 12px) 100%, 0 100%);
    padding-left: var(--space-3);
  }
  .pipeline-column:nth-child(even) .pipeline-col-header {
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 12px 50%);
  }
  /* Shorter cap: two rows of columns are already a long page. */
  .pipeline-col-cards { max-height: min(50vh, 480px); }
}

@media (max-width: 620px) {
  /* One column per row. A chevron pointing right is meaningless in a single
     stack, so the headers become plain bars. */
  .pipeline-board { grid-template-columns: minmax(0, 1fr); }
  .pipeline-column .pipeline-col-header,
  .pipeline-column:first-child .pipeline-col-header,
  .pipeline-column:last-child .pipeline-col-header,
  .pipeline-column:nth-child(odd) .pipeline-col-header,
  .pipeline-column:nth-child(even) .pipeline-col-header {
    clip-path: none;
    padding-left: var(--space-3);
  }
  .pipeline-col-cards { max-height: min(44vh, 380px); }
  .board-toolbar-actions { margin-left: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .pipeline-col-header, .pipeline-card { transition: none; }
  .filter-popover { animation: none; }
}

/* The funnel on a filter button - sized and baselined with the label rather
   than sitting on the text baseline like a glyph would. */
.funnel-icon { vertical-align: -2px; margin-right: 2px; }

/* Owner initials on a pipeline card - the small avatar-ish chip from the
   screenshot. A circle of initials, not a photo: this platform has no
   staff avatars, and the full address is on the element's title. */
.owner-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  border-radius: var(--radius-pill);
  background: var(--accent-soft);
  color: var(--accent);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  font-variant-numeric: normal;
  cursor: default;
}
/* ---- The billing board ----------------------------------------------
   The three columns money is actually in - what has not been billed, what
   is waiting on a person, what is waiting on a client - plus the cards,
   pills and charge tables inside them. Classification and wording live in
   public/billingLanes.js; this is only how it looks.

   Page-local to billing.html until 2026-09-11, when a client's money
   became this same board filtered to that one client, opened as a stacked
   pane from the client slide-over (public/clientMoneyPane.js) rather than
   a "Billing" tab that listed raw charge codes and then navigated you off
   the client. Two surfaces, one board, so these rules are shared rather
   than copied - a copy is how the client's view would drift back into
   being its own worse thing. */
/* The board (2026-09-11, Chris: "Why do we have billing, Invoices and
   invoice review?? can't that all be one thing?"). Three columns, one
   per state money is actually in - see public/billingLanes.js for the
   classification and why paid invoices are a filter, not a column. */
.billing-board {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 16px;
  align-items: start;
  margin-top: 16px;
}
/* One column at a time below this, rather than three unreadable slivers
   - matches the breakpoints styles.css already uses for .view-layout. */
@media (max-width: 1100px) { .billing-board { grid-template-columns: 1fr; } }

.board-col {
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}
.board-col-head {
  padding: 12px 14px 10px;
  border-bottom: 1px solid var(--line);
}
.board-col-title-row { display: flex; align-items: baseline; gap: 8px; }
.board-col-title { font-weight: 700; font-size: var(--text-base); color: var(--ink); }
.board-col-count {
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--accent);
  background: var(--badge-bg);
  border-radius: 999px;
  padding: 1px 9px;
  white-space: nowrap;
}
.board-col-blurb { font-size: var(--text-sm); color: var(--ink-soft); margin-top: 3px; line-height: 1.45; }
/* The per-column history filter. Chris: paid invoices are history and
   get a filter, not a standing column - same for already-invoiced
   charges and for batches already pushed to Xero. */
.board-col-filter { margin-top: 8px; display: flex; align-items: center; gap: 6px; }
.board-col-filter select { min-width: 0; width: 100%; margin: 0; font-size: 12.5px; padding: 5px 8px; }
.board-col-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.board-col-actions button { font-size: var(--text-sm); padding: 5px 10px; }
/* Each column scrolls on its own so one busy column can't push the
   other two off the bottom of the screen. */
.board-col-body { padding: 6px 14px 12px; max-height: 62vh; overflow-y: auto; overscroll-behavior: contain; }
.board-col-foot { padding: 8px 14px 12px; border-top: 1px solid var(--line); }
.board-empty { color: var(--ink-soft); font-size: var(--text-base); padding: 10px 0; }

/* A row in a column. Deliberately not a table: three columns of an
   eleven-column invoice table side by side is exactly the "less columns"
   Chris asked for. */
.bill-card {
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 10px 12px;
  margin-top: 8px;
  background: var(--card);
  cursor: pointer;
}
.bill-card:hover { background: var(--hover-bg); }
.bill-card.flagged { border-left: 3px solid var(--status-error-fg); }
.bill-card.stranded { border-left: 3px solid var(--priority-high-fg); }
.bill-card-top { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; }
.bill-card-name { font-weight: 600; font-size: var(--text-base); color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
.bill-card-amount { font-variant-numeric: tabular-nums; font-weight: 700; white-space: nowrap; }
.bill-card-meta { font-size: var(--text-sm); color: var(--ink-soft); margin-top: 3px; line-height: 1.5; overflow-wrap: anywhere; }
.bill-card-pills { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
.bill-card-actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 8px; }
.bill-card-actions button { font-size: var(--text-sm); padding: 4px 10px; }

.pill {
  display: inline-block;
  font-size: var(--text-xs);
  font-weight: 700;
  border-radius: 999px;
  padding: 2px 8px;
  white-space: nowrap;
}
.pill-draft { background: var(--badge-bg); color: var(--ink-soft); }
.pill-issued { background: var(--badge-bg); color: var(--accent); }
.pill-part_paid { background: var(--priority-high-bg); color: var(--priority-high-fg); }
.pill-paid { background: var(--status-success-bg); color: var(--status-success-fg); }
.pill-void { background: var(--status-error-bg); color: var(--status-error-fg); }
.pill-pending { background: var(--badge-bg); color: var(--accent); }
.pill-approved { background: var(--status-success-bg); color: var(--status-success-fg); }
.pill-auto { background: var(--badge-bg); color: var(--accent-purple); }
.pill-synced { background: var(--status-success-bg); color: var(--status-success-fg); }
.pill-unsynced { background: var(--badge-bg); color: var(--ink-soft); }
.pill-failed { background: var(--status-error-bg); color: var(--status-error-fg); }
.pill-muted { background: var(--badge-bg); color: var(--ink-soft); }

.xero-state { font-size: var(--text-sm); color: var(--ink-soft); }
.xero-state.synced { color: var(--success); }
.xero-state.failed, .xero-state.diverged { color: var(--status-error-fg); }

.anomaly-list { list-style: none; padding: 0; margin: 8px 0 0; }
.anomaly-list li {
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--status-error-fg);
  background: var(--status-error-bg);
  border-radius: 8px;
  padding: 6px 8px;
  margin-bottom: 4px;
}
.anomaly-list li:last-child { margin-bottom: 0; }
.sync-error {
  font-size: var(--text-sm);
  color: var(--status-error-fg);
  background: var(--status-error-bg);
  border-radius: 8px;
  padding: 6px 8px;
  margin-top: 8px;
  word-break: break-word;
}
.charge-table { width: 100%; border-collapse: collapse; margin-top: 12px; font-size: var(--text-base); }
.charge-table th {
  text-align: left;
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  font-weight: 600;
  padding: 4px 8px 4px 0;
  border-bottom: 1px solid var(--line);
}
.charge-table td { padding: 6px 8px 6px 0; border-bottom: 1px solid var(--line); color: var(--ink); }
.charge-table tr:last-child td { border-bottom: none; }
.charge-table td.num, .charge-table th.num { text-align: right; padding-right: 0; font-variant-numeric: tabular-nums; }
.gst-tag { font-size: var(--text-xs); color: var(--ink-soft); }

/* The shared slide-over is sized for a reading pane; the recurring
   charges pane carries a list plus an edit form plus its own views
   sidebar, so it gets more room. Everything else about the mechanism
   (backdrop, transform-only animation, Escape to close) is unchanged. */
.slide-over-panel--wide { width: min(920px, 96vw); }


.notification-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 320px;
  overflow-y: auto;
}
.notification-list__item {
  padding: 8px 12px;
  border-bottom: 1px solid var(--line);
}
.notification-list__item--unread {
  background: var(--surface-2, rgba(0, 0, 0, 0.03));
  font-weight: 600;
}
.notification-list__item p {
  margin: 0 0 2px;
}
.notification-list__item a {
  color: inherit;
  text-decoration: none;
}
.notification-list__item a:hover {
  text-decoration: underline;
}
.notification-list__meta {
  font-size: var(--text-sm);
  color: var(--ink-soft);
}
.notification-list__empty {
  padding: 12px;
  color: var(--ink-soft);
  font-size: var(--text-base);
}

/* ---- The Documents pane ---------------------------------------------
   Client documents (public/clientDocuments.js) inside the client
   workspace. These class hooks existed from the day the pane shipped and
   were defined in NO stylesheet, so the pane rendered as plain stacked
   text - which is why the filename/access separator had to be markup
   rather than a rule (docs/BACKLOG.md §9.6, 2026-09-18). A document is a
   row: the name and its access word on one line, the facts under it, the
   controls under those. */
.cd-row {
  border-top: 1px solid var(--line);
  padding: var(--space-3) 0;
}
.cd-row:first-child { border-top: 0; }
.cd-row-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.cd-row-name { font-weight: 600; }
.cd-row-sep { color: var(--ink-soft); }
.cd-row-access { color: var(--ink-soft); font-size: var(--text-sm); }
.cd-row[data-access-level="billing_gated"] .cd-row-access { color: var(--priority-high-fg); font-weight: 600; }
.cd-row-meta { color: var(--ink-soft); font-size: var(--text-sm); margin-top: 2px; }
.cd-row-refusal { color: var(--ink-soft); font-size: var(--text-sm); margin: 6px 0 0; }
.cd-row-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}
.cd-raise-note { font-size: var(--text-sm); }
.cd-row-status { color: var(--ink-soft); font-size: var(--text-sm); margin: 6px 0 0; min-height: 1em; }

/* ---- The portal-access card in the client pane (public/clientPortal.js,
   backlog F22, 2026-09-23) ----
   One row per contact, and its own words for each state. The link box is the
   one-time invitation URL and is deliberately the loudest thing in the card:
   it is shown once and replaced by "on your clipboard" after a copy, so a
   reader who looks back knows whether they still have it. */
.client-portal-title { display: block; margin-top: 16px; font-size: var(--text-lg); }
.client-portal-intro { margin: 4px 0 10px; }
.client-portal-link {
  margin: 8px 0;
  font-family: ui-monospace, monospace;
  font-size: var(--text-sm);
  word-break: break-all;
}
.client-portal-refusal { color: var(--priority-high-fg); font-size: var(--text-sm); margin: 8px 0; }
.client-portal-contact {
  border-top: 1px solid var(--line);
  padding: 10px 0;
}
.client-portal-who { font-size: var(--text-sm); }
.client-portal-state { margin-top: 2px; }
.client-portal-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}
.client-portal-empty { margin: 6px 0; }

/* ---- One confirmation, for the whole app (public/confirmDialog.js) ----
   Built on first use and removed when answered, so nothing on any page has to
   carry a hidden dialog. Cancel is the quiet button and the destructive
   confirm is the loud one; which is which is the caller's choice, and the
   component's own rule is that Cancel holds the focus. */
.confirm-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  z-index: 1000;
}
.confirm-dialog {
  background: var(--card);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg, var(--shadow-sm));
  padding: var(--space-5);
  max-width: 480px;
  width: 100%;
}
.confirm-dialog-title { margin: 0 0 var(--space-2); font-size: var(--text-lg); }
.confirm-dialog-body { margin: 0; color: var(--ink-soft); font-size: var(--text-base); }
.confirm-dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

/* ---- The branding colours in Settings (D5, 2026-09-19) ----------------
   Three rows, each a colour and the text colour that sits on it - see
   public/settingsPage.js's BRAND_COLOR_ROLES for what each one is for. The
   ratio sentence under each row is the point of the block: overriding the
   automatic text colour blind is how an unreadable document reaches a
   client. */
.brand-color-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 10px;
  align-items: end;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 10px 12px;
  margin-bottom: 10px;
}
.brand-color-row-head { grid-column: 1 / -1; display: flex; flex-direction: column; gap: 2px; }
.brand-color-row-head strong { font-size: var(--text-base); }
.brand-color-pair { display: inline-flex; align-items: center; gap: 6px; }
.brand-color-pair input[type="color"] {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--card);
  cursor: pointer;
}
.brand-color-pair input[type="text"] { max-width: 110px; font-family: ui-monospace, monospace; }
.brand-color-row .ghost-button { justify-self: start; }
.brand-color-status { grid-column: 1 / -1; margin: 0; }
.brand-color-status .error { color: var(--status-error-fg); }
