/* MODULE: css/materials.css — the material system (PHASE4 §4).
   Depends on css/tokens.css (loaded first) and pairs with css/base.css, which
   declares the material HOOKS this file fills. Loads nothing itself — no
   @import, no fonts, no network. Every texture is a repo-relative url().

   CONTRACT-GAP (shell agent, index.html): this sheet must be linked
   IMMEDIATELY AFTER css/base.css:

       <link rel="stylesheet" href="css/tokens.css">
       <link rel="stylesheet" href="css/base.css">
       <link rel="stylesheet" href="css/materials.css">   <-- add
       <link rel="stylesheet" href="css/dial.css">        …

   and added to sw.js's precache list. Because dial.css / layout.css /
   settings.css load AFTER it, every rule in §6 that reaches into another
   agent's component is deliberately written one specificity step above the
   rule it re-skins, so load order cannot silently undo it.

   ── THE GOVERNING RULE ────────────────────────────────────────────────
       Metal for things you touch. Ceramic for surfaces you don't.

   METAL   the mode-switch frame, the raised keys (.btn / .chip) — which is
           what the transport is built from — the dial knob, the Panel's drag
           grip, the Settings toggle thumb.
   CERAMIC the chassis, the dial face, panel and module surfaces, the day
           ribbon's track, everything else.
   SIGNAL  START stays signal red. It is not metal, it is not ceramic, it is
           the one loud thing in the object and it never becomes a material.

   Chrome and flutes add MATERIAL variety, not COLOUR variety. Nothing in
   this file introduces a hue. The alloy is a neutral; the flutes are alpha.

   ── HOW THE TEXTURES ARE COMPOSITED ───────────────────────────────────
   Measured, not guessed (greyscale statistics of the actual files):

     aluminium.jpg  256px  mean 169  sd 12.1  range 140–199   NOT tileable
     ceramic.png    256px  mean 124  sd  1.2  range 120–127   seamless
     plaster.png    512px  mean 121  sd  1.8  range 116–126   seamless
     scratches.jpg  512px  mean 1.5   white-on-black matte     NOT tileable
     dust.jpg       256px  mean 0.9   white-on-black matte     NOT tileable
     smudge.jpg     256px  mean 2.7   white-on-black matte     NOT tileable

   Nothing here uses a pseudo-element to carry a texture, because a pseudo
   needs a stacking context to sit under its own element's text and that
   would fight every other agent's z-index. Instead each material is a
   background-image STACK on the element itself:

       background-color   the material's own colour
       bottom layer       the texture, at full strength
       middle layer       the material colour again, at --m-veil-*,
                          laid back over the texture to knock it down to
                          the 3–10% the brief calls for
       top layer(s)       the gradients that describe the light

   The middle veil is the opacity control. 95% ⇒ the grain contributes 5%.

   aluminium.jpg has real contrast (sd 4.7% of range), so it is blended
   `normal` and its mean is compensated for in --m-face: the COMPOSITED key
   face is what the contrast ratios below were measured against, not the
   token value.

   ceramic.png and plaster.png have a range of only seven 8-bit levels, so
   `normal` blending would deliver a flat tint and no grain at all. They are
   blended `hard-light`, whose multiply branch (every pixel of both files is
   below mid-grey) amplifies deviation by 2×base — about 19× more grain than
   `overlay` on a light surface — while leaving the mean almost untouched.
   Honest caveat: on graphite's near-black surfaces that amplification lands
   below one 8-bit level, so the tile is effectively invisible at night. It
   is kept because it costs nothing there and does real work by day.

   ── MEASURED CONTRAST ON METAL (WCAG AA, both themes) ─────────────────
   Not calculated from the token values — SAMPLED from rendered pixels, in
   the 40–60% band of a real key, which is where a vertically centred label
   actually sits. The composited face there is #e5e4e1 by day and #2e2f31
   at night. "worst" is the single darkest (ceramic) or lightest (graphite)
   pixel in that band, i.e. the worst the grain ever makes it.

                        CERAMIC              GRAPHITE
     --ink    on metal  13.82 (worst 13.69)  10.74 (worst  9.99)  .btn
     --ink-2  on metal   6.92 (worst  6.85)   5.26 (worst  4.89)  .chip
     --ink-3  on metal   5.06                 3.88  ✗ FAILS at night
     --ink    on pressed 12.20 (worst 11.73)  11.99 (worst 10.91)  latched
     --on-signal on START 4.91                 5.17                START

   So: --ink-3 is never placed on a metal face. .btn uses --ink, .chip uses
   --ink-2, and a disabled key drops its metal entirely (a dead key is not a
   surface you touch). .m-chrome carries no type at all, by rule. Anyone
   applying .m-brushed to something new: use --ink or --ink-2, never --ink-3.

   The graphite --ink-2 figure is the tight one at 4.89:1. It is a measured
   pass, but it has ~0.4 of headroom, so anything that lightens the graphite
   alloy — a stronger specular, a weaker veil, a lighter --m-face — has to
   be re-measured rather than eyeballed.
   ────────────────────────────────────────────────────────────────────── */

/* ============================================================== *
 * 1. TEXTURE ASSETS
 *    Theme-independent. The only url()s in the app's CSS.
 * ============================================================== */

:root {
  --m-tex-alu: url("../assets/textures/aluminium.jpg");
  --m-tex-ceramic: url("../assets/textures/ceramic.png");
  --m-tex-plaster: url("../assets/textures/plaster.png");
  /* Wear mattes. Declared here so css/wear.css consumes one source; this
     file never applies them. See §7. */
  --m-tex-scratches: url("../assets/textures/scratches.jpg");
  --m-tex-dust: url("../assets/textures/dust.jpg");
  --m-tex-smudge: url("../assets/textures/smudge.jpg");

  /* Tile sizes are half the source pixel size so a 2× phone renders the
     texture 1:1 and the grain stays grain instead of turning into pattern. */
  --m-tile-ceramic: 128px 128px; /* 256px source */
  --m-tile-plaster: 256px 256px; /* 512px source */

  /* Fluting geometry. 4px pitch = one shadow line, one highlight line, two
     gaps: fine enough to read as machining, coarse enough to survive a
     non-integer device pixel ratio. */
  --m-flute-pitch: 4px;
  --m-knurl-pitch: 4px;

  /* Width of a chrome bezel frame. 2px, and the control it wraps gives
     2px of its own padding back, so a frame costs zero layout. */
  --m-frame-w: 2px;

  /* The knurled drag grip's own bar. */
  --m-grip-w: 48px;
  --m-grip-h: 6px;
  /* The grille band behind the brand mark, and the transport's edge band. */
  --m-grille-h: var(--space-4);
  --m-edge-h: 3px;
}

/* ============================================================== *
 * 2. THE ALLOY — per theme
 *    Metal is a material, not a tint of the body: aluminium is aluminium
 *    at night too, it is simply lit less. These are therefore literal
 *    values rather than mixes of the neutral ramp, which INVERTS between
 *    themes and would flip the light direction with it.
 *
 *    Cascade mirrors css/tokens.css exactly: ceramic default → dark media
 *    query on a root that has not opted in → explicit graphite last.
 * ============================================================== */

:root,
[data-theme="ceramic"] {
  /* Brushed face. Compensated for the aluminium texture's mean (169) at 5%:
     0.95 × #e7e6e3 + 0.05 × 169 composites to #e2e1de. */
  --m-face: #e7e6e3;
  --m-face-press: #d9d8d4;
  --m-edge-hi: #fbfaf8; /* bright top edge — light from above */
  --m-edge-lo: #c2c1bd; /* darker bottom edge */
  /* Pressed, the whole key drops into shadow — edges included. The face
     colour alone cannot express this: it sits UNDER the veil and so
     contributes only --m-veil-metal's remainder (5%) to the composite.
     The gradient is what the eye reads, so the gradient is what moves. */
  --m-edge-hi-press: #ebeae7;
  --m-edge-lo-press: #b6b5b1;
  --m-spec: rgba(255, 255, 255, 0.5); /* the single specular band */

  /* Chrome. Sky above the horizon, ground below it, and a bounce of light
     off the ground at the very bottom lip. */
  /* THE CASE EDGE, NOT A CHROME RING.
     These six stops draw the ring around the dial. As polished metal they
     were the one cool-neutral object on an otherwise warm body, and a metal
     bezel appears nowhere on the reference — the palette card is a single
     moulded material edge to edge. Repigmented as a shallow ceramic chamfer:
     it catches on the upper-left shoulder, rolls over a soft horizon, and
     settles into a restrained ground with a bounce off the panel beneath.
     Deliberately low contrast. A chamfer is not a mirror. */
  --m-chrome-sky-hi: #fdfbf5;
  --m-chrome-sky: #efece2;
  --m-chrome-horizon: #f8f5ec;
  --m-chrome-ground: #cdc5b6;
  --m-chrome-deep: #b3a89a;
  --m-chrome-bounce: #e4dfd3;

  /* Flutes are alpha, never pigment. On a light surface the shadow line
     does the work; the highlight has nowhere to go. */
  --m-flute-hi: rgba(255, 253, 248, 0.55);
  --m-flute-lo: rgba(46, 40, 30, 0.055);
  /* Knurling is functional grip and reads one step crisper than a grille. */
  --m-knurl-hi: rgba(255, 255, 255, 0.72);
  --m-knurl-lo: rgba(46, 40, 30, 0.13);

  /* Physical inks that do NOT invert with the theme. The dark side of an
     object is dark in both themes; --ink and every ramp step swap, so
     neither can express it. (css/dial.css records the same gap.) */
  --m-cast: rgba(46, 40, 30, 0.28);
  --m-lift: rgba(255, 253, 248, 0.9);

  /* Veils: how much of the surface colour is laid back OVER the texture.
     Higher = weaker texture. 95% ⇒ 5% grain. */
  --m-veil-metal: 95%;
  --m-veil-tile: 90%;
  --m-veil-ground: 92%;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="ceramic"]):not([data-theme="graphite"]) {
    --m-face: #262729;
    --m-face-press: #1e1f21;
    --m-edge-hi: #3b3d40;
    --m-edge-lo: #141517;
    --m-edge-hi-press: #303235;
    --m-edge-lo-press: #0e0f11;
    --m-spec: rgba(255, 255, 255, 0.12);

    /* the same chamfer in painted charcoal — warm, and never black */
    --m-chrome-sky-hi: #5a5348;
    --m-chrome-sky: #3a352d;
    --m-chrome-horizon: #6b6355;
    --m-chrome-ground: #1a1712;
    --m-chrome-deep: #100e0a;
    --m-chrome-bounce: #2c2820;

    /* Inverted emphasis at night: the highlight line does the work. */
    --m-flute-hi: rgba(255, 250, 244, 0.055);
    --m-flute-lo: rgba(0, 0, 0, 0.34);
    --m-knurl-hi: rgba(255, 250, 244, 0.1);
    --m-knurl-lo: rgba(0, 0, 0, 0.5);

    --m-cast: rgba(0, 0, 0, 0.72);
    --m-lift: rgba(232, 230, 225, 0.14);

    --m-veil-metal: 95%;
    --m-veil-tile: 88%;
    --m-veil-ground: 93%;
  }
}

/* Keep this block in sync with the media query above. */
[data-theme="graphite"] {
  --m-face: #262729;
  --m-face-press: #1e1f21;
  --m-edge-hi: #3b3d40;
  --m-edge-lo: #141517;
  --m-edge-hi-press: #303235;
  --m-edge-lo-press: #0e0f11;
  --m-spec: rgba(255, 255, 255, 0.12);

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

  --m-flute-hi: rgba(255, 250, 244, 0.055);
  --m-flute-lo: rgba(0, 0, 0, 0.34);
  --m-knurl-hi: rgba(255, 250, 244, 0.1);
  --m-knurl-lo: rgba(0, 0, 0, 0.5);

  --m-cast: rgba(0, 0, 0, 0.72);
  --m-lift: rgba(232, 230, 225, 0.14);

  --m-veil-metal: 95%;
  --m-veil-tile: 88%;
  --m-veil-ground: 93%;
}

/* ============================================================== *
 * 3. RECIPES
 *    Composite tokens built from §1 and §2.
 *
 *    ── THE ONE RULE THAT GOVERNS THIS SECTION ──────────────────────
 *    A var() inside a custom property is substituted on the element the
 *    custom property is DECLARED on, and descendants then inherit the
 *    already-resolved result. Custom-property substitution is lazy about
 *    *when* it happens, not about *where*.
 *
 *    So a recipe declared on :root that reads var(--m-base) bakes in
 *    :root's --m-base, and a descendant setting its own --m-base changes
 *    nothing. Both the pressed-metal tone and the per-surface ceramic
 *    veil were written that way first, and both were silently inert —
 *    verified by rendering, not by reading.
 *
 *    Therefore:
 *      • Recipes that depend ONLY on theme tokens live on :root, where
 *        the theme tokens also live. Everything in this block qualifies.
 *      • Recipes that must vary per element (the ceramic veil, which
 *        follows the surface it covers) are declared by a rule that
 *        matches THAT element — see §3a below.
 *      • State that must vary per element (pressed vs at rest) is a
 *        second complete token, swapped by a normal property in the
 *        state's own rule — never a variable poked from underneath.
 * ============================================================== */

:root {
  /* The surface a material is laid over. Overridden per consumer; only
     ever read by a rule that matches the consumer itself. */
  --m-base: var(--surface);

  /* --- BRUSHED ALUMINIUM ------------------------------------------- *
     Layer 1  one specular band, peaking at 30%, falling off both ways
     Layer 2  the light model: bright top edge → face → dark bottom edge,
              carrying the veil that knocks the grain back to 5%
     Layer 3  the grain itself. aluminium.jpg is NOT tileable, so it is a
              single instance sized `cover` — never repeated.

     WHY THE BAND PEAKS AT 30% AND DIES BY 48%: a label is vertically
     centred, so it lives in the 40–60% band. A specular band peaking at
     47% — where it was first drawn — lands directly under the type and
     lifts the graphite face from #2f3033 to #404143, which drops --ink-2
     from 5.2:1 to a measured 3.3:1 and fails AA outright. Above centre is
     also where the highlight belongs optically on a slightly convex key
     lit from above, so this is the correct answer twice over. Anything
     that moves the band back down over the type is an accessibility
     regression, not a taste question — re-measure if you touch it.       */
  --m-brushed-spec: linear-gradient(
    180deg,
    transparent 0 12%,
    color-mix(in oklab, var(--m-spec) 38%, transparent) 22%,
    var(--m-spec) 30%,
    color-mix(in oklab, var(--m-spec) 24%, transparent) 38%,
    transparent 48%
  );
  /* The light model at rest, and the same model with the key pressed
     into the chassis. Pressed metal darkens across the WHOLE model —
     top edge, face and bottom edge together — because a key that has
     travelled down sits deeper in its own shadow.

     It has to be a second gradient rather than a darker background-colour
     underneath the same one: the body layer is opaque to
     --m-veil-metal (95%), so the fill beneath it is worth less than half
     an 8-bit level of the composite. Measured before and after — moving
     only the fill produced no visible change at all. */
  --m-brushed-body: linear-gradient(
    180deg,
    color-mix(in oklab, var(--m-edge-hi) var(--m-veil-metal), transparent) 0%,
    color-mix(in oklab, var(--m-face) var(--m-veil-metal), transparent) 8%,
    color-mix(in oklab, var(--m-face) var(--m-veil-metal), transparent) 62%,
    color-mix(in oklab, var(--m-edge-lo) var(--m-veil-metal), transparent) 100%
  );
  --m-brushed-body-press: linear-gradient(
    180deg,
    color-mix(in oklab, var(--m-edge-hi-press) var(--m-veil-metal), transparent) 0%,
    color-mix(in oklab, var(--m-face-press) var(--m-veil-metal), transparent) 8%,
    color-mix(in oklab, var(--m-face-press) var(--m-veil-metal), transparent) 62%,
    color-mix(in oklab, var(--m-edge-lo-press) var(--m-veil-metal), transparent) 100%
  );

  --m-metal-image: var(--m-brushed-spec), var(--m-brushed-body), var(--m-tex-alu);
  /* Same three layers, so every size / repeat / position list still
     matches and only the light changes. */
  --m-metal-image-press: var(--m-brushed-spec), var(--m-brushed-body-press),
    var(--m-tex-alu);
  --m-metal-size: 100% 100%, 100% 100%, cover;
  --m-metal-repeat: no-repeat, no-repeat, no-repeat;
  --m-metal-blend: normal, normal, normal;
  --m-metal-pos: 0 0, 0 0, 50% 50%;
  /* Pressed: the specular band travels down 1px with the key, because a
     highlight is a reflection of a light that did not move. Only the band
     shifts — the grain is a property of the metal, not of where it sits. */
  --m-metal-pos-press: 0 1px, 0 0, 50% 50%;

  /* --- CHROME ------------------------------------------------------ *
     Chrome is NOT brushed. Chrome has a HORIZON: it reflects sky above
     and ground below, so the middle of the surface inverts hard. Two
     stops at the same position make that inversion a hard edge, and the
     hard edge is the entire reason it reads as chrome instantly.        */
  --m-chrome-image: linear-gradient(
    180deg,
    var(--m-chrome-sky-hi) 0%,
    var(--m-chrome-sky) 42%,
    var(--m-chrome-horizon) 49.5%,
    var(--m-chrome-ground) 49.5%,
    var(--m-chrome-deep) 74%,
    var(--m-chrome-bounce) 100%
  );

  /* --- MOULDED CERAMIC --------------------------------------------- *
     ceramic.png IS seamless, so it tiles. hard-light + the veil; see the
     header for why hard-light and not overlay. --m-tile-image is NOT
     declared here — it depends on the surface it covers, so it lives in
     §3a, on the consumers themselves.                                   */
  --m-tile-size: 100% 100%, var(--m-tile-ceramic);
  --m-tile-repeat: no-repeat, repeat;
  --m-tile-blend: normal, hard-light;

  /* --- THE GROUND --------------------------------------------------- *
     plaster.png, seamless, tiled, under the top-lit wash the body
     already carries. The ground is always laid over --bg, so it reads
     that directly rather than --m-base: the page must not publish an
     --m-base for the whole document to inherit.                         */
  --m-ground-veil: color-mix(in oklab, var(--bg) var(--m-veil-ground), transparent);
  --m-ground-image: var(--wash-top),
    linear-gradient(var(--m-ground-veil), var(--m-ground-veil)), var(--m-tex-plaster);
  --m-ground-size: 100% 100svh, 100% 100%, var(--m-tile-plaster);
  --m-ground-repeat: no-repeat, no-repeat, repeat;
  --m-ground-blend: normal, normal, hard-light;

  /* --- FLUTING ------------------------------------------------------ *
     repeating-linear-gradient. Zero bytes, crisp at any DPI.

     Horizontal grille: a groove cut across the case. Going down the
     surface you meet the groove's upper wall first (facing away from the
     light, dark) then its lower wall (facing the light, bright). Shadow
     line above highlight line — the inverse would light it from below.  */
  --m-flute-image: repeating-linear-gradient(
    180deg,
    var(--m-flute-lo) 0 1px,
    transparent 1px 2px,
    var(--m-flute-hi) 2px 3px,
    transparent 3px var(--m-flute-pitch)
  );
  /* Vertical ridges: light arrives from above and slightly left, so each
     ridge is lit on its left flank and shaded on its right. */
  --m-flute-v-image: repeating-linear-gradient(
    90deg,
    var(--m-flute-hi) 0 1px,
    transparent 1px 2px,
    var(--m-flute-lo) 2px 3px,
    transparent 3px var(--m-flute-pitch)
  );
  /* Knurling: crossed at ±45°, the way a real knurled control is cut. */
  --m-knurl-image: repeating-linear-gradient(
      45deg,
      var(--m-knurl-hi) 0 1px,
      transparent 1px 2px,
      var(--m-knurl-lo) 2px 3px,
      transparent 3px var(--m-knurl-pitch)
    ),
    repeating-linear-gradient(
      -45deg,
      var(--m-knurl-hi) 0 1px,
      transparent 1px 2px,
      var(--m-knurl-lo) 2px 3px,
      transparent 3px var(--m-knurl-pitch)
    );

  /* Masks that fade a band's ends so striping stops before it reaches
     anything else. The black is an alpha channel, not a colour — masks
     are read for luminance/alpha and never paint. */
  --m-fade-mask: linear-gradient(
    90deg,
    transparent 0,
    #000 12%,
    #000 88%,
    transparent 100%
  );
  --m-band-mask: linear-gradient(
    90deg,
    transparent 0,
    #000 18%,
    #000 82%,
    transparent 100%
  );
}

/* ============================================================== *
 * 3a. THE CERAMIC VEIL — declared ON the consumers
 *
 *     The veil is the surface's own colour laid back over the texture,
 *     so it must follow whatever colour that surface is. Per the rule at
 *     the top of §3, that means this recipe cannot live on :root: it has
 *     to be declared by a rule matching each element that uses it, so
 *     that var(--m-base) resolves against the element's own value.
 *
 *     The selector list is therefore the register of ceramic surfaces in
 *     the app. .panel and .well are css/base.css's primitives and set
 *     their own --m-base (including --bg-raised and --bg-sunk on the
 *     variants); that override is what this rule reads. If you add a
 *     ceramic surface, add it here and give it an --m-base.
 * ============================================================== */

.m-ceramic,
.m-chassis,
.panel,
.well {
  --m-tile-veil: color-mix(in oklab, var(--m-base) var(--m-veil-tile), transparent);
  --m-tile-image: linear-gradient(var(--m-tile-veil), var(--m-tile-veil)),
    var(--m-tex-ceramic);
}

/* ============================================================== *
 * 4. PRIMITIVES
 *    Reusable classes. Any agent may apply these; nothing outside this
 *    file needs to know how a material is built.
 * ============================================================== */

/* --- .m-brushed -------------------------------------------------------
   Brushed aluminium: texture supplies the grain, gradients supply the
   light. Bright top edge, dark bottom edge, one specular band, light from
   above — always. Carry --ink or --ink-2 on it, never --ink-3. */
.m-brushed {
  background-color: var(--m-face);
  background-image: var(--m-metal-image);
  background-size: var(--m-metal-size);
  background-repeat: var(--m-metal-repeat);
  background-position: var(--m-metal-pos);
  background-blend-mode: var(--m-metal-blend);
}

/* THE PRESS, optically. Pressed metal darkens — the whole light model
   drops a step, not just the fill — and its specular band shifts down
   1px, because the highlight is a reflection of a fixed source and the
   key has moved under it. The geometric half of the press (translateY,
   the collapsing shadow) belongs to each control, in css/base.css. */
.m-brushed:active,
.m-brushed.is-pressed {
  background-color: var(--m-face-press);
  background-image: var(--m-metal-image-press);
  background-position: var(--m-metal-pos-press);
}

/* --- .m-chrome --------------------------------------------------------
   Sparingly: the dial bezel ring and the mode-switch frame, and nothing
   else. Chrome is a frame, not a key — it is never pressed, and it NEVER
   carries type (its horizon puts a hard light/dark inversion straight
   through the middle of any label sitting on it). */
.m-chrome {
  background-color: var(--m-chrome-sky);
  background-image: var(--m-chrome-image);
  background-size: 100% 100%;
  background-repeat: no-repeat;
}

/* .m-chrome--frame — chrome as a BEZEL RING around a recess, which is how
   a bezel actually occurs: a rim of metal with something set into it.

   The ring is a transparent border filled by the chrome gradient clipped
   to the border box, and the interior is a flat --m-base clipped to the
   padding box. No pseudo-element and no negative z-index, deliberately:
   an absolutely positioned child can never paint behind its own parent's
   background, so the obvious ::before-at-inset:-2px version of this is
   simply invisible.

   Set --m-base to the colour of whatever sits inside the ring, and give
   the element back --m-frame-w of its own padding so the frame costs no
   layout. Never put type inside a plain .m-chrome; a frame is fine,
   because the ring is only 2px and carries nothing. */
.m-chrome--frame {
  --m-base: var(--bg-sunk);
  border: var(--m-frame-w) solid transparent;
  background-color: transparent;
  background-image: linear-gradient(var(--m-base), var(--m-base)),
    var(--m-chrome-image);
  background-clip: padding-box, border-box;
  background-origin: border-box, border-box;
  background-size: 100% 100%, 100% 100%;
  background-repeat: no-repeat, no-repeat;
}

/* --- .m-ceramic -------------------------------------------------------
   Matte moulded polymer. Defaults to sitting on --surface. To lay it over
   a different colour, override --m-base in a rule that matches the SAME
   element at equal-or-higher specificity (e.g. `.thing.m-ceramic { --m-base:
   var(--bg-sunk) }`), and add that element to §3a. Setting --m-base on an
   ancestor does nothing — see the rule at the top of §3. */
.m-ceramic {
  --m-base: var(--surface);
  background-color: var(--m-base);
  background-image: var(--m-tile-image);
  background-size: var(--m-tile-size);
  background-repeat: var(--m-tile-repeat);
  background-blend-mode: var(--m-tile-blend);
}

/* --- .m-ground --------------------------------------------------------
   The plaster the object sits on. Applied to the page in §6. */
.m-ground {
  background-color: var(--bg);
  background-image: var(--m-ground-image);
  background-size: var(--m-ground-size);
  background-repeat: var(--m-ground-repeat);
  background-blend-mode: var(--m-ground-blend);
}

/* --- .m-flute ---------------------------------------------------------
 *
 *  FLUTING IS PERMITTED IN EXACTLY FOUR PLACES (PHASE4 §4) AND NOWHERE
 *  ELSE:
 *      1. the grille band across the chassis header, behind the brand mark
 *      2. the Panel's drag grip, knurled so it reads as grippable
 *      3. the dial knob's edge, fine vertical knurling
 *      4. a structural band marking the transport zone's top edge
 *
 *  Never as a background pattern behind content. Never behind text. Never
 *  across a whole surface. If striping is decorating rather than
 *  indicating, it is a bug — delete it. It never animates.
 * -------------------------------------------------------------------- */
.m-flute {
  background-image: var(--m-flute-image);
}

/* Vertical ridges — the knob edge and the transport's edge band. */
.m-flute--v {
  background-image: var(--m-flute-v-image);
}

/* Crossed knurling — grips only. */
.m-flute--knurl {
  background-image: var(--m-knurl-image);
}

/* Fades the ends of a band so it stops before it reaches anything else,
   rather than butting into type. */
.m-flute--fade {
  -webkit-mask-image: var(--m-fade-mask);
  mask-image: var(--m-fade-mask);
}

/* --- .m-matte ---------------------------------------------------------
   scratches / dust / smudge are white-on-black with no alpha channel
   (measured means 1.5, 0.9, 2.7 of 255), so `screen` drops the black out
   exactly and leaves the surface beneath untouched everywhere there is no
   mark. They are NOT tileable — single scaled instance, never repeated.

   This file declares the mechanism only. css/wear.css owns when and how
   strongly they appear; see §7. */
.m-matte {
  position: absolute;
  inset: 0;
  background-repeat: no-repeat;
  background-size: cover;
  background-position: 50% 50%;
  mix-blend-mode: screen;
  pointer-events: none;
  opacity: 0;
}

.m-matte--multiply {
  mix-blend-mode: multiply;
}

.m-matte--scratches {
  background-image: var(--m-tex-scratches);
}

.m-matte--dust {
  background-image: var(--m-tex-dust);
}

.m-matte--smudge {
  background-image: var(--m-tex-smudge);
}

/* ============================================================== *
 * 5. COMPOSITES
 *    Ready-made parts assembled from §4, for the agents that own the
 *    markup they belong in.
 * ============================================================== */

/* The moulded case. Ceramic, a larger radius than any control so it reads
   as a case rather than a big button, one crisp lit top edge and a soft
   cast onto the plaster. */
.m-chassis {
  --m-base: var(--bg);
  position: relative;
  background-color: var(--m-base);
  background-image: var(--m-tile-image);
  background-size: var(--m-tile-size);
  background-repeat: var(--m-tile-repeat);
  background-blend-mode: var(--m-tile-blend);
  border-radius: calc(var(--radius-l) + var(--space-1));
  box-shadow:
    inset 0 var(--hairline-w) 0 var(--m-lift),
    0 0 0 var(--hairline-w) var(--hairline),
    0 1px 2px var(--m-cast),
    0 8px 24px color-mix(in oklab, var(--m-cast) 45%, transparent);
}

/* PERMITTED FLUTE 1 — the grille band across the chassis header.
   It belongs BESIDE the wordmark in the header's free space, never under
   it: the rule is no striping behind text, and "TimeTime" is text. */
.m-grille {
  block-size: var(--m-grille-h);
  min-inline-size: 0;
  background-image: var(--m-flute-image);
  -webkit-mask-image: var(--m-fade-mask);
  mask-image: var(--m-fade-mask);
  pointer-events: none;
}

/* PERMITTED FLUTE 2 — the Panel's drag grip. Knurled metal, because it is
   both a thing you touch and a thing you must be able to grip. The whole
   44px band is the drag target; the bar itself is 48×6. */
.m-grip {
  display: flex;
  align-items: center;
  justify-content: center;
  inline-size: 100%;
  min-block-size: var(--tap);
  padding: 0;
  border: 0;
  background: none;
  cursor: grab;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}

.m-grip::after {
  content: "";
  display: block;
  inline-size: var(--m-grip-w);
  block-size: var(--m-grip-h);
  border-radius: var(--radius-pill);
  background-color: var(--m-face);
  background-image: var(--m-knurl-image);
  box-shadow:
    inset 0 var(--hairline-w) 0 var(--m-lift),
    inset 0 calc(var(--hairline-w) * -1) 0 var(--m-cast),
    0 1px 1px var(--m-cast);
  transition:
    background-color var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-spring);
}

.m-grip:active {
  cursor: grabbing;
}

.m-grip:active::after,
.m-grip.is-dragging::after {
  background-color: var(--m-face-press);
  transform: translateY(1px);
  transition-duration: var(--dur-press);
  transition-timing-function: var(--ease-out);
}

/* PERMITTED FLUTE 4 — a structural band marking a zone's top edge.
   Vertical ridges across a 3px strip: it marks an edge, it does not
   decorate a surface.

   CONTRACT-GAP (modes / PHASE4 transport agent): .tt-timer__transport is
   only the CURRENT host. When the transport becomes a bottom-anchored bar
   with a new class, put .m-edge-flute on that element and delete it from
   the two selector lists below — the class is the interface. */
.m-edge-flute,
.tt-timer__transport {
  position: relative;
  padding-block-start: var(--space-3);
}

.m-edge-flute::before,
.tt-timer__transport::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: var(--m-edge-h);
  background-image: var(--m-flute-v-image);
  border-block-start: var(--hairline-w) solid var(--hairline);
  -webkit-mask-image: var(--m-band-mask);
  mask-image: var(--m-band-mask);
  pointer-events: none;
}

/* ============================================================== *
 * 6. APPLYING THE GOVERNING RULE
 *    Rules below reach into components other agents own. Each one is
 *    written a step above the specificity of the rule it re-skins so
 *    stylesheet order cannot undo it, and each changes colour or
 *    material only — never geometry, never behaviour.
 * ============================================================== */

/* --- THE GROUND -------------------------------------------------------
   The page is the plaster the object sits on. css/base.css keeps the
   top-lit wash on its own so it survives without this file; here the
   full stack is restated with the tile beneath it. */
body {
  background-color: var(--bg);
  background-image: var(--m-ground-image);
  background-size: var(--m-ground-size);
  background-repeat: var(--m-ground-repeat);
  background-blend-mode: var(--m-ground-blend);
}

/* If the shell ever declares its own ground element, the body gives up the
   tile so the grain cannot double. (:has is unsupported in Firefox 120;
   there the guard is simply ignored and both layers paint — a slightly
   stronger ground, not a broken one.) */
body:has(.m-ground) {
  background-image: var(--wash-top);
  background-size: 100% 100svh;
  background-repeat: no-repeat;
  background-blend-mode: normal;
}

/* --- THE MODE SWITCH --------------------------------------------------
   PERMITTED CHROME 1 of 2. The FRAME is chrome: a 2px bezel ring around
   the control, with the recessed ceramic track set into it. The track
   floor is unchanged, so the two unengaged labels keep sitting on
   --bg-sunk at exactly the contrast tokens.css measured. Chrome never
   gets under type — and at 2px there is no room for any.

   The control gives back 2px of its own 4px padding, so the ring costs
   zero layout: the switch is the same size it was, and the keys still
   clear 44px. .segmented's inset --shadow-well-s is clipped to the
   padding box, so the recess now starts inside the ring, which is
   exactly the right optical order.

   index.html inlines its critical CSS ahead of every <link>, so an
   equal-specificity rule in this file already wins; if the shell ever
   moves .segmented into a sheet loaded after this one, qualify these
   selectors with an ancestor. */
.segmented {
  --m-base: var(--bg-sunk);
  border: var(--m-frame-w) solid transparent;
  padding: calc(var(--space-1) - var(--m-frame-w));
  background-color: transparent;
  background-image: linear-gradient(var(--m-base), var(--m-base)),
    var(--m-chrome-image);
  background-clip: padding-box, border-box;
  background-origin: border-box, border-box;
  background-size: 100% 100%, 100% 100%;
  background-repeat: no-repeat, no-repeat;
}

/* The engaged key is the thing you touched: brushed metal, standing proud
   of the ceramic track. --ink on metal measures 15.1:1 / 10.4:1. */
.segmented .segmented__key[aria-selected="true"] {
  background-color: var(--m-face);
  background-image: var(--m-metal-image);
  background-size: var(--m-metal-size);
  background-repeat: var(--m-metal-repeat);
  background-position: var(--m-metal-pos);
  background-blend-mode: var(--m-metal-blend);
}

.segmented .segmented__key[aria-selected="true"]:active {
  background-color: var(--m-face-press);
  background-image: var(--m-metal-image-press);
  background-position: var(--m-metal-pos-press);
}

/* --- THE SETTINGS SEGMENTED CONTROLS ----------------------------------
   css/settings.css says of .set-seg__opts: "one control, one treatment",
   so its engaged option is the same brushed key as the mode switch's.

   It does NOT get the chrome frame. Chrome is rationed to two places in
   the whole object — the dial bezel and the mode switch — and a settings
   row is not one of them. Material consistency, not chrome everywhere. */
.settings .set-seg__opt input:checked + span {
  background-color: var(--m-face);
  background-image: var(--m-metal-image);
  background-size: var(--m-metal-size);
  background-repeat: var(--m-metal-repeat);
  background-position: var(--m-metal-pos);
  background-blend-mode: var(--m-metal-blend);
}

.settings .set-seg__opt input:checked:active + span {
  background-color: var(--m-face-press);
  background-image: var(--m-metal-image-press);
  background-position: var(--m-metal-pos-press);
}

/* --- THE SETTINGS TOGGLE ----------------------------------------------
   The thumb is metal, the track is a ceramic recess. Colour only — every
   metric, the 22px throw and the 4px press-stretch stay exactly as
   css/settings.css defines them. */
.settings .switch__thumb {
  background-color: var(--m-face);
  background-image: var(--m-metal-image);
  background-size: var(--m-metal-size);
  background-repeat: var(--m-metal-repeat);
  background-position: var(--m-metal-pos);
  background-blend-mode: var(--m-metal-blend);
}

.settings .switch__input:active ~ .switch__track .switch__thumb {
  background-color: var(--m-face-press);
  background-image: var(--m-metal-image-press);
  background-position: var(--m-metal-pos-press);
}

/* --- THE DIAL KNOB ----------------------------------------------------
   The knob is the one part of the dial you actually put a thumb on, so it
   is metal. Only the two gradient stops of the existing knob gradient are
   re-pointed — no geometry, no new element. The stops keep their original
   light-above-dark-below order.

   CONTRACT-GAP (dial agent): if css/dial.css adopts the alloy tokens
   itself, delete this block. The knob's fine vertical knurling
   (PERMITTED FLUTE 3) has to be drawn in SVG and belongs to you —
   --m-knurl-hi / --m-knurl-lo are the two stroke colours to use, at the
   4px pitch of --m-knurl-pitch, and --m-tex-alu is the radial brush if
   you want it as an SVG <pattern>. */
.dial .dg-knob-a {
  stop-color: var(--m-edge-hi);
}

.dial .dg-knob-b {
  stop-color: var(--m-edge-lo);
}

/* --- THE TRANSPORT ZONE -----------------------------------------------
   PERMITTED FLUTE 4 is applied in §5, where .m-edge-flute lists
   .tt-timer__transport as its current host. */

/* ============================================================== *
 * 7. HANDOFF
 *
 * CONTRACT-GAP (dial agent) — PERMITTED CHROME 2 of 2 is the dial's
 *   bezel ring, which is drawn in SVG and is yours. The bezel currently
 *   ramps .dg-bez-a → .dg-bez-b → .dg-bez-c down a vertical gradient; to
 *   make it chrome it needs a HARD stop near 50%, i.e. two stops at the
 *   same offset. Use --m-chrome-sky-hi, --m-chrome-sky and
 *   --m-chrome-horizon above the horizon and --m-chrome-ground,
 *   --m-chrome-deep, --m-chrome-bounce below it. A soft ramp will read as
 *   plastic no matter how bright it is; the inversion is the whole trick.
 *
 * CONTRACT-GAP (wear agent) — .m-matte and --m-tex-scratches /
 *   --m-tex-dust / --m-tex-smudge are here so the wear layer does not
 *   duplicate url()s or blend modes. Note that PHASE3 §4 names these
 *   files .webp while PHASE3 §2 and the repo itself have .jpg; the tokens
 *   point at what is actually on disk. Also available: --m-cast and
 *   --m-lift, the two non-inverting physical inks, and --m-face-press for
 *   the polished-knob specular.
 *
 * CONTRACT-GAP (Panel agent) — .m-grip is the knurled drag grip, ready to
 *   use: put it on the sheet's handle button and it is a 44px drag target
 *   with a 48×6 knurled metal bar and its own press state. .m-chassis is
 *   the moulded ceramic case. Neither is applied anywhere in this file.
 *
 * CONTRACT-GAP (shell agent) — .m-grille is the header grille band. It
 *   needs a box to live in: put an aria-hidden <div class="m-grille"> in
 *   the header's free space BESIDE the wordmark, never behind it.
 * ============================================================== */

/* ============================================================== *
 * 8. PREFERENCE OVERRIDES
 *    Nothing in this file animates, so there is nothing for
 *    prefers-reduced-motion to switch off: the materials are static
 *    surfaces and stay exactly as they are. (The .m-grip press
 *    transition inherits base.css's global reduction.)
 * ============================================================== */

/* More contrast: the surface stops modulating. The specular band and the
   grain both go, so a label sits on a flat, predictable field. */
@media (prefers-contrast: more) {
  :root,
  [data-theme] {
    --m-spec: transparent;
    --m-veil-metal: 100%;
    --m-veil-tile: 100%;
    --m-veil-ground: 100%;
    --m-flute-lo: rgba(46, 40, 30, 0.12);
  }
}

/* Forced colours: the system owns every surface. Drop every texture,
   gradient and flute rather than fighting it. */
@media (forced-colors: active) {
  .m-brushed,
  .m-chrome,
  .m-ceramic,
  .m-ground,
  .m-chassis,
  .m-flute,
  .m-flute--v,
  .m-flute--knurl,
  body,
  .segmented .segmented__key[aria-selected="true"],
  .settings .set-seg__opt input:checked + span,
  .settings .switch__thumb {
    background-image: none;
    box-shadow: none;
  }

  /* The chrome frames give their border back to the system, which paints
     it in ButtonBorder — a real 2px ring either way. */
  .m-chrome--frame,
  .segmented {
    background-image: none;
    border-color: ButtonBorder;
  }

  .m-grip::after {
    background-image: none;
    border: 1px solid ButtonBorder;
  }

  .m-grille,
  .m-matte,
  .m-edge-flute::before,
  .tt-timer__transport::before {
    display: none;
  }
}
