@font-face {
  font-family: 'Stagwood Sprite 64';
  src: url('fonts/stagwood-sprite-64.ttf') format('truetype');
  font-display: block;
  /* Built by scripts/font/build.py. Its own metrics (ascent 8px, descent 2px, no gap)
     put the baseline 11px down a 16px line box, which is what centres a 6px capital. */
}

:root {
  /* spec/ui-colors.png: the official Sprite UI palette, a 14-step neutral
     ramp from white to near-black, lightest first. Exposed in full (not
     just the steps used below) so any future UI work pulls from these
     exact values instead of inventing new grays. */
  --gray-0: #FFFFFF;
  --gray-1: #F3F2F1;
  --gray-2: #E1DFDD;
  --gray-3: #C8C6C4;
  --gray-4: #A19F9D;
  --gray-5: #979693;
  --gray-6: #797775;
  --gray-7: #484644;
  --gray-8: #3B3A39;
  --gray-9: #323130;
  --gray-10: #292827;
  --gray-11: #252423;
  --gray-12: #201F1E;
  --gray-13: #1B1A19;

  /* Semantic tokens, each pinned to one of the ramp steps above (kept as
     separate variables since callers reference them by role, not by
     ramp position). */
  --bg-base: var(--gray-13);
  --bg-elevated: var(--gray-12);
  --bg-hover: var(--gray-11);
  --border: var(--gray-10);
  --border-strong: var(--gray-7);
  --hairline: rgba(255, 255, 255, 0.05);
  /* Panel-edge treatment: a lightened tint of the panels' own bg-elevated
     fill (same "highlight = lighter shade of own color" rule §1.2 uses for
     buttons) on each panel's outer edge, vs. a plain dark shadow where a
     side panel visually stacks in front of a top/bottom one. */
  --panel-edge-highlight: #2E2E33;
  --panel-edge-shadow: rgba(0, 0, 0, 0.4);
  --panel-edge-shadow-solid: color-mix(in srgb, #000 40%, var(--bg-elevated)); /* the same shade laid over a panel, for a shape that sits over whatever is behind it */
  --accent: #BE1425; /* sampled from spec/stagwood.png, dominant red pixel */
  --accent-shadow: #7A0C18;
  --accent-light: #CE4F5C; /* accent mixed 25% toward white, for versatility */
  --accent-dark: #8E0F1C; /* accent mixed 25% toward black, for versatility */
  --text-primary: var(--gray-1);
  --text-secondary: var(--gray-4);
  --text-muted: var(--gray-5);
  --text-tertiary: var(--gray-6); /* between the secondary text and the faint controls: canvas names at rest */
  --text-faint: var(--gray-7); /* barely there: controls that should not compete with the content */

  --checker-light: #DEDEDE;
  --checker-dark: #CFCFCF;

  /* Stagwood Sprite 64 draws each of its pixels 1/16 em wide (unitsPerEm 1024, 64 units per
     pixel; a capital is 6 pixels tall), so it is only crisp when font-size is
     a whole multiple of 16 device pixels: 16, 32, 48. Any other size, 18 and
     12 included, resamples every glyph and blurs it. pixel-snap.js keeps
     these on that grid whatever the display's pixel ratio. */
  --font-body: 16px;
  --font-header: 32px;
  /* One pixel of an icon (icons.js): two font pixels, always a whole number of device
     pixels and even, so an icon centres exactly. Key icons in a line of text use one
     font pixel instead (.icon--inline), which makes them as tall as a capital. */
  --icon-px: calc(var(--font-body) / 8);

  --unit: 4px; /* small, incidental spacing: tooltips, the Controls modal. Not the grid unit below. */

  /* The grid system, in real CSS `em`: one number (--font-body) scales
     every block, panel and padding in lockstep, literally, not a
     compounding `calc()` chain. Every block-sized element carries its
     own clearance, so gaps between elements come from their own size,
     never from extra margin/padding tacked on around them (panels/rows
     themselves carry zero padding). This only works because every grid
     element shares the same font-size (--font-body, inherited from
     body): an element set in a smaller context (the Controls modal, a
     tooltip) is deliberately outside the grid and never uses these.

     CSS `em` = font-size, not glyph width: Stagwood Sprite 64's capitals are only
     0.375em tall, so don't eyeball the block size against letter
     width. A 1-block (1.5em) row is built from the text's own line
     (line-height:1, exactly 1em) plus 0.25em of clearance top and
     bottom: --pad is that clearance, sized so pad + line + pad lands
     exactly on --block, not a fraction chosen by feel. */
  --block: 1.5em;
  --meter-h: 4px; /* project capacity meter; the collab button shrinks by this */
  --pad: 0.25em;
  --panel-width: 9em; /* every side panel is 6 blocks wide */

  /* Every icon in the app is a plain Unicode character, not a drawn
     shape: but the pixel font doesn't define most of these
     glyphs, so each one was silently falling back to whatever font the
     browser happened to substitute per-character, at inconsistent
     metrics. Force one explicit fallback stack so every icon glyph
     renders from the same font, at the same size, everywhere. */
  --icon-font: 'Segoe UI Symbol', 'Noto Sans Symbols', 'Apple Symbols', sans-serif;

  /* ---- UI kit tokens: every literal below the :root block comes from here ---- */

  /* Colour scrims and tints */
  --accent-a75: rgba(190, 20, 37, 0.75);
  --black: #000000;
  --black-a0: rgba(0, 0, 0, 0);
  --black-a03: rgba(0, 0, 0, 0.03);
  --black-a10: rgba(0, 0, 0, 0.1);
  --black-a30: rgba(0, 0, 0, 0.3);
  --black-a35: rgba(0, 0, 0, 0.35);
  --black-a50: rgba(0, 0, 0, 0.5);
  --black-a60: rgba(0, 0, 0, 0.6);
  --chip-default: #444;
  --white-a20: rgba(255, 255, 255, 0.2);

  /* Strokes and offsets */
  --border-width: 1px;
  --outline-offset: -2px;
  --stroke-strong: 2px;

  /* Spacing scale (px) */
  --space-1: 1px;
  --space-12: 12px;
  --space-16: 16px;
  --space-2: 2px;
  --space-24: 24px;
  --space-3: 3px;
  --space-32: 32px;
  --space-4: 4px;
  --space-5: 5px;
  --space-6: 6px;
  --space-8: 8px;

  /* Component dimensions */
  --edge-trigger: 10px;
  --export-bar-w: 96px;
  --help-key-w: 260px;
  --help-w: 620px;
  --modal-max-h: 80vh;
  --modal-max-w: 90vw;
  --modal-w: 420px;

  /* Type */
  --font-small: 16px; /* the font has no crisp size below 16 */
  --font-title: 32px;

  /* Opacity */
  --opacity-fading: 0.6;
  --opacity-ghost: 0.9;
  --opacity-muted: 0.4;
  --opacity-soft: 0.8;

  /* Motion */
  --dur-base: 150ms;
  --dur-fast: 120ms;
  --dur-instant: 80ms;
  --dur-quick: 100ms;
  --dur-slow: 180ms;

  /* Layering */
  --z-base: 0;
  --z-label: 20;
  --z-modal: 2000;
  --z-tag: 2001; /* the corner tags stay above a modal's backdrop */
  --z-panel: 10;
  --z-preview: 3000;
  --z-cursor: 4000;
  --z-raised: 1;
  --z-slideout: 1000;
  --z-tooltip: 30;
  --z-focus: 100; /* above any child of a panel */
  --z-trigger: 5;

  /* Composite effects */
  --bevel-chip: inset 0 1px 0 0 var(--white-a20), inset -1px 0 0 0 var(--black-a30), inset 0 -5px 0 0 var(--black-a35);
  --bevel-chip-pressed: inset 0 1px 0 0 var(--white-a20), inset -1px 0 0 0 var(--black-a30), inset 0 -7px 0 0 var(--black-a35);
  --edge-highlight-top: inset 0 1px 0 var(--panel-edge-highlight);
  --edge-shadow-above: 0 calc(var(--border-width) * -1) 0 var(--black-a03); /* a hairline just outside a panel's top edge */
  --edge-shadow-below: 0 var(--border-width) 0 var(--black-a03); /* the same hairline just outside a panel's bottom edge */
  --shadow-drag: 0 4px 12px var(--black-a50);
}

* { box-sizing: border-box; }

/* ---- Button (universal interactive control) ---------------------------
   Flat fill with a 1px hairline highlight on the top/left edge and a
   hairline shadow on the bottom/right edge: depth and separation from
   two thin lines, not a chunky raised face + drop-shadow layer. Every
   clickable control in the app (icon button, text button, tab, toggle)
   is a `.btn`, so hover/active/focus/selected states live in one place
   instead of being reimplemented per feature. */
/* Inline rename (ui.js's startInlineEdit) toggles contenteditable on the
   name label in place: no swapped-in <input>, so only the browser's own
   default focus outline needs suppressing to keep the caret without the
   box-around-text "form field" look. */
[contenteditable="true"] { outline: none; }

.btn {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--block);
  padding: 0;
  margin: 0;
  background: var(--bg-elevated);
  border-style: solid;
  border-width: var(--border-width);
  border-color: var(--panel-edge-highlight) var(--panel-edge-shadow) var(--panel-edge-shadow) var(--panel-edge-highlight);
  color: var(--text-primary);
  font: inherit;
  font-size: var(--font-body);
  --cursor: click;
  white-space: nowrap;
}
.btn:hover { background: var(--bg-hover); }
.btn:active { background: var(--bg-base); }
.btn:focus-visible { outline: var(--stroke-strong) solid var(--accent); outline-offset: var(--outline-offset); z-index: var(--z-raised); }
.btn.selected, .btn.active {
  background: var(--accent);
  border-color: var(--accent-light) var(--accent-shadow) var(--accent-shadow) var(--accent-light);
}
.btn.selected:hover, .btn.active:hover { background: var(--accent-dark); }
.btn:disabled { opacity: var(--opacity-muted); pointer-events: none; }

/* Square, icon-font glyph, no text padding: hamburgers,
   the +/resize/delete buttons. */
.btn--icon { width: var(--block); flex: 0 0 var(--block); padding: 0; font-family: var(--icon-font); }
/* Fill button (design system: Button/Tile/Tile-bar/Panel): fills the full
   width of whatever it sits in, rather than the fixed square/icon size
   every other button variant uses. A column stack (slide-out list, preset
   panel) just wants that; a horizontal `.tile-bar` additionally wants it
   to *share* the row equally with its siblings, which needs `flex` on the
   main (horizontal) axis specifically, not `width`: the two are set
   separately below so a fill button in a vertical stack never stretches
   its height to match (flex:1 on a column's main axis means height, not
   width). `button({ fill: true })` in ui.js.
   New File/New Layer are the canonical fill buttons. */
.btn--fill { width: 100%; min-width: calc(var(--block) * 2); border-left-width: 0; border-right-width: 0; }
.tile-bar > .btn--fill { flex: 1 1 0; }

/* Hidden until its container is hovered, then slides in from the edge
   it's flush against: the one mechanism for every "only show on hover"
   button (file resize/export, the project panel's New/Open). */
.btn--reveal { flex: 0 0 0; width: 0; border-width: 0; overflow: hidden; transition: flex-basis var(--dur-fast) ease, width var(--dur-fast) ease; }
.reveal-on-hover:hover .btn--reveal { flex-basis: var(--block); width: var(--block); border-width: var(--border-width); }

/* ---- Tile (row-style composite item: file row, layer row) ------------
   Same depth language as `.btn` but sized for content (thumbnail, label,
   controls) rather than a single glyph. */
.tile {
  position: relative;
  display: flex;
  align-items: stretch;
  height: var(--block);
  background: var(--bg-elevated);
  --cursor: click;
  box-shadow: inset 0 calc(var(--border-width) * -1) 0 var(--hairline);
}
.tile:hover { background: var(--bg-hover); }
/* A selected row has no edge lines at all: a line left on the accent fill reads
   as a red stripe under or over the row's controls. (Edge lines here are inset
   shadows, not borders, so they take no space: an edge on one side only would
   pull the text half a pixel off centre.) */
.tile.selected, .tile.active { background: var(--accent); color: var(--text-primary); box-shadow: none; border-top-width: 0; border-bottom-width: 0; }
.tile--tall { height: calc(var(--block) * 2); }
/* A button in a tall row (layers) fills its height instead of staying one block. */
.tile--tall > .btn { height: auto; }
/* Pointer-based reorder feedback (see makeReorderable in ui.js): the row
   being dragged fades in place (the floating .drag-ghost is what actually
   tracks the cursor), the rows slide to open the gap, and dragging past the
   panel's edge marks it for removal with the remove icon: the same "dragged off = remove" cue the palette
   chips already use. */
.tile.dragging { opacity: var(--opacity-muted); }
.tile.removing { opacity: 1; }

.drag-ghost {
  z-index: var(--z-modal);
  pointer-events: none;
  opacity: var(--opacity-ghost);
  box-shadow: var(--shadow-drag);
}
.drag-ghost.removing { opacity: var(--opacity-fading); }

/* ---- Tile bar (horizontal strip of buttons/tiles) --------------------- */
.tile-bar { display: flex; align-items: stretch; }

/* ---- Panel (floating or docked control surface) ------------------------
   Shared surface treatment for every popup/slide-out/docked panel: same
   hairline highlight/shadow edge as `.btn`, at panel scale. */
.panel {
  background: var(--bg-elevated);
  border-style: solid;
  border-width: var(--border-width);
  border-color: var(--panel-edge-highlight) var(--panel-edge-shadow) var(--panel-edge-shadow) var(--panel-edge-highlight);
}

/* Hard rule: no visible scrollbars anywhere, ever: scrollable elements
   stay scrollable (wheel/touch/drag), the bar itself just never renders. */
* {
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* old Edge */
}
*::-webkit-scrollbar { display: none; } /* Chromium/WebKit */

/* Each element's cursor is named by `--cursor` (inherited); inverted-cursor.js draws it
   in place of the OS cursor, which is hidden once that script has started. */
html { --cursor: arrow; }
/* Names rename in place, but only their text is editable (inverted-cursor.js). */
.file-row-name, .layer-label, .project-name { --cursor: text-on-text; }
input, textarea, [contenteditable="true"] { --cursor: text; }
.reference-section .layer-label { --cursor: click; } /* reference names are not renamed in place */
.custom-cursor, .custom-cursor * { cursor: none !important; }
html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: var(--bg-elevated);
  color: var(--text-primary);
  font-family: 'Stagwood Sprite 64', monospace;
  font-size: var(--font-body);
  line-height: 1;
  -webkit-font-smoothing: none;
  font-smooth: never;
  text-rendering: optimizeSpeed;
  user-select: none;
}

/* Corner radius: 0px everywhere, no exceptions (§1). */
* { border-radius: 0; }

h1, h2, .header-text {
  text-transform: uppercase;
  font-size: var(--font-header);
}

#app {
  position: relative;
  height: 100%;
  width: 100%;
}

/* The canvas always fills the entire app, edge to edge, regardless of which
   panels are open: panels are overlays drawn on top of it, never flex
   siblings that resize it. This is what keeps the canvas centered under
   panel show/hide instead of shifting/squishing it. */
#canvas-area {
  position: absolute;
  inset: 0;
  z-index: var(--z-base);
}

/* Above every panel so it can blend with the whole page, but inverted-cursor.js
   only shows it while the pointer is over the canvas itself. */
.inverted-cursor {
  position: fixed;
  left: 0;
  top: 0;
  z-index: var(--z-cursor);
  pointer-events: none;
  mix-blend-mode: difference;
  will-change: transform;
}
.inverted-cursor[hidden] { display: none; }

#sprite-canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* Collab (§ collab plan, phase 1): one box per remote participant's cursor,
   positioned over the canvas in canvas-pixel space (main.js's
   updateRemoteCursorOverlay computes left/top/width/height per frame). */
.collab-cursor {
  position: absolute;
  z-index: var(--z-trigger);
  pointer-events: none;
  outline: 2px solid var(--collab-color, #fff);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6);
}
.collab-cursor > span {
  position: absolute;
  left: 100%;
  top: 100%;
  padding: 0 4px;
  white-space: nowrap;
  color: #fff;
  background: rgba(0, 0, 0, 0.7);
}
/* Collab button: reads "Collab" until a session is up, then that fades out and three user icons (host, guest 1, guest 2)
   fade in evenly spaced, dim until that slot is filled, then lit in the person's colour with their name beside them. It
   sits in the slot the meter leaves under the new canvas button, and its sides leave half of what a block has around one
   pixel column: the room the fold arrow keeps to its right too. */
.collab-btn { justify-content: flex-start; padding: 0 calc((var(--block) - var(--icon-px)) / 2); height: calc(var(--block) - var(--meter-h)); }
.collab-label, .collab-slots { transition: opacity var(--dur-base) ease; }
.collab-label { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; }
.collab-slots { display: flex; width: 100%; min-width: 0; opacity: 0; }
.collab-btn.collab-on .collab-label { opacity: 0; }
.collab-btn.collab-on .collab-slots { opacity: 1; }
.collab-slot { flex: 1 1 0; min-width: 0; display: flex; align-items: center; gap: var(--space-4); pointer-events: auto; }
.collab-name { flex: 1 1 0; min-width: 0; overflow: hidden; white-space: nowrap; text-align: left; }
/* Slots must beat the generic button-icon colours (rest, hover, selected) above. The outline keeps a red host visible on the solid red live button and a black guest 2 visible on the dark one. */
.btn.collab-btn .collab-slot.present .icon {
  background: var(--collab-color);
  filter: drop-shadow(var(--icon-px) 0 0 var(--text-faint)) drop-shadow(calc(var(--icon-px) * -1) 0 0 var(--text-faint)) drop-shadow(0 var(--icon-px) 0 var(--text-faint)) drop-shadow(0 calc(var(--icon-px) * -1) 0 var(--text-faint));
}

/* Invisible edge-hover zones that reveal a panel (§15). */
.edge-trigger { position: absolute; z-index: var(--z-trigger); }
.edge-trigger-left { left: 0; top: 0; bottom: 0; width: var(--edge-trigger); }
.edge-trigger-right { right: 0; top: 0; bottom: 0; width: var(--edge-trigger); }
.edge-trigger-top { left: 0; right: 0; top: 0; height: var(--edge-trigger); }
.edge-trigger-bottom { left: 0; right: 0; bottom: 0; height: var(--edge-trigger); }

/* Collection/group view (§ project panel group select) is a read-only
   overview of other files' already-composited pixels: no colors, layers,
   or frames of its own to show, so all three panels and their reveal
   triggers are removed outright rather than left open-but-pointless.
   main.js toggles this class via setActiveGroup(). */
body.group-view #timeline-trigger, body.group-view #timeline-bar,
body.group-view #layers-trigger, body.group-view #layers-panel,
body.group-view #palette-trigger, body.group-view #palette-bar {
  display: none !important;
}

/* Panels overlay the canvas (never in flow), each anchored to its edge and
   sliding by collapsing width/height with a transition. */
.panel-overlay {
  position: absolute;
  z-index: var(--z-panel);
  background: var(--bg-elevated);
  overflow: hidden;
}
.panel-overlay-left { left: 0; }
.panel-overlay-right { right: 0; }
.panel-overlay-top { top: 0; left: 0; right: 0; }
.panel-overlay-bottom { bottom: 0; left: 0; right: 0; }

/* Side panels run the full viewport height: Timeline and Palette are the
   ones that shrink horizontally (below) to clear whichever is open, so
   nothing actually overlaps despite that. */
.side-panel {
  width: var(--panel-width);
  top: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  transition: width var(--dur-slow) ease;
}
.side-panel.panel-hidden { width: 0; }
/* A panel on the left collapses toward the screen edge like the right-hand one: its content keeps its
   full width and is pinned to the panel's moving (right) edge, so it travels off the page with it instead
   of staying put while the box wipes shut over it. */
.panel-overlay-left.side-panel { align-items: flex-end; }
.panel-overlay-left.side-panel > * { width: calc(var(--panel-width) - var(--border-width)); }
/* Outer-edge highlight: a lightened tint of the panel's own fill,
   separating it from the canvas/viewport behind it. Only while visible:
   border-left/right renders full-height regardless of width, so a
   collapsed (width:0) panel would otherwise still show as a hairline. */
.panel-overlay-left.side-panel:not(.panel-hidden) { border-left: var(--border-width) solid var(--panel-edge-highlight); }
.panel-overlay-right.side-panel:not(.panel-hidden) { border-right: var(--border-width) solid var(--panel-edge-highlight); }

/* ---- Timeline (top edge, shrinks around open side panels) ------------ */
#timeline-bar {
  height: calc(var(--block) * 2);
  left: var(--push-left, 0);
  right: var(--push-right, 0);
  transition: height var(--dur-slow) ease, left var(--dur-slow) ease, right var(--dur-slow) ease,
    border-left var(--dur-slow) ease, border-right var(--dur-slow) ease;
  display: flex;
  align-items: stretch;
}
/* Border only while actually visible: border-top has height 0 has no
   height dependency, so a collapsed (height:0) panel would otherwise
   still render as a persistent hairline instead of fully disappearing. */
#timeline-bar:not(.panel-hidden) { border-top: var(--border-width) solid var(--panel-edge-highlight); }
/* The palette bar's hairline, mirrored under the timeline. */
#timeline-bar:not(.panel-hidden) { box-shadow: var(--edge-shadow-below); }
#timeline-bar.panel-hidden { height: 0; }

/* ---- Palette bar (bottom edge, shrinks around open side panels) ------ */
#palette-bar {
  height: var(--block); /* matches the chip row (§7.2): needed so height can transition */
  left: var(--push-left, 0);
  right: var(--push-right, 0);
  transition: height var(--dur-slow) ease, left var(--dur-slow) ease, right var(--dur-slow) ease,
    border-left var(--dur-slow) ease, border-right var(--dur-slow) ease;
  display: flex;
  align-items: stretch;
}
#palette-bar:not(.panel-hidden) { border-bottom: var(--border-width) solid var(--panel-edge-highlight); }
/* A dark hairline outside the top highlight, so the bar stays separate from the
   canvas when a chip colour is close to the canvas colour. An outer shadow, so
   the chips inside can't cover it. */
#palette-bar:not(.panel-hidden) { box-shadow: var(--edge-shadow-above); }
#palette-bar.panel-hidden { height: 0; }

/* Always as far right/down as it can be: flush with the window's bottom
   edge when the palette is closed, only pushed up above it when the
   palette is actually there: same idea for the layers panel and
   --push-right. main.js keeps both custom properties in sync. */
#help-tag {
  position: absolute;
  right: var(--push-right, 0);
  bottom: var(--push-bottom, 0);
  z-index: var(--z-tag);
  /* One block in all, so it stays on the grid: the bottom border and a 1px row
     of top padding (which the highlight overlay below draws over) come out of
     it, and the content, stretched to what is left, is centred between the two
     lines. */
  height: var(--block);
  padding-top: var(--border-width); /* the 1px highlight row, so the content is centred between it and the bottom border */
  display: flex;
  align-items: stretch;
  transition: right var(--dur-slow) ease, bottom var(--dur-slow) ease;
  /* Top edge sits 1px in from the corner instead of flush with it: a
     plain `border-top` (from `.panel`) reads flush against the canvas
     above; this pulls the hairline itself down a px while keeping the
     same color/weight. */
  border-top: none;
}
/* The top highlight is drawn by an overlay, not an inset box-shadow: a
   shadow paints beneath the tag's children, so any solid child (the swatch,
   a hovered link) covered it. The overlay sits above them and ignores the
   pointer. Both corner tags are absolutely positioned, so it anchors to them. */
#help-tag::after, #tool-tag::after {
  content: '';
  position: absolute;
  inset: 0;
  box-shadow: var(--edge-highlight-top);
  pointer-events: none;
}
#help-tag.hidden-tag { display: none; }
/* Hold-`+Left/Right keyboard focus (main.js): same treatment a real
   :focus-visible ring would give, just driven by our own held-key state
   instead of actual DOM focus. */
.help-nav-focused { outline: var(--stroke-strong) solid var(--accent); outline-offset: var(--outline-offset); }
/* The report-bug/Discord/help buttons are plain `.btn`s, whose universal
   height is a full block: but this tag is a `.panel` with its own 1px
   border eating into that space, so a full-block-tall child overflows
   past it. 100% always matches whatever's actually left. */

/* Static (non-interactive) icon-sized glyph: e.g. the zoom icon next to
   its readout, which isn't itself clickable. Real icon buttons use
   `.btn.btn--icon` instead, for the hover/focus/border treatment. A
   plain full block, same as every other icon in the app: the tag
   itself (not this) accounts for its own border, see #help-tag. */
.tag-icon {
  width: var(--block);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--icon-font);
  font-size: var(--font-body);
  color: var(--text-secondary);
}

/* Tool reference tag: same idea as #help-tag, mirrored to the bottom
   left: hugs the corner, pushed right by the Project panel instead of
   left by the Layers panel. "`" toggles both tags at once. */
#tool-tag {
  position: absolute;
  left: var(--push-left, 0);
  bottom: var(--push-bottom, 0);
  z-index: var(--z-tag);
  height: var(--block); /* see #help-tag */
  padding-top: var(--border-width);
  display: flex;
  align-items: stretch;
  transition: left var(--dur-slow) ease, bottom var(--dur-slow) ease;
  /* .panel's generic button-bevel puts the highlight on top/left and the
     shadow on right/bottom: but this tag hugs the viewport's LEFT edge, so
     its canvas-facing edge is the right one, which needs a light hairline
     instead. `--hairline` (not the bolder `--panel-edge-highlight` used for
     a real bevel edge) keeps it subtle: this edge doesn't sit against an
     open/closed panel the way a side panel's does. */
  border-right-color: var(--hairline);
  /* Top edge 1px in from the corner: see #help-tag. */
  border-top: none;
}
#tool-tag.hidden-tag { display: none; }
/* While the tag eases to a new width (tag-motion.js's slideWidth): what the shrinking width cuts off stays hidden. */
#tool-tag.sliding { overflow: hidden; transition: left var(--dur-slow) ease, bottom var(--dur-slow) ease, width var(--dur-base) ease; }
.tool-tag-label {
  display: flex;
  align-items: center;
  padding: var(--space-2) var(--space-8);
  font-size: var(--font-body);
  color: var(--text-secondary);
  white-space: nowrap;
  /* Text fades out and in on a change (tag-motion.js's fadeText) and a warning's red fades in and out. */
  transition: opacity var(--dur-fast) ease, background-color var(--dur-base) ease;
}
/* The plain class rule's own `display: flex` above would otherwise beat
   the `hidden` attribute's default UA style (same-origin author rules
   always win over the UA stylesheet, regardless of specificity or source
   order): this compound selector outranks it. Used by the group grid's
   tip section (main.js's updateToolTag) when there's nothing to show. */
.tool-tag-label[hidden] { display: none; }
/* Divider between the zoom % and the tool info: not between the zoom
   icon and its own %, which reads as one unit. */
/* Flush against the zoom icon immediately to its left: that icon already
   supplies its own clearance (it's a full block, centered glyph), so no
   left padding is added here on top of it (same "padding isn't
   additive" rule the grid uses everywhere else). */
.tool-tag-label--divider { padding-left: 0; border-right: var(--border-width) solid var(--hairline); }
.tool-tag-swatch {
  width: var(--block);
  border-left: var(--border-width) solid var(--hairline);
}
/* Export progress (main.js's onExportProgress): takes the tool label's
   own slot while an export is running, same padding so nothing shifts. A
   plain track (the outer box, border+background) with the fill as an
   absolutely-positioned child spanning it left-to-right by %. */
.tool-tag-export {
  display: flex;
  align-items: center;
  padding: 0 var(--space-8);
  width: var(--export-bar-w);
}
.tool-tag-export[hidden] { display: none; } /* same override `.tool-tag-label[hidden]` needs, see its own comment */
.tool-tag-export-track {
  position: relative;
  width: 100%;
  height: var(--space-6);
  background: var(--bg-base);
  border: var(--border-width) solid var(--hairline);
}
.tool-tag-export-fill {
  position: absolute;
  inset: 0;
  width: 0%;
  background: var(--accent);
  transition: width var(--dur-fast) ease;
}
/* Warnings and errors the user needs to see (main.js's onAlert and export
   failure marker): white on red. `alert-pulse` blinks the text for a few
   seconds first, for the urgent ones. */
.tool-tag-alert {
  background: var(--accent);
  color: var(--text-primary);
}
.alert-pulse { animation: alert-pulse 0.5s ease-in-out 6; }
@keyframes alert-pulse { 50% { color: transparent; } }
/* Lingering export-failure marker (main.js's onExportProgress): same slot
   sizing as .tool-tag-label, plus a click affordance. */
.tool-tag-export-error {
  --cursor: click;
}
.tool-tag-export-error[hidden] { display: none; }

/* Where an open side panel visually stacks in front of the Timeline/
   Palette (their shrunk edge butts against it): a light line on the seam, then
   a shadow line just inside it, so the two panels read as separate surfaces
   and the side panel as the one on top. */
#timeline-bar.pushed-left, #palette-bar.pushed-left { border-left: var(--border-width) solid var(--panel-edge-highlight); }
#timeline-bar.pushed-right, #palette-bar.pushed-right { border-right: var(--border-width) solid var(--panel-edge-highlight); }
/* The shadow line just inside the light seam is one overlay across both edges,
   on ::before: ::after belongs to the focus ring (.kb-focused). */
#timeline-bar::before, #palette-bar::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: var(--z-trigger);
  background:
    linear-gradient(var(--seam-l, transparent), var(--seam-l, transparent)) left / var(--border-width) 100% no-repeat,
    linear-gradient(var(--seam-r, transparent), var(--seam-r, transparent)) right / var(--border-width) 100% no-repeat;
}
#timeline-bar.pushed-left, #palette-bar.pushed-left { --seam-l: var(--panel-edge-shadow); }
#timeline-bar.pushed-right, #palette-bar.pushed-right { --seam-r: var(--panel-edge-shadow); }
/* The palette's leftmost control is the hamburger button, which brings its own
   light edge: a light seam line would double up, so the seam is just the shadow. */
#palette-bar.pushed-left { border-left-color: var(--panel-edge-shadow); --seam-l: transparent; }

.fps-field {
  /* One block wide as before; --block is in em and this field's font is larger,
     so it is spelled out from the body size. */
  flex: 0 0 calc(var(--font-body) * 1.5);
  min-width: 0;
  height: 100%;
  display: flex;
  justify-content: center;
  /* Placed from the top, not centred: the panel is two blocks less its 1px top border, so
     centring the line puts the baseline on a half pixel and blurs it. 7 of 16 to the em of
     the body font is 7 whole font pixels, which (the line being 1em tall, its 10-pixel
     glyph box centred in it) puts the capitals in the middle of the panel. */
  padding-top: calc(var(--font-body) * 7 / 16);
  background: var(--bg-elevated);
  /* An inset shadow, not a border: a border takes a pixel off the width the number is centred in. */
  box-shadow: inset calc(var(--border-width) * -1) 0 0 var(--hairline);
  color: var(--text-primary);
  font-size: var(--font-header);
  --cursor: drag-vertical; /* a vertical drag changes it (timeline-panel.js) */
}
.fps-field[contenteditable="true"] { --cursor: text; outline: none; }

/* Onion-skin toggle stacked over the add-frame button, 1 block each:
   together they match the 2-block timeline panel height. */
.timeline-stack { display: flex; flex-direction: column; flex: 0 0 var(--block); width: var(--block); }

.onion-toggle { color: var(--text-secondary); }

.frame-strip {
  flex: 1 1 auto;
  display: flex;
  align-items: stretch;
  overflow-x: auto;
  position: relative;
}

.frame-tile {
  position: relative;
  flex: 0 0 auto;
  height: calc(var(--block) * 2);
  align-self: center;
  padding-right: var(--border-width); /* a hairline of room to the right of every frame */
  border-top: var(--border-width) solid var(--black-a30); /* shares the layer-thumbnail hairline (§6) */
  --cursor: click;
}
/* Native drag-and-drop reorder (ui.js's attachNativeDragReorder): same
   dim-while-dragging convention as `.tile.dragging`/`.chip.dragging`. */
.frame-tile.dragging { opacity: var(--opacity-muted); }
/* Dragged off the timeline: removed on release. The tile's ::after is its active marker, so the red is its own fill showing through a faded thumbnail. */
.frame-tile.removing { opacity: 1; background: var(--accent); }
.frame-tile.removing canvas { opacity: var(--opacity-muted); }
.frame-tile::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--space-8) * -1);
  height: var(--space-5);
  background: var(--border-strong);
}
.frame-tile.active::after { background: var(--accent); }
/* T+Shift+Left/Right multi-frame select: a red outline around every
   selected tile, distinct from the underline that already marks the single
   active frame. */
.frame-tile--selected { outline: var(--stroke-strong) solid var(--accent); outline-offset: var(--outline-offset); }
/* Layers-panel Shift+Up/Down multi-select: same red outline convention. */
.layer-row--selected { outline: var(--stroke-strong) solid var(--accent); outline-offset: var(--outline-offset); }
/* Focus-based control scheme: Ctrl+Arrow (or Tab-cycle) ring on whichever panel
   currently owns the keyboard (main.js's syncFocusRing). */
/* An overlay, not an outline: a browser paints an element's outline beneath its
   positioned children (every row and button here), so the panel's content
   covered it. z-index only has to beat the panel's own children. */
.kb-focused::after {
  content: '';
  position: absolute;
  inset: 0;
  border: var(--stroke-strong) solid var(--accent); /* a border, not a shadow: borders snap to whole device pixels */
  pointer-events: none;
  z-index: var(--z-focus);
}
.frame-tile canvas { display: block; height: calc(var(--block) * 2); width: auto; image-rendering: pixelated; }

.frame-insert-gap {
  flex: 0 0 0;
  position: relative;
  width: 0;
  transition: width var(--dur-quick) ease;
}
.frame-insert-gap:hover { width: var(--space-24); }
.frame-insert-btn {
  font-family: var(--icon-font);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  background: var(--bg-hover);
  color: var(--text-primary);
  border: none;
  --cursor: click;
}
.frame-insert-gap:hover .frame-insert-btn { opacity: 1; }

/* ---- Palette chip (§1.3, §7.2) -------------------------------------- */
.chip {
  position: relative;
  flex: 1 1 0;
  min-width: var(--space-32);
  height: var(--block);
  --cursor: click;
  background: var(--chip-color, var(--chip-default));
  /* Every edge cue (right-edge hairline between chips, top highlight,
     bottom shadow) is an inset box-shadow, not a border: chips have
     fractional widths (they split the bar evenly, flex:1 1 0, not whole
     pixels), and a `border` and a `box-shadow` don't always snap to the
     same sub-pixel boundary on a fractional box: a border-right visibly
     failed to reach the full height on some chips because of this exact
     mismatch, reading as if the hairline "stopped short." One paint
     layer for the whole outline, so every edge rounds together. */
  box-shadow:
    var(--bevel-chip);
  transition: transform var(--dur-instant) ease, box-shadow var(--dur-instant) ease;
}

.chip:hover, .chip.pressed { filter: brightness(1.25); }
/* Lifts up, not down (§1.3): shadow deepens to match. Keeps the same
   top-highlight/right-hairline layers as the base rule; only the
   bottom-shadow layer's depth changes. */
.chip.pressed {
  transform: translateY(calc(var(--space-2) * -1));
  box-shadow:
    var(--bevel-chip-pressed);
}
.chip.dragging { opacity: var(--opacity-muted); }
/* Dragged past the palette's edge: about to be dropped/removed. */
.chip.removing { opacity: 1; }
/* Dragged off the panel: a red wash with the remove icon over it. */
.tile.removing::after, .chip.removing::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: var(--z-raised);
  background: var(--accent-a75); /* var(--accent) at partial opacity */
}
.tile.removing::before, .chip.removing::before, .frame-tile.removing::before {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  z-index: calc(var(--z-raised) + 1);
  width: calc(var(--icon-px) * 6);
  height: calc(var(--icon-px) * 6);
  background: var(--text-primary);
  -webkit-mask: url('icons/remove.svg') center / 100% 100% no-repeat;
  mask: url('icons/remove.svg') center / 100% 100% no-repeat;
}

/* No primary/secondary "in use" indicator (§1.3): deliberately deferred,
   chips stay visually identical regardless of primary/secondary status. */

/* Hamburger: opens the palette-preset slide-out (right of the chip row). */
.palette-hamburger { flex: 0 0 var(--block); }

/* Fixed clipping window: chips never spill past the bar's edge
   horizontally. Native overflow-x scrolling is deliberately not used:
   palette.js hand-rolls wheel scrolling via a transform on .chip-row so it
   can enforce the fixed-slot-width/peek-chip behavior instead of free
   native scroll. Vertical overflow stays visible so the hex label can pop
   up above a chip without being clipped by this box. */
.chip-viewport {
  display: flex;
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: hidden;
  overflow-y: visible;
}

.chip-row {
  display: flex;
  align-items: stretch;
}

/* Hex code, revealed above the chip on hover: click opens the color
   picker. A button (not inert text) so it's directly clickable. */
.chip-hex-label {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-bottom: var(--space-4);
  background: var(--bg-elevated);
  border: var(--border-width) solid var(--border-strong);
  color: var(--text-primary);
  font-family: inherit;
  font-size: var(--font-small);
  padding: var(--space-2) var(--space-4);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) ease;
  z-index: var(--z-label);
}
.chip:hover .chip-hex-label { opacity: 1; pointer-events: auto; }

/* Add-chip button: always visible at the bar's leading edge (§7.2's
   hover-reveal was a needless extra step: the "+" belongs with the rest
   of the row, not hidden). */
.chip-add { flex: 0 0 var(--block); color: var(--text-secondary); }

/* ---- Color picker popup (§7.2) --------------------------------------- */
.color-picker-popup { padding: 0; gap: 0; }

.picker-square { display: block; --cursor: crosshair; }

.picker-hue {
  width: 100%;
  height: var(--space-16);
  margin: 0; /* the default 2px side margin pushed the slider past the popup's right edge */
  -webkit-appearance: none;
  background: linear-gradient(to right, red, yellow, lime, cyan, blue, magenta, red);
  border: var(--border-width) solid var(--border);
}
.picker-hue::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: var(--space-6);
  height: var(--space-16);
  background: var(--text-primary);
  border: var(--border-width) solid var(--black);
}

.picker-hex-row {
  display: flex;
  align-items: center;
  padding-left: var(--space-4);
  background: var(--bg-base);
  border: var(--border-width) solid var(--border-strong);
  color: var(--text-secondary);
  font-size: var(--font-body);
}
.picker-hex-row .picker-hex { border: 0; outline: none; padding-left: 0; flex: 1; min-width: 0; }

.picker-hex {
  background: var(--bg-base);
  border: var(--border-width) solid var(--border-strong);
  color: var(--text-primary);
  font-family: inherit;
  font-size: var(--font-body);
  padding: var(--space-2) var(--space-4);
  width: 100%;
}

/* ---- Palette-preset slide-out (opened by the hamburger) -------------- */
.palette-preset-panel .btn { justify-content: flex-start; }
.palette-preset-panel .palette-rule { border-top: var(--border-width) solid var(--hairline); }
.palette-row { display: flex; }

/* ---- Project capacity meter: plain filled rectangle, no gradient ------ */
/* The collab button gives up the meter's height so the two together stay one
   block tall, keeping the panel's footer on the grid. */
/* The footer (add row, collab button, meter, header) keeps its size when the file list is longer than the panel: as flex items they would
   otherwise all give up a share of the overflow, and the new canvas button ended up a few pixels tall. */
#project-panel > :not(.file-list) { flex-shrink: 0; }
.project-add-row { height: var(--block); }
.project-add-row .btn { height: 100%; }
.capacity-meter { height: var(--meter-h); background: var(--accent-dark); border-top: var(--border-width) solid var(--hairline); }
/* min-width: a sliver is always lit, even at 0% load: purely for looks */
.capacity-fill { height: 100%; min-width: var(--space-3); background: var(--accent); }
.capacity-meter.full { --cursor: click; }

/* ---- New-File size popup: custom W x H row --------------------------- */
.size-popup { min-width: calc(var(--block) * 4); }
.size-row { display: flex; }
.size-row input::placeholder { color: var(--text-muted); }
/* No spin arrows: sizes are typed. */
.size-row input[type="number"] { appearance: textfield; -moz-appearance: textfield; }
.size-row input::-webkit-inner-spin-button, .size-row input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.sheet-popup { min-width: calc(var(--block) * 6); }
.sheet-fields { flex-wrap: wrap; }
.sheet-fields input { width: 50%; }
.size-row input {
  background: var(--bg-base);
  border: var(--border-width) solid var(--border-strong);
  color: var(--text-primary);
  font-family: inherit;
  font-size: var(--font-body);
  padding: var(--space-2) var(--space-4);
  width: 50%;
  min-width: 0;
}

/* ---- Trim and resize: the dimmed canvas, anchor pips, resize bar ------ */
/* The same backdrop as the help modal; the corner tags stay above it. Clicking it cancels. */
.canvas-dim {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  background: var(--black-a0);
  pointer-events: none;
  transition: background var(--dur-slow) ease;
}
.canvas-dim.visible { background: var(--black-a60); pointer-events: auto; }
/* Pips are placed on the canvas's corners, edges and centre by anchor-pips.js; a square each, centred on its point. */
.anchor-pips {
  position: fixed;
  inset: 0;
  z-index: var(--z-tag);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-slow) ease;
}
.anchor-pips.visible { opacity: 1; }
.anchor-pip {
  position: absolute;
  width: var(--block);
  height: var(--block);
  margin: calc(var(--block) / -2) 0 0 calc(var(--block) / -2);
  transition: width var(--dur-fast) ease, height var(--dur-fast) ease, margin var(--dur-fast) ease;
  padding: 0;
  background: var(--text-secondary);
  border: var(--border-width) solid var(--black);
  pointer-events: none;
  --cursor: click;
}
.anchor-pips.visible .anchor-pip { pointer-events: auto; }
.anchor-pips.measuring .anchor-pip { transition: none; }
.anchor-pip:hover { background: var(--text-primary); }
.anchor-pip.selected { background: var(--accent); }
/* The tool tag's resize section (resize-bar.js) and the preset sizes stacked over it. */
.resize-bar { gap: var(--space-4); }
.resize-bar[hidden] { display: none; }
.resize-bar input {
  background: var(--bg-base);
  border: var(--border-width) solid var(--border-strong);
  color: var(--text-primary);
  font-family: inherit;
  font-size: var(--font-body);
  padding: var(--space-2) var(--space-4);
  width: 6ch;
  min-width: 0;
  outline: none;
  appearance: textfield;
  -moz-appearance: textfield;
}
.resize-bar input::-webkit-inner-spin-button, .resize-bar input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.resize-bar input::placeholder { color: var(--text-muted); }
.resize-presets {
  position: fixed;
  z-index: var(--z-tag);
  display: flex;
  flex-direction: column;
  width: max-content; /* as wide as the largest option */
}
.resize-presets[hidden] { display: none; }
/* Left-aligned like the slide-out menus, with room to the left of the labels. */
.resize-presets .btn { padding: 0 var(--pad) 0 calc(var(--pad) * 2); }
.resize-presets .btn--fill { justify-content: flex-start; min-width: 0; }

/* ---- Project panel (§7) --------------------------------------------- */
.project-header {
  display: flex;
  align-items: stretch;
  height: var(--block);
  box-shadow: inset 0 var(--border-width) 0 var(--hairline); /* divider from the file list above it */
}
.project-name {
  flex: 1 1 auto;
  /* Literal 1em padding on top/bottom/right: not centered inside a taller
     block via flexbox (that leaves the actual gap up to the rendered
     line's own box, not a fixed value). Top/bottom pin the row to
     exactly 1 block tall (1em + 1 line + 1em, line-height:1 makes the
     line itself exactly 1em) by construction. Left gets a little extra:
     the panel carries no padding of its own, and the plain single `--pad`
     every other row uses read too tight for the project's own name. Right:
     sits flush against the New/Open buttons, hidden (0-width) until
     hovered, so there's no block there to supply clearance in the resting
     state. */
  padding: var(--pad) var(--pad) var(--pad) calc(var(--pad) * 2);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.panel-add-btn { font-family: var(--icon-font); }
.icon {
  display: block;
  flex: none;
  background: currentColor;
  -webkit-mask: center / 100% 100% no-repeat;
  mask: center / 100% 100% no-repeat;
  pointer-events: none;
}
/* Every icon you can act on is dim like the fold arrows: faint at rest, full on hover or focus,
   dark red when its button is switched on (the accent fill it sits on). */
.btn .icon { background: var(--text-faint); }
.btn:hover .icon, .btn:focus-visible .icon { background: var(--text-primary); }
.btn.selected .icon, .btn.active .icon { background: var(--accent-dark); }
.btn.selected:hover .icon, .btn.active:hover .icon { background: var(--text-primary); }
/* the heart sits a device pixel low in its block: lift it */
.icon--lift { transform: translateY(calc(var(--icon-px) / -2)); }
/* the app icon sits two device pixels low */
.icon--lift-2 { transform: translateY(calc(var(--icon-px) * -1)); }
/* the help tag's icons all sit a device pixel higher; `translate` adds to the lifts above rather than replacing them */
#help-tag .icon { translate: 0 calc(var(--icon-px) / -2); }
/* The menu icon is centred: its bars are 3 of the 6 pixels wide, so half the spare room moves it over. In a selected tile every button icon is dark red, like the handle. */
.icon--menu { translate: calc(var(--icon-px) * 1.5) 0; }
.tile.selected .btn .icon, .tile.active .btn .icon { background: var(--accent-dark); }
.tile.selected .btn:hover .icon, .tile.active .btn:hover .icon { background: var(--text-primary); }
.btn:hover .icon--heart { background: var(--accent); }
.icon--new { transform: translateY(calc(var(--icon-px) / -2)); } /* its top row is empty: lift it so the drawing centres */
/* A key icon inside a line of text: sits on the baseline, as tall as a capital. */
.icon--inline { display: inline-block; vertical-align: baseline; --icon-px: calc(var(--font-body) / 16); }

.file-list {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
}
.file-stack { margin-top: auto; }
.file-row { box-shadow: inset 0 calc(var(--border-width) * -1) 0 var(--black-a10); overflow: hidden; }
.layer-row--nested .layer-label { padding-left: calc(var(--pad) * 2); }
.file-row--nested .file-row-name { padding-left: calc(var(--pad) * 2 + var(--block) / 4); }

.file-row-name {
  flex: 1 1 auto;
  /* Literal padding, not flex-centered inside a taller row: see
     .project-name. A canvas inside a collection is indented further. */
  padding: var(--pad) var(--pad) var(--pad) calc(var(--pad) * 2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Hold-D color-pick preview: offset from the actual cursor/sample point so
   it never covers what's being picked. */
.d-pick-preview {
  position: fixed;
  z-index: var(--z-preview);
  width: var(--space-16);
  height: var(--space-16);
  border: var(--border-width) solid var(--black);
  pointer-events: none;
}

/* ---- Slide-Out Context Bar (CONTEXT.md vocabulary) -------------------
   The one mechanism for every secondary/contextual control surface that
   isn't a full docked panel: file rename/resize, the new-file size
   picker, palette presets: a stack of buttons sliding out from the
   element that triggered it, never a floating text dropdown. */
.slide-out-bar {
  position: fixed;
  z-index: var(--z-slideout);
  display: flex;
  flex-direction: column;
  transition: transform var(--dur-base) ease, opacity var(--dur-base) ease, width var(--dur-base) ease;
}
/* Same left/right inset every other row/label in the app gives its text
   (.file-row-name, .project-name, etc.): a fill button sized to a bare
   text width would otherwise read as edge-to-edge cramped. */
/* The pixel font ends every word with a blank column (one font pixel, 1/16 em) after the last glyph,
   so the right padding is that much smaller to leave the same room to the ink on both sides. */
.slide-out-bar .btn { padding: 0 calc(var(--pad) - 1em / 16) 0 var(--pad); }
/* Left-aligned, not centred: the bar is exactly as wide as its longest label plus the padding either
   side, so the shorter labels leave their spare room on the right. */
.slide-out-bar .btn--fill { justify-content: flex-start; }
/* `.btn--fill`'s own 2-block floor is for its other callers (export
   format/scale, "Add layer or group"): a slide-out menu is deliberately
   allowed to be as narrow as its longest label needs (slide-out.js's
   pinToContentWidth is what actually sets the width). */
.slide-out-bar .btn--fill { min-width: 0; }

/* Chevron pointing back at whichever tile/anchor opened the bar: flush
   against it (no gap/floating space), side set by positionSlideOut(). For
   'left'/'right' the bar itself is now pinned to the panel's outer edge
   (positionSlideOut), not the anchor, so the chevron's negative offset is
   what does the actual pointing: it overlaps back onto the panel by a few
   px, rendered above it (the bar's z-index applies to its children too),
   rather than floating in the gap between panel and bar. */
.slide-out-chevron {
  position: absolute;
  width: 0;
  height: 0;
  border: var(--space-6) solid transparent;
}
/* Colored to match whichever edge of the bar the chevron actually touches
   (`.panel`'s own bevel: highlight on top/left, shadow on bottom/right):
   not a generic gray, so it reads as that edge folding out to a point
   rather than an unrelated triangle floating in front of the panel. */
.slide-out-bar[data-side="right"] .slide-out-chevron { left: calc(var(--space-12) * -1); transform: translateY(-50%); border-right-color: var(--panel-edge-highlight); }
.slide-out-bar[data-side="left"] .slide-out-chevron { right: calc(var(--space-12) * -1); transform: translateY(-50%); border-left-color: var(--panel-edge-shadow-solid); }
/* The triangle is the near half of its 12px box, so the box goes fully past the bar's outer edge (padding edge + border) to sit flush against it. */
.slide-out-bar[data-side="up"] .slide-out-chevron { bottom: calc((var(--space-12) + var(--border-width)) * -1); transform: translateX(-50%); border-top-color: var(--panel-edge-shadow-solid); }
.slide-out-bar[data-side="down"] .slide-out-chevron { top: calc((var(--space-12) + var(--border-width)) * -1); transform: translateX(-50%); border-bottom-color: var(--panel-edge-highlight); }

/* Open Project (§ project panel "Open" menu item): a docked full-height side
   panel (like Project/Layers), not a floating popup: flush beside Project,
   never independently pinned, only ever opened alongside it, and pushes the
   tool tag over the same way Project itself does. */
#open-project-panel { left: var(--open-project-left, var(--panel-width)); }
/* Bottom-anchored (§ open-project-panel.js): title comes after the list in
   DOM order, so its divider needs to face the other way: a line above it
   (from the list it's now under), not below. */
#open-project-panel .side-panel-title { box-shadow: inset 0 var(--border-width) 0 var(--hairline); }
/* The docked side panel that slides out next to Project (Open Project)
   rather than being one of the four primary edge panels: a title row plus a
   scrollable body. */
.side-panel-title {
  /* Literal padding, not flex-centered: see .project-name. 1em top/bottom
     plus the 1em line gives exactly 1 block, so no explicit height needed. */
  padding: var(--pad);
  color: var(--text-secondary);
  text-transform: uppercase;
  box-shadow: inset 0 calc(var(--border-width) * -1) 0 var(--hairline);
}
/* No padding/gap of its own: the panel is flush, and every child here is
   already a block-sized (or block-row) element that carries its own
   clearance. */
.side-panel-body { flex: 1 1 auto; display: flex; flex-direction: column; overflow-y: auto; }

.open-project-empty { padding: var(--pad); color: var(--text-muted); }

/* ---- Keybind help modal ("?") ---------------------------------------- */
.keybind-help-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  background: var(--black-a0);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background var(--dur-slow) ease;
}
.keybind-help-overlay.visible { background: var(--black-a60); }
.keybind-help-overlay .keybind-help-panel {
  transform: translateY(var(--space-24));
  opacity: 0;
  transition: transform var(--dur-slow) ease, opacity var(--dur-slow) ease;
}
.keybind-help-overlay.visible .keybind-help-panel {
  transform: translateY(0);
  opacity: 1;
}
.keybind-help-panel {
  padding: calc(var(--unit) * 3);
  width: min(var(--help-w), var(--modal-max-w));
  max-height: var(--modal-max-h);
  overflow-y: auto;
  margin: auto;
  /* Pixel font: font-size must stay a multiple of 16 device pixels or
     glyphs blur (see --font-body). */
  font-size: var(--font-body);
}
.keybind-help-title {
  font-size: var(--font-title);
  margin-bottom: calc(var(--unit) * 2);
  text-align: center;
}
.keybind-help-group {
  color: var(--accent);
  text-transform: uppercase;
  margin-top: calc(var(--unit) * 3);
  margin-bottom: var(--unit);
  text-align: center;
}
.keybind-help-group:first-of-type { margin-top: 0; }
.keybind-help-row {
  display: flex;
  gap: calc(var(--unit) * 3);
  padding: var(--space-2) 0;
  color: var(--text-secondary);
}
.keybind-help-frame { display: flex; flex-direction: column; align-items: center; margin: auto; }
.keybind-help-more {
  display: flex;
  flex: none;
  /* The padding is both the space between the arrow and the panel and a bigger target to click. */
  padding: var(--space-6) var(--space-8);
  color: var(--text-primary);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-base) ease;
  --cursor: click;
}
.keybind-help-more.visible { opacity: 1; pointer-events: auto; }
.keybind-help-more:hover, .keybind-help-more:active, .keybind-help-more.pressed { color: var(--accent); }
/* Twice the size the key icons have in the rows: whole multiples of the icon's art pixel, so they stay crisp. */
.keybind-help-more .icon--inline { --icon-px: calc(var(--font-body) / 8); }
.keybind-help-key {
  flex: 0 0 var(--help-key-w);
  text-align: right;
  color: var(--text-primary);
}

/* ---- Generic modal (export-error "more info", main.js) ---------------
   Same overlay+panel+fade technique as .keybind-help-overlay above, kept
   as its own classes rather than shared: two unrelated features. */
.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  background: var(--black-a0);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background var(--dur-slow) ease;
}
.modal-overlay.visible { background: var(--black-a60); }
.modal-overlay .modal-panel {
  transform: translateY(var(--space-24));
  opacity: 0;
  transition: transform var(--dur-slow) ease, opacity var(--dur-slow) ease;
}
.modal-overlay.visible .modal-panel {
  transform: translateY(0);
  opacity: 1;
}
.modal-panel {
  padding: calc(var(--unit) * 3);
  width: min(var(--modal-w), var(--modal-max-w));
  margin: auto;
  font-size: var(--font-body);
}
.modal-title {
  font-size: var(--font-title);
  margin-bottom: calc(var(--unit) * 2);
  text-align: center;
  color: var(--accent);
}
.modal-body {
  color: var(--text-secondary);
  text-align: center;
}

/* ---- Layers panel (§11, ui-design-system §4) ------------------------- */
.layer-stack {
  margin-top: auto;
  overflow-y: auto;
  max-height: 100%;
}

.layer-row { position: relative; }

/* Opacity handle: overlays the thumbnail's right edge, revealed on hover;
   --t is 0 at the top (opaque) to 1 at the bottom, kept inside the thumbnail. */
.opacity-pip {
  position: absolute;
  right: 0;
  top: calc((100% - var(--space-8)) * var(--t, 0));
  width: var(--space-8);
  height: var(--space-8);
  background: var(--text-primary);
  border: var(--border-width) solid var(--black);
  --cursor: drag-vertical;
  opacity: 0;
  z-index: var(--z-trigger);
}
.layer-thumb:hover .opacity-pip { opacity: 1; }

/* text-snap.js nudges text and thumbnails onto whole device pixels with these two custom properties. Any rule of its own that sets
   `translate` on such an element has to add them. */
:where([style*="--snap"]) { translate: var(--snap-x, 0px) var(--snap-y, 0px); }

/* On the red of a selected row the faint grey has about the same value as the fill: a dark red instead, so it recedes but reads. */
.tile.selected .fold-arrow, .tile.active .fold-arrow { color: var(--accent-dark); }

/* Collection/group header fold toggle: the one clickable element that
   collapses/expands; the name text itself only renames (double-click), it
   no longer also folds on a plain click. */
.fold-arrow {
  font-family: var(--icon-font);
  /* The padding gives the arrow the room to its right that the collab button keeps at its sides, without moving the arrow. */
  flex: 0 0 calc(var(--block) + var(--icon-px) * 2.5);
  padding-right: calc(var(--icon-px) * 2.5);
  height: 100%;
  --cursor: click;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-body);
  color: var(--text-faint);
}
.fold-arrow:hover,
.collection-header.selected:hover .fold-arrow, .layer-group-header.selected:hover .fold-arrow { color: var(--text-primary); }
/* The project icon labels the project header: an icon, not a button. */
/* First-run hints (main.js setHint): body[data-hint] names the one control that breathes between its dim colour and the
   accent. While the project panel is shut, a short accent bar at the foot of its edge, on the left, does the same.
   With reduced motion the pulse is off and the control stays solid accent. */
@keyframes nudge-pulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
body[data-hint="folder"] .project-icon .icon,
body:is([data-hint="menu"], [data-hint="new"]) .project-menu .icon,
body[data-hint="colors"] .palette-hamburger .icon,
body[data-hint="kofi"] .kofi-btn .icon,
body[data-report] .bug-btn .icon { background: var(--accent); animation: nudge-pulse 1.4s ease-in-out infinite; }
body[data-hint="discord"] .discord-btn .icon { background: var(--text-primary); animation: nudge-pulse 1.4s ease-in-out infinite; }
body[data-hint="new"] .hint-target { animation: nudge-pulse 1.4s ease-in-out infinite; }
body:is([data-hint="folder"], [data-hint="menu"], [data-hint="new"]) .edge-trigger-left::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: var(--space-3);
  height: var(--block);
  background: var(--accent);
  animation: nudge-pulse 1.4s ease-in-out infinite;
  pointer-events: none;
}
/* Collab button while a session waits for a second person: breathes to the accent. */
.btn.collab-waiting { background: var(--accent); animation: nudge-pulse 1.4s ease-in-out infinite; }
/* The folder icon as an autosave writes (main.js flashSaveIcon): snaps to bright, then fades to its dim. */
@keyframes save-flash { from { background: var(--text-primary); } to { background: var(--text-faint); } }
.project-icon.saving .icon { animation: save-flash 700ms ease-out; }
@media (prefers-reduced-motion: reduce) {
  .project-icon.saving .icon { animation: none; }
  .alert-pulse { animation: none; }
  .btn.collab-waiting { animation: none; opacity: 1; }
  body[data-hint] .icon, body[data-report] .bug-btn .icon, body[data-hint] .hint-target, body[data-hint] .edge-trigger-left::after { animation: none; opacity: 1; }
  body[data-hint="new"] .hint-target { background: var(--accent); }
}
/* the project header's menu icon sits a device pixel higher than the row menus' */
.project-menu .icon { transform: translateY(calc(var(--icon-px) / -2)); }
.project-icon { flex: 0 0 var(--block); display: flex; align-items: center; justify-content: center; color: var(--text-secondary); }
/* A light line above and a shadow line below, so a header reads as a raised
   divider between the rows it groups instead of one more row. Inset shadows,
   not borders: a border would push the text down inside the row. */
.collection-header, .layer-group-header {
  box-shadow: inset 0 var(--border-width) 0 var(--panel-edge-highlight), inset 0 calc(var(--border-width) * -1) 0 var(--panel-edge-shadow);
}

.layer-thumb {
  position: relative;
  flex: 0 0 auto;
  height: 100%;
  line-height: 0;
  border-top: var(--border-width) solid var(--black-a30); /* §4.3: 30% black, thumbnail column only */
}
.layer-thumb canvas { display: block; height: 100%; width: auto; image-rendering: pixelated; }
/* Same reason as .tile.selected: on the accent fill that edge reads as a dark red stripe. */
.tile.selected .layer-thumb, .tile.active .layer-thumb { border-top-width: 0; }

.thumb-eye {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--black-a50);
  color: var(--text-primary);
  opacity: 0;
  pointer-events: none;
}
.layer-thumb:hover .thumb-eye { opacity: 1; }
.thumb-eye.hidden-indicator { color: var(--accent); }

/* Group header's visibility toggle (layers-panel.js): no thumbnail to
   overlay here, so it sits inline in a full-height square. */
.eye-pip {
  flex: 0 0 var(--block);
  width: var(--block);
  height: 100%;
  --cursor: click;
  display: flex;
  align-items: center;
  justify-content: center;
}
.eye-pip { color: var(--text-faint); }
.eye-pip:hover { color: var(--text-primary); }
.eye-pip.hidden-indicator { color: var(--accent-dark); }
.eye-pip.hidden-indicator:hover { color: var(--accent-light); }

.layer-label {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  /* Centered, not padding-anchored: the layer row is 1.5 blocks tall, not
     1, so the label is deliberately vertically centered against the
     thumbnail rather than pinned 1em from the top. */
  padding: 0 var(--pad);
  box-shadow: inset 0 var(--border-width) 0 var(--black-a10); /* §4.3: 10% black, label column only */
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
/* Names are dim until their row is hovered, selected, or being renamed. */
.file-row-name, .layer-label, .project-name { color: var(--text-secondary); }
.file-row .file-row-name { color: var(--text-tertiary); }
.tile:hover .file-row-name, .tile:hover .layer-label, .project-header:hover .project-name,
.tile.selected .file-row-name, .tile.active .file-row-name, .tile.selected .layer-label, .tile.active .layer-label,
.file-row-name[contenteditable="true"], .layer-label[contenteditable="true"], .project-name[contenteditable="true"] { color: var(--text-primary); }

