/* /rentals and /rentals/design.
 *
 * Both pages, one stylesheet, because the wish list pill is genuinely shared
 * and the two were about to become copies of each other.
 *
 * Everything here builds on tokens.css and base.css. No colour, type or
 * spacing value is invented — if something needed a value that is not a token,
 * that is a sign the token set is wrong, not a licence to hardcode.
 */

/* ============================================================================
   THE HOUSE TUNING PACKET, SCOPED TO THE TWO PAGES IT WAS BUILT FOR
   dev_sandbox/mockups/house-lab/HANDOFF.css, 2026-08-29. Approved by Darrell.

   ⚠️ THIS BLOCK IS WHY THESE THREE VALUES ARE NOT IN emerald.css.
   The packet retunes three SHARED tokens, and moving them at the house level
   is what it says to do — but the packet was designed and previewed against
   exactly two surfaces, the inventory wall and the design canvas. Its own §4
   records the rest: "home.css, pages.css, base.css and the shop read the same
   three tokens changed in §1 and were never previewed against them."

   They were global for one deploy, on 2026-08-29, and it showed immediately.
   home.css reads the three 16 times and base.css 9 more; base.css is the
   shared shell, so every hairline on the home page became a full emerald
   stroke, the "Why we exist" trio copy (`color: var(--wash)`) went from pale
   mist to mid green, and every button in the nav and footer desaturated —
   across four pages nobody had looked at. Darrell, 2026-08-30: those were not
   changes he asked for.

   ⚠️ SCOPING IS NOT A COLOUR MOVE, and it does not reopen "no colour moves
   beyond the three tokens in the packet". The values are the packet's, exactly
   and unchanged. What changed is WHERE they apply: the two pages the packet
   governs, which is where it was previewed. Everything else the packet does —
   chip sizing, the rail, selection, the glow, the two zooms, the halved
   motion — was already page-scoped by living in this file and design.css.
   The tokens were the only part that leaked, because they were declared
   globally.

   ⚠️ rentals.css IS LOADED BY BOTH PAGES AND emerald.css LOADS BEFORE IT ON
   EACH — /rentals reads tokens, base, emerald, pages, rentals; /rentals/design
   reads tokens, base, emerald, rentals, design. That is what makes ONE block
   here enough, and it is also what a page added later has to honour: a rentals
   page that does not load this file gets the house values, not the packet's.

   ⚠️ DO NOT "TIDY" THESE BACK INTO emerald.css. It will read as removing a
   duplicate. It re-breaks four pages.
   ============================================================================ */
body.em {
  --rule-line: #0E5F41;  /* house #E2DDD5. Full emerald stroke, 7.69:1 on paper */
  --wash:      #4C9A74;  /* house #E7EEE9. Row hover, chip fill, selection ring */
  --amber:     #45705C;  /* house #0E5F41. 5.64:1 on paper, still clears AA     */
}

/* ⚠️ AND --muted MUST NOT BE PAINTED ON --wash ANYWHERE IN THIS FILE OR IN
   design.css. On the house wash it is 5.08:1; on the wash above it is 1.76:1.
   emerald.css carries the same warning beside the token. */

/* ============================================================
   WISH LIST PILL + TRAY — shared by both pages

     .dockwrap.fixed    floats bottom-right          (browse)
     .dockwrap.inline   sits in the canvas hud bar

   The canvas variant exists because that page deliberately has nothing
   floating over the board: a floating button had pieces landing underneath it,
   which is why its controls were consolidated into a bar. The pill is styled
   identically either way; only its anchoring differs.
   ============================================================ */

.dockwrap { display: flex; flex-direction: column; align-items: stretch; gap: 0.5rem; }

.dockwrap.fixed { position: fixed; right: 1.5rem; bottom: 1.5rem; z-index: 150; transform: translateY(140%); transition: transform 0.4s var(--ease-out); }
.dockwrap.fixed.on { transform: none; }

/* In the bar the pill loses the shadow and tightens up so it does not change
   the bar's height. The tray still opens upward, absolutely positioned.

   ⚠️ `relative` IS LOAD-BEARING, not tidying. The tray below is absolute with
   bottom: calc(100% + …), which means "one containing-block height above that
   block's bottom edge" — so the containing block has to BE the pill. This was
   `static`, which left every ancestor up to <body> unpositioned, and the tray
   opened a full page-height above the viewport: hidden=false, display:flex,
   correct classes, 368px tall, painted at top:-377. Nothing in the DOM was
   wrong, which is why it read as "the button does nothing".
   The fixed variant never hit this because `fixed` makes its own block. */
.dockwrap.inline { position: relative; }
.dockwrap.inline .dock { padding: 0.42rem 0.8rem; box-shadow: none; border-radius: var(--radius-sm); font-size: var(--label-sm); gap: 0.6rem; }
/* right: 0 aligns the tray's edge with the pill's, which is what the open and
   close animations both claim is happening — it rolls out of the pill and back
   into it, so it must not be inset from it. */
.dockwrap.inline .tray { position: absolute; right: 0; bottom: calc(100% + 0.55rem); z-index: 140; }
.dockwrap.inline:not(.on) { opacity: 0.5; pointer-events: none; }

.dock { background: var(--ink); color: var(--ground); border-radius: var(--radius); padding: 0.9rem 1.25rem; display: flex; align-items: center; gap: 1rem; font-family: var(--sans); font-size: var(--label-md); letter-spacing: 0.09em; text-transform: uppercase; box-shadow: 0 10px 30px rgba(15, 15, 14, 0.22); }
.dock b { color: var(--honey); font-variant-numeric: tabular-nums; }
.dock-open { background: none; border: 0; color: inherit; font: inherit; letter-spacing: inherit; text-transform: inherit; cursor: pointer; display: flex; align-items: center; gap: 0.55rem; padding: 0; }
.dock-open svg { transition: transform 0.3s var(--ease-out); }
.dock-open[aria-expanded="true"] svg { transform: rotate(180deg); }
.dockwrap.trayopen .dock-submit { display: none; }
.dock-submit, .tray-submit { background: var(--honey); color: var(--ink); border: 0; border-radius: var(--radius-sm); padding: 0.5rem 0.9rem; font-family: var(--sans); font-size: var(--label-sm); letter-spacing: 0.1em; text-transform: uppercase; cursor: pointer; }
.dock-submit:hover, .tray-submit:hover { background: var(--amber); }

/* ⚠️ [hidden] is display:none in the UA sheet, and ANY author display rule
   beats it. .tray{display:flex} once left the tray painted at all times —
   invisible to a check that read el.hidden, because that property was
   perfectly correct and only the pixels disagreed. Never drop this. */
.tray[hidden] { display: none; }
.undo[hidden] { display: none; }

/* ⚠️ IT ROLLS, IT DOES NOT SHRINK.
   The tray used to scale toward its bottom-right corner on close, so it
   appeared to fall away into the corner of the screen rather than back into
   the pill it came out of. clip-path takes the top edge down to the bottom
   with the box untouched: full width the whole way, nothing scaled, nothing
   translated — a blind rolling up, and the last thing visible is the edge
   nearest the pill. */
.tray { width: min(23rem, calc(100vw - 3rem)); background: var(--paper); border: 1px solid var(--rule-line); border-radius: var(--radius); box-shadow: 0 18px 44px rgba(15, 15, 14, 0.20); display: flex; flex-direction: column; overflow: hidden; animation: trayin 0.30s var(--ease-out); }
@keyframes trayin { from { clip-path: inset(100% 0 0 0); } to { clip-path: inset(0 0 0 0); } }

/* Settling back into the pill rather than blinking out. Slower than the open
   (0.44s against 0.26s) because arriving wants to feel prompt and leaving
   wants to feel unhurried — and because the thing it collapses into is the
   thing you are about to look at. */
.tray.closing { animation: trayout 0.34s cubic-bezier(0.32, 0, 0.24, 1) forwards; pointer-events: none; }
/* No opacity either: fading while rolling reads as two things happening. */
@keyframes trayout { from { clip-path: inset(0 0 0 0); } to { clip-path: inset(100% 0 0 0); } }
@media (prefers-reduced-motion: reduce) { .tray, .tray.closing { animation: none; } }

.tray-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.9rem 1rem 0.7rem; border-bottom: 1px solid var(--rule-line); }
.tray-head h2 { font-family: var(--sans); font-size: var(--label-sm); font-weight: 400; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }

/* Collapse, not dismiss. An x reads as "discard this list", which is exactly
   the wrong promise on a panel whose job is to hold what you picked. A down
   arrow says where the panel is going: back into the pill, which is also the
   direction it travels. */
.tray-x { width: 28px; height: 28px; flex: none; display: grid; place-items: center; padding: 0; border: 0; border-radius: 50%; background: var(--ink); color: var(--honey); cursor: pointer; transition: background 0.18s, color 0.18s; }
.tray-x:hover, .tray-x:focus-visible { background: var(--honey); color: var(--ink); }
.tray-x svg { display: block; }

.tray-list { list-style: none; margin: 0; padding: 0.35rem; overflow-y: auto; max-height: min(42vh, 21rem); }
.tray-list li { display: flex; align-items: center; gap: 0.7rem; padding: 0.4rem 0.45rem; border-radius: var(--radius-sm); }
.tray-list li + li { border-top: 1px solid var(--rule-line); }
.tray-thumb { width: 46px; height: 46px; flex: none; border-radius: var(--radius-sm); background: var(--ground); display: grid; place-items: center; overflow: hidden; }
.tray-thumb img { width: 100%; height: 100%; object-fit: contain; }
.tray-thumb span { font-family: var(--sans); font-size: 0.7rem; letter-spacing: 0.06em; color: var(--muted); }
/* A linen shade in the tray is the shade itself. The inset rule is what keeps
   Chalk White from reading as an empty square. */
.tray-thumb.tray-shade { background: var(--cloth); box-shadow: inset 0 0 0 1px rgba(29, 29, 27, 0.12); }
.tray-name { flex: 1; min-width: 0; }
/* ⚠️ 500, NOT 400, AND 400 WAS AGAINST A HOUSE RULE — the same defect as
   .cpanel__listname b, one file over, and the same cause: the display face at
   the one weight the system's "What never ships" list names outright. Darrell,
   2026-09-01, on the inventory rail: "the current font is too hard to read."
   These are the product names in the wish tray, which is the list he was
   reading. system.mjs §7 could not see either, because it reads h1,h2,h3. */
.tray-name b { display: block; font-family: var(--serif); font-weight: 500; font-size: 0.98rem; line-height: 1.2; }
.tray-name span { font-family: var(--sans); font-size: var(--label-xs); letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
/* ⚠️ LEFT CIRCULAR ON PURPOSE, along with .tray-x and .chip .kill. A 26px
   round icon button is a different object from a rounded rectangle, and the
   system's square-corner rule is about panels, cards and controls that carry
   a label. Squaring these three would produce three tiny boxes with a glyph
   in them, which is worse and is not what the rule is asking for. */
.tray-rm { background: none; border: 1px solid var(--rule-line); border-radius: 999px; width: 26px; height: 26px; flex: none; cursor: pointer; color: var(--muted); font-size: 0.95rem; line-height: 1; }
.tray-rm:hover, .tray-rm:focus-visible { border-color: var(--amber); color: var(--amber-deep); }
.tray-empty { padding: 1.1rem 1rem; color: var(--muted); font-style: italic; }
.tray-foot { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 0.7rem 0.8rem; border-top: 1px solid var(--rule-line); background: var(--wash); }
.tray-full { background: none; border: 0; border-bottom: 1px solid var(--amber); padding: 0 0 1px; cursor: pointer; font-family: var(--sans); font-size: var(--label-sm); letter-spacing: 0.1em; text-transform: uppercase; color: var(--amber-deep); }

/* Undo sits opposite the pill so it never covers the control that produced it. */
.undo { position: fixed; left: 1.5rem; bottom: 1.5rem; z-index: 160; background: var(--ink); color: var(--ground); border-radius: var(--radius); padding: 0.7rem 0.9rem 0.7rem 1.1rem; display: flex; align-items: center; gap: 1rem; font-family: var(--sans); font-size: var(--label-sm); letter-spacing: 0.06em; box-shadow: 0 10px 30px rgba(15, 15, 14, 0.22); }
.undo button { background: none; border: 0; border-bottom: 1px solid var(--honey); color: var(--honey); font: inherit; letter-spacing: 0.1em; text-transform: uppercase; padding: 0 0 1px; cursor: pointer; }

/* ⚠️ Scroll anchoring off for this page, and it is load-bearing.
 *
 * The room dock deliberately changes its own height by ~456px as you scroll
 * past it. Scroll anchoring exists to hide exactly that kind of shift: the
 * browser sees content resize and moves scrollY to keep the view steady — which
 * moves the page back across the threshold that caused the collapse, which
 * re-expands the dock, which moves scrollY again. Measured: 12 state flips and
 * 6 backward jumps over 14 wheel ticks, with the page travelling 270px of the
 * 1680 asked for. The wheel and the browser take turns and nothing moves.
 *
 * It has to be on the SCROLLER (the document), not on the dock. Setting it on
 * the dock does nothing, because the anchor node the browser picks is in the
 * content below the dock, not the dock itself.
 * ============================================================ */
/* The dock that needed this is gone. It was here because collapsing a sticky
   box at the top of the page made the browser compensate scrollY, which
   re-expanded it — the two took turns and the page stopped moving. Nothing on
   this page changes size above the viewport any more, so the guard goes with
   the thing it was guarding. */

/* ============================================================
   BROWSE  /rentals
   ============================================================ */

/* ⚠️ .browse-head IS GONE. This page wore a masthead of its own — an eyebrow
   and a headline in a bare wrap — while the other four wore the system's.
   It now carries .page-head.page-head--solo out of pages.css, so the type
   scale, the rule and the standfirst measure are one definition for the whole
   site instead of two that agreed by coincidence. Nothing replaces these two
   rules here; if the masthead needs tuning, tune it in pages.css. */

/* ⚠️ THE ONE RULE THE SHARED MASTHEAD CANNOT CARRY. .page-head sets padding
   top only, because on About, FAQ and Contact the thing beneath it is a
   .sec whose own padding opens the gap. This page's grid is a .shell and has
   none, so the standfirst finished 0px above the room index — measured, not
   guessed. Reading --sec-pad rather than a number keeps this in step with the
   section rhythm if that token ever moves. */
body.em .page-head + .shell { margin-top: 1.5rem; }

/* ---------- the masthead on a page whose content is a tool ----------
   ⚠️ THIS PAGE CANNOT AFFORD THE FULL EDITORIAL MASTHEAD, and the reason is
   measurable rather than aesthetic. About, FAQ and Contact open on a masthead
   and then flow; a visitor scrolls and loses nothing. This page opens on a
   masthead and then presents a WORKING SURFACE — a room index whose hover
   paints a photograph into the rail beneath it. At 5rem of lead and a stacked
   standfirst the grid began at 581px, which put that photograph below a 900px
   fold: hover a room, and its picture appears somewhere you cannot see.
   checkRentalsImages reported it as `the room hero decodes on hover  0`.

   So the type scale and the composition are the system's, and only the
   VERTICAL BUDGET is this page's own. If the masthead grows again, that same
   row is what will say so. */
body.em .page-head--index { padding-top: 1.5rem; }
/* Two declarations doing two different jobs, and it is worth saying which is
   which, because the first reading of this got it backwards.

   ⚠️ max-width: none IS THE LOAD-BEARING ONE. The system's other media-less
   masthead, .page-head--solo, sets max-width: 20ch. That measure is right on
   FAQ and wrong here: at 20ch this headline breaks into two lines, the
   masthead grows 78px, and the room index's hover photograph drops to 140px
   of visible frame — measured. This page shipped that regression for an hour
   on 2026-08-29 and checkRentalsImages is what reported it. The declaration
   also has to survive somebody adding --solo back alongside --index, which is
   the likeliest way it returns; rentals.css loading after pages.css is what
   makes it win. There is a fault for exactly that.

   The font-size is a JUDGEMENT, not a fix. At the system's full 68px clamp
   with no measure the headline still sets in one line and the fold is still
   fine (210px of frame, also measured). 52px is here because a catalogue
   index wants a HEADER and 68px across 1050px is a hero — and a hero is what
   the pages with nothing else to show get. Everything else about the masthead
   is the system's: same face, same eyebrow, same rule, same standfirst, same
   sentence case, same single italic. */
body.em .page-head--index h1 { font-size: clamp(30px, 3.6vw, 52px); max-width: none; margin-top: 0.5rem; }
body.em .page-head--index .page-rule { margin: 0.875rem 0; }
body.em .page-head--index .stand { max-width: 56ch; }
/* ---------- the room index ----------
 *
 * ⚠️ THIS LIST MUST NOT CHANGE HEIGHT. Its predecessor expanded a room's groups
 * inline; travelling from a tall open room to a short one, the panel that
 * closed sat ABOVE the row being aimed at, so the row moved out from under the
 * cursor — measured at 202px. The groups now live above the grid instead.
 *
 * Names are right-aligned to one hard edge facing the products, which also
 * empties the left margin — where the numerals go, because a marginal note
 * belongs in a margin. */
.roomindex { list-style: none; margin: 0; padding: 0; }
.roomindex li button {
  width: 100%; position: relative; background: none; border: 0; cursor: pointer;
  display: grid; grid-template-columns: 2.1rem 1fr 2.4ch; gap: 0.3rem;
  align-items: baseline; padding: 0.58rem 0;
  font-family: var(--serif); font-size: 1.24rem; color: var(--ink);
}
/* ⚠️ The count track is a FIXED width with tabular figures. Let it size to its
   content and a two-digit room sits 1ch further left than a one-digit one, so
   the names go ragged — the exact thing right alignment is here to fix. */
.roomindex li button span { justify-self: end; text-align: right; }
.roomindex li button em,
.roomindex li button i { font-style: normal; font-family: var(--sans); font-variant-numeric: tabular-nums; }
/* ⚠️ 0.6875rem is 11px, which is this brand's stated type floor. Both of these
   were set below it (9.4px and 10px). Small light type does not only read
   badly — it antialiases so heavily that a contrast sample of the backdrop
   comes back 0.4 low, so the measurement stops being trustworthy at the same
   size the reading does. */
.roomindex li button i { justify-self: start; font-size: 0.6875rem; letter-spacing: 0.14em; color: var(--rule-line); transition: color 0.22s ease; }
.roomindex li button em { justify-self: end; font-size: 0.6875rem; letter-spacing: 0.12em; color: var(--muted); }
/* A rule drawn by hand from the RIGHT, the edge the names hang off — a line
   drawing away from its own anchor reads backwards. */
.roomindex li button::after {
  /* ⚠️ Clear of the type, not merely below it. At 1.2px the rule sat inside the
     rounding of a contrast sample taken at 2x and dragged the count from 4.83
     to 4.18 — and a rule that close to a baseline reads as a strikethrough
     anyway. */
  content: ''; position: absolute; right: calc(2.4ch + 0.5rem); bottom: 0.1rem; height: 2px; width: 0;
  background: var(--honey); transition: width 0.34s var(--ease-out);
}
.roomindex li button:hover::after,
.roomindex li button:focus-visible::after,
.roomindex li button[aria-pressed="true"]::after { width: calc(100% - 2.1rem - 2.9ch - 0.5rem); }
.roomindex li button:hover i { color: var(--muted); }
/* ⚠️ Honey is 1.37:1 on this ground — measured. It is a flourish, never the
   only thing saying "this room is live": the name goes italic and the numeral
   turns amber. Do not remove either of those and leave the rule alone. */
.roomindex li button[aria-pressed="true"] span { font-style: italic; }
.roomindex li button[aria-pressed="true"] i { color: var(--amber); }
.roomindex li button[disabled] { opacity: 0.45; cursor: default; }
.roomindex .listrow { border-top: 1px solid var(--rule-line); margin-top: 0.4rem; padding-top: 0.2rem; }
.roomindex .listrow i { color: var(--amber); font-size: 0.7rem; }

/* ⚠️ The aspect-ratio is what holds the column steady, so the frame occupies
   the same space whether or not the room has a photograph. Toggling it away
   for the one room without one resized the rail and moved the page. */
.idxhero { margin: 1.2rem 0 0; position: relative; aspect-ratio: 4 / 5; overflow: hidden; border-radius: var(--radius-sm); }
/* A room whose photography has not been shot yet. Stroke and wash, the same
   language the unphotographed product tiles use, so it reads as pending
   rather than as broken. */
.idxhero.nophoto { background: var(--wash); border: 1px dashed var(--amber); }
.idxhero.nophoto img { display: none; }
.idxhero.nophoto figcaption { position: static; padding: 0.9rem; color: var(--ink); background: none; }
.idxhero.nophoto::after {
  content: 'Photography coming'; position: absolute; left: 0.9rem; bottom: 0.9rem;
  font-family: var(--sans); font-size: 0.6875rem; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--amber-deep);
}
.idxhero img { width: 100%; height: 100%; object-fit: cover; display: block; animation: idxfade 0.5s var(--ease-out); }
@keyframes idxfade { from { opacity: 0; } }
/* ⚠️ The credit is on the photograph, not in a list elsewhere. It has to clear
   the caption's gradient and hold 4.5:1 against the darkest part of it. */
.idxhero .credit {
  position: absolute; right: 0.8rem; top: 0.7rem; z-index: 2;
  font-family: var(--sans); font-size: 0.6875rem; letter-spacing: 0.08em;
  color: var(--paper); background: rgba(29, 29, 27, 0.72);
  padding: 0.2rem 0.45rem; border-radius: var(--radius-sm);
}
.idxhero .credit a { color: var(--honey); text-underline-offset: 2px; }
.idxhero.nophoto .credit { display: none; }
.idxhero figcaption {
  position: absolute; inset: auto 0 0 0; padding: 2.4rem 0.8rem 0.7rem; color: var(--paper);
  font-family: var(--text); font-size: 0.95rem; line-height: 1.35;
  background: linear-gradient(to top, rgba(29, 29, 27, 0.92), transparent);
}

/* ---------- the second level, with the products ---------- */
/* ⚠️ [hidden] loses to display:flex. Without this the row stayed painted as an
   empty bordered strip while its hidden attribute said otherwise — the element
   reported hidden and was on screen, which is how a check passes on something
   a visitor can see. */
.groups[hidden] { display: none; }
.groups {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 1.4rem;
  margin: 0.7rem 0 0; padding-bottom: 0.8rem; border-bottom: 1px solid var(--rule-line);
}
.groups .glab { font-family: var(--sans); font-size: 0.6875rem; letter-spacing: 0.2em; text-transform: uppercase; color: var(--muted); }
.groups button {
  position: relative; background: none; border: 0; cursor: pointer; padding: 0.1rem 0;
  font-family: var(--text); font-size: 1.06rem; color: var(--muted); transition: color 0.18s ease;
}
.groups button em { font-style: normal; font-family: var(--sans); font-size: 0.6875rem; letter-spacing: 0.08em; opacity: 0.6; margin-left: 0.35rem; font-variant-numeric: tabular-nums; }
.groups button::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -3px; height: 2px;
  background: var(--honey); transform: scaleX(0); transform-origin: left;
  transition: transform 0.26s var(--ease-out);
}
.groups button:hover { color: var(--ink); }
.groups button:hover::after,
.groups button:focus-visible::after,
.groups button[aria-pressed="true"]::after { transform: scaleX(1); }
.groups button[aria-pressed="true"] { color: var(--amber-deep); font-style: italic; }

.shell { display: grid; grid-template-columns: 15rem 1fr; gap: 2rem; align-items: start; padding-bottom: var(--sec-pad); }
/* ⚠️ The index's first row is aligned to the results heading beside it, not to
   the top of the column. With the search box gone the rail simply started
   higher than the title it belongs to, which read as two panels that had come
   apart. The offset is the heading's own leading, so it tracks the type. */
.rail { position: sticky; top: 6rem; display: grid; gap: 1.5rem; padding-top: 0.35rem; }
.rail h2 { font-family: var(--sans); font-size: var(--label-xs); font-weight: 400; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); margin-bottom: 0.5rem; }
.rail ul { list-style: none; margin: 0; padding: 0; }
.rail h2 { display: flex; justify-content: space-between; align-items: baseline; }
.rail .search { display: none; }
.rail h2 .clear { font: inherit; letter-spacing: 0.12em; text-transform: none; border: 0; background: none; padding: 0; cursor: pointer; color: var(--amber-deep); }
#cats li.listrow { margin-top: 0.45rem; padding-top: 0.45rem; border-top: 1px solid var(--rule-line); }
#cats li.listrow button b { font-weight: 400; }
/* ⚠️ THE RESTING EDGE IS EVERGREEN, NOT THE HAIRLINE. Darrell, 2026-09-02,
   the same note he left on /faq's field: "let's keep this dark green ... when
   you click in it, let's fill that bar dark green and the typing text will be
   the paper white." pages.css .faq-search is the deliberate second copy of
   this rule and moves with it. */
.search { width: 100%; font-family: var(--text); font-size: 1rem; padding: 0.6rem 0.8rem; border: 1px solid var(--evergreen); border-radius: var(--radius-sm); background: var(--paper); color: var(--ink);
  transition: background-color .22s var(--ease-out), color .22s var(--ease-out); }
.clear { background: none; border: 0; border-bottom: 1px solid var(--amber); padding: 0 0 1px; cursor: pointer; font-family: var(--sans); font-size: var(--label-sm); letter-spacing: 0.1em; text-transform: uppercase; color: var(--amber-deep); justify-self: start; }

/* ---------- INV-12 · keep this list ---------- */
.keeplist { border: 1px solid var(--rule-line); border-radius: var(--radius);
  padding: 1.1rem 1.25rem; margin-bottom: 1.25rem; }
.keeplist[hidden] { display: none; }
.keeplist-h { font-family: var(--serif); font-weight: 400; font-size: 1.15rem; margin: 0 0 .85rem; }
.keeplist-cols { display: grid; gap: 1.1rem; }
@media (min-width: 46rem) { .keeplist-cols { grid-template-columns: 1fr 1fr; gap: 1.75rem; } }
.keep-form label { display: block; font-family: var(--sans); font-size: var(--label-xs);
  letter-spacing: .12em; text-transform: uppercase; color: var(--muted); margin-bottom: .4rem; }
.keep-row { display: flex; gap: .5rem; }
.keep-row input { flex: 1; min-width: 0; font-family: var(--text); font-size: .95rem;
  padding: .5rem .7rem; border: 1px solid var(--rule-line);
  border-radius: var(--radius-sm); background: var(--paper); color: var(--ink); }
.keep-row input:focus-visible, .keep-btn:focus-visible {
  outline: 2px solid var(--amber); outline-offset: 2px; }
.keep-btn { font-family: var(--sans); font-size: var(--label-sm); letter-spacing: .1em;
  text-transform: uppercase; cursor: pointer; white-space: nowrap;
  background: none; border: 1px solid var(--amber); border-radius: var(--radius-sm);
  color: var(--amber-deep); padding: .5rem .9rem; }
.keep-btn:hover { background: var(--wash); }
.keep-btn[disabled] { opacity: .55; cursor: default; }
.keep-msg { font-family: var(--text); font-size: .88rem; margin: .5rem 0 0; color: var(--ink); }
.keep-msg[hidden] { display: none; }

/* ---------- INV-1 · the search field and its predictions ----------
 * ⚠️ THE FOCUS STATE IS THE HOUSE OUTLINE, NOT A BORDER SWAP. The reference
 * sheet rested the field on --rule-line and focused it on --emerald. After the
 * 2026-08-29 packet those are the SAME VALUE, #0E5F41, and the contrast between
 * them is 1.00:1 — the field would have had no visible focus state at all.
 * outline-based focus is unaffected by that collision and is what the rest of
 * the house already uses. */
.searchwrap { position: relative; margin-bottom: 1rem; }
/* ⚠️ THE FIELD FILLS. IT DOES NOT GLOW AND IT DOES NOT GO GOLD. Darrell,
   2026-09-02: "remove the glow on the search and remove the transition to the
   gold outline ... when you click in it, let's fill that bar dark green and
   the typing text will be the paper white." This replaces the gilt-border-
   plus-64px-halo state that landed on 2026-09-01; the history of THAT state
   explains why the outline is suppressed here rather than restyled —

     the field's own border is set in this file at (0,2,0), and
     `body.em :focus-visible` sets outline-color to --gilt at (0,2,1). The
     theme wins on colour, so a focused field wore its own edge AND a gold
     ring floating 2px off it. `outline: none` in the shorthand sets
     outline-STYLE, which the theme rule never touches, so the ring goes and
     the state is carried by the fill.

   ⚠️ THE FILL IS THE INDICATOR AND IT IS A BIGGER ONE THAN THE RING WAS.
   Evergreen against paper is 12.2:1, four times the 3:1 SC 1.4.11 asks of a
   non-text state change, and it moves the whole control rather than one
   hairline. Paper on evergreen is 12.2:1 for the text she types.

   ⚠️ THE PLACEHOLDER HAS TO MOVE WITH THE GROUND, or it stays muted taupe and
   lands near 1.4:1 on evergreen.

   ⚠️ THE PREDICTIONS LIST BELOW STAYS ON PAPER. It is a menu, not part of the
   field, and .qlist keeps its own ground — a filled field with a filled list
   under it reads as one dark slab and the boundary between what she typed and
   what we are offering disappears.

   ⚠️ forced-colors PUTS THE OUTLINE BACK. In High Contrast the OS repaints
   background and border from its own palette, so the fill stops being a state
   change at all and `outline: none` would leave nothing. */
.search:focus-visible {
  outline: none;
  background: var(--evergreen);
  border-color: var(--evergreen);
  color: var(--paper);
  caret-color: var(--paper);
}
.search:focus-visible::placeholder { color: color-mix(in srgb, var(--wash) 70%, var(--evergreen)); }
@media (forced-colors: active) {
  .search:focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
}

.qlist {
  position: absolute; z-index: 40; left: 0; right: 0; top: calc(100% + 4px);
  margin: 0; padding: .25rem 0; list-style: none; max-height: 19rem; overflow-y: auto;
  background: var(--paper); border: 1px solid var(--rule-line);
  border-radius: var(--radius-sm); box-shadow: 0 12px 28px -18px rgba(10, 9, 8, .45);
}
.qlist[hidden] { display: none; }
.qlist li {
  padding: .5rem .8rem; cursor: pointer; display: flex; gap: .6rem; align-items: baseline;
  font-family: var(--text); font-size: .95rem;
}
/* ⚠️ THE ACTIVE ROW IS NOT A :hover. Arrow keys move a virtual cursor the
   pointer knows nothing about, and aria-activedescendant is what a screen
   reader follows. Styling only :hover would leave keyboard users with an
   announced selection they cannot see. */
.qlist li[aria-selected="true"] { background: var(--wash); }
.qlist li .qroom {
  margin-left: auto; font-family: var(--sans); font-size: var(--label-xs);
  letter-spacing: .09em; text-transform: uppercase; color: var(--muted);
}
.qlist li mark { background: none; color: inherit; font-weight: 500; }

/* ---------- INV-3 / INV-4 · the empty result ----------
 * ⚠️ THE CONTAINER RESTS ON --rule-line, WHICH IS NOW A 7.69:1 EMERALD STROKE.
 * That makes this a heavy box around an apology, and it is built that way on
 * purpose: colours are locked (packet §1.5 C). What can be done WITHOUT
 * touching a colour is keeping the recovery controls on --amber, so the way
 * OUT of the empty state is still the loudest thing inside the box. */
.empty {
  border: 1px solid var(--rule-line); border-radius: var(--radius);
  padding: 1.5rem; margin-top: 1.25rem;
}
.empty[hidden] { display: none; }
.empty-say { font-family: var(--serif); font-size: 1.2rem; line-height: 1.4; margin: 0; }
.empty-acts { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 1rem; }
.empty-acts button {
  font-family: var(--sans); font-size: var(--label-sm); letter-spacing: .1em;
  text-transform: uppercase; cursor: pointer;
  background: none; border: 1px solid var(--amber); border-radius: var(--radius-sm);
  color: var(--amber-deep); padding: .45rem .9rem;
}
.empty-acts button:hover { background: var(--wash); }
.empty-acts button:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }
.empty-near { margin-top: 1.5rem; }
.empty-nearlab {
  font-family: var(--sans); font-size: var(--label-xs); letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted); margin: 0 0 .75rem;
}
.empty-near[hidden] { display: none; }

.resbar { display: flex; align-items: baseline; gap: 1rem; padding-bottom: 0.75rem; border-bottom: 1px solid var(--ink); }
.resbar h2 { font-family: var(--serif); font-weight: 400; font-size: 1.75rem; margin-right: auto; }
.resbar .count { font-family: var(--sans); font-size: var(--label-sm); letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); font-variant-numeric: tabular-nums; }
.viewtoggle { display: inline-flex; gap: 0.25rem; }
.pill { background: var(--paper); border: 1px solid var(--rule-line); border-radius: 999px; padding: 0.3rem 0.85rem; cursor: pointer; font-family: var(--sans); font-size: var(--label-sm); letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink); }
.pill[aria-pressed="true"] { background: var(--ink); color: var(--ground); border-color: var(--ink); }

/* ⚠️ BETWEEN 5 AND 7 COLUMNS, IN CSS, WITH NOTHING MEASURING THE WINDOW.
 *
 *   at7   = (100% - 6 gaps) / 7   the widest a track can be and still fit 7
 *   at5   = (100% - 4 gaps) / 5   the widest a track can be and still fit 5
 *   track = min( at5, max( at7, floor ) )
 *
 * Wide, at7 clears the floor and exactly 7 fit. Narrow, the floor wins and at5
 * caps it at exactly 5. Between, auto-fill lands inside the range. One
 * expression, no resize listener, nothing to fall out of sync.
 *
 * ⚠️ This is dead code unless .shell is allowed to be wider than 80rem — see
 * .wide below. Beside the index the products could never exceed ~748px, so the
 * grid measured 5 columns at 1440 and still 5 at 3000. */
.wall {
  /* 21px gap, packet §3. --floor is unchanged: 10.5rem already computes to
     the 168px the packet asks for. */
  --gap: 21px; --floor: 10.5rem;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(
    min(calc((100% - 4 * var(--gap)) / 5),
        max(calc((100% - 6 * var(--gap)) / 7), var(--floor))), 1fr));
  gap: var(--gap); padding-top: 1.25rem; transition: opacity 0.17s ease;
}
/* Only the gallery is let out this far; body copy keeps the reading measure. */
.wrap.wide { max-width: 108rem; }
@media (max-width: 60rem) {
  .wall { grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr)); }
}
.wall.swapping { opacity: 0; }
.wall.list { grid-template-columns: 1fr; }
/* Scrolling a card into view must clear the sticky dock.
 *
 * Without this, tabbing to a card or following a link to one parks it
 * underneath the compacted dock — visible enough to look focused, covered
 * enough that a click lands on the dock instead. The value is the compact
 * dock's height plus a little air. */
.it { scroll-margin-top: 7rem; display: flex; flex-direction: column; border: 1px solid var(--rule-line); border-radius: var(--radius); background: var(--paper); overflow: hidden; }
.wall.list .it { flex-direction: row; align-items: center; }
/* ⚠️ THE CARD IS SQUARE AND THE PHOTOGRAPH FITS INSIDE IT, not the other way
   round.
   aspect-ratio is not a hard constraint: an in-flow child taller than the
   ratio pushes the box open, and `max-height: 100%` cannot stop it because the
   percentage resolves against a height the ratio has not settled yet. A 560px
   champagne flute is roughly 1:3, so its card grew to three times the width of
   a plate's and tore a hole in the row.
   Taking the image out of flow is what makes the ratio hold — it can then be
   given the whole box and told to contain itself. */
.it figure { margin: 0; position: relative; aspect-ratio: 1; background: var(--paper); }
.wall.list .it figure { width: 6rem; aspect-ratio: 1; flex: none; }
.it picture { display: contents; }   /* a <picture> is inline and would become the box */
/* ⚠️ THE INSET IS THE WHOLE PRODUCT-VISIBILITY BUDGET ON THIS PAGE. At 22px
   the smallest piece in the catalogue renders 128x72px at 1440. The photograph
   is contained and never cropped, so every pixel added here is product the
   reader loses. Was 1rem/16px; 22px is the packet's figure. */
.it figure img {
  position: absolute; inset: 22px;
  width: calc(100% - 44px); height: calc(100% - 44px);
  object-fit: contain;
  /* ⚠️ --ease-out, NOT --ease. The packet writes `var(--ease)`, which is
     aliased in design.css:135 — a file /rentals does not load. It loads
     tokens, base, emerald, pages and this one. An unresolvable var() makes
     the whole declaration invalid at computed-value time, so the packet's
     line would have left the zoom SNAPPING with nothing to show for it.
     --ease-out is the token this alias points at and it lives in tokens.css,
     which every page loads. */
  transition: transform .12s var(--ease-out);
}

/* ---------- INV-10 · the wall, waiting ----------
 * ⚠️ ONE TIMELINE FOR THE WHOLE GROUP, NOT ONE PER TILE. Eighteen independent
 * shimmers read as eighteen things loading; one sweep crossing the group reads
 * as the group loading. The animation is on the container for that reason, and
 * the tiles themselves never animate.
 *
 * ⚠️ THIS SWEEP IS NOT COVERED BY "MOTION 50%". It is a 1.4s LOOP, not a
 * transition, and halving it stops it reading as loading and starts it reading
 * as a flicker. The packet's halving governs two named transition rules.
 *
 * ⚠️ THE TILES TAKE --rule-line LIKE EVERYTHING ELSE, which after the 2026-08-29
 * packet is a 7.69:1 emerald stroke — so this is twelve emerald-outlined
 * rectangles rather than the grey wireframe a placeholder usually is. Colours
 * are locked. The lever that is NOT a colour is the sweep, so it is kept slow
 * and low-contrast: the tiles should read as waiting, not flashing. */
.wall--loading { position: relative; }
.wall--loading::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(100deg,
    transparent 30%, color-mix(in srgb, var(--wash) 22%, transparent) 50%, transparent 70%);
  animation: sk-sweep 1.4s linear infinite;
}
@supports not (color: color-mix(in srgb, red 50%, transparent)) {
  .wall--loading::after {
    background: linear-gradient(100deg, transparent 30%, rgba(76, 154, 116, .22) 50%, transparent 70%);
  }
}
@keyframes sk-sweep { from { transform: translateX(-60%); } to { transform: translateX(60%); } }

.sk { display: flex; flex-direction: column; border: 1px solid var(--rule-line);
      border-radius: var(--radius); background: var(--paper); overflow: hidden; }
/* Matched to .it: a square figure, then the body's padding and two text lines.
   The figure's aspect-ratio is what stops the grid reflowing when the real
   cards arrive. */
.sk-fig { aspect-ratio: 1; }
.sk-line { height: .55rem; margin: 0 1rem .4rem; background: var(--rule-line); opacity: .18; border-radius: 2px; }
.sk-line:first-of-type { margin-top: .2rem; }
.sk-line--short { width: 45%; margin-bottom: .75rem; }

/* Never started, not merely stilled. A reader who asked for less motion gets
   the shape of the page without a loop running behind it. */
@media (prefers-reduced-motion: reduce) {
  .wall--loading::after { animation: none; display: none; }
}

/* ---- the wall card hover: ZOOM, packet §3 ----
   NEW behaviour: rentals.css had no hover on .it at all. `overflow: hidden` is
   already on .it, so the only support this needed was the transition above.
   ⚠️ THE SCALE STAYS INSIDE THE FRAME AT THIS INSET, AND THAT IS NOT LUCK.
   The image is absolutely positioned and scales from centre, so it crosses the
   figure only when the inset drops below roughly 3.3% of the card height —
   about 6px on a 180px card. If the inset is ever tightened, re-check this:
   the art would reach the caption before .it's overflow ever clipped it.
   ⚠️ IT LANDS ON 156 CARDS. A transform on an image is compositor-friendly and
   the cheapest of the five hovers considered; Lift and Glow would each repaint
   a large area on every pointer move across the wall. */
.it:hover figure img { transform: scale(1.07); }

/* ⚠️ A SHADE CARD IS THREE LAYERS AND ONLY ONE OF THEM WAS MOVING. Darrell,
   2026-09-03: "when you hover over a napkin card, the napkin image bumps, but
   the colored overlay does not."

   The rule above scales `figure img`, which on these 74 cards is `.nap img` —
   the photograph. The SHADE is `.nap i`, a masked fill, and the highlight is
   `.nap .sheen`; both are siblings of that image, both absolutely inset, and
   neither is an `img`. So the photograph grew 7% inside a mask that stayed
   put, which reads as the cloth sliding out from under its own colour.

   Scaling the BOX moves all three as one, which is what it is. The image's own
   transform has to be cancelled or it compounds with its parent's and the
   photograph zooms 14% against a 7% mask — the same misalignment, smaller and
   harder to name. `.cloth` gets the same treatment: it is a single element, so
   it never showed the bug, but a linen tile that did not react while the
   napkin beside it did would read as broken. */
.it.shade figure .napbox, .it.shade figure .cloth {
  transition: transform 0.45s var(--ease-out);
}
.it.shade:hover figure img { transform: none; }
.it.shade:hover figure .napbox, .it.shade:hover figure .cloth { transform: scale(1.07); }

/* The handoff requires every added motion to respect this. A reader who has
   asked for less motion gets the card and its photograph, and no zoom. */
@media (prefers-reduced-motion: reduce) {
  .it figure img { transition: none; }
  .it:hover figure img { transform: none; }
  /* The shade cards carry the same courtesy on the box that carries the zoom. */
  .it.shade figure .napbox, .it.shade figure .cloth { transition: none; }
  .it.shade:hover figure .napbox, .it.shade:hover figure .cloth { transform: none; }
}
.wall.list .it figure img { inset: 0.5rem; width: calc(100% - 1rem); height: calc(100% - 1rem); }
.it figure .sketch { position: absolute; inset: 1rem; place-content: center; }
/* A piece awaiting its photograph gets a line drawing of the KIND of thing it
   is. Initials were a placeholder for a placeholder — "CP" tells a reader
   nothing about a charger plate. Stroke, never fill, which is the one palette
   law this brand has. */
/* ⚠️ AN UNPHOTOGRAPHED PIECE GETS NO CARD TREATMENT AT ALL — no wash, no
   dashed edge. Darrell, 2026-08-18: both of them marked a real, rentable
   product as a broken card in a grid of good ones, and the customer cannot act
   on the distinction anyway. The line drawing inside says everything that
   needs saying, and it says it about the PIECE rather than about the card.
   ⚠️ `.unshot` IS STILL SET ON THE ELEMENT and is still what browse.js reads.
   Do not delete the class because the rule went; it is how a shade card, a
   photographed card and a sketched one are told apart. */
.it .sketch { display: grid; justify-items: center; gap: 0.5rem; color: var(--amber); }
.it .sketch svg { width: 4rem; height: 4rem; opacity: 0.6; }
/* ⚠️ No opacity. At 0.9 this measured 3.83:1 against the tile — a translucent
   colour is a weaker colour, and the token only clears 4.5:1 at full strength. */
.it .sketch em { font-style: normal; font-family: var(--sans); font-size: 0.6875rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--amber-deep); }
.wall.list .it .sketch svg { width: 2.4rem; height: 2.4rem; }
.wall.list .it .sketch em { display: none; }

/* ---------- linen and napkin shades ----------
 * Not products in Goodshuffle and not awaiting photography either: the linen
 * IS a colour, so the swatch is the piece rather than a stand-in for it. The
 * tile therefore keeps the solid border a real product gets, never the dashed
 * one .unshot uses. See linens.js. */
/* ⚠️ aspect-ratio 1 IS LOad-BEARING, not tidiness. Without it the frame takes
   the napkin's 5.6:10 and the shade tiles stand 1.24x taller than the product
   tiles beside them — the grid check catches it, and it is the sort of thing
   that reads as the page being broken rather than as a design. */
/* ⚠️ PAPER, LIKE EVERY OTHER CARD. Darrell, 2026-08-18: the wash behind a
   swatch tinted the one thing on the card that IS a colour, so Chalk White sat
   on a gold bed and read warmer than the cloth Kristi actually rents. It also
   made 74 of the ~230 cards a different object from the rest of the grid for
   no reason a bride could name. */
.it.shade figure { display: grid; place-items: center; padding: 1rem; aspect-ratio: 1; background: var(--paper); }
/* A cloth to the edges of its frame, with the inset shadow that keeps a pale
   shade from dissolving into the tile behind it. Chalk White on paper is the
   case this exists for. */
.it.shade .cloth {
  width: 100%; height: 100%; border-radius: calc(var(--radius) * 0.6);
  background: var(--cloth);
  box-shadow: inset 0 0 0 1px rgba(29, 29, 27, 0.10), 0 2px 6px rgba(29, 29, 27, 0.08);
}
.it.shade .napbox { display: block; height: 100%; aspect-ratio: 5.6 / 10; margin: 0 auto; }
.it.shade .cloth, .it.shade .napbox { max-width: 100%; }
/* ⚠️ Ported from design.css, not shared. Two stylesheets, one photograph: the
   canvas ships design.css and this page does not, so the three rules the
   napkin needs live in both. Change one, change the other — the tint curve in
   linens.js is what actually keeps them agreeing. */
.it.shade .nap { position: relative; display: block; width: 100%; height: 100%; isolation: isolate;
  filter: drop-shadow(0 4px 7px rgba(29, 29, 27, 0.18)); }
.it.shade .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;
}
.it.shade .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. */
.it.shade .nap .sheen {
  mix-blend-mode: screen; opacity: var(--naps, 0);
  filter: brightness(var(--napsb)) contrast(var(--napsc));
}
.it.shade .nap.shot img { mix-blend-mode: normal; filter: none; }
.wall.list .it.shade figure { padding: 0.5rem; }
/* ⚠️ A COLUMN, SO THE PRICE CAN BE PUSHED TO THE BOTTOM OF IT. The cards in a
   row are already equal height — the grid stretches them and .add sits at the
   foot — but everything above .add was top-flowed, so a three-line title or a
   second meta line (the linen shades carry one, most products do not) dropped
   the price 20-odd pixels below its neighbours. Prices are the figure a bride
   scans ACROSS a row, and a scan needs a line to run along. */
.it .body { padding: 0 1rem 0.75rem; flex: 1; display: flex; flex-direction: column; }
.wall.list .it .body { padding: 0.75rem 1rem; }
.it h3 { font-family: var(--serif); font-weight: 400; font-size: 1.0625rem; line-height: 1.2; }
.it .meta { display: block; font-family: var(--sans); font-size: var(--label-xs); letter-spacing: 0.09em; text-transform: uppercase; color: var(--muted); margin-top: 0.25rem; }
.it .price { display: block; font-family: var(--text); font-size: 1rem; color: var(--amber-deep); margin-top: auto; padding-top: 0.5rem; }
.it .add { margin-top: auto; background: none; border: 0; border-top: 1px solid var(--rule-line); padding: 0.7rem; cursor: pointer; font-family: var(--sans); font-size: var(--label-sm); letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink); }
/* ⚠️ THIS WAS A 999px PILL ON A SQUARE-CORNERED SYSTEM, and it survived the
   rebrand because body.em zeroes the RADIUS TOKENS and this was a literal.
   It also survived build/verify/system.mjs, which reads the rendered page in
   its default state and never presses List. Anything behind a toggle or an
   empty container is outside that check by construction. */
/* ⚠️ WAS A HARDCODED 0, AND BEFORE THAT A 999px PILL. The pill was the wrong
   shape on a square-cornered house; a literal 0 became the wrong shape the day
   the house softened. It reads the token now, so it follows the house in both
   directions and cannot go stale a third time. */
.wall.list .it .add { margin: 0 1rem 0 auto; border: 1px solid var(--rule-line); border-radius: var(--radius); }
/* ⚠️ EVERGREEN GROUND, MIST LETTERS, AND THE PRESSED STATE IS THE SAME PAIR.
   Darrell, 2026-09-02, naming both values: "hover over ground color: #0A3D2B
   evergreen / --amber-deep, and the hover over letters color: #E7EEE9 mist /
   --wash ... when you click it the copy will stay as we have it and it will
   lock in the hover colors."

   Two things that were true before and are not any more:
     · hover was --wash as a GROUND, so hovering an on-list card moved from an
       emerald fill to a mist fill and read as un-selecting it. The two states
       now differ by nothing at all, which is what "lock in" means: the hover
       is a preview of what the click leaves behind.
     · pressed was --amber (#0E5F41) over --paper. Emerald is the stroke green;
       this is a filled area, and the system's area green is evergreen.

   ⚠️ --honey, NOT --wash, AND TAKING HIM AT HIS WORD IS WHAT BREAKS THIS.
   He wrote "#E7EEE9 mist / --wash", naming the token and the hex together —
   and on THIS page they are not the same colour. rentals.css:49 re-scopes
   --wash to #4C9A74 on both rentals pages, deliberately and with a warning
   attached. Written as `var(--wash)` the label came out emerald-lit on
   evergreen at 2.8:1, measured, which is unreadable and looks close enough to
   right in a screenshot to survive a review.

   The hex is what he meant and --honey is the token carrying it here, so that
   is what is written. --honey's own comment calls it "fills that sit under ink
   type", which this is not; the exception is deliberate and is the reason this
   paragraph exists rather than a bare token swap. Mist on evergreen is 10.3:1.

   ⚠️ THE GENERAL LESSON, because this file is where it will happen again: a
   token name in a comment is a claim about a value, and on these two pages
   three of them are re-scoped. Read the value, not the name.

   ⚠️ THE LABEL STILL CHANGES AND THAT IS DELIBERATE. "the copy will stay as we
   have it" is about not touching the wording, not about freezing it — sync()
   in browse.js still swaps Add / On list / Remove, and a button whose only
   feedback was colour would leave a screen reader with nothing. aria-pressed
   carries the state; the colours are the visual half of the same fact.

   ⚠️ NO FLIP HERE. The comment asked for the KM Brands motion suite's Flip 05
   hinge between the two states and Darrell pulled it the same day — "the
   motions were getting a little heavy and a little distracting from what we
   wanted visitors to actually be doing." A crossfade is what is left, and the
   card wall is 113 buttons deep, which is exactly where a hinge stops being a
   flourish and becomes weather. */
.it .add {
  transition: background-color .24s var(--ease-out),
              color .24s var(--ease-out),
              border-color .24s var(--ease-out);
}
.it .add:hover,
.it .add:focus-visible,
.it .add[aria-pressed="true"] {
  /* border-color, not border-top-color: the grid card has only a top edge but
     .wall.list .it .add carries all four, and the list view was the half that
     kept a hairline round an emerald fill. */
  background: var(--evergreen); color: var(--honey); border-color: var(--evergreen);
}
.empty { padding: 3rem 0; color: var(--muted); font-style: italic; }

@media (max-width: 60rem) {
  .shell { grid-template-columns: 1fr; }
  .rail { position: static; }
  /* Below the split the index becomes a scroller and the hero is dead weight. */
  .roomindex { display: flex; overflow-x: auto; gap: 0.2rem; }
  .roomindex li button { grid-template-columns: auto auto; white-space: nowrap; padding: 0.5rem 0.7rem; }
  .roomindex li button i, .idxhero { display: none; }
  .roomindex .listrow { border-top: 0; border-left: 1px solid var(--rule-line); margin: 0 0 0 0.3rem; padding-left: 0.3rem; }
}

/* The same credit on the canvas card. Not a link — the card is itself a
   button, and an anchor inside a button is invalid and unreachable by
   keyboard. The linked version lives on /rentals. */
.scard-credit {
  position: absolute; right: 0.5rem; top: 0.45rem; z-index: 3;
  font-family: var(--sans); font-size: 0.625rem; letter-spacing: 0.06em;
  color: var(--paper); background: rgba(29, 29, 27, 0.72);
  padding: 0.15rem 0.4rem; border-radius: var(--radius-sm);
}

/* ============================================================
   CANVAS  /rentals/design
   ============================================================ */

/* The canvas fills exactly what is left below the header, so the control bar
 * lands on screen instead of below the fold.
 *
 * ⚠️ This calc is only the no-JS fallback. design.js measures the app's own
 * offsetTop and sets the height outright, because a formula that enumerates
 * the elements above it is wrong the moment one is added — summing the topbar
 * and nav still left 49px of overflow from something else on the page.
 *
 * It was calc(100vh - 3.5rem), and 3.5rem is not the height of anything, so
 * the app ran well past the bottom of the window and took the control bar with
 * it. A new visitor had to scroll to discover that Group pieces, Clear board
 * and their own wish list existed at all.
 *
 * dvh, not vh: on a phone, vh is the tallest the viewport ever gets, so the
 * bar would sit under the browser's own chrome whenever that chrome is
 * showing. */
.app { display: flex; flex-direction: column; height: calc(100dvh - var(--chrome, 10rem)); min-height: 34rem; }
/* Belt and braces: if the app is ever taller than the viewport anyway — a very
   short window, a zoomed page — the bar sticks to the bottom rather than
   scrolling out of reach. */
.hud { position: sticky; bottom: 0; }
.striphint { flex: none; padding: 0.5rem var(--gut) 0; font-family: var(--sans); font-size: var(--label-sm); letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
.strip { display: flex; gap: 0.4rem; padding: 0.4rem var(--gut) 0.5rem; flex: none; }

.scard { position: relative; flex: 1 1 0; min-width: 0; height: 5.5rem; border: 1px solid var(--rule-line); border-radius: var(--radius-sm); overflow: hidden; background: var(--paper); cursor: pointer; padding: 0; transition: flex-grow 0.45s var(--ease-out), border-color 0.3s ease; }
.scard[aria-expanded="true"] { flex-grow: 1.9; border-color: var(--ink); }
.scard[aria-pressed="true"] { border-color: var(--amber); box-shadow: inset 0 0 0 1px var(--amber); }
.scard img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* ⚠️ THE VEIL READS --honey, IT DOES NOT NAME A COLOUR. It carried
   rgba(255,208,97,…) literally, which is Honey Pulse's honey written out by
   hand — so on the emerald ground the whole strip would have stayed yellow
   while every token around it went green, and nothing in the file could have
   said it was wrong. --honey is the WASH role in both palettes (honey on
   Honey Pulse, emerald mist on emerald), so the mix follows the ground.
   The label above it stays ink on both, which is what the role guarantees. */
.scard .veil { position: absolute; inset: 0; background: linear-gradient(to bottom,
    color-mix(in srgb, var(--honey) 30%, transparent) 0%,
    color-mix(in srgb, var(--honey) 80%, transparent) 100%); transition: transform 0.5s var(--ease-out); }
.scard[aria-expanded="true"] .veil { transform: translateY(100%); }
.scard .lab { position: absolute; inset: auto 0 0 0; z-index: 2; padding: 0.45rem 0.7rem; color: var(--ink); font-family: var(--sans); font-size: var(--label-sm); letter-spacing: 0.09em; text-transform: uppercase; display: flex; justify-content: space-between; gap: 0.5rem; align-items: center; text-shadow: 0 0 6px rgba(255,255,255,0.9), 0 0 18px rgba(255,255,255,0.7); }
.scard .n { color: color-mix(in srgb, var(--ink) 62%, transparent); font-variant-numeric: tabular-nums; font-size: var(--label-xs); }
.scard .pin { display: none; width: 11px; height: 11px; margin-right: 0.35rem; }
.scard.locked .pin { display: inline-block; }

/* "Your list" is a filter over every room, not another room, so it gets no
   photograph. A cream card with a gold rule reads as belonging to the strip
   while plainly not being one of the six. */
.scard.listcard { flex-grow: 0.62; background: var(--wash); border-color: var(--amber); }
.scard.listcard[aria-expanded="true"] { flex-grow: 0.95; }
.scard.listcard .lab { color: var(--amber-deep); text-shadow: none; }
.scard.listcard .n { color: color-mix(in srgb, var(--amber-deep) 70%, transparent); }
.scard.listcard:disabled { opacity: 0.42; cursor: default; border-color: var(--rule-line); }
.scard.listcard:disabled .lab { color: var(--muted); }

.stage { position: relative; flex: 1; min-height: 0; margin: 0 var(--gut); }
.board { position: absolute; border: 1px dashed var(--rule-line); border-radius: var(--radius); background: var(--paper); transition: border-color 0.25s ease, background 0.25s ease; }
.board.hot { border-color: var(--amber); background: var(--wash); }
.board-hint { position: absolute; inset: 0; display: grid; place-items: center; font-family: var(--text); font-style: italic; font-size: 1.0625rem; color: var(--muted); pointer-events: none; transition: opacity 0.3s ease; }
.board.has .board-hint { opacity: 0; }
.glabel { position: absolute; font-family: var(--sans); font-size: var(--label-xs); letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); opacity: 0; transition: opacity 0.4s ease; pointer-events: none; }
.glabel.in { opacity: 1; }

.chip { position: absolute; width: var(--chip, 62px); height: var(--chip, 62px); transform-origin: center; border: 1px solid var(--rule-line); border-radius: var(--radius-sm); background: var(--paper); display: grid; place-items: center; cursor: grab; touch-action: none; padding: 6px; opacity: 0; transform: translate(-50%, -50%) scale(0.5); transition: opacity 0.4s ease, transform 0.6s var(--ease-out); box-shadow: 0 3px 10px rgba(15,15,14,0.09); }
.chip.in { opacity: 1; transform: translate(-50%, -50%); }
.chip:hover { border-color: var(--ink); z-index: 5; box-shadow: 0 6px 18px rgba(15,15,14,0.16); }
.chip picture { display: contents; }
.chip img { object-fit: contain; pointer-events: none; }

/* ⚠️ A frame chip's image is sized by absolute inset, not by max-height.
 *
 * `max-height: 100%` on a centred grid item does not resolve — the percentage
 * is cyclic against an auto-sized grid area and the browser drops it. A 62px
 * chip was therefore painting a 48x173 image: it hung 90px below its own box,
 * pushed the bottom row of inventory past the fold, and put 50px of pointless
 * scroll under a canvas that is supposed to fit the window exactly.
 *
 * This is the same failure this project hit once before, on a different
 * element, and fixed the same way: absolute inset against a definitely-sized
 * parent, with margin:auto to centre.
 *
 * Scoped to :not(.placed) deliberately. A PLACED piece's image is meant to
 * overrun its wrapper — that is why the collection hull measures the image
 * rather than the box. */
.chip:not(.placed) img {
  position: absolute; inset: 6px; margin: auto;
  width: auto; height: auto;
  max-width: calc(100% - 12px); max-height: calc(100% - 12px);
}
.chip .noimg { font-family: var(--sans); font-size: 0.7rem; letter-spacing: 0.06em; color: var(--muted); }
.chip .tip { position: absolute; bottom: -1.5rem; left: 50%; transform: translateX(-50%); font-family: var(--sans); font-size: var(--label-xs); white-space: nowrap; background: var(--ink); color: var(--ground); padding: 0.18rem 0.45rem; border-radius: 3px; opacity: 0; pointer-events: none; transition: opacity 0.18s; z-index: 20; }
.chip:hover .tip { opacity: 1; }
.chip.dragging { z-index: 60; transition: none; cursor: grabbing; box-shadow: 0 18px 40px rgba(15,15,14,0.28); }

/* A placed piece is part of the arrangement, not a catalogue tile: the frame,
   fill and shadow all go, and it takes the size its category earns. */
/* ⚠️ The SIZE GOES ON THE IMAGE, and the wrapper shrink-wraps it.
 *
 * It used to go on the wrapper, with the image unconstrained inside. So a
 * water glass whose box was set to 74px painted at 269px — the number was
 * applied to something nobody can see, and the picture did whatever its
 * natural dimensions wanted. That is the actual source of the "ginormous
 * glassware": not the wrong inches, but inches applied to the wrong element.
 *
 * Sizing the image and letting the wrapper follow makes what is measured and
 * what is rendered the same object. It is also why drawGroups() measures the
 * image — that was already working around this. */
.chip.placed { width: auto; height: auto; border-color: transparent; background: none; box-shadow: none; padding: 0; }
.chip.placed img { width: var(--pw, auto); height: var(--ph, auto); max-width: var(--maxw, none); max-height: var(--maxh, none); object-fit: contain; }
.chip.placed:hover { border-color: transparent; box-shadow: none; }
.chip.placed .tip { display: none; }
.chip .kill { position: absolute; top: -9px; right: -9px; width: 22px; height: 22px; border-radius: 50%; border: 1px solid var(--rule-line); background: var(--paper); color: var(--muted); font-size: 0.75rem; line-height: 1; cursor: pointer; opacity: 0; transition: opacity 0.18s; z-index: 30; }
.chip:hover .kill, .chip .kill:focus-visible { opacity: 1; }

.lassosvg { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 55; }
.lassosvg path { fill: color-mix(in srgb, var(--honey) 18%, transparent); stroke: var(--amber); stroke-width: 1.5; stroke-dasharray: 5 4; }

/* A group is drawn UNDER its pieces and the BODY never intercepts a pointer,
   so a piece inside one is still individually draggable. Interactive parts are
   opted in one at a time: the tab, and the four edges. */
.group { position: absolute; border: 1.5px solid var(--amber); border-radius: var(--radius); background: color-mix(in srgb, var(--honey) 9%, transparent); pointer-events: none; z-index: 1; transition: border-color 0.2s; }
.group .tab { position: absolute; top: -0.85rem; left: 0.7rem; z-index: 2; display: inline-flex; align-items: center; gap: 0.4rem; background: var(--paper); border: 1px solid var(--amber); border-radius: 0; padding: 0.14rem 0.6rem; pointer-events: auto; font-family: var(--sans); font-size: var(--label-xs); letter-spacing: 0.1em; text-transform: uppercase; color: var(--amber-deep); white-space: nowrap; cursor: text; }
.group .tab.unnamed { font-style: italic; color: var(--muted); }
.group .tab .n { color: var(--muted); }
.group .tab button { background: none; border: 0; cursor: pointer; color: var(--muted); font-size: 0.8rem; line-height: 1; padding: 0 0 0 0.15rem; }

/* Grab handles on the border, as four strips: pointer-events cannot punch a
   hole in the middle of an element, so "the border drags, the inside does not"
   has to be built from the edges outward. They sit in the padding the hull
   already leaves, so a strip almost never lands on top of a piece. */
.gedge { position: absolute; pointer-events: auto; cursor: grab; z-index: 0; touch-action: none; }
.gedge.t { left: -7px; right: -7px; top: -7px; height: 14px; }
.gedge.b { left: -7px; right: -7px; bottom: -7px; height: 14px; }
.gedge.l { top: -7px; bottom: -7px; left: -7px; width: 14px; }
.gedge.r { top: -7px; bottom: -7px; right: -7px; width: 14px; }
.group:hover { border-color: var(--amber-deep); }
.group.moving { border-color: var(--amber-deep); background: color-mix(in srgb, var(--honey) 15%, transparent); }
.group.moving .gedge { cursor: grabbing; }

.presets { position: absolute; z-index: 120; background: var(--paper); border: 1px solid var(--rule-line); border-radius: var(--radius); box-shadow: 0 14px 34px rgba(15,15,14,0.2); padding: 0.5rem; display: none; gap: 0.2rem; min-width: 13rem; }
.presets.on { display: grid; }
.presets input { font-family: var(--sans); font-size: 0.875rem; padding: 0.45rem 0.55rem; border: 1px solid var(--rule-line); border-radius: var(--radius-sm); }
.presets button { text-align: left; background: none; border: 0; padding: 0.4rem 0.55rem; border-radius: var(--radius-sm); cursor: pointer; font-family: var(--sans); font-size: 0.8125rem; }
.presets button:hover { background: var(--wash); }

/* Every control in one place. They used to be in three, and a floating button
   over the canvas had pieces landing underneath it. Nothing floats now. */
.hud { position: relative; z-index: 100; flex: none; display: flex; align-items: center; gap: 0.6rem; padding: 0.55rem var(--gut); border-top: 1px solid var(--rule-line); background: var(--paper); }
.hud .status { display: flex; align-items: baseline; gap: 0.5rem; margin-right: auto; font-family: var(--sans); font-size: var(--label-sm); letter-spacing: 0.09em; text-transform: uppercase; color: var(--muted); }
.hud .status b { font-size: 1rem; color: var(--ink); font-variant-numeric: tabular-nums; }
.hud .sep { width: 1px; height: 1.4rem; background: var(--rule-line); margin: 0 0.3rem; }
.hud .group-btn { border-color: var(--amber); color: var(--amber-deep); }
.hud .group-btn[aria-pressed="true"] { background: var(--honey); color: var(--ink); border-color: var(--honey); }
.hud .group-btn .dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: currentColor; margin-right: 0.45rem; vertical-align: 1px; }

/* ---------- the tray's scrollbar wears the brand ----------
 * The stack and the shelf on the canvas already paint theirs honey on wash,
 * and the tray sat next to them showing the browser's default. Same treatment
 * so one page does not carry two scrollbars.
 *
 * ⚠️ Always painted, never overlay. macOS hides overlay scrollbars until you
 * scroll, which is exactly when a list that continues past its edge needs to
 * say so — the canvas shelf learned this the hard way, and its comment
 * records it. `scrollbar-width: auto` rather than thin is what keeps it. */
.tray-list {
  scrollbar-width: auto; scrollbar-color: var(--amber) var(--wash);
}
.tray-list::-webkit-scrollbar { width: 10px; }
.tray-list::-webkit-scrollbar-track { background: var(--wash); border-radius: 5px; }
.tray-list::-webkit-scrollbar-thumb {
  background: var(--honey); border: 2px solid var(--wash); border-radius: 5px;
}
.tray-list::-webkit-scrollbar-thumb:hover { background: var(--amber); }

/* A collection, named on the tile. Glassware arrives in sets — the five Pleat
   pieces belong together — and the grid already sorts them into one run, so
   this says WHY they are adjacent rather than leaving it to be noticed. */
/* ⚠️ ONE LINE, ALWAYS, AND RESERVED EVEN WHEN EMPTY. Left to wrap, "White
   Raised Rim Stoneware collection" ran to two lines of uppercase micro-type
   and took its whole grid row 75px taller than a row of unlabelled pieces —
   measured at 1.24x, over the fifth the wall check allows. Reserving the line
   costs every tile 1em and buys a wall that is level whatever a collection
   happens to be called. The full name stays available on hover. */
.it .fam {
  display: block; margin-top: 0.15rem; min-height: 1em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-family: var(--sans); font-size: var(--label-xs); letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--amber-deep);
}


/* ============================================================
   THE EMERALD GROUND — /rentals
   ============================================================
   ⚠️ SCOPED TO body.em BECAUSE THIS FILE IS SHARED. /rentals/design links
   the same stylesheet and is still on Honey Pulse, where --gilt does not
   exist: an unscoped `background: var(--gilt)` would resolve to nothing
   there and the canvas would lose its submit button's fill entirely. Scope
   first, unscope when the last page arrives.

   ⚠️ THE HOVER WAS A REAL CONTRAST FAILURE WAITING FOR THE PALETTE SWAP.
   .dock-submit rested on --honey and hovered to --amber with an ink label.
   On Honey Pulse that is ink on #b8860b. On emerald the same two tokens
   become mist and EMERALD, so the hover would have put ink on #0E5F41 at
   2.59:1 — the exact failure emerald.css §4 exists to describe, arriving
   silently through tokens that were correct on the palette they were
   written for. Nothing in the file could have flagged it: both rules read
   roles, and the roles were right. It is the ROLE MAPPING that moved.

   So the wish-list submit becomes what it actually is: the enquiry moment
   on this page, and therefore the page's one gilt. The system's ember —
   gilt fill, ink label at 6.11:1, and a hover that adds a GLOW rather than
   changing the ground, so there is no frame that can fail mid-transition.
   Gilt-deep was the tempting hover and it measures 4.05:1 under ink, which
   is why the glow is not a stylistic preference. */
/* ⚠️ THIS CONTROL GAVE UP ITS GOLD, 2026-08-31. Darrell's call: the "next step"
   close panel wears the ember on Home, About and now Rentals, and the system
   permits exactly one gold event per page. So the wish-list submit takes
   emerald — paper on emerald is 7.7:1, and the hover keeps the ember's shape
   (a glow, not a change of ground) so no frame can fail mid-transition.

   The comment above argued this button IS the page's enquiry moment and should
   hold the gold. That argument still stands on its merits; it was overruled
   deliberately in favour of one consistent close panel across the three pages.
   If it is ever reversed, un-gild the close panel in the same commit —
   system.mjs:294 counts visible gold controls and two fails /rentals. */
body.em .dock-submit,
body.em .tray-submit {
  background: var(--amber);
  color: var(--paper);
  transition: box-shadow .45s ease, transform .3s var(--ease-out);
}
body.em .dock-submit:hover,
body.em .tray-submit:hover,
body.em .dock-submit:focus-visible,
body.em .tray-submit:focus-visible {
  background: var(--amber);
  box-shadow: 0 0 0 1px rgba(76,154,116,.85), 0 6px 24px rgba(14,95,65,.40),
              0 0 40px rgba(14,95,65,.26);
  transform: translateY(-1px);
}

/* ⚠️ SQUARE, WHOLE HOUSE. The view toggle was a 999px pill, which is the one
   shape the system names outright in its governance list. It reads as a
   control either way; the radius was the only thing making it foreign. */
body.em .pill { border-radius: 0; }
