/* MODULE: css/chassis.css — the shell frame, the header chassis, the mode
   switch, the transport zone and the day-ribbon strip's placement (PHASE4 §2
   and §4). Depends on css/tokens.css, css/type.css, css/base.css and
   css/materials.css, all of which load before it. Loads nothing itself: no
   @import, no font, no url() that is not already declared in materials.css.

   It does NOT own the dial's artwork (css/dial.css), the instrument's own
   layout (css/modes.css), the Panel's finished design (css/panel.css), the
   dashboard grid (css/layout.css) or any module (css/mod-*.css).

   ── WHAT THIS FILE IS FOR ─────────────────────────────────────────────
   PHASE4 §2 replaces the scrolling dashboard with a single fixed screen:

       1  header — brand mark, CLOCK/TIMER/ALARM, two icon controls
       2  the dial
       3  the digital readout
       4  the session label
       5  the transport (reset · START · +1 · +5)
       6  one thin day-ribbon strip

   "No page scroll on any device in the default state. If it scrolls, the
   design has failed." Everything else is in the Panel.

   ── HOW THE NO-SCROLL GUARANTEE IS BUILT ──────────────────────────────
   Not with viewport-height arithmetic per element — that breaks the moment
   a sequence stepper appears or a font falls back. ONE definite height is
   handed down an unbroken chain, and the dial absorbs whatever is left:

       .app-shell    block-size: 100dvh − the safe areas       (definite)
         ├ .chassis    auto      the header band
         ├ .app-stage  minmax(0, 1fr)                          (definite)
         │   └ .mode-panel   flex: 1 1 auto  (css/modes.css)
         │       └ .tt-timer     the instrument, sized by css/modes.css:
         │                       the dial is sized on the BLOCK axis and is
         │                       the element that gives way when it is tight
         └ .day-strip  auto      ~10px of track and one line of type

   This file states the heights of rows 1 and 3 and hands row 2 a definite
   box; css/modes.css does the rest inside it. .app-stage is the one scroll
   port in the app, which is what lets the ALARM list — a genuinely
   unbounded list — scroll without the page ever moving.

   ── REACHING INTO OTHER AGENTS' COMPONENTS ────────────────────────────
   Only two rules below touch a mode's element, both in §4, both about the
   HEIGHT BUDGET rather than about how the instrument is drawn. Each is
   written one specificity step above the rule it corrects
   (`.app-stage .tt-timer__readout`, never `.tt-timer__readout`) so load
   order cannot undo it, and each carries a CONTRACT-GAP note saying what
   the owning agent could adopt to delete it.
   ────────────────────────────────────────────────────────────────────── */

/* The Panel fallback in §8 is the one thing here that another agent is
   expected to replace wholesale. Declaring the layer up front means every
   unlayered rule in css/panel.css outranks it whatever its specificity, so
   src/panel.js can take the container over without out-specifying anything
   or knowing this file exists. Nothing else in this sheet is layered. */
@layer tt-shell-fallback;

/* ============================================================== *
 * 0. LOCAL TOKENS
 *    Composed from css/tokens.css. Nothing here is a new value that
 *    could have come from the design system.
 * ============================================================== */

:root {
  /* THE ONE DEFINITE HEIGHT. svh first for engines without dvh; dvh is what
     PHASE2 §5 requires, because 100vh is wrong in mobile Safari and would
     push the transport under the browser chrome. Both are measured against
     the CURRENT viewport, so neither can ever exceed it — the safe-area
     insets are subtracted because css/base.css spends them as padding on
     <body>, outside this box. */
  --ch-shell-h: calc(100svh - var(--safe-top) - var(--safe-bottom));

  /* The header band's inner padding, and the height of its first row —
     one touch target, so the grille band can be centred on it by maths
     rather than by eye. */
  --ch-chassis-pad: var(--space-2);
  --ch-chassis-row: var(--tap);

  /* The day-ribbon strip: PHASE4 §2 asks for ~10px of track and a single
     line of type. 10 + 8 + 16 = 34px, and 8 and 16 are grid units. */
  --ch-strip-track: 10px;

  /* Icon optical size, composed from the space scale rather than guessed —
     the same 20px css/layout.css derives for its own furniture, so every
     glyph in the app is cut to one size. */
  --ch-icon: calc(var(--space-4) + var(--space-1));

  /* THE READOUT'S CEILING. --fs-display is fluid in the INLINE direction
     (13vw) because that is the axis a clamp can see. On a wide, short
     window — 1280×800 is exactly that, and a real browser window at that
     size is shorter still — 13vw resolves to the 136px ceiling and the
     numerals eat the dial's height budget: at 1280×800 the disc lands
     around 270px against 136px of type, which is not the instrument the
     Time Timer is. Capping the numerals at 13svh (104px there, and no
     change at all on a phone, where --fs-display is already 56px) hands
     those pixels back to the disc. It is a PROPORTION, not a type size:
     the scale token still decides everything until the window gets short
     enough to argue. svh, not dvh, so the numerals do not resize when
     mobile Safari's chrome slides away. */
  --ch-readout-cap: 13svh;

  /* The Panel fallback. ~380px per PHASE4 §2. */
  --ch-panel-w: 23.75rem;
  --ch-panel-sheet-h: 72svh;

  /* An opaque nameplate that occludes the grille flutes beneath it, fading
     out on its trailing edge so it does not read as a pasted-on box. The
     0%-mix is --bg with zero alpha: interpolating to the keyword
     `transparent` would drag the ramp through transparent BLACK and grey
     the fade in ceramic. */
  --ch-plate-end: color-mix(in oklab, var(--bg) 0%, transparent);

  /* A recess milled into a metal billet: dark at the top lip, a lit lower
     lip, no drop shadow at all — a machined pocket is IN the surface, it
     does not sit on it. Built from the two non-inverting physical inks in
     css/materials.css so it stays correct in both themes. */
  --ch-machined-recess:
    inset 0 1px 2px var(--m-cast),
    inset 0 3px 6px color-mix(in oklab, var(--m-cast) 55%, transparent),
    inset 0 6px 14px color-mix(in oklab, var(--m-cast) 28%, transparent),
    inset 0 -1px 0 var(--m-lift);
}

@supports (block-size: 100dvh) {
  :root {
    --ch-shell-h: calc(100dvh - var(--safe-top) - var(--safe-bottom));
  }
}

/* ============================================================== *
 * 1. THE SHELL FRAME
 *    Three rows, one definite height, no page scroll.
 * ============================================================== */

.app-shell {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  gap: var(--space-4);
  block-size: var(--ch-shell-h);
  /* css/base.css sets min-block-size: 100% for the old scrolling shell.
     A minimum taller than the definite height would win and reintroduce the
     scroll this whole file exists to remove. */
  min-block-size: 0;
}

/* No JavaScript: there is no instrument to fit on one screen, only the
   <noscript> explanation, so the page becomes an ordinary document again
   and is allowed to scroll. The boot script in index.html stamps
   data-js="1" before first paint, so this never flashes. */
html:not([data-js]) .app-shell {
  block-size: auto;
  min-block-size: var(--ch-shell-h);
}

/* ============================================================== *
 * 2. THE CHASSIS — the header as a moulded front panel
 *
 *    .m-chassis (css/materials.css) supplies the ceramic tile, the case
 *    radius and the cast shadow onto the plaster. This file supplies the
 *    grid and the grille.
 * ============================================================== */

.chassis {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas: "brand modes tools";
  align-items: center;
  gap: var(--space-3);
  padding: var(--ch-chassis-pad);
  /* The grille shares its cells with the brand and the tools, so the stack
     inside the header needs a context of its own. */
  isolation: isolate;
}

/* PERMITTED FLUTE 1 (PHASE4 §4) — the grille band across the chassis
   header, behind the brand mark.
 *
 *  It spans the whole of the first row and paints a --m-grille-h band
 *  centred in it, so the same rule is correct whether the header is one
 *  row wide or two rows tall. The brand and the tools sit ON it, on their
 *  own opaque plates: the band genuinely runs BEHIND the mark, and the
 *  rule that no striping ever sits behind TEXT is kept, because the plates
 *  occlude every flute under a letterform. The ends fade out through
 *  --m-fade-mask so the striping stops rather than butting into the case
 *  edge. It does not animate, and there is nothing here that could. */
.chassis__grille {
  grid-column: 1 / -1;
  grid-row: 1;
  z-index: 0;
  align-self: stretch;
  min-block-size: var(--ch-chassis-row);
  /* .m-flute supplies background-image: var(--m-flute-image). */
  background-size: 100% var(--m-grille-h);
  background-position: 50% 50%;
  background-repeat: no-repeat;
  -webkit-mask-image: var(--m-fade-mask);
  mask-image: var(--m-fade-mask);
  pointer-events: none;
}

/* --- the nameplate --------------------------------------------------- */

.brand {
  grid-area: brand;
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-block-size: var(--ch-chassis-row);
  min-inline-size: 0;
  padding-inline: var(--space-1) var(--space-5);
  margin-inline-start: calc(var(--space-1) * -1);
  /* The plate: opaque over the flutes, fading out on the trailing edge. */
  background-image: linear-gradient(
    90deg,
    var(--bg) 0,
    var(--bg) calc(100% - var(--space-5)),
    var(--ch-plate-end) 100%
  );
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  font-weight: var(--wt-medium);
  line-height: var(--lh-body);
  letter-spacing: var(--tr-title);
  font-variation-settings: var(--vf-body);
  user-select: none;
  -webkit-user-select: none;
}

.brand__mark {
  inline-size: var(--ch-icon);
  block-size: var(--ch-icon);
  flex: 0 0 auto;
  color: var(--ink-3);
}

.brand__ring {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
}

.brand__sector {
  fill: var(--ink-3);
  stroke: none;
}

/* --- the two icon controls ------------------------------------------- */

.chassis__tools {
  grid-area: tools;
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
  min-block-size: var(--ch-chassis-row);
  padding-inline: var(--space-5) var(--space-1);
  margin-inline-end: calc(var(--space-1) * -1);
  background-image: linear-gradient(
    270deg,
    var(--bg) 0,
    var(--bg) calc(100% - var(--space-5)),
    var(--ch-plate-end) 100%
  );
}

.icon {
  inline-size: var(--ch-icon);
  block-size: var(--ch-icon);
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Slider knobs sit on the settings glyph's rails and must occlude them. */
.icon__knob {
  fill: var(--bg-raised);
  stroke: currentColor;
  stroke-width: 1.75;
}

/* The Panel key latches while the Panel is open — it is a toggle, and a
   toggle that is on reads as a key held down (css/base.css .is-latched). */
.chassis__tools .btn[aria-expanded="true"] {
  --key-face: var(--key-face-press);
  --key-image: var(--key-image-press);
  --key-pos: var(--key-pos-press);
  color: var(--ink);
  box-shadow: var(--shadow-press);
  transform: translateY(1px);
}

/* --- the narrow header ------------------------------------------------ *
   Below 48rem the brand, the selector and two keys cannot share one line
   without the selector's legends colliding, so the selector takes a line of
   its own and runs the full width of the case — which is what a real
   Braun mode bar does anyway. The grille stays on the first row. */
@media (max-width: 47.999rem) {
  .chassis {
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "brand tools"
      "modes modes";
    row-gap: var(--space-2);
  }

  .chassis__grille {
    grid-column: 1 / -1;
    grid-row: 1;
  }
}

/* ============================================================== *
 * 3. THE MODE SWITCH — a machined aluminium segmented control
 *
 *    PHASE4 §4: metal for things you touch. The whole selector is one
 *    piece of brushed aluminium inside a chrome bezel frame, and the
 *    ENGAGED segment is a pocket MILLED INTO it — which is what a real
 *    machined selector does, and the opposite of a web tab that floats up
 *    off its track.
 *
 *    This deliberately supersedes css/materials.css §6, which skins the
 *    track as a ceramic recess with the engaged key standing proud. Both
 *    are defensible; PHASE4 §4 lists the mode switch under METAL, and a
 *    milled pocket is the more honest reading of a physical selector. The
 *    frame treatment (2px chrome ring, clipped border-box) is materials'
 *    and is reproduced here only because a single element cannot carry two
 *    independent background stacks.
 *
 *    CONTRACT-GAP (materials agent): if css/materials.css adopts the
 *    brushed track and the milled pocket itself, delete §3 entirely.
 *
 *    CONTRAST, against the measured table in css/materials.css's header:
 *      unengaged  --ink-2 on brushed metal   6.92 : 1  /  5.26 : 1
 *      engaged    --ink   on pressed metal  12.20 : 1  / 11.99 : 1
 *    --ink-3 is never placed on metal (3.88:1 at night), which is why an
 *    unengaged legend is --ink-2 here and not the --ink-3 it used to be.
 * ============================================================== */

.modes {
  grid-area: modes;
  position: relative;
  z-index: 1;
  min-inline-size: 0;
  justify-self: center;
  inline-size: 100%;
  max-inline-size: 26rem;
}

/* The billet: chrome ring on the border box, brushed alloy on the padding
   box. Four layers, one per background-* list position, so nothing in
   materials' three-layer metal recipe has to be re-derived. */
.chassis .segmented {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  /* CONTRACT with css/motion.css §3: that sheet slides the engaged face
     between the three positions by translating it a whole column plus this
     gap, and reads the figure from --mo-seg-gap, whose default it sets to
     calc(--space-1 / 2). Declaring the same value here is what keeps the
     slide landing exactly on each key — matching its assumption is more
     robust than out-specifying it. */
  gap: calc(var(--space-1) / 2);
  inline-size: 100%;
  padding: calc(var(--space-1) - var(--m-frame-w));
  border: var(--m-frame-w) solid transparent;
  border-radius: var(--radius-pill);
  background-color: transparent;
  background-image: var(--m-brushed-spec), var(--m-brushed-body),
    var(--m-tex-alu), var(--m-chrome-image);
  background-clip: padding-box, padding-box, padding-box, border-box;
  background-origin: border-box, border-box, border-box, border-box;
  background-size: 100% 100%, 100% 100%, cover, 100% 100%;
  background-repeat: no-repeat, no-repeat, no-repeat, no-repeat;
  background-position: 0 0, 0 0, 50% 50%, 0 0;
  background-blend-mode: normal, normal, normal, normal;
  /* A billet sits ON the chassis: a hairline seat and a tight contact
     shadow, no floating elevation. */
  box-shadow:
    inset 0 var(--hairline-w) 0 var(--m-lift),
    0 1px 1px var(--m-cast);
}

.chassis .segmented__key {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-block-size: var(--tap);
  padding-inline: var(--space-3);
  border: 0;
  border-radius: var(--radius-pill);
  background-color: transparent;
  background-image: none;
  color: var(--ink-2);
  box-shadow: none;
  font-family: var(--font-sans);
  font-size: var(--fs-label);
  font-weight: var(--wt-medium);
  line-height: var(--lh-label);
  letter-spacing: var(--tr-label);
  font-variation-settings: var(--vf-label);
  font-feature-settings: var(--feat-caps);
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition:
    color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-med) var(--ease-out),
    background-color var(--dur-med) var(--ease-out);
}

/* The ring sits 2px outside the key; an offset outline would be drawn on
   top of it. Inset instead, so the focus ring reads as a lit edge inside
   the pocket. */
.chassis .segmented__key:focus-visible {
  outline-offset: calc(var(--ring-offset) * -1);
}

@media (hover: hover) {
  .chassis .segmented__key[aria-selected="false"]:hover {
    color: var(--ink);
  }
}

/* THE POCKET, ON THE KEY. Same alloy, in shadow, cut into the billet.
   Ordered after the rules above and specific enough (0,4,0) to beat
   css/materials.css's `.segmented .segmented__key[aria-selected="true"]`
   (0,3,0), which would otherwise raise it back out of the surface.

   This is the STATIC form, and it is what renders when css/motion.css is
   absent or :has() is unsupported. When both are present that sheet blanks
   every key's own face at (0,5,0) and moves the face onto a sliding
   pseudo-element instead; the block after this one re-cuts the pocket
   there, so the two treatments never both paint. */
.chassis .segmented .segmented__key[aria-selected="true"] {
  background-color: var(--m-face-press);
  background-image: var(--m-metal-image-press);
  background-size: var(--m-metal-size);
  background-repeat: var(--m-metal-repeat);
  background-position: var(--m-metal-pos-press);
  background-blend-mode: var(--m-metal-blend);
  color: var(--ink);
  box-shadow: var(--ch-machined-recess);
}

/* A key already at the bottom of its pocket has nowhere to travel, so it
   does not move on press — it only darkens. The unengaged keys do travel:
   they are still keys. */
.chassis .segmented .segmented__key[aria-selected="false"]:active {
  background-color: color-mix(in oklab, var(--m-face-press) 60%, transparent);
  box-shadow: var(--ch-machined-recess);
  transition-duration: var(--dur-press);
}

/* THE POCKET, SLIDING.
   css/motion.css §3 gives the switcher a single face on .segmented::before
   that slides between the three positions on a spring — PHASE3 §3's "one
   movement, no bounce-and-settle theatrics" — and dresses it as a RAISED
   key, because that is the treatment materials.css publishes. On a machined
   billet the moving part is the pocket, not a proud tile, so only the paint
   is re-pointed here: the geometry, the slide, the spring and the press
   travel all stay that file's.

   The selectors mirror its own, prefixed with .chassis, purely to clear its
   specificity — (0,6,0) and (0,5,0) against its (0,5,0) and (0,4,0). A
   browser without :has() drops these rules entirely and falls back to the
   static pocket above, which is the correct behaviour in both directions. */
.chassis
  .segmented:has(> .segmented__key:nth-child(3)):not(:has(> .segmented__key:nth-child(4)))::before {
  background-color: var(--m-face-press);
  background-image: var(--m-metal-image-press);
  background-size: var(--m-metal-size);
  background-repeat: var(--m-metal-repeat);
  background-position: var(--m-metal-pos-press);
  background-blend-mode: var(--m-metal-blend);
  box-shadow: var(--ch-machined-recess);
}

/* …AND THE POCKET HAS TO ARRIVE AT THE RIGHT KEY.
   css/motion.css parks the indicator with `translate: 0` in a rule selected
   by
       .segmented:has(> …:nth-child(3)):not(:has(> …:nth-child(4)))::before
   which is (0,5,0), while the two rules that move it are selected by
       .segmented:has(> …:nth-child(2)[aria-selected="true"])::before
   which is only (0,4,0). The parked value therefore wins outright and the
   face never leaves the first key — verified in the browser: with TIMER
   engaged the computed `translate` was 0px and the pocket sat under CLOCK.

   Re-stated here at (0,6,0), with the same arithmetic and the same
   --mo-seg-gap, so the switcher is correct today.

   CONTRACT-GAP (motion agent): add `:not(:has(> .segmented__key:nth-child(4)))`
   to css/motion.css §3's two translate rules — or drop it from the parked
   rule — and delete this block; it sets identical values, so removing it
   after the fix changes nothing on screen. */
.chassis
  .segmented:has(> .segmented__key:nth-child(2)[aria-selected="true"]):not(
    :has(> .segmented__key:nth-child(4))
  )::before {
  translate: calc(100% + var(--mo-seg-gap, 0px));
}

.chassis
  .segmented:has(> .segmented__key:nth-child(3)[aria-selected="true"]):not(
    :has(> .segmented__key:nth-child(4))
  )::before {
  translate: calc(200% + var(--mo-seg-gap, 0px) * 2);
}

/* Pressing the engaged key: a pocket cannot travel further down, so the
   shadow deepens instead of the face moving. */
.chassis .segmented:has(> .segmented__key[aria-selected="true"]:active)::before {
  transform: none;
  box-shadow:
    inset 0 2px 3px var(--m-cast),
    inset 0 5px 10px color-mix(in oklab, var(--m-cast) 60%, transparent),
    inset 0 10px 20px color-mix(in oklab, var(--m-cast) 30%, transparent),
    inset 0 -1px 0 var(--m-lift);
}

/* The indicator lamp beside the engaged legend. The one signal-coloured
   thing in the header, and the only colour in the whole chassis. */
.segmented__led {
  inline-size: 6px;
  block-size: 6px;
  flex: 0 0 auto;
  border-radius: var(--radius-pill);
  background-color: var(--signal);
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-out);
}

.chassis .segmented__key[aria-selected="true"] .segmented__led {
  opacity: 1;
}

/* ============================================================== *
 * 4. THE STAGE — the instrument, and nothing else
 *
 *    The instrument's own layout belongs to css/modes.css, which sizes the
 *    dial on the block axis for exactly this reason. This section gives it
 *    a definite box to work in and arbitrates the two things a mode cannot
 *    know about from inside itself: how tall the shell's other two rows
 *    are, and what PHASE4 §2 says is allowed on the screen at all.
 * ============================================================== */

.app-stage {
  /* Flex, not grid, so css/modes.css's `.mode-panel { flex: 1 1 auto }`
     does what its author intended and the mode fills the row. */
  display: flex;
  flex-direction: column;
  min-block-size: 0;
  min-inline-size: 0;
  /* The app's ONE scroll port. In the default view nothing reaches it: the
     instrument is sized to fit. It exists for the ALARM list, which is a
     genuinely unbounded list, and as the safety valve on a viewport too
     short for the instrument — in both cases the PAGE still does not move,
     which is the requirement. */
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

.mode-panel {
  min-block-size: 0;
  min-inline-size: 0;
}

.mode-panel:focus {
  outline: none;
}

/* --- the numerals' height ceiling ------------------------------------ *
   PHASE4 §3 makes the numerals the composition, and css/type.css §5 owns
   their treatment entirely — 300 weight, −0.035em, opsz 32, tabular, and
   the ban on shadows, gradients and animation. The ONE thing added here is
   the block-axis ceiling, and it is added here rather than in css/modes.css
   because it is a shell-level height decision: only this file knows that a
   header band and a day-ribbon strip are also on the screen. See
   --ch-readout-cap for the arithmetic.

   CONTRACT-GAP (modes agent): if css/modes.css takes --ch-readout-cap into
   its own .tt-timer__readout / .tt-clock__hm rules, delete this block. */
.app-stage .tt-timer__readout,
.app-stage .tt-clock__hm {
  font-size: min(var(--fs-display), var(--ch-readout-cap));
}

/* --- everything PHASE4 §2 does not list ------------------------------ *
   The face (Original / Custom) and repeat toggles are not among the six
   things the default view is allowed to show, and both already have a
   home: Settings → Appearance → dial scale, and Settings → Behaviour →
   repeat. Evicting them hands 56px back to the disc, which is the control
   the whole product is about. That is this file's reading of "that is the
   whole app", and this rule is it.

   css/modes.css restores the row at (0,3,0) and argues the other side in
   writing — that Original vs Custom is the control which explains what the
   disc MEANS, and that src/tutorial.js steps 4 and 5 target this row by
   selector, so hiding it silently drops two of the tour's fifteen steps.
   Both are good points, and the second one is a real regression.

   SETTLED (integrator): the row STAYS, and this rule is deleted rather
   than left to lose quietly, so css/modes.css is now its only owner.

   Three reasons the mode's reading wins. PHASE4 overrides PHASE2 §2–§4 and
   says so by name; it does not override PHASE2 §6, which calls Original vs
   Custom "the single most confusing control in the app" and spends a whole
   tutorial step on it. Second, the face toggle is not a seventh thing
   competing with the instrument — it is the caption on the disc, the one
   control that says what the red area MEANS, and PHASE4 §2's list already
   counts the readout and the label as part of the instrument on the same
   logic. Third, hiding it silently drops tutorial steps 4 and 5, and a
   tour that teaches thirteen of fifteen features is a regression a user
   would notice where 56px of disc is one they would not.

   Cost, measured and accepted: the dial is 262px rather than 318px at
   1280×800, and 128px rather than ~188px at 320×568. The no-scroll
   guarantee holds either way — verified at 320×568, 390×844, 768×1024,
   1280×800 and 844×390 with the row visible. */

/* ============================================================== *
 * 5. THE TRANSPORT ZONE
 *
 *    PERMITTED FLUTE 4 — the structural band marking this zone's top edge
 *    — is applied by css/materials.css §5 through .m-edge-flute, which
 *    src/modes/timer.js now carries on the transport itself. The grid
 *    inside it belongs to css/modes.css. What is left, and what this file
 *    owns, is one guarantee: you never have to scroll to reach START.
 * ============================================================== */

/* On a window short enough that the instrument cannot fit even after the
   dial has given up everything it can, .app-stage scrolls — and the one
   thing that must not scroll away is the control you came for. Sticky
   costs nothing in the normal case, because there is no overflow to stick
   against, so it is scoped to the case that needs it rather than left on
   permanently where its backing plate would be a flat patch on a textured
   body. 40rem ≈ 640px: below that, no arrangement of dial, numerals,
   label and transport clears the two shell rows. */
@media (max-height: 40rem) {
  .app-stage .tt-timer__transport {
    position: sticky;
    inset-block-end: 0;
    z-index: var(--z-raised);
    background-color: var(--bg);
  }
}

/* ============================================================== *
 * 6. THE DAY-RIBBON STRIP
 *
 *    PHASE4 §2, item 6: one thin strip, full width, ~10px tall, with a
 *    single line of type. The track is CERAMIC — it is a surface you look
 *    at, not one you touch — so it takes no metal and no chrome.
 *
 *    index.html renders a self-contained fallback into this element so the
 *    default view is complete on its own. CONTRACT-GAP (modules agent /
 *    integrator): mounting the real src/modules/day-ribbon.js here instead
 *    is a matter of setting data-strip-enhanced on #day-strip, which stops
 *    the fallback renderer; these placement rules stay valid either way.
 * ============================================================== */

.day-strip {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-inline-size: 0;
}

.day-strip__track {
  position: relative;
  block-size: var(--ch-strip-track);
  inline-size: 100%;
  border-radius: var(--radius-pill);
  background-color: var(--bg-sunk);
  box-shadow: var(--shadow-well-s);
  overflow: hidden;
}

/* Time LEFT is red, the same convention the disc uses. The spent part of
   the day is the bare recess, so the red shrinks from the left as the day
   goes — the ribbon depletes exactly the way the disc does. */
.day-strip__left {
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  inset-inline-start: 100%;
  background-color: var(--signal);
  transition: inset-inline-start var(--dur-slow) var(--ease-out);
}

/* Hour gridlines: structure, not decoration — they are what turns a bar
   into a scale. --hairline-strong, never pure black, and drawn as one
   repeating gradient rather than as elements. --ch-strip-hour is written
   by the shell from the real waking window, so the ticks stay one per hour
   when that window is edited. */
.day-strip__ticks {
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    90deg,
    var(--hairline-strong) 0 var(--hairline-w),
    transparent var(--hairline-w) var(--ch-strip-hour, 6.667%)
  );
  pointer-events: none;
}

.day-strip__line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  min-inline-size: 0;
}

.day-strip__read {
  min-inline-size: 0;
  overflow: hidden;
  color: var(--ink-2);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.day-strip__window {
  flex: 0 0 auto;
  color: var(--ink-3);
}

/* Outside the waking window entirely: the strip says so plainly, and the
   track drops to the spent colour rather than pretending there is day
   left to look forward to. */
.day-strip[data-state="before"] .day-strip__left,
.day-strip[data-state="after"] .day-strip__left {
  background-color: var(--signal-dim);
}

/* ============================================================== *
 * 7. FOCUS MODE (CONTRACT §11, PHASE4 §2)
 *    Chrome away. The dial, the numerals and the session label remain.
 *    src/modes/timer.js already strips its own chrome off
 *    .tt-timer[data-focus="1"]; this removes the shell's.
 * ============================================================== */

html:not([data-focus="true"]) .focus-exit {
  display: none;
}

.focus-exit {
  position: fixed;
  inset-block-start: calc(var(--safe-top) + var(--space-3));
  inset-inline-end: calc(var(--safe-right) + var(--space-3));
  z-index: var(--z-sticky);
}

html[data-focus="true"] .chassis,
html[data-focus="true"] .day-strip {
  display: none;
}

html[data-focus="true"] .focus-exit {
  display: inline-flex;
}

/* ============================================================== *
 * 8. THE PANEL — the container, and a working fallback
 *
 *    #panel-root is the mount point PHASE4 §2 gives to src/panel.js. It is
 *    kept deliberately BARE: that file appends its own fixed-position root,
 *    scrim and live region into it, so anything this sheet did to the
 *    container — a position, a background, a display:none while closed —
 *    would be inherited by a Panel that manages all three itself.
 *
 *    The fallback is therefore a SIBLING (#panel-fallback), not a child,
 *    and index.html hides it the moment #panel-root gains a child. It is
 *    additionally inside @layer tt-shell-fallback, so any unlayered rule in
 *    css/panel.css outranks it whatever the specificity.
 *
 *    CONTRACT-GAP (integrator, src/main.js): src/panel.js is on disk and is
 *    not imported yet. Call createPanel(document.getElementById('panel-root'),
 *    store, ctx) from boot() and this fallback retires itself — no change to
 *    index.html or to this file is needed.
 * ============================================================== */

/* Not layered: the container must be inert whoever owns it. */
.app-panel-mount {
  display: contents;
}

@layer tt-shell-fallback {
  .app-panel {
    position: fixed;
    z-index: var(--z-modal);
    display: none;
    flex-direction: column;
    min-block-size: 0;
    color: var(--ink);
    /* Ceramic: a panel is a surface you look at. The veil recipe has to be
       declared on the element that uses it — see css/materials.css §3. */
    --m-base: var(--surface);
    --m-tile-veil: color-mix(in oklab, var(--m-base) var(--m-veil-tile), transparent);
    --m-tile-image: linear-gradient(var(--m-tile-veil), var(--m-tile-veil)),
      var(--m-tex-ceramic);
    background-color: var(--m-base);
    background-image: var(--m-tile-image);
    background-size: var(--m-tile-size);
    background-repeat: var(--m-tile-repeat);
    background-blend-mode: var(--m-tile-blend);
    box-shadow:
      inset 0 var(--hairline-w) 0 var(--m-lift),
      0 0 0 var(--hairline-w) var(--hairline),
      0 8px 32px var(--m-cast);
    transition: translate var(--dur-med) var(--ease-out);
  }

  html[data-panel="open"] .app-panel {
    display: flex;
    translate: 0 0;
  }

  .app-panel__head {
    display: grid;
    grid-template-columns: var(--tap) minmax(0, 1fr) var(--tap);
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    border-block-end: var(--hairline-w) solid var(--hairline);
  }

  .app-panel__title {
    grid-column: 2;
    color: var(--ink-3);
    text-align: center;
  }

  .app-panel__close {
    grid-column: 3;
  }

  .app-panel__body {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
    min-block-size: 0;
    padding: var(--space-4);
    padding-block-end: max(var(--space-4), var(--safe-bottom));
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
  }

  /* The scrim. Dimmed but still visible — PHASE4 §2 is explicit that the
     instrument stays legible behind the drawer. */
  .app-scrim {
    position: fixed;
    inset: 0;
    z-index: calc(var(--z-modal) - 1);
    display: none;
    background-color: color-mix(in oklab, var(--ramp-900) 26%, transparent);
    opacity: 0;
    transition: opacity var(--dur-med) var(--ease-out);
  }

  html[data-panel="open"] .app-scrim {
    display: block;
    opacity: 1;
  }

  /* MOBILE — a bottom sheet with the knurled grip at its head. */
  @media (max-width: 63.999rem) {
    .app-panel {
      inset-block-end: 0;
      inset-inline: 0;
      max-block-size: var(--ch-panel-sheet-h);
      border-start-start-radius: calc(var(--radius-l) + var(--space-1));
      border-start-end-radius: calc(var(--radius-l) + var(--space-1));
      translate: 0 100%;
    }
  }

  /* DESKTOP — a drawer beside the instrument. The instrument does not move
     or resize: the drawer is fixed and overlays it. */
  @media (min-width: 64rem) {
    .app-panel {
      inset-block: 0;
      inset-inline-end: 0;
      inline-size: min(var(--ch-panel-w), 100%);
      translate: 100% 0;
    }

    .app-panel__grip {
      display: none;
    }
  }
}

/* ============================================================== *
 * 9. THE NO-JAVASCRIPT FALLBACK
 * ============================================================== */

.noscript {
  align-self: start;
  max-inline-size: var(--measure);
  margin-block: var(--space-5);
  padding: var(--space-5);
  border: var(--hairline-w) solid var(--hairline);
  border-radius: var(--radius-l);
  background-color: var(--surface);
  box-shadow: var(--shadow-panel);
}

.noscript h2 {
  margin-block-end: var(--space-2);
}

.noscript p {
  color: var(--ink-2);
}

.noscript p + p {
  margin-block-start: var(--space-3);
}

/* ============================================================== *
 * 10. PREFERENCE OVERRIDES
 * ============================================================== */

/* css/base.css already collapses every duration to 0.01ms under reduced
   motion, which covers the Panel's slide and the strip's fill. Stated again
   for the three properties this file animates directly, so the rule holds
   even if this sheet is read on its own. */
@media (prefers-reduced-motion: reduce) {
  .app-panel,
  .app-scrim,
  .day-strip__left {
    transition: none;
  }
}

@media (forced-colors: active) {
  /* The system owns every surface: the grille, the plates and the milled
     pocket are all material, and material does not survive forced colours.
     The engaged segment keeps its meaning through a real border. */
  .chassis__grille,
  .brand,
  .chassis__tools {
    background-image: none;
  }

  .chassis .segmented {
    background-image: none;
    box-shadow: none;
    border-color: ButtonBorder;
  }

  /* The pocket has no material left to be cut into, so it stops painting
     and the engaged key is marked by the system's own Highlight pair —
     which css/motion.css §9 already sets. Nothing here fights it. */
  .chassis .segmented .segmented__key[aria-selected="true"],
  .chassis .segmented:has(> .segmented__key:nth-child(3))::before {
    background-image: none;
    box-shadow: none;
  }

  .day-strip__track,
  .day-strip__left {
    box-shadow: none;
    forced-color-adjust: none;
  }
}
