/* Cotena design tokens — see docs/DESIGN.md.
 *
 * Colour carries STATE, never category (§3). Categories are distinguished by
 * type, position and icon. Nine colours exist; anything outside this list is a
 * bug. No gradients as decoration, no shadows — depth is surface value plus a
 * 1px hairline.
 *
 * ── One palette, authored once. This file is it. ─────────────────────────────
 *
 * R3 in DESIGN.md says "one palette, authored once", and until now that was
 * false in a way nothing could catch. The palette was written twice — here, and
 * again as hex literals in `tailwind.config.js` — with no build step relating
 * the copies, and `make css` baked the Tailwind copy into the checked-in
 * `app.css` as literal channels:
 *
 *     .bg-page { background-color: rgb(20 18 15 / var(--tw-bg-opacity,1)) }
 *
 * Zero `var(--page)`/`var(--card)` references in 2,600 compiled utilities. So editing a hex
 * here changed the hand-written rules below and nothing else, and the two
 * halves of the app drifted apart silently — no error, no failing test, just
 * cards in one colour and fields in another.
 *
 * Now the channels are the source and Tailwind is told to compose them:
 *
 *     colors: { void: 'rgb(var(--c-page) / <alpha-value>)' }
 *     -> .bg-void      { background-color: rgb(var(--c-page) / var(--tw-bg-opacity,1)) }
 *     -> .bg-void\/60  { background-color: rgb(var(--c-page) / 0.6) }
 *
 * Two consequences worth having. Every compiled utility now resolves through
 * this file, so the palette really does live in one place and the opacity
 * modifiers (`bg-page/60`, the sheet scrims) keep working. And a colour scheme
 * becomes a media query over eleven lines rather than a recompile. There is no
 * light scheme in this pass — it was argued for and refused, on the arithmetic
 * in DESIGN.md §3 — but it is now a costed change rather than a rewrite.
 *
 * Channels are space-separated RGB, not hex, because that is the only form
 * `rgb()` can take an alpha into. The `--page` / `--accent` / `--ink` aliases
 * are resolved colours, kept so the hand-written rules further down this file
 * read as colours rather than as channel arithmetic.
 */
:root {
  /* Surfaces — depth is surface value, not shadow. Warm, not the cold blue-black
     it was: #08080A at 1.08:1 from its own card gave the app the depth of a
     switched-off screen, and a near-black with gold on it is a luxury-watch
     advert, which is a register this app has no business borrowing. */
  --c-page:   20 18 15;    /* #14120F — app background */
  --c-card:   29 27 23;    /* #1D1B17 — cards, sheets, the working surface */
  --c-well:  38 35 32;    /* #262320 — a surface on a surface: inputs, pressed rows */
  --c-rule:   58 53 47;    /* #3A352F — 1px hairlines. 1.54:1 on the page, up from 1.32 */

  /* The accent. A pale violet-slate — diluted ink, not a pastel and not a glow.
     Reached by arithmetic rather than taste: the best accessibility property this
     system has is a light fill carrying near-black text (a 64px primary, legible
     at arm's length), and all 105 on-fill sites depend on it. Keeping that
     requires the accent to clear 7:1 against the page, i.e. relative luminance
     ≥ 0.325 — and at that luminance the only families available are cream, gold,
     lime, cyan and pink. Cream collapses into the text colour's 413 sites. Gold
     IS brass, the thing being removed. Lime is Whoop and Nike. Cyan sits ~50°
     from --live and would make "is this the button or is this done?" a colour
     judgement at 12px. Pink polarises across a 25-to-70 audience. */
  --c-accent:     201 188 228;  /* #C9BCE4 — 10.50:1 as text on the page */
  --c-accent-hot: 220 216 242;  /* #DCD8F2 — pressed. Never less legible than at rest */

  /* Text */
  --c-ink:     244 241 234;  /* #F4F1EA — primary. Warm, never pure white */
  --c-ink-dim: 181 174 166;  /* #B5AEA6 — secondary. 7.12:1 on --well, up from 4.93 */

  /* State — and nothing else gets a colour */
  --c-live: 127 214 155;   /* #7FD69B — in progress, done. Calmer than notification green */
  --c-alert: 240 154 134;   /* #F09A86 — see the licence below. NOT "over your target" */

  /* Resolved aliases, for the hand-written rules in this file. */
  --page:   rgb(var(--c-page));
  --card:   rgb(var(--c-card));
  --well:  rgb(var(--c-well));
  --rule:   rgb(var(--c-rule));
  --accent:      rgb(var(--c-accent));
  --accent-hot:  rgb(var(--c-accent-hot));
  --accent-mute: rgb(var(--c-accent) / 0.12);
  --ink:      rgb(var(--c-ink));
  --ink-dim:  rgb(var(--c-ink-dim));
  --live:       rgb(var(--c-live));
  --alert:       rgb(var(--c-alert));

  /* Text on a filled accent or state colour. Identical to the page by value and
     deliberately separate by name: 105 sites currently say `text-void`, which
     reads as "the page background reused as text" and is not what any of them
     means — three of them sit on --live and --alert fills, not the accent, so
     naming this after the accent would be wrong within a week. */
  --c-on-fill: 20 18 15;
  --on-fill:   rgb(var(--c-on-fill));

  /* Type roles (§4). Data speaks in Archivo; the app speaks in Literata. */
  --font-readout: 'Archivo', system-ui, sans-serif;
  --font-ui:      'Archivo', system-ui, sans-serif;
  --font-voice:   'Literata', Georgia, serif;

  /* Space — 4px base. Only these values. */
  --s-1: 4px; --s-2: 8px; --s-3: 12px; --s-4: 16px;
  --s-6: 24px; --s-8: 32px; --s-12: 48px;
  --gutter: 20px;

  /* Radius — 0 full-bleed, 14 cards, 999 pills. 6px (rounded-md) is banned. */
  --r-card: 14px;
  --r-pill: 999px;

  /* Structure */
  --reach-top: 58vh;
  --tabbar-h: 56px;
  --target-action: 48px;
  --target-primary: 64px;
  --target-ledger: 44px;

  /* Motion — three motions exist (§7) */
  --m-commit: 140ms;
  --m-sheet: 260ms;
  --e-sheet: cubic-bezier(.32, .72, 0, 1);
}

/* ── The Readout: a number you change by touching it (§6) ────────────────── */

.readout {
  font-family: var(--font-readout);

  /* 100%, not 125%. DESIGN.md §4 chose Archivo Expanded in as many words
     because "a wide grotesque at 88px reads like a timing display or a
     scoreboard, which is the gym's own vernacular" — and the gym's vernacular is
     the thing being removed. Normal width keeps the two properties that were
     actually load-bearing (tabular stability, and not looking like Inter) and
     drops the stadium clock. One value, zero bytes: the vendored Archivo covers
     wdth 62.5%-125% already. */
  font-stretch: 100%;

  /* 650, not 600. Expanded at 600 read heavier because there is more ink per
     glyph; at normal width the same nominal weight loses the number's presence
     against its own label. Inside the vendored 400-700 range, so also free. */
  font-weight: 650;

  /* FUNCTIONAL, NOT STYLISTIC — do not remove. _rack.html's rest timer counts
     down in place and the keypad and commit screens update under the thumb.
     Proportional figures change width as the digits change, so the number
     visibly jitters, and jitter reads as cheap. */
  font-variant-numeric: tabular-nums;

  line-height: 0.9;
  color: var(--ink);
}
.readout-xl { font-size: 88px; line-height: 0.9; }
.readout-l  { font-size: 56px; line-height: 0.95; }
.readout-m  { font-size: 32px; line-height: 1; }
.readout-s  { font-size: 22px; line-height: 1.1; }

/* A unit is never the same size as its value — but 12px uppercase was the wrong
   way to make that true. This is the label under the largest number on the
   screen, and it was set at the type floor, in caps, with 0.06em of tracking:
   all-caps destroys the word shape a low-vision reader recognises before they
   resolve letters, and tracking at 12px makes each glyph a separate object to
   assemble. 15px sentence case is still unmistakably subordinate to a 32-88px
   readout, and it is readable at arm's length.

   The uppercase is verified test-free before removal: journey03_units_test.go:55
   and journey06_test.go:236 compare with strings.EqualFold, and
   e2e_layout_test.go:392 reads textContent, which text-transform never
   touches. */
.unit {
  font-family: var(--font-ui);
  font-stretch: 100%;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--ink-dim);
  line-height: 1.2;
}

/* The app's own voice — wordmark, screen titles, empty states. Never data.
   A READING serif, not a display serif: low stroke contrast and a large
   x-height are what survive 28px light-on-dark, where optical stroke thinning
   is worst. See fonts.css for why the face changed and why it is pinned at 450
   rather than 400. */
.voice {
  font-family: var(--font-voice);
  font-size: 28px;
  line-height: 1.2;
  font-weight: 450;
  letter-spacing: -0.005em;
  color: var(--ink);
}

/* ── The chain ─────────────────────────────────────────────────────────────
 *
 * Cotena is quotidie + catena: the daily chain. The name describes an object —
 * a row of links, one added each day — which is a form rather than a feeling,
 * and that is the whole differentiation. Every rival in this category brands on
 * transformation: before-and-after, peaks, arrows, flames. Cotena brands on
 * accumulation, which is also the honest description of what the product does.
 *
 * It is drawn in exactly TWO places: the app mark, and the Home progress fill.
 *
 * Home is the only determinate fill in the product — `pctOf` occurs once, and
 * render.go clamps it to 100 — so this bar mathematically cannot show a gap or
 * an overshoot. That is the reason it is allowed to be a chain at all: there is
 * no broken-chain state, so there is nothing to punish anybody for. Do NOT put
 * this on the capture progress bars; capture_type.js caps them at 92% by design
 * and they would show an unfinished chain on every single AI call.
 *
 * And it is never a count. No streak number, no chain length, no "day N", no
 * consecutive-day figure, no flame, no calendar of filled squares, no pass/fail
 * mark, and no copy about keeping or breaking it — not in the app, not on the
 * splash, not in a store screenshot. A chain of days is a mechanic that fails by
 * definition: every chain breaks, and the day it breaks is the day the person
 * most needed the app not to comment. No such data exists in this codebase and
 * adding it is a new mechanic, not a reskin.
 */
:root { --link-run: 9px; --link-gap: 3px; }

.chain-fill {
  background-color: transparent;
  background-image: repeating-linear-gradient(90deg,
    var(--accent) 0 var(--link-run),
    transparent var(--link-run) calc(var(--link-run) + var(--link-gap)));
  border-radius: var(--r-pill);
}

/* ── The document never scrolls sideways ──────────────────────────────────
 *
 * The section navs are full-bleed: they pull themselves outside the page gutter
 * with `-mx-gutter` so the row of pills can scroll to the physical edge of the
 * screen. That only works while the page's own padding IS the gutter. Pages
 * carrying `px-4` (16px) made the nav 4px wider than the viewport on each side,
 * and the whole document picked up 8px of horizontal scroll — small enough to
 * look like a rendering fault rather than a layout bug, which is exactly how it
 * was reported.
 *
 * The mismatched pages are fixed, but this is the guarantee: nothing in this app
 * is meant to scroll the document sideways.
 *
 * `clip`, not `hidden`: `overflow-x: hidden` makes the element a scroll
 * container, which silently breaks `position: sticky` inside it.
 */
html { overflow-x: clip; }

/* ── Fields ───────────────────────────────────────────────────────────────
 *
 * A field is a SURFACE, not a box with a line round it: --well sitting on
 * --card, one hairline, the card radius. That is the same depth model as
 * every other component here, and it is the whole difference between looking
 * like an app and looking like a web form.
 *
 * Four rules earn their place:
 *
 *   - 17px, never smaller. Mobile Safari zooms the entire page when a focused
 *     input is under 16px, and that zoom is the single loudest "this is a
 *     website" tell there is. It cannot be turned off from CSS.
 *   - 52px tall. The 48px action target plus room, so a thumb hits it and the
 *     text is not vertically crushed against the border.
 *   - appearance: none. Otherwise iOS renders its own inset shadow inside
 *     every input and select, and no amount of border styling removes it.
 *   - Focus thickens the hairline to brass WITHOUT reflowing: border-color
 *     plus a zero-offset outline, so the edge reads as 2px and nothing moves.
 *     The default focus-visible ring is deliberately overridden here — a
 *     detached 2px halo at 2px offset is the web-form look we are removing —
 *     but only for fields, which keep an unmistakable focused state of their
 *     own. Everything else still gets the ring.
 */

/* Sentence case, like every other label in the app. The uppercase here was
   asserted by tests/e2e_misc_test.go against progress_photos/compare.html's
   "Before"/"After" — chromedp.Text returns rendered text, so the transform was
   load-bearing on a test, and that test's comment repeated DESIGN.md's error
   that `micro` is "the only uppercase in the system". It never was: this rule
   and `.unit` were. Both are now sentence case and the assertion moved with
   them. */
.field-label {
  display: block;
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--ink-dim);
  margin-bottom: var(--s-2);
}

.field-input,
.field-select,
.field-textarea {
  display: block;
  width: 100%;
  min-height: 52px;
  padding: 14px var(--s-4);
  font-family: var(--font-ui);
  font-size: 17px;
  line-height: 1.3;
  color: var(--ink);
  background-color: var(--well);
  border: 1px solid var(--rule);
  border-radius: var(--r-card);
  -webkit-appearance: none;
  appearance: none;
  transition: border-color var(--m-commit) ease-out,
              background-color var(--m-commit) ease-out;
}

.field-textarea {
  min-height: 92px;
  padding-top: var(--s-3);
  line-height: 1.45;
  resize: vertical;
}

.field-input::placeholder,
.field-textarea::placeholder { color: var(--ink-dim); }

.field-input:focus,
.field-select:focus,
.field-textarea:focus {
  outline: 1px solid var(--accent);
  outline-offset: 0;
  border-color: var(--accent);
  background-color: var(--card);
}

/* The chevron is drawn here rather than left to the platform, because the
   platform's is black-on-white inside a dark field.
 *
 * This is the one hardcoded colour left in the system, and it is hardcoded
 * because it cannot be anything else: the glyph lives inside a percent-encoded
 * data URI in a background-image, and a data URI cannot read a custom property
 * or inherit currentColor. Masking the element instead is not the way out —
 * `mask-image` clips the whole element, so the field's own surface, border and
 * text would go with it.
 *
 * So it stays a literal, and TestSelectChevronMatchesItsToken in
 * tests/palette_test.go asserts the literal equals --c-ink-dim. Before that
 * test existed this was the only colour in the app that no palette change could
 * reach: the 20px glyph whose entire job is to say "this is a select" would have
 * kept the old dim grey on the new surface, at 2.4:1, with nothing failing and
 * nobody looking. */
.field-select {
  padding-right: 44px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%23B5AEA6'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='m19.5 8.25-7.5 7.5-7.5-7.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--s-4) center;
  background-size: 20px 20px;
}

/* A field carrying a rejected value says so on the field, not only in a toast
   the user has already scrolled past. */
.field-input[aria-invalid="true"],
.field-select[aria-invalid="true"],
.field-textarea[aria-invalid="true"] { border-color: var(--alert); }

/* A Readout you type into.
 *
 * The number itself stays a Readout — same face, same tabular figures, same
 * size — but it sits on the same raised surface and hairline as every other
 * field in the app, so "this is a control you tap" is carried by one visual
 * rule rather than learned per screen. Contrast is untouched: --ink on
 * --well is the pairing the type scale was checked against.
 */
.readout-field {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-6);
  background-color: var(--well);
  border: 1px solid var(--rule);
  border-radius: var(--r-card);
  transition: border-color var(--m-commit) ease-out;
}
.readout-field:active { border-color: var(--accent); }
.readout-field:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.field-hint {
  display: block;
  font-family: var(--font-ui);
  font-size: 14px;
  line-height: 1.3;
  color: var(--ink-dim);
  margin-top: 6px;
}

/* Date, time and number inputs render platform chrome — a picker indicator, a
   spinner — in the OS's light palette unless the page declares its scheme.
   One line, and every native picker in the app turns dark. */
:root { color-scheme: dark; }

/* Number spinners are a mouse affordance on a touch-first app, and they steal
   8px from the value. */
.field-input[type="number"]::-webkit-outer-spin-button,
.field-input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.field-input[type="number"] { -moz-appearance: textfield; }

/* The picker indicator is dark-on-dark by default once color-scheme is set;
   lift it to the dim text colour so it reads as a control. */
.field-input::-webkit-calendar-picker-indicator { opacity: 0.55; cursor: pointer; }
.field-input::-webkit-calendar-picker-indicator:hover { opacity: 1; }

/* ── Motion (§7). Three motions; everything else is not authored. ─────────── */

@keyframes commit-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
.commit { animation: commit-in var(--m-commit) ease-out; }

.sheet {
  transition: transform var(--m-sheet) var(--e-sheet);
}

@media (prefers-reduced-motion: reduce) {
  .commit { animation: none; }
  .sheet  { transition: opacity 100ms linear; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
  }
}

/* ── Accessibility floor (§11) ───────────────────────────────────────────── */

:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ── Swipe-to-reveal rows (web/static/js/swipe.js) ───────────────────────── */

.swipe-row { position: relative; overflow: hidden; }
.swipe-content {
  position: relative;
  z-index: 1;
  background: var(--page);
  will-change: transform;
  /* Let the browser own vertical scrolling; the script only claims horizontal
     gestures, so panning down a long list is never interrupted. */
  touch-action: pan-y;
}
.swipe-actions { position: absolute; inset: 0 0 0 auto; display: flex; align-items: stretch; }
.swipe-actions button,
.swipe-actions a {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 80px;
  padding: 0 var(--s-4);
  font-family: var(--font-ui);
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
  background: var(--well);
}
.swipe-actions .is-destructive button, .swipe-actions button.is-destructive {
  background: var(--alert);
  color: var(--page);
}
/* A row inside a slab needs matching content, or the sliding panel reveals the
   wrong colour behind it. */
.bg-card .swipe-content { background: var(--card); }

/* ── R5: not yet real ─────────────────────────────────────────────────────
 *
 * A hairline brass left edge is the app's single visual vocabulary for a
 * mutation that is queued locally and has not reached the server yet
 * (DESIGN.md R5). One device, used everywhere, so it never has to be explained
 * twice — and deliberately not a colour change on the row itself, because
 * colour carries state and "pending" is not the same state as "over target".
 *
 * Behaviour reads data-* only (R2): removing this rule changes appearance and
 * nothing else.
 */
[data-pending] {
  border-left: 2px solid var(--accent);
  padding-left: var(--s-3);
}

/*
 * The working screen. "Waiting is a screen, not a spinner in a corner. If the
 * AI is thinking, that is the whole screen, and it says what it is doing."
 *
 * display, NOT opacity. htmx's own .htmx-indicator hides with opacity:0, and an
 * element at opacity 0 still takes every click in its box — so a full-screen
 * working overlay built that way is invisible and, throughout the entire time
 * the user is typing, on top. The symptom is a screen where nothing responds and
 * nothing appears to be wrong.
 */
.working-overlay { display: none; }
.working-overlay.htmx-request { display: flex; }

/*
 * ── Navigation without the flash: tried, and not shipped ──────────────────
 *
 * Every navigation here is a real page load, and a real page load paints white
 * between the old document and the new one. That flash is a large part of why
 * navigation "feels slow" independently of how long it takes, and cross-document
 * view transitions fix it in one at-rule:
 *
 *     @view-transition { navigation: auto; }
 *
 * It works, and it is not enabled, because it breaks the e2e suite in a way
 * that is not the suite's fault to fix cheaply. During a cross-document
 * transition the browser snapshots the outgoing document and swaps the live
 * one underneath it; every CDP node id chromedp is holding goes stale at that
 * moment. Eight tests failed — "Could not find node with given id" on controls
 * that were visible a line earlier, and 60s context timeouts on the
 * navigation-heavy ones. Making them pass means adding settle-and-requery logic
 * to every navigation in the suite, which is a large amount of test machinery
 * to buy a 120ms cross-fade.
 *
 * Worth revisiting if the suite ever grows a navigation helper that re-resolves
 * nodes after settling — at that point this is one line and a re-run.
 */

/*
 * ── Tappability of non-interactive dismiss targets ────────────────────────
 *
 * Every sheet dismisses by tapping its scrim, and every scrim is a bare <div>
 * whose handler is delegated on `document`. In WKWebView that combination does
 * not work: WebKit only bubbles a click up from an element it considers
 * clickable — a button, a link, something with an inline onclick, or something
 * with cursor:pointer. A plain div gets the touch and the event stops there, so
 * the delegated listener never runs and the tap is silently dropped.
 *
 * The symptom is not a slow dismiss, it is a dead one: the scrim works
 * sometimes, because the tap happened to land on a child that IS clickable.
 * That reads as flakiness rather than as a missing CSS declaration, which is
 * why it survived so long.
 *
 * cursor:pointer is inert on touch — there is no cursor — so this buys the
 * event delegation and costs nothing else.
 */
[data-capture-close],
[data-capture-back],
[data-capture-open],
[data-sheet-dismiss-all],
[data-stepper-close],
[data-keypad-close] {
  cursor: pointer;
}

/*
 * ── Pressed feedback ──────────────────────────────────────────────────────
 *
 * A tap that starts a network request must say so before the network answers,
 * or the user's only evidence that anything happened is the screen changing —
 * which is exactly the thing that is slow. `:active` alone is not enough: iOS
 * withholds it until it has decided the touch is not a scroll, so the state
 * that exists to acknowledge a tap arrives after the tap.
 *
 * `[data-pressed]` is set on pointerdown by native.js, unconditionally and
 * without waiting for anything. R2: behaviour reads data-*, and removing this
 * rule changes only how the press looks.
 */
[data-pressed] {
  opacity: 0.6;
  transition: opacity 60ms linear;
}

/*
 * ── In-flight htmx controls ───────────────────────────────────────────────
 *
 * htmx adds .htmx-request to the element that issued a request for the life of
 * that request. Three screens opted into a full working overlay; every other
 * htmx control in the app had no pending state at all, so a ✕ or a chooser row
 * looked identical whether the tap had registered or not. On a round trip to
 * eu-west-2 that is a second or more of a control that appears dead, and the
 * user's rational response is to tap it again.
 *
 * Scoped away from .working-overlay, which owns its own display rule above,
 * and away from <body>, which wears .htmx-request for the length of every
 * hx-boost navigation — dimming and freezing the entire page on each one is
 * the opposite of the point.
 */
.htmx-request:not(.working-overlay):not(body) {
  opacity: 0.55;
  pointer-events: none;
}
