/* 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); }

.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; }

input[type="text"], input[type="email"], input[type="password"], input[type="search"],
input[type="number"], input[type="tel"], input[type="url"], 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); }
.check input { margin: 0; width: 24px; height: 24px; flex: none; accent-color: var(--accent); }
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; }

/* ── 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. */
.wordmark { 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)); }
.tiles > * { min-width: 0; }

/* ── 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 ────────────────────────────────────────────────────────────────── */
.stat { padding: var(--space-4); }
.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; }

/* ── 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); 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 ──────────────────────────────────────────────────────── */
/* 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; }
}
