/* ==========================================================================
   PARTS — the control vocabulary, rebuilt from the Braun replication spec
   ==========================================================================

   This file exists because the controls had drifted from the brief in four
   specific ways, all of them named in review:

     1. The mode selector was a BORDERED BOX. §4 of the spec is explicit —
        "Where a cap sits inside a housing, there is a real gap of 1–2px
        showing darkness — not a border stroke. A uniform 1px border reads as
        a drawn line; a gradient reads as a shadowed void."

     2. Words were printed ON the caps. Nothing in the reference does this.
        Every control there is a bare part with its legend silkscreened on
        the PANEL beside or beneath it:

            <button class="push" data-mat="cap" data-d="4"></button>
            <div class="lbl">phono</div>

        §7: "Labels sit centered below each control … Labels are printed on
        the panel." A word moulded into a key cap is a thing 1970s hardware
        almost never did, and it is the single change that most makes this
        read as software pretending to be an object.

     3. There was no on light. In the reference an interlocked key carries a
        lamp dot, and the panel carries a power lamp. State is shown by a
        lamp and by the key sitting DOWN in its tray — never by a fill.

     4. Everything was too close together. The reference runs 42px between
        control groups and 11px between a control and its legend. Rams's
        layout rule is "groups separated by real air rather than rules or
        boxes"; tight spacing is what forces you to reach for a box.

   Nothing here invents a new look. It applies the brief that was already
   agreed, to the parts that were not yet built to it.

   ARCHITECTURE. These are NEW class names — .part, .cap, .key, .keytray,
   .legend. They collide with nothing, so this file adds no override layer:
   the markup moves to these classes and the old .segmented__* / .btn--lg
   rules simply stop matching. That is what makes the superseded sections in
   controls.css safely deletable afterwards, with the fingerprint harness as
   the proof rather than a guess.

   LIGHT. Every value below is driven by the vector src/light.js already
   publishes on <html>, so these parts sit in the same room as the dial and
   the case:

     --rim-x / --rim-y     unit vector toward the key light, in px
     --turn-x / --turn-y   the opposite: the surface turning away
     --rim-a / --turn-a    their strengths, from the sun's elevation
     --sh-x / --sh-y       the shadow vector (contact + ambient offsets)
     --sh-blur --sh-alpha  ambient shadow
     --hl-x / --hl-y       specular centre, as a percentage offset

   §2 of the spec: "The direction never varies between components." That is
   only true if there is one source of truth, and this is it.
   ========================================================================== */

:root {
  /* Warm brown-black. §1: "all shadows use this, never pure black". */
  --part-sh: 58, 48, 38;

  /* Cap face, three stops. RETUNED to the palette card: the caps there are
     near-FLAT matte discs, not domed. The face barely shades across itself —
     what describes the form is the collar and the rim, not a gradient. So the
     three stops sit close together and the material does the rest. */
  --cap-hi: #fbf8f0;
  --cap-mid: #f5f2e9;
  --cap-lo: #e6e1d6;

  /* THE COLLAR — the single most identifiable thing in the reference.
     Every cap on that card is a CYLINDER: a flat top and a visible side wall
     descending into the panel, darkest where it meets the surface. It is what
     makes those buttons read as machined parts rather than as circles with a
     shadow. Drawn as a real ring of tone under the cap, never as a border. */
  --collar: #b3a89a;
  --collar-deep: #8a7f70;

  /* The tray a cap sits in. Only a HAIR darker than the panel — a shallow
     channel milled into the same piece of plastic, not a separate grey part
     laid on top. Stepping the tone hard was what made this read as a box
     with a border round it: the eye takes any hard tonal boundary as an
     edge, and no amount of inner shading argues it out of that. The recess
     has to be described by the SHADING (dark inner wall on the light side,
     a catch on the far side) with the tone almost unchanged. */
  --tray-hi: #eeeae2;
  --tray-lo: #dcd7cf;

  /* The seam. Not a colour a border would use — an occlusion that falls off. */
  --seam: 28, 27, 25;

  /* Lamp. §1: the ONLY saturated colour, and it appears only where something
     is genuinely active. */
  --lamp-hot: #f5623a; /* the lit dome of the lens */
  --lamp: #ed3f1c; /* DR06 — the lamp is the BRIGHT member of the family; a
                      lamp is emitting, so it sits above the painted red */
  --lamp-deep: #bf1b1b; /* DR06 — its shadowed rim is the painted red */
  --lamp-off: #c4c0b8;
  --lamp-off-deep: #a8a49c;

  /* DERIVED, NOT LITERAL. These were hardcoded to the ceramic palette, so on a
     skin that inverts the ramp — chrome at night is a dark body with
     light ink — the silkscreen stayed near-black on near-black and measured
     1.25:1. Pointing them at the semantic tokens means a legend follows
     whatever material it is printed on, automatically, and a new skin cannot
     reintroduce the bug by forgetting to override two more variables. */
  --legend-ink: var(--ink);
  --legend-dim: var(--muted-ink);

  /* INK PRINTED ON A CAP, which is NOT the same ground as ink printed on the
     panel. --legend-ink follows the body, and that is right for a legend
     silkscreened on the fascia — but a settings key carries its label ON its
     own face, and a cap is its own material. On any skin whose
     ramps are inverted (dark body, light ink), following the body put light
     text on a cream cap and the unselected keys went blank. Cap ink therefore
     tracks the CAP, and §20.2 flips it for the one material whose caps are
     dark in both themes. */
  --cap-ink: #2b2620;
  --cap-ink-dim: #6b6156;

  /* §7: air. One scale, used everywhere, so gaps agree across the panel. */
  --part-gap: 2rem; /* between control groups */
  --part-legend-gap: 0.625rem; /* control → its own legend  (reference: 11px) */
}

html[data-theme='graphite'] {
  --cap-ink: #ece7dd;
  --cap-ink-dim: #c2b8a8; /* clears alloy's lifted graphite cap, the lightest of the seven */
  --cap-hi: #423d36;
  --cap-mid: #34302a;
  --cap-lo: #262219;
  --collar: #16130f;
  --collar-deep: #0b0906;
  --tray-hi: #2a2a2e;
  --tray-lo: #131316;
  --seam: 0, 0, 0;
  --lamp-off: #4c4c52;
  --lamp-off-deep: #303035;
  /* inherits from --ink / --muted-ink, which already flip with the theme */
}

@media (prefers-color-scheme: dark) {
  html:not([data-theme='ceramic']) {
    --cap-ink: #ece7dd;
    --cap-ink-dim: #c2b8a8;
    --cap-hi: #423d36;
    --cap-mid: #34302a;
    --cap-lo: #262219;
    --collar: #16130f;
    --collar-deep: #0b0906;
    --tray-hi: #2a2a2e;
    --tray-lo: #131316;
    --seam: 0, 0, 0;
    --lamp-off: #4c4c52;
    --lamp-off-deep: #303035;
    --legend-ink: #cfcbc3;
    --legend-dim: #85817a;
  }
}

/* --------------------------------------------------------------------------
   1. THE PART — a control and the legend printed next to it

   This is the reference's `.unit`, and it is the only assembly on the panel.
   The cap is bare. The word is on the surface. They are separate objects and
   the markup says so.
   -------------------------------------------------------------------------- */

.part {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: var(--part-legend-gap);
}

.part--beside {
  flex-direction: row;
  align-items: center;
  gap: 0.875rem;
}

/* §7: 9–10px, lowercase, letterspaced ~0.06em, never bold, and given a
   whisper of inset below the glyph so it reads as silkscreen sitting IN the
   panel rather than type floating above it. */
.legend {
  font-size: 0.5625rem;
  line-height: 1.35;
  letter-spacing: 0.06em;
  text-transform: lowercase;
  font-weight: 400;
  color: var(--legend-ink);
  text-align: center;
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4);
  white-space: nowrap;
  user-select: none;
}

html[data-theme='graphite'] .legend,
:root:not([data-theme='ceramic']) .legend {
  /* On a dark panel the silkscreen is a lighter ink and the inset flips: the
     relief catches from below, so the ghost line goes dark. */
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5);
}

.legend--dim {
  color: var(--legend-dim);
}

/* A two-word legend where the live word is the one that pressing performs.
   Straight out of the reference's `.speeds` block, which prints 45 / 33 / 16
   and emphasises the engaged one. A fixed silkscreen cannot change its mind,
   so it prints both poles and lights the one that is true — which is exactly
   how the hardware handled it, and is honest in a way a button whose word
   changes underneath your finger is not. */
.legend--pair {
  display: inline-flex;
  gap: 0.5em;
}

.legend--pair > span {
  color: var(--legend-dim);
  transition: color 140ms ease;
}

.legend--pair > span[data-live='1'] {
  color: var(--legend-ink);
}

/* --------------------------------------------------------------------------
   2. THE CAP — a proud part, three surfaces, and a stack of shadows

   §5: a cap has a top face, a chamfer, and a side wall, and they are drawn
   separately. §3: every raised element gets a MINIMUM of two shadows — the
   tight contact shadow where it meets the panel, and the wide ambient
   occlusion of its own volume. The contact shadow is the one people forget,
   and its absence is why most skeuomorphic UI floats.
   -------------------------------------------------------------------------- */

.cap {
  --cap-throw: 3px; /* how proud it stands; drives every shadow below */

  position: relative;
  display: grid;
  place-items: center;
  border: 0; /* §"Do not": no uniform border standing in for a bevel */
  padding: 0;
  cursor: pointer;
  color: transparent; /* the cap carries no word. See §1 of the file header. */
  font-size: 0;
  appearance: none;
  -webkit-tap-highlight-color: transparent;

  background-color: var(--cap-mid);
  background-image:
    /* A near-flat matte face. On the palette card the cap tops carry almost
       no gradient at all — the light that describes them lands on the RIM and
       the COLLAR, not across the face. An obvious face gradient is the thing
       that makes a CSS button look like a CSS button, so this is deliberately
       weak: a wide, low specular and a shallow directional wash. */
    radial-gradient(
      72% 66% at calc(50% + var(--hl-x, -8%)) calc(50% + var(--hl-y, -12%)),
      rgba(255, 253, 247, calc(var(--hl-alpha, 0.7) * 0.26)) 0%,
      rgba(255, 253, 247, 0) 74%
    ),
    linear-gradient(
      var(--light-angle, 145deg),
      var(--cap-hi) 0%,
      var(--cap-mid) 52%,
      var(--cap-lo) 100%
    );

  box-shadow:
    /* 1. THE RIM. A hairline catch on the upper-left arc only, never a ring.
       Sub-pixel and warm — plastic edges catch light warm, not blue. */
    inset var(--rim-x, 0.8px) var(--rim-y, 1.2px) 0 -0.5px
      rgba(255, 253, 247, calc(var(--rim-a, 0.86) * 0.98)),
    /* 2. The face turning away on the opposite arc. */
      inset calc(var(--turn-x, -0.8px) * 1.5) calc(var(--turn-y, -1.2px) * 1.5) 1.4px
      rgba(var(--part-sh), calc(var(--turn-a, 0.17) * 1.15)),
    /* 3. The bounce — panel light kicking back onto the bottom edge. Subtle,
       and doing enormous work: without it the cap looks pasted on. */
      inset 0 -1px 0 rgba(255, 253, 247, 0.34),
    /* 4. THE COLLAR — the cylinder wall. Three stacked hairlines with NO blur,
       darkening as they descend, so the cap has real visible height standing
       off the panel. This is the reference's signature and the reason those
       buttons read as machined parts rather than as discs with a shadow. */
      0 1px 0 var(--collar),
    0 2px 0 var(--collar),
    0 3px 0 var(--collar-deep),
    /* 5. Contact shadow — tight, dark, almost no blur. Where the part meets
       the panel. The shadow everyone forgets, and its absence is why most
       skeuomorphic UI floats. */
      calc(var(--sh-x, 0.9px) * 0.35) calc(var(--sh-y, 2.4px) * 0.5 + 2px) 2px
      rgba(var(--part-sh), 0.42),
    /* 6. Ambient occlusion — wide, soft, low. The volume blocking sky light. */
      var(--sh-x, 0.9px) calc(var(--sh-y, 2.4px) * 1.6 + 2px) var(--sh-blur, 10px)
      rgba(var(--part-sh), var(--sh-alpha, 0.19));

  transition:
    transform 110ms cubic-bezier(0.22, 0.9, 0.35, 1),
    box-shadow 110ms cubic-bezier(0.22, 0.9, 0.35, 1),
    background-color 110ms ease;
}

.cap--round {
  inline-size: 3rem;
  block-size: 3rem;
  border-radius: 50%;
}

.cap--lg {
  inline-size: 4rem;
  block-size: 4rem;
  border-radius: 50%;
  --cap-throw: 4px;
}

/* §8: press = TRAVEL, not colour change. The cap moves down, the contact
   shadow collapses, the ambient shrinks, the wall disappears because it is
   now inside the panel, and the face dims because less sky reaches it. */
.cap:active,
.cap[data-down='1'] {
  /* §8: press = TRAVEL. The cap descends INTO its own collar, so the collar
     shortens by exactly the distance travelled and the contact shadow
     collapses. Nothing scales, nothing changes colour — a moulded part
     cannot deform, it can only move. */
  transform: translateY(2px);
  box-shadow:
    inset var(--rim-x, 0.8px) var(--rim-y, 1.2px) 0 -0.5px
      rgba(255, 253, 247, calc(var(--rim-a, 0.86) * 0.5)),
    inset calc(var(--turn-x, -0.8px) * 3) calc(var(--turn-y, -1.2px) * 3) 5px
      rgba(var(--part-sh), 0.28),
    0 1px 0 var(--collar-deep),
    0 1px 2px rgba(var(--part-sh), 0.38);
  background-color: color-mix(in srgb, var(--cap-mid) 94%, rgb(var(--part-sh)));
}

.cap:disabled {
  cursor: default;
  opacity: 0.42;
}

/* §8: "Keyboard focus is a visible ring OUTSIDE the housing, so it doesn't
   break the material." */
.cap:focus-visible,
.key:focus-visible {
  outline: 2px solid var(--signal);
  outline-offset: 4px;
}

/* --------------------------------------------------------------------------
   3. THE SIGNAL CAP — the one lit part

   §6: "don't just fill it orange. Radial gradient with a hotter, slightly
   desaturated centre falling to a deeper rim — light from a bulb behind
   translucent plastic", a faint bleed onto the panel around it, and a contact
   shadow warmed toward red. Its unlit sibling has the SAME geometry; only the
   lamp changes.
   -------------------------------------------------------------------------- */

.cap--signal {
  /* THE ONE RED PART. Painted lacquer, not an emitting lens — the palette
     card's red button is a moulded cap in a different pigment, lit by the
     same room, with the same collar and the same rim as its neighbours. Only
     the colour changes. Nothing glows: glow is what the LAMPS are for, and
     spending it here would leave nothing to say "running". */
  background-color: var(--signal-key);
  background-image:
    radial-gradient(
      72% 66% at calc(50% + var(--hl-x, -8%)) calc(50% + var(--hl-y, -12%)),
      rgba(255, 238, 230, calc(var(--hl-alpha, 0.7) * 0.22)) 0%,
      rgba(255, 238, 230, 0) 74%
    ),
    linear-gradient(
      var(--light-angle, 145deg),
      color-mix(in srgb, var(--signal-key) 84%, #fff) 0%,
      var(--signal-key) 52%,
      color-mix(in srgb, var(--signal-key) 82%, #000) 100%
    );
  box-shadow:
    inset var(--rim-x, 0.8px) var(--rim-y, 1.2px) 0 -0.5px rgba(255, 226, 214, 0.72),
    inset calc(var(--turn-x, -0.8px) * 1.5) calc(var(--turn-y, -1.2px) * 1.5) 1.6px
      rgba(60, 8, 8, 0.34),
    inset 0 -1px 0 rgba(255, 226, 214, 0.24),
    /* its own collar, in its own pigment */
      0 1px 0 color-mix(in srgb, var(--signal-key) 66%, #000),
    0 2px 0 color-mix(in srgb, var(--signal-key) 52%, #000),
    0 3px 0 color-mix(in srgb, var(--signal-key) 36%, #000),
    calc(var(--sh-x, 0.9px) * 0.35) calc(var(--sh-y, 2.4px) * 0.5 + 2px) 2px
      rgba(52, 10, 8, 0.44),
    var(--sh-x, 0.9px) calc(var(--sh-y, 2.4px) * 1.6 + 2px) var(--sh-blur, 10px)
      rgba(var(--part-sh), var(--sh-alpha, 0.19));
}

.cap--signal:active,
.cap--signal[data-down='1'] {
  transform: translateY(2px);
  box-shadow:
    inset var(--rim-x, 0.8px) var(--rim-y, 1.2px) 0 -0.5px rgba(255, 226, 214, 0.4),
    inset calc(var(--turn-x, -0.8px) * 3) calc(var(--turn-y, -1.2px) * 3) 5px
      rgba(60, 8, 8, 0.42),
    0 1px 0 color-mix(in srgb, var(--signal-key) 36%, #000),
    0 1px 2px rgba(52, 10, 8, 0.44);
}

/* --------------------------------------------------------------------------
   4. THE KEY TRAY — an interlocked bank

   §4: adjacent keys share a seam, and the seam is a GRADIENT, darkest exactly
   at the joint and gone within ~3px. The tray itself is recessed, so §2's
   inversion applies to it: dark inner shadow on the light side, light on the
   shadow side, and NO drop shadow — a hole does not cast one.

   There is no border anywhere in this block. That was the note.
   -------------------------------------------------------------------------- */

.keytray {
  /* NO HOUSING. The keys sit straight on the panel.

     This went through three attempts — a filled tray, then an unfilled tray
     with a shaded recess, then this — and the note each time was the same:
     the panel behind the keys is the problem, not the shade of it. It was
     right. A recess is a closed shape, and a closed shape around a group of
     controls reads as a box drawn around them no matter how softly it is
     lit. Removing the tone did not remove the shape.

     So there is no shape. The keys are proud caps on the surface, held
     together by touching each other and by the occlusion in their seams,
     which is what makes a bank read as a bank. Nothing is drawn around them.

     This element now does one job: hold the keys in a row and set the
     column rhythm the legends beneath align to. */
  display: inline-flex;
  padding: 0;
  border: 0;
  border-radius: 0;
  background-image: none;
  box-shadow: none;
}

.key {
  position: relative;
  /* Sized for a 64px header rail, not for a stereo front. The reference's
     52x44 keys sit on a panel the size of a hi-fi; scaled to this chassis
     that is the same optical weight. */
  inline-size: 4rem;
  block-size: 2.125rem;
  margin-inline-end: 3px;
  border: 0;
  border-radius: 4px;
  padding: 0;
  cursor: pointer;
  color: transparent;
  font-size: 0;
  appearance: none;
  -webkit-tap-highlight-color: transparent;

  background-color: var(--cap-mid);
  background-image:
    radial-gradient(
      76% 66% at calc(50% + var(--hl-x, -8%)) calc(50% + var(--hl-y, -12%)),
      rgba(255, 253, 247, calc(var(--hl-alpha, 0.7) * 0.24)) 0%,
      rgba(255, 253, 247, 0) 76%
    ),
    linear-gradient(
      var(--light-angle, 145deg),
      var(--cap-hi) 0%,
      var(--cap-mid) 52%,
      var(--cap-lo) 100%
    );

  box-shadow:
    inset var(--rim-x, 0.8px) var(--rim-y, 1.2px) 0 -0.5px
      rgba(255, 253, 247, calc(var(--rim-a, 0.86) * 0.98)),
    inset calc(var(--turn-x, -0.8px) * 1.5) calc(var(--turn-y, -1.2px) * 1.5) 1.4px
      rgba(var(--part-sh), calc(var(--turn-a, 0.17) * 1.15)),
    inset 0 -1px 0 rgba(255, 253, 247, 0.32),
    /* the same cylinder wall the round caps have, so a key and a push read as
       parts off the same production line rather than two different widgets */
      0 1px 0 var(--collar),
    0 2px 0 var(--collar),
    0 3px 0 var(--collar-deep),
    calc(var(--sh-x, 0.9px) * 0.35) calc(var(--sh-y, 2.4px) * 0.5 + 2px) 2px
      rgba(var(--part-sh), 0.4),
    var(--sh-x, 0.9px) calc(var(--sh-y, 2.4px) * 1.4 + 2px) var(--sh-blur, 10px)
      rgba(var(--part-sh), calc(var(--sh-alpha, 0.19) * 0.9));

  transition:
    transform 110ms cubic-bezier(0.22, 0.9, 0.35, 1),
    box-shadow 110ms cubic-bezier(0.22, 0.9, 0.35, 1),
    background-color 110ms ease;
}

.key:last-child {
  margin-inline-end: 0;
}

/* THE TARGET IS BIGGER THAN THE PART.
   A key cap is sized to look right on the panel — 34px tall, and 24px for the
   face selector. Both clear WCAG 2.5.8's 24x24 floor, and both are still a
   mean thing to ask a shaking or imprecise thumb to hit. So the visible part
   keeps its scale and an invisible extension takes the target past 44px.

   Vertical only: growing it sideways would overlap the neighbouring key in
   the bank, and a target that steals its neighbour's taps is worse than a
   small one. */
.key::before {
  content: '';
  position: absolute;
  inset-block: -5px;
  inset-inline: 0;
}

.key--sm::before {
  inset-block: -10px;
}

/* The seam. Two real gradients in the gap between caps, NOT a border stroke:
   darkest at the joint, gone within 4px. Their strength follows the light —
   the side facing away from the key light occludes harder. */
.key__seam {
  position: absolute;
  inset-block: 2px;
  inline-size: 4px;
  pointer-events: none;
}

.key__seam--lead {
  inset-inline-start: -3px;
  background-image: linear-gradient(270deg, rgba(var(--seam), 0.62), rgba(var(--seam), 0));
}

.key__seam--trail {
  inset-inline-end: -3px;
  background-image: linear-gradient(90deg, rgba(var(--seam), 0.62), rgba(var(--seam), 0));
}

/* THE SELECTED KEY IS DOWN. This is the whole idea: an interlocked bank shows
   its state by one key staying depressed in the tray. No fill, no border, no
   colour swap — the thing you asked about. §"cap" in the reference engine:

       const pressed = el.classList.contains('on');
       const d = pressed ? Math.max(.7, d0*.26) : d0;   // throw collapses
       const dim = pressed ? .93 : 1;                   // face dims

   and the housing rim throws an occlusion across it from the light side. */
.key[aria-selected='true'],
.key[aria-checked='true'] {
  /* SEATED. An interlocked key bank shows its state by one key staying down
     in the panel — the mechanical latch. The cap descends into its own
     collar, so the collar all but disappears, the face dims because less sky
     reaches a recessed surface, and the panel throws an occlusion across it
     from the light side. No fill, no border, no colour swap. */
  transform: translateY(3px);
  background-color: color-mix(in srgb, var(--cap-mid) 89%, rgb(var(--part-sh)));
  background-image:
    radial-gradient(
      76% 66% at calc(50% + var(--hl-x, -8%)) calc(50% + var(--hl-y, -12%)),
      rgba(255, 253, 247, calc(var(--hl-alpha, 0.7) * 0.1)) 0%,
      rgba(255, 253, 247, 0) 76%
    ),
    linear-gradient(
      var(--light-angle, 145deg),
      color-mix(in srgb, var(--cap-hi) 80%, rgb(var(--part-sh))) 0%,
      color-mix(in srgb, var(--cap-mid) 86%, rgb(var(--part-sh))) 52%,
      color-mix(in srgb, var(--cap-lo) 86%, rgb(var(--part-sh))) 100%
    );
  /* No bright rim on a seated key: the lit chamfer and the occlusion both
     live on the light side, so drawing them together cancels them out — and
     that is what previously made the SELECTED key read as the proud one. */
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 4.5) calc(var(--rim-y, 1.2px) * 4.5) 6px
      rgba(var(--part-sh), 0.4),
    inset calc(var(--turn-x, -0.8px) * 1.2) calc(var(--turn-y, -1.2px) * 1.2) 1.5px
      rgba(255, 253, 247, 0.2),
    0 0 0 var(--collar-deep),
    0 1px 1px rgba(var(--part-sh), 0.3);
}

/* The seams deepen when the key beside them drops — a wider gap occludes
   harder, which is the reference's `boost` term. */
.key[aria-selected='true'] .key__seam,
.key[aria-checked='true'] .key__seam {
  filter: brightness(0.72);
}

.key:active {
  transform: translateY(3px);
}

/* THE ON LIGHT. A lamp dot on the key itself — grey glass unlit, and lit from
   behind when the key is engaged. §6: the geometry never changes, only the
   lamp does. This is what was missing. */
.key__lamp {
  position: absolute;
  inset-inline-start: 50%;
  inset-block-start: 6px;
  inline-size: 7px;
  block-size: 7px;
  translate: -50% 0;
  border-radius: 50%;
  background-image: radial-gradient(
    circle at 38% 30%,
    color-mix(in srgb, var(--lamp-off) 70%, #fff) 0%,
    var(--lamp-off) 55%,
    var(--lamp-off-deep) 100%
  );
  box-shadow:
    inset var(--rim-x, 0.8px) var(--rim-y, 1.2px) 1px rgba(var(--part-sh), 0.5),
    0 1px 0 rgba(255, 252, 245, 0.5);
  transition:
    background-image 160ms ease,
    box-shadow 160ms ease;
}

.key[aria-selected='true'] .key__lamp,
.key[aria-checked='true'] .key__lamp {
  background-image: radial-gradient(
    circle at 38% 30%,
    var(--lamp-hot) 0%,
    var(--lamp) 55%,
    var(--lamp-deep) 100%
  );
  box-shadow:
    inset var(--rim-x, 0.8px) var(--rim-y, 1.2px) 1px rgba(120, 30, 0, 0.4),
    0 0 7px rgba(240, 80, 30, 0.75);
}

/* The legends for a key bank sit on the panel below the tray, one per key,
   on the same column centres as the keys they name. A grid, so they cannot
   drift out of register with the caps. */
.keybank {
  display: inline-grid;
  justify-items: center;
  gap: var(--part-legend-gap);
}

.keybank__legends {
  display: grid;
  grid-template-columns: repeat(var(--key-count, 2), 4rem);
  gap: 3px;
  inline-size: 100%;
  padding-inline: 0;
}

/* The face selector: the same bank, smaller, because it is a setting rather
   than the mode switch. Same construction, so it cannot drift out of the
   vocabulary the way a bespoke chip did. */
.key--sm {
  inline-size: 3.5rem;
  block-size: 1.5rem;
}

.tt-timer__legendgroup .keybank {
  --part-legend-gap: 0.3125rem;
}

.keybank__legends--sm {
  grid-template-columns: repeat(2, 3.5rem);
}

/* On a dark panel the recess reads inverted: the light catch on the far wall
   is the brightest thing around, and at 0.45 it traced the tray's outline and
   put the border straight back. A dark matte surface returns far less light,
   so the catch drops and the occlusion carries the form instead. */

/* --------------------------------------------------------------------------
   5. THE POWER LAMP — the case knows whether it is running

   "Where is the on light?" — on the panel, by the wordmark, where a power
   lamp belongs. Unlit grey glass while idle; lit while a session is running.
   §1: one saturated colour, used only where something is genuinely active.
   A timer that is counting IS the thing that is active, so this is the honest
   place to spend it.
   -------------------------------------------------------------------------- */

/* The lamp already existed at .brand::after with running / overtime / paused
   wired up — it was simply too dark to read as a lens, so it looked like a
   drilled hole rather than an indicator, which is why the question "where is
   the on light?" was a fair one.

   Four sections of controls.css had a go at this element (§21b, §41.2, §48.4
   and one more), each overriding the last. This is deliberately the only
   definition that runs last, so the answer is here rather than distributed
   across four opinions. Unlit it is GREY GLASS, not a dark pit: an unlit lamp
   still catches sky on its dome, and that is the difference between reading
   as a lamp that is off and reading as a hole. */
html body .brand::after {
  inline-size: 9px !important;
  block-size: 9px !important;
  border-radius: 50% !important;
  background-image: radial-gradient(
    circle at 36% 30%,
    color-mix(in srgb, var(--lamp-off) 74%, #fff) 0%,
    var(--lamp-off) 52%,
    var(--lamp-off-deep) 100%
  ) !important;
  /* The lens sits IN the panel, so §2's inversion applies: dark on the
     light-side inner wall, a catch on the far wall, and no drop shadow. */
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 1.6) calc(var(--rim-y, 1.2px) * 1.6) 1.5px
      rgba(var(--part-sh), 0.5),
    inset calc(var(--turn-x, -0.8px) * 0.8) calc(var(--turn-y, -1.2px) * 0.8) 1px
      rgba(255, 252, 245, 0.45),
    calc(var(--turn-x, -0.8px) * 0.7) calc(var(--turn-y, -1.2px) * 0.7) 0
      rgba(255, 252, 245, 0.6) !important;
  transition:
    background-image 260ms ease,
    box-shadow 260ms ease !important;
}

/* PAUSED IS NOT RUNNING, SO IT MUST NOT LOOK THE SAME.
   §41.2 of controls.css gave paused the full lamp, which made a held session
   indistinguishable from a counting one at a glance — the exact thing an
   indicator exists to prevent. A paused session is loaded but not counting,
   and the honest analogue is a lamp on low current: lit enough to say "there
   is a session here", too dim to say "it is running", and with no bloom,
   because bloom is what the eye reads as brightness. */
html[data-run='paused'] body .brand::after {
  background-image: radial-gradient(
    circle at 36% 30%,
    color-mix(in srgb, var(--lamp-hot) 55%, var(--lamp-off)) 0%,
    color-mix(in srgb, var(--lamp) 52%, var(--lamp-off)) 52%,
    color-mix(in srgb, var(--lamp-deep) 58%, var(--lamp-off-deep)) 100%
  ) !important;
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 1.6) calc(var(--rim-y, 1.2px) * 1.6) 1.5px
      rgba(var(--part-sh), 0.4),
    calc(var(--turn-x, -0.8px) * 0.7) calc(var(--turn-y, -1.2px) * 0.7) 0
      rgba(255, 252, 245, 0.5) !important;
}

html[data-run='running'] body .brand::after,
html[data-run='overtime'] body .brand::after {
  background-image: radial-gradient(
    circle at 36% 30%,
    var(--lamp-hot) 0%,
    var(--lamp) 52%,
    var(--lamp-deep) 100%
  ) !important;
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 1.6) calc(var(--rim-y, 1.2px) * 1.6) 1.5px
      rgba(120, 30, 0, 0.45),
    0 0 10px rgba(240, 80, 30, 0.7),
    0 0 22px rgba(240, 80, 30, 0.22) !important;
}

/* --------------------------------------------------------------------------
   5b. A PRIMARY BUTTON MUST NEVER BE INVISIBLE

   The tutorial's "Next" — the one control that advances onboarding — was
   rendering as WHITE TEXT ON A WHITE CARD. Measured contrast 1.06:1. A new
   user saw a card with a visible "Skip" and nothing else, which is a fair
   description of broken onboarding.

   It is a collision between two `!important` rules of equal specificity in
   controls.css, decided by source order:

     §21  .btn--primary          { background-color: var(--signal-key) !important;
                                   color: var(--on-signal) !important; }   ← line 1099
     §21  .btn--quiet,
          .btn--plain            { background-color: transparent !important; } ← line 1119

   `.tut__next` carries BOTH `btn--primary` and `btn--plain`, so the later rule
   strips the fill while the white ink from the earlier one survives, because
   nothing sets `color` back.

   The two class names are contradictory by intent — "plain" means no body,
   "primary" means the signal key — and the honest resolution is that a
   PRIMARY ACTION WINS. Fixed here rather than by reordering controls.css,
   because reordering a 1,376-!important file to fix one button is how that
   file got this way.
   -------------------------------------------------------------------------- */

.btn--primary.btn--plain,
.btn--primary.btn--quiet {
  background-color: var(--signal-key) !important;
  background-image: none !important;
  color: var(--on-signal) !important;
  border-radius: 4px !important;
  padding-inline: 1rem !important;
  box-shadow:
    0 2px 0 color-mix(in oklab, var(--signal-key) 62%, #000),
    0 3px 8px rgba(var(--part-sh), 0.28) !important;
}

.btn--primary.btn--plain:active,
.btn--primary.btn--quiet:active {
  transform: translateY(2px) !important;
  box-shadow: 0 0 0 color-mix(in oklab, var(--signal-key) 62%, #000) !important;
}

/* --------------------------------------------------------------------------
   6. AIR

   §7: "groups separated by real air rather than rules or boxes." The transport
   row was the tightest thing on the panel and it is the row you look at most.
   -------------------------------------------------------------------------- */

.tt-timer__transportmain {
  gap: var(--part-gap) !important;
  align-items: start !important;
}

.tt-timer__transportside {
  gap: var(--part-gap) !important;
  align-items: start !important;
}

/* ONE BASELINE FOR EVERY LEGEND IN THE ROW.
   Rams's layout rule, and the one thing that most separates a laid-out panel
   from an arranged one: "label baselines that agree across the whole panel."
   The caps are deliberately different sizes — the primary is the biggest
   thing you press and should be — so the legends cannot simply follow their
   own caps or they land at three different heights, which is what happened.

   Every part in the transport therefore reserves the same cap row, sized to
   the tallest cap, and centres its cap inside it. The legends then start on
   one line no matter what is above them. */
.tt-timer__transportmain .part {
  display: grid;
  grid-template-rows: 4rem auto;
  align-items: center;
  justify-items: center;
}

/* --------------------------------------------------------------------------
   7. REDUCED MOTION / FORCED COLOURS
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .cap,
  .key,
  .key__lamp,
  .pilot,
  .legend--pair > span {
    transition-duration: 1ms;
  }
}

@media (forced-colors: active) {
  .cap,
  .key,
  .keytray {
    border: 1px solid ButtonBorder;
    background: ButtonFace;
    box-shadow: none;
  }

  .key[aria-selected='true'],
  .key[aria-checked='true'] {
    background: Highlight;
    transform: none;
  }

  .legend {
    color: ButtonText;
    text-shadow: none;
  }
}

/* ==========================================================================
   8. THE FINISH PASS — the surfaces that were still generic web UI
   ==========================================================================

   The palette and the parts now agree, but four things were still arriving
   from a component library rather than from a workshop: the readout, the
   label strip, the repeat switch, and the day ribbon. Each is rebuilt here in
   the same material language — one light from the upper left, warm shadow ink,
   a real edge, and no effect that is not describing a physical surface.

   Everything below is presentation only. No selector here changes a value, a
   binding, a state name or a behaviour; each one restyles a node that already
   existed and already worked.
   ========================================================================== */

/* --------------------------------------------------------------------------
   8.1 THE READOUT — a display plate let into the case

   It was a flat rounded rectangle in a slightly green-grey. A real instrument
   readout is a plate SET INTO the case: the panel is cut, the plate sits below
   the surface, and the cut edge catches light on the far side. So it gets the
   recess inversion — dark on the light-side inner wall, a catch on the far
   wall — and no drop shadow, because a hole does not cast one.
   -------------------------------------------------------------------------- */

.tt-timer__readout {
  background-color: color-mix(in srgb, var(--face) 88%, var(--edge)) !important;
  background-image: linear-gradient(
    var(--light-angle, 145deg),
    color-mix(in srgb, var(--face) 78%, var(--edge)) 0%,
    color-mix(in srgb, var(--face) 96%, var(--edge)) 62%
  ) !important;
  border: 0 !important;
  border-radius: 3px !important;
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 2.2) calc(var(--rim-y, 1.2px) * 2.2) 4px
      rgba(var(--part-sh), 0.28),
    inset calc(var(--turn-x, -0.8px) * 1.2) calc(var(--turn-y, -1.2px) * 1.2) 1px
      rgba(255, 253, 247, 0.6),
    calc(var(--turn-x, -0.8px) * 0.6) calc(var(--turn-y, -1.2px) * 0.6) 0
      rgba(255, 253, 247, 0.55) !important;
  /* Tabular figures are already on; this keeps the plate from breathing as
     the digits change width class. */
  font-variant-numeric: tabular-nums !important;
  letter-spacing: 0.01em !important;
}

/* --------------------------------------------------------------------------
   8.2 THE LABEL STRIP — a writable field, not a chat input

   A full-width grey pill is the most generic object on the page. On an
   instrument this is a card slot or a write-on strip: a shallow trough with a
   printed edge, and a focus state that lights the trough rather than drawing a
   browser ring around it.
   -------------------------------------------------------------------------- */

.tt-timer__task {
  background-color: color-mix(in srgb, var(--face) 92%, var(--edge)) !important;
  border: 0 !important;
  border-radius: 2px !important;
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 2) calc(var(--rim-y, 1.2px) * 2) 3.5px
      rgba(var(--part-sh), 0.24),
    inset calc(var(--turn-x, -0.8px) * 1) calc(var(--turn-y, -1.2px) * 1) 1px
      rgba(255, 253, 247, 0.55),
    calc(var(--turn-x, -0.8px) * 0.5) calc(var(--turn-y, -1.2px) * 0.5) 0
      rgba(255, 253, 247, 0.5) !important;
  transition: box-shadow 140ms ease, background-color 140ms ease !important;
}

.tt-timer__task:focus,
.tt-timer__task:focus-visible {
  outline: none !important;
  background-color: color-mix(in srgb, var(--face) 99%, #fff) !important;
  /* The trough lights from within and the signal marks the live edge. This is
     a visible focus indicator in its own right, and it is not colour alone —
     the inner shadow deepens and the ground lifts at the same time. */
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 2.4) calc(var(--rim-y, 1.2px) * 2.4) 4px
      rgba(var(--part-sh), 0.3),
    inset 0 -2px 0 var(--signal),
    0 0 0 2px color-mix(in srgb, var(--signal) 28%, transparent) !important;
}

/* --------------------------------------------------------------------------
   8.3 THE REPEAT SWITCH — a slide switch, not an iOS toggle

   The pill-with-a-circle is the single most recognisable component-library
   object in software, and the brief rules it out by name. This is the same
   element, restyled as the thing it represents: a thumb that TRAVELS in a
   milled channel, with a hard detent at each end. The channel is cut into the
   panel, so it is dark at the top-left inner wall; the thumb is a proud part,
   so it carries the same collar and rim as every cap on the panel.
   -------------------------------------------------------------------------- */

html body .switch__track,
html[data-theme] body .switch__track {
  border: 0 !important;
  border-radius: 3px !important;
  background-color: color-mix(in srgb, var(--face) 84%, var(--edge)) !important;
  background-image: none !important;
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 2.4) calc(var(--rim-y, 1.2px) * 2.4) 4px
      rgba(var(--part-sh), 0.34),
    inset calc(var(--turn-x, -0.8px) * 1.2) calc(var(--turn-y, -1.2px) * 1.2) 1px
      rgba(255, 253, 247, 0.5),
    calc(var(--turn-x, -0.8px) * 0.5) calc(var(--turn-y, -1.2px) * 0.5) 0
      rgba(255, 253, 247, 0.5) !important;
}

html body .switch__thumb {
  border: 0 !important;
  border-radius: 2px !important;
  background-color: var(--cap-mid) !important;
  background-image: linear-gradient(
    var(--light-angle, 145deg),
    var(--cap-hi) 0%,
    var(--cap-mid) 52%,
    var(--cap-lo) 100%
  ) !important;
  box-shadow:
    inset var(--rim-x, 0.8px) var(--rim-y, 1.2px) 0 -0.5px
      rgba(255, 253, 247, calc(var(--rim-a, 0.86) * 0.95)),
    inset calc(var(--turn-x, -0.8px) * 1.2) calc(var(--turn-y, -1.2px) * 1.2) 1.2px
      rgba(var(--part-sh), 0.16),
    0 1px 0 var(--collar),
    0 2px 0 var(--collar-deep),
    0 2px 3px rgba(var(--part-sh), 0.34) !important;
}

/* Engaged: the channel takes the signal, so the state is readable from the
   track as well as from where the thumb sits — position AND fill, never fill
   alone. */
html body .switch__input:checked + .switch__track,
html[data-theme] body .switch__input:checked + .switch__track {
  background-color: color-mix(in srgb, var(--signal) 88%, #000) !important;
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 2.4) calc(var(--rim-y, 1.2px) * 2.4) 4px
      rgba(40, 4, 4, 0.45),
    inset calc(var(--turn-x, -0.8px) * 1.2) calc(var(--turn-y, -1.2px) * 1.2) 1px
      rgba(255, 220, 210, 0.24) !important;
}

/* --------------------------------------------------------------------------
   8.4 THE DAY RIBBON — a calibrated scale, not a progress bar

   It was a solid saturated bar the full width of the case, which made the
   loudest thing on the page a piece of ambient context. On an instrument this
   is a printed scale with a travelling index: the track is engraved into the
   panel, the ticks are scribed, and only the part that has ELAPSED carries
   pigment. Same data, same behaviour, correct emphasis.
   -------------------------------------------------------------------------- */

.day-strip__track {
  background-color: color-mix(in srgb, var(--face) 82%, var(--edge)) !important;
  border: 0 !important;
  border-radius: 1px !important;
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 1.6) calc(var(--rim-y, 1.2px) * 1.6) 2.5px
      rgba(var(--part-sh), 0.26),
    calc(var(--turn-x, -0.8px) * 0.5) calc(var(--turn-y, -1.2px) * 0.5) 0
      rgba(255, 253, 247, 0.5) !important;
}

/* An engraved tick: a dark scribe with a light catch below it, which is how a
   cut line in a pale surface actually reads. Not a plain grey dash. */
.day-strip__ticks {
  opacity: 1 !important;
}

/* --------------------------------------------------------------------------
   8.5 NO STOCK CONTROLS ANYWHERE

   A safety net, not a style: anything that still resolves to a platform widget
   gets the instrument's own ink so a browser default cannot survive in a
   corner of the settings panel that nobody screenshotted.
   -------------------------------------------------------------------------- */

input[type='range'],
input[type='checkbox'],
input[type='radio'],
select,
progress {
  accent-color: var(--signal);
}

::selection {
  background: color-mix(in srgb, var(--signal) 26%, transparent);
}

/* --------------------------------------------------------------------------
   8.6 THE REPEAT SWITCH IS THE SAME PART

   `repeat` is not a .switch — it is a .chip[aria-pressed] whose ::after draws
   an entire pill switch in one element, with the thumb painted as a gradient
   and moved by background-position. That construction is clever and it works,
   so it is kept exactly: same element, same animation, same state selectors.

   Only the shape changes. `border-radius: 999px` plus a circular thumb is the
   platform toggle the brief rules out by name; a slide switch is a RECTANGULAR
   thumb travelling in a milled rectangular channel, with a detent at each end.
   Swapping the radial gradient for a linear one with hard stops gives a
   square-ended thumb without adding a node or touching the JS.
   -------------------------------------------------------------------------- */

html body .chip[aria-pressed]::after,
html[data-theme] body .chip[aria-pressed]::after {
  --sw-thumb: calc(var(--sw-h) - var(--sw-pad) * 2);

  border-radius: 2.5px !important;
  background-color: color-mix(in srgb, var(--face) 84%, var(--edge)) !important;
  /* the thumb: a hard-edged block, not a dot */
  background-image: linear-gradient(
    var(--light-angle, 145deg),
    var(--cap-hi) 0%,
    var(--cap-mid) 52%,
    var(--cap-lo) 100%
  ) !important;
  background-size: var(--sw-thumb) var(--sw-thumb) !important;
  background-repeat: no-repeat;
  background-position: var(--sw-pad) center !important;
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 2) calc(var(--rim-y, 1.2px) * 2) 3px
      rgba(var(--part-sh), 0.32),
    inset calc(var(--turn-x, -0.8px) * 1) calc(var(--turn-y, -1.2px) * 1) 1px
      rgba(255, 253, 247, 0.5) !important;
}

html body .chip[aria-pressed='true']::after,
html[data-theme] body .chip[aria-pressed='true']::after {
  background-color: color-mix(in srgb, var(--signal) 88%, #000) !important;
  background-position: calc(100% - var(--sw-pad)) center !important;
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 2) calc(var(--rim-y, 1.2px) * 2) 3px
      rgba(40, 4, 4, 0.45),
    inset calc(var(--turn-x, -0.8px) * 1) calc(var(--turn-y, -1.2px) * 1) 1px
      rgba(255, 220, 210, 0.22) !important;
}

/* The detent: it snaps, it does not glide. Unchanged in behaviour — this only
   restates the press feedback now that the fill above is different. */
html body .chip[aria-pressed]:active::after {
  filter: brightness(0.95);
}

/* --------------------------------------------------------------------------
   8.7 THE DAY RIBBON'S ACTIVE SPAN

   `.day-strip__left` is the time still ahead of you today, and it was filled
   with raw --signal at full strength across most of the width. That made the
   largest, loudest object on the page a piece of ambient context sitting under
   the instrument — and it competed directly with the disc, which is the one
   thing that is supposed to own the red.

   Rams's rule is that the saturated colour appears where something is
   genuinely active. Both of these are legitimately "active", so they cannot
   both be at full strength; the one you are USING wins. The ribbon keeps the
   pigment and drops the intensity, so it still reads as the same system and
   still reads as time-remaining — quietly, from the corner of the eye, which
   is all it was ever meant to do.

   Data, behaviour and the transition are untouched.
   -------------------------------------------------------------------------- */

html body .day-strip__left {
  background-color: color-mix(in srgb, var(--signal) 46%, var(--face)) !important;
}

/* The hour gridlines must read THROUGH the active span, or the ribbon stops
   being a scale and becomes a progress bar again. */
html body .day-strip__ticks {
  z-index: 1 !important;
}

/* The now-marker is the only thing on the ribbon that should draw the eye at
   full strength: it is a single index line, and an index line is exactly what
   a saturated colour is for. */
html body .day-strip__now,
html body .day-strip [data-now] {
  background-color: var(--signal) !important;
}

/* ==========================================================================
   9. THE SERVICE COMPARTMENT — the settings panel

   Everything here is a restyle of a control that already exists and already
   works. No field, section, value, state or handler is touched.

   The panel was arriving as a white sheet with pill-shaped segmented controls
   — the two things that most made it read as a settings screen bolted to an
   instrument rather than as the inside of one. On real hardware the service
   compartment is the same casting as the outside: same material, same light,
   smaller parts.
   ========================================================================== */

html body .settings__panel {
  background-color: var(--ceramic) !important;
  background-image: linear-gradient(
    var(--light-angle, 145deg),
    color-mix(in srgb, var(--ceramic) 96%, #fff) 0%,
    var(--ceramic) 46%,
    color-mix(in srgb, var(--ceramic) 92%, var(--edge)) 100%
  ) !important;
  /* The cut where the compartment opens: a hard edge on the light side and
     the case's own shadow falling into the room beyond it. */
  border-inline-start: 0 !important;
  box-shadow:
    inset 1px 0 0 rgba(255, 253, 247, 0.7),
    -1px 0 0 var(--edge),
    -14px 0 34px rgba(var(--part-sh), 0.16) !important;
}

/* --------------------------------------------------------------------------
   9.1 SEGMENTED CONTROLS ARE KEY BANKS

   `theme`, `dial scale` and `clock` were pill tracks with a rounded thumb —
   the same platform-toggle shape ruled out on the front panel, so it cannot
   be allowed to survive in here. Rebuilt as the interlocked bank the front
   panel already uses: a shallow channel, square-ended keys, and the ENGAGED
   key seated below the others. Same markup, same radio semantics.
   -------------------------------------------------------------------------- */

/* NO CHANNEL. These keys used to sit in a milled tray — a tinted, inset
   rectangle drawn behind the row. On the front panel that tray is right,
   because the front panel IS a moulded fascia and the channel is part of the
   moulding. In the settings drawer it is not: the drawer is a flat service
   compartment, and a recessed hole behind every row read as a box drawn
   around the buttons rather than as anything the object is made of.

   The keys keep their own bodies, so nothing is lost — a proud key on a flat
   surface still reads as a key, and ENGAGED is still a POSITION (it seats,
   below) rather than a fill. What goes away is only the hole. */
html body .set-seg__opts {
  border-radius: 0 !important;
  padding: 0 !important;
  gap: 6px !important; /* the tray's 3px was tight because the tray framed it */
  background-color: transparent !important;
  background-image: none !important;
  box-shadow: none !important;
}

/* an unselected key: proud, with the same collar as every cap on the panel */
html body .set-seg__opt span {
  border-radius: 2px !important;
  background-color: var(--cap-mid) !important;
  background-image: linear-gradient(
    var(--light-angle, 145deg),
    var(--cap-hi) 0%,
    var(--cap-mid) 52%,
    var(--cap-lo) 100%
  ) !important;
  color: var(--cap-ink-dim) !important;
  box-shadow:
    inset var(--rim-x, 0.8px) var(--rim-y, 1.2px) 0 -0.5px
      rgba(255, 253, 247, calc(var(--rim-a, 0.86) * 0.9)),
    0 1px 0 var(--collar),
    0 2px 0 var(--collar-deep),
    0 2px 2px rgba(var(--part-sh), 0.26) !important;
}

/* ENGAGED: it sits down in the channel. Not a fill — a position. */
html body .set-seg__opt input:checked + span {
  background-color: color-mix(in srgb, var(--cap-mid) 90%, rgb(var(--part-sh))) !important;
  background-image: linear-gradient(
    var(--light-angle, 145deg),
    color-mix(in srgb, var(--cap-hi) 82%, rgb(var(--part-sh))) 0%,
    color-mix(in srgb, var(--cap-mid) 88%, rgb(var(--part-sh))) 52%,
    color-mix(in srgb, var(--cap-lo) 88%, rgb(var(--part-sh))) 100%
  ) !important;
  color: var(--cap-ink) !important;
  transform: translateY(2px) !important;
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 4) calc(var(--rim-y, 1.2px) * 4) 5px
      rgba(var(--part-sh), 0.36),
    0 0 0 var(--collar-deep),
    0 1px 1px rgba(var(--part-sh), 0.28) !important;
  /* the seat is now the whole story, so it is drawn a touch deeper */
  border-radius: var(--r-part) !important;
}

html body .set-seg__opt input:active + span {
  transform: translateY(2px) !important;
}

/* --------------------------------------------------------------------------
   9.2 THE VOLUME SLIDER — a fader in a milled slot

   `accent-color` already carries the new red, but the track and thumb were
   still the platform's. A fader is a recessed slot with a proud thumb riding
   in it — the same two objects as everything else here.
   -------------------------------------------------------------------------- */

html body .set-range {
  appearance: none !important;
  -webkit-appearance: none !important;
  background: transparent !important;
  block-size: 1.5rem !important;
}

html body .set-range::-webkit-slider-runnable-track {
  block-size: 5px;
  border-radius: 2px;
  background-color: color-mix(in srgb, var(--face) 84%, var(--edge));
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 1.6) calc(var(--rim-y, 1.2px) * 1.6) 2.5px
      rgba(var(--part-sh), 0.3),
    0 1px 0 rgba(255, 253, 247, 0.6);
}

html body .set-range::-moz-range-track {
  block-size: 5px;
  border-radius: 2px;
  background-color: color-mix(in srgb, var(--face) 84%, var(--edge));
  box-shadow: inset 0 1px 2.5px rgba(var(--part-sh), 0.3);
}

html body .set-range::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  inline-size: 14px;
  block-size: 22px;
  margin-block-start: -8.5px;
  border: 0;
  border-radius: 2px;
  background-color: var(--cap-mid);
  background-image: linear-gradient(
    var(--light-angle, 145deg),
    var(--cap-hi) 0%,
    var(--cap-mid) 52%,
    var(--cap-lo) 100%
  );
  box-shadow:
    inset var(--rim-x, 0.8px) var(--rim-y, 1.2px) 0 -0.5px rgba(255, 253, 247, 0.9),
    0 1px 0 var(--collar),
    0 2px 0 var(--collar-deep),
    0 2px 3px rgba(var(--part-sh), 0.34);
}

html body .set-range::-moz-range-thumb {
  inline-size: 14px;
  block-size: 22px;
  border: 0;
  border-radius: 2px;
  background-color: var(--cap-mid);
  box-shadow: 0 1px 0 var(--collar), 0 2px 3px rgba(var(--part-sh), 0.34);
}

/* ==========================================================================
   10. ALIGNMENT AND EDGES — the finish that was actually missing

   Measured, not eyeballed. Every value below fixes a number that was wrong.
   ========================================================================== */

/* --------------------------------------------------------------------------
   10.1 THE MODE BANK SITS ON THE DIAL'S AXIS

   It was 143px to the left of it. The cause is worth recording because it is
   the second time this exact trap has bitten this file: `.modes` carries
   `grid-area: modes`, and the chassis area map is only `"brand tools"`. An
   unmapped area name does not error — it silently creates an implicit track.
   `.modes` therefore landed in a box that happens to be centred correctly
   (460→876, centre 668, dead on the dial), but it is `display: block`, so the
   key bank inside it simply sat against the left edge of that box.

   So the container was right and the content was wrong, which is why it
   looked deliberate enough to survive several passes. Centre the content.
   -------------------------------------------------------------------------- */

.chassis .modes {
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
}

/* --------------------------------------------------------------------------
   10.2 ONE RADIUS LOGIC

   Six different radii were in play — 21.36 / 4 / 4 / 3 / 2 / 1 / 50% — with no
   rule connecting them, which is the kind of thing that reads as sloppy long
   before anyone can say why. There are only three kinds of edge on this
   object, so there are three radii:

     round parts   50%    a knob or a push is round, full stop
     moulded parts 3px    every rectangular cap, key, plate, trough and recess
                          comes out of the same tooling, so it has the same
                          corner
     the case      18px   a bigger moulding, and the only thing allowed to be
                          visibly softer than a part

   Nothing else. If a new element needs a fourth radius, it is the element
   that is wrong.
   -------------------------------------------------------------------------- */

:root {
  --r-part: 3px;
  --r-case: 18px;
}

html body .tt-timer__readout,
html body .tt-timer__task,
html body .key,
html body .key--sm,
html body .day-strip__track,
html body .set-seg__opts,
html body .set-seg__opt span,
html body .switch__track,
html body .chip[aria-pressed]::after {
  border-radius: var(--r-part) !important;
}

html body .switch__thumb,
html body .set-range::-webkit-slider-thumb {
  border-radius: 2px !important; /* a thumb is smaller than the slot it rides in */
}

html body .app-shell {
  border-radius: var(--r-case) !important;
}

/* --------------------------------------------------------------------------
   10.3 THE READOUT ON THE AXIS

   It measured 1px left of the dial's centre. One pixel is invisible alone and
   is exactly the kind of thing that accumulates into "something is off".
   The cause is an odd content width resolving to a half pixel; giving the
   plate an even width lands it on the axis.
   -------------------------------------------------------------------------- */

html body .tt-timer__readout {
  min-inline-size: 9rem !important;
  padding-inline: 0 !important;
  text-align: center !important;
}

/* --------------------------------------------------------------------------
   10.4 THE CASE STAYS FULL WIDTH

   I briefly narrowed the case to the instrument's own measure when the field
   was closed, to kill the dead space either side. That was my call, it was not
   asked for, and it was reverted: this is a full-width desktop app.

   The dead space is still a real composition problem, but the answer is to put
   something in it, not to shrink the object. Left here as a note rather than a
   rule so the next attempt starts from the right end.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   10.5 THE TASK FIELD TAKES THE DIAL'S MEASURE

   It was 416px against the dial's 420 — inset by two pixels a side. Not enough
   to look intentional, exactly enough to look like a mistake. Everything in
   this column now measures from the same edge.
   -------------------------------------------------------------------------- */

html body .tt-timer__task {
  inline-size: 100% !important;
  max-inline-size: none !important;
  margin-inline: 0 !important;
}

/* ==========================================================================
   11. THE DETAILS THAT READ AS SLOPPY
   ========================================================================== */

/* --------------------------------------------------------------------------
   11.1 ONE FOCUS RING, AND IT IS NOT BLUE

   There were two focus systems running at once. Most controls took
   `2px solid var(--signal)` at a 3px offset. But §49.5 of controls.css gave
   `.btn`, `.btn--icon` and `.chip` a hardcoded `#6e9bc4` — a pale blue — at a
   5px offset. So tabbing across the panel changed BOTH the colour and the
   geometry of the ring depending on which control you landed on, and the blue
   is the only cool hue anywhere on a warm object. It is the ring around the
   drawer button.

   One ring: the signal, 2px, offset 3px, outside the housing so it never cuts
   the material. Nothing else gets to have an opinion.
   -------------------------------------------------------------------------- */

html body .btn:focus-visible,
html body .btn--icon:focus-visible,
html body .chip:focus-visible,
html body .segmented__key:focus-visible,
html body .cap:focus-visible,
html body .key:focus-visible,
html body a:focus-visible,
html body input:focus-visible,
html body [role='slider']:focus-visible {
  outline: 2px solid var(--signal) !important;
  outline-offset: 3px !important;
}

/* --------------------------------------------------------------------------
   11.2 SHADOWS THAT ARE NOT CLIPPED

   The icon buttons in the panel and settings headers sit flush against their
   container's edge, and those containers clip (`overflow: hidden` on the panel
   sheet, `auto` on the settings body). A cap's contact and ambient shadows
   fall down and to the right by design, so flush against a clipping edge they
   were being sliced off — the part looked cut out rather than placed on.

   Give them the room their own shadow needs. This is padding on the container,
   not a change to the shadow: the light model stays intact.
   -------------------------------------------------------------------------- */

html body .tt-panel__sheet,
html body .settings__body {
  padding-block-end: max(1rem, env(safe-area-inset-bottom)) !important;
}

html body .settings__head,
html body .tt-panel__head {
  padding-inline-end: 1rem !important;
}

/* --------------------------------------------------------------------------
   11.3 A TYPE SCALE WITH A RULE

   What was on screen: 9px, 9px, 10px, 11px, 16px, 16px, 32px, at weights
   300 / 400 / 500, with tracking of normal / -0.18 / 0.32 / 0.54 / 0.66 / 1.6.
   Three different sizes (9, 10, 11) were all doing the same job — a small
   printed label — which is why nothing looked deliberate: the eye reads three
   near-identical sizes as a mistake, not as a hierarchy.

   Four sizes, and each one means something:

     32px  the readout       the only display figure
     16px  the writable field the only thing you type into
     11px  secondary print   a full sentence of context
      9px  the silkscreen    a label ON the panel, tracked, lowercase

   The readout also moves off weight 300. A display plate set in a thin face
   is a screen habit; instrument figures are printed, and printed figures have
   body. 400 with the tabular figures already in place.
   -------------------------------------------------------------------------- */

html body .tt-timer__readout {
  font-weight: 400 !important;
  letter-spacing: 0.01em !important;
}

/* the small printed labels all become ONE size and ONE tracking */
html body .legend,
html body .tt-timer__grouplabel,
html body .day-strip__read,
html body .set-seg__legend,
html body .set-section__title {
  font-size: 0.5625rem !important; /* 9px */
  font-weight: 400 !important;
  letter-spacing: 0.06em !important;
  line-height: 1.35 !important;
}

/* secondary print — the one size allowed to carry a sentence */
html body .tt-timer__meta {
  font-size: 0.6875rem !important; /* 11px */
  letter-spacing: 0.04em !important;
}

/* the writable field joins the system instead of sitting at browser default */
html body .tt-timer__task {
  letter-spacing: 0.01em !important;
}

/* --------------------------------------------------------------------------
   11.4 THE WRITABLE STRIP BELONGS TO THE SAME FAMILY

   It was reading as a flat grey slab — the largest single block of foreign
   tone on the face, and unrelated to the readout trough sitting directly above
   it. Two recesses in the same column should be cut from the same material and
   lit by the same lamp; they were not.

   Same ground as the readout, same rim, and its ink joins the panel's rather
   than sitting at the browser's default near-black.
   -------------------------------------------------------------------------- */

html body .tt-timer__task {
  background-color: color-mix(in srgb, var(--face) 88%, var(--edge)) !important;
  color: var(--ink) !important;
  padding-inline: 0.875rem !important;
}

html body .tt-timer__task::placeholder {
  color: var(--muted-ink) !important;
  opacity: 1 !important;
}

/* --------------------------------------------------------------------------
   11.5 NO RULE ACROSS THE FACE

   A hairline ran the full width above the transport — the only drawn line on
   the whole object, and the exact thing Rams's layout rule rejects: "groups
   separated by real air rather than rules or boxes." It was there to separate
   the controls from the instrument, which is a job that spacing already does
   better. Removed, and the air it was standing in for is given below.
   -------------------------------------------------------------------------- */

html body .tt-timer__transport {
  border-block-start: 0 !important;
  background-image: none !important;
}

/* --------------------------------------------------------------------------
   11.6 THE VERTICAL RHYTHM I DID NOT SHIP

   The gaps down this column are 12, 7, 8, 8, 22, 26 — six values with no
   relationship between any two of them, and that irregularity is a real part
   of why the face reads as unmeasured.

   I tried to fix it twice and reverted both. Recording why, so the next person
   does not spend the same hour:

   The shell is a FIXED HEIGHT that must not scroll, and every one of these
   blocks is laid out by a parent that already positions it. Margins added here
   therefore ADD to the existing gaps rather than replacing them. The first
   attempt (8/20/36) pushed the transport bodily through the day ribbon. The
   second, budgeted to +3px total, still landed the transport legends 22px
   below the ribbon's top edge, because three 8px margins stack on top of the
   7/8/8 that were already there.

   Doing this properly means changing where the spacing is DECLARED — the
   parent's `gap` and the shell's row track sizes — which is a layout change,
   not a reskin, and it needs the height budget reallocated at the same time.
   That is a real piece of work and it should be done deliberately, not
   smuggled in under a paint job.

   So the spacing here is unchanged from before this pass. It is the one item
   on the list I could not honestly finish.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   11.7 THE TROUGH IS THE INPUT, NOT THE BOX AROUND IT

   `.tt-timer__task` is a layout DIV; the control is `input.field` inside it,
   14px narrower on each side and carrying its own 12px pill radius and its own
   ground. Styling the wrapper as a recess (§8.2) therefore produced TWO nested
   recesses with different radii and different tones — a sharp-cornered panel
   with a pill floating inside it. That is the doubled background, and it is
   the most visible single piece of sloppiness left on the face.

   The wrapper goes back to being nothing. The input becomes the trough, and
   every other text field in the app gets the same treatment so there is one
   writable surface in the system rather than two.
   -------------------------------------------------------------------------- */

html body .tt-timer__task {
  background-color: transparent !important;
  background-image: none !important;
  box-shadow: none !important;
  padding: 0 !important;
}

html body .tt-timer__task .field,
html body input.field,
html body input[type='text'],
html body input[type='search'] {
  inline-size: 100% !important;
  border: 0 !important;
  border-radius: var(--r-part) !important;
  padding-inline: 0.875rem !important;
  background-color: color-mix(in srgb, var(--face) 88%, var(--edge)) !important;
  background-image: none !important;
  color: var(--ink) !important;
  letter-spacing: 0.01em !important;
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 2) calc(var(--rim-y, 1.2px) * 2) 3.5px
      rgba(var(--part-sh), 0.24),
    inset calc(var(--turn-x, -0.8px) * 1) calc(var(--turn-y, -1.2px) * 1) 1px
      rgba(255, 253, 247, 0.55),
    calc(var(--turn-x, -0.8px) * 0.5) calc(var(--turn-y, -1.2px) * 0.5) 0
      rgba(255, 253, 247, 0.5) !important;
  transition: box-shadow 140ms ease, background-color 140ms ease !important;
}

html body .tt-timer__task .field::placeholder,
html body input.field::placeholder {
  color: var(--muted-ink) !important;
  opacity: 1 !important;
}

html body .tt-timer__task .field:focus,
html body .tt-timer__task .field:focus-visible,
html body input.field:focus,
html body input.field:focus-visible {
  outline: none !important;
  background-color: color-mix(in srgb, var(--face) 99%, #fff) !important;
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 2.4) calc(var(--rim-y, 1.2px) * 2.4) 4px
      rgba(var(--part-sh), 0.3),
    inset 0 -2px 0 var(--signal),
    0 0 0 2px color-mix(in srgb, var(--signal) 26%, transparent) !important;
}

/* --------------------------------------------------------------------------
   11.8 NO PANEL BEHIND THE TOOL CLUSTER

   `.chassis__tools` paints `linear-gradient(270deg, #edebe2 …)` behind the
   three icon buttons — a lighter rectangle sitting under a group of controls,
   which is exactly the "background behind an element" that reads as unfinished.
   Nothing on the reference has a plate behind a group; parts sit directly on
   the panel and are held together by alignment, not by a box.
   -------------------------------------------------------------------------- */

html body .chassis__tools {
  background-image: none !important;
  background-color: transparent !important;
}

/* The grille is a 0x0 leftover that still carries a repeating gradient. It
   renders nothing, but it is one more thing painting for no reason. */
html body .chassis__grille {
  display: none !important;
}

/* ==========================================================================
   12. REAL MATERIAL ON THE CASE

   The repo ships photographed textures — panel.jpg, ceramic.png, ground.jpg,
   plaster.png, aluminium.jpg, scratches.jpg, dust.jpg, smudge.jpg — and most
   of them are wired up: the desk uses ground.jpg, the wear layer uses
   scratches and dust, the panel sheet uses ceramic.png, the tab uses
   aluminium.jpg.

   The CASE had none. Not because it was never given one: `.app-shell` carries
   FIVE competing `background-image` declarations, every one of them
   `!important`, in this order —

       1  url(../assets/textures/panel.jpg)   the real material
       2  none                                kills it
       3  none                                kills it again
       4  radial-gradient(...)                a flat lighting wash
       5  radial-gradient(...)                the one that actually wins

   — so the photograph is loaded and then thrown away by a gradient. The
   largest surface in the product, the one every other part sits on, was the
   only one rendered as pure CSS. That is why it reads as flat no matter how
   carefully the pigment is tuned: there is no material there, only colour.

   Fixed by LAYERING rather than replacing. The lighting gradient still does
   the lighting; the photograph rides on top of it in `soft-light`, which
   preserves the tint exactly and contributes only its deviation from mid-grey
   — i.e. grain, not tone. Plus the spec's own 1.5% turbulence over the top to
   kill gradient banding at large radii.
   ========================================================================== */

/* ONE material mechanism, not two. I first layered the photograph into
   `background-image` AND added a ::before overlay; both were blending, both
   were lifting the ground, and in graphite the two together turned dark
   enamel into khaki canvas. The background-image layer is removed and the
   ::before below is the single place material is applied — one layer, one
   blend mode, one opacity, tunable per theme. */
html[data-theme] body .app-shell,
html:not([data-theme]) body .app-shell,
html body .app-shell {
  background-image: radial-gradient(
    120% 90% at calc(30% + var(--hl-x, -8%)) calc(12% + var(--hl-y, -12%)),
    color-mix(in srgb, var(--ramp-150) 92%, #fff) 0%,
    var(--ramp-150) 48%,
    color-mix(in srgb, var(--ramp-150) 90%, var(--edge)) 100%
  ) !important;
  background-size: cover !important;
  background-repeat: no-repeat !important;
  background-blend-mode: normal !important;
}

/* --------------------------------------------------------------------------
   12.1 THE PARTS GET MATERIAL TOO

   A moulded cap is the same plastic as the panel, just formed. Without any
   grain the caps read as vector shapes sitting on a grained ground, which is
   its own kind of wrong — the eye notices that the parts are smoother than the
   thing they are mounted to.
   -------------------------------------------------------------------------- */

html body .cap,
html body .key,
html body .btn--icon {
  background-blend-mode: soft-light, normal, normal !important;
}

html body .cap::after,
html body .key::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background-image: url('../assets/textures/ceramic.png');
  background-size: 220px 220px;
  background-repeat: repeat;
  mix-blend-mode: soft-light;
  opacity: 0.5;
}

/* the round caps clip their grain to their own circle */
html body .cap--round::after,
html body .cap--lg::after {
  border-radius: 50%;
}

/* --------------------------------------------------------------------------
   12.2 A MATERIAL LAYER WITH A DIAL ON IT

   Blending the photograph into `background-image` works, but on a near-white
   ground `soft-light` contributes almost nothing — the grain was technically
   present and visually absent, which is the same as not doing it.

   So the material gets its own layer over the case, with an opacity I can
   actually turn. `multiply` is the honest operator here: a photograph of a
   moulded surface is mostly a record of where light DIDN'T reach, and multiply
   is what darkening-only means. The layer is inert to the pointer and sits
   under everything else in the shell.
   -------------------------------------------------------------------------- */

html body .app-shell {
  position: relative;
  isolation: isolate;
}

html body .app-shell::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  pointer-events: none;
  background-image: url('../assets/textures/panel.jpg');
  background-size: 560px 560px;
  background-repeat: repeat;
  mix-blend-mode: multiply;
  opacity: 0.28;
}

/* every real control sits ABOVE the material, never under it */
html body .app-shell > * {
  position: relative;
  z-index: 1;
}

/* At night the same photograph would crush the blacks, so it switches to
   screen — a dark enamel shows its texture in the HIGHLIGHTS, not the shadows.
   Same material, opposite operator, because the ground inverted. */
/* panel.jpg is a photograph of a LIGHT ceramic surface. `screen` on a dark
   ground therefore does not add texture, it adds the photograph's brightness —
   it lifted the enamel to a flat khaki and destroyed the night theme. The
   operator has to be one that carries the base tone through: soft-light keeps
   the dark exactly where it is and contributes only the deviation, which is
   the grain and nothing else. */
html[data-theme='graphite'] body .app-shell::before,
html:not([data-theme='ceramic']):not([data-theme='graphite']) body .app-shell::before {
  /* Third operator, and the reasoning is arithmetic rather than taste:
     panel.jpg is a photograph of a LIGHT ceramic surface, so its mean sits
     well above mid-grey. `screen` added its brightness outright and turned the
     enamel khaki; `soft-light` lightens for any source above 50% and did the
     same thing more slowly. `overlay` keys off the BASE instead of the source,
     and on a dark base overlay behaves as multiply — it can only darken. The
     grain survives; the enamel stays enamel. */
  mix-blend-mode: overlay;
  opacity: 0.16;
}

/* --------------------------------------------------------------------------
   13. THE TRANSPORT IS A ROW OF KEYS AT ONE PITCH

   Measured before this change, against the dial's axis:

       reset  −88      START  0      +1  +88      +5  +168
       gaps:      88          88            80

   Two faults in one row. The gaps are not equal — 88, 88, 80 — and the mass
   is lopsided: one key to the left of centre, two to the right, so the group's
   optical centre sits ~40px right of the key it is supposed to be centred on.
   The row was a three-column grid (128px / 64px / 128px) with each side pushed
   INWARD toward the middle, which keeps START on the dial's axis but only by
   letting the ends fall wherever the content width happens to put them.

   The trade this fixes, stated plainly: START no longer sits exactly on the
   dial's axis. It cannot, while being wider than its neighbours in a row of
   four evenly spaced keys. A constant pitch is what a real transport has and
   what the eye actually checks — nobody measures a key against a dial above
   it, but everybody sees an uneven gap. So: one row, one pitch, centred as a
   group.
   -------------------------------------------------------------------------- */

html body .tt-timer__transportmain {
  display: flex !important;
  align-items: start !important;
  justify-content: center !important;
  gap: var(--part-gap) !important;
  grid-template-columns: none !important;
}

html body .tt-timer__transportside {
  display: flex !important;
  align-items: start !important;
  justify-content: center !important;
  gap: var(--part-gap) !important;
  inline-size: auto !important;
  min-inline-size: 0 !important;
  grid-column: auto !important;
  flex: 0 0 auto !important;
}

/* ==========================================================================
   14. THE KEYS AS ACTUAL PARTS

   At 1:1 the mode and face keys were reading as flat plates with drop shadows.
   Three specific causes, all mine:

   1. THE SEAMS WERE BARS. `.key__seam` is a 4px element sitting just outside
      each key's left and right edge. At full opacity against a pale panel it
      does not read as an occluded gap — it reads as a grey vertical bar
      standing beside the key, i.e. as another component. A seam is the
      DARKNESS BETWEEN two parts that touch; if the parts do not touch there is
      nothing to occlude, and a drawn bar is just a border in disguise.

   2. THE COLLAR WAS A SHELF. It was three hard, zero-blur bands stacked under
      the cap (`0 1px 0`, `0 2px 0`, `0 3px 0`). Three flat steps of tone read
      as a printed ledge. A real cylinder wall TURNS: it is lightest where it
      leaves the top face and darkest where it meets the panel, and that is a
      gradient, not a stack of lines.

   3. THE SEATED KEY WAS A STROKED BOX. A hard dark inset all the way round is
      an outline. A key sitting down in the panel is dark where the housing
      shades it — top and light-side only — and still catches a little bounce
      on the far wall.
   ========================================================================== */

/* 1. the bars go. The keys sit apart on the panel, so there is no seam. */
html body .key__seam {
  display: none !important;
}

/* 2. a wall that turns, drawn as one graded shadow rather than three steps */
html body .key {
  box-shadow:
    /* chamfer, lit arc — upper-left only */
    inset var(--rim-x, 0.8px) var(--rim-y, 1.2px) 0 -0.5px
      rgba(255, 253, 247, calc(var(--rim-a, 0.86) * 0.95)),
    /* the face turning away */
      inset calc(var(--turn-x, -0.8px) * 1.6) calc(var(--turn-y, -1.2px) * 1.6) 2px
      rgba(var(--part-sh), calc(var(--turn-a, 0.17) * 1.2)),
    /* bounce off the panel onto the bottom edge */
      inset 0 -1px 0 rgba(255, 253, 247, 0.3),
    /* THE WALL — one band, blurred downward so it reads as a turned surface */
      0 1px 0 color-mix(in srgb, var(--collar) 55%, var(--cap-lo)),
    0 2px 1.5px color-mix(in srgb, var(--collar) 78%, transparent),
    /* contact shadow where the wall meets the panel */
      0 3px 2.5px rgba(var(--part-sh), 0.34),
    /* ambient */ 0 5px 10px rgba(var(--part-sh), calc(var(--sh-alpha, 0.19) * 0.8))
      !important;
}

/* 3. seated: shaded by the housing on the light side, not outlined all round */
html body .key[aria-selected='true'],
html body .key[aria-checked='true'] {
  box-shadow:
    inset calc(var(--rim-x, 0.8px) * 5) calc(var(--rim-y, 1.2px) * 5) 7px
      rgba(var(--part-sh), 0.38),
    inset calc(var(--turn-x, -0.8px) * 1.4) calc(var(--turn-y, -1.2px) * 1.4) 2px
      rgba(255, 253, 247, 0.24),
    0 1px 0 color-mix(in srgb, var(--collar) 40%, var(--cap-lo)),
    0 1px 2px rgba(var(--part-sh), 0.26) !important;
}

/* The round caps get the same corrected wall, so the whole family agrees. */
html body .cap {
  box-shadow:
    inset var(--rim-x, 0.8px) var(--rim-y, 1.2px) 0 -0.5px
      rgba(255, 253, 247, calc(var(--rim-a, 0.86) * 0.98)),
    inset calc(var(--turn-x, -0.8px) * 1.6) calc(var(--turn-y, -1.2px) * 1.6) 2px
      rgba(var(--part-sh), calc(var(--turn-a, 0.17) * 1.2)),
    inset 0 -1px 0 rgba(255, 253, 247, 0.32),
    0 1px 0 color-mix(in srgb, var(--collar) 55%, var(--cap-lo)),
    0 2px 1.5px color-mix(in srgb, var(--collar) 78%, transparent),
    0 3px 2.5px rgba(var(--part-sh), 0.34),
    0 6px 12px rgba(var(--part-sh), calc(var(--sh-alpha, 0.19) * 0.9)) !important;
}

html body .cap--signal {
  box-shadow:
    inset var(--rim-x, 0.8px) var(--rim-y, 1.2px) 0 -0.5px rgba(255, 226, 214, 0.7),
    inset calc(var(--turn-x, -0.8px) * 1.6) calc(var(--turn-y, -1.2px) * 1.6) 2px
      rgba(60, 8, 8, 0.36),
    inset 0 -1px 0 rgba(255, 226, 214, 0.22),
    0 1px 0 color-mix(in srgb, var(--signal-key) 62%, #000),
    0 2px 1.5px color-mix(in srgb, var(--signal-key) 40%, transparent),
    0 3px 2.5px rgba(52, 10, 8, 0.4),
    0 6px 12px rgba(var(--part-sh), calc(var(--sh-alpha, 0.19) * 0.9)) !important;
}

/* ==========================================================================
   15. THE SETTINGS PANEL — one margin, one type scale

   Measured: 59 elements share a left edge of 935px, and then strays sit at
   938, 947, 994, 1060, 1062… Some of those are legitimately indented controls,
   but 938 and 947 are body-copy blocks missing the margin by 3px and 12px —
   close enough to look like a mistake rather than an indent, which is exactly
   what "haphazardly thrown there" describes.

   Type was worse: EIGHT distinct styles in one panel, including three
   different 12px treatments (400/normal, 500/uppercase/1.68, 500/none/-0.13)
   and two different 16px ones. A settings panel needs three roles, so it gets
   three styles:

     9px   uppercase, tracked   the section rule — a silkscreen heading
     13px  medium, sentence     the name of a setting
     12px  regular, sentence    what the setting does

   Everything else is one of those three or it is a control.
   ========================================================================== */

/* one margin. The body owns the inset; nothing inside re-states it. */
html body .settings__body {
  padding-inline: 1.5rem !important;
}

html body .settings__body > *,
html body .set-section,
html body .set-section > *,
html body .set-seg,
html body .set-seg > * {
  margin-inline: 0 !important;
}

html body .set-section {
  padding-inline: 0 !important;
}

/* 1. the section rule */
html body .set-section__title {
  font-size: 0.5625rem !important;
  font-weight: 400 !important;
  text-transform: uppercase !important;
  letter-spacing: 0.16em !important;
  color: var(--muted-ink) !important;
  margin-block: 1.75rem 0.875rem !important;
}

/* 2. the name of a setting */
html body .set-switch__label,
html body .set-row__label,
html body .settings__body label > span:first-child,
html body .set-field__label {
  font-size: 0.8125rem !important;
  font-weight: 500 !important;
  letter-spacing: 0 !important;
  text-transform: none !important;
  color: var(--ink) !important;
}

/* 3. what it does */
html body .set-switch__hint,
html body .set-row__hint,
html body .set-note,
html body .settings__body p {
  font-size: 0.75rem !important;
  font-weight: 400 !important;
  letter-spacing: 0 !important;
  text-transform: none !important;
  line-height: 1.5 !important;
  color: var(--muted-ink) !important;
  margin-block: 0.25rem 0 !important;
}

/* the segmented legends join the panel's own silkscreen, not a fourth style */
html body .set-seg__legend {
  font-size: 0.5625rem !important;
  font-weight: 400 !important;
  text-transform: lowercase !important;
  letter-spacing: 0.06em !important;
  color: var(--muted-ink) !important;
  margin-block-end: 0.5rem !important;
}

/* the keys inside a segmented control carry the same silkscreen as the panel's
   own keys — they were at 12px/500/uppercase/1.68, a style used nowhere else */
html body .set-seg__opt span {
  font-size: 0.5625rem !important;
  font-weight: 400 !important;
  text-transform: lowercase !important;
  letter-spacing: 0.06em !important;
}

/* consistent rhythm between settings rows, so nothing floats */
html body .set-seg,
html body .set-switch,
html body .set-row {
  margin-block-end: 1.125rem !important;
}

/* --------------------------------------------------------------------------
   15.1 THE REAL CLASS NAMES

   My first pass at the settings type guessed selectors that do not exist
   (.set-switch__label and friends), so it added a style instead of replacing
   any — nine in total, worse than the eight it started with. These are the
   names actually in the DOM, read off the rendered panel:

     .set-row__title      16px/500  the name of a setting   → 13px
     .set-sub__title      12px/400  a sub-heading           → 9px silkscreen
     .set-marks__unit     12px/500u a unit label            → 9px silkscreen
     .field-label         12px/500u a field label           → 9px silkscreen
     .btn--plain          16px/500  a button in the panel   → 13px
     .u-num values        12px/500  a measured value        → kept, tabular

   Four roles left: silkscreen (9), body (12), name/action (13), value (12
   tabular). Everything in the panel is one of those.
   -------------------------------------------------------------------------- */

html body .settings__body .set-row__title {
  font-size: 0.8125rem !important;
  font-weight: 500 !important;
  letter-spacing: 0 !important;
  text-transform: none !important;
  color: var(--ink) !important;
}

html body .settings__body .set-sub__title,
html body .settings__body .set-marks__unit,
html body .settings__body .field-label {
  font-size: 0.5625rem !important;
  font-weight: 400 !important;
  text-transform: uppercase !important;
  letter-spacing: 0.16em !important;
  color: var(--muted-ink) !important;
}

html body .settings__body .btn,
html body .settings__body .btn--plain {
  font-size: 0.8125rem !important;
  font-weight: 500 !important;
  text-transform: none !important;
  letter-spacing: 0 !important;
}

/* a measured value is tabular so columns of them do not shift */
html body .settings__body .u-num,
html body .settings__body .set-volume__value,
html body .settings__body .set-mark__label {
  font-size: 0.75rem !important;
  font-weight: 500 !important;
  font-variant-numeric: tabular-nums !important;
  letter-spacing: 0 !important;
  text-transform: none !important;
  color: var(--ink) !important;
}

/* One row title sits outside .settings__body's subtree and kept 16px. Scope to
   the panel instead of the body so no instance is missed. */
html body .settings__panel .set-row__title {
  font-size: 0.8125rem !important;
  font-weight: 500 !important;
  letter-spacing: 0 !important;
  text-transform: none !important;
}

/* ==========================================================================
   16. MOTION, ICONS, TARGETS — the three measured inconsistencies left

   Audited across every visible element in the shell:

     shadows   18/18 point right/down          ← already correct, one light
     radii     50% / 3px / one 4px stray       ← nearly done
     motion    10 durations, 4 easing curves   ← no system
     icons     8 stroke widths                 ← no system
     targets   6 controls under 44px           ← reach

   The first two are fine. The last three are fixed here.
   ========================================================================== */

/* --------------------------------------------------------------------------
   16.1 A MOTION SCALE

   Durations in use were 0.07 / 0.1 / 0.11 / 0.14 / 0.16 / 0.22 / 0.24 / 0.26 /
   0.48s across four different curves. Nothing repeated, so nothing felt
   authored — motion that varies at random reads as jitter even when each
   individual value is defensible.

   Three durations, because there are three kinds of change on this object:

     70ms   a part moves under your finger   — must feel mechanical, not animated
     160ms  a state resolves                 — a lamp lights, a fill changes
     280ms  something enters or leaves       — a panel, a sheet

   One curve for anything physical: parts have mass, so they leave quickly and
   settle. Linear only for light, which has no inertia.
   -------------------------------------------------------------------------- */

:root {
  --m-press: 70ms;
  --m-state: 160ms;
  --m-move: 280ms;
  --m-ease: cubic-bezier(0.22, 0.9, 0.35, 1);
}

html body .cap,
html body .key,
html body .switch__thumb,
html body .set-seg__opt span,
html body .chip[aria-pressed]::after {
  transition-duration: var(--m-press) !important;
  transition-timing-function: var(--m-ease) !important;
}

html body .key__lamp,
html body .brand::after,
html body .legend--pair > span,
html body .switch__track,
html body .tt-timer__task .field,
html body input.field {
  transition-duration: var(--m-state) !important;
  transition-timing-function: linear !important;
}

html body .settings__panel,
html body .tt-panel__sheet,
html body .day-strip__left {
  transition-duration: var(--m-move) !important;
  transition-timing-function: var(--m-ease) !important;
}

/* --------------------------------------------------------------------------
   16.2 ONE ICON WEIGHT

   Eight stroke widths were rendering: 0.3, 0.55, 0.8, 1, 1.35, 1.6, 1.75 and
   4.14. Icons at mixed weights read as clip art collected from different
   places, which is the single fastest way to lose the "one object" illusion —
   the eye reads stroke weight as material thickness.

   One weight for every piece of chrome iconography, and round joins, because
   these are engraved marks on a panel rather than drawn illustrations.
   -------------------------------------------------------------------------- */

html body .btn--icon svg,
html body .chassis__tools svg,
html body .settings svg,
html body .tt-panel svg {
  stroke-width: 1.5px !important;
  stroke-linecap: round !important;
  stroke-linejoin: round !important;
  vector-effect: non-scaling-stroke;
}

/* the wordmark's dial glyph is a filled sector, not a stroked icon — it must
   not inherit the chrome weight */
html body .brand__mark path {
  stroke-width: 0 !important;
}

/* --------------------------------------------------------------------------
   16.3 EVERY TARGET REACHES 44px

   The face keys (56x24), the mode keys (64x34), the repeat chip (93x34) and
   the session field (38 tall) were all under the comfortable minimum. The keys
   already had invisible extensions; the chip and the field did not.

   The visible part keeps its size — a small key is correct on a panel — and an
   inert pseudo-element carries the reach. Vertical only, so no target steals
   its neighbour's taps.
   -------------------------------------------------------------------------- */

html body .chip {
  position: relative;
}

html body .chip::before {
  content: '';
  position: absolute;
  inset-block: -5px;
  inset-inline: 0;
  pointer-events: auto;
}

html body .tt-timer__task .field,
html body input.field {
  min-block-size: 2.75rem !important;
}

/* the one remaining 4px radius joins the system */
html body .tt-timer__readout {
  border-radius: var(--r-part) !important;
}

/* 16.4 — the remaining strays.

   The stroke-width finding was partly a false alarm and is worth recording as
   one: the dial's tick marks legitimately carry different weights (major vs
   minor), and forcing those to one value would destroy the scale's legibility.
   Only CHROME ICONS needed unifying, and they are now 1.5px. The dial keeps
   its own hierarchy.

   Durations: everything interactive is now 70ms or 160ms. These are the last
   containers still on their own timings. */
html body .tt-panel,
html body .tt-mod,
html body .settings__scrim,
html body .tt-panel__panes {
  transition-duration: var(--m-move) !important;
  transition-timing-function: var(--m-ease) !important;
}

/* The session field measured 38px tall — under the comfortable minimum, and
   the one target the extension trick cannot help because you must be able to
   click precisely into text. It gets real height instead. */
html body .tt-timer__task .field,
html body input.field,
html body .tt-timer__task input {
  block-size: 2.75rem !important;
  min-block-size: 2.75rem !important;
}

/* 16.5 — matching specificity, again.

   `html[data-dock="desktop"] body .tt-timer__task input` pins the field to
   2.375rem at (0,3,3) !important. This is the FIFTH time in this pass that a
   correction has had to escalate specificity to land, and it is worth naming
   as a structural fact rather than patching quietly: controls.css states most
   of its rules at (0,3,3) with !important, so every later correction must
   match or exceed that weight. Precision is therefore expensive here, and the
   cost compounds — which is the real reason small details keep surviving. */
html[data-dock='desktop'] body .tt-timer__task input,
html[data-dock='desktop'] body .tt-timer__task .field,
html[data-theme] body .tt-timer__task .field {
  block-size: 2.75rem !important;
  min-block-size: 2.75rem !important;
  padding-block: 0 !important;
}

/* ==========================================================================
   17. THE ENCLOSURE — making it a physical object

   The note was that this still reads flat and generated rather than like a
   thing you could touch, with virtual guitar pedals as the reference. That is
   a fair and specific target, and it is worth being precise about WHY those
   look real, because it is not "more texture":

     1. The enclosure has THICKNESS. A pedal is a milled aluminium box: you can
        see the chamfer catch light on the top edge and the wall fall into
        shadow underneath. Our case was a rectangle with a radius — no edge at
        all, so nothing said how thick it is.
     2. The surface has HIGH-FREQUENCY DETAIL AT REAL STRENGTH. Ours was a
        photograph at 0.28 opacity over a CSS gradient — a whisper. You cannot
        feel a whisper.
     3. There is HARDWARE. Screws, seams, fasteners. Not decoration: they are
        what tells you the object was assembled rather than drawn.
     4. Surfaces DARKEN AT THE MARGINS. Every real object turns away from the
        camera at its edges. A flat fill never does.
     5. There is WEAR. A pedal that has been on a board has micro-scratches
        catching light. Perfection reads as render; imperfection reads as
        object.

   All five are geometry and light, not new assets. Built here.
   ========================================================================== */

/* --------------------------------------------------------------------------
   17.1 THE CASE HAS A MILLED EDGE

   A chamfer is two facets: the top one catches the key light almost to
   specular, the wall beneath turns away hard. Drawn as opposed hairline insets
   so the edge reads as machined rather than blurred, plus a real cast shadow
   under the whole body so it sits ON something.
   -------------------------------------------------------------------------- */

html[data-theme] body .app-shell,
html:not([data-theme]) body .app-shell,
html body .app-shell {
  box-shadow:
    /* the chamfer, lit facet — upper-left, bright, hairline */
    inset var(--rim-x, 0.8px) var(--rim-y, 1.2px) 0 0 rgba(255, 255, 253, 0.95),
    inset calc(var(--rim-x, 0.8px) * 2) calc(var(--rim-y, 1.2px) * 2) 1px 0
      rgba(255, 253, 247, 0.5),
    /* the chamfer, wall turning away — lower-right */
      inset calc(var(--turn-x, -0.8px) * 2) calc(var(--turn-y, -1.2px) * 2) 1px 0
      rgba(var(--part-sh), 0.34),
    inset calc(var(--turn-x, -0.8px) * 4) calc(var(--turn-y, -1.2px) * 4) 3px 0
      rgba(var(--part-sh), 0.16),
    /* the body sitting on the desk: contact, then volume */
      0 2px 3px rgba(var(--part-sh), 0.3),
    0 10px 24px rgba(var(--part-sh), 0.2),
    0 28px 60px rgba(var(--part-sh), 0.14) !important;
}

/* --------------------------------------------------------------------------
   17.2 THE SURFACE AT REAL STRENGTH, PLUS WEAR AND FALLOFF

   Three layers on one pseudo-element:
     · the moulded panel photograph, at a strength you can actually see
     · micro-scratches, which is what makes a surface read as USED
     · a margin falloff, because every real surface turns away at its edges

   The panel photograph is 512px and was being tiled at 560px — i.e. upscaled,
   which softens exactly the high-frequency detail that sells material. Tiled
   at 300px it renders BELOW its native resolution, so the grain stays crisp.
   -------------------------------------------------------------------------- */

/* Operator matters more than opacity. `multiply` at 0.62 gave real strength
   but dragged the warm bone tone toward the photograph's own mid-greys — the
   case went muddy. To have BOTH strength and hue you have to separate the two
   jobs: soft-light carries the grain and preserves the base colour exactly,
   and the tonal work (falloff, depth) is done by the gradient underneath in
   the shell's own background, where it can be tuned without touching material.

   The scratches also came back as long white streaks — that is damage, not
   wear. Real micro-wear is below the threshold of "a mark you could point at".
   Dropped to a level you notice only by its absence. */
/* THE ASSETS WERE EMPTY. Measured standard deviation of every texture in the
   repo, sampled from the files themselves:

       ceramic.png     mean 123.5   stddev  1.17
       panel.jpg       mean 223.0   stddev  1.13
       plaster.png     mean 120.8   stddev  1.80
       ground.jpg      mean 230.7   stddev  1.03
       aluminium.jpg   mean 169.4   stddev 11.94   ← the only one with content

   A standard deviation of ~1 out of 255 is not a faint texture, it is a FLAT
   FILE. Four of the five contained essentially no variation, which is why no
   blend mode or opacity ever made the surface read as material: there was
   nothing in the file to blend. Multiply produced mud because it carried the
   file's TONE; soft-light produced nothing because it carries DEVIATION and
   there was none to carry.

   The grain is therefore synthesised — seamless multi-octave value noise on a
   wrapping lattice, normalised to mean 128 so soft-light carries it exactly
   and cannot shift hue.

   BLOTCH IS LOW-FREQUENCY ENERGY. The first synthesised map included octaves
   down to 1/16 of the tile and read as stained paper, especially in dark where
   the blend amplifies it. Every map now stops at 1/64 — mid and high frequency
   only — so they differ in CHARACTER without any of them patching.
   ========================================================================== */

/* --------------------------------------------------------------------------
   SKINS — seven materials, each in light and dark

   Skin is orthogonal to theme: skin decides what the object is MADE OF, theme
   decides what light is falling on it. Every skin therefore has to work in
   both, which is why they are two controls and not one list — collapsing them
   would force a combinatorial menu (light chrome, dark chrome, light linen…)
   instead of two independent choices.

   Every grain map is a real photograph run through the same pipeline:
   greyscale -> high-pass (keep what the photo says about SURFACE, discard what
   it says about lighting, which is ours to supply) -> seamless wrap-blend ->
   normalise to mean 128 so soft-light carries it and cannot shift hue.

       ceramic    moulded ABS tooth        the default Braun object
       stone      honed limestone          mineral, matte
       alloy      anodised aluminium       matte, fine, the softest metal
       brushed    brushed stainless        satin, strongly directional
       chrome     polished chrome          a mirror: no tooth, all reflection
       enamel     vitreous enamel on steel glassy, the finest grain of the set
       linen      speaker grille cloth     woven, the coarsest of the set

   THREE METALS, THREE PROCESSES. Anodising, brushing and polishing do not
   produce three shades of grey — they produce three different relationships
   with light, and that is what separates them here. Anodised aluminium is
   matte and scatters: narrow value range, weak specular, soft edge. Brushed
   steel is satin and anisotropic: the grain IS the material, so it carries the
   strongest map of the set at the largest tile. Polished chrome has no tooth
   at all — its character is the reflection, so it gets the WEAKEST map, the
   widest value range and the hardest specular. If two of them ever look alike,
   the fault is in those three numbers, not in the pigment.

   The palette moves with the material where the material demands it — steel
   cannot be bone white and linen cannot be grey — and, since §20.5, so does
   the signal. See that section for why the "one red everywhere" rule that used
   to sit here was wrong.
   -------------------------------------------------------------------------- */

:root,
html[data-skin='ceramic'],
.set-swatch[data-skin='ceramic'] {
  --grain: url('../assets/textures/grain-fine.png');
  --grain-size: 190px;
  --grain-light: 0.5;
  --grain-dark: 0.34;
}

html[data-skin='stone'],
.set-swatch[data-skin='stone'] {
  --grain: url('../assets/textures/grain-stone.png');
  --grain-size: 260px;
  --grain-light: 0.62;
  --grain-dark: 0.4;
  --ramp-150: #e9e7de;
  --ramp-100: #f1efe6;
  --ramp-200: #e7e4da;
}

html[data-skin='stone'][data-theme='graphite'],
html[data-theme='graphite'] .set-swatch[data-skin='stone'] {
  --ramp-150: #1f1e1b;
  --ramp-100: #272521;
  --ramp-200: #1a1916;
}

/* ALLOY — anodised aluminium. The matte one. Narrow value range because a
   scattering surface has nowhere to put a highlight, and a weak-to-moderate
   grain because anodising fills the tooth rather than cutting it. */
html[data-skin='alloy'],
.set-swatch[data-skin='alloy'] {
  --grain: url('../assets/textures/grain-alloy.png');
  --grain-size: 300px;
  --grain-light: 0.5;
  --grain-dark: 0.38;
  --ramp-050: #f2f1ed;
  --ramp-100: #eae9e4;
  --ramp-150: #e2e1db;
  --ramp-200: #dbdad4;
  --ramp-250: #d1d0c9;
  --ramp-300: #c2c1b9;
}

html[data-skin='alloy'][data-theme='graphite'],
html[data-theme='graphite'] .set-swatch[data-skin='alloy'] {
  --ramp-050: #3a3a36;
  --ramp-100: #2f2f2c;
  --ramp-150: #272724;
  --ramp-200: #201f1d;
  --ramp-250: #191918;
  --ramp-300: #121211;
}

/* ENAMEL — fired glass on steel. The smoothest surface here, so the grain is
   deliberately the weakest: enamel's character is its near-absence of texture
   and its slightly cooler, cleaner white. */
html[data-skin='enamel'],
.set-swatch[data-skin='enamel'] {
  --grain: url('../assets/textures/grain-enamel.png');
  --grain-size: 170px;
  --grain-light: 0.4;
  --grain-dark: 0.28;
  --ramp-150: #f0efeb;
  --ramp-100: #f7f6f3;
  --ramp-200: #eae9e4;
  --ramp-300: #d6d5cf;
}

html[data-skin='enamel'][data-theme='graphite'],
html[data-theme='graphite'] .set-swatch[data-skin='enamel'] {
  --ramp-150: #1e1f20;
  --ramp-100: #262728;
  --ramp-200: #191a1b;
  --ramp-300: #0e0f10;
}

/* BRUSHED — stainless steel, satin. The grain is the whole identity, so it
   takes the LARGEST tile of the set (long strokes, not short scratches) and
   the STRONGEST layer. The body is a real steel grey — deliberately several
   steps darker and cooler than anodised alloy, because two metals that differ
   only in hue read as one metal in two paints. The ink goes cool with it. */
html[data-skin='brushed'],
.set-swatch[data-skin='brushed'] {
  --grain: url('../assets/textures/grain-brushed.png');
  --grain-size: 420px;
  --grain-light: 0.85;
  --grain-dark: 0.62;
  --ramp-000: #ffffff;
  --ramp-050: #cfd4d8;
  --ramp-100: #c2c7cc;
  --ramp-150: #b5bbc1;
  --ramp-200: #a9afb6;
  --ramp-250: #9aa1a8;
  --ramp-300: #848c94;
  --ramp-400: #5d666d;
  --ramp-600: #40474d;
  --ramp-700: #2c3237;
  --ramp-800: #1f2427;
  --ramp-900: #14181b;
}

html[data-skin='brushed'][data-theme='graphite'],
html[data-theme='graphite'] .set-swatch[data-skin='brushed'] {
  --ramp-050: #3c4247;
  --ramp-100: #31363a;
  --ramp-150: #282c30;
  --ramp-200: #202428;
  --ramp-250: #191c1f;
  --ramp-300: #111416;
  --ramp-400: #4d545a;
  --ramp-600: #9aa3ab;
  --ramp-700: #bcc5cc;
  --ramp-800: #d9e0e6;
  --ramp-900: #eef2f5;
}

/* CHROME — polished, and therefore the opposite problem to brushed: a mirror
   has no tooth to photograph. Its grain map is the WEAKEST of the seven and
   exists only to stop the surface being mathematically flat; everything you
   read as chrome comes from the value RANGE (the widest here, near-white to
   near-black in one part) and from the specular, which §20.4 takes to full.
   Cool, because chrome reflects sky. */
html[data-skin='chrome'],
.set-swatch[data-skin='chrome'] {
  --grain: url('../assets/textures/grain-chrome.png');
  --grain-size: 200px;
  --grain-light: 0.28;
  --grain-dark: 0.2;
  --ramp-000: #ffffff;
  --ramp-050: #f0f5fa;
  --ramp-100: #dee6ee;
  --ramp-150: #ccd7e2;
  --ramp-200: #b9c6d4;
  --ramp-250: #a2b2c3;
  --ramp-300: #8195a9;
  --ramp-400: #5c6f81;
  --ramp-600: #414d58;
  --ramp-700: #2b343d;
  --ramp-800: #1c232a;
  --ramp-900: #10161b;
}

/* BLACK CHROME at night — the same polish over a dark substrate, which is a
   real finish and not an inversion. The range stays wide; it just moves down. */
html[data-skin='chrome'][data-theme='graphite'],
html[data-theme='graphite'] .set-swatch[data-skin='chrome'] {
  --ramp-050: #40474e;
  --ramp-100: #2c3238;
  --ramp-150: #21262b;
  --ramp-200: #191d21;
  --ramp-250: #111417;
  --ramp-300: #090b0d;
  --ramp-400: #4a5762;
  --ramp-600: #9dabb6;
  --ramp-700: #c0cad3;
  --ramp-800: #dfe6ec;
  --ramp-900: #f2f6fa;
}

/* LINEN — grille cloth. The coarsest weave of the set, so it takes the
   largest tile and the strongest layer; a textile that reads as smooth reads
   as plastic. */
html[data-skin='linen'],
.set-swatch[data-skin='linen'] {
  --grain: url('../assets/textures/grain-linen.png');
  --grain-size: 300px;
  --grain-light: 0.72;
  --grain-dark: 0.5;
  --ramp-150: #e4dfd1;
  --ramp-100: #ece8dc;
  --ramp-200: #ded8c8;
  --ramp-300: #cabfa8;
}

html[data-skin='linen'][data-theme='graphite'],
html[data-theme='graphite'] .set-swatch[data-skin='linen'] {
  --ramp-150: #24221c;
  --ramp-100: #2c2a23;
  --ramp-200: #1d1b16;
  --ramp-300: #131210;
}

html body .app-shell::before {
  background-image: var(--grain) !important;
  /* rendered BELOW the 512px source so the tooth stays crisp instead of being
     upscaled into the soft patches that read as blotching */
  background-size: var(--grain-size) var(--grain-size) !important;
  background-repeat: repeat !important;
  background-blend-mode: normal !important;
  filter: none !important;
  mix-blend-mode: soft-light !important;
  opacity: var(--grain-light) !important;
}

html[data-theme='graphite'] body .app-shell::before,
html:not([data-theme='ceramic']):not([data-theme='graphite']) body .app-shell::before {
  mix-blend-mode: soft-light !important;
  opacity: var(--grain-dark) !important;
}

/* The parts are moulded from the same stock, so they carry the same tooth at
   the same scale. A cap smoother than the panel it sits on breaks the
   illusion faster than no texture at all. */
html body .cap::after,
html body .key::after {
  background-image: var(--grain) !important;
  background-size: var(--grain-size) var(--grain-size) !important;
  mix-blend-mode: soft-light !important;
  opacity: calc(var(--grain-light) * 0.6) !important;
}

/* Anodised parts scatter, so the three cap stops sit CLOSE together — a matte
   cylinder is described by its collar, not by a gradient across its face.
   Compare with chrome below, where the same three stops are pulled as far
   apart as the ramp allows. That spread is the difference between the two
   materials, and it is deliberate. */
html[data-skin='alloy'] body .cap,
html[data-skin='alloy'] body .key {
  --cap-hi: #f7f6f2;
  --cap-mid: #eae9e4;
  --cap-lo: #d6d5ce;
  --collar: #a8a79f;
  --collar-deep: #85847c;
}

html[data-skin='alloy'][data-theme='graphite'] body .cap,
html[data-skin='alloy'][data-theme='graphite'] body .key {
  --cap-hi: #4e4e49;
  --cap-mid: #3b3b37;
  --cap-lo: #2a2a27;
  --collar: #171715;
  --collar-deep: #0b0b0a;
}

/* Stone is a mineral body: slightly cooler and more matte than the ceramic. */
html[data-skin='stone'],
.set-swatch[data-skin='stone'] {
  --ramp-150: #e9e7de;
  --ramp-100: #f1efe6;
  --ramp-200: #e7e4da;
}

html[data-skin='stone'][data-theme='graphite'],
html[data-theme='graphite'] .set-swatch[data-skin='stone'] {
  --ramp-150: #1f1e1b;
  --ramp-100: #272521;
  --ramp-200: #1a1916;
}

/* The tonal falloff moves to the shell's own background, under the material,
   so it shapes the light without touching the surface. */
html[data-theme] body .app-shell,
html:not([data-theme]) body .app-shell,
html body .app-shell {
  background-image:
    radial-gradient(
      132% 108% at calc(32% + var(--hl-x, -8%)) calc(14% + var(--hl-y, -12%)),
      rgba(255, 255, 255, 0) 40%,
      rgba(var(--part-sh), 0.09) 76%,
      rgba(var(--part-sh), 0.2) 100%
    ),
    radial-gradient(
      120% 90% at calc(30% + var(--hl-x, -8%)) calc(12% + var(--hl-y, -12%)),
      color-mix(in srgb, var(--ramp-150) 92%, #fff) 0%,
      var(--ramp-150) 48%,
      color-mix(in srgb, var(--ramp-150) 90%, var(--edge)) 100%
    ) !important;
  background-size: cover, cover !important;
  background-repeat: no-repeat, no-repeat !important;
  background-blend-mode: normal, normal !important;
}

/* --------------------------------------------------------------------------
   17.3 FASTENERS

   Four screws, one at each corner of the case, drawn on the shell's existing
   ::after so no markup changes. Each is a real countersunk head: a dark
   recessed ring, a domed slug lit from the upper left, and a slot that casts
   its own shadow. They are small and quiet — you are not meant to look at
   them, you are meant to believe the case is held together.
   -------------------------------------------------------------------------- */

html body .app-shell::after {
  content: '' !important;
  position: absolute !important;
  inset: 0 !important;
  z-index: 2 !important;
  pointer-events: none !important;
  border-radius: inherit !important;
  --screw:
    radial-gradient(
        circle at 50% 50%,
        rgba(var(--part-sh), 0.34) 0 2.6px,
        rgba(var(--part-sh), 0.16) 2.6px 3.4px,
        rgba(0, 0, 0, 0) 3.4px
      ),
    radial-gradient(
        circle at calc(50% + var(--hl-x, -8%) * 0.3) calc(50% + var(--hl-y, -12%) * 0.3),
        color-mix(in srgb, var(--cap-hi) 92%, #fff) 0 1.1px,
        var(--cap-mid) 1.1px 2.1px,
        color-mix(in srgb, var(--cap-lo) 70%, rgb(var(--part-sh))) 2.1px 2.5px,
        rgba(0, 0, 0, 0) 2.5px
      ),
    linear-gradient(
        118deg,
        rgba(0, 0, 0, 0) calc(50% - 0.45px),
        rgba(var(--part-sh), 0.5) calc(50% - 0.45px) calc(50% + 0.45px),
        rgba(0, 0, 0, 0) calc(50% + 0.45px)
      );
  background-image: var(--screw), var(--screw), var(--screw), var(--screw) !important;
  background-size: 9px 9px !important;
  background-repeat: no-repeat !important;
  background-position:
    18px 18px,
    calc(100% - 18px) 18px,
    18px calc(100% - 18px),
    calc(100% - 18px) calc(100% - 18px) !important;
}

/* ==========================================================================
   18. THE VERTICAL RHYTHM — finished, and smaller than I thought

   I reverted this twice and recorded it as unfinishable without a layout
   change. That was wrong, and the reason is worth keeping: I had measured the
   WRONG ELEMENTS. The original "12, 7, 8, 8, 22, 26" sampled readout and meta,
   which live INSIDE .tt-timer__stage, mixed with siblings that live outside
   it. Two different coordinate systems compared as if they were one list.

   Measured properly, .tt-timer is a flex column with `gap: 8px`, and its
   children already sit at a uniform 8 / 8 / 8. The outer rhythm was never
   broken. That is also why my margin-based "fixes" blew the height budget:
   they added to a gap that was already correct and already uniform.

   Two values are genuinely off the scale, and only two:

       transport margin-block-end   26.7px   → 24px  (an arbitrary value)
       readout → meta                7px     →  8px  (inside the stage)

   Both are corrections of a few pixels, and both FREE space rather than
   consuming it, so the fixed-height shell is unaffected.
   ========================================================================== */

html body .tt-timer__transport {
  margin-block-end: 1.5rem !important;
}

html body .tt-timer__readout {
  margin-block-end: 0.5rem !important;
}

html body .tt-timer__meta {
  margin-block: 0 !important;
}

/* ==========================================================================
   19. THE DIAL SAYS "TURN ME"

   The single biggest intuitiveness gap: the dial is the primary input and, at
   rest, nothing on it invites a hand. A physical Time Timer has a knob you can
   see; ours had a handle that only became obvious once you were already
   dragging. People who do not discover the drag never meet the product.

   The affordance has to be honest and quiet — a pulsing hint would be noise
   for someone using this to concentrate. So: the handle carries a permanent
   knurled grip (metal for the thing you touch, which is the existing rule),
   and the cursor states the gesture. Nothing animates on its own.
   ========================================================================== */

html body .dial [class*='knob'],
html body .dial [class*='handle'] {
  cursor: grab;
}

html body .dial [class*='knob']:active,
html body .dial [class*='handle']:active,
html body .dial:active {
  cursor: grabbing;
}

/* The whole face is draggable, so the whole face says so. */
html body .tt-timer__dial [role='slider'] {
  cursor: grab;
}

html body .tt-timer__dial [role='slider']:active {
  cursor: grabbing;
}

/* ==========================================================================
   §20  SKIN IDENTITY — a material is not a colour

   Up to here a skin changed two things: the ramp and the grain map. That is
   why the seven read as one object in seven paint jobs rather than as seven
   objects. A material decides more than its tone — it decides how its parts
   are made, how sharply they can be moulded, how its face is printed, and
   what its lettering is cut from. This section supplies the rest.

   Three rules held throughout:

     1. THE SIGNAL IS ONE IDEA, NOT ONE HEX. See §20.5 — a single red across
        seven materials was the wrong rule, and it showed.
     2. LEGIBILITY IS NOT A STYLE CHOICE. Every numeral face below was measured
        for tabular advance before it was allowed near the readout — Georgia
        was tried and rejected on exactly that test (18.4px of drift per 100px,
        because its figures are old-style). A skin may change the voice of the
        type; it may not make the clock harder to read.
     3. NO NEW BYTES. Every face is one the system already has. The app ships
        one 42KB woff2 and this section does not add a second — a timer that
        must work offline on a train cannot spend a megabyte on seven fonts,
        and each stack degrades through the system's own face back to Inter.
   ========================================================================== */

/* --------------------------------------------------------------------------
   §20.1  TYPE — what the lettering is cut from

   Each face is the one that materially belongs to the object, not a mood:

     ceramic   Inter            the Braun ABS panel; the grotesque it was made in
     enamel    Helvetica Neue   porcelain-enamel signage is Swiss, always
     stone     Optima           drawn by Zapf from carved Florentine stone
     alloy     SF Mono          engraved instrument panels are monoline machine cuts
     brushed   Helvetica Neue   the engraved steel instrument panel
     chrome    Futura           deco and American automotive, both geometric
     linen     Gill Sans        British grille-cloth hi-fi wore Gill and nothing else
   -------------------------------------------------------------------------- */

html[data-skin='enamel'],
.set-swatch[data-skin='enamel'] {
  --font-sans: "Helvetica Neue", Helvetica, "Inter", -apple-system, Arial, sans-serif;
  --font-num: "Helvetica Neue", Helvetica, "Inter", -apple-system, Arial, sans-serif;
}

html[data-skin='stone'],
.set-swatch[data-skin='stone'] {
  --font-sans: Optima, "Optima Nova", Candara, "Gill Sans", "Inter", sans-serif;
  --font-num: Optima, "Optima Nova", Candara, "Gill Sans", "Inter", sans-serif;
}

/* Anodised aluminium is the modern extruded part — Avenir's geometric
   humanism is the type that goes on one. */
html[data-skin='alloy'],
.set-swatch[data-skin='alloy'] {
  --font-sans: "Avenir Next", Avenir, "Inter", -apple-system, sans-serif;
  --font-num: "Avenir Next", Avenir, "Inter", -apple-system, sans-serif;
}

/* Brushed is the one skin whose numerals are monospaced, and it is not a
   flourish: an engraved steel panel is cut by a machine that moves in fixed
   steps, so its figures are necessarily equal-width. Monospace is also tabular
   by definition, which makes this the safest of the seven for the readout. */
html[data-skin='brushed'],
.set-swatch[data-skin='brushed'] {
  --font-sans: "Helvetica Neue", Helvetica, "Inter", Arial, sans-serif;
  --font-num: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
  --font-num-feat: "tnum" 1;
}

/* Chrome-era means deco and it means American automotive, and both of those
   are drawn in geometric sans. Futura's figures were measured tabular before
   they were allowed near the readout. */
html[data-skin='chrome'],
.set-swatch[data-skin='chrome'] {
  --font-sans: Futura, "Futura PT", "Century Gothic", "Inter", sans-serif;
  --font-num: Futura, "Futura PT", "Century Gothic", "Inter", sans-serif;
}

html[data-skin='linen'],
.set-swatch[data-skin='linen'] {
  --font-sans: "Gill Sans", "Gill Sans MT", "Avenir Next", "Inter", sans-serif;
  --font-num: "Gill Sans", "Gill Sans MT", "Avenir Next", "Inter", sans-serif;
}

/* Inter's optical-size axis exists only in Inter. On any skin that has left
   it, the bundle is inert at best and, on a variable system face, could bind
   a real axis of the same name — so it is cleared rather than left dangling. */
html[data-skin='enamel'],
html[data-skin='stone'],
html[data-skin='alloy'],
html[data-skin='brushed'],
html[data-skin='chrome'],
html[data-skin='linen'],
.set-swatch[data-skin='linen'] {
  --vf-display: normal;
  --vf-title: normal;
  --vf-body: normal;
  --vf-label: normal;
  --vf-micro: normal;
}

/* Inter's feature set — ss01, cv01, cv08 — exists only in Inter's subset, and
   naming a feature a face does not have is harmless but pointless. Kept
   honest so nothing downstream believes a stylistic set is applied. */
html[data-skin='stone'],
html[data-skin='alloy'],
html[data-skin='chrome'],
html[data-skin='linen'],
html[data-skin='enamel'],
.set-swatch[data-skin='enamel'] {
  --feat-text: "calt" 1;
  --feat-caps: "calt" 1, "case" 1;
  --feat-num: "tnum" 1;
  --feat-zero: "tnum" 1;
}

/* --------------------------------------------------------------------------
   §20.2  THE PARTS — what the buttons are moulded from

   A cap is a cylinder: a flat top and a side wall descending into the panel.
   What changes per material is the pigment of that top, the colour of the
   wall, and how hard a specular the surface returns. Enamel and chrome are
   glossy and hold a tight highlight; stone, linen and anodised alloy are matte
   and barely catch. The three metals are separated here by the SPREAD between
   their three cap stops — tight for anodising, moderate for brushing, extreme
   for polish — because that spread is what the eye actually reads as finish.
   -------------------------------------------------------------------------- */

html[data-skin='enamel'] body .cap,
html[data-skin='enamel'] body .key,
html[data-skin='enamel'] body .set-seg__opt span,
html[data-skin='enamel'] body .switch__thumb,
html[data-skin='enamel'] body .btn,
html[data-skin='enamel'] body .chip:not([aria-pressed]),
html[data-skin='enamel'] body .segmented__key[aria-selected='true'] {
  --cap-hi: #ffffff;
  --cap-mid: #f7f7f4;
  --cap-lo: #e6e6e1;
  --collar: #b6b6ae;
  --collar-deep: #8b8b83;
}

html[data-skin='enamel'][data-theme='graphite'] body .cap,
html[data-skin='enamel'][data-theme='graphite'] body .key,
html[data-skin='enamel'][data-theme='graphite'] body .set-seg__opt span,
html[data-skin='enamel'][data-theme='graphite'] body .switch__thumb,
html[data-skin='enamel'][data-theme='graphite'] body .btn,
html[data-skin='enamel'][data-theme='graphite'] body .chip:not([aria-pressed]),
html[data-skin='enamel'][data-theme='graphite'] body .segmented__key[aria-selected='true'] {
  --cap-hi: #4b4c50;
  --cap-mid: #36373b;
  --cap-lo: #25262a;
  --collar: #131417;
  --collar-deep: #08090b;
}

html[data-skin='stone'] body .cap,
html[data-skin='stone'] body .key,
html[data-skin='stone'] body .set-seg__opt span,
html[data-skin='stone'] body .switch__thumb,
html[data-skin='stone'] body .btn,
html[data-skin='stone'] body .chip:not([aria-pressed]),
html[data-skin='stone'] body .segmented__key[aria-selected='true'] {
  --cap-hi: #f2f0e9;
  --cap-mid: #e9e7de;
  --cap-lo: #d7d4c9;
  --collar: #a5a193;
  --collar-deep: #7e7a6d;
}

html[data-skin='stone'][data-theme='graphite'] body .cap,
html[data-skin='stone'][data-theme='graphite'] body .key,
html[data-skin='stone'][data-theme='graphite'] body .set-seg__opt span,
html[data-skin='stone'][data-theme='graphite'] body .switch__thumb,
html[data-skin='stone'][data-theme='graphite'] body .btn,
html[data-skin='stone'][data-theme='graphite'] body .chip:not([aria-pressed]),
html[data-skin='stone'][data-theme='graphite'] body .segmented__key[aria-selected='true'] {
  --cap-hi: #3d3c37;
  --cap-mid: #2f2e2a;
  --cap-lo: #222120;
  --collar: #121110;
  --collar-deep: #080807;
}

/* Satin steel: a moderate spread — more than matte anodising, far less than a
   mirror — and a collar that is genuinely darker steel rather than a shadow. */
html[data-skin='brushed'] body .cap,
html[data-skin='brushed'] body .key,
html[data-skin='brushed'] body .set-seg__opt span,
html[data-skin='brushed'] body .switch__thumb,
html[data-skin='brushed'] body .btn,
html[data-skin='brushed'] body .chip:not([aria-pressed]),
html[data-skin='brushed'] body .segmented__key[aria-selected='true'] {
  --cap-hi: #eef1f4;
  --cap-mid: #d5dade;
  --cap-lo: #b4bbc2;
  --collar: #7f878e;
  --collar-deep: #5c646b;
}

html[data-skin='brushed'][data-theme='graphite'] body .cap,
html[data-skin='brushed'][data-theme='graphite'] body .key,
html[data-skin='brushed'][data-theme='graphite'] body .set-seg__opt span,
html[data-skin='brushed'][data-theme='graphite'] body .switch__thumb,
html[data-skin='brushed'][data-theme='graphite'] body .btn,
html[data-skin='brushed'][data-theme='graphite'] body .chip:not([aria-pressed]),
html[data-skin='brushed'][data-theme='graphite'] body .segmented__key[aria-selected='true'] {
  --cap-hi: #5b636b;
  --cap-mid: #434a51;
  --cap-lo: #2e343a;
  --collar: #15191d;
  --collar-deep: #0a0c0e;
}

/* Phenolic resin, polished. Dark by nature in BOTH themes — the theme changes
   the room, not the moulding — so its caps are a lifted brown-black rather
   than anything approaching cream. */
/* A MIRROR, and this is where it is actually made. The three cap stops are
   pulled as far apart as any part on the object gets — near-white at the lit
   edge, mid at the face, a deep blue-grey where it turns away — because that
   compressed sky-to-ground sweep IS what the eye reads as polish. Put these
   next to the anodised stops above: same geometry, same light, opposite
   material. */
html[data-skin='chrome'] body .cap,
html[data-skin='chrome'] body .key,
html[data-skin='chrome'] body .set-seg__opt span,
html[data-skin='chrome'] body .switch__thumb,
html[data-skin='chrome'] body .btn,
html[data-skin='chrome'] body .chip:not([aria-pressed]),
html[data-skin='chrome'] body .segmented__key[aria-selected='true'] {
  --cap-hi: #ffffff;
  --cap-mid: #e4ecf3;
  --cap-lo: #a3b6c8;
  --collar: #6d8196;
  --collar-deep: #41556a;
}

html[data-skin='chrome'][data-theme='graphite'] body .cap,
html[data-skin='chrome'][data-theme='graphite'] body .key,
html[data-skin='chrome'][data-theme='graphite'] body .set-seg__opt span,
html[data-skin='chrome'][data-theme='graphite'] body .switch__thumb,
html[data-skin='chrome'][data-theme='graphite'] body .btn,
html[data-skin='chrome'][data-theme='graphite'] body .chip:not([aria-pressed]),
html[data-skin='chrome'][data-theme='graphite'] body .segmented__key[aria-selected='true'] {
  --cap-hi: #93a5b6;
  --cap-mid: #4c5a68;
  --cap-lo: #262e36;
  --collar: #0d1116;
  --collar-deep: #04060a;
}

html[data-skin='linen'] body .cap,
html[data-skin='linen'] body .key,
html[data-skin='linen'] body .set-seg__opt span,
html[data-skin='linen'] body .switch__thumb,
html[data-skin='linen'] body .btn,
html[data-skin='linen'] body .chip:not([aria-pressed]),
html[data-skin='linen'] body .segmented__key[aria-selected='true'] {
  --cap-hi: #faf6ea;
  --cap-mid: #f1ecde;
  --cap-lo: #dcd5c1;
  --collar: #ada38a;
  --collar-deep: #857c66;
}

html[data-skin='linen'][data-theme='graphite'] body .cap,
html[data-skin='linen'][data-theme='graphite'] body .key,
html[data-skin='linen'][data-theme='graphite'] body .set-seg__opt span,
html[data-skin='linen'][data-theme='graphite'] body .switch__thumb,
html[data-skin='linen'][data-theme='graphite'] body .btn,
html[data-skin='linen'][data-theme='graphite'] body .chip:not([aria-pressed]),
html[data-skin='linen'][data-theme='graphite'] body .segmented__key[aria-selected='true'] {
  --cap-hi: #423e33;
  --cap-mid: #333027;
  --cap-lo: #25231d;
  --collar: #131210;
  --collar-deep: #090806;
}

/* The alloy caps were already cut in §19; they are restated here only so the
   settings keys and the toggle thumb join the same family the front panel
   already had. */
html[data-skin='alloy'] body .set-seg__opt span,
html[data-skin='alloy'] body .switch__thumb,
html[data-skin='alloy'] body .btn,
html[data-skin='alloy'] body .chip:not([aria-pressed]),
html[data-skin='alloy'] body .segmented__key[aria-selected='true'] {
  --cap-hi: #fbfaf7;
  --cap-mid: #e3e1db;
  --cap-lo: #c2bfb7;
  --collar: #8d8a83;
  --collar-deep: #6a675f;
}

html[data-skin='alloy'][data-theme='graphite'] body .set-seg__opt span,
html[data-skin='alloy'][data-theme='graphite'] body .switch__thumb,
html[data-skin='alloy'][data-theme='graphite'] body .btn,
html[data-skin='alloy'][data-theme='graphite'] body .chip:not([aria-pressed]),
html[data-skin='alloy'][data-theme='graphite'] body .segmented__key[aria-selected='true'] {
  --cap-hi: #6d6a63;
  --cap-mid: #4a4842;
  --cap-lo: #33312c;
  --collar: #1b1915;
  --collar-deep: #0e0d0a;
}

/* --------------------------------------------------------------------------
   §20.3  EDGES — how sharply the material can be made

   Radius is a manufacturing fact. Machined alloy and fired enamel hold a
   near-square arris; a deco phenolic moulding was generously radiused because
   the press demanded it; a fabric-wrapped part has no hard edge at all. One
   token, seven honest answers, and the three-value radius system is preserved
   because every value below is still a PART radius — the case and the sheet
   keep theirs.
   -------------------------------------------------------------------------- */

html[data-skin='chrome'],
.set-swatch[data-skin='chrome'] { --r-part: 1px; } /* polish holds the sharpest arris */
html[data-skin='brushed'],
.set-swatch[data-skin='brushed'] { --r-part: 2px; }
html[data-skin='enamel'],
.set-swatch[data-skin='enamel'] { --r-part: 2px; }
html[data-skin='stone'],
.set-swatch[data-skin='stone'] { --r-part: 2px; }
html[data-skin='ceramic'],
.set-swatch[data-skin='ceramic'] { --r-part: 3px; }
html[data-skin='alloy'],
.set-swatch[data-skin='alloy'] { --r-part: 4px; } /* anodised extrusions are broken-edged */
html[data-skin='linen'],
.set-swatch[data-skin='linen'] { --r-part: 7px; }

/* --------------------------------------------------------------------------
   §20.4  THE FACE — how the dial is printed

   The numerals already follow §20.1, because css/dial.css asks for
   var(--font-num) rather than naming a face. What is left is the PRINTING:
   how heavy the strokes are, how the ticks are cut, and how much the surface
   shines. Screen-print on ceramic, acid-etch on aluminium, stamped enamel and
   litho on a paper-faced wooden dial are four genuinely different processes
   and they do not share a stroke weight.
   -------------------------------------------------------------------------- */

/* Fired enamel: the crispest printing of the set, because the ink is glass
   fused into glass. Hairline minors, a hard specular. */
html[data-skin='enamel'] body .dial {
  --dial-tick-w: 0.45;
  --dial-tick-major-w: 1.2;
  --dial-tick-a: 0.82;
  --dial-num-wt: 400;
  --dial-num-track: 0.01em;
  --dial-spec-a: 1;
}

/* Honed stone takes ink softly and never returns a highlight. Everything is a
   shade heavier and a shade quieter, and the ticks are rounded because a
   sandblasted mark has no sharp end. */
html[data-skin='stone'] body .dial {
  --dial-tick-w: 0.7;
  --dial-tick-major-w: 1.5;
  --dial-tick-a: 0.62;
  --dial-tick-cap: round;
  --dial-num-wt: 500;
  --dial-num-track: 0.02em;
  --dial-spec-a: 0.45;
}

/* Anodised: the dye sits IN the surface, so the printing is soft-edged and the
   face returns almost nothing. The lowest specular of the three metals, and
   that is the whole point of having three. */
html[data-skin='alloy'] body .dial {
  --dial-tick-w: 0.6;
  --dial-tick-major-w: 1.4;
  --dial-tick-a: 0.7;
  --dial-num-wt: 500;
  --dial-spec-a: 0.35;
}

/* Etched steel: fine, exact, monoline, and a satin anisotropic sheen — halfway
   between the anodised face above and the mirror below. The numerals are
   monospaced, so they are tracked out slightly to stop the equal advances
   reading as gappy at 8px. */
html[data-skin='brushed'] body .dial {
  --dial-tick-w: 0.5;
  --dial-tick-major-w: 1.15;
  --dial-tick-a: 0.85;
  --dial-num-wt: 400;
  --dial-num-size: 7.4px;
  --dial-num-track: 0.03em;
  --dial-spec-a: 0.72;
}

/* Chrome: printed on glass over a polished plate, so the marks are the
   crispest and the surface returns everything. Full specular — the top of the
   range, against anodised alloy's 0.35 at the bottom of it. Futura's figures
   are wide, so they come down a little to keep the clear space around the
   ring. */
html[data-skin='chrome'] body .dial {
  --dial-tick-w: 0.42;
  --dial-tick-major-w: 1.1;
  --dial-tick-a: 0.9;
  --dial-num-wt: 500;
  --dial-num-size: 7.6px;
  --dial-num-track: 0.02em;
  --dial-spec-a: 1;
}

/* Screen-print on a textile-faced board: the coarsest and the softest. Round
   tick ends, heavier strokes, almost no shine. */
html[data-skin='linen'] body .dial {
  --dial-tick-w: 0.72;
  --dial-tick-major-w: 1.55;
  --dial-tick-a: 0.6;
  --dial-tick-cap: round;
  --dial-num-wt: 500;
  --dial-num-track: 0.02em;
  --dial-spec-a: 0.4;
}

/* --------------------------------------------------------------------------
   §20.5  THE SIGNAL, PER MATERIAL

   This section replaces a rule that used to be stated two ways in this file:
   "the signal never moves — one red, everywhere, because it means the same
   thing on every surface." That reasoning is sound about MEANING and wrong
   about COLOUR, and the difference was visible: a warm brown-red sitting on a
   cool steel body is not the same red doing the same job, it is two colours
   arguing. Red is the most temperature-sensitive hue there is, and a ground
   that shifts from warm oatmeal to cool blue-grey drags it with it.

   So the signal keeps ONE JOB and gets seven cuts. Each one is the same red
   idea rotated to its material's own temperature: warm bodies get a red with
   more orange in it, cool bodies get a red with more blue. It is still the
   only saturated colour on the object, still means "this is the time you have
   left", and still appears nowhere decorative.

   Every value below was measured, not chosen by eye: the disc against the face
   it depletes on, and the signal-as-type against the panel it prints on.
   -------------------------------------------------------------------------- */

/* CERAMIC — the original. Warm off-white body, warm red. Unchanged. */

/* STONE — a quiet mineral body wants a quieter red; full saturation on a
   honed limestone panel reads as a sticker stuck to a wall. */
html[data-skin='stone'],
.set-swatch[data-skin='stone'] {
  --signal: #a8352c;
  --signal-text: #8f2b23;
  --signal-dim: #6d221c;
  --signal-light: #c9553f;
}
html[data-skin='stone'][data-theme='graphite'],
html[data-theme='graphite'] .set-swatch[data-skin='stone'] {
  --signal: #e2694f;
  --signal-text: #f08163;
  --signal-dim: #8a4030;
  --signal-light: #f5906f;
}

/* ALLOY — anodised aluminium is the warmest of the three metals, so its red
   carries the most orange of the three. */
html[data-skin='alloy'],
.set-swatch[data-skin='alloy'] {
  --signal: #c33a24;
  --signal-text: #a52f1d;
  --signal-dim: #7d2515;
  --signal-light: #e4593a;
}
html[data-skin='alloy'][data-theme='graphite'],
html[data-theme='graphite'] .set-swatch[data-skin='alloy'] {
  --signal: #f0552c;
  --signal-text: #ff7550;
  --signal-dim: #8f3620;
  --signal-light: #ff8560;
}

/* BRUSHED — steel is cool, so the red rotates off orange and toward crimson.
   Side by side with alloy's above, this is the whole argument for §20.5. */
html[data-skin='brushed'],
.set-swatch[data-skin='brushed'] {
  --signal: #bd2f3d;
  /* darker than the other light skins on purpose: brushed's panel is a real
     steel grey rather than an off-white, so the same red measured 3.87:1 on it */
  --signal-text: #7d1c27;
  --signal-dim: #781c25;
  --signal-light: #dd4f5e;
}
html[data-skin='brushed'][data-theme='graphite'],
html[data-theme='graphite'] .set-swatch[data-skin='brushed'] {
  --signal: #ef5566;
  --signal-text: #ff7383;
  --signal-dim: #8e333e;
  --signal-light: #ff8593;
}

/* CHROME — the coolest body of the seven, and the only one with real blue in
   it. Its red goes furthest toward crimson for exactly that reason: an orange
   red on a blue-grey mirror is the single worst pairing available here. */
html[data-skin='chrome'],
.set-swatch[data-skin='chrome'] {
  --signal: #c41f3f;
  --signal-text: #a41833;
  --signal-dim: #7c1226;
  --signal-light: #e14461;
}
html[data-skin='chrome'][data-theme='graphite'],
html[data-theme='graphite'] .set-swatch[data-skin='chrome'] {
  --signal: #f04a6b;
  --signal-text: #ff6d88;
  --signal-dim: #8d2c40;
  --signal-light: #ff8098;
}

/* ENAMEL — vitreous enamel signage has an actual traditional red, and this is
   it: cleaner and slightly cooler than the ceramic, because the ground is. */
html[data-skin='enamel'],
.set-swatch[data-skin='enamel'] {
  --signal: #c8102e;
  --signal-text: #a60c26;
  --signal-dim: #7d0a1d;
  --signal-light: #e63a53;
}
html[data-skin='enamel'][data-theme='graphite'],
html[data-theme='graphite'] .set-swatch[data-skin='enamel'] {
  --signal: #f5442f;
  --signal-text: #ff6b57;
  --signal-dim: #932a1d;
  --signal-light: #ff7d6b;
}

/* LINEN — a warm oatmeal weave, so the red goes earthy. Anything brighter
   floats off the cloth instead of being printed on it. */
html[data-skin='linen'],
.set-swatch[data-skin='linen'] {
  --signal: #ad4a2e;
  --signal-text: #8e3a23;
  --signal-dim: #6d2c1a;
  --signal-light: #cd6a48;
}
html[data-skin='linen'][data-theme='graphite'],
html[data-theme='graphite'] .set-swatch[data-skin='linen'] {
  --signal: #ef7a4e;
  --signal-text: #ff9670;
  --signal-dim: #8f4a2f;
  --signal-light: #ffa585;
}

/* --------------------------------------------------------------------------
   §20.6  THE MATERIAL PREVIEW

   Seven words in a row is not a way to choose a material. The swatch shows the
   thing itself: the real case tone, the real grain map at a shrunken tile, the
   real dial face and the real signal red — all of it inherited, via the
   selector list above, from the very same declarations the full-size object
   uses. Nothing here restates a colour, so a swatch cannot go stale.
   -------------------------------------------------------------------------- */

html body .set-seg--swatch .set-seg__opts {
  display: flex !important;
  flex-wrap: wrap !important;
  gap: 10px !important;
}

html body .set-seg--swatch .set-seg__opt {
  flex: 0 0 auto !important;
  min-inline-size: 0 !important;
}

/* The cap treatment from §9.1 selects `.set-seg__opt span`, which reaches EVERY
   span inside a key — including the two the swatch is built from. So the reset
   has to name them, not just the outer one: without this the chip was painted
   --cap-mid with a cap gradient over it, and all seven samples came out the
   same colour whatever material they were supposed to be showing. */
html body .set-seg--swatch .set-seg__opt span {
  background-color: transparent !important;
  background-image: none !important;
  box-shadow: none !important;
  transform: none !important;
}

html body .set-seg--swatch .set-seg__opt > .set-swatch {
  display: flex !important;
  flex-direction: column !important;
  align-items: center !important;
  gap: 6px !important;
  padding: 0 !important;
  background: none !important;
  background-image: none !important;
  box-shadow: none !important;
  transform: none !important;
  border-radius: 0 !important;
  color: var(--muted-ink) !important;
  text-transform: none !important;
  letter-spacing: 0 !important;
  font-size: 0.6875rem !important;
  font-weight: 500 !important;
  cursor: pointer;
}

/* the sample itself — a small piece of the case, seen from directly above */
html body .set-seg--swatch .set-seg__opt .set-swatch__chip {
  position: relative;
  display: block;
  inline-size: 46px;
  block-size: 46px;
  border-radius: var(--r-part, 3px);
  background-color: var(--ramp-150) !important;
  box-shadow:
    inset 0 0 0 1px rgba(var(--part-sh), 0.16),
    0 1px 0 var(--collar, rgba(0, 0, 0, 0.18)),
    0 2px 4px rgba(var(--part-sh), 0.24) !important;
  overflow: hidden;
}

/* the material's own grain, at a tile scaled down with the sample so the tooth
   reads at the same relative size it does on the full panel */
html body .set-seg--swatch .set-swatch__chip::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: var(--grain);
  background-size: calc(var(--grain-size, 200px) / 4.2) calc(var(--grain-size, 200px) / 4.2);
  background-repeat: repeat;
  mix-blend-mode: soft-light;
  opacity: var(--grain-light, 0.5);
}

html[data-theme='graphite'] body .set-seg--swatch .set-swatch__chip::before {
  opacity: var(--grain-dark, 0.38);
}

/* the face, with a quarter of the disc still to run — the smallest drawing
   that is unmistakably THIS app rather than a colour chip */
html body .set-seg--swatch .set-swatch__chip::after {
  content: '';
  position: absolute;
  inset: 8px;
  border-radius: 50%;
  background-color: var(--ramp-200);
  background-image: conic-gradient(
    var(--signal) 0deg 90deg,
    transparent 90deg 360deg
  );
  box-shadow: inset 0 0 0 1px rgba(var(--part-sh), 0.22);
}

html body .set-seg--swatch .set-seg__opt .set-swatch__name {
  color: var(--muted-ink) !important;
}

html body .set-seg--swatch .set-seg__opt input:checked + .set-swatch,
html body .set-seg--swatch .set-seg__opt input:checked + .set-swatch .set-swatch__name {
  color: var(--ink) !important;
}

/* SELECTED is a seated ring, not a tint: tinting a material sample would lie
   about the material. */
html body .set-seg--swatch .set-seg__opt input:checked + .set-swatch .set-swatch__chip {
  box-shadow:
    inset 0 0 0 1px rgba(var(--part-sh), 0.16),
    0 0 0 2px var(--bg-sunk),
    0 0 0 4px var(--signal-key),
    0 2px 5px rgba(var(--part-sh), 0.3) !important;
}

html body .set-seg--swatch .set-seg__opt input:focus-visible + .set-swatch .set-swatch__chip {
  outline: 2px solid var(--signal);
  outline-offset: 6px;
}

@media (hover: hover) {
  html body .set-seg--swatch .set-seg__opt:hover .set-swatch__chip {
    transform: translateY(-1px) !important;
  }
}

@media (prefers-reduced-motion: reduce) {
  html body .set-seg--swatch .set-seg__opt:hover .set-swatch__chip { transform: none; }
}

/* --------------------------------------------------------------------------
   §20.7  WHAT SITS ON THE SIGNAL

   Recorded because it is the sort of thing that gets "fixed" twice.

   --signal-key is the red with a LABEL on it: "Replace my data", "Polish it",
   the tour's "Next". The depleting disc carries no text at all — the transport
   keys are deliberately bare, with their legends printed on the panel beside
   them — so the disc is a large graphic and not a contrast-of-text problem.

   The label flips with the theme, and that is the part worth knowing: white on
   the red in daylight, near-black (--on-signal #14090a) at night. Which means
   the two themes want OPPOSITE things from the key. In daylight it has to stay
   dark enough to carry white. At night it has to stay BRIGHT enough to carry
   black — so the night reds are correct precisely because they glow, and
   darkening them, which looks like the safe direction, is the one that breaks
   them. It was tried here and measured worse.

   So --signal-key: var(--signal) stands in both themes, and every value in
   §20.5 was checked against the label its own theme actually uses.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   §20.8  TWO INKS THAT THE MATERIAL MOVED

   --cap-ink-dim is set once for daylight and once for night, which works while
   every skin's caps sit near the same value. Two do not: brushed's daylight cap
   is a cool mid-grey rather than an off-white, and chrome's night cap is a lit
   blue-grey rather than a dark one. Both measured just under AA (4.30 and
   3.61), so both get their own dim ink rather than the shared one.
   -------------------------------------------------------------------------- */

/* SCOPED TO DAYLIGHT, and the reason is worth keeping: written unqualified,
   `html[data-skin='brushed']` ties with `html[data-theme='graphite']` on
   specificity and, being later in the file, won — so this dark ink landed on
   brushed's DARK night cap and measured 1.12:1. A skin rule that changes an
   ink must say which light it is talking about. */
html[data-skin='brushed']:not([data-theme='graphite']) { --cap-ink-dim: #4c5157; }
@media (prefers-color-scheme: dark) {
  html[data-skin='brushed']:not([data-theme='ceramic']) { --cap-ink-dim: #c2b8a8; }
}
html[data-skin='chrome'][data-theme='graphite'] { --cap-ink-dim: #dde5ec; }

/* Brushed's daylight panel is a steel grey, so its tertiary ink needs to be a
   step darker than the neutral ramp gives it (4.26:1 before). */
html[data-skin='brushed']:not([data-theme='graphite']) { --ramp-600: #343a40; }

/* Alloy's night panel is a touch lighter than the others, so its tertiary ink
   came out at 4.33 on it. Lifted just enough to clear. */
html[data-skin='alloy'][data-theme='graphite'] { --ramp-600: #a9a094; }

/* --------------------------------------------------------------------------
   §20.9  CHROME MIRRORS, IT DOES NOT SHADE

   Every other material here is lit: a lamp sits up and to the left, and the
   radial in §12 shades the panel away from it. A polished surface does not
   work that way. It has no diffuse shading at all — its tone at any point is
   whatever is REFLECTED there, which for a panel lying under a room is sky
   above, a hard turn at the horizon, and darker ground below.

   So chrome, and only chrome, swaps the radial for a horizon sweep. Without
   this it was a smooth cool-grey panel, which is what unpolished metal looks
   like; the mirror was only happening on the caps.

   The amplitude is deliberately held to a few steps of the ramp. A full
   sky-to-ground sweep looks spectacular in isolation and would put type on a
   ground several shades from the one it was measured against — the panel is
   still a surface people have to read off.
   -------------------------------------------------------------------------- */

html[data-skin='chrome'] body .app-shell {
  background-image: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--ramp-150) 78%, #ffffff) 0%,
    color-mix(in srgb, var(--ramp-150) 90%, #ffffff) 34%,
    var(--ramp-150) 49%,
    /* the horizon: the one hard turn, and the whole tell */
    color-mix(in srgb, var(--ramp-150) 88%, var(--ramp-300)) 51%,
    color-mix(in srgb, var(--ramp-150) 92%, var(--ramp-300)) 78%,
    color-mix(in srgb, var(--ramp-150) 84%, var(--ramp-300)) 100%
  ) !important;
}

/* Black chrome reflects a dark room, so the sweep runs the other way: the
   brightest band is the horizon catch, not the top. */
html[data-skin='chrome'][data-theme='graphite'] body .app-shell {
  background-image: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--ramp-150) 88%, #000000) 0%,
    var(--ramp-150) 44%,
    color-mix(in srgb, var(--ramp-150) 72%, var(--ramp-400)) 50%,
    var(--ramp-150) 56%,
    color-mix(in srgb, var(--ramp-150) 86%, #000000) 100%
  ) !important;
}

/* --------------------------------------------------------------------------
   §20.10  THE CLOCK'S WINDOW

   Repointing the digital plate at the ramp (css/controls.css) stopped it being
   a warm grey rectangle on a cool metal panel, but --bg-sunk is the floor of a
   WELL, and on the three metals that floor is mid-toned enough that the dimmed
   seconds fell to 3.99:1. A window is not a well: it is lit from behind. So it
   sits a few steps above the sunk floor, which is both truer to the part and
   enough to carry the secondary figure.
   -------------------------------------------------------------------------- */

html body .tt-clock__digital {
  background-color: color-mix(in srgb, var(--ramp-250) 52%, var(--ramp-050)) !important;
}

/* ==========================================================================
   §21  PORTRAIT PHONES — the case the breakpoints missed

   css/controls.css §26b fixes, in its own words, "the readout printed ON TOP
   of the dial — the stage had no layout of its own, so its children stacked".
   That fix is real and it works. It is also inside `@media (min-width: 75rem)`.
   The landscape-phone case has its own block at `(orientation: landscape) and
   (max-height: 560px)`. A phone held UPRIGHT matches neither, which is why the
   desktop was fine and the thing people actually hold was not.

   NO MAGIC NUMBER. The first attempt here fitted --instr to a line measured by
   bisection, and it was wrong in principle: a constant subtracted from vh only
   holds while every other row keeps the height it had on the day it was
   measured, and the rows change with state — the meta line appears when a
   duration is set, the task field grows with its own text. The dial is the one
   element on the panel that can be any size and still do its job, so it should
   be the one that ABSORBS whatever is left rather than the one that predicts
   it. Flex does that natively; the constant was me doing arithmetic the
   browser was willing to do continuously.
   ========================================================================== */

@media (max-width: 74.99rem) and (orientation: portrait) {
  /* THE STAGE OWNS ITS LAYOUT — the §26b fix, un-gated — and BOTH modes get
     the same one.

     This is also where the clock face stopped agreeing with the timer face.
     Both containers were `grid-template-rows: minmax(0, 1fr)` with the dial
     CENTRED in that first row. The row is whatever is left after the rows
     below it, and the clock has far less below it than the timer — a digital
     line and a date, against a readout, a meta line, a task field, a face
     selector and the transport. So the clock's leftover row is taller, and a
     dial centred in a taller row sits lower. Measured: 48px lower at 393x780,
     81px at 414x896, 137px at 768x1024 — it grows with the screen, which is
     the signature of a centring difference rather than an offset. */
  html body .tt-timer,
  html body .tt-clock {
    display: flex !important;
    flex-direction: column !important;
    align-items: center !important;
    justify-content: flex-start !important;
    min-block-size: 0 !important;
    gap: 0.625rem !important;
    /* NO `overflow: hidden` here. It was tried, and it does not fix an
       oversized column — it hides one, by cutting the transport keys off at
       the case edge. A control that is clipped is worse than one that is
       merely close: it still takes the tap. The size is solved above instead,
       so there is nothing left to clip. */
  }

  html body .tt-timer__stage,
  html body .tt-clock__stage {
    display: flex !important;
    flex-direction: column !important;
    align-items: center !important;
    justify-content: flex-start !important;
    inline-size: 100% !important;
    max-inline-size: 100% !important;
    flex: 0 0 auto !important;
    min-block-size: 0 !important;
    block-size: auto !important;
    position: static !important;
    gap: 0.5rem !important;
  }

  /* ONE SIZE, BOTH MODES — and it has to be a shared value, not an elastic one.
     Letting the dial simply absorb whatever slack its own column had was tried
     here and is wrong for a specific reason: the two modes have different
     amounts of content under the dial, so "the remainder" is a different number
     in each, and the faces came out 60px and 273px on the same screen. The
     requirement is that they are identical, which means the size must be
     computed from the viewport — shared — and must fit the BUSIER of the two
     modes. Timer is the busier one, so timer sets the budget and clock inherits
     it and keeps the leftover as air.

     The constant is DERIVED, not fitted. Timer mode's rows other than the dial
     measure, at every portrait size:

         task 44 + face/repeat 41 + transport 90 + readout & meta 90 + gaps 31
           = 296px of fixed content
         + 156px of header above and 34px of day strip below

     which lands the same answer at four different viewports — 100vh - 489, 492,
     494, 494. Rounded to 500 for margin. Measuring the parts rather than
     bisecting the whole is why this one agrees with itself: the earlier fitted
     line was read off a state where the meta line happened to be absent, and it
     was 52px optimistic at every size as a result.

     88vw caps a wide-but-short window, 360px stops it dominating a tablet, and
     112px is the anti-collapse floor. */
  html body .tt-timer__stage,
  html body .tt-clock__stage {
    /* svh, NOT vh — this is the iPhone bug in one unit. With the URL bar at the
       BOTTOM, iOS Safari reports 100vh as the LARGE viewport, the height the
       page would have if that bar were hidden. The case is sized in dvh (see
       css/controls.css) so it correctly shrinks to the visible area, but the
       dial was being computed from vh and came out sized for a screen ~90px
       taller than the one it is on — so it pushed the transport keys down
       behind the bar and they were cut in half.

       100svh is the SMALL viewport: the height with the browser chrome shown,
       which is the smallest it will ever be. Sizing to that means the keys
       cannot be clipped in either state; when the bar hides, the case grows
       and the extra height becomes air under the transport rather than a
       relayout. The plain-vh line stays first as the fallback for anything
       that predates the unit.

       THE SAFE-AREA TERM, added after the same symptom came back from a real
       iPhone. svh solved the browser-chrome case and could not solve this one:
       INSTALLED TO THE HOME SCREEN THERE IS NO BROWSER CHROME, so svh is the
       whole 932px screen — while the app still hands ~59px to the notch and
       ~34px to the home indicator through its own safe-area padding. The dial
       was therefore sized for 932px of room inside an 839px box, the transport
       overflowed `.app-stage` by 23px, and `overflow: hidden` ate every key
       legend: four bare caps and no words. Subtracting the insets is what makes
       the formula describe the box the keys are actually in. In a browser tab
       both insets are 0 and nothing changes.

       And the reserve went 500 -> 524, because at the corrected height the
       chrome still needed 23px more than 500 was admitting. */
    --instr: clamp(112px, min(88vw, calc(100vh - 534px)), 360px) !important;
    --instr: clamp(112px, min(88vw, calc(100svh - 534px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px))), 360px) !important;
  }

  html body .tt-timer__dial,
  html body .tt-clock__dial {
    flex: 0 0 auto !important;
    inline-size: auto !important;
    block-size: auto !important;
    min-block-size: 0 !important;
    max-inline-size: 100% !important;
  }

  html body .tt-timer__stage > .dial,
  html body .tt-timer__dial > .dial,
  html body .tt-clock__stage > .dial,
  html body .tt-clock__dial > .dial {
    inline-size: var(--instr) !important;
    block-size: var(--instr) !important;
    min-block-size: 0 !important;
    max-block-size: none !important;
    max-inline-size: none !important;
    aspect-ratio: 1 / 1 !important;
    flex: 0 0 auto !important;
    margin-inline: auto !important;
  }

  /* The rows that are NOT elastic say so, so the flex algorithm takes its
     slack from the dial and never from a control. */
  html body .tt-timer__task,
  html body .tt-timer__legend,
  html body .tt-timer__transport,
  html body .tt-timer__readout,
  html body .tt-timer__meta {
    flex: 0 0 auto !important;
  }

  /* The countdown, a step down. At phone sizes the plate was reading as large
     as the dial it belongs to, which inverts the hierarchy — the disc is the
     instrument and the numerals are its confirmation, not the other way round.
     A step, not a demotion: it stays the biggest type on the panel. */
  html body .tt-timer__readout,
  html body .tt-timer__readout .t-display {
    font-size: clamp(1.75rem, 4.6vh, 2.5rem) !important;
  }

  /* RECLAIM THE BOTTOM. Measured at 393x660: the rows summed to 365px inside a
     446px container — 49px of slack — and on top of that the transport carried
     a 24px bottom margin and sat 65px clear of the day strip. That space was
     doing nothing except making the dial smaller. The keys keep their touch
     targets; only the dead air around the block goes. */
  html body .tt-timer__transport {
    margin-block-end: 0.25rem !important;
    padding-block-start: 0.25rem !important;
  }
}

/* --------------------------------------------------------------------------
   §21.1  THE SETTINGS DRAWER MUST NOT SCROLL SIDEWAYS

   The seven material swatches are the widest thing in the drawer, and on a
   390px phone the row exceeded the body by ~40px. `overflow: auto` answered
   that with a HORIZONTAL scrollbar, the body was scrolled right, and every
   label lost its first few characters — "APPEARANCE" read "EARANCE",
   "material" read "ial", "ceramic" read "mic". Nothing was broken visually;
   the drawer had simply been pushed sideways.

   Two fixes, because either alone is a patch: the swatches lay out on a grid
   that always fits whatever width it is given, and the body refuses to scroll
   sideways at all so no future row can do this again.
   -------------------------------------------------------------------------- */

html body .settings__body {
  overflow-x: hidden !important;
  overscroll-behavior: contain;
}

html body .set-seg--swatch .set-seg__opts {
  display: grid !important;
  grid-template-columns: repeat(auto-fit, minmax(3.25rem, 1fr)) !important;
  gap: 0.625rem 0.5rem !important;
  inline-size: 100% !important;
}

html body .set-seg--swatch .set-seg__opt {
  inline-size: 100% !important;
  min-inline-size: 0 !important;
}

/* the sample keeps its square, the label wraps under it rather than setting
   the column width */
html body .set-seg--swatch .set-swatch__chip {
  inline-size: min(2.875rem, 100%) !important;
  block-size: min(2.875rem, 100%) !important;
  aspect-ratio: 1 / 1;
}

html body .set-seg--swatch .set-swatch__name {
  inline-size: 100% !important;
  text-align: center !important;
  overflow-wrap: anywhere;
  line-height: 1.2;
}

/* --------------------------------------------------------------------------
   §21.2  ONE "LEFT OF YOUR DAY", NOT TWO

   CLOCK mode borrows the day-ribbon module and gives it a full-width slot:
   "Your day / 3h 58m of your day left, until 11:00 PM", with an hour scale
   under it. The strip along the bottom of the case says "3H 58M OF YOUR DAY
   LEFT" and "08:00-23:00" — in every mode, including this one. The same fact,
   twice on one screen, forty pixels apart.

   The strip is the one that stays: it is always present, it costs one line,
   and it is the ambient version the app is built around. The borrowed ribbon
   is the one that goes, which also returns its whole block to the clock face —
   the thing people came to look at.

   Hidden rather than unmounted on purpose: src/main.js LENDS the single live
   module to this mode and takes it back on exit, and that lending is what
   guarantees there is only ever one copy of it. Breaking that to save a few
   hidden nodes would trade a real invariant for nothing.
   -------------------------------------------------------------------------- */

html body .tt-clock__ribbon,
html body .tt-clock [data-slot='day-ribbon'] {
  display: none !important;
}

/* --------------------------------------------------------------------------
   §21.3  THE SHORTEST SCREENS GIVE UP THE STRIP, NOT THE INSTRUMENT

   At 320x568 the fixed rows need 296px, the header 156px and the strip 34px —
   more than the screen has before the dial gets a single pixel. Something has
   to go, and the order is decided by what the app is FOR: the dial is the
   product, the transport operates it, the task names it. The day strip is
   ambient context, and its numbers are already in the panel under DAY.

   So on a screen this short the strip stands down and the instrument keeps its
   size. This is the same call as §21.2 — when the same fact is on screen
   twice, the smaller, quieter copy is the one that leaves.
   -------------------------------------------------------------------------- */

@media (max-height: 600px) and (orientation: portrait) {
  html body .day-strip {
    display: none !important;
  }
}

/* --------------------------------------------------------------------------
   §21.4  NOTHING IN THE DRAWER MAY HANG OFF THE EDGE

   With §21.1 stopping the drawer scrolling sideways, an over-wide row no
   longer drags the labels out of view — it puts its right-hand control past
   the edge where the clip makes it unreachable, which is worse. Measured at
   393px: one 44px icon button overhanging by 42px.

   So the rows wrap instead. A settings row is a label and its control; when
   both cannot sit on one line, the honest answer on a narrow screen is two
   lines, not a control you cannot touch.
   -------------------------------------------------------------------------- */

html body .settings__body .set-section > *,
html body .settings__body .set-actions,
html body .settings__body .set-row,
html body .settings__body .set-sub,
html body .settings__body .set-danger {
  max-inline-size: 100% !important;
  min-inline-size: 0 !important;
}

html body .settings__body .set-actions,
html body .settings__body .set-row,
html body .settings__body .set-sub,
/* the alert-marks row is the one that actually did it: a nowrap flex line whose
   time field and add button together want 297px inside a 369px body, with the
   button ending 42px past the edge */
html body .settings__body .set-marks__add,
html body .settings__body .set-marks {
  flex-wrap: wrap !important;
}

html body .settings__body .set-marks__add > * {
  min-inline-size: 0 !important;
  max-inline-size: 100% !important;
}

/* --------------------------------------------------------------------------
   §21.5  THE SMALLEST CASE OF ALL

   375x555 is an SE-class phone with the URL bar showing — the shortest window
   this app will realistically be opened in. There, `100svh - 500px` resolves
   below the 112px floor, the floor wins, and the 12px legend row under the
   transport ends up outside the case.

   The floor exists so the dial can never collapse to nothing, not because
   112px is sacred. Twenty pixels off it buys back the row that names the keys,
   and a named key is worth more than twenty pixels of disc: the disc still
   reads as an area at 92px, whereas an unlabelled transport is four identical
   circles.
   -------------------------------------------------------------------------- */

@media (max-height: 580px) and (orientation: portrait) {
  html body .tt-timer__stage,
  html body .tt-clock__stage {
    --instr: clamp(92px, min(88vw, calc(100vh - 486px)), 360px) !important;
    --instr: clamp(92px, min(88vw, calc(100svh - 486px)), 360px) !important;
  }
}

/* --------------------------------------------------------------------------
   §21.6  THE DRAWER MUST NOT GUILLOTINE ITS CONTENT

   .tt-panel__clip stops short of the bottom of the screen on purpose — the
   space below it is reserved for the transport, which stays reachable while
   the drawer is open. That is right. What is wrong is what the cut LOOKS like:
   the last card is sliced through its middle with a hard horizontal edge, the
   transport's panel begins immediately underneath, and the result reads as a
   drawer that broke rather than one that scrolls.

   Two changes, neither of which moves the boundary:

   1. The pane gets bottom padding equal to the part of the sheet hanging below
      the cut, so the last card can actually be scrolled fully into view. Before
      this the pane's scroll range ended at the same place the clip did, and the
      bottom card could never be seen whole no matter how far you scrolled.

   2. The cut is feathered. A mask that fades the last few pixels says
      "continues below" in the way a hard edge says "ended badly". It is applied
      only while there is more to scroll to, so a short pane keeps a clean edge.
   -------------------------------------------------------------------------- */

@media (max-width: 47.99rem) {
  html body .tt-panel__pane {
    /* --tt-sheet-hidden is the live snap offset, published by src/panel.js: the
       height of the sheet that is currently below the clip and therefore
       unreachable. Adding it here extends the scroll range by exactly that
       much, so the last card can always be scrolled fully into view. */
    padding-block-end: calc(
      var(--tt-sheet-hidden, 0px) + var(--tt-panel-lip, 1.5rem) + var(--space-4, 1rem)
    ) !important;
    scroll-padding-block-end: var(--space-4, 1rem);
    /* the feathered edge — transparent for the last 14px */
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 14px), transparent 100%);
    mask-image: linear-gradient(to bottom, #000 calc(100% - 14px), transparent 100%);
  }

  /* Safari does not reliably clip a 3D-transformed descendant to an ancestor's
     `overflow: hidden`, which lets the layer underneath bleed a hairline along
     the rounded edge. Giving the clip its own stacking context and its own mask
     makes the clip a real boundary rather than an advisory one. */
  html body .tt-panel__clip {
    isolation: isolate;
    -webkit-mask-image: -webkit-radial-gradient(#000, #000); /* forces a real clip in WebKit */
  }
}

/* ==========================================================================
   §22  LANDSCAPE — where two rules were fighting over one number

   Turning a phone sideways produced a dial clipped through its top edge in
   TIMER and a dial sitting on top of the digital readout in CLOCK. Both come
   from the same cause, and it is not the landscape layout itself.

   css/controls.css has a landscape block at `(orientation: landscape) and
   (max-height: 560px)` that lays the instrument out in two columns and sizes
   `.tt-timer__stage` to `min(54vh, 40vw)`. Further down the file — 5000 lines
   further — sits the fix for a PORTRAIT problem, scoped only by
   `@media (max-width: 74.99rem)`. A phone on its side satisfies that too, and
   being later it wins, so `--instr` resolved to `min(38vh, 70vw)` while the
   container around it had been built for `min(54vh, 40vw)`. The box and the
   thing inside it were sized by different formulas; the difference is what got
   clipped.

   That later rule also, deliberately, gives BOTH stages the same value — its
   comment says "there was never a reason for CLOCK to have a different one".
   The landscape block predates that and names only `.tt-timer__stage`, so in
   landscape the clock fell back to its own sizing and drifted from the timer
   again. Exactly the fault that rule exists to prevent, reappearing in the one
   orientation it does not cover.

   So: one declaration, both stages, landscape's own measure. Height is the
   scarce axis when a phone is on its side, which is why the vh term leads.
   ========================================================================== */

/* ── RETIRED: the landscape-phone block ────────────────────────────────────
   Everything that used to live here shrank the PORTRAIT layout until it fitted
   a landscape phone: a smaller dial, a shorter chassis, tighter transport keys.
   Landscape no longer runs that layout. It runs its own instrument — the
   full-bleed linear gauge in src/bar.js — and css/bar.css §2 owns the shell
   state that goes with it.

   These rules were not merely redundant, they were actively in the way: they
   were written as `html body .tt-timer__stage`, which out-specifies anything
   css/bar.css can say about the same element no matter what order the
   stylesheets load in. Overriding them was not an option; retiring them was.

   See docs/superpowers/specs/2026-08-25-landscape-face-design.md.
   ───────────────────────────────────────────────────────────────────────── */

/* ==========================================================================
   §23  THE DIAL IS THE PRODUCT — it gives up space last

   Stated priority: the visual clock is the most important thing on the screen
   and should always be as large as it can be. Everything below follows from
   that one sentence.

   The dial had been the ELASTIC element: a fixed stack of rows took what it
   needed and the disc absorbed whatever was left, which on a 600px phone was
   112px. That is backwards. The disc is what the app is FOR — it is the whole
   reason to open a visual timer rather than read a number — so it should be
   the last thing to give up a pixel, not the first.

   So the rows stand down in order of how much they matter, and the dial takes
   the space each one releases. The order is not arbitrary:

     1. the meta line     "original face · ends 8:44 pm · then counts up"
                          — description of state the dial already shows
     2. the face/repeat   configuration, changed rarely, also in Settings
     3. the task field    naming the session; useful, not required to run one

   What never stands down: the dial, the countdown, the transport. Below the
   last rung the floor takes over and the dial simply stops shrinking.

   Measured costs, from §21's accounting: meta ~26px with its gap, face/repeat
   ~51px, task ~54px. Each rung subtracts its row from the constant.
   ========================================================================== */

@media (max-width: 74.99rem) and (orientation: portrait) and (max-height: 46rem) {
  html body .tt-timer__meta { display: none !important; }
  html body .tt-timer__stage,
  html body .tt-clock__stage {
    --instr: clamp(112px, min(88vw, calc(100vh - 474px)), 360px) !important;
    --instr: clamp(112px, min(88vw, calc(100svh - 474px)), 360px) !important;
  }
}

@media (max-width: 74.99rem) and (orientation: portrait) and (max-height: 42rem) {
  html body .tt-timer__legend { display: none !important; }
  html body .tt-timer__stage,
  html body .tt-clock__stage {
    /* 423 -> 443, and the safe-area terms, for the same reason as the reserve in
       §21.4 above: at 393x660 the transport overflowed `.app-stage` by 9px and
       the key legends were clipped away by its `overflow: hidden`. This one was
       already doing it before that fix — the two reserves are tuned separately
       and both were short. tests/portrait-legends.spec.js now measures the
       clearance at every viewport so the next short reserve fails in CI. */
    --instr: clamp(112px, min(88vw, calc(100vh - 443px)), 360px) !important;
    --instr: clamp(112px, min(88vw, calc(100svh - 443px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px))), 360px) !important;
  }
}

@media (max-width: 74.99rem) and (orientation: portrait) and (max-height: 38rem) {
  html body .tt-timer__task { display: none !important; }
  html body .tt-timer__stage,
  html body .tt-clock__stage {
    --instr: clamp(104px, min(88vw, calc(100vh - 369px)), 360px) !important;
    --instr: clamp(104px, min(88vw, calc(100svh - 369px)), 360px) !important;
  }
}

/* A landscape phone shorter than ~380px keeps a little more chrome than the
   taller ones do — measured 206px of cost against 180px at every other size —
   so the shortest rung pays for it rather than the dial being cropped again. */

/* ==========================================================================
   §24  LANDSCAPE ALONG THE LONG AXIS — attempted, reverted, and why

   Mobbin's precedent for "one object matters" is consistent and it is right:
   Opal's clock, Timepage's face and (Not Boring) Timer's numerals all put the
   object as large as the frame allows and collapse the chrome to a single
   cluster; TIDE and Journal float that cluster rather than banding it.

   Applied here that means landscape should be a ROW — instrument at full
   height on one side, countdown and transport stacked beside it — because the
   long axis is the one with room, and §22's answer stacks along the short one.
   It was built and measured: the disc went from 245px to 334px, 78% of the
   screen height instead of 57%.

   It is not in the file because it looked worse than the number suggests. The
   readout is a CHILD OF THE STAGE, so turning the timer into a row moves the
   dial into a left column and leaves the readout stacked underneath it inside
   that column — the disc pinned to the left edge and clipped, the countdown
   half cut off, and two thirds of the frame empty. CLOCK mode has no transport
   at all, so its right column is simply blank.

   Doing it properly needs the readout and the transport lifted out of the
   stage into a sibling column — a DOM change in src/modes/timer.js and
   src/modes/clockface.js, not a media query. That is the next piece of work
   here, and it is worth doing; what is NOT worth doing is shipping the CSS-only
   half of it, which is why §22's centred layout stands for now.
   ========================================================================== */



/* ==========================================================================
   §26  LANDSCAPE — THE CIRCLE OWNS THE HEIGHT, THE CORNERS TAKE THE REST

   A phone on its side is short and wide. Every earlier answer here read that as
   a height shortage and stacked anyway — dial, countdown beneath it, controls
   beneath that — so the disc got 57% of the height while ~500px of width sat
   empty either side of it. Hiding rows made the stack shorter; it never stopped
   being a stack.

   The geometry says otherwise. A circle sized to the full height of a 932x430
   frame is ~410px across, which leaves 261px of empty column on EACH side. That
   is not space to hide things from — it is exactly where the controls belong.
   Put them there and nothing is above or below the disc, so nothing competes
   with it for the only axis that is scarce.

   So: the instrument takes the whole height in the middle; the countdown and
   the mode keys take the left column; the transport and the chrome take the
   right. Everything that describes rather than operates — the meta line, the
   task field, the face selector, the day strip — is behind a button, which is
   what the drawer is for.

   Positioned absolutely rather than re-parented: the readout is a child of the
   stage and the transport a sibling of it, so flow layout cannot put them in
   the side columns without a DOM change. Absolute placement escapes the nesting
   and leaves both modes' markup untouched.
   ========================================================================== */


/* --------------------------------------------------------------------------
   §27  A LEGEND YOU CANNOT READ IS NOT A LABEL

   Reported from an iPhone 16: the labels under the transport keys are
   invisible. They were not hidden and not clipped — an earlier check confirmed
   both and called them fine. They were 9px.

   That check asked whether they were RENDERED. The only question that matters
   is whether they can be READ, and 9px on a phone held at arm's length cannot
   be. iOS does not set standing text below 11pt; neither should this.

   It matters more here than in most places: the keys are deliberately bare —
   no text on the cap, the legend beside it is the only thing naming what the
   control does. An unreadable legend turns four identical circles into a
   guess about which one resets the timer.

   Pinned by tests/legends-are-legible.spec.js.
   -------------------------------------------------------------------------- */

html body .tt-timer__transport span.legend,
html body .keybank__legends span.legend,
html body .tt-timer__grouplabel {
  font-size: 0.6875rem !important; /* 11px — the floor, not a preference */
  letter-spacing: 0.04em !important;
}

/* On a phone there is room to be generous, and this is the label people are
   actually squinting at. */
@media (max-width: 47.99rem) and (orientation: portrait) {
  html body .tt-timer__transport span.legend {
    font-size: 0.75rem !important; /* 12px */
  }
}

/* --------------------------------------------------------------------------
   §28  ONE DIAMETER FOR THE TOOLS CLUSTER

   The three keys in the header do the same kind of job — each opens something —
   and they were three different sizes: the floating-window key at 44px and the
   Panel and Settings keys at 52px, because a rule further up sets a 3.25rem
   minimum on the photographed icon cap and only two of the three inherit it.
   A row of identical jobs should be a row of identical parts.

   44px is the size, not 52, for two reasons. It is the touch minimum, so
   nothing is lost. And these are secondary: they open drawers, while the dial
   IS the app — three 52px discs in the corner were competing with it for
   attention on a phone. Landscape already arrived at 44 independently, so this
   also makes the two orientations agree.

   Pinned by tests/chrome-is-consistent.spec.js.
   -------------------------------------------------------------------------- */

html body .chassis .btn--icon,
html body .app-head .btn--icon,
html body .chassis__tools .btn,
html body .chassis button.btn--round {
  min-inline-size: 2.75rem !important; /* 44px */
  min-block-size: 2.75rem !important;
  inline-size: 2.75rem !important;
  block-size: 2.75rem !important;
  aspect-ratio: 1 / 1 !important;
}
