/* Cyber Academy — design tokens and components.
 *
 * Every value here is transcribed from notes/design-rulebook.md, which is the
 * visual contract. No value is invented: the rulebook's own rule is that a design
 * question is never settled by inventing a token. When a value is missing, it
 * comes from the BOIT fixture named in the rulebook header, and the decision is
 * recorded in the rulebook — never only here.
 *
 * Two rules govern nearly everything below and are easy to break by habit:
 *   * border-radius NEVER appears (§3). Shape is which corners are CUT, done with
 *     clip-path, and the count carries meaning.
 *   * No drop shadows and no gradients (§1). The only effect is the glow (§6), and
 *     it is a DARK-GROUND effect only.
 */

:root {
  /* Dark surface ladder — §2 */
  --bg-primary: #000000;
  --bg-secondary: #181b20;
  --bg-tertiary: #232833;
  --border-subtle: #313846;

  /* Dark text tokens — §2 */
  --text-heading: #e5e9ef;
  --text-body: #c7cdd9;
  --text-muted: #b0b7c6;
  --text-inverse: #000000;

  /* Operator (BOIT) semantic map — §4 */
  --action: #d3fd22;
  --accent-detail: #5e1db2;
  --link: #5bfbf8;
  --destructive: #fe00ae;
  /* §9 fixes the destructive label to black ink on BOTH grounds, so it cannot ride on
   * --text-inverse: that token flips to white under .content.light for the primary
   * action, and the destructive button would have followed it off the rulebook. */
  --destructive-ink: #000000;

  /* Run-state colours — §5. Never customer-configurable. */
  --status-success: #00d84a;
  --status-warning: #ffb200;
  --status-error: #d70c0f;
  --status-neutral: #6a7488;

  /* NOT run states — §5 says so explicitly. These serve alert bars and notices,
     which is what keeps run-state colours reserved for run state. */
  --status-alert-high: #ff6a00;
  --status-alert-high-tint: #ff6a0026;
  --status-info: #245bff;

  /* The unconfigured customer default — §4. Seeded customers override it. */
  --brand-primary-default: #5e1db2;

  --grid: 8px;
  --cut: 8px;
  /* THE EDGE COLOUR OF A CLIPPED OUTLINE, and the reason it is a variable rather than a
   * value repeated per component. A cut-corner outline is drawn in two different ways at
   * once — the straight sides are the element's own `border`, the two diagonals are
   * rotated pseudo-element rules — so a component that states its colour twice states it
   * in two places that drift. `.icon-button` had bordered sides in `--border-subtle` and
   * diagonals in `currentColor`, which on the settings control meant near-white diagonals
   * across an almost invisible frame: one shape, two weights, and it read as broken.
   * Both halves now read this, so a component changes its edge colour once or not at all.
   * The default is `currentColor`, which is what an outline button wants. */
  --edge: currentColor;
}

/* Corner cuts — §3. Named by COUNT, not by component: the assignment table in
 * the rulebook is the single place a component's shape is decided, and a class
 * named after a component would have to be duplicated the moment a second one
 * shares the shape. */
.cut-two {
  clip-path: polygon(
    0 0, calc(100% - var(--cut)) 0, 100% var(--cut),
    100% 100%, var(--cut) 100%, 0 calc(100% - var(--cut))
  );
}
.cut-one {
  clip-path: polygon(0 0, calc(100% - var(--cut)) 0, 100% var(--cut), 100% 100%, 0 100%);
}
.cut-all {
  clip-path: polygon(
    var(--cut) 0, calc(100% - var(--cut)) 0, 100% var(--cut),
    100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%,
    var(--cut) 100%, 0 calc(100% - var(--cut)), 0 var(--cut)
  );
}

/* Inconsolata is the product face for EVERY role (§7) — there is no sans and no
 * serif. One variable file covers the weights in use (Regular, SemiBold, Bold,
 * Black); the licence check required before shipping the binary is recorded in
 * fonts/README.md and the OFL text travels with it.
 *
 * font-display: swap so text renders immediately in the published fallback stack
 * rather than being invisible while the face loads. */
@font-face {
  font-family: Inconsolata;
  src: url("/fonts/Inconsolata.ttf") format("truetype-variations"),
       url("/fonts/Inconsolata.ttf") format("truetype");
  font-weight: 200 900;
  font-style: normal;
  font-display: swap;
}

/* THE DOCUMENT DOES NOT SCROLL — the application's content region does. `#app` is given
 * the full height here so `.app-shell` has a definite height to divide between its header
 * and its scrolling region; without it the shell's `height: 100%` resolves against an
 * auto-height parent and collapses back to document scrolling. */
html, body, #app {
  height: 100%;
  margin: 0;
  padding: 0;
  background: var(--bg-primary);
  color: var(--text-body);
  font-family: Inconsolata, "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace;
  font-size: 16px;               /* Body */
  line-height: 1.6;
}

/* §3: there is no radius scale and `border-radius` never appears — not even as a
 * defensive zero. Shape is which corners are CUT. */
* { box-sizing: border-box; }

/* Controls behave like controls. Double-clicking a tab, a chip or a nav item used to
 * select its label, which is the browser's default for text and exactly wrong for
 * something you press. The list is enumerated rather than global on purpose: prose,
 * data, e-mail addresses, names and figures must all stay selectable, because copying
 * them out is real work an operator does. */
button,
.btn,
.tab,
.chip,
.tag,
.badge,
.avatar,
.avatar-menu-item,
.breadcrumb,
.steps,
.choice,
.tile-name,
.tile-section-label,
.run-count-label,
.stat-label,
.table th,
label.field > .field-label {
  -webkit-user-select: none;
  user-select: none;
}

/* Type scale — §7, used as published, no sizes added. */
.page-title { font-size: 28px; text-transform: uppercase; color: var(--text-heading); margin: 0 0 calc(var(--grid) * 2); }
/* An explicit margin, because the browser's own `h3` default is `1em` top AND bottom —
   20 px each at this size, off the 8 px grid in both directions, and the reason a section
   heading sat in a pool of space above content it was meant to introduce. §9's spacing
   is an 8 px grid with 24 px between blocks. */
.section-title { font-size: 20px; text-transform: uppercase; color: var(--text-heading); margin: calc(var(--grid) * 3) 0 calc(var(--grid) * 2); }
.section-title:first-child { margin-top: 0; }
/* The empty and loading placeholders sit inside the block they replace, so they take the
   block's own rhythm rather than adding a second one on top of it. */
.load-state { padding: calc(var(--grid) * 2) 0; }
.muted { color: var(--text-muted); }

/* A deferred control is drawn in the mockups but does nothing this phase, and must never
   look clickable — a control that silently ignores a click is worse than one that says
   why it will not respond. This base rule belongs to the CONCEPT: three near-identical
   copies already existed (.tab, .chip, .avatar-menu-item, each repeating the same two
   declarations), and the fourth deferred surface added since inherited the muted colour
   from .muted while silently keeping a pointer cursor. The three specific rules are left
   in place deliberately — they are more specific than this one and removing them would
   make the result depend on rule order against .tab and .chip's own colour. */
.is-deferred { color: var(--text-muted); cursor: default; }

/* --------------------------------------------------------------------------
 * Buttons — §9. Two corners cut (top-right + bottom-left). Every label carries
 * the `< >` prefix, which is part of the CTA voice and is written in the markup
 * rather than added here, so a screen reader announces it as text.
 * -------------------------------------------------------------------------- */
.btn {
  font: inherit;
  font-weight: 700;              /* Body CTA */
  text-transform: uppercase;
  padding: 0 24px;                /* §9, the manual's measured value */
  height: 40px;
  border: 0;
  cursor: pointer;
  clip-path: polygon(
    0 0, calc(100% - var(--cut)) 0, 100% var(--cut),
    100% 100%, var(--cut) 100%, 0 calc(100% - var(--cut))
  );
}
/* THE OUTLINE THAT FOLLOWS THE CUT (§3 + §9).
 *
 * `border` plus `clip-path` cannot draw this shape: the clip removes the two corners,
 * and with them the piece of border that would have run along each diagonal, so an
 * outline button rendered as a rectangle with two bites taken out of its edge. The
 * missing 8 px of stroke is what made the component look broken.
 *
 * The four straight sides are still the element's own border. The two diagonals are
 * drawn as 1 px rules rotated 45°, anchored at the exact points the clip path cuts
 * through, and coloured `currentColor` so they follow the button's text colour on both
 * grounds without a second rule per variant. Offsets are +1px because an absolutely
 * positioned child is laid out against the PADDING box, which the 1 px border insets.
 *
 * `--cut` is read rather than hard-coded, so a component that changes its cut size
 * keeps a correct outline. */
.cut-two-outline { position: relative; }
.cut-two-outline::before,
.cut-two-outline::after {
  content: "";
  position: absolute;
  height: 1px;
  width: calc(var(--cut) * 1.41422);
  background: var(--edge);
  transform-origin: 0 0;
  pointer-events: none;
}
/* top-right: from (w − cut, 0) to (w, cut) */
.cut-two-outline::before { left: calc(100% - var(--cut) + 1px); top: -1px; transform: rotate(45deg); }
/* bottom-left: from (0, h − cut) to (cut, h); shifted a pixel inward so the clip keeps it */
.cut-two-outline::after  { left: -1px; top: calc(100% - var(--cut) + 1px); transform: rotate(45deg) translateY(-1px); }

.btn-primary { background: var(--action); color: var(--text-inverse); }
/* accent-glow on hover of the filled primary — a lime halo around the lime fill (§6). */
/* GLOW ON A CLIPPED ELEMENT IS `filter`, NEVER `box-shadow` (§6). `clip-path` clips the
 * element's whole rendering, its box-shadow included, so every glow on a cut-corner
 * control was being drawn and then thrown away — the hover halo and the focus ring both
 * rendered as nothing at all. `drop-shadow` is applied after the clip and follows the
 * clipped silhouette, so the halo traces the cut corners instead of a rectangle. */
.btn-primary:hover:not(:disabled) { filter: drop-shadow(0 0 6px var(--action)) drop-shadow(0 0 12px rgba(211, 253, 34, 0.6)); }
/* §9: destructive = pink fill, black text — the one variant that is not the action
 * colour, reserved for an act that removes rows nothing in this release can restore.
 * Its glow is pink too (§9 names the exception explicitly), and follows the same
 * `filter` rule as every other glow on a clipped control. */
.btn-destructive { background: var(--destructive); color: var(--destructive-ink); }
.btn-destructive:hover:not(:disabled) { filter: drop-shadow(0 0 6px var(--destructive)) drop-shadow(0 0 12px rgba(254, 0, 174, 0.6)); }
.btn-destructive:focus-visible { filter: drop-shadow(0 0 4px rgba(254, 0, 174, 0.7)); }
.btn:disabled { background: var(--border-subtle); color: var(--text-muted); cursor: default; }
/* §9: on DARK, focus-visible is `subtle-glow` in the action colour and nothing
 * else. The 2 px outline belongs to the light ground, where glow is forbidden —
 * it is added in the .content.light block below, not here. */
.btn:focus-visible { outline: none; filter: drop-shadow(0 0 4px rgba(211, 253, 34, 0.7)); }

/* --------------------------------------------------------------------------
 * Inputs — §9: bg-primary inside their panel, 1 px border, 40 px tall, 16 px
 * horizontal padding, SHARP corners (§3 gives inputs no cut).
 * -------------------------------------------------------------------------- */
.field { display: block; margin-bottom: calc(var(--grid) * 2); }
.field-label {
  display: block;
  font-size: 12px;               /* Label */
  /* Sentence case, not uppercase — §9 uppercases headings and button labels,
     never form labels. */
  color: var(--text-muted);
  margin-bottom: var(--grid);
}
.input {
  display: block;
  width: 100%;
  height: 40px;
  padding: 0 16px;
  background: var(--bg-primary);
  border: 1px solid var(--border-subtle);
  color: var(--text-body);
  font: inherit;
}
.input:focus-visible {
  outline: none;                  /* dark ground: glow only — §9 */
  box-shadow: 0 0 8px rgba(211, 253, 34, 0.35);
}

/* --------------------------------------------------------------------------
 * Alert bar — §9: sharp corners, a 2 px status left rule.
 * -------------------------------------------------------------------------- */
.alert {
  /* §5: 2 px left rule in the status colour, background the status tint.
     `status-alert-high` — NOT a run-state colour: §5 reserves those for run and
     course state and forbids reusing them for unrelated UI, and a wrong password
     is not a run state. */
  border-left: 2px solid var(--status-alert-high);
  background: var(--status-alert-high-tint);
  padding: calc(var(--grid) * 1.5) calc(var(--grid) * 2);
  margin-bottom: calc(var(--grid) * 2);
}
/* §5: title UPOZORNĚNÍ in Body CTA (16 px bold), message in Body SM. */
/* §5 draws the bar as a row: the `zap` glyph in the status colour, then the title and the
   message beside it. The icon takes the status colour because §8 makes an icon the colour
   of its context, and its context here is the alert's own severity. */
.alert { display: flex; gap: var(--grid); align-items: flex-start; }
.alert-icon { color: var(--status-alert-high); display: inline-flex; flex: none; margin-top: 3px; }
.alert-title { font-size: 16px; font-weight: 700; text-transform: uppercase; color: var(--status-alert-high); }
.alert-body { font-size: 14px; color: var(--text-body); }

/* --------------------------------------------------------------------------
 * Login — mockup 01-login.svg.
 * -------------------------------------------------------------------------- */
.login-page { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: calc(var(--grid) * 3); }
/* A panel: level 1 on the page, and SHARP — panels take no corner cut (§3). */
.login-panel { width: 100%; max-width: 420px; background: var(--bg-secondary); padding: 24px; }  /* §9 */
/* The mockup (01-login.svg, every artboard) opens the panel with the BOIT mark and the
 * product name on one line — the same lockup the application header carries, so the
 * screen a person meets first and the chrome they live in afterwards state one identity.
 * The mark had been dropped here entirely and the product name set in lime, which is
 * neither of §4's two permitted colourways of the brand. */
.login-brand { display: flex; align-items: center; gap: var(--grid); margin-bottom: calc(var(--grid) * 2); }
.login-title { font-size: 28px; text-transform: uppercase; color: var(--text-heading); margin: calc(var(--grid) * 2) 0 var(--grid); }
.login-subtitle { font-size: 14px; color: var(--text-muted); margin: 0 0 calc(var(--grid) * 3); }
.login-panel .btn { width: 100%; margin-top: var(--grid); }
/* Deferred control: muted text, never a link, so it does not look clickable. */
.login-deferred { font-size: 12px; text-transform: uppercase; color: var(--text-muted); margin: calc(var(--grid) * 2) 0 0; cursor: default; }

/* --------------------------------------------------------------------------
 * Top panel — §4: no background of its own, no bottom rule.
 * -------------------------------------------------------------------------- */
/* AN APPLICATION SHELL, NOT A DOCUMENT. The shell fills the viewport exactly, the header
 * takes the height it needs, and the routed content takes what is left and scrolls INSIDE
 * itself. The browser window never scrolls during normal use, so the header is stable
 * because it is outside the scrolling region rather than because it is stuck to the top of
 * one — a sticky header still rides a scrolling document, which is what made the panel
 * shift under rubber-banding and made every overlay hung from it need a scroll offset.
 *
 * `min-height: 0` on the scrolling child is the load-bearing line and the one that looks
 * removable: a flex item's automatic minimum size is its CONTENT size, so without it the
 * item refuses to shrink below its content, grows the shell past the viewport, and the
 * document starts scrolling again — the exact behaviour this replaces, and with the
 * overflow rule apparently in place.
 *
 * `100dvh` after `100vh` so a mobile browser's retracting toolbar does not leave the
 * bottom of the content permanently under it; the fallback is kept for anything that does
 * not know the dynamic unit. */
.app-shell { height: 100vh; height: 100dvh; display: flex; flex-direction: column; overflow: hidden; }
.app-shell > main, .app-shell > .content { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; }

/* THE PANEL IS A FIXED BAND OF THE SHELL, not a sticky element of a scrolling document.
 * §4 makes it always present for the superadmin — "so it is always clear where they are
 * and there is always a way back" — and it now is, structurally: it sits outside the
 * region that scrolls, so there is no scroll position at which it could be anywhere else.
 *
 * IT CARRIES A BOTTOM RULE, WHICH §4 PREVIOUSLY FORBADE. That prohibition described a
 * panel at the top of a scrolling page, where the content moving beneath it is itself the
 * signal that the panel is a separate thing. Under an application shell nothing moves past
 * it, so with no rule the header and the content read as one undivided surface and the
 * eye cannot find where the fixed chrome ends. The rule is 1 px in `border/subtle` — the
 * single border colour of §1 and §2, the same stroke a row separator uses, and the same
 * one the profile menu takes so that the two read as one system. It is the lightest mark
 * that states the edge; an accent bar or a shadow would state it far more loudly than a
 * boundary needs. Recorded in `notes/design-rulebook.md` §4 and §12.
 *
 * Its ground is `--bg-primary`, the PAGE colour rather than a surface of its own, so §4's
 * other rule stands. The value is read from the token here rather than inherited, because
 * `.content.light` redefines the same token for the customer ground below and the panel
 * must not follow it.
 *
 * `position: relative` makes the panel the containing block for the profile menu, so the
 * menu hangs from the panel's bottom EDGE rather than from the avatar's, and therefore
 * cannot overlap the chrome it belongs to. */
.top-panel {
  position: relative;
  flex: none;
  z-index: 30;
  background: var(--bg-primary);
  border-bottom: 1px solid var(--border-subtle);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: calc(var(--grid) * 2) calc(var(--grid) * 3);
  gap: calc(var(--grid) * 2);
}
/* Identity first, then location. The gap between the lockup and the trail is three times
 * the gap inside the lockup, which is what makes the mark and the product name read as
 * one thing and the breadcrumb as another. */
.top-panel-left { display: flex; align-items: center; gap: calc(var(--grid) * 3); min-width: 0; }
/* THE LOCKUP IS ONE LINK TO THE OVERVIEW. Mark and product name are a single anchor
 * rather than two, because they are one identity and one destination — two adjacent links
 * to the same place is two tab stops and two things to explain. It is a real anchor, so
 * middle-click, copy-link and open-in-new-tab work on the product identity exactly as they
 * do on the trail. */
.brand-lockup {
  display: flex; align-items: center; gap: calc(var(--grid) * 1.5); flex: none;
  text-decoration: none; color: inherit; cursor: pointer;
  padding: var(--grid); margin: calc(var(--grid) * -1);
}
.brand-lockup:hover .product-name { color: var(--action); }
.brand-lockup:focus-visible { outline: none; filter: drop-shadow(0 0 4px rgba(211, 253, 34, 0.7)); }
.brand-lockup:focus-visible .product-name { color: var(--action); }
/* The real bitmap at §4's 3.15:1 ratio, at the 24 px of §8's mid icon step rather than the
 * 18 px the rulebook first fixed: at 18 px the mark read as a decorative glyph beside the
 * product name instead of as the operator's brand, which is the one thing the top-left of
 * the chrome exists to say. No fill, no padding, no letter-spacing — the mark carries its
 * own lime tiles. Recorded in `notes/design-rulebook.md` §4 and §12. */
.wordmark { display: block; height: 24px; width: auto; }
.product-name { color: var(--text-muted); font-size: 16px; white-space: nowrap; }
.breadcrumb { display: flex; align-items: center; gap: var(--grid); font-size: 14px; text-transform: uppercase; min-width: 0; overflow: hidden; }
.breadcrumb-item { display: inline-flex; align-items: center; min-width: 0; }
.breadcrumb-link, .breadcrumb-current { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.breadcrumb-sep { color: var(--text-muted); margin-right: var(--grid); }
.breadcrumb-link { background: none; border: 0; font: inherit; text-transform: uppercase; color: var(--action); cursor: pointer; padding: 0; }
.breadcrumb-current { color: var(--action); }

/* Avatars are SQUARE frames with all four corners cut — never circles (§3). */
.avatar {
  width: 32px; height: 32px;
  background: var(--bg-tertiary);
  color: var(--text-heading);
  /* No stroke: §1's permitted-stroke list is exhaustive and avatars are not on
     it. The bg-tertiary surface is what separates the frame from the panel. */
  border: 0;
  font: inherit; font-size: 12px; cursor: pointer;
  clip-path: polygon(
    var(--cut) 0, calc(100% - var(--cut)) 0, 100% var(--cut),
    100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%,
    var(--cut) 100%, 0 calc(100% - var(--cut)), 0 var(--cut)
  );
}
.top-panel-right { flex: none; display: flex; align-items: center; }
/* A menu is a passive container: sharp (§3), on the level-1 `bg-secondary` surface.
 *
 * IT HANGS FROM THE PANEL, NOT FROM THE AVATAR. Its containing block is `.top-panel`, so
 * `top: 100%` is the panel's bottom edge — the one place it can open from without covering
 * part of the chrome that owns it. Anchored to the avatar it began at the avatar's bottom,
 * which is inside the panel, and the menu overlapped the last few pixels of the header.
 * Nothing here does arithmetic on the panel's height, so the alignment survives the
 * smaller panel below 900 px.
 *
 * Its right edge lines up with the avatar's because both are inset by the panel's own
 * horizontal padding.
 *
 * THE BORDER IS THE PANEL'S BORDER. §1 rations strokes and §2 says a floating menu is
 * separated by its surface over a scrim — which is true against content and false against
 * the panel, where `bg-secondary` on `bg-primary` at the shared edge left the menu looking
 * like a continuation of the chrome. Taking the panel's own 1 px `border/subtle` on the
 * three free sides makes the two read as one system: the header ends at that stroke and
 * the menu resumes it. The top side is deliberately absent — the panel's bottom rule
 * already draws that edge, and a second line there would double it. */
.avatar-menu {
  position: absolute;
  right: calc(var(--grid) * 3);
  /* BELOW the panel's bottom rule, not on it. An absolutely positioned element offsets
   * from its containing block's PADDING box, so `top: 100%` alone lands the menu one pixel
   * high and its own fill then paints over the rule — the header's edge would break for
   * exactly the width of the open menu. Sitting a pixel lower, the panel's rule reads as
   * the menu's top edge, which is why the menu draws none of its own. */
  top: calc(100% + 1px);
  min-width: 240px;
  background: var(--bg-secondary);
  border: 1px solid var(--border-subtle);
  border-top: 0;
  z-index: 40;
  padding-top: var(--grid);
}
.avatar-menu-email { padding: var(--grid) calc(var(--grid) * 2); font-size: 12px; color: var(--text-muted); }
/* §2: a list row nested inside a level-1 menu sits on bg-tertiary. */
.avatar-menu-item { display: block; width: calc(100% - 16px); margin: 0 8px 8px; text-align: left; background: var(--bg-tertiary); border: 0; font: inherit; font-size: 14px; text-transform: uppercase; color: var(--text-body); padding: calc(var(--grid) * 1.5) calc(var(--grid) * 2); cursor: pointer; }
.avatar-menu-item:hover:not(.is-deferred) { background: var(--bg-tertiary); color: var(--action); }
.avatar-menu-item.is-deferred { color: var(--text-muted); cursor: default; }

.content { padding: calc(var(--grid) * 3); }
/* The scrim that closes the profile menu on a click anywhere else. Transparent and
 * beneath the menu, above everything else — a menu you can only close by finding the
 * avatar again is a menu that gets left open, which is the state that leaked. */
.menu-scrim { position: fixed; inset: 0; z-index: 35; background: transparent; border: 0; padding: 0; cursor: default; }
/* The probe's holding screen sits OUTSIDE `.app-shell` — it is what renders before there
 * is a session to draw a shell for — so it carries its own full height rather than
 * inheriting the shell's. `height`, not `min-height`: `#app` is now exactly the viewport,
 * and a `min-height: 100vh` child of it would be the one element able to push the document
 * into scrolling. */
.booting { height: 100%; }

/* --------------------------------------------------------------------------
 * Light surface — the customer-branded ground (§2, §4).
 *
 * GLOW IS A DARK-GROUND EFFECT AND MUST NOT APPEAR HERE (§6): the manual renders
 * none on white, and a blurred halo on white reads as a grey box. Focus on light
 * is a 2 px outline instead.
 * -------------------------------------------------------------------------- */
.content.light {
  --bg-primary: #ffffff;
  --bg-secondary: #f4f6f8;
  --bg-tertiary: #e9ecef;
  --text-heading: #12161a;
  --text-body: #525f7a;
  --text-muted: #8a95a5;
  /* The customer's primary drives the action role on branded surfaces. The
   * unconfigured default is purple (§4); a seeded customer overrides it. */
  --action: var(--brand-primary-default);
  --text-inverse: #ffffff;
  background: var(--bg-primary);
  color: var(--text-body);
}
.content.light .btn-primary:hover:not(:disabled) { filter: none; box-shadow: none; }
/* §6: glow is a dark-ground effect and never appears on light. §9 replaces it
 * here with the 2 px action-colour outline. */
.content.light .btn:focus-visible,
.content.light .input:focus-visible { filter: none; box-shadow: none; outline: 2px solid var(--action); outline-offset: 2px; }
.content.light .icon-button:focus-visible { filter: none; outline: 2px solid var(--action); outline-offset: 2px; }
/* §9: on light the focus outline takes the button's own colour, and DESTRUCTIVE STAYS
 * PINK — it is not a customer-brandable role. The customer settings screen is a light
 * ground and carries the only destructive button on one. */
.content.light .btn-destructive:hover:not(:disabled) { filter: none; box-shadow: none; }
.content.light .btn-destructive:focus-visible { outline-color: var(--destructive); }

/* The removal block at the foot of the settings form. It carries no colour of its own —
 * the pink button is the whole of the distinction — and is separated from the ordinary
 * fields by the one border colour §2 publishes, so nothing here is an invented token. */
.danger-zone { margin-top: calc(var(--grid) * 4); padding-top: calc(var(--grid) * 3); border-top: 1px solid var(--border-subtle); }
.danger-zone .field-note { margin-bottom: calc(var(--grid) * 2); }

/* --------------------------------------------------------------------------
 * Tiles — all four corners cut (§3): an atomic mark that stands alone.
 * -------------------------------------------------------------------------- */
.tile-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: calc(var(--grid) * 3); }

/* TILES — §3 gives them all four corners cut, §9 their anatomy: bg-secondary, a 2 px top
 * rule in the family colour, an identity row at the top, and figures BOTTOM-ALIGNED on
 * shared baselines with Label captions and an even column pitch.
 *
 * The cut is 12 px, which is what `resources/mockups/03-superadmin-home.svg` draws
 * (`M404 148 H700 L712 160 …` — a 12 px chamfer on all four corners) and what §3's
 * "8–12 px" band permits for a tile. It is set as a local `--cut` rather than a second
 * clip-path, so `.cut-all` stays the one place the shape is written; children that take
 * their own cut reset it for themselves.
 *
 * The fixed minimum height is what makes a row of tiles a row rather than a ragged
 * staircase — the mockup's tiles are all 224 px whatever their content. */
.tile {
  --cut: 12px;
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 224px;
  background: var(--bg-secondary);
  padding: calc(var(--grid) * 3);
  color: inherit;
  text-decoration: none;
}

/* The 2 px top rule, INSET BY THE CUT so it spans exactly the tile's top edge — the
 * mockup draws it as `<rect x="404" y="148" width="296" height="2">` against a tile
 * spanning 392…712, i.e. 12 px in from each side. Drawn full-bleed it was clipped into a
 * flaring trapezoid by the chamfer and read as a detached neon bar floating above the
 * card rather than as the card's own edge. */
.tile-rule { position: absolute; top: 0; left: var(--cut); right: var(--cut); height: 2px; z-index: 2; }
/* An operator-owned entity — a course — takes the action colour for its family rule (§9:
   "lime for operator entities, the customer's primary for a customer tile"). */
.tile-rule-operator { background: var(--action); }

/* The covering anchor: the whole tile is one target, and the controls that sit above it
   in the identity row stay reachable because they are given a higher layer. */
.tile-cover { position: absolute; inset: 0; z-index: 1; }
/* `.tile-rule` is excluded because it is ABSOLUTELY positioned and this rule would
   override that, dropping the accent strip into the flow at the top of the identity row —
   which is exactly where it appeared. */
.tile > *:not(.tile-cover):not(.tile-rule) { position: relative; z-index: 2; pointer-events: none; }
/* …except the controls, which must still be reachable through the layer above the cover. */
.tile a, .tile button { pointer-events: auto; }
.tile-cover:focus-visible { outline: none; box-shadow: 0 0 8px rgba(211, 253, 34, 0.35); }
.tile:has(.tile-cover:hover), .tile:has(.tile-cover:focus-visible) { background: var(--bg-tertiary); }

/* The identity row: glyph square, name, and the row's own trailing control. */
/* The identity row: the mark, then a column that may wrap, then the row's own control.
 * `align-items: flex-start` and a shrinkable identity column are what stop a long name
 * being squeezed into a two-line stub by the tag and the icon button beside it. */
.tile-head { display: flex; align-items: flex-start; gap: calc(var(--grid) * 1.5); margin-bottom: var(--grid); }
.tile-head .tile-glyph { margin-top: 2px; }
.tile-head .icon-button { margin-left: auto; }
.tile-identity-tags { display: flex; flex-wrap: wrap; gap: calc(var(--grid) * 0.5); margin-top: 2px; }
/* 40 px in the mockup (416…456), all four corners cut at 8 px, and filled with the
 * customer's OWN primary with derived ink — §4's "the customer's primary shows as the
 * tile's 2 px top rule and its logo". A uniform grey square made every customer look the
 * same at a glance, which is the one thing a customer list must not do. */
.tile-glyph {
  --cut: 8px;
  width: 40px; height: 40px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--bg-tertiary); color: var(--text-heading);
  font-weight: 700; font-size: 16px; line-height: 1;
  flex: none; overflow: hidden;
}
.tile-glyph img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tile-name { font-size: 20px; font-weight: 700; text-transform: uppercase; color: var(--text-heading); line-height: 1.2; min-width: 0; }
.tile-identity { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.tile-meta, .tile-subline { font-size: 12px; color: var(--text-muted); line-height: 1.3; }
.tile-section-label { font-size: 12px; text-transform: uppercase; color: var(--text-muted); margin: calc(var(--grid) * 2) 0 var(--grid); }

/* THE FIGURE ROW — one grid, four equal columns, each figure centred over its caption
 * and every figure on one baseline. It was a flex row with a fixed gap, so the columns
 * sat wherever the digit widths left them and the two-line caption pushed its neighbours
 * out of alignment: four numbers of equal weight, arranged by accident.
 *
 * `align-items: end` puts the figures on a shared baseline (§9). The caption reserves two
 * lines whatever it needs, so a card whose captions wrap and one whose captions do not
 * still agree on where the row below them starts. */
.run-counts, .course-counts {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  align-items: end;
  gap: var(--grid);
  margin-top: auto;
}
.run-count, .course-count { text-align: center; min-width: 0; }
/* H1 36, §9: "H1 36 wherever a tile carries four figure columns (Display MD 40 does not
 * fit a 320 px tile)". Black weight, as the mockup's `.fig` sets it. */
.run-count-figure, .course-count-figure {
  font-size: 36px; font-weight: 900; line-height: 1; color: var(--text-heading);
  font-variant-numeric: tabular-nums;
}
/* §5's run-state colours, used here exactly as the mockup fills them. `aktivní` takes
 * status-success: §5 permits it for a healthy aggregate, and the mockup draws it green. */
.run-count-figure.state-active     { color: var(--status-success); }
.run-count-figure.state-stalled    { color: var(--status-warning); }
.run-count-figure.state-terminated { color: var(--status-error); }
.run-count-figure.state-completed  { color: var(--status-neutral); }
/* Captions keep their case — §7 uppercases headings, nav items, tags and button labels,
 * and these are none of the four. The mockup sets them lowercase. */
.run-count-label, .course-count-label {
  font-size: 12px; line-height: 1.15; color: var(--text-muted);
  margin-top: calc(var(--grid) * 0.75);
  min-height: 2.3em;
  display: block;
  overflow-wrap: anywhere;
}

/* The tile's footer: the one remaining fact at the left, the chevron that says the whole
 * tile is a way in at the right. */
.tile-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--grid); margin-top: calc(var(--grid) * 2); min-height: 24px; }
.tile-chevron { color: var(--action); flex: none; display: inline-flex; }

/* The ghost tile: bg-primary fill with a 1 px outline, same size and shape as its
 * siblings (§2), and — §9 — the FIRST tile of its section. It is now a real control, so
 * it is a button rather than a div and carries the hover its siblings do. */
/* THE OUTLINE IS DRAWN, NOT BORDERED. A `border` under `cut-all` loses its stroke along
 * all four diagonals — the same defect `.cut-two-outline` fixes for buttons — and the
 * ghost tile showed as four disconnected edges. Here the fix can be simpler than the
 * button's, because §2 fixes this tile's fill as `bg-primary`, the PAGE ground: a filled
 * cut-all shape in the border colour with a 1 px inset cut-all shape in the page ground
 * over it leaves exactly a 1 px ring that follows the whole geometry. The button cannot
 * do this — it sits on whatever surface its screen puts it on. */
.tile-ghost {
  background: transparent;
  border: 0;
  color: var(--text-heading);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: calc(var(--grid) * 2);
  text-transform: uppercase; font-size: 16px; font-weight: 700;
  font-family: inherit; cursor: pointer; width: 100%;
}
.tile-ghost::before,
.tile-ghost::after {
  content: "";
  position: absolute;
  clip-path: polygon(
    var(--cut) 0, calc(100% - var(--cut)) 0, 100% var(--cut),
    100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%,
    var(--cut) 100%, 0 calc(100% - var(--cut)), 0 var(--cut)
  );
  pointer-events: none;
}
.tile-ghost::before { inset: 0;   background: var(--border-subtle); }
.tile-ghost::after  { inset: 1px; background: var(--bg-primary); }
.tile-ghost > * { position: relative; z-index: 1; }
.tile-ghost:hover::before { background: var(--action); }
.tile-ghost:hover { color: var(--action); }
.tile-ghost:focus-visible { outline: none; filter: drop-shadow(0 0 4px rgba(211, 253, 34, 0.7)); }
.tile-ghost-plus { color: var(--action); display: inline-flex; }
.tile-ghost.is-deferred { color: var(--text-muted); cursor: default; }
.tile-ghost.is-deferred:hover::before { background: var(--border-subtle); }
.tile-ghost.is-deferred:hover { color: var(--text-muted); }

/* ICON BUTTON — §9: 32 × 32, two corners cut, 1 px border, exactly one 16 px icon, no
 * fill. The outline follows the full clipped geometry through `.cut-two-outline`. */
.icon-button {
  --cut: 8px;
  width: 32px; height: 32px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  --edge: var(--border-subtle);
  background: transparent; border: 1px solid var(--edge);
  color: var(--text-heading); cursor: pointer; padding: 0;
}
.icon-button:hover, .icon-button:focus-visible { --edge: var(--action); color: var(--action); }
.icon-button:active { --edge: var(--action); color: var(--action); background: var(--bg-tertiary); }
.icon-button:focus-visible { outline: none; filter: drop-shadow(0 0 4px rgba(211, 253, 34, 0.7)); }

/* A tag carries ONE cut corner — design-rulebook.md §9 ("Tags: Body SM, one corner cut"),
   drawn that way in every mockup that shows one; 03-superadmin-home.svg builds it as an
   explicit path and labels it "outline tag: one corner cut". The cut lives here rather
   than on each call site because it is intrinsic to a tag, not a per-use decision: a
   contract a caller has to remember is one a caller eventually forgets, and tags were
   rendering square everywhere because every caller had.

   `align-self: center` and `white-space: nowrap` are what fix the badge that sat a few
   pixels below its heading and the one that broke across two lines: a tag is a label, and
   a label that wraps is no longer a tag. */
.tag { font-size: 14px; line-height: 1.4; text-transform: uppercase; padding: 1px var(--grid);
       white-space: nowrap; align-self: center; flex: none;
       clip-path: polygon(0 0, calc(100% - var(--cut)) 0, 100% var(--cut), 100% 100%, 0 100%); }
.tag-muted { background: var(--bg-tertiary); color: var(--text-muted); }
/* The mockup's `# KONCEPT` is an OUTLINE tag — no fill, a 1 px border-subtle stroke —
   which §9 lists first among the tag variants. */
.tag-outline { background: transparent; border: 1px solid var(--border-subtle); color: var(--text-heading); }
.section-gap { margin-top: calc(var(--grid) * 4); }

/* --------------------------------------------------------------------------
 * The record header — ONE shape for a customer, an employee and a course.
 *
 * The glyph square and the title sit on one baseline: `align-items: flex-start` with the
 * identity column's first line box aligned to the square's centre is what stops the name
 * floating a few pixels above the mark beside it. The square is `flex: none` so a long
 * name cannot squeeze it out of round — which is the misalignment that showed up wherever
 * a customer's name wrapped.
 * -------------------------------------------------------------------------- */
.record-header {
  display: flex;
  align-items: flex-start;
  gap: calc(var(--grid) * 2);
  margin-bottom: calc(var(--grid) * 3);
  flex-wrap: wrap;
}
/* The 40 px square's own optical centre line is 20 px down; the H2's is ~19 px. Nudging
   the square rather than the text keeps the title on the page's own baseline grid. */
.record-header > .tile-glyph { margin-top: 2px; }
.record-identity { min-width: 0; flex: 1 1 320px; }
.record-identity .page-title { margin-bottom: 2px; overflow-wrap: anywhere; }
.record-name-compact { font-size: 20px; text-transform: uppercase; color: var(--text-muted); }
.record-meta { font-size: 14px; color: var(--text-muted); }
.header-actions { margin-left: auto; display: flex; gap: var(--grid); flex-wrap: wrap; align-items: center; }
.record-header.is-compact { margin-bottom: calc(var(--grid) * 2); }

.btn-secondary { background: transparent; border: 1px solid var(--edge); color: var(--action); }
.btn-secondary:disabled { --edge: var(--border-subtle); color: var(--text-muted); background: transparent; }

/* Tabs — ONE corner cut, top-right (§3): a directional tag. */
.tab-row { display: flex; gap: var(--grid); margin-bottom: calc(var(--grid) * 3); flex-wrap: wrap; }
.tab { padding: var(--grid) calc(var(--grid) * 2); font-size: 14px; text-transform: uppercase; color: var(--text-muted); text-decoration: none; background: transparent; }
.tab.is-active { background: var(--bg-tertiary); color: var(--action); }
.tab.is-deferred { color: var(--text-muted); cursor: default; }

/* Stat tiles. The big figure is Display MD (§7). */
/* STAT TILES — the same atomic mark as every other tile (§3: all four corners, 8–12 px),
 * so they take the SAME 12 px cut. `resources/mockups/07-customer-overview.svg` draws
 * them at 12 (`M60 224 H288 L300 236 …`) on the level-2 surface directly on the page —
 * which §2 sanctions — each with the 2 px top rule in the customer's primary that every
 * tile in this system carries. Ours had none of the three, which is why a stat tile and a
 * customer tile read as two unrelated components on screens one click apart. */
.stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: calc(var(--grid) * 3); margin-bottom: calc(var(--grid) * 3); }
.stat-tile {
  --cut: 12px;
  position: relative;
  background: var(--bg-tertiary);
  padding: calc(var(--grid) * 3);
}
/* Inset by the cut so it spans exactly the tile's top edge — the same treatment, and the
   same reason, as `.tile-rule`. */
.stat-tile::before {
  content: "";
  position: absolute;
  top: 0; left: var(--cut); right: var(--cut);
  height: 2px;
  background: var(--action);
}
/* Display MD 40 — §9 allows it for a lone figure, and a stat tile carries exactly one. */
.stat-figure { font-size: 40px; line-height: 1.1; color: var(--text-heading); font-variant-numeric: tabular-nums; }
/* §5 permits status-success for a healthy aggregate, and the mockup fills these figures
   exactly so: headcount and active runs green, stalled amber. A figure never wears a state
   colour that could be misread as a different state, which is why only these three do. */
.stat-figure.state-healthy { color: var(--status-success); }
.stat-figure.state-stalled { color: var(--status-warning); }
.stat-label { font-size: 14px; color: var(--text-muted); }
.stat-sub { font-size: 12px; color: var(--text-muted); margin-top: var(--grid); }

/* Panels and tables are passive containers: SHARP (§3). Rows are separated by
 * the 1 px border rule and are at least 48 px tall (§9). */
.panel { background: var(--bg-secondary); padding: calc(var(--grid) * 3); }
.table-controls { display: flex; align-items: center; gap: var(--grid); margin-bottom: calc(var(--grid) * 2); flex-wrap: wrap; }
.search-form { display: flex; gap: var(--grid); flex: 1 1 320px; }
.search-input { flex: 1; }
/* §9: "Filter chips: resting = outline; active = filled in the action colour with inverse
   text." Resting was a filled bg-tertiary chip, which reads as a selected state and left a
   whole row of chips looking chosen at once. */
.chip { padding: var(--grid) calc(var(--grid) * 1.5); font-size: 12px; background: transparent; border: 1px solid var(--border-subtle); color: var(--text-body); }
.chip.is-deferred { color: var(--text-muted); cursor: default; }
.table { width: 100%; border-collapse: collapse; }
.table th { text-align: left; font-size: 12px; text-transform: uppercase; color: var(--text-muted); font-weight: 400; padding: var(--grid); border-bottom: 1px solid var(--border-subtle); }
.table td { padding: var(--grid); border-bottom: 1px solid var(--border-subtle); height: 48px; font-size: 14px; }
.employee-name { color: var(--text-heading); }
.employee-email { font-size: 12px; color: var(--text-muted); }
.pager { display: flex; align-items: center; gap: var(--grid); margin-top: calc(var(--grid) * 2); flex-wrap: wrap; }
.pager .muted { margin-right: auto; font-size: 14px; }

/* --------------------------------------------------------------------------
 * Runs, employee detail, enrolment, modals.
 * -------------------------------------------------------------------------- */
.badge { display: inline-block; font-size: 12px; text-transform: uppercase; padding: 2px var(--grid); background: var(--bg-tertiary); color: var(--text-body); }
/* Run-state colours — §5. Never customer-configurable, so they are NOT derived
 * from the branding tokens above. */
.badge.state-active     { color: var(--status-success); }
.badge.state-stalled    { color: var(--status-warning); }
.badge.state-terminated { color: var(--status-error); }
.badge.state-paused,
.badge.state-completed  { color: var(--status-neutral); }
.badge-gated { margin-left: var(--grid); color: var(--status-neutral); }

/* Progress: track plus fill in the run's state colour (§5 / 03 §6.3). */
.progress { display: inline-flex; align-items: center; gap: var(--grid); }
.progress-track { display: inline-block; width: 90px; height: 6px; background: var(--bg-tertiary); }
.progress-fill { display: block; height: 100%; background: var(--status-success); }
.progress-fill.state-stalled { background: var(--status-warning); }
/* A PAUSED run keeps its bar, in the neutral grey (§5). */
.progress-fill.state-paused  { background: var(--status-neutral); }
.progress-text { font-size: 12px; color: var(--text-muted); white-space: nowrap; }
/* The two terminal states DROP the bar entirely (§5). */
.progress-terminal { font-size: 12px; color: var(--status-neutral); display: inline-flex; align-items: center; gap: calc(var(--grid) * 0.5); }  /* a run state — correct here */
/* §5: the 16 px `pause` glyph precedes a paused run's bar, in the same neutral grey. */
.progress-icon { color: var(--status-neutral); display: inline-flex; flex: none; }
.progress-terminal.state-terminated { color: var(--status-error); }

.filter-chips { display: flex; gap: var(--grid); flex-wrap: wrap; }
.chip.is-active { background: var(--action); border-color: var(--action); color: var(--text-inverse); }
.chip:not(.is-active):hover { border-color: var(--action); color: var(--action); }
button.chip { font: inherit; cursor: pointer; }
.btn-small { height: 32px; font-size: 12px; padding: 0 var(--grid); }  /* §9: small = 32 px */
.actions-cell { white-space: nowrap; }
.actions-cell .btn { margin-right: var(--grid); }
.employee-link { color: var(--action); text-decoration: none; }
.employee-link:hover { text-decoration: underline; }
.body-actions { display: flex; gap: var(--grid); margin-bottom: calc(var(--grid) * 2); flex-wrap: wrap; }
a.btn { display: inline-flex; align-items: center; text-decoration: none; }

/* Run cards — a content card with a top rule takes the BOTTOM-RIGHT cut only (§3). */
.run-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: calc(var(--grid) * 2); }
.run-card { background: var(--bg-secondary); padding: calc(var(--grid) * 2); border-top: 2px solid var(--action); }
.cut-bottom-right { clip-path: polygon(0 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%); }
.run-card-head { display: flex; justify-content: space-between; align-items: center; gap: var(--grid); margin-bottom: var(--grid); }
.run-card-course { color: var(--text-heading); }
.run-card-meta { font-size: 12px; color: var(--text-muted); margin-top: var(--grid); }

/* Modals are passive containers: SHARP (§3). */
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 50; padding: calc(var(--grid) * 2); }
.modal { background: var(--bg-secondary); padding: calc(var(--grid) * 3); width: 100%; max-width: 460px; max-height: 90vh; overflow: auto; }
.modal-title { font-size: 20px; text-transform: uppercase; color: var(--text-heading); margin: 0 0 calc(var(--grid) * 2); }
.modal-actions { display: flex; gap: var(--grid); justify-content: flex-end; margin-top: calc(var(--grid) * 2); flex-wrap: wrap; }

/* Enrolment steps — progressive disclosure (§10): one step at a time. */
.steps { display: flex; gap: calc(var(--grid) * 2); margin-bottom: calc(var(--grid) * 3); font-size: 12px; text-transform: uppercase; color: var(--text-muted); flex-wrap: wrap; }
.step.is-active { color: var(--action); }
.step.is-done { color: var(--text-body); }
.choice-list { display: flex; flex-direction: column; gap: var(--grid); }
.choice-list.scroll { max-height: 320px; overflow: auto; }
.choice { display: flex; justify-content: space-between; gap: var(--grid); padding: calc(var(--grid) * 1.5) calc(var(--grid) * 2); background: var(--bg-tertiary); border: 0; font: inherit; text-align: left; cursor: pointer; color: var(--text-body); }
.choice.is-selected { background: var(--action); color: var(--text-inverse); }
.choice.is-selected .muted { color: var(--text-inverse); }
.choice-row { display: flex; align-items: center; gap: var(--grid); padding: var(--grid); border-bottom: 1px solid var(--border-subtle); }
.choice-row .muted { margin-left: auto; font-size: 12px; }

/* --------------------------------------------------------------------------
 * Everything the finishing pass added. Each block names the rulebook section it
 * realises; no value here is invented.
 * -------------------------------------------------------------------------- */

/* Live search — §9's forgiving search box, with the lucide `search` glyph of §8 set
   inside the field rather than beside it. The field is sunken (`bg-primary`) and sharp
   like every other input (§3, §9); only the padding changes to make room for the icon. */
.live-search { position: relative; flex: 1 1 320px; min-width: 220px; }
.live-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--text-muted); display: inline-flex; pointer-events: none; }
.live-search-input { padding-left: 36px; }

/* The filter rows. Two groups, one per dimension, each wrapping independently so a
   customer with many departments does not push the state chips off the row. */
.filter-groups { display: flex; flex-wrap: wrap; gap: var(--grid) calc(var(--grid) * 2); width: 100%; }
.table-controls { align-items: flex-start; }

/* Table rows that lead somewhere. The row is the target and the name is the anchor —
   both go to the same place, so the pointer is on the row and the underline on the name. */
tr.is-clickable { cursor: pointer; }
tr.is-clickable:hover td { background: var(--bg-tertiary); }
.employee-name-link { color: var(--text-heading); text-decoration: none; }
.employee-name-link:hover, tr.is-clickable:hover .employee-name-link { color: var(--action); text-decoration: underline; }

/* Inline content links — §4/B-03: cyan on dark, underlined. NOT the action colour, which
   §4 reserves for navigation and breadcrumb links. */
.inline-link { color: var(--link); text-decoration: underline; }
.content.light .inline-link { color: #ae00ff; }

/* Contact details as controls — an address and a number you can act on. The icon takes
   the link's own colour (§8: "icon colour is the text colour of its context"). */
.contact-line { display: flex; flex-wrap: wrap; align-items: center; gap: var(--grid) calc(var(--grid) * 2); font-size: 14px; color: var(--text-muted); }
.contact-link { display: inline-flex; align-items: center; gap: calc(var(--grid) * 0.75); color: var(--link); text-decoration: none; }
.contact-link:hover { text-decoration: underline; }
.contact-link:focus-visible { outline: none; box-shadow: 0 0 8px rgba(211, 253, 34, 0.35); }
.content.light .contact-link { color: #ae00ff; }
.contact-fact { color: var(--text-muted); }

/* The way back out of a deep record. Navigation, so it is the action colour (§4). */
.back-link { display: inline-flex; align-items: center; gap: calc(var(--grid) * 0.5); color: var(--action); text-decoration: none; font-size: 14px; text-transform: uppercase; margin-bottom: calc(var(--grid) * 2); -webkit-user-select: none; user-select: none; }
.back-link:hover { text-decoration: underline; }

/* The avatar menu's identity block: who is signed in, above what they can do. */
.avatar-menu-identity { padding: var(--grid) calc(var(--grid) * 2) calc(var(--grid) * 1.5); }
.avatar-menu-name { color: var(--text-heading); font-size: 14px; }

/* Selects are inputs, so they are sharp and sunken like the rest (§9). The native arrow
   is left in place: replacing it would mean drawing a control the manual does not
   publish, and §8 forbids a hand-drawn glyph standing in for an icon. */
.select { padding-right: calc(var(--grid) * 1.5); }

/* A form's own action row, distinct from a modal's: right-aligned, and separated from the
   last field by the 24 px panel rhythm rather than by the 16 px field rhythm — the gap
   under the final input was the same as between two inputs, so the button read as another
   field rather than as the end of the form. */
.form-actions { display: flex; justify-content: flex-end; gap: var(--grid); margin-top: calc(var(--grid) * 3); padding-top: calc(var(--grid) * 3); border-top: 1px solid var(--border-subtle); flex-wrap: wrap; }
.field-note { font-size: 12px; color: var(--text-muted); margin: 0 0 calc(var(--grid) * 2); }
.readonly-value { font-size: 16px; color: var(--text-body); padding: calc(var(--grid) * 0.5) 0; }
.save-confirmation { color: var(--status-success); font-size: 14px; margin: 0 0 calc(var(--grid) * 2); }

/* The modal's own breathing room. 24 px between the last field and the action row, and
   24 px of panel padding around everything — §9's measured values, applied to a surface
   that had been running the field rhythm right up to its own edge. */
.modal-actions { margin-top: calc(var(--grid) * 3); padding-top: calc(var(--grid) * 3); border-top: 1px solid var(--border-subtle); }
.modal .field:last-of-type { margin-bottom: 0; }
.modal .field-note { margin-top: calc(var(--grid) * 2); }

/* A screen that states what it does not do yet, rather than a control that ignores you. */
.notice-panel { max-width: 640px; }
.notice-body { color: var(--text-body); margin: var(--grid) 0 calc(var(--grid) * 3); }

/* Customer settings. */
.settings-panel { max-width: 640px; }
.settings-panel .section-title { margin: calc(var(--grid) * 3) 0 calc(var(--grid) * 2); }
.settings-panel .section-title:first-child { margin-top: 0; }
.logo-field { display: flex; gap: calc(var(--grid) * 2); align-items: flex-start; margin-bottom: calc(var(--grid) * 2); flex-wrap: wrap; }
/* The preview is a glyph square at tile scale: same shape, same cut, so the operator sees
   the mark in the frame it will actually be drawn in. */
.logo-preview { width: 72px; height: 72px; flex: none; background: var(--bg-tertiary); display: flex; align-items: center; justify-content: center; overflow: hidden; font-size: 12px; }
.logo-preview img { width: 100%; height: 100%; object-fit: contain; }
.logo-controls { flex: 1 1 260px; min-width: 0; display: flex; flex-direction: column; gap: var(--grid); align-items: flex-start; }
.file-input { height: auto; padding: calc(var(--grid) * 1.25) 12px; }
.colour-fields { display: flex; gap: calc(var(--grid) * 2); flex-wrap: wrap; }
.colour-fields .field { flex: 1 1 180px; }
/* A colour input is a swatch, so it gets the input's frame and none of its text padding. */
.colour-fields .input[type="color"] { padding: 4px; height: 40px; cursor: pointer; }

/* A record's own figure row is not a tile's: it sits under the header on the page rather
   than inside a 320 px card, so it is bounded to the width four figures actually need
   instead of being stretched across a 1440 px screen. */
.content > div > .course-counts { max-width: 640px; margin-top: 0; margin-bottom: calc(var(--grid) * 3); }

/* Course entitlements: one row per customer, on the level-2 surface a nested row takes. */
.entitlement-row { gap: calc(var(--grid) * 1.5); background: var(--bg-tertiary); border-bottom: 0; padding: var(--grid) calc(var(--grid) * 1.5); }
.entitlement-glyph { width: 32px; height: 32px; font-size: 12px; }
.entitlement-identity { display: flex; align-items: center; gap: var(--grid); flex: 1; min-width: 0; flex-wrap: wrap; }
.entitlement-name { color: var(--text-heading); text-decoration: none; }
.entitlement-name:hover { color: var(--action); text-decoration: underline; }

/* --------------------------------------------------------------------------
 * Bulk selection — §9: "bulk actions via a checkbox column and a docked action bar
 * (bg-secondary strip, two corners cut) that appears with a selection".
 * -------------------------------------------------------------------------- */

/* §9's checkbox: a 16 px SHARP square with a 1 px border; checked is an action-colour
 * fill with a `check` glyph. `appearance: none` is what lets it be that instead of the
 * platform's rounded control — §3 allows no radius anywhere, and a native checkbox is
 * round-cornered on every platform this ships to. */
.checkbox {
  appearance: none;
  -webkit-appearance: none;
  width: 16px; height: 16px;
  margin: 0;
  background: var(--bg-primary);
  border: 1px solid var(--border-subtle);
  cursor: pointer;
  display: inline-block;
  position: relative;
  flex: none;
}
.checkbox:checked { background: var(--action); border-color: var(--action); }
/* The `check` glyph, drawn as the two strokes of the lucide path rather than as a font
   character — §8 forbids a unicode symbol standing in for an icon. */
.checkbox:checked::after {
  content: "";
  position: absolute;
  left: 5px; top: 1px;
  width: 4px; height: 9px;
  border: solid var(--text-inverse);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.checkbox:indeterminate { background: var(--action); border-color: var(--action); }
.checkbox:indeterminate::after {
  content: "";
  position: absolute;
  left: 3px; top: 6px;
  width: 8px; height: 2px;
  background: var(--text-inverse);
}
.checkbox:disabled { cursor: default; opacity: 0.4; }
.checkbox:focus-visible { outline: none; box-shadow: 0 0 8px rgba(211, 253, 34, 0.35); }
.content.light .checkbox:focus-visible { box-shadow: none; outline: 2px solid var(--action); outline-offset: 2px; }

/* The selection column is narrow — but ONLY where there is one. Scoped by what the cell
   actually contains rather than by its position, because "first column" is also the name
   column of every other table in the application, and pinning that to 40 px squeezed
   people's names into two lines on a screen with room to spare. */
.select-cell,
.table th:first-child:has(.checkbox) { width: 40px; padding-right: 0; }
.table tr.is-selected td { background: var(--bg-tertiary); }

/* The docked bar. Sticky to the bottom of the viewport so it stays reachable however far
 * down a long table the selection was made — it is the only control that acts on the
 * selection, and a bar that scrolls away with the row that started it is a bar the
 * operator has to hunt for. */
.bulk-bar {
  position: sticky;
  bottom: calc(var(--grid) * 2);
  z-index: 20;
  display: flex;
  align-items: center;
  gap: calc(var(--grid) * 2);
  flex-wrap: wrap;
  background: var(--bg-secondary);
  padding: calc(var(--grid) * 1.5) calc(var(--grid) * 3);
  margin-top: calc(var(--grid) * 2);
}
.bulk-count { color: var(--text-heading); font-size: 14px; }
.bulk-clear {
  background: none; border: 0; padding: 0;
  font: inherit; font-size: 14px;
  color: var(--action); cursor: pointer; text-decoration: underline;
}
.bulk-actions { margin-left: auto; display: flex; gap: var(--grid); flex-wrap: wrap; }

/* A scroll target under a STICKY header must stop below it, not behind it. `scrollIntoView`
   aligns to the viewport's top edge and knows nothing about the panel floating there, so
   the region it lands on would start under the chrome. */
[id="employee-table"] { scroll-margin-top: calc(var(--grid) * 9); }

/* Badges and tags do not wrap — §32 of the finishing pass, and §9's own definition: a tag
   is a label, and a label broken across two lines has stopped being one. The run card's
   head lets the badge keep its width and the course name give way instead. */
.badge { white-space: nowrap; }
.run-card-head { align-items: flex-start; }
.run-card-course { min-width: 0; overflow-wrap: anywhere; }
.badge-gated { white-space: nowrap; }

/* Employee detail's run cards, and the geometry they share with every other card. A card
   with a top rule takes the BOTTOM-RIGHT cut only (§3), which is what `.cut-bottom-right`
   above draws — the class was applied to the run card and to nothing else, so the same
   conceptual card had two shapes on two screens. */
.run-cards { align-items: start; }
.run-card { min-width: 0; }

/* --------------------------------------------------------------------------
 * Responsiveness — §11: the WHOLE admin application is usable on mobile. Tiles stack into
 * one column, tables collapse, every action stays reachable, and the top panel persists.
 * Cut sizes, the `< >` prefix and the type scale do not change between viewports.
 * -------------------------------------------------------------------------- */
@media (max-width: 900px) {
  .content { padding: calc(var(--grid) * 2); }
  .top-panel { padding: calc(var(--grid) * 1.5) calc(var(--grid) * 2); gap: var(--grid); }
  .top-panel-left { gap: calc(var(--grid) * 2); }
  .tile-grid { grid-template-columns: 1fr; }
  .header-actions { margin-left: 0; width: 100%; }
}

@media (max-width: 640px) {
  /* §10: on mobile the breadcrumb's segments drop FROM THE RIGHT, and they are never
     reworded or abbreviated. The product name goes first — the mark still says whose
     product this is — and then the trail keeps only its final segment, which is where the
     reader actually is. */
  /* Scoped to the TOP PANEL. The login panel uses the same lockup and has no space
     pressure at all — hiding the product name there took the product's own name off the
     first screen a person meets, to solve a crowding problem that screen does not have. */
  .top-panel .product-name { display: none; }
  .breadcrumb-item:not(:last-child) { display: none; }
  .breadcrumb-item:last-child .breadcrumb-sep { display: none; }

  /* §11: tables collapse into stacked sharp bg-secondary cards with the same fields. Each
     cell carries its column's name from `data-label`, because a value with no header
     above it is a number nobody can read. */
  .table, .table tbody, .table tr, .table td { display: block; width: 100%; }
  .table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .table tr { background: var(--bg-secondary); margin-bottom: var(--grid); padding: var(--grid); border-bottom: 0; }
  .table td { height: auto; border-bottom: 0; padding: 4px var(--grid); display: flex; justify-content: space-between; gap: var(--grid); }
  .table td::before { content: attr(data-label); font-size: 12px; text-transform: uppercase; color: var(--text-muted); flex: none; }
  .table td:first-child { display: block; }
  .table td:first-child::before { content: none; }

  .run-cards { grid-template-columns: 1fr; }
  .stat-row { grid-template-columns: 1fr; }
  .colour-fields { flex-direction: column; }
  .pager { gap: var(--grid); }
  .pager .muted { margin-right: 0; width: 100%; }
  .modal { padding: calc(var(--grid) * 2); }
  .record-identity .page-title { font-size: 24px; }
}
