/* ═══════════════════════════════════════════════════════════════════════════
   VESPER, the app's own layer. STRUCTURE ONLY.

   design/DECISIONS.md section 14: the visual language is NOT final. He rejected
   it on 2026-08-27 as flat and generic and it is being redone in Codex against
   more references. So this build is features, state and structure, and the look
   stays swappable.

   THE RULE THIS FILE KEEPS. Every colour, every type size, every space here is
   a token out of vesper.css. Nothing paints with a literal, nothing is measured
   in JavaScript, and there are no inline styles anywhere in the app. A redesign
   is therefore a pass over vesper.css and this file, not a rewrite of app.js.

   vesper.css is the token and component layer and it is copied in from
   design/system/vesper.css by app/bin/sync-design.mjs. Do not edit the copy.
   Classes prefixed vsp- come from there. Classes with no prefix are this file,
   which is the app's own furniture: the frame, the pager, how a screen scrolls,
   and the handful of layouts the four tabs and the corridor need.
   ═══════════════════════════════════════════════════════════════════════════ */

* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  background: var(--vsp-base);
  color: var(--vsp-ink);
  font-family: var(--vsp-ui);
  -webkit-font-smoothing: antialiased;
  overscroll-behavior: none;
}

/* The hidden attribute is how every screen in this app turns a state off, and a
   class that sets display quietly defeats it: .vsp-empty is display grid, so an
   empty state stayed on screen underneath a full one. This is the one !important
   in the app and it exists so that hidden means hidden. */
[hidden] { display: none !important; }

/* A row, a card and an intake option are all real buttons, because a thing you
   tap is a button and a div with a click handler is not. A button arrives with
   a fill, a border, a centred label, its own font and its own shrink-to-fit
   width that it was never asked for, and on this ground that reads as a broken
   screen. This is the same bug vesper.css section 6 fixes for the tab.

   It is an element selector, so every class in vesper.css still wins over it. */
button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  margin: 0;
  -webkit-appearance: none;
  appearance: none;
}
.vsp-row, .vsp-card, .vsp-opt { width: 100%; text-align: left; }

/* ── 1. The frame ─────────────────────────────────────────────────────────
   On his phone this is the whole viewport. There is no desktop frame: the app
   is installed to a home screen. */
.app {
  position: fixed;
  inset: 0;
  overflow: hidden;
  isolation: isolate;
}

.screens { position: absolute; inset: 0; z-index: 2; }
.pager {
  position: absolute; inset: 0;
  display: flex;
  will-change: transform;
}
.pane { position: relative; width: 100%; height: 100%; flex: none; overflow: hidden; }

/* what is behind the corridor is GONE, not dimmed. vesper.js sets this class. */
.vsp-hidden-behind { opacity: 0; }

/* A pushed screen: a sub-screen of a tab, a corridor screen, or a first-run
   question. It starts off to the right and is out of the tab order until it is
   in. vesper.js toggles is-in. */
.over {
  position: absolute; inset: 0; z-index: 3;
  visibility: hidden;
  will-change: transform;
}
.over.is-in { visibility: visible; }
/* What a pushed screen sits on, and design/mockups/motion-v1.html is the
   authority for it rather than the scaffold's simplification.

   A SUB-SCREEN takes a wash: the living ground still reads through it and the
   screen underneath is dimmed, offset and blurred out of legibility. The wash
   is the veil three times over, because one was thin enough to leave the row
   labels behind it readable, and two screens at once is the same failure the
   corridor's opacity rule exists to prevent.

   A CORRIDOR SCREEN takes no wash at all, data-solid. The pager beneath it is
   at opacity 0 rather than covered, so the ground reads straight through and
   Tonight is genuinely gone rather than hidden behind something. Cold means
   cold, visually as well as structurally. */
.over::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: var(--vsp-veil), var(--vsp-veil), var(--vsp-veil);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
  transition: opacity var(--vsp-t-world) ease;
}
.over[data-solid]::before {
  background: none;
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
.pager { transition: opacity var(--vsp-t-move) var(--vsp-sp-move); }

/* The first run's ground deepens as he moves through it, one step per area of
   HIM.md, so the last question sits somewhere darker than the first.
   DECISIONS.md section 4. */
.over[data-depth="1"]::before { opacity: 0.46; }
.over[data-depth="2"]::before { opacity: 0.58; }
.over[data-depth="3"]::before { opacity: 0.70; }
.over[data-depth="4"]::before { opacity: 0.80; }
.over[data-depth="5"]::before { opacity: 0.90; }
.over[data-depth="6"]::before { opacity: 1; }

/* The scrolling body of any screen. Nothing else in the app scrolls. */
.vp {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  padding: 0 var(--vsp-pad);
  padding-top: max(env(safe-area-inset-top, 0px), var(--vsp-s-4));
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.vp::-webkit-scrollbar { display: none; }

/* A flex column shrinks its children before it scrolls, so at 200 percent text
   the glass cards were squeezed and clipped their own rows instead of the
   screen getting longer. Nothing in a scrolling screen shrinks. The two things
   that are meant to fill, the writing surface and a first-run question, say so
   themselves further down and win on source order. */
.vp > * { flex: none; }

/* Room at the foot of a tab screen for the floating bar. */
.clear { height: var(--vsp-tabs-clear); flex: none; }

/* The way on, pinned to the bottom of a corridor screen. It arrives after the
   thing it lets him leave. */
.foot {
  margin-top: auto;
  padding-bottom: max(env(safe-area-inset-bottom, 0px), var(--vsp-s-7));
  padding-top: var(--vsp-s-6);
  display: grid; gap: var(--vsp-s-3);
  flex: none;
}
/* A foot on a screen that keeps the tab bar has to clear it. After .foot, or
   the two rules tie on specificity and .foot's padding wins on source order. */
.foot-bar { padding-bottom: var(--vsp-tabs-clear); }

/* ── 2. Stacks ────────────────────────────────────────────────────────────
   Siblings in a column, one gap between them. Every vertical rhythm in the app
   is this or the screen gutter. */
.stack { display: grid; gap: var(--vsp-gap); align-content: start; }
.stack-wide { display: grid; gap: var(--vsp-s-5); align-content: start; }

/* A glass card holding several rows, hairline between them. */
.group { padding: 0; overflow: hidden; }
.group > * + * { border-top: 1px solid var(--vsp-hair); }

/* A row of controls side by side. */
.side { display: flex; gap: var(--vsp-s-3); align-items: center; }
.side > .grow { flex: 1; min-width: 0; }

/* The head of a screen: a label over a title, tight. */
.head { display: grid; gap: var(--vsp-s-2); padding-bottom: var(--vsp-s-5); }
.head-line { display: flex; align-items: baseline; justify-content: space-between; gap: var(--vsp-s-4); }

/* Space above a block that starts a new thought. Margin rather than padding,
   because .set also sits on glass cards and padding would put the gap inside
   the card and leave the two cards touching. */
.set { margin-top: var(--vsp-s-7); }

/* ── 3. The stagger index ─────────────────────────────────────────────────
   vesper.css staggers a screen's children by --vsp-i. It is set here by
   position rather than written onto each element, so nothing in the markup or
   in app.js carries a style attribute and a list built at runtime staggers
   without being told to.

   Fourteen because the first run is fourteen questions and no screen in the app
   has more direct children than that. */
.vsp-stagger > *:nth-child(1),  .steps > *:nth-child(1)  { --vsp-i: 0; }
.vsp-stagger > *:nth-child(2),  .steps > *:nth-child(2)  { --vsp-i: 1; }
.vsp-stagger > *:nth-child(3),  .steps > *:nth-child(3)  { --vsp-i: 2; }
.vsp-stagger > *:nth-child(4),  .steps > *:nth-child(4)  { --vsp-i: 3; }
.vsp-stagger > *:nth-child(5),  .steps > *:nth-child(5)  { --vsp-i: 4; }
.vsp-stagger > *:nth-child(6),  .steps > *:nth-child(6)  { --vsp-i: 5; }
.vsp-stagger > *:nth-child(7),  .steps > *:nth-child(7)  { --vsp-i: 6; }
.vsp-stagger > *:nth-child(8),  .steps > *:nth-child(8)  { --vsp-i: 7; }
.vsp-stagger > *:nth-child(9),  .steps > *:nth-child(9)  { --vsp-i: 8; }
.vsp-stagger > *:nth-child(10), .steps > *:nth-child(10) { --vsp-i: 9; }
.vsp-stagger > *:nth-child(11), .steps > *:nth-child(11) { --vsp-i: 10; }
.vsp-stagger > *:nth-child(12), .steps > *:nth-child(12) { --vsp-i: 11; }
.vsp-stagger > *:nth-child(13), .steps > *:nth-child(13) { --vsp-i: 12; }
.vsp-stagger > *:nth-child(14), .steps > *:nth-child(14) { --vsp-i: 13; }

/* ── 4. Lists ─────────────────────────────────────────────────────────────
   Every list in the app is a ul with the bullets off. */
.list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--vsp-gap); }
.list-tight { gap: 0; }

/* His OWED 3 ruling: everything, under the month it happened, twelve at a time.
   The month is a label and the count beside a title is a plain figure that does
   not animate, because a number easing toward a figure is a progress ring. */
.month { padding: var(--vsp-s-7) 0 var(--vsp-s-3); }
.month:first-child { padding-top: var(--vsp-s-3); }

/* A dated line: the date above, the thing under it. Used by the record, the
   list of what he has been taught, his questions kept, and what it knows. */
.dated { display: grid; gap: var(--vsp-s-2); padding: var(--vsp-s-5) 0; }
.dated + .dated { border-top: 1px solid var(--vsp-hair); }

/* ── 5. The corridor ──────────────────────────────────────────────────────
   Three screens, one way. No bar, no gesture, and OWED 18's way out is a word
   at the top beside the lesson label, at label size and in the quiet ink, so it
   reads as furniture rather than as an offer. */
.corridor-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--vsp-s-4);
  padding-bottom: var(--vsp-s-5);
}
.leave {
  background: none; border: 0; padding: var(--vsp-s-2) 0 var(--vsp-s-2) var(--vsp-s-4);
  font: 600 var(--vsp-t-label)/1.2 var(--vsp-ui);
  letter-spacing: var(--vsp-tr-label); text-transform: uppercase;
  color: var(--vsp-ink-quiet);
  cursor: pointer;
}

/* The lesson prose. */
.prose > * { margin: 0 0 var(--vsp-s-6); }
.prose > *:last-child { margin-bottom: 0; }
.prose h2 {
  font: 500 var(--vsp-t-body)/1.3 var(--vsp-ui);
  letter-spacing: var(--vsp-tr-label); text-transform: uppercase;
  color: var(--vsp-ink-3);
  padding-top: var(--vsp-s-4);
}

/* The writing surface. It fills the screen because writing is the default
   production, and the field has no visible box: DECISIONS.md section 5. */
.write { display: flex; flex-direction: column; flex: 1; min-height: 0; }

/* ── 6. The confirmation dialog ───────────────────────────────────────────
   Two of them: leaving the lesson early, his OWED 18 ruling, and turning down
   the notification. It sits over the screen it belongs to rather than floating
   above the whole app, and staying is the plain control while leaving is the
   quiet one, because leaving must be available and does not need encouraging. */
.ask-over {
  position: absolute; inset: 0; z-index: 6;
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: var(--vsp-pad);
  padding-bottom: max(env(safe-area-inset-bottom, 0px), var(--vsp-s-7));
  background: var(--vsp-scrim);
  opacity: 0;
  transition: opacity var(--vsp-t-fade) ease;
}
.ask-over.is-in { opacity: 1; }
.ask-box {
  padding: var(--vsp-s-6);
  display: grid; gap: var(--vsp-s-5);
  transform: translate3d(0, 18px, 0);
  transition: transform var(--vsp-t-move) var(--vsp-sp-settle);
}
.ask-over.is-in .ask-box { transform: translate3d(0, 0, 0); }

/* ── 7. The gate ──────────────────────────────────────────────────────────
   OWED 6: the key field is not masked. He is pasting a long random string on a
   phone in the dark and could not see whether the paste took, and nobody is
   reading over his shoulder for a key to his own app. */
.gate { position: absolute; inset: 0; z-index: 8; display: flex; flex-direction: column; }
.gate-body { margin: auto 0; display: grid; gap: var(--vsp-s-5); }

/* ── 8. Waiting, failing, offline ─────────────────────────────────────────
   OWED 5. The shape of the thing coming, then the thing. A failure says what
   happened and what to do, in that order, in the Harbour world rather than as
   a browser error, because it is read at 18:35 by someone about to go on shift.

   The offline line is quieter than a banner on purpose. It is a fact about the
   signal, not an alarm. */
.waiting { display: grid; gap: var(--vsp-s-6); padding: var(--vsp-s-7) 0; }
.failed { display: grid; gap: var(--vsp-s-5); padding: var(--vsp-s-7) 0; justify-items: start; }
.failed p { font: 300 var(--vsp-t-read)/var(--vsp-lh-read) var(--vsp-read);
            color: var(--vsp-ink-read); margin: 0; text-wrap: pretty; }
.offline { display: flex; align-items: center; gap: var(--vsp-s-3); padding: var(--vsp-s-3) 0; }
.offline-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--vsp-half); flex: none; }

/* ── 9. The first run ─────────────────────────────────────────────────────
   One question a screen, tap to choose, and the ground deepens as he goes
   through so the last question sits somewhere darker than the first.
   DECISIONS.md section 4. */
.intake { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.intake-q { padding-bottom: var(--vsp-s-6); display: grid; gap: var(--vsp-s-4); }
.intake-nav { display: flex; gap: var(--vsp-s-3); align-items: center; }
.intake-nav > .grow { flex: 1; }

/* ── 10. Odds ─────────────────────────────────────────────────────────────
   Two things that carry no layout of their own and exist so nothing in app.js
   has to reach for a style attribute. */
.tint { color: var(--vsp-tint); }
.hide { display: none !important; }
