/* Self-hosted, served from bytes embedded in the binary: the portal makes NO
 * third-party request — never reintroduce a hosted-webfont import. */
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/fonts/dm-sans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
    U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/fonts/dm-sans-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308,
    U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113,
    U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 300 600;
  font-display: swap;
  src: url('/fonts/jetbrains-mono-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
    U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Declared once, up front: this ordering is what lets every rule below be
 * written without a single `!important`. */
@layer reset, tokens, base, layout, components, utilities;

/* KEEP IN SYNC: the tokens, base and components blocks were ported from one
 * source together; a re-sync must update all three, not one. */
@layer tokens {
  :root,
  html.dark {
    color-scheme: dark;

    --fg: 255 255 255;
    --ink: 2 7 12;
    --ink-900: 15 25 44;
    --ink-950: 10 16 23;
    --line: 37 71 94;
    --line-soft: 25 47 61;
    --line-strong: 78 114 142;
    --muted: 140 148 154;
    --accent: 149 209 255;
    --cyan: 28 236 184;
    --gold: 255 235 131;
    --warn: 255 197 131;
    --violet: 99 135 242;
    --danger: 255 86 86;
    --danger-soft: 255 118 118;
    --chip: 45 51 57;

    /* Validated dark-mode series: #2e86e0 #0aa87e #a8891a #a45fd8 */
    --chart-1: 46 134 224;
    --chart-2: 10 168 126;
    --chart-3: 168 137 26;
    --chart-4: 164 95 216;

    --font-sans: 'DM Sans', -apple-system, system-ui, sans-serif;
    --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;

    /* Elevation. Local to this sheet: the design system ships no shadow token,
     * and these values are DARK-theme shadows, overridden on light. */
    --shadow-float: 0 16px 48px rgb(0 0 0 / 0.55);
    --shadow-lift: 0 24px 70px rgb(0 0 0 / 0.7);

    /* The modal scrim. Local because the design system's own
     * `--surface-background-pop-up-transparent` is opaque white on light. */
    --scrim: color-mix(in srgb, var(--surface-background-pop-up) 70%, transparent);

    /* The rim on a filled primary pill. A token rather than one value: the same
     * line colour would draw a bright ring around the light theme's dark fill. */
    --pill-primary-border: var(--borders-line-2);
  }

  html.light {
    color-scheme: light;

    --fg: 12 24 38;
    --ink: 240 244 248;
    --ink-900: 224 233 243;
    --ink-950: 255 255 255;
    --line: 200 213 226;
    --line-soft: 223 231 239;
    --line-strong: 138 160 182;
    --muted: 94 108 122;
    --accent: 11 108 180;
    --cyan: 0 135 104;
    --gold: 158 122 8;
    --warn: 176 94 10;
    --violet: 99 135 242;
    --danger: 205 43 43;
    --danger-soft: 186 32 32;
    --chip: 221 229 237;

    /* Validated light-mode series: #0b6cb4 #008768 #9e7a08 #8a3cc3 */
    --chart-1: 11 108 180;
    --chart-2: 0 135 104;
    --chart-3: 158 122 8;
    --chart-4: 138 60 195;

    /* Far weaker, and tinted navy rather than pure black: the same opacity
     * that barely registers on a near-black page reads as soot on white. */
    --shadow-float: 0 16px 48px rgb(12 24 38 / 0.12);
    /* None at all on the modal: the scrim is what separates the panel from the
     * page here. The dropdowns keep `--shadow-float` — no scrim behind them. */
    --shadow-lift: none;

    /* DARK, on the light theme — deliberately not the token's own value: a white
     * scrim leaves a white modal panel with nothing to sit against. */
    --scrim: rgb(12 24 38 / 0.45);

    /* Its own fill, so the pill reads as ringless. The 1px stays in the box
     * model, so primary and secondary pills are still the same height. */
    --pill-primary-border: var(--button-primary-background-default);
  }

  /* JS-off fallback: the pre-paint script always writes `dark` or `light`, so with
   * JS on this never matches. Values duplicate html.light — keep the two adjacent. */
  @media (prefers-color-scheme: light) {
    html:not(.dark):not(.light) {
      color-scheme: light;

      --fg: 12 24 38;
      --ink: 240 244 248;
      --ink-900: 224 233 243;
      --ink-950: 255 255 255;
      --line: 200 213 226;
      --line-soft: 223 231 239;
      --line-strong: 138 160 182;
      --muted: 94 108 122;
      --accent: 11 108 180;
      --cyan: 0 135 104;
      --gold: 158 122 8;
      --warn: 176 94 10;
      --violet: 99 135 242;
      --danger: 205 43 43;
      --danger-soft: 186 32 32;
      --chip: 221 229 237;
      --chart-1: 11 108 180;
      --chart-2: 0 135 104;
      --chart-3: 158 122 8;
      --chart-4: 138 60 195;
      --shadow-float: 0 16px 48px rgb(12 24 38 / 0.12);
      --shadow-lift: none;
      --scrim: rgb(12 24 38 / 0.45);
      --pill-primary-border: var(--button-primary-background-default);
    }
  }
}

/* Tailwind Preflight equivalent. Hand-written CSS has to reproduce it or ported
 * components sit on different defaults than they were designed against. */
@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  html {
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
  }

  body,
  h1,
  h2,
  h3,
  h4,
  p,
  figure,
  blockquote,
  dl,
  dd {
    margin: 0;
  }

  ol,
  ul {
    margin: 0;
    padding: 0;
    list-style: none;
  }

  h1,
  h2,
  h3,
  h4 {
    font-size: inherit;
    font-weight: inherit;
  }

  img,
  svg,
  video,
  canvas {
    display: block;
    max-width: 100%;
  }

  button,
  input,
  select,
  textarea {
    font: inherit;
    color: inherit;
    background: transparent;
    border: 0;
    margin: 0;
  }

  button {
    cursor: pointer;
  }

  table {
    border-collapse: collapse;
    border-spacing: 0;
  }

  th {
    text-align: inherit;
    font-weight: inherit;
  }

  dialog {
    padding: 0;
    border: 0;
    color: inherit;
    background: transparent;
    max-width: none;
    max-height: none;
  }

  /* The only !important in the sheet: the UA's `[hidden] { display: none }` does
   * not reliably beat a `display: table-row` coming from a component rule. */
  [hidden] {
    display: none !important;
  }
}

/* --- Base ---------------------------------------------------------------- */
@layer base {
  html,
  body {
    height: 100%;
  }

  body {
    color: rgb(var(--fg));
    background: rgb(var(--ink));
    font-family: var(--font-sans);
    font-size: 14px;
    line-height: 1.45;
  }

  a {
    color: rgb(var(--accent));
    text-decoration: none;
  }

  a:hover {
    color: rgb(var(--fg));
  }

  ::selection {
    background: rgb(var(--accent) / 0.25);
  }

  ::placeholder {
    color: rgb(var(--muted));
  }

  ::-webkit-scrollbar {
    width: 8px;
    height: 8px;
  }

  ::-webkit-scrollbar-thumb {
    background: rgb(var(--line));
    border-radius: 4px;
  }

  ::-webkit-scrollbar-track {
    background: transparent;
  }

  /* Native menus follow `color-scheme`, but the closed <select> is ours. */
  select option {
    background: rgb(var(--ink-950));
    color: rgb(var(--fg));
  }

  /* The keyboard focus ring — deliberately NOT on form fields. `:where()` keeps
   * this at zero specificity so any component can override it. */
  :where(a, button, summary, [tabindex]):focus-visible {
    outline: 2px solid rgb(var(--accent));
    outline-offset: 2px;
  }

  :where(a, button, summary, [tabindex]):focus:not(:focus-visible) {
    outline: none;
  }

  /* Form fields are the exception: the offset ring landed OUTSIDE the field's own
   * `:focus` border change, which is the focus signal — see `.dc-input:focus`. */
  :where(input, select, textarea):focus,
  :where(input, select, textarea):focus-visible {
    outline: none;
  }

  /* A native <dialog> takes focus when it opens, so the UA drew its ring around
   * the whole element. The panel inside carries the visible border. */
  dialog:focus,
  dialog:focus-visible {
    outline: none;
  }
}

/* --- Layout: app shell ---------------------------------------------------- */
@layer layout {
  .app {
    display: flex;
    min-height: 100vh;
  }

  /* The design system's own four-stop page gradient. Token names are lowercase —
   * `var(--Surface-Page-Stop-1, …)` resolved to its dark fallback in BOTH themes. */
  .app-gradient {
    background: linear-gradient(
      116deg,
      var(--surface-page-stop-1) 2%,
      var(--surface-page-stop-2) 48%,
      var(--surface-page-stop-3) 74%,
      var(--surface-page-stop-4) 100%
    );
  }

  /* The sidebar's own width, published as a variable because `.app-footer`
   * is fixed and has to start exactly where the sidebar ends. */
  .app {
    --sidebar-w: 16.5rem;
  }

  /* Pinned to the VIEWPORT, not the document. `align-self` matters: without it the
   * flex default stretches this item and `sticky` has nothing to move within. */
  .app-sidebar {
    position: sticky;
    top: 0;
    align-self: flex-start;
    /* `dvh` after `vh`, never instead of it: the `vh` line is the fallback, and
     * alone it overshoots a retracting mobile URL bar and hides the card. */
    height: 100vh;
    height: 100dvh;
    /* No `overflow` here, deliberately: it would make the sidebar a scroll
     * container and clip `.profile-menu-panel` (see portal.js `placePanel`). */
    width: var(--sidebar-w);
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    gap: 3rem;
    padding: 2rem 1.25rem 3.5rem;
    background: rgb(var(--ink) / 0.88);
    backdrop-filter: blur(40px);
    -webkit-backdrop-filter: blur(40px);
  }

  .brand {
    display: flex;
    align-items: center;
  }

  .brand-mark {
    display: flex;
    align-items: center;
    gap: 1rem;
    color: rgb(var(--fg));
  }

  .brand-mark-icon {
    flex-shrink: 0;
  }

  .brand-mark-text {
    display: flex;
    flex-direction: column;
    gap: 0.125rem;
    min-width: 0;
  }

  .brand-mark-name {
    font-size: 16px;
    font-weight: 700;
    line-height: 20px;
  }

  .brand-mark-sub {
    font: 300 10px/16px var(--font-mono);
    text-transform: uppercase;
    letter-spacing: 0.02em;
    color: var(--text-secondary-2);
    white-space: nowrap;
  }

  .nav {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    /* The one part of the sidebar allowed to scroll; `min-height: 0` is what lets
     * a flex item shrink below its content so the overflow has somewhere to go. */
    min-height: 0;
    overflow-y: auto;
  }

  .nav-link {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    height: 40px;
    padding: 0.5rem 1.25rem;
    border-radius: 8px;
    border: 1px solid transparent;
    font-size: 12px;
    color: rgb(var(--fg));
    transition: color 150ms, background-color 150ms, border-color 150ms;
  }

  .nav-link:hover {
    background: rgb(var(--chip) / 0.4);
    border-radius: 8px;
  }

  /* The selected entry: the design's "Tertiary Button Dark" in its pressed
   * state (Figma 148:1095) — a gradient hairline over the box surface, a soft
   * lavender glow along the inside top and bottom edges, bold label, violet
   * icon. The surface and the label are theme tokens rather than the frame's
   * #0A1017 and white, which are the dark theme's values: the light theme
   * gets the same shape on a white box.
   *
   * The hairline is the design's exported stroke: #6387F2 at the top and
   * bottom edges, #202E4B (`--borders-line-2`) through the middle, at 48%
   * opacity. A border cannot take a gradient, so it is the `.ring` mask
   * technique on `::before`, over the 1px transparent border the base rule
   * already reserves. The SVG's gradient vector runs 6.5° off vertical and
   * starts above the box; 173.5deg with the stops re-projected onto the
   * box's own gradient line (0, .124, .805, .940 → 10.4, 21.1, 80.5, 92.2%)
   * draws the same thing. */
  .nav-link[aria-current='page'] {
    position: relative;
    border-radius: 8px;
    background: var(--surface-background-box);
    backdrop-filter: blur(7px);
    box-shadow:
      inset 0 6px 6px 0 var(--surface-background-frosted),
      inset 0 -6px 6px 0 var(--surface-background-frosted);
    color: var(--text-primary);
    font-weight: 700;
    line-height: 16px;
  }

  .nav-link[aria-current='page']::before {
    content: "";
    position: absolute;
    inset: -1px;
    border-radius: inherit;
    padding: 1px;
    background: linear-gradient(
      173.5deg,
      var(--icon-accents-violet) 10.4%,
      var(--borders-line-2) 21.1%,
      var(--borders-line-2) 80.5%,
      var(--icon-accents-violet) 92.2%);
    opacity: 0.48;
    mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
    mask-composite: exclude;
    -webkit-mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    pointer-events: none;
  }

  .nav-link[aria-current='page'] .nav-icon {
    color: var(--icon-accents-violet);
  }

  .nav-icon {
    flex-shrink: 0;
  }

  /* The sidebar's "something needs you" count — pushed to the row's far
   * edge, same accent as the active-link border so it reads as "this is
   * what the active violet means when nothing is selected yet". */
  .nav-badge {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 999px;
    background: rgb(var(--accent));
    color: rgb(var(--ink-950));
    font-size: 10px;
    font-weight: 700;
    line-height: 1;
  }

  /* The viewer card (#78) — now the `<summary>` of the profile dropdown (#90), so
   * it is the click target and the panel it opens holds sign-out. */
  .profile-menu {
    position: relative;
  }

  .sidebar-profile {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    height: 48px;
    padding: 0.25rem 0.5rem;
    border: 1px solid transparent;
    border-radius: 0.75rem;
    background: rgb(var(--ink-950) / 0.9);
    backdrop-filter: blur(20px);
    cursor: pointer;
  }

  /* Every menu trigger in the portal is the <summary> of a <details data-menu>;
   * one reset for the family, so the next trigger cannot ship with the triangle. */
  details[data-menu] > summary {
    list-style: none;
  }

  details[data-menu] > summary::marker,
  details[data-menu] > summary::-webkit-details-marker {
    display: none;
    content: '';
  }

  .profile-menu[open] > .sidebar-profile {
    border-color: var(--border-primary-button-border);
  }

  .profile-menu[open] .sidebar-profile-chevron {
    transform: rotate(180deg);
  }

  /* Opens UPWARD: the card is the last thing in the sidebar, so a panel
   * below it would land off-screen. */
  .profile-menu-panel {
    position: absolute;
    bottom: calc(100% + 6px);
    left: 0;
    right: 0;
    z-index: 55;
    background: var(--surface-background-box);
    border: 1px solid var(--borders-line-2);
    border-radius: 8px;
    box-shadow: var(--shadow-float);
    padding: 0.375rem;
    display: flex;
    flex-direction: column;
    gap: 0.125rem;
  }

  .sidebar-profile-avatar {
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    border-radius: 0.625rem;
    border: 0.5px solid rgb(var(--line-strong));
    display: flex;
    align-items: center;
    justify-content: center;
    color: rgb(var(--muted));
  }

  .sidebar-profile-text {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 0.125rem;
  }

  .sidebar-profile-name {
    font-size: 12px;
    color: rgb(var(--fg));
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .sidebar-profile-meta {
    font-size: 10px;
    color: rgb(var(--muted));
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .sidebar-profile-role {
    color: rgb(var(--accent));
    text-transform: uppercase;
    letter-spacing: 0.5px;
  }

  .sidebar-profile-chevron {
    flex-shrink: 0;
    color: rgb(var(--muted));
    transition: transform 150ms;
  }

  .app-main {
    flex: 1;
    min-width: 0;
    /* Bottom padding clears the fixed `.app-footer` so the last table row
     * scrolls out from under it rather than sitting behind it. */
    padding: 1.5rem 2.5rem 5.5rem;
  }

  /* The fixed footer bar (#90). Starts where the sidebar ends, so it reads as the
   * main column's own chrome. */
  .app-footer {
    position: fixed;
    left: var(--sidebar-w);
    right: 0;
    bottom: 0;
    z-index: 40;
    height: 3rem;
    box-sizing: border-box;
    padding: 0 2rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.5rem;
    background: rgb(var(--ink) / 0.88);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
  }

  .app-footer-note {
    font: 300 12px/16px var(--font-mono);
    text-transform: uppercase;
    letter-spacing: 0.02em;
    color: var(--text-secondary-2);
    white-space: nowrap;
  }

  /* Both glyphs stay lit; the knob slides to say which side is active. */
  .theme-switch {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    flex-shrink: 0;
    background: none;
    border: 0;
    padding: 0;
    color: var(--text-secondary-2);
  }

  .theme-switch-icon {
    flex-shrink: 0;
    transition: color 150ms;
  }

  /* The active side is the lit one: `aria-pressed="true"` means light, so the sun
   * leads there and the moon recedes. Both glyphs stay VISIBLE. */
  .theme-switch[aria-pressed='true'] .theme-switch-icon-light,
  .theme-switch[aria-pressed='false'] .theme-switch-icon-dark {
    color: var(--text-primary);
  }

  .theme-switch-track {
    position: relative;
    width: 2.25rem;
    height: 1rem;
    border-radius: 9999px;
    /* A SEMANTIC token, not the `--colors-blues-*` primitive it started as:
     * primitives are one value across both themes, so the track stayed navy. */
    background: var(--borders-line-2);
    transition: background-color 150ms;
  }

  .theme-switch-knob {
    position: absolute;
    top: 50%;
    left: 2px;
    width: 0.75rem;
    height: 0.75rem;
    margin-top: -0.375rem;
    border-radius: 9999px;
    /* Fixed white, not `--text-primary`: that token flips, which on light gave a
     * near-black dot and inverted the whole control. */
    background: var(--color-greyscale-white);
    transition: left 150ms;
  }

  /* aria-pressed is the state: true = light, so the knob sits by the sun. */
  .theme-switch[aria-pressed='true'] .theme-switch-knob {
    left: calc(100% - 0.75rem - 2px);
  }

  .page-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
  }

  /* The header's action group. Bare buttons would each become a flex child of the
   * space-between `.page-head` and drift apart; this keeps the pair adjacent. */
  .page-actions {
    display: flex;
    align-items: center;
    gap: 0.8rem;
    flex-shrink: 0;
  }

  .page-actions:empty {
    display: none;
  }

  /* Breadcrumb above the page title: the only way back off a detail page,
   * since the sidebar keeps the SECTION lit rather than the leaf. */
  .breadcrumb {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    margin-bottom: 0.25rem;
    font: var(--type-body-medium);
  }

  .breadcrumb a {
    color: var(--text-accent);
  }

  .breadcrumb a:hover {
    color: var(--text-primary);
  }

  .breadcrumb-sep,
  .breadcrumb-current {
    color: var(--text-secondary-2);
  }

  .breadcrumb-current {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .page-title {
    margin-top: 0.5rem;
    font: 500 18px/22px var(--font-sans);
  }

  .page-sub {
    margin-top: 0.5rem;
    font: var(--type-body-medium);
    color: var(--text-secondary-2);
  }

  /* A heading that names the card under it — the `<h2>` to `.page-title`'s `<h1>`.
   * No margin of its own: callers pair it with its card inside a `stack-3`. */
  .section-title {
    font: 500 15px/20px var(--font-sans);
    color: var(--text-primary);
  }

  /* Chrome-less pages (404, access denied). */
  .shell-bare {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.5rem;
  }

  /* One width for the card, whatever ends up inside it: a centred flex item is
   * shrink-to-fit, so a long server message stretched the card out to 866px. */
  .shell-bare > .dc-card {
    width: 100%;
    max-width: 27rem;
  }
}

/* `dc-*` is ported verbatim from the mockup's @layer components. Change a `dc-*`
 * rule only as a deliberate divergence; everything unprefixed is portal-local. */
@layer components {
  .dc-card {
    background: var(--surface-background-box);
    /* `--borders-line-2`, the same token the table's own row rules and the
     * toolbar controls use, so the card edge and its inner lines are one colour. */
    border: 1px solid var(--borders-line-2);
    border-radius: 8px;
    /* So the table header's background clips to the rounded top corners. Safe for
     * row menus: panels go `position: fixed` on open (portal.js `placePanel`). */
    overflow: hidden;
  }

  .dc-card-pad {
    padding-top: 3rem;
    padding-left: 2.5rem;
    padding-right: 2.5rem;
    padding-bottom: 3rem;
  }

  .dc-label {
    font: var(--type-label-big);
    text-transform: uppercase;
    color: var(--text-secondary-2);
    letter-spacing: 0.02em;
  }

  /* The heading footnote (`ui::hint`). Sits inside a `.dc-label`, which is
   * uppercase and letter-spaced — the bubble undoes both. */
  .hint {
    display: inline-flex;
    align-items: center;
    margin-left: 0.3125rem;
    vertical-align: -0.1875em;
    color: var(--icon-secondary);
    /* The pointer that means "there is an explanation here", not the hand
     * that means "this navigates". */
    cursor: help;
  }

  .hint:hover,
  .hint:focus-visible {
    color: var(--text-primary);
  }

  .hint-icon {
    flex-shrink: 0;
  }

  /* Measured from the header CELL, not from the icon inside it — see
   * `.dc-table th`'s `position: relative`. */
  .hint-bubble {
    position: absolute;
    top: calc(100% + 6px);
    /* Anchored right, so it opens INTO the table; a first-column hint would need
     * the opposite. `0.5rem` is the header cell's own padding. */
    right: 0.5rem;
    z-index: 60;
    /* Wide enough for the sentence, capped so it cannot span the card. */
    width: max-content;
    max-width: 17rem;
    padding: 0.5rem 0.625rem;
    /* The POP-UP surface, not the box one: the box token is the card's own colour,
     * so a bubble painted in it was invisible except for its outline. */
    background: var(--surface-background-pop-up);
    border: 1px solid var(--borders-line-2);
    border-radius: 8px;
    box-shadow: var(--shadow-float);
    font: var(--type-body);
    /* Everything a header cell imposes, undone: `nowrap` would let a sentence run
     * straight past the `max-width` above instead of wrapping inside it. */
    text-transform: none;
    letter-spacing: normal;
    white-space: normal;
    color: var(--text-primary);
    text-align: left;
    text-wrap: pretty;
    /* `visibility`, not `display`: the bubble stays in the accessibility tree
     * for `aria-describedby` and can be transitioned. */
    opacity: 0;
    visibility: hidden;
    transition: opacity 120ms ease-out, visibility 120ms;
  }

  .hint:hover .hint-bubble,
  .hint:focus-within .hint-bubble {
    opacity: 1;
    visibility: visible;
  }

  /* Tooltip for an icon-only control (the /invites decision buttons), to the
   * Runtime styleguide's spec. The icon alone carries the verb on screen, so
   * the word has to be reachable by pointer too — `aria-label` already names
   * the button for assistive tech, which is why the bubble is `aria-hidden`
   * rather than a second description of the same control.
   *
   * Opens UPWARD: these buttons sit in the last column of a `.table-scroll`,
   * and a bubble hanging below the final row is the one place this can be
   * clipped. `pointer-events: none` so crossing the bubble never re-triggers
   * the hover it was opened by. */
  .tip {
    position: relative;
  }

  .tip-bubble {
    position: absolute;
    bottom: calc(100% + 8px);
    left: 50%;
    transform: translateX(-50%);
    z-index: 60;
    width: max-content;
    max-width: 14rem;
    padding: 0.375rem 0.625rem;
    border-radius: 10px;
    border: 1px solid var(--borders-line-2);
    /* #06122A, the design's own value. Deliberately the primitive and NOT
     * `--surface-background-hover`: the Figma variable carrying that name
     * belongs to the Runtime styleguide, where it is this navy, while THIS
     * repo's token of the same name is a 20%-alpha blue wash that would
     * render the bubble nearly transparent. Same name, different scale. */
    background: var(--color-navy-navy-850);
    box-shadow:
      inset 0 5px 15px 0 rgb(255 255 255 / 0.15),
      0 4px 12px 0 rgb(0 0 0 / 0.2);
    font: var(--type-body);
    /* The white primitive, NOT `--text-primary`. The surface above is a fixed
     * navy in both themes, while `--text-primary` flips with the theme — so
     * on the light theme the bubble drew near-black text on near-black navy
     * and the sentence was there but unreadable. A fixed surface takes a
     * fixed foreground. */
    color: var(--color-greyscale-white);
    white-space: nowrap;
    text-align: center;
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    transition: opacity 120ms ease-out, visibility 120ms;
  }

  /* The wrapping variant, for a bubble carrying a sentence rather than a
   * verb. `nowrap` plus `max-width` is a contradiction the one-word case
   * never exposes: the width caps the box while the wrap rule refuses to
   * use it, so a sentence overflows instead of filling the three lines it
   * was given room for. */
  .tip-bubble-text {
    white-space: normal;
    width: 20rem;
    max-width: min(20rem, 60vw);
    text-align: left;
    /* Anchored to the trigger's left edge rather than centred on it. The
     * centred default suits a narrow bubble over a button mid-row; a 20rem
     * one hanging off a section heading — which sits at the left margin —
     * would put half its width out in the gutter. */
    left: 0;
    transform: none;
  }

  /* A bubble placed by portal.js (`initClipTips`) rather than by CSS: it is
   * appended to <body> and pinned to viewport coordinates, because the
   * table cells it annotates sit inside `.dc-card` and `.table-scroll`, which
   * both clip anything absolutely positioned within them. Sized to its text
   * up to the `.tip-bubble-text` cap, and shown as soon as it exists. */
  .tip-float {
    position: fixed;
    bottom: auto;
    width: max-content;
    max-width: min(20rem, calc(100vw - 1rem));
    overflow-wrap: anywhere;
    opacity: 1;
    visibility: visible;
    transition: none;
  }

  /* The trigger when the thing being annotated is not itself interactive
   * (a section heading). Carries no button chrome — it is a glyph you can
   * reach with the keyboard, not a control that does anything. */
  .tip-btn {
    display: inline-flex;
    align-items: center;
    padding: 0;
    border: 0;
    background: none;
    color: var(--text-secondary-2);
    cursor: help;
  }

  .tip-btn:hover,
  .tip-btn:focus-visible {
    color: var(--text-primary);
  }

  .tip-icon {
    display: block;
  }

  /* Baseline-aligning a 15px glyph against a 15px heading leaves it sitting
   * a shade high, so the pair is centred on the line box instead. */
  .section-title-tipped {
    display: flex;
    align-items: center;
    gap: 0.4rem;
  }

  .tip:hover .tip-bubble,
  .tip:focus-visible .tip-bubble {
    opacity: 1;
    visibility: visible;
  }

  .card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    flex-wrap: wrap;
    padding: 0.625rem 1.125rem;
    border-bottom: 1px solid var(--borders-line-2);
    background: var(--surface-contrast-low-2);
  }

  .dc-input {
    width: 100%;
    background: var(--surface-background-box);
    border: 1px solid var(--borders-line-2);
    border-radius: 5px;
    padding: 0.375rem 0.625rem;
    font: var(--type-body-medium);
    color: var(--text-primary);
  }

  .dc-input:focus {
    border-color: var(--border-primary-button-border);
  }

  /* A disabled input has to LOOK inert: `.dc-input` sets its own background and
   * colour, so the UA's greying of a disabled field never applies. */
  .dc-input:disabled {
    opacity: 0.4;
  }

  .dc-input-sm {
    padding: 0.25rem 0.5rem;
    font-size: 13px;
  }

  .dc-input-auto {
    width: auto;
  }

  .dc-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.375rem;
    border: 1px solid var(--border-primary-button-border);
    border-radius: 9999px;
    padding: 0.5rem 1.125rem;
    font: var(--type-body-medium);
    color: var(--text-primary);
    transition: color 150ms, background-color 150ms, border-color 150ms;
  }

  .dc-btn:hover {
    border-color: var(--border-primary-button-border);
    background: var(--surface-background-hover);
    color: var(--text-primary);
  }

  .dc-btn:disabled {
    opacity: 0.4;
    pointer-events: none;
  }

  .dc-btn-primary {
    border-color: var(--border-primary-button-border);
    background: var(--button-primary-background-default);
    color: var(--button-primary-text-default);
  }

  .dc-btn-primary:hover {
    background: var(--button-primary-background-hover);
    border-color: transparent;
    color: var(--button-primary-text-default);
  }

  .dc-btn-danger {
    border-color: var(--border-critical);
    color: var(--text-negative);
  }

  .dc-btn-danger:hover {
    background: var(--state-urgent-background);
    border-color: var(--border-critical);
    color: var(--text-negative);
  }

  /* Size modifiers replace hub's `!px-2 !py-0.5` utility overrides. */
  .dc-btn-sm {
    padding: 0.125rem 0.5rem;
    font-size: 12px;
  }

  .dc-btn-icon {
    padding: 0.25rem 0.5rem;
  }

  /* Tables. No outer border: the card supplies it. Rows separate with a top
   * border rather than striping, which survives rows hidden by the filter. */
  .table-scroll {
    overflow-x: auto;
  }

  .dc-table {
    width: 100%;
  }

  /* The header row carries its own surface, a shade off the card's, so the
   * column labels read as a band rather than as a first row (#90). */
  .dc-table thead tr {
    background: var(--surface-contrast-low-2);
  }

  .dc-table th {
    padding: 0.625rem 0.5rem;
    text-align: left;
    white-space: nowrap;
    border-bottom: 1px solid var(--borders-line-2);
    /* The frame of reference for a heading's `.hint-bubble`, so the bubble hangs
     * from the bottom of the CELL rather than from the middle of the icon. */
    position: relative;
  }

  .dc-table td {
    padding: 0.625rem 0.5rem;
    font: var(--type-body);
    border-top: 1px solid var(--borders-line-2);
    vertical-align: middle;
  }

  /* Border only. This rule used to also set `background:` — the same token
   * `:hover` uses — so the first row of every table rendered in the hover state. */
  .dc-table tbody tr:first-child td {
    border-top: 0;
  }

  .dc-table tbody tr:hover {
    background: var(--surface-background-hover);
  }

  .dc-table .right {
    text-align: right;
    font-variant-numeric: tabular-nums;
    /* The row's right gutter: 16px from the card edge, matching the header cell
     * above it so a numeric column and the actions column line up. */
    padding-right: 1rem;
  }

  /* The /invites tables: fit the card, never scroll it. Auto layout, so the
   * columns holding addresses share whatever the compact ones leave, in
   * proportion to what they hold. The compact columns (chips, dates, the
   * organization) are pinned in rem by the `.invite-col-*` block below, so
   * they stay put as the reader switches tabs — the jump that once made
   * these tables `table-layout: fixed` was the compact columns re-measuring
   * themselves per tab, and pinning them is enough to stop it.
   *
   * Headings keep `.dc-table th`'s `nowrap`: under auto layout that makes
   * each heading a floor its column cannot shrink below, so no label is
   * ever crushed onto two lines or run into its neighbour. `.table-scroll`
   * still wraps the table, as a last resort for a viewport narrower than
   * the headings themselves. */
  .dc-table-fitted {
    table-layout: auto;
  }

  /* `.cell-clip` is `nowrap`, so under auto layout its full sentence would
   * become the column's minimum and push the table past the card. Zero
   * width with a 100% floor makes it claim nothing when columns are sized,
   * then fill whatever its column was given. */
  .dc-table-fitted .cell-clip {
    width: 0;
    min-width: 100%;
  }

  /* A column that no longer widens to fit must be allowed to break what it
   * holds; an address has no spaces to wrap at. */
  .break-anywhere {
    overflow-wrap: anywhere;
  }

  /* One line, truncated with an ellipsis — for free text whose length this
   * service does not control, where wrapping would let one row grow to
   * three times its neighbours' height. Hovering a cell marked
   * `data-clip-tip` shows the full text in a `.tip-float` bubble, but only
   * when the line actually clipped (portal.js `initClipTips`).
   *
   * Left-aligned, overriding the `.right` these cells used to carry: an
   * ellipsis truncates at the END of the line, which under right alignment
   * would put the dots where the reader starts reading. */
  .cell-clip {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    text-align: left;
  }

  /* The /invites column skeleton, as classes rather than `<col style>`: the
   * CSP forbids inline styles, and `router_composition` fails the build over
   * one.
   *
   * Only the COMPACT columns are pinned, each to its widest heading or value
   * (measured, plus the 1rem of cell padding; 1.5rem on a `.right` cell).
   * The same kind of value keeps the same width on every tab. The address
   * columns — invitee, requester, decider — carry no width: under
   * `.dc-table-fitted`'s auto layout they share the rest of the card in
   * proportion to their content, which is what spends the space the date
   * column used to sit on. */
  .invite-col-subject-decisions,
  .invite-col-subject-requests,
  .invite-col-subject-log,
  .invite-col-person,
  .invite-col-actor {
    width: auto;
  }

  /* "Organization" heading, and organization names up to about 6rem. */
  .invite-col-org {
    width: 7.5rem;
  }

  /* Sized by its HEADING, not its content: the chips are one word, while
   * "Invited user role" needs 8rem to stay on one line. Used twice per row
   * on the queue (the invitee's role, then the requester's). */
  .invite-col-role {
    width: 9rem;
  }

  /* `DISPLAY_DATE_FORMAT` — "Oct 20, 2026" — is about 5.4rem; this is that
   * plus padding and no more. */
  .invite-col-when {
    width: 6.5rem;
  }

  /* The queue's reject/approve pair, right-aligned. */
  .invite-col-tail {
    width: 7rem;
  }

  /* "Your requests"' status chip, right-aligned. */
  .invite-col-tail-requests {
    width: 9rem;
  }

  .invite-col-decision {
    width: 6rem;
  }

  /* The only column holding a sentence somebody typed, so it cannot be sized
   * to its content: it gets this floor plus whatever the addresses leave, and
   * `.cell-clip` truncates the rest behind a hover bubble. The floor is
   * deliberately low: on a narrow screen this is the column that should give
   * way first, since its full text is a hover away and an address is not. */
  .invite-col-reason {
    width: 6rem;
  }

  /* Every page holding a table: the page itself stays put and only the
   * table scrolls, under headings that stay in view. The main column is
   * pinned to the viewport, and each box on the path from it down to a
   * `.table-scroll` becomes a flex column that may shrink, so a table's card
   * ends at the footer instead of running the page past it. Everything off
   * that path — page header, filter toolbar, detail cards, the pager — keeps
   * its own height, so the table is the only thing that gives. A short table
   * keeps its own height too: nothing here grows a box to fill.
   *
   * With several tables on one page they share what is left, each scrolling
   * on its own. `overflow-y` on the main column is only the last resort, for
   * a page whose fixed parts alone outgrow the viewport.
   *
   * Desktop only: below 860px the sidebar becomes a bar across the top and
   * the page scrolls as a whole, which is the better trade on a phone. */
  @media (min-width: 861px) {
    .app-main:has(.table-scroll) {
      height: 100vh;
      height: 100dvh;
      display: flex;
      flex-direction: column;
      overflow-y: auto;
    }

    .app-main:has(.table-scroll) > .stack-5 {
      flex: 1;
    }

    .app-main :is(.stack-5, .stack-3, [data-tabs], [data-tab-panel], [data-table], .dc-card):has(.table-scroll):not([hidden]) {
      display: flex;
      flex-direction: column;
      min-height: 0;
    }

    .app-main :is(.stack-5, .stack-3, [data-tabs], [data-tab-panel], [data-table], .dc-card):has(.table-scroll) > :not(:has(.table-scroll)):not(.table-scroll) {
      flex-shrink: 0;
    }

    .app-main .table-scroll {
      min-height: 0;
      overflow: auto;
    }

    /* Each heading cell carries the band's colour itself: a sticky `<th>`
     * leaves its row's background behind, and the band has to stay opaque
     * for the rows to pass under it. */
    .app-main .table-scroll thead th {
      position: sticky;
      top: 0;
      z-index: 2;
      background: var(--surface-contrast-low-2);
    }
  }

  /* An empty list ("Nothing is waiting on a decision right now."): the
   * message sits in the middle of its card, not at its top-left corner. */
  .empty-state {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 12rem;
    text-align: center;
  }

  /* On desktop the card also takes the page's remaining height, so the
   * message lands in the centre of the page body rather than in a strip
   * under the tabs. The same pinned-column shape as the table pages above:
   * the main column is the viewport's height and the boxes down to the card
   * pass the remaining space along. */
  @media (min-width: 861px) {
    .app-main:has(.empty-state) {
      height: 100vh;
      height: 100dvh;
      display: flex;
      flex-direction: column;
      overflow-y: auto;
    }

    .app-main:has(.empty-state) > .stack-5,
    .app-main:has(.empty-state) > .stack-5 > [data-tabs],
    [data-tab-panel]:not([hidden]):has(> .empty-state) {
      display: flex;
      flex-direction: column;
      flex: 1;
      min-height: 0;
    }

    .empty-state {
      flex: 1;
    }
  }

  /* A sortable column heading: the label becomes a button, with an arrow
   * that shows the current direction (`aria-sort` on the `<th>`, set by
   * portal.js `initSortable`). Unsorted, the arrow is faint. */
  .th-sort {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
    color: inherit;
    cursor: pointer;
  }

  .th-sort:hover,
  .th-sort:focus-visible {
    color: var(--text-primary);
  }

  .th-sort-icon {
    opacity: 0.4;
    transition: transform 150ms, opacity 150ms;
  }

  [aria-sort="ascending"] .th-sort-icon,
  [aria-sort="descending"] .th-sort-icon {
    opacity: 1;
  }

  [aria-sort="descending"] .th-sort-icon {
    transform: rotate(180deg);
  }

  /* A table cell that leads somewhere (the organization column on /invites,
   * "Managed by" on /tenants). Distinct from `.cell-strong`, which some of
   * those names also carry: this one is about being followable, not weight.
   *
   * Accent-coloured at rest, not only on hover. The hover-only version was
   * indistinguishable from plain text until the pointer happened to cross
   * it, which on a page where SOME organization names link and others
   * deliberately do not left no way to tell the two apart. */
  .cell-link {
    color: var(--text-accent);
    text-decoration: none;
  }

  .cell-link:hover {
    text-decoration: underline;
  }

  .cell-stack {
    display: flex;
    flex-direction: column;
    gap: 0.125rem;
  }

  .cell-strong {
    font-weight: 500;
  }

  .cell-actions {
    display: flex;
    gap: 0.375rem;
    flex-wrap: nowrap;
    justify-content: flex-end;
    white-space: nowrap;
  }

  /* Put this on an element INSIDE a table cell, never on the `<td>` — flex on a
   * `<td>` stops it being a cell, so `vertical-align: middle` no longer applies. */
  .cell-chips {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    flex-wrap: wrap;
  }

  /* Chips (#90). The tone is chosen in Rust and passed in, so the template never
   * string-matches a domain value to pick a colour. */
  .chip {
    display: inline-flex;
    align-items: center;
    border-radius: 9999px;
    background: var(--color-greyscale-grey-12);
    padding: 0.125rem 0.625rem;
    font: var(--type-body);
    white-space: nowrap;
  }

  .chip-muted {
    color: var(--text-secondary-2);
  }

  .chip-cyan {
    background: color-mix(in srgb, var(--text-positive) 14%, transparent);
    color: var(--text-positive);
  }

  .chip-gold {
    background: color-mix(in srgb, var(--state-warning-text) 14%, transparent);
    color: var(--state-warning-text);
  }

  .chip-violet {
    background: color-mix(in srgb, var(--icon-accents-violet) 14%, transparent);
    color: var(--icon-accents-violet);
  }

  .chip-danger {
    background: color-mix(in srgb, var(--text-negative) 14%, transparent);
    color: var(--text-negative);
  }

  .chip-warn {
    background: color-mix(in srgb, var(--state-warning-text) 14%, transparent);
    color: var(--state-warning-text);
  }

  .chip-accent {
    background: color-mix(in srgb, var(--text-accent) 14%, transparent);
    color: var(--text-accent);
  }

  .chip-mono {
    font-family: var(--font-mono);
  }

  /* A chip that is a BUTTON (the /invites decision column): the pill shape
   * and tone come from `.chip` + its tone class, exactly as the status
   * column's read-only pills do, so one decision and its eventual status
   * read as the same object. All this modifier adds is the hit area a bare
   * glyph needs and a hover the read-only chip has no use for. The hover
   * deepens the tone the chip already carries via `currentColor`, so it
   * needs no per-tone rule of its own. */
  .chip-btn {
    padding: 0.5rem;
    transition: background-color 150ms;
  }

  .chip-btn:hover {
    background: color-mix(in srgb, currentColor 26%, transparent);
  }

  .chip-btn .decision-icon {
    width: 20px;
    height: 20px;
  }

  /* Role chip (#90): glyph + uppercased role. Which tier gets the blue accent is
   * decided in Rust — see `ui::role_chip`. */
  .role-chip {
    gap: 0.4375rem;
    padding: 0.25rem 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-primary);
  }

  /* The admin tier's own colour, #A1CCFF; the fill arrives through `currentColor`.
   * A tint of it rather than a primitive, which is one value in both themes. */
  .role-chip-admin {
    background: color-mix(in srgb, var(--color-brand-light-blue) 16%, transparent);
    color: var(--color-brand-light-blue);
  }

  /* #A1CCFF is a dark-theme colour — on white it fails contrast, so light
   * mode takes the design system's own themed accent instead. */
  html.light .role-chip-admin {
    background: color-mix(in srgb, var(--text-accent) 12%, transparent);
    color: var(--text-accent);
  }

  .role-chip-icon {
    flex-shrink: 0;
  }

  /* A leading glyph in a table cell (the org icon on /partners). */
  .cell-icon {
    flex-shrink: 0;
    display: flex;
    color: var(--icon-secondary);
  }

  .chip .dot {
    width: 6px;
    height: 6px;
    border-radius: 9999px;
    margin-right: 0.375rem;
    background: rgb(var(--muted));
  }

  .chip-product-analyzer .dot {
    background: rgb(var(--chart-1));
  }

  .chip-product-runtime .dot {
    background: rgb(var(--chart-2));
  }

  /* Stat tiles across the top of a detail page (#90). `auto-fit` rather than a
   * fixed count so the row reflows when a tile is gated away. */
  .stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: 1rem;
  }

  /* The customer detail page's summary card. Not `.stat-grid`, whose tiles each
   * carry their own border — these are fields INSIDE one card. */
  .summary-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
    gap: 1.25rem 1.5rem;
  }

  .summary-field {
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
    min-width: 0;
    /* The last resort, not the plan: a value with nowhere left to go breaks
     * mid-token rather than pushing out of the card — see `.summary-field-wide`. */
    overflow-wrap: anywhere;
  }

  /* `1 / -1`, NOT `span 2`: a span is never clamped to the explicit grid, so at
   * one column it grew an implicit second track and the card overflowed. */
  .summary-field-wide {
    grid-column: 1 / -1;
  }

  .stat-tile {
    border: 1px solid var(--borders-line-2);
    border-radius: 10px;
    padding: 1rem 1.5rem;
    display: flex;
    flex-direction: column;
    gap: 1rem;
    justify-content: center;
    background: var(--surface-background-box);
  }

  .stat-tile-label {
    font: 300 12px/16px var(--font-mono);
    text-transform: uppercase;
    letter-spacing: 0.02em;
    color: var(--text-secondary-2);
    white-space: nowrap;
  }

  .stat-tile-value {
    font: 400 32px/24px var(--font-mono);
    color: var(--text-primary);
    white-space: nowrap;
  }

  /* A date is narrower per glyph than a count, so it gets its own size
   * rather than forcing the whole row wider to fit 10 mono characters. */
  .stat-tile-value-sm {
    font-size: 18px;
    line-height: 24px;
  }

  /* Tab strip. `portal.js` hides the inactive panels; with scripts off every
   * panel stays visible, which is worse-looking but never hides data. */
  .tab-strip {
    display: flex;
    align-items: stretch;
    gap: 1.75rem;
    border-bottom: 1px solid var(--borders-line-2);
  }

  .tab {
    border: 0;
    background: none;
    padding: 0 0 0.625rem;
    font: var(--type-body-medium);
    color: var(--text-secondary-2);
    white-space: nowrap;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    cursor: pointer;
  }

  .tab:hover {
    color: var(--text-primary);
  }

  .tab[aria-selected='true'] {
    color: var(--color-brand-cornflower);
    border-bottom-color: var(--color-brand-cornflower);
  }

  /* Toolbar above a table. */
  .toolbar {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
  }

  /* The filter bar, OUTSIDE the table card (#90). `.card-head` (the in-card
   * variant this replaces) stays for surfaces not in this pass. */
  .table-toolbar {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    flex-wrap: wrap;
  }

  .table-toolbar .page-info {
    margin-left: auto;
  }

  /* A search `.dc-input` with a leading icon (#90). The icon is `pointer-events:
   * none` and purely decorative — the input keeps its own accessible label. */
  .search-field {
    position: relative;
  }

  .search-field .dc-input {
    padding-left: 2.25rem;
    border-radius: 8px;
    /* Scoped to the search field rather than set on `.dc-input`: toolbar controls
     * sit on the PAGE; a bare `.dc-input` keeps `--border-lines`. */
    border-color: var(--borders-line-2);
  }

  /* The mockup's own 300px search box; shrinks rather than overflowing. */
  .table-toolbar .search-field .dc-input {
    width: 18.75rem;
    max-width: 100%;
    padding-top: 0.5rem;
    padding-bottom: 0.5rem;
  }

  /* Custom select chevron drawn in CSS, not a `url(data:…)` background: the
   * portal's own CSP is `img-src 'self'`, which blocks a data: URI outright. */
  .pill-select,
  select.dc-input {
    appearance: none;
    -webkit-appearance: none;
    padding-right: 2.5rem;
  }

  .select-field {
    position: relative;
    display: block;
  }

  .select-field::after,
  .filter-trigger::after {
    content: '';
    position: absolute;
    right: 1rem;
    top: 50%;
    width: 0.4375rem;
    height: 0.4375rem;
    /* Two adjacent borders rotated 45° = a chevron pointing down. */
    border-right: 1.4px solid var(--icon-secondary);
    border-bottom: 1.4px solid var(--icon-secondary);
    transform: translateY(-70%) rotate(45deg);
    pointer-events: none;
  }

  /* The OPEN select. `appearance: none` above only ever reached the closed
   * control; `base-select` hands the picker to CSS, and `@supports` is the fallback. */
  @supports (appearance: base-select) {
    .select-field select,
    .select-field select::picker(select) {
      appearance: base-select;
    }

    /* The wrapper's own `::after` chevron stays and the built-in one goes: two
     * chevrons otherwise stack in the same corner. */
    .select-field select::picker-icon {
      display: none;
    }

    /* The same panel the row menus use — same surface, edge, radius, shadow and
     * inset — because they are the same object to a reader. */
    .select-field select::picker(select) {
      /* ALWAYS below the control, and scroll rather than flip: the UA's
       * `position-try-fallbacks` flipped it above, over the field's own label. */
      position-area: block-end span-inline-end;
      position-try-fallbacks: none;
      /* Roughly six options before it scrolls. `overscroll-behavior` contains the
       * scroll chain, which otherwise scrolls the dialog behind an open picker. */
      max-height: 15rem;
      overflow-y: auto;
      overscroll-behavior: contain;
      margin-top: 6px;
      border: 1px solid var(--borders-line-2);
      border-radius: 10px;
      background: var(--surface-background-box);
      box-shadow: var(--shadow-float);
      padding: 0.375rem;
    }

    .select-field option {
      display: flex;
      align-items: center;
      padding: 0.5rem 0.625rem;
      font: var(--type-body-medium);
      color: var(--text-primary);
      cursor: pointer;
    }

    /* Dividers between options, on the same rule the row menus use. */
    .select-field option + option {
      border-top: 1px solid var(--borders-line-2);
    }

    /* `:hover` alone leaves keyboard users with no visible cursor: the
     * picker moves focus with the arrow keys and never fires hover. */
    .select-field option:hover,
    .select-field option:focus {
      background: var(--surface-background-hover);
    }

    /* The current value reads as chosen by WEIGHT. The UA's `::checkmark` is
     * suppressed rather than styled: it reserves a column of space per option. */
    .select-field option:checked {
      font-weight: 500;
    }

    .select-field option::checkmark {
      display: none;
    }
  }

  .pill-select {
    /* Same edge as the search box beside it and the card below — see
     * `.search-field .dc-input`. */
    border: 1px solid var(--borders-line-2);
    border-radius: 9999px;
    background-color: var(--surface-background-box);
    padding: 0.5rem 2.5rem 0.5rem 0.875rem;
    font: var(--type-body);
    color: var(--text-secondary-2);
  }

  .pill-select:hover {
    border-color: var(--border-primary-button-border);
  }

  /* The one `.pill-select` that is a real `<button>` — "Clear filters" draws no
   * chevron, so the right padding reserved for it is dead space here. */
  .pill-select[data-filter-clear] {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding-right: 0.875rem;
  }

  /* A <select> matches the sheet-wide outline reset and has no `:focus` border of
   * its own — without this the filter pills have no keyboard focus indicator. */
  .pill-select:focus-visible {
    outline: 2px solid rgb(var(--accent));
    outline-offset: 2px;
  }

  /* A `<details data-menu>` wearing `.pill-select`, so it inherits the menu
   * behaviour (outside click, Escape) that `initMenus` gives the row menus. */
  .filter-menu {
    position: relative;
  }

  .filter-trigger {
    display: inline-flex;
    align-items: center;
    cursor: pointer;
    white-space: nowrap;
  }

  /* The chevron is `.select-field::after`'s rule, shared by selector rather than
   * copied. Here it also turns over, because this control has an open state. */
  .filter-menu[open] > .filter-trigger::after {
    transform: translateY(-20%) rotate(-135deg);
  }

  .filter-menu[open] > .filter-trigger {
    border-color: var(--border-primary-button-border);
  }

  /* The surface is `.row-menu-panel`'s, shared by selector there; only the
   * anchoring edge and the width rule are this panel's own. */
  .filter-panel {
    left: 0;
    /* Never narrower than its own trigger, never narrower than its longest
     * option. */
    min-width: 100%;
    width: max-content;
    /* The option list grows with the data, so the panel has to scroll rather than
     * run off the window. `vh` because what it must fit inside is the viewport. */
    max-height: 60vh;
    overflow-y: auto;
  }

  .filter-option {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.5rem 0.625rem;
    font: var(--type-body-medium);
    color: var(--text-primary);
    cursor: pointer;
    white-space: nowrap;
  }

  .filter-option:hover {
    background: var(--surface-background-hover);
  }

  /* The option's own words take the slack, so a count sits hard against the
   * panel's right edge and the column of numbers lines up. */
  .filter-option-text {
    flex: 1 1 auto;
  }

  /* `appearance: none` and drawn here, because the UA checkbox ignores the theme.
   * Both marks are borders and backgrounds for the CSP reason above — no data URI. */
  .filter-check {
    appearance: none;
    -webkit-appearance: none;
    position: relative;
    flex-shrink: 0;
    width: 20px;
    height: 20px;
    margin: 0;
    border: 1px solid var(--borders-line-2);
    border-radius: 6px;
    background: rgb(var(--chip) / 0.6);
    cursor: pointer;
  }

  /* Ticked: NOT a filled swatch — the box keeps its dark face and the mark alone
   * carries the colour. Both tokens are theme-aware, which this pair has to be. */
  .filter-check:checked {
    background: transparent;
    border-color: var(--icon-primary);
  }

  /* Two adjacent borders rotated 45° = a tick — a border, not an image, for
   * the CSP reason the chevron above documents. */
  .filter-check:checked::after {
    content: '';
    position: absolute;
    left: 5.5px;
    top: 2px;
    width: 6px;
    height: 10px;
    border-right: 2px solid var(--state-success-icon);
    border-bottom: 2px solid var(--state-success-icon);
    transform: rotate(45deg);
  }

  /* Part of the set, not all of it: the same edge as ticked, with a dash where
   * the tick would be. One control in three states, not two controls. */
  .filter-check:indeterminate {
    background: transparent;
    border-color: var(--icon-primary);
  }

  .filter-check:indeterminate::after {
    content: '';
    position: absolute;
    left: 4px;
    right: 4px;
    top: 50%;
    height: 2px;
    border-radius: 1px;
    background: var(--state-success-icon);
    transform: translateY(-50%);
  }

  .filter-check:focus-visible {
    outline: 2px solid rgb(var(--accent));
    outline-offset: 2px;
  }

  .search-field-icon {
    position: absolute;
    left: 0.625rem;
    top: 50%;
    transform: translateY(-50%);
    color: var(--icon-secondary);
    pointer-events: none;
  }

  .page-info {
    font: var(--type-body);
    color: var(--text-secondary-2);
    white-space: nowrap;
  }

  .pager {
    display: flex;
    gap: 0.25rem;
    justify-content: flex-end;
    padding: 0.75rem 1rem;
  }

  .pager:empty {
    display: none;
  }

  .page-btn {
    min-width: 1.75rem;
    border: 1px solid var(--borders-line-2);
    border-radius: 5px;
    padding: 0.125rem 0.5rem;
    font: var(--type-body);
    color: var(--text-secondary-2);
    font-variant-numeric: tabular-nums;
  }

  .page-btn:hover:not(:disabled) {
    color: var(--text-primary);
    border-color: var(--border-primary-button-border);
  }

  .page-btn[aria-current='page'] {
    color: var(--text-primary);
    border-color: var(--border-primary-button-border);
  }

  .page-btn:disabled {
    opacity: 0.4;
    pointer-events: none;
  }

  .empty-state {
    padding: 2rem 0;
    text-align: center;
    font: var(--type-body-medium);
    color: var(--text-disabled);
  }

  /* Forms. */
  .field {
    display: block;
  }

  .field-label {
    display: block;
    margin-bottom: 0.375rem;
  }

  /* The reset zeroes every `<p>`'s margin, which put "Forgot password?"
   * flush against the password box with no breathing room at all. */
  .field-below-link {
    margin-top: 0.625rem;
  }

  .form-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
    gap: 0.75rem;
  }

  .form-stack {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
  }

  .form-actions {
    display: flex;
    gap: 0.5rem;
    justify-content: flex-end;
  }

  /* An advisory note: a bare left bar, no fill. The filled variant read as
   * an alert box and out-weighed the fields above it. */
  .notice {
    padding: 0 0 0 0.75rem;
    border-left: 2px solid var(--state-warning-text);
    background: none;
    font: 400 13px/18px var(--font-sans);
    color: var(--text-secondary-2);
    text-wrap: pretty;
  }

  .is-error {
    color: var(--text-negative);
  }

  /* Modals are native <dialog>: Escape, focus trap, inert background and top-layer
   * stacking come from the platform — no z-index here, no keydown handler. */
  .dc-modal {
    margin: auto;
    width: 100%;
    max-width: 40rem;
    padding: 5vh 1rem;
  }

  /* The scrim: see `--scrim` for the per-theme value. `::backdrop` reads a `:root`
   * custom property only by inheritance, which older engines did not always do. */
  .dc-modal::backdrop {
    background: var(--scrim);
  }

  .modal-panel {
    background: var(--surface-background-box);
    /* 0.5px, not 1px: the design draws every modal edge as a hairline, which
     * a 2x display renders as a true half-pixel and a 1x one rounds up. */
    border: 0.5px solid var(--borders-line-2);
    border-radius: 10px;
    padding: 2rem;
    box-shadow: var(--shadow-lift);
    display: flex;
    flex-direction: column;
    gap: 2rem;
    max-height: 90vh;
    overflow-y: auto;
  }

  /* The close button sits alone above the title, right-aligned. */
  .modal-close {
    display: flex;
    justify-content: flex-end;
    /* Pulls the row tight to the panel's top padding without shrinking the
       button's own hit area. */
    margin-bottom: -1rem;
  }

  /* Sized to the icon `ui::modal_close` puts inside it. */
  .modal-x {
    width: 2.5rem;
    height: 2.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-secondary-2);
  }

  .modal-x:hover {
    color: var(--text-primary);
  }

  .modal-intro {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
  }

  .modal-title {
    font: 500 22px/29px var(--font-sans);
    color: var(--text-primary);
  }

  .modal-lede {
    font: 400 14px/20px var(--font-sans);
    color: var(--text-primary);
    text-wrap: pretty;
  }

  .modal-text {
    font: var(--type-body-medium);
  }

  /* An icon beside the title, for a modal whose whole purpose is one verdict
   * (approve/reject) rather than a neutral action. */
  .modal-heading {
    display: flex;
    align-items: center;
    gap: 0.75rem;
  }

  .modal-heading .decision-icon {
    width: 2rem;
    height: 2rem;
  }

  /* Same tones `.chip-cyan`/`.chip-danger` use for status. The ICON alone
   * carries the verdict's colour — the lede below it stays the ordinary
   * `.modal-lede` grey, so the panel reads as one explanatory paragraph
   * rather than as a warning banner. */
  .modal-heading-positive {
    color: var(--text-positive);
  }

  .modal-heading-negative {
    color: var(--text-negative);
  }

  /* The decision modals state a consequence rather than give an
   * instruction, so their lede sits a step back from `.modal-lede`'s
   * default — the title and its verdict icon are what should carry. */
  .modal-lede-muted {
    color: var(--text-secondary-2);
  }

  /* The decision modal's read-only context card (who, what role, which
   * organization) — a surface sitting inside the panel, the same job
   * `--surface-background-pop-up` already does for a dropdown floating over
   * a page, reused here instead of inventing a second nested-card token. */
  .modal-summary {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    align-self: stretch;
    gap: 1rem;
    padding: 1rem;
    border-radius: 8px;
    background: var(--surface-background-pop-up);
  }

  .modal-summary-row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
  }

  /* The design's own halo: a blurred neutral disc sitting under a crisp
   * glyph, not a solid-filled badge. The blur is CSS, not baked into the
   * icon SVG, so it stays on the `::before` alone — blurring the real
   * element would blur the glyph drawn on top of it too. */
  .modal-summary-icon {
    position: relative;
    flex-shrink: 0;
    width: 30px;
    height: 30px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--state-neutral-icon);
  }

  .modal-summary-icon::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 9999px;
    background: var(--state-neutral-background);
    filter: blur(4px);
  }

  /* The glyph is drawn about a quarter of the way across the design's
   * 48-unit canvas, so the SVG is sized at ~4x the glyph it should show.
   * Out of flow, because the halo above owns the 30px layout box and a
   * 60px SVG inside the flex row would shove the label sideways. */
  .modal-summary-icon .info-glyph {
    position: absolute;
    width: 60px;
    height: 60px;
    /* Opts out of the `svg { max-width: 100% }` reset, which would otherwise
     * clamp this deliberately oversized canvas back to the 30px halo. */
    max-width: none;
  }

  /* `role_chip_slot` renders both tier glyphs and hides one. The UA rule
   * for `[hidden]` is `display: none`, but it loses to any author `display`
   * an SVG picks up, so state it here rather than depend on cascade luck. */
  .role-chip-icon[hidden] {
    display: none;
  }

  /* The form's own rhythm: field groups, then actions. */
  .modal-form {
    display: flex;
    flex-direction: column;
    gap: 2rem;
  }

  .modal-fields {
    display: flex;
    flex-direction: column;
    gap: 1rem;
  }

  /* Two fields side by side (Account type + Product, Tenant + Role). Falls
     back to stacked on a narrow viewport. */
  .field-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
  }

  @media (max-width: 34rem) {
    .field-row {
      grid-template-columns: 1fr;
    }
  }

  /* Modal inputs are the taller variant the mockup draws. */
  .modal-panel .dc-input {
    padding: 0.6875rem 0.875rem;
    border-radius: 8px;
  }

  /* A field inside its own frosted panel (design: `box.svg`) — the inverse
   * of `.modal-summary` above it: there the card is darker than the modal,
   * here it is lighter and the input well recedes into it. The `.dc-input`
   * inside already paints `--surface-background-box` on `--borders-line-2`,
   * which IS the design's inner box, so only this wrapper is new. */
  .field-panel {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    padding: 1rem;
    border-radius: 8px;
    /* The design's own angle and stops. 10% alpha, not the 20% the
     * `--color-blue-*-20-trans` tokens carry, so the two stops are spelled
     * out: #23364C and #3C4D61, the same pair those tokens hold. */
    background: linear-gradient(
      92deg,
      rgb(35 54 76 / 0.1) 5.44%,
      rgb(60 77 97 / 0.1) 91.58%);
    backdrop-filter: blur(5px);
  }

  /* A GRADIENT border, on a panel whose own fill is translucent.
   *
   * The usual two-background trick (fill clipped to padding-box, gradient to
   * border-box) needs an OPAQUE fill to hide the gradient behind the middle
   * of the box; these panels are 10%-alpha glass, so the gradient reads
   * straight through and floods them. This paints the ring on `::before`
   * instead and masks away everything but the edge, which does not care what
   * the fill underneath is doing.
   *
   * `padding` is the ring's thickness. Unlike `border-width`, which Chrome
   * floors at one pixel, a padding honours sub-pixel values. */
  .ring {
    position: relative;
  }

  .ring::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    border-radius: inherit;
    padding: 1px;
    mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
    mask-composite: exclude;
    -webkit-mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    pointer-events: none;
  }

  /* The information card: lit at the top AND bottom edge, neutral through
   * the middle — the design's four stops, which is how a glass panel catches
   * light along both horizontal edges. */
  .ring-glass::before {
    background: linear-gradient(
      180deg,
      var(--color-blue-blue-500-2) 0%,
      var(--color-blue-blue-700-2) 12.39%,
      var(--color-blue-blue-700-2) 80.49%,
      var(--color-blue-blue-500-2) 93.99%);
  }

  /* The reason box: a Figma *diamond* gradient centred on the top edge —
   * #A1CCFF at top-centre, fading out toward the corners and down the sides.
   * CSS has no diamond gradient, but one centred on an edge is exactly two
   * linear fades, one per half: `to bottom left|right` on a half-width box
   * makes the iso-lines run corner to corner, and its 50% line lands on the
   * far corners where the diamond reaches zero. The design's second stop is
   * `#66666600` — transparent, so only the fade matters, not the hue. */
  .ring-lit::before {
    background:
      linear-gradient(to bottom left, var(--color-brand-blue), transparent 50%)
        left top / 50% 100% no-repeat,
      linear-gradient(to bottom right, var(--color-brand-blue), transparent 50%)
        right top / 50% 100% no-repeat;
  }

  .modal-actions {
    display: flex;
    gap: 1rem;
    justify-content: flex-end;
    align-items: center;
  }

  /* For a form whose primary button must come first in the markup, because
   * Enter submits with the first submit button: still drawn right-aligned
   * with the primary on the right. */
  .modal-actions-reversed {
    flex-direction: row-reverse;
    justify-content: flex-start;
  }

  /* Initials avatar (#90): ported from the Exein Analyzer design system's
   * AvatarName component — a fixed 32px circle, wherever a row names a person. */
  .avatar-initials {
    flex-shrink: 0;
    width: 2rem;
    height: 2rem;
    border-radius: 9999px;
    background: var(--color-brand-lavender-12);
    box-shadow:
      inset 0 0 0 1px var(--color-brand-lavender-12),
      inset 0 5px 15px rgb(255 255 255 / 0.15);
    display: flex;
    align-items: center;
    justify-content: center;
    font: var(--type-body-medium);
    color: var(--text-primary-2);
  }

  /* Pill buttons (#90): the design system's PrimaryButtonDark shape and tokens at
   * its own 11px/40px spec. Distinct from `.dc-btn`, the mockup's other size. */
  .pill-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    height: 2.5rem;
    border: 1px solid var(--borders-line-2);
    border-radius: 9999px;
    padding: 0 1.25rem;
    font: 400 11px/1 var(--font-sans);
    color: var(--text-primary);
    background: none;
    transition: color 150ms, background-color 150ms, border-color 150ms;
  }

  .pill-btn:hover {
    background: var(--surface-background-hover);
  }

  .pill-btn:disabled {
    opacity: 0.4;
    pointer-events: none;
  }

  /* NEVER point `border-color` at a token that can be invalid: an invalid
   * `border-color` falls back to `currentColor` and the pill drew a WHITE ring. */
  .pill-btn-primary {
    border-color: var(--pill-primary-border);
    background: var(--button-primary-background-default);
    color: var(--button-primary-text-default);
  }

  .pill-btn-primary:hover {
    background: var(--button-primary-background-hover);
  }

  /* A feature's global level (ombrello#315). The theme switch's track and knob,
   * but it opens a confirm dialog rather than flipping in place, so the state is
   * `data-on`, not `aria-pressed`. */
  .flag-switch {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    background: none;
    border: 0;
    padding: 0;
    color: var(--text-secondary-2);
    cursor: pointer;
  }

  .flag-switch:disabled {
    cursor: not-allowed;
    opacity: 0.6;
  }

  .flag-switch-track {
    position: relative;
    width: 2.25rem;
    height: 1rem;
    border-radius: 9999px;
    background: var(--borders-line-2);
    transition: background-color 150ms;
  }

  .flag-switch-knob {
    position: absolute;
    top: 50%;
    left: 2px;
    width: 0.75rem;
    height: 0.75rem;
    margin-top: -0.375rem;
    border-radius: 9999px;
    background: var(--color-greyscale-white);
    transition: left 150ms;
  }

  .flag-switch[data-on='true'] {
    color: var(--text-positive);
  }

  .flag-switch[data-on='true'] .flag-switch-track {
    background: var(--text-positive);
  }

  .flag-switch[data-on='true'] .flag-switch-knob {
    left: calc(100% - 0.75rem - 2px);
  }

  /* The feature-flag tables' column skeleton, the `.invite-col-*` approach:
   * every compact column is pinned in rem, so the one flexible column (the
   * customer or user) resolves to the same width in each table and the
   * customer and user override tables on one page line up column for column.
   * Widths are the widest value plus cell padding: a chip, a UTC datetime,
   * and the three-button control with its "(off)" hint. */
  .flag-col-subject {
    width: auto;
  }

  .flag-col-value {
    width: 7rem;
  }

  .flag-col-updated {
    width: 13rem;
  }

  .flag-col-effective {
    width: 12rem;
  }

  .flag-col-override {
    width: 15rem;
  }

  /* The tri-state override control: inherit / on / off as one pill group. The
   * pressed button is the current state; pressing it again is a no-op write. */
  .seg-control {
    display: inline-flex;
    border: 1px solid var(--borders-line-2);
    border-radius: 9999px;
    overflow: hidden;
  }

  .seg-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    height: 2rem;
    border: 0;
    padding: 0 0.875rem;
    font: 400 11px/1 var(--font-sans);
    color: var(--text-secondary-2);
    background: none;
    transition: color 150ms, background-color 150ms;
  }

  .seg-btn + .seg-btn {
    border-left: 1px solid var(--borders-line-2);
  }

  .seg-btn:hover {
    background: var(--surface-background-hover);
  }

  .seg-btn[aria-pressed='true'] {
    background: color-mix(in srgb, var(--text-accent) 14%, transparent);
    color: var(--text-accent);
  }

  .seg-btn:disabled {
    cursor: not-allowed;
    pointer-events: none;
  }

  .seg-btn:disabled:not([aria-pressed='true']) {
    opacity: 0.5;
  }

  .seg-hint {
    opacity: 0.7;
  }

  /* Native <details>/<summary>, so a row's actions stay reachable with JS off;
   * portal.js only adds polish. `position: relative` is for its panel placement. */
  .row-menu {
    position: relative;
    display: flex;
    justify-content: flex-end;
  }

  .row-menu-trigger {
    width: 2rem;
    height: 2rem;
    border-radius: 9999px;
    border: 1px solid transparent;
    /* `--icon-secondary`, NOT `--button-primary-icon-default`: that one is white
     * in BOTH themes, so the dots were white on white on the light card. */
    color: var(--icon-secondary);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    flex-shrink: 0;
  }

  .row-menu-trigger:hover,
  .row-menu[open] > .row-menu-trigger {
    border-color: var(--borders-line-2);
    background: var(--surface-background-hover);
  }

  /* In a page header the menu stands beside a pill button, so it takes the pill's
   * height and `--text-primary` — not a literal white, invisible on light. */
  .page-actions .row-menu-trigger {
    width: 2.5rem;
    height: 2.5rem;
    color: var(--text-primary);
    /* Edged at rest, not only on hover as it is in a table: up here it stands
     * alone beside a bordered pill. Same token the pill uses. */
    border-color: var(--borders-line-2);
  }

  /* The glyph grows with its button, or the dots sit marooned in the middle
   * of a circle half again their old size. */
  .page-actions .row-menu-trigger svg {
    width: 14px;
    height: 14px;
  }

  /* One surface for every list of choices floating over the page. Shared by
   * selector rather than copied: the profile panel was copied and has drifted. */
  .row-menu-panel,
  .filter-panel {
    position: absolute;
    top: calc(100% + 6px);
    z-index: 55;
    background: var(--surface-background-box);
    border: 1px solid var(--borders-line-2);
    border-radius: 10px;
    box-shadow: var(--shadow-float);
    padding: 0.375rem;
    display: flex;
    flex-direction: column;
    /* No gap: the actions sit flush so the rule between them reads as one divider
     * spanning the panel. */
    gap: 0;
  }

  .row-menu-panel {
    right: 0;
    min-width: 12rem;
  }

  /* Names the row the open panel belongs to — it floats over neighbouring
   * rows, so without this the actions are unattributed. */
  .row-menu-head {
    display: block;
    padding: 0.375rem 0.625rem 0.25rem;
    font: var(--type-label-small);
    font-size: 10px;
    line-height: 14px;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: var(--text-secondary-2);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .row-menu-icon {
    flex-shrink: 0;
  }

  /* The glyph a page-header `.pill-btn` carries. Laid out by `.pill-btn`'s own
   * `gap`; this only keeps it from being squeezed by a long label. */
  .btn-icon {
    flex-shrink: 0;
  }

  .row-menu-item {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    width: 100%;
    padding: 0.5rem 0.625rem;
    border: 0;
    border-radius: 6px;
    background: none;
    text-align: left;
    font: var(--type-body-medium);
    color: var(--text-primary);
    cursor: pointer;
    white-space: nowrap;
  }

  .row-menu-item:hover {
    background: var(--surface-background-hover);
  }

  /* An action that is present but cannot be taken. `:hover` is neutralised
   * explicitly, or `.row-menu-item:hover` still makes the row read as clickable. */
  .row-menu-item:disabled {
    opacity: 0.4;
    cursor: not-allowed;
  }

  .row-menu-item:disabled:hover {
    background: none;
  }

  .row-menu-item-danger {
    color: var(--text-negative);
  }

  /* DECLARED HERE, after `.row-menu-item`, and that placement is the whole rule:
   * that rule's `border: 0` is equally specific and would win on source order. */
  /* Direct children, not `.row-menu-item + .row-menu-item`: on `/my-org` each
   * action is wrapped in its own `<form>`, which that tighter selector misses. */
  .row-menu-panel > * + *,
  .filter-panel > * + * {
    border-top: 1px solid var(--borders-line-2);
  }

  /* …except under the subject heading, which is a label rather than an
   * action — a rule there reads as separating the menu from its own title. */
  .row-menu-head + * {
    border-top: 0;
  }

  /* Square, now that the items are flush and divided: a rounded hover background
   * between two full-width rules left a notch at each end of the line. */
  .row-menu-panel .row-menu-item {
    border-radius: 0;
  }


  /* The set-password card (auth_set_password.html): a centred card on the
   * chrome-less layout, for the audience that must not get portal navigation. */
  .auth-card-shell {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2rem 1rem;
  }

  /* The wordmark, inside the card and above the heading. `margin-bottom` rather
   * than the `.stack-3` gap alone: it reads as a masthead over the heading. */
  .auth-card-brand {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    margin-bottom: 0.5rem;
    color: var(--text-primary);
  }

  .auth-card {
    width: 100%;
    max-width: 620px;
  }

  /* Sentence case, and its own class rather than `.dc-label`: every other label
   * in the portal is an uppercase micro-label; this one asks to be readable. */
  .field-label-sentence {
    display: block;
    margin-bottom: 0.375rem;
    font: var(--type-body-medium);
    color: var(--text-primary);
  }

  /* A field with a control inside it. The input keeps its own padding on the
   * right so text never runs under the button. */
  .field-affix {
    position: relative;
    display: block;
  }

  .field-affix .dc-input {
    padding-right: 2.75rem;
  }

  .field-affix-btn {
    position: absolute;
    right: 0.5rem;
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    border-radius: 6px;
    background: none;
    color: var(--icon-secondary);
    cursor: pointer;
  }

  .field-affix-btn:hover {
    color: var(--text-primary);
  }

  /* The slash is drawn only while the password is HIDDEN, so the glyph reads
   * as "reveal" rather than as a state nobody can interpret. */
  .field-affix-btn[aria-pressed='true'] .field-affix-icon-slash {
    display: none;
  }

  /* The requirements list. Always rendered; `data-state` is what portal.js flips
   * per rule, and its absence — scripts off — leaves a readable neutral list. */
  .rule-list {
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .rule {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    font: var(--type-body-medium);
    color: var(--text-secondary-2);
  }

  .rule-mark {
    flex-shrink: 0;
    width: 6px;
    height: 6px;
    border-radius: 9999px;
    background: currentColor;
  }

  .rule[data-state='pass'] {
    color: var(--text-positive);
  }

  .rule[data-state='fail'] {
    color: var(--text-negative);
  }

  .field-error {
    font: var(--type-body-medium);
    color: var(--text-negative);
  }

  .btn-block {
    width: 100%;
  }

  /* The submit's loading state. A spinner that exists only while `data-loading`
   * is set, so the button has no reserved gap the rest of the time. */
  .btn-spinner {
    display: none;
    width: 0.875rem;
    height: 0.875rem;
    border-radius: 9999px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    animation: spin 600ms linear infinite;
  }

  .pill-btn[data-loading] .btn-spinner {
    display: block;
  }

  @keyframes spin {
    to {
      transform: rotate(360deg);
    }
  }

  /* Respect a reduced-motion preference: the spinner still appears, it just
   * stops rotating, so the button state is still legible. */
  @media (prefers-reduced-motion: reduce) {
    .btn-spinner {
      animation: none;
    }
  }

  /* Toasts (#90): driven by portal.js from the same `?success=`/`?error=` query
   * params the flash contract already used — the server-side signal is unchanged. */
  .toast-stack {
    position: fixed;
    top: 1.5rem;
    right: 1.5rem;
    z-index: 70;
    display: flex;
    /* Plain `column`, never `column-reverse`: the stack grows down from a fixed
     * top edge, so an arriving toast never shoves the ones already reading. */
    flex-direction: column;
    gap: 0.5rem;
    align-items: flex-end;
    pointer-events: none;
  }

  /* Narrow viewports: span the width instead of pinning to a corner. `width: auto`
   * is what lets the stack's `stretch` win over the fixed 21rem below. */
  @media (max-width: 40rem) {
    .toast-stack {
      left: 1rem;
      right: 1rem;
      top: 1rem;
      align-items: stretch;
    }

    .toast {
      width: auto;
    }
  }

  /* One accent per severity, read by the top rule, the wash under it and the
   * icon. Success is the unmarked case: `.toast` alone is a success toast. */
  .toast {
    --toast-accent: var(--text-positive);

    pointer-events: auto;
    position: relative;
    /* The top rule below is a full-bleed child; without this it would run
     * straight across the rounded corners. */
    overflow: hidden;
    display: grid;
    grid-template-columns: auto 1fr auto;
    /* Centred on the whole text block, not on the title: the icon and the
     * dismiss button are about the toast, not about its first line. */
    align-items: center;
    column-gap: 0.875rem;
    /* A fixed width, not `max-width`: the stack is a column of cards and
     * ragged edges read as three unrelated things. Long messages wrap. */
    width: 21rem;
    max-width: 100%;
    background-color: var(--surface-background-pop-up);
    /* The accent bleeding down off the top rule. Its strength is a variable
     * because the same mix is not the same picture in both themes. */
    background-image: linear-gradient(
      180deg,
      color-mix(in srgb, var(--toast-accent) var(--toast-wash, 9%), transparent),
      transparent 45%
    );
    border: 1px solid var(--borders-line-2);
    border-radius: 12px;
    padding: 1rem 1.125rem;
    box-shadow: var(--shadow-float);
    color: var(--text-primary);
  }

  /* A 9% wash that is a faint glow on a near-black card is a tinted card on a
   * white one. `html.light` alone: a toast only exists because portal.js built it. */
  html.light .toast {
    --toast-wash: 4%;
  }

  .toast-error {
    --toast-accent: var(--text-negative);
  }

  .toast-warning {
    --toast-accent: var(--text-warning);
  }

  /* The lit top edge. Faded out at both ends so it reads as light falling on
   * the card's rim rather than a border segment that stops dead. */
  .toast::before {
    content: '';
    position: absolute;
    inset: 0 0 auto;
    height: 1px;
    background: linear-gradient(
      90deg,
      transparent 4%,
      var(--toast-accent) 45%,
      transparent 96%
    );
  }

  /* The ring and its glyph are one SVG: `currentColor` inherits the accent,
   * and the exclamation's dot is the one filled part (see `toastIcon`). */
  .toast-icon {
    width: 20px;
    height: 20px;
    color: var(--toast-accent);
    stroke: currentColor;
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
    fill: none;
  }

  .toast-body {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    min-width: 0;
    /* The card is a fixed 21rem with `overflow: hidden`, and the copy carries
     * addresses — one long unbreakable token would otherwise be cut off. */
    overflow-wrap: anywhere;
  }

  .toast-title {
    font: 500 14px/16px var(--font-sans);
    color: var(--text-primary);
    text-wrap: pretty;
  }

  .toast-text {
    font: 400 14px/16px var(--font-sans);
    color: var(--text-secondary-2);
    text-wrap: pretty;
  }

  /* The only way to clear an ERROR toast, which does not expire on its own (see
   * `showToast`) — hence a real button with a label. */
  .toast-x {
    width: 1.5rem;
    height: 1.5rem;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-secondary-2);
    font-size: 16px;
    line-height: 1;
  }

  .toast-x:hover {
    color: var(--text-primary);
  }

  /* The exit is the slower of the two on purpose. `forwards` holds the last frame
   * so there is no flash of a fully opaque card before `dismissToast` removes it. */
  @media (prefers-reduced-motion: no-preference) {
    .toast {
      animation: toast-in 160ms ease-out;
    }

    .toast-leaving {
      animation: toast-out 260ms ease forwards;
    }
  }

  /* Outside the motion guard: with animation suppressed this is the whole
   * exit, so the card still goes before its node does. */
  .toast-leaving {
    opacity: 0;
    pointer-events: none;
  }

  @keyframes toast-in {
    from {
      opacity: 0;
      transform: translateX(-0.5rem);
    }
  }

  @keyframes toast-out {
    from {
      opacity: 1;
    }

    to {
      opacity: 0;
      transform: translateX(-0.375rem);
    }
  }
}

/* Utilities: a deliberately short list. Porting Tailwind's scale is the road back
 * to a build step; these are the handful the templates actually need. */
@layer utilities {
  .stack-5 {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
  }

  .stack-3 {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
  }

  .row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
  }

  .grow {
    flex: 1;
  }

  /* A form taking a flex slot (the sign-out POST form): the form gets the
     slot, the button inside fills the form. */
  .grow > .dc-btn {
    width: 100%;
  }

  .muted {
    color: rgb(var(--muted));
  }

  .mono {
    font-family: var(--font-mono);
  }

  /* Figures that line up column-wise — dates and counts. `nowrap` because
   * every value this class is used on is a single short token or a
   * formatted date, and breaking one across two lines doubles a row's
   * height to say nothing new. */
  .tabular {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }

  .text-sm {
    font-size: 13px;
  }

  .text-xs {
    font-size: 12px;
  }

  /* Type scale (#90), ported verbatim from the Exein Analyzer design system's
   * tokens/typography.css, so a future re-sync stays a diff. */
  .type-heading-3 {
    font: 500 16px/16px var(--font-sans);
  }

  .type-body-medium {
    font: 400 14px/16px var(--font-sans);
  }

  .type-body {
    font: 400 12px/16px var(--font-sans);
  }

  .type-body-small {
    font: 400 10px/14px var(--font-sans);
  }

  .type-label-big {
    font: 300 12px/16px var(--font-mono);
    letter-spacing: 0.02em;
    text-transform: uppercase;
  }

  .type-label-small {
    font: 300 9px/8px var(--font-mono);
    letter-spacing: 0.02em;
    text-transform: uppercase;
  }

  /* The full identifier, on one line. `min-width` in `ch` makes the cell RESERVE
   * room for all 36 characters — a partially elided UUID cannot be matched. */
  .cell-id {
    white-space: nowrap;
    min-width: 36ch;
  }

  .max-w-xs {
    max-width: 20rem;
  }

  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }

  /* With JS off no class is ever written to <html>, so the switch cannot work —
   * hide it rather than ship a dead control. */
  html:not(.dark):not(.light) .theme-switch {
    display: none;
  }
}

/* Deliberately unlayered, and the only block in the sheet that is: unlayered
 * rules outrank every layer. Keep new rules here layout-only. */
@media (max-width: 860px) {
  .app {
    display: block;
  }

  /* A full-width bar across the top, so every part of the desktop rule that pins
   * it to the side of the viewport has to come back off. */
  .app-sidebar {
    position: static;
    height: auto;
    align-self: auto;
    width: auto;
    flex-direction: row;
    align-items: center;
    gap: 1rem;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid rgb(var(--line-soft));
  }

  .nav {
    display: flex;
    flex-direction: row;
    /* A row of links in a bar has nothing to scroll; the desktop overflow
     * would only add a scrollbar track. */
    overflow: visible;
  }

  /* The viewer card is the <summary> that opens the panel holding Log out —
   * hiding it leaves a narrow viewport with no way out of the session at all. */
  .profile-menu {
    margin-left: auto;
  }

  /* The card is at the TOP of the screen here, not the bottom of a column, so
   * a panel opening upward would land off-screen. */
  .profile-menu-panel {
    top: calc(100% + 6px);
    bottom: auto;
    left: auto;
    right: 0;
    min-width: 12rem;
  }

  /* The sidebar is a full-width top bar now, so the footer starts at the left
   * edge rather than after a 16.5rem column that is no longer beside it. */
  .app-footer {
    left: 0;
  }

  /* Bottom padding still has to clear the fixed footer — see `.app-main` in the
   * layout layer. A uniform 1rem put the last row of every table behind the bar. */
  .app-main {
    padding: 1rem 1rem 4.5rem;
  }
}
