/* skin.css — the half of the reskin a Loom theme cannot express.
 *
 * nominate.css remaps every token the toolkit reads, which converts colour,
 * elevation, type family and radius across all ~65 widgets at once. What it
 * cannot touch is anything loom.css writes as a literal: border WIDTHS (1px,
 * ~148 times), font WEIGHTS, the press gesture, the label idiom, and about
 * twenty box-shadows on overlays that were authored before --sh-* existed.
 *
 * Those literals are this file. It is deliberately short — if it starts
 * growing past a screen or two, that is the signal that the right move is a
 * patch upstream in cbloom rather than a thicker override layer here.
 *
 * ── Cascade ────────────────────────────────────────────────────────────────
 * Loaded last, so plain selectors win at equal specificity and !important is
 * reserved for the three genuinely global invariants in §0. Everything below
 * is either the same specificity as the rule it replaces or one class deeper.
 *
 * ── What is deliberately NOT here ──────────────────────────────────────────
 * The site's component styles. site/src/styles/tokens.css is the whole
 * editorial design system and only its :root block crosses into the lab
 * (vendor-loom.mjs explains why). So .btn below restyles LOOM's button; it
 * does not import the site's. Two systems fighting over twelve shared class
 * names is a worse outcome than one system wearing the other's palette.
 */

/* ══ §0 · Global invariants ═════════════════════════════════════════════════
   Three rules that are true of this design system everywhere, which is what
   earns them !important. Each is a stated property of the system rather than
   a fix for a particular widget. */

/* ⚠️ The root font size is OURS, and this is the highest-leverage line in the
   file.

   loom.css sets `html, body { font-size: 13px }`. Every size in components.css
   is expressed in `rem` against the design system's 16px root, so under Loom's
   root the entire language renders at **81% of its designed size** — uniformly,
   which is the trap. Nothing looks broken, nothing is misaligned, every
   proportion is internally consistent; the whole page is just quietly too small
   and reads as timid. It cost cbprospect a round of "it doesn't quite have the
   shine and polish", and no amount of reading either stylesheet would have
   surfaced it — comparing /skin/preview.html against the app side by side did.

   Loom's own widgets do not move with this: loom.css is ~1,620 px values
   against ~20 rem values, so it is a pixel stylesheet in practice. */
html,
body {
  font-size: 16px;
}

/* No blurs. Anywhere. loom.css carries seven backdrop-filters on scrims and
   popovers; a frosted overlay under hard, zero-blur shadows is the same
   category error as a soft shadow and reads as a different product. */
*,
*::before,
*::after {
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
}

/* The sanctioned exceptions, and they are the design system's own. Opt in
   with `data-glass`; everything else stays flat.

     - STICKY CHROME. The site and the lab both glass their header: a
       translucent bar blurred over the starfield. That is part of the look the
       invariant above exists to protect, not a violation of it.
     - THE BUSY OVERLAY. Holding the previous view on screen, blurred, while
       the next one loads (operator, 2026-08-17: "slightly blur everything
       underneath whilst loading").

   The AMOUNT is a variable with its default in the fallback, not a value
   baked into this rule. The overlay wants ~3px where the header wants 12, and
   the alternative — a second `!important` somewhere else to beat this one —
   is how a global invariant quietly stops being one. Set `--ax-glass-blur` on
   the element; the opt-in stays a single path with a single !important.

   ⚠️ `--ax-glass-blur`, NOT `--glass-blur`. The unprefixed name is LOOM'S, and
   `nominate.css` deliberately sets it to 0px to flatten Loom's own glass
   widgets. Reusing it here read that 0 through inheritance and silently
   un-blurred the header — the one surface this exception exists for. App-owned
   properties live in the `--ax-*` namespace for exactly this reason. */
[data-glass] {
  -webkit-backdrop-filter: blur(var(--ax-glass-blur, 12px)) !important;
  backdrop-filter: blur(var(--ax-glass-blur, 12px)) !important;
}

/* One focus ring, never removed: 2px gold at 3px offset. loom.css sets
   `outline: none` on inputs and swaps a border colour instead, which is a
   focus indicator that disappears against a themed border. */
:focus-visible {
  outline: 2px solid var(--n-gold) !important;
  outline-offset: 3px;
  border-radius: 2px;
}

/* A hard stop, not a softening. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ══ §1 · Ground ════════════════════════════════════════════════════════════ */

/* Flat, deliberately — the one place this skin declines to bring the site's
   look across.

   The site paints its editorial pages on a plum gradient with two colour
   glows, and lays a two-layer starfield over the top (body::before, dot grids
   at 140×128 and 180×176). That texture is right for a landing page and wrong
   behind a 4,000-row operator table at 11px: it puts a regular pattern of
   light dots directly behind dense type, and the eye reads them as part of the
   data. The ported auth pages DO take the full ground — see auth.css — because
   a centred card on an empty page is exactly what the texture was designed
   for. Ground by surface, not by rule.

   (Recorded because the first read of those dots was wrong: they look like
   gradient dither banding in a screenshot, and were diagnosed as such before
   the source said otherwise.) */
body {
  background: var(--bg);
  font-family: var(--font-body);
}

/* ══ §2 · Depth ═════════════════════════════════════════════════════════════
   Every overlay in loom.css that predates the --sh-* tokens and carries a
   literal blurred shadow. Enumerated rather than blanketed, because a blanket
   box-shadow rule would also flatten the ones the theme already got right. */
#loom-search-panel,
.loom-tenant-menu,
.loom-notify-popover,
.loom-inbox-popover,
#loom-log-panel,
#loom-harness,
.loom-modal,
.lsp-results,
.loom-ckdd-panel,
.loom-drawer:not(.collapsed),
.loom-appgroup-panel,
.modal,
.toast,
.loom-bulk-bar,
.loom-info-modal,
.lcv-pane {
  box-shadow: var(--n-hard-card);
}

/* ══ §3 · Line weight ═══════════════════════════════════════════════════════
   2px mauve, never a hairline and never grey. loom.css writes `1px solid
   var(--border)` inline in ~148 rules; these are the structural containers
   where the difference is what makes the design read as ours. Table interiors
   are deliberately left at 1px — 2px between every row draws a grid, not a
   table. */
.zoo-demo,
.kpi-card,
.detail-panel,
.data-table,
.loom-modal,
.modal,
.loom-info-modal,
.toast,
.drop-zone,
.lcv-canvas,
.lcv-pane,
.ctrl-select,
.ctrl-input,
.field input,
.field select,
.field textarea {
  border-width: 2px;
  border-style: solid;
  border-color: var(--border);
}

/* Radius. The tokens cover components that use --r-*; these are the literals
   (4px / 6px / 8px / 10px / 12px scattered through loom.css). Close to square,
   because soft corners under a hard shadow read as a bevel. */
.zoo-demo,
.zoo-code,
.kpi-card,
.detail-panel,
.data-table,
.badge,
.btn,
.ctrl-select,
.ctrl-input,
.field input,
.field select,
.field textarea,
.loom-modal,
.modal,
.loom-info-modal,
.toast,
.lcv-canvas,
.lcv-pane {
  border-radius: var(--r-md);
}

/* ══ §4 · The label idiom ═══════════════════════════════════════════════════
   Mono, uppercase, .14em tracking, gold. It is the one typographic signature
   this system has, it appears on every surface of the site, and it is the
   cheapest way to make a borrowed toolkit read as ours. Loom already sets
   these labels uppercase at .06–.12em in --subdued; this is a re-spec, not a
   new pattern. */
.section-title,
.kpi-card .kpi-label,
.field label:not(.loom-switch),
.data-table th {
  font-family: var(--font-mono);
  letter-spacing: 0.14em;
  color: var(--n-gold);
  font-weight: 700;
}

/* The section title is the page's eyebrow, so it gets the eyebrow's rule —
   a short gradient hairline running off the end of the words. */
.section-title {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}
.section-title::after {
  content: "";
  flex: 1;
  height: 2px;
  max-width: 4rem;
  background: linear-gradient(90deg, var(--border), transparent);
}

/* Table headers stay quieter than a section title — gold at full strength on
   every column shouts over the data underneath. */
.data-table th {
  color: color-mix(in srgb, var(--n-gold) 70%, var(--subdued));
}

/* Source locations in the zoo are addresses, and the system paints addresses
   in the source blue. */
.zoo-meta .zoo-loc {
  color: var(--n-blue);
}

/* ══ §5 · Buttons and pills ═════════════════════════════════════════════════ */

/* The press gesture: translate 2px on both axes into the shadow, never a
   scale. The shadow shrinks by the same 2px so the button visibly travels
   toward the page instead of just sliding. */
.btn,
.loom-pill,
.loom-hot-btn,
.loom-modal-btn,
.loom-wizard-btn {
  transition: transform 0.15s, border-color 0.15s, background 0.15s, color 0.15s;
}
.btn:active,
.loom-pill:active,
.loom-hot-btn:active,
.loom-modal-btn:active,
.loom-wizard-btn:active {
  transform: translate(2px, 2px);
}

/* A filled button gets the hard shadow; a bare .btn is Loom's neutral
   secondary and stays flat, matching .btn-ghost on the site. */
.btn-blue,
.btn-green,
.btn-red {
  box-shadow: var(--n-hard);
}
.btn-blue:active,
.btn-green:active,
.btn-red:active {
  box-shadow: 2px 2px 0 rgba(7, 5, 14, 0.35);
}

/* ── CONTRAST, not taste ────────────────────────────────────────────────
   loom.css hardcodes `color: #fff` on a themed fill in twenty-one places.
   That was correct for its own palette — vermillion #E8461C, forest #2D9B5A,
   scarlet #DB3939 are all dark enough to carry white text.

   Every fill in ours is a light tint. White lands at roughly 2.5:1 on accent,
   1.9:1 on mint and 2.4:1 on pink: a sweep of AA failures inherited without a
   pixel of layout changing, which is the specific way a palette remap goes
   wrong. It is also invisible to every check except a contrast audit, because
   the buttons look fine — they look, in fact, exactly like the design.

   Fixed as one list rather than one at a time. The first pass patched the
   three variants that happened to be on screen and left eighteen; enumerating
   the whole set from `grep 'color: *#fff' loom.css` is the difference between
   a fix and a sighting. The system already names the answer:
   --accent-foreground, the deep plum under every accent fill on the site. */
.btn-blue,
.btn-green,
.btn-red,
.loom-pill.active,
.loom-pill-solid,
.loom-subnav-link.active,
.loom-wizard-btn.primary,
.loom-wizard-step.done .loom-wizard-dot,
.loom-wizard-step.active .loom-wizard-dot,
.loom-svgr-btn.primary,
.loom-notify-badge,
.loom-inbox-badge,
.loom-confidential-bar,
.pagination .pg-num.active:disabled,
.loom-bulk-act:hover,
.loom-bulk-act.danger:hover {
  color: var(--n-accent-foreground);
  font-weight: 700;
}
.btn-blue:hover,
.loom-pill.active:hover {
  background: var(--n-gold);
  border-color: var(--n-gold);
  opacity: 1;
}

/* ── The hot button ─────────────────────────────────────────────────────
   Loom's primary action, and the one widget with a depth model of its own:
   a coloured halo, an inset white top-highlight for a subtle 3D lift, and a
   press that combines translateY with a scale. All three are the opposite of
   this system, which is flat, hard-shadowed, and never scales on press.

   The colour fix is scoped with :not([style*="--hot-bg"]) rather than applied
   flat, and that is deliberate. loomHotBtn takes an arbitrary opts.color and
   emits it as an inline `--hot-bg`, so a blanket dark label would be
   unreadable on the dark custom colours the zoo's own palette row passes in.
   Buttons on the system accent get the system's foreground; buttons given a
   caller's colour keep Loom's white, and their contrast stays the caller's
   problem — which is a real gap in loomHotBtn's API, not something a skin can
   paper over. */
/* :not(.tint):not(.outline) as well, because those two variants are not
   filled — they paint --hot-bg as the TEXT on a transparent or 14%-tinted
   ground. Forcing the dark label onto them put deep plum on near-plum at
   1.22:1, which is worse than what it replaced. Caught by re-running the
   audit after the fix rather than by reading the rule. */
.loom-hot-btn:not([style*="--hot-bg"]):not(.tint):not(.outline) {
  color: var(--n-accent-foreground);
}
.loom-hot-btn {
  box-shadow: var(--n-hard);
  transition: transform 0.15s, box-shadow 0.15s, background 0.15s;
}
.loom-hot-btn:not(.tint):not(.outline):hover {
  filter: none;
  background: var(--n-gold);
  box-shadow: var(--n-hard);
}
.loom-hot-btn:active {
  transform: translate(2px, 2px);
  filter: none;
  box-shadow: 2px 2px 0 rgba(7, 5, 14, 0.35);
}
/* The icon inside a hot button is force-inverted to white by loom.css to sit
   on its dark fills. On a light fill with a dark label the glyph has to invert
   the other way, or it disappears into the button. */
.loom-hot-btn:not([style*="--hot-bg"]):not(.tint):not(.outline) img {
  filter: brightness(0);
}

/* Gold on hover, everywhere, like every other interactive surface we own. */
.btn:hover {
  opacity: 1;
}
.loom-pill:hover {
  border-color: var(--n-gold);
  color: var(--n-gold);
}
.tab:hover {
  color: var(--n-gold);
}

/* The active tab's underline is the accent bar; thicken it to the system's
   2px line weight so it matches the borders around it. */
.tab {
  border-bottom-width: 3px;
}

/* ══ §5b · Overlays ═════════════════════════════════════════════════════════
   An overlay needs a different depth cue than a card, and finding that out is
   the one place the hard-shadow model does not just port across.

   On the page ground, `8px 8px 0 rgba(7,5,14,.34)` reads because the plum
   behind it is lighter than the shadow. A modal sits on a 55%-black scrim, so
   the shadow is black-on-black and contributes nothing — the first pass
   produced a modal that appeared to have no edge at all. The shadow stays
   (it catches the lighter parts of the page at the corners), but the EDGE is
   what does the work up here, so overlays get a brighter border than cards
   do. That is a rule about where the element sits, not about what it is. */
.loom-modal,
.modal,
.loom-info-modal,
.toast,
.loom-tenant-menu,
.loom-notify-popover,
.loom-inbox-popover,
.lsp-results,
.loom-ckdd-panel,
.loom-appgroup-panel,
.loom-bulk-bar {
  border: 2px solid color-mix(in srgb, var(--border) 55%, var(--n-foreground));
}

/* The toast keeps its 4px status-coloured left edge — that stripe IS the
   status, and it is the one border in the system that is allowed to be a
   colour other than mauve. */
.toast {
  border-left-width: 5px;
}

/* Modal buttons are a separate class from .btn and carry the same inherited
   `color: #fff` contrast failure on both filled variants. */
.loom-modal-btn.primary,
.loom-modal-btn.danger {
  color: var(--n-accent-foreground);
  font-weight: 700;
  box-shadow: var(--n-hard);
}
.loom-modal-btn.primary:hover {
  background: var(--n-gold);
  border-color: var(--n-gold);
}
.loom-modal-btn.danger:hover {
  /* Loom darkens toward black on hover. Ours is a light pink, so mixing it
     with #000 lands somewhere muddy and unrelated; deepening toward the page
     ground keeps it recognisably the same colour. */
  background: color-mix(in srgb, var(--n-pink) 78%, var(--n-space));
  border-color: var(--n-pink);
}

/* ── A loomModal BODY has to be able to scroll (cbapex#186, from cbsvi#125) ──
   Save VI's field lead, from a 351px iPhone: "I can only see one page worth of
   old entries. It does not allow me to scroll." MEASURED at 390x760 with a
   60-row ledger in `loomModal.prompt({bodyHtml})`, before this rule:

       modal box    16 ->  744   capped at calc(100dvh - 32px), correctly
       modal BODY   66 -> 2487   1727px of it below the fold
       Close button 2519 -> 2549 entirely off screen

   loom.css caps `.loom-modal` and makes it a flex column, then gives the
   scroll pair to ONE of the two bodies it can hold — `.loom-form-body`, the
   form modal's. `.loom-modal-body`, the read-only prompt's, got neither, and
   the read-only panel is exactly the one that runs long: an activity log, a
   history table, a provenance trace.

   ⚠️ BOTH HALVES OR NEITHER. `overflow-y: auto` alone does nothing in a flex
   column: the item's default `min-height: auto` refuses to shrink below its
   content, so the body still grows past the capped box. Specificity (0,2,0)
   beats loom.css's (0,1,0), so this does not depend on load order.
   `loom-modal-scroll.test.ts` pins the pair. cbsvi carried the identical block
   in its app-owned layout.css (cbsvi b2e1b48) until the kit took it over. */
.loom-modal .loom-modal-body {
  overflow-y: auto;
  min-height: 0;
}
.loom-modal-btn.primary:active,
.loom-modal-btn.danger:active {
  box-shadow: 2px 2px 0 rgba(7, 5, 14, 0.35);
}

/* ══ §6 · Badges ════════════════════════════════════════════════════════════
   loom.css tints these with hardcoded hex-with-alpha (#22c55e22, #3b82f622,
   #8b5cf6) that predate its own semantic tokens, so they survived the theme
   remap unchanged and were the last three web-default primaries left on the
   page. Re-derived from the palette via color-mix so they track it. */
.badge {
  font-family: var(--font-mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border: 1px solid currentColor;
}
.badge-green {
  background: color-mix(in srgb, var(--n-mint) 14%, transparent);
  color: var(--n-mint);
}
.badge-amber {
  background: color-mix(in srgb, var(--n-gold) 14%, transparent);
  color: var(--n-gold);
}
.badge-red {
  background: color-mix(in srgb, var(--n-pink) 14%, transparent);
  color: var(--n-pink);
}
.badge-blue {
  background: color-mix(in srgb, var(--n-accent) 14%, transparent);
  color: var(--n-accent);
}
.badge-purple {
  background: color-mix(in srgb, var(--n-violet) 14%, transparent);
  color: var(--n-violet);
}
.badge-muted {
  background: color-mix(in srgb, var(--n-line) 22%, transparent);
  color: var(--subdued);
}

/* ══ §7 · Chrome ════════════════════════════════════════════════════════════ */

/* Platform controls that cannot work here, removed rather than left inert —
   a dead control in a design review gets reviewed as a design.
     · the global app search indexes a tenant's apps; there is one page here
     · the mode toggle switches light/dark, and this system has one mode
     · the theme picker offers ten Loom themes, and we vendor none of them
   All three are stated on the lab's own /zoo/ page instead, where the note is
   information rather than a broken affordance. */
#loom-search,
.hdr-search,
#loom-search-panel,
#loom-mode-btn,
#theme-select,
.theme-select {
  display: none !important;
}

/* The header separator that sat next to the hidden Campaign Brain mark. With
   the mark gone it is a floating 1px tick in the top-left corner. */
.loom-home-mark + .hdr-sep {
  display: none;
}

/* The zoo's own demo panel is the page's most repeated element, so it carries
   the card shadow — this is the surface a reviewer is actually looking at. */
.zoo-demo {
  box-shadow: var(--n-hard);
  background: var(--surface);
}

/* Code samples sit on the page ground, not on a card, so they read as output
   rather than as another panel. */
.zoo-code {
  border: 2px solid var(--border);
}

/* ══ §8 · Type — Loom's widgets on the kit's scale ══════════════════════════
   The kit's type scale (components.css § Type, zoo § Type) covered every kit
   file and none of loom.css, which sets its widgets in px: a loomDataTable at
   12px with 10px headers beside an ax-ledger at 14px with 11px headers, form
   labels at 10px, filter inputs at 11px, modal titles at 15px. Each Loom
   widget the apps actually call (data tables 20 call sites, toasts 99, form
   and prompt modals in all sixteen, pills, switches) takes the step its kit
   equivalent takes, so the two families read as one:

     table cells, inputs, modal body, buttons, toast   sm     (as ax-ledger td, ax-input, ax-toast)
     modal title                                       md     (as ax-dialog-title)
     pills, switch label, modal sub/err/log            xs
     table headers, form labels, group counts          micro  (as ax-ledger th, ax-filter)

   Same selectors as loom.css, loaded after it, so these win on order alone. */
.data-table,
.data-table td,
.loom-modal-body,
.loom-modal-btn,
.modal textarea,
.filter-bar select,
.filter-bar input,
.loom-form-row input:not([type="checkbox"]):not([type="radio"]),
.loom-form-row select,
.loom-form-row textarea,
.toast {
  font-size: var(--ax-fs-sm);
}
.loom-modal-title,
.modal h2 {
  font-size: var(--ax-fs-md);
}
.loom-pill,
.loom-pill-select,
.loom-switch,
.loom-modal-err,
.loom-modal-log,
.modal .modal-sub {
  font-size: var(--ax-fs-xs);
}
.data-table th,
.data-table .dt-gcount,
.dt-gcard .dt-gcount,
.modal-section h4,
.field label:not(.loom-switch),
.loom-form-row label {
  font-size: var(--ax-fs-micro);
}
