:root{
  /* Neutral, deliberately. The greys used to lean blue, which put a second hue on a screen whose
     whole job is to let three diff colours be the only ones - and the sidebars are white rather than
     a lighter grey, so the wall reads as the thing being looked at and the columns as paper. */
  --well:#EAEAEB; --panel:#FFFFFF; --panel-2:#F4F4F5; --line:#E4E4E6; --line-2:#CFCFD2;
  --canvas:#F6F7F5; --canvas-dot:#CCD2C9;
  --canvas-dot-radius:1px; --canvas-grid:26px;
  --ink:#17181A; --ink-2:#5C5F63; --ink-3:#64676B;
  /* Graphite: the accent is ink, not a hue. Selection is the darkest thing on the page, which
     leaves colour to mean one thing only - a frame moved. See --new/--warn/--crit below. */
  --accent:#1F242B; --accent-soft:#E3E6EA; --accent-line:#B3B9C1;
  --new:#2563A8;   /* added: its own hue, because the accent no longer carries one */
  /* The secondary, and the only warm thing here. It carries identity - the mark, the tab you are
     on, the live chip - and it never appears on the canvas, because `changed` is amber and two
     warm marks in the same place would each weaken the other. */
  --gold:#DDBC16; --gold-soft:#FCF6CE;
  --warn:#9C7420; --crit:#B94A42;
  --shadow:0 1px 2px rgba(20,26,34,.10), 0 8px 24px -12px rgba(20,26,34,.22);
  --sans:"Pretendard Variable",system-ui,-apple-system,"SF Pro Text","Segoe UI Variable Text","Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  --mono:"IBM Plex Mono","Pretendard Variable",ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;

  /* One type scale, by role rather than by size, so "make it bigger" is one edit here instead of
     nineteen literals that drift apart. Every rule below references these.

     The canvas labels (track / step / frame) also use them, and those are inverse-zoom scaled to a
     constant screen size. The bands that hold them are a fraction of the frame (see .track below) and
     so do not grow with these steps: from about 40% zoom down, a label is taller than its band and
     lies over the frame above it, which is allowed. Growing type here therefore costs picture rather
     than layout - it does not move a single frame. */
  /* Pretendard roles: 400 body, 500 controls, 600 headings. */
  --t-title:16px;   /* the one heading in a region */
  --t-body:14px;    /* prose and list rows - the sizes you actually read */
  --t-body-2:13px;/* secondary prose: notes, frame names */
  --t-label:13px;   /* compact controls and labels */
  --t-meta:12px;    /* secondary metadata and technical identifiers */
  --t-micro:12px;   /* compact counts, never smaller than metadata */
  --t-prose:17px;
  --leading-prose:1.65;
  --leading-ko:1.75;
  --t-code:14px;
  --t-hero:44px;
  --t-hero-mobile:32px;
  --t-article:32px;
  --t-section:24px;
  color-scheme:light;
}
@media (prefers-color-scheme:dark){:root{
  --well:#121213; --panel:#1A1A1C; --panel-2:#212123; --line:#2A2A2D; --line-2:#3A3A3E;
  --canvas:#121213; --canvas-dot:#363D37;
  --ink:#EAEAEB; --ink-2:#9B9CA0; --ink-3:#909297;
  --accent:#E7EAEE; --accent-soft:#232931; --accent-line:#4A525C;
  --new:#5AA0E8;
  --gold:#DDBC16; --gold-soft:#302B0C;
  --warn:#C99640; --crit:#D5665C;
  --shadow:0 1px 2px rgba(0,0,0,.4), 0 12px 32px -14px rgba(0,0,0,.7);
  color-scheme:dark;
}}
:root[data-theme="light"]{
  --well:#EAEAEB; --panel:#FFFFFF; --panel-2:#F4F4F5; --line:#E4E4E6; --line-2:#CFCFD2;
  --canvas:#F6F7F5; --canvas-dot:#CCD2C9;
  --ink:#17181A; --ink-2:#5C5F63; --ink-3:#64676B;
  --accent:#1F242B; --accent-soft:#E3E6EA; --accent-line:#B3B9C1; --new:#2563A8;
  --gold:#DDBC16; --gold-soft:#FCF6CE;
  --warn:#9C7420; --crit:#B94A42;
  --shadow:0 1px 2px rgba(20,26,34,.10), 0 8px 24px -12px rgba(20,26,34,.22); color-scheme:light;
}
:root[data-theme="dark"]{
  --well:#121213; --panel:#1A1A1C; --panel-2:#212123; --line:#2A2A2D; --line-2:#3A3A3E;
  --canvas:#121213; --canvas-dot:#363D37;
  --ink:#EAEAEB; --ink-2:#9B9CA0; --ink-3:#909297;
  --accent:#E7EAEE; --accent-soft:#232931; --accent-line:#4A525C; --new:#5AA0E8;
  --gold:#DDBC16; --gold-soft:#302B0C;
  --warn:#C99640; --crit:#D5665C;
  --shadow:0 1px 2px rgba(0,0,0,.4), 0 12px 32px -14px rgba(0,0,0,.7); color-scheme:dark;
}

*{box-sizing:border-box}
/* The UA rule for `[hidden]` is `display:none`, and any author rule that sets `display` outruns it.
   That is easy to do by accident and the symptom is not "it is still visible" - it is a hidden
   element covering a sibling. `.review` set `display:flex`, so a hidden changeset sat over the whole
   canvas with its own background, and the deck read as blank with only labels showing. */
[hidden]{display:none !important}
html,body{height:100%}
/* Navigation and details keep their columns. The canvas has a stable toolbar for orientation
   and fit controls, so returning to the journey never requires opening a sidebar. */
body{
  margin:0; --col-l:var(--nav-width, 268px); display:grid; grid-template-columns:var(--col-l, 268px) 1fr var(--col-r, 308px); grid-template-rows:min-content minmax(0,1fr);
  background:var(--well); color:var(--ink); font:var(--t-body)/1.5 var(--sans); overflow:hidden;
  /* No `-webkit-font-smoothing: antialiased`. On macOS it thins every stroke - it is why this read
     as washed out - and the default rendering is what the rest of the OS looks like. */
  text-rendering:optimizeLegibility;
}
/* Korean chrome keeps words together while still yielding at a hard viewport edge. Captured app
   documents live in their own frames and are deliberately outside this language/layout policy. */
html:lang(ko) body{line-height:1.6}
html:lang(ko) :where(.rail,.ctx,.canvas-mobile,.dock,.modal-card,.preview,.review,.toast,.viewer-boot-error){
  word-break:keep-all; overflow-wrap:anywhere;
}
html:lang(ko) :where(.pill,.ctx-tabs,.zenbar,.mcp-clients,.rv-sides){max-width:100%}
html:lang(ko) #toolPill{max-width:100%; overflow-x:auto}
html:lang(ko) .mcp-bridge :is(strong,small){white-space:normal}
html:lang(ko) .mcp-command-wrap button{height:auto; min-height:30px; white-space:normal}
/* Local editing is the canvas plus its conversation. The explanatory context column is useful when
   reviewing a deck, but during an edit it only steals room from the thing changing. */
body.editing{--col-l:var(--nav-width, 320px); --col-r:0px}
body.editing .ctx{display:none}
body.editing #commentBtn{display:none}
/* Embedded as another page's work area: the canvas alone. Both columns collapse at boot (see
   `embed=stage` in viewer.js), so the camera never sees the wide shape. */
body.stage-only{--col-l:0px; --col-r:0px; grid-template-rows:100vh}
body.stage-only .stage, body.stage-only .review{grid-row:1}
body.stage-only .canvas-mobile{display:none}
body.stage-only .rail{display:none}
body.stage-only .ctx{display:none}
button{font:500 var(--t-body-2)/1.4 var(--sans); color:inherit; background:none; border:none; cursor:pointer}
:focus-visible{outline:2px solid var(--accent); outline-offset:2px; border-radius:4px}
.grow{flex:1}
.mono{font-family:var(--mono)}

.bar{
  grid-column:1/-1; display:flex; align-items:center; gap:10px; padding:0 12px;
  background:var(--panel); border-bottom:1px solid var(--line);
}
.mark{display:flex; align-items:center; gap:7px; font-weight:600; letter-spacing:-.01em; font-size:var(--t-title); color:inherit; text-decoration:none}
/* Only when it goes somewhere: opened from a folder there is no project page to reach. */
a.mark[href]{cursor:pointer}
a.mark[href]:hover span{text-decoration:underline}
.mark i{width:8px; height:8px; border-radius:2px; background:var(--accent)}
.chip{
  display:inline-flex; align-items:center; gap:6px; padding:2px 7px; border-radius:5px;
  border:1px solid var(--line-2); font-family:var(--sans); font-size:var(--t-meta); color:var(--ink-2);
}
.chip.live{border-color:var(--accent-line); color:var(--accent); background:var(--accent-soft)}
.chip.down{border-color:var(--warn); color:var(--warn); background:transparent}
.dot{width:6px; height:6px; border-radius:50%; background:currentColor}
.btn{
  height:26px; padding:0 9px; border-radius:6px; border:1px solid var(--line-2);
  color:var(--ink-2); font-size:var(--t-body);
}
.btn:hover{background:var(--panel-2); color:var(--ink)}

/* Explicit cells for all three, because grid places definitely-positioned items *before* auto ones
   regardless of DOM order. Giving `.review` a cell was enough to evict the auto-placed `.stage` into
   the next free one - the 232px rail column - and the canvas painted itself 232px wide. */
.rail{
  grid-column:1; grid-row:1/-1; display:flex; flex-direction:column; min-height:0;
  background:var(--panel); border-right:1px solid var(--line);
}
/* Two panes. The top one is short and scrolls on its own - a list of journeys is a handful of rows
   and should not push the screens below it off the bottom. */
.pane{display:flex; flex-direction:column; min-height:0}
.pane.pages{flex:0 0 auto; max-height:36vh}
.pane.layers{flex:1}
/* A heading reads as a word. Uppercase mono with tracking is a label for a machine-ish thing - an
   id, an axis, a route - and "Flows" is none of those.
   It is also the pane's own control, so it is a band with room to press rather than a caption. */
.pane>h2{flex:none; margin:0}
.pane-hd{
  display:flex; align-items:center; gap:6px; width:100%; text-align:left;
  padding:13px 14px 9px; border-radius:0;
  font:600 var(--t-body-2)/1.5 var(--sans); letter-spacing:normal; color:var(--ink);
}
.pane-hd:hover{background:var(--panel-2)}
.pane-hd .tw{transform:rotate(90deg)}
.pane.shut .pane-hd .tw{transform:none}
.pane.shut .pane-list{display:none}
.pane.shut{flex:0 0 auto}
/* Shut, it is a band; open, it is a band and a list - so the border belongs to the section. */
.pane.pages{border-bottom:1px solid var(--line)}
/* No right padding: a selected row runs to the edge of the column, the way a row in a list does.
   The gap on the right used to leave the fill floating in the middle of the panel. */
.pane-list{flex:1; overflow-y:auto; padding:0 0 14px 10px}
.pane-list .empty{margin:2px 6px; font-size:var(--t-body-2); color:var(--ink-3)}
/* A flow row: the whole row is the target, and exactly one is current. */
.pg{
  display:flex; align-items:center; gap:8px; width:100%; text-align:left;
  padding:6px 18px 6px 10px; border-radius:7px 0 0 7px; color:var(--ink); font-size:var(--t-body-2);
}
.pg .n{flex:1; min-width:0; white-space:normal; overflow-wrap:anywhere}
.pg .r{flex:none; font-family:var(--mono); font-size:var(--t-micro); color:var(--ink-2)}
.pg:hover{background:var(--panel-2); color:var(--ink)}
/* Three depths of the same answer, told apart by how much colour is in the fill rather than by a
   mark beside it: the flow is a wash of ink, the screen a wash of gold, the state solid gold. Read
   top to bottom the tint gets stronger, which is also the order of how specific each one is.
   `color-mix` rather than a literal rgba so the ink follows the theme - 20% black on a dark panel is
   nothing at all. */
.pg[aria-current="true"]{
  background:color-mix(in srgb, var(--ink) 8%, transparent); color:var(--ink); font-weight:600;
}
/* A screen, and its states as children it can disclose. */
/* No rule down the side: the indent already says these belong to the screen above, and a hairline
   next to a filled row reads as a box drawn around nothing. */
.ly .states{display:none; margin:1px 0 3px}
.ly.open .states{display:block}
.ly.open .tw{transform:rotate(90deg)}
.tw{width:12px; height:12px; flex:none; color:var(--ink-2); transition:transform 90ms ease;
    display:inline-flex; align-items:center; justify-content:center}
.tw .chev{width:11px; height:11px}
.st{
  /* Sans, like every other row here. Mono is for things you compare character by character - an id,
     a route, a hash. A state's name is read, not scanned. */
  display:block; width:100%; text-align:left; padding:5px 18px 5px 38px; border-radius:7px 0 0 7px;
  font:var(--t-meta)/1.5 var(--sans); color:var(--ink-2);
}
.st:hover{background:var(--panel-2); color:var(--ink)}
.st[aria-current="true"]{
  background:color-mix(in srgb, var(--gold) 20%, transparent); color:var(--ink); font-weight:500;
}
/* What you have open. Not a bar: it is the head of the thing that navigates it, which is where a
   file's name belongs. */
.who{flex:none; padding:13px 14px 14px; border-bottom:1px solid var(--line); background:var(--panel-2); position:relative}
.who-1{display:flex; align-items:center; gap:7px}
.who .mark{display:flex; align-items:center; gap:7px; font-weight:600; font-size:var(--t-body); letter-spacing:-.01em; min-width:0}
.who .mark i{width:8px; height:8px; border-radius:2px; background:var(--gold); flex:none}
.who .mark span{overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.who .size{display:block; margin-top:5px; font:var(--t-meta)/1 var(--sans); color:var(--ink-2)}
.ai-follow{display:flex; flex-direction:column; align-items:stretch; gap:6px; min-width:0; margin-top:11px}
.ai-follow[hidden]{display:none}
.ai-follow button{
  display:flex; align-items:center; justify-content:center; gap:7px; width:100%; padding:7px 9px; border-radius:7px;
  border:1px solid var(--line-2); background:var(--panel); color:var(--ink-2);
  font:var(--t-body-2)/1 var(--sans); font-weight:500;
}
.ai-follow button:hover{border-color:var(--ink-3); color:var(--ink)}
.ai-follow button svg{width:15px; height:15px; flex:none}
.ai-follow button[aria-pressed="true"]{
  background:var(--gold); border-color:color-mix(in srgb, var(--gold) 72%, #000); color:#17181A;
  box-shadow:0 1px 0 rgb(255 255 255 / .32) inset, 0 4px 12px -7px color-mix(in srgb, var(--gold) 76%, #000);
}
.ai-follow>span{
  min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding:0 2px;
  font:var(--t-micro)/1.2 var(--sans); color:var(--ink-2);
}
.who .live{display:flex; align-items:center; gap:5px; margin-top:6px; font:var(--t-micro)/1 var(--sans); color:var(--gold)}
.who .live[hidden]{display:none}
.who .live .dot{width:5px; height:5px; border-radius:50%; background:var(--gold); flex:none}
.who .live.down{color:var(--crit)}
.who .live.down .dot{background:var(--crit)}
.who select{
  display:block; width:100%; margin-top:7px; font:var(--t-body-2)/1 var(--sans); padding:5px 6px;
  border-radius:6px; border:1px solid var(--line-2); background:var(--panel); color:var(--ink);
}
.who>a{display:block; margin-top:5px; font:var(--t-micro)/1 var(--mono); color:var(--ink-3); text-decoration:none}
.who>a:hover{color:var(--accent)}

/* The editing conversation is part of the left rail, immediately below the workspace identity.
   It deliberately uses plain messages rather than bubbles: this is a working log, not another app
   nested inside Mockspace. */
.edit-chat{
  display:flex; flex:0 0 min(38vh, 360px); min-height:190px; flex-direction:column;
  border-bottom:1px solid var(--line); background:var(--panel);
}
.edit-chat[hidden]{display:none}
.edit-chat-hd{display:flex; align-items:center; gap:8px; padding:11px 12px 7px}
.edit-chat-hd h2{margin:0; font-size:var(--t-body-2); font-weight:600}
.edit-chat-hd span{margin-left:auto; font:var(--t-micro)/1 var(--sans); color:var(--ink-3)}
.edit-chat-hd span.live{color:var(--gold)}
.edit-chat-messages{
  flex:1; min-height:0; overflow-y:auto; padding:2px 12px 9px; display:flex; flex-direction:column; gap:10px;
}
.edit-chat-empty{margin:auto 0; color:var(--ink-3); font-size:var(--t-body-2); line-height:1.5}
.edit-message{display:flex; flex-direction:column; gap:2px}
.edit-message .meta{display:flex; gap:6px; font:var(--t-micro)/1.3 var(--sans); color:var(--ink-3)}
.edit-message[data-role="user"] .meta strong{color:var(--ink)}
.edit-message[data-role="agent"] .meta strong{color:var(--gold)}
.edit-message .copy{margin:0; white-space:pre-wrap; overflow-wrap:anywhere; font-size:var(--t-body-2); line-height:1.45}
.edit-message .artifacts{font:var(--t-micro)/1.4 var(--mono); color:var(--ink-3); overflow-wrap:anywhere}
.edit-chat form{display:grid; grid-template-columns:1fr auto; gap:7px; padding:8px; border-top:1px solid var(--line)}
.edit-chat textarea{
  min-width:0; resize:none; padding:8px 9px; border-radius:7px; border:1px solid var(--line-2);
  background:var(--panel-2); color:var(--ink); font:var(--t-body-2)/1.4 var(--sans);
}
.edit-chat textarea:focus{outline:2px solid var(--accent-line); outline-offset:0}
.edit-chat form button{
  align-self:stretch; padding:0 12px; border-radius:7px; background:var(--accent); color:var(--panel);
  font-size:var(--t-body-2); font-weight:500;
}
.edit-chat form button:disabled{opacity:.45; cursor:default}
body.editing .pane.pages{max-height:22vh}
.scr{
  display:flex; align-items:center; gap:7px; width:100%; text-align:left;
  padding:5px 18px 5px 10px; border-radius:7px 0 0 7px; color:var(--ink); font-size:var(--t-body-2);
}
.scr:hover{background:var(--panel-2); color:var(--ink)}
/* The screen carries the stronger gold and its state the lighter one: the screen is what you picked,
   and the state is which of its versions is showing. Strength follows what was chosen, not how deep
   the row sits. */
.scr[aria-current="true"]{
  background:color-mix(in srgb, var(--gold) 46%, transparent); color:var(--ink); font-weight:500;
}
.scr .n{flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.scr .r{font-family:var(--mono); font-size:var(--t-micro); color:var(--ink-2)}

/* Dots stay airy at overview zoom: fixed screen-space spacing, with the camera's pan offset.
   The pattern belongs behind captures, never on chrome or inside their documents. */
.stage,.review{
  background-color:var(--canvas);
  background-image:radial-gradient(circle at var(--canvas-dot-radius) var(--canvas-dot-radius), var(--canvas-dot) var(--canvas-dot-radius), transparent 0);
  background-size:var(--canvas-grid) var(--canvas-grid);
}
.stage{
  grid-column:2;
  /* `clip`, not `hidden`. `hidden` still makes this a scroll container, and anything that scrolls an
     element into view - a focus, a selection - scrolls it *programmatically* while the camera keeps
     positioning the plane by transform. The two then add up, and the deck lands a frame off. `clip`
     creates no scroll container at all, so the transform is the only thing that can move the plane. */
  /* `min-width:0` explicitly: a grid item's automatic minimum is its content, and `overflow:clip`
     does not waive that the way `hidden` does - so the middle column grew to the width of the whole
     deck and pushed the right column off the screen. */
  position:relative; overflow:clip; min-width:0; min-height:0;
  touch-action:none;
}
/* Figma-style collaboration halo: following is a viewport mode, so its strongest signal belongs
   around the viewport rather than inside a small sidebar control. The inset ring never changes the
   canvas geometry and never intercepts a pan or click. */
.stage.following-ai::after{
  content:""; position:absolute; inset:0; z-index:24; pointer-events:none;
  box-shadow:inset 0 0 0 3px var(--gold), inset 0 0 0 4px color-mix(in srgb, var(--gold) 42%, transparent);
}
.ai-canvas-badge{
  position:absolute; top:0; left:50%; z-index:25; transform:translateX(-50%);
  display:flex; align-items:center; gap:7px; max-width:min(70%, 560px); padding:6px 11px 7px;
  border:0; border-radius:0 0 8px 8px; background:var(--gold); color:#17181A;
  box-shadow:0 5px 16px -10px #000; cursor:pointer;
  font:var(--t-meta)/1 var(--sans);
}
.ai-canvas-badge[hidden]{display:none}
/* Broken by a hand on the camera: the same badge, quiet, is the way to hand it back. */
.ai-canvas-badge[data-on="false"]{ background:var(--gold-soft); color:var(--ink); box-shadow:none }
.ai-canvas-badge[data-on="false"] i{ background:transparent; box-shadow:inset 0 0 0 1.5px currentColor }
.ai-canvas-badge i{width:7px; height:7px; border-radius:50%; background:#17181A; flex:none}
.ai-canvas-badge strong{font-weight:600; white-space:nowrap}
.ai-canvas-badge span{min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; opacity:.72}
.stage.ai-moving::after{animation:ai-halo-arrive 520ms ease-out}
@keyframes ai-halo-arrive{
  0%{box-shadow:inset 0 0 0 7px var(--gold), inset 0 0 28px color-mix(in srgb, var(--gold) 34%, transparent)}
  100%{box-shadow:inset 0 0 0 3px var(--gold), inset 0 0 0 4px color-mix(in srgb, var(--gold) 42%, transparent)}
}
@media (prefers-reduced-motion:reduce){.stage.ai-moving::after{animation:none}}
.stage.grab{cursor:grab}
.stage.grabbing{cursor:grabbing}
.stage.grabbing iframe{pointer-events:none}
/* ---------------- the bands the labels sit in ----------------
   Each level owns a band above it - the flow title, the screen title, each frame's state label -
   and the band is a *fraction of the frame*, which is a plane-space number and therefore the same
   at every zoom. `--rw` is the row's widest frame (set in renderPlane), `--fw` the column's own.

   They used to be `calc(40px * var(--iz))`: a constant number of *screen* pixels, so the gap above a
   frame was a thirtieth of the frame at 100% and a third of one at 10%. Zooming in did not enlarge
   the deck, it squeezed it - the horizontal gaps below are plane-space and held their proportion
   while the vertical ones collapsed, and every frame slid out from under the pointer on the way.
   That is also what made plane layout a function of the zoom, and so what forced the two-pass
   measure in `frameTo`/`fitWidth`, the pin re-derivation, and a layout allowed to lag the camera.
   None of those exist any more.

   The fractions are the old screen-pixel bands taken at 42% - the zoom a deck is actually read at -
   so the deck keeps the proportions it had there and merely holds them everywhere else.

   The labels themselves are still a constant size on screen (`scale(var(--iz))` below), which is the
   point of them: this deck is read by name. Below about 40% they are therefore taller than the band
   that holds them and lie over the frame above. That is the deliberate trade - a name you can read
   over a picture, rather than a picture spaced apart to make room for a name. */
.plane{position:absolute; top:0; left:0; transform-origin:0 0; display:flex; flex-direction:column; gap:64px}
.track{
  --band-track:calc(var(--rw, 1440) * 1px * .066);
  --band-step:calc(var(--rw, 1440) * 1px * .083);
  position:relative; display:flex; flex-direction:column; padding-top:var(--band-track);
}
.track-row{display:flex; align-items:flex-start; gap:56px}

/* ---------------- the walkable prototype ----------------
   Full screen, the way a preview is. Reading a deck and walking an app are two postures, not two
   panes - so this covers the deck rather than sitting beside it, and `esc` gives the deck back. */
#tracks{display:contents}
.preview{
  position:fixed; inset:0; z-index:60; display:flex; flex-direction:column;
  background:var(--well);
}
.preview[hidden]{display:none}
.proto-hd{
  display:flex; align-items:center; gap:10px; flex:none; height:44px; padding:0 12px;
  background:var(--panel); border-bottom:1px solid var(--line);
}
.proto-hd .n{font-weight:600; font-size:var(--t-title); letter-spacing:-.01em}
.proto-hd .grow{flex:1}
.proto-hd .live{
  display:inline-flex; align-items:center; gap:5px; font:var(--t-micro)/1 var(--sans);
  padding:2px 7px; border-radius:5px;
  background:var(--accent-soft); color:var(--accent); border:1px solid var(--accent-line);
}
.preview.unconfigured .proto-hd .live{background:none; color:var(--warn); border-color:var(--warn)}
.proto-hd button{
  font:var(--t-body-2)/1 var(--sans); padding:5px 9px; border-radius:6px; cursor:pointer;
  border:1px solid var(--line-2); background:var(--panel-2); color:var(--ink-2);
}
.proto-hd button:hover{border-color:var(--accent-line); color:var(--accent)}
/* Said in the chrome and not in a poster nobody rereads: a submit lands on a page rendered from
   static mock data, and that will otherwise be filed as a bug. */
.proto-foot{font:var(--t-micro)/1 var(--sans); color:var(--ink-3)}
/* The trail, in the deck's own names for the pages. A step onto a path the deck does not cover shows
   muted rather than being dropped - "the walk left the deck" is worth seeing. */
.proto-crumbs{
  display:flex; align-items:center; gap:6px; min-width:0; max-width:52ch; overflow-x:auto;
  scrollbar-width:none; font:var(--t-meta)/1.6 var(--sans); color:var(--ink-3);
}
.proto-crumbs::-webkit-scrollbar{display:none}
.proto-crumbs .c{white-space:nowrap}
.proto-crumbs .c.at{color:var(--ink); font-weight:500}
.proto-crumbs .c.off{font-style:italic}
.proto-crumbs .sep{color:var(--line-2)}
/* At the width the workspace declared, centred - a preview shows the app, not the window. */
.proto-body{position:relative; flex:1; min-height:0; display:grid; justify-items:center; background:var(--well)}
.proto-body>iframe{
  width:min(var(--proto-w, 1440px), 100%); height:100%; border:0;
  background:var(--panel); border-left:1px solid var(--line); border-right:1px solid var(--line);
}
.proto-poster{
  position:absolute; inset:0; z-index:2; display:flex; flex-direction:column; align-items:center;
  justify-content:center; gap:9px; text-align:center; padding:32px; cursor:pointer;
  background:var(--well); color:var(--ink-2); font-size:var(--t-body);
}
.proto-poster b{color:var(--ink); font-size:21px; font-weight:600}
.proto-poster code{font-family:var(--mono); background:var(--panel-2); padding:1px 6px; border-radius:4px}
.proto-poster .p{color:var(--ink-3); max-width:52ch}
/* Gone once mounted, not merely while open: reopening must land on the walk, not on "click to start". */
.preview.mounted .proto-poster{display:none}

/* Where the walk is, marked on the deck behind it. Distinct from selection and from a diff mark:
   this one says "the live app is on this screen right now". */
.fr.walking{outline:2px solid var(--accent); outline-offset:3px}

/* a step is one column of the row: the screen, with its states stacked underneath */
/* The state gap is a fraction of the column's *own* frame rather than the row's, so a narrow
   column keeps its own rhythm; the title band is the row's, because a band that differed per column
   would leave the frames across a row starting at different heights. */
.step{
  --band-state:calc(var(--fw, 1440) * 1px * .053);
  position:relative; flex:none; display:flex; flex-direction:column;
  gap:var(--band-state); padding-top:var(--band-step);
}
/* ---------------- the label stack ----------------
   Three labels hang off the same edge - the frame's state chip, the screen above it, the flow above
   that - and each is a constant size on screen while the bands between them are plane-space. So a
   band deep enough to stack them at 100% is nowhere near deep enough at 13%, and they land on each
   other: two labels on top of each other cost both of them, where a label over a *picture* costs
   neither. Hence the stack is spaced in screen pixels rather than left to the bands - each one is
   hung off the edge below it and lifted clear of the one under it by that one's height on screen.
   `scale()` comes first in the transform, so the translations that follow are in screen units.
   The three lifts (0 / 22 / 44) are the stacking order, and the only reason they are literals is
   that what has to clear a chip is the chip's height, which CSS cannot ask for. */
.step-hd{
  position:absolute; top:var(--band-step); left:0;
  transform-origin:0 0; transform:scale(var(--iz)) translateY(-100%) translateY(-22px);
  display:flex; align-items:baseline; gap:7px; white-space:nowrap;
  max-width:calc(var(--fw) * var(--z) * 1px); overflow:hidden;
}
.step-hd .n{font-weight:500; font-size:var(--t-body); color:var(--ink)}
.step-hd .r{font-family:var(--mono); font-size:var(--t-meta); color:var(--ink-3); overflow:hidden; text-overflow:ellipsis}
.step-hd .c{
  font-family:var(--sans); font-size:var(--t-micro); padding:1px 5px; border-radius:4px;
  border:1px solid var(--line-2); color:var(--ink-3); flex:none;
}
/* Hung off the top of the row rather than off the top of the track, for the same reason: what it has
   to stay clear of is the screen title, and that is anchored to the row too. */
.track-hd{
  position:absolute; top:var(--band-track); left:0;
  transform-origin:0 0; transform:scale(var(--iz)) translateY(-100%) translateY(-44px);
  display:flex; align-items:baseline; gap:10px; white-space:nowrap;
}
.track-hd .n{font-weight:600; font-size:var(--t-title); letter-spacing:-.01em}
.track-hd .fx{
  font-family:var(--sans); font-size:var(--t-meta); padding:1px 6px; border-radius:5px;
  background:var(--accent-soft); color:var(--accent); border:1px solid var(--accent-line);
}
.track-hd .d{font-family:var(--sans); font-size:var(--t-meta); color:var(--ink-3)}

.fr{position:relative; flex:none}
.fr-hd{
  position:absolute; bottom:100%; left:0; z-index:2; transform-origin:0 100%; transform:scale(var(--iz));
  display:flex; align-items:baseline; gap:7px; padding-bottom:5px; white-space:nowrap;
  max-width:calc(var(--fw) * var(--z) * 1px); overflow:hidden; cursor:pointer;
}
.fr-hd .n{font-weight:500; font-size:var(--t-body-2); color:var(--ink-2)}
.fr.sel>.fr-hd .n{color:var(--accent)}
.fr-hd .st{
  font-family:var(--sans); font-size:var(--t-meta); padding:1px 6px; border-radius:5px; flex:none;
  background:var(--panel-2); border:1px solid var(--line-2); color:var(--ink-2);
}
.fr.sel>.fr-hd .st{background:var(--accent-soft); border-color:var(--accent-line); color:var(--accent); font-weight:500}
.fr-hd .r{font-family:var(--mono); font-size:var(--t-meta); color:var(--ink-3); overflow:hidden; text-overflow:ellipsis}
.fr-body{
  position:relative; background:var(--panel); overflow:hidden; box-shadow:var(--shadow);
  outline:calc(1px * var(--iz)) solid var(--line);
}
/* Selection is a ring *outside* the frame, set apart by a gap - so it reads as "this one" from across
   the canvas rather than as a border the screen happens to have. It is a `box-shadow` and not an
   `outline` because the outline is the diff's channel: `.fr.sel` used to set outline-color, which
   meant clicking a changed frame to look at it erased the very mark that said it changed. */
.fr.sel>.fr-body{
  box-shadow:var(--shadow),
    0 0 0 calc(2px * var(--iz)) var(--well),
    0 0 0 calc(4px * var(--iz)) var(--accent);
}
/* AI sight is independent of selection. With Follow off it remains a quiet dashed locator while
   the human pans elsewhere; with Follow on the normal selection ring may sit over it. */
.fr.ai-target>.fr-body::before{
  content:"AI"; position:absolute; z-index:4; top:calc(7px * var(--iz)); left:calc(7px * var(--iz));
  padding:calc(2px * var(--iz)) calc(5px * var(--iz)); border-radius:calc(4px * var(--iz));
  background:var(--gold); color:#17181A; pointer-events:none;
  font:600 calc(var(--t-micro) * var(--iz))/1 var(--sans);
}
.fr.ai-target>.fr-body{
  box-shadow:var(--shadow),
    0 0 0 calc(2px * var(--iz)) var(--well),
    0 0 0 calc(5px * var(--iz)) var(--gold);
}
.track.ai-target>.track-hd .n{color:var(--gold)}

/* The cheap level of detail. Sits under the iframe layer so promoting a frame to live just
   mounts the iframe on top - the picture never flashes away, and demoting reveals it again
   rather than an empty box. object-fit:none + top-left origin so the image lines up 1:1 with
   where the live frame would be. */
.fr-body{position:relative; background:transparent}
/* Unfocused frames are not elements at all any more - they are pixels on #paint, one surface for
   the whole canvas. So a frame body is an empty transparent box with an outline, and the only
   document on the page is the frame you clicked. */
.fr-body>iframe{position:relative; z-index:1}
/* An unmounted iframe still paints an opaque white box, which would cover the paint layer
   showing through. Hide it until it actually has something to show. */
.fr-body>iframe:not([src]){display:none}
/* Keep the painted capture visible while its inert HTML document loads above it. */
body[data-mockspace-static="1"] .fr-body>iframe[src]:not([data-snapshot-ready]){opacity:0}

/* One surface, screen-space, behind the plane. Never hit-tested: the frames' own veils take
   clicks, and this has no elements to hit anyway. */
#paint{
  position:absolute; inset:0; width:100%; height:100%;
  pointer-events:none; z-index:0; display:block;
}
.plane{position:relative; z-index:1}
/* A frame with no thumbnail yet and not live: a plain surface, not a broken one. */
.fr.thumb>.fr-body>img.thumb-img:not([src]){background:
  repeating-linear-gradient(45deg, var(--bg-2) 0 6px, var(--bg-3, var(--bg-2)) 6px 12px)}
.fr-body iframe{display:block; border:0; width:100%; height:100%; background:#fff}
/* review: a status stripe on the frame, and an onion-skin overlay for before/after */
/* Three statuses, told apart by pattern as well as colour - "added" used to share the selection
   colour, so the two were indistinguishable. */
.fr.d-changed>.fr-body{outline:calc(2px * var(--iz)) solid var(--warn)}
.fr.d-added>.fr-body{outline:calc(2px * var(--iz)) dashed var(--new)}
.fr.d-removed>.fr-body{outline:calc(2px * var(--iz)) solid var(--crit); opacity:.5}

/* A corner flag on the frame itself, counter-scaled so it is the same size at 5% and at 100%. The
   text chip lives in `.fr-hd`, which is clipped at low zoom - at 4% only one of ten changed frames
   still showed its label, which is where "the mark is wrong" came from. */
.fr.d-changed>.fr-body::after,
.fr.d-added>.fr-body::after,
.fr.d-removed>.fr-body::after{
  content:""; position:absolute; top:0; right:0;
  width:calc(11px * var(--iz)); height:calc(11px * var(--iz));
  clip-path:polygon(100% 0, 0 0, 100% 100%);
}
.fr.d-changed>.fr-body::after{background:var(--warn)}
.fr.d-added>.fr-body::after{background:var(--new)}
.fr.d-removed>.fr-body::after{background:var(--crit)}
/* A real visual diff, composited by the browser: stack before and after, blend the top one
   with `difference`. Identical pixels cancel to black; anything that moved lights up. Catches
   what the fingerprints cannot - a shifted box, a changed CSS rule, a different font metric. */
.fr-body.onion{background:#000}
.fr-body.onion iframe{position:absolute; inset:0}
.fr-body.onion iframe:not(.basef){mix-blend-mode:difference}
.fr-hd .dchip{
  font-family:var(--sans); font-size:var(--t-micro); padding:1px 5px; border-radius:4px; flex:none;
  background:var(--warn); color:var(--panel); font-weight:500;
}
.fr.d-added .fr-hd .dchip{background:var(--new)}
.fr.d-removed .fr-hd .dchip{background:var(--crit)}
.pill .dsum{font-family:var(--sans); font-size:var(--t-meta); color:var(--ink-2); padding:0 6px}
.pill button[disabled]{opacity:.35; cursor:not-allowed}
/* Published snapshots may mount before selection; their iframe layer must not take the canvas's input. */
.fr-veil{position:absolute; inset:0; z-index:2; background:transparent} /* clicks select the frame, not the app */
.fr.sel .fr-veil{pointer-events:none}                          /* selected frame is interactive */

/* Folding is a width, so one variable does it and everything positioned against a column follows.
   The columns are still in the DOM and still rendered - unfolding must not cost a re-render, and a
   hidden column that has to be rebuilt is a column that flickers. */
/* Full screen: both columns go, and what mattered about them floats over the canvas instead. */
body.zen{--col-l:0px; --col-r:0px}
.rail, .ctx{min-width:0; overflow:clip; position:relative}
body.zen .rail, body.zen .ctx{border:none}
/* `.zenbar`, not `.zen` - the body carries `zen` as its state, and a bare `.zen` rule matched it too,
   turning the page itself into a fixed flex box. */
.zenbar{
  position:fixed; top:10px; z-index:26; display:flex; align-items:center; gap:6px; padding:5px 8px;
  background:var(--panel); border:1px solid var(--line); border-radius:10px; box-shadow:var(--shadow);
}
.zenbar[hidden]{display:none}
.zen-l{left:10px}
.zen-r{right:10px}
.zenbar .mark{display:flex; align-items:center; gap:7px; font-size:var(--t-body-2); font-weight:600; color:var(--ink)}
.zenbar .mark i{width:8px; height:8px; border-radius:2px; background:var(--gold); flex:none}
.zenbar button{
  display:flex; align-items:center; justify-content:center; width:26px; height:26px;
  border-radius:6px; border:none; background:none; color:var(--ink-2);
}
.zenbar button svg{width:15px; height:15px; display:block}
.zenbar button:hover{background:var(--panel-2); color:var(--ink)}
.zenbar button.text{
  width:auto; gap:6px; padding:0 10px; font-size:var(--t-body-2); font-weight:400; color:var(--ink);
  border:1px solid var(--line-2);
}
/* A panel with one side shaded, not a chevron. `‹` says "left", which is a direction; this says
   which thing is about to go away. */
.fold{
  position:absolute; top:11px; z-index:5; display:flex; align-items:center; justify-content:center;
  width:26px; height:26px; border-radius:7px; color:var(--ink-2); border:1px solid transparent;
}
.fold svg{width:18px; height:18px; display:block}
.chev{width:13px; height:13px; display:block}
.fold:hover{color:var(--ink); background:var(--panel); border-color:var(--line-2)}
/* Each fold sits on the edge its column disappears towards, so it is never where the column's own
   controls are - the right one used to land on top of the first action icon. */
.rail>.fold{right:6px}

/* Top right, over the canvas. Walking is the one thing on this screen that is not reading, so it is
   not in the row of controls that are about reading. */
.walkbtn{
  position:fixed; top:12px; right:320px; z-index:22; display:flex; align-items:center; gap:7px;
  padding:7px 12px; border-radius:8px; font-size:var(--t-body-2); font-weight:500;
  background:var(--panel); color:var(--ink); border:1px solid var(--line-2); box-shadow:var(--shadow);
}
.walkbtn[hidden]{display:none}
.walkbtn i{width:6px; height:6px; border-radius:50%; background:var(--accent); flex:none}
.walkbtn:hover{border-color:var(--accent-line); color:var(--accent)}

/* Centred on the canvas rather than balanced between its neighbours: the zoom pill and the review
   pill are different widths, so equal spacers put this slightly off every time. */
#toolPill{position:absolute; left:50%; transform:translateX(-50%)}

.dock{
  position:fixed; left:calc(var(--col-l, 268px) + 12px); right:calc(var(--col-r, 308px) + 12px);
  bottom:10px; display:flex; align-items:flex-end; gap:8px;
  pointer-events:none; z-index:20;
}
.dock>*{pointer-events:auto}
/* The two things you do to what is on the canvas. Bigger than a toolbar chip, because they are the
   only two and the bar is otherwise empty; gold when one is armed, the way everything else here says
   "this is on". */
#toolPill{padding:5px}
#toolPill button[id]{
  display:flex; align-items:center; justify-content:center; gap:7px; height:34px; padding:0 13px;
  border-radius:8px; color:var(--ink); white-space:nowrap;
  /* The pill's own rule is mono, which is right for a zoom percentage and wrong for a verb. */
  font:var(--t-body-2)/1 var(--sans); font-weight:400;
}
#toolPill button[id] svg{width:15px; height:15px; display:block; flex:none}
/* The camera's own controls, quieter than the two verbs beside them - they are adjustments, not
   things you set out to do. */
#toolPill .z{
  min-width:max(30px, 5ch); height:34px; padding:0 8px; border-radius:8px;
  font:var(--t-body-2)/1 var(--sans); color:var(--ink-2);
  /* A zoom value is one token: Korean's overflow-wrap:anywhere must not split "100%" into two lines. */
  white-space:nowrap; flex:none;
}
#toolPill .z:hover{background:var(--panel-2); color:var(--ink)}
#toolPill .sep{width:1px; height:20px; background:var(--line); margin:0 4px}
#toolPill button[id][hidden]{display:none}
#toolPill button[id][aria-pressed="true"]{
  background:color-mix(in srgb, var(--gold) 46%, transparent); color:var(--ink);
}

/* Embedded fit and zoom controls share the same dock, including at narrow widths. */
body.stage-only #toolPill { width: max-content; max-width: calc(100vw - 1rem); flex-wrap: wrap; justify-content: center }
body.stage-only #toolPill :is(.canvas-fit, .canvas-zoom) { display: flex; align-items: center; gap: .125rem; flex: none }
body.stage-only #toolPill .canvas-fit button { flex: none; min-height: 2.125rem; border: 0 }
body.stage-only.inspecting #toolPill :is(.canvas-fit, .canvas-zoom-separator) { display: none }
@media (max-width: 31rem) {
  body.stage-only #toolPill .canvas-zoom-separator { display: none }
  body.stage-only #toolPill button, body.stage-only #toolPill .canvas-fit button { min-height: 2.75rem }
}

/* The view switch, in the column it changes the contents of. */
.viewsw{display:flex; gap:3px; padding:10px 10px 0; flex:none}
.viewsw[hidden]{display:none}
.viewsw button{
  flex:1; padding:7px 8px; border-radius:7px; font-size:var(--t-body-2); color:var(--ink-2);
}
.viewsw button:hover{background:var(--panel-2); color:var(--ink)}
.viewsw button[aria-pressed="true"]{
  background:color-mix(in srgb, var(--gold) 46%, transparent); color:var(--ink); font-weight:500;
}

/* Which version is open, and what it is measured against. Two lines: the names, then when and how
   much moved - the second is context for the first, not a peer of it. */
.build{display:flex; align-items:center; flex-wrap:wrap; gap:0 6px; padding:2px 0 0}
.build .t{font-size:var(--t-body-2); color:var(--ink); font-weight:500; letter-spacing:-.005em}
.build input.t{
  flex:1; min-width:0; padding:1px 5px; border:1px solid var(--accent-line); border-radius:5px;
  background:var(--panel); color:var(--ink); font-family:inherit; font-weight:500;
}
.build .w{flex-basis:100%; font:var(--t-micro)/1.5 var(--sans); color:var(--ink-3)}
.build .pen{
  display:flex; padding:2px; border:0; border-radius:5px; background:none; color:var(--ink-3); cursor:pointer;
}
.build .pen svg{width:13px; height:13px; display:block}
.build .pen:hover{background:var(--panel-2); color:var(--ink)}
.build .pen[hidden]{display:none}

.pill{
  display:flex; align-items:center; gap:2px; padding:3px; border-radius:9px;
  background:var(--panel); border:1px solid var(--line-2); box-shadow:var(--shadow);
}
/* `hidden` is only a UA-stylesheet display:none, so the explicit display above outranks it. Without
   this the comment pill stayed on screen in a baked snapshot - where there is no server to write a
   comment to - offering a control that could not work. Same reason .ctx[hidden] is spelled out. */
.pill[hidden]{display:none}
.pill button{
  height:24px; min-width:26px; padding:0 8px; border-radius:6px; color:var(--ink-2);
  font-family:var(--sans); font-size:var(--t-body-2);
}
.pill button:hover{background:var(--panel-2); color:var(--ink)}
.pill button[aria-pressed="true"]{background:var(--accent-soft); color:var(--accent); font-weight:500}
.pill .lbl{padding:0 6px; font:var(--t-label)/1 var(--sans); letter-spacing:normal; text-transform:none; color:var(--ink-3)}
.pill .sep{width:1px; align-self:stretch; background:var(--line); margin:2px 3px}
.zval{min-width:44px}


/* the frame's design context - the only panel, and only when a frame is selected */
.ctx{
  grid-column:3; overflow-y:auto; padding:14px 18px 60px;
  background:var(--panel); border-left:1px solid var(--line);
}
.ctx[hidden]{display:none}
/* Nothing selected is a state worth rendering, not a reason to leave a column empty. */
.ctx .nosel{color:var(--ink-3); font-size:var(--t-body-2); margin:2px 0 0; line-height:1.5}
/* Marks, not sentences. Three verbs spelled out read as a paragraph to parse; these are pressed. */
/* No box around them. Three outlined squares in a row read as a toolbar to be inspected; these are
   marks you press, and the hover is where the affordance belongs. */
/* Two things to read about one frame. */
/* Selected is a fill, not a rule under one word. The rest of this screen already says "here" with a
   wash of gold - the screen you picked, the state that is showing - and an underline was a second
   grammar for the same idea. The separator stays, but it belongs to the row: it divides the two
   things you can read from the reading itself, and runs the full width to say so. */
/* The row is inset by the column's padding, and then each pill adds its own - so the tab's *text*
   started 11px further in than every heading below it. Pull the row back by exactly the pill's
   padding so the two left edges are one line. The separator still runs the full width, because it
   belongs to the column and not to the pills. */
.ctx-tabs{
  display:flex; align-items:center; gap:3px; margin:0 -18px 16px; padding:0 18px 9px 7px;
  border-bottom:1px solid var(--line);
}
.ctx-tabs .grow{flex:1}
.ctx-tabs button{padding:6px 11px; border-radius:7px; font-size:var(--t-body-2); color:var(--ink-2)}
.ctx-tabs button:hover{background:var(--panel-2); color:var(--ink)}
/* Ink, not gold. Gold means "the thing you picked on the canvas"; which of two panes you are reading
   is a different question and should not answer with the same colour. */
.ctx-tabs button[aria-selected="true"]{
  background:color-mix(in srgb, var(--ink) 8%, transparent); color:var(--ink); font-weight:500;
}
.ctx-tabs .n{
  margin-left:5px; font:var(--t-micro)/1.5 var(--sans); padding:0 5px; border-radius:8px;
  background:color-mix(in srgb, var(--ink) 10%, transparent); color:var(--ink-2); font-weight:500;
}
.ctx-tabs .n:empty{display:none}
/* The one action that belongs to this column, on the same line as what it acts on. */
.ctx-tabs button.text{
  display:flex; align-items:center; gap:6px; padding:5px 11px; border-radius:7px;
  font-size:var(--t-body-2); font-weight:400;
  color:var(--ink); border:1px solid var(--line-2); background:var(--panel);
}
.ctx-tabs button.text:hover{background:var(--panel-2); border-color:var(--ink-3)}
.share svg,.mcp-connect svg{width:14px; height:14px; flex:none}
.rail-mcp{
  display:grid; grid-template-columns:18px minmax(0,1fr) 14px; align-items:center; gap:8px;
  width:100%; margin-top:10px; padding:8px 9px; text-align:left; border-radius:8px;
  border:1px solid color-mix(in srgb,var(--accent) 22%,var(--line));
  background:color-mix(in srgb,var(--accent) 5%,var(--panel));
}
.rail-mcp:hover{background:var(--panel-2); border-color:var(--accent-line)}
.rail-mcp>svg{width:16px; height:16px; color:var(--accent)}
.rail-mcp>span{min-width:0}
.rail-mcp strong{display:block; color:var(--ink); font-size:var(--t-body-2); font-weight:600}
.rail-mcp small{display:block; margin-top:1px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--ink-3); font-size:var(--t-micro)}
.rail-mcp svg.arr{width:13px; height:13px; color:var(--ink-3)}
#ctxComments[hidden], #ctxBody[hidden]{display:none}
.cm{
  display:block; width:100%; text-align:left; padding:8px 9px; border-radius:8px;
  border:1px solid var(--line); background:var(--panel-2); margin-bottom:6px;
}
.cm:hover{border-color:var(--accent-line)}
.cm .who{display:block; font:var(--t-micro)/1.4 var(--sans); color:var(--ink-3); margin-bottom:3px}
.cm .body{display:block; font-size:var(--t-body-2); line-height:1.45; color:var(--ink)}
.cm.done .body{text-decoration:line-through; color:var(--ink-3)}
/* Sized like the left column's rows rather than like a card that was floating over the canvas: a
   permanent column is read alongside the deck, not instead of it. */
.ctx h2{margin:0 0 6px; font:600 var(--t-meta)/1.5 var(--sans); letter-spacing:normal; color:var(--ink-2)}
.ctx h3{margin:0 0 2px; font:600 var(--t-body)/1.5 var(--sans); letter-spacing:-.011em}
.ctx .route{font-family:var(--mono); font-size:var(--t-meta); color:var(--ink-2); overflow-wrap:anywhere}
.ctx .why{margin:10px 0 0; font-size:var(--t-body-2); line-height:1.55; color:var(--ink)}
.ctx h4{
  margin:18px 0 6px; font:600 var(--t-meta)/1.5 var(--sans); letter-spacing:normal; color:var(--ink-2);
}
.ctx ul{margin:0; padding-left:16px; display:flex; flex-direction:column; gap:6px}
.ctx li{font-size:var(--t-body-2); line-height:1.45; color:var(--ink); overflow-wrap:anywhere}
.ctx code{font-family:var(--mono); font-size:var(--t-meta); color:var(--ink-2); overflow-wrap:anywhere}
.ctx .close{display:none; position:absolute; top:8px; right:9px; color:var(--ink-3); font-size:17px; line-height:1; padding:2px 5px; border-radius:4px}
.ctx .close:hover{color:var(--ink); background:var(--panel-2)}
.ctx .tags{display:flex; flex-wrap:wrap; gap:4px}
.ctx .tags span, .ctx .tags button{
  font-family:var(--sans); font-size:var(--t-micro); padding:1px 5px; border-radius:4px;
  border:1px solid var(--line-2); color:var(--ink-3);
}
.ctx .tags span[data-goto-state],
.ctx .tags span[data-goto-screen]{cursor:pointer}
.ctx .tags span[data-goto-state]:hover,
.ctx .tags span[data-goto-screen]:hover{border-color:var(--accent-line); color:var(--accent)}
.ctx .tags span.on, .ctx .tags button.on{border-color:var(--accent-line); color:var(--accent); background:var(--accent-soft)}
/* ---- comments: a numbered pin at a point on the canvas, and one note ----
   The pin lives in plane space so pan/zoom is the plane's own transform, and counter-scales off
   --iz so it stays the same size on screen - a 12px pin at 5% zoom would be half a pixel. */
.pins{position:absolute; top:0; left:0; width:0; height:0; z-index:3}
.pin{
  position:absolute; transform-origin:0 100%; transform:scale(var(--iz));
  min-width:20px; height:20px; padding:0 5px; border-radius:10px 10px 10px 2px;
  background:var(--accent); color:#fff; border:1px solid rgb(0 0 0 / .18);
  font:600 var(--t-micro)/18px var(--sans); box-shadow:var(--shadow); cursor:pointer;
}
.pin:hover{filter:brightness(1.08)}
.pin.on{outline:2px solid var(--accent); outline-offset:2px}
.pin.done{background:var(--panel-2); color:var(--ink-3); border-color:var(--line-2)}
/* Placed on a frame that no longer exists: the note is still worth reading, the position is a guess. */
.pin.floating{background:var(--warn)}
.stage.placing{cursor:crosshair}
/* While placing, every frame's veil takes the click - including the selected one, which is a live
   app that normally takes its own. The z-index matters as much as pointer-events: a mounted iframe
   is `z-index:1` and paints above the veil, so without lifting the veil over it a click meant to
   pin a comment onto the screen you are reading vanishes into the app and nothing happens. */
.stage.placing .fr-veil{pointer-events:auto; z-index:2; cursor:crosshair}

.note{
  position:fixed; z-index:41; width:296px; padding:10px 11px; border-radius:10px;
  background:var(--panel); border:1px solid var(--line-2); box-shadow:var(--shadow);
}
.note[hidden]{display:none}
.note .who{font:var(--t-micro)/1.4 var(--sans); color:var(--ink-3)}
.note .body{margin:6px 0 0; font-size:var(--t-body); line-height:1.5; white-space:pre-wrap}
.note .warn{margin:7px 0 0; font-size:var(--t-body-2); line-height:1.45; color:var(--warn)}
.note textarea{
  width:100%; resize:vertical; padding:7px 8px; border-radius:7px; background:var(--bg, transparent);
  border:1px solid var(--line-2); color:var(--ink); font:var(--t-body)/1.5 var(--sans);
}
.note textarea:focus-visible{outline:2px solid var(--accent); outline-offset:-1px}
.note .row{display:flex; align-items:center; gap:5px; margin-top:9px}
.note .row button{
  height:26px; padding:0 9px; border-radius:6px; border:1px solid var(--line-2);
  color:var(--ink-2); font-size:var(--t-body-2);
}
.note .row button:hover{background:var(--panel-2); color:var(--ink)}
.note .row button.primary{background:var(--accent); border-color:var(--accent); color:#fff}
.note .row button.danger:hover{color:var(--crit); border-color:var(--crit)}

/* ---- share dialog ---------------------------------------------------------------------------
   Above everything except the walk, which covers the deck entirely and is the one thing you can be
   inside while sharing - so this sits over that too. */
.modal{
  position:fixed; inset:0; z-index:70; padding:24px;
  display:flex; align-items:center; justify-content:center;
}
.modal[hidden]{display:none}
/* Its own element rather than a shadow on the card: it is what a click outside lands on. */
.modal-scrim{position:absolute; inset:0; background:color-mix(in srgb, var(--ink) 38%, transparent)}
/* The click that reveals a fixed overlay must finish on its opener, not fall through to the scrim
   that appeared under the pointer during that same gesture. */
.modal.opening .modal-scrim{pointer-events:none}
.modal-card{
  position:relative; width:min(460px, 100%); max-height:100%; overflow:auto;
  padding:15px 16px 13px; border-radius:12px;
  background:var(--panel); border:1px solid var(--line-2); box-shadow:var(--shadow);
}
.modal-card h2{margin:0; font-size:var(--t-title); font-weight:600; color:var(--ink)}
.modal-card .sub{margin:3px 0 0; font-size:var(--t-body-2); line-height:1.45; color:var(--ink-2)}
/* Label and value on one row: every line answers "what does this link carry", and a value indented
   under its own label reads as four sections instead of one list. */
.sh-what{margin:11px 0 0; display:flex; flex-direction:column; gap:6px}
.sh-what>div{display:flex; gap:10px; align-items:baseline}
.sh-what dt{flex:none; width:74px; font:var(--t-micro)/1.5 var(--sans); color:var(--ink-3)}
.sh-what dd{
  margin:0; min-width:0; font-size:var(--t-body-2); line-height:1.5; color:var(--ink);
  overflow-wrap:anywhere;
}
/* The link itself, readable but not the subject: what it opens is above, in words. */
.sh-url{
  width:100%; margin-top:12px; padding:7px 8px; border-radius:7px;
  background:var(--panel-2); border:1px solid var(--line-2); color:var(--ink-2);
  font:var(--t-meta)/1.5 var(--mono); text-overflow:ellipsis;
}
.sh-url:focus-visible{outline:2px solid var(--accent); outline-offset:-1px; color:var(--ink)}
.sh-hint{margin:7px 0 0; font-size:var(--t-body-2); line-height:1.45; color:var(--warn)}
.sh-hint[hidden]{display:none}
.modal-card .row{display:flex; align-items:center; gap:6px; margin-top:13px}
.modal-card .row button{
  height:28px; padding:0 11px; border-radius:7px; border:1px solid var(--line-2);
  color:var(--ink-2); font-size:var(--t-body-2);
}
.modal-card .row button:hover{background:var(--panel-2); color:var(--ink)}
/* `--panel`, not white: the accent inverts between themes and a fixed white label disappears into
   it in dark mode. */
.modal-card .row button.primary{background:var(--accent); border-color:var(--accent); color:var(--panel)}
/* By id, so the confirmed state cannot lose to `.row button.primary` or to the `:hover` under the
   pointer that just pressed it. */
#shareCopy.copied{background:var(--gold); border-color:var(--gold); color:var(--ink)}

/* MCP is a separate operation from sharing. The connection sheet reads like a boundary diagram
   followed by a short setup procedure; the only saturated marks are the two endpoints. */
.mcp-card{width:min(560px,100%); padding:18px}
.mcp-title-row{display:flex; align-items:flex-start; gap:12px}
.mcp-title-row>div{min-width:0; flex:1}
.mcp-eyebrow{
  display:block; margin-bottom:5px; font:var(--t-micro)/1 var(--sans);
  letter-spacing:normal; text-transform:none; color:var(--accent);
}
.mcp-x{
  width:28px; height:28px; flex:none; margin:-4px -4px 0 0; border-radius:7px;
  color:var(--ink-3); font-size:20px; line-height:1;
}
.mcp-x:hover{background:var(--panel-2); color:var(--ink)}
.mcp-bridge{
  display:grid; grid-template-columns:1fr minmax(72px,.7fr) 1fr; align-items:center;
  margin-top:17px; padding:12px; border:1px solid var(--line); border-radius:9px;
  background:var(--panel-2);
}
.mcp-bridge>div{display:flex; align-items:center; gap:8px; min-width:0}
.mcp-bridge>div:last-child{justify-self:end}
.mcp-bridge>div>span{display:block}
.mcp-bridge strong{display:block; font-size:var(--t-body-2); font-weight:600; white-space:nowrap}
.mcp-bridge small{display:block; margin-top:2px; color:var(--ink-3); font:var(--t-micro)/1.2 var(--sans); white-space:nowrap}
.mcp-node{width:10px; height:10px; flex:none; border-radius:50%; box-shadow:0 0 0 4px var(--panel)}
.mcp-node.deployed{background:var(--accent)}
.mcp-node.local{background:var(--gold)}
.mcp-wire{display:flex; align-items:center; gap:5px; color:var(--ink-3)}
.mcp-wire i{height:1px; flex:1; background:var(--line-2)}
.mcp-wire b{font:600 var(--t-micro)/1 var(--sans); letter-spacing:normal}
.mcp-use-now{margin-top:17px}
.mcp-step-copy strong{display:block; font-size:var(--t-body-2); font-weight:600; color:var(--ink)}
.mcp-step-copy span{display:block; margin-top:2px; font-size:var(--t-body-2); color:var(--ink-3)}
.mcp-use-now .sh-what{
  padding:9px 10px; border:1px solid var(--line); border-radius:8px; background:var(--panel-2);
}
.mcp-use-now .sh-what dt{width:104px}
.mcp-clients{display:flex; gap:4px}
.mcp-clients button{
  padding:5px 9px; border:1px solid var(--line-2); border-radius:6px;
  color:var(--ink-2); font-size:var(--t-body-2);
}
.mcp-clients button:hover{background:var(--panel-2)}
.mcp-clients button[aria-selected="true"]{background:var(--ink); border-color:var(--ink); color:var(--panel)}
.mcp-command-wrap{
  display:grid; grid-template-columns:minmax(0,1fr) auto; gap:7px; align-items:start; margin-top:8px;
}
.mcp-command-wrap>code{
  display:block; min-height:50px; padding:8px 9px; border-radius:7px; overflow:auto;
  background:var(--ink); color:var(--panel); font:var(--t-meta)/1.5 var(--mono); white-space:pre-wrap; overflow-wrap:anywhere;
}
.mcp-command-wrap.context{grid-template-columns:1fr; margin-top:10px}
.mcp-command-wrap.context>code{
  max-height:140px; background:color-mix(in srgb,var(--accent) 7%,var(--panel));
  border:1px solid color-mix(in srgb,var(--accent) 20%,var(--line)); color:var(--ink-2);
}
.mcp-command-wrap.context button{justify-self:start; background:var(--accent); border-color:var(--accent); color:var(--panel)}
.mcp-command-wrap.context button:hover{background:var(--accent); filter:brightness(.96)}
.mcp-command-wrap button{
  height:30px; padding:0 10px; border-radius:7px; border:1px solid var(--line-2);
  color:var(--ink); font-size:var(--t-body-2); white-space:nowrap;
}
.mcp-command-wrap button:hover{background:var(--panel-2)}
.mcp-command-wrap button.copied{background:var(--gold); border-color:var(--gold); color:var(--ink)}
.mcp-private{margin:7px 0 0; color:var(--ink-3); font-size:var(--t-micro); line-height:1.45}
.mcp-private code{font-family:var(--mono); color:var(--ink-2)}
.mcp-setup{margin-top:15px; border-top:1px solid var(--line); border-bottom:1px solid var(--line)}
.mcp-setup summary{
  display:flex; align-items:center; justify-content:space-between; gap:10px; padding:11px 1px;
  cursor:pointer; list-style:none; color:var(--ink);
}
.mcp-setup summary::-webkit-details-marker{display:none}
.mcp-setup summary span{display:block}
.mcp-setup summary strong{display:block; font-size:var(--t-body-2); font-weight:600}
.mcp-setup summary small{display:block; margin-top:2px; color:var(--ink-3); font-size:var(--t-micro)}
.mcp-setup summary i{width:7px; height:7px; flex:none; border-right:1.5px solid currentColor; border-bottom:1.5px solid currentColor; transform:rotate(45deg); transition:transform .15s ease}
.mcp-setup[open] summary i{transform:rotate(225deg)}
.mcp-setup-body{padding:2px 1px 13px}
.mcp-path{display:block; margin-top:10px; color:var(--ink-2); font-size:var(--t-body-2); font-weight:500}
.mcp-path input{
  display:block; width:100%; margin-top:5px; padding:7px 8px; border:1px solid var(--line-2); border-radius:7px;
  background:var(--panel-2); color:var(--ink); font:var(--t-meta)/1.4 var(--mono);
}
.mcp-path input:focus-visible{outline:2px solid var(--accent); outline-offset:-1px}
.mcp-path-help{margin:6px 0 0; color:var(--ink-3); font-size:var(--t-micro); line-height:1.45}
.mcp-path-help code{font-family:var(--mono); color:var(--ink-2)}
.mcp-example{
  margin:8px 0 0; padding:7px 9px; border-left:2px solid var(--gold);
  background:color-mix(in srgb,var(--gold) 8%,transparent); color:var(--ink-2);
  font-size:var(--t-body-2); line-height:1.45;
}
.mcp-guardrail{
  display:flex; align-items:flex-start; gap:9px; margin-top:17px; padding:10px 11px;
  border-radius:8px; background:color-mix(in srgb,var(--accent) 7%,var(--panel));
  border:1px solid color-mix(in srgb,var(--accent) 22%,var(--line));
}
.mcp-guardrail svg{width:16px; height:16px; flex:none; margin-top:1px; color:var(--accent)}
.mcp-guardrail p{margin:0; color:var(--ink-2); font-size:var(--t-body-2); line-height:1.48}
.mcp-guardrail strong{color:var(--ink); font-weight:600}

@media (max-width:620px){
  .mcp-bridge{grid-template-columns:1fr; gap:10px}
  .mcp-bridge>div:last-child{justify-self:start}
  .mcp-wire{width:100%; padding-left:3px}
  .mcp-command-wrap{grid-template-columns:1fr}
  .mcp-command-wrap button{justify-self:start}
}

@media (prefers-reduced-motion:reduce){*{transition:none !important}}

/* Standalone boot recovery must work before the normal canvas controls bind. */
.viewer-boot-error{position:fixed; inset:0; z-index:1000; display:grid; place-items:center; padding:1.5rem; overflow:auto; background:var(--well); color:var(--ink)}
.viewer-boot-error>div{width:100%; max-width:30rem; padding:2rem; border:1px solid var(--line); border-radius:1rem; background:var(--panel); box-shadow:var(--shadow)}
.viewer-boot-error h1{margin:0; font:600 var(--t-section)/1.3 var(--sans)}
.viewer-boot-error p{margin:1rem 0 1.5rem; color:var(--ink-2); overflow-wrap:anywhere}
.viewer-boot-actions{display:flex; flex-wrap:wrap; gap:.75rem}
.viewer-boot-actions :is(button,a){display:inline-flex; align-items:center; justify-content:center; min-height:2.75rem; padding:.5rem 1rem; border:1px solid var(--line-2); border-radius:.5rem; background:var(--panel-2); color:var(--ink); font:500 var(--t-body)/1.5 var(--sans); text-decoration:none; cursor:pointer}
.viewer-boot-actions :is(button,a):focus-visible{outline:2px solid var(--accent); outline-offset:.2rem}

/* Says what a link inside a frame did: which screen it moved to, or that nothing renders it.
   A destination with no frame is a hole in the canvas, so it is called out rather than ignored. */
.toast{
  position:fixed; left:50%; bottom:58px; transform:translateX(-50%);
  max-width:min(70ch, 80vw); padding:7px 13px; border-radius:8px;
  background:var(--ink); color:var(--panel); font-size:var(--t-body); line-height:1.45;
  box-shadow:0 6px 22px rgb(0 0 0 / .18); z-index:40; pointer-events:none;
}
.toast.warn{background:var(--ink); color:var(--panel); border:2px solid var(--crit)}


/* ---- review mode: how much of a row is shown ------------------------------------------------
   A row whose shape moved is read whole. A row that only changed inside it shows the frames that
   moved, and says how many it is not showing - the journey is not news when it did not move. */
.track-hd .why{
  font-family:var(--sans); font-size:var(--t-micro); padding:1px 6px; border-radius:5px;
  background:var(--panel-2); border:1px solid var(--line-2); color:var(--ink-3);
}
.track-hd .why.shape{background:var(--warn); border-color:var(--warn); color:var(--panel); font-weight:500}

/* ---- review: one comparison at a time -------------------------------------------------------
   Sits where the stage sits, and replaces it while a page change is under review. The stage is a
   camera over a plane; this is a document with one job, so it scrolls normally and has no zoom. */
.review{
  grid-column:2; position:relative; overflow:auto;
  display:flex; flex-direction:column; gap:12px; padding:14px 16px 20px;
}
.rv-bar{
  position:sticky; top:0; z-index:3; display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  background:var(--canvas); padding-bottom:10px;
}
.rv-bar .prog{font:var(--t-meta)/1 var(--sans); color:var(--ink-3)}

/* The comparison. Frames are captured at their real width, so each pane scales one down rather than
   reflowing it - a review of a 1440px screen at 700px is still that screen. */
.rv-body{display:flex; gap:12px; align-items:flex-start}
.rv-pane{position:relative; overflow:hidden; border:1px solid var(--line); border-radius:8px; background:var(--panel); box-shadow:var(--shadow); flex:1; min-width:0}
.rv-pane>iframe{position:absolute; top:0; left:0; border:0; transform-origin:0 0}
.rv-lbl{
  font:var(--t-micro)/1 var(--sans); letter-spacing:normal; text-transform:none;
  color:var(--ink-3); margin-bottom:5px; display:flex; justify-content:space-between;
}
.rv-col{flex:1; min-width:0}
/* 2-up on a capture narrower than the column it would get: one column instead of two, before above
   after, each at the frame's own width rather than stretched across a row it does not need. The
   gap here is the one `layoutTwoUp` measures against - change both or the threshold lies. */
.rv-body.stacked{flex-direction:column; align-items:center}
.rv-body.stacked>.rv-col{flex:none; width:100%}
/* swipe: the after layer is clipped, so dragging uncovers before underneath */
.rv-pane.swipe>iframe.after{clip-path:inset(0 0 0 var(--split,50%))}
.rv-handle{position:absolute; top:0; bottom:0; width:2px; background:var(--accent); z-index:3; cursor:ew-resize; touch-action:none}
/* A 2px line is a 2px target. The grab area is 24px of nothing on either side of it, which is what
   the hand is actually aiming at. */
.rv-handle::before{content:""; position:absolute; top:0; bottom:0; left:-12px; right:-12px}
/* While a drag is live the pictures must not take the pointer: capture keeps the events coming here
   either way, but an iframe under the cursor still shows a text caret and a grab cursor, which reads
   as the handle having been dropped. */
.rv-pane.dragging{cursor:ew-resize}
.rv-pane.dragging>iframe{pointer-events:none}
.rv-handle::after{
  content:"⇄"; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  width:26px; height:26px; border-radius:50%; background:var(--accent); color:var(--panel);
  display:grid; place-items:center; font:var(--t-meta)/1 var(--sans); box-shadow:var(--shadow);
}
/* flip: same box, crossfaded - the change reads as movement rather than as two pictures */
.rv-pane.flip>iframe{transition:opacity 110ms linear}
.rv-pane.flip>iframe.before{opacity:0}
.rv-pane.flip.showBefore>iframe.before{opacity:1}
.rv-pane.flip.showBefore>iframe.after{opacity:0}
/* Both pictures stay mounted and only their opacity moves, so the swap is a swap and not a reload. */
.rv-tap{position:absolute; inset:0; z-index:2; cursor:pointer}
/* fade: the same crossfade, held still at whatever point the slider is at */
.rv-pane.fade>iframe.after{opacity:calc(var(--mix, 50) / 100); transition:opacity 70ms linear}
.rv-mix{display:flex; align-items:center; gap:9px; margin-top:8px; font:var(--t-micro)/1 var(--sans);
        letter-spacing:normal; text-transform:none; color:var(--ink-3)}
.rv-mix input{flex:1; min-width:0; accent-color:var(--accent); cursor:ew-resize}
@media (prefers-reduced-motion:reduce){.rv-pane.flip>iframe,.rv-pane.fade>iframe{transition:none}}

.rv-meta{
  font:var(--t-meta)/1.5 var(--sans); color:var(--ink-3); display:flex; gap:10px; flex-wrap:wrap;
  border-top:1px solid var(--line); padding-top:10px;
}
.rv-meta b{color:var(--ink-2); font-weight:500}
.rv-empty{margin:auto; text-align:center; color:var(--ink-3); font-size:var(--t-body-2); max-width:44ch}

/* The rail becomes the changeset. `.cs` rides on `.scr` - shape, padding and the gold selection all
   come from there - and adds only what a moved frame has that a screen has not. */
.rail .cs{cursor:pointer}
.rail .cs .n .sub{color:var(--ink-3)}
.rail .cs[aria-current="true"] .n .sub{color:var(--ink-2)}
.rail .cs .r{display:flex; gap:6px; font-family:var(--sans); font-size:var(--t-micro); color:var(--ink-3)}
.rail .cs .r .ax{color:var(--warn)}

/* The view switch is the one dock control a changeset keeps, so the review body leaves room for it. */
.review{padding-bottom:56px}
#viewPill button[aria-pressed="true"]{background:var(--accent-soft); border-color:var(--accent-line); color:var(--accent); font-weight:500}

/* ---- presence: who else is in this deck, and where they are looking ----
   Two surfaces, because they answer two questions. The stack is chrome: fixed size, top-right of the
   canvas, readable whatever the camera is doing. The marks are content: they live in the plane, so
   pan and zoom move them with the frames they are about, and they counter-scale off --iz for the same
   reason pins do - at 5% a name label would be half a pixel tall. */
.whostack{
  position:absolute; top:10px; right:12px; z-index:12; display:flex; align-items:center; gap:0;
  padding:3px; border-radius:999px; background:var(--panel); border:1px solid var(--line-2);
  box-shadow:var(--shadow);
}
.whostack[hidden], .stage-only .whostack{display:none}
.whostack .av{
  width:24px; height:24px; margin-left:-6px; border-radius:50%; border:2px solid var(--panel);
  display:grid; place-items:center; cursor:pointer; color:#fff; background:var(--ink-3);
  font:600 var(--t-micro)/1 var(--sans); letter-spacing:normal;
}
.whostack .av:first-child{margin-left:0}
/* Lifted rather than recoloured on hover: the colour is the person's identity here, not a state. */
.whostack .av:hover{transform:translateY(-1px)}
.whostack .av:focus-visible{outline:2px solid var(--accent); outline-offset:1px}
/* Nobody's camera to go to - they have the deck open but have not moved yet. */
.whostack .av.adrift{cursor:default; opacity:.55}
/* Whose camera this one is tied to. A ring rather than a colour change: the fill is their identity
   colour and overwriting it would make the person you follow the one you cannot recognise. */
.whostack .av.following{box-shadow:0 0 0 2px var(--panel), 0 0 0 4px var(--accent)}
.whostack .more{
  min-width:24px; height:24px; margin-left:-6px; padding:0 6px; border-radius:999px;
  border:2px solid var(--panel); background:var(--panel-2); color:var(--ink-2);
  display:grid; place-items:center; font:600 var(--t-micro)/1 var(--sans);
}

.peers{position:absolute; top:0; left:0; width:0; height:0; z-index:2; pointer-events:none}
/* The name, on the cursor rather than on a rectangle: the mark it labels is now the pointer. */
.peers .tag{
  position:absolute; transform-origin:0 0; transform:scale(var(--iz)) translate(14px, 2px);
  height:17px; padding:0 6px; border-radius:4px 4px 4px 1px; color:#fff;
  font:600 var(--t-micro)/17px var(--sans); white-space:nowrap; box-shadow:var(--shadow);
}
/* The pointer, as an arrow that does not rotate or scale with the plane. */
.peers .cur{position:absolute; transform-origin:0 0; transform:scale(var(--iz))}
.peers .cur svg{display:block; width:14px; height:18px; filter:drop-shadow(0 1px 1px rgb(0 0 0 / .35))}

/* While a hand is on the camera, both moving surfaces get a compositing layer of their own.
   The plane has a transform and so already has one, but a *zoom* changes its scale, and this is what
   lets Chrome scale the layer it has rather than re-rasterise the whole deck on every wheel tick.
   `#paint` is not transformed - it is promoted for the opposite reason: its texture is rewritten on
   every frame of a pan, and on its own layer that costs no repaint of anything around it.
   Only during the gesture, because `will-change` left on holds that memory for a layer nothing is
   moving. See `gestureBeat` in viewer.js for what adds and drops the class. */
.stage.panning .plane, .stage.panning #paint{will-change:transform}

.who > .theme-control { margin-top:10px }
/* Product documents keep their OS preference across explicit Mockspace theme changes. */
iframe { color-scheme:light dark }

/* Canvas orientation stays in the navigation column, outside the camera's coordinate system. */
.canvas-guide{flex:none; padding:.75rem; border-bottom:1px solid var(--line); font-size:var(--t-body-2)}
.canvas-guide>p{margin:0 0 .5rem; color:var(--ink-2); overflow-wrap:anywhere}
.canvas-fit{display:flex; gap:.5rem}
.canvas-fit button{flex:1; min-height:2.75rem; border:1px solid var(--ink-3); border-radius:.3rem; font-size:var(--t-body-2)}
.canvas-fit button:hover{background:var(--panel-2)}
.canvas-fit button[aria-pressed="true"]{background:var(--panel-2); color:var(--ink); font-weight:600}
.canvas-fit button:disabled{opacity:.5; cursor:default}
.canvas-guide details{margin-top:.5rem}
.canvas-guide summary{cursor:pointer; min-height:2.75rem; display:list-item; align-content:center; color:var(--ink-2)}
.canvas-guide details p{margin:.25rem 0; color:var(--ink-2)}
.canvas-guide details button{min-height:2.75rem; padding:0 .75rem; text-decoration:underline}
.ctx .tags button{font-family:var(--sans); font-size:var(--t-body-2); min-height:2.75rem; padding:.35rem .6rem; border-color:var(--ink-3)}
.ctx .tags button:hover{background:var(--panel-2); color:var(--ink)}
.canvas-mobile{grid-column:2; grid-row:1; display:flex; flex-wrap:wrap; align-items:center; gap:4px; min-width:0; min-height:52px; padding:8px 12px; background:var(--panel); border-bottom:1px solid var(--line)}
#canvasNav{display:none}
#canvasMobileLocation{flex:1 1 80px; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding-right:8px; color:var(--ink-2); font-size:var(--t-body-2)}
.canvas-mobile button{display:inline-flex; align-items:center; justify-content:center; min-height:32px; min-width:32px; padding:0 9px; border:0; border-radius:5px; font-size:var(--t-meta); color:var(--ink-2); white-space:nowrap}
.canvas-mobile button:hover{background:var(--panel-2); color:var(--ink)}
.canvas-mobile button:disabled{opacity:.5; cursor:default}
.canvas-mobile svg{width:16px; height:16px; fill:none; stroke:currentColor; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round}
.canvas-mobile .canvas-fit{display:flex; gap:2px; margin-left:6px; padding-left:8px; border-left:1px solid var(--line)}
.canvas-mobile .canvas-fit button{flex:none; min-height:32px; border:0}
body:not(.stage-only) .stage, body:not(.stage-only) .review{grid-row:2}
.ctx{grid-row:1/-1}
body.zen .zenbar{top:64px}
@media (max-width:48rem){
  body:not(.stage-only){--col-l:0px; --col-r:0px; grid-template-rows:5.5rem minmax(0,1fr)}
  body:not(.stage-only) .stage, body:not(.stage-only) .review{grid-row:2}
  body:not(.stage-only) .ctx{display:none}
  body:not(.stage-only) .rail{display:none; position:fixed; inset:5.5rem auto 0 0; width:min(22rem,100%); z-index:35; border-right:1px solid var(--ink-3); background:var(--panel)}
  body.canvas-nav-open:not(.stage-only) .rail{display:flex}
  body:not(.stage-only) .rail>.fold{display:none}
  body:not(.stage-only) .canvas-mobile{position:fixed; inset:0 0 auto; height:5.5rem; display:grid; grid-template-columns:auto minmax(0,1fr) 44px 44px; grid-template-rows:44px 44px; padding:0 8px; gap:0; z-index:36}
  #canvasNav{display:inline-flex; grid-column:1; grid-row:1; font-weight:500}
  #canvasMobileLocation{grid-column:2; grid-row:1; padding:0 6px; font-size:var(--t-meta)}
  #previousScreen{grid-column:3; grid-row:1}
  #nextScreen{grid-column:4; grid-row:1}
  .canvas-mobile .canvas-fit{grid-column:1/-1; grid-row:2; margin:0; padding:0; border:0; gap:8px}
  .canvas-mobile button, .canvas-mobile .canvas-fit button{min-height:44px; min-width:44px; padding:0 8px}
  body:not(.stage-only) .zenbar{display:none}
  body:not(.stage-only) .dock{left:.5rem; right:.5rem}
  body:not(.stage-only) #toolPill{max-width:100%; overflow-x:auto; justify-content:flex-start}
  body:not(.stage-only) #toolPill button{min-width:2.75rem; min-height:2.75rem; flex-shrink:0}
  body:not(.stage-only) #toolPill [data-z="one"]{display:none}
  .rail .st,.rail .scr,.rail .pg{min-height:2.75rem}
}

/* Review orientation and evidence share the deck's type and semantic colour tokens. */
body.inspecting .dock{justify-content:center;flex-wrap:wrap}
body.inspecting .dock>.grow{display:none}
body.inspecting #toolPill{position:static;transform:none}
.rv-sides{display:flex;border:1px solid var(--line-2);border-radius:.5rem;overflow:hidden}
.rv-sides button,#rvHighlights,#rvRemoved{font:500 var(--t-meta)/1.4 var(--sans);color:var(--ink-2);background:var(--panel);border:1px solid var(--line-2);border-radius:.4rem;padding:.45em .7em;cursor:pointer}
.rv-sides button{border:0;border-radius:0}
.rv-sides button[aria-pressed=true],#rvHighlights[aria-pressed=true]{background:var(--accent);color:var(--panel)}
.rv-orientation{font:500 var(--t-meta)/1.4 var(--sans);color:var(--ink);overflow-wrap:anywhere}
#rvFlipHint{font:var(--t-micro)/1.4 var(--sans);color:var(--ink-3)}
.rv-evidence{display:flex;gap:.6rem;flex-wrap:wrap;align-items:center;font:var(--t-meta)/1.5 var(--sans);color:var(--ink-2)}
.rv-body{position:relative}
.rv-body>.rv-col>.rv-lbl{font:var(--t-meta)/1.4 var(--sans);letter-spacing:normal;text-transform:none;gap:.5em;min-height:2.8em;overflow-wrap:anywhere}
.rv-body:not([data-mode=twoup])>.rv-col.before{position:absolute;inset:0;visibility:hidden}
.rv-body[data-mode=after].showBefore>.rv-col.before,.rv-body[data-mode=flip].showBefore>.rv-col.before,.rv-body[data-mode=swipe]>.rv-col.before,.rv-body[data-mode=fade]>.rv-col.before{visibility:visible}
.rv-body:is([data-mode=after],[data-mode=flip]).showBefore>.rv-col.after{visibility:hidden}
.rv-body:is([data-mode=swipe],[data-mode=fade])>.rv-col.after{position:relative;z-index:1}
.rv-body[data-mode=swipe]>.rv-col.after{clip-path:inset(0 0 0 var(--split,50%))}
.rv-body[data-mode=fade]>.rv-col.after{opacity:calc(var(--mix,50)/100)}
.rv-body>.rv-handle,.rv-body>.rv-tap{display:none}
.rv-body[data-mode=swipe]>.rv-handle{display:block;left:var(--split,50%);top:2.8em}
.rv-body[data-mode=flip]>.rv-tap{display:block;top:2.8em}
.rv-body.dragging iframe{pointer-events:none}
.rv-pane>.rv-empty{padding:1em;min-height:12em}
.rv-marks{position:absolute;inset:0;pointer-events:none;overflow:hidden}
.rv-mark{position:absolute;border:2px solid var(--new);color:var(--new);border-radius:.15rem}
.rv-mark.removed{border:2px dashed var(--crit);color:var(--crit)}
.rv-mark.changed{border:3px double var(--warn);color:var(--warn)}
.rv-mark>span{position:absolute;left:0;top:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background:var(--panel);font:500 var(--t-micro)/1.35 var(--sans);padding:0 .2em}
@media(max-width:600px){
  .rv-body[data-mode=twoup]{flex-direction:column}
  .rv-body[data-mode=twoup]>.rv-col{width:100%;flex:none}
  .review{padding:.6rem}
  .rv-bar{gap:.45rem}
  .rv-bar button{min-height:2.75rem}
  #cmpPill{max-width:100%;overflow:auto}
  #cmpPill button{padding:.5em;font-size:var(--t-micro)}
}

/* Human controls and counts share the UI face; paths and code retain Mono. */
:where(.chip,.step-hd .c,.track-hd .fx,.track-hd .d,.pill .dsum,#toolPill .z,.ctx-tabs .n,.pin,.whostack .more){font-variant-numeric:tabular-nums}
:where(.proto-hd button,.pill button,.viewsw button,.modal-card .row button){font-weight:500; line-height:1.4}
:where(b,strong){font-weight:600}
