/* design-system: page-library v1 | components.css */
/* The page library's components, each with every state.

THE RULE
A component reads tier 3 tokens, falls back to tier 2, and never reads tier 1 or a literal.
If a value is missing, it goes in tokens.css and says so; it does not appear here.

STATES ARE PART OF THE COMPONENT
Default, hover, active, focus-visible, disabled, and where it applies loading and invalid.
Writing a state on the page instead of here is how two buttons end up with two focus rings.
*/

/* ── gradient text ────────────────────────────────────────────────────────── */
/* One phrase in a headline, filled with the theme's ramp. It is a component because the Safari
   fallback is two declarations that are wrong to repeat by hand: without the fill colour the text
   renders transparent-but-painted, which looks like a missing glyph. */
.grad-text {
  background: var(--accent-grad-text, var(--accent));
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}

/* ── button ──────────────────────────────────────────────────────────────── */
.btn, .navbtn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-1);
  min-height: var(--control-h); padding: 0 var(--btn-pad);
  border: var(--border) solid var(--btn-line);
  background: var(--btn-bg); color: var(--ink);
  border-radius: var(--btn-radius);
  font: var(--btn-weight) var(--btn-fs)/1 var(--font-sans);
  text-decoration: none; white-space: nowrap; cursor: pointer;
}
.btn:hover, .navbtn:hover { background: var(--btn-bg-hover); text-decoration: none; }
.btn:active, .navbtn:active { transform: translateY(.5px); }
.btn:disabled, .btn[aria-disabled="true"] {
  opacity: .5; cursor: not-allowed; transform: none;
}
.btn:disabled:hover { background: var(--btn-bg); }

.btn.primary {
  background: var(--accent); border-color: var(--accent);
  color: var(--accent-ink); box-shadow: var(--shadow);
}
.btn.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.btn.primary:disabled { background: var(--accent); }

.btn.gradient {
  background: var(--accent-grad, var(--accent));
  border-color: transparent; color: var(--accent-ink); box-shadow: var(--shadow);
}
.btn.gradient:hover { background: var(--accent-grad, var(--accent)); filter: brightness(1.08); }

.btn.ghost { background: transparent; border-color: transparent; color: var(--ink2); }
.btn.ghost:hover { background: var(--sunken); }

.btn.danger { color: var(--alert); border-color: var(--alert-line); background: var(--bg); }
.btn.danger:hover { background: var(--alert-soft); }

/* The ask-size rung (R6, his grade 2026-10-25: "better rounded corners on the buttons"). A 48px
   control carried the small-control radius, so the hero's own primary read boxier than the screen it
   replaced. Radius is a property of CONTROL SIZE here, not of the button component: a console button
   stays at --radius-sm because a 36px box with a 14px corner is a pill, and a 48px ask takes
   --radius-lg so it reads as one object rather than a small button scaled up. Size, not decoration:
   the label also takes the reading step, because 13.5px on a 48px control looks undersized. */
.btn.lg {
  min-height: 48px; padding: 0 var(--space-5);
  border-radius: var(--radius-lg);
  font-size: var(--fs-base);
}

.navbtn.bare { background: transparent; border-color: transparent; color: var(--ink2); }
.navbtn.bare:hover { background: var(--sunken); }

.btn.sm, .navbtn.sm { min-height: var(--control-h-sm); padding: 0 var(--space-2); font-size: 12.5px; }
.btn.block, .navbtn.block { width: 100%; }

/* Loading: the label stays, a spinner replaces the icon, and the width does not jump. The
   button itself carries a slow opacity pulse, so "still working" is legible even where the
   pseudo-element spinner is not painted (and so the reduced-motion contract has something to
   keep alive, instead of freezing the only signal that the app is not hung). */
.btn[aria-busy="true"] { pointer-events: none; animation: busy 1.4s ease-in-out infinite; }
@keyframes busy { 50% { opacity: .74; } }
.btn[aria-busy="true"]::before {
  content: ""; width: 10px; height: 10px; border-radius: 50%;
  border: 2px solid currentColor; border-right-color: transparent;
  animation: spin 700ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ── field ───────────────────────────────────────────────────────────────── */
.field { display: flex; flex-direction: column; gap: var(--space-1); }
/* an inline field sits in a filter bar: the control sizes to its content, the label sits above */
.field.inline select, .field.inline input { min-height: var(--space-6); width: auto; }
.field label { font: 500 var(--fs-sm)/1.3 var(--font-sans); color: var(--ink2); }
/* a bare hint outside a field is still a hint: muted, never ink (R4: the shadcn treatment) */
.hint { font-size: 12px; color: var(--muted); }
.field .hint { font-size: 12px; color: var(--muted); }
.field .error { font-size: 12px; color: var(--alert); display: flex; gap: var(--space-1); align-items: center; }

/* EVERY INPUT THAT TAKES TEXT OR A DATE. `type="date"` was missing from this list, so a date control was the one
   field on a form that took no height, no padding, no border and no font from the sheet: measured 2026-10-28 on a
   consumer's live form at 1280, a 283x21 native widget beside 36px siblings, which the rendered gate reports as a
   WCAG 2.5.8 target-size failure at every width. `time`, `datetime-local`, `month` and `week` are the same control
   family and were missing with it, so they are closed here in one line rather than one per report. A file, range,
   colour or checkbox input is NOT in the family: each paints its own control and takes its height elsewhere. */
input[type="text"], input[type="email"], input[type="password"], input[type="search"],
input[type="number"], input[type="tel"], input[type="url"],
input[type="date"], input[type="time"], input[type="datetime-local"], input[type="month"], input[type="week"],
select, textarea {
  width: 100%;
  min-height: var(--control-h); padding: var(--space-2) var(--space-3);
  border: var(--border) solid var(--field-line);
  border-radius: var(--field-radius);
  background: var(--field-bg); color: var(--ink);
  font: var(--fs-base)/1.4 var(--font-sans);
}
textarea { min-height: 96px; resize: vertical; }
/* the ring from base.css is the focus affordance; a text field also tints its border so
   the caret's context is obvious. No `outline: none` here, because the ring IS the outline. */
input:focus-visible, select:focus-visible, textarea:focus-visible {
  border-color: var(--accent);
}
input:disabled, select:disabled, textarea:disabled {
  background: var(--sunken); color: var(--muted); cursor: not-allowed;
}
input[readonly] { background: var(--sunken); }
input[aria-invalid="true"], select[aria-invalid="true"], textarea[aria-invalid="true"] {
  border-color: var(--alert); box-shadow: 0 0 0 3px var(--alert-soft);
}

/* a checkbox is a control: the target floor applies to it, so it is 24px, not 13 */
.check { display: flex; gap: var(--space-2); align-items: flex-start; font-size: var(--fs-sm); color: var(--ink2); }
/* a checkbox is a touch target wherever it sits: the 24px floor is global, not per-context */
input[type="checkbox"], input[type="radio"] { margin: 0; width: 24px; height: 24px; min-width: 24px; accent-color: var(--accent); }
.check input { margin: 0; flex: none; }
td input[type="checkbox"], th input[type="checkbox"], input[type="radio"] {
  width: 24px; height: 24px; min-width: 24px; flex: none;
  accent-color: var(--accent); margin: 0;
}

/* ── card ────────────────────────────────────────────────────────────────── */
.card {
  background: var(--card-bg);
  border: var(--border) solid var(--card-line);
  border-radius: var(--card-radius);
  box-shadow: var(--shadow);
  min-width: 0;
}
.card-pad { padding: var(--card-pad); }

/* ── badge ───────────────────────────────────────────────────────────────── */
.badge {
  display: inline-flex; align-items: center; gap: var(--space-1);
  height: var(--badge-h); padding: 0 var(--space-2);
  border-radius: var(--badge-radius);
  font: 500 var(--badge-fs)/1 var(--font-sans);
  background: var(--sunken); color: var(--ink2);
  border: var(--border) solid var(--line);
}
.badge.ok     { background: var(--ok-soft);    color: var(--ok);    border-color: var(--ok-line); }
.badge.warn   { background: var(--warn-soft);  color: var(--warn);  border-color: var(--warn-line); }
.badge.alert  { background: var(--alert-soft); color: var(--alert); border-color: var(--alert-line); }
.badge.accent { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-soft); }
.dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; display: inline-block; }

/* A FEATURE chip, and a second component rather than a taller badge (R6, his grade 2026-10-25: the
   hand-built screen's chips "have better spacing"). A badge is a STATUS: 22px because it sits in a
   table row or a panel head beside text that sets its rhythm. A hero's feature chips are read at a
   glance and need air, so this one takes its height from the badge rung plus one space step, and its
   colours are the NEUTRAL pair: a feature is not a state, and --ok/--warn/--alert mean state only. */
.pill {
  display: inline-flex; align-items: center;
  height: calc(var(--badge-h) + var(--space-2));
  padding: 0 var(--space-3);
  border-radius: var(--radius-pill);
  font: 500 var(--fs-sm)/1 var(--font-sans);
  background: var(--sunken); color: var(--ink2);
  border: var(--border) solid var(--line);
}

/* ── table wrapper ───────────────────────────────────────────────────────── */
/* The class was named in the docs, in the component notes and in the prompt that generates pages,
   and it was never implemented. A generated dashboard wrapped its table in `.table-wrap` and the
   document still scrolled sideways at 390px, because a wrapper that does not clip scrolls with the
   page. This is the rule the name promised. */
.table-wrap { overflow-x: auto; max-inline-size: 100%; -webkit-overflow-scrolling: touch; }

/* ── panel row ───────────────────────────────────────────────────────────── */
/* A card's header and footer are the same object: a padded band with one rule. Writing the
   padding on the page seven times is how two cards end up with two header heights. */
.panelrow {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-4) var(--space-4);
  border-bottom: var(--border) solid var(--line);
}
.panelrow.foot { border-bottom: 0; border-top: var(--border) solid var(--line); }

/* A list row inside a card: icon, text, trailing value, one rule under it. */
.lrow {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--border) solid var(--line);
}
.lrow:last-child { border-bottom: 0; }
.lrow.on { background: var(--accent-soft); }

/* A row separated by a rule ABOVE it, for stacked prose blocks that are not in a card. */
.rowline { padding: var(--space-4) 0; border-top: var(--border) solid var(--line); }

/* ── icon tile ───────────────────────────────────────────────────────────── */
/* The 30px accent square used for a feature's icon. It is a container, so it holds the icon
   at 16px and never a glyph from the text face. */
.ico-tile {
  width: 30px; height: 30px; border-radius: var(--radius);
  background: var(--accent-soft); color: var(--accent);
  display: flex; align-items: center; justify-content: center;
  margin-bottom: var(--space-2);
}

/* ── tab ─────────────────────────────────────────────────────────────────── */
/* One item of a segmented control or a settings rail. aria-current is the only selected
   state; a class called .active would be a second way to say the same thing. */
.tab {
  display: inline-flex; align-items: center;
  min-height: var(--space-6); padding: 0 var(--space-2);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm); color: var(--ink2);
  background: transparent; border: var(--border) solid transparent;
  text-decoration: none;
}
.tab:hover { background: var(--sunken); text-decoration: none; }
.tab[aria-current="page"], .tab[aria-selected="true"] {
  background: var(--raised); border-color: var(--line);
  color: var(--ink); font-weight: 500; box-shadow: var(--shadow);
}

/* ── wordmark ────────────────────────────────────────────────────────────── */
/* A customer's logo drawn as its name, because a library cannot ship their trademark. */
/* A wordmark is a LINK, so it is a target: 44x14 measured on an emitted page that put it in a nav with
   nothing giving it height. min-block-size makes it legal wherever it sits, rather than asking every page
   to know which container supplies its box. */
.wordmark {
  display: inline-flex; align-items: center;
  min-block-size: var(--space-6);
  font: 600 var(--fs-base)/1 var(--font-sans);
  color: var(--muted); letter-spacing: -.01em;
}

/* ── plan feature list ───────────────────────────────────────────────────── */
/* A priced plan's feature list. The tick is a text character on purpose: an icon font would be
   a second typeface and a second failure mode. */
.plan-desc { margin-top: var(--space-2); min-height: 38px; }
/* A priced plan is a column whose last child is the ask. The ask sits on the floor of the card,
   so three plans with three different feature counts still put their buttons on one line. */
.plan { display: flex; flex-direction: column; }
.plan > .btn.block, .plan > .navbtn.block, .plan > .btn { margin-top: auto; }
.plan > .btn:not(.block), .plan > .navbtn:not(.block) { align-self: flex-start; }
.strong { font-weight: 500; }
.planlist { margin: var(--space-4) 0 0; padding: 0; list-style: none; display: grid; gap: var(--space-2); font-size: var(--fs-sm); }
.planlist li { display: flex; gap: var(--space-2); }
.planlist .tick { color: var(--ok); }

/* ── display number ──────────────────────────────────────────────────────── */
/* A single number used as the subject of a section, not as body copy. */
.display { font: 600 var(--fs-display)/1 var(--font-sans); letter-spacing: var(--tracking-hero); }

/* ── table cells that are controls ───────────────────────────────────────── */
/* A checkbox in a row is a control with a label, so the cell centres it and the hit area is
   the cell, not the 16px box the browser draws. */
.check-cell { display: flex; align-items: center; gap: var(--space-2); }
.check-cell input[type="checkbox"] { accent-color: var(--accent); }

/* ── quote and citation ──────────────────────────────────────────────────── */
/* The quoted figure, and the line that says where it came from. A quote is not a card: it is the
   thing the page exists to deliver, so it gets a rule on the reading edge and nothing else. */
.quote {
  margin: var(--space-4) 0 0;
  padding: var(--space-1) 0 var(--space-1) var(--space-4);
  border-left: 3px solid var(--accent);
  max-inline-size: var(--measure);
}
.cite { display: block; margin-top: var(--space-2); font-size: var(--fs-sm); color: var(--muted); }

/* ── term list ───────────────────────────────────────────────────────────── */
/* A list of named things, each with a paragraph. Not a table: there is nothing to compare across
   columns, and a table would invent an alignment the content does not have. */
.terms { display: grid; gap: var(--space-4); margin: var(--space-4) 0 0; padding: 0; list-style: none; }
.terms > li { display: grid; gap: var(--space-1); }
/* `.label`, not `.term`: `.term` is the terminal component, and a class that means two things is
   the collision this system exists to remove. It rendered the article's terms as black consoles. */
.terms .label { font-weight: 600; }

/* ── meter ───────────────────────────────────────────────────────────────── */
/* A proportion you can see instead of read. The track is the sunken surface, the fill is the
   accent, and neither carries a value: the number beside it does. */
.meter { height: 7px; background: var(--sunken); border-radius: var(--radius-pill); overflow: hidden; }
.meter > i { display: block; height: 100%; background: var(--accent); border-radius: inherit; }

/* ── skeleton ────────────────────────────────────────────────────────────── */
.sk { background: var(--sunken); border-radius: var(--radius-sm); height: 12px; }
.sk.line  { height: 10px; }
.sk.title { height: 26px; }

/* ── avatar, kbd ─────────────────────────────────────────────────────────── */
.avatar {
  width: 32px; height: 32px; border-radius: 50%;
  background: var(--sunken); border: var(--border) solid var(--line);
  display: inline-flex; align-items: center; justify-content: center;
  font: 600 var(--fs-label)/1 var(--font-sans); color: var(--ink2);
}
.avatar.sm { width: 26px; height: 26px; font-size: var(--fs-micro); }
.avatar.xs { width: 22px; height: 22px; font-size: var(--fs-micro); }
/* .on is the avatar in a completed step: the mark carries the state, so the label does not */
.avatar.on { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
/* three step states, and they must not be told apart by hue alone: done is filled with a tick,
   current is filled and ringed, upcoming is outlined. */
.avatar.current { box-shadow: 0 0 0 3px var(--accent-soft); }
.avatar.lg { width: 56px; height: 56px; font-size: var(--fs-h3); }
.kbd {
  display: inline-flex; align-items: center; min-height: 20px; padding: 0 var(--space-1);
  border: var(--border) solid var(--line2); border-bottom-width: 2px;
  border-radius: var(--radius-sm); font: 500 var(--fs-micro)/1 var(--font-mono);
  color: var(--muted); background: var(--bg);
}

/* ── tiles ───────────────────────────────────────────────────────────────── */
/* R3 verdict 2026-10-22: shadcn's card tiles, graded. A stat grid that auto-fits and never
   squeezes below a 150px tile; the R2-graded dashboard stance stays the default. */
.tiles { display: grid; gap: var(--gap-card); grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }

/* ── cover ────────────────────────────────────────────────────────────────────
   A playlist, an album, a file, a project: the thing a person picks from a shelf needs a shape to look at.
   The page supplies the glyph inside; this supplies the square, the tint and the radius. Without it a list
   of media has nothing to be a grid OF, which is why the playlists page came back as a table. */
.cover {
  aspect-ratio: 1;
  border-radius: var(--radius);
  background: var(--accent-soft);
  color: var(--accent);
  display: grid;
  place-items: center;
  overflow: hidden;
}
.cover .icon { inline-size: 38%; block-size: 38%; stroke-width: 1.25; }
.cover.sm { inline-size: var(--space-7); aspect-ratio: 1; }
.tiles > * { min-width: 0; }

/* ── rail ────────────────────────────────────────────────────────────────────────
   The settings surface's nav: a column of icon and label rows with small-cap group labels. It is the app
   shell's `.side` without the shell, so a page that is not a full-height app can still carry the same nav
   language. Shape copied from the desktop app's settings overlay (13rem rail beside its pane); the width is
   the page's, since this is a card in a column rather than a grid track. */
.rail {
  display: flex; flex-direction: column; gap: var(--space-1);
  padding: var(--space-2);
  background: var(--raised); border: var(--border) solid var(--line); border-radius: var(--radius);
}
.rail .sect {
  font: 600 var(--fs-micro)/1 var(--font-sans); color: var(--muted);
  padding: var(--space-3) var(--space-2) var(--space-1); letter-spacing: .02em;
}
.rail a {
  display: flex; align-items: center; gap: var(--space-2);
  min-height: 32px; padding: 0 var(--space-2);
  border-radius: var(--radius-sm); color: var(--ink2); font-size: 13.5px;
  text-decoration: none;
}
.rail a:hover { background: var(--sunken); }
.rail a[aria-current="page"] {
  background: var(--raised); color: var(--ink); font-weight: 500;
  box-shadow: var(--shadow); border: var(--border) solid var(--line);
}
.rail .icon { color: var(--muted); }

/* ── shell ───────────────────────────────────────────────────────────────────────
   A rail and the pane beside it, at the desktop app's settings proportions (13rem = 208px, the rail beside
   its pane), collapsing to one column at the width where a rail stops being a rail. `.app` is this shape with
   a full-height window around it; a page that is not a window needs the shape without the height. */
.shell { display: grid; grid-template-columns: 208px minmax(0, 1fr); gap: var(--space-4); align-items: start; }
@media (max-width: 47.5rem) { .shell { grid-template-columns: minmax(0, 1fr); } }

/* ── thumb ───────────────────────────────────────────────────────────────────────
   A miniature of an arrangement, the way the desktop app's layout picker draws one: rows of live zones as
   filled blocks, and a zone the arrangement leaves empty as a dashed outline rather than a fainter block.
   Colour is derived from the ink, so it is legible on every theme without leaning on the accent. */
.thumb {
  display: flex; flex-direction: column; gap: var(--space-1);
  height: 44px; padding: var(--space-1);
  border: var(--border) solid var(--line); border-radius: var(--radius-sm); background: var(--sunken);
}
.thumb-row { display: flex; gap: var(--space-1); flex: 1; min-height: 0; }
.thumb-cell { flex: 1; min-width: 0; border-radius: var(--radius-sm); background: color-mix(in srgb, var(--ink) 16%, transparent); }
.thumb-cell.rest { background: transparent; border: 1px dashed color-mix(in srgb, var(--ink) 30%, transparent); }

/* ── empty ───────────────────────────────────────────────────────────────── */
/* R2 verdict 2026-10-22: loading defaults to skeletons (.sk); active work is a .busy line that
   names what is running; a loaded-and-idle screen is this pattern, with exactly one action. */
.empty { display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: var(--space-2); padding: var(--space-6) var(--space-4); }
.empty .label { justify-content: center; }
.empty .hint { max-width: 38ch; margin: 0; }
.empty .btn { margin-top: var(--space-2); }

/* ── stat ────────────────────────────────────────────────────────────────── */
/* A stat is a CARD, not a padded column. Measured on two generated dashboards side by side: the same four
   numbers read as a wireframe when they sat directly on the page background and as an instrument panel
   when each sat on its own raised surface with a hairline edge. */
.stat {
  padding: var(--space-4);
  background: var(--raised);
  border: var(--border) solid var(--line);
  border-radius: var(--radius);
}
.stat .k { font-size: 12.5px; color: var(--muted); }
.stat .v {
  font: 600 26px/1.2 var(--font-sans); letter-spacing: var(--tracking-tight);
  margin-top: var(--space-1); font-variant-numeric: tabular-nums;
}
.stat .d { font-size: 12.5px; margin-top: var(--space-1); display: flex; gap: var(--space-1); align-items: center; }
/* A delta says which way the number moved, so it carries the colour of the news. Measured on a generated
   dashboard: "+6 since 09:00" and "Up 8 ms" both rendered in the muted grey, which is a number that refuses
   to say whether it is good. */
.stat .d.ok { color: var(--ok); }
.stat .d.warn { color: var(--warn); }
.stat .d.alert { color: var(--alert); }

/* ── sparkline ─────────────────────────────────────────────────────────────────
   The one thing the generated pages were missing that the eye reads as quality: a number beside its own
   recent shape. The page writes one inline SVG per stat, in a 100x24 viewBox, and this rule supplies the
   box, the weight and the colour, so four of them in a row read as one instrument instead of four
   drawings. Direction is a variant (ok, warn, alert) rather than a literal, because a rising drift and a
   rising listener count are opposite news in the same colour otherwise. */
.spark {
  display: block;
  inline-size: 100%;
  block-size: var(--space-6);
  margin-block-start: var(--space-2);
  overflow: visible;
}
/* One colour drives the line AND its fill, so a variant is one word on the element. The fill is the line's
   own colour at 12% rather than a soft token, because the soft tokens are surfaces and a 12% wash of the
   stroke is what reads as depth under a 1.5px line instead of a second panel. */
.spark { color: var(--accent); }
.spark polyline { fill: none; stroke: currentColor; stroke-width: 1.5; vector-effect: non-scaling-stroke;
  stroke-linecap: round; stroke-linejoin: round; }
.spark .spark-area { fill: currentColor; opacity: .12; stroke: none; }
.spark.ok { color: var(--ok); }
.spark.warn { color: var(--warn); }
.spark.alert { color: var(--alert); }

/* ── table ───────────────────────────────────────────────────────────────── */
table { width: 100%; border-collapse: collapse; font-size: var(--table-fs); }
th {
  text-align: left; font: 600 var(--table-head-fs)/1 var(--font-sans);
  color: var(--ink2); padding: var(--space-2) var(--space-3);
  background: var(--sunken); border-bottom: var(--border) solid var(--line); white-space: nowrap;
}
td { padding: var(--table-cell-pad); border-bottom: var(--border) solid var(--line); vertical-align: middle; }
tbody tr:hover { background: var(--sunken); }
tbody tr:last-child td { border-bottom: 0; }
td.num, th.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-family: var(--font-mono); font-size: 12.5px;
}

/* ── brand mark ──────────────────────────────────────────────────────────── */
.brand { display: flex; align-items: center; gap: var(--space-2); font-weight: 600; font-size: 14.5px; }
.brand .mark {
  width: 24px; height: 24px; border-radius: var(--radius-sm);
  background: var(--accent-grad, var(--accent)); color: var(--accent-ink);
  display: flex; align-items: center; justify-content: center;
  font: 700 var(--fs-label)/1 var(--font-sans);
}

/* ── empty state ─────────────────────────────────────────────────────────── */
.empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; padding: var(--space-7) var(--space-5);
  border: var(--border) dashed var(--line2);
  border-radius: var(--radius); background: var(--sunken);
}
.empty .ico {
  width: 44px; height: 44px; border-radius: var(--space-3);
  background: var(--bg); border: var(--border) solid var(--line);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--fs-h3); margin-bottom: var(--space-3);
}

/* ── terminal ────────────────────────────────────────────────────────────── */
.term {
  background: var(--term-bg); color: var(--term-ink);
  border-radius: var(--radius); border: var(--border) solid var(--term-line);
  overflow: hidden; font: 12.5px/1.65 var(--font-mono);
  box-shadow: var(--shadow-lg);
}
.term .bar {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) var(--space-3); background: var(--term-bar);
  border-bottom: var(--border) solid var(--term-line2);
  font-size: 11.5px; color: var(--term-dim);
}
.term .bar .lights { display: flex; gap: var(--space-1); }
.term .bar i { width: 9px; height: 9px; border-radius: 50%; background: var(--term-lamp); display: block; }
.term pre { margin: 0; padding: var(--space-4) var(--space-4) var(--space-5); overflow-x: auto; white-space: pre; }
.term .p { color: var(--term-ok); } .term .c { color: var(--term-dim); }
.term .y { color: var(--term-warn); } .term .r { color: var(--term-alert); } .term .b { color: var(--term-info); }
.caret { display: inline-block; width: 7px; height: 14px; background: var(--term-ink); vertical-align: -2px; animation: blink 1.1s steps(1) infinite; }
@keyframes blink { 50% { opacity: 0; } }

/* ── command palette ─────────────────────────────────────────────────────── */
.palette {
  width: 100%; max-width: 560px; background: var(--raised);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-modal); overflow: hidden;
}
.palette .in { display: flex; gap: var(--space-2); align-items: center; padding: var(--space-3) var(--space-4); border-bottom: var(--border) solid var(--line); color: var(--muted); }
.palette .in input { border: 0; box-shadow: none; padding: 0; min-height: 24px; font-size: var(--fs-base); }
.palette .grp { font: 600 var(--fs-micro)/1 var(--font-sans); color: var(--muted); padding: var(--space-3) var(--space-4) var(--space-1); }
.palette .it { display: flex; gap: var(--space-2); align-items: center; padding: var(--space-2) var(--space-4); font-size: 13.5px; }
.palette .it[data-on] { background: var(--accent-soft); }
.palette .it .k { margin-left: auto; }

/* ── reduced motion ──────────────────────────────────────────────────────── */
/* ── emphasis and icons ──────────────────────────────────────────────────────────────
   Two additions, and both because the system could be correct and still read as empty: a generated
   page had classes for everything and used four of them.

   .band is the one emphasis surface a page gets, for whatever the page is actually about. It is the
   soft accent tint with a solid accent edge, NOT the accent gradient, because text on a gradient
   cannot be measured. The gradient's lightest stop decides legibility and it is not a colour the
   contract can name, so a band built on it would carry a declaration that cannot fail. Text here is
   --ink, which measures 13.7:1 to 16.1:1 on --accent-soft in all five themes; --accent on the same
   tint is only 2.94:1 in tape and 3.76:1 in amp, which is why no accent-coloured text sits in a band. */
.band {
  position: relative;
  overflow: hidden;
  padding: var(--space-5) var(--space-5) var(--space-5) calc(var(--space-5) + var(--space-2));
  border: var(--border) solid var(--line);
  border-radius: var(--radius-box);
  background: var(--accent-soft);
  color: var(--ink);
}
.band::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: calc(var(--space-1) / 2);
  background: var(--accent);
}
.band .band-title { font-size: var(--fs-h3); line-height: 1.25; font-weight: 650; }
.band .band-note { color: var(--ink2); font-size: var(--fs-sm); }

/* .icon is every inline SVG a page carries: one em box, the text's own colour, one stroke weight.
   Rule 2 already asks for inline SVG with currentColor, and that alone still lets a row of eight
   icons read as eight different drawings. This is the shared weight and box that stops it. */
.icon {
  inline-size: 1em;
  block-size: 1em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
  vertical-align: -0.125em;
}
.icon.lg { inline-size: 1.5em; block-size: 1.5em; }
.icon.sm { inline-size: 0.875em; block-size: 0.875em; }

/* Authored as the exception, not the default: motion is written normally and removed here,
   so a new animation is covered by default instead of being forgotten. The caret is the one
   thing that must still read as "live", so it holds at full opacity rather than freezing. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: var(--dur-0) !important;
    animation-iteration-count: 1 !important;
    transition-duration: var(--dur-0) !important;
    scroll-behavior: auto !important;
  }
  .caret { animation: none; opacity: 1; }
  /* the one exception, and it needs !important to beat the universal rule above: a
     loading indicator that freezes reads as a hung app, which is worse than motion */
  .btn[aria-busy="true"] { animation: busy 1.6s ease-in-out infinite !important; }
  .btn[aria-busy="true"]::before { animation: spin 1.6s linear infinite !important; }
}

/* THE ESTATE ROW: the door and the pages a stranger can open, on every page.
   Why it exists: measured 2026-10-26, /answers/ carried zero links to a sibling app and zero links to /. Four
   public pages, each an island, with the only map on the front page. This is the map, in the header, on all of
   them. It wraps, because five links beside a brand do not fit 390px. */
.estate { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-3); }
/* 24px floor, because the render gate measured these at 19px tall and a link that small is a miss on a phone.
   The padding is horizontal only: the row already spaces its items vertically. */
.estate a {
  display: inline-flex; align-items: center; min-block-size: 24px; padding-inline: var(--space-1);
  font-size: var(--fs-sm); color: var(--muted); text-decoration: none;
  /* Measured at 390px: without this, "Sign in" and two-word labels split across lines and the header reads as
     noise. A link is a unit; the ROW wraps, the link never does. */
  white-space: nowrap;
}
.estate a:hover { color: var(--ink); text-decoration: none; }
/* The current page is marked by weight and an underline as well as colour: measured on a 390px screenshot, the
   colour alone was indistinguishable from its siblings, which makes "you are here" a claim the eye cannot check. */
.estate a[aria-current="page"] { color: var(--ink); font-weight: 600; text-decoration: underline; text-underline-offset: 3px; }

/* ── ask ─────────────────────────────────────────────────────────────────────
   THE ONE INPUT A PAGE IS ABOUT, as against a field in a form. Full width, 64px, set at reading size,
   with the page's primary control beside it and nothing else on the same level.

   Measured on the launcher (2026-10-27): a 36px search field beside three other 36px controls reads as
   one more control, and a page made of one more control has no subject. The same page with this bar is
   a launcher, and the difference was the height and the type size, not the position. */
.ask { display: flex; gap: var(--space-3); align-items: stretch; }
.ask input {
  flex: 1; min-width: 0; min-height: var(--space-8); padding: 0 var(--space-5);
  font: 400 var(--fs-h3)/1.2 var(--font-sans); color: var(--ink);
  background: var(--raised); border: var(--border) solid var(--line2);
  border-radius: var(--radius-box); box-shadow: var(--shadow-lg);
}
.ask input::placeholder { color: color-mix(in srgb, var(--ink) 58%, transparent); }
.ask .btn,
.ask .navbtn { min-height: var(--space-8); padding: 0 var(--space-6); font-size: var(--fs-prose); }

/* A SECTION WHOSE SUMMARY IS A ROW. `.section` is the disclosure a catalogue is built from: the summary
   is a flex row (label, count, chevron), and a flex row's height is its tallest line, which measured
   22px here against the 24 a control owes. The floor is set once, on the shape, rather than padded onto
   each summary by the page. */
.section > summary { min-height: var(--space-5); align-items: center; }



/* ── the estate bar ───────────────────────────────────────────────────────────────────────────────────────
   ADOPTED from `zpe-nav/public/zpe-bar.css` on 2026-10-28, by the owner's call that the bar belongs to the design
   system rather than to a worker's private taste. This block is that file's chrome, translated to this system's
   roles and measures: every `--zpe-*` reads a token, and the bar's own numbers (52px, 11px at 0.2em, the 68rem
   line, the reserve) live in tokens.css as `--bar-*`. The class names are the ones `zpe-bar.js` emits, because the
   component was adopted and not renamed: no node in the estate changes, and there is one copy of these rules.

   OUT OF SCOPE, and this list is what makes the entry admissible: the registry panel, its search, its groups, its
   counts and its lifecycle marks stay in `zpe-nav/public/zpe-bar.css`. That is an app's data surface, not a nav
   component. What this block draws is the bar: its box, its row, its items, the account corner, the fade variant
   the apex uses, the empty host's reserve, the no-script row, and the phone rule that keeps one row. */
.zpe-nav {
  /* The bar's own family, not the page's. Measured on /vault on 2026-10-28: a page whose body is a mono stack
   * inherited through to the items, so the one bar read as monospace on one surface and sans on the rest. The
   * estate's family is the bar's; a page keeps its own type below. */
  font-family: var(--font-sans);
  position: sticky;
  top: 0;
  z-index: 60;
  display: flex;
  /* NOWRAP, measured on /pool 2026-10-28. A page whose container is narrower than the bar's own line pushed the
   * account corner onto a second row and the bar rendered 100px tall instead of 52 while still looking plausible.
   * A bar that can wrap cannot be adopted by an app screen, and every one of those is narrower than the front door. */
  flex-wrap: nowrap;
  align-items: center;
  gap: 0.5rem 1.5rem;
  /* Full-bleed, because that is what the estate's own bars already are, and measured on a 68rem page the row's
     ends sat at the window edge while the content column stopped 20rem in. The padding holds the row to a
     68rem line on a wide screen and falls back to the gutter when the window is narrower than that. */
  padding: 0.85rem max(var(--bar-gutter), (100% - 68rem) / 2);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-2) ease;
}

/* The hairline is for a bar over prose. A page whose own chrome fades into a starfield gets none: the apex's
 * header has no border in any state, and a 1px line over the field is the seam he named on 2026-10-28. */
.zpe-nav-stick[data-stuck='true'],
.zpe-nav-stick.zpe-nav-open {
  border-bottom-color: var(--line);
}

body .zpe-nav .zpe-nav-mark {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  color: var(--ink);
  border: 0;
  text-decoration: none;
}

.zpe-nav > *,
.zpe-nav-items > * {
  flex-shrink: 0;
  white-space: nowrap;
}

.zpe-nav-items {
  display: flex;
  /* One story only: the row cannot wrap anywhere. Below 34rem the section links leave the bar and reappear in the
   * panel's sections, which is why the bar is a single line at every width. The breakpoint moved from 30rem on
   * 2026-10-28: measured at 500px with a 24-character account name, the corner was squeezed to 24px of ellipsis
   * because the four links were still in the row. At 34rem the links leave first and the name keeps its room. */
  flex-wrap: nowrap;
  align-items: center;
  gap: 0.25rem 1.35rem;
  font-size: var(--bar-fs);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
}

body .zpe-nav .zpe-nav-item {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 24px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
}

body .zpe-nav .zpe-nav-item:hover {
  color: var(--ink);
}

body .zpe-nav .zpe-nav-item[aria-current='page'] {
  color: var(--ink);
  font-weight: 500;
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 1px;
}

.zpe-nav-caret {
  width: 9px;
  height: 6px;
  transition: transform var(--dur-2) ease;
}

.zpe-nav-apps[aria-expanded='true'] .zpe-nav-caret {
  transform: rotate(180deg);
}

.zpe-nav-account {
  /* The corner is the only child allowed to give ground. Measured 2026-10-28 by a cold review with a 24-character
   * name: with every child at flex-shrink 0 the bar's content ran 613px wide in a 500px window, so the account
   * affordance sat entirely off-screen in the narrow containers the nowrap rule exists for. It truncates instead. */
  flex-shrink: 1;
  min-width: 0;
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 1rem;
  min-height: 24px;
  font-size: var(--bar-fs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}

/* A 24px floor on the link itself, not just on the row around it: measured by another repo's render gate on
 * 2026-10-28, the corner link computed 49x16 (an inline anchor inside a 24px flex row), so the only account
 * affordance in the bar was a 16px tap target on every surface that adopted it. */
body .zpe-nav .zpe-nav-account a {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  border: 0;
  color: inherit;
  text-decoration: none;
}

body .zpe-nav .zpe-nav-account a:hover,
body .zpe-nav .zpe-nav-account a.zpe-nav-you {
  color: var(--ink);
}

/* ── the fade variant: for a page whose own header waits for the hero to go ─────────────────────
   The apex is that page. Measured 2026-10-28: `opacity: 0` at scrollY 0 and 400, `opacity: 1` at 900 on a 900px
   viewport, on a 500ms transition with a 4px rise. So the mark is there from the first pixel (the page has its own
   hero type; it does not need a wordmark competing with it), the items arrive on the same 500ms and 4px, and the
   bar only takes a ground once it is over content, because a strip of blur over a starfield is a seam. */
/* The wake line: an invisible marker at 0.8 of the viewport, observed rather than measured on every scroll. */
.zpe-nav-line {
  position: absolute;
  top: 80vh;
  left: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
  opacity: 0;
}

.zpe-nav-fade {
  position: fixed;
  inset-inline: 0;
  top: 0;
  background: transparent;
  border-bottom-color: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  transition: background 500ms ease, border-color 500ms ease;
}

.zpe-nav-fade .zpe-nav-items,
.zpe-nav-fade .zpe-nav-account {
  opacity: 0;
  transform: translateY(-4px);
  pointer-events: none;
  transition: opacity 500ms ease, transform 500ms ease;
}

/* Awake, the fade bar stays a piece of the sky: transparent, unblurred, no rule. A wash and a blur were here
 * first and they are the same seam in a softer form, which on the apex is a band across the stars. What arrives
 * is the items and the account corner, and nothing else. */
.zpe-nav-fade[data-awake='true'],
.zpe-nav-fade.zpe-nav-open {
  background: transparent;
  border-bottom-color: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.zpe-nav-fade[data-awake='true'] .zpe-nav-items,
.zpe-nav-fade[data-awake='true'] .zpe-nav-account,
.zpe-nav-fade.zpe-nav-open .zpe-nav-items,
.zpe-nav-fade.zpe-nav-open .zpe-nav-account {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

/* THE PLACEHOLDER'S HEIGHT, BEFORE THE SCRIPT LANDS. The bar is filled after parse, so a page that reserved
   nothing jumped by the bar's own height when it arrived. Two adopters worked this out independently and carried
   the rule page-local (/services/ and, through zpe-kit's frame, /privacy/remove), which is the drift this sheet
   exists to end: a reserve is a property of the bar, not of each page that hosts it. Measured 2026-10-28 at 1440:
   3.4rem against a 52px bar, so the content below moves by 2px rather than 52. */
[data-zpe-bar] { min-height: 3.4rem; }

/* NO JAVASCRIPT, and no bar: a page that carries the estate row inside <noscript> still gets a usable one.
   Measured 2026-10-28 with the script blocked: /blog kept 54px of reserved emptiness and its only navigation was
   the footer, and /services/ had none at all. The row below is styled by this same sheet, so a page must link it
   in its own head rather than let the script inject it. */
.zpe-noscript {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem 1.2rem;
  min-height: 3.4rem;
  padding: 0.6rem var(--bar-gutter);
  border-bottom: 1px solid var(--line);
  font-family: var(--font-sans);
  font-size: var(--bar-fs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

body .zpe-noscript a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  color: var(--muted);
  border: 0;
  text-decoration: none;
}

body .zpe-noscript a:hover {
  color: var(--ink);
}

/* The four section links, shown inside the panel only where the bar hides them. */
.zpe-nav-sections {
  display: none;
  flex-wrap: wrap;
  gap: 0.4rem 1.2rem;
  margin: 0 0 1.1rem;
  padding: 0 0 0.9rem;
  border-bottom: 1px solid var(--line);
  font-size: var(--bar-fs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.zpe-nav-sections a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  color: var(--muted);
  border: 0;
  text-decoration: none;
}

.zpe-nav-sections a:hover,
.zpe-nav-sections a[aria-current='page'] {
  color: var(--ink);
}

@media (max-width: 34rem) {
  .zpe-nav-sections {
    display: flex;
  }
}


/* A phone keeps one row: a mark, Apps and the account. Measured 2026-10-28 at 390px, five items beside the
   mark and Sign in wrapped the bar to 133px, which is 16% of an 844px screen held sticky over every scroll. The
   four links move into the panel, so nothing is lost, and the panel is one press away. The line moved from 30rem
   to 34rem on 2026-10-28: at 500px the links were still in the bar and squeezed the account corner to 24px. */
@media (max-width: 34rem) {
  .zpe-nav .zpe-nav-item:not(.zpe-nav-apps) {
    display: none;
  }
}

@media (max-width: 34rem) {
  .zpe-nav {
    gap: 0.35rem 1rem;
    padding-block: var(--space-2);
  }
  .zpe-nav-account {
    margin-left: 0;
  }
  .zpe-nav-groups {
    grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  }
}

