/* ==========================================================================
   ZYME DESIGN LANGUAGE
   Shared theme for dashboard (9001), assets (9002), portal (9003),
   gallery (9000). Single source of truth — do not redeclare these values
   in a page's own <style> block.

   Source: Paper file "ZYME UI design"
     v1 "Design Language — Spec"             §01–§09
     v2 "Design Language — Extended objects" §10–§15
   Handoff bundle: ~/design-inbox/ (TOKENS.css, DESIGN.md, COMPONENTS.md)

   THE GOVERNING IDEA (v2 §14)
     "The work carries the colour." The interface is monochrome so the only
     saturated thing on screen is what the user made. Colour is spent, not
     decorated. Exactly one job qualifies: telling you something has failed.

   THE FIVE THINGS EASIEST TO GET WRONG (DESIGN.md §11)
     1. Two inverted elements. One per screen; on a page with no expensive
        verb, none.
     2. Spending the lime on anything but failure.
     3. Using --color-wash as a hover fill. It is not one. Hover is a
        contrast lift or a border change.
     4. Letting the type scale creep. The h1 is 14px. On purpose.
     5. Adding air. The ladder stops at 16px.
   ========================================================================== */

:root {

  /* --- COLOUR: surfaces ------------------------------------------------
     §01 r01: one background colour for every surface. A card, an input, a
     modal and the page body are the same value. No elevation by lightness. */
  --color-bg:      #1a1a1a;
  --color-surface: #1a1a1a;   /* declared separately by §09; same value */

  /* --- COLOUR: ink ---------------------------------------------------- */
  --color-text:   #e9e9e3;    /* text, all borders, focus, fills, handles */
  --color-accent: #e9e9e3;    /* §09 declares it separately; same value */
  --color-muted:  #8f8f88;    /* labels, hints, filenames, empty states */

  /* --- COLOUR: the four derivations (§02) ------------------------------ */
  --color-line:  rgba(233, 233, 227, 0.22);  /* every edge. always 1px. */
  --color-wash:  rgba(233, 233, 227, 0.08);  /* drag-over + active strip ONLY */
  --color-scrim: rgba(26, 26, 26, 0.72);     /* modal / lightbox ground */

  /* --- COLOUR: the one hue (v2 §10) ------------------------------------
     Phosphor lime. Failure, and actions that destroy work. Chosen because
     at this chroma it does not occur in photographs, scans or plates, so it
     reads as instrument rather than image.
     BUDGET: one alarmed thing per screen · never a fill behind content ·
     never inside a thumbnail or canvas · success gets no hue · not a brand
     colour. */
  --color-alarm: #a9ff00;

  /* --- BORDERS -------------------------------------------------------- */
  --hairline:            1px;                 /* the only border width */
  --zyme-border-default: var(--color-line);   /* rest — 22% */
  --zyme-border-strong:  var(--color-text);   /* hover & focus — 100% */
  --zyme-border-alarm:   var(--color-alarm);
  /* There is no third "subtle" tier. Two colours, one width. */

  /* --- FOCUS (v2 §12/§14) ----------------------------------------------
     Hover is a border-colour change. KEYBOARD focus adds a second 1px rule
     offset 2px outside the first. Drawn, never glowed. */
  --zyme-focus-outline-width:  1px;
  --zyme-focus-outline-offset: 2px;
  --zyme-focus-outline-color:  var(--color-text);

  /* --- TEXT ROLES ------------------------------------------------------ */
  --zyme-text-primary:   var(--color-text);
  --zyme-text-secondary: var(--color-muted);
  --zyme-text-inverse:   var(--color-bg);
  --zyme-text-alarm:     var(--color-alarm);
  --zyme-disabled-opacity: 0.4;   /* §05: "nothing else. never a greyed variant." */

  /* --- LINKS -----------------------------------------------------------
     RESOLVED 2026-07-31 (OPEN-QUESTIONS Q1 — neither sheet contains a link).
     Decision: the system's own border rule, applied to text. Underline rests
     at --color-line and goes full contrast on hover. Invents no new device;
     the hue stays reserved for failure. */
  --zyme-link-underline:       var(--color-line);
  --zyme-link-underline-hover: var(--color-text);

  /* --- PRIMARY ACTION (§01 r05 · §04 · v2 §12) -------------------------
     The only place ink and ground swap. That single inversion is the entire
     visual budget for emphasis. One per screen. */
  --action-h: 36px;             /* +6px — the only element allowed to break 30 */
  --zyme-action-fill:  var(--color-text);
  --zyme-action-label: var(--color-bg);
  --zyme-action-hover-inset: 3px;   /* the border is drawn INSIDE it */

  /* --- CONTROL METRICS (§04 / §09) -------------------------------------
     §01 r04: departing from 30px is a hierarchy statement, not a convenience. */
  --ctl-h: 30px;
  --bar-h: 44px;                /* status bar — one bottom line, no fill */
  --container-slider:     18px; /* "a readout, not a field" */
  --knob:                 14px;
  --zyme-microbtn-w:      22px;
  --zyme-microbtn-h:      18px;
  --zyme-key-chip-w:      48px;
  --zyme-key-chip-h:      20px;
  --zyme-overlay-close-w: 24px;
  --zyme-overlay-close-h: 22px;
  --zyme-menu-row-h:      24px;
  --zyme-indicator-size:  8px;  /* a square, not a circle */
  --zyme-checkbox-size:   12px;
  --zyme-row-select-lane: 12px;

  /* --- SPACING (§04 / §09) ---------------------------------------------
     2 3 4 6 8 10 12 14 16. Not 4- or 8-based — a hand-tuned dense ladder
     that coarsens as it grows. 8 is the workhorse. 16 is the ceiling, and is
     reserved for the one region that matters. */
  --spacing-2xs:  2px;
  --spacing-xs:   3px;
  --spacing-sm:   4px;
  --spacing-md:   6px;
  --spacing-lg:   8px;    /* THE DEFAULT GAP */
  --spacing-xl:  10px;
  --spacing-2xl: 12px;
  --spacing-3xl: 14px;    /* §09 declares it; §04 gives it no role */
  --spacing-4xl: 16px;

  /* --- RADIUS (§09 · v2 §14) -------------------------------------------
     One radius, and it belongs to every element class. v2 amends the rule's
     standing, not its value: square is house style, not physics. If a future
     control genuinely reads better rounded, round it and write down why. */
  --radius-none: 0px;

  /* --- TYPE ------------------------------------------------------------
     Q2 RESOLVED: keep the system stack. These pages render in the viewer's
     browser, not on the server, so the host's fontconfig is irrelevant.
     system-ui gives Segoe UI on Windows (what the sheets were authored
     against) and SF Pro on macOS/iOS.
     Q3 RESOLVED: system mono stack. Zero bytes shipped, no CDN on the
     public portal, nothing to keep in sync — consistent with §07's stance
     on icon libraries. */
  --font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --zyme-numeric: tabular-nums;

  /* sizes — 10/11/12/13/14 and stop. No display tier: size is not a
     hierarchy tool here. The h1 is 14px. */
  --text-2xs:  10px;
  --text-xs:   11px;
  --text-sm:   12px;
  --text-base: 13px;
  --text-md:   14px;

  --font-weight-normal:   400;
  --font-weight-semibold: 600;   /* panel headers only */
  --font-weight-bold:     700;   /* primary action only */

  /* tracking — §01 r08: if it is tracked it is a label; if it is untracked
     it is content. Load-bearing semantics, not decoration. */
  --tracking-title:  0.033em;
  --tracking-key:    0.04em;
  --tracking-label:  0.064em;
  --tracking-action: 0.1em;

  --leading-none:    100%;
  --leading-relaxed: 140%;
  --leading-normal:  145%;

  /* --- ELEVATION -------------------------------------------------------
     The complete elevation system. Depth exists only as the scrim. */
  --zyme-shadow-none: none;

  /* --- MOTION (§01 r10 · v2 §13/§15) -----------------------------------
     Three transitions exist. Easing is linear for all three. */
  --zyme-duration-border:        120ms;
  --zyme-duration-progress:      150ms;
  --zyme-duration-indeterminate: 900ms;
  --zyme-easing: linear;

  /* --- LAYOUT (§06 · amended v2 §14) ----------------------------------- */
  --rail-l:    300px;   /* INPUTS */
  --rail-r:    460px;   /* CONTROLS — commit pinned last */
  --overlay-w: 380px;
  --breakpoint-desktop: 1440px;
  --breakpoint-fold:     720px;   /* public pages only */

  /* lane discipline (§04): fixed tracks, never gap alone. Label lanes
     truncate with an ellipsis rather than pushing the control out of
     alignment, so the numeric column stays readable as a column. */
  --zyme-lane-label: 96px;
  --zyme-lane-value: 58px;
  --zyme-lane-ts:    64px;   /* log feed timestamp */
  --zyme-lane-level: 56px;   /* log feed level word */
}


/* ==========================================================================
   BASE
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--color-bg);
  color: var(--color-text);
}

body {
  font-family: var(--font-sans);
  font-size: var(--text-md);
  font-weight: var(--font-weight-normal);
  line-height: var(--leading-normal);
  -webkit-font-smoothing: antialiased;
}

/* Tabular figures everywhere a number can change (§03). On a monitoring
   dashboard, proportional digits make every readout twitch. */
input, table, .zyme-num, .zyme-mono, time { font-variant-numeric: var(--zyme-numeric); }

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

/* The h1 is 14px and is the same size as body text. This looks like a
   mistake until you see it in place (DESIGN.md §11.4). */
h1 {
  font-size: var(--text-md);
  font-weight: var(--font-weight-normal);
  line-height: var(--leading-normal);
  margin: 0;
}

h2 {
  font-size: var(--text-sm);
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-title);
  color: var(--color-text);
  margin: 0;
}

/* Links — see --zyme-link-underline above. */
a {
  color: var(--color-text);
  text-decoration: none;
  border-bottom: var(--hairline) solid var(--zyme-link-underline);
  transition: border-color var(--zyme-duration-border) var(--zyme-easing);
}
a:hover { border-bottom-color: var(--zyme-link-underline-hover); }

/* Keyboard focus — the second rule, offset outside the first. Never a glow,
   never a lift, never a fill change. */
:focus { outline: none; }
:focus-visible {
  outline: var(--zyme-focus-outline-width) solid var(--zyme-focus-outline-color);
  outline-offset: var(--zyme-focus-outline-offset);
}

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

::selection { background: var(--color-text); color: var(--color-bg); }

/* Disabled is the same element at .4. Never a greyed variant. */
[disabled], .is-disabled {
  opacity: var(--zyme-disabled-opacity);
  pointer-events: none;
}

/* The `hidden` attribute is a user-agent rule, and ANY author rule beats it.
   Nearly every component below sets an explicit `display`, which would
   silently defeat `el.hidden = true` — the element stays on screen and its
   close button appears broken. Restore the attribute's meaning globally.
   This is the one !important in the file and it earns its keep. */
[hidden] { display: none !important; }


/* ==========================================================================
   TYPE ROLES (§03's nine levels)
   ========================================================================== */

.zyme-title      { font-size: var(--text-md); font-weight: 400; }                        /* 1 */
.zyme-status     { font-size: var(--text-base); color: var(--color-muted); }             /* 3 */
.zyme-status.is-complete { color: var(--color-text); }
.zyme-panel-title{ font-size: var(--text-sm); font-weight: 600;
                   text-transform: uppercase; letter-spacing: var(--tracking-title); }   /* 4 */
.zyme-overlay-title { font-size: var(--text-sm); font-weight: 400;
                   text-transform: uppercase; letter-spacing: var(--tracking-label); }   /* 5 */
.zyme-inline     { font-size: var(--text-sm); }                                          /* 6 */
.zyme-group-title{ font-size: var(--text-xs); font-weight: 400; color: var(--color-muted);
                   text-transform: uppercase; letter-spacing: var(--tracking-label); }   /* 7 */
.zyme-label,
.zyme-hint       { font-size: var(--text-xs); color: var(--color-muted); }               /* 8 */
.zyme-dense      { font-size: var(--text-2xs); font-variant-numeric: var(--zyme-numeric); } /* 9 */

/* §07: hints qualify, they don't explain. 11px, muted, directly under its
   control, stating a consequence the control cannot show. Nowhere else. */
.zyme-hint { display: block; margin-top: var(--spacing-md); }
.zyme-hint.is-error { color: var(--color-alarm); }

/* Null is an em dash, so the row keeps its height and the column keeps its
   lane (§07). Use .zyme-null::before or write the character directly. */
.zyme-null { color: var(--color-muted); }
.zyme-null::before { content: "\2014"; }


/* ==========================================================================
   THE ACTION LADDER (v2 §12)
   Primary 36px inverted · Secondary 30px outlined · Danger outlined alarm ·
   Quiet label only. "Danger is outlined, never filled — it destroys work, it
   does not deserve the eye's first stop."
   ========================================================================== */

.zbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: var(--ctl-h);
  padding: 0 var(--spacing-xl);
  border: var(--hairline) solid var(--color-line);
  border-radius: var(--radius-none);
  background: var(--color-bg);
  color: var(--color-text);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: var(--font-weight-normal);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  text-decoration: none;   /* a .zbtn on an <a> is a button, not a link */
  cursor: pointer;
  transition: border-color var(--zyme-duration-border) var(--zyme-easing),
              color var(--zyme-duration-border) var(--zyme-easing);
}
.zbtn:hover { border-color: var(--color-text); }

/* Primary — full width, borderless, inverted. The label describes what will
   actually happen ("Generate grid"), not a generic verb. */
.zbtn--primary {
  height: var(--action-h);
  width: 100%;
  border: none;
  background: var(--zyme-action-fill);
  color: var(--zyme-action-label);
  font-size: var(--text-base);
  font-weight: var(--font-weight-bold);
  letter-spacing: var(--tracking-action);
}
/* v2 §12 resolves v1's dead end: this element has no border for the hover
   rule to act on, so the rule is drawn inside it. Negative outline-offset,
   not a shadow. */
.zbtn--primary:hover,
.zbtn--primary:focus-visible {
  outline: var(--hairline) solid var(--color-bg);
  outline-offset: calc(-1 * var(--zyme-action-hover-inset));
}

.zbtn--danger { border-color: var(--color-alarm); color: var(--color-alarm); }
.zbtn--danger:hover { border-color: var(--color-alarm); }   /* never filled */

.zbtn--quiet {
  border-color: transparent;
  background: none;
  color: var(--color-muted);
  padding: 0 var(--spacing-md);
}
.zbtn--quiet:hover { border-color: transparent; color: var(--color-text); }

/* Micro button — lives in panel headers "so they never compete with the
   content". 3px between adjacent buttons. */
.zbtn--micro {
  height: var(--zyme-microbtn-h);
  width: var(--zyme-microbtn-w);
  padding: 0;
  font-size: var(--text-xs);
  letter-spacing: 0;
}
.zbtn--micro + .zbtn--micro { margin-left: var(--spacing-xs); }

.zbtn--icon { width: var(--ctl-h); padding: 0; letter-spacing: 0; }

/* There are no success or warning button variants, by rule (v2 §10 r4). A
   confirming action is a secondary button; its meaning is in its label. */


/* ==========================================================================
   FIELDS (§05 · §04 · v2 §12)
   ========================================================================== */

.zfield,
.zselect {
  height: var(--ctl-h);
  width: 100%;
  padding: 0 var(--spacing-lg);
  border: var(--hairline) solid var(--color-line);
  border-radius: var(--radius-none);
  background: var(--color-bg);         /* the same value as the page */
  color: var(--color-text);
  font-family: inherit;
  font-size: var(--text-md);
  transition: border-color var(--zyme-duration-border) var(--zyme-easing);
}
.zfield:hover, .zselect:hover { border-color: var(--color-text); }
.zfield:focus-visible, .zselect:focus-visible { border-color: var(--color-text); }

/* Error is a hint that changed colour. Nothing moves, nothing is added, the
   row keeps its height (v2 §12). */
.zfield.is-error { border-color: var(--color-alarm); }
.zfield.is-error + .zyme-hint,
.zyme-label.is-error { color: var(--color-alarm); }

/* Number field — 58px, right-aligned with a 7px inset, tabular, no steppers. */
.zfield--num {
  width: var(--zyme-lane-value);
  padding: 0 7px 0 0;
  text-align: right;
  font-variant-numeric: var(--zyme-numeric);
}
.zfield--num::-webkit-outer-spin-button,
.zfield--num::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.zfield--num { -moz-appearance: textfield; appearance: textfield; }

/* Select — chevron 10×6, stroke muted at 1.5, inset 9px. Option lists are
   pulled from the server, never hardcoded, so the control can't drift from
   the engine (§05). */
.zselect {
  appearance: none;
  padding-right: 26px;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238f8f88' stroke-width='1.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 9px center;
}

/* Label above, 6px gap. */
.zyme-label { display: block; margin-bottom: var(--spacing-md); }

/* Slider — 18px track, ink fill from the left, 2px ink handle at the fill's
   leading edge. Shorter than a control: it is a readout, not a field. Keep
   both halves — the fill box reads as a gauge even when nobody drags it. */
.zslider {
  appearance: none;
  width: 100%;
  height: var(--container-slider);
  padding: 0;
  border: var(--hairline) solid var(--color-line);
  border-radius: var(--radius-none);
  background: var(--color-bg);
  cursor: pointer;
  transition: border-color var(--zyme-duration-border) var(--zyme-easing);
}
.zslider:hover { border-color: var(--color-text); }
.zslider::-webkit-slider-runnable-track { height: 100%; background: transparent; }
.zslider::-webkit-slider-thumb {
  appearance: none;
  width: 2px;
  height: calc(var(--container-slider) - 2px);   /* inside the 1px border */
  margin-top: 0;
  border: none;
  border-radius: var(--radius-none);
  background: var(--color-text);
}
.zslider::-moz-range-track { height: 100%; background: transparent; }
.zslider::-moz-range-progress { height: 100%; background: var(--color-wash); }
.zslider::-moz-range-thumb {
  width: 2px; height: calc(var(--container-slider) - 2px);
  border: none; border-radius: var(--radius-none); background: var(--color-text);
}

/* The canonical continuous control: label 96 / slider fluid / number 58.
   Label lane truncates with an ellipsis rather than pushing the control out
   of alignment. */
.zlane {
  display: grid;
  grid-template-columns: var(--zyme-lane-label) 1fr var(--zyme-lane-value);
  align-items: center;
  gap: var(--spacing-lg);
}
.zlane > .zyme-label {
  margin: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}


/* ==========================================================================
   TOGGLE (v2 §11)
   Track 30×18 — the width of a control and the height of a slider track, so
   it lands on the existing lane rhythm without inventing a size. Two squares,
   not a pill. Toggle when the change takes effect immediately; checkbox when
   the value is submitted with a form. That is the whole rule.
   ========================================================================== */

.ztoggle { display: inline-block; position: relative;
           width: var(--ctl-h); height: var(--container-slider); }
.ztoggle input { position: absolute; opacity: 0; width: 100%; height: 100%;
                 margin: 0; cursor: pointer; }
.ztoggle .track {
  position: absolute; inset: 0;
  border: var(--hairline) solid var(--color-line);
  border-radius: var(--radius-none);
  background: transparent;
  transition: border-color var(--zyme-duration-border) var(--zyme-easing);
  pointer-events: none;
}
.ztoggle .track::before {
  content: "";
  position: absolute;
  /* Absolute children resolve against the padding box, so 0 here is already
     1px inside the border. Outer 30 = 1 border + 14 knob + 14 travel + 1
     border, which is the only reading of v2 §11 where all three numbers
     hold at once. Vertically: inner 16, knob 14, so 1px top and bottom. */
  top: 1px; left: 0;
  width: var(--knob); height: var(--knob);
  background: var(--color-muted);
  border-radius: var(--radius-none);
  transition: transform var(--zyme-duration-border) var(--zyme-easing);
}
.ztoggle input:hover + .track,
.ztoggle input:focus-visible + .track { border-color: var(--color-text); }
.ztoggle input:checked + .track {
  background: var(--color-text);
  border-color: var(--color-text);
}
.ztoggle input:checked + .track::before {
  background: var(--color-bg);
  transform: translateX(var(--knob));   /* travel = 14px */
}


/* ==========================================================================
   CHECKBOX — amended (v2 §11)
   v1 drew the box at 100% in both states, which left it no hover state and
   made an unchecked box look active. Unchecked now rests at 22%.
   ========================================================================== */

.zcheck { display: inline-flex; align-items: center; gap: var(--spacing-lg);
          font-size: var(--text-sm); cursor: pointer; }
.zcheck input { position: absolute; opacity: 0; width: 0; height: 0; }
.zcheck .box {
  position: relative;
  width: var(--zyme-checkbox-size); height: var(--zyme-checkbox-size);
  flex: none;
  border: var(--hairline) solid var(--color-line);
  border-radius: var(--radius-none);
  transition: border-color var(--zyme-duration-border) var(--zyme-easing);
}
.zcheck:hover .box,
.zcheck input:focus-visible + .box { border-color: var(--color-text); }
.zcheck input:checked + .box,
.zcheck input:indeterminate + .box { border-color: var(--color-text); }
.zcheck input:checked + .box::after {
  content: ""; position: absolute; inset: 2px;
  background: var(--color-text);              /* 6×6 filled square */
}
.zcheck input:indeterminate + .box::after {
  content: ""; position: absolute;
  left: 2px; right: 2px; top: 4px; height: 2px;
  background: var(--color-text);              /* 6×2 bar */
}


/* ==========================================================================
   TAG (v2 §11)
   "A tag is a 10px tracked label in a 1px box — the group container at its
   smallest. It is never filled, so it never becomes a second inverted
   element."
   ========================================================================== */

.ztag {
  display: inline-block;
  padding: var(--spacing-2xs) 5px;
  border: var(--hairline) solid var(--color-line);
  border-radius: var(--radius-none);
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--color-muted);
}
.ztag--pending  { border-color: var(--color-line);  color: var(--color-muted); }
.ztag--saved    { border-color: var(--color-text);  color: var(--color-text); }
.ztag--rejected { border-color: var(--color-alarm); color: var(--color-alarm); }


/* ==========================================================================
   STATUS SQUARE + SENTENCE (§05 · v2 §10)
   The severity ladder: four-plus distinguishable states out of ONE hue, by
   varying which PART of the pair takes it. This is what replaces
   green/amber/red. "The square takes the hue, the sentence does not.
   Severity is a ladder, not a switch — and it costs the artwork half as much."
   ========================================================================== */

.zstatus { display: inline-flex; align-items: center; gap: var(--spacing-lg);
           font-size: var(--text-base); }
.zstatus .sq {
  width: var(--zyme-indicator-size); height: var(--zyme-indicator-size);
  flex: none;
  border-radius: var(--radius-none);
  background: var(--color-text);
}

/* error · destructive — square lime, sentence lime */
.zstatus--error   .sq { background: var(--color-alarm); }
.zstatus--error       { color: var(--color-alarm); }
/* warning — square lime, sentence full contrast */
.zstatus--warning .sq { background: var(--color-alarm); }
.zstatus--warning     { color: var(--color-text); }
/* ok · complete — square full, sentence full */
.zstatus--ok      .sq { background: var(--color-text); }
.zstatus--ok          { color: var(--color-text); }
/* running · active — square full, sentence muted */
.zstatus--running .sq { background: var(--color-text); }
.zstatus--running     { color: var(--color-muted); }
/* pending · info — square 40% ink, sentence muted */
.zstatus--pending .sq { background: rgba(233, 233, 227, 0.4); }
.zstatus--pending     { color: var(--color-muted); }
/* unknown · no data — hollow square, em dash */
.zstatus--unknown .sq { background: transparent;
                        border: var(--hairline) solid var(--color-line); }
.zstatus--unknown     { color: var(--color-muted); }

/* The status bar — 44px, bounded by one bottom line. No fill, no header
   treatment (DESIGN.md §11.10). */
.zstatusbar {
  height: var(--bar-h);
  display: flex; align-items: center;
  padding: 0 var(--spacing-2xl);
  border-bottom: var(--hairline) solid var(--color-line);
}


/* ==========================================================================
   PANEL / GROUP (§05)
   "No fill, no radius, no shadow — a group is a 1px outline with a tracked
   title." Panels are not interactive surfaces.
   ========================================================================== */

.zpanel { border: var(--hairline) solid var(--color-line);
          border-radius: var(--radius-none); background: var(--color-bg); }
.zpanel > .head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--spacing-md) var(--spacing-lg) var(--spacing-md) var(--spacing-xl);
  border-bottom: var(--hairline) solid var(--color-line);
  font-size: var(--text-sm);
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-title);
  color: var(--color-text);   /* full contrast: it names a live region */
}
.zpanel > .body { padding: var(--spacing-sm); }
.zpanel > .foot {
  min-height: 24px;
  padding: 5px var(--spacing-xl);
  border-top: var(--hairline) solid var(--color-line);
  font-size: var(--text-xs);
  color: var(--color-muted);
}

/* Group container — the smaller sibling. Groups are named after the engine
   concept they patch, not translated into product language (§07). */
.zgroup {
  border: var(--hairline) solid var(--color-line);
  border-radius: var(--radius-none);
  padding: var(--spacing-lg) var(--spacing-xl) var(--spacing-xl);
  display: flex; flex-direction: column; gap: var(--spacing-md);
}
.zgroup > .title {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--color-muted);
}

/* A module card is an ordinary group. Disabled is the whole card at .4 — no
   desaturation, no border change, no overlay. */
.zgroup.is-off { opacity: var(--zyme-disabled-opacity); }


/* ==========================================================================
   DATA TABLE (v2 §13)
   Zebra is banned — a second fill would make the drag-over wash meaningless.
   Row hover is a contrast lift, not a fill, "so §01 rule 06 survives contact
   with a table."
   ========================================================================== */

.ztable { width: 100%; border-collapse: collapse; }
.ztable th {
  text-align: left;
  padding: 0 var(--spacing-xl) var(--spacing-lg);
  border-bottom: var(--hairline) solid var(--color-line);
  font-size: var(--text-xs);
  font-weight: var(--font-weight-normal);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--color-muted);
}
.ztable td {
  padding: 9px var(--spacing-xl);
  border-bottom: var(--hairline) solid var(--color-line);
  font-size: var(--text-sm);
  color: var(--color-muted);
  transition: color var(--zyme-duration-border) var(--zyme-easing);
}
.ztable tbody tr:hover td { color: var(--color-text); }   /* contrast lift */
.ztable tbody tr.is-error td { color: var(--color-alarm); }
.ztable .num {
  text-align: right;
  font-family: var(--font-mono);
  font-variant-numeric: var(--zyme-numeric);
}
.ztable .lane-select { width: var(--zyme-row-select-lane); padding-right: 0; }


/* ==========================================================================
   THUMBNAIL GRID CELL (§05 · v2 §11)
   A comparison surface: equal cells, each carrying the one value that
   differs. Per-cell state is a word in the middle of the cell — there is no
   spinner in this system.
   ========================================================================== */

.zgrid { display: grid; gap: var(--spacing-lg);
         grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }

.zcell {
  border: var(--hairline) solid var(--color-line);
  border-radius: var(--radius-none);
  background: var(--color-bg);
  transition: border-color var(--zyme-duration-border) var(--zyme-easing);
}
.zcell:hover { border-color: var(--color-text); }

.zcell > .strip {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--spacing-md);
  padding: var(--spacing-2xs) 5px;
  border-bottom: var(--hairline) solid var(--color-line);
  font-size: var(--text-2xs);
  font-variant-numeric: var(--zyme-numeric);
  color: var(--color-muted);
  background: transparent;
}
.zcell > .body { display: block; width: 100%; }
.zcell > .body img { display: block; width: 100%; height: auto; }

/* Selected — the label strip inverts. It reads across a full grid at a
   glance, cannot be confused with hover (border) or drag-over (wash), and
   sits OUTSIDE the image bounds so it never marks the work itself. */
.zcell.is-selected > .strip { background: var(--color-text); color: var(--color-bg); }

/* Reached / running / done — the wash strip. Only ever here and on the
   dropzone. */
.zcell.is-active > .strip { background: var(--color-wash); color: var(--color-text); }

/* A strip sitting OVER imagery uses the 72% scrim, not the 8% wash (§02). */
.zcell > .strip.over-image { background: var(--color-scrim); color: var(--color-text); }

/* Per-cell state word, centred, 11px muted. Never a spinner. */
.zcell > .body .stateword {
  display: flex; align-items: center; justify-content: center;
  min-height: 96px;
  font-size: var(--text-xs);
  color: var(--color-muted);
}


/* ==========================================================================
   DROPZONE (§05)
   Dashed is the ONLY dashed border in the system. It means "drop target" and
   nothing else — never decorative.
   ========================================================================== */

.zdrop {
  display: flex; align-items: center; justify-content: center;
  padding: var(--spacing-4xl);
  border: var(--hairline) dashed transparent;   /* present but invisible, so
                                                   drag-over costs no reflow */
  border-radius: var(--radius-none);
  font-size: var(--text-sm);
  color: var(--color-muted);
  transition: border-color var(--zyme-duration-border) var(--zyme-easing),
              background-color var(--zyme-duration-border) var(--zyme-easing);
}
.zdrop.is-over { border-color: var(--color-text); background: var(--color-wash); }


/* ==========================================================================
   PROGRESS (§05 · v2 §13)
   "A 3px line welded to the bottom edge of the content it describes." No
   track is drawn — the remainder is bare ground.
   ========================================================================== */

.zprogress { position: relative; height: 3px; background: transparent;
             overflow: hidden; }
.zprogress > .fill {
  display: block; height: 100%; width: 0;
  background: var(--color-text);
  transition: width var(--zyme-duration-progress) var(--zyme-easing);
}
/* Indeterminate — a 22% segment traverses. Admitted as the third transition
   because the alternative is a spinner, which the system bans outright, and
   a server request genuinely has no knowable duration. */
.zprogress.is-indeterminate > .fill {
  width: 22%;
  animation: zyme-traverse var(--zyme-duration-indeterminate) var(--zyme-easing) infinite;
  transition: none;
}
@keyframes zyme-traverse {
  from { transform: translateX(-100%); }
  to   { transform: translateX(455%); }   /* 100/22 — exits fully at the right */
}
/* The one motion the doc flags as needing a reduced-motion ruling
   (OPEN-QUESTIONS Q7). Falling back to a static segment, not a spinner. */
@media (prefers-reduced-motion: reduce) {
  .zprogress.is-indeterminate > .fill { animation: none; width: 100%; opacity: .4; }
}


/* ==========================================================================
   NOTICE — the toast replacement (v2 §13)
   "Nothing floats in this system." A message takes a place in the layout at
   the top of the region it concerns, pushes that region down, and stays
   until dismissed or superseded. No timeout.
   ========================================================================== */

.znotice {
  display: flex; align-items: center; gap: var(--spacing-lg);
  padding: var(--spacing-xl) var(--spacing-2xl);
  border: var(--hairline) solid var(--color-line);
  border-radius: var(--radius-none);
  font-size: var(--text-sm);
  color: var(--color-text);
}
.znotice .sq {
  width: var(--zyme-indicator-size); height: var(--zyme-indicator-size);
  flex: none; background: var(--color-text);
}
.znotice .msg { flex: 1; }
.znotice--alarm { border-color: var(--color-alarm); color: var(--color-alarm); }
.znotice--alarm .sq { background: var(--color-alarm); }


/* ==========================================================================
   LOG / EVENT FEED (v2 §10)
   Three fixed lanes so the levels form a column you can scan without
   reading. The severity ladder, moved onto the level word.
   ========================================================================== */

.zfeed { display: flex; flex-direction: column; }
.zfeed .ev {
  display: grid;
  grid-template-columns: var(--zyme-lane-ts) var(--zyme-lane-level) 1fr;
  gap: var(--spacing-lg);
  padding: 7px var(--spacing-xl);
  border-bottom: var(--hairline) solid var(--color-line);
  font-size: var(--text-sm);
  color: var(--color-muted);
}
.zfeed .ts {
  font-family: var(--font-mono);
  font-variant-numeric: var(--zyme-numeric);
  color: var(--color-muted);
}
.zfeed .lvl {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--color-muted);
}
.zfeed .ev--warn  .lvl { color: var(--color-alarm); }
.zfeed .ev--warn  .msg { color: var(--color-text); }
.zfeed .ev--error .lvl { color: var(--color-alarm); }
.zfeed .ev--error .msg { color: var(--color-alarm); }


/* ==========================================================================
   MODAL + LIGHTBOX
   ========================================================================== */

.zscrim { position: fixed; inset: 0; background: var(--color-scrim);
          display: flex; align-items: center; justify-content: center; }

.zmodal {
  width: var(--overlay-w);
  border: var(--hairline) solid var(--color-line);
  border-radius: var(--radius-none);
  background: var(--color-bg);
  box-shadow: var(--zyme-shadow-none);
}
.zmodal > .head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--spacing-xl) var(--spacing-2xl);
  border-bottom: var(--hairline) solid var(--color-line);
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
}
.zmodal > .body { padding: var(--spacing-2xl);
                  display: flex; flex-direction: column; gap: var(--spacing-lg); }
.zclose {
  width: var(--zyme-overlay-close-w); height: var(--zyme-overlay-close-h);
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0;
  border: var(--hairline) solid var(--color-line);
  border-radius: var(--radius-none);
  background: none; color: var(--color-text);
  font-size: var(--text-xs); line-height: 1; cursor: pointer;
  transition: border-color var(--zyme-duration-border) var(--zyme-easing);
}
.zclose:hover { border-color: var(--color-text); }

/* Lightbox — "the only surface in the system with no box around the
   content." Every piece of chrome pushed OUTSIDE the image: no caption bar
   across it, no border on it, no controls floating over it. The 380px
   overlay width does not apply — a lightbox is sized by the work. */
.zlightbox { position: fixed; inset: 0; background: var(--color-scrim);
             display: flex; flex-direction: column;
             padding: var(--spacing-4xl); gap: var(--spacing-lg); }
.zlightbox > .chrome-top { display: flex; justify-content: flex-end; }
.zlightbox > .stage { flex: 1; min-height: 0;
                      display: flex; align-items: center; justify-content: center; }
.zlightbox > .stage img { max-width: 100%; max-height: 100%;
                          display: block; border: none; }
.zlightbox > .chrome-bottom {
  display: flex; align-items: center; justify-content: space-between;
  font-size: var(--text-xs); color: var(--color-muted);
}
.zlightbox > .chrome-bottom .counter {
  font-family: var(--font-mono); font-variant-numeric: var(--zyme-numeric);
}


/* ==========================================================================
   KEYBOARD KEY (§05)
   All chips share the 48px lane "so the descriptions form a column" — a
   fixed slot, not shrink-to-fit, even for one character. Labels are word
   case: Enter, R, S, Esc.
   ========================================================================== */

.zkbd {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--zyme-key-chip-w); height: var(--zyme-key-chip-h);
  flex: none;
  border: var(--hairline) solid var(--color-line);
  border-radius: var(--radius-none);
  font-family: inherit;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-key);
  color: var(--color-text);
}
.zkeyrow { display: flex; align-items: center; gap: var(--spacing-2xl); }
.zkeyrow + .zkeyrow { margin-top: var(--spacing-lg); }
.zkeyrow .desc { font-size: var(--text-sm); color: var(--color-muted); }


/* ==========================================================================
   EMPTY & LOADING (v2 §13)
   One muted sentence of plain prose, centred, in the region's own box. No
   illustration, no button, never a skeleton — "a shape pretending to be
   content it does not have."
   ========================================================================== */

.zempty {
  display: flex; align-items: center; justify-content: center;
  padding: var(--spacing-4xl);
  border: var(--hairline) solid var(--color-line);
  font-size: var(--text-sm);
  color: var(--color-muted);
}


/* ==========================================================================
   SHELL (§06 · amended v2 §14)
   Reading order is data flow: input → transformation → configuration.
   The shell does not scroll. ONE named region inside it may, where content
   is of genuinely unknowable length. Three such regions on one screen means
   the layout is wrong.
   ========================================================================== */

.zshell {
  display: grid;
  grid-template-columns: var(--rail-l) 1fr var(--rail-r);
  height: 100vh;
  overflow: hidden;
}
.zshell > * { min-width: 0; min-height: 0; }
.zscroll { overflow-y: auto; }   /* one per screen */

/* The centre stage is the only place with generous padding. Do not smooth
   this out — the dense stuff is dense, and the important stuff is not. */
.zstage { padding: var(--spacing-4xl); display: flex; flex-direction: column; }

/* Public pages only: a single fold at 720 where the three regions stack in
   reading order and controls go full width at the canonical 30px. No second
   design system, no second token set, one breakpoint. */
@media (max-width: 720px) {
  .zshell { grid-template-columns: 1fr; height: auto; overflow: visible; }
  .zstage { padding: var(--spacing-2xl); }
  .zbtn { width: 100%; }
}
