/* ─────────────────────────────────────────────────────────────────────
   UNIT SYSTEM: site/units/. The homepage and the site/lab/units*.html
   experiments all build on it.

   S = Square Unit (px):  the pitch of the grid.
   L = Line Unit  (px):   NOT a stroke: it's the gap between tan squares.
   Origin = top-centre of the page. Gap centres sit on n·S from origin,
   so a tan square fills [n·S + L/2, (n+1)·S − L/2].

   Anything that snaps:  position = n·S − L/2,  size = n·S + L
   → its edges land on the outer edges of the gaps, covering them fully.

   LAYERS. Every unit carries --layer (integer, default 0). Its shadow
   paints at  layer·1000  and its rising body at  layer·1000 + 500 + v·10.
   So inside one layer every shadow sits under every body, and a unit on a
   higher layer (shadow included) sits above everything on lower layers.
   This only works while all units share ONE stacking context (the stage):
   nothing between the stage and a unit's shadow/body may set z-index,
   transform, opacity < 1, filter, etc.

   TOKENS. S, L, the palette, timing and every rise value are site-wide
   tokens in tokens.css; edit them there or from /lab/units-dashboard, not
   here. This file only READS them.

   ROLES. A unit's rise comes from its ROLE (data-role, set by createUnit):
   button, tile, field, box, card, chip or media (see ROLES in tokens.js). Pages
   don't pass rise numbers; a unit sets its own --v-* only to show one
   specific state (a specimen in the component library).

   FOCUS. No focus rings anywhere (no outlines, no blur). Keyboard focus shows
   as the treatment the --bb-focus-button / --bb-focus-field tokens name,
   copied onto <html data-focus-button / data-focus-field> by tokens.js.

   DARK. <html data-theme="dark"> swaps the palette for its --bb-dark-*
   twins. Nothing sets it yet; the dashboard previews it.
   ───────────────────────────────────────────────────────────────────── */
/* First rule in the file (an @import after any other rule is ignored). */
@import url('./tokens.css');
@property --v { syntax: '<number>'; inherits: true; initial-value: 0; }
/* piece-band focus scale (unit-pieceband.js), registered so it can transition */
@property --pscale { syntax: '<number>'; inherits: false; initial-value: 1; }

:root {
  /* --S, --L and the palette (--gap --tan --ink --yellow --panel --white
     --sand --muted --on-yellow) come from tokens.css */
  --ox: 50vw;            /* grid origin x, set (pixel-snapped) by unit-grid.js */
  --font-body: 'Lexend', system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace;
}
/* DARK, not live: nothing sets data-theme yet (the dashboard previews it).
   Every palette token takes its dark twin from tokens.css. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --gap: var(--bb-dark-gap); --tan: var(--bb-dark-tan); --ink: var(--bb-dark-ink);
  --yellow: var(--bb-dark-yellow); --panel: var(--bb-dark-panel); --white: var(--bb-dark-white);
  --sand: var(--bb-dark-sand); --muted: var(--bb-dark-muted); --on-yellow: var(--bb-dark-on-yellow);
}
* { box-sizing: border-box; }
/* clip (not hidden): stops sideways scroll from anything that overhangs the
   page edge (hero art, pieces) without making body a scroll container. */
html, body { margin: 0; overflow-x: clip; }
body {
  background: var(--gap);
  font-family: var(--font-body);
  color: var(--ink);
  min-height: 100vh;
}
/* (.unit-grid, the old fixed canvas, is gone: the grid is now the body background, see unit-grid.js) */

/* Everything that snaps lives in the stage. Its (0,0) IS the grid origin.
   z-index makes it the one stacking context all unit layers share. */
.unit-stage { position: absolute; left: var(--ox); top: 0; width: 0; height: 0; z-index: 1; }

/* ── UNIT (button or box) ───────────────────────────────────────────── */
.ubtn {
  --gx: 0; --gy: 0;          /* placement, in squares from origin */
  --w: 6;  --h: 2;           /* size, in squares */
  --layer: 0;
  --stroke: var(--bb-stroke);   /* face inset per side, × L  (1 → face = box − 2L) */
  --fill: var(--yellow);
  /* −5 … 5, from the unit's role (below). With no role: a box at rest,
     a button's hover/press/pressed. */
  --v-rest: var(--bb-box-rest); --v-hover: var(--bb-button-hover);
  --v-press: var(--bb-button-press); --v-pressed: var(--bb-button-pressed);
  --dur: var(--bb-dur); --dur-press: var(--bb-dur-press);
  --ease: var(--bb-ease);

  --v: var(--v-rest);
  --d: calc(max(0, var(--v)) / 10 * var(--S));        /* rise travel  (v=5 → S/2) */
  --p: calc(max(0, var(--v) * -1) / 10 * var(--S));   /* press travel (v=−5 → S/2) */
  --k: calc(var(--L) * var(--stroke));

  position: absolute;
  left:   calc(var(--gx) * var(--S) - var(--L) / 2);
  top:    calc(var(--gy) * var(--S) - var(--L) / 2);
  width:  calc(var(--w)  * var(--S) + var(--L));
  height: calc(var(--h)  * var(--S) + var(--L));
  /* NO z-index here: it would make the unit its own stacking context and
     drag its shadow up with it. Shadow and body are layered separately. */
  transition: --v var(--dur) var(--ease);

  display: block; padding: 0; border: 0; background: none;
  font: inherit; color: var(--ink);
  -webkit-tap-highlight-color: transparent;
}
:is(button, a).ubtn { cursor: pointer; text-decoration: none; --v-rest: var(--bb-button-rest); }

/* ROLES: each maps its tokens (tokens.css) onto the unit's own --v-*. */
.ubtn[data-role="button"] {
  --v-rest: var(--bb-button-rest); --v-hover: var(--bb-button-hover);
  --v-press: var(--bb-button-press); --v-pressed: var(--bb-button-pressed);
}
.ubtn[data-role="tile"] {
  --v-rest: var(--bb-tile-rest); --v-hover: var(--bb-tile-hover);
  --v-press: var(--bb-tile-press); --v-pressed: var(--bb-tile-pressed);
}
.ubtn[data-role="field"] { --v-rest: var(--bb-field-rest); }
.ubtn[data-role="box"]   { --v-rest: var(--bb-box-rest); }
.ubtn[data-role="card"]  { --v-rest: var(--bb-card-rest); }
.ubtn[data-role="chip"]  { --v-rest: var(--bb-chip-rest); }
.ubtn[data-role="media"] { --v-rest: var(--bb-media-rest); }
/* A disabled control is inert: it rests like a box and neither rises nor presses. */
.ubtn:is(:disabled, [aria-disabled="true"]), .ubtn:is(:disabled, [aria-disabled="true"]):is(:hover, :active) {
  --v-rest: var(--bb-box-rest); --v: var(--v-rest); cursor: default;
}
:is(button, a).ubtn:hover  { --v: var(--v-hover); }
:is(button, a).ubtn:active { --v: var(--v-press); transition-duration: var(--dur-press); }
.ubtn[hidden], .utext[hidden] { display: none; }

/* GROUP: several units behind one link/control (the logo lockup) rise and
   press together. The group is a zero-size box at the origin, so the units
   inside still place from the grid origin, and it sets no z-index, so its
   units stay in the stage's one stacking context. */
.ugroup { position: absolute; left: 0; top: 0; width: 0; height: 0; display: block; }
.ugroup:hover > .ubtn  { --v: var(--v-hover); }
.ugroup:active > .ubtn { --v: var(--v-press); transition-duration: var(--dur-press); }
/* PRESSED = latched down: a selected swatch, a toggled-on control. It stays
   down through hover and press until something else is chosen, at its own
   depth (--v-pressed), deeper than a click's momentary --v-press so "chosen"
   reads differently from "being clicked". .held is the older name. */
.ubtn:is(.pressed, .held), .ubtn:is(.pressed, .held):hover, .ubtn:is(.pressed, .held):active { --v: var(--v-pressed); }

/* ── FOCUS: never a ring ─────────────────────────────────────────────
   Every outline goes, on every element of a unit page (and of the label
   makers, which link this file). Keyboard focus shows instead as ONE of a
   few treatments, picked by the focus tokens. The rules key on
   <html data-focus-*>; with no attribute (no JS) the defaults apply:
   a button rises as on hover, a field's face turns white. */
:focus, :focus-visible { outline: none; }
/* .focus-demo shows a treatment without real focus (the dashboard's specimens). */
:root:is(:not([data-focus-button]), [data-focus-button="rise"])
  :is(:is(button, a).ubtn:not(.pressed, .held, :active):focus-visible, .ugroup:not(:active):focus-visible > .ubtn, .ubtn.focus-demo:not(.ufield)) { --v: var(--v-hover); }
:root[data-focus-button="yellow"] :is(:is(button, a).ubtn:focus-visible, .ugroup:focus-visible > .ubtn, .ubtn.focus-demo:not(.ufield)) > .body > .face { background: var(--yellow); color: var(--on-yellow); }
:root[data-focus-button="white"]  :is(:is(button, a).ubtn:focus-visible, .ugroup:focus-visible > .ubtn, .ubtn.focus-demo:not(.ufield)) > .body > .face { background: var(--white); }
:root[data-focus-button="ink"]    :is(:is(button, a).ubtn:focus-visible, .ugroup:focus-visible > .ubtn, .ubtn.focus-demo:not(.ufield)) > .body > .face { background: var(--ink); color: var(--panel); }

/* A field: .ufield (a unit with an input in its face) or .uslider (a range
   input placed like a unit). A text field is focused however it was reached,
   so :focus-within; a slider only for the keyboard. */
:root:is(:not([data-focus-field]), [data-focus-field="white"]) .ufield:is(:focus-within, .focus-demo) > .body > .face,
:root:is(:not([data-focus-field]), [data-focus-field="white"]) .uslider:focus-visible { background: var(--white); }
:root[data-focus-field="yellow"] .ufield:is(:focus-within, .focus-demo) > .body > .face,
:root[data-focus-field="yellow"] .uslider:focus-visible { background: var(--yellow); }
:root[data-focus-field="yellow"] .ubtn.ufield:is(:focus-within, .focus-demo) :is(input, .face) { color: var(--on-yellow); }
:root[data-focus-field="rise"] .ufield:is(:focus-within, .focus-demo) { --v: var(--v-hover); }
:root[data-focus-field="ink"] .ufield:is(:focus-within, .focus-demo) > .body > .face,
:root[data-focus-field="ink"] .uslider:focus-visible { background: var(--ink); }
:root[data-focus-field="ink"] .ubtn.ufield:is(:focus-within, .focus-demo) :is(input, .face) { color: var(--panel); }
:root[data-focus-field="ink"] .uslider:focus-visible::-webkit-slider-thumb { background: var(--panel); }
:root[data-focus-field="ink"] .uslider:focus-visible::-moz-range-thumb { background: var(--panel); }

.ubtn > .shadow, .ubtn > .body { position: absolute; inset: 0; background: var(--ink); }
.ubtn > .shadow { z-index: calc(var(--layer) * 1000); }
.ubtn > .body  {
  z-index: calc(var(--layer) * 1000 + 500 + var(--v) * 10);   /* rise is the z-axis */
  transform: translate(calc(var(--d) * -1), calc(var(--d) * -1));
}
/* PRESS never reflows. The visible face shrinks from the top-left by --p while
   its bottom-right corner stays put, but the face BOX keeps its rest size, so
   nothing inside it (wrapped text, images placed in %) is laid out again: the
   box slides p/2 down-right and a clip trims p/2 off every side, which leaves
   exactly [k + p, size - k] showing. Content moves by p/2 (as centred content
   did when the box itself shrank) and never re-wraps. (Tobias, 2026-09-30: a
   store card's last word jumped a line on every press.) */
.ubtn .face {
  position: absolute;
  inset: var(--k);
  transform: translate(calc(var(--p) / 2), calc(var(--p) / 2));
  clip-path: inset(calc(var(--p) / 2));
  background: var(--fill);
  display: grid; place-items: center; overflow: hidden;
  font-weight: 600; font-size: calc(var(--S) * var(--fs, var(--bb-fs))); white-space: nowrap;
}
/* A yellow face (placeUnit marks it data-tone="yellow") takes --on-yellow for
   its text: the same ink in light mode, and still dark in dark mode, where
   --ink turns light. */
.ubtn[data-tone="yellow"] > .body > .face { color: var(--on-yellow); }
/* Absolutely placed so the percentages resolve against the face; as a grid
   item, a raster's width/height attributes won out over max-height. */
.ubtn .face img { position: absolute; left: 7%; top: 14%; width: 86%; height: 72%; object-fit: contain; }
/* PHOTO FILL: PHOTOS ONLY (a user's photo, a dirt photo, and any mask laid over a
   photo). Never piece renders, icons, text or anything else.
   Two parts, so no light hairline can show between the ink outline and a photo at any
   zoom (Tobias, 2026-09-29):
   1. The photo is drawn 0.5px PAST each edge of its box, and the box (a unit's face, or
      any overflow-clipped holder) clips it. Its own soft, resampled edge pixels land
      outside the clip instead of on the box's last pixel.
   2. The box a photo sits in turns INK while it shows one. A face sits on fractional
      pixels, so its edge pixel is part face, part the ink body under it; with a light
      face that blend reads as a light line, with an ink face it is ink either way.
   The ink applies only once the photo has a picture: photoImg (sort-v2/pass-ui.js) marks
   an <img> data-wait until it loads, so an empty or broken photo leaves its box's own
   fill alone. object-fit is the caller's: cover by default, .lb-img (the big view)
   overrides it to contain, where the ink is the matte around the picture. */
.photo-fill, .ubtn .face img.photo-fill {
  position: absolute; left: -0.5px; top: -0.5px; right: auto; bottom: auto;
  width: calc(100% + 1px); height: calc(100% + 1px); max-width: none; object-fit: cover;
}
:has(> img.photo-fill[src]:not([data-wait])),
.ubtn .face:has(> img.photo-fill[src]:not([data-wait])) { background: var(--ink); }

/* TEXT on the grid: placed like a unit but on the line centres (it has no
   box to cover the lines with). Sits above its layer's shadows and below
   its layer's bodies, so it reads on top of a lower-layer panel. */
.utext {
  --gx: 0; --gy: 0; --w: 10; --layer: 0;
  position: absolute; margin: 0; font-size: inherit;
  left: calc(var(--gx) * var(--S));
  top:  calc(var(--gy) * var(--S));
  width: calc(var(--w) * var(--S));
  z-index: calc(var(--layer) * 1000 + 400);
}

/* ── SHARED PAGE PIECES ─────────────────────────────────────────────── */
/* A zero-size wrapper at the origin: groups markup (nav, footer, form)
   without moving or stacking the units inside it. */
.uwrap { position: absolute; left: 0; top: 0; width: 0; height: 0; margin: 0; padding: 0; border: 0; }

.ubtn svg.icon { width: calc(var(--S) * .7); height: calc(var(--S) * .7); }
.ubtn svg.stroke { fill: none; stroke: currentColor; stroke-width: 2.5; stroke-linecap: square; }
/* Filled marks (the social icons) stay filled even on a page whose own CSS
   styles a bare .icon as an outline (/front/button.css does). */
.ubtn svg.icon:not(.stroke) { fill: currentColor; stroke: none; }

/* A text field inside a unit face. */
.ufield > .body > .face { place-items: stretch; }
.ubtn.ufield input {
  all: unset; box-sizing: border-box; width: 100%; height: 100%;
  padding: 0 calc(var(--S) * .4);
  font: 500 calc(var(--S) * .45) var(--font-body); color: var(--ink);
}
.ufield input::placeholder { color: #9A8355; }
/* The search clear-x, in ink instead of the browser's blue. */
.ufield input::-webkit-search-cancel-button {
  -webkit-appearance: none; appearance: none;
  width: calc(var(--S) * .38); height: calc(var(--S) * .38);
  margin-left: calc(var(--S) * .2);
  background: var(--ink); cursor: pointer;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4 4l16 16M20 4L4 20' stroke='black' stroke-width='4'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4 4l16 16M20 4L4 20' stroke='black' stroke-width='4'/%3E%3C/svg%3E") center / contain no-repeat;
}

.utext.label { line-height: var(--S); text-align: center; font-weight: 700; font-size: calc(var(--S) * .62); }
.utext.fine { font-size: 12.5px; line-height: 1.6; text-align: center; }
.utext.fine p { margin: 0 0 .35em; }
.utext.fine a { color: var(--ink); }

.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* Search results (unit-search.js): 2-tall rows: thumbnail, number, name. */
.usearch-row > .body > .face {
  display: flex; justify-content: flex-start; align-items: center;
  gap: calc(var(--S) * .35); padding: 0 calc(var(--S) * .3);
  font-size: calc(var(--S) * .45); font-weight: 500;
}
.usearch-row .face img, .usearch-row .face .usearch-noimg {
  position: static; flex: none;
  width: calc(var(--S) * 1.5); height: calc(var(--S) * 1.5); object-fit: contain;
}
.usearch-num { font-family: var(--font-mono); font-weight: 700; font-size: .92em; flex: none; }
.usearch-name { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.usearch-empty { padding-left: calc(var(--S) * .2); font-weight: 600; }
/* keyboard/mouse-highlighted row reads as hovered */
.usearch-row.is-active { --v: var(--v-hover); }

/* Piece band (unit-pieceband.js): the strip fills its unit's face, which clips it. */
.pband { position: absolute; inset: 0; }
/* `.ubtn .face .pband img`, not `.pband img`: the face's logo rule (.ubtn .face img,
   left 7%, top 14%) out-ranks the short form and silently shifted every piece. */
.ubtn .face .pband img { position: absolute; left: 0; top: 0; user-select: none; -webkit-user-drag: none; }

/* DROPDOWN (unit-select.js; component library I5 / I8): a button with the choice
   and a caret, a flush list of 1-tall tiles under it, an optional custom field. */
.usel > .body > .face, .usel-row > .body > .face {
  display: flex; align-items: center; justify-content: flex-start;
  gap: calc(var(--S) * .2); padding: 0 calc(var(--S) * .3);
}
.usel .grow, .usel-row .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; text-align: left; }
.ubtn svg.usel-caret { width: calc(var(--S) * .55); height: calc(var(--S) * .55); flex: none; }
.usel-field input { text-align: left; }

/* STILL: a control that keeps its rest rise through hover and press (a photo preview
   that opens on tap: it stands up off the page, but it isn't a button to push). */
.ubtn.still, .ubtn.still:hover, .ubtn.still:active { --v: var(--v-rest); }
