/* Lo-fi folder-tree portfolio. Every value comes from design-system.css. */

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: var(--space-24) var(--space-24) var(--space-144);
  background: var(--color-background);
  color: var(--color-foreground-primary);
  font-family: var(--font);
  font-size: var(--text-13);
  font-weight: var(--text-13-regular);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

/* Old-school: pinned to the top-left, never centred — `margin: 0` is what keeps
   it there, so don't "fix" it to `0 auto`. This max-width is the only thing
   setting line length now that the body has no measure cap of its own. */
.wrap {
  max-width: var(--content-max-width);
  margin: 0;
}


/* ---------------------------------------------------------------- pickers */

/* The page is two file pickers stacked: name → info/contact, then ~/work →
   the experience list. Both get the identical header treatment, so the only
   difference between them is what's inside. */

.tree--meta {
  margin-bottom: var(--space-40);
}

.tree-header {
  display: flex;
  align-items: center;
  gap: var(--tree-gap);
  /* One value, not padding plus margin — those were the gaps above and below
     the keyline this header used to carry. */
  margin-bottom: var(--space-8);
}

/* Trig, the mascot from the Trader Jones project (headphones removed and
   version A's hair grafted on — the cans are a Blocks thing). Pinned to the
   bottom-right of the viewport rather than sitting in the flow, so he stays
   put while the tree scrolls. `pointer-events: none` keeps him from swallowing
   clicks on anything he happens to sit over. */
.mascot {
  position: fixed;
  right: var(--space-24);
  bottom: var(--space-24);
  z-index: 20;
  width: 56px;
  height: 56px;
  display: block;
  pointer-events: none;
  image-rendering: pixelated;
}

/* h1 and span both land here, so reset the heading's own type and margin. */
.tree-root {
  font-size: var(--text-13);
  font-weight: var(--text-13-regular);
  line-height: 1.6;
  margin: 0;
  color: var(--color-foreground-primary);
}

.tree-header__aside {
  /* Pushed right now the header packs from the left. */
  margin-left: auto;
  font-size: var(--text-13);
  color: var(--color-foreground-primary);
  text-align: right;
  /* The years counter changes several times a second — tabular figures stop
     the text reflowing under it on every tick. */
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------------ nodes */

/* Row layout mirrors the Blocks sample-browser file picker:
   [ triangle ][ folder icon ][ label ................ years / note ]
   Each nesting level indents by the triangle + icon width, so a child's icon
   sits under its parent's label. */
:root {
  --tree-caret: 12px;
  --tree-icon: 20px;
  --tree-gap: var(--space-8);
  /* One level of nesting. Sized so a child's folder icon starts exactly where
     its parent's label does: the label is `caret + gap + icon + gap` from the
     row's start, the child's icon is `caret + gap` from the child row's start,
     so the step is the difference — `gap + icon`.
     Note this can't also centre the child's caret under the parent's icon;
     those two only coincide if `--tree-caret` equals `--tree-icon`, and the
     caret slot is deliberately narrower than the icon. Icon-to-label wins,
     because it's the alignment you actually see as a column. */
  --tree-indent: calc(var(--tree-gap) + var(--tree-icon));
  /* Where a row's label starts, measured from the row's left edge. Opened copy
     and figures line up to this so they sit under the labels, not under the
     icons. */
  --tree-label-offset: calc(var(--tree-caret) + 2 * var(--tree-gap) + var(--tree-icon));
  /* Every row carries this permanently, and an open folder's panel is the same
     value again. Hover doesn't touch the fill at all — it recolours the text
     (see below), so there's no second, stronger tint to keep in step. */
  --tree-row-tint: rgba(255, 255, 255, 0.3);
  /* Row height, set outright rather than derived from the type: a round 32px
     doesn't fall out of a 20.8px line box and whole-pixel padding. Rows use it
     as a min-height with their contents centred, and the sticky offsets for
     open folders read the same value, so the two can't drift apart. */
  --tree-row: 32px;
  /* Gutter between rows, letting the page colour through so each row reads as
     its own block rather than the fills merging into one panel. */
  --tree-row-gap: var(--space-4);
  /* How long a row takes to pick up its hover colours. Label, trailing column,
     icon and the row outline all read this one value — they change together, so
     if they ever drift apart the slower one reads as lag. */
  --tree-hover-fade: 50ms;
  /* How far across the panel an open folder's copy runs, as a fraction. The
     left edge of that copy steps in with nesting; this right edge doesn't, so
     content at every depth stops on the same line. */
  --tree-measure: 0.8;
}

.node--leaf,
.node__summary {
  display: flex;
  align-items: center;
  gap: var(--tree-gap);
  /* min- rather than a fixed height so a row that wraps on a narrow screen can
     still grow instead of clipping its own contents. */
  min-height: var(--tree-row);
  /* The negative inline margin cancels the padding, so the fill extends 8px
     past the content on each side while the labels and years stay on the same
     x they'd sit at without it. The two have to move together — change one and
     every label and year shifts sideways. Body carries 24px, so there's room
     for the overhang without the rows forcing a horizontal scrollbar. */
  padding: 0 var(--space-8);
  margin: 0 calc(-1 * var(--space-8)) var(--tree-row-gap);
  background: var(--tree-row-tint);
}

/* Nothing behind a terminal row to open, so the pointer says so outright. It
   also gets no hover colour — the blocked cursor is the whole feedback. */
.node--leaf {
  color: var(--color-foreground-primary);
  cursor: not-allowed;
}

/* The outline is carried permanently and transparent, rather than applied when
   the row is focused, so it can fade in on the same curve the label and icon
   colours use. Applied on the state itself it has nothing to transition *from*
   and snaps in instead. */
.node__summary {
  cursor: pointer;
  list-style: none;
  outline: 1px solid transparent;
  outline-offset: -1px;
  transition: outline-color var(--tree-hover-fade) ease;
}

/* An open folder's row stays put while its contents scroll past, so you can
   always see which folder you're inside. Nested folders park underneath their
   ancestors rather than over them (hence --depth), and shallower rows sit on
   top so a child slides beneath its parent. Its opaque background is set
   further down, alongside the open-folder tint it has to match. */
.node__details[open] > .node__summary {
  position: sticky;
  /* One row plus its gutter per level, or parked headers would creep upward by
     the gap amount with every level of nesting. */
  top: calc(var(--depth, 0) * (var(--tree-row) + var(--tree-row-gap)));
  z-index: calc(10 - var(--depth, 0));
}

/* Suppress the native disclosure marker; the triangle does that job. */
.node__summary::-webkit-details-marker {
  display: none;
}

.node__summary::marker {
  content: '';
}

.node__caret {
  flex: none;
  width: var(--tree-caret);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-red);
}

/* Terminal rows carry a triangle too, so the column reads consistently — grey
   rather than brand, since there's nothing behind it to open. */
.node__caret--muted {
  color: var(--color-foreground-30);
}

.node__caret svg {
  width: 8px;
  height: 8px;
  display: block;
  transition: transform 0.12s ease;
}

.node__details[open] > .node__summary .node__caret svg {
  transform: rotate(90deg);
}

.node__icon {
  flex: none;
  width: var(--tree-icon);
  height: var(--tree-icon);
  display: block;
  color: var(--color-foreground-70);
  transition: color var(--tree-hover-fade) ease;
}

.node--leaf .node__icon {
  color: var(--color-foreground-30);
}


.node__icon svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* Hover recolours the row rather than filling it: label, trailing column and
   folder/file icon all go brand red. The trailing column and the icon set their
   own colours, so they have to be named here rather than left to inherit the
   row's. The caret is left alone — it's already red on anything openable, and
   staying grey on a terminal row is what marks that row as inert.
   Terminal rows are excluded entirely: there's nothing behind them to open, so
   they get the blocked cursor below instead of a colour that would read as an
   invitation to click. */
.node__summary:hover,
.node__summary:hover .node__trailing,
.node__summary:hover .node__icon {
  color: var(--color-red);
}

/* An open folder is one flat panel for its whole height — the same tint the
   rows carry, so opening a folder reads as the block filling in rather than as
   a different shade appearing.
   It also takes the rows' 8px overhang on each side, so the panel's edges line
   up with the closed rows above and below it instead of sitting inset from
   them. The matching padding cancels it inwards, leaving `.node__content` on
   the same x it was on before.
   Top-level only: nested folders share these edges, and translucent fills
   stacked on each other would darken with every level. */
.tree > .node__children > .node > .node__details[open] {
  background: var(--tree-row-tint);
  margin-inline: calc(-1 * var(--space-8));
  padding-inline: var(--space-8);
}

/* The gutter below a row comes from the summary's own bottom margin — which is
   inside the panel once the folder is open, and so no longer separates it from
   the row beneath. The open node carries the gap itself instead. */
.node__details[open] {
  margin-bottom: var(--tree-row-gap);
}

/* Rows inside that panel would otherwise paint their own tint on top of it and
   show up as darker bands. They sit on the panel instead. */
.node__details[open] > .node__content .node--leaf,
.node__details[open] > .node__content .node__summary {
  background: transparent;
}

/* An open folder's row is sticky, so it needs an opaque background or content
   scrolling beneath would show through it — the row tint painted over the page
   colour, which lands on exactly the panel's shade. The second selector
   restates it for nested open folders, which the transparent rule above would
   otherwise catch.
   The box-shadow extends that opaque fill down through the row's own gutter.
   Parked headers are spaced a row *plus a gutter* apart (see the sticky offset
   above), so without it there's a 4px slot between each parked row with nothing
   opaque in it, and the page scrolls visibly through the stack. A shadow rather
   than extra height, so the row's own box is untouched — and when the row isn't
   parked the shadow lands on the panel in the panel's own colour, so it costs
   nothing.
   `color-mix` has to restate the tint's 30%: a translucent shadow would let the
   content through, which is the whole thing we're fixing. */
.node__details[open] > .node__summary,
.node__details[open] > .node__content .node__details[open] > .node__summary {
  background:
    linear-gradient(var(--tree-row-tint), var(--tree-row-tint)),
    var(--color-background);
  box-shadow: 0 var(--tree-row-gap) 0
    color-mix(in srgb, var(--color-white) 30%, var(--color-background));
}

/* Inset, not offset. An offset ring strays outside the row's own box and gets
   painted over by the sticky ancestor above it, which sits on a higher z-index
   with an opaque background — so the top of the ring was being cut off. Drawn
   inside the row, nothing can cover it.
   scroll-margin-top keeps a row tabbed into from landing underneath the stack
   of sticky folder headers. */
.node__summary:focus-visible {
  outline-color: var(--color-red);
  scroll-margin-top: calc(3 * var(--tree-row));
}

/* The ring is keyboard-only. The mouse gets the colour change and nothing else
   — pointing at a row and opening it are both self-evident with a pointer,
   whereas tabbing needs something to say where you are.
   Tab to a root row that's open and the ring grows to wrap the whole panel
   rather than just the row: open, the row and its contents are one block, and a
   ring around the row alone boxes the handle of something much bigger.
   `> .node__summary` in the :has() keeps it to the root row's own focus — tab
   on into a nested row and the ring moves down to that row instead of staying
   wrapped around everything.
   Drawn as an overlay rather than an `outline` on the panel: the row is sticky
   with a z-index and an opaque background, so a parent's outline paints
   *underneath* it and the top edge of the ring disappears behind the row. The
   overlay sits above the row instead (z-index clears the sticky stack), and
   ignores pointer events so it can't swallow a click. */
.tree > .node__children > .node > .node__details[open] {
  position: relative;
}

.tree > .node__children > .node > .node__details[open]:has(> .node__summary:focus-visible)::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid var(--color-red);
  pointer-events: none;
  z-index: 11;
}

/* ...and the open row drops its own ring, or there'd be a second one sitting
   just inside the panel's. */
.tree > .node__children > .node > .node__details[open] > .node__summary:focus-visible {
  outline-color: transparent;
}

.node__label {
  font-size: var(--text-13);
  transition: color var(--tree-hover-fade) ease;
}

/* The whole trailing column — years, notes, the live counter, row links —
   shares one treatment, so nothing in it competes with anything else. */
.node__trailing {
  margin-left: auto;
  padding-left: var(--space-16);
  display: flex;
  gap: var(--space-16);
  font-size: var(--text-13);
  color: var(--color-foreground-primary);
  text-align: right;
  transition: color var(--tree-hover-fade) ease;
}

.node__note-slot,
.node__note-link {
  font-variant-numeric: tabular-nums;
}

.node__note-link {
  color: inherit;
  text-decoration: none;
}

.node__note-link:hover {
  color: var(--color-red);
}

.node__year {
  flex: none;
  min-width: 4ch;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.node__details[open] > .node__content {
  padding-left: var(--tree-indent);
}


/* --------------------------------------------------------- expanded body */

/* The measure is set by `.node__block` inside this, not here. */
.node__body {
  padding: var(--space-8) 0 var(--space-24);
}

/* Sections run together as one feed inside an open folder, separated by space
   and their own heading — no rule between them. */
.node__section {
  margin-top: var(--space-48);
}

/* That separator is for the space *between* sections. A node with no copy of
   its own (ABC listen, ABC iview) has its first section heading as the very
   first thing in the panel, and renderBody() only appends the node's own block
   when it has something in it — so `:first-child` catches exactly that case and
   drops the leading gap. A node that does have copy keeps the full separation
   below it. */
.node__section:first-child {
  margin-top: 0;
}

/* Copy, links and figures start inline with the label of the folder they came
   out of, so opening a folder reads as its own heading continuing rather than
   as a fresh block at some other indent.
   `.node__content` has already indented this by one nesting step (which is what
   puts child *rows* where they go), so the block only has to make up the
   difference to the label column. Expressed as the difference rather than as a
   number, so retuning either token can't leave the copy hanging.
   The right edge stops at `--tree-measure` across the panel, and lands on the
   same x at every depth — copy one level down starts further in but still ends
   in line with the copy above it.
   That's why it can't be a plain `max-width: 80%`: percentages resolve against
   `.node__content`, which has been narrowed by one `--tree-indent` per level of
   nesting, so each level would stop somewhere slightly different. Adding the
   accumulated indent back to `100%` reconstructs the panel's own width, and the
   remaining fraction of *that* is the padding. `--depth` is set per row by
   tree.js and inherits down here — the same value the sticky offsets read. */
.node__block {
  padding-left: calc(var(--tree-label-offset) - var(--tree-indent));
  padding-right: calc(
    (1 - var(--tree-measure)) *
    (100% + (var(--depth, 0) + 1) * var(--tree-indent))
  );
}

.node__text {
  margin: 0;
}

.node__text + .node__text {
  margin-top: var(--space-12);
}

/* Copy resuming after a `{ list }` body entry — the paragraph gap can't come
   from `.node__text + .node__text` there. Double the usual gap: the list's own
   4px row rhythm reads as tighter than a paragraph break, so the standard 12px
   isn't enough to close the list off. */
.node__meta + .node__text {
  margin-top: var(--space-24);
}

/* Taller than it is wide, as in v1 — `--radius-full` on a non-square box gives
   an oval rather than a circle, which frames a face instead of cropping it to
   the width of one. `cover` keeps the crop centred at any source ratio.
   v1's box was 112 × 136; this is that at 80%, with the ratio held by
   `aspect-ratio` rather than a second rounded pixel value so the oval can't
   drift as the width is retuned. */
.node__portrait {
  width: 90px;
  aspect-ratio: 14 / 17;
  object-fit: cover;
  border-radius: var(--radius-full);
  display: block;
  margin-top: var(--space-16);
}

.node__links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  /* Each button sizes to its own label — the default `stretch` would run them
     the full width of the copy measure. */
  align-items: flex-start;
  gap: var(--space-8);
}

/* Copy introducing the links, e.g. Contact's line above the LinkedIn button. */
.node__text + .node__links {
  margin-top: var(--space-16);
}

/**
 * The chrome-era raised button, drawn the way the original does it: a 2px
 * bevel, lit and shaded on opposite corners for the hard outer edge, with a
 * softer light/shade pair inset a pixel behind it so the edge has depth rather
 * than reading as a plain two-tone frame.
 *
 * The face is `--color-background`, which is *darker* than the panel the
 * button sits on (that's the page colour under a 30% white tint). A raised
 * object catching light on its top-left is the whole illusion, and it only
 * works if the face is darker than what surrounds it.
 *
 * One recipe for every button-shaped thing on the site: the row links carry
 * it as `.retro-btn`, the before/after control as `.retro-switch` — a single
 * chassis whose halves share it. One palette to retune.
 */
.retro-btn,
.retro-switch {
  /* Each side in two steps — the hard outer edge, then a softer one behind it.
     Neither extreme is at full strength: pure white and pure black read as
     lines drawn on the button rather than as light and shadow on its edges. */
  --btn-edge-light: rgba(255, 255, 255, 0.75);
  --btn-edge-shade: rgba(0, 0, 0, 0.9);
  --btn-light: rgba(255, 255, 255, 0.55);
  --btn-shade: var(--color-foreground-30);
  --btn-keyline: rgba(0, 0, 0, 0.15);
  /* Brand red at full strength jumps a keyline that sits at 15% black at rest.
     Held back to 30% it still reads as the hover, without the line becoming
     the loudest thing on the panel. */
  --btn-keyline-hover: color-mix(in srgb, var(--color-red) 30%, transparent);
  /* Pressed nudges the label a pixel into the button without resizing it —
     one padding grows as the other shrinks. */
  --btn-nudge: 0px;
}

.retro-btn {
  display: inline-block;
  padding:
    calc(var(--space-4) + var(--btn-nudge))
    calc(var(--space-8) - var(--btn-nudge))
    calc(var(--space-4) - var(--btn-nudge))
    calc(var(--space-8) + var(--btn-nudge));
  position: relative;
  color: var(--color-foreground-primary);
  background: var(--color-background);
  /* `inherit` first, for the <button> incarnation — buttons don't inherit the
     page font on their own the way anchors do. */
  font: inherit;
  font-size: var(--text-12);
  font-weight: var(--text-12-regular);
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  border: 2px solid;
  border-color:
    var(--btn-edge-light) var(--btn-edge-shade)
    var(--btn-edge-shade) var(--btn-edge-light);
  box-shadow:
    inset 1px 1px 0 var(--btn-light),
    inset -1px -1px 0 var(--btn-shade);
}

/* The keyline sits inside the bevel, where the focused default button in the
   original draws its dotted rect. It's permanent rather than a focus state —
   the site marks focus in red. */
.retro-btn::after {
  content: '';
  position: absolute;
  inset: 2px;
  border: 1px solid var(--btn-keyline);
  pointer-events: none;
}

/* Same red-on-hover as every other interactive thing in the tree. */
.retro-btn:hover {
  color: var(--color-red);
}

.retro-btn:hover::after {
  border-color: var(--btn-keyline-hover);
}

/* Bevel inverted, label nudged down-right: the button is pushed in. */
.retro-btn:active {
  --btn-nudge: 1px;

  border-color:
    var(--btn-edge-shade) var(--btn-edge-light)
    var(--btn-edge-light) var(--btn-edge-shade);
  box-shadow:
    inset 1px 1px 0 var(--btn-shade),
    inset -1px -1px 0 var(--btn-light);
}

.retro-btn:focus-visible {
  outline: 1px solid var(--color-red);
  outline-offset: 2px;
}

.node__meta {
  list-style: none;
  margin: var(--space-12) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  color: var(--color-foreground-primary);
}

/* Opt-in split for a long list. `grid-auto-flow: column` against an explicit
   row count is what fills the first column before starting the second — the
   default row flow would deal the items across the columns instead, which
   scrambles the reading order. Equal `1fr` tracks, so the columns divide the
   copy measure evenly and every column starts at the same x regardless of how
   long the names in the one before it are. */
.node__meta--split {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(var(--meta-rows), auto);
  grid-template-columns: repeat(var(--meta-cols), minmax(0, 1fr));
  column-gap: var(--space-24);
}

.node__meta li::before {
  content: '└ ';
  color: var(--color-foreground-30);
}

.node__award {
  margin-top: var(--space-16);
}

.node__award-label {
  display: block;
  font-size: var(--text-13);
  color: var(--color-foreground-70);
}

.node__award-logo {
  width: 140px;
  height: auto;
  margin-top: var(--space-4);
}

/* ---------------------------------------------------------------- figures */

.node__figures {
  display: flex;
  flex-direction: column;
  gap: var(--space-48);
  margin-top: var(--space-24);
  max-width: 100%;
}

.fig {
  margin: 0;
}

.fig--inset {
  max-width: 520px;
}

/* Deliberately no opacity fade-in here. v1 faded images from opacity 0 once a
   JS `load` handler added a class, which means anything that stops the
   transition from running leaves the image permanently invisible — a hidden or
   backgrounded tab freezes transitions at their start value, and that is
   exactly what happened during development. Images now just render. */
.fig__img {
  width: 100%;
  height: auto;
  display: block;
  border-radius: var(--radius-md);
  background-color: rgba(0, 0, 0, 0.05);
}

.fig__img--no-bg {
  background-color: transparent;
}

/* Locks the box to the "to" image's ratio so a compare doesn't reflow the
   page when swapped to a "from" image of a slightly different ratio. */
.fig__img[data-compare-to] {
  object-fit: cover;
}

.fig__caption {
  /* Deliberately off-scale: 4 sat too tight against the image, 8 let the
     caption drift loose from it. Split the difference. */
  margin-top: 6px;
  font-size: var(--text-13);
  color: var(--color-foreground-primary);
}

/* The borrowed components scale themselves with `transform: scale()`, which
   doesn't contribute to layout height — so the box has to be padded by at
   least the artwork's overflow or it spills out of its own frame. The tallest
   is a 48px box at scale 2, i.e. 24px proud on each side; 40px covers that and
   leaves the caption a normal gap rather than a canyon. Raise this if a
   component is ever scaled past 2. */
.fig--component .fig__component {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-48);
  padding: var(--space-40) 0;
}

/* Each component scales itself from its centre by default, which pushes the
   artwork right of the copy above it as the scale goes up. Growing from the
   left edge instead is what actually lines it up. Set here rather than in the
   borrowed sheets: it's about placement on this page, not the component. */
.fig--component .fig__component > * {
  transform-origin: left center;
}

.fig-row {
  display: grid;
  grid-template-columns: repeat(var(--fig-row-cols, 2), minmax(0, 1fr));
  align-items: start;
  gap: var(--space-24);
}

/* Component figures centre their artwork inside their own box, and that box is
   content-sized — so the 48px agent button and the 40px thinking indicator end
   up with centres a few px apart when they sit side by side. Stretching just
   the component cells to a common height puts every centre on one line. Scoped
   to `.fig--component` so image rows keep the grid's `align-items: start`. */
.fig-row > .fig--component {
  align-self: stretch;
  display: flex;
  flex-direction: column;
}

.fig-row > .fig--component .fig__component {
  flex: 1;
}

.fig-row__footer {
  grid-column: 1 / -1;
  display: flex;
  justify-content: flex-start;
}

/* --------------------------------------------------- before/after control */

.figure-toggle {
  margin-top: var(--space-8);
}

/* The before/after control as one switch rather than two adjacent buttons:
   the chassis carries the hard outer bevel once, the halves sit flush inside
   it sharing a single keyline divider, and the selected half is sunk into the
   frame — soft inner bevel inverted, face shaded, label nudged — while the
   other half stays proud. Exactly one half is ever down, which is what makes
   it read as a rocker switch rather than buttons that happen to touch. */
.retro-switch {
  display: inline-flex;
  align-items: stretch;
  background: var(--color-background);
  border: 2px solid;
  border-color:
    var(--btn-edge-light) var(--btn-edge-shade)
    var(--btn-edge-shade) var(--btn-edge-light);
}

.retro-switch button {
  font: inherit;
  font-size: var(--text-12);
  font-weight: var(--text-12-regular);
  /* The raised half steps back so the pressed one reads as current — same
     between-weight the trailing column uses for the job. */
  color: var(--color-foreground-70);
  background: transparent;
  border: 0;
  cursor: pointer;
  padding:
    calc(var(--space-4) + var(--btn-nudge))
    calc(var(--space-8) - var(--btn-nudge))
    calc(var(--space-4) - var(--btn-nudge))
    calc(var(--space-8) + var(--btn-nudge));
  /* The raised half's soft bevel — same recipe as the button face. */
  box-shadow:
    inset 1px 1px 0 var(--btn-light),
    inset -1px -1px 0 var(--btn-shade);
}

/* The halves share one divider rather than each drawing an edge. */
.retro-switch button + button {
  border-left: 1px solid var(--btn-keyline);
}

/* No red and no hover state anywhere in the switch — the state is physical,
   not painted: the pressed half sits in its grey well, the raised half
   catches the light, and the pointer cursor is the only invitation. */

/* The selected half, held down: the face drops into plain shadow — darker
   grey, no bevel lines of its own — with the label nudged into the frame.
   A flat dark well against the other half's raised highlight is what sells
   the press. */
.retro-switch button.is-active {
  --btn-nudge: 1px;
  background: rgba(0, 0, 0, 0.1);
  box-shadow: none;
  color: var(--color-foreground-primary);
}

.retro-switch button:focus-visible {
  outline: 1px solid var(--color-red);
  outline-offset: -3px;
}

/* ----------------------------------------------------------------- mobile */

@media (max-width: 720px) {
  body {
    padding: var(--space-24) var(--space-16) var(--space-80);
  }

  /* Halved from the desktop inset. Stacked on the body's own 16px this was
     eating 32px a side, which is what forced the masthead's role/years onto a
     second line — the name plus the counter needs 312px and only had 311. */
  .wrap {
    padding: var(--space-8);
  }

  /* The role/years keeps its right-hand column here, same as the rows below
     it. `flex-wrap` stays as the safety net: if the counter ever outgrows the
     room, it drops under the name rather than wrapping mid-word. */
  .tree-header {
    flex-wrap: wrap;
    gap: var(--space-4);
  }

  /* The year stays in its right-hand column at this width — the labels are
     short enough that there's room, and a year sitting under its own folder
     reads as a second row rather than as that row's date. `flex-wrap` is the
     safety net: a label long enough to genuinely collide still pushes the
     trailing column onto its own line rather than scrolling the row sideways.
     It keeps `margin-left: auto` there, so it stays right-aligned either way. */
  .node--leaf,
  .node__summary {
    flex-wrap: wrap;
  }

  .node__trailing {
    padding-left: var(--space-8);
  }

  /* The 80% measure earns its keep on a wide screen; here it just throws away
     a third of the panel. Copy and figures take the folder's full width, less
     a small right padding so nothing runs flush into the panel's edge. */
  .node__block {
    padding-right: var(--space-12);
  }

  .node__details[open] > .node__content {
    padding-left: var(--space-12);
  }

  /* Trig swaps to the left edge here. Bottom-right on a phone puts him over
     the trailing column — the years — and near the thumb; on the left he sits
     under the caret/icon column, which is the emptiest part of the row. */
  .mascot {
    right: auto;
    left: var(--space-16);
  }

  .fig-row {
    grid-template-columns: 1fr;
  }

  /* Figures are full-bleed and stacked here, so 48px reads as a gap you have
     to scroll through rather than a separation between them. */
  .node__figures {
    gap: var(--space-32);
  }

  /* Two columns of "NSW Department of Justice" at this width leaves each name
     nowhere to go but a wrap mid-phrase. Back to one. */
  .node__meta--split {
    grid-auto-flow: row;
    grid-template-rows: none;
    grid-template-columns: minmax(0, 1fr);
  }

}
