/* ==========================================================================
   EVAL · components.css — the family's primitives. Documented in COMPONENTS.md.
   Owner: engine agent. Twelve feature agents draw with these so every window
   looks like the same product. If you need one that is not here, say so in
   your report; do not invent a parallel one in a feature file.
   ========================================================================== */

/* ---------- the tap lift (law §3: 2px down over 60ms, back over 180ms) ----
   EVAL.motion.lift() adds .lifted, which sets transform and overrides only the
   DURATION on the way down. A control whose own rule declares a transition
   without `transform` in it never animates: it snaps 2px and back. So the
   family declares the transform transition once, for every control, and any
   rule that sets its own transition list must keep `transform` in it.
   Measured by tests/suites/93-motion.cjs across every page and curtain. */
button, [role="button"] { transition: transform var(--dur-liftUp) var(--ease); }

/* ---------- card -------------------------------------------------------- */
.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 14px 16px;
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* a card is a button on half the surfaces in the app (the Gross lanes, the
     Vitals and Appointments cards): the lift belongs to the primitive */
  transition: transform var(--dur-liftUp) var(--ease);
}
.card.flush { padding: 0; overflow: hidden; }
.card.soft { background: var(--surf); border-color: var(--line2); box-shadow: none; }
.card.panel { background: var(--panel); border-color: var(--panelLine); border-radius: var(--r-lg); box-shadow: var(--shadowUp); padding: 18px; }

/* ---------- card head (title + optional pill) --------------------------- */
.card-head { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; min-height: 28px; }
.card-head h2 {
  font-family: var(--font-head); font-size: var(--fs-h2); font-weight: 800;
  letter-spacing: var(--ls-head); color: var(--ink);
}
.card-head .sp { flex: 1; }

/* ---------- section head (inside a curtain) ----------------------------- */
.sec-head { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; margin-top: 16px; }
.sec-head:first-child { margin-top: 0; }
.sec-head h3 {
  font-family: var(--font-head); font-size: var(--fs-label); font-weight: 800;
  letter-spacing: .4px; color: var(--muted); text-transform: uppercase;
}
.sec-head .ln { flex: 1; height: 1px; background: var(--line); }

/* ---------- pill (status is always a pill, never colored text) ---------- */
.pill {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: var(--fs-pill); font-weight: 700; line-height: 1;
  padding: 5px 11px; border-radius: var(--r-pill);
  background: var(--plainBg); color: var(--plainInk); white-space: nowrap;
  transition: background-color var(--dur-pill) var(--ease), color var(--dur-pill) var(--ease);
}
.pill.good { background: var(--goodBg); color: var(--goodInk); }
.pill.warn { background: var(--warnBg); color: var(--warnInk); }
.pill.bad { background: var(--badBg); color: var(--badInk); }
.pill.lav { background: var(--lav); color: var(--purInk); }
.pill.peach { background: var(--peachBg); color: var(--peachInk); }
.pill.solid { background: var(--grad); color: var(--onGrad); }
.pill.lg { font-size: var(--fs-label); padding: 7px 14px; }
/* a 13px glyph in a 12px line made some pills 23px and their neighbours 22px,
   which reads as a mistake wherever two sit side by side: the glyph tucks back
   into the text's own box so every pill in a row is exactly one height */
.pill > svg { margin: -1px 0; }

/* A pill that IS a control. Chrome and buttons must not look the same, so a
   tappable pill carries the one affordance the laws give a tappable thing —
   the disclosure chevron — plus the 44px hit box that goes with it. Tone,
   type and height are the static pill's, so the family still reads as one.
   Built only by EVAL.dom.pill(text, tone, {onTap}); never hand-rolled. */
.pill.tapin {
  position: relative; padding-right: 7px; gap: 4px;
  transition: transform var(--dur-liftUp) var(--ease),
              background-color var(--dur-pill) var(--ease),
              color var(--dur-pill) var(--ease);
}
/* the 13px glyph is a pixel taller than the 12px line beside it, and a pill
   that is 23px next to a 22px one reads as a mistake: pull it back into the
   text's own box so chrome and control sit on the same baseline and height */
.pill.tapin > .cv { display: flex; flex: 0 0 auto; opacity: .78; margin: -1px 0; }
.pill.tapin::after { content: ''; position: absolute; inset: -11px; }

/* ---------- chip (a tappable question / shortcut row) ------------------- */
.chip {
  display: flex; align-items: center; gap: 11px; width: 100%;
  height: 50px; padding: 0 13px; border-radius: 15px; text-align: left;
  background: var(--card); border: 1px solid var(--line); box-shadow: var(--shadow);
  transition: transform var(--dur-liftUp) var(--ease);
}
.chip .ic {
  width: 28px; height: 28px; border-radius: 9px; flex: 0 0 auto; color: var(--pur);
  background: var(--lav); display: flex; align-items: center; justify-content: center;
}
.chip .lb {
  flex: 1; min-width: 0; font-size: var(--fs-row); font-weight: 600; color: var(--ink2);
  letter-spacing: -.1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.chip .cv { flex: 0 0 auto; display: flex; color: var(--muted2); opacity: .8; }

/* ---------- buttons ----------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  height: 44px; padding: 0 20px; border-radius: var(--r-pill);
  font-family: var(--font-head); font-size: var(--fs-h3); font-weight: 700;
  letter-spacing: -.2px; white-space: nowrap;
  transition: transform var(--dur-liftUp) var(--ease);
}
.btn-primary { background: var(--grad); color: var(--onGrad); box-shadow: var(--glow); }
.btn-ghost { background: var(--surf2); border: 1px solid var(--line); color: var(--ink2); }
.btn.wide { width: 100%; }
.btn.round { width: 44px; height: 44px; padding: 0; }

/* ---------- avatar ------------------------------------------------------ */
.avatar {
  width: 26px; height: 26px; border-radius: var(--r-pill); flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-head); font-size: var(--fs-pill); font-weight: 800;
  letter-spacing: -.2px; color: var(--onGrad); background: var(--grad);
}
.avatar.lg { width: 38px; height: 38px; font-size: var(--fs-row); }
/* a role the store holds (Desk, Recon, Lot / photos) — not a person, not a
   missing one: the glyph says "whoever is on this station today" */
.avatar.role { background: var(--lav); color: var(--purInk); }
.avatar.ghost { background: var(--ghost); color: var(--muted); }

/* ---------- the assistant's mark ----------------------------------------
   The gradient squircle that heads the assistant wherever it appears — the
   Home panel, the phone greeting strip, the Ask page. It was declared twice,
   at two sizes, in two feature files; one shape, one place. */
.gmark {
  width: 38px; height: 38px; border-radius: var(--r-mark); flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  background: var(--grad); color: var(--onGrad); box-shadow: var(--glow);
}
@media (max-width: 999px) { .gmark { width: 34px; height: 34px; } }

/* ---------- rank chip --------------------------------------------------- */
.rank {
  width: 30px; height: 30px; border-radius: var(--r-sm); flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-head); font-size: var(--fs-row); font-weight: 800;
  background: var(--lav); color: var(--purInk);
}
.rank.warn { background: var(--warnBg); color: var(--warnInk); }
.rank.bad { background: var(--badBg); color: var(--badInk); }
.rank.good { background: var(--goodBg); color: var(--goodInk); }

/* ---------- grade chip (letter + count) --------------------------------- */
.gradechip {
  display: inline-flex; align-items: center; gap: 5px;
  background: var(--surf2); border-radius: 9px; padding: 4px 9px;
}
.gradechip b { font-family: var(--font-head); font-size: var(--fs-h3); font-weight: 800; }
.gradechip span { font-size: var(--fs-sub); font-weight: 600; color: var(--muted); }
.gradechip.good b { color: var(--goodInk); }
.gradechip.lav b { color: var(--purInk); }
.gradechip.warn b { color: var(--warnInk); }
.gradechip.bad b { color: var(--badInk); }

/* ---------- bar (with an optional target tick) -------------------------- */
.bar {
  position: relative; height: 8px; border-radius: var(--r-pill);
  background: var(--line); overflow: hidden; width: 100%;
}
.bar > i {
  position: absolute; left: 0; top: 0; bottom: 0; width: 0;
  border-radius: var(--r-pill); background: var(--gradR);
}
.bar > i.good { background: var(--green); }
.bar > i.warn { background: var(--amber); }
.bar > i.bad { background: var(--red); }
.bar > i.warm { background: linear-gradient(90deg, var(--amber), var(--peach)); }
/* the target tick. It has to read on three backgrounds — the empty track, a
   purple fill and a status fill — in both themes, so it is ink at low alpha
   (darkens in light, lightens in dark) rather than a card-colored gap. */
.bar > u {
  position: absolute; top: -1px; bottom: -1px; width: 2.5px; border-radius: 2px;
  background: var(--ink); opacity: .34;
}
html[data-theme="dark"] .bar > u { opacity: .52; }
.bar.tall { height: 12px; }
.bar.thin { height: 6px; }

/* ---------- ring -------------------------------------------------------- */
.ring { position: relative; flex: 0 0 auto; }
.ring svg { transform: rotate(-90deg); }
.ring .ring-track { stroke: var(--line); fill: none; }
/* An instrument at zero — no objective typed, nothing verified yet — used to
   draw a complete, evenly grey circle: the shape COMPONENTS.md §2 calls a
   loading spinner, and in a product whose promise is that the feeds landed an
   honest nothing must never read as "still loading". At 0% the track opens
   into dashes, so the ring says "at rest" instead of "turning", and the pill
   beside it carries the story ("No objective"). One no-data vocabulary: the
   designed empty() for a block, the dashed ring for a zeroed instrument. */
.ring:has(.ring-arc[data-sweep="0"]) .ring-track,
.ring.zero .ring-track { stroke: var(--line3); stroke-dasharray: 9 9; }
.ring .ring-arc { fill: none; stroke: var(--pur); stroke-linecap: round; }
.ring .ring-arc.good { stroke: var(--green); }
.ring .ring-arc.warn { stroke: var(--amber); }
.ring .ring-arc.bad { stroke: var(--red); }
/* The pace mark on a ring is the bar's target tick, bent round the dial: one
   crisp perpendicular notch that CROSSES the whole stroke, in ink at low alpha
   so it darkens on a light track and lightens on a dark one and still reads on
   a purple, green, amber or red arc. It was a grey lozenge at .92, which floated
   beside the dial as a stray hyphen on the two rings whose arc had not reached
   it and vanished into the green on the one that had. Same token, same alphas
   and same job as `.bar > u`, so the instrument reads the same everywhere. */
.ring .ring-mark { stroke: var(--ink); stroke-width: 3; stroke-linecap: butt; opacity: .34; }
html[data-theme="dark"] .ring .ring-mark { opacity: .52; }
.ring .ring-num {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-family: var(--font-head); font-size: var(--fs-d1); font-weight: 800; letter-spacing: -1px;
  color: var(--ink);
}

/* ---------- tile (one big figure + its label) --------------------------- */
.tile {
  background: var(--surf); border: 1px solid var(--line2); border-radius: var(--r-md);
  padding: 12px 13px; display: flex; flex-direction: column; gap: 3px; min-width: 0;
}
.tile b {
  font-family: var(--font-head); font-size: var(--fs-d2); font-weight: 800;
  letter-spacing: -1px; line-height: var(--lh-tight); color: var(--ink);
}
.tile span { font-size: var(--fs-sub); font-weight: 500; color: var(--muted); line-height: 1.3; }
/* A tile's figure is always --ink. Law §5: status is a pill, never coloured
   text — and the same figure may not be red on the Store page and black in the
   curtain it opens. The Gross curtain is the family's model: the money prints
   in ink and the tone rides the pill beside it, so a tile that has to carry a
   verdict carries a pill (see .tile > .pill below), and the four tones live
   only on the pill, the bar, the ring and the rank chip.
   tests/suites/92-status.cjs measures every figure on screen for this. */
.tile > .pill { align-self: flex-start; }

/* ---------- kv row ------------------------------------------------------ */
.kv { display: flex; align-items: center; gap: 12px; min-height: 36px; }
.kv .k { flex: 1; min-width: 0; font-size: var(--fs-row); font-weight: 500; color: var(--muted); }
.kv .v { font-family: var(--font-head); font-size: var(--fs-h3); font-weight: 800; color: var(--ink); white-space: nowrap; }
.kv + .kv { border-top: 1px solid var(--line2); }

/* ---------- list + row -------------------------------------------------- */
.list { display: flex; flex-direction: column; min-height: 0; }
.list > .row + .row { border-top: 1px solid var(--line2); }
/* the done row settles to 55% 400ms after the tick (law §2). Opacity is one of
   the two properties the motion law lets us animate, and a 45-point step in one
   frame reads as a glitch, so the settle is a settle. */
.row {
  display: flex; align-items: center; gap: 11px; width: 100%; text-align: left;
  min-height: 52px; padding: 8px 0; min-width: 0;
  transition: transform var(--dur-liftUp) var(--ease),
              opacity var(--dur-liftUp) var(--ease);
}
.row .body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.row .t1 { display: flex; align-items: center; gap: 9px; min-width: 0; }
.row .t1 .tt {
  flex: 1; min-width: 0; font-family: var(--font-head); font-size: var(--fs-h3);
  font-weight: 700; letter-spacing: -.25px; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.row .t2 { display: flex; align-items: center; gap: 9px; min-width: 0; }
.row .t2 .det {
  flex: 1; min-width: 0; font-size: var(--fs-sub); font-weight: 450; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.row .who { font-size: var(--fs-sub); font-weight: 600; color: var(--ink2); white-space: nowrap; }
.row.done { opacity: .55; }

/* ---------- done box ---------------------------------------------------- */
.donebox {
  width: 44px; height: 44px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  transition: transform var(--dur-liftUp) var(--ease);
}
/* the circle is a fixed 40px disc, never a flex item that can be squeezed:
   flex-shrink here is what turned it into a 32x40 capsule on every task row */
/* Ticking a task is the one thing the GM does in here every morning, so the
   unchecked state has to be found at arm's length: a 2px --wellLine ring on the
   quiet surface reads as a WELL — something with a bottom, waiting to be
   filled. At 1.8px of --line3 it measured 1.2:1 against the card behind it and
   read as an avatar slot or a rendering artefact, and in dark it all but
   vanished. The filled state is unchanged. */
.donebox > i {
  width: 40px; height: 40px; flex: 0 0 auto; min-width: 40px;
  border-radius: var(--r-pill);
  border: 2px solid var(--wellLine); background: var(--surf2);
  display: flex; align-items: center; justify-content: center;
  transition: background-color 200ms var(--ease), border-color 200ms var(--ease), transform var(--dur-liftUp) var(--ease);
}
.donebox > i svg { opacity: 0; transform: scale(.7); transition: opacity 160ms var(--ease), transform 200ms var(--ease); color: var(--onGrad); }
.donebox.on > i { background: var(--green); border-color: var(--green); }
.donebox.on > i svg { opacity: 1; transform: none; }

/* ---------- input pill (the ask bar) ------------------------------------ */
.inputpill {
  display: flex; align-items: center; gap: 10px; height: 58px; padding: 0 8px 0 18px;
  border-radius: var(--r-pill); background: var(--card);
  border: 1.5px solid var(--line3); box-shadow: var(--shadowUp); flex: 0 0 auto;
}
.inputpill input, .inputpill .ph {
  flex: 1; min-width: 0; font-size: var(--fs-h3); font-weight: 500;
  color: var(--ink); background: none; border: 0; outline: none;
}
/* a bare input measures ~19px tall and fails the 44px law, so the primitive
   carries the height instead of every window working around it.
   A question longer than the pill (a tapped chip lands its text here) ends in an
   ellipsis rather than being sliced mid-letter against the mic. */
.inputpill input { height: 46px; text-overflow: ellipsis; }
.inputpill .ph { color: var(--muted2); }
.inputpill input::placeholder { color: var(--muted2); }
.inputpill .mic {
  width: 44px; height: 44px; border-radius: var(--r-pill); flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center; color: var(--muted);
  background: var(--surf2); border: 1px solid var(--line2);
  transition: transform var(--dur-liftUp) var(--ease);
}
.inputpill .send {
  width: 44px; height: 44px; border-radius: var(--r-pill); flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  background: var(--grad); color: var(--onGrad); box-shadow: var(--glow);
  transition: transform var(--dur-liftUp) var(--ease);
}
/* On a 393px phone the slot was 225px and the one sentence it has to hold —
   "Ask Val anything about your store", the place the GM meets Val by name and
   the first thing he reads on Home — measured 229 and ellipsed to "…your sto…".
   The pill gives those four pixels back out of its own padding and gaps rather
   than shrinking the type, which would put the family's smallest sentence under
   the 11px floor. The two hit boxes do not move. */
@media (max-width: 420px) {
  .inputpill { gap: 8px; padding-left: 15px; }
}

/* ---------- summary card (the collapsed rail) --------------------------- */
.sum {
  display: flex; flex-direction: column; justify-content: space-between; gap: 6px;
  min-height: 72px; min-width: 0; text-align: left; padding: 10px 13px 11px;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r-md);
  box-shadow: var(--shadow); transition: transform var(--dur-liftUp) var(--ease);
}
.sum .s1 { display: flex; align-items: center; gap: 8px; }
.sum .s1 b {
  flex: 1; min-width: 0; font-size: var(--fs-label); font-weight: 700; color: var(--muted);
  letter-spacing: -.1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sum .s1 .cv { flex: 0 0 auto; display: flex; color: var(--muted2); opacity: .7; }
.sum .s2 { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.sum .s2 strong {
  font-family: var(--font-head); font-size: var(--fs-d3); font-weight: 800;
  letter-spacing: -.7px; line-height: 1; color: var(--ink);
}
.sum .s2 em {
  font-style: normal; font-size: var(--fs-sub); font-weight: 500; color: var(--muted);
  min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sum .s2 .pill { margin-left: auto; align-self: center; }

/* ---------- honest empty state ------------------------------------------ */
.empty {
  display: flex; align-items: center; gap: 13px; padding: 12px 14px;
  border-radius: var(--r-md); border: 1px solid var(--line2);
  background: linear-gradient(100deg, var(--peachBg), var(--lav2));
}
.empty .ic {
  width: 38px; height: 38px; border-radius: var(--r-pill); flex: 0 0 auto;
  background: var(--card); border: 1px solid var(--line); color: var(--amber);
  display: flex; align-items: center; justify-content: center;
}
.empty .tx { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
.empty .tx b {
  font-family: var(--font-head); font-size: var(--fs-h2); font-weight: 800;
  letter-spacing: var(--ls-head); color: var(--ink);
}

/* ---------- helpers ----------------------------------------------------- */
.hair { height: 1px; background: var(--line); }
.sp { flex: 1; }
.grow { flex: 1; min-height: 8px; }
.lifted { transform: translateY(2px); transition-duration: var(--dur-liftDown); }
/* A figure inside prose — the greeting, an answer, a task's why — is set in the
   brand ink, one weight for all of them. It never takes a status colour: a red
   numeral inside a sentence is a verdict told in colour alone (law §5) and it
   reads as an error in the text. The verdict is a pill. */
.num { font-family: var(--font-head); font-weight: 800; letter-spacing: -.1px; color: var(--purInk); }

/* ---------- a list that scrolls inside a card ---------------------------
   It says so by DISSOLVING its last row, never by slicing one through the
   middle of its letters and never by a caption. EVAL.dom.moreFade(el) sets the
   flag while there really is more below and drops it the moment the GM reaches
   the end, so nothing he has already read is dimmed. The mask reads alpha, so
   the token in it is doing the job of opaque, not the job of a colour. */
/* The mask has a CLEAR zone, not just a ramp — the shape the assistant panel's
   own foot fade already uses (home.css, and 12-home-fit.cjs holds it there).
   One straight 42px ramp meant whatever sat at the container's foot — a
   "Carried to #10 ›" pill, a done circle, a "1 task" report chip — was drawn
   WHOLE at around a third of its ink, and a complete control at a third of its
   ink does not read as "there is more below": it reads as disabled, and EVAL
   has no disabled state to tell it apart from. Now the deepest 16px is gone
   outright and the 32px above it ramps, so a control at the edge is a trace of
   its own top rather than a ghost of all of it. */
[data-more="1"] {
  -webkit-mask-image: linear-gradient(to bottom, var(--ink) calc(100% - 48px), transparent calc(100% - 16px));
  mask-image: linear-gradient(to bottom, var(--ink) calc(100% - 48px), transparent calc(100% - 16px));
}

/* The same rule for the OTHER edge. A scroller that opens away from the top — the
   delegate directory and the owner picker both centre the current pick on open —
   sliced its first visible row through the letters with nothing saying the list
   continued upward. Mirror of the above, and the two combine when the GM is in
   the middle of a long list. */
/* A list that ends on a WHOLE row (EVAL.dom.fitRows) says "there is more" with a soft
   bottom edge, not with the 48px ramp above — that ramp exists to dissolve a row cut
   through its letters, and over a row that is whole it erases the row. */
[data-fit="1"][data-more="1"] {
  -webkit-mask-image: linear-gradient(to bottom, var(--ink) calc(100% - 16px), transparent 100%);
  mask-image: linear-gradient(to bottom, var(--ink) calc(100% - 16px), transparent 100%);
}
[data-fit="1"][data-more-top="1"][data-more="1"] {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, var(--ink) 16px, var(--ink) calc(100% - 16px), transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, var(--ink) 16px, var(--ink) calc(100% - 16px), transparent 100%);
}

[data-more-top="1"] {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, var(--ink) 20px);
  mask-image: linear-gradient(to bottom, transparent 0, var(--ink) 20px);
}

[data-more-top="1"][data-more="1"] {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, var(--ink) 20px, var(--ink) calc(100% - 48px), transparent calc(100% - 16px));
  mask-image: linear-gradient(to bottom, transparent 0, var(--ink) 20px, var(--ink) calc(100% - 48px), transparent calc(100% - 16px));
}
