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


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


/* -------------------------------------------------------------------------
   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-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; }
.board-place{ width: 1.6em; color: var(--ink-mute); font-variant-numeric: tabular-nums; }
/* 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); font-weight: var(--fw-med); }
.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(--font-ui);
  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-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(--font-num);
  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. sailyn.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(--font-ui);
  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. */
@keyframes coinBump{40%{ transform: scale(1.14); } }

/* =========================================================================
   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 sailyn.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{ 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,
/* 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{ 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); }
.row:hover{ background: var(--surface-2); }
.chip:hover:not(:disabled){ 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{ 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. */

  /* 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,
.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.

   The tab bar opts out by [data-tab], and that exclusion is the whole reason
   the rail looked the way it did. A tab is `<button data-tab="tasks">` with no
   class until it is the current one, so five of the six matched this rule and
   rendered as filled, outlined, 3px-radius rectangles. The rail's own rule,
   500 lines away in sailyn.css under a comment explaining that six outlined
   rectangles are the last of the old card language, sets border:none and
   background:none on `#app nav button` and lost the cascade: (1,1,1) here
   against (1,0,2) there. The fix documented in that comment had never once
   been visible. A tab bar is chrome with its own treatment, not a control
   that forgot its class.
   ========================================================================= */

#app button:not([class]):not([data-tab]),
#app button[class=""]:not([data-tab]),
.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]):not([data-tab]):hover,
  .sheet button:not([class]):hover,
.onboard button:not([class]):hover{ background: var(--line-soft); }
}
#app button:not([class]):not([data-tab]):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]):not([data-tab]):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 doubled rule along its top edge, so it sits on the app rather than in it.

   Scoped to the phone. Unscoped it also applied to the desktop rail, where it
   overrode the rail's --surface-2 plane and hung a shadow down the length of
   the sidebar. sailyn.css is tested for the tab bar having no shadow; the rule
   that gave it one was in this file, where that test does not look. */
@media (max-width: 879px){
#app nav{
  background: var(--surface-solid);
  box-shadow: 0 -1px 0 var(--line);
}
}

/* =========================================================================
   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(100svh - 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(--font-ui);
  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 Sailyn 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);
}

/* Goals, when there is more than one.

   Two goals with five and three milestones each pushed the timetable most of a
   screen down, and the timetable is the thing the Plan page exists for. The
   list scrolls once it gets past about two goals' worth, so the page keeps its
   shape however many outcomes somebody is carrying. */
.goals-list {
  /* dvh, not vh. On a phone vh is the tallest the viewport ever gets, with the
     browser chrome hidden, so a list sized in vh is taller than the screen for
     as long as the address bar is showing. */
  max-height: min(52dvh, 520px);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* A fade rather than a hard edge, so a cut-off card reads as "more below"
     instead of as a rendering fault. */
  mask-image: linear-gradient(to bottom, #000 calc(100% - 24px), transparent);
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 24px), transparent);
  padding-right: var(--space-1);
}
.goals-list::-webkit-scrollbar { width: 6px; }
.goals-list::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; }

/* A milestone and its remove control share a row. The control is the same
   quiet treatment as every other destructive thing: visible, reversible,
   and not shouting. */
.mile-wrap { display: flex; align-items: center; gap: var(--space-1); }
.mile-wrap .mile-row { flex: 1; min-width: 0; }
/* "I finished one", on a step of a shared goal that is counted in things.
   Beside the remove control rather than inside the row, because the row is
   itself a button and a button inside a button is not a thing. Held to its
   content so a long unit word cannot squeeze the step's own name to nothing. */
.mile-log { flex-shrink: 0; white-space: nowrap; font-size: var(--fs-1); padding: 4px 8px; }
.mile-x { width: 28px; height: 28px; }
.mile-x svg { width: 13px; height: 13px; }
.mile-n {
  font-size: var(--fs-1); color: var(--ink-mute);
  font-variant-numeric: tabular-nums; margin-left: auto; padding-left: var(--space-2);
}

/* Committed against sustainable. Over is stated, not just coloured: colour
   alone is the signal that disappears for the people most likely to have
   over-committed at two in the morning. */
.cap-note.over { color: var(--danger); }

/* A goal that will not land in the time left.

   Stated once, with the two things that actually help attached. A warning that
   names a problem and offers nothing is a reminder that you are behind, which
   is the single thing this product exists not to do. */
.goal-late {
  margin: var(--space-3) 0;
  padding: var(--space-3);
  border-radius: var(--r-control);
  background: var(--surface-2);
  border-left: 2px solid var(--warn);
}
.goal-late-t { font-size: var(--fs-2); font-weight: var(--fw-med); color: var(--ink); }
.goal-late-b { font-size: var(--fs-2); color: var(--ink-soft); line-height: 1.5; margin-top: 2px; }
.goal-late-a { display: flex; gap: var(--space-2); margin-top: var(--space-3); flex-wrap: wrap; }
.goal-late-a .btn { min-height: 36px; padding: 0 var(--space-3); font-size: var(--fs-2); }

#gfixscrim input[type="date"] {
  width: 100%; padding: var(--space-3);
  font-family: inherit; font-size: var(--fs-3); color: var(--ink);
  background: var(--surface-2);
}

/* Setup: the three doors.

   Recommended and manual as cards rather than a segmented control, because
   they are not two settings of one thing: they are two different amounts of
   work, and the card has room to say so. */
.setup-doors { display: grid; gap: var(--space-3); }
.setup-door {
  display: block; width: 100%; text-align: left;
  padding: var(--space-4); border-radius: var(--r-card);
  background: var(--surface-2); border: 1px solid var(--line);
  cursor: pointer;
}
.setup-door-t { display: block; font-size: var(--fs-4); font-weight: var(--fw-med); color: var(--ink); }
.setup-door-s { display: block; font-size: var(--fs-2); color: var(--ink-soft); line-height: 1.5; margin-top: 4px; }
@media (hover: hover) and (pointer: fine) {
  /* Hover is not selection. The accent border says "this one is chosen" and
     neither of these is chosen until it is pressed, so hover lifts the surface
     and firms the existing line instead of borrowing the state colour. */
  .setup-door:hover { border-color: var(--line-strong, var(--ink-mute)); background: var(--surface); }
}
.setup-door:focus-visible { box-shadow: var(--focus-ring); outline: none; }

/* What it changed. Two columns so the setting and its value line up down the
   list and can be scanned rather than read. */
.setup-sum { margin-top: var(--space-3); }
.setup-sum-row {
  display: flex; gap: var(--space-3); align-items: baseline;
  padding: var(--space-2) 0; border-bottom: 1px solid var(--line-soft);
}
.setup-sum-row:last-child { border-bottom: 0; }
.setup-sum-k { flex: none; width: 42%; font-size: var(--fs-2); color: var(--ink-mute); }
.setup-sum-v { flex: 1; font-size: var(--fs-2); color: var(--ink); }

/* A picked trait in the first question. It was a plain card with no selected
   state at all, which is fine for pick-one and unusable for pick-many. */
.pain.on { border-color: var(--accent); background: var(--accent-wash); }
.pain.on .pain-t { color: var(--accent-ink, var(--ink)); font-weight: var(--fw-hi); }

/* A summary row that opens.
   The chevron turns rather than swapping glyph, so the row reads as the same
   thing in two states instead of two different rows. */
.setup-sum-item { border-bottom: 1px solid var(--line-soft); }
.setup-sum-item:last-child { border-bottom: 0; }
.setup-sum-row.as-btn {
  display: flex; align-items: baseline; gap: var(--space-3);
  width: 100%; text-align: left; padding: var(--space-3) 0;
  background: transparent; border: 0; cursor: pointer; color: inherit;
}
.setup-sum-cx {
  width: 16px; height: 16px; flex: none; color: var(--ink-mute);
  align-self: center; transition: transform var(--dur-fast) var(--ease);
}
.setup-sum-item.open .setup-sum-cx { transform: rotate(180deg); }
.setup-sum-item.open .setup-sum-k { color: var(--ink); }
.setup-sum-note { display: block; font-size: var(--fs-1); color: var(--ink-mute); margin-top: 2px; }
.setup-sum-edit { padding: 0 0 var(--space-3); }
@media (prefers-reduced-motion: reduce) { .setup-sum-cx { transition: none; } }
@media (hover: hover) and (pointer: fine) {
  .setup-sum-row.as-btn:hover .setup-sum-cx { color: var(--ink); }
}
.setup-sum-row.as-btn:focus-visible { box-shadow: var(--focus-ring); outline: none; border-radius: var(--r-control); }

/* ---------------------------------------------------------------------------
   Team goals.

   Two halves of one objective, and they must never read as the same thing.
   Your steps use the milestone row exactly as an ordinary goal does, because
   they are ordinary work. What another person owes is a different kind of fact
   and gets a different treatment: no checkbox, because there is nothing here
   you can tick, and a leading hairline rather than a box, because it is a run
   of content under a rule like everything else in this system.

   Nothing in here introduces a colour, a radius or a weight that the rest of
   the app does not already use.
   --------------------------------------------------------------------------- */

/* The label above each half. Quieter than a section heading, because these sit
   inside a goal rather than beside one. */
.tg-h {
  font-size: var(--fs-1); font-weight: var(--fw-med);
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-mute);
  margin: var(--space-4) 0 var(--space-2);
  padding-bottom: var(--space-1);
  border-bottom: 1px solid var(--line);
}
.goal--team .tg-h:first-of-type { margin-top: var(--space-3); }

.tg-deps { display: flex; flex-direction: column; }
/* One edge, never four. The rule is the separation; the state is on the left
   because a colour on its own is the signal that disappears first. */
.tg-dep {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) 0 var(--space-2) var(--space-3);
  border-left: 2px solid var(--line);
}
.tg-dep + .tg-dep { border-top: 1px solid var(--line-soft); }
/* Due is machine state, which is what --tide is for. */
.tg-dep.is-due { border-left-color: var(--tide); }
/* Delivered is settled, so it goes quiet rather than lighting up. --gold is
   what the person has earned and this is not that: somebody else did it.
   Resolved things receding is already how a finished milestone reads. */
.tg-dep.is-in { border-left-color: var(--line-strong); }
.tg-dep.is-in .tg-dep-t, .tg-dep.is-in .tg-who { color: var(--ink-mute); font-weight: var(--fw-body); }
/* Overdue is a firmer version of due rather than a different kind of thing, so
   it is the same field said more strongly. A second colour here would be a
   warning, and nothing has gone wrong: somebody else is late and the only
   useful response is to ask them. */
.tg-dep.is-late { border-left-color: var(--tide); border-left-width: 3px; }
.tg-dep.is-late .tg-dep-s { color: var(--ink-soft); font-weight: var(--fw-med); }
/* Cancelled, or no longer needed. Settled like delivered and quieter still,
   because it is the one state where there is nothing left to do about it. */
.tg-dep.is-off { border-left-color: var(--line-soft); }
.tg-dep.is-off .tg-dep-t, .tg-dep.is-off .tg-who,
.tg-dep.is-off .tg-dep-s { color: var(--ink-mute); font-weight: var(--fw-body); }

.tg-dep-b { flex: 1; min-width: 0; }
.tg-dep-t {
  font-size: var(--fs-3); font-weight: var(--fw-body); color: var(--ink-soft);
  line-height: 1.35; overflow-wrap: anywhere;
}
/* The person first, because "whose" is the question this half answers. */
.tg-who { font-weight: var(--fw-med); color: var(--ink); }
.tg-dep-s {
  font-size: var(--fs-1); color: var(--ink-mute); margin-top: 2px;
}
/* Every date and count in this block is a number and is set as one. */
.tg-dep-s .num, .tg-hrs .chip {
  font-family: var(--font-num); font-variant-numeric: tabular-nums;
}
.tg-dep.is-in .tg-dep-s { color: var(--ink-mute); }

/* 36px, the same as the in-row buttons on a goal that will not land. Smaller
   than a standalone control on purpose: it sits inside a dense row, and the
   precedent for that in this app is .goal-late-a .btn. */
.tg-ask {
  flex-shrink: 0; min-height: 36px;
  padding: 0 var(--space-3); font-size: var(--fs-2);
}

/* A commitment: a goal with a rate or a ceiling where the steps would be.
 *
 * It reuses the goal card rather than inventing a second one, because it is the
 * same object in the same list and two shapes side by side on one screen is the
 * most visible kind of seam. What is different is what it has to say: no
 * milestone list, and two lines of numbers instead, so the numbers are set as
 * numbers rather than left as prose that happens to contain digits. */
.goal--commit .goal-sub .num,
.goal--commit .ip-line .num {
  font-family: var(--font-num); font-variant-numeric: tabular-nums;
}
.ip-line {
  font-size: var(--fs-3); color: var(--ink); font-weight: var(--fw-med);
  margin-top: var(--space-2);
}
/* Past a ceiling somebody set for themselves. Emphasis by weight, not by
   colour: this is a fact about their day, not machine state and not a warning,
   and the sentence beside it already says what happened. */
.ip-line.is-over .num { font-weight: var(--fw-hi); }
.goal--commit .region-acts { margin-top: var(--space-3); }

/* The sheet that builds one. A commitment is a small group of fields, so it
   gets a rule above it rather than a card around it. */
.tg-row { padding: var(--space-3) 0; border-top: 1px solid var(--line); }
.tg-row:first-child { border-top: 0; padding-top: var(--space-1); }
.tg-row-top {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2); margin-bottom: var(--space-2);
}
.tg-seg { flex: 1; min-width: 0; }
.tg-in {
  width: 100%; margin-top: var(--space-2);
  padding: var(--space-2) var(--space-3);
  font-family: inherit; font-size: var(--fs-3); color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--r-control);
}
.tg-when {
  display: flex; align-items: center; gap: var(--space-2);
  flex-wrap: wrap; margin-top: var(--space-2);
}
.tg-date {
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-num); font-variant-numeric: tabular-nums;
  font-size: var(--fs-2); color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--r-control);
}
.tg-hrs { margin: 0; }
.tg-hrs .chip { min-height: 32px; padding: 0 var(--space-3); font-size: var(--fs-2); }


/* =========================================================================
   COMPOSITION

   The three primitives every screen was improvising, and the reason each one
   read as a run of fragments rather than a page.

   Before this block, a screen was a flat list of siblings under #main: a
   heading, then the thing it introduced, then another heading, all peers, all
   at the same weight. Insights had six of them. Nothing grouped, so nothing
   could outrank anything, and a screen with one important answer on it looked
   exactly like a screen with six unimportant ones.

   Three things fix that, and they are the three the design rules already
   described without ever providing:

     .page    the shell. One column on a phone, a primary column and a
              context column on a wide screen, so a laptop stops being a
              phone with a sidebar and 40% dead space to the right.
     .region  the unit. One hairline on the top edge, a heading, and a run of
              content under it. Never a fill, never four edges, never a lift.
     .lead    the screen's focal answer, above the first rule. Exactly one per
              screen, and the only thing on it at the top of the type scale.
   ========================================================================= */

/* ---- the page shell ---------------------------------------------------- */

.page{ display: flex; flex-direction: column; gap: var(--space-6); }

/* The columns are flex on every width. On a phone .page-side simply follows
   .page-main in the flow, so the markup is the reading order: nothing is
   duplicated for narrow screens and nothing is hidden from them. */
.page-main, .page-side{ display: flex; flex-direction: column; gap: var(--space-6); min-width: 0; }

/* ---- the region -------------------------------------------------------- */

.region{
  border-top: 1px solid var(--line);
  padding-top: var(--space-4);
  min-width: 0;
}
/* A region that opens a column has nothing above it to be separated from, and
   a rule with nothing on the other side of it is decoration. */
.region--open{ border-top: 0; padding-top: 0; }
/* The rule softens where a region is a subdivision rather than a new subject. */
.region--sub{ border-top-color: var(--line-soft); }

.region-head{
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-3); margin-bottom: var(--space-3);
}
/* The count and the action belong to the heading and to each other, so they
   group at the far end of the rule. Left as three space-between children the
   count landed in the middle, attached to neither. */
.region-tools{
  display: flex; align-items: baseline; gap: var(--space-4);
  flex-shrink: 0; min-width: 0;
}
.region-title{
  font-size: var(--fs-4); font-weight: var(--fw-med); color: var(--ink);
  letter-spacing: -0.01em; line-height: 1.3;
}
/* What the heading is worth: a duration, a count, a state. Numeric face,
   because it is nearly always one of the three. */
.region-aside{
  font-family: var(--font-num); font-variant-numeric: tabular-nums;
  font-size: var(--fs-2); font-weight: var(--fw-med); color: var(--ink-mute);
  white-space: nowrap; flex-shrink: 0;
}
/* An action belonging to the region rather than to the screen.
   This is where the full-width buttons went. Four stacked blocks under a list
   is four things shouting at the same volume as the list; on the heading line
   they are available without competing with anything. */
.region-act{
  font-size: var(--fs-2); font-weight: var(--fw-med); color: var(--ink-soft);
  background: none; border: 0; padding: var(--space-1) 0;
  border-bottom: 1px solid var(--line);
  white-space: nowrap; flex-shrink: 0;
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.region-act:focus-visible{ box-shadow: var(--focus-ring); outline: none; }
.region-note{
  font-size: var(--fs-2); color: var(--ink-mute); line-height: 1.5;
  margin-bottom: var(--space-3); max-width: 62ch;
}

/* ---- the lead ---------------------------------------------------------- */

/* The screen's answer, and the only region on it that is allowed to be loud.
   No rule above it: it is the first thing, so there is nothing to separate it
   from, and a line across the top of the most important thing on the screen
   reads as a lid. */
.lead{ min-width: 0; }
.lead-eyebrow{
  display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap;
  font-size: var(--fs-2); font-weight: var(--fw-med); color: var(--ink-mute);
  letter-spacing: var(--track-label); text-transform: uppercase;
  margin-bottom: var(--space-3);
}
.lead-eyebrow .why{ text-transform: none; letter-spacing: 0; color: var(--ink-soft); font-weight: var(--fw-body); }
.lead-title{
  font-size: var(--fs-6); font-weight: var(--fw-hi); line-height: 1.08;
  letter-spacing: var(--track-title); color: var(--ink);
  text-wrap: balance; margin-bottom: var(--space-4);
}
/* The facts under the answer. A run of quiet metadata, not a row of chips:
   these describe the thing, they are not things to press. */
.lead-facts{
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--space-2) var(--space-4);
  font-size: var(--fs-3); font-weight: var(--fw-med); color: var(--ink-soft);
  margin-bottom: var(--space-5);
}
.lead-facts b{
  color: var(--ink); font-weight: var(--fw-hi);
  font-family: var(--font-num); font-variant-numeric: tabular-nums;
}
.lead-actions{ display: flex; flex-direction: column; gap: var(--space-2); }
/* The escape hatches. Under the one thing to press, quieter than it, and on
   one line rather than two stacked blocks, because two full-width buttons
   below a primary read as three primaries. */
.lead-outs{
  display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-5);
  margin-top: var(--space-3);
}
.lead-outs .btn{ min-height: 32px; padding: 0; width: auto; justify-content: flex-start; }

/* ---- a measured quantity ----------------------------------------------- */

/* Numbers are the voice, so a figure and the words around it are one unit
   with the figure carrying the weight. This replaces eleven ad hoc
   "<b>4h 20m</b> planned" spans that each picked their own size. */
.metrics{ display: flex; flex-wrap: wrap; gap: var(--space-5) var(--space-6); }
.metric{ display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.metric-n{
  font-family: var(--font-num); font-variant-numeric: tabular-nums;
  font-size: var(--fs-5); font-weight: var(--fw-med); color: var(--ink);
  line-height: 1; letter-spacing: -0.02em;
}
.metric-l{ font-size: var(--fs-2); color: var(--ink-mute); font-weight: var(--fw-body); }
.metric--quiet .metric-n{ color: var(--ink-soft); font-size: var(--fs-4); }

/* ---------- the way past the header ----------
   The one control on any Sailyn page that exists solely for a keyboard.

   It lives here rather than in sailyn.css or pages.css because both the app and
   the eleven public pages load this file, and a skip link that is on one of
   them is a skip link somebody learns not to trust.

   Off the top of the screen rather than in an .sr-only clip, and it comes back
   on focus. A skip link that stays invisible while focused is a control only a
   screen reader knows about, and the people who need it most are sighted
   keyboard users who can see where focus is.

   Above every layer: the sticky site header is z-index 60 and the app's scrims
   are var(--z-scrim), so a link that appears under either is a link that
   appears nowhere. */
.skiplink{
  position: fixed; top: 0; left: 0; z-index: 9999;
  transform: translateY(-120%);
  margin: var(--space-2);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-solid, var(--surface));
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--r-control);
  font-size: var(--fs-3); font-weight: var(--fw-med);
  text-decoration: none;
  /* The one transition, on the one property that moves. */
  transition: transform var(--dur-fast, 140ms) var(--ease, ease);
}
.skiplink:focus, .skiplink:focus-visible{
  transform: translateY(0);
  outline: none; box-shadow: var(--focus-ring);
}
/* A destination is not a tab stop, so it takes no ring of its own when the
   link lands on it. */
#main:focus{ outline: none; }
