/* The combination builder's own CSS.
 *
 * Lived in a <style> block inside build.html until 2026-11-02, which was
 * invisible locally and fatal in production: the live site sends
 * `style-src 'self'` with no 'unsafe-inline', so the browser dropped all
 * 393 lines of it and served a page with the shared stylesheet only. The
 * dev server sends no CSP at all, so nothing local could show it (D-351).
 *
 * It is a FILE now for that reason, not for tidiness. Anything added here
 * must stay a file: an inline style block or a style="" attribute is
 * blocked on the live site and nowhere else.
 */
/* Only what the combination row needs on top of the site's own stylesheet.
     Everything structural - .wrap, .masthead, .crumbs, .band, .panel-stack,
     .panel, .swatch-grid - is style.css's, so this page inherits the
     responsive behavior the product page already has: the panels sit beside
     the band on a wide screen and below it on a phone. */
  /* The band's height comes from --band-h, which build.js sets from the same
     scene.band ramp the manifest carries, so the viewport narrows before it
     shortens exactly as on the product page. Its visibility is style.css's
     `has-js` gate, which build.js also honors - no override here, or the
     no-JavaScript fallback breaks. */
  /* The blueprint backdrop, not the salon plate: this page is a drawing of a
     product that does not exist yet, so it reads as a schematic rather than
     as a photograph of something you could buy today. #grid is ruled in the
     ground plane by build.js - 1 cm major, 1 mm minor - so the paper lies on
     the same surface the product stands on. */
  .band.blueprint { background:#0e3f63; }
  /* No size or weight here: style.css already has `h1 { font-size: 28px;
     font-weight: 600 }`, which is what every other page's title uses. The
     override made this page the only one with a different title. */
  .page-title { margin:0 0 1.25rem; }
  /* The contact shadow lies under everything in the viewer. */
  /* width/height, not just inset: an <svg> does not stretch to an inset box
     the way a <div> does - it falls back to its 300x150 intrinsic size and
     clips everything outside it. */
  #feet { position:absolute; inset:0; width:100%; height:100%;
          overflow:visible; pointer-events:none; }
  /* Plan view: the footprint outlined where the product was. */
  #plan { position:absolute; inset:0; width:100%; height:100%;
          overflow:visible; pointer-events:none; display:none; }
  .band.plan #plan { display:block; }
  .band.plan #feet { display:none; }
  /* No non-scaling-stroke: the weight is set in plane millimetres so it
     thickens with the drawing, like the dimension rules and the lettering. */
  #plan .out { fill:rgba(255,255,255,.05); stroke:#fff; }
  #plan .plan-label { fill:#dcecfa; text-anchor:middle; opacity:.85;
                      font-family:'Londrina Solid','Century Gothic','Futura',sans-serif;
                      font-weight:100; letter-spacing:.08em; }
  #grid { position:absolute; inset:0; background-repeat:no-repeat; }
  /* #row is the scaled layer: the fit is one transform here and each module
     inside carries only its own place in the row, in master-frame pixels. */
  #row { position:absolute; inset:0; will-change:transform; }
  /* The remote tray, directly ABOVE the viewport (D-297). It was a strip
     inside the band first, which is what Martin asked to try, and it worked
     - but it had to be reserved out of the fit on top of the control row's
     own strip, and on a phone the two together took about a third of the
     band. Out here it costs the drawing nothing at all.

     Its height is still --tray-h, now only because the cells are sized by
     HEIGHT (D-293) and something has to set it. The fallbacks are
     trayBand()'s own two numbers, so the first paint is not a different
     height from the second. */
  /* `.wrap` is a flex column with explicit orders in style.css - .stage-wrap
     is 3, the feature list 4 - and anything without one defaults to 0. Both
     of these need saying out loud: the tray happened to land in the right
     place by source order, and .plan-help without an order jumped ABOVE the
     viewport (D-321). */
  /* Clear air between the strip's scrollbar and the top of the blueprint
     (Martin, 2026-11-03). Taken from ABOVE the bar rather than added to the
     strip: `box-sizing:border-box` means this padding comes out of --tray-h,
     so .tray-scroll stretches that much shorter and its scrollbar rises with
     it. .tray-scroll gives back the same amount from its own bottom padding,
     so the cells keep every pixel they had and the strip is the same height
     it was. */
  .tray { order:2; height:var(--tray-h, 128px); margin:0; display:flex;
          align-items:stretch; --tray-gap:5px; padding-bottom:var(--tray-gap);
          /* The fades are positioned against this box (D-365). */
          position:relative;
          /* White, and the shadow is what holds the white remotes off it
             (D-310). The mid gray this replaced separated them by tone; here
             the two-layer drop shadow does it instead, which is why that
             went in first. */
          background:var(--tray-bg, #fff); }
  html.bare .tray { display:none; }
  /* The bottom padding is the SHADOW's room, not spacing. A scroller clips
     both axes - `overflow-x:auto` forces the vertical one to auto as well,
     so a drop shadow cannot escape the box however the tray above is set -
     and the risen shadow reaches 9 + 13 px below the art, which was being
     cut off square at the foot of the strip (D-313). The strip grew by the
     same amount, so the remotes kept their size.

     The SIDE padding is 5px to match .swatches, and has to stay matched:
     build.js pulls this box out by exactly its own side padding to land the
     content on the gutter (D-316), so the padding IS the overhang, and the
     scrollbar is drawn across the whole box. At 14 the bar reached 9px
     further out than the palette's - invisible until D-369 made the bar
     itself visible, and then plainly crooked (D-370). 5px still clears the
     resting shadow (2 + 4px); it clips the outer 8px of the risen one's 13px
     blur on the first and last cell, which at .34 alpha is not visible.

     Do not re-state this padding in the narrow block below. --tray-gap came
     out of the height at every width but the compensating bottom padding was
     only changed here, so the override silently kept 20 and the narrow strip
     lost 5px of cell height between D-369 and D-370. */
  .tray-scroll { flex:1; min-width:0; display:flex; align-items:stretch;
                 gap:10px; padding:6px 5px 15px; overflow-x:auto; overflow-y:hidden;
                 /* The native bar is gone on every engine, and .tray-bar is
                    drawn in its place (D-373). iOS Safari never showed one
                    here - an accelerated scroller gets a transient overlay
                    bar and no ::-webkit-scrollbar styling at all - so the
                    strip read as three remotes on a phone. Nothing in CSS
                    fixes that, so the bar became an element.
                    Hiding it rather than leaving it to the engines is the
                    point: one indicator that looks the same everywhere,
                    and the end of the scrollbar-width vs ::-webkit-scrollbar
                    precedence trap D-369 had to work around. */
                 scrollbar-width:none;
                 /* Horizontal panning is the scroller's; everything else is
                    the drag's, and the engine stops hunting for a gesture it
                    is not going to get. */
                 touch-action:pan-x; }
  /* A mouse drag along the strip scrolls it (dragToScroll). The cell under
     the cursor must not look pressed while the strip moves under it. */
  /* The dot row's room, taken ONLY while the dots are showing - trayDots()
     sets this class on exactly the condition that unhides them.
     Conditional because the native bar it replaces was: a scrollbar takes
     6px out of the content box when the strip overflows and nothing when it
     fits. Reserving it unconditionally cost the cells 6px at every width
     wide enough not to scroll, which is most desktops - measured against
     production at 1024px as 76 against 82 before this rule existed. The
     same 6px drift as D-370, found the same way, this time before shipping. */
  .tray-scroll.has-dots { padding-bottom:21px; }
  .tray-scroll.dragging { cursor:grabbing; }
  .tray-scroll.dragging .tray-item { pointer-events:none; }
  .tray-scroll::-webkit-scrollbar { display:none; }
  /* The scroll indicator: a dot per remote (D-373). Positioned against
     .tray, which shares .tray-scroll's edges exactly - the same condition
     the fades above rely on - and sitting in the 6px band the native bar
     used to occupy, so taking the bar away moved nothing.

     `--tray-gap` is .tray's own bottom padding, so `bottom` of that lands
     the row flush on the scroller's bottom edge, where the native bar was
     drawn. The fades clear it with `6px + --tray-gap`, unchanged.

     Dots, not a bar, because the problem is a COUNT. A customer seeing
     three remotes and no sign of more concludes there are three; a bar
     answers "there is more", while five dots answer "there are five",
     which is the question actually being got wrong.

     Tapping one scrolls that remote into view (Martin, 2026-11-04). A 6px
     dot is far under the 44px touch target a control wants, and the row
     cannot grow to carry one without eating the cells' shadow room (D-313)
     or overlapping the blueprint and stealing taps meant for either. So the
     buttons are CONTIGUOUS instead - 16px wide and the full height of the
     band, no gap between them, with the 6px dot drawn centered inside. The
     target is still small, but there is no dead space in the row: a tap
     anywhere across the group lands on some dot, and the worst miss is a
     neighbor, which scrolls one remote further than intended and is undone
     by tapping again. That is the right trade for a convenience - swiping
     the strip is still the primary way through it.

     Centered in the gap, not sat on the scroller's edge (Martin,
     2026-11-04). The row spans the whole band below the cells and centers
     its dots in it, rather than pinning them to one end: the band is the
     scroller's bottom padding while the dots are showing (21px) plus
     .tray's own --tray-gap, and `align-items:center` then does the
     arithmetic. Written as a span rather than as a computed `bottom` so
     that changing either padding moves the dots with it instead of
     silently leaving them off-center - the D-370 failure mode. */
  /* The ROW itself stays transparent to the pointer. It spans the full
     width and is mostly empty, so capturing there would swallow drags that
     begin just under the cells. Only the buttons take events. */
  .tray-dots { position:absolute; left:0; right:0; bottom:0;
               height:calc(21px + var(--tray-gap)); display:flex;
               justify-content:center; align-items:stretch;
               pointer-events:none; }
  .tray-dots[hidden] { display:none; }

  /* --dot-pitch is ONE number for both scrollers (D-374). Martin:
     "accuracy isn't super important... clicking a neighboring one will have
     the same effect - get the one you want in view", so the buttons can be
     tighter than a hit target would otherwise allow. That is what makes the
     palette possible at all: 22 dots at the 16px this started on is 352px,
     wider than the phone it has to fit; at 12 it is 264. Shared so the two
     rows cannot drift apart, which would undo the consistency that is the
     reason the palette has dots at all.

     --dot-size is shared for the same reason, and was added after the
     palette's dots came out as 12x10 OVALS: they had been built with the
     button itself carrying the color, so the dot took the button's
     rectangle instead of a circle inside it. Both rows now draw the dot as
     a pseudo-element of exactly these dimensions, and the button around it
     is only ever the hit area.

     --dot-size-off is the OUT-OF-VIEW size, and it is the main thing that
     says "there are more" (Martin). The strip could carry that in color
     alone, dark against pale; the palette cannot, because its color is
     spoken for by the jump bar. Raising the palette's faded opacity to
     protect the colors threw the signal away - the right answer was to stop
     using color for state at all and use SIZE, which is free in both rows.
     Both now shrink when out of view, so the two express the state the same
     way and each keeps whatever contrast its own palette allows. */
  /* NO border-radius on these buttons. Hit testing follows a rounded
     corner, so a 4px radius cut four notches out of every button's hit
     area - 66 dead pixels across the palette's row, at the top and bottom
     edges, exactly where a finger aiming between two dots lands. The
     button is invisible anyway; only the dot inside it is seen. The focus
     ring squares off with it, which is no loss on a 12px control. */
  .tray-dot { appearance:none; -webkit-appearance:none; border:0; padding:0;
              margin:0; background:none; font:inherit;
              width:var(--dot-pitch, 12px);
              flex:0 0 auto; cursor:pointer; pointer-events:auto;
              display:flex; align-items:center; justify-content:center;
              border-radius:0; }
  /* .20 against .62 rather than .18 against .55: a 6px dot is read at arm's
     length on a phone, and the pair has to separate at that size without the
     unlit ones disappearing - they carry the COUNT, which is the whole
     reason the dots are here. The dot is a pseudo-element so the BUTTON can
     be the hit target without the hit target being what you see. */
  .tray-dot::before { content:''; width:var(--dot-size-off, 4px);
                      height:var(--dot-size-off, 4px); border-radius:50%;
                      background:var(--tray-dot, rgba(0,0,0,.20));
                      transition:background-color 120ms linear,
                                 width 120ms linear, height 120ms linear; }
  .tray-dot.on::before { width:var(--dot-size, 6px);
                         height:var(--dot-size, 6px);
                         background:var(--tray-rule, rgba(0,0,0,.62)); }
  .tray-dot:focus-visible { outline:2px solid var(--tray-rule, rgba(0,0,0,.62));
                            outline-offset:-1px; }
  /* The strip always scrolls sideways, so these are its leading and trailing
     edges rather than the palette's top and bottom - which is why they are
     their own classes instead of reusing `.fade`, whose base rule in
     style.css is the vertical panel case and whose horizontal override lives
     inside a media query. `updateFades` only toggles `on`, so the names are
     free (D-365).

     The gradient is `--tray-bg`, not a literal white: the strip's background
     is a variable (D-310) and a hardcoded `#fff` would show as a pale block
     the moment that is themed.

     `bottom:6px` clears the scrollbar, whose height is declared six lines
     up. Washing the thumb at each end would be the same mistake the palette
     avoids with its own `right:12px`. */
  .tray-fade { position:absolute; top:0; bottom:calc(6px + var(--tray-gap)); width:30px;
               pointer-events:none; opacity:0; transition:opacity 160ms; }
  .tray-fade.start { left:0;
                     background:linear-gradient(to right,
                                                var(--tray-bg, #fff), transparent); }
  .tray-fade.end { right:0;
                   background:linear-gradient(to left,
                                              var(--tray-bg, #fff), transparent); }
  .tray-fade.on { opacity:1; }
  /* flex:0 0 auto on both, or the groups SHRINK to fit instead of letting
     the strip scroll - and a vendor caption, which is nowrap, then spills out
     of its own group and under the next one's rule. */
  .tray-group { flex:0 0 auto; display:flex; flex-direction:column; gap:3px;
                min-height:0; }
  /* Grouped by manufacturer, which Martin asked for, with the vendor said
     once over the group rather than repeated into every cell of a strip that
     has to scroll. The key is the code prefix, not the caption (D-292). */
  .tray-group + .tray-group { border-left:1px solid var(--tray-rule, rgba(0,0,0,.18));
                              padding-left:10px; }
  /* The gray that used to be the background, now carrying the caption
     against white (D-310). */
  /* 13px is the buy panel's "Your combination", which is the smallest type
     on this page a visitor is expected to read - Martin, 2026-11-01: the
     tray's labels should be "at least as large" as it (D-307). */
  .tray-vendor { margin:0; font-size:13px; line-height:17px; letter-spacing:.06em;
                 font-weight:700; text-transform:uppercase;
                 color:var(--tray-caption, #868f97); white-space:nowrap; }
  /* stretch, so a cell is exactly as tall as the row and the art box is the
     row less the label. The art is bottom-aligned inside that box instead,
     which is what puts every family on one baseline. */
  .tray-cells { flex:1; min-height:0; display:flex; align-items:stretch; gap:0; }
  /* A cell is sized by HEIGHT (D-293). The strip's height is then a number
     that is chosen, rather than one the longest remote dictates, and a tall
     remote costs horizontal density instead - which the scrolling makes
     free. All the art sits on one baseline, which is what makes the shared
     scale legible: a Hue is longer than a Pico because it is.

     The label is BESIDE the picture, not under it (D-306): the names are the
     shop's own and run to "Smart Dimmer Switch Remote (earlier)", which
     under a 70 px picture would set the cell's width four times over. Beside
     it, the name wraps into the height the picture already needs. */
  .tray-item { flex:0 0 auto; display:flex; align-items:flex-end; gap:7px;
               min-height:0; position:relative;
               border:0; background:none; padding:0; font:inherit;
               color:var(--tray-ink, #1d262d); cursor:grab; touch-action:pan-x;
               -webkit-user-select:none; user-select:none; }
  /* A second product under one manufacturer gets a divider of its own -
     lighter than the one between manufacturers, and short of the full
     height, so the grouping still reads at a glance. */
  .tray-item + .tray-item { padding-left:11px; }
  .tray-item + .tray-item::before {
    content:''; position:absolute; left:0; top:34%; bottom:2px; width:1px;
    background:var(--tray-rule-soft, rgba(0,0,0,.12)); }
  /* The picture is scenery: the button takes the pointer. */
  .tray-item > * { pointer-events:none; }
  .tray-item img, .tray-ghost img { -webkit-user-drag:none; }
  /* Too wide to add AND too wide to replace with. */
  .tray-item:disabled { opacity:.3; cursor:default; }
  /* Too wide to add, but it can still replace the indicated slot, so it taps
     and does not drag. */
  .tray-item.no-add:not(:disabled) { cursor:pointer; }
  /* It is in the air; what you see is the ghost. */
  .tray-item.lifted { opacity:.3; }
  .tray-item:focus-visible { outline:2px solid #0e3f63; outline-offset:2px;
                             border-radius:4px; }
  .tray-art { flex:0 0 auto; height:100%; display:flex; align-items:flex-end; }
  /* The size is set on the element's STYLE by build.js, from the crop box
     and one scale for the whole strip. max-height and object-fit are the
     backstop only: if the strip ever measured a little short, the art
     shrinks in proportion rather than stretching.

     drop-shadow, not box-shadow: the art is a cut-out with real alpha, so
     the shadow has to follow the remote's silhouette rather than the
     element's rectangle. Two layers, because one blur cannot be both the
     tight edge that says "resting on this" and the soft spread that says
     "above it" (D-308). */
  .tray-art > * { display:block; max-height:100%; object-fit:contain;
                  filter:drop-shadow(0 1px 1px rgba(14,22,30,.40))
                         drop-shadow(0 2px 4px rgba(14,22,30,.28));
                  transition:filter 190ms ease-out, transform 190ms ease-out; }
  /* Risen. The contact shadow steps back and softens while the ambient one
     lengthens and spreads - which is what lifting off a page actually does
     to a shadow, and is why changing only the blur reads as a glow instead.
     2px of travel is the least that registers; more reads as sliding. */
  .tray-item:hover:not(:disabled) .tray-art > *,
  .tray-item:focus-visible .tray-art > * {
    transform:translateY(-2px);
    filter:drop-shadow(0 3px 3px rgba(14,22,30,.30))
           drop-shadow(0 9px 13px rgba(14,22,30,.34)); }
  /* In the air for real, so it keeps the risen shadow the whole way - the
     ghost is a copy of the art and would otherwise lose it (D-295). */
  .tray-ghost > * { filter:drop-shadow(0 4px 4px rgba(14,22,30,.28))
                           drop-shadow(0 12px 18px rgba(14,22,30,.34)); }
  /* A gentler version of the same thing, with no travel: the shadow still
     says which cell the pointer is on, and nothing moves. */
  @media (prefers-reduced-motion: reduce) {
    .tray-art > * { transition:filter 120ms linear; }
    .tray-item:hover:not(:disabled) .tray-art > *,
    .tray-item:focus-visible .tray-art > * { transform:none; } }
  /* The placeholder: the module's footprint in millimetres, until the
     overhead render lands (D-293). non-scaling-stroke so the outline is one
     screen pixel and a half whatever the cell's scale is. */
  .tray-ph rect { fill:rgba(14,22,30,.05); stroke:#5c6b77; stroke-width:1.2;
                  vector-effect:non-scaling-stroke; }
  /* Wraps, and bounded: the longest name is five words. Bottom-aligned with
     the picture, so the names sit on the same line the art stands on. */
  .tray-label { font-size:13px; line-height:17px; max-width:104px;
                text-align:left; padding-bottom:1px; }
  /* What the finger is holding: the remote out of the tray until it crosses
     into the viewport, then the pedestal that remote sits in (D-295). On the
     BODY and fixed, so it is not trapped in the band's or the stage's
     stacking context, and it can be held over the page's own chrome. */
  .tray-ghost { position:fixed; left:0; top:0; z-index:1000;
                pointer-events:none; will-change:transform; }
  .tray-ghost .ghost-mod { position:relative; }
  .tray-ghost .mod { position:absolute; display:block; }
  /* Nothing was added: it goes back where it came from, on the drawer's own
     curve. */
  .tray-ghost.back { transition:transform 240ms cubic-bezier(.22,.61,.36,1),
                                opacity 240ms ease; opacity:0; }
  /* Undo and redo, upper-left inside the viewport, where it does not compete
     with the palette on the right (D-335). */
  .viewer-controls { position:absolute; top:12px; left:12px; z-index:2;
                     display:flex; flex-direction:column; gap:6px;
                     align-items:flex-start; }
  /* The view pills and the units sit across one row at the FOOT of the
     viewport, which keeps the top-left corner to undo alone (D-335). The
     box is shrink-to-fit, so the row wraps only when it is wider than the
     band: on a phone Remotes drops to a second row. */
  .viewer-controls.at-foot { top:auto; bottom:12px; flex-direction:row;
                             flex-wrap:wrap; align-items:center; }
  /* The band gives up its TOP corners: the tray sits on them, and a rounded
     corner under a square strip shows the page through the notch. Its bottom
     corners are untouched, so the pair reads as one block (D-304). The
     SIDES are matched in build.js, from the band's own computed margin -
     style.css insets the band at more than one breakpoint and the numbers do
     not agree with each other, so mirroring the element beats restating the
     rules here and drifting from them later. */
  .band { border-start-start-radius:0; border-start-end-radius:0; }
  /* The narrow strip, matching trayBand()'s other number. */
  @media (max-width:720px) {
    .tray { height:var(--tray-h, 105px); }
    .tray-scroll { gap:8px; }
    .tray-cells { gap:8px; }
    .tray-group + .tray-group { padding-left:8px; }
    /* The foot row is three pills, 359 px at the shared padding, and a
       393 px iPhone leaves it 349: Remotes wrapped. 8 px from the edges and
       14 px side padding make it 335 px, which fits down to a 375 px phone.
       Scoped to the band, so the main site's pills keep the token (D-339). */
    .band { --pill-pad:14px; }
    .viewer-controls { top:8px; left:8px; }
    .viewer-controls.at-foot { top:auto; bottom:8px; }
  }
  .units { display:flex;
           border:1px solid rgba(255,255,255,.28); border-radius:999px;
           overflow:hidden; background:rgba(14,63,99,.55);
           backdrop-filter:blur(6px); }
  /* Big enough to hit on a phone: 44 pt is the target, and these were 24. */
  /* The size lives in style.css as --pill-h / --pill-pad / --pill-font, which
     the main site's `.feature-toggle` reads too (D-287). These were the
     originals; moving them to tokens changes nothing here. */
  /* inline-flex and a gap so a button can carry an icon beside its label, as
     the main site's Features toggle does (D-288). The in/mm buttons have no
     icon and are unaffected: one flex item lays out where the text did. */
  .units button { border:0; background:none; color:#cfe4f5; cursor:pointer;
                  font:inherit; font-size:var(--pill-font); line-height:1;
                  padding:11px var(--pill-pad); min-height:var(--pill-h);
                  display:inline-flex; align-items:center; gap:7px; }
  /* NO font-weight change. Pressing a pill used to take its label from 400
     to 600, and bold text is wider: Footprint grew 2.0px, Remotes 1.4px,
     in/mm 0.4px, so the row shuffled every time one was used (Martin,
     2026-11-02: "if you enable one, it gets larger... It shouldn't").
     The weight was redundant anyway - a pressed pill already inverts to a
     light ground with dark ink, which says "on" far louder than a half-step
     of weight. Reserving the bold width instead would mean a hidden copy of
     the label, and the button is an inline-flex row with a gap, so that copy
     lands BESIDE the text and adds 7px rather than stacking behind it
     (D-354). */
  .units button[aria-pressed="true"] { background:#dcecfa; color:#0e3f63; }
  .units button:focus-visible { outline:2px solid #dcecfa; outline-offset:-2px; }
  /* Undo with nothing to undo stays in place, dimmed, so the pill does not
     change size or jump as the history fills and empties (D-334). */
  .units button:disabled { opacity:.4; cursor:default; }
  #undo button + button { border-left:1px solid rgba(255,255,255,.28); }
  #dims { position:absolute; inset:0; width:100%; height:100%;
          overflow:visible; pointer-events:none; }
  /* Plan mode drops the product and leaves the drawing: the schematic is
     what a combination IS before it is printed, and it is the view that
     answers "will it fit on my shelf". */
  .band.plan #row { display:none; }
  /* Hide the remote layer, do not drop it from #row. draw() fits the row to a
     bound taken from the crop boxes, `rbox` included, and leaving that alone
     is the point: the drawing keeps the size it has with the remotes on, and
     the dimension lines and the lettering do not move. Taking the remotes out
     of the bound as well would refit the barer pedestal bigger and make the
     whole drawing jump every time the button is used. */
  .band.no-remotes #row .mod.remotes { display:none; }
  /* The slot footprints sit above everything so a click lands on the slot the
     eye is pointing at, not on whichever sprite rectangle happens to overlap. */
  #hits { position:absolute; inset:0; width:100%; height:100%; overflow:visible;
          z-index:30; }
  /* Above #hits so a chip takes the click rather than the slot behind it. */
  /* Above #hits so a chip takes the click rather than the slot behind it.
     Drawn INSIDE the ground matrix, so the chips and their lettering shear
     with the paper exactly as the preview's #tool does. */
  #tools { position:absolute; inset:0; width:100%; height:100%; overflow:visible;
           pointer-events:none; z-index:50; }
  #tools .chip { pointer-events:auto; cursor:pointer; }
  #tools .chip circle, #tools .chip rect {
    fill:rgba(12,32,50,.88); stroke:rgba(255,255,255,.45); }
  #tools .chip:hover circle, #tools .chip:hover rect { fill:rgba(40,92,136,.95); }
  #tools .chip text { fill:#eaf3fb; font-weight:600; text-anchor:middle;
    pointer-events:none; user-select:none; }
  #tools .chip:focus-visible circle, #tools .chip:focus-visible rect {
    outline:none; stroke:#9fd0f5; }
  #tools .chip.off { pointer-events:none; opacity:.28; }
  #buy-note.over { color:#c0392b; font-weight:600; }
  /* The standing instructions, in a headed section AFTER the panels, where
     they outlive the empty sheet (D-321) and come between nothing (D-322).
     The page's own voice, not the blueprint's: this is page text about the
     drawing, not lettering on it.

     order:5 because `.wrap` is a flex column with explicit orders in
     style.css - .stage-wrap is 3, which carries the band AND the palette, so
     anything at 4 or more is after both. Without one it defaults to 0 and
     lands above the viewport. */
  .how-to { order:5; display:flex; flex-direction:column; gap:14px;
            padding-top:28px; padding-bottom:8px; }
  /* Four numbered stages, in the order a visitor meets them (D-353). A real
     <ol> carries the numbering as structure; `list-style:none` plus a counter
     puts the marker ON the heading baseline instead of in a list gutter, which
     the grid underneath would not line up to. */
  .how-to-steps { list-style:none; margin:0; padding:0; counter-reset:how-to;
                  display:flex; flex-direction:column; gap:26px; }
  .how-to-steps > li { counter-increment:how-to;
                       display:flex; flex-direction:column; gap:12px; }
  /* A LABEL, not a title. Set like the tray vendor captions above - caps,
     tracked, bold, gray - because set as a plain dark heading it was a third
     weight of black between the page title and the item titles, competing
     with both (Martin, 2026-11-02: "what we have now isn't working"). Caps
     and the 700 weight carry the visual weight the gray gives up.

     These ARE the section now: "How to use this tool" is gone, so each stage
     opens its own block and is sized to do that - 18px against the page
     title's 22px, and well clear of the 15px item titles it contains. It was
     13px, which put it SMALLER than its own children; caps and gray were
     covering that rather than fixing it. Tracking eases to .05em because
     caps need less of it as they grow.

     The gray is #6b767f, NOT the tray caption's #868f97: that measures
     3.29:1 on white and this is a heading, so it takes the lightest gray in
     the same hue that still clears 4.5:1, at 4.64:1. */
  .how-to-steps > li > h2 { margin:0; font-size:18px; line-height:23px;
                            font-weight:700; text-transform:uppercase;
                            letter-spacing:.05em; color:#6b767f; }
  /* The number is generated, not typed, so renumbering is a question of order
     and never of editing the headings by hand. It inherits the label color
     rather than carrying its own: the whole line is one mark now. */
  .how-to-steps > li > h2::before { content:counter(how-to) ". "; }
  /* One column per action, as many across as fit. Each is a sub-head naming
     the thing a visitor wants to do and the gesture that does it (D-324). */
  .how-to-grid { display:grid; gap:16px 28px;
                 grid-template-columns:repeat(auto-fit, minmax(230px, 1fr)); }
  .how-to-grid h3 { margin:0 0 3px; font-size:15px; font-weight:600;
                    color:var(--ink, #1d262d); }
  .how-to-grid p { margin:0; color:#5b6670; font-size:14px; line-height:1.5; }
  .how-to-grid b { font-weight:600; color:#1d262d; }
  /* The + and − chips, printed at text size: the same dark disc and pale
     glyph as the real ones on the plan (#tools .chip above), so the sentence
     and the control it names can be matched by eye without being told which
     is which.

     DRAWN, not set. The chip on the plan carries a font glyph at 8mm in a
     15mm disc, and copying that proportion into a 14px paragraph left a
     thread of a + lost in a blob - a glyph that reads at 44px does not read
     at 16. These arms are 53% of the diameter at 11% weight, which is the
     same chip at the size prose can show it (D-329). */
  .how-to-grid .ctl {
    width:1.18em; height:1.18em; vertical-align:-.26em; margin:0 .04em; }
  .how-to-grid .ctl circle { fill:#0c2032; }
  .how-to-grid .ctl path {
    stroke:#eaf3fb; stroke-width:2.6; stroke-linecap:round; }
  /* The empty sheet's invitation is DRAWN, on the plane, by build.js - this
     paragraph is the same words for a screen reader, which cannot follow
     lettering sheared into a ground matrix (D-319). */
  .empty-note { position:absolute; width:1px; height:1px; margin:-1px;
                padding:0; overflow:hidden; clip-path:inset(50%);
                white-space:nowrap; }
  #empty-plan { position:absolute; inset:0; width:100%; height:100%;
                overflow:visible; pointer-events:none; display:none; }
  .band.empty #empty-plan { display:block; }
  /* The blueprint's own face (D-252), as the dimension lettering uses. No
     font-size here: the size is a presentation attribute in plane
     millimetres, and a CSS rule would beat it and pin the lines to screen
     pixels. Sentence case rather than the dimensions' caps - this is a
     sentence, and Londrina at 100 is a thin enough stroke that caps at this
     length read as shouting. */
  #empty-plan text { fill:#cfe4f5; text-anchor:start;
                     font-family:'Londrina Solid','Century Gothic','Futura',sans-serif;
                     font-weight:100; letter-spacing:.05em; }
  html.bare #empty-plan { display:none; }
  #hits .slot { fill:transparent; stroke:none; cursor:pointer; }
  #hits .slot:hover { fill:rgba(255,255,255,.07); }
  #hits .slot:focus-visible { outline:none; fill:rgba(255,255,255,.10); }
  #hits .slot.sel { fill:rgba(159,208,245,.16); stroke:#9fd0f5; stroke-width:1.5; }
  #dims .dim { stroke:#9fd0f5; stroke-width:1.6; vector-effect:non-scaling-stroke; }
  #dims .ext { stroke:#9fd0f5; stroke-width:1.2; stroke-dasharray:6 4.5;
               opacity:.75; vector-effect:non-scaling-stroke; }
  /* The preview's own treatment (D-252): Londrina Solid, all caps, tracked
     out. The weight is 100 - the face is a single solid stroke, so the low
     weights are the ones that read as drafting lettering. */
  #dims text { fill:#dcecfa; text-anchor:middle;
               font-family:'Londrina Solid','Century Gothic','Futura',sans-serif;
               font-weight:100; text-transform:uppercase; letter-spacing:.08em; }
  /* No font-size here. The size is set as a presentation ATTRIBUTE in plane
     millimetres, and a CSS rule beats a presentation attribute - these two
     lines silently pinned every label to 14/16 screen px and undid the whole
     point of drawing inside the plane group. */
  /* A module is two stacked layers at the same place: the pedestal, which is
     tinted, and the remote, which never is (FND-024). Both are positioned
     from their crop box in the 1920x1440 camera frame (FND-026). */
  #row .mod { position:absolute; display:block; }
  #row canvas.mod { image-rendering:auto; }
  .slots { display:flex; gap:.5rem; flex-wrap:wrap; align-items:center; }
  .mock { opacity:.55; }
  /* A disabled Add to cart (no price for the row) is dimmed, and keeps its
     color under the pointer: a hover that answers suggests a click that
     does something. */
  .buy:disabled, .buy:disabled:hover { background:var(--brand); cursor:default; opacity:.55; }
  /* `?bare=1`: the viewport and nothing else, so a capture of the window IS
     the picture. `manifest/build_builder_tile.py` uses it to photograph the
     catalog tile at whatever aspect the tile wants (D-284). Inert without the
     parameter, and it hides rather than removes, so the page still builds
     exactly as it normally does. */
  html.bare, html.bare body { margin:0; background:#0e3f63; }
  html.bare .masthead, html.bare .crumbs, html.bare .page-title,
  html.bare .panel-stack, html.bare footer,
  html.bare .viewer-controls, html.bare #tools, html.bare #hits,
  html.bare .limit-note { display:none; }
  /* The plate limit, said while it is in force (D-336). Yellow so it reads
     as a caution against the blueprint - but as the TEXT, not behind it.
     Martin, 2026-11-02: the pill was "too-much" (D-345). It is a line of
     explanation, not a control, and dressing it like the undo and unit
     pills made the one thing on the page that takes no pointer look like
     the most clickable. Nothing is lost by dropping the background: the
     blueprint is #0e3f63, so #ffd84d on it measures 7.95:1, against the
     pill's 11.50:1 and the 4.5:1 AA asks for at this size.
     Placed by placeLimitNote(), which centers it over the drawing's own
     area and drops it under the undo pill when the two would touch. It
     takes no pointer: it explains the grayed-out remotes, it is not one. */
  .limit-note { position:absolute; top:12px; left:50%; z-index:3;
                width:max-content; max-width:calc(100% - 24px);
                color:#ffd84d;
                font-size:15px; line-height:1.35; font-weight:600;
                text-align:center; pointer-events:none; }
  .limit-note[hidden] { display:none; }
  html.bare .wrap { max-width:none; min-height:0; box-shadow:none; }
  html.bare .stage-wrap { padding:0; }
  html.bare .band { margin:0; width:100vw; height:100vh; border-radius:0; }
