/* CONSOLE — the site-wide instrument theme.
   ─────────────────────────────────────────────────────────────────────────────────────────────
   ONE file, loaded LAST on every page. It does not replace chain.css, signal.css,
   desktop-tokens.css, tokens.css or any of the -chain.css bridges — it re-points the tokens they
   already read and adds the hardware character on top of the classes they already emit.

   Why tokens rather than rewrites: every page in this repo pulls its colour from a named custom
   property (--cn-* on the Chain pages, --bg/--panel/--text/--line on the self-contained ones,
   --sig-accent on the Signal pages). Re-pointing those is the same move chain.css already made
   for --lime, and it is why one file can carry all pages without touching their markup.

   The look: a cold graphite console. Navy-black grounds, silkscreen-cream legends, amber for
   what is live and red-orange for lamps, depth reserved for instruments (display windows, wells,
   rockers, lamps) and never spent on the list chrome around them. Inactive things recede by
   TONE ONLY — never by blurring anything that carries text.

   Load order matters: this must be the last stylesheet in <head> on each page. See APPLY.md. */

/* ══ 1 · GROUND ═══════════════════════════════════════════════════════════════════════════════
   The console palette, declared once. Everything below is derived from these six values. */
:root{
  --co-black:        #030507;   /* the viewer — the only true black on the page */
  --co-chassis:      #0D1116;   /* the case */
  --co-panel:        #12171E;   /* the face controls sit on */
  --co-well:         #0B0F14;   /* recessed bays inside the panel */
  --co-glass:        #06090D;   /* display windows */
  --co-ink:          #EFE7D6;   /* silkscreen cream */
  --co-amber:        #E8863A;   /* live / selected */
  --co-amber-hi:     #F6C79C;
  --co-lamp:         #FF7A3C;   /* LEDs only */
  --co-lamp-off:     rgba(104,60,38,0.40);
  --co-teal:         #5AAE9E;   /* the one secondary — sources, neutrals, "measured" */

  --co-edge:         rgba(239,231,214,0.055);  /* the hairline that defines a face */
  --co-edge-strong:  rgba(239,231,214,0.10);
  /* Alias. Seven rules in this file were written against --co-edge-2, which was never defined
     anywhere reachable from studio.html — an undefined custom property makes the whole
     `border:1px solid var(...)` declaration invalid, so all seven borders silently painted
     NOTHING: the Advanced drawer's top rule, the reset-module divider, the dial/lamp/frame
     wells, the action buttons and the stat cards were all edgeless. Caught by an audit against
     the design prototype, not by eye — a missing hairline reads as "slightly off", not as a
     bug, which is exactly why it survived several rounds of visual comparison. */
  --co-edge-2:       var(--co-edge-strong);
  --co-lift:         rgba(214,231,250,0.045);  /* a face catching light */
  --co-lift-on:      rgba(214,231,250,0.085);

  --co-shell: radial-gradient(120% 90% at 62% 45%, #1A222C 0%, #12171E 42%, #070A0D 100%);
}

/* ══ 2 · RETOKEN ══════════════════════════════════════════════════════════════════════════════
   Each block re-points one existing token family. Nothing here is a new name for a page to learn. */

/* 2a · CHAIN (index, studio, about, approach, credits, custom, gallery, mono, print, privacy) */
:root{
  --cn-page:   var(--co-chassis);
  --cn-shell:  var(--co-shell);
  --cn-well:   var(--co-well);
  --cn-ink:    var(--co-ink);
  --cn-amber:  var(--co-amber);
  --cn-amber-hi: #F5A44E;
  --cn-ink-on-amber: #0D1116;
  --cn-ink-on-white: #0D1116;

  --cn-surf:      rgba(214,231,250,0.045);
  --cn-surf-off:  rgba(214,231,250,0.022);
  --cn-surf-btn:  rgba(214,231,250,0.07);
  --cn-surf-track:rgba(214,231,250,0.12);

  --cn-line-off:  rgba(239,231,214,0.045);
  --cn-line-head: rgba(239,231,214,0.07);
  --cn-line-div:  rgba(239,231,214,0.09);
  --cn-line:      rgba(239,231,214,0.09);
  --cn-line-2:    rgba(239,231,214,0.10);
  --cn-line-btn:  rgba(239,231,214,0.12);
  --cn-line-chip: rgba(239,231,214,0.18);

  /* The chain ramp: cold at module 01, amber at module 28, every module's accent interpolated
     between the two by its position. It is the one piece of colour in the app that carries
     information — you can tell roughly where you are in the process without reading a word.
     studio.html's own CHAIN_GRAD_A has always been #B9C4C2; this said #7E8C93, so the JS-drawn
     tints and the CSS-drawn ones were walking two different ramps and the early modules came out
     a half-step colder in one than in the other. One ramp now. */
  --cn-grad-a:    #B9C4C2;
  --cn-grad-b:    var(--co-amber);
}

/* the ink ladder, restruck in cream */
:root{
  --cn-t-16: rgba(239,231,214,0.16); --cn-t-22: rgba(239,231,214,0.22);
  --cn-t-30: rgba(239,231,214,0.30); --cn-t-35: rgba(239,231,214,0.35);
  --cn-t-40: rgba(239,231,214,0.40); --cn-t-42: rgba(239,231,214,0.44);
  --cn-t-45: rgba(239,231,214,0.48); --cn-t-50: rgba(239,231,214,0.52);
  --cn-t-55: rgba(239,231,214,0.56); --cn-t-60: rgba(239,231,214,0.62);
  --cn-t-62: rgba(239,231,214,0.64); --cn-t-68: rgba(239,231,214,0.68);
  --cn-t-70: rgba(239,231,214,0.72); --cn-t-72: rgba(239,231,214,0.74);
  --cn-t-75: rgba(239,231,214,0.78); --cn-t-80: rgba(239,231,214,0.82);
  --cn-t-85: rgba(239,231,214,0.86); --cn-t-90: rgba(239,231,214,0.90);
  --cn-t-92: rgba(239,231,214,0.92); --cn-t-12: rgba(239,231,214,0.12);
}

/* 2b · SIGNAL + DESKTOP TOKENS (signal.html, restore.html, and the shared shell on every page) */
:root{
  --sig-accent: var(--co-amber);
  --bg:            var(--co-chassis);
  --surface:       var(--co-panel);
  --card:          var(--co-panel);
  --card-2:        #171D25;
  --border:        var(--co-edge);
  --border-strong: var(--co-edge-strong);
  --text:          var(--co-ink);
  --text-2:        rgba(239,231,214,0.62);
  --text-3:        rgba(239,231,214,0.40);
  --text-4:        rgba(239,231,214,0.28);
  --lime:          var(--co-amber);
  --lime-ink:      var(--co-amber);
  --lime-wash:     color-mix(in srgb, var(--co-amber) 8%, transparent);
  --lime-line:     color-mix(in srgb, var(--co-amber) 45%, transparent);
}

/* 2c · THE SELF-CONTAINED PAGES.
   m.html, m2.html, microfilm.html, microfilm2.html, autopilot.html, compose.html, lightmap.html,
   offline.html and scope-window.html each declare their own :root. Because this file is linked
   AFTER their inline <style>, re-declaring the same names here re-skins them with no markup edit.
   (--gold is autopilot's accent, --ink* are m2's, --panel-N are the mobile pages'.) */
:root{
  --panel:    var(--co-panel);
  --panel-2:  #171D25;
  --panel-3:  var(--co-well);
  --line:     var(--co-edge);
  --line-2:   var(--co-edge-strong);
  --ink:      var(--co-ink);
  --ink-2:    rgba(239,231,214,0.62);
  --ink-3:    rgba(239,231,214,0.40);
  --accent:   var(--co-amber);
  --gold:     var(--co-amber);
  --danger:   #FF6B5E;
}

/* ══ 3 · THE SHELL ════════════════════════════════════════════════════════════════════════════ */
body{ background:var(--co-chassis); color:var(--co-ink); }

.cn-shell{ background:var(--co-shell); }

/* ── EMULSION ─────────────────────────────────────────────────────────────────────────────────
   A console is a physical object and physical objects are not perfectly smooth. This is the one
   texture in the system: SVG fractal turbulence, desaturated, laid over the whole viewport at 6%.

   It does two jobs. It stops the large flat grounds banding — a 120% / 90% radial running #1A222C
   to #070A0D is exactly the kind of long, shallow ramp an 8-bit display steps through in visible
   rings, and a few percent of noise dithers those away for free. And it gives every surface a
   common grain, which is most of why a set of panels reads as one milled object rather than as
   boxes that happen to share a palette.

   STEPPED, NOT DRIFTING. The frame jumps between three positions and holds; it never tweens.
   Continuous movement reads as video noise crawling over a screen. Discrete jumps read as film —
   each frame is its own piece of emulsion — and that is the difference between "this app has a
   noise overlay" and "this thing was photographed". Three steps at .32s is about 9fps, near
   enough to hand-cranked that the eye stops tracking it and simply accepts the surface as grained.

   Cheap by construction: one fixed, composited layer, transform-only animation, no repaint of
   anything underneath, never hit-testable. */
@keyframes co-emulsion{
  0%,   33%  { transform:translate3d(0, 0, 0); }
  34%,  66%  { transform:translate3d(-7%, 4%, 0); }
  67%, 100%  { transform:translate3d(4%, -6%, 0); }
}
.cn-shell::after{
  content:''; position:fixed; inset:-12%; z-index:9; pointer-events:none;
  opacity:.06; will-change:transform;
  background-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27180%27%20height%3D%27180%27%3E%3Cfilter%20id%3D%27n%27%3E%3CfeTurbulence%20type%3D%27fractalNoise%27%20baseFrequency%3D%270.82%27%20numOctaves%3D%273%27%20stitchTiles%3D%27stitch%27%2F%3E%3CfeColorMatrix%20type%3D%27saturate%27%20values%3D%270%27%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D%27180%27%20height%3D%27180%27%20filter%3D%27url%28%23n%29%27%2F%3E%3C%2Fsvg%3E");
  background-repeat:repeat; background-size:180px 180px;
  animation:co-emulsion .32s steps(1, end) infinite;
}
/* The grain is texture, not information — anyone who has asked for less motion gets the surface
   without the flicker, not a different surface. */
@media (prefers-reduced-motion: reduce){
  .cn-shell::after{ animation:none; }
}

/* The bar is glass, not a panel: no hard seam between it and the work below. */
.cn-bar{
  background:linear-gradient(180deg, rgba(74,90,110,0.16) 0%, rgba(38,48,60,0.10) 48%, rgba(10,14,18,0.16) 100%);
  border-bottom:0;
  box-shadow:inset 0 1px 0 rgba(214,228,244,.07), inset 0 -1px 0 rgba(0,0,0,.5);
}

/* The viewer is the centre of the world: black, borderless, haloed. */
.cn-view, .cn-stage-view, .viewport, .canvas-wrap, .cn-gal-loupe-stage{
  background:var(--co-black) !important;
  border:0 !important;
  box-shadow:0 0 0 1px rgba(239,231,214,0.06),
             0 0 90px -20px rgba(180,206,236,0.30),
             0 30px 80px -40px rgba(0,0,0,0.9);
}

/* ══ 4 · MODULES — quiet faces ════════════════════════════════════════════════════════════════
   One flat surface, one hairline. The open module is marked by a tint bar INSET into its own
   border, so a closed card carries no chrome at all. */
.cn-mod, .card, .panel-category .card{
  border-radius:8px;
  border:0;
  background:var(--co-lift);
  box-shadow:inset 0 0 0 1px var(--co-edge);
  transition:opacity 180ms var(--cn-ease, ease), background 180ms var(--cn-ease, ease);
}
/* al's session, 2026-08-23: renderChainStack() (studio.html ~21571) emits plain `.on`/`.open`
   on .cn-mod — never `.is-on`/`.is-open`/`[data-on]`/`[aria-expanded]`. This was flagged as the
   single most likely thing to need touching (HANDOVER.md §5.1) and it was wrong: without this,
   `.cn-mod:not(.is-on):not(.is-open)` below always matched, so every Chain module rendered
   permanently at the "off" 0.68 opacity regardless of real state — the theme's own headline
   "recede by tone" rule never engaged on the app it was built for. Added the real selectors
   alongside the guessed ones rather than replacing them, in case `.card`'s own is-on/open/
   aria-expanded pattern (print.html's panel-category cards) is real elsewhere in the app. */
.cn-mod.on, .cn-mod.is-on, .card.is-on, .cn-mod[data-on="true"]{ background:var(--co-lift-on); }
.cn-mod.open, .cn-mod.is-open, .card.open, .cn-mod[aria-expanded="true"]{
  background:rgba(214,231,250,0.075);
  box-shadow:inset 0 0 0 1px var(--co-edge-strong),
             inset 3px 0 0 var(--tint, var(--co-amber));
}
/* the old 4px edge bar hanging off every card is folded into the box-shadow above */
.cn-mod-edge{ display:none !important; }

.cn-mod-head, .card-head{ border-bottom:0; background:none; }
.cn-mod.open > .cn-mod-head, .cn-mod.is-open > .cn-mod-head, .card.open > .card-head{ border-bottom:1px solid var(--co-edge-strong); }

/* legends, set like panel silkscreen */
.cn-mod-title, .card-head .name{
  text-transform:uppercase;
  letter-spacing:0.09em;
  font-size:11px;
  text-shadow:0 1px 0 rgba(0,0,0,.5);
}
.cn-mod-step, .row-num{ letter-spacing:0.08em; }

/* recede by tone only — NEVER blur anything carrying text */
.cn-mod:not(.on):not(.open):not(.is-on):not(.is-open){ opacity:0.68; filter:saturate(.6); }
.cn-mod.on:not(.open):not(.is-open), .cn-mod.is-on:not(.is-open){ opacity:0.90; filter:saturate(.92); }
.cn-mod.open, .cn-mod.is-open{ opacity:1; filter:none; }

/* ══ 5 · THE READOUT — a lit display window ═══════════════════════════════════════════════════ */
.cn-hero, .panel-hero, .mono-hero{
  position:relative;
  border-radius:6px;
  border:0;
  overflow:hidden;
  background:linear-gradient(180deg, var(--co-glass), #0A0E14);
  box-shadow:inset 0 2px 6px rgba(0,0,0,.7),
             inset 0 0 0 1px rgba(239,231,214,.07),
             0 1px 0 rgba(255,255,255,.05);
}
.cn-hero::after, .panel-hero::after{
  content:''; position:absolute; inset:0; pointer-events:none; opacity:.35;
  background-image:repeating-linear-gradient(180deg, rgba(0,0,0,.55) 0 1px, transparent 1px 3px);
}
.cn-hero > *, .panel-hero > *{ position:relative; }
.cn-hero-kicker{ color:rgba(232,134,58,0.62); letter-spacing:0.26em; }
.cn-hero-val b{ color:var(--co-amber-hi); text-shadow:0 0 14px rgba(232,134,58,.35); letter-spacing:-0.02em; }
.cn-hero-val span{ color:rgba(246,199,156,0.62); }
.cn-hero-cap{ color:rgba(239,231,214,0.52); }

/* ══ 6 · SWITCHES ═════════════════════════════════════════════════════════════════════════════ */
/* PATCH-modrow.md §2, applied verbatim (oklab color-mix, circular thumb, no separate lamp
   element — the earlier <i class="co-led"> experiment is dropped; legibility off-state now
   comes from the switch's own visible track/thumb contrast instead, per this patch's own
   acceptance criterion "switch off shows an empty track left of the thumb, without hover" —
   verify that actually holds live before trusting it over the lamp). --tint is set inline per
   module by chainModCardHTML, so the amber differs between INPUT and LOOK modules as intended.
   .sw stays split off — a different control (e.g. print.html's panel-category toggles). */
.cn-switch{
  flex:none; width:33px; height:18px; padding:0; border:0; cursor:pointer;
  border-radius:999px; background:rgba(0,0,0,.34);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.10), inset 0 1px 2px rgba(0,0,0,.5);
  transition:background .16s ease, box-shadow .16s ease;
}
.cn-switch > i{
  display:block; width:14px; height:14px; margin:2px; border-radius:50%;
  background:linear-gradient(180deg,#2A323C,#141A22);
  box-shadow:0 1px 2px rgba(0,0,0,.6), inset 0 0 0 1px rgba(255,255,255,.08);
  transition:transform .16s cubic-bezier(.4,0,.2,1), background .16s ease, box-shadow .16s ease;
}
.cn-switch[aria-checked="true"]{
  background:color-mix(in oklab, var(--tint, var(--co-amber)) 34%, rgba(0,0,0,.34));
  box-shadow:inset 0 0 0 1px color-mix(in oklab, var(--tint, var(--co-amber)) 45%, transparent);
}
.cn-switch[aria-checked="true"] > i{
  transform:translateX(15px);
  background:var(--tint, var(--co-amber));
  box-shadow:0 0 8px color-mix(in oklab, var(--tint, var(--co-amber)) 60%, transparent), 0 1px 2px rgba(0,0,0,.5);
}
/* THE EDIT LAMP. al: "this little light needs to be brighter and more hazed, like an orange
   HAL 9000." The right reference, and it names exactly what was missing: HAL's eye is not a
   coloured dot, it is a HOT WHITE CENTRE inside a deep bloom that falls away over a long
   distance, and it is the FALLOFF that sells it as something emitting rather than something
   painted. This was a flat 5px fill with no glow at all.

   Three things build it:
   1. A radial-gradient face, not a flat one — near-white at the core, through amber, to a deep
      red at the rim. The hot core is what makes it read bright without washing the whole dot out.
   2. A STACK of four glows rather than one. A single box-shadow has one falloff curve and always
      resolves as a ring at some radius; four at increasing size and decreasing alpha sum to a
      haze with no edge in it, which is what "hazed" actually means.
   3. An inset highlight, so the glass over the lamp catches the room — the same trick the switch
      knob uses, and the reason the lamp reads as set INTO the panel rather than stuck on it.

   --co-lamp rather than --tint: the palette reserves that colour for LEDs, and this is an LED.
   The module's chain tint still carries its position on the card rim and the switch, which is
   where R9 puts it — a lamp is the colour a lamp is. 6px, up from 5: at 5 the haze had nothing
   to come out of. */
.cn-mod-dirty{
  flex:none; width:6px; height:6px; border-radius:50%; margin-right:3px;
  background:radial-gradient(circle at 50% 42%,
    #FFF2E0 0%, #FFD9A0 20%, var(--co-lamp) 52%, #E8481A 100%);
  box-shadow:
    0 0 5px  0px color-mix(in srgb, #FF7A3C 85%, transparent),
    0 0 10px 1px color-mix(in srgb, #FF5B22 60%, transparent),
    0 0 17px 2px color-mix(in srgb, #FF4A14 32%, transparent),
    0 0 24px 4px color-mix(in srgb, #F0380A 16%, transparent),
    inset 0 0 2px rgba(255,242,224,.9),
    inset 0 1px 1px rgba(255,255,255,.35);
  opacity:0; transition:opacity .16s ease;
}
.cn-mod.has-edits .cn-mod-dirty{ opacity:1; } /* spec said .is-dirty — real class is has-edits */
/* UNPOWERED. al: "the on/off light needs lesser glowy off state."
   A module that is switched OFF but still carries edits was showing the lamp at full brightness,
   which says "this is live" about something that is doing nothing to the picture — and on a card
   already receding by tone it was the loudest thing on it. An LED on an unpowered module does not
   glow; it is a dark ember with the room reflecting off its dome, and you can still see it is
   there. So the edits stay legible and the state stops lying.
   The face keeps the same construction, just banked right down: a dull core, no outward haze at
   all, and only the faint inset that says there is glass over it. */
.cn-mod:not(.on).has-edits .cn-mod-dirty{
  background:radial-gradient(circle at 50% 42%,
    #6E432A 0%, #5A3520 30%, #472817 60%, #34190D 100%);
  box-shadow:inset 0 0 2px rgba(255,242,224,.22), inset 0 1px 1px rgba(255,255,255,.10);
  opacity:.85;
}

/* LEFTPANEL.md §5: reset moves out of the row, into a plain text row at the foot of the open
   module's body — same verb (chainResetModule via data-cn-reset), new home. */
.cn-mod-resetfoot{
  display:flex; align-items:center; gap:7px; width:100%; margin-top:14px; padding:9px 4px 2px;
  border-top:1px solid var(--co-edge-2); background:none; border-left:0; border-right:0; border-bottom:0;
  cursor:pointer; font-family:inherit; color:rgba(239,231,214,.5);
}
.cn-mod-resetfoot:hover{ color:var(--co-amber); }
.cn-mod-resetfoot svg{ width:13px; height:13px; flex:none; fill:none; stroke:currentColor; stroke-width:1.4; }
.cn-mod-resetfoot span{ font-size:9px; letter-spacing:.16em; }

.sw{
  width:30px; height:17px; border-radius:4px;
  background:rgba(0,0,0,.34) !important;
  box-shadow:inset 0 1px 3px rgba(0,0,0,.6);
}
.sw i, .sw::after{
  width:13px; height:13px; border-radius:3px; top:2px;
  background:linear-gradient(180deg, rgba(239,231,214,0.42), rgba(239,231,214,0.20));
  box-shadow:0 1px 2px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.35);
}
.sw.on i, .sw[data-on="true"]::after{
  transform:translateX(13px);
  background:linear-gradient(180deg, #F7CFA6, var(--tint, var(--co-amber)));
}

/* THE LAMP — add <i class="co-led"> before a switch, or let the state class light it.
   al: "this little light needs to be brighter and more hazed, like an orange HAL 9000."
   Right reference, and it tells you exactly what was missing. HAL's eye is not a coloured dot:
   it is a HOT WHITE CENTRE inside a deep red bloom that falls away over a long distance, and it
   is the falloff that sells it as something emitting rather than something painted.

   Three things build that here:
   1. A radial-gradient FACE rather than a flat fill, so the lamp has a small near-white core at
      55% opacity of its own width, then amber, then deep lamp-red at the rim. That core is what
      makes it read as bright without making the whole dot pale.
   2. A STACK of glows rather than one. A single box-shadow has one falloff curve and always reads
      as a ring at some radius; four at increasing size and decreasing alpha (5px -> 22px) sum to
      a smooth haze with no edge to it, which is what "hazed" actually means.
   3. An inset highlight top-left, so the glass over the lamp catches the room light — the same
      trick the switch knob uses, and the reason the lamp sits IN the panel rather than on it.
   Off is unchanged: a dead lamp is a dark recess with no glow at all, and the contrast between
   the two states is most of what makes the lit one read as lit. */
.co-led{
  width:7px; height:7px; border-radius:50%; flex:none; display:block;
  background:var(--co-lamp-off);
  box-shadow:inset 0 1px 2px rgba(0,0,0,.75);
  transition:background .16s ease, box-shadow .16s ease;
}
.is-on > .co-led, .co-led.on, .cn-mod.on .co-led{
  background:radial-gradient(circle at 50% 42%,
    #FFF2E0 0%, #FFD9A0 22%, var(--co-lamp) 55%, #E8481A 100%);
  box-shadow:
    0 0 5px  0px color-mix(in srgb, #FF7A3C 85%, transparent),
    0 0 10px 1px color-mix(in srgb, #FF5B22 62%, transparent),
    0 0 16px 2px color-mix(in srgb, #FF4A14 34%, transparent),
    0 0 22px 4px color-mix(in srgb, #F0380A 18%, transparent),
    inset 0 0 2px rgba(255,242,224,.9),
    inset 0 1px 1px rgba(255,255,255,.35);
}

/* ══ 7 · SILKSCREEN LABELS ════════════════════════════════════════════════════════════════════ */
.cn-section-label, .cn-sec-head span, .field-label, .subhead,
.templates-label, .panel-category-label, .report-panel-head{
  font-size:8.5px;
  letter-spacing:0.22em;
  text-transform:uppercase;
  color:rgba(239,231,214,0.46);
}
.cn-section-label, .cn-collapsible{
  margin-top:18px; padding-top:10px;
  border-top:1px solid rgba(239,231,214,0.09);
  box-shadow:none;
}

/* ══ 8 · STAGE RAIL — anodised tiles, one lit ═════════════════════════════════════════════════ */
.cn-stage{
  border-radius:8px;
  border:0;
  background:rgba(210,226,244,0.045);
  box-shadow:inset 0 0 0 1px rgba(239,231,214,0.045);
  /* An UNLIT tile needs pale ink. The rule below sets the four text rows to color:inherit, which
     overrode chain.css's per-tile --stage-ink — and only the selected tile went on to declare a
     colour, so the other four inherited black and rendered near-black-on-near-black. The rail read
     as four empty boxes with one lit one. chain.css's own comment records this exact failure being
     fixed once already ("you cannot see the titles and you cannot see the icons"); the theme layer
     landed on top and undid it. Declared here so the pair is complete: unlit is cream, lit is ink. */
  color:rgba(239,231,214,0.70);
  opacity:0.74;
  filter:saturate(.65);
  transition:opacity 180ms var(--cn-ease, ease);
}
.cn-stage[aria-selected="true"], .cn-stage.is-active{
  opacity:1; filter:none;
  background:linear-gradient(180deg, #F0A055, #D9762F 55%, #B85C22);
  box-shadow:inset 0 0 0 1px rgba(255,214,170,.35),
             0 0 22px -4px color-mix(in srgb, var(--co-lamp) 65%, transparent);
  color:#0D1116;
}
.cn-stage-title, .cn-stage-label, .cn-stage-num, .cn-stage-count{ color:inherit; }

/* ══ 9 · RECESSED BAYS — any scrolling list of options ════════════════════════════════════════
   Long lists (film stocks, look sheets, mask lists, gallery filters) sit in a bay so they read as
   one unit instead of loose rows. Add .co-bay to the scroll container. */
.co-bay{
  position:relative;
  border-radius:8px;
  overflow:hidden;
  background:#080C11;
  box-shadow:inset 0 2px 8px rgba(0,0,0,.6), inset 0 0 0 1px rgba(239,231,214,0.06);
}
.co-bay::before{
  content:''; position:absolute; inset:0; pointer-events:none; opacity:.5;
  background-image:repeating-linear-gradient(180deg, rgba(239,231,214,.028) 0 1px, transparent 1px 25px);
}
.co-bay > *{ position:relative; max-height:184px; overflow-y:auto; padding:5px 5px 6px;
  display:flex; flex-direction:column; gap:1px; }
.co-bay-row{
  display:flex; align-items:center; gap:10px;
  width:100%; height:25px; flex:none; padding:0 9px;
  border:0; border-radius:999px; background:transparent; cursor:pointer;
  font:inherit; font-size:10px; letter-spacing:0.03em;
  color:rgba(239,231,214,0.62);
}
.co-bay-row:hover{ background:rgba(239,231,214,0.05); }
.co-bay-row[aria-selected="true"], .co-bay-row.on{
  color:var(--co-amber-hi);
  background:linear-gradient(180deg,
    color-mix(in srgb, var(--co-amber) 22%, transparent),
    color-mix(in srgb, var(--co-amber) 10%, transparent));
  box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--co-amber) 50%, transparent), 0 1px 0 rgba(0,0,0,.4);
}

/* ══ 10 · CONTROLS ════════════════════════════════════════════════════════════════════════════ */
.cn-chip, .chip, .cn-brush-tab{
  border-radius:999px;
  border:1px solid rgba(239,231,214,0.12);
  background:rgba(239,231,214,0.05);
  color:rgba(239,231,214,0.66);
}
.cn-chip[aria-pressed="true"], .chip.on, .cn-chip.on, .cn-brush-tab.on{
  color:var(--co-amber-hi);
  border-color:color-mix(in srgb, var(--co-amber) 44%, transparent);
  background:color-mix(in srgb, var(--co-amber) 18%, transparent);
}
.cn-text-input, .txt, .select{
  border-radius:8px;
  border:1px solid rgba(239,231,214,0.12);
  background:rgba(239,231,214,0.05);
}
/* PICKER — the design puts the LABEL OUTSIDE the control and makes the VALUE the pill:
       Intent   [ Straight · keep 65%                    ⌄ ]
   chain.css builds it the other way round: one pill wrapping label + ‹ + value + ›. So the
   wrapper is stripped back to a bare flex row here and .cn-picker-val-btn becomes the pill.
   The ‹ › step arrows are hidden rather than deleted: they're real buttons with real delegated
   handlers (data-cn-pick="-1"/"1"), and the value button beside them (data-cn-pick-open) opens
   a menu that reaches every option directly — so hiding them removes a redundant path, not the
   only path. Left in the DOM deliberately: deleting them is a markup change in studio.html for
   no visual gain, and the menu is the affordance the design actually shows. */
.cn-picker{
  background:none; border:0; padding:0; height:auto; gap:10px;
}
.cn-picker-label{
  font-size:11.5px; letter-spacing:0; text-transform:none;
  color:rgba(239,231,214,0.72); flex:none;
}
.cn-picker-arrow{ display:none; }
.cn-picker-val-btn{
  flex:1; min-width:0; max-width:none; text-align:left;
  height:28px; padding:0 22px 0 8px; border-radius:8px;
  font-size:11px; color:var(--co-ink);
  border:1px solid rgba(239,231,214,0.14);
  background:rgba(239,231,214,0.06);
  position:relative;
}
.cn-picker-val-btn:hover{
  background:rgba(239,231,214,0.06);
  border-color:color-mix(in srgb, var(--co-amber) 40%, transparent);
}
/* The chevron. NOTE: the prototype's picker is a native <select>, so this glyph is the
   BROWSER's there and is not authored anywhere in the design source — every number below is
   app-side geometry chosen to sit correctly in the 28px pill, not an extracted value. */
.cn-picker-val-btn::after{
  content:''; position:absolute; right:9px; top:50%; width:6px; height:6px;
  margin-top:-4.5px; border-right:1.4px solid rgba(239,231,214,.5);
  border-bottom:1.4px solid rgba(239,231,214,.5);
  transform:rotate(45deg); pointer-events:none;
}
/* a list of more than five options is a dropdown, not a field of pills */
.cn-picker-menu{
  border-radius:8px;
  border:1px solid var(--co-edge-strong);
  background:color-mix(in srgb, var(--co-well) 92%, #16202A 8%);
  box-shadow:0 18px 40px -18px rgba(0,0,0,.85);
}
.cn-action, .dz-btn, .foot-cta-btn{
  border-radius:8px;
  border:1px solid rgba(239,231,214,0.14);
  background:rgba(239,231,214,0.06);
  color:var(--co-ink);
}
/* Inside the Advanced drawer the design shows action buttons as full-width rows with the label
   LEFT-aligned, sized by PADDING rather than a fixed height — chain.css centres them and floors
   them at 38px. Scoped to .cn-advanced-body so this doesn't restyle action buttons elsewhere in
   the app (instrument rows use their own .cn-inst-action). `min-height:0` is load-bearing:
   without it chain.css's 38px floor still governs and the padding never gets to set the height.
   .cn-action.done already renders the amber outline + amber tint the design shows on "Take a
   reading" once a reading exists, so that state needs nothing here. */
.cn-advanced-body .cn-action{
  min-height:0; padding:7px 11px; border-radius:8px;
  justify-content:flex-start; font-size:11px; letter-spacing:0.04em;
}
/* chain.css styles this at three classes, so the rule above cannot reach it — without this the
   paired icon button ends up a different height and radius from the button it pairs with. */
.cn-advanced-body .cn-action-split > .cn-action.icon-only{
  border-radius:8px; min-height:0;
}
.cn-action:hover, .dz-btn:hover, .foot-cta-btn:hover{
  border-color:color-mix(in srgb, var(--co-amber) 44%, transparent);
  background:color-mix(in srgb, var(--co-amber) 14%, transparent);
}
/* Cascade fix: the 2-class amber :hover above sits in a LATER file than chain.css's
   `.cn-action.primary{ background:var(--cn-amber) }`, so hovering a primary (filled amber)
   button replaced its fill with a 14% wash while --cn-ink-on-amber (#0D1116) stayed on the
   text — near-black ink on a near-black panel, i.e. the label vanished on hover. */
.cn-action.primary:hover{
  background:var(--cn-amber); border-color:transparent;
}
.cn-cta{
  background:var(--co-amber);
  color:#1A0D03;
  box-shadow:0 0 22px -8px color-mix(in srgb, var(--co-lamp) 70%, transparent);
}
.cn-checkbox{ border-radius:5px; border:1px solid rgba(239,231,214,0.28); }
.cn-checkbox[aria-checked="true"]{ background:var(--co-amber); border-color:var(--co-amber); }
.cn-swatch{ border-radius:8px; box-shadow:inset 0 0 0 1px rgba(239,231,214,.18); }
.cn-swatch[aria-pressed="true"]{ box-shadow:inset 0 0 0 2px var(--co-ink); }

/* Tick strips keep their instrument feel but sit on the darker ground. Only the tick COLOUR is
   re-pointed here — chain.css paints each tick as a 1px hairline inside a wider cell (a
   left-to-right gradient that's transparent either side of the centre line), and overriding
   `background` outright replaced that gradient with a solid fill, so every fine graduation
   became a chunky bar and the whole ruler read as a segmented block instead of a scale. Set
   --tick instead: chain.css's own gradient reads it, so the hairline geometry survives. */
.cn-ticks--slider span:not(.cn-marker){ --tick: rgba(239,231,214,.24); }
.cn-marker{ background:var(--co-amber); box-shadow:0 0 14px 2px color-mix(in srgb, var(--co-amber) 50%, transparent); }
.cn-marker i{ border-top-color:var(--co-amber); }
/* The value reads as plain type on the label line — no box, no fill, no border — and the
   -/+ steppers go, matching the reference. The value stays click-to-type (chain.css's own
   .cn-tool-val-input swaps in on click and keeps its own box while editing, which is the one
   moment the affordance actually needs to show), and the steppers' job is still covered by
   dragging the ruler itself. */
.cn-tool-val{
  border:0 !important; background:none !important; padding:2px 0 !important;
  font-size:15px; color:var(--co-ink);
}
.cn-tool-val:hover{ border:0 !important; color:var(--co-amber-hi); }
.cn-step{ display:none !important; }

/* Advanced-drawer type scale. The design's whole drawer sits at ~11.5px, NOT the 15px used
   above — the reference screenshots are ~2x zoomed (a 752px image of the 366px column), which
   is exactly the trap that made several earlier passes read as "close but too big". Label and
   value share the size; the 0.72-vs-full-ink alpha gap is what separates them, not weight. */
.cn-advanced-body .cn-tool-val{ font-size:11.5px; color:#EFE7D6; min-width:0; text-align:left; }
/* chain.css states click-to-type must swap in place with no layout shift — the input has to
   track the span's size or that contract breaks the moment you click a value. */
.cn-advanced-body .cn-tool-val-input{ font-size:11.5px; padding:2px 0; text-align:left; width:46px; }
.cn-advanced-body .cn-tool-name{
  display:block; font-size:11.5px; color:rgba(239,231,214,0.72);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.cn-advanced-body .cn-tool-name .cn-hue-dot{ vertical-align:middle; margin-right:6px; }
/* Unit: the design's own alpha here measures ~3.4:1, and chain.css carries an explicit note
   that someone already raised this token BECAUSE small type at that contrast failed AA. Taking
   the design value verbatim would knowingly re-break that fix, so this ships the design's SIZE
   at the app's CONTRAST. min-width (not the old fixed width) because manifest.js ships units as
   long as 'µm/px' and ' stops', which overflow a fixed box. */
.cn-advanced-body .cn-tool-unit{
  font-size:10px; color:var(--cn-t-65, rgba(238,242,241,.65)); width:auto; min-width:22px;
}
/* Three sizes on one line (11.5 / 11.5 / 10) — centring floats the unit inside its line box;
   baseline is what makes the row read as a single printed line. */
.cn-advanced-body .cn-tool-head:has(.cn-tool-name){ align-items:baseline; gap:8px; }
.cn-advanced-body .cn-tool-head:has(.cn-tool-name) .cn-ctlreset,
.cn-advanced-body .cn-tool-head:has(.cn-tool-name) .cn-step{ align-self:center; }

/* ══ 11 · GALLERY ═════════════════════════════════════════════════════════════════════════════ */
.cn-gal-well{ background:transparent; }
.cn-gal-card{
  border-radius:8px;
  border:0;
  background:var(--co-lift);
  box-shadow:inset 0 0 0 1px var(--co-edge);
  transition:box-shadow 180ms var(--cn-ease, ease), opacity 180ms var(--cn-ease, ease);
}
.cn-gal-card:hover{ box-shadow:inset 0 0 0 1px var(--co-edge-strong); }
.cn-gal-card[aria-selected="true"], .cn-gal-card.picked{
  box-shadow:inset 0 0 0 1px var(--co-amber), 0 0 22px -10px color-mix(in srgb, var(--co-lamp) 70%, transparent);
}
.cn-gal-card.rejected{ opacity:0.5; filter:saturate(.5); }   /* tone, not blur */
.cn-gal-thumb{ background:var(--co-black); }
.cn-gal-mark, .cn-gal-reject, .cn-gal-remove, .cn-gal-loupebtn, .cn-gal-icon{
  border-radius:50%;
  background:rgba(11,15,20,.72);
  box-shadow:inset 0 0 0 1px rgba(239,231,214,.14);
}
.cn-gal-star[aria-pressed="true"], .cn-gal-star.on{ color:var(--co-amber); }
.cn-gal-loupe{ background:color-mix(in srgb, var(--co-black) 92%, transparent); }
.cn-gal-modalbox{
  border-radius:8px;
  border:1px solid var(--co-edge-strong);
  background:var(--co-panel);
  box-shadow:0 30px 80px -40px rgba(0,0,0,.9);
}

/* ══ 12 · PRINT + MONO panels ═════════════════════════════════════════════════════════════════ */
.panel, .report-panel{
  border-radius:8px;
  border:0;
  background:var(--co-lift);
  box-shadow:inset 0 0 0 1px var(--co-edge);
}
.panel-category-header{ border-bottom:1px solid rgba(239,231,214,0.09); }
.dropzone{
  border-radius:8px;
  border:1px dashed rgba(239,231,214,0.14);
  background:color-mix(in srgb, var(--co-well) 82%, transparent);
}
.spot, .cn-status i{ background:var(--co-lamp); box-shadow:0 0 8px 1px color-mix(in srgb, var(--co-lamp) 60%, transparent); }

/* ══ 13 · MOBILE (m.html, m2.html) ════════════════════════════════════════════════════════════ */
.sheet, .tabbar, .statusbar, .pill, .action{
  background:var(--co-panel);
  border-color:var(--co-edge);
  color:var(--co-ink);
}
.tabbar .tab[aria-selected="true"], .pill.on, .action.primary{
  color:var(--co-amber);
  border-color:color-mix(in srgb, var(--co-amber) 45%, transparent);
  background:color-mix(in srgb, var(--co-amber) 14%, transparent);
}

/* ══ 14 · LINKS + FOCUS ═══════════════════════════════════════════════════════════════════════ */
a{ color:var(--co-amber); text-decoration:none; }
a:hover{ color:#F5A44E; }
:focus-visible{ outline:2px solid rgba(232,134,58,0.6); outline-offset:2px; }

::-webkit-scrollbar{ width:6px; height:6px; }
::-webkit-scrollbar-thumb{ background:rgba(239,231,214,0.14); border-radius:3px; }
::-webkit-scrollbar-track{ background:transparent; }

/* ══ 15 · MOTION ══════════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce){
  *, *::before, *::after{ transition-duration:0.01ms !important; animation-duration:0.01ms !important; }
}

/* ══ 16 · INSTRUMENTS ═════════════════════════════════════════════════════════════════════════
   Per-module bespoke controls (INSTRUMENTS.md) — the first is Halation (#21). Shared instrument
   chrome (.cn-inst) lives here; anything specific to one instrument is prefixed .cn-inst-<name>.
   .cn-section-label (chain.css) is reused verbatim for the silkscreen headers inside — no new
   label style invented for instruments, matching rule "the app has a token for it." */
.cn-inst{ margin-top:2px; }

/* Display window — the first thing in every open module's body, confirmed against the real
   Chain Instruments.dc.html bundle (not just INSTRUMENTS.md's prose): label, one big value,
   one line of context underneath. Same glass/window language .cn-hero already established
   elsewhere in this file, reused rather than a third "value display" pattern invented. */
/* al (1): "a little bit of breathing room at the top of these windows — they are very tight to
   the top". .cn-mod-body has padding-top 0, so the display window sat flush against the module
   header with a measured gap of exactly 0px. The room is given here rather than on the body, so
   only the window moves and every other first-child (widgets that open with a section label)
   keeps its own spacing. */
.cn-mod-body > .cn-inst > .cn-dispwin:first-child{ margin-top:10px; }
/* Measured off the design bundle's own computed styles, which this had drifted about 6% above at
   every step — a uniform scale-up nobody chose, which is why the window read slightly coarse next
   to the reference. Reference values: window padding 12/14/11 and a third, outer shadow lifting
   the top edge; label 8px at 2.08px tracking on amber at .62; value 200-weight 30px with -0.6px
   tracking; unit 11.5px at .69px on #F6C79C at .62; context 10.5px/14.175px on cream at .52 with
   NO tracking, because it is the one line of prose in the design. */
.cn-dispwin{
  position:relative; border-radius:6px; overflow:hidden; padding:12px 14px 11px;
  background:linear-gradient(180deg, var(--co-glass), #0A0E14);
  box-shadow:inset 0 2px 6px rgba(0,0,0,.7), inset 0 0 0 1px rgba(239,231,214,.07),
             0 1px 0 rgba(255,255,255,.05);
}
.cn-dispwin-label{ font-size:8px; letter-spacing:.26em; text-transform:uppercase; color:rgba(232,134,58,.62); }
/* al (2): "there is an overlap on this text making it hard to read". The boxes were not actually
   intersecting — measured at 2px clear — but 2px between a 30px number carrying a 14px glow and
   a 10.5px caption is not separation, it is a collision the glow finishes off. line-height pinned
   so the box tracks the glyphs rather than the browser's default leading.
   tabular-nums because this is the largest figure on the panel and it changes on every drag:
   proportional digits re-measure the string on each tick, so the number jitters horizontally
   under your own finger. The single most-seen instance of that fault in the app — it is on all 28. */
.cn-dispwin-value{ font-size:30px; line-height:1.05; font-weight:200; color:var(--co-amber-hi);
  text-shadow:0 0 14px rgba(232,134,58,.35); letter-spacing:-.02em; margin-top:5px;
  font-variant-numeric:tabular-nums; }
.cn-dispwin-value span{ font-size:11.5px; font-weight:400; letter-spacing:.06em;
  color:rgba(246,199,156,.62); margin-left:4px; }
.cn-dispwin-context{ font-size:10.5px; line-height:1.35; color:rgba(239,231,214,.52); margin-top:9px; }

/* Advanced drawer — the collapsed foot of every module's body, holding the full manifest
   control set (the generic slider/picker/chips/check/colour/action renderer, chainControlsHTML
   — unchanged, just no longer always-visible) plus the reset row moved here from the module
   row (LEFTPANEL.md §5). */
/* Names the token directly rather than the --co-edge-2 alias, so this hairline survives even if
   that alias is ever removed again. */
.cn-advanced{ margin-top:20px; border-top:1px solid var(--co-edge-strong); }
.cn-advanced-head{
  display:flex; align-items:center; justify-content:space-between; width:100%;
  padding:12px 2px 6px; background:none; border:0; cursor:pointer; font-family:inherit;
  font-size:9px; letter-spacing:.20em; color:rgba(239,231,214,.55);
}
.cn-advanced-head:hover{ color:var(--co-ink); }
/* The header never brightened when the drawer opened — an open disclosure should read as the
   active one. (The open colour IS in the design source; the hover colour is not, and is kept
   because removing hover feedback from a button would be a regression, not a fidelity gain.) */
.cn-advanced.open .cn-advanced-head{ color:var(--co-ink); }
.cn-advanced-chev{ width:12px; height:12px; flex:none; fill:none; stroke:currentColor; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; transition:transform .16s ease; }
.cn-advanced.open .cn-advanced-chev{ transform:rotate(180deg); }
/* 0, not 4px: lets the first row's own top margin collapse out to the body edge. Combined with
   the header's 6px bottom padding and the 8px row margin below, that lands the design's exact
   14px from header text to first control. */
.cn-advanced-body{ padding-top:0; }
/* Ruler geometry — the single largest reason rows were taller than the design. Drag-safe: the
   hit-test reads only the strip's left edge and width, never its height. */
.cn-advanced-body .cn-ticks--slider{ height:26px; margin-top:10px; }
/* Tick ladder, shifted down to fit the 26px track. SOURCE ORDER IS LOAD-BEARING: `.home.on` and
   `:hover .home` tie at equal specificity, so the hover states must come last — same ordering
   chain.css itself relies on. Only the fine (12px) and major (19px) heights come from the
   design; the .on/.home states are the app's own, moved by the same -2px so the ladder stays
   internally consistent. */
.cn-advanced-body .cn-ticks--slider span:not(.cn-marker){ height:12px; }
.cn-advanced-body .cn-ticks--slider span:not(.cn-marker):nth-child(12n+1){ height:19px; }
.cn-advanced-body .cn-ticks--slider span:not(.cn-marker).on{ height:15px; }
.cn-advanced-body .cn-ticks--slider span:not(.cn-marker):nth-child(12n+1).on{ height:21px; }
.cn-advanced-body .cn-ticks--slider span:not(.cn-marker).home{ height:23px; }
.cn-advanced-body .cn-ticks--slider span:not(.cn-marker).home.on{ height:24px; }
.cn-advanced-body .cn-ticks--slider:hover span:not(.cn-marker){ height:14px; }
.cn-advanced-body .cn-ticks--slider:hover span:not(.cn-marker):nth-child(12n+1){ height:21px; }
.cn-advanced-body .cn-ticks--slider:hover span:not(.cn-marker).home{ height:25px; }
/* Vertical rhythm. .cn-action-split must be restated here: chain.css sets it at equal
   specificity from an earlier file, so without this the paired action row picks up the 8px and
   breaks — the exact bug chain.css's own comment documents having fixed once already. */
.cn-advanced-body .cn-section-label{
  margin-top:12px; padding-top:10px; border-top:1px solid var(--co-edge-strong);
  font-size:9px; letter-spacing:.20em; color:rgba(239,231,214,.55);
}
.cn-advanced-body .cn-section-label:first-child{ margin-top:8px; padding-top:0; border-top:0; }
.cn-advanced-body .cn-tool{ margin-top:8px; }
.cn-advanced-body .cn-action-split > .cn-tool{ margin-top:0; }
/* Field-path caption under every Advanced row — "adjustments.exposure", "action · take" — the
   real manifest dataP/action this control writes, per al's screenshot of the real prototype.
   Sits right under the row it belongs to, before the next one starts. */
/* Monospace is a deliberate app decision, not a design-source value — this is the one surface
   meant to read as source rather than product (the drawer is literally headed MANIFEST.JS).
   The bottom margin goes: it already collapsed against the next row, and dropping it lets the
   row-rhythm rule above govern spacing in one place instead of two. */
/* .cn-fieldpath is gone — see chainToolRowHTML. It was the only monospace in this design, and
   it existed to serve a drawer that used to brand itself MANIFEST.JS. */

/* ONE origin for every part of the lamp. The bloom, the dot and the dashed ring were pinned to
   left:64px while the drag arrow was positioned from calc(50% + r) — i.e. from the middle of the
   box, ~104px away from the lamp it was supposed to be attached to. That is the "disconnected"
   al saw: an arrow floating in empty space to the right of a lamp it had no visible relationship
   to. Everything now hangs off --lamp-x / --lamp-y, so the parts cannot drift apart again. */
.cn-inst-lamp{
  /* CENTRED. al (19): "the lamp needs to be more centred."
     It sat at 31% across — a workaround from an earlier round, to give the radius arm somewhere
     to grow without running off the right edge. That was solving the wrong problem: the ring is
     drawn CENTRED on the lamp, so it already grows both ways, and the arm at its longest is 70px
     against a half-width of about 165px. There was always room. A light source in a well belongs
     in the middle of it — anywhere else and the well reads as a crop of a bigger picture rather
     than as the housing the lamp sits in. Taller too: the bloom at full spread is 138px across
     and needed the room. */
  --lamp-x:50%; --lamp-y:50%;
  position:relative; margin-top:12px; height:186px; border-radius:8px;
  cursor:move; touch-action:none;
  background:var(--co-glass); border:1px solid var(--co-edge-2); overflow:hidden;
  cursor:ew-resize; touch-action:none;
}
/* Diffusion's lamp is a PLANE, not a line — across is spread, up is strength — so it takes the
   move cursor rather than .cn-inst-lamp's ew-resize. Its ground is painted inline from crush and
   opacity: the white-mist family lifts the whole frame, and the well lifts with it. */
.cn-difflamp{ cursor:move; }
/* 07 Clone & Heal — the brush well. al (10): "Clone and heal tool visual is weak - at a min we
   need to have at least a brush size changer - something like halation." Same geometry as
   .cn-inst-lamp, because it is the same kind of instrument: a soft-edged thing whose size and
   hardness belong under your hand. Full width matters for more than looks — wireChainInstDrags
   spreads the control's whole range across the handle's own box, so at 96px the brush's 19 units
   of travel were unplaceable. */
.cn-brush-preview{ position:relative; margin-top:12px; height:186px; border-radius:8px;
  background:var(--co-glass); border:1px solid var(--co-edge-2); overflow:hidden;
  cursor:ew-resize; touch-action:none; }
.cn-brush-preview svg{ position:absolute; inset:0; width:100%; height:100%; display:block; }
.cn-brush-preview .cap{ font-family:inherit; font-weight:400; font-size:8px; letter-spacing:.10em; }
.cn-inst-bloom{
  position:absolute; left:var(--lamp-x); top:var(--lamp-y); border-radius:50%;
  transform:translate(-50%,-50%); pointer-events:none;
}
.cn-inst-lamp-dot{
  position:absolute; left:var(--lamp-x); top:var(--lamp-y); width:12px; height:12px; margin:-6px 0 0 -6px;
  border-radius:50%; background:#fff; pointer-events:none; z-index:2;
}
/* The radius ring is a FUNCTIONAL guide — it is the spread you are setting, drawn — so it cannot
   sit at the hairline alpha the panel uses for decorative edges. At --co-edge-strong (0.10 on
   cream) it was invisible against the bloom it is meant to be measuring, which left the drag
   handle looking like a mark floating in space. Amber, because it belongs to the gesture. */
.cn-inst-lamp-ring{
  position:absolute; left:var(--lamp-x); top:var(--lamp-y);
  border-radius:50%; pointer-events:none;
  border:1px dashed color-mix(in srgb, var(--co-amber) 42%, transparent);
  transform:translate(-50%,-50%);
}
/* The leader: a dashed line from the lamp out to the handle, so the handle reads as the END of a
   radius rather than a free-floating mark. Width is set inline from the same armR the ring uses. */
.cn-inst-lamp-leader{
  position:absolute; top:var(--lamp-y); left:var(--lamp-x); height:0;
  border-top:1px dashed color-mix(in srgb, var(--co-amber) 55%, transparent);
  pointer-events:none;
}
.cn-inst-lamp-arrow{
  position:absolute; top:var(--lamp-y); width:2px; height:20px; margin-top:-30px;
  background:var(--co-amber); pointer-events:none;
  box-shadow:0 0 12px 1px color-mix(in srgb, var(--co-amber) 45%, transparent);
}
/* The grab dot where the leader meets the rim — the thing you are actually dragging. */
.cn-inst-lamp-arrow::after{
  content:''; position:absolute; left:50%; top:20px; width:9px; height:9px; margin-left:-4.5px;
  border-radius:50%; background:var(--co-amber);
}
.cn-inst-lamp-arrow::before{
  content:''; position:absolute; top:-6px; left:50%; transform:translateX(-50%);
  border-left:5px solid transparent; border-right:5px solid transparent; border-bottom:6px solid var(--co-amber);
}
.cn-inst-lamp-readout{
  position:absolute; right:12px; bottom:10px; font-size:15px; font-weight:300; color:var(--co-amber-hi);
  text-shadow:0 0 14px rgba(232,134,58,.35);
}
.cn-inst-lamp-readout span{ font-size:10px; color:rgba(246,199,156,.62); margin-left:2px; }
.cn-inst-ramp{
  position:relative; margin-top:12px; height:26px; border-radius:6px; overflow:hidden;
  background:linear-gradient(90deg,#000,var(--co-ink)); cursor:ew-resize; touch-action:none;
}
.cn-inst-ramp-fill{ position:absolute; top:0; bottom:0; left:0; right:0; background:color-mix(in srgb, var(--co-amber) 32%, transparent); }
.cn-inst-ramp-cut{
  position:absolute; top:-7px; bottom:-1px; width:2px; margin-left:-1px; border-radius:1px;
  background:var(--co-amber); box-shadow:0 0 14px 2px color-mix(in srgb, var(--co-amber) 50%, transparent);
}
.cn-inst-swatches{ margin-top:12px; display:grid; grid-template-columns:repeat(5,1fr); gap:8px; }
.cn-inst-swatch{
  display:flex; flex-direction:column; align-items:center; gap:7px;
  background:none; border:0; padding:0; cursor:pointer; font-family:inherit;
}
.cn-inst-swatch .dot{ width:30px; height:30px; border-radius:50%; display:block; }
.cn-inst-swatch .lbl{ font-size:8px; letter-spacing:.08em; }

/* Instrument redesign — #01 Exposure Baseline's meter dial. */
.cn-inst-dialwrap{
  position:relative; margin-top:12px; height:118px; border-radius:8px;
  background:var(--co-glass); border:1px solid var(--co-edge-2);
  cursor:ew-resize; touch-action:none; overflow:hidden;
}
.cn-inst-dial-svg{ position:absolute; inset:0; width:100%; height:100%; pointer-events:none; }
.cn-inst-dial-readout{
  position:absolute; left:50%; bottom:10px; transform:translateX(-50%);
  font-size:19px; font-weight:300; color:var(--co-amber-hi); text-shadow:0 0 14px rgba(232,134,58,.35);
  pointer-events:none;
}
.cn-inst-dial-readout span{ font-size:11px; color:rgba(246,199,156,.62); margin-left:2px; }
.cn-inst-dial-footer{
  margin-top:8px; font-size:9px; letter-spacing:.14em; text-align:center; color:rgba(239,231,214,.42);
}
.cn-inst-dial-end{
  position:absolute; bottom:10px; font-size:11px; color:rgba(239,231,214,.4); pointer-events:none;
}
.cn-inst-dial-end.lo{ left:14px; } .cn-inst-dial-end.hi{ right:14px; }

/* Shared action row — "Measure the frame"/"Reset", "+ Add point"/"Clear": a primary amber
   button paired with a plain secondary one, same idiom console.css's .cn-cta already uses for
   the one true "go" action elsewhere, just sized for an instrument's own row instead of the bar. */
.cn-inst-actionrow{ display:flex; gap:10px; margin-top:14px; }
.cn-inst-action{
  flex:1; height:48px; border-radius:8px; border:1px solid var(--co-edge-2); background:none;
  cursor:pointer; font-family:inherit; font-size:14px; font-weight:500; color:rgba(239,231,214,.75);
  transition:background .14s ease, border-color .14s ease;
}
.cn-inst-action:hover{ border-color:var(--co-edge-strong); }
.cn-inst-action.primary{
  /* 500, not 600. Urbanist is loaded at 200/300/400/500 only (public/assets/fonts/), so a 600
     here has no matching face and the browser SYNTHESISES one — a faux-bold with smeared,
     mechanically-thickened strokes, on the single most prominent control in the panel. That is a
     real part of what reads as cheap, and it is invisible in code review because the declaration
     itself looks fine. */
  background:var(--co-amber); border-color:var(--co-amber); color:#1A0D03; font-weight:500;
  box-shadow:0 0 24px -10px rgba(232,134,58,.8);
}
.cn-inst-action.primary:hover{ filter:brightness(1.06); }

/* Measure points mini-frame — a real, live snapshot of the current frame (not a mock), points
   at the probe's real x/y. Second surface onto the SAME solverState.probes the main canvas
   drags — see wireChainMeterDialDrag's own comment for why this can't drift from it. */
.cn-inst-frame{
  position:relative; margin-top:12px; aspect-ratio:3/2; border-radius:8px; overflow:hidden;
  background:var(--co-glass) center/cover no-repeat; border:1px solid var(--co-edge-2);
}
.cn-inst-point{
  position:absolute; width:26px; height:26px; margin:-13px 0 0 -13px; border-radius:50%;
  border:2px solid var(--co-amber); background:rgba(232,134,58,.14);
  display:flex; align-items:center; justify-content:center;
  font-size:10px; color:var(--co-amber-hi); cursor:grab; touch-action:none;
}
.cn-inst-point:active{ cursor:grabbing; }
/* Overlaid on the photo itself, not a separate line below it — matches the reference exactly.
   Gradient scrim only behind the text's own strip, not the whole frame, so it doesn't dim
   whatever's sitting right above it. */
/* al: "still getting a little leak at the bottom of these gradients."
   Every one of these scrims ramped to 85-92% opacity around the 55-60% mark and then sat FLAT
   at that value all the way to the bottom edge — so the last band of the photograph was never
   covered, and a bright subject (sun on water, a blown sky) glowed through under the caption.
   A scrim whose job is to carry type has to finish the job: each now ramps from transparent
   through a softer mid to near-opaque AT 100%, which also makes the falloff smoother than the
   old hard landing at 55%. */
.cn-inst-frame-footer{
  position:absolute; left:0; right:0; bottom:0; padding:16px 12px 8px;
  background:linear-gradient(180deg, transparent 0%, rgba(6,9,11,.62) 42%, rgba(6,9,11,.97) 100%);
  font-size:9px; letter-spacing:.14em; color:rgba(239,231,214,.85); pointer-events:none;
}

/* "What it measured" — three real stat cards, same well/border language as everything else
   in this file, no new visual idiom invented for them. */
.cn-inst-stats{ display:grid; grid-template-columns:repeat(3,1fr); gap:8px; margin-top:12px; }
.cn-inst-stat{
  display:flex; flex-direction:column; gap:6px; padding:11px 12px; border-radius:8px;
  background:var(--co-well); border:1px solid var(--co-edge-2);
}
.cn-inst-stat .lbl{ font-size:9px; letter-spacing:.1em; text-transform:uppercase; color:rgba(239,231,214,.45); }
.cn-inst-stat .val{ font-size:17px; font-weight:400; color:var(--co-ink); }

/* Instrument #02 Colour Calibration — the Kelvin map. .cn-inst-frame is reused as-is; `.exact`
   is the one addition: the blocks are measured over the real proxy, so the backdrop must not be
   cropped by `cover` or the grid and the measurement stop corresponding on a non-3:2 photo. */
.cn-inst-frame.exact{ background-size:100% 100%; }
.cn-kgrid{ position:absolute; inset:0; display:grid; }
/* THE LOUPE. Each cell paints its own patch of the same measured proxy the frame behind it is
   showing — 600%/400% for the 6x4 grid, positioned by index — so at rest it lands exactly on top
   of the frame's image and you cannot tell it is there. On hover the cell scales about its own
   centre and that patch magnifies with it: a real loupe over the tile you are inspecting, for the
   price of one background-position. No second image, no fetch, no canvas.
   The 24 labels are 7.5px over a photograph, which is as small as this system goes and was the
   thing al could not read — so the enlarged state stops relying on them and shows the full
   reading on glass instead. */
.cn-kcell{ position:relative; padding:0; border:0; cursor:pointer;
  background-image:var(--patch); background-size:600% 400%;
  background-position:var(--bx) var(--by); background-repeat:no-repeat;
  box-shadow:inset 0 0 0 0.5px rgba(239,231,214,.14);
  transition:transform .16s cubic-bezier(.2,.7,.3,1), box-shadow .16s ease; }
.cn-kcell.empty{ background-image:none; }
.cn-kcell:hover{ transform:scale(2.05); z-index:30;
  box-shadow:0 0 0 1.5px var(--co-amber), 0 10px 26px -8px rgba(0,0,0,.95); }
/* The small label steps aside for the full reading, on its own scrim so it is legible over
   whatever the photograph happens to be doing underneath. */
.cn-kcell .zoom{ position:absolute; left:0; right:0; bottom:0; opacity:0; pointer-events:none;
  display:flex; align-items:baseline; justify-content:center; gap:3px;
  padding:3px 0 4px; background:linear-gradient(180deg, transparent 0%, rgba(5,8,11,.70) 42%, rgba(5,8,11,.98) 100%);
  font-size:8px; letter-spacing:.06em; color:rgba(239,231,214,.62);
  transition:opacity .16s ease; }
.cn-kcell .zoom b{ font-size:11px; font-weight:400; letter-spacing:0; color:var(--co-amber-hi);
  font-variant-numeric:tabular-nums; }
.cn-kcell .zoom i{ font-style:normal; font-size:7.5px; color:rgba(239,231,214,.45); margin-left:2px; }
.cn-kcell:hover .zoom{ opacity:1; }
.cn-kcell:hover .k{ opacity:0; }
.cn-kcell .k{ transition:opacity .12s ease; }
.cn-kcell.on{ background:color-mix(in srgb, var(--co-amber) 20%, transparent);
  box-shadow:inset 0 0 0 1.5px var(--co-amber); }
.cn-kcell.empty{ cursor:default; }
/* Cream, not pure white. The ink ladder in this file is alpha on 239,231,214; a label set on
   255,255,255 reads a shade cooler than every other label on the panel, which is the kind of
   half-step that makes a set of controls look assembled rather than designed. */
.cn-kcell .k{ position:absolute; left:3px; bottom:2px; font-size:7.5px; letter-spacing:.02em;
  color:rgba(239,231,214,.72); text-shadow:0 1px 3px rgba(0,0,0,.9); }
.cn-kcell.on .k{ color:var(--co-amber-hi); }
.cn-kcell .neu{ position:absolute; right:3px; top:3px; width:5px; height:5px; border-radius:50%;
  background:var(--co-teal); box-shadow:0 0 0 1px rgba(0,0,0,.5); }
.cn-kstrip{ margin-top:12px; padding:8px 11px; border-radius:6px; font-size:10px;
  color:var(--co-amber); background:color-mix(in srgb, var(--co-amber) 12%, transparent);
  border:1px solid color-mix(in srgb, var(--co-amber) 40%, transparent); }
.cn-kfoot{ margin-top:8px; display:flex; justify-content:space-between; gap:8px;
  font-size:9px; letter-spacing:.10em; color:rgba(239,231,214,.42); }
.cn-kfoot .neu{ color:var(--co-teal); }
/* Same 26px/6px/ew-resize geometry as .cn-inst-ramp, but overflow:visible — the needle and the
   median marker deliberately overhang, and .cn-inst-ramp's overflow:hidden would clip them.
   Gradient is the app's own kelvin ramp so this bar and the Fine Kelvin slider in the drawer
   below read as one instrument rather than two different scales. */
.cn-inst-kbar{ position:relative; margin-top:12px; height:26px; border-radius:6px;
  cursor:ew-resize; touch-action:none; overflow:visible;
  background:linear-gradient(90deg, #4a7fd6, #E8863A); }
.cn-inst-kbar .tick{ position:absolute; top:0; bottom:0; width:1px; background:rgba(11,18,20,.35); }
.cn-inst-kbar .med{ position:absolute; top:-4px; bottom:-4px; width:1.5px;
  background:var(--co-panel); opacity:.75; }
.cn-kends{ margin-top:9px; display:flex; justify-content:space-between;
  font-size:9px; letter-spacing:.1em; color:rgba(239,231,214,.42); }

/* Instrument #03 Negative Conversion.
   al (5): "this fake negative looks cheap, is there another way it can be done."
   The sprocket rails and the three-frame dividers are gone — see the comment in studio.html's
   chainRebateWidgetHTML for what they were inventing and why the rails were also a lie about
   where you could sample. What is left is one full-bleed well painted in the REAL base density
   with the REAL raw negative in it, so every pixel of it is samplable, and the hero above it is
   the stock's own characteristic curve sampled from the LUT the shader itself reads. */
.cn-rebate{ position:relative; margin-top:12px; padding:9px; border-radius:8px; overflow:hidden;
  cursor:crosshair; border:1px solid var(--co-edge-2); }
/* The eyedropper reads fx/fy straight against this box's own edges (studio.html's click handler)
   and assumes 0..1 spans the full source image. `cover` broke that: on a 3:2 photo inside the old
   fixed 320x100 box it centre-cropped to the middle ~47% of the rows, so clicking what LOOKED
   like the top of the frame actually sampled a row a quarter of the way down — the base density
   picked was never the pixel under the cursor. aspect-ratio + stretch-fill removes the crop
   entirely: the box takes the thumbnail's own shape, so every displayed pixel is one real source
   pixel and fx/fy map 1:1. Same .exact treatment the Kelvin map already uses for this exact
   problem (console.css, Colour Calibration block). */
.cn-rebate-window{ position:relative; width:100%; height:100px; max-height:230px;
  background:#0B0F14 center/cover no-repeat; border-radius:2px; }
.cn-rebate-window.exact{ height:auto; background-size:100% 100%; }
/* The curve. Same glass-and-hairline treatment as Log Conversion's transfer plot, because it is
   the same kind of object: a real transfer function, sampled from the real encoder. */
.cn-negcurve{ position:relative; margin-top:12px; height:132px; border-radius:8px;
  background:var(--co-glass); border:1px solid var(--co-edge-2); overflow:hidden; }
.cn-negcurve svg{ position:absolute; inset:0; width:100%; height:100%; }
.cn-negcurve .ax{ position:absolute; font-size:8px; letter-spacing:.10em;
  color:rgba(239,231,214,.30); text-shadow:0 1px 0 rgba(0,0,0,.55); pointer-events:none; }
.cn-negcurve .ax.x{ left:14px; bottom:3px; }
.cn-negcurve .ax.y{ left:14px; top:5px; }
.cn-negswatch{ margin-top:10px; display:flex; align-items:center; gap:8px;
  font-size:9px; letter-spacing:.10em; color:rgba(239,231,214,.42); }
.cn-negswatch .sw{ width:16px; height:16px; border-radius:4px; flex:none;
  box-shadow:inset 0 0 0 1px rgba(0,0,0,.4); }
.cn-negwarn{ margin-top:10px; padding:8px 11px; border-radius:6px; font-size:10px; line-height:1.45;
  color:rgba(239,231,214,.72); background:rgba(239,231,214,0.05);
  border:1px solid var(--co-edge-2); }
.cn-negwarn.ok{ color:var(--co-amber); background:color-mix(in srgb, var(--co-amber) 12%, transparent);
  border-color:color-mix(in srgb, var(--co-amber) 40%, transparent); }
.cn-negwarn .cn-inst-action{ margin-top:8px; height:34px; font-size:12px; }
.cn-negchans{ margin-top:12px; display:grid; grid-template-columns:repeat(3,1fr); gap:8px; }
.cn-negch{ padding:10px 11px; border-radius:8px; background:var(--co-well);
  border:1px solid var(--co-edge-2); cursor:ew-resize; touch-action:none; }
.cn-negch .ch{ font-size:9px; letter-spacing:.1em; }
.cn-negch .ch.r{ color:#E8615A; } .cn-negch .ch.g{ color:#6FBF73; } .cn-negch .ch.b{ color:#5B8BE0; }
.cn-negch .val{ display:block; margin-top:4px; font-size:17px; color:var(--co-ink); }
.cn-negch .bar{ display:block; margin-top:7px; height:3px; border-radius:2px;
  background:rgba(239,231,214,.12); overflow:hidden; }
.cn-negch .bar i{ display:block; height:100%; border-radius:2px; }
.cn-negch .bar i.r{ background:#E8615A; } .cn-negch .bar i.g{ background:#6FBF73; } .cn-negch .bar i.b{ background:#5B8BE0; }

/* Instrument #04 Log Conversion — the transfer graph. Both curves are sampled from the real
   encoders, so this plot is the actual transform, not an illustration of one. */
.cn-inst-graph{ position:relative; margin-top:12px; height:190px; border-radius:8px;
  background:var(--co-glass); border:1px solid var(--co-edge-2); overflow:hidden; }
.cn-inst-graph svg{ position:absolute; inset:0; width:100%; height:100%; }
.cn-inst-graph .lbl{ position:absolute; font-size:8px; letter-spacing:.12em; }
/* al (6): "the text is still overlapping in log."
   The labels used to sit against the RIGHT edge, one at top:9px and one at top:38%, on the
   theory that each would land beside its own curve. That theory was measured and it is wrong for
   almost every profile. Rec.709 reaches 1.0 at x=1, i.e. the very top of the plot, so its own
   stroke runs THROUGH its own label from x=292 of 337 onward — for all fourteen profiles,
   including 'none'. And top:38% of the 190px box is a band at y 72-84, which is exactly where ten
   of the thirteen camera curves terminate (S-Log2 80.2, S-Log3 79.4, V-Log 78.9, Canon Log 74.4,
   Canon Log 2 81.5, Canon Log 3 81.0, LogC3 83.6, N-Log 78.0, F-Log2 83.9, D-Log 81.3); only
   F-Log at 61.8, Log3G10 at 96.1 and Blackmagic Film at 90.1 miss it.
   Both move to the TOP-LEFT, stacked, which is the one corner every curve in the set stays out
   of: the highest any camera curve reaches at the labels' right edge is F-Log at svg-y 61.8, and
   Rec.709 does not climb above svg-y 36 until 72% of the plot width (243.7px of 337) — 78px clear
   of the widest label. left:22px, not 10px, so the label and its swatch clear the y-axis the
   widget draws at svg-x 15.7. A 10px rule of the stroke's own colour keeps each label attached to
   its curve now that they no longer sit next to one. */
.cn-inst-graph .lbl.out{ left:22px; top:9px; color:#B48CE8; }
.cn-inst-graph .lbl.cam{ left:22px; top:24px; color:rgba(239,231,214,.42); }
/* Said when the output leg is switched off, in the same top-left stack the two curve legends
   use, so the absence of a curve is stated rather than left as a blank half of the plot. */
.cn-inst-graph .lbl.none{ left:22px; bottom:10px; color:rgba(239,231,214,.34); }
.cn-inst-graph .lbl.none::before{ background:transparent; box-shadow:inset 0 0 0 1px currentColor; }
.cn-inst-graph .lbl::before{ content:''; display:inline-block; width:10px; height:2px;
  margin-right:6px; vertical-align:middle; background:currentColor; }

/* ── Instruments 05–28 ─────────────────────────────────────────────────────────────────────
   Shared shapes first, then the per-instrument pieces. Everything sits on the same tokens and
   the same 11.5px/9px scale as the rest of the drawer. */

/* A neutral variant of the threshold ramp: no black-to-white gradient, just a well the marker
   travels along — used wherever the value isn't a luminance. */
.cn-inst-ramp.neutral{ background:var(--co-well); box-shadow:inset 0 1px 3px rgba(0,0,0,.5); }
.cn-inst-ramp.neutral .cn-inst-ramp-fill{ background:color-mix(in srgb, var(--co-amber) 22%, transparent); }

/* 05 Crop & Straighten — the spirit-level tape. */
.cn-crop-rule{ position:relative; margin-top:12px; height:34px; border-radius:6px;
  background:var(--co-well); border:1px solid var(--co-edge-2); overflow:hidden;
  cursor:ew-resize; touch-action:none; }
.cn-crop-rule .minor{ position:absolute; left:0; right:0; top:14px; height:12px;
  background:repeating-linear-gradient(to right, rgba(255,255,255,.24) 0 1px, transparent 1px calc(100% / 45)); }
.cn-crop-rule .major{ position:absolute; left:0; right:0; top:8px; height:20px;
  background:repeating-linear-gradient(to right, rgba(255,255,255,.46) 0 1px, transparent 1px calc(100% / 9)); }
.cn-crop-rule .needle{ position:absolute; top:2px; bottom:4px; width:2px; margin-left:-1px;
  background:var(--co-amber); box-shadow:0 0 14px 2px color-mix(in srgb, var(--co-amber) 50%, transparent); }
/* The ratio tray is a <select> now (see .cn-cropratio-sel) — .cn-crop-ratio's button rules were
   left behind, in two near-identical copies at opposite ends of this file, styling markup nothing
   emits any more. */
.cn-crop-ratios{ margin-top:12px; }

/* 06 Perspective — the keystone quad. */
/* No fixed height. The widget hands this element an inline aspect-ratio taken from the FRAME,
   so a height of 165px fought it: at that height a 9:16 portrait resolved to a 93px-wide
   instrument with about 230px of dead glass beside it, and a 16:9 one wanted 294px. The
   aspect-ratio sizes it to the panel width instead, with a cap so a tall portrait cannot push
   the rails below the fold. */
.cn-inst-quad{ position:relative; margin-top:12px; max-height:230px; border-radius:8px;
  background:var(--co-glass); border:1px solid var(--co-edge-2); overflow:hidden;
  cursor:ew-resize; touch-action:none; }
.cn-inst-quad svg{ position:absolute; inset:0; width:100%; height:100%; }

/* 09 Sky Recovery — the REAL segmentation mask over the frame, plus its mask points. Teal is
   the app's "measured" colour, so the overlay reads as something detected rather than styled.
   (An earlier version drew a horizon line positioned by the `feather` value — feather is an
   edge-softness amount, not a vertical position, so that line looked like a control and
   controlled nothing. Removed.) */
/* No screen blend. Screen lightens by (1 - (1-a)(1-b)), which never reaches zero for any non-zero
   plate value — so every pixel the mask touches at all got brighter, and the soft tail of an edge
   read as a halo rather than as a fading mask. It also meant the two plates ADDED where they
   overlap, which is not the relationship: teal is what the model found, amber is the part of that
   which will actually be rebuilt, so amber sitting ON teal is the honest depiction. */
.cn-sky-mask{ position:absolute; inset:0; background-size:100% 100%; background-repeat:no-repeat;
  pointer-events:none; }
.cn-sky-seed{ position:absolute; width:12px; height:12px; margin:-6px 0 0 -6px; border-radius:50%;
  border:2px solid var(--co-amber); background:rgba(232,134,58,.25); cursor:grab;
  box-shadow:0 1px 4px rgba(0,0,0,.6); }
.cn-sky-seed:active{ cursor:grabbing; }
.cn-sky-seed.sub{ border-color:#E8615A; background:rgba(232,97,90,.25); }

/* 19 Microfilm — the density wedge: a real taper, cream to black. */
.cn-inst-wedgebar{ position:relative; margin-top:12px; height:44px; border-radius:6px;
  cursor:ew-resize; touch-action:none; overflow:hidden; border:1px solid var(--co-edge-2);
  background:linear-gradient(90deg, #EFE7D6, #0B0F14);
  clip-path:polygon(0 22%, 100% 0, 100% 100%, 0 78%); }
.cn-inst-wedgebar .needle{ position:absolute; top:0; bottom:0; width:2px; margin-left:-1px;
  background:var(--co-amber); box-shadow:0 0 12px 2px color-mix(in srgb, var(--co-amber) 55%, transparent); }

/* 22 Grain + 27 Finishing — the loupe. */
.cn-inst-loupe{ position:relative; margin-top:12px; width:100%; aspect-ratio:1; border-radius:50%;
  /* move, not ew-resize: this loupe drags radius across AND amount up/down, and a horizontal
     cursor silently promises half the gesture. */
  overflow:hidden; border:2px solid rgba(239,231,214,.30); cursor:move; touch-action:none;
  background:var(--co-well); }
.cn-inst-loupe svg{ position:absolute; inset:0; width:100%; height:100%; }
.cn-inst-loupe.photo{ background-repeat:no-repeat; }
.cn-inst-loupe .empty{ position:absolute; inset:0; display:flex; align-items:center;
  justify-content:center; font-size:9px; letter-spacing:.14em; color:rgba(239,231,214,.35); }

/* 23 Cinema Looks — the contact sheet. */
.cn-contact{ margin-top:12px; display:grid; grid-template-columns:repeat(3,1fr); gap:6px; }
.cn-contact-cell{ position:relative; aspect-ratio:3/2; border-radius:6px; overflow:hidden;
  cursor:pointer; padding:0; border:1px solid var(--co-edge-2);
  background:var(--co-well) center/cover no-repeat; }
.cn-contact-cell.on{ border-color:var(--co-amber);
  box-shadow:0 0 0 1px var(--co-amber), 0 0 16px -4px color-mix(in srgb, var(--co-amber) 60%, transparent); }
.cn-contact-cell .lbl{ position:absolute; left:0; right:0; bottom:0; padding:8px 5px 3px;
  font-size:7.5px; letter-spacing:.06em; color:rgba(239,231,214,.9); text-align:left;
  background:linear-gradient(180deg, transparent 0%, rgba(6,9,11,.66) 45%, rgba(6,9,11,.97) 100%);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.cn-contact-cell.on .lbl{ color:var(--co-amber-hi); }

/* 25 Diffusion Filter — grade buttons, each previewing its own bloom strength. */
/* The grade row is .cn-plateb's geometry and states, not a second chip: it was 40px tall at 11px
   with no tracking, no hover and no amber wash when selected, which is a different control
   language three lines below a row that uses the real one. The per-grade bloom preview is kept —
   it is the one thing this row does that a plain chip cannot, and it is driven by the real --g —
   but on cream rather than pure white, which is not on the palette. */
.cn-grades{ margin-top:12px; display:grid; grid-template-columns:repeat(5,1fr); gap:5px; }
.cn-grade{ height:34px; border-radius:6px; cursor:pointer; padding:0;
  font-family:inherit; font-size:9px; letter-spacing:.1em; color:rgba(239,231,214,.5);
  border:1px solid var(--co-edge-2);
  background:radial-gradient(circle at 50% 125%, rgba(239,231,214,calc(var(--g,.3) * 0.30)) 0%, transparent 70%), var(--co-well);
  transition:color .14s ease, border-color .14s ease; }
.cn-grade:hover{ color:var(--co-amber-hi); border-color:color-mix(in srgb, var(--co-amber) 45%, transparent); }
.cn-grade.on{ border-color:var(--co-amber); color:var(--co-amber);
  background:radial-gradient(circle at 50% 125%, rgba(239,231,214,calc(var(--g,.3) * 0.30)) 0%, transparent 70%),
             color-mix(in srgb, var(--co-amber) 12%, transparent); }

/* 28 Export — destinations with their real notes. */
.cn-dests{ margin-top:12px; display:grid; grid-template-columns:repeat(2,1fr); gap:6px; }
.cn-dest{ padding:10px 11px; border-radius:8px; cursor:pointer; font-family:inherit;
  text-align:left; background:var(--co-well); border:1px solid var(--co-edge-2); }
.cn-dest.on{ border-color:var(--co-amber); background:color-mix(in srgb, var(--co-amber) 12%, transparent); }
.cn-dest .n{ display:block; font-size:11.5px; color:var(--co-ink); }
.cn-dest.on .n{ color:var(--co-amber-hi); }
.cn-dest .s{ display:block; margin-top:3px; font-size:9px; letter-spacing:.06em; color:rgba(239,231,214,.42); }

/* 10 Facelift — the region map. Amber fill = included; teal stroke on the eyes when off means
   "protected on purpose", which is different from "not switched on yet" (the design's own
   distinction). Whole block dims until faces are found, because until then the regions have
   nothing to act on. */
/* The face is the module's hero, so it gets a hero's housing: a full-width well with the
   instrument centred in it and lit from above, which is the same construction Halation gives its
   lamp. It used to be a 120px thumbnail pinned to the left of a column of four buttons — the
   smallest object in a module that is entirely about a face. */
.cn-facewell{ position:relative; margin-top:12px; height:252px; border-radius:8px;
  background:var(--co-well); border:1px solid var(--co-edge-2);
  box-shadow:inset 0 2px 12px -5px rgba(0,0,0,.9);
  display:flex; align-items:center; justify-content:center; }
.cn-facewell.dim{ opacity:.5; }
.cn-facemap-svg{ height:212px; width:auto; display:block; overflow:visible; }
.cn-facemap-svg path{ transition:fill .14s ease, stroke .14s ease; }
/* Engraved, not drawn on: a cream hairline with a black one sitting just under it, which is the
   same trick the silkscreen legends use one dimension down. The construction lines take no
   pointer events — they describe the face, they are not part of the control. */
.cn-facemap-rule{ pointer-events:none; }
.cn-facemap-rule ellipse, .cn-facemap-rule path{
  fill:none; stroke:rgba(239,231,214,.20); stroke-width:1.1; stroke-linecap:round;
  filter:drop-shadow(0 1px 0 rgba(0,0,0,.55));
}
/* Four across, under the face, rather than a vertical column beside it — so the face gets the
   full width and the regions read as a set of four equal things, which is what they are. */
.cn-faceregions{ margin-top:12px; display:grid; grid-template-columns:repeat(4,1fr); gap:5px; }
.cn-faceen{ margin-top:12px; display:flex; }
/* A <button> with no font declared falls back to the browser's own 13.3333px Arial. It only ever
   showed here because the two spans inside set their own size — the button's own metrics were
   still off-system, which is how a row ends up a pixel taller than its neighbours for no visible
   reason. */
.cn-faceregion{ font-family:inherit; font-size:9px; letter-spacing:.14em;
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:5px;
  height:44px; padding:0 4px; border-radius:6px; text-align:center;
  background:var(--co-well); border:1px solid var(--co-edge-2); cursor:pointer;
  transition:border-color .14s ease, background .14s ease; }
.cn-faceregion.on{ border-color:color-mix(in srgb, var(--co-amber) 45%, transparent);
  background:color-mix(in srgb, var(--co-amber) 12%, transparent); }
.cn-faceregion .n{ font-size:9px; letter-spacing:.14em; color:rgba(239,231,214,.62); }
.cn-faceregion.on .n{ color:var(--co-amber); }
.cn-faceregion .s{ font-size:9px; letter-spacing:.08em; color:rgba(239,231,214,.38); }
.cn-faceregion.on .s{ color:var(--co-amber-hi); }

/* ── 12 Zones — the Adams ladder, measured against this frame ──────────────────────────────────
   The block's HEIGHT is its exposure (54px at 0 EV, ±28.8px across the control's own declared
   range, set inline by chainZonesWidgetHTML) — that is the module's whole thesis, and it is why
   dominance moves OUT of the height and into the 3px amber bar inside each block, fed by
   lastZoneCoverage (drawZoneScope's real per-render getImageData pass). Swatches come from
   ZONE_RAMP, the same eleven the false-colour overlay paints onto the photograph — never a
   second palette, or the strip and the canvas would disagree about what Zone VII looks like. */
/* One bay holding both: the frame's tones on top, the ladder you place them on underneath, read
   left-to-right from black to white in both. They were two separate bordered boxes with a gap
   between them, which made two unrelated objects out of a single idea. */
.cn-zonebay{ margin-top:12px; padding:10px 10px 12px; border-radius:8px;
  background:var(--co-well); border:1px solid var(--co-edge-2);
  box-shadow:inset 0 2px 12px -6px rgba(0,0,0,.9); }
.cn-zonehist{ height:54px; border-radius:3px; overflow:hidden; opacity:.72; }
.cn-zonehist canvas{ display:block; width:100%; height:100%; }
/* A hairline where the histogram's baseline meets the ladder's top — the seam that says these
   two share an axis rather than merely sitting near each other. This claim used to be wrong: the
   canvas drew a linear 0-255 histogram under eleven log2-spaced zone blocks, so a spike could
   land up to two blocks from the zone its own pixels belonged to. drawAdjustmentsHistogram now
   bins this one canvas (data-hist-scale="zone") by luminanceToZone instead — see its own comment
   in studio.html — so the seam finally means what it says. */
.cn-zonehist + .cn-zonestrip{ border-top:1px solid rgba(239,231,214,.07); padding-top:9px; }

.cn-zonestrip{ position:relative; margin-top:0; height:158px; touch-action:none; cursor:ns-resize; }
/* THE ENGRAVED SCALE. al (13): "Zones levels needs to be clearer - its hard to tell what levels
   they are at as a starting point." There was one line, at zero, so a block's height said only
   "moved", never "by how much" — and at rest all eleven sit at the same 96px with nothing to
   read. The ticks are positioned from studio.html's own BASE/SWING/evMax, not from fixed offsets
   here, so the scale cannot drift from the ladder it measures. Hairlines over the blocks, the way
   a scale is engraved on a meter face rather than printed behind it; zero keeps the dashed,
   brighter treatment because it is the datum and not just another division. */
.cn-zonetick{ position:absolute; left:0; right:0; height:0; z-index:3; pointer-events:none;
  border-top:1px solid rgba(239,231,214,.09); }
.cn-zonetick.zero{ border-top:1px dashed rgba(239,231,214,.42); }
/* Left-hand gutter, over Zone 0 — the darkest block on the ramp, so the type never needs a
   plate behind it to stay legible. */
.cn-zonetick-lb{ position:absolute; left:4px; z-index:4; pointer-events:none;
  font-size:8px; letter-spacing:.10em; color:rgba(239,231,214,.30);
  font-variant-numeric:tabular-nums; text-shadow:0 1px 0 rgba(0,0,0,.55); }
.cn-zonetick-lb.zero{ color:rgba(239,231,214,.46); }
/* 3px of real gap over a dark ground, not 1px. ZONE_RAMP_HEX crowds at both ends — 0 and I are
   #1D1D1D and #2C2C2C, VIII/IX/X are all within a few points of cream — so adjacent blocks were
   running together into one wash and the ladder stopped reading as steps. The palette itself is
   left alone deliberately: it is the same ramp the false-colour overlay paints on the photograph,
   and a second palette here would make the strip and the canvas disagree about what Zone VII is.
   Separation is the honest fix; recolouring would not be. */
.cn-zonestrip-row{ position:absolute; inset:0; display:flex; align-items:flex-end; gap:3px;
  background:rgba(6,9,11,.55); border-radius:3px; }

.cn-zoneblock{ position:relative; flex:1; min-width:0; box-sizing:border-box;
  border:2px solid transparent; border-radius:0; padding:0 0 3px; margin:0;
  display:flex; flex-direction:column; align-items:center; justify-content:flex-end; gap:2px;
  overflow:hidden; cursor:ns-resize; font-family:inherit; }
.cn-zoneblock.on{ border-color:var(--co-amber); }
/* "Edited" is a cream rim everywhere else it appears in this file (.hsl-wedge.edited, the mask
   dot). A white one here made the same state mean two different colours in two instruments. */
.cn-zoneblock.edited{ box-shadow:inset 0 0 0 1px rgba(239,231,214,.42); }
.cn-zoneblock-pop{ position:absolute; left:2px; bottom:2px; width:3px; border-radius:2px;
  background:var(--co-amber); opacity:.85; pointer-events:none; }
.cn-zonestrip-row{ padding-top:0; }
/* Both carried a text-shadow of nothing over a ramp that runs from near-black to near-white, so
   whichever end you looked at, one of them was fighting its own background. The shadow is picked
   from the block's own ink colour by currentColor, so it darkens light blocks and lightens dark
   ones without needing a second per-zone value. */
.cn-zoneblock-lb{ position:relative; font-size:9.5px; letter-spacing:.04em;
  pointer-events:none; font-weight:500; }
.cn-zoneblock{ text-shadow:0 1px 2px rgba(0,0,0,.28); }
.cn-zoneblock.light-ink{ text-shadow:0 1px 2px rgba(0,0,0,.55); }
.cn-zoneblock.dark-ink{ text-shadow:0 1px 2px rgba(255,255,255,.45); }

.cn-zonefoot{ margin-top:10px; display:flex; justify-content:space-between; gap:8px;
  font-size:9px; letter-spacing:.10em; color:rgba(239,231,214,.42);
  font-variant-numeric:tabular-nums; }
.cn-zonefoot .on{ color:var(--co-amber); }

/* ── 11 Exposure & Colour — one histogram, five named grips ────────────────────────────────────
   The canvas is the real drawAdjustmentsHistogram() pass; the grips ride on top of it. Knob
   margin-bottom is set inline from chainInstPct — 0..100px inside a 150px well — so a -5..5 EV
   field and a -100..100 field both use the full travel rather than the design's `v*0.5+50`,
   which is only correct for a -100..100 grip. */
.cn-histo-well{ position:relative; margin-top:12px; height:150px; border-radius:8px;
  overflow:hidden; background:var(--co-well); border:1px solid var(--co-edge-2);
  touch-action:none; }
.cn-histo-well .cn-histogram{ position:absolute; inset:0; width:100%; height:100%; display:block; }
.cn-histo-grip{ position:absolute; top:0; bottom:0; width:26px; margin-left:-13px;
  display:flex; flex-direction:column; align-items:center; justify-content:flex-end;
  padding-bottom:6px; cursor:ns-resize; touch-action:none; }
.cn-histo-guide{ position:absolute; top:0; bottom:0; width:1px; background:rgba(232,134,58,.22); }
.cn-histo-guide.on{ background:rgba(232,134,58,.7); }
.cn-histo-knob{ position:relative; width:14px; height:14px; border-radius:3px;
  background:rgba(239,231,214,.22); }
.cn-histo-knob.on{ background:var(--co-amber); }
.cn-histo-grip:hover .cn-histo-knob{ background:var(--co-amber-hi); }
/* 7px is the design's own literal value and sits below SPEC-console §1's 8px silkscreen floor.
   Kept because SPEC-console's own header says the prototype wins on a disagreement — but this
   is the one place in the console that breaches the shared scale, so it is flagged here. */
.cn-histo-name{ position:absolute; bottom:4px; font-size:7px; letter-spacing:.06em;
  color:rgba(239,231,214,.42); white-space:nowrap; pointer-events:none; }
.cn-histo-vals{ margin-top:10px; display:flex; justify-content:space-between;
  font-size:9px; letter-spacing:.1em; color:rgba(239,231,214,.42);
  font-variant-numeric:tabular-nums; }
.cn-histo-vals .on{ color:var(--co-amber); }

/* ── LOOK instruments — pieces the design had that the widgets were missing ────────────────── */

/* 19 Microfilm · light table. Each cell is the user's own frame under a stock's name — a chooser,
   not a claimed preview of a transform that has not been run (see the widget's own comment). */
.cn-mftable{ margin-top:12px; display:grid; grid-template-columns:repeat(4,1fr); gap:8px; }
.cn-mfstock{ display:flex; flex-direction:column; gap:5px; padding:0; background:none; border:0;
  cursor:pointer; font-family:inherit; text-align:left; min-width:0; }
.cn-mfstock .cell{ position:relative; display:block; border-radius:3px; overflow:hidden;
  aspect-ratio:3/2; background:var(--co-well) center/cover no-repeat;
  outline:1px solid var(--co-edge-2); outline-offset:-1px; }
/* outline, NOT inset box-shadow: an inset shadow paints immediately after the background and
   BEFORE in-flow content, so the moment these cells started holding a real <canvas> the amber
   selected-ring vanished behind it. outline paints over the child. */
.cn-mfstock.on .cell{ outline-color:var(--co-amber); }
.cn-mfstock .lbl{ font-size:9px; letter-spacing:.02em; color:rgba(239,231,214,.50);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.cn-mfstock.on .lbl{ color:var(--co-amber); }
/* Format buttons are sized by the negative they stand for — the enlargement is the point. */
.cn-mffmts{ margin-top:12px; display:flex; align-items:flex-end; gap:6px; }
.cn-mffmt{ border-radius:4px; background:rgba(239,231,214,.05); border:1px solid var(--co-edge-2);
  color:rgba(239,231,214,.45); font-family:inherit; font-size:9px; letter-spacing:.10em;
  cursor:pointer; padding:0; }
.cn-mffmt.on{ background:color-mix(in srgb, var(--co-amber) 22%, transparent);
  border-color:var(--co-amber); color:var(--co-amber); }

/* 23 Cinema Looks · the sheet scrolls rather than truncating — all 34 reachable. */
/* Cinema Looks is the one module where the picker IS the instrument — a contact sheet of your own
   frame through 34 looks is not a list of names, it is the comparison you came to make. It keeps
   its height. Stated here so the exception is deliberate and not an oversight. */
.cn-contact-scroll{ margin-top:12px; max-height:300px; overflow-y:auto; border-radius:8px; }

/* 26 Dirt · plate tray scrolls; placement puck is a real two-axis handle on posX/posY. */
.cn-tiles-scroll{ max-height:200px; overflow-y:auto; padding-right:4px; }
.cn-dirtplace{ margin-top:12px; position:relative; width:100%; border-radius:8px;
  background:var(--co-well); box-shadow:inset 0 0 0 1px var(--co-edge-2);
  cursor:move; touch-action:none; overflow:hidden; }
.cn-dirtplace.empty{ cursor:default; }
.cn-dirtplace .frame{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  pointer-events:none; }
/* The plate is sized and placed inline by chainTilesWidgetHTML from the RENDER's own geometry
   (dirtBaseSize + dirtCoverFactor), so nothing here may set width, height, position or transform. */
.cn-dirtplace .plate{ position:absolute; pointer-events:none; }
/* Rule-of-thirds guides — aiming a light leak is a composition decision. */
.cn-dirtplace .grid{ position:absolute; inset:0; pointer-events:none;
  background:
    linear-gradient(to right, transparent 33.27%, rgba(239,231,214,.16) 33.33%, rgba(239,231,214,.16) 33.4%, transparent 33.46%),
    linear-gradient(to right, transparent 66.6%, rgba(239,231,214,.16) 66.66%, rgba(239,231,214,.16) 66.73%, transparent 66.8%),
    linear-gradient(to bottom, transparent 33.27%, rgba(239,231,214,.16) 33.33%, rgba(239,231,214,.16) 33.4%, transparent 33.46%),
    linear-gradient(to bottom, transparent 66.6%, rgba(239,231,214,.16) 66.66%, rgba(239,231,214,.16) 66.73%, transparent 66.8%); }
.cn-dirtplace-empty{ position:absolute; left:0; right:0; top:50%; transform:translateY(-50%);
  text-align:center; font-size:9px; letter-spacing:.1em; color:rgba(239,231,214,.42);
  pointer-events:none; }
/* A ring, not a dot: the puck marks the plate's CENTRE, and a 16px dot over a photograph reads as
   a blemish rather than a handle. Hollow so the frame stays readable under it. */
.cn-dirtplace .puck{ position:absolute; width:74px; height:74px; margin:-37px 0 0 -37px;
  border-radius:50%; border:2px solid var(--co-amber); box-sizing:border-box;
  background:color-mix(in srgb, var(--co-amber) 9%, transparent);
  box-shadow:0 0 18px -4px color-mix(in srgb, var(--co-amber) 60%, transparent);
  pointer-events:none; }
.cn-dirtplace .puck::after{ content:''; position:absolute; left:50%; top:50%; width:7px; height:7px;
  margin:-3.5px 0 0 -3.5px; border-radius:50%; background:var(--co-amber); }

/* Zoom ladder — a tick rule, matching the design's "the floor, not the ceiling". */
.cn-zoomlad{ position:relative; margin-top:12px; height:26px; cursor:ew-resize; touch-action:none; }
.cn-zoomlad .t{ position:absolute; top:8px; width:1px; height:10px; margin-left:-.5px;
  background:rgba(239,231,214,.20); }
.cn-zoomlad .t.maj{ top:4px; height:18px; background:rgba(239,231,214,.34); }
.cn-zoomlad .cut{ position:absolute; top:0; bottom:0; width:2px; margin-left:-1px; border-radius:1px;
  background:var(--co-amber);
  box-shadow:0 0 14px 2px color-mix(in srgb, var(--co-amber) 50%, transparent); }

/* Blend / flip / fit buttons under the viewfinder. */
.cn-plateb-row{ margin-top:12px; display:flex; gap:6px; }
.cn-plateb{ flex:1; min-width:0; height:34px; border-radius:6px; cursor:pointer; padding:0;
  font-family:inherit; font-size:9px; letter-spacing:.1em; color:rgba(239,231,214,.5);
  border:1px solid var(--co-edge-2); background:transparent;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.cn-plateb:hover{ color:var(--co-amber-hi); border-color:color-mix(in srgb, var(--co-amber) 45%, transparent); }
.cn-plateb.on{ color:var(--co-amber); border-color:var(--co-amber);
  background:color-mix(in srgb, var(--co-amber) 12%, transparent); }
/* Zones' inspection row: a switch, then a pair. The pair is grouped so the row does not read as
   three alternatives, and it takes twice the width because it holds two buttons. */
.cn-zonefc-row{ align-items:stretch; }
.cn-zonefc-scope{ flex:2; min-width:0; display:flex; gap:6px; margin-left:6px; }
.cn-zonefc-scope .cn-plateb{ flex:1; }
.cn-plateb[disabled]{ opacity:.34; cursor:default; }
.cn-plateb[disabled]:hover{ color:rgba(239,231,214,.5); border-color:var(--co-edge-2); }
.cn-platefoot{ margin-top:10px; display:flex; justify-content:space-between; gap:8px;
  font-size:9px; letter-spacing:.1em; color:rgba(239,231,214,.42);
  font-variant-numeric:tabular-nums; }

/* 24 Lenses · coating. Only the two elements with real fields (distortion grid, coatingEra) are
   drawn — the design's focal and aperture handles have no backing here. */
.cn-coats{ margin-top:12px; display:flex; gap:6px; }
.cn-coat{ flex:1; height:34px; border-radius:6px; cursor:pointer; padding:0; font-family:inherit;
  font-size:9px; letter-spacing:.08em; display:flex; flex-direction:column; align-items:center;
  justify-content:center; gap:6px; color:rgba(239,231,214,.52); border:1px solid var(--co-edge-2);
  background:transparent; }
.cn-coat.on{ color:var(--co-amber); border-color:var(--co-amber);
  background:color-mix(in srgb, var(--co-amber) 12%, transparent); }
.cn-coat .bar{ display:block; width:22px; height:2px; border-radius:1px; opacity:.45; }
.cn-coat.on .bar{ opacity:1; }

/* Thumbnail canvases — one per chooser cell, painted from an offscreen cache by
   chainThumbPaintVisible(). Sits under the cell's own label gradient and selected ring.
   Fades in on .ready so a sheet develops rather than popping row by row. */
.cn-thumbcv{ position:absolute; inset:0; width:100%; height:100%; display:block;
  object-fit:cover; opacity:0; transition:opacity .18s ease; }
.cn-thumbcv.ready{ opacity:1; }
.cn-contact-cell{ background:var(--co-well); }

/* ── 23 Cinema Looks · A/B ─────────────────────────────────────────────────────────────────────
   The engine has always been able to blend one look toward another (blendLookParams + the `eff`
   line in cinemaLooks.render); only the legacy Bench card exposed it. Badge is dark ink on amber,
   per the prototype — cream on amber measures about 1.9:1 at this size, and the badge is the one
   thing distinguishing the A cell from the B cell. */
.cn-contact-badge{ position:absolute; top:3px; right:3px; width:13px; height:13px; border-radius:3px;
  background:var(--co-amber); color:#1A0D03; font-size:8.5px; line-height:13px; text-align:center;
  font-variant-numeric:tabular-nums; pointer-events:none; z-index:2; }
.cn-contact-badge.b{ background:var(--co-ink); color:#0B0F14; }
.cn-contact-cell.is-b{ border-color:var(--co-ink); }

.cn-tbar-ends{ margin-top:12px; display:flex; justify-content:space-between; gap:12px;
  font-size:8.5px; letter-spacing:.1em; color:rgba(239,231,214,.55); }
.cn-tbar-ends span{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.cn-tbar{ margin-top:4px; position:relative; height:34px; cursor:ew-resize; touch-action:none;
  display:flex; align-items:center; }
.cn-tbar .track{ position:absolute; left:0; right:0; height:4px; border-radius:2px;
  background:rgba(239,231,214,.14); }
.cn-tbar .fill{ position:absolute; left:0; height:4px; border-radius:2px;
  background:color-mix(in srgb, var(--co-amber) 55%, transparent); }
.cn-tbar .handle{ position:absolute; width:44px; height:20px; margin-left:-22px; border-radius:5px;
  background:var(--co-ink); box-shadow:0 2px 10px -3px rgba(0,0,0,.8); pointer-events:none; }
.cn-tbar-foot{ margin-top:9px; display:flex; justify-content:space-between; align-items:center;
  font-size:9px; letter-spacing:.1em; color:rgba(239,231,214,.42);
  font-variant-numeric:tabular-nums; }
.cn-tbar-clear{ background:none; border:1px solid var(--co-edge-2); border-radius:4px;
  color:rgba(239,231,214,.55); font-family:inherit; font-size:8px; letter-spacing:.1em;
  padding:3px 7px; cursor:pointer; }
.cn-tbar-clear:hover{ color:var(--co-amber); border-color:var(--co-amber); }
.cn-inst-hint{ margin-top:11px; font-size:9px; letter-spacing:.04em; color:rgba(239,231,214,.38); }

/* ── 24 Lenses · the cutaway ───────────────────────────────────────────────────────────────────
   Grips are HTML boxes laid over the SVG rather than <circle> elements inside it:
   wireChainInstDrags derives its sensitivity from the handle's own width, and a 24px circle would
   gear 186mm of focal length into 24px of travel. Each grip is a real ~29%-wide drag surface. */
.cn-cutaway{ margin-top:12px; background:var(--co-well); border:1px solid var(--co-edge-2);
  border-radius:8px; padding:8px; }
/* height:auto + aspect-ratio, not a fixed px height with preserveAspectRatio="none" — a fixed
   96px against a variable width scales x and y differently at every width but 204.8px (96/150),
   which this instrument column never is. The iris — the one element on this schematic that
   claims to be the literal bokeh shape (buildApertureSamples' own curvature=1.0 draws a true
   circle) — rendered as an ellipse 0.59-0.65x as tall as wide, and the two .cap labels rendered
   at ~4.8px, under this app's own documented 8px silkscreen floor. Locking the box's aspect to
   the viewBox's makes scaleX===scaleY for any width, so xMidYMid meet never actually needs to
   letterbox — kept anyway for consistency with every other geometry-bearing instrument SVG in
   this file, which already uses it. */
.cn-cutaway svg{ display:block; width:100%; height:auto; aspect-ratio:320/150; }
.cn-cutaway .cap{ font-family:inherit; font-weight:400; font-size:7.5px; letter-spacing:.10em; }

/* Equal thirds in a flex row. The first version hand-placed these at 34/50/67% at 29% wide, so
   FOCAL overlapped APERTURE by 13% and APERTURE overlapped DISTORTION by 12% — the outer grip
   swallowed pointerdown over the shared strip and the middle control was partly unreachable.
   Columns cannot overlap by construction, and the gap is real separation, not a hoped-for one. */
.cn-lensrail{ display:flex; gap:10px; margin-top:10px; }
.cn-lensgrip{ position:relative; flex:1; min-width:0; height:46px;
  cursor:ew-resize; touch-action:none; }
.cn-lensgrip .rail{ position:absolute; left:0; right:0; top:9px; height:2px;
  border-radius:1px; background:rgba(232,134,58,.20); pointer-events:none; }
.cn-lensgrip:hover .rail{ background:rgba(232,134,58,.42); }
.cn-lensgrip .nub{ position:absolute; top:4px; width:12px; height:12px; margin-left:-6px;
  border-radius:50%; background:#12171E; border:1.6px solid var(--co-amber); box-sizing:border-box;
  pointer-events:none; }
.cn-lensgrip .nub::after{ content:''; position:absolute; left:50%; top:50%; width:3.6px;
  height:3.6px; margin:-1.8px 0 0 -1.8px; border-radius:50%; background:var(--co-amber); }
/* name above the value, 15px apart — they are separate rows, never stacked at the same top. */
.cn-lensgrip .name{ position:absolute; left:0; right:0; top:21px; text-align:center;
  font-size:7px; letter-spacing:.10em; color:rgba(239,231,214,.45); pointer-events:none;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.cn-lensgrip .val{ position:absolute; left:0; right:0; top:32px; text-align:center;
  font-size:9px; color:var(--co-amber); font-variant-numeric:tabular-nums; pointer-events:none;
  white-space:nowrap; }

/* Depth-of-field note under the rail — says what the f-stop is doing, or why it is doing nothing. */
.cn-bokehnote{ margin-top:12px; font-size:9px; line-height:1.5; letter-spacing:.02em;
  color:rgba(239,231,214,.42); }
.cn-bokehnote.on{ color:rgba(246,199,156,.72); }

/* ── 22 Grain · the loupe ──────────────────────────────────────────────────────────────────────
   Real grain from the real engine over a flat grey field, not a drawing of grain. Grey rather than
   the photograph on purpose: judging crystal size and chroma is exactly what a picture's own
   detail gets in the way of. */
.cn-grainloupe{ position:relative; margin-top:12px; width:100%; aspect-ratio:1; border-radius:50%;
  overflow:hidden; border:2px solid rgba(239,231,214,.30); cursor:move; touch-action:none;
  background:var(--co-well); }
.cn-grainloupe img{ position:absolute; inset:0; width:100%; height:100%; display:block;
  object-fit:cover; image-rendering:pixelated; pointer-events:none; }
.cn-grainloupe .wait{ position:absolute; left:0; right:0; top:50%; transform:translateY(-50%);
  text-align:center; font-size:9px; letter-spacing:.1em; color:rgba(239,231,214,.38); }
/* Crosshair ticks, as on a real focusing loupe. */
/* The crosshair is part of the loupe component. Grain had it; Finishing — the one instrument
   whose whole claim is that it is judged at 1:1 — did not, so it had no centre reference. */
.cn-grainloupe .tick, .cn-inst-loupe .tick{ position:absolute; background:var(--co-amber); opacity:.65; pointer-events:none; }
.cn-grainloupe .tick.t, .cn-inst-loupe .tick.t{ left:50%; top:4px; width:1px; height:16px; margin-left:-.5px; }
.cn-grainloupe .tick.b, .cn-inst-loupe .tick.b{ left:50%; bottom:4px; width:1px; height:16px; margin-left:-.5px; }
.cn-grainloupe .tick.l, .cn-inst-loupe .tick.l{ top:50%; left:4px; height:1px; width:16px; margin-top:-.5px; }
.cn-grainloupe .tick.r, .cn-inst-loupe .tick.r{ top:50%; right:4px; height:1px; width:16px; margin-top:-.5px; }
/* Format gauge — button widths carry the real frame widths, so the row is itself a scale. */
.cn-gauges{ margin-top:12px; display:flex; align-items:flex-end; gap:5px; }
.cn-gauge{ height:26px; min-width:0; border-radius:6px; background:rgba(239,231,214,.05);
  border:1px solid var(--co-edge-2); color:rgba(239,231,214,.45); font-family:inherit;
  font-size:9px; letter-spacing:.1em; cursor:pointer; padding:0; overflow:hidden; }
.cn-gauge.on{ background:color-mix(in srgb, var(--co-amber) 22%, transparent);
  border-color:var(--co-amber); color:var(--co-amber); }

/* ── 25 Diffusion Filter · the tray ────────────────────────────────────────────────────────────
   Both the hero and every tray cell are the REAL diffusionFilter stage rendered over a synthetic
   target — specular points on a dark ground with a mid-grey wedge — which is what a diffusion
   filter is actually judged on, and the only way the black-halation families' shadow crush and
   the white families' lift are visible at all. The previous preview was a CSS radial gradient:
   it moved with the slider but could not show either. */
/* THE HERO HAS TO BE THE BIGGEST THING. This well — your own frame through the selected filter,
   and the surface you drag to defocus — was 150px under a 250px scrolling tray of 24 swatches.
   The picker was two thirds taller than the thing it picks for, so the eye landed on the tray and
   the instrument read as a caption above a grid. Well up, tray capped: the hero leads, and the
   tray is still a full three rows before it scrolls, which is all a chooser needs to show. */
.cn-diffwell{ position:relative; margin-top:12px; height:228px; border-radius:8px; overflow:hidden;
  background:var(--co-well); border:1px solid var(--co-edge-2);
  cursor:ew-resize; touch-action:none; }
.cn-diffwell .cn-thumbcv{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  display:block; pointer-events:none; }
.cn-diffwell .wait{ position:absolute; left:0; right:0; top:50%; transform:translateY(-50%);
  text-align:center; font-size:9px; letter-spacing:.1em; color:rgba(239,231,214,.38); }
.cn-diffwell .rd{ position:absolute; right:11px; bottom:9px; font-size:9px; letter-spacing:.1em;
  color:var(--co-amber); font-variant-numeric:tabular-nums; pointer-events:none;
  text-shadow:0 1px 6px rgba(0,0,0,.9); }
.cn-difftray{ margin-top:12px; max-height:186px; overflow-y:auto; padding-right:4px;
  display:grid; grid-template-columns:repeat(3,1fr); gap:6px; }
.cn-difftile{ position:relative; aspect-ratio:3/2; border-radius:6px; overflow:hidden; padding:0;
  cursor:pointer; border:1px solid var(--co-edge-2); background:var(--co-well); }
.cn-difftile.on{ border-color:var(--co-amber);
  box-shadow:0 0 0 1px var(--co-amber), 0 0 16px -4px color-mix(in srgb, var(--co-amber) 60%, transparent); }
.cn-difftile .lbl{ position:absolute; left:0; right:0; bottom:0; padding:8px 4px 3px;
  font-size:8px; letter-spacing:.04em; color:rgba(239,231,214,.9); text-align:left;
  background:linear-gradient(180deg, transparent 0%, rgba(6,9,11,.68) 45%, rgba(6,9,11,.97) 100%);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.cn-difftile.on .lbl{ color:var(--co-amber-hi); }

/* ── 20 Film Emulation · a curve you can shape ─────────────────────────────────────────────────
   Knobs ride vertical rails at the toe and shoulder ends of the plot rather than being drawn on
   the curve path: buildFilmToneCurveSVG owns the curve maths, and a handle placed by a second copy
   of that maths would drift off the line it claims to sit on the first time either changed. */
/* The lane is 24px, not 16, and the knob's track stops short of it. At 16px the tag was still
   positioned INSIDE the knob's own box (bottom:2px of a box that ended 16px up), so it was never
   in a lane at all — it just sat in the track's bottom 13px, and any value low enough to put the
   dot there printed the type over it. Measured at shoulder 2.3 on a 0..20 control: 2.3px of
   overlap. 24px clears the lowest reachable dot (centre on the track's own bottom edge, 6.5px
   radius) with room for the 11.3px tag under it. */
.cn-filmcurve-live{ position:relative; padding-bottom:24px; }
.cn-filmknob{ position:absolute; top:0; bottom:24px; width:34px; margin-left:-17px;
  cursor:ns-resize; touch-action:none; }
.cn-filmknob .rail{ position:absolute; left:50%; top:8px; bottom:8px; width:1px; margin-left:-.5px;
  background:rgba(232,134,58,.20); pointer-events:none; }
.cn-filmknob:hover .rail{ background:rgba(232,134,58,.45); }
/* The identity mark: where this control leaves the stock's own curve alone. Bone rather than
   amber, because it is a reference and not a value — the amber on this rail always means the
   thing you are setting. */
.cn-filmknob .neutral{ position:absolute; left:50%; width:9px; height:1px; margin:0 0 0 -4.5px;
  background:rgba(239,231,214,.32); pointer-events:none; }
.cn-filmknob .dot{ position:absolute; left:50%; width:13px; height:13px; margin:0 0 -6.5px -6.5px;
  border-radius:50%; background:var(--co-glass); border:2px solid var(--co-amber);
  box-sizing:border-box; pointer-events:none;
  box-shadow:0 0 12px -2px color-mix(in srgb, var(--co-amber) 60%, transparent); }
/* al (14): "the copy is leaking out of the frame" — the tag hung 20px below its dot, so with the
   toe near the bottom of the plot it fell past the box (measured: SHOULDER 1.1px outside, TOE
   2.8px). The tags sit in a dedicated lane under the plot, on one baseline, which also makes the
   two values comparable instead of floating at whatever height their knob happens to be.
   That fix was inert for a while: the markup carried an inline bottom:<pct>% on the tag as well
   as on the dot, and an inline declaration outranks a stylesheet, so the type went on printing
   over its own knob (measured: 6.5px of overlap on both tags). The inline declaration is gone
   from the tag — do not "fix" any future drift here with !important, which would leave the lie
   sitting in the markup for the next reader. */
.cn-filmknob .tag{ position:absolute; left:50%; top:100%; margin-top:9px; transform:translateX(-50%); white-space:nowrap;
  font-size:7.5px; letter-spacing:.08em; color:var(--co-amber);
  font-variant-numeric:tabular-nums; pointer-events:none; text-shadow:0 1px 5px rgba(0,0,0,.9); }

/* Edge-code list. The key in MEASURED_STOCKS IS the edge code, so this needs no second source. */
/* Same rule, Film Emulation: the characteristic curve is the instrument — it is the module's
   whole subject and the thing you drag — and this list of edge codes is how you pick a stock for
   it. At 230px the list was taller than the curve above it. */
.cn-filmlist{ margin-top:12px; max-height:168px; overflow-y:auto; border-radius:8px;
  background:var(--co-well); border:1px solid var(--co-edge-2); padding:3px; }
.cn-filmrow{ display:flex; align-items:center; gap:10px; width:100%; padding:7px 8px;
  background:none; border:0; border-radius:5px; cursor:pointer; font-family:inherit; text-align:left; }
.cn-filmrow:hover{ background:rgba(239,231,214,.05); }
.cn-filmrow.on{ background:color-mix(in srgb, var(--co-amber) 14%, transparent); }
/* Four sprocket holes, the way an edge code sits beside the perforations on real stock. */
.cn-filmrow .perf{ flex:none; width:26px; height:7px; background-repeat:repeat-x;
  background-size:7px 7px; opacity:.5;
  background-image:linear-gradient(to right, rgba(239,231,214,.75) 0 4px, transparent 4px 7px); }
.cn-filmrow.on .perf{ opacity:1;
  background-image:linear-gradient(to right, var(--co-amber) 0 4px, transparent 4px 7px); }
.cn-filmrow .code{ flex:none; width:52px; font-size:10px; letter-spacing:.04em;
  color:rgba(239,231,214,.72); font-variant-numeric:tabular-nums; }
.cn-filmrow .name{ flex:1; min-width:0; font-size:10px; color:rgba(239,231,214,.58);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.cn-filmrow.on .code, .cn-filmrow.on .name{ color:var(--co-amber); }

/* ── 05 Crop & Straighten ──────────────────────────────────────────────────────────────────────
   The in-frame box MIRRORS the real crop rect; it is not a second editor for it. Opening this
   module arms the real on-canvas overlay (eight handles, rotate gesture, live guide), all writing
   the same stageParams.cropStraighten.crop — a draggable copy here would be a rival author of one
   rectangle. So the corners are drawn where the real handles are, and carry pointer-events:none. */
.cn-cropframe{ position:relative; margin-top:12px; width:100%; border-radius:8px; overflow:hidden;
  background:var(--co-well); box-shadow:inset 0 0 0 1px var(--co-edge-2); }
/* al (7): "the edges need to be in a bit to allow rotation on the preview image."
   The stage is where the photograph and the crop box live; the frame around it is the margin a
   tilted frame needs somewhere to go. 10% is the measured figure for +/-10.2 degrees of visible
   tilt on a 3:2 frame. Percentage insets resolve against the containing block's WIDTH for
   left/right and its HEIGHT for top/bottom, so an equal inset scales both axes by (1-2x) and
   preserves the aspect exactly — no separate correction needed.
   No overflow:hidden here: the tilted corners have to sweep into the margin. The frame still
   clips, which is what stops .cn-cropbox's 9999px scrim greying out the whole panel.
   data-cn-cropframe lives on THIS element, not the frame: wireChainCropFrame derives its 0..1
   crop coordinates from getBoundingClientRect, and the same rect feeds chainCropSurfaceW, so
   leaving the attribute on the padded outer box would offset every crop drag by the margin. */
.cn-cropstage{ position:absolute; inset:10%; }
.cn-cropframe img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  display:block; pointer-events:none; }
.cn-cropbox{ position:absolute; box-sizing:border-box; border:1px solid var(--co-amber);
  box-shadow:0 0 0 9999px rgba(6,9,11,.52); pointer-events:none; }
/* The guide is drawn, not faked. It was four gradients pinned at 33.3% and 66.6% — a
   rule-of-thirds grid whichever of the seven guides was selected — so the instrument and its own
   caption disagreed six times out of seven. The markup is now an SVG built from the same line
   generators the canvas uses; all this has to do is hold it and keep the strokes hairline. */
/* inset:-1px, not 0: an absolutely positioned child is placed against the PADDING box, which
   .cn-cropbox's own 1px border insets — so inset:0 drew the guide across a rect 2px narrower
   than the crop rect the canvas measures its lines against. Pulling it out by the border width
   makes the two agree exactly.
   (Sized explicitly rather than left to inset alone: an SVG carries an intrinsic aspect from its
   viewBox, so width/height:auto squares it off instead of stretching to the offsets.) */
.cn-cropbox svg.guide{ position:absolute; inset:-1px;
  width:calc(100% + 2px); height:calc(100% + 2px);
  overflow:visible; pointer-events:none; }
.cn-cropbox svg.guide line, .cn-cropbox svg.guide path{ fill:none; stroke-width:1;
  vector-effect:non-scaling-stroke;
  stroke:color-mix(in srgb, var(--co-amber) 45%, transparent); }
.cn-cropbox .h{ position:absolute; width:11px; height:11px; border-radius:2px;
  background:var(--co-amber); pointer-events:none; }
.cn-cropbox .h.tl{ left:-6px; top:-6px; }
.cn-cropbox .h.tr{ right:-6px; top:-6px; }
.cn-cropbox .h.bl{ left:-6px; bottom:-6px; }
.cn-cropbox .h.br{ right:-6px; bottom:-6px; }
/* Horizon rule — real ticks, majors every 10th, matching the other tick instruments. */
.cn-crop-rule .t{ position:absolute; top:8px; width:1px; height:10px; margin-left:-.5px;
  background:rgba(239,231,214,.20); }
.cn-crop-rule .t.maj{ top:4px; height:18px; background:rgba(239,231,214,.34); }


/* ── 06 Perspective ────────────────────────────────────────────────────────────────────────────
   Corner handles are two-axis grips onto pVertical/pHorizontal — this engine has no four-corner
   field, the warp is two scalars — with a per-corner sign so that dragging a corner INWARD moves
   that corner. Inward is the only direction the warp has: opening an edge would sample pixels
   that are not in the photograph. A corner already against the frame is therefore at a limit,
   and is drawn hollow to say so rather than sitting solid and ignoring the pointer.
   (This paragraph used to say "away from the centre opens that edge" — the symmetric behaviour
   this stage had before it became inward-only. The signs it justified put four of the sixteen
   gestures 18.6px in the opposite direction to the drag; see chainKeystoneWidgetHTML.) */
.cn-inst-quad{ position:relative; }
/* Hollow when the corner is against the frame and cannot travel further out. The warp is
   inward-only, so a corner sitting on the boundary genuinely has nowhere to go in that direction
   — drawn solid it reads as a handle that ignores you. */
.cn-quad-h.pinned{ background:transparent; box-shadow:inset 0 0 0 2px var(--co-amber),
  0 0 0 2px rgba(11,18,20,.65); }
.cn-quad-h{ position:absolute; width:15px; height:15px; margin:-7.5px 0 0 -7.5px; border-radius:3px;
  background:var(--co-amber); cursor:move; touch-action:none;
  box-shadow:0 0 0 2px rgba(11,18,20,.65), 0 0 14px -2px color-mix(in srgb, var(--co-amber) 65%, transparent); }
.cn-quad-h:hover{ background:var(--co-amber-hi); }

/* ── 07 Lens Corrections ───────────────────────────────────────────────────────────────────────
   Grid in cool ink at 5 lines a side, not 7 in panel-amber: it is a MEASURING instrument, and a
   denser mesh became a texture that hid the very bowing it exists to show. */
.cn-meshwell{ position:relative; margin-top:12px; height:170px; border-radius:8px;
  background:var(--co-well); border:1px solid var(--co-edge-2);
  cursor:ew-resize; touch-action:none; display:flex; align-items:center; justify-content:center; }
.cn-meshwell svg{ display:block; width:76%; height:76%; }
.cn-meshwell .rd{ position:absolute; left:0; right:0; bottom:9px; text-align:center; font-size:9px;
  letter-spacing:.1em; color:var(--co-amber); font-variant-numeric:tabular-nums;
  pointer-events:none; }
/* Fringe check: a real 400% corner from the live preview, nearest-neighbour so fringes stay hard. */
.cn-fringewell{ position:relative; margin-top:12px; height:96px; border-radius:8px; overflow:hidden;
  background:var(--co-well); border:1px solid var(--co-edge-2); }
.cn-fringewell img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  display:block; image-rendering:pixelated; pointer-events:none; }
.cn-fringewell .wait{ position:absolute; left:0; right:0; top:50%; transform:translateY(-50%);
  text-align:center; font-size:9px; letter-spacing:.1em; color:rgba(239,231,214,.38); }
/* al (9): "the copy on blue is hard to read." These legends sit ON the photograph, so their
   contrast was whatever the corner happened to be — measured on the review frame, RED at 1.68:1
   and BLUE at 2.68:1 against what was under them, where small type wants 4.5:1. A text-shadow
   softens an edge; it does not raise a ratio. A plate does, and makes it the same number on
   every photograph. */
.cn-fringewell .lbl{ position:absolute; top:6px; font-size:9px; letter-spacing:.1em;
  pointer-events:none; padding:2px 7px; border-radius:999px;
  background:rgba(6,9,11,.86); box-shadow:0 0 0 1px rgba(239,231,214,.08); }
.cn-fringewell .lbl.r{ left:7px; color:#F08A72; }
.cn-fringewell .lbl.b{ right:7px; color:#8FBCF0; }
/* One roller, because lateral CA is red and blue scaling in opposite directions about a single
   correction — data-cn-drag2 writes caBlue from the same delta with the sign inverted. */
.cn-caroll{ position:relative; margin-top:9px; height:30px; border-radius:6px;
  background:linear-gradient(90deg, rgba(232,112,90,.30), rgba(11,15,20,0) 42%,
                             rgba(11,15,20,0) 58%, rgba(111,168,232,.30));
  border:1px solid var(--co-edge-2); cursor:ew-resize; touch-action:none; }
.cn-caroll .t{ position:absolute; top:10px; width:1px; height:9px; margin-left:-.5px;
  background:rgba(239,231,214,.20); }
.cn-caroll .t.maj{ top:6px; height:17px; background:rgba(239,231,214,.34); }
.cn-caroll .cut{ position:absolute; top:-2px; bottom:-2px; width:2px; margin-left:-1px;
  border-radius:1px; background:var(--co-amber);
  box-shadow:0 0 14px 2px color-mix(in srgb, var(--co-amber) 50%, transparent); }
/* Same problem, same fix: this number sits on a red-to-blue roller, so it had no fixed ground to
   be read against — measured at 1.02:1 against the surface behind it. */
.cn-caroll .rd{ position:absolute; right:7px; top:50%; transform:translateY(-50%); font-size:9px;
  letter-spacing:.1em; color:rgba(239,231,214,.82); font-variant-numeric:tabular-nums;
  pointer-events:none; padding:2px 7px; border-radius:999px;
  background:rgba(6,9,11,.86); box-shadow:0 0 0 1px rgba(239,231,214,.08); }

/* Working-resolution readout in the viewer toolbar. FULL is the settled render; PROXY is the
   cheaper pass the pipeline uses while a drag is in flight, which is genuinely softer on screen —
   naming it turns "the render looks mushy mid-drag" from a bug report into a caption. */
.cn-resread{ font-size:9px; letter-spacing:.10em; color:rgba(239,231,214,.42);
  font-variant-numeric:tabular-nums; white-space:nowrap; }
.cn-resread.proxy{ color:var(--co-amber); }

/* Finishing loupe now holds a real rendered crop rather than a background-image. */
.cn-inst-loupe.photo img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  display:block; pointer-events:none; }

/* Facelift — mode row, selected region, and that region's own controls inline. */
.cn-facemode{ margin-top:12px; display:flex; gap:6px; }
.cn-faceregion.sel{ outline:1px solid var(--co-amber); outline-offset:-1px; }
.cn-faceregion-ctls{ margin-top:2px; }

/* Offset bar under the trackballs — the real offsetMaster, black to white the way the design
   draws it, because that is what lifting every channel together does to the picture. */
.cn-offsetbar{ position:relative; margin-top:12px; height:34px; border-radius:8px;
  background:linear-gradient(90deg, #05070a, #8a8a86 50%, #F2EDE4);
  border:1px solid var(--co-edge-2); cursor:ew-resize; touch-action:none; }
.cn-offsetbar .cut{ position:absolute; top:-3px; bottom:-3px; width:2px; margin-left:-1px;
  border-radius:1px; background:var(--co-amber);
  box-shadow:0 0 14px 2px color-mix(in srgb, var(--co-amber) 55%, transparent); }

/* Colour Zones · hue-vs-saturation pad. Horizontal is the selected band's hue shift, vertical its
   saturation — the two you trade against each other. The gradient is built from that band's OWN
   centre hue (set inline), so the pad shows the range you are moving within, not a generic
   rainbow: the band is already chosen, this is the nudge inside it. Saturation fades downward
   toward grey because that is what taking saturation out of a band does. */
.cn-huepad{ position:relative; margin-top:12px; height:104px; border-radius:8px; overflow:hidden;
  cursor:move; touch-action:none; border:0;
  box-shadow:inset 0 0 0 1px var(--co-edge-2), inset 0 2px 14px -8px rgba(0,0,0,.95);
  background:
    linear-gradient(to bottom, rgba(11,15,20,0) 0%, rgba(11,15,20,.10) 46%, rgba(120,120,116,.55) 100%),
    linear-gradient(90deg, var(--h0), var(--h1) 50%, var(--h2)); }
/* Seats the pad in the panel instead of letting a flat gradient block float on top of it. */
.cn-huepad::after{ content:''; position:absolute; inset:0; pointer-events:none; border-radius:8px;
  background:radial-gradient(120% 130% at 50% 45%, transparent 40%, rgba(6,9,13,.42) 100%); }
.cn-huepad .grid{ position:absolute; inset:0; pointer-events:none;
  background:
    linear-gradient(90deg, transparent 49.6%, rgba(11,15,20,.28) 49.7% 50.3%, transparent 50.4%),
    linear-gradient(to bottom, transparent 49.6%, rgba(11,15,20,.28) 49.7% 50.3%, transparent 50.4%); }
.cn-huepad .puck{ position:absolute; width:14px; height:14px; margin:-7px 0 0 -7px;
  border-radius:50%; background:var(--co-ink); pointer-events:none;
  box-shadow:0 0 0 2px rgba(11,18,20,.7), 0 2px 9px -2px rgba(0,0,0,.85); }

/* ── 13 Light Zones · the orb ──────────────────────────────────────────────────────────────────
   A sphere lit from the REAL az/el, with the key as the thing you drag around it. Everything on
   it is a live field — falloff sets the orbit radius and the shadow's length, litWarm the key's
   colour, fill how far the unlit side lifts — so moving any control makes the orb answer. A flat
   dial could say where the key was; it could never say what the key does. */
/* grab, not move: the key is a bead you carry around the track, placed at your pointer's own
   bearing, so the cursor names a grab rather than a nudge in some direction. */
.cn-orbwrap{ position:relative; margin-top:12px; border-radius:8px; background:var(--co-well);
  border:1px solid var(--co-edge-2); cursor:grab; touch-action:none; overflow:hidden; }
.cn-orbwrap:active{ cursor:grabbing; }
.cn-orbwrap svg{ display:block; width:100%; height:auto; }
.cn-orbwrap .cap{ font-family:inherit; font-weight:400; font-size:8px; letter-spacing:.10em; }
.cn-zoneexps{ margin-top:12px; display:flex; gap:6px; }
.cn-zoneexp{ flex:1; min-width:0; border-radius:8px; padding:9px 10px 8px;
  background:rgba(239,231,214,.04); border:1px solid var(--co-edge-2); }
.cn-zoneexp .dot{ display:block; width:13px; height:13px; border-radius:50%; margin-bottom:7px;
  box-shadow:inset 0 0 0 1px rgba(11,15,20,.4); }
.cn-zoneexp .l{ display:block; font-size:8px; letter-spacing:.12em; color:rgba(239,231,214,.42); }
.cn-zoneexp .v{ display:block; margin-top:3px; font-size:15px; font-weight:300;
  color:var(--co-amber-hi); font-variant-numeric:tabular-nums; }

/* Form controls do not inherit the page font — a <button>, <select>, <input> or <textarea> with
   no font declared falls back to the browser's own 13.3333px Arial. Every such control in this
   console has had to remember to say font-family:inherit, and the ones that forgot were only
   caught by auditing computed styles, because a textless icon button renders identically either
   way and a text one is merely a pixel off. Said once here, so it cannot be forgotten again.
   Urbanist is loaded at 200/300/400/500 ONLY: anything heavier is synthesised into a faux-bold
   with mechanically-thickened strokes, which is a real and surprisingly visible part of what
   makes an interface look cheap. */
.cn-left button, .cn-left select, .cn-left input, .cn-left textarea,
.cn-mod button, .cn-mod select, .cn-mod input, .cn-mod textarea,
.cn-inst button, .cn-inst select, .cn-inst input{ font-family:inherit; }

/* ══ 6 · THE SECOND PASS ══════════════════════════════════════════════════════════════════════
   al: "it all looks very rushed and not very design focused... I want things to be slick and
   stylish not just cheap looking."
   
   He was right, and the cause was not taste but method: each instrument below was styled as it
   was built, in isolation, inventing its own surface every time — declared font-families that
   knocked Urbanist out, 1px borders where this file uses inset hairlines, cream circles where it
   uses amber squares, monospace where it uses none at all. Measured against the design bundle
   (lab/chain-instruments-bundle.html) the app's OWN base was already correct to within a
   half-pixel: section labels 8.5px/1.87px/.46 with a .09 top rule, the display window 200-weight
   at 30px in #F6C79C. Only the new work had drifted off it.
   
   So this block declares nothing of its own. Every surface is --co-glass or --co-well behind an
   inset --co-edge-2 hairline; every label is 9px at .1em; every chip follows .cn-plateb; every
   number is --co-amber-hi; and font-family is never set, so Urbanist reaches all of it. */

/* ── 05 Crop · the eight handles, and the ratio list ───────────────────────────────────────────
   The frame and the box above are unchanged and already right. These are the missing pieces: the
   controller has always had eight handles and the box drew four, and the ratio list became a
   <select> that had no styling at all — a raw browser dropdown sitting in the middle of a
   darkroom console, which is exactly the "cheap" al could see. Handles are AMBER SQUARES because
   that is what .cn-cropbox .h already is; they are not restyled, only positioned. */
/* INSIDE the box, not straddling it. The frame clips (it has to — .cn-cropbox's scrim is a
   9999px spread box-shadow, and without overflow:hidden it would grey out the whole panel), so
   handles hung at -6px are half-clipped at the frame edge — and the DEFAULT crop is the full
   frame, which meant that out of the box not one of the eight was reachable. drawCropOverlay
   already solved exactly this on the canvas by insetting a handle that sits on the boundary; the
   comment there records that testing only the true corner made "aiming at the middle of the
   handle you can see" a miss. Same fix, applied unconditionally, which also keeps the eight in
   one place instead of moving them about as the rect nears an edge. */
.cn-cropbox .h.nw{ left:2px;  top:2px; }
.cn-cropbox .h.n { left:calc(50% - 5.5px); top:2px; }
.cn-cropbox .h.ne{ right:2px; top:2px; }
.cn-cropbox .h.e { right:2px; top:calc(50% - 5.5px); }
.cn-cropbox .h.se{ right:2px; bottom:2px; }
.cn-cropbox .h.s { left:calc(50% - 5.5px); bottom:2px; }
.cn-cropbox .h.sw{ left:2px;  bottom:2px; }
.cn-cropbox .h.w { left:2px;  top:calc(50% - 5.5px); }
/* The edge handles are the quieter pair — a corner sets two axes, an edge one — so they read
   smaller until you are on them. Lit from the controller's OWN hit-test, so what brightens is
   exactly what a press takes hold of. */
.cn-cropbox .h.n, .cn-cropbox .h.s, .cn-cropbox .h.e, .cn-cropbox .h.w{
  width:9px; height:9px; opacity:.8; }
.cn-cropstage[data-cn-crop-hover] .cn-cropbox .h{ transition:transform .12s ease, box-shadow .12s ease; }
.cn-cropstage[data-cn-crop-hover="nw"] .h.nw, .cn-cropstage[data-cn-crop-hover="n"] .h.n,
.cn-cropstage[data-cn-crop-hover="ne"] .h.ne, .cn-cropstage[data-cn-crop-hover="e"] .h.e,
.cn-cropstage[data-cn-crop-hover="se"] .h.se, .cn-cropstage[data-cn-crop-hover="s"] .h.s,
.cn-cropstage[data-cn-crop-hover="sw"] .h.sw, .cn-cropstage[data-cn-crop-hover="w"] .h.w{
  opacity:1; transform:scale(1.25);
  box-shadow:0 0 0 2px rgba(11,18,20,.7), 0 0 12px -2px color-mix(in srgb, var(--co-amber) 70%, transparent); }
.cn-cropstage[data-cn-crop-hover="move"] .cn-cropbox{ border-color:var(--co-amber-hi); }
.cn-cropstage[data-cn-cropframe]{ cursor:default; touch-action:none; user-select:none; }
/* A ratio is one exclusive choice from thirteen, so it is a list — but dressed as a plate, not as
   the operating system's dropdown. Same height, radius, tracking and colour as .cn-crop-ratio was. */
.cn-cropratio-sel{ width:100%; height:34px; margin-top:12px; padding:0 30px 0 11px;
  appearance:none; -webkit-appearance:none; cursor:pointer;
  font-family:inherit; font-size:9px; letter-spacing:.1em; color:rgba(239,231,214,.62);
  border:1px solid var(--co-edge-2); border-radius:6px; background-color:rgba(239,231,214,.05);
  background-image:linear-gradient(45deg, transparent 50%, rgba(239,231,214,.42) 50%),
                   linear-gradient(135deg, rgba(239,231,214,.42) 50%, transparent 50%);
  background-position:calc(100% - 16px) calc(50% - 1px), calc(100% - 11px) calc(50% - 1px);
  background-size:5px 5px, 5px 5px; background-repeat:no-repeat;
  transition:border-color .14s ease, color .14s ease; }
.cn-cropratio-sel:hover{ color:var(--co-amber-hi);
  border-color:color-mix(in srgb, var(--co-amber) 45%, transparent); }
.cn-cropratio-sel:focus{ outline:none; border-color:var(--co-amber); color:var(--co-amber); }
.cn-cropratio-sel option{ background:var(--co-panel); color:var(--co-ink); }

/* ── 18 Masks · the in-frame ───────────────────────────────────────────────────────────────────
   Identical surface to the crop frame, because it is the same idea and should not look like a
   different one: the photograph, recessed behind an inset hairline, with the shapes over it. */
.cn-maskframe{ position:relative; margin-top:12px; width:100%; border-radius:8px; overflow:hidden;
  background:var(--co-well); box-shadow:inset 0 0 0 1px var(--co-edge-2);
  touch-action:none; user-select:none; }
.cn-maskframe img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  display:block; opacity:.5; pointer-events:none; }
.cn-maskframe svg{ position:absolute; inset:0; width:100%; height:100%; pointer-events:none;
  overflow:visible; }
/* Two rows of plates, one per question — draw it yourself, or let the model find it. */
.cn-maskai{ display:flex; flex-wrap:wrap; gap:5px; margin-top:12px; }
.cn-maskai .cn-plateb{ flex:1 1 auto; min-width:64px; }

/* ── 09 Tone Curve · the frame's own tones behind it ───────────────────────────────────────────
   The histogram is the whole reason this reads as a photograph rather than as maths, so it is not
   decoration and does not get decoration's opacity — but it must stay under the curve, so it sits
   at .34 and the plot keeps full contrast above it. */
.cn-curve-wrap{ position:relative; }
.cn-curve-histo{ position:absolute; inset:0; width:100%; height:100%; opacity:.34;
  pointer-events:none; border-radius:8px; }
.cn-curve-svg{ position:relative; z-index:1; }
.cn-curvechans{ display:flex; gap:5px; margin-top:12px; }
.cn-curvechan{ flex:1; min-width:0; height:34px; border-radius:6px; cursor:pointer; padding:0;
  font-family:inherit; font-size:9px; letter-spacing:.1em; color:rgba(239,231,214,.5);
  border:1px solid var(--co-edge-2); background:transparent;
  transition:color .14s ease, border-color .14s ease, background .14s ease; }
.cn-curvechan:hover{ color:var(--co-amber-hi);
  border-color:color-mix(in srgb, var(--co-amber) 45%, transparent); }
.cn-curvechan.on{ color:var(--co-amber); border-color:var(--co-amber);
  background:color-mix(in srgb, var(--co-amber) 12%, transparent); }
/* A channel button says which channel it is by BEING that channel when it is on. */
.cn-curvechan.ch-red.on{ color:#E5544C; border-color:#E5544C;
  background:color-mix(in srgb, #E5544C 12%, transparent); }
.cn-curvechan.ch-green.on{ color:#4FB35C; border-color:#4FB35C;
  background:color-mix(in srgb, #4FB35C 12%, transparent); }
.cn-curvechan.ch-blue.on{ color:#5A8AD0; border-color:#5A8AD0;
  background:color-mix(in srgb, #5A8AD0 12%, transparent); }
.cn-curve-axis{ display:flex; justify-content:space-between; margin-top:9px;
  font-size:9px; letter-spacing:.1em; color:rgba(239,231,214,.30); }

/* ── 09 Sky · what it found, what it will touch, and what it did ───────────────────────────────
   Teal is the app's one secondary and it already means "measured" — so teal is what the detector
   FOUND. Amber means live, so amber is the subset the stage will actually change once Blown
   threshold has had its say. The two readings are the point of the window. */
.cn-inst-frame[data-cn-skyframe]{ cursor:crosshair; touch-action:none; user-select:none; }
/* The ledger — measured numbers that were being computed and thrown away. Uses the shared stat
   card below rather than a third bespoke figure layout. */
/* ── 17 Highlight Roll-off · the step wedge ────────────────────────────────────────────────────
   Two strips of hard-edged bands in one recessed bay: what the frame does with no roll-off, and
   what it does through the real curve. A wedge has STEPS, so the bands are hard stops — a
   gradient between them would be a different tool answering a different question. */
.cn-rollwedge{ position:relative; margin-top:12px; padding:24px 12px 15px; border-radius:8px;
  background:var(--co-well); border:1px solid var(--co-edge-2);
  box-shadow:inset 0 2px 12px -6px rgba(0,0,0,.9);
  cursor:ew-resize; touch-action:none; }
.cn-rollrow{ display:flex; align-items:center; gap:9px; }
.cn-rollrow + .cn-rollrow{ margin-top:7px; }
.cn-rollrow .lb{ flex:0 0 62px; font-size:8px; letter-spacing:.14em;
  color:rgba(239,231,214,.38); text-align:right; }
.cn-rollrow .lb.on{ color:var(--co-amber); }
.cn-rollrow .strip{ flex:1; height:38px; border-radius:3px; display:block;
  box-shadow:inset 0 0 0 1px rgba(239,231,214,.07); }
/* The two marks that make the wedge read as a scale rather than as two grey bars: where diffuse
   white falls, and where the shoulder starts. Both sit on the axis they belong to — a calc() that
   walks the STRIP's own coordinate frame, not a bare percentage plus a margin patch. `left:P%` on
   an absolutely-positioned child resolves against the ancestor's PADDING box, and margin-left is
   added on top of that — so a plain `left:${pct}%; margin-left:71px` was mixing a px origin with
   a %-of-a-different-box scale. The strip column starts 84px into the wedge's OUTER edge (1 border
   + 12 padding + the .lb column's 62 + the row's 9 gap) and is W-97px wide (border+padding on both
   sides, plus .lb and the gap once); at the shipped default knee (0.70) that put the KNEE mark
   +38.9px off its own strip — 17% of it — and at the top of the scale it landed 70px past the
   wedge's own right edge, off the instrument entirely.
   calc(83px + (100% - 95px)*frac) is exact, not approximate: the % term still resolves against
   the padding box (width W-2, both borders excluded), so it evaluates to 83 + frac*(W-2-95) =
   83 + frac*(W-97); adding back the 1px border the padding box itself excludes gives an absolute
   position of 84 + frac*(W-97) — exactly stripLeft(84) + frac*stripWidth(W-97). 83/95 duplicate
   four other declared values (this box's own border/padding, .cn-rollrow's gap, .lb's flex-basis)
   on purpose, matching them exactly rather than re-deriving a fifth constant; if any of those four
   ever change, these two have to change with them or the mark drifts off the strip again. */
/* The two marks sit on OPPOSITE sides of the wedge — white above, knee below — because they are
   often within a few percent of each other (with no headroom the white point IS diffuse white,
   about eight percent from a default knee) and two labels on the same baseline collide into one
   unreadable word. Staggering them means they can never overlap at any setting. */
.cn-rollmark{ position:absolute; top:10px; bottom:22px; width:0; pointer-events:none; }
.cn-rollmark i{ position:absolute; top:0; bottom:0; left:-0.5px; width:1px; display:block; }
.cn-rollmark b{ position:absolute; left:0; transform:translateX(-50%);
  font-size:7.5px; font-weight:400; letter-spacing:.12em; white-space:nowrap; }
.cn-rollmark.knee b{ bottom:-16px; }
.cn-rollmark.white b{ top:-13px; }
.cn-rollmark.white{ top:22px; }
.cn-rollmark.knee i{ background:var(--co-amber);
  box-shadow:0 0 6px 0 color-mix(in srgb, var(--co-amber) 55%, transparent); }
.cn-rollmark.knee b{ color:var(--co-amber); }
.cn-rollmark.white i{ background:repeating-linear-gradient(180deg,
  rgba(239,231,214,.42) 0 3px, transparent 3px 6px); }
.cn-rollmark.white b{ color:rgba(239,231,214,.36); }

/* ── the shared stat card ──────────────────────────────────────────────────────────────────────
   One figure, one label, in the pattern .cn-zoneexp already established for Light Zones' three
   exposures: a small raised card, the label tracked and quiet above, the number large and light
   in --co-amber-hi below. Roll-off and Sky both needed to report measured numbers and both had
   grown their own two-column grid, which wrapped into ragged three-line labels the moment the
   panel was its real 330px wide. This is the one shape they should both have been using. */
.cn-statrow{ margin-top:12px; display:flex; flex-wrap:wrap; gap:6px; }
.cn-stat{ flex:1 1 calc(50% - 3px); min-width:0; border-radius:8px; padding:9px 10px 8px;
  background:rgba(239,231,214,.04); border:1px solid var(--co-edge-2); }
.cn-stat .l{ display:block; font-size:8px; letter-spacing:.12em; line-height:1.4;
  color:rgba(239,231,214,.42); text-transform:uppercase; }
.cn-stat .v{ display:block; margin-top:4px; font-size:15px; font-weight:300; line-height:1.1;
  color:var(--co-amber-hi); font-variant-numeric:tabular-nums; }
.cn-stat .v .u{ font-size:9px; letter-spacing:.08em; color:rgba(246,199,156,.55); margin-left:3px; }

/* ── 17 Colour Zones · the dominance wheel ─────────────────────────────────────────────────────
   al: "this still looks pretty cheap and nasty."
   
   It was styled by the legacy Bench rules, which predate this console palette and fight it: a
   1.5px stroke in --bg drew a hard black outline around every wedge, .edited added
   drop-shadow(0 0 3px rgba(255,255,255,.55)) — a white blur, the one thing this palette has no
   place for — and .selected ringed the wedge in --lime, a colour that exists nowhere else on the
   page. Eight flat 70%/55% fills with black outlines and a halo is a pie chart, not an instrument.
   
   These rules load last and re-point all of it at the console: gradient fills so a wedge is a lit
   surface, no stroke, the selected one lit in ITS OWN hue, and the ring scale drawn behind so the
   heights read as the measurement the section label says they are. */
.hsl-wedge{ stroke:none; cursor:pointer; opacity:.52;
  transition:opacity .16s ease, filter .16s ease; }
.hsl-wedge:hover{ opacity:.85; }
/* The halo was the loudest thing on the panel — and it lands on whichever band happens to be
   most present, so a photograph with one strong colour lit a big saturated wedge and blew the
   quiet, precise instrument around it. Selection is carried by opacity first (.52 -> 1, which is
   the system's own way of saying live) and the glow is now a hint that the wedge is emitting
   rather than a light source in its own right. */
.hsl-wedge.selected{ opacity:1;
  filter:drop-shadow(0 0 5px color-mix(in srgb, var(--band-hi) 34%, transparent)); }
/* Edited is a state, not a glow: a quiet cream rim on the wedge itself. */
.hsl-wedge.edited{ stroke:rgba(239,231,214,.55); stroke-width:1; filter:none; }
.hsl-wedge.selected.edited{ stroke:var(--co-amber-hi); stroke-width:1.25;
  filter:drop-shadow(0 0 5px color-mix(in srgb, var(--band-hi) 34%, transparent)); }
/* The scale the wedge heights are read against. */
.hsl-ring{ fill:none; stroke:rgba(239,231,214,.10); stroke-width:1; }
.hsl-ring.dim{ stroke:rgba(239,231,214,.05); stroke-dasharray:2 4; }
/* Lit from just above centre so it reads as a recessed disc carrying a reading, not a hole cut in
   the middle of the instrument — which is what a flat --co-glass fill at this radius looked like. */
.hsl-wheel-hub{ stroke:var(--co-edge-2); stroke-width:1; }
/* The hub names the band the controls below are about to edit, and stops there. It used to print
   that band's coverage at 22px — the same fact the wheel already draws as radius, under a section
   label that says so in words, directly beneath the display window's own 30px figure. Two big
   numbers stacked is two heroes, and an instrument with two heroes has none. This is a label, so
   it is set as one. */
.hsl-hub-name{ font-family:inherit; font-size:9px; font-weight:400; letter-spacing:.24em;
  fill:rgba(239,231,214,.5); }
.hsl-wheel-wrap{ margin:12px 0 0; }
/* The band row follows .cn-plateb like every other chip row, with the band's colour as a dot
   rather than as a tinted slab behind the label — eight tinted slabs is the "cheap" reading. */
/* Eight bands on a 363px panel is two rows of four — a wrapping flex row gave 5 + 3, which reads
   as an accident rather than a layout. */
.hsl-band-btns{ display:grid; grid-template-columns:repeat(4,1fr); gap:5px; margin-top:12px; }
.hsl-band-btn{ min-width:0; height:34px; padding:0 8px; border-radius:6px;
  display:inline-flex; align-items:center; justify-content:center; gap:6px; cursor:pointer;
  font-family:inherit; font-size:9px; letter-spacing:.08em; color:rgba(239,231,214,.5);
  border:1px solid var(--co-edge-2); background:transparent; opacity:1;
  transition:color .14s ease, border-color .14s ease, background .14s ease; }
.hsl-band-btn .dot{ flex:0 0 auto; width:7px; height:7px; border-radius:50%;
  background:var(--band-color); box-shadow:inset 0 0 0 1px rgba(11,15,20,.35); }
.hsl-band-btn .nm{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.hsl-band-btn:hover{ color:var(--co-amber-hi); filter:none;
  border-color:color-mix(in srgb, var(--co-amber) 45%, transparent); }
.hsl-band-btn.selected{ color:var(--co-amber); border-color:var(--co-amber);
  background:color-mix(in srgb, var(--co-amber) 12%, transparent); }
.hsl-band-btn.edited{ box-shadow:none; }
.hsl-band-btn.edited .dot{ box-shadow:inset 0 0 0 1px rgba(11,15,20,.35),
  0 0 0 2px color-mix(in srgb, var(--co-ink) 35%, transparent); }


/* ── the split-toning pair ─────────────────────────────────────────────────────────────────────
   These sit directly under the dominance wheel, so they have to belong to it. Left as full-strength
   conic rainbows they read as a colour picker bolted onto an instrument. The conic stays — hue IS
   angular — but it is damped toward the panel and the saturation wash over it is deepened, so the
   pair reads as the same family of muted, lit material as the wedges above. */
/* No filter: the conic's own stops are the muted band hues now, so damping the whole element
   would take a second bite out of colours that are already where they should be. */
.cn-wheel::after{ content:''; position:absolute; inset:0; border-radius:50%; pointer-events:none;
  box-shadow:inset 0 0 0 1px rgba(239,231,214,.08), inset 0 6px 18px -8px rgba(0,0,0,.8); }
.cn-wheel-puck{ background:var(--co-ink);
  box-shadow:0 0 0 2px rgba(11,18,20,.7), 0 2px 9px -2px rgba(0,0,0,.85); }

/* ══ SILKSCREEN ═══════════════════════════════════════════════════════════════════════════════
   The legends on a real console are ink sitting ON the panel, not light coming out of it. A
   single hard black shadow one pixel below is the whole trick: it gives the glyph an edge where
   it meets the face, so it reads as printed rather than as text drawn in a box. Hard (0 blur) on
   purpose — a soft shadow reads as a drop shadow, which is a web idiom; this is a printing one.

   Applied only to the small tracked uppercase legends, never to the display-window value (that
   one is EMITTED — it has a warm glow instead, which is the opposite treatment and the reason
   the two never read as the same kind of thing) and never to the one line of prose in the design. */
.cn-section-label > span,
.cn-dispwin-label,
.cn-kends > span,
.cn-stat .l,
.cn-zoneexp .l,
.cn-skyledger .l,
.cn-curve-axis > span,
.cn-advanced-head,
.cn-mod-resetfoot span,
.cn-tbar-ends > span,
.cn-inst-stat .l{
  text-shadow:0 1px 0 rgba(0,0,0,.55);
}

/* ══ RADII ════════════════════════════════════════════════════════════════════════════════════
   Three sizes carry meaning and the rest was drift. 8px is a panel FACE, 6px is GLASS or a
   control cut into a face, 999px is a CAPSULE and only appears where the pill shape itself says
   something (a switch track, a badge, a status bead). Everything that had reached 9, 10, 11, 12
   or 14 was somebody reaching for "a bit rounder" — which is exactly how a set of parts stops
   looking machined. Collapsed to 8 at each site rather than overridden in one block, so there is
   one truth per declaration.

   The small ladder (1–5px) stays as it is: those are ticks, knobs, beads and cuts, where the
   radius is a chamfer rather than a corner. */

/* ══ THE FOUR STATES ══════════════════════════════════════════════════════════════════════════
   al: "buttons need to have stages — right now it's not clear if they are clicked or not."

   He is right and it was worse than it looked: an audit of all 35 clickable classes in the
   instrument layer found NOT ONE with an :active state, and most had no :hover either. Every
   control in the app went straight from rest to selected with nothing in between, so a press
   produced no acknowledgement at all — you clicked, and the only evidence was whether the thing
   you clicked happened to change something visible somewhere else.

   Four states, stated once here rather than per control, because the whole point is that they
   are the same everywhere:

     REST     a flat face with a hairline. The control is available.
     HOVER    ink lifts, hairline warms toward amber. "This is a thing you can press."
     PRESS    the face goes INTO the panel — an inset shadow, ink dimmed a touch. On a console a
              key goes down and into shadow when you push it, and that is the whole of the
              feedback: mechanical, immediate, gone the moment you let go.
     ON       amber wash, amber rim, amber ink. Already existed on most controls.

   PRESS DELIBERATELY TOUCHES ONLY SHADOW AND INK — never background or border — so it composes
   with both rest AND on. Pressing a selected chip still reads as pressed, which it would not if
   :active fought the amber for the same properties.

   Press is fast (.06s) and release eases back (.14s): a key falls quicker than it returns. */
.cn-plateb, .cn-inst-action, .cn-dest, .cn-coat, .cn-grade, .cn-faceregion, .cn-curvechan,
.hsl-band-btn, .cn-gauge, .cn-filmrow, .cn-mffmt, .cn-kcell, .cn-step, .cn-cta,
.cn-difftile, .cn-contact-cell, .cn-mfstock, .cn-dirt-tile, .cn-zoneblock, .cn-swatch,
.cn-inst-swatch, .cn-negswatch{
  transition:background .14s ease, border-color .14s ease, color .14s ease,
             box-shadow .14s ease, opacity .14s ease;
}
/* HOVER — for the two thirds of controls that had none. Kept to ink and hairline so it never
   competes with the amber that means "selected". */
.cn-coat:hover, .cn-dest:hover, .cn-faceregion:hover, .cn-gauge:hover, .cn-mffmt:hover,
.cn-step:hover, .cn-swatch:hover, .cn-inst-swatch:hover,
.cn-negswatch:hover, .hsl-band-btn:hover{
  border-color:color-mix(in srgb, var(--co-amber) 40%, transparent);
  color:var(--co-amber-hi);
}
/* Cells holding a canvas or a photograph get a RING rather than a face change — there is a
   picture where the background would be, and dimming it would be dimming the thing you are
   trying to judge. */
.cn-difftile:hover, .cn-contact-cell:hover, .cn-dirt-tile:hover, .cn-mfstock:hover .cell{
  outline:1px solid color-mix(in srgb, var(--co-amber) 55%, transparent); outline-offset:-1px;
}
/* PRESS — the face goes into the panel. */
.cn-plateb:active, .cn-inst-action:active, .cn-dest:active, .cn-coat:active, .cn-grade:active,
.cn-faceregion:active, .cn-curvechan:active, .hsl-band-btn:active, .cn-gauge:active,
.cn-filmrow:active, .cn-mffmt:active, .cn-kcell:active, .cn-step:active, .cn-cta:active,
.cn-difftile:active, .cn-contact-cell:active, .cn-mfstock:active, .cn-dirt-tile:active,
.cn-zoneblock:active, .cn-swatch:active, .cn-inst-swatch:active, .cn-negswatch:active{
  box-shadow:inset 0 2px 6px -1px rgba(0,0,0,.75), inset 0 0 0 1px rgba(0,0,0,.35);
  filter:brightness(.88);
  transition-duration:.06s;
}
/* The primary CTA is already a lit amber face, so it presses by dimming rather than by shadow —
   a lamp being pushed in, not a key. */
.cn-inst-action.primary:active, .cn-cta:active{
  filter:brightness(.86); box-shadow:inset 0 2px 7px -1px rgba(0,0,0,.45);
}
@media (prefers-reduced-motion: reduce){
  .cn-plateb, .cn-inst-action, .cn-dest, .cn-coat, .cn-grade, .cn-faceregion, .cn-curvechan,
  .hsl-band-btn, .cn-gauge, .cn-filmrow, .cn-mffmt, .cn-kcell, .cn-step, .cn-cta,
  .cn-difftile, .cn-contact-cell, .cn-mfstock, .cn-dirt-tile, .cn-zoneblock, .cn-swatch,
  .cn-inst-swatch, .cn-negswatch{ transition:none; }
}

/* ── 22 Grain · the four-quadrant loupe ────────────────────────────────────────────────────────
   The loupe is a comparison instrument, so it compares: four real renders of the same grey field
   through the same physical model at four speeds, hard-divided, each labelled and clickable. The
   dividers are drawn as a single cross rather than four borders so the meeting point is one clean
   join instead of four abutting lines. */
.cn-quadrule{ position:absolute; pointer-events:none; z-index:2;
  background:rgba(6,9,11,.62); box-shadow:0 0 0 0.5px rgba(239,231,214,.14); }
.cn-quadrule.v{ left:50%; top:0; bottom:0; width:1px; margin-left:-0.5px; }
.cn-quadrule.h{ top:50%; left:0; right:0; height:1px; margin-top:-0.5px; }
/* All four labels sit toward the CENTRE of the loupe — top row at the bottom of their cell,
   bottom row at the top of theirs. The loupe is a circle and the cells are squares, so a label
   parked at the outer edge of a bottom cell falls in the corner the circle clips away. Pulled
   inward they land in the widest part of the disc, where there is room for all four. */
.cn-quadcell{ position:absolute; width:50%; height:50%; padding:0; margin:0; z-index:3;
  background:none; border:0; cursor:pointer; font-family:inherit;
  display:flex; justify-content:center; }
/* The labels clear the zero disc at the centre — far enough out not to crowd it, far enough in
   that the circle does not clip them. */
.cn-quadcell.top{ align-items:flex-end; }
.cn-quadcell.bottom{ align-items:flex-start; }
.cn-quadcell.top .lb{ margin-bottom:34px; }
.cn-quadcell.bottom .lb{ margin-top:34px; margin-bottom:0; }

/* THE ZERO POINT — a clean disc of the same grey field, no grain on it, exactly where the four
   quadrants meet. Every speed touches the reference along its own inner corner, so the comparison
   is against nothing rather than against a memory of nothing. Deliberately not a control: it is a
   null, and "no grain" is what the module's switch is for. */
.cn-quadzero{ position:absolute; left:50%; top:50%; width:54px; height:54px; z-index:4;
  margin:-27px 0 0 -27px; border-radius:50%; background:#7a7a76; padding:0; border:0;
  cursor:pointer; font-family:inherit;
  display:flex; align-items:center; justify-content:center;
  box-shadow:0 0 0 1px rgba(6,9,11,.72), 0 0 0 2px rgba(239,231,214,.18),
             inset 0 1px 3px rgba(0,0,0,.28);
  transition:box-shadow .14s ease; }
.cn-quadzero:hover{ box-shadow:0 0 0 1px rgba(6,9,11,.72), 0 0 0 2px var(--co-amber),
  0 0 16px -2px color-mix(in srgb, var(--co-amber) 60%, transparent),
  inset 0 1px 3px rgba(0,0,0,.28); }
.cn-quadzero:active{ box-shadow:0 0 0 1px rgba(6,9,11,.72), 0 0 0 2px var(--co-amber),
  inset 0 2px 6px rgba(0,0,0,.45); }
.cn-quadzero .lb{ font-size:9px; letter-spacing:.1em; color:rgba(11,15,20,.62);
  font-variant-numeric:tabular-nums; transition:color .14s ease; }
.cn-quadzero:hover .lb{ color:rgba(11,15,20,.9); }
.cn-quadcell .lb{ margin-bottom:9px; padding:2px 6px; border-radius:999px;
  font-size:8px; letter-spacing:.1em; font-variant-numeric:tabular-nums;
  color:rgba(239,231,214,.62); background:rgba(6,9,11,.62);
  box-shadow:inset 0 0 0 1px rgba(239,231,214,.10);
  transition:color .14s ease, background .14s ease, box-shadow .14s ease; }
.cn-quadcell:hover .lb{ color:var(--co-amber-hi);
  box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--co-amber) 45%, transparent); }
/* The speed you are actually on is ringed rather than merely tinted — on a field of grain a
   colour change alone is easy to lose. */
.cn-quadcell.on .lb{ color:#1A0D03; background:var(--co-amber);
  box-shadow:0 0 0 1px var(--co-amber), 0 0 12px -2px color-mix(in srgb, var(--co-amber) 70%, transparent); }

/* ── Histogram channel toggles (Exposure & Colour) ────────────────────────────────────────────
   al: "if say the blue is high in the mid tones I can click the blue toggle and pull it in line."
   Sits inline at the right end of the histogram's own section label, so the choice reads as a
   property OF that instrument rather than a separate row of controls above it. Deliberately
   small and quiet: this retargets the five grips, it does not change what the histogram draws —
   every channel stays visible, which is the whole point of choosing one to pull.
   Colours match .cn-curvechan exactly, because these grips and the Tone Curve module's channel
   tabs edit the SAME per-channel curve — two surfaces onto one thing should not disagree. */
.cn-section-label-row{ display:flex; align-items:center; justify-content:space-between; gap:10px; }
.cn-section-label-row > span:first-child{ min-width:0; }
.cn-histochans{ display:flex; gap:3px; flex-shrink:0; }
.cn-histochan{ width:22px; height:18px; border-radius:4px; cursor:pointer; padding:0;
  font-family:inherit; font-size:9px; letter-spacing:.06em; color:rgba(239,231,214,.42);
  border:1px solid var(--co-edge-2); background:transparent;
  transition:color .14s ease, border-color .14s ease, background .14s ease; }
.cn-histochan:hover{ color:var(--co-amber-hi);
  border-color:color-mix(in srgb, var(--co-amber) 45%, transparent); }
.cn-histochan.on{ color:var(--co-amber); border-color:var(--co-amber);
  background:color-mix(in srgb, var(--co-amber) 12%, transparent); }
.cn-histochan.ch-red.on{ color:#E5544C; border-color:#E5544C;
  background:color-mix(in srgb, #E5544C 14%, transparent); }
.cn-histochan.ch-green.on{ color:#4FB35C; border-color:#4FB35C;
  background:color-mix(in srgb, #4FB35C 14%, transparent); }
.cn-histochan.ch-blue.on{ color:#5A8AD0; border-color:#5A8AD0;
  background:color-mix(in srgb, #5A8AD0 14%, transparent); }

/* A hint that is reporting REAL DAMAGE rather than offering context — the reconstructed headroom
   being clipped a second time by too low a white point. Amber rather than the usual quiet grey,
   because the difference between "here is how this works" and "this setting is throwing your
   rebuilt detail away" has to be visible without reading. */
.cn-inst-hint.warn{ color:var(--co-amber); }

/* ── Per-module presets ───────────────────────────────────────────────────────────────────────
   al: "It should be minimised as default." A quiet single row above the display window, so every
   tool gains a preset library without any of them gaining a permanent block of chrome. Styled off
   .cn-collapsible, the drawer primitive already used for control groups, so an opened preset bar
   reads as the same kind of thing as an opened group rather than a new species of panel. */
.cn-mpreset{ margin-bottom:10px; border:1px solid var(--co-edge-2); border-radius:7px;
  background:color-mix(in srgb, var(--co-well) 60%, transparent); overflow:hidden; }
.cn-mpreset-head{ display:flex; align-items:center; gap:7px; width:100%; padding:7px 10px;
  background:none; border:0; cursor:pointer; font-family:inherit; font-size:9px;
  letter-spacing:.16em; text-transform:uppercase; color:rgba(239,231,214,.5); }
.cn-mpreset-head:hover{ color:var(--co-amber-hi); }
.cn-mpreset-chev{ width:12px; height:12px; flex-shrink:0; fill:none; stroke:currentColor;
  stroke-width:1.6; transform:rotate(-90deg); transition:transform .16s ease; }
.cn-mpreset.open .cn-mpreset-chev{ transform:rotate(0deg); }
.cn-mpreset-count{ margin-left:auto; font-size:9px; letter-spacing:.04em;
  color:var(--co-amber); opacity:.85; }
.cn-mpreset-body{ display:flex; align-items:center; gap:6px; flex-wrap:wrap;
  padding:0 10px 9px; }
.cn-mpreset-pick{ flex:1; min-width:120px; height:26px; border-radius:5px; padding:0 7px;
  font-family:inherit; font-size:10px; color:var(--co-ink);
  background:var(--co-well); border:1px solid var(--co-edge-2); }
.cn-mpreset-empty{ flex:1; min-width:120px; font-size:9px; letter-spacing:.03em;
  color:rgba(239,231,214,.34); }
.cn-mpreset-save,.cn-mpreset-del{ height:26px; padding:0 10px; border-radius:5px; cursor:pointer;
  font-family:inherit; font-size:9px; letter-spacing:.08em; text-transform:uppercase;
  background:transparent; border:1px solid var(--co-edge-2); color:rgba(239,231,214,.6);
  transition:color .14s ease, border-color .14s ease; }
.cn-mpreset-save:hover{ color:var(--co-amber); border-color:var(--co-amber); }
.cn-mpreset-del:hover{ color:#E5544C; border-color:#E5544C; }

/* Tool activation spinner — al: "a little loading circle next to the Hal light would work to
   distract the user... right now that second feels like limbo." Sits immediately before the dirty
   lamp so the two read as one status cluster. Only ever visible between the switch flipping and
   the first render landing; the switch itself is already in its new position underneath it, so
   this says "coming", never "waiting to start". */
.cn-mod-busy{ flex:none; width:9px; height:9px; margin-right:1px; border-radius:50%;
  border:1.5px solid color-mix(in srgb, var(--co-amber) 26%, transparent);
  border-top-color:var(--co-amber); opacity:0; transform:scale(.6);
  transition:opacity .12s ease, transform .12s ease; }
.cn-mod.is-busy .cn-mod-busy{ opacity:1; transform:scale(1); animation:cnModSpin .62s linear infinite; }
@keyframes cnModSpin{ to{ transform:rotate(360deg); } }
@media (prefers-reduced-motion: reduce){
  .cn-mod.is-busy .cn-mod-busy{ animation:none; }
}

/* "Set focus point" — al: "if this requires a button at the top add it." Sits inline on the Depth
   of field label so the instruction is beside the thing it acts on, not buried in a note under it.
   Lit once a focal point exists, so the button also reports state rather than only offering an
   action. */
.cn-focusarm{ height:19px; padding:0 8px; border-radius:4px; cursor:pointer; flex-shrink:0;
  font-family:inherit; font-size:8.5px; letter-spacing:.1em; text-transform:uppercase;
  background:transparent; border:1px solid var(--co-edge-2); color:rgba(239,231,214,.55);
  transition:color .14s ease, border-color .14s ease, background .14s ease; }
.cn-focusarm:hover{ color:var(--co-amber); border-color:var(--co-amber); }
.cn-focusarm.on{ color:var(--co-amber); border-color:color-mix(in srgb, var(--co-amber) 55%, transparent);
  background:color-mix(in srgb, var(--co-amber) 10%, transparent); }
.cn-mpreset-del[disabled]{ opacity:.32; cursor:not-allowed; }
.cn-mpreset-del[disabled]:hover{ color:rgba(239,231,214,.6); border-color:var(--co-edge-2); }

/* FIXER SETTLE. The auto-run must never be a surprise, so the canvas says it is counting: a thin
   amber progress line across the top of the viewer that drains over the 5s. Stop-motion honest —
   it is the actual wait, not a decorative pulse — and any gesture removes the class, which is the
   same instant the timer is cancelled. */
/* position:relative only while armed — it anchors the bar and, unlike most positioning changes,
   cannot shift layout, so scoping it to the armed state costs nothing and touches no other state. */
body.fixer-armed .preview-panel{ position:relative; }
body.fixer-armed .preview-panel::after{
  content:''; position:absolute; left:0; top:0; height:2px; width:100%;
  background:linear-gradient(90deg, var(--co-amber), color-mix(in srgb, var(--co-amber) 25%, transparent));
  transform-origin:left center; animation:fixerSettle 5s linear forwards;
  pointer-events:none; z-index:6;
}
@keyframes fixerSettle{ from{ transform:scaleX(1); } to{ transform:scaleX(0); } }
@media (prefers-reduced-motion: reduce){
  body.fixer-armed .preview-panel::after{ animation:none; opacity:.5; }
}
