/* =========================================================================
   Primitives.

   Every screen composes from these and nothing else. A screen that needs a
   shape not in this file needs a new primitive here with a reason, not a
   styled div at the call site.

   The audit found five separate treatments of "primary action" living on
   adjacent screens. That is not five decisions, it is one decision made five
   times by whoever was editing that file. Collapsing them is the point.

   Loads after tokens.css and before navis.css.
   ========================================================================= */


/* -------------------------------------------------------------------------
   Button

   Three variants. Not four, and not a fifth that appears on one screen.

   The one rule that carries the most weight: --accent appears at most once
   per screen as a filled element, and it marks the next action. Hierarchy is
   made of contrast, and there is no contrast left if everything is already
   at maximum.
   ------------------------------------------------------------------------- */

.btn {
  /* 44px is the smallest reliably tappable target, and an ADHD product is
     used one-handed while walking more than most. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--r-control);
  border: none;
  background: none;
  font-family: inherit;
  font-size: var(--fs-3);
  line-height: 1.2;
  cursor: pointer;
  text-align: center;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--dur-fast) var(--ease),
              opacity var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}

.btn:disabled { opacity: 0.45; cursor: default; }
.btn:not(:disabled):active { transform: scale(0.98); }

/* Full width is for empty states and modals, where the button is the only
   thing to do. Inline buttons hug their label; a full-width button in a row
   of controls claims a priority it does not have. */
.btn--block { display: flex; width: 100%; }

.btn--primary {
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: var(--fw-hi);
}
.btn--primary:not(:disabled):active { background: var(--accent-deep); }

.btn--secondary {
  background: var(--surface-2);
  color: var(--ink);
  font-weight: var(--fw-med);
  /* No border. A fill and a border are two ways of saying the same thing,
     and using both is what makes a button look heavier than its rank. */
}
.btn--secondary:not(:disabled):active { background: var(--line-soft); }

.btn--tertiary {
  background: none;
  color: var(--ink-soft);
  font-weight: var(--fw-med);
  padding-left: var(--space-2);
  padding-right: var(--space-2);
}
.btn--tertiary:not(:disabled):active { color: var(--ink); }

/* Destructive. Rare, and never the primary on a screen. */
.btn--danger { background: none; color: var(--danger); font-weight: var(--fw-med); }

/* An icon in a button is a static meaningful icon or it is absent. A
   decorative glyph next to a label is noise the eye has to discard. */
.btn svg { width: 18px; height: 18px; flex-shrink: 0; }


/* -------------------------------------------------------------------------
   Card
   ------------------------------------------------------------------------- */

.card {
  background: var(--surface);
  border-radius: var(--r-card);
  padding: var(--space-4);
  /* No border and no shadow. Surfaces separate by value. A card that is both
     lighter than the page and outlined and lifted is saying the same thing
     three times. */
  border: none;
  box-shadow: none;
  /* Never a fixed height: content decides, and a card sized to its tallest
     possible state is mostly empty most of the time. */
}

/* Body text is left aligned. Centre alignment is permitted in a standalone
   empty state and nowhere else, because centred prose has no consistent left
   edge for the eye to return to on each line. */
.card p, .card .card-body { text-align: left; }


/* -------------------------------------------------------------------------
   ListRow

   Two lines: what it is, then what is true about it.
   ------------------------------------------------------------------------- */

.row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 44px;
  padding: var(--space-3) 0;
  text-align: left;
  background: none;
  border: none;
  font-family: inherit;
}
.row + .row { border-top: 1px solid var(--line-soft); }

.row-body { flex: 1; min-width: 0; }
.row-title { font-size: var(--fs-3); font-weight: var(--fw-med); color: var(--ink); line-height: 1.35; }
.row-meta  { font-size: var(--fs-2); font-weight: var(--fw-body); color: var(--ink-mute); line-height: 1.45; margin-top: 2px; }

/* Destructive controls are visible, and reversible.
 *
 * These were hidden behind a long press so a thumb could not trip them. The
 * reveal keyed off `.row`, and no delete button in the app is inside a `.row`
 * -- they sit in .taskrow, .pd-* and friends -- so nothing was ever
 * removable. Undiscoverable and broken is worse than visible and risky.
 *
 * Visible but quiet: it reads as metadata until you approach it, then it
 * declares itself. The mis-tap is answered by undo rather than by hiding, so
 * the control stays where a person expects it. */
.row-destroy {
  opacity: 1;
  pointer-events: auto;
  color: var(--ink-mute);
  flex-shrink: 0;
  width: 32px; height: 32px;
  display: inline-grid; place-items: center;
  border-radius: var(--r-control);
  transition: color var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease);
}
.row-destroy svg { width: 15px; height: 15px; }
@media (hover: hover) and (pointer: fine) {
  .row-destroy:hover { color: var(--danger); background: var(--bad-wash); }
}
.row-destroy:active { color: var(--danger); }
.row-destroy:focus-visible { box-shadow: var(--focus-ring-bad); outline: none; }

/* The undo bar. Six seconds, above the tab bar, out of the way of the thumb
   that just pressed delete. */
.undobar {
  position: fixed; left: 50%; transform: translate(-50%, 120%);
  bottom: calc(var(--nav-h) + max(var(--space-3), var(--safe-bottom)) + var(--space-2));
  z-index: var(--z-sheet);
  display: flex; align-items: center; gap: var(--space-3);
  max-width: min(440px, calc(100vw - var(--space-5) * 2));
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
  background: var(--surface-solid);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow-3);
  opacity: 0;
  transition: transform var(--dur-base) var(--ease-out-quint),
              opacity var(--dur-base) var(--ease-out-quint);
}
.undobar.show { transform: translate(-50%, 0); opacity: 1; }
.undo-msg { font-size: var(--fs-2); color: var(--ink-soft); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
.undo-go { min-height: 36px; padding: 0 var(--space-3); color: var(--accent); font-weight: var(--fw-hi); }
@media (prefers-reduced-motion: reduce) {
  .undobar { transition: opacity var(--dur-base) linear; transform: translate(-50%, 0); }
}


/* -------------------------------------------------------------------------
   Chip
   ------------------------------------------------------------------------- */

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  height: 32px;
  padding: 0 var(--space-3);
  border-radius: var(--r-pill);
  background: var(--surface-2);
  color: var(--ink-soft);
  font-family: inherit;
  font-size: var(--fs-2);
  font-weight: var(--fw-med);
  border: none;
  white-space: nowrap;
}
.chip--on { background: var(--accent-wash); color: var(--accent); }
.chip--sys { background: var(--tide-wash); color: var(--tide); }


/* -------------------------------------------------------------------------
   EmptyState

   One line, one action. If a concept needs a paragraph to be usable, the
   concept is wrong, not the copy.
   ------------------------------------------------------------------------- */

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-4);
  padding: var(--space-7) var(--space-4);
}
.empty-ic { color: var(--ink-mute); }
.empty-ic svg { width: 28px; height: 28px; }
.empty-line {
  font-size: var(--fs-3);
  font-weight: var(--fw-body);
  color: var(--ink-soft);
  line-height: 1.5;
  max-width: 30ch;
}
.empty .btn { max-width: 280px; }


/* -------------------------------------------------------------------------
   ChallengeCard and BoardRow

   Shells only, styled from the base tokens like everything else. No
   referenced here, so these render correctly on the untuned surface first
   and gain the theme later rather than depending on it.
   ------------------------------------------------------------------------- */

.challenge {
  background: var(--surface);
  border-radius: var(--r-card);
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.challenge-title { font-size: var(--fs-3); font-weight: var(--fw-med); color: var(--ink); }
.challenge-counts { font-size: var(--fs-2); color: var(--ink-mute); line-height: 1.45; }
.challenge-days  { font-size: var(--fs-1); font-weight: var(--fw-med); color: var(--ink-mute);
  text-transform: uppercase; letter-spacing: 0.06em; }

.board { display: flex; flex-direction: column; }
.board-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  font-size: var(--fs-3);
  color: var(--ink-soft);
}
.board-row + .board-row { border-top: 1px solid var(--line-soft); }
.board-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Numbers are compared down a column, so they have to sit on the same grid.
   Proportional digits make a leaderboard shimmer as it updates. */
.board-num { font-variant-numeric: tabular-nums; font-weight: var(--fw-med); color: var(--ink); }
/* The user's own row pins to the top and is the only one carrying the accent.
   No avatar, no podium, no medals: a rank is a number and a name. */
.board-row--me { color: var(--ink); }
.board-row--me .board-num { color: var(--accent); font-weight: var(--fw-hi); }


/* -------------------------------------------------------------------------
   Two layout wrappers. Not primitives, but a row of chips and a row of
   buttons need a rule somewhere and the alternative is an inline style.
   ------------------------------------------------------------------------- */

.chiprow, .man-btns {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

/* -------------------------------------------------------------------------
   Screen title

   One per screen, in the display face. The other permitted use of the serif
   is the wordmark. Everything else is the UI face.
   ------------------------------------------------------------------------- */

.screen-title {
  font-family: var(--display);
  font-size: var(--fs-6);
  font-weight: var(--fw-med);
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin: 0;
}
.screen-sub {
  font-size: var(--fs-2);
  font-weight: var(--fw-body);
  color: var(--ink-mute);
  margin-top: var(--space-1);
  margin-bottom: var(--space-4);
}

/* What this press is worth, shown before it happens. */
.mults {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
  justify-content: center;
}

/* =========================================================================
   Your light.

   This was a black instrument panel with mono type, an angular clip-path and
   a scanning sweep. It read as an aviation readout dropped into a warm, soft
   product, and recolouring it would have left the geometry and the typeface
   still saying "telemetry".

   So it is a card like every other card: same surface, same radius, same
   padding, same type. The scene below it carries the atmosphere. A component
   does not need its own visual language to matter.
   ========================================================================= */

.stage {
  background: var(--surface);
  border-radius: var(--r-card);
  padding: var(--space-4);
  margin-bottom: var(--space-4);
}
.stage-top { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.stage-main { display: flex; align-items: center; gap: var(--space-4); margin: var(--space-3) 0 var(--space-4); }
.stage-name { font-size: var(--fs-5); font-weight: var(--fw-med); color: var(--ink); line-height: 1.2; }
.stage-sub  { font-size: var(--fs-2); color: var(--ink-mute); margin-top: var(--space-1);
  font-variant-numeric: tabular-nums; }

/* The lighthouse, drawn from the range rather than beside it. */
.stage-row { display: flex; align-items: center; gap: var(--space-4); margin: var(--space-2) 0 var(--space-3); }
.house { width: 52px; height: 66px; flex-shrink: 0; }
.lh-built { fill: var(--ink-mute); }
/* The lamp and the beams carry the signal; the masonry does not. */
.lh-built path:nth-last-child(-n+2) { fill: var(--accent); }
/* What is coming, at the edge of visible. Not a locked padlock: an outline of
   the next piece is a reason to keep going, where a padlock is a refusal. */
.lh-next { fill: none; stroke: var(--line); stroke-width: 1; stroke-dasharray: 2 2; }

/* =========================================================================
   The light moment.

   The single orchestrated moment in the product. Everything else is quiet
   deliberately, and that restraint is what buys this one.
   ========================================================================= */

.lm {
  background: var(--surface);
  display: grid;
  place-items: center;
  padding: var(--space-5);
}
.lm-body { text-align: center; max-width: 30ch; }

/* The section label, in the app's own type. Mono and wide tracking were the
   readout costume, and the moment does not need one to feel like an event. */
.lm-lbl {
  font-size: var(--fs-2); font-weight: var(--fw-med);
  color: var(--ink-mute);
}
.lm-house { display: flex; justify-content: center; margin: var(--space-5) 0 var(--space-4); }
.lm-house .house { width: 96px; height: 120px; }

.lm-nums {
  display: flex; align-items: baseline; justify-content: center; gap: var(--space-3);
  font-family: var(--display);
  font-size: var(--fs-6); font-weight: var(--fw-med);
}
/* Where it was, still legible. The number that moved is not the only thing
   that matters: seeing what it moved from is what makes it a change rather
   than a value. */
.lm-from { color: var(--ink-mute); opacity: 0.5; }
.lm-arrow { color: var(--ink-mute); font-size: var(--fs-4); }
.lm-to { color: var(--accent); }

.lm-sub {
  font-size: var(--fs-3); color: var(--ink-soft);
  line-height: 1.5; margin: var(--space-4) 0 var(--space-5);
}
.lm .btn--tertiary { color: var(--ink-mute); }

@media (prefers-reduced-motion: no-preference) {
  /* 900ms, once, and nothing loops. The beam reaching further is the whole
     idea, so the only thing that moves is the number arriving and the house
     settling under it. */
  .lm.show .lm-house { animation: lmRise 900ms var(--ease) both; }
  .lm.show .lm-to    { animation: lmArrive 900ms var(--ease) both 200ms; }
  .lm.show .lm-sub   { animation: lmArrive 600ms var(--ease) both 500ms; }
  @keyframes lmRise   { from { opacity: 0; transform: translateY(12px) scale(0.94); }
                        to   { opacity: 1; transform: none; } }
  @keyframes lmArrive { from { opacity: 0; transform: translateY(6px); }
                        to   { opacity: 1; transform: none; } }
}
/* Reduced motion is not "no animation at all", it is a crossfade. The moment
   still happens; it simply does not move. */
@media (prefers-reduced-motion: reduce) {
  .lm.show .lm-house, .lm.show .lm-to, .lm.show .lm-sub { animation: none; opacity: 1; }
}

/* Empty states are a composition, not an apology.

   These were cut to one line and one button, on the reasoning that an empty
   state should be an invitation rather than a manual. Rendered, the result
   read as unfinished: a lot of white space, a small sentence, nothing to
   anchor the eye. A mark, a heading, a sentence and two ranked actions is
   still four elements, and it is the difference between deliberate and
   forgotten. */
.empty-mark { width: 32px; height: 32px; fill: var(--accent); opacity: 0.55; }
/* One step below the screen title, always. navis.css also carries an
   `.empty h2` rule at higher specificity, so both are set to --fs-5: a
   primitive that loses to a legacy selector is a primitive in name only. */
.empty-h {
  font-family: var(--display);
  font-size: var(--fs-5);
  font-weight: var(--fw-med);
  letter-spacing: -0.015em;
  color: var(--ink);
  margin: 0;
}
.empty .btn { width: 100%; max-width: 300px; }
.empty .btn + .btn { margin-top: calc(var(--space-2) * -1); }

/* One next milestone with a real bar, the rest behind a disclosure.

   Three requirements shown at once ("0 of 5 things in place, 25 more focused
   hours, 896 more coins" against a balance of 4) read as a wall rather than a
   goal. These rules were lost when the Story block was rewritten and the
   class-coverage audit caught it. */
.milestone { margin-top: var(--space-3); }
.ms-top { display: flex; justify-content: space-between; align-items: baseline;
  font-size: var(--fs-2); color: var(--ink-soft); margin-bottom: var(--space-2); }
.ms-goal { color: var(--ink-mute); }
.ms-more { margin-top: var(--space-3); }
.ms-more summary { font-size: var(--fs-2); color: var(--ink-mute); cursor: pointer; }
.ms-row { display: flex; justify-content: space-between; padding: var(--space-2) 0;
  font-size: var(--fs-2); color: var(--ink-soft); }
.ms-row + .ms-row { border-top: 1px solid var(--line-soft); }

/* Routines on Now: a glance-and-tick strip, not a list.

   These were chips, became full rows under a "same data renders the same way"
   rule, and the rule was wrong here. On Now you tick one in passing on the way
   to something else; on Tasks you manage it. Same data, different job,
   different shape. Three full-width rows pushed the actual content off the
   first screen. */
.routines { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-4); }
.chip.rt { gap: var(--space-2); }
.chip.rt .rt-box {
  width: 16px; height: 16px; flex-shrink: 0;
  border: 1.5px solid var(--line); border-radius: 999px;
  display: grid; place-items: center;
}
.chip.rt.done .rt-box { border-color: currentColor; }
.chip.rt .rt-box svg { width: 10px; height: 10px; }
.chip.rt.done { text-decoration: none; }

/* Coins, in the header.

   They lived here, moved to Story to stop the amber badge fighting the
   wordmark, and then could not be found: a currency you cannot see is one you
   forget you have, and the shop sat two taps behind a tab. Back in the header,
   but quiet: surface fill instead of a filled accent, and the accent used only
   for the dot. */
.coinpill {
  display: inline-flex; align-items: center; gap: var(--space-2);
  height: 32px; padding: 0 var(--space-3);
  border-radius: var(--r-pill);
  background: var(--surface-2);
  color: var(--ink);
  font-size: var(--fs-2); font-weight: var(--fw-med);
  font-variant-numeric: tabular-nums;
  transition: background var(--dur-fast) var(--ease);
}
.coinpill:active { background: var(--line-soft); }
.coinpill svg { width: 10px; height: 10px; fill: var(--accent); }
.coinpill.bump { animation: coinBump 520ms var(--ease); }
@keyframes coinBump { 40% { transform: scale(1.14); } }
@media (prefers-reduced-motion: reduce) { .coinpill.bump { animation: none; } }

/* =========================================================================
   Motion.

   There was none. Not restraint, absence: screens swapped instantly, rows
   appeared fully formed, nothing acknowledged a press. An interface with no
   motion does not read as calm, it reads as unfinished, because the eye gets
   no confirmation that anything happened.

   Three rules keep it from becoming decoration:

   1. Motion answers an action. Nothing moves on its own, nothing loops.
   2. Entrances stagger by position so a list arrives in reading order. That
      is what makes it feel authored rather than uniformly faded in.
   3. Every animation has a reduced-motion alternative that still shows the
      content. Reduced motion means do not move, not do not tell me.
   ========================================================================= */

@media (prefers-reduced-motion: no-preference) {

  /* Screen entrances live in navis.css, which loads after this file and
     already defines `settle` on #app main > *. Two entrance animations on the
     same elements is one that silently never runs. */

  /* Rows within a list, staggered in reading order. Capped at eight so a long
     list does not turn into a slow reveal the person waits through. */
  .tasklist > *, .board > *, .routines > * {
    animation: rowIn 280ms var(--ease-out-quart) both;
  }
  .tasklist > *:nth-child(1), .routines > *:nth-child(1) { animation-delay: 20ms; }
  .tasklist > *:nth-child(2), .routines > *:nth-child(2) { animation-delay: 45ms; }
  .tasklist > *:nth-child(3), .routines > *:nth-child(3) { animation-delay: 70ms; }
  .tasklist > *:nth-child(4) { animation-delay: 95ms; }
  .tasklist > *:nth-child(5) { animation-delay: 120ms; }
  .tasklist > *:nth-child(n+6) { animation-delay: 140ms; }

  @keyframes rowIn {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: none; }
  }

  /* Sheets rise from the edge they are attached to. */
  .sheet { transition: transform var(--dur-base) var(--ease-out-quint); }
  .sheet:not(.show) { transform: translateY(100%); }
  .sheet.show { transform: none; }

  /* A press is acknowledged immediately. 90ms, because feedback that arrives
     after the finger has left is not feedback. */
  .btn, .row, .chip, .card { transition: transform 90ms var(--ease-out-quart),
                                         background 140ms var(--ease-out-quart); }
  .btn:active, .chip:active { transform: scale(0.97); }
  .row:active { transform: scale(0.995); }

  /* Progress fills catch up rather than jumping. */
  .bar i, .sys-bar i { transition: width 520ms var(--ease-out-quint); }
}

/* The content is visible with every animation stripped: these are `both`-filled
   entrances on elements that are already in the layout, not reveals gated on a
   class. A transition that never fires must not leave the screen blank. */
@media (prefers-reduced-motion: reduce) {
  #app main > *, .tasklist > *, .board > *, .routines > * {
    animation: fadeIn 1ms linear both;
  }
  @keyframes fadeIn { from { opacity: 1; } to { opacity: 1; } }
  .sheet { transition: none; }
}

/* =========================================================================
   Interaction states.

   The audit that prompted this counted 205 pressable elements against one
   hover rule and one focus-visible rule. Nothing responded to a cursor and
   keyboard users got no visible focus anywhere outside the nav rail. That is
   the single largest reason the interface reads as a prototype: an element
   that does not acknowledge the pointer does not feel like a control.

   Five states for everything that can be pressed. Defined once here rather
   than per component, so a control added later inherits them by default
   instead of being the one that forgot.
   ========================================================================= */

/* Focus is never removed, only replaced. :focus-visible so a mouse press does
   not leave a ring behind, but a keyboard tab always does. */
.btn:focus-visible,
.row:focus-visible,
.chip:focus-visible,
.coinpill:focus-visible,
.shop-buy:focus-visible,
.shop-equip:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[role="switch"]:focus-visible,
[role="checkbox"]:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}
/* A ring drawn outside the element, so it is not clipped by a parent and does
   not shift anything when it appears. */
.btn, .row, .chip, .coinpill { outline-offset: 2px; }

/* Hover. Pointer-only: on a touch screen :hover sticks after a tap and leaves
   the last thing pressed looking permanently active. */
@media (hover: hover) and (pointer: fine) {
  .btn--primary:hover:not(:disabled)   { background: var(--accent-deep); }
  .btn--secondary:hover:not(:disabled) { background: var(--line-soft); }
  .btn--tertiary:hover:not(:disabled)  { color: var(--ink); background: var(--surface-2); }
  .btn--danger:hover:not(:disabled)    { background: var(--bad-wash); }
  .row:hover                           { background: var(--surface-2); }
  .chip:hover:not(:disabled)           { background: var(--line-soft); }
  .coinpill:hover                      { background: var(--line-soft); }
  .ms-more summary:hover               { color: var(--ink); }
}

/* Disabled. Visibly unavailable rather than silently inert: a control that
   looks pressable and does nothing is worse than one that looks unavailable. */
.btn:disabled,
.chip:disabled,
button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  pointer-events: none;
}
/* pointer-events:none would also kill the cursor, so the wrapper keeps it. */
.btn:disabled { pointer-events: auto; }
.btn:disabled:active { transform: none; }

/* Inputs. A border colour change alone is easy to miss; the ring is what
   actually says "typing goes here now". */
input, select, textarea {
  border: 1px solid var(--line);
  border-radius: var(--r-control);
  background: var(--surface);
  transition: border-color var(--dur-fast) var(--ease),
              box-shadow var(--dur-fast) var(--ease);
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--focus-ring);
}
input[aria-invalid="true"], textarea[aria-invalid="true"] {
  border-color: var(--bad);
}
input[aria-invalid="true"]:focus, textarea[aria-invalid="true"]:focus {
  box-shadow: var(--focus-ring-bad);
}
/* Placeholders need the same 4.5:1 as body text. The muted-grey default is
   the most common contrast failure in a form. */
::placeholder { color: var(--ink-mute); opacity: 1; }

/* Toggles and checkboxes read their state from the attribute, so the visual
   and the accessible state cannot drift apart. */
[role="switch"][aria-checked="true"],
[role="checkbox"][aria-checked="true"] { color: var(--accent); }

/* Elevation, assigned by what a thing is rather than by how it looks.

   Cards sit on the page, so they take the lightest step or none at all and
   lean on the surface ramp instead. A dropdown is temporary and belongs above
   its trigger. A sheet is modal and belongs above the app. Depth that does not
   correspond to a category is decoration. */
.card                       { box-shadow: var(--shadow-1); }
.stage, .challenge          { box-shadow: var(--shadow-1); }
.menu, .shop-panel          { box-shadow: var(--shadow-2); }
#app nav                    { box-shadow: var(--shadow-2); }
.sheet, .onboard, .lm-body  { box-shadow: var(--shadow-4); }

@media (hover: hover) and (pointer: fine) {
  /* A card that lifts on hover is only correct when the whole card is the
     control. A card you read does not respond to the cursor. */
  a.card:hover, button.card:hover { box-shadow: var(--shadow-2); }
}

/* =========================================================================
   Motion, the rest of it.

   What existed covered entrances and presses. These are the transitions
   between states, which is where an interface stops feeling like a series of
   redraws and starts feeling like one thing changing.

   The rule that keeps it from becoming noise: motion only ever describes a
   change the person caused. Nothing here loops, nothing plays on a timer,
   and nothing moves while they are trying to read.
   ========================================================================= */

@media (prefers-reduced-motion: no-preference) {

  /* Tabs. The indicator and the weight change together so the eye follows one
     movement rather than noticing two. */
  #app nav button { transition: color var(--dur-fast) var(--ease-out-quart),
                                background var(--dur-fast) var(--ease-out-quart),
                                font-weight 1ms; }
  #app nav button svg { transition: color var(--dur-fast) var(--ease-out-quart),
                                    transform 180ms var(--ease-out-quint); }
  #app nav button.on svg { transform: translateY(-1px); }

  /* Chips answer immediately, because a chip is usually a toggle and a toggle
     that lags feels like it did not register. */
  .chip { transition: background var(--dur-fast) var(--ease-out-quart),
                      color var(--dur-fast) var(--ease-out-quart),
                      transform 90ms var(--ease-out-quart); }

  /* A routine ticking off. The fill lands before the label fades, so the
     order reads as cause then consequence. */
  .chip.rt .rt-box { transition: border-color 140ms var(--ease-out-quart),
                                 background 140ms var(--ease-out-quart); }
  .chip.rt.done .rt-box svg { animation: tick 260ms var(--ease-out-quint) both; }
  @keyframes tick {
    from { opacity: 0; transform: scale(0.4); }
    to   { opacity: 1; transform: none; }
  }

  /* The scrim fades, the sheet rises. Two different durations on purpose: the
     backdrop should already be settling as the sheet arrives, or the sheet
     looks like it is dragging the background up with it. */
  .scrim { transition: opacity 220ms var(--ease-out-quart); }

  /* Toast. Same shape as the undo bar, since they occupy the same slot and
     should not look like two different systems. */
  .toast { transition: transform var(--dur-base) var(--ease-out-quint),
                       opacity var(--dur-base) var(--ease-out-quint); }

  /* Disclosure. Rotating the marker is the whole animation; expanding height
     is not worth the layout thrash. */
  .ms-more summary::marker, details summary::marker { transition: none; }
  details[open] > summary { color: var(--ink); }

  /* Numbers that change. The coin count bumps rather than swapping, so a
     change you did not initiate still gets noticed. */
  .coinpill .num { display: inline-block; transition: transform 180ms var(--ease-out-quint); }

  /* A card being removed. It collapses rather than vanishing, so the list
     does not jump under the thumb that pressed delete. */
  .row.removing, .taskrow.removing {
    animation: collapse 240ms var(--ease-out-quart) both;
    overflow: hidden;
  }
  @keyframes collapse {
    from { opacity: 1; max-height: 200px; }
    to   { opacity: 0; max-height: 0; padding-top: 0; padding-bottom: 0; }
  }
}

/* Everything above has a still equivalent. Reduced motion means the change
   still happens and still reads; it just does not travel. */
@media (prefers-reduced-motion: reduce) {
  #app nav button svg,
  .chip.rt.done .rt-box svg,
  .coinpill .num { animation: none; transform: none; transition: none; }
  .row.removing, .taskrow.removing { animation: none; display: none; }
}

/* =========================================================================
   The floor.

   29 controls render as a bare <button>. A bare button has no background, no
   border, no padding and inherits the page font at the browser's default
   size, so it reads as a slightly odd run of text rather than something you
   press. "Sign out", "Turn on notifications", "Accept", "Use my location" and
   26 others were all in that state.

   Rather than hunt every call site, every button in the app tree gets a
   baseline. A control that forgets its class is now merely plain, not
   invisible. Anything carrying .btn, .row or .chip opts out and keeps its
   own treatment.
   ========================================================================= */

#app button:not([class]),
#app button[class=""],
.sheet button:not([class]),
.onboard button:not([class]) {
  min-height: 44px;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--r-control);
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--ink);
  font-family: inherit;
  font-size: var(--fs-3);
  font-weight: var(--fw-med);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out-quart);
}
@media (hover: hover) and (pointer: fine) {
  #app button:not([class]):hover,
  .sheet button:not([class]):hover,
  .onboard button:not([class]):hover { background: var(--line-soft); }
}
#app button:not([class]):focus-visible,
.sheet button:not([class]):focus-visible,
.onboard button:not([class]):focus-visible { outline: none; box-shadow: var(--focus-ring); }
#app button:not([class]):active { transform: scale(0.97); }

/* The tab bar is the one piece of chrome that must always read. In light mode
   a white bar on a near-white page separated only by a hairline, which is why
   it looked absent even while it was working. It gets its own solid value and
   a shadow cast upward, so it sits on the app rather than in it. */
#app nav {
  background: var(--surface-solid);
  box-shadow: 0 -1px 0 var(--line), var(--shadow-2);
}

/* =========================================================================
   The onboarding questions.

   Full screen, one question at a time, because five questions on one page is
   a form and a form is the thing people close. One at a time with a visible
   count is a conversation with an end in sight.
   ========================================================================= */

.obq { display: grid; place-items: center; padding: var(--space-5); }
.obq-body {
  width: 100%; max-width: 460px;
  background: var(--surface);
  border-radius: var(--r-card);
  padding: var(--space-5);
  box-shadow: var(--shadow-4);
  max-height: calc(100dvh - var(--space-7));
  overflow-y: auto;
}

.obq-top { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.obq-step { font-size: var(--fs-1); color: var(--ink-mute); letter-spacing: 0.04em; }
/* "Skip all of this" is a real offer, not a dark-pattern whisper. Somebody who
   cannot face five questions at nine at night is exactly who this is for. */
.obq-top .btn--tertiary { min-height: 32px; font-size: var(--fs-2); }

.obq-t {
  font-family: var(--display);
  font-size: var(--fs-5); font-weight: var(--fw-med);
  line-height: 1.2; letter-spacing: -0.015em;
  color: var(--ink); margin: var(--space-4) 0 var(--space-2);
  text-wrap: balance;
}
.obq-b { font-size: var(--fs-2); color: var(--ink-soft); line-height: 1.5; margin: 0; }

.obq-opts {
  display: flex; flex-wrap: wrap; gap: var(--space-2);
  margin: var(--space-4) 0;
}
/* Options wrap and size to their text. A grid would make "No energy for it"
   and "It has to be done properly or not at all" the same width, which reads
   as a quiz rather than a list of things that might be true. */
.obq-opt { height: auto; min-height: 38px; padding: var(--space-2) var(--space-3);
  text-align: left; white-space: normal; }

.obq-own { display: block; margin-bottom: var(--space-4); }
.obq-own-l { display: block; font-size: var(--fs-1); color: var(--ink-mute);
  margin-bottom: var(--space-2); }
.obq-own textarea {
  width: 100%; padding: var(--space-3);
  font-family: inherit; font-size: var(--fs-3); color: var(--ink);
  background: var(--surface-2); resize: vertical;
}

.obq-nav { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.obq-nav .btn--primary { flex: 1; max-width: 220px; margin-left: auto; }

@media (prefers-reduced-motion: no-preference) {
  .obq.show .obq-body { animation: riseObq 320ms var(--ease-out-quint) both; }
  @keyframes riseObq { from { opacity: 0; transform: translateY(10px) scale(0.99); }
                       to   { opacity: 1; transform: none; } }
}

/* What Navis says back, as you answer.

   Deterministic and instant: a model round trip between every question would
   put a spinner in the middle of onboarding, which is the one place this
   product cannot afford one. The line states what the answer just changed, so
   the app is visibly taking shape while it is being filled in. */
.obq-reply {
  margin: 0 0 var(--space-4);
  padding: var(--space-3);
  border-radius: var(--r-control);
  background: var(--tide-wash, var(--surface-2));
  border-left: 2px solid var(--tide);
  font-size: var(--fs-2);
  line-height: 1.5;
  color: var(--ink-soft);
}
@media (prefers-reduced-motion: no-preference) {
  .obq-reply { animation: replyIn 240ms var(--ease-out-quint) both; }
  @keyframes replyIn { from { opacity: 0; transform: translateY(4px); }
                       to   { opacity: 1; transform: none; } }
}

/* Account setup shares the question shell, so the two read as one
   conversation rather than two systems that happen to run in sequence. */
#acctroot input[type="text"] {
  width: 100%;
  padding: var(--space-3);
  font-family: inherit; font-size: var(--fs-4); color: var(--ink);
  background: var(--surface-2);
}
