/* The design canvas — the P5 'covers' concept from dev_sandbox/mockups/canvas-round,
 * rebuilt on the site's tokens. Grammar notes travel with their rules; the
 * decision history lives in the sandbox files and the git log there.
 *
 * NAMESPACE: rentals.css owns .shelf/.workspace/.spread/.rmv/.inuse/.app for the
 * wish tray and the browse page, so this page's own vocabulary is .shelf/
 * .workspace/.spread/.rmv/.inuse/.canvasapp. Renaming back is how the two
 * stylesheets start fighting over the same class on one page. */

/* ---------- rooms, in the Marginalia grammar ----------
   ⚠️ EVERY SELECTOR HERE IS A DIRECT-CHILD SELECTOR, deliberately. P4 nests a
   category list inside each room, and a bare `.rooms button` reaches it: the
   honey rule and the italic-when-open both landed on every category at once,
   so the whole list read as selected and the counts sat under a gold bar. */
.rooms { list-style: none; margin: 0; padding: 0; }
.rooms > li > button {
  width: 100%; background: none; border: 0; position: relative;
  display: grid; grid-template-columns: 1.9rem 1fr 2.4ch; align-items: baseline; gap: .3rem;
  padding: .42rem 0; font-family: var(--serif); font-size: 1.04rem; color: var(--ink);
}
.rooms > li > button span { justify-self: end; text-align: right; }
/* ⚠️ 11px IS A FLOOR, NOT A PREFERENCE. Layer 1 rule 11 of the KM Brands house
   rules: nothing below 11px renders anywhere, on any page, at any breakpoint.
   This was 8.5px and the count below was 9px; both predate the rule being
   applied to the canvas. Two digits at 11px with .14em of tracking measure
   about 16px in a 1.9rem (30.4px) column, so the size moves and nothing else
   has to. */
.rooms > li > button i {
  font-style: normal; font-family: var(--sans); font-size: 11px; letter-spacing: .14em;
  color: var(--rule-line); font-variant-numeric: tabular-nums; transition: color .22s;
}
/* Matched to `.subs button em` below, which has carried the same content —
   a count, tabular, right-aligned — at 11px in a 2.4ch column since it was
   written. That rule is the proof this fits: same column, same digits, same
   grid. The .1em tracking goes with the size change because the sibling does
   not have it, and two rules drawing the same thing differently is how they
   drift apart. */
.rooms > li > button em {
  font-style: normal; font-family: var(--sans); font-size: 11px;
  color: var(--muted); justify-self: end; font-variant-numeric: tabular-nums;
}
.rooms > li > button::after {
  content: ''; position: absolute; right: 2.4ch; bottom: .28rem; height: 2px; width: 0;
  background: var(--honey); transition: width .34s var(--ease);
}
.rooms > li > button:hover::after, .rooms > li.on > button::after { width: calc(100% - 1.9rem - 2.9ch); }
.rooms > li.on > button span { font-style: italic; }
.rooms > li.on > button i { color: var(--amber); }

/* ---------- the tray ---------- */
.shelf { display: flex; gap: .35rem; overflow-x: auto; scrollbar-width: thin; padding: .3rem 0; }
.tchip {
  width: 54px; height: 54px; flex: none; border: 1px solid var(--rule-line);
  background: var(--paper); display: grid; place-items: center; padding: 5px;
  touch-action: none; transition: border-color .18s, transform .18s var(--ease);
}
/* The Edge hover is gone, packet 2026-08-29 §2: the chip hover is ZOOM, and
   the options are mutually exclusive. Letting both stand makes the chip lift
   and scale at once, which reads as a wobble. Its `.shelf .tchip` partner and
   that rule's :focus-visible half went with it. */
.tchip img { max-width: 100%; max-height: 100%; object-fit: contain; }
.tsk { font-family: var(--serif); font-size: .78rem; color: var(--rule-line); }

.ghost {
  position: fixed; z-index: 200; pointer-events: none; transform: translate(-50%,-50%);
  opacity: .55; filter: drop-shadow(0 10px 18px rgba(0,0,0,.22)); transition: opacity .15s;
}
.ghost.over { opacity: .95; }
/* ⚠️ `> img`, NOT ANY DESCENDANT — THIS IS WHY A DRAGGED NAPKIN DOUBLED.
   Darrell, 2026-09-03: "it's almost like the image turns into a doubled up,
   there's a napkin under it that is almost transparent and then a smaller
   napkin of the same color in the center."

   A napkin is three layers in one box: `.nap i` is the SHADE, painted through
   a mask stretched to `100% 100%`, and `.nap img` is the PHOTOGRAPH, absolutely
   inset to fill that same box and multiplied over it. They line up only while
   both fill the box exactly. This rule reached the photograph too and gave it
   `object-fit: contain`, which letterboxes it inside the box — so the mask
   stayed full size and the photograph shrank to fit inside it. The
   "transparent napkin" is the mask alone and the "smaller napkin in the
   center" is the contained photograph. Both are one napkin, taken apart.

   The child combinator keeps this on the ghost the product chips build, where
   the <img> IS the direct child and contain is what stops a tall piece
   stretching. The napkin's own rules then own the napkin. */
.ghost > img { display: block; object-fit: contain; }
.ghost span { font-family: var(--serif); background: var(--paper); padding: .4rem .6rem; }

/* ---------- placed pieces ---------- */
/* ⚠️ width: max-content IS THE FIX FOR THE SHRINKING GLASS, and the bug it
 * closes was a genuine runaway. Darrell, 2026-08-16: dragging a piece toward
 * the right edge made it "shrink to just microscopic", on every table card and
 * on the open floor, while left, top and bottom clamped correctly.
 *
 * An absolutely positioned box with `left` set and `right` auto is
 * shrink-to-fit, and its AVAILABLE WIDTH is the containing block minus left.
 * base.css caps every image at `max-width: 100%`. So at left:790 on an 859px
 * board the piece had 69px to live in and the photograph was squeezed to fit
 * — and then paint() re-clamped using that smaller offsetWidth, which moved it
 * further right, which left less room, which squeezed it again. Each pointer
 * move roughly halved it: 138px, 69px, 34px.
 *
 * Only the right edge, because only `left` eats the available width. That is
 * why three sides behaved and one did not.
 *
 * max-content sizes the box to the photograph regardless of where it sits, and
 * max-width: none stops the global image cap reaching inside. */
.pc {
  position: absolute; width: max-content; max-width: none;
  transform: translate(-50%,-50%); touch-action: none; cursor: grab;
}
.pc.drag { cursor: grabbing; z-index: 50; }
.pc > img, .pc > .psk {
  display: block; object-fit: contain; max-width: none;
  pointer-events: none; -webkit-user-drag: none;
}
/* ⚠️ A PIECE WITH NO CUTOUT IS STILL A BOX YOU HAVE TO AIM AT. This span used
   to take its width from its title, so a long name made a wide invisible
   target that reached well past anything drawn — Darrell found he could grab
   a pedestal from empty space above it. paint() now sets both dimensions, and
   the label is clipped to whatever box the piece's real inches give it. */
.pc > .psk {
  box-sizing: border-box; display: grid; place-items: center; text-align: center;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  font-family: var(--serif); font-size: .8rem; color: var(--muted);
  border: 1px dashed var(--amber); padding: .5rem; background: color-mix(in srgb, var(--honey) 8%, transparent);
}
.pc .x {
  position: absolute; left: -9px; top: -9px; width: 17px; height: 17px; padding: 0;
  border: 1px solid var(--rule-line); background: var(--paper); border-radius: 50%;
  font-size: 11px; line-height: 1; color: var(--muted); opacity: 0; transition: opacity .15s;
}
.pc .tag {
  /* -24px, not -20px: the label went 9.5px -> 11px (Layer 1 rule 11) and grew
     about 4px taller. The offset is what keeps it off the card it labels. */
  position: absolute; left: 50%; top: -24px; transform: translateX(-50%);
  background: var(--ink); color: var(--ground); font-size: 11px; letter-spacing: .04em;
  padding: 2px 6px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity .15s;
}
.pc:hover .x, .pc:hover .tag, .pc.sel .x, .pc.sel .tag { opacity: 1; }


/* P5 — the covers, elevated. Forked from covers.css and reworked; board.css
 * still carries the shared chrome. Everything here is the P5 concept:
 * the dog-ear fold, the manifest totem, the drop ceremony, the honey-ramp
 * skin from the live site.
 */

:root {
  /* Page-locals only; everything else comes from tokens.css. */
  --fold-dur: 0.46s;
  --ease: var(--ease-out);
}

/* ---------- browser surfaces carry the design too ----------
   Selection, scrollbars and focus rings ship as browser defaults that belong
   to no design system. Cheapest possible signal that this page was built. */
::selection { background: var(--honey); color: var(--ink); }
/* ⚠️ A DRAG IS NOT A SELECTION. On small pieces the drag starts on the
   image itself and the browser answers with an image-selection — painted
   honey by the rule above, and it lingers after the drop. Nothing placed
   on a board is selectable; the one caret on this page is the rename. */
.pc, .pc *, .cov, .cov * { user-select: none; -webkit-user-select: none; }
:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; border-radius: var(--radius-sm); }
.side, .cards { scrollbar-width: thin; scrollbar-color: var(--rule-line) transparent; }
.cards::-webkit-scrollbar { height: 6px; }
.side::-webkit-scrollbar { width: 6px; }
.cards::-webkit-scrollbar-thumb,
.side::-webkit-scrollbar-thumb { background: var(--rule-line); border-radius: 3px; }

.nav .wish { font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--amber-deep); }
.nav .wish b { font-variant-numeric: tabular-nums; }

/* ⚠️ 11px is the brand's stated type floor, measured on the live site the
   hard way. board.css predates the rule, so its small labels are raised
   here rather than edited there — P4 still reads that file. */
.rooms > li > button i { font-size: 11px; }
.rooms > li > button em { font-size: 11px; }
.tsk { color: var(--muted); }
.pc .tag { font-size: 11px; }

/* 232px rail, packet §2 — was 15rem (240px).
   ⚠️ TWO TRACKS, NOT THREE. The packet writes `232px 1fr auto`, but .workspace
   has exactly two children in the markup, .side and .main. A third track would
   never be occupied. */
.workspace { flex: 1; display: grid; grid-template-columns: 232px 1fr; min-height: 0; }
/* ⚠️ The rooms hold still; the stack does the moving. The panel is a
   column: the index keeps whatever height it wants, and the stack takes
   what is left and scrolls its own overflow behind the honey bar. */
/* 3px edge, packet §2 — "heavy". It was 1px against a 1.35:1 hairline; the
   token underneath it is now a 7.69:1 stroke, so this is a deliberate wall. */
.side { border-right: 3px solid var(--rule-line); padding: .85rem;
  display: flex; flex-direction: column; overflow: hidden; }
.side h3 { flex: none; font-size: 11px; letter-spacing: .2em; text-transform: uppercase;
  color: var(--muted); font-weight: 400; margin: 0 0 .25rem; }
.side .sub { flex: none; font-family: var(--text); font-style: italic; font-size: .9rem;
  color: var(--muted); margin: 0 0 .6rem; }
.side .rooms { flex: none; }
/* `position: relative` is load-bearing: the event dock is absolutely
   positioned against this box so it overlays the canvas instead of
   becoming a third grid column that reflows the board when it opens. */
.main { display: flex; flex-direction: column; min-width: 0; position: relative; }

/* ---------- rooms, with their categories underneath ---------- */
.rooms > li > .subs { list-style: none; margin: 0 0 .5rem; padding: 0 0 0 1.9rem; }
/* ⚠️ [hidden] loses to display:block on specificity. It has to be said here or
   a closed room paints its categories while reporting itself closed. */
.rooms > li > .subs[hidden] { display: none; }
/* A room with no categories opens straight onto its shelf — the empty list
   must not leave its margin behind. */
.rooms > li > .subs:empty { display: none; }
.subs button {
  width: 100%; background: none; border: 0; text-align: right; position: relative;
  display: grid; grid-template-columns: 1fr 2.4ch; gap: .4rem; align-items: baseline;
  padding: .2rem 0; font-family: var(--text); font-size: .95rem; color: var(--muted);
  transition: color .18s;
}
.subs button span { justify-self: end; }
.subs button em { font-style: normal; font-family: var(--sans); font-size: 11px;
  color: var(--muted);
  justify-self: end; font-variant-numeric: tabular-nums; }
.subs button:hover { color: var(--ink); }
.subs li.on button { color: var(--ink); font-style: italic; }
.subs li.on button em { color: var(--amber-deep); opacity: 1; }

/* ---------- the tray bar ----------
   The tray earns a headline: which shelf you are looking at, said in the
   serif, with the live detail of whatever the pointer is over said quietly
   at the right. The peek line is fixed-height so hovering never shifts
   the cards below. */
.shelfrow { padding: .5rem 1.1rem .55rem; border-bottom: 1px solid var(--rule-line); }
.shelfbar { display: flex; align-items: baseline; gap: 1rem; min-height: 1.6rem; }
.shelfname { font-family: var(--serif); font-weight: 400; font-size: 1.15rem; margin: 0; }
.shelfname i { font-style: italic; font-family: var(--text); font-size: .95rem; color: var(--muted); }
.shelfpeek { margin-left: auto; font-family: var(--text); font-style: italic; font-size: .95rem;
  color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.shelfpeek b { font-weight: 400; color: var(--ink); }

/* ---------- the photo booth builder ----------
   One booth, two questions. The tiles are the only two looks that exist;
   the hours are pills; the add button blooms once both answers are in —
   same seed, same promise as every gold action on the site. */
.booth { display: flex; align-items: center; flex-wrap: wrap; gap: 1rem; padding: .5rem 0 .2rem; }
.booth[hidden] { display: none; }
.booth .blbl { font-family: var(--text); font-style: italic; font-size: .98rem; color: var(--muted); }
.booth .btiles { display: flex; gap: .5rem; }
.booth .btile {
  width: 88px; padding: 6px 6px 5px; display: grid; gap: 3px; justify-items: center;
  border: 1px solid var(--rule-line); border-radius: var(--radius); background: var(--paper);
  transition: border-color .18s, box-shadow .18s, transform .22s var(--ease);
}
.booth .btile:hover { border-color: var(--amber); transform: translateY(-2px); }
.booth .btile.on { border-color: var(--honey); box-shadow: 0 0 0 2px var(--honey); }
.booth .btile img { height: 62px; object-fit: contain; }
.booth .btile em { font-style: normal; font-family: var(--sans); font-size: 11px;
  letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.booth .btile.on em { color: var(--ink); }
.booth .bhours { display: flex; gap: .35rem; }
.booth .bhours button {
  border: 1px solid var(--rule-line); border-radius: var(--radius-sm); background: var(--paper);
  padding: .35rem .7rem; font-family: var(--sans); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted);
  transition: border-color .18s, box-shadow .18s, color .18s;
}
.booth .bhours button:hover { border-color: var(--amber); color: var(--ink); }
.booth .bhours button.on { border-color: var(--honey); box-shadow: 0 0 0 2px var(--honey); color: var(--ink); }
.booth .badd {
  position: relative; overflow: hidden; isolation: isolate;
  font-family: var(--sans); font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink); background: transparent;
  border: 1.5px solid var(--amber); border-radius: var(--radius);
  padding: .5rem 1rem .5rem 1.7rem; white-space: nowrap;
}
.booth .badd::before {
  content: ''; position: absolute; z-index: -1; left: 13px; top: 50%;
  width: var(--btn-dot); height: var(--btn-dot);
  margin: calc(var(--btn-dot) / -2) 0 0 calc(var(--btn-dot) / -2);
  border-radius: 50%; background: var(--amber);
  transform: scale(1); transform-origin: 50% 50%;
  transition: transform var(--btn-dur) var(--btn-ease); will-change: transform;
}
.booth .badd:hover::before, .booth .badd:focus-visible::before { transform: scale(55); }
.booth .badd[hidden] { display: none; }
.booth .bprice { font-family: var(--text); font-style: italic; font-size: .95rem; color: var(--amber-deep); }

/* ---------- the tray ----------
   Bigger chips than P4 — 64px is where a charger photo stops being an icon
   and starts being a product.
   ⚠️ WRAPS AND SCROLLS DOWN, NEVER SIDEWAYS. A horizontal strip is a
   trackpad gesture wearing a mouse-hostile disguise: the wheel did nothing
   and nothing said "there is more to the right". Rows wrap, the wheel
   scrolls, and the honey scrollbar is always painted — macOS overlay
   scrollbars were hiding the one clue the strip had. */
.shelf {
  display: flex; flex-wrap: wrap; gap: .4rem;
  max-height: 9.4rem; overflow-y: auto; overflow-x: hidden;
  padding: .45rem 8px .45rem 2px;
  scrollbar-width: auto; scrollbar-color: var(--amber) var(--wash);
}
.shelf::-webkit-scrollbar { width: 10px; }
.shelf::-webkit-scrollbar-track { background: var(--wash); border-radius: 5px; }
.shelf::-webkit-scrollbar-thumb { background: var(--honey); border: 2px solid var(--wash);
  border-radius: 5px; }
.shelf::-webkit-scrollbar-thumb:hover { background: var(--amber); }
/* ⚠️ THE PACKET'S CHIP BLOCK WAS WRITTEN AGAINST THE BASE `.tchip` RULE, AND
   THIS ONE OVERRIDES IT. The packet describes a 54px chip with `padding: 5px`
   and `max-width/max-height` on the image, asking for 64px at 62% art. That is
   the rule at the top of this file. Every chip on the page is rendered into
   `#shelf`, so `.shelf .tchip` (0,2,0) wins over `.tchip` (0,1,0) and
   `.shelf .tchip > img` (0,2,1) wins over `.tchip img` (0,1,1) — the packet's
   block applied literally would have been overridden and changed nothing
   visible.
   The INTENT is applied here instead, where it ships: 64px was already true,
   `padding: 0` was already true, and `overflow: hidden` is what the zoom hover
   actually needed. The art ratio moves on the `> img` rule below. */
.shelf .tchip {
  width: 64px; height: 64px; flex: none; border: 1px solid var(--rule-line);
  border-radius: var(--radius); background: var(--paper);
  padding: 0; position: relative; touch-action: none; cursor: grab;
  overflow: hidden;                       /* REQUIRED by the zoom hover */
  /* motion 50%, packet §2 — .18s/.22s/.22s halved */
  transition: border-color .09s, transform .11s var(--ease), box-shadow .11s var(--ease);
}
/* ⚠️ Pinned to the chip's own box, not sized by the grid track. A percentage
   max-height inside an auto grid row resolves to nothing, which is how a
   wine glass stem climbed out of its chip and into the row below.
   ⚠️ DIRECT CHILD ONLY. The napkin chip carries an img of its own, nested
   inside the tint machinery — pinning that one stretched the white multiply
   photograph across the whole chip and drew a colour veil around the
   napkin. Product photos are direct children; the napkin's is not. */
/* 62% art, packet §2.
   ⚠️ THE INSET IS MEASURED FROM THE PADDING BOX, NOT THE 64px CHIP. `100%` on
   an absolutely positioned child resolves against the padding box, which is
   62px here — the chip's 64px less its 1px border on each side. So the arithmetic
   is (62 - 39.7) / 2 = 11px, giving 40px of art against the 64px chip: 62.5%.
   Doing it against 64 gives inset 12px and 59.4%, which is what this rule said
   first and what a probe of the rendered page caught.
   It was inset 6px, which drew 52/64 = 81% — the figure the packet quotes for
   the live chip, reached there by a different route (54px less 10px padding). */
.shelf .tchip > img { position: absolute; inset: 11px;
  width: calc(100% - 22px); height: calc(100% - 22px); object-fit: contain;
  transition: transform .12s var(--ease); }

/* ---- the chip hover: ZOOM ----
   ⚠️ DIRECT CHILD, for the reason the rule above is a direct child: the napkin
   chip carries an img of its own inside the tint machinery, and scaling that
   one drags the multiply photograph and its colour veil with it.
   Written for pointer AND keyboard together, because the Edge hover it
   replaces had a :focus-visible partner and dropping it would make the shelf
   mouse-only. */
.shelf .tchip:hover > img,
.shelf .tchip:focus-visible > img { transform: scale(1.14); }

/* Same requirement as the wall's zoom. The glow is a box-shadow, not motion,
   but its transition is stilled here too so a hover is a state change rather
   than an animation for a reader who asked for neither. */
@media (prefers-reduced-motion: reduce) {
  .shelf .tchip > img { transition: none; }
  .shelf .tchip:hover > img,
  .shelf .tchip:focus-visible > img { transform: none; }
  .card.table:not(.open):hover { transition: none; }
}
/* The catalogue sketch, at every size it appears: the /rentals line
   drawing standing in for a photograph that has not been shot yet. */
.shelf .tchip .tsk { display: grid; place-items: center; height: 100%; color: var(--muted); }
.shelf .tchip .tsk .glyph { width: 32px; height: 32px; }
.cov .sk { display: grid; place-items: center; }
.cov .sk .glyph { width: 72%; height: 72%; }
.stack li > span .glyph { width: 26px; height: 26px; color: var(--muted); display: block; margin: 0 auto; }
/* A chip whose piece is already on the selected table wears the amber seed,
   so the tray always knows what the table knows. */
.shelf .tchip.inuse::after { content: ''; position: absolute; right: 4px; top: 4px;
  width: 7px; height: 7px; border-radius: 50%; background: var(--amber);
  box-shadow: 0 0 0 2px var(--paper); }

/* A swatch is chosen, not carried — it takes a click and shows which one is
   on. FLAT COLOUR, no photograph: two rounds of fabric imagery each brought
   a pattern of their own, so the linen is a paint chip now — Darrell's
   final call, 2026-08-14. The napkin keeps its photograph; a napkin is an
   object, a linen here is a colour. */
.shelf .tchip.dress { cursor: pointer; padding: 0; overflow: hidden; }
/* ⚠️ ONE MARK FOR "ON THE TABLE". A chosen shade wears exactly the seed a
   placed product wears — same dot, same corner — so a bride learns the
   mark once. The paper halo is what keeps it legible on a navy swatch.
   Honey rings belong to CARD selection alone. */
.shelf .tchip.dress.on::after { content: ''; position: absolute; right: 4px; top: 4px;
  width: 7px; height: 7px; border-radius: 50%; background: var(--amber);
  box-shadow: 0 0 0 2px var(--paper); }
.tchip.dress .cloth {
  display: block; width: 100%; height: 100%; background: var(--cloth);
}
.napbox { display: block; height: 100%; margin: 0 auto; }

/* ⚠️ THE SWATCH'S WHOLE APPEARANCE WAS SCOPED TO THE CHIP, AND THE GHOST IS
   NOT THE CHIP. `.tchip.dress .cloth` above carries display:block, the size AND
   `background: var(--cloth)` — so a clone of that markup dropped anywhere else
   is a bare inline <span> with no colour and no box. Dragging a linen produced
   exactly that: Darrell, 2026-09-03, "it just has like this ghost container".
   It ignored the width and height the drag sets on it, too, because an inline
   box has neither.
   The napkin never showed this because `.napbox` on the line above is
   unscoped, so its clone kept its display and its inline aspect-ratio. */
.ghost .cloth {
  display: block; background: var(--cloth);
  border-radius: calc(var(--radius) * 0.6);
  box-shadow: inset 0 0 0 1px rgba(29, 29, 27, .10);
}

/* ---------- the stack ----------
   Below the rooms now, and it scrolls its own overflow — a tall cover must
   not push the room index off the page. Same honey bar as the tray, so
   "there is more" reads the same wherever it happens. */
.stack { list-style: none; margin: 0; padding: 0 6px 0 0; display: grid; gap: .3rem;
  align-content: start;
  flex: 1 1 auto; min-height: 5rem; overflow-y: auto;
  scrollbar-width: auto; scrollbar-color: var(--amber) var(--wash); }
.stack::-webkit-scrollbar { width: 10px; }
.stack::-webkit-scrollbar-track { background: var(--wash); border-radius: 5px; }
.stack::-webkit-scrollbar-thumb { background: var(--honey); border: 2px solid var(--wash);
  border-radius: 5px; }
.stack::-webkit-scrollbar-thumb:hover { background: var(--amber); }
.stack li {
  display: grid; grid-template-columns: 30px 1fr auto; gap: .5rem; align-items: center;
  border: 1px solid var(--rule-line); border-radius: var(--radius-sm);
  background: var(--paper); padding: .3rem .45rem;
}
/* ⚠️ THE LINEN ROWS WORE var(--wash) AND IT STOPPED BEING A TINT.
   This was a pale wash marking the two rows that are a surface rather than a
   piece, back when --wash was the house mist #E7EEE9. The 2026-08-29 packet
   re-scoped --wash to #4C9A74 for body.em here, and a whisper became a
   saturated green block behind one product row — Darrell, 2026-08-31: "the
   napkins are coming through and their card fill is not matching the others."
   It also took the row's own sub-label under AA, which nothing was watching.
   Same shape as the mist star in the favourite seal: a token moved underneath
   a rule that had been written for its old value. Every row is paper now, and
   the swatch is what says a napkin is a napkin. */
.stack img { max-width: 30px; max-height: 30px; object-fit: contain; }
.stack b { font-family: var(--text); font-size: .95rem; font-weight: 400; line-height: 1.15; }
.stack b span { display: block; font-family: var(--sans); font-size: 11px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted); }
.stack .drop { border: 0; background: none; color: var(--muted); font-size: 13px; padding: 0 .2rem;
  transition: color .15s; }
/* The accent does the warning — this brand never raises its voice in red. */
.stack .drop:hover { color: var(--amber-deep); }
.stack .none { border-style: dashed; color: var(--muted); font-family: var(--text); font-style: italic;
  grid-template-columns: 1fr; padding: .5rem; font-size: .92rem; }
.stack .napswatch { display: block; width: 22px; height: 22px; border: 1px solid rgba(0,0,0,.15);
  border-radius: var(--radius-sm); }

/* ---------- the card strip ---------- */
/* ⚠️ THE RIGHT PADDING IS THE EVENT DOCK'S GUTTER, not a spacing preference.
   The dock's tab is vertically centred over this strip, so without a reserved
   lane the last card sits UNDER it. Caught by screenshot, not by a check: the
   tab was fully on screen and the canvas had not been resized, so every
   assertion was true while the tab was painted on top of the add-a-table card.
   AGENTS.md §2, trust the screenshot over the assertion.
   ⚠️ It is CONSTANT, present whether the dock is open or shut. Adding it on
   open would resize the boards, which is the thing the overlay exists to
   avoid. */
.cards { flex: 1; display: flex; gap: .6rem; padding: .8rem 3rem .8rem .8rem; min-height: 0; overflow-x: auto; }
.card {
  position: relative; flex: 0 0 7.2rem; min-width: 0; border: 1px solid var(--rule-line);
  border-radius: var(--radius-lg);
  background: var(--paper); overflow: hidden; cursor: pointer;
  /* motion 50%, packet §2. ⚠️ --fold-dur IS LEFT ALONE ON PURPOSE: it drives
     the card's fold-open, which is the app's own choreography rather than a
     hover or state transition, and the lab never previewed it. */
  transition: flex-grow var(--fold-dur) var(--ease), flex-basis var(--fold-dur) var(--ease),
    border-color .1s, box-shadow .125s var(--ease);
  display: flex; flex-direction: column;
}
/* ⚠️ WIDE, NOT OPEN. Table styles exist to be compared, so they share the
   strip evenly; `open` only says which card a drop lands on. The floor keeps
   the old room-card grammar — collapsed until opened, then it takes the room. */
.card.spread { flex: 1 1 0; min-width: 13rem; cursor: default; }
/* ---- the table-card hover: GLOW, packet §2 ----
   ⚠️ :not(.open) IS LOAD-BEARING. A selected card must not also show its
   hover, or it carries two competing edges.
   ⚠️ The glow and the selection are now one step apart in the same green —
   halo --amber #45705C, selection ring --wash #4C9A74. They never land on the
   same card, but they read as siblings when a hovered card sits beside a
   selected one. Recorded, per the packet, as something to look at on the
   board rather than something to pre-emptively change. */
.card:not(.open):hover { border-color: var(--amber); }
.card.table:not(.open):hover {
  box-shadow: 0 0 0 1px var(--amber),
              0 0 36px -6px color-mix(in srgb, var(--amber) 65%, transparent);
}
@supports not (color: color-mix(in srgb, red 50%, transparent)) {
  .card.table:not(.open):hover {
    box-shadow: 0 0 0 1px var(--amber), 0 0 36px -6px rgba(69, 112, 92, .65);
  }
}
/* Named while you are carrying something over it, so a drop is never a guess
   about which table it landed on. */
/* ⚠️ ONE LINE, NOT TWO. Selection is the bold honey alone — the border
   joins the ring's colour instead of sitting inside it as a dark hairline.
   Hover keeps its quiet amber; the two states never stack. */
/* Selection moves to --wash, packet §2. It is NOT --honey: that token also
   drives the dock count, the tray close button and the room-index underline,
   so moving it to recolour a table card would silently recolour all four.
   ⚠️ SELECTING A CARD NOW LOWERS ITS EDGE CONTRAST, and this is known and
   accepted. The resting border is --rule-line #0E5F41 at 7.69:1; the selected
   border is --wash #4C9A74 at 3.40:1, and both rings drop a step at the same
   time (3px to 2px here, 2px to 1px below). The packet names three ways out —
   hold the ring at 2px, select with --amber, or return the resting border to a
   quiet neutral — and applies none of them. Do not apply one here either: it
   is a design call to make while looking at the board. */
.card.receiving { border-color: var(--wash); box-shadow: 0 0 0 2px var(--wash); }
/* ⚠️ .card.open, NOT .card.table.open. Darrell, 2026-08-31: "I'd like the
   selection outline to apply to every card, every table card, and the open
   floor." The floor was the only card that could be selected and not say so —
   it had been showing its selection by GROWING instead, which is the same
   behaviour the ear replaced. One card is selected at a time, so one outline
   means one thing wherever it appears. */
.card.open { border-color: var(--wash); box-shadow: 0 0 0 1px var(--wash); }

/* ---------- the control panel ----------
   No gradient, no wash: the card's foot is its own SECTION, cut from the
   cloth by a hard hairline the way every container on the site is drawn.
   The cloth is the exhibit; this strip is the label under the frame — so it
   is always paper and ink, whatever the linen above it is wearing. Name and
   description on the left at a size that owns the strip; the count, the
   price, and the two text actions tucked against the right. */
.card .caption { pointer-events: none; }
.card.spread .caption {
  position: static; flex: none; z-index: 4; pointer-events: auto;
  display: grid; grid-template-columns: 1fr auto; align-items: center; gap: .1rem .9rem;
  padding: .6rem .85rem .65rem;
  background: var(--paper); border-top: 1px solid var(--rule-line);
  color: var(--ink);
}
.card .caption .who { min-width: 0; }
.card .caption b { font-family: var(--serif); font-weight: 400; font-size: 1.35rem;
  line-height: 1.15; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card .caption b[contenteditable] { outline: none; border-bottom: 1px dashed currentColor; }
/* The rename cue, drawn in the page's own hand: hover the name and a
   dashed amber rule offers the edit — no browser tooltip. */
.card.table.spread .caption b { cursor: text; border-bottom: 1px dashed transparent;
  transition: border-color .18s; }
.card.table.spread .caption b:hover { border-bottom-color: var(--amber); }
.card.spread .caption i { font-family: var(--text); font-style: italic; font-size: .95rem;
  color: var(--muted); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card .caption .meta { display: grid; justify-items: end; gap: .15rem; }
.card .caption em { font-style: normal; font-family: var(--sans); font-size: 11px;
  letter-spacing: .1em; text-transform: uppercase; color: var(--muted);
  white-space: nowrap; font-variant-numeric: tabular-nums; }
/* A folded card turns its name on its side rather than truncating it — the
   panel needs width a folded spine does not have, so the fold keeps the old
   overlay grammar: name up the edge, count in the chip. */
.card:not(.spread) .caption { position: absolute; inset: 0 auto 0 0; z-index: 4;
  writing-mode: vertical-rl; transform: rotate(180deg); background: none;
  padding: .6rem .45rem; display: flex; align-items: center; }
.card:not(.spread) .caption b { font-size: 1.05rem; }
.card:not(.spread) .caption .who { min-width: auto; }
.card:not(.spread) .caption i, .card:not(.spread) .caption .meta { display: none; }

/* The folded manifest wears a count so the tally survives at a glance. */
.countchip { position: absolute; left: 50%; bottom: .5rem; transform: translateX(-50%); z-index: 5;
  min-width: 20px; height: 20px; padding: 0 5px; border-radius: 10px; pointer-events: none;
  display: grid; place-items: center; font-family: var(--sans); font-size: 11px;
  font-variant-numeric: tabular-nums;
  background: var(--cap-wash, rgba(248,248,247,.96)); color: var(--ink-on, var(--ink));
  border: 1px solid color-mix(in srgb, currentColor 25%, transparent); }
.countchip[hidden] { display: none; }

/* ---------- the card IS the linen ----------
   In flow now, not an overlay: the surface takes what the control panel
   leaves, so the cloth and the panel meet on a real line instead of a fade.
   ⚠️ FLAT COLOUR, deliberately. The photographed weave went through two
   generations (stripy polyester, then a cleaner Higgsfield base) and each
   still read as a pattern of its own on a full card. The linen is a swatch
   now — the colour, evenly applied, nothing else. The imagery lives in the
   pieces and the napkin, where objects belong. */
.surface {
  position: relative; flex: 1 1 auto; min-height: 0; isolation: isolate;
  background: var(--cloth, var(--paper));
}
.card.spread .surface { cursor: default; }

/* ---------- the dog-ear ----------
   The fold control is a turned-back corner of the cloth itself: the notch
   shows the page behind, the flap is the linen's paler underside, and it
   peels wider when the pointer reaches for it. The label rides beside it so
   the gesture still has a name.
   ⚠️ The button is the whole 44px corner, not the drawing — a fold target you
   have to hit at 22px is a fold you miss half the time. */
.card .tools { position: absolute; right: 0; top: 0; z-index: 6;
  display: flex; align-items: flex-start; gap: 5px; }
.card .fold {
  position: relative; width: 44px; height: 44px; padding: 0; border: 0; background: none;
  color: var(--ink-on, var(--ink));
}
.card .fold .ear { position: absolute; right: 0; top: 0; width: var(--earw, 20px); height: var(--earw, 20px);
  transition: width .25s var(--ease), height .25s var(--ease), filter .25s; }
/* The notch: the page showing where the cloth has been lifted away. */
.card .fold .ear::before { content: ''; position: absolute; inset: 0;
  background: linear-gradient(to bottom left, var(--ground) 50%, transparent 50.5%); }
/* The flap: the underside of the linen, paler than its face, with the soft
   shadow a lifted corner throws. */
.card .fold .ear::after { content: ''; position: absolute; inset: 0;
  background: linear-gradient(to bottom left, transparent 50%,
    color-mix(in srgb, var(--cloth, #fff) 62%, #fff) 50.5%);
  border-radius: 0 0 0 var(--radius-sm);
  box-shadow: -3px 3px 7px rgba(29,29,27,.28); }
.card .fold:hover .ear, .card .fold:focus-visible .ear { --earw: 30px; }
/* The ear announces itself: ONE pull and fall back — the same tug hover
   gives, on birth, with the word "Fold" flashing beside it so the corner
   gets named exactly once. The double bounce read as a stumble. */
@keyframes earpeek {
  45% { width: 32px; height: 32px; }
}
@keyframes lblflash {
  0%, 100% { opacity: 0; transform: translateX(4px); }
  25%, 75% { opacity: 1; transform: none; }
}
.card.table:hover .fold .ear { animation: earpeek .6s var(--ease); }
.card.hatch .fold .ear { animation: earpeek .7s var(--ease) .5s; }
.card.hatch .fold .lbl { animation: lblflash 1.5s var(--ease) .4s; }
/* Clean standing text, no container — the flat linen leaves nothing busy
   behind it for a label to need shelter from. */
.card .fold .lbl { position: absolute; right: 34px; top: 7px;
  font-family: var(--sans); font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-on, var(--ink));
  opacity: 0; transform: translateX(4px); transition: opacity .2s, transform .25s var(--ease);
  pointer-events: none; white-space: nowrap; }
.card .fold:hover .lbl, .card .fold:focus-visible .lbl { opacity: 1; transform: none; }
/* On a folded card the ear reads "open me": it sits pinned flat and the whole
   card is the unfold, so the ear only confirms on hover. */
.card:not(.spread) .fold .ear { --earw: 16px; }
.card:not(.spread) .fold .lbl { right: 26px; }

/* ---------- card actions ----------
   Standing text in the panel, nothing else: the house caps, muted until
   wanted, with the honey rule drawing underneath on hover — the same move
   the room index and the nav make. No frames, no chips, no chrome. */
.acts { display: flex; gap: 1rem; }
.acts button {
  position: relative; border: 0; background: none; padding: 1px 0 4px;
  font-family: var(--sans); font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--muted); transition: color .18s;
}
.acts button:hover, .acts button:focus-visible { color: var(--ink); }
.acts button::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 1px; height: 2px;
  background: var(--honey); transform: scaleX(0); transform-origin: 0 50%;
  transition: transform .34s var(--ease);
}
.acts button:hover::after, .acts button:focus-visible::after { transform: scaleX(1); }

/* ---------- pieces on the cover ----------
   Reconciled, never wiped — so left/top/width/height transitions carry a
   piece between its cover slot and its manifest row. The transition matches
   the card's own fold, which is what makes the two read as one gesture.
   ⚠️ Yes, these are layout properties, on purpose. A piece is centred by
   translate(-50%,-50%) and positioned in % of a parent that is itself
   resizing, so transform cannot express this move. At most seven pieces
   animate per card, once per fold — a choreography, not a scroll effect. */
/* ⚠️ SAME SHRINK-TO-FIT TRAP AS .pc, AND THE FIX FOR .pc DID NOT REACH HERE.
 * Corrected 2026-08-16 after Darrell reported the open floor fixed and the
 * table cards still shrinking. Two rendering paths, one bug, and only one of
 * them had been driven: floor pieces are .pc through mount()/paint(), cover
 * pieces are .cov through reconcile()/setPiece(). Verifying one and reporting
 * both fixed is the defect, not the missing property.
 *
 * `left` is a PERCENTAGE here, so at left:95% the available width is 5% of the
 * surface and base.css's `img { max-width: 100% }` squeezes the photograph
 * into it. The clamp itself was always sound — pieceBox() reads naturalWidth,
 * never offsetWidth — so unlike .pc this shrank without walking, which is
 * exactly what Darrell described.
 *
 * width: max-content sizes the box to the photograph wherever it sits. An
 * explicit inline width still wins, which is what the napkin needs. */
.cov { position: absolute; width: max-content; max-width: none;
  transform: translate(-50%,-50%); pointer-events: none;
  transition: left var(--fold-dur) var(--ease), top var(--fold-dur) var(--ease),
    width var(--fold-dur) var(--ease), height var(--fold-dur) var(--ease),
    opacity .25s, transform var(--fold-dur) var(--ease); }
.cov > img, .cov > .sk, .cov > .napbox { max-width: none; }
.cov.enter { opacity: 0; transform: translate(-50%,-58%) scale(1.1); }
.cov.exit { opacity: 0; transform: translate(-50%,-46%) scale(.85); }
/* Pieces are movable once they are on the table — the lineup says where a
   piece lands, the bride says where it stays. The grab is only offered on a
   wide card; the folded manifest is a display, not a control. */
.card.table.spread .cov { pointer-events: auto; cursor: grab; touch-action: none; }
.card.table.spread .cov:hover > img { filter: drop-shadow(0 9px 15px rgba(29,29,27,.30)); }
.cov.drag { transition: none; cursor: grabbing; }
.cov.drag > img { transition: none; filter: drop-shadow(0 12px 20px rgba(29,29,27,.34)); }
.cov img { -webkit-user-drag: none; user-select: none; }
/* Each piece carries its own remove. Enabled only where the piece itself is
   interactive — inside pointer-events:none the button must not re-awaken. */
.cov .px {
  position: absolute; left: -7px; top: -7px; width: 18px; height: 18px; padding: 0;
  border: 0; background: var(--cap-wash, rgba(248,248,247,.92));
  border-radius: var(--radius-sm);
  font-size: 11px; line-height: 1; color: var(--ink-on, var(--ink));
  opacity: 0; pointer-events: none; z-index: 8;
  transition: opacity .15s, background .18s, color .18s;
}
.card.table.spread .cov .px { pointer-events: auto; }
.card.table.spread .cov:hover .px, .card.table.spread .cov .px:focus-visible { opacity: 1; }
/* Honey on hover — the accent does the warning, the way the site would. */
.cov .px:hover { background: var(--honey); color: var(--ink); }
.cov.drag .px { opacity: 0; }
/* ⚠️ Direct child only. The napkin's own <img> carries a brightness/contrast
   filter this drop-shadow would silently replace. */
.cov > img { display: block; object-fit: contain; filter: drop-shadow(0 6px 10px rgba(29,29,27,.16));
  transition: width var(--fold-dur) var(--ease), height var(--fold-dur) var(--ease); }
.cov .sk { font-family: var(--serif); font-size: .7rem; color: var(--amber-deep);
  border: 1px dashed var(--amber); border-radius: var(--radius-sm);
  padding: .35rem .5rem; background: color-mix(in srgb, var(--honey) 10%, transparent); }
/* The manifest's "+n" lived here. Removed 2026-08-16 with the badge itself:
   the folded card now draws every candle and votive rather than one and a
   count, so there is nothing left to summarise. */
/* The landing: a settle with weight, and a halo that says "it took". */
.cov.landed { animation: settle .5s var(--ease); }
.cov.landed::after { content: ''; position: absolute; inset: -8px; border-radius: 50%;
  border: 2px solid var(--honey); opacity: 0; animation: halo .7s var(--ease) .05s; }
@keyframes settle { 0% { scale: 1.14; } 60% { scale: .985; } 100% { scale: 1; } }
@keyframes halo { 0% { opacity: .9; scale: .6; } 100% { opacity: 0; scale: 1.15; } }

/* The promise before the commit: while a piece is carried over a table, a
   dashed ring marks the slot — and the inches — it will land on.
   ⚠️ Above every piece, or the ring for a charger slot hides behind the
   charger it is about to replace. */
/* ⚠️ ROUND BY SLOT, exactly as .gh is — see the note in showSlotGhost(). This
   was an unconditional 50%, which drew a circle over every rectangular slot.
   The two rules are deliberately identical in shape and in the list of slots
   they name; if one moves the other has to move with it. */
.slotghost { position: absolute; transform: translate(-50%,-50%); z-index: 7; pointer-events: none;
  border: 2px dashed var(--amber); border-radius: var(--radius-sm); background: color-mix(in srgb, var(--honey) 12%, transparent);
  /* The glow is what keeps the ring legible when it lands exactly on the
     rim of the piece it is about to replace — dashed amber alone vanishes
     into a gold charger of the same inches. */
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--honey) 45%, transparent), inset 0 0 0 3px rgba(255,255,255,.5);
  animation: slotbreathe 1.3s ease-in-out infinite;
  transition: left .15s, top .15s, width .15s, height .15s; }
/* The round slots, named exactly as .gh names them. Chargers and plates are
   round and the catalogue sizes them on a single axis; everything else is
   previewed in the rectangle its piece occupies. */
.slotghost[data-slot="charger"],
.slotghost[data-slot="dinner"],
.slotghost[data-slot="salad"] { border-radius: 50%; }
/* ⚠️ THE SCALE LIVES IN `transform`, BESIDE THE TRANSLATE, AND THAT IS THE
   WHOLE FIX. Darrell, 2026-09-01: "the drop targets appear to all be pulsing
   from the bottom right of the shape. I'd like that pulse to occur from dead
   center."

   It was the independent `scale` property against a `transform: translate
   (-50%,-50%)`. CSS composes those as translate, rotate, scale, THEN the
   transform property — so the -50% centring shift was itself scaled, growing
   to -51.75% at the peak. The element drifted up and left exactly as much as
   it grew, which pins the bottom-right corner and reads as the pulse starting
   there. Nothing was off-centre at rest, which is why it only showed in
   motion. One transform, one origin, one matrix: it now grows about its own
   middle. */
@keyframes slotbreathe { 50% { transform: translate(-50%, -50%) scale(1.035); } }

/* ---------- CAN-4 · the five-step spine ----------
 * ⚠️ STATE IS NEVER CARRIED BY COLOUR ALONE. Each step also changes its
 * numeral: a done step swaps the digit for a check, and the current step is
 * marked with aria-current plus a weight and a rule under it. A reader who
 * cannot separate the greens still gets the state. */
.spine {
  display: flex; flex-wrap: wrap; gap: 0 1.6rem; list-style: none;
  margin: 0; padding: .55rem 1.1rem;
  border-bottom: 1px solid var(--rule-line); background: var(--paper);
}
.spine li {
  display: flex; align-items: baseline; gap: .4rem;
  font-family: var(--sans); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted);
  padding: .2rem 0; border-bottom: 2px solid transparent;
}
.spine .sp-n {
  font-variant-numeric: tabular-nums; font-size: 11px;
  min-width: 1.1em; text-align: center; color: var(--muted);
}
.spine .sp-v { text-transform: none; letter-spacing: .02em; color: var(--ink); }
/* Step 1 is a control. It inherits the row's layout so the spine reads as one
   line of five whatever the middle one is made of. */
.spine .sp-go {
  display: inherit; align-items: inherit; gap: inherit;
  font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit;
  background: none; border: 0; padding: 0; margin: 0; cursor: pointer;
}
.spine .sp-go:hover .sp-l { color: var(--amber-deep); }
.spine li[data-state="done"] { color: var(--amber-deep); }
.spine li[data-state="done"] .sp-n { color: var(--amber-deep); }
.spine li[data-state="now"] { color: var(--ink); border-bottom-color: var(--amber); }
.spine li[data-state="now"] .sp-l { font-weight: 500; }
.spine li[data-state="todo"] .sp-v { color: var(--muted); }

/* ---------- CAN-5 · the empty board as a map of the setting ----------
 * Replaced the single .coverhint ring, which marked the charger and nothing
 * else. Every empty slot is now a labelled ghost standing on the exact line
 * its piece will stand on.
 *
 * ⚠️ FAINT IS SPENT HERE, NOT ON A COLOUR. Darrell specified these as faint,
 * and after the 2026-08-29 packet --rule-line is a 7.69:1 emerald stroke.
 * Colours are locked, so the levers are the ones that are not colour: the
 * CONTAINER carries the opacity, the outline is a dashed hairline rather than
 * the 2px the old ring used or the 2-3px the selection rings use, and the
 * label is the half that carries the meaning. A solid outline at full weight
 * would make an empty board look full, which defeats the whole item.
 *
 * ⚠️ NOT A CLICK TARGET. pointer-events stay off: an empty slot has nothing to
 * place, and a ghost that swallowed the click would promise something the slot
 * cannot do. aria-hidden on the container, because the slots themselves carry
 * their own labels and doubling them reads as stutter. */
.ghosts { position: absolute; inset: 0; z-index: 2; pointer-events: none;
  opacity: .42; color: var(--ink-on, var(--ink)); transition: opacity .3s; }
.ghosts[hidden] { display: none; }
.gh { position: absolute; transform: translate(-50%,-50%);
  display: grid; place-items: center;
  border: 1px dashed currentColor; border-radius: var(--radius-sm); }
/* ⚠️ ROUND BY SLOT, NOT BY POSITION. This was `.gh:first-child`, which rounds
   whichever ghost is first in the list rather than the round ones — so with
   the plates already placed, the NAPKIN drew as a circle. Chargers and plates
   are round and the catalogue sizes them on a single axis; everything else
   sits in the rectangle its piece occupies. */
.gh[data-slot="charger"],
.gh[data-slot="dinner"],
.gh[data-slot="salad"] { border-radius: 50%; }
/* ⚠️ ABOVE THE SHAPE, NOT UNDER IT. Darrell, 2026-09-01. Under the shape the
   plate trio's three names landed on each other — they share a base line — and
   the fix for that was a stacked column pulled away from the shapes entirely.
   Above, with the two lower plates hung off their right shoulders, each name
   sits with its own thing and nothing collides. */
.gh em { position: absolute; bottom: 100%; margin-bottom: .2rem;
  left: 50%; transform: translateX(-50%);
  font-family: var(--text); font-style: italic; font-size: .72rem;
  white-space: nowrap; opacity: .9; }
/* ⚠️ THE NAME SITS IN THE BOX'S EMPTY TOP-RIGHT CORNER, and the corner is the
   whole trick. These two slots are CIRCLES in square boxes, so the corner is
   the one part of the box the plate does not reach — a name there is hard
   against the plate's edge without ever covering it.

   ⚠️ MOVED DOWN AND RIGHT AGAIN ON 2026-09-01, second pass. Above the box and
   right-aligned, "Dinner plate" was landing on the glass slot's dashed
   rectangle, which sits over the charger's right shoulder. Darrell: move both
   down and right together, just off the edge of the plate. Dropping to the
   box's top line clears the glass entirely — it ends where this begins — and
   the outward shift puts the name beside its own circle rather than over the
   nest of three. */
.gh[data-anchor="right"] em {
  left: auto; right: 0; bottom: auto; top: 0;
  margin-bottom: 0;
  /* ⚠️ TUNED AGAINST THE GLASS SLOT, NOT PICKED. At 52%/-30% the "Dinner
     plate" name cleared the glass box's bottom edge by two pixels, which is
     rounding rather than clearance and would close up the first time a label
     or a fold changed. Further right and further down puts real air under it.
     The numbers are checked on every run — see the landing-target report. */
  transform: translate(64%, -12%);
}

/* ⚠️ `.ghkey` IS RETIRED, 2026-09-01, and this note is where it stood. It was
   the plate trio's three names in one column under the setting, built because
   three labels under three shapes sharing a base line stack on each other.
   Labels moved above their shapes and the two lower plates hang theirs to the
   right, which separates the same three names without moving them away from
   what they name. Nothing references .ghkey; see the ghosts loop in design.js. */

/* ---------- the napkin: one photograph, every shade ----------
 * `i` is the flat shade cut to the silhouette; `img` is the white photograph
 * multiplied over it, its tonal range re-spread first — see napkinTint(). */
.nap { position: relative; display: block; width: 100%; height: 100%; isolation: isolate;
  filter: drop-shadow(0 5px 8px rgba(29,29,27,.20)); }
.nap i {
  position: absolute; inset: 0; background: var(--nap);
  -webkit-mask: var(--napimg) center / 100% 100% no-repeat;
          mask: var(--napimg) center / 100% 100% no-repeat;
}
.nap img {
  position: absolute; inset: 0; width: 100%; height: 100%; display: block;
  mix-blend-mode: multiply; filter: brightness(var(--napb)) contrast(var(--napc));
}
/* ⚠️ THE HIGHLIGHT A MULTIPLY CANNOT INVENT. `img` above only ever darkens,
   so on a dark shade the whole cloth — weave, hem, fold — lands inside that
   shade's own luminance: on black, five levels out of 255, which is a block
   and not linen. `.sheen` is the SAME photograph a second time, filtered so
   every thread at or below the photograph's average tone clamps to black and
   only the lit ones survive, then screened back over the result. Its curve
   (--napsb / --napsc) is solved from the photograph's measured mean in
   build/napkin-mask.mjs; its strength (--naps) is the shade's, from
   napkinTint(). Light shades never emit the element at all. */
.nap .sheen {
  mix-blend-mode: screen; opacity: var(--naps, 0);
  filter: brightness(var(--napsb)) contrast(var(--napsc));
}
.nap.shot img { mix-blend-mode: normal; filter: none; }

/* ---------- add a table ----------
   ⚠️ GONE FROM THIS FILE ON 2026-08-31, and it is worth saying where it went.
   `.addcard` was a dashed vertical tab holding the end of the card row — a
   screen away from every other control on a wide board, and the one that
   starts the work. It is a section of the control panel now and an item on
   its collapsed rail: panel.css, `.cpanel__btn` and `.cpanel__railitem`.
   The resting-state argument it carried is not lost, only relocated — the
   panel states its invitation the same way, at rest rather than on hover.

   Its dashed edge is still cited as house precedent by the ghost note in
   design.js. That reference names this removal so it does not read as a
   pointer at nothing. */

/* ---------- the floor card ----------
   Plain white, start to finish — the cream grid read as decoration and the
   floor's job is to disappear behind what is placed on it. */
.card.floor .pc { pointer-events: auto; }
/* ⚠️ board.css's hover tag and corner × are RETIRED here, not restyled —
   the name lives in the stack now, and the controls live in the centred
   cluster below. Hidden rather than forked, because board.js still writes
   them and P4 still wants them. */
.card.floor .pc .tag, .card.floor .pc .x { display: none; }
/* The piece's own controls: a wash cluster at its centre, shown on hover —
   remove, and (lounge only) a flip across the vertical axis, so a chair
   facing left can face right. */
.card.floor .pc .ctrl {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); z-index: 6;
  display: flex; gap: 4px; opacity: 0; pointer-events: none; transition: opacity .15s;
}
.card.floor .pc:hover .ctrl, .card.floor .pc.sel .ctrl { opacity: 1; pointer-events: auto; }
.card.floor .pc .ctrl button {
  width: 22px; height: 22px; padding: 0; border: 0; border-radius: var(--radius-sm);
  background: rgba(248,248,247,.94); color: var(--ink); display: grid; place-items: center;
  font-size: 12px; line-height: 1;
  box-shadow: 0 2px 8px rgba(29,29,27,.22);
  transition: background .18s;
}
.card.floor .pc .ctrl button:hover { background: var(--honey); }
/* The flip flips the PICTURE, never the controls. */
.pc.flipped > img { transform: scaleX(-1); }
.cov.flipped > img { transform: scaleX(-1); }
/* ⚠️ THE FLOOR'S OWN HONEY RING WENT ON 2026-08-31. It was the "lighter,
   misty color" Darrell reported: --honey at 2px, so the one card that was not
   a table announced its selection in a different colour and a different
   weight from every card beside it. It takes .card.open now, like the rest.
   Selection is one question; a second answer to it is not a second meaning. */
/* Folded, the floor draws a strip of its largest pieces beside the sideways
   name — see floorFoldedSpecs(). A blind card was the whole complaint. */
.card.floor:not(.open) .cov > img { filter: drop-shadow(0 2px 3px rgba(29,29,27,.18)); }

/* ---------- the maths ----------
 *
 * ⚠️ ONE LINE, AND READ-ONLY. Darrell, 2026-08-17: "this bottom section is
 * taking up too much vertical space... on a laptop this takes up a sizeable
 * amount of real estate." It did, for two reasons that are both fixed here:
 * every figure stacked its value UNDER its label (`b { display: block }`),
 * which made the bar two lines tall on its own, and it also carried the guest
 * input, the stepper and the wish tray, which made it WRAP to three.
 *
 * The controls moved into the event dock. What is left is a glanceable strip:
 * label and value on one baseline, nothing that wraps, nothing that grows.
 */
/* ⚠️ THE MATHS BAR IS GONE, 2026-08-31, AND ITS FIGURES WITH IT. Guests, per
   table and on-the-floor were all being said twice once the spine landed —
   once at the top as steps, once at the foot as a strip — and the canvas was
   paying about 40px for the second copy. Darrell: "we've got duplicate
   information on the screen now ... what I'd like to do is give the design
   canvas more real estate."
   The note was the one thing on that strip that was not a duplicate, so it
   moved up beside the steps rather than going with them. */

/* The note is the first thing to go when the bar runs out of room, rather than
   a figure. It is encouragement; the figures are information. */
/* ⚠️ IT CARRIES THE MARK IT IS TALKING ABOUT. Darrell, 2026-08-20: "this is
   a key instructional prompt and it's nearly invisible in this location."
   It was muted italic on a grey strip — the type treatment this file uses for
   things that do not matter — and it was asking for the one action the send
   refuses without. It keeps its place, because the strip is always on screen
   and moving it would cost a layout; what changes is that it stops
   whispering. The star is the argument: "mark the one you love" is abstract
   until the mark is sitting in the sentence, and it is the same glyph, the
   same amber and the same circle as the seal on every card. Sentence stays
   sentence case per Layer 1 rule 9. */

/* ---------- the favourite seal ----------
 *
 * The one design that gets quoted, said on the card itself rather than only
 * in a caption action. Darrell, 2026-08-16: the text button at the bottom was
 * not clear enough, and the mark has to survive the fold.
 *
 * ⚠️ IT IS A BUTTON ON EVERY CARD, not a badge on one. A mark that only
 * appears once it is earned cannot be used to move it, which is the thing she
 * actually wants to do. Unchosen it sits quiet at low opacity; hover or focus
 * brings it up; chosen it fills amber and states the word.
 *
 * Amber, not the honey used for "currently open". Two different questions
 * deserve two different answers, and a card can be both at once. */
.card .fseal {
  position: absolute; left: 0; top: 0; z-index: 7;
  display: flex; align-items: center; gap: .4rem;
  padding: .45rem .6rem; border: 0; background: none; cursor: pointer;
  color: var(--muted); opacity: .45; transition: opacity .18s, color .18s;
}
.card .fseal:hover, .card .fseal:focus-visible { opacity: 1; color: var(--amber-deep); }
/* ⚠️ THE RESTING PIP DOES NOT FOLLOW THE CLOTH, AND THAT IS THE FIX FOR A
   REAL BUG. Darrell, 2026-09-01: "when a table linen is set, and you select
   this as a favorite, then select another table design as a favorite, the
   original favorite stays shaded when it should return to the preselected
   design."

   Nothing was wrong with the model — the click handler clears every table's
   `favorite` and draw() toggles .isfav off correctly. It was wrong on screen,
   and only on dark cloth. This background was var(--cap-wash), which design.js
   sets per card to follow the linen: rgba(20,20,18,.88) once onLinen() decides
   the cloth needs pale ink. So on a dark linen the UNSET pip was already a
   near-black disc, and the SET pip is --amber-deep, a dark green disc. Two
   dark circles a few points apart. The favourite moved and the card she moved
   it away from went on looking claimed.

   The seal does sit over the cloth, so following it was a reasonable instinct
   for legibility, and on pale linens it was invisible as a defect. But this
   control's whole job is to say set or unset, and a state indicator that
   borrows its ground from the thing behind it can always be made to collide
   with its own other state. Filled-and-dark now means favourite and nothing
   else; the pip keeps the paper ground on every cloth, and the star inside it
   reads against the pip rather than against the linen.

   The literal rather than var(--wash) for the same reason the claimed state
   below uses one: the 2026-08-29 packet re-scopes --wash to #4C9A74 here. */
.card .fseal .pip {
  width: 22px; height: 22px; flex: none; border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid currentColor; background: rgba(248,248,247,.92);
  transition: background .18s, border-color .18s, transform .3s var(--ease);
}
.card .fseal .pip svg { width: 12px; height: 12px; fill: currentColor; }
/* Sentence case per Layer 1 rule 9; 11px is the floor per rule 11. */
.card .fseal .fword {
  font-family: var(--sans); font-size: 11px; letter-spacing: .12em;
  text-transform: uppercase; white-space: nowrap;
}
.card:not(.isfav) .fseal .fword { opacity: 0; transition: opacity .18s; }
.card:not(.isfav) .fseal:hover .fword,
.card:not(.isfav) .fseal:focus-visible .fword { opacity: 1; }

/* ⚠️ THE CLAIMED STATE ONLY, AND THE MIST IS A LITERAL ON PURPOSE.
   Darrell, 2026-08-31: deep emerald circle, mist star, on the FAVOURITED seal
   — resting and hover stay exactly as they were, so the seal still gets
   quieter the less it has to say.
   #E7EEE9 is the house mist and would normally be var(--wash). It cannot be
   here: the 2026-08-29 packet re-scopes --wash to #4C9A74 for body.em on
   /rentals and /rentals/design, so var(--wash) would paint a mid-green star
   on a deep-green circle — 1.9:1, a star nobody can see. The literal is the
   house value; the token is the one that moved. */
.card.isfav .fseal { opacity: 1; color: var(--amber-deep); }
.card.isfav .fseal .pip {
  background: var(--amber-deep);            /* #0A3D2B */
  border-color: var(--amber-deep);
  color: #E7EEE9;
}
.card.isfav .fseal .pip svg { fill: #E7EEE9; }
/* A small settle when it is claimed, so moving the favourite reads as an
   event rather than as two cards quietly swapping styles. */
.card.isfav .fseal .pip { animation: sealset .42s var(--ease); }
@keyframes sealset { from { transform: scale(.6) rotate(-25deg); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) { .card.isfav .fseal .pip { animation: none; } }

/* ⚠️ FOLDED, THE WORD TURNS RATHER THAN VANISHING. Darrell asked for it to
   stay legible on a collapsed card, and a folded spine is ~100px wide with a
   vertical name already running up it. Borrowing that same grammar keeps one
   language on the card instead of inventing a second. */
.card:not(.spread) .fseal {
  flex-direction: column; gap: .3rem; padding: .4rem .25rem;
}
.card:not(.spread) .fseal .fword { writing-mode: vertical-rl; letter-spacing: .1em; }
.card:not(.spread):not(.isfav) .fseal .fword { display: none; }

/* The caption action stays as the seal's readable twin: a star on its own is
   a rebus, and two affordances for one decision is cheap. */
.card .caption .acts .fav { white-space: nowrap; }
.card.isfav .caption .acts .fav { color: var(--amber-deep); font-weight: 500; }
.card.isfav { box-shadow: inset 0 3px 0 0 var(--amber); }
/* ⚠️ THE FAVOURITE KEEPS THE SELECTION RING RATHER THAN REPLACING IT. This
   swapped the wash ring for a honey one, so selecting the favourite looked
   nothing like selecting any other card — which is exactly what Darrell hit:
   "when I click on the second card, it looks like it doesn't have the same
   outline." Two different questions, two marks: the stripe says which one gets
   quoted, the ring says which one you are working on, and a card is allowed to
   be both. */
.card.isfav.open { box-shadow: inset 0 3px 0 0 var(--amber), 0 0 0 1px var(--wash); }

/* Asked for and not yet given: the send was refused because nothing is
   marked, so the thing to click says so rather than a toast doing it alone. */
.card.askfav .fseal { opacity: 1; color: var(--amber-deep); }
/* ⚠️ A GLOW OUT OF THE STAR, NOT A NUDGE OF THE WHOLE DISC, and it does not
   stop. Darrell, 2026-09-01: "we should give the favorite icon on the design
   cards a pulsing glow radiating from the center of the star growing to fill
   the circle." This was `scale(1.18)` twice over 1.8 seconds.

   Two things were wrong with that, and the second is the one that mattered.
   A scale is a shove rather than a light, so on four cards at once it read as
   the row twitching. And it ran exactly twice: the instructional window it
   accompanied stayed up until she answered, so the mark that pointed AT the
   window outlived it by nothing and the ask went quiet while still being
   asked. Now that the window is gone from the spine-button path — see
   askFavorite() — the glow is the whole ask, so it runs until she answers and
   clearFavAsk() takes the class off.

   ⚠️ overflow: hidden IS WHAT MAKES IT "FILL THE CIRCLE". The glow is a
   radial gradient scaled from the centre out; unclipped it would grow past the
   22px disc as a soft square and read as a smudge under the star. The pip is
   already border-radius 50%, so clipping to it is the circle.

   ⚠️ THE STAR IS LIFTED ABOVE IT. ::after paints after the svg in tree order,
   so without a stacking order the glow would wash over the mark it is drawing
   attention to. */
.card.askfav .fseal .pip { position: relative; overflow: hidden; }
.card.askfav .fseal .pip svg { position: relative; z-index: 1; }
.card.askfav .fseal .pip::after {
  content: "";
  position: absolute; inset: 0; z-index: 0; border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--amber-deep) 62%, transparent) 0%,
    color-mix(in srgb, var(--amber-deep) 28%, transparent) 45%,
    transparent 72%);
  animation: askfavglow 1.9s var(--ease) infinite;
}
@keyframes askfavglow {
  0%   { transform: scale(.12); opacity: 0; }
  28%  { opacity: 1; }
  100% { transform: scale(1); opacity: 0; }
}
/* Stilled, not removed: the ask is still on, so the mark still has to read as
   marked. It rests at the state the animation spends most of its time near. */
@media (prefers-reduced-motion: reduce) {
  .card.askfav .fseal .pip::after { animation: none; transform: scale(.75); opacity: .8; }
}
/* A `.mathbar .send` rule used to sit here, styling a control the wish tray had
   already taken over. It went on 2026-08-20; the bar itself went on 2026-08-31
   with the duplicate figures. The send action lives on the dock. */

/* ---------- the notice ----------
   The toast, given the house voice: paper, hairline, an amber seed, and the
   unhurried exit the tray taught — prompt in, easy out. */
.notice {
  /* Sat clear of the maths bar, which is gone as of 2026-08-31 — the offset
     stays because the toast still wants air under it, not because a strip is
     down there to avoid. */
  position: fixed; left: 50%; bottom: 4.5rem; transform: translateX(-50%); z-index: 300;
  display: flex; align-items: center; gap: .5rem;
  background: var(--paper); color: var(--ink); border: 1px solid var(--rule-line);
  border-radius: var(--radius); padding: .5rem .85rem;
  font-family: var(--text); font-size: .98rem;
  box-shadow: 0 10px 24px -10px rgba(29,29,27,.35);
  opacity: 0; translate: 0 8px; transition: opacity .2s, translate .3s var(--ease);
  pointer-events: none;
}
.notice.show { opacity: 1; translate: 0 0; }
.notice[hidden] { display: none; }
.notice i { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--amber); }

/* ---------- the notice, when it is a refusal ----------
 * ⚠️ A VARIANT, NOT A NEW `.notice`. Darrell, 2026-09-07: the send gate "matches
 * exactly with the rest of the webpage, it's not very eye-catching" — he wants
 * the rail's voice on it. The temptation is to restyle `.notice` itself, and it
 * would be wrong: ONE element serves every toast on this page and most of them
 * are ordinary confirmations — "Charger Glass Gold → Table 1", "Back in its
 * place." Eighteen call sites in design.js. A velvet card shouting on every
 * successful drop is a page that shouts constantly, and a signal that fires on
 * everything signals nothing. Only askGuests() asks for this class.
 *
 * ⚠️ THE PANEL'S TOKENS, NOT APPROXIMATIONS OF THEM. --velvet ground, a 2px
 * --gilt border and 3px radius are `.cpanel`'s own three declarations
 * (panel.css:111-113), copied deliberately so the refusal reads as the rail
 * leaning out to answer rather than as a second dark thing with its own taste.
 * If the panel's ground ever moves, this has to move with it — it is the same
 * decision, and the whole point is that she sees one surface, not two.
 *
 * ⚠️ --champagne FOR THE SENTENCE, NOT --emerald-lit. The rail sets body copy
 * in --emerald-lit at 5.86:1, which is right for a note sitting quietly under a
 * field. This is a refusal that stopped her send, so it takes the rail's
 * loudest type role, the one its buttons use. Contrast, in the ledger's format
 * (panel.css:786):
 *
 *     refusal text  --champagne on --velvet   12.78:1   ✓
 *     the seed      --gilt      on --velvet    6.11:1   ✓  (a dot, not type)
 *
 * ⚠️ NO PULSE, AND THAT IS THE CHOICE THE RETIRED SAVE REMINDER PAID FOR: "a
 * crisp edge that blinks reads as an alarm beside a canvas somebody is trying
 * to design on" — the finding recorded under the save reminder above. A velvet
 * card on a paper page is already the highest-contrast thing on screen. The
 * glow is the panel's own focus idiom (panel.css:260) held steady, not flashed.
 * The family is unchanged because it was never wrong: `.notice` was already
 * var(--text), which is what the rail's headings are set in. */
.notice.urgent {
  background: var(--velvet);
  border: 2px solid var(--gilt);
  border-radius: 3px;
  color: var(--champagne);
  padding: .6rem 1rem;
  box-shadow: 0 10px 24px -10px rgba(10, 9, 8, .45),
              0 0 64px -13px var(--champagne);
}
.notice.urgent i { background: var(--gilt); }

/* ---------- the ghost that follows the pointer ---------- */
.ghost { border-radius: var(--radius-sm); rotate: -2deg; }
.ghost.over { rotate: 0deg; }

/* ---------- motion is a courtesy, never a toll ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important;
    animation-iteration-count: 1 !important; }
}

/* ---------- fitting the site's chrome ----------
   The app takes the viewport below the header; measureChrome() in design.js
   sets the height the way the old canvas did, from the app's own offsetTop.
   Inside, the workspace grid owns the room. */
.canvasapp { display: flex; flex-direction: column; min-height: 420px; overflow: hidden; }
.workspace { flex: 1; min-height: 0; }


/* ---------- send your designs, and the date field ----------
 *
 * ⚠️ MOVED OUT ON 2026-09-02, TO site/assets/css/send.css. Roughly 220 lines:
 * the modal, its veil, the two-step form, the checkbox, the venue group and
 * the whole calendar.
 *
 * They were here and /rentals could not read them. Both rentals pages open
 * this dialog — browse.js imports openSendForm the same way design.js does —
 * but only this page loads design.css, so on /rentals the form arrived with no
 * styling and, having no `position: fixed`, rendered at the foot of the
 * document with the browser scrolled to it. It had been that way since the
 * control panel put a Send button on the browse page.
 *
 * ⚠️ DO NOT MOVE THEM BACK. The same mistake as the event dock, which left for
 * panel.css on 2026-08-31 for the same reason. design.css is the canvas's
 * stylesheet; anything the browse page can reach must not live here. */



/* ---------- the event dock ----------
 *
 * ⚠️ GONE FROM THIS FILE ON 2026-08-31. Roughly 400 lines of `.edock*` lived
 * here: the rail, the panel, the figures she sets, saving, the first-visit
 * callout and the swap. All of it is the control panel now, in its own
 * stylesheet — site/assets/css/panel.css — which /rentals loads too. One
 * component, both rentals pages, and that was the point of moving it.
 *
 * ⚠️ WHAT IS STILL BELOW, AND WHY IT DID NOT GO WITH IT. The favourite
 * prompt and the save reminder are `.edock*`-named and they are NOT the
 * dock: they are callouts that point AT it, they render on the canvas
 * ground rather than on the panel, and they keep their paper treatment for
 * that reason. They are named for a component that no longer exists, which
 * is a stale name and is on the list to fix — separately, because each of
 * them carries fault anchors pinned to its literal source and renaming them
 * in the same commit as this move would put both changes in one blast
 * radius.
 *
 * ⚠️ AND ITS GROUND MOVED EVEN THOUGH IT DID NOT. `.edockfav` is
 * positioned absolutely against the panel stage, which is
 * now `.cpanel__stage` in panel.css. It is a full-viewport fixed overlay,
 * exactly as `.edock` was, so the geometry below still resolves. That is not
 * a coincidence — askFavorite() measures the prompt against the stage rect,
 * and the stage was given a real box to keep this working.
 */

/* ---------- asked for a favorite ----------
 * ⚠️ IT SITS BESIDE SEND, BECAUSE SEND IS WHAT REFUSED. Darrell, 2026-08-18:
 * the old version was a toast at the foot of the page plus a 2.4s pulse on the
 * cards, and he missed it completely while KNOWING it was supposed to fire. A
 * refusal has to appear where the click happened, not somewhere else on the
 * page a moment later.
 */
/* ⚠️ RE-DECLARED HERE ON 2026-08-31, NOT DUPLICATED BY ACCIDENT. This keyframe
   was defined inside the event dock's block, and both prompts below animate
   with it. When the dock moved to panel.css the definition went with it and
   these two silently stopped animating — a dead `animation:` name does not
   error, it just does nothing, which is the failure mode that survives a
   review. panel.css has its own copy under its own name for the callout it
   kept; this one belongs to the two prompts that stayed. */
@keyframes edockcallin {
  from { opacity: 0; transform: translate(10px, -50%); }
  to   { opacity: 1; transform: translate(0, -50%); }
}
.edockfav {
  position: absolute; right: 3.3rem; z-index: 5;
  width: max-content; max-width: min(17.5rem, calc(100vw - 7rem));
  display: flex; align-items: center; gap: .7rem;
  padding: .6rem .85rem;
  background: var(--paper); border: 1px solid var(--amber); border-radius: var(--radius);
  box-shadow: 0 14px 32px rgba(15, 15, 14, .16);
  animation: edockcallin .3s var(--ease-out);
}
.edockfav::after {
  content: ''; position: absolute; right: -6px; top: 50%; margin-top: -5px;
  width: 10px; height: 10px; background: var(--paper);
  border-right: 1px solid var(--amber); border-top: 1px solid var(--amber);
  transform: rotate(45deg);
}
/* ⚠️ A CIRCLE, BECAUSE THE CARDS CARRY A CIRCLE. This was a rounded square
   for four days while every table card wore `.card .fseal .pip` — a 22px
   circle with the same star in it — so the prompt was showing her a picture
   of something that is not on screen anywhere. Darrell, 2026-08-20: "this
   should be a true match to what users see on the table cards." It is drawn
   at twice the card's size and in the state the cards are IN at that moment —
   `.card.askfav .fseal .pip`, amber-deep on the pale wash — rather than the
   claimed state. Nothing is claimed when this fires; that is why it fired. A
   filled amber disc would pop harder and would be a picture of the AFTER,
   handed to somebody being asked to find the BEFORE. Proportions are the
   card's: the star is 55% of the circle there and here. */
.edockfavpip {
  flex: none; display: grid; place-items: center;
  width: 44px; height: 44px;
  border: 1.5px solid currentColor; border-radius: 50%;
  background: var(--paper);
  color: var(--amber-deep);
}
.edockfavpip svg { width: 24px; height: 24px; fill: currentColor; }
/* ⚠️ ONE PRESS, THEN THE SEED'S OWN BREATH. The press demonstrates the
   gesture; the halo is the reminder's pulse, borrowed so the two prompts on
   this rail speak one language — Darrell, 2026-08-20. They can share the
   element because they no longer share a property: the press is transform
   only, the halo is box-shadow only. When both wrote box-shadow, whichever
   was declared second silently erased the other's ring. */
.edockfav .edockfavpip {
  animation: edockfavtap .8s var(--ease-out) .25s 1 both,
             edockseed 3.2s ease-in-out infinite;
}
@keyframes edockfavtap {
  0%   { transform: none; }
  45%  { transform: scale(.86); }
  100% { transform: none; }
}
.edockfav p { margin: 0; font-family: var(--text); font-size: .92rem; line-height: 1.35; color: var(--ink); }
/* ---------- the save reminder ----------
 * ⚠️ THE RULES THAT WERE HERE ARE GONE, 2026-09-02. `.edocknudge`, its beak,
 * its seed, its two-line copy and its Later link — roughly sixty lines styling
 * a card that no longer gets built. Darrell asked for the card to go and for a
 * glow on the rail's Send section in its place; design.js carries the full
 * note under "the save reminder, retired", and panel.css carries the glow.
 *
 * ⚠️ TWO THINGS THAT LOOK LIKE THEY SHOULD HAVE GONE WITH IT AND DID NOT.
 * `@keyframes edockseed` is kept below because .edockfav's pip animates on it
 * — the favourite prompt is a different card and it is still live. And
 * `@keyframes edockcallin` is shared with .edockfav for the same reason.
 * Deleting either by association is the obvious mistake here.
 *
 * ⚠️ WHAT IS WORTH CARRYING FORWARD, since the argument outlived the markup:
 * the card was M6-04, "the seed", picked from six on 2026-08-20 — a quiet
 * border with an 11px honey seed on a 3.2s halo that is all blur and almost no
 * spread, so it glows without ever resolving into an edge. It replaced a 2.8s
 * ring that pulsed to 7px of hard shadow, because a crisp edge that blinks
 * reads as an alarm beside a canvas somebody is trying to design on. If a
 * prompt is ever added to this page again, that is the finding to start from.
 */
}
/* ⚠️ THE RESTING HALO IS ON THE ELEMENT, NOT ONLY IN THE KEYFRAMES. Under
   reduced motion the animation is dropped and this is all that is left — the
   seed has to hold its resting frame there, or the one lit thing on a
   deliberately quiet card goes out for the people most likely to need it. */
@keyframes edockseed {
  0%, 100% { box-shadow: 0 0 10px 3px color-mix(in srgb, var(--amber) 30%, transparent); }
  50%      { box-shadow: 0 0 32px 12px color-mix(in srgb, var(--amber) 60%, transparent); }
}

@media (prefers-reduced-motion: reduce) {
  /* ⚠️ The ring STAYS. Removing the animation must not remove the signal, or
     the one thing that makes the tab findable disappears for the people most
     likely to be using a keyboard to find it. */
  /* ⚠️ The rail's and the panel's reduced-motion rules moved to panel.css with
     the component on 2026-08-31. They are not missing; they are not this
     file's any more. */
  /* The square keeps its ring; only the press is dropped. */
  .edockfav .edockfavpip { animation: none;
    box-shadow: 0 0 10px 3px color-mix(in srgb, var(--amber) 30%, transparent); }
  /* The word holds the lit end of its cycle rather than the dark one — under
     reduced motion the pulse is the only thing saying which section, so it
     stops on the frame that says it. */
}

/* Narrow screens: the panel takes the width it needs and the canvas is behind
   it rather than beside it. Nothing else changes, because nothing else has to. */
@media (max-width: 40rem) {
  /* ⚠️ `.cpanel__call` IS NOT IN THIS LIST, and as of 2026-09-07 it is not in
     the stylesheet either — the callout was deleted; see panel.css. The note
     that stood here is kept for the rule it was really about, which outlives
     it: panel.css loads AFTER this file and sets the same properties at the
     same specificity, so a narrow-screen override written here for anything
     `.cpanel*` simply loses, and reads as a rule that stopped working. Put it
     beside the component, in panel.css. */
  .edockfav { right: 2.2rem; max-width: min(15rem, calc(100vw - 5rem)); }
}


/* ⚠️ SQUARE, WHOLE HOUSE. The note badge was a 999px pill, which is the one
   shape the system's governance list names outright. The pip beside it stays
   a circle: a dot is not a rounded rectangle and the rule was never aimed at
   one. Scoped to body.em like every other emerald rule in this file's
   neighbours, because rentals.css is shared. */
body.em .note { border-radius: 0; }

/* ---------- Your list, the room that is hers ----------
   ⚠️ A STAR WHERE THE OTHERS CARRY A NUMBER, and it needs two more px than a
   number does. The `i` column is tuned for two tabular digits at the 11px
   floor with .14em of tracking between them; a single glyph in that setting
   sits small and the tracking pushes it off its own centre. Tracking off,
   size up, same column. */
.rooms > li[data-s="yourlist"] > button i {
  font-size: 13px; letter-spacing: 0; line-height: 1;
}
/* The one shelf that can legitimately be empty — every other room is
   guaranteed stock by the catalogue, so no other shelf has this state. */
.shelfempty {
  margin: 0; padding: .9rem 1rem; max-width: 46ch;
  font-family: var(--text); font-size: .95rem; line-height: 1.5;
  color: var(--muted);
}
