/* MODULE: css/dial.css — every colour, alpha, hairline and animation the dial uses.
   Depends on css/tokens.css (loaded first) and, when it is in the cascade,
   css/materials.css (PHASE4 §4 — the chrome and knurl palettes; every reference to
   an --m-* token here carries a fallback, so the dial is correct either way).
   Authored for the DOM that src/dial.js builds: a .dial wrapper holding .dial__svg
   and .dial__wear. Loads nothing itself.

   Why the colours live here and not in the JS: src/dial.js writes geometry only.
   Gradient stops carry classes (.dg-*) and get their stop-color / stop-opacity from
   this file, so switching between ceramic and graphite costs zero JavaScript and
   zero repaint bookkeeping — the cascade does all of it. */

/* ============================================================== *
 * 0. THE OPTICAL MODEL — one light, stated once
 *
 *    LIGHT COMES FROM ABOVE, SLIGHTLY LEFT. In this file's terms that is
 *    an azimuth of −30° measured clockwise from 12 o'clock; src/dial.js
 *    holds the same number as LIGHT_DEG and derives every gradient axis,
 *    the specular arc's centre and every cast-shadow offset from it.
 *    Nothing here is allowed to invent a second direction.
 *
 *    Surface by surface, and why each one shades the way it does:
 *
 *    THE WELL is CONCAVE. Its wall on the light's OWN side faces away from
 *      the light and is the dark one; the far wall catches the light and
 *      bounces a little back. Dark lip upper-left, bounce lower-right.
 *      (tokens.css's --shadow-well is the vertical component of this; §2
 *      adds the −30° bias on top rather than replacing it.)
 *
 *    THE BEZEL is CONVEX, so it does the opposite: bright at upper-left,
 *      falling away to lower-right. A concave rim and a convex rim shaded
 *      oppositely at nearly the same radius is not a contradiction — it is
 *      the whole reason the object reads as a ring set into a recess.
 *
 *      The bezel is CHROME (PHASE4 §4), which adds one horizontal feature
 *      to an otherwise −30° component: the HORIZON, the hard inversion
 *      where the reflection stops being sky and starts being ground. That
 *      is a reflection of the world, and the world is level however the key
 *      light is placed, so it lands at 3 and 9 o'clock. It is the only
 *      thing in this file with a vertical axis, and it is deliberate.
 *
 *    THE FACE is a flat floor: the near wall's shadow across its upper-left,
 *      the far wall's bounce across its lower-right, and an occlusion
 *      vignette whose focus is pushed toward the light so it is heavier on
 *      the side facing away.
 *
 *    THE DISC is a PART LYING ON THE FACE, not a coloured region of it. Its
 *      outline is darkened where it meets the ceramic, least on the lit side
 *      and most on the shaded one; its last two percent of radius is a top
 *      chamfer rolling into a side wall, which is its thickness; and it
 *      drops two shadows — a tight, dark one that is the contact line and a
 *      soft one that is the ambient. Both are offset down-and-right along
 *      the light. Take any of the three away and it is a pie chart again.
 *
 *    THE KNOB and THE SPINDLE are convex like the bezel, and their cast
 *      shadows fall down-and-right at the same −30°.
 *
 *    VERIFYING IT — do this again after touching anything above. Screenshot
 *    .dial, sample relative luminance round a ring at 30° intervals, and
 *    check the brightest sample is −30° with a monotonic falloff from it.
 *    Measured on a 336px dial, ceramic then graphite:
 *
 *      bezel band   r=96.3u   peak −30°  0.854 / 0.172   trough +150°
 *      well lip     r=105.5u  INVERTED — trough at −30°, 0.405 against
 *                             0.416 at 0° and 0.453 at +30°, because a
 *                             concave surface must shade the opposite way
 *      well floor   r≤104u    tokens.css's straight-down --shadow-well
 *                             takes over; trough returns to 0°. Expected:
 *                             §2's −30° bias is a rim effect by design
 *      disc         r=30u     peak −30°  ·  r=55u  peak −30°  ·  r=64u −45°
 *
 *    ONE HONEST EXCEPTION, in graphite only: the well's lip is already
 *    clipped at black by --shadow-well's own layers, so a darker shadow has
 *    nowhere left to go and −30° and 0° tie at 0.0005. At night the well's
 *    direction is carried by where the BOUNCE is instead — +150° measures
 *    0.0086 against the lip's 0.0005, a 17× difference in the right place.
 *    That is a property of a near-black theme, not a defect, and the fix is
 *    not to lighten the lip: the object would stop reading as recessed.
 *
 *    TWO MAXIMA 180° APART IS THE FAILURE THIS MODEL EXISTS TO PREVENT, and
 *    it is what shipped: the old sheen lightened the disc's top with white
 *    AND its bottom with --ramp-300, which is a light warm grey by day. The
 *    disc measured 0.2163 at 15°, 0.1893 at 90° and 0.1994 at 135° — a
 *    highlight, a dark band, and a second highlight. Sample it, do not
 *    eyeball it; at these amplitudes the eye reads "shiny" either way.
 * ============================================================== */

/* ============================================================== *
 * 1. LOCAL TOKENS
 *
 *    CONTRACT-GAP (design-system agent, css/tokens.css): §3 exposes no
 *    theme-independent physical ink. --ink and every ramp step invert
 *    between themes, so neither can express "the dark side of an object",
 *    which stays dark in ceramic AND in graphite. Two opaque literals per
 *    theme are declared below and EVERY other physical alpha is mixed from
 *    them, so there is one pair to replace, not a dozen rgba()s. They mirror
 *    the shadow inks tokens.css already bakes into --shadow-well /
 *    --shadow-raise, and css/materials.css records the same gap for
 *    --m-cast / --m-lift. If tokens.css ever exports a --shadow-ink pair,
 *    point --dial-shadow-ink / --dial-lift-ink at it and delete nothing else.
 *
 *    CONTRACT-GAP (css/materials.css): the chrome palette below is a
 *    var() reference to --m-chrome-* with a literal fallback, because metal
 *    is metal at night — it does not invert, it is simply lit less, so the
 *    neutral ramp cannot supply it. When materials.css is in the cascade it
 *    is the single source and the fallbacks never resolve. index.html does
 *    LINKS materials.css (it does, and has for a while), so --m-chrome-* IS
 *    defined and the fallbacks below NEVER resolve. Retuning the bezel here
 *    alone changes nothing on screen; materials.css is the single source and
 *    it is where the case edge is actually pigmented. The fallbacks are kept
 *    only as a degradation path and are deliberately in sync.
 * ============================================================== */

.dial {
  --dial-face: var(--ramp-200); /* FACE #ece9e0 — a recess sits BELOW the case,
                                   so it takes the reference's recessed tone,
                                   not the case tone it had before */
  --dial-spec: var(--ramp-000); /* specular highlight */
  --dial-bounce: var(--ramp-050); /* light bouncing off the far well wall */
  --dial-shade: var(--ramp-300); /* the ramp is a DEPTH ladder: 300 is dark at night,
                                    a warm mid-tone by day — correct in both */

  /* The two physical inks, and everything mixed from them. */
  --dial-shadow-ink: #2e281e;
  --dial-lift-ink: #fffdf8;
  --dial-cast: color-mix(in srgb, var(--dial-shadow-ink) 30%, transparent);
  --dial-contact: color-mix(in srgb, var(--dial-shadow-ink) 52%, transparent);
  --dial-well-lip: color-mix(in srgb, var(--dial-shadow-ink) 34%, transparent);
  --dial-lift: color-mix(in srgb, var(--dial-lift-ink) 92%, transparent);
  --dial-well-bounce: color-mix(in srgb, var(--dial-lift-ink) 55%, transparent);

  --dial-spec-a: 0.95; /* specular strength, dialled back at night */
  /* Fine black print. On the card the markings are near-black and crisp,
     not grey — printing is opaque ink on ceramic, and grey ticks are a
     screen habit. The minor ticks stay a step back so the ladder still
     reads at a glance. */
  --dial-tick: var(--ink-2);
  --dial-tick-major: var(--ink);
  --dial-num: var(--ink);

  /* THE CASE EDGE — was a chrome bezel, now moulded ceramic.
     A polished metal ring is the single thing that most made the dial read as
     a separate grey component dropped onto a warm body: chrome has a cool
     neutral hue and a hard specular horizon, and neither exists anywhere on
     the reference. The palette card is one material, edge to edge.

     The gradient stops keep their jobs — sky, horizon, ground, deep, bounce —
     because the geometry that consumes them is unchanged. Only the pigment
     moves: a shallow warm case edge that catches light on its upper-left
     shoulder, turns over a soft horizon, and falls into a restrained ground
     tone, with a bounce coming back off the panel underneath. Low contrast on
     purpose. A case edge is a chamfer, not a mirror. */
  --dial-chr-sky-hi: var(--m-chrome-sky-hi, #fdfbf5);
  --dial-chr-sky: var(--m-chrome-sky, #efece2);
  --dial-chr-horizon: var(--m-chrome-horizon, #f8f5ec);
  --dial-chr-ground: var(--m-chrome-ground, #cdc5b6);
  --dial-chr-deep: var(--m-chrome-deep, #b3a89a);
  --dial-chr-bounce: var(--m-chrome-bounce, #e4dfd3);

  /* Brushed aluminium for the knob (PHASE4: metal for things you touch). */
  --dial-alu-hi: var(--m-edge-hi, var(--ramp-050));
  --dial-alu-lo: var(--m-edge-lo, var(--ramp-250));
  --dial-alu-spec: var(--m-spec, color-mix(in srgb, var(--dial-lift-ink) 50%, transparent));

  /* PERMITTED FLUTE 3 (PHASE4 §4) — the knob edge's knurling. */
  --dial-knurl-hi: var(--m-knurl-hi, color-mix(in srgb, var(--dial-lift-ink) 72%, transparent));
  --dial-knurl-lo: var(--m-knurl-lo, color-mix(in srgb, var(--dial-shadow-ink) 13%, transparent));

  /* The disc. --dial-fill is the one property src/dial.js writes at runtime: the
     approach ramp interpolates it toward --signal-warn. Everything derived from
     it — the chamfer, the side wall, the contact edge — follows automatically. */
  --dial-fill: var(--signal);
  /* Shading mixes toward a genuine dark ink, NOT toward --signal-dim. Mixing a
     red toward another red of near-identical lightness moved the rim ~0.1 L*
     from the fill — i.e. the disc had no perceptible edge and read as a flat
     pie chart. This ink is dark enough that an 88% mix is actually visible. */
  --dial-shade-ink: #3a0a0a; /* retuned with the pigment. The old #4a1503 was a
                                dark ORANGE-brown, chosen when the signal was
                                #e4380d; mixing the new deep red toward it
                                pulled the rim and wall back toward orange and
                                the sector lost its single-pigment reading. A
                                red lacquer shades toward its own black. */

  /* MATTE, NOT GLOSSY. This stop used to lift 14% toward specular white,
     which put a sheen across the sector — the "cheap glossy pie" read. Painted
     lacquer scatters: it takes light as a faint, broad lift near the lit edge
     and nothing more. 8% is enough to say the surface is turned toward the
     lamp and little enough that it never becomes a highlight. */
  --dial-fill-lit: color-mix(in oklab, var(--dial-fill) 92%, var(--ramp-000));
  --dial-fill-rim: color-mix(in oklab, var(--dial-fill) 88%, var(--dial-shade-ink));
  /* The sector has THICKNESS, but a painted sector is thin. These two stops
     draw its cut edge and the wall below it; at 46/30 they made the disc read
     as a raised bulbous pie rather than as lacquer laid into a recess. Pulled
     back so the boundary stays crisp and legible while the wall reads as a
     couple of tenths of a millimetre of paint, not a moulding. */
  --dial-fill-edge: color-mix(in oklab, var(--dial-fill) 62%, var(--dial-shade-ink));
  --dial-fill-wall: color-mix(in oklab, var(--dial-fill) 48%, var(--dial-shade-ink));

  /* THE WEAR HOOK. src/wear.js writes this, 0…1. See §10. */
  --dial-wear: 0;
}

[data-theme="graphite"] .dial {
  --dial-shadow-ink: #000000;
  /* Warm, and no longer near-white: at #e8e6e1 with a 0.5 specular this drew a
     bright polished arc across the top-left of the case — the single most
     "chrome" thing left in the night theme, and the reference has no polished
     ring anywhere. A painted charcoal housing returns a dim, warm sheen. */
  --dial-lift-ink: #b8ad9a;
  --dial-cast: color-mix(in srgb, var(--dial-shadow-ink) 72%, transparent);
  --dial-contact: color-mix(in srgb, var(--dial-shadow-ink) 82%, transparent);
  --dial-well-lip: color-mix(in srgb, var(--dial-shadow-ink) 74%, transparent);
  --dial-lift: color-mix(in srgb, var(--dial-lift-ink) 14%, transparent);
  --dial-well-bounce: color-mix(in srgb, var(--dial-lift-ink) 12%, transparent);
  --dial-spec-a: 0.22; /* a chamfer catch, not a mirror */
  --dial-shade-ink: #6b2409;

  --dial-chr-sky-hi: var(--m-chrome-sky-hi, #5a5348);
  --dial-chr-sky: var(--m-chrome-sky, #3a352d);
  --dial-chr-horizon: var(--m-chrome-horizon, #6b6355);
  --dial-chr-ground: var(--m-chrome-ground, #1a1712);
  --dial-chr-deep: var(--m-chrome-deep, #100e0a);
  --dial-chr-bounce: var(--m-chrome-bounce, #2c2820);

  --dial-alu-hi: var(--m-edge-hi, #3b3d40);
  --dial-alu-lo: var(--m-edge-lo, #141517);
  --dial-alu-spec: var(--m-spec, color-mix(in srgb, #ffffff 12%, transparent));

  --dial-knurl-hi: var(--m-knurl-hi, color-mix(in srgb, var(--dial-lift-ink) 12%, transparent));
  --dial-knurl-lo: var(--m-knurl-lo, color-mix(in srgb, var(--dial-shadow-ink) 50%, transparent));
}

/* Keep in sync with the block above. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="ceramic"]):not([data-theme="graphite"]) .dial {
    --dial-shadow-ink: #000000;
    /* Warm, and no longer near-white: at #e8e6e1 with a 0.5 specular this drew a
     bright polished arc across the top-left of the case — the single most
     "chrome" thing left in the night theme, and the reference has no polished
     ring anywhere. A painted charcoal housing returns a dim, warm sheen. */
  --dial-lift-ink: #b8ad9a;
    --dial-cast: color-mix(in srgb, var(--dial-shadow-ink) 72%, transparent);
    --dial-contact: color-mix(in srgb, var(--dial-shadow-ink) 82%, transparent);
    --dial-well-lip: color-mix(in srgb, var(--dial-shadow-ink) 74%, transparent);
    --dial-lift: color-mix(in srgb, var(--dial-lift-ink) 14%, transparent);
    --dial-well-bounce: color-mix(in srgb, var(--dial-lift-ink) 12%, transparent);
    --dial-spec-a: 0.22; /* a chamfer catch, not a mirror */
    --dial-shade-ink: #6b2409;

    /* The same case edge in painted charcoal — warm, and never black. */
    --dial-chr-sky-hi: var(--m-chrome-sky-hi, #5a5348);
    --dial-chr-sky: var(--m-chrome-sky, #3a352d);
    --dial-chr-horizon: var(--m-chrome-horizon, #6b6355);
    --dial-chr-ground: var(--m-chrome-ground, #1a1712);
    --dial-chr-deep: var(--m-chrome-deep, #100e0a);
    --dial-chr-bounce: var(--m-chrome-bounce, #2c2820);

    --dial-alu-hi: var(--m-edge-hi, #3b3d40);
    --dial-alu-lo: var(--m-edge-lo, #141517);
    --dial-alu-spec: var(--m-spec, color-mix(in srgb, #ffffff 12%, transparent));

    --dial-knurl-hi: var(--m-knurl-hi, color-mix(in srgb, var(--dial-lift-ink) 12%, transparent));
    --dial-knurl-lo: var(--m-knurl-lo, color-mix(in srgb, var(--dial-shadow-ink) 50%, transparent));
  }
}

/* ============================================================== *
 * 2. THE WELL
 *    .dial is the recess opening. The SVG is inset inside it so a thin
 *    ring of the recess floor stays visible around the bezel — the gap
 *    that says "this part drops in", not "this is printed on".
 * ============================================================== */

.dial {
  position: relative;
  display: block;
  inline-size: 100%;
  aspect-ratio: 1;
  border-radius: 50%;
  background-color: var(--bg-sunk);

  /* One stacking context for the whole instrument. src/wear.js's mattes are
     mix-blend-mode: screen, and a blend mode composites against its stacking
     context's backdrop — without this the patina would blend against the page
     behind the dial instead of against the dial's own artwork. It costs nothing
     (.dial already establishes a containing block) and it stops the wear layer
     leaking. See §10. */
  isolation: isolate;

  /* Pointer Events only, one code path for mouse / touch / pen. Without this,
     iOS steals the gesture for scrolling before pointermove ever fires. */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
}

.dial:not(.is-static) {
  cursor: grab;
}

.dial.is-dragging {
  cursor: grabbing;
}

/* Press physics. Every button in the app depresses; the dial — the primary touch
   target — changed only the mouse cursor, which does not exist on a phone. The
   nub sinks and its cast shadow tightens the instant a finger lands. */
.dial.is-dragging .dial__knobFace,
.dial.is-dragging .dial__knobRing {
  r: 5.9px;
}

.dial.is-dragging .dial__knobCast {
  opacity: 0.55;
}

.dial__knobFace,
.dial__knobRing,
.dial__knobCast {
  transition:
    r var(--dur-press) var(--ease-out),
    opacity var(--dur-press) var(--ease-out);
}

/* MINOR: the knob and hub casts were the only hard-edged shadows in the app —
   a flat 1-unit crescent reads as a second outlined disc, not as a shadow. */
.dial__knobCast,
.dial__hubCast {
  filter: blur(0.5px);
}

/* The recess walls, painted OVER the face — an inset box-shadow on .dial itself
   would sit behind the SVG and be invisible. z-index keeps it above the artwork
   and pointer-events keeps it out of the drag path.

   --shadow-well is tokens.css's straight-down groove; the two layers after it
   rotate the model to −30°, putting the dark lip at upper-left and the bounce at
   lower-right. They are deliberately given a large negative spread so they hug
   the rim and die within a few px: the bezel band sits ~17px further in and its
   direction belongs to the SVG, which can place it at exactly the right radius.
   A broad bias here would fight the specular instead of framing it.

   Measured on a 460px dial: at r=105.5u — 3px inside the rim, where these two
   layers live — the darkest point is −30°. By r=104u the token's own vertical
   layers dominate again and it is 0°. That is the intended handover, not a
   failure to bias; the well is a groove lit from above whose LIP carries the
   −30° signature, and the bezel inside it carries the rest. */
.dial::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: 50%;
  box-shadow:
    var(--shadow-well),
    /* offsets are the light vector × 4.4, so they rotate with LIGHT_DEG too */
    inset 2.2px 3.8px 5px -1px var(--dial-well-lip),
    inset -2.2px -3.8px 5px -1px var(--dial-well-bounce);
  pointer-events: none;
}

.dial__svg {
  position: absolute;
  inset: 3.4%;
  inline-size: 93.2%;
  block-size: 93.2%;
  overflow: visible;
  /* Every pointer event is handled on .dial, whose bounding box the angle maths
     already accounts for. Nothing inside the SVG needs to hit-test. */
  pointer-events: none;
}

.dial:focus-visible {
  outline: var(--ring-w) solid var(--signal);
  outline-offset: var(--ring-offset);
}

/* ============================================================== *
 * 3. GRADIENT STOPS
 *    src/dial.js emits <stop> elements with these classes and no colour.
 *    Every axis but the chrome horizon runs lit-end → shaded-end, so
 *    offset 0 always means "facing the light" and offset 1 "facing away".
 * ============================================================== */

/* CHROME. Sky above the horizon, ground below it, a bounce off the ground at
   the very bottom lip, and a HARD inversion between --dial-chr-horizon and
   --dial-chr-ground where the two stops share an offset. That edge is the whole
   trick: a soft ramp of the same colours reads as plastic however bright it is. */
.dg-chr-a { stop-color: var(--dial-chr-sky-hi); }
.dg-chr-b { stop-color: var(--dial-chr-sky); }
.dg-chr-c { stop-color: var(--dial-chr-horizon); }
.dg-chr-d { stop-color: var(--dial-chr-ground); }
.dg-chr-e { stop-color: var(--dial-chr-deep); }
.dg-chr-f { stop-color: var(--dial-chr-bounce); }

/* The key light across the whole ring: a lift on the side facing the light,
   nothing across the middle, a shade on the side facing away. This is what
   breaks the horizon's left/right symmetry.

   THE TWO MIDDLE STOPS ARE BOTH TRANSPARENT AND THAT IS NOT REDUNDANT. SVG
   interpolates stops non-premultiplied, so white@0 → black@0.44 passes through
   mid-grey@0.22, which LIGHTENS whatever is under it. One transparent stop put a
   measurable bright patch at −150° — a second light source, in the one place the
   model says must be darkest. Never let a gradient segment span two hues with a
   transparent endpoint; give each side its own zero-alpha stop of its own colour. */
.dg-bkey-a { stop-color: var(--dial-spec); stop-opacity: calc(var(--dial-spec-a) * 0.22); }
.dg-bkey-b { stop-color: var(--dial-spec); stop-opacity: 0; }
.dg-bkey-b2 { stop-color: var(--dial-shadow-ink); stop-opacity: 0; }
.dg-bkey-c { stop-color: var(--dial-shadow-ink); stop-opacity: 0.44; }

/* The one specular arc, centred on the light. PHASE3 §2: "one specular arc,
   upper-left, falling off — not a full ring." */
.dg-spec-a { stop-color: var(--dial-spec); stop-opacity: var(--dial-spec-a); }
.dg-spec-b { stop-color: var(--dial-spec); stop-opacity: calc(var(--dial-spec-a) * 0.34); }
.dg-spec-c { stop-color: var(--dial-spec); stop-opacity: 0; }

/* Face: the near well wall casts a soft shadow across the upper-left of the
   plate… */
.dg-ftop-a { stop-color: var(--dial-shade); stop-opacity: 0.24; }
.dg-ftop-b { stop-color: var(--dial-shade); stop-opacity: 0; }

/* …and light bouncing off the far wall lifts the lower-right of it. */
.dg-fbnc-a { stop-color: var(--dial-bounce); stop-opacity: 0.5; }
.dg-fbnc-b { stop-color: var(--dial-bounce); stop-opacity: 0; }

/* Ambient occlusion hugging the rim — the last few percent of the radius only.
   Its focus is pushed toward the light in src/dial.js, so the band is tight on
   the lit side and wide on the shaded one instead of being a uniform ring. */
.dg-vig-a { stop-color: var(--dial-shade); stop-opacity: 0; }
.dg-vig-b { stop-color: var(--dial-shade); stop-opacity: 0.1; }
.dg-vig-c { stop-color: var(--dial-shade); stop-opacity: 0.34; }

/* THE DISC'S THICKNESS. Flat across 86% of the radius, then a top chamfer that
   catches the light, then the chamfer rolling over, then the side wall in
   shadow. The whole event happens in the last 14% and is worth about 1.5 user
   units of apparent depth — enough to get a fingernail under, not enough to
   notice as an effect. */
.dg-disc-a { stop-color: var(--dial-fill); }
.dg-disc-b { stop-color: var(--dial-fill); }
.dg-disc-c { stop-color: var(--dial-fill-lit); }
.dg-disc-d { stop-color: var(--dial-fill-rim); }
.dg-disc-e { stop-color: var(--dial-fill-wall); }

/* THE DISC'S OUTLINE, where it meets the ceramic. Darkened all the way round —
   least on the lit side, where the chamfer is catching light, most on the shaded
   side, where you are looking at the side wall. Because it is a stroke on the
   sector path it follows both radial cuts as well as the arc, so the leading
   edge gets its thickness for free however far the disc has swept. */
.dg-edge-a { stop-color: var(--dial-fill-rim); }
.dg-edge-b { stop-color: var(--dial-fill-edge); }
.dg-edge-c { stop-color: var(--dial-fill-wall); }

/* The disc's share of the room light, in user space rather than on the shape:
   the light does not rotate with the wedge, so a quarter-turn of disc in the
   upper right is lit at its upper-left just as the full disc would be.

   The far end mixes toward --dial-shade-ink and NOT toward --dial-shade. That is
   the fix for the two-source bug: --dial-shade is --ramp-300, a LIGHT warm grey
   by day, so laying it over a dark red lightened the bottom of the disc while
   the white above lightened the top — two highlights 180° apart with a dark band
   between them. --dial-shade-ink is dark in both themes, which is what a shaded
   side has to be.

   Two transparent stops in the middle, for the same non-premultiplied reason as
   .dg-bkey-* above. */
.dg-shn-a { stop-color: var(--dial-spec); stop-opacity: calc(var(--dial-spec-a) * 0.17); }
.dg-shn-b { stop-color: var(--dial-spec); stop-opacity: 0; }
.dg-shn-b2 { stop-color: var(--dial-shade-ink); stop-opacity: 0; }
.dg-shn-c { stop-color: var(--dial-shade-ink); stop-opacity: 0.2; }

.dg-hub-a { stop-color: var(--ramp-050); }
.dg-hub-b { stop-color: var(--ramp-200); }

/* Brushed aluminium: bright edge toward the light, one specular band before
   centre, dark edge away.

   CONTRACT-GAP (css/materials.css §6, "THE DIAL KNOB"): that block re-points
   .dg-knob-a / .dg-knob-b at --m-edge-hi / --m-edge-lo one specificity step
   above these rules. It is now redundant — the dial reads the same two alloy
   tokens itself, so the knob is byte-identical whichever rule wins — and its own
   comment says to delete it once that happened. It has happened; the block is
   safe to remove, and safe to leave. The knurling it handed over (PERMITTED
   FLUTE 3) is drawn in SVG below, at --m-knurl-hi / --m-knurl-lo. */
.dg-knob-a { stop-color: var(--dial-alu-hi); }
.dg-knob-s { stop-color: var(--dial-alu-spec); }
.dg-knob-b { stop-color: var(--dial-alu-lo); }

.dg-kcap-a { stop-color: var(--dial-alu-hi); }
.dg-kcap-b { stop-color: var(--dial-alu-lo); }

/* ============================================================== *
 * 4. FACE
 * ============================================================== */

.dial__plate {
  fill: var(--dial-face);
}

.dial__wash {
  /* The three shading layers over the plate. Painted once, never touched again. */
  stroke: none;
}

/* The pocket the disc sits in — a printed hairline, so an empty dial still shows
   where the red belongs. */
.dial__track {
  stroke: var(--hairline);
  stroke-width: 0.5;
}

.dial__rim {
  stroke: var(--hairline);
  stroke-width: 0.6;
}

.dial__rim--out {
  stroke: var(--hairline-strong);
  stroke-width: 0.7;
}

.dial__bezelSpec {
  stroke-linecap: round;
  /* WEAR HOOK: PHASE3 §4 item 4 — the specular arc dulls to 82% at full patina. */
  opacity: calc(1 - 0.18 * var(--dial-wear));
}

/* ============================================================== *
 * 5. THE PRINTED SCALE
 *    All 60 minor marks are one <path> and all 12 majors are another, so a
 *    scale change is two attribute writes plus the numerals.
 * ============================================================== */

/* HOW THE FACE IS PRINTED — tokens, not constants, because a printed face is
   a property of the material it is printed on. Screen-printed ceramic, etched
   metal and stamped enamel do not share a stroke weight, and css/parts.css §20
   cuts each one. The values here are the ceramic originals, so nothing moves
   unless a skin asks it to. */
.dial__minor {
  stroke: var(--dial-tick);
  stroke-width: var(--dial-tick-w, 0.55);
  stroke-linecap: var(--dial-tick-cap, butt);
  opacity: var(--dial-tick-a, 0.75);
}

.dial__major {
  stroke: var(--dial-tick-major);
  stroke-width: var(--dial-tick-major-w, 1.35);
  stroke-linecap: var(--dial-tick-cap, butt);
}

.dial__num {
  fill: var(--dial-num);
  font-family: var(--font-num);
  font-variant-numeric: var(--font-num-variant);
  font-feature-settings: var(--font-num-feat);
  font-size: var(--dial-num-size, 8px);
  font-weight: var(--dial-num-wt, 500);
  letter-spacing: var(--dial-num-track, var(--track-0));
}

/* ============================================================== *
 * 6. THE DISC
 * ============================================================== */

.dial__pulse {
  /* The haptic-surrogate impulse scales about the dial centre. Explicit origin in
     view-box units so it never depends on the wedge's own bounding box, which
     changes shape every frame. */
  transform-box: view-box;
  transform-origin: 100px 100px;
}

/* The disc is a part LYING ON the face. Two shadows, both offset down-and-right
   along the light:

     the tight one   the CONTACT line. Short, dark, barely blurred. This is the
                     one that has to win locally, because the face's bounce
                     brightens exactly the lower-right quadrant the disc's
                     shadow falls into — a broad soft shadow simply loses there,
                     and the plate ends up reading brighter below the disc than
                     above it, which is the opposite of an object sitting on a
                     surface.
     the soft one    the ambient occlusion around the whole part.

   src/dial.js animates this group's transform for the detent, so the origin has
   to be the dial centre and not the wedge's own box, which changes every frame. */
.dial__disc {
  transform-box: view-box;
  transform-origin: 100px 100px;
  filter:
    drop-shadow(0.5px 0.9px 0.7px var(--dial-contact))
    drop-shadow(1px 1.7px 2.2px var(--dial-cast));
}

/* No `stroke` here: src/dial.js sets stroke="url(#…-edge)" as a presentation
   attribute, and any CSS stroke declaration — even a var() — would override the
   per-instance gradient id, which CSS cannot name. Width and joins only. */
.dial__discFill,
.dial__discFillFull {
  stroke-width: 0.8;
  stroke-linejoin: round;
}

.dial__discSheen,
.dial__discSheenFull {
  stroke: none;
  pointer-events: none;
}

/* Overtime: the disc is spent, and a thin counter-arc grows from 12 o'clock. */
.dial__overArc,
.dial__overFull {
  stroke: var(--signal-dim);
  stroke-linecap: butt;
}

/* Approach ramp (PHASE3 §3). The fill has already interpolated toward
   --signal-warn by the time this class lands; the breathing is the second half
   of the signal. Two seconds, 1 → 0.82 — a breath, not a strobe. */
@keyframes tt-dial-breathe {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.82;
  }
}

.dial.is-approaching .dial__disc {
  animation: tt-dial-breathe 2s var(--ease-in-out) infinite;
}

/* The hue shift alone is not a signal: #e4380d and #f07100 are the same colour
   to a protanope/deuteranope, and under prefers-reduced-motion the breathe is
   switched off, leaving hue as the ONLY cue. So the approach also changes the
   disc's SHAPE — a thicker, dashed edge — and lights the empty track's hairline.
   Both survive reduced motion and both are visible without colour vision. */
.dial.is-approaching .dial__discFill,
.dial.is-approaching .dial__discFillFull {
  stroke-width: 1.6;
  stroke-dasharray: 4 3;
}

.dial.is-approaching .dial__track {
  stroke: var(--hairline-strong);
  opacity: 1;
}

/* ============================================================== *
 * 7. THE KNOB
 *    The affordance that says "twist me". Present when the dial is
 *    settable and idle or paused, and while a drag is in flight; gone
 *    while time is actually running, when there is nothing to grab.
 *
 *    Metal, per PHASE4 §4's governing rule — it is the one part of the
 *    dial you actually put a thumb on. Brushed aluminium body, a knurled
 *    rim (PERMITTED FLUTE 3), and a smooth turned cap in the middle.
 * ============================================================== */

.dial__knob {
  opacity: 0;
  transition: opacity var(--dur-med) var(--ease-out);
}

.dial.is-idle .dial__knob,
.dial.is-paused .dial__knob,
.dial.is-dragging .dial__knob,
.dial:focus-visible .dial__knob {
  opacity: 1;
}

.dial.is-static .dial__knob,
.dial.is-overtime .dial__knob {
  opacity: 0;
}

@media (hover: hover) {
  .dial:not(.is-static):hover .dial__knob {
    opacity: 1;
  }
}

.dial__knobCast {
  fill: var(--dial-cast);
}

/* Knurling. Fine vertical flutes cut about the knob's own axis, each ridge with
   a lit left flank and a shaded right flank, because the light is above and to
   the left. Drawn as chords rather than a repeating background because SVG
   geometry cannot take one — same zero bytes, same crispness at any DPI, and it
   foreshortens toward the silhouette the way a turned rim actually does. */
.dial__knobKnurl {
  stroke-width: 0.3;
  stroke-linecap: butt;
}

.dial__knobKnurl--hi {
  stroke: var(--dial-knurl-hi);
}

.dial__knobKnurl--lo {
  stroke: var(--dial-knurl-lo);
}

.dial__knobRing {
  stroke: var(--hairline-strong);
  stroke-width: 0.5;
}

/* The turned cap: smooth in the middle, so the knurling reads as being on the
   EDGE and not across the whole face. Its hairline is the machined step down. */
.dial__knobGroove {
  stroke: var(--hairline);
  stroke-width: 0.5;
}

.dial__knobSpec {
  stroke: var(--dial-lift);
  stroke-width: 0.7;
  stroke-linecap: round;
  /* WEAR HOOK: PHASE3 §4 item 3 — the thumbed knob gets SHINIER, not duller.
     Real metal polishes where it is touched. 0.82 → 1.0 across the wear curve. */
  stroke-opacity: calc(0.82 + 0.18 * var(--dial-wear));
}

/* ============================================================== *
 * 8. THE SPINDLE
 * ============================================================== */

.dial__hubCast {
  fill: var(--dial-cast);
}

.dial__hubRing {
  stroke: var(--hairline-strong);
  stroke-width: 0.5;
}

.dial__hubDot {
  fill: var(--ink-3);
}

/* ============================================================== *
 * 9. PREFERENCE OVERRIDES
 * ============================================================== */

@media (prefers-reduced-motion: reduce) {
  /* No breathing, no fade — state changes land instantly. src/dial.js also makes
     pulse() and the scrub detent no-ops, skips the settle tween and the
     completion settle, so nothing on the dial moves that the user did not move
     themselves. The audio tick and the vibration still fire: reduced *motion* is
     not reduced *feedback*. */
  .dial.is-approaching .dial__disc {
    animation: none;
    opacity: 1;
  }

  .dial__knob,
  .dial__knobFace,
  .dial__knobRing,
  .dial__knobCast {
    transition: none;
  }
}

@media (prefers-contrast: more) {
  .dial {
    --dial-tick: var(--ink-2);
    --dial-num: var(--ink);
  }

  .dial__minor {
    opacity: 1;
    stroke-width: 0.65;
  }

  .dial__discFill,
  .dial__discFillFull {
    stroke-width: 1;
  }
}

@media (forced-colors: active) {
  /* Gradients and shadow layers all collapse; keep the geometry legible instead.
     These CSS declarations beat src/dial.js's fill= / stroke= presentation
     attributes, which is exactly what is wanted here. */
  .dial::after {
    box-shadow: none;
    border: 1px solid CanvasText;
  }

  .dial__plate {
    fill: Canvas;
  }

  .dial__wash,
  .dial__discSheen,
  .dial__discSheenFull,
  .dial__knobCast,
  .dial__hubCast,
  .dial__bezelSpec,
  .dial__bezelKey,
  .dial__knobKnurl,
  .dial__wear {
    display: none;
  }

  .dial__discFill,
  .dial__discFillFull {
    fill: Highlight;
    stroke: CanvasText;
  }

  .dial__overArc,
  .dial__overFull {
    stroke: CanvasText;
  }

  .dial__minor,
  .dial__major,
  .dial__rim,
  .dial__rim--out,
  .dial__track {
    stroke: CanvasText;
  }

  .dial__bezel {
    stroke: CanvasText;
    stroke-width: 1;
  }

  .dial__num {
    fill: CanvasText;
  }

  .dial__knobFace,
  .dial__knobCap,
  .dial__hubFace {
    fill: Canvas;
  }

  .dial__knobRing,
  .dial__knobGroove,
  .dial__hubRing {
    stroke: CanvasText;
  }

  .dial:focus-visible {
    outline: 2px solid Highlight;
  }
}

/* ============================================================== *
 * 10. THE WEAR HOOK — for src/wear.js + css/wear.css (PHASE3 §4)
 *
 *     This file does NOT implement the patina. It provides the surface
 *     the patina lands on and the numbers it needs to land accurately.
 *
 *     THE LAYER. src/dial.js appends
 *         <div class="dial__wear" aria-hidden="true"></div>
 *     as the last child of .dial, and returns it as `api.wear`. It is
 *     empty, inert, clipped to the circle and exactly the size of the
 *     instrument, and the rule below is all it ever gets from this file.
 *
 *     Three guarantees it can be built on, and they are the contract:
 *       · .dial sets `isolation: isolate`, so anything screen-blended in
 *         here composites against the dial's own artwork and STOPS at the
 *         instrument. It can never reach the page behind it.
 *       · the box carries no z-index of its own, so it does not open a
 *         stacking context this file did not ask for — css/wear.css is
 *         free to open one itself, and does.
 *       · .dial::after (the recess walls, z-index 2) still paints above
 *         it. Dust and scratches sit under the glass, not on the well.
 *
 *     css/wear.css takes the box, floods it with black, isolates it and
 *     screens the whole GROUP onto the artwork rather than screening each
 *     matte separately. That is pixel-identical (screen is associative)
 *     and turns four per-frame blends into one, which is PHASE3 §4's "one
 *     composited layer" taken literally. Nothing here needs to change for
 *     it, and nothing here should assume the simpler arrangement.
 *
 *     THE GEOMETRY. src/dial.js publishes these on the .dial element, as
 *     percentages of the .dial box's RADIUS, which is the unit CSS radial
 *     gradients and inset shadows want. They are derived from the same
 *     constants the SVG is drawn from, so they cannot drift:
 *
 *       --dial-r-bezel   the bezel band's centreline   (~89.75%)
 *       --dial-r-face    the ceramic face plate         (~87.03%)
 *       --dial-r-disc    the disc's rim — this is the one the sweep ghost
 *                        wants, because it is the circle the disc's edge
 *                        traces over and over                  (~62.44%)
 *       --dial-r-orbit   the knob's orbit                (~60.76%)
 *       --dial-r-knob    the knob's own radius            (~4.66%)
 *       --dial-light     the light's azimuth, clockwise from 12   (−30deg)
 *
 *     They are derived in src/dial.js from the same constants the SVG is
 *     drawn from, so a change to a radius here cannot silently desynchronise
 *     the patina — a sweep ghost placed at var(--dial-r-disc) is on the
 *     circle the disc's edge actually traces, today and after any edit.
 *
 *     THE PROPERTY. Set --dial-wear on .dial (or anything above it), 0…1,
 *     from getWear(). It defaults to 0 and is already routed into the two
 *     effects that live INSIDE the SVG, where an outside stylesheet would
 *     have to hard-code this file's private class names to reach them:
 *
 *       PHASE3 §4 item 3, knob polish  .dial__knobSpec stroke-opacity
 *                                      0.82 → 1.0 (it gets SHINIER)
 *       PHASE3 §4 item 4, bezel wear   .dial__bezelSpec opacity
 *                                      1.0 → 0.82
 *
 *     Items 1, 2, 5 and 6 — micro-scratches, the sweep ghost, the ceramic
 *     patina, dust and smudge — are all matte or surface work and belong to
 *     css/wear.css. Nothing here touches type, and nothing here is on the
 *     per-frame path: --dial-wear is read by the cascade, not by the render
 *     loop, so the dial's 60fps budget is untouched however often it moves.
 * ============================================================== */

.dial__wear {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  overflow: hidden;
  pointer-events: none;
/* No z-index, deliberately: it is the last positioned child, so it already
     paints over the SVG, and a z-index here would open a stacking context and
     isolate the mattes from the artwork they are supposed to blend with. */
}

