/* ===========================================================
   BOARD: the Doshus UI spec primitives (UI-SPEC.md)
   .board > .panel on a fine grid, placed by data-x/y/w/h, arranged by js/board.js.
   Every color reads --accent (an L C H triple), set once per scope. Spacing, radius, type and
   font scales live in shared.css.
   =========================================================== */

/* Accent routing: one attribute picks the accent, everything inside follows */
[data-accent="finance"]  { --accent: var(--accent-finance); }
[data-accent="crypto"]   { --accent: var(--accent-crypto); }
[data-accent="taxes"]    { --accent: var(--accent-taxes); }
[data-accent="invest"]   { --accent: var(--accent-invest); }
[data-accent="networth"] { --accent: var(--accent-networth); }
[data-accent="lounge"]   { --accent: var(--accent-lounge); }
[data-accent="amzn"]     { --accent: var(--accent-amzn); }
[data-accent="myth"]     { --accent: var(--accent-myth); }
[data-accent="discord"]  { --accent: var(--accent-discord); }

.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* ─── BOARD: 24 columns of 2rem rows (12 and 6 in the smaller tiers, at the end); panels inset by a
   gutter so cells stay exact ─── */
.board-wrap { max-width: 1440px; margin: 0 auto; padding: var(--sp-m) clamp(1rem, 3vw, 2rem) 6rem;
    position: relative; z-index: 10;
}
/* The default accent, at zero specificity so a data-accent on the board itself wins; a board in a
   panel takes its panel's */
:where(.board:not(.panel .board)) { --accent: var(--accent-crypto); }
.board { --cols: 24; --row: 2rem; --gutter: 0.625rem;
    /* Arranging feel, tune here: how strongly the cell field shows and the ghost's fill and
       glow (alphas, 0–1), then how long panels and the ghost glide (in ms; js/board.js reads
       both). The ghost's colors live in .board-ghost, the field's in .board::before */
    --field-opacity: 0.7; --ghost-fill: 0.3; --ghost-glow: 0.75;
    --glide: 1000ms; --ghost-glide: 180ms;
    /* How long a finger rests on a grab bar before the panel lifts (a mouse grabs at once), and
       the narrowest a panel can get, in columns */
    --hold: 500ms; --min-cols: 4;
    /* Panel glass: how dark the fill is (alpha) and what it does to the page behind it. Over the
       live star field the blur redoes itself every frame, which costs a weak GPU more than the
       stars do (Chromium, software GPU: 7 fps blurred, 17 without), so a machine the frame-rate
       watcher catches struggling gets .is-lean (js/board.js): no blur, a darker fill instead */
    --panel-fill: 0.45; --panel-backdrop: blur(5px);
    position: relative; display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
    grid-auto-rows: var(--row);
}
/* The snap field: a pixel screen of cells that lights up while arranging, with room to drop below */
.board::before { content: ''; position: absolute; inset: 0; pointer-events: none; opacity: 0;
    transition: opacity 0.25s ease;
    background: radial-gradient(120% 60% at 50% 0%, oklch(var(--nebula-purple) / 0.9), oklch(var(--nebula-blue) / 0.5) 60%, transparent);
    --tile: calc(100% / var(--cols)) var(--row);
    mask: linear-gradient(to right, oklch(var(--space-oled)) calc(100% - 2px), transparent 0) 0 0 / var(--tile),
          linear-gradient(to bottom, oklch(var(--space-oled)) calc(100% - 2px), transparent 0) 0 0 / var(--tile);
    mask-composite: intersect;
}
.board.is-lean { --panel-fill: 0.6; --panel-backdrop: none; }
/* While arranging: room to drop below the last panel, and no backdrop blur. Blurring live
   stars behind every moving panel is the heaviest thing on the page; dark glass stands in */
.board.is-arranging { padding-bottom: calc(var(--row) * 8); user-select: none; }
.board.is-arranging > .panel { backdrop-filter: none; background: oklch(var(--space-oled) / 0.72); }
/* A frame is another page (the Lounge's game): a pointer over it leaves the gesture, its moves and
   its release unseen, even captured. While arranging, and while a press waits to lift a panel
   (Spotify's block is held by its dots alone), every frame lets the pointer through */
:root:has(.board.is-arranging, .panel.is-pressed) iframe { pointer-events: none; }
.board.is-arranging::before { opacity: var(--field-opacity); }
/* The ghost: where the panel will land, in the grabbed panel's own accent. It breathes by
   fading a glow layer (opacity runs on the compositor; an animated box-shadow repaints) */
.board-ghost { grid-column: var(--x) / span var(--w); grid-row: var(--y) / span var(--h); margin: var(--gutter) calc(var(--gutter) + var(--inset, 0px));
    position: relative; pointer-events: none; border-radius: var(--r-l); border: 2px dashed oklch(var(--accent));
    background: radial-gradient(circle at 50% 40%, oklch(var(--accent) / var(--ghost-fill)), oklch(var(--accent) / 0.08) 75%);
    box-shadow: 0 0 36px oklch(var(--accent) / var(--ghost-glow)), inset 0 0 28px oklch(var(--accent) / 0.45);
}
.board-ghost::after { content: ''; position: absolute; inset: -2px; border-radius: inherit;
    border: 2px solid oklch(var(--star-white) / 0.85);
    box-shadow: 0 0 60px oklch(var(--accent) / var(--ghost-glow)), inset 0 0 40px oklch(var(--accent) / 0.6);
    animation: ghost-breathe 1.4s ease-in-out infinite alternate;
}
.board-ghost[hidden] { display: none; }
@keyframes ghost-breathe { from { opacity: 0; } to { opacity: 1; } }

.board-bar { display: flex; justify-content: flex-end; align-items: center; gap: var(--sp-xs); max-width: 1440px;
    min-height: 2.25rem; /* holds its height while its buttons come and go, so the board never jumps */
    margin: 0 auto; padding: 0 clamp(1rem, 3vw, 2rem); position: relative; z-index: 10;
}
/* A bar the page placed itself (data-board-bar) centers in its spot; the page can restyle it.
   doshus.net homes it at the foot of the hero. A page with a board per slide homes all their
   bars there and hides all but the showing slide's */
[data-board-bar].board-bar { justify-content: center; padding: 0; }
.hero > .board-bar { margin-top: 1.5rem; }
.board-bar[hidden] { display: none; }
.board-bar button, .board-menu button {
    font: var(--fs-s) var(--font-mono); color: var(--text-main); cursor: pointer;
    padding: var(--sp-xs) var(--sp-s); border-radius: var(--r-s);
    background: oklch(var(--space-oled) / 0.5); border: 1px solid oklch(var(--accent-crypto) / 0.4);
}
.board-bar button:hover, .board-menu button:hover { border-color: oklch(var(--accent-crypto)); }
.board-menu { margin: auto; /* a popover centers by margin: auto, which a page's * { margin: 0 } reset zeroes */
    padding: var(--sp-m); border-radius: var(--r-l); color: var(--text-main);
    background: oklch(var(--space-oled) / 0.85); backdrop-filter: blur(12px);
    border: 1px solid oklch(var(--accent-crypto) / 0.5);
}
.board-menu:popover-open { display: grid; gap: var(--sp-xs); }
.board-menu::backdrop { background: oklch(var(--space-oled) / 0.5); }

/* ─── PANEL ─── */
/* A host sets a panel in from its cells' sides with --inset (UI-SPEC). Registered, it computes to a
   length (an em against the panel's own font), so the ghost, which copies it, frames the same spot */
@property --inset { syntax: '<length-percentage>'; inherits: false; initial-value: 0px; }
/* Without JS the panels still flow as 6×8 blocks; board.js sets --x/--y/--w/--h from data-* */
.panel { grid-column: var(--x, auto) / span var(--w, 6); grid-row: var(--y, auto) / span var(--h, 8);
    position: relative; display: flex; flex-direction: column; min-width: 0; min-height: 0; margin: var(--gutter) calc(var(--gutter) + var(--inset, 0px));
    background: oklch(var(--space-oled) / var(--panel-fill)); backdrop-filter: var(--panel-backdrop);
    border: 1px solid oklch(var(--accent) / 0.28); border-radius: var(--r-l);
    transition: border-color 0.35s ease, box-shadow 0.35s ease, scale 0.2s ease, opacity 0.35s ease;
}
.panel[hidden] { display: none; }
.panel::before { content: ''; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
    background: radial-gradient(120% 80% at 0% 0%, oklch(var(--accent) / 0.10), oklch(var(--accent) / 0.02) 40%, transparent 70%);
}
.panel::after { content: ''; position: absolute; inset: 0 var(--r-l) auto; height: 1px; pointer-events: none;
    background: linear-gradient(90deg, transparent, oklch(var(--accent) / 0.7), transparent);
}
:where(.board:not([data-surface="clear"])) > .panel:is(:hover, :focus-within) {
    border-color: oklch(var(--accent) / 0.85);
    box-shadow: 0 12px 38px oklch(var(--accent) / 0.25), 0 0 0 1px oklch(var(--accent) / 0.4) inset;
}
/* A clear board (data-surface="clear") keeps the page's sky as every panel's back: no glass, frame,
   grab dots or controls at rest, so the page reads as it did before it was a board (on touch
   screens the controls and dots stay faint). Reached for, a panel shows a faint frame and its
   controls: keyboard focus inside it (a click's focus leaves nothing behind), a pointer on it where
   pointers hover, a finger holding it (which glows, as on every board). While arranging, every panel
   shows where it is. A page can still dress a panel itself (the Lounge's category boxes) */
.board[data-surface="clear"] { --panel-fill: 0; --panel-backdrop: none; }
:where(.board[data-surface="clear"]) > .panel { border-color: transparent; }
:where(.board[data-surface="clear"]) > .panel::before, :where(.board[data-surface="clear"]) > .panel::after { opacity: 0; transition: opacity 0.35s ease; }
:where(.board[data-surface="clear"]) > .panel > .panel-dots::before { opacity: 0; }
@media (hover: none) {
    :where(.board[data-surface="clear"]) > .panel:not(.is-collapsed) > :is(.panel-head, .panel-grab) > .panel-tools,
    :where(.board[data-surface="clear"]) > .panel:not(.is-collapsed) > .panel-edge[data-dir="se"],
    :where(.board[data-surface="clear"]) > .panel > .panel-dots::before { opacity: 0.35; }
}
:where(.board[data-surface="clear"]) > .panel:is(:has(:focus-visible), .is-holding, .is-dragging, .is-resizing) { border-color: oklch(var(--accent) / 0.45); box-shadow: 0 0 26px oklch(var(--accent) / 0.16); }
:where(.board[data-surface="clear"]) > .panel:is(:has(:focus-visible), .is-holding, .is-dragging, .is-resizing)::before,
:where(.board[data-surface="clear"]) > .panel:is(:has(:focus-visible), .is-holding, .is-dragging, .is-resizing)::after,
:where(.board[data-surface="clear"]) > .panel:is(:has(:focus-visible), .is-holding, .is-dragging, .is-resizing) > :is(.panel-head, .panel-grab) > .panel-tools,
:where(.board[data-surface="clear"]) > .panel:is(:has(:focus-visible), .is-holding, .is-dragging, .is-resizing) > .panel-edge[data-dir="se"],
:where(.board[data-surface="clear"]) > .panel:is(:has(:focus-visible), .is-holding, .is-dragging, .is-resizing) > .panel-dots::before { opacity: 1; }
@media (hover: hover) {
    :where(.board[data-surface="clear"]) > .panel:hover { border-color: oklch(var(--accent) / 0.45); box-shadow: 0 0 26px oklch(var(--accent) / 0.16); }
    :where(.board[data-surface="clear"]) > .panel:hover::before, :where(.board[data-surface="clear"]) > .panel:hover::after,
    :where(.board[data-surface="clear"]) > .panel:hover > :is(.panel-head, .panel-grab) > .panel-tools,
    :where(.board[data-surface="clear"]) > .panel:hover > .panel-edge[data-dir="se"],
    :where(.board[data-surface="clear"]) > .panel:hover > .panel-dots::before { opacity: 1; }
}
.board[data-surface="clear"].is-arranging > .panel:not(.is-dragging, .is-resizing) { background: oklch(var(--space-oled) / 0.35); border-color: oklch(var(--accent) / 0.5); }
.board[data-surface="clear"].is-arranging > .panel::before, .board[data-surface="clear"].is-arranging > .panel::after { opacity: 1; }
/* A finger resting on a panel charges a glow as it swells, so the hold reads as a grab; it selects no
   text and opens no picture menu meanwhile (a block is held by what it holds, not a strip) */
.panel.is-holding { scale: 1.02; transition-duration: var(--hold); user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
    border-color: oklch(var(--accent)); box-shadow: 0 0 0 2px oklch(var(--accent) / 0.55), 0 0 44px oklch(var(--accent) / 0.45);
}
.panel.is-dragging, .panel.is-resizing { z-index: 50; border-color: oklch(var(--accent));
    box-shadow: 0 0 0 2px oklch(var(--accent) / 0.7), 0 24px 60px oklch(var(--accent) / 0.4);
}
/* Only the carried panel keeps a layer of its own; the ones gliding aside get one from their
   animation for as long as it runs */
.panel.is-dragging { position: fixed; margin: 0; opacity: 0.92; scale: 1.02; cursor: grabbing; will-change: translate; }

.panel-head { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center;
    padding: var(--sp-m) var(--sp-m) var(--sp-xs);
    position: relative; cursor: grab; touch-action: pan-y pinch-zoom;
    user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
/* Grab dots: top center of every panel, in the strip above the title (and clear of a meme's
   label and video buttons); a grab handle of their own, beside the head and a meme's strip */
.panel-dots { position: absolute; top: 0; left: 50%; translate: -50% 0; z-index: 4; width: 2.75rem; height: 0.8rem;
    cursor: grab; touch-action: pan-y pinch-zoom; -webkit-touch-callout: none;
}
.panel-dots::before { content: ''; position: absolute; top: 3px; left: 50%; translate: -50% 0; width: 18px; height: 8px;
    opacity: 0.5; transition: opacity 0.2s ease;
    background: radial-gradient(circle, oklch(var(--accent)) 1.1px, transparent 1.6px) 0 0 / 6px 4px;
}
:where(.board:not([data-surface="clear"])) > .panel:is(:hover, :focus-within, .is-holding, .is-dragging) > .panel-dots::before { opacity: 1; }
/* Pressed, they light up: which also tells Chromium they answer a press, so a finger on them isn't
   moved onto a link beside or under them (its touch adjustment), as on a block of links */
.panel-dots:active::before { opacity: 1; }
.panel-icon { grid-column: 1; margin-right: var(--sp-s); width: 2.5rem; height: 2.5rem; display: grid; place-items: center; border-radius: 9px;
    background: oklch(var(--accent) / 0.12); border: 1px solid oklch(var(--accent) / 0.4); color: oklch(var(--accent));
}
.panel-icon svg { width: 1.4rem; height: 1.4rem; }
.panel-head h2 { grid-column: 2; font-family: var(--font-panel); font-size: clamp(1rem, 0.55rem + 4.75cqi, var(--fs-l));
    line-height: 1; margin: 0; overflow-wrap: anywhere; text-align: start; color: oklch(var(--accent));
}
/* The tag and the tools share the head's last slot, so whichever shows, the title keeps its room.
   Spacing rides on the icon and that slot, so a head without an icon starts its title at the edge */
.panel-head > :is(.panel-tag, .panel-tools) { grid-area: 1 / 3; justify-self: end; margin-left: var(--sp-s); }
/* A narrow head drops its icon; narrower still, the title takes a line of its own under the tools */
@container panel (max-width: 15rem) { .panel-icon { display: none; } }
@container panel (max-width: 10rem) { .panel:not(.is-collapsed) > .panel-head h2 { grid-area: 2 / 1 / auto / -1; margin-top: var(--sp-2xs); } }
.panel-tag { font: var(--fs-xs) var(--font-mono); letter-spacing: 0.15em; color: var(--text-muted); }
/* What a panel holds answers to the panel's own width (@container panel, cqi units), never the
   window's, since a visitor can make any panel narrow. The head, body and foot are the
   containers, not the panel: Chromium 141 drops the layout inside a size container moved by
   moveBefore() */
.panel-head, .panel-body, .panel-foot { container: panel / inline-size; }
/* Content scrolls inside, so a panel can be sized smaller than what it holds. The body clips at
   its top edge, so an open head hands it half its bottom padding: room for a first row's hover
   lift and focus ring, with the content where it was (collapsed, the bar keeps its padding) */
.panel:not(.is-collapsed) > .panel-head { padding-bottom: var(--sp-2xs); }
.panel-body { flex: 1; min-height: 0; overflow: auto; display: flex; flex-direction: column; gap: var(--sp-s);
    padding: var(--sp-xs) var(--sp-m) var(--sp-m); position: relative; z-index: 1;
    scrollbar-width: thin; scrollbar-color: oklch(var(--accent) / 0.45) transparent;
}
.panel-body > * { flex: none; }
/* The kit's paragraph: a default, so a page's own class for its text wins */
:where(.panel-body > p) { margin: 0; font-size: 0.82rem; line-height: 1.55; opacity: 0.8; }
.panel-foot { display: flex; flex-wrap: wrap; justify-content: space-between; column-gap: var(--sp-s); padding: var(--sp-xs) var(--sp-m);
    border-top: 1px dashed oklch(var(--star-white) / 0.08);
    font: 0.65rem var(--font-mono); letter-spacing: 0.08em; color: var(--text-muted);
}
.panel-foot b { color: oklch(var(--accent)); font-weight: 600; }

/* Media panel: a meme slot main.js fills (it owns the inner div's children), labeled since it
   has no head */
.panel-media > .random-meme-fixed { flex: 1; min-height: 0; position: relative; overflow: hidden; border-radius: inherit;
    background: oklch(var(--space-oled) / 0.6);
}
.panel-media > .random-meme-fixed::before { content: 'MEDIA / NODE'; position: absolute; top: 0.55rem; left: 0.65rem;
    z-index: 2; pointer-events: none; padding: 2px 6px; border-radius: 4px;
    font: 0.6rem var(--font-mono); letter-spacing: 0.14em; text-transform: uppercase; color: oklch(var(--accent) / 0.7);
    background: oklch(var(--space-oled) / 0.55); border: 1px solid oklch(var(--accent) / 0.35); backdrop-filter: blur(4px);
}
/* A picture stretches to its panel's shape (Doshus, 2026-10-17: he likes the stretch); a video keeps
   its own, centered */
.panel-media > .random-meme-fixed :is(img, video) { display: block; width: 100%; height: 100%; object-fit: fill; }
.panel-media > .random-meme-fixed video { object-fit: contain; }
/* On a clear board a meme wears its own frame, as the Lounge's boxes did: the panel is the frame,
   frosted, its slot unlabeled, lit while it's reached for. The frost goes while arranging and on a
   struggling machine, with the panels', and in a block, whose panel already frosts what's behind it
   (a second blur there moved no pixel more than 3 of 255, at a GPU pass's cost) */
.board[data-surface="clear"] > .panel-media { background: oklch(var(--space-oled) / 0.05); backdrop-filter: blur(10px);
    border: 2px solid oklch(var(--accent) / 0.3); border-radius: 15px; padding: 0.5rem;
}
.board[data-surface="clear"] > .panel-media:not(.is-holding, .is-dragging, .is-resizing):has(:focus-visible) { border-color: oklch(var(--accent) / 0.7); box-shadow: 0 10px 30px oklch(var(--accent) / 0.4); }
@media (hover: hover) { .board[data-surface="clear"] > .panel-media:not(.is-holding, .is-dragging, .is-resizing):hover { border-color: oklch(var(--accent) / 0.7); box-shadow: 0 10px 30px oklch(var(--accent) / 0.4); } }
.board[data-surface="clear"] > .panel-media:is(.is-holding, .is-dragging, .is-resizing) { border-color: oklch(var(--accent)); }
.board[data-surface="clear"] > .panel-media > .random-meme-fixed { background: none; border-radius: 10px; }
.board[data-surface="clear"] > .panel-media > .random-meme-fixed::before { content: none; }
.board:is(.is-arranging, .is-lean) .panel-media, .panel .board > .panel-media { backdrop-filter: none; }
/* A headless panel's grab bar: an invisible strip over the top-left, stopping short of the
   right edge where a video player keeps its buttons; its tools sit at the strip's end */
.panel-grab { position: absolute; top: 0; left: 0; z-index: 3; width: max(40%, 100% - 8rem); height: 2.75rem;
    cursor: grab; touch-action: pan-y pinch-zoom; border-radius: var(--r-l) 0 0 0;
    user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}

/* ─── PANEL CONTROLS (added by js/board.js) ─── */
/* The head is the grab bar. Collapse and ✕ show at its end on hover; the move button appears
   only when a keyboard reaches it, beside them rather than among them, so the head never reflows */
.panel-tools { display: flex; gap: var(--sp-2xs); position: relative; z-index: 5; opacity: 0; transition: opacity 0.2s ease; }
.panel-grab > .panel-tools { position: absolute; top: 50%; right: var(--sp-s); translate: 0 -50%; }
.panel-tools > button, .panel-tools > .board-tools > button { width: 1.75rem; height: 1.75rem; display: grid; place-items: center; padding: 0; cursor: pointer;
    font-size: 0.8rem; color: oklch(var(--accent)); background: oklch(var(--space-oled) / 0.6);
    border: 1px solid oklch(var(--accent) / 0.3); border-radius: var(--r-s);
}
:is(.panel-tools > button, .panel-tools > .board-tools > button):hover { border-color: oklch(var(--accent)); }
.panel-grip { position: absolute; top: 0; right: calc(100% + var(--sp-2xs)); }
/* A headless panel's strip can be narrow: there the grip sits under ✕, inside the panel */
.panel-grab .panel-grip { top: calc(100% + var(--sp-2xs)); right: 0; }
.panel-grip:not(:focus-visible) { width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.panel:not(.panel-media):is(:hover, :focus-within) > .panel-head > .panel-tag { visibility: hidden; }
.panel.is-dragging :is(.panel-head, .panel-grab, .panel-dots) { cursor: grabbing; }
/* Collapsed to its title bar: the head alone, its tools in view so it's clear how to open it. The
   title keeps to one line (cut with …), so the bar is the same height at any width */
.panel.is-collapsed :is(.panel-body, .panel-foot, .panel-edge) { display: none; }
.panel.is-collapsed .panel-head h2 { line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.panel.is-collapsed .panel-tools { opacity: 1; }
.panel.is-collapsed .panel-tag { visibility: hidden; }
.panel-collapse { transition: rotate 0.25s ease; }
.panel.is-collapsed .panel-collapse { rotate: -90deg; }
/* Resize handles straddle every edge and corner; only the corner shows itself */
.panel-edge { position: absolute; inset: 10px; z-index: 4; padding: 0; border: 0; background: none; touch-action: none; }
.panel-edge[data-dir^="n"] { top: -5px; bottom: auto; height: 10px; }
.panel-edge[data-dir^="s"] { bottom: -5px; top: auto; height: 10px; }
.panel-edge[data-dir$="e"] { right: -5px; left: auto; width: 10px; }
.panel-edge[data-dir$="w"] { left: -5px; right: auto; width: 10px; }
.panel-edge[data-dir="n"], .panel-edge[data-dir="s"] { cursor: ns-resize; }
.panel-edge[data-dir="e"], .panel-edge[data-dir="w"] { cursor: ew-resize; }
.panel-edge[data-dir="ne"], .panel-edge[data-dir="sw"] { cursor: nesw-resize; }
.panel-edge[data-dir="nw"], .panel-edge[data-dir="se"] { cursor: nwse-resize; }
.panel-edge[data-dir="se"] { right: 0; bottom: 0; width: 1.3rem; height: 1.3rem; opacity: 0; border-radius: 0 0 var(--r-l) 0;
    background: linear-gradient(135deg, transparent 55%, oklch(var(--accent) / 0.7) 55% 62%, transparent 62% 72%, oklch(var(--accent) / 0.7) 72% 79%, transparent 79%);
}
:where(.board:not([data-surface="clear"])) > .panel:is(:hover, :focus-within) > :is(.panel-head, .panel-grab) > .panel-tools,
:where(.board:not([data-surface="clear"])) > .panel:is(:hover, :focus-within) > .panel-edge[data-dir="se"] { opacity: 1; }
@media (hover: none) {
    :where(.board:not([data-surface="clear"])) > .panel > :is(.panel-head, .panel-grab) > .panel-tools,
    :where(.board:not([data-surface="clear"])) > .panel > .panel-edge[data-dir="se"] { opacity: 1; }
    .panel:not(.panel-media) .panel-tag { visibility: hidden; }
}
/* Touch: one big corner to resize by, instead of eight thin edges a thumb catches by accident */
@media (pointer: coarse) {
    .panel-edge:not([data-dir="se"]) { display: none; }
    .panel-edge[data-dir="se"] { width: 2.25rem; height: 2.25rem; }
    .panel-foot { padding-right: 2.75rem; } /* its status word stays clear of the corner */
}

/* ─── CONTENT PRIMITIVES ─── */
/* group: a titled cluster, e.g. a grid of the site's link buttons (.qlBtns, shared.css) */
.group { display: grid; gap: var(--sp-xs); padding: var(--sp-s);
    background: oklch(var(--accent) / 0.04); border: 1px solid oklch(var(--accent) / 0.25); border-radius: var(--r-m);
}
.group > h3 { margin: 0; font: 600 0.72rem var(--font-mono); letter-spacing: 0.12em; text-transform: uppercase; color: oklch(var(--accent)); }
.group > p { margin: 0; font-size: var(--fs-s); line-height: 1.5; color: var(--text-muted); }

/* fold: native <details> accordion that eases open */
.fold { background: oklch(var(--accent) / 0.04); border: 1px solid oklch(var(--accent) / 0.22); border-radius: var(--r-m); }
.fold > summary { display: flex; justify-content: space-between; cursor: pointer; list-style: none;
    padding: var(--sp-s) var(--sp-m); border-radius: var(--r-m);
    font: 600 var(--fs-s) var(--font-mono); letter-spacing: 0.04em; color: oklch(var(--accent)); background: oklch(var(--accent) / 0.06);
}
.fold > summary::-webkit-details-marker { display: none; }
.fold > summary::after { content: '▾'; transition: rotate 0.3s ease; }
.fold[open] > summary::after { rotate: 180deg; }
.fold > summary:hover { background: oklch(var(--accent) / 0.12); }
.fold::details-content { block-size: 0; overflow: clip; transition: block-size 0.35s ease, content-visibility 0.35s allow-discrete; }
.fold[open]::details-content { block-size: auto; }
.fold > :not(summary) { margin: var(--sp-xs) var(--sp-s); }

/* tip: name, what it does, a command, a link */
.tip { display: grid; gap: 0.35rem; padding: 0.55rem 0.65rem; border-radius: 6px;
    background: oklch(var(--space-oled) / 0.4); border: 1px solid oklch(var(--accent) / 0.15);
}
.tip > b { font: 600 0.76rem var(--font-mono); letter-spacing: 0.02em; color: oklch(var(--accent)); }
.tip > p { margin: 0; font-size: 0.74rem; line-height: 1.5; color: var(--text-muted); }
.tip > code { display: block; overflow-x: auto; white-space: pre; padding: 0.4rem 0.55rem; border-radius: 4px;
    font: 0.72rem var(--font-mono); color: var(--text-main);
    background: oklch(var(--space-oled) / 0.7); border-left: 2px solid oklch(var(--accent));
}
.tip > code::before { content: '$ '; color: oklch(var(--accent) / 0.7); }
.tip > a { width: max-content; max-width: 100%; padding-bottom: 1px; text-decoration: none;
    font: var(--fs-xs) var(--font-mono); color: oklch(var(--accent)); border-bottom: 1px dashed oklch(var(--accent) / 0.4);
}
.tip > a::after { content: ' ↗'; }
.tip > a:hover { color: var(--text-main); border-color: var(--text-main); }

/* One focus ring for every control in the system */
.board-bar button:focus-visible, .board-menu button:focus-visible, .panel-tools button:focus-visible,
.panel-edge:focus-visible, .fold > summary:focus-visible, .tip > a:focus-visible {
    outline: 2px solid oklch(var(--accent)); outline-offset: 2px; opacity: 1;
}

/* ─── TIERS: each has its own layout and save (js/board.js) ─── */
/* Foldables and tablets: 12 columns. By default the wide layout flows onto half the columns, two
   panels to a row; a visitor can still narrow one to a third */
@media (max-width: 1080px) {
    .board:not(.panel .board) { --cols: 12; --min-cols: 4; }
}
/* Phones: 6 columns (Doshus, 2026-10-11; 4 before). By default every panel is full width and as
   tall as its content; half the width is the narrowest (a third is too thin for a title or the
   keyboard's grip on a 320px phone), with two thirds and five sixths between. Stacked panels get twice the air
   between them (Doshus, 2026-10-10), and the board's side padding gives back the extra, so the
   panels keep their width */
@media (max-width: 640px) {
    .board:not(.panel .board) { --cols: 6; --min-cols: 3; --gutter: 1rem; }
    .board-wrap { padding-inline: 0.5rem; }
}

/* ─── A BOARD IN A PANEL (UI-SPEC § Boards in a panel): its blocks move within the panel ─── */
/* Its tier is its own width's (the panel's body), not the window's: a visitor can narrow its panel */
@container panel (max-width: 1080px) { .board { --cols: 12; --min-cols: 4; } }
@container panel (max-width: 640px) { .board { --cols: 6; --min-cols: 3; } }
/* A block made taller than what it holds holds it in its middle (safe: what's too tall for it
   scrolls from its top, none of it out of reach) */
.panel .board > .panel > .panel-body { justify-content: safe center; }
/* Its Reset and Add widget sit with its panel's tools, as they are */
.panel-tools > .board-tools { display: contents; }
.panel-tools > .board-tools > button[hidden] { display: none; }
/* A block's grab strip covers nothing it holds: the block answers the strip's presses itself
   (board.js), and its thin edges tile the gutter beside it, each its own margin and border, so a
   neighbour's meets it there and neither covers the other or what they hold. Its ✕ and
   corner still sit on what it holds (it has no head, and its content runs to its edges): a pointer
   sees them as it reaches the block, but while a finger or pen is in use (board.js's data-touch: a
   touch screen, or a touch laptop's last press, set on the press, before Chromium picks the tap's
   click target) they'd be live unseen, so then they wait, unseen and untouchable, until the block
   is focused or picked (a finger holding it, or having set it down, until a press lands
   elsewhere). Its dots are the cue */
.panel .board > .panel > .panel-grab { pointer-events: none; }
.panel .board > .panel > .panel-grab > .panel-tools { pointer-events: auto; }
.panel .board > .panel > .panel-edge { --out: calc(var(--gutter) + 1px); }
.panel .board > .panel > .panel-edge[data-dir^="n"] { top: calc(-1 * var(--out)); height: var(--out); }
.panel .board > .panel > .panel-edge[data-dir^="s"]:not([data-dir="se"]) { bottom: calc(-1 * var(--out)); height: var(--out); }
.panel .board > .panel > .panel-edge[data-dir$="e"]:not([data-dir="se"]) { right: calc(-1 * var(--out)); width: var(--out); }
.panel .board > .panel > .panel-edge[data-dir$="w"] { left: calc(-1 * var(--out)); width: var(--out); }
.panel .board > .panel.is-picked > .panel-grab > .panel-tools, .panel .board > .panel.is-picked > .panel-edge[data-dir="se"] { opacity: 1; }
:root[data-touch] .panel .board > .panel:not(.is-picked, .is-holding, .is-dragging, .is-resizing, :has(:focus-visible)) > .panel-grab > .panel-tools,
:root[data-touch] .panel .board > .panel:not(.is-picked, .is-holding, .is-dragging, .is-resizing, :has(:focus-visible)) > .panel-edge { opacity: 0; pointer-events: none; }
/* While its blocks are arranged, its room below them shows past its panel's bottom, over what's there */
.panel:has(> .panel-body > .board.is-arranging) { z-index: 40; }
.panel-body:has(> .board.is-arranging) { overflow: visible; }
