/* ══════════════════════════════════════════════════════════════════
   base.css — structure, character grid, typography.

   Everything in here is theme-independent: it holds for the terminal
   (VFD) look and for the paper look alike. Colour, weight, glow and
   flicker live in css/theme-*.css — see theme-terminal.css for why
   that split is drawn where it is.

   Layout from Figma 64:53 (desktop) and 71:155 (mobile).
══════════════════════════════════════════════════════════════════ */

:root{
  /* Animation drivers, defaulted to their inert values so the page is
     correct with no JS at all — 1 means "fully lit, no flicker", which
     is precisely the Figma design. A theme that wants neither simply
     never consumes them. */
  --blink: 1;            /* boot segment driver: hard 0 or 1, never eased */
  --b1: 1; --b2: 1;      /* flicker brightness, written by the loop */
}

*{ margin:0; padding:0; box-sizing:border-box; }
html, body{ height:100%; }

/* ── THEME HOOKS ──────────────────────────────────────────────────
   The single declaration site for every themed effect. Theme files
   supply values; this decides where they land. Each falls back to
   `none`, so a theme that simply doesn't declare one gets no effect
   rather than a broken property — which is how paper omits glow and
   flicker without owning a single selector.

   Palette-driven rules (hover invert, copy flash) live here too:
   they're written in terms of --fg/--bg, so they are already correct
   in both themes and duplicating them per theme would be a way to
   get them out of sync, not a way to control them. */
.crest{ filter:var(--fx-crest, none); }
.crest-mark{ filter:var(--glow-crest, none); }
.tree, .pane, .divider, .scroll{
  filter:var(--fx-panel, none);
  text-shadow:var(--glow-text, none);
}
.theme-toggle{ filter:var(--fx-toggle, none); }
.photo-list img{ filter:var(--fx-panel-img, none); }

body{
  background:var(--bg);
  color:var(--fg);
  font-family:'Doto', ui-monospace, monospace;
  font-weight:var(--weight, 500);           /* theme-*.css: Medium vs SemiBold */
  font-variation-settings:'ROND' 0;         /* square dots, not round */
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}

/* ── LAYOUT ───────────────────────────────────────────────────────
   Desktop: crest sits to the LEFT of the tree, vertically centred on
   the first row — the row's `---` reads as a wire running out of it.
   Mobile: crest sits ABOVE, so that first connector becomes `|--`
   and the trunk descends from the crest instead. Both are in Figma;
   the swap is handled at the bottom of this block.

   Figma 1:7 puts 24px on the root's top and sides and 24px on .dir's
   bottom, which is just 24px all round — collapsed into one padding
   here. Mobile (1:351) is genuinely asymmetric: 76 top, 62 bottom. */

/* The side insets only matter because index.html sets viewport-fit=cover:
   that hands us the full screen, notch included, so a notched device in
   LANDSCAPE (iPhone 15 rotated is 852 wide — the desktop breakpoint)
   would otherwise run the crest and tree straight under the Dynamic
   Island. max() keeps Figma's 24px everywhere the inset is smaller,
   which is every non-notched context. Vertical padding deliberately
   stays a plain number — see the mobile block for why. */
.stage{
  min-height:100svh;
  display:flex;
  /* .dir carries the bottom */
  padding:24px max(24px, env(safe-area-inset-right, 0px)) 0
                max(24px, env(safe-area-inset-left,  0px));
}

/* Top-anchored, not centred. The previous revision centred this and
   then had to pin it to position:fixed on first layout, because a
   flex-centered box re-centers on every height change and collapsing
   a group would make the crest and every row jump. Left-aligning
   removes the cause rather than compensating for it: .dir's top-left
   simply never moves, so there is nothing to pin and nothing to
   re-measure on resize. pinDir() and its mobile percentage-width
   workaround were deleted with this change — see git history if that
   bug ever resurfaces.

   space-between is what puts the theme toggle on the bottom edge
   (Figma 1:1013 / 1:352); .dir stretches full height via .stage's
   default align-items:stretch. */
.dir{
  display:flex; flex-direction:column;
  align-items:flex-start; justify-content:space-between;
  flex:1 0 0; min-width:0;
  padding-bottom:24px;
}

/* The crest-beside-tree pairing. Was ".dir" before the toggle needed
   a row of its own beneath it. */
.tree-row{ display:flex; align-items:flex-start; width:100%; }

/* The crest's LAYOUT box. The crest itself is absolutely positioned
   inside it so that flying to centre-screen costs no reflow. */
/* A <button> now (backlog #3), so it needs the same UA-chrome strip
   .label already does — background/border/padding, and cursor since
   buttons default to it anyway but this makes the intent explicit.

   color:inherit is the one that actually matters here, and its
   absence is a real bug this shipped with: .crest-mark recolours via
   `background:currentColor`, and a <button> does NOT inherit color by
   default — it gets the UA's own button-text color instead, same
   button-breaks-inheritance gotcha .label already documents below.
   The crest wasn't gone, it was rendering in the wrong color, likely
   near-invisible against --bg. */
.crest-slot{
  position:relative; flex:none;
  width:64px; height:64px;
  background:none; border:0; padding:0;
  color:inherit;
  cursor:pointer;
}
.crest-slot:focus-visible{ outline:1px solid var(--fg); outline-offset:2px; }

.crest{
  position:absolute; left:50%; top:50%;
  width:200px; height:200px; margin:-100px 0 0 -100px;
  transform-origin:50% 50%;

  /* Rendered at 200px and scaled DOWN to the 40px the dir wants.
     Deliberate: the boot state is the one held on screen for seconds,
     so that's the one that must be pixel-crisp. Authoring at 40px and
     scaling up would soften exactly the state people stare at. */
  transform:scale(.2);

  /* steps(), not an eased curve. A continuous cubic-bezier glide reads
     as a modern UI transition — a sprite tweening across a page. A
     segmented display doesn't do that: it re-addresses which segments
     are driven, discretely. steps(6) hops through 6 combined
     position+scale states rather than interpolating between them, so
     it reads as a stepper-motor carriage relocating, not an easing
     curve — consistent with the blink below, which is also a hard
     step and never a fade. */
  transition:transform 480ms steps(6, jump-end);

  opacity:var(--blink);
  will-change:transform, filter;
}

/* Masked, not <img>. The crest SVG has #8DFFFB baked into its single
   path and currentColor does not cross an <img> boundary, so it could
   never follow the theme. As a mask only the alpha is used and the
   colour comes from background — which makes it recolour for free,
   keeps the asset an external cacheable file, and leaves drop-shadow
   working (it reads the post-mask alpha, i.e. the crest's silhouette).
   boot.js measures the .crest span, not this, so nothing there
   changes. */
.crest-mark{
  display:block; width:100%; height:100%;
  background:currentColor;
  -webkit-mask:url(/img/pirouzi-crest.svg) center / contain no-repeat;
  mask:url(/img/pirouzi-crest.svg) center / contain no-repeat;
  user-select:none;
}

/* ── THE TREE ─────────────────────────────────────────────────────
   A real character grid. Doto is monospace at exactly 0.6em advance
   (measured: every glyph 12px at 20px), so --cell is exact and every
   connector lands on a column.

   letter-spacing is folded into --cell because `ch` does not account
   for it; at 4 cells the drift would already be ~1px and the wrapped
   continuation lines would sit off-grid. */
.tree{
  --track:-0.22px;                          /* Figma 1:1177 tracking */
  --cell:calc(0.6em + var(--track));
  font-size:22px; line-height:1;            /* Figma leading-none */
  letter-spacing:var(--track);
  padding-top:22px;
  will-change:filter;
}
/* Grid check: 0.6em at 22px is 13.2px, less 0.22px tracking = 12.98px
   per cell. The longest row (`|   \`-- mothogram_living_sculpture.srk`,
   38 cells) measures 493.2px against Figma's stated 494 — so the type
   size and tracking below are the real ones, not an approximation
   that happens to look close.

   Tracking is -0.01em at both breakpoints (-0.22 at 22px, -0.2 at
   20px), so it is the SIZE that changes between desktop and mobile,
   not the letterfit. */

.row{
  /* Hanging indent, which is the whole fix for the mobile fold: the
     padding pushes the row right by the connector's width and the
     negative indent pulls only the FIRST line back out. So a long
     name wraps underneath itself instead of collapsing back to
     column 0 — the thing Figma couldn't express because connector
     and label had to share one text box. */
  padding-left:calc(var(--ind) * var(--cell));
  text-indent:calc(var(--ind) * var(--cell) * -1);
  white-space:pre-wrap;
  overflow-wrap:anywhere;
  margin-bottom:4px;                        /* Figma gap-[4px] */
  opacity:0;                                /* revealed by the print */
}
.row:last-child{ margin-bottom:0; }
.row.lit{ opacity:1; }

.conn{ white-space:pre; }                   /* connectors never invert */

/* Inline, NOT a flex item — an inline background breaks into per-line
   fragments when it wraps, so the invert follows the text's actual
   shape the way a terminal selection does. A block would paint one
   rectangle over the ragged edge. */
.label{
  display:inline;
  /* <button> does not inherit type/colour/letter-spacing by default —
     without these the group rows (spirouzi/, project/, photograph/,
     log/ — the only .label elements that are <button>s) would fall
     back to the UA font and knock every connector off the grid.

     text-shadow:inherit is the same fix for a different property: the
     UA stylesheet's button reset blocks it specifically, even with
     the above already in place — confirmed empirically, only <button>
     labels lost the glow, <a> and <span> ones inherited it fine. It
     is harmless in a theme that sets no text-shadow at all. */
  font:inherit;
  letter-spacing:inherit;
  color:inherit;
  text-shadow:inherit;
  background:none;
  border:0;
  padding:0;
  text-align:left;
  text-decoration:none;
}
a.label, button.label{ cursor:pointer; }

/* Two independent collapse axes, deliberately NOT the same class.
   .hidden is the group fold (clicking `photograph/`); .folded is the
   overflow fold behind `more...`. A row past the display cap sits
   inside a group, so it is subject to both — sharing one class would
   make re-opening the group also spill the overflow it was hiding. */
.row.hidden, .row.folded{ display:none; }

/* ── PANE ─────────────────────────────────────────────────────────
   Desktop columns, measured off Figma 1:239:

     24 │ dir 573 │ 32 │ divider 14 │ 32 │ pane 573 │ …
        x=24       x=629             x=675

   The dir column is flex:1 while nothing is open (Figma 1:7 has it
   spanning the full 1232) and snaps to a fixed 573 once a pane
   mounts. Both columns run to the bottom EDGE of the frame — the
   24px bottom inset lives inside .dir, which is why .stage's padding
   is open at the bottom. */

.divider, .pane-wrap, .scroll{ display:none; }

/* height, not min-height, and only once a pane is open, and only at
   this breakpoint. Desktop's Figma frames are a fixed 832 — the right
   pane is meant to scroll INTERNALLY inside that, not push the whole
   page taller. Without this, .pane's overflow-y:auto has nothing to
   clip against: .stage would just grow to fit whatever the tallest
   child wants (a real bug this hit — scrollHeight and clientHeight
   came back identical because nothing capped clientHeight at all).
   Scoped to desktop because mobile's drill-down is meant to keep
   growing and scrolling as a normal page, same as /data and /log
   already do there. */
@media (min-width:720px){
  .stage.is-open{ height:100svh; }
}

.stage.is-open .dir{ flex:none; width:573px; }

.stage.is-open .divider{
  display:flex; flex-direction:column;
  gap:22px;                                 /* 22 glyph + 22 gap = 44 pitch */
  flex:none; width:14px;
  margin-left:32px;
  padding-top:22px;                         /* same baseline as row 1 */
  font-size:22px; line-height:22px;
  letter-spacing:-0.22px;
  overflow:hidden;
}
/* gap, NOT margin-bottom on each glyph: a trailing margin on the last
   one counts toward the column's content height, which made this the
   tallest thing in the row and pushed .stage 6px past the viewport.
   gap emits nothing after the final item. Count is derived from
   height in js/router.js, not hardcoded to Figma's 18. */
.divider span{ flex:none; height:22px; }

.stage.is-open .pane-wrap{
  display:flex;
  flex:none; width:573px;
  margin-left:32px;
}

/* Flush against the pane's right edge (675+573=1248) — no 32px gutter
   like the divider has on its left. Sandy's correction (2026-08-11):
   the drawn frames show it 6px further out, at 1242. */
.stage.is-open.has-scroll .scroll{
  display:block;
  flex:none; width:14px;
  position:relative;
}
.scroll-thumb{
  position:absolute; left:0; width:14px; height:36px;
  cursor:grab; touch-action:none;
  font-size:22px; line-height:18px;         /* Figma 1:121 — leading TIGHTER
                                                than the glyph, which is what
                                                stacks the two ':' into 36px */
  text-align:center;
}
.scroll-thumb:active{ cursor:grabbing; }
.scroll-thumb span{ display:block; }

.pane{
  flex:1 0 0;
  display:flex; flex-direction:column;
  font-size:18px; line-height:1;
  letter-spacing:-0.18px;

  /* The scroll CONTAINER, always — js/scrollbar.js decides per pane
     whether content is long enough to need the visible track, but the
     pane itself must already be scrollable (by wheel, trackpad, or
     keyboard) regardless, including on the frames that never get a
     custom track drawn beside them (data, log). Native scrollbar
     hidden because the custom one replaces it visually; nothing about
     native scroll INPUT is disabled. */
  overflow-y:auto;
  scrollbar-width:none;
}
.pane::-webkit-scrollbar{ display:none; }

/* Backlog #1 (2026-08-12): the pane itself no longer fades in as one
   block — .pane used to carry opacity:0/.pane.lit the way this .pr
   rule now does, but per-block was replaced by per-LINE, matching how
   the tree's own .row/.row.lit never fades the whole .tree at once
   either. router.js's revealPane() marks every .pr in document order,
   staggered at the tree's own PRINT_STEP, so both surfaces read as
   the same machine printing. */
.pr{ opacity:0; }
.pr.lit{ opacity:1; }

.pane-inner{
  flex:1 0 0;
  display:flex; flex-direction:column;
  gap:22px;
  padding:22px 0;
}
.pane-blocks{ display:flex; flex-direction:column; gap:22px; }

.pane-title{ font-size:22px; letter-spacing:-0.22px; }

/* The back row only exists on mobile, where the tree is replaced
   rather than sat beside — so it carries the theme toggle too, which
   would otherwise disappear with the hidden dir column. */
.back-row{ display:none; }

.data-head{ display:flex; align-items:flex-start; gap:22px; }
.data-photo{ flex:none; width:178px; height:178px; object-fit:cover; display:block; }

/* 5 rows of 18px + 4 gaps of 22px = 178, matching the photo beside
   it exactly. That equality is the layout — don't change one number
   without the other. */
.stat{
  flex:1 0 0; min-width:0;
  display:flex; flex-direction:column; justify-content:center; gap:22px;
}

/* white-space:pre throughout: the label padding in js/pages.js IS the
   column alignment, and collapsing it would break the grid. */
.stat-row, .bio p, .links{ white-space:pre; }

/* A gauge is one atomic glyph run, never a phrase to re-flow. Without
   this it only misbehaves at the mobile breakpoint, where .stat-row
   relaxes to pre-wrap (below): the unfilled cells are HYPHENS, and the
   line breaker treats every hyphen as a legal break point. So a bar with
   a long dash run gets packed onto the label's line and split mid-bar,
   while the shorter ones move down whole — `execution` ([\\\\------],
   6 dashes) was the only row long enough to trigger it, which made it
   look like a one-off typo rather than a rule. Confirmed by swapping the
   dashes for backslashes at identical character count: the break
   vanishes, so it was never about width. keep-all doesn't help (it
   doesn't govern hyphen breaks) and inline-block shifts the baseline
   2px; pre matches the other four rows exactly. No-op on desktop, where
   .stat-row is already pre. */
.gauge{ white-space:pre; }

.crypto-row{ white-space:pre-wrap; overflow-wrap:anywhere; }

/* Instant, not eased. A terminal selection does not fade.

   font-weight, not a raw font-variation-settings override — Doto's
   wght axis controls literal dot SIZE per glyph (it's a dot-matrix
   font), and body already lets font-weight drive wght while
   font-variation-settings only pins ROND. So bumping weight here is
   the correct lever: at 500 (Figma's Medium), a near-black glyph on a
   solid fill reads low-contrast because the dots don't cover enough
   of each cell. At 800 they do — which is also physically correct,
   mirroring how real segment displays render reverse-video with
   fuller fill. Verified this doesn't shift the character grid:
   advance width is exactly 12px across the entire 100–900 range.

   text-shadow:none too — inverted text is dark ink on lit phosphor,
   not lit phosphor itself, so it shouldn't carry the glow. In paper
   there is no glow to cancel and this is simply inert. */
.label:hover,
.crypto-addr.copied{
  background:var(--fg); color:var(--bg);
  font-weight:800; text-shadow:none;
}
.label:focus-visible{ outline:1px solid var(--fg); outline-offset:1px; }

/* A link inside a sentence has no brackets or connectors to mark it,
   so it carries an underline — the only place the site underlines
   anything. On hover the invert takes over and the rule would just be
   a dark line across a filled block, so it goes. */
.label.inline{ text-decoration:underline; text-underline-offset:2px; }
.label.inline:hover{ text-decoration:none; }

/* ── THEME TOGGLE ─────────────────────────────────────────────────
   Figma 1:619. 8px side padding + 24 + 4 gap + 24 + 8 = the 68px the
   component reports. Each option is a 28px column bottom-aligned, so
   both icons share a baseline and the 4px active dot rides above the
   one that's selected — hence visibility rather than display on the
   dot: it has to keep occupying its 4px or the active icon would sit
   4px lower than the inactive one. */
.theme-toggle{
  display:flex; align-items:center; gap:4px;
  padding:0 8px;
  flex:none;
  opacity:0;                                /* lit at the end of the print */
}
.theme-toggle.lit{ opacity:1; }

.theme-opt{
  display:flex; flex-direction:column;
  align-items:center; justify-content:flex-end;
  height:28px;
  font:inherit; color:inherit;
  background:none; border:0; padding:0;
  cursor:pointer;
}
.theme-dot{
  width:4px; height:4px;
  background:currentColor;
  visibility:hidden;
}
.theme-opt.is-active .theme-dot{ visibility:visible; }
.theme-icon{ display:block; width:24px; height:24px; }

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

/* Desktop shows `---` on the first row (a wire out of the crest at
   left); mobile shows `|--` (a trunk down from the crest above). */
.m-only{ display:none; }

@media (max-width:719px){
  /* Figma 1:351 — asymmetric, 76 top / 62 bottom. Both are measured from
     the PHYSICAL SCREEN edges: that frame is 393x852 (the whole iPhone 15
     screen) and draws no status bar, so y=76 is 76px from the top of the
     device, not from the top of the web viewport. Those are only the same
     coordinate space because index.html sets viewport-fit=cover — without
     it iOS Safari hands back a viewport that already starts below the
     status bar, and these numbers land ~59px too low with a matching dead
     gap under the toggle. Kept as literal Figma values for exactly that
     reason: cover fixes the coordinate space, so nothing here needs to
     compensate for it. */
     The BOTTOM is the one number cover can't rescue. Figma's 62px is
     clearance from the screen edge, but on a phone that strip is mostly
     Safari's own URL bar — 100svh already ends above it, so a literal
     62px stacks a second gap on top of the browser's and the toggle
     floats well clear of the bottom. Subtracting the inset gives back
     roughly the margin the design actually intends; the max() floor
     keeps Figma's 62px intact anywhere the inset is 0 (Android, desktop
     narrow), so this only engages where it's needed. */
  .stage{
    padding:76px max(24px, env(safe-area-inset-right, 0px))
                 max(24px, calc(62px - env(safe-area-inset-bottom, 0px)))
                 max(24px, env(safe-area-inset-left,  0px));
  }
  .dir{ padding-bottom:0; }                 /* .stage owns it here */
  .tree-row{ flex-direction:column; align-items:flex-start; }
  .crest{ left:20px; margin-left:-100px; }  /* left-aligned in the box, per 1:353 */
  .tree{
    --track:-0.2px;                         /* still -0.01em, at 20px */
    font-size:20px;
    padding-top:0; padding-left:15px; width:100%;
  }
  .d-only{ display:none; }
  .m-only{ display:inline; }

  /* Drill-down, not two panes: the tree is replaced outright rather
     than shrunk, so `< back` is the only way up — and it maps to real
     history, same as the browser's own gesture. */
  .stage.is-open .dir,
  .stage.is-open .divider{ display:none; }
  .stage.is-open .pane-wrap{ width:100%; margin-left:0; flex:1 0 0; }

  /* Scoped OUT here rather than resized: Figma's mobile scroll column
     (4:444) reserves 14px alongside a NARROWER content column (345,
     not the full width .pane-wrap takes at this breakpoint) — nothing
     has asked for that squeeze yet, and native touch scrolling already
     works on .pane without it. Flagged in the plan rather than guessed. */
  .stage.is-open.has-scroll .scroll{ display:none; }

  .pane{ font-size:16px; letter-spacing:-0.16px; }
  .pane-title, .back{ font-size:20px; letter-spacing:-0.2px; }

  /* space-between is what pins the social links to the bottom edge
     (Figma 4:372) instead of letting them follow the wallet block. */
  .pane-inner{ gap:20px; padding:0 0 22px; justify-content:space-between; }
  .pane-blocks{ gap:20px; }

  /* Height is pinned to the back LABEL's 20px, not the row's tallest
     child. The toggle is 28px and Figma 4:333 never had it here — it
     arrives from the dir column, which is hidden at this breakpoint —
     so letting it size the row would push the title, photo, bio and
     wallets 8px down from where Figma puts them. It overhangs the row
     by 4px either side instead, which costs nothing: the block above
     is padding and the gap below is 20px. */
  .back-row{
    display:flex; align-items:center; justify-content:space-between;
    width:100%; height:20px;
  }

  .data-photo{ width:160px; height:160px; }
  /* Gauges drop under their labels here (Figma 4:376) — 2 lines of
     16px per row is the 32px pitch, and the wrap happens on the
     label's own trailing pad, so no extra markup is needed. */
  .stat{ gap:0; }
  .stat-row{ white-space:pre-wrap; }

  /* One blank line between wallets, which is 1 line-height at 16px. */
  .crypto-row + .crypto-row{ margin-top:16px; }
}

/* Until the crest has been measured and parked at centre screen there
   is nothing sensible to show — this avoids one frame of it sitting in
   the tree before the boot starts. */
body.measuring .crest, body.measuring .tree{ visibility:hidden; }

@media (prefers-reduced-motion:reduce){
  .crest{ transition:none; }
}
