/* Sentio — a reading instrument, not a dashboard.
 *
 * Palette is a Bergen night: a deep blue-green dark rather than neutral black,
 * because the subject's world is coastal, wet and northern, and a flat #000 would
 * belong to no place at all. The accent is not a brand colour — it is the body's
 * own exertion, so the page warms as the day's load rises and stays cold when it
 * does not. Nothing else is allowed to be bright.
 *
 * Type is IBM Plex: drawn as an engineering family, which is what this is.
 * Condensed carries the headings, Mono carries every number so figures align in
 * columns and can be compared at a glance.
 */

@font-face { font-family: "Plex Cond"; src: url("fonts/plex-cond-600.woff2") format("woff2");
             font-weight: 600; font-display: swap; }
@font-face { font-family: "Plex"; src: url("fonts/plex-sans-400.woff2") format("woff2");
             font-weight: 400; font-display: swap; }
@font-face { font-family: "Plex Mono"; src: url("fonts/plex-mono-400.woff2") format("woff2");
             font-weight: 400; font-display: swap; }
@font-face { font-family: "Plex Mono"; src: url("fonts/plex-mono-500.woff2") format("woff2");
             font-weight: 500; font-display: swap; }

:root {
  --ink:        #070E10;
  --ink-2:      #0E181B;
  --ink-3:      #142226;
  --line:       #1C2C30;
  --frost:      #E4EBE9;
  --muted:      #6B7F81;
  --muted-dim:  #46585A;

  /* Exertion, cold to hot. Used for the ribbon and for nothing else. */
  --rest:  #3C7B7A;
  --easy:  #6C9E63;
  --work:  #C8A22A;
  --hard:  #CE5D39;

  /* One colour per load axis, so a reader learns the pairing once and it holds
     on both panes: the bar under the day and the line on the fortnight are the
     same quantity and must not be two different colours. Taken from the ramp
     above rather than added to it — effort keeps the shade the 80 bpm rail
     already uses, and movement takes the cold end, which is the one place on
     the ramp that never means "working hard". */
  --effort:   var(--work);
  --movement: #5E93A8;

  --sp: 20px;
  --radius: 14px;
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--ink);
  color: var(--frost);
  font-family: "Plex", system-ui, sans-serif;
  font-size: 16px;
  line-height: 1.5;
  padding: max(env(safe-area-inset-top), 12px) 0 max(env(safe-area-inset-bottom), 24px);
  -webkit-font-smoothing: antialiased;
}

.mono { font-family: "Plex Mono", ui-monospace, monospace; font-variant-numeric: tabular-nums; }

[hidden] { display: none !important; }

/* --- first run ------------------------------------------------------- */

.setup { padding: 18vh var(--sp) 0; max-width: 420px; margin: 0 auto; }
.setup__title {
  font-family: "Plex Cond", sans-serif; font-weight: 600;
  font-size: 40px; letter-spacing: -0.015em; margin: 0 0 8px;
}
.setup__note { color: var(--muted); margin: 0 0 24px; }
.setup__error { color: var(--hard); font-size: 14px; margin-top: 14px; }

.field {
  width: 100%; padding: 15px 16px; margin-bottom: 12px;
  background: var(--ink-2); color: var(--frost);
  border: 1px solid var(--line); border-radius: var(--radius);
  font-family: "Plex Mono", monospace; font-size: 15px;
}
.field:focus-visible { outline: 2px solid var(--rest); outline-offset: 2px; }

/* --- header ---------------------------------------------------------- */

.head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 16px; padding: 8px var(--sp) 22px;
}
.head__weekday {
  display: block; color: var(--muted); font-size: 13px;
  letter-spacing: 0.14em; text-transform: uppercase;
}
.head__date {
  font-family: "Plex Cond", sans-serif; font-weight: 600;
  font-size: 34px; line-height: 1.05; letter-spacing: -0.01em; margin: 2px 0 0;
}
.head__sun { text-align: right; }
.head__daylight { display: block; font-size: 20px; font-weight: 500; }
.head__sunlabel {
  color: var(--muted-dim); font-size: 11px;
  letter-spacing: 0.14em; text-transform: uppercase;
}

/* --- the ribbon ------------------------------------------------------ */

.ribbon { margin: 0; padding: 0 var(--sp); }
/* The labels live in a gutter beside the plot, not on top of it. Inside, they
   always collide with the trace — the baseline especially, since the fill sits on
   it by definition, so there is no quiet corner to put a number in. */
.ribbon__plot { position: relative; padding-left: 32px; }
.ribbon svg {
  display: block; width: 100%; height: 210px;
  border-radius: var(--radius); background: var(--ink-2);
  /* A long press is the scrub gesture. Without this Android offers to select
     text and pops a context menu instead. */
  -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none;
}
.ribbon__yaxis {
  position: absolute; inset: 0; width: 32px; pointer-events: none;
}
.ribbon__yaxis span {
  position: absolute; right: 7px; transform: translateY(-50%);
  color: var(--muted-dim); font-size: 11px; line-height: 1;
}
/* The resting rate is the one line the fill is measured from, so it is the one
   label that is a reference rather than a gridline. */
.ribbon__yaxis span.is-base { color: var(--muted); }
.ribbon__axis {
  padding: 8px 0 0; color: var(--muted-dim); font-size: 11px;
}
/* The hour marks belong to the plot, so they are measured from the plot: the same
   32px gutter it is inset by, and each label centred on its true fraction of the
   remaining width. Laid out with `space-between` across the whole caption they were
   describing a box 32px wider than the one the trace is drawn in — 04:00 sat 1.8 h
   to the left of where 04:00 actually is, 22:00 sat 0.7 h left, and the error
   shrinking across the day is what makes it read as a shifted clock rather than a
   misplaced label. */
.ribbon__axis--hours {
  /* Explicit height: the labels are taken out of flow, so nothing else gives this
     box one, and the gap above them is the padding the flow layout used to add. */
  position: relative; margin-left: 32px; padding: 0; height: calc(8px + 1.2em);
}
.ribbon__axis--hours span {
  position: absolute; top: 8px; transform: translateX(-50%);
}
/* Holds the current reading, or the scrubbed one while a finger is down. One
   line, one job — swapping the content in place means no layout shift when a
   finger lands, and the baseline stays put whichever is showing. */
.now {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin: 0 0 10px; min-height: 34px;
  font-variant-numeric: tabular-nums;
  -webkit-user-select: none; user-select: none;
}
.now b {
  color: var(--frost); font-weight: 500; font-size: 30px; line-height: 1;
}
.now__note { color: var(--muted); font-size: 13px; }
/* The page still scrolls over it, and so does the pager. The hold that starts a
   scrub is arbitrated in JS: a finger that moves before the timer fires is a
   scroll and is handed straight back, and one that does not becomes a scrub and
   takes the gesture exclusively via the class below. */
.ribbon svg { touch-action: pan-x pan-y; }
.ribbon svg.is-scrubbing { touch-action: none; }

/* --- the pager -------------------------------------------------------- */

/* Native scroll-snap rather than a transform: it gives the rubber-banding and
   the mid-swipe tracking for free, and a hand-written version of either on a
   phone is a long way of arriving somewhere worse. */
.pager {
  display: flex; overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory; overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.pager::-webkit-scrollbar { display: none; }
.pane { flex: 0 0 100%; min-width: 0; scroll-snap-align: start; }

.pager__dots {
  display: flex; justify-content: center; gap: 7px;
  padding: 12px 0 0;
}
.pager__dot {
  appearance: none; border: 0; padding: 0; cursor: pointer;
  width: 22px; height: 3px; border-radius: 2px;
  background: var(--line); transition: background 180ms ease;
}
.pager__dot[aria-current="true"] { background: var(--muted); }
.pager__dot:focus-visible { outline: 2px solid var(--rest); outline-offset: 3px; }

/* --- the fortnight ---------------------------------------------------- */

/* The legend carries the current value of each axis, so the pane answers "where
   am I" as well as "which way is it going" and the swipe costs nothing. */
.trend__key { display: flex; align-items: baseline; gap: 7px; }
.trend__swatch { width: 10px; height: 3px; border-radius: 2px; }
.trend__key--effort .trend__swatch { background: var(--effort); }
.trend__key--movement .trend__swatch { background: var(--movement); }
.trend__name { color: var(--muted); font-size: 13px; }
.trend__value { color: var(--frost); font-size: 15px; font-weight: 500; }
.trend__value--over { color: var(--hard); }
.trend .now { font-size: 13px; gap: 16px; }
.trend__empty { color: var(--muted); font-size: 13px; }

/* A wider gutter than the ribbon's, because one of these labels is a word. "1.0"
   on its own does not say what it is a multiple of, and the sentence that would
   explain it sits below the fold on this pane. */
.trend .ribbon__plot { padding-left: 46px; }
.trend .ribbon__yaxis { width: 46px; }

/* One column per day, so a weekday sits under its own stem. The ribbon's hour marks
   solve the same problem the other way, by absolute position — there are five of them
   and they land on quarters rather than on cells. Both are measured from the plot's
   own left edge, which is the part that was wrong. */
.trend .ribbon__axis {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  text-align: center; padding-left: 46px;
}
.trend .ribbon__axis .is-now { color: var(--muted); }

.stale {
  margin: 14px var(--sp) 0; padding: 11px 14px;
  border: 1px solid color-mix(in oklab, var(--work) 40%, var(--line));
  border-radius: 10px; color: var(--work);
  font-size: 13px; background: color-mix(in oklab, var(--work) 7%, transparent);
}

/* --- how the day is going -------------------------------------------- */

.dayline {
  margin: 14px var(--sp) 0; color: var(--muted); font-size: 14px;
}
.dayline b {
  color: var(--frost); font-weight: 500; font-family: "Plex Mono", monospace;
}

/* --- load in context -------------------------------------------------- */

.load { padding: 22px var(--sp) 0; }
.load__bar { display: flex; gap: 4px; margin-bottom: 12px; }

/* One row per axis: what it is, how full it is, how full in figures. The two sit
   together because they disagree on most heavy days and the disagreement is the
   information. */
.axis { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.axis__name {
  flex: 0 0 74px; color: var(--muted); font-size: 11px;
  letter-spacing: 0.14em; text-transform: uppercase;
}
.axis .load__bar { flex: 1; margin-bottom: 0; }
.axis__pct {
  flex: 0 0 42px; text-align: right;
  font-family: "Plex Mono", monospace; font-variant-numeric: tabular-nums;
  font-size: 13px; color: var(--frost);
}
.axis__pct--heavy { color: var(--hard); }
.load__tick {
  flex: 1; height: 4px; border-radius: 2px; background: var(--line);
}
.load__tick--on { background: var(--work); }
/* No `--over` tick here: the fallback bar this serves cannot exceed a full day, and
   the standing gauge below draws its own over state. A rule nothing selects is a rule
   that outlives the reason for it. */

/* The standing gauge (ADR-0014). Continuous rather than ten ticks, because it has to
   draw two lengths against each other — what the day carried in and what it has added
   — and a tick that is half one and half the other cannot say which.

   The scale runs to twice the usual level, so the mark sits at the middle and a bad
   week has somewhere to go. */
.gauge {
  position: relative; flex: 1; display: flex;
  height: 6px; border-radius: 3px; background: var(--line); overflow: hidden;
}
/* What the days behind him left, after the night's decay. Dimmer than today's own
   work: it is the same substance, but it is not what he did this morning. */
.gauge__carried { background: color-mix(in oklab, var(--work) 45%, var(--ink-3)); }
.gauge__added { background: var(--work); }
.axis--movement .gauge__carried {
  background: color-mix(in oklab, var(--movement) 45%, var(--ink-3));
}
.axis--movement .gauge__added { background: var(--movement); }
/* Red is for the top tenth of his own days, not for being above the usual load —
   that is where he is about half the time, and a bar that alarms every other day
   stops being an alarm (ADR-0017). */
.gauge--heavy .gauge__carried { background: color-mix(in oklab, var(--hard) 45%, var(--ink-3)); }
.gauge--heavy .gauge__added { background: var(--hard); }
/* Where his own days usually reach. Drawn over the fill, never under it: the whole
   reading is the position of the fill against this mark. */
.gauge__usual {
  position: absolute; left: 50%; top: 0; bottom: 0; width: 1px;
  background: var(--ink); opacity: 0.85;
}
/* Off the end of the scale. Rare by construction, and it must not look like a bar
   that merely happens to be full. */
.gauge--past { border-radius: 3px 0 0 3px; }
.gauge--past::after {
  content: ""; position: absolute; right: 0; top: 0; bottom: 0; width: 5px;
  background: var(--ink);
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}
/* The mark means nothing unexplained, and an unexplained denominator is the opaque
   score this project exists to replace. One quiet line, once. */
.load__legend {
  margin: 2px 0 12px; color: var(--muted-dim); font-size: 11px;
  letter-spacing: 0.02em;
}
.load__line { margin: 0; font-size: 15px; }
.load__line strong { font-family: "Plex Mono", monospace; font-weight: 500; }
.load__sub { margin: 5px 0 0; color: var(--muted); font-size: 13px; }

/* --- the one thing that is due --------------------------------------- */

.action { padding: 26px var(--sp) 0; }

.card {
  background: var(--ink-2); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 22px;
}
.card__title {
  font-family: "Plex Cond", sans-serif; font-weight: 600;
  font-size: 23px; margin: 0 0 4px; letter-spacing: -0.005em;
}
.card__note { color: var(--muted); font-size: 14px; margin: 0 0 18px; }
.card__note:last-child { margin-bottom: 0; }

.btn {
  width: 100%; padding: 17px 20px; border-radius: 12px;
  border: 1px solid var(--line); background: var(--ink-3); color: var(--frost);
  font-family: "Plex Cond", sans-serif; font-weight: 600; font-size: 17px;
  letter-spacing: 0.005em; cursor: pointer;
  transition: transform 120ms ease, background 160ms ease;
}
.btn:active { transform: scale(0.985); }
.btn:focus-visible { outline: 2px solid var(--rest); outline-offset: 3px; }
.btn--primary {
  background: color-mix(in oklab, var(--rest) 26%, var(--ink-3));
  border-color: color-mix(in oklab, var(--rest) 50%, var(--line));
}

/* Scales: five targets big enough for cold hands. */
.scale { margin-bottom: 20px; }
.scale__label {
  display: flex; justify-content: space-between; align-items: baseline;
  color: var(--muted); font-size: 13px;
  letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 9px;
}
.scale__value { color: var(--frost); font-size: 13px; letter-spacing: 0; text-transform: none; }
.scale__row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 7px; }
.scale__dot {
  aspect-ratio: 1 / 1; border-radius: 11px;
  border: 1px solid var(--line); background: var(--ink-3);
  color: var(--muted-dim); font-family: "Plex Mono", monospace; font-size: 15px;
  cursor: pointer; transition: background 150ms ease, color 150ms ease;
}
.scale__dot[aria-pressed="true"] {
  background: color-mix(in oklab, var(--rest) 40%, var(--ink-3));
  border-color: var(--rest); color: var(--frost);
}
.scale__dot:focus-visible { outline: 2px solid var(--rest); outline-offset: 2px; }

.card__cancel { margin: 14px 0 0; text-align: center; }

/* Moving today's warm-up time (ADR-0015). A native time input, so the OS picker does
   the work — the alternative was nudge buttons, and the correction that actually
   happens is ninety minutes wide. Sized like the scales rather than like a form
   field: it is pressed with the same cold hands, once, and then the card closes.

   The number itself is Mono, as every figure here is, so 07:45 lines up with the
   07:45 the card was showing a moment ago. */
.timein {
  width: 100%; margin-bottom: 16px; padding: 15px 16px;
  border: 1px solid var(--line); border-radius: 12px;
  background: var(--ink-3); color: var(--frost);
  font-family: "Plex Mono", monospace; font-variant-numeric: tabular-nums;
  font-size: 22px; letter-spacing: 0.02em; text-align: center;
}
.timein:focus-visible { outline: 2px solid var(--rest); outline-offset: 3px; }
/* Safari and Chrome both hand the clock glyph the accent colour of the host page,
   which here would be the one colour reserved for exertion. */
.timein::-webkit-calendar-picker-indicator { filter: invert(0.55); cursor: pointer; }

.tags { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
.tag {
  padding: 9px 14px; border-radius: 999px;
  border: 1px solid var(--line); background: transparent; color: var(--muted);
  font-family: "Plex", sans-serif; font-size: 14px; cursor: pointer;
}
.tag[aria-pressed="true"] {
  border-color: var(--work); color: var(--work);
  background: color-mix(in oklab, var(--work) 12%, transparent);
}
.tag:focus-visible { outline: 2px solid var(--rest); outline-offset: 2px; }

/* --- the day still missing a label ------------------------------------ */

/* Where the 90-day strip used to be. The strip drew ninety facts and asked for
   nothing; this draws one and asks for a finger. It is absent on the days — most
   days — when there is nothing to catch up on. */
.recall { padding: 26px var(--sp) 0; }
.recall__btn {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  width: 100%; padding: 14px 16px; border-radius: var(--radius);
  border: 1px solid var(--line); background: var(--ink-2); color: var(--frost);
  font: inherit; text-align: left; cursor: pointer;
  transition: border-color 180ms ease, background 180ms ease, transform 120ms ease;
}
.recall__btn:active { transform: scale(0.99); }
.recall__btn:hover { border-color: var(--muted-dim); background: var(--ink-3); }
.recall__btn:focus-visible { outline: 2px solid var(--rest); outline-offset: 2px; }
.recall__text { font-size: 14px; }
.recall__more { color: var(--rest); font-size: 13px; white-space: nowrap; }

.linkish {
  padding: 0; border: 0; background: none; color: var(--rest);
  font: inherit; text-decoration: underline; text-underline-offset: 3px;
  cursor: pointer;
}
.linkish:focus-visible { outline: 2px solid var(--rest); outline-offset: 2px; }

/* --- footer ---------------------------------------------------------- */

.foot { padding: 30px var(--sp) 0; }
.foot__line { margin: 0 0 5px; color: var(--muted); font-size: 13px; }
.foot__line--muted { color: var(--muted-dim); }
/* Loss and silence are the two things in this line that are not routine, and they
   are the reason it exists. Everything else here stays the colour of a footnote. */
.foot__line--warn { color: var(--work); }
.foot__actions {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; margin: 18px 0 0;
}
.foot__forget {
  padding: 0; border: 0; background: none;
  color: var(--muted-dim); font-family: inherit; font-size: 13px;
  text-decoration: underline; text-underline-offset: 3px; cursor: pointer;
}

/* --- toast ----------------------------------------------------------- */

.toast {
  position: fixed; left: 50%; bottom: calc(24px + env(safe-area-inset-bottom));
  transform: translate(-50%, 14px);
  padding: 11px 20px; border-radius: 999px;
  background: var(--ink-3); border: 1px solid var(--line); color: var(--frost);
  font-size: 14px; opacity: 0; pointer-events: none;
  transition: opacity 200ms ease, transform 200ms ease;
}
.toast--on { opacity: 1; transform: translate(-50%, 0); }

/* --- motion ----------------------------------------------------------- */

/* Motion here has one job: to say "this arrived just now" once, and then stop.
 *
 * The page is glanced at, often outdoors, often with one hand and a cold thumb.
 * Anything that keeps moving after the first second competes with the trace for
 * attention and loses the reader the thing they opened the app to see. So: a single
 * settle on first paint, a single draw-on for the trace, and exactly one thing that
 * repeats — the pulse on the live reading, because it is the only part of the page
 * that is still happening.
 *
 * Every duration is under a second. A phone taken out of a pocket to check one number
 * should not be showing an animation by the time it is being read.
 */

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

/* Staggered by section, top to bottom, so the eye lands where the day starts.
   Small increments: the last one is in place 260 ms after the first. */
#app > * { animation: settle 420ms cubic-bezier(.22,.61,.36,1) both; }
#app > .head    { animation-delay: 0ms; }
/* The plots are inside the pager now, so the selector that used to catch the ribbon
   directly no longer does. Without this line the whole frame arrived with the header
   instead of after it, and the stagger lost its first step. */
#app > .pager   { animation-delay: 40ms; }
#app > .pager__dots { animation-delay: 70ms; }
#app > .dayline { animation-delay: 90ms; }
#app > .load    { animation-delay: 130ms; }
#app > .action  { animation-delay: 170ms; }
#app > .recall  { animation-delay: 210ms; }
#app > .foot    { animation-delay: 250ms; }

/* The fill comes up under the trace as it draws, rather than being there before it.
   Scoped to `.is-intro`, which JS sets on the first paint only: the SVG is rebuilt on
   every refresh, and without the class the fill would re-animate each time the phone
   is unlocked — which reads as a delay, not as arrival. */
.ribbon svg.is-intro path[fill^="url"] { animation: settle 620ms 120ms ease-out both; }

/* The one repeating thing on the page. Slow, small, and on the live reading only —
   a past day has nothing that is still happening, and does not get it. */
@keyframes pulse {
  0%, 100% { opacity: 0.9; r: 3; }
  50%      { opacity: 0.35; r: 5; }
}
.ribbon__pulse { animation: pulse 2.8s ease-in-out infinite; }

/* The card that asks for something is the only card that appears, so it may say so
   once. Anything more insistent than this reads as an alert, and it is not one. */
.action .card { animation: settle 380ms 120ms cubic-bezier(.22,.61,.36,1) both; }

/* Scrubbing dims everything below the plot, so the reading being pulled out is the
   only lit thing. One opacity change, driven by a class on <body> rather than by
   :has() — the state is already known in JS, and a selector that silently does
   nothing on an older browser is a worse way to find that out. */
.dayline, .load, .action { transition: opacity 200ms ease; }
body.is-scrubbing .dayline,
body.is-scrubbing .load,
body.is-scrubbing .action { opacity: 0.4; }

@media (prefers-reduced-motion: reduce) {
  /* Not merely faster: none of it. The settle and the pulse are decoration, and a
     reader who has asked for stillness is asking for the numbers, not the arrival. */
  *, *::before, *::after {
    transition-duration: 1ms !important;
    animation: none !important;
  }
}
