/*
 * Cinegram runtime styles.
 *
 * Colours are defined as tokens on bare :root so the light palette always has
 * a value, then overridden for the dark theme. The runtime stamps
 * data-theme on <html>, and prefers-color-scheme covers the untouched default.
 *
 * The type face is a token for the same reason the colours are: a surface that
 * wants the mainframe skin below has to be able to reset it without hunting
 * through the rules that draw the chrome.
 */

:root {
  --dgm-font: ui-sans-serif, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --dgm-bg: #ffffff;
  --dgm-panel: #f6f7f9;
  /* The panel colour with the diagram showing through it. Only the tool rail
     wants it — a control floating on the stage has to admit what it is
     covering — and it is a token rather than an rgba() in place because a
     panel and its translucent form must change together. */
  --dgm-panel-soft: rgba(246, 247, 249, 0.72);
  --dgm-border: #dfe3e8;
  --dgm-fg: #1c2024;
  --dgm-muted: #6b7480;
  --dgm-accent: #2f6feb;
  --dgm-accent-soft: rgba(47, 111, 235, 0.14);
  /* What is written *on* the accent — Play, the active step's number. White
     against both blues, and a token because a skin is free to pick an accent
     white cannot be read on. Scheme-independent, so it is stated once. */
  --dgm-on-accent: #ffffff;
  --dgm-busy: #d97706;
  --dgm-response: #16a34a;
  --dgm-fail: #dc2626;
  --dgm-warn-bg: #fff4e5;
  --dgm-warn-fg: #8a4b08;
  --dgm-shadow: rgba(16, 24, 40, 0.12);
}

:root[data-theme='dark'],
:root:not([data-theme='light']) {
  --dgm-bg: #0f1216;
  --dgm-panel: #171b21;
  --dgm-panel-soft: rgba(23, 27, 33, 0.72);
  --dgm-border: #2a313a;
  --dgm-fg: #e6e9ee;
  --dgm-muted: #98a2b0;
  --dgm-accent: #6ea8fe;
  --dgm-accent-soft: rgba(110, 168, 254, 0.18);
  --dgm-busy: #fbbf24;
  --dgm-response: #4ade80;
  --dgm-fail: #f87171;
  --dgm-warn-bg: #3a2a12;
  --dgm-warn-fg: #f6c177;
  --dgm-shadow: rgba(0, 0, 0, 0.5);
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme='dark']) {
    --dgm-bg: #ffffff;
    --dgm-panel: #f6f7f9;
    --dgm-panel-soft: rgba(246, 247, 249, 0.72);
    --dgm-border: #dfe3e8;
    --dgm-fg: #1c2024;
    --dgm-muted: #6b7480;
    --dgm-accent: #2f6feb;
    --dgm-accent-soft: rgba(47, 111, 235, 0.14);
    --dgm-busy: #d97706;
    --dgm-response: #16a34a;
    --dgm-fail: #dc2626;
    --dgm-warn-bg: #fff4e5;
    --dgm-warn-fg: #8a4b08;
    --dgm-shadow: rgba(16, 24, 40, 0.12);
  }
}

/* --- the mainframe skin ------------------------------------------------- *
 *
 * Cinegram's own theme, applied to the player's chrome. The palette's home is
 * www/assets/stylesheets/mainframe.css, which sets it as --cg-* for the
 * website and documents what every colour means: light is greenbar, the
 * fanfold paper a line printer banded pale green; dark is an IBM 3279 colour
 * display, blue-black glass with the seven-colour field palette it actually
 * had, where colour *was* the field attribute — green is a field you can act
 * on, turquoise is protected text you read, yellow is a literal, red went
 * wrong. So the mapping here is semantic rather than decorative: the accent is
 * the actionable green, a response is turquoise, waiting is yellow (ochre on
 * paper, where yellow is illegible), a failure is red.
 *
 * site/palette_test.go pins the hexes that must agree with mainframe.css,
 * because the palette now lives in two files that no build step connects.
 *
 * Opt-in, on the root element, and never the default: the whole stylesheet is
 * byte-synced into editors/vscode/media/, and a branded monospace player
 * inside someone's Markdown preview reads as a broken font. The surfaces that
 * ask for it are the ones cinegram owns end to end — the emitted page, the
 * pages `cinegram site` builds, the playground.
 *
 * The three blocks mirror the three above, and for the same reason: an
 * attribute selector already outranks bare :root, so the resolution order has
 * to come from position in the file, not from specificity. Light values first
 * so a skinned page always has a colour, the dark override second, and the
 * prefers-color-scheme guard last for the page whose theme nobody has stamped
 * yet. */

:root[data-dgm-skin='mainframe'] {
  /* One family, both schemes — a 3270 had one, and the roles separate by
     weight, case and colour instead. Ordered so a machine with IBM Plex Mono
     installed gets the face IBM drew looking at exactly these terminals, and
     every other machine still lands on a real monospace. No @font-face: a
     webfont this page cannot fetch is a silent fallback to something
     arbitrary, and the page has to work from the filesystem. */
  --dgm-font:
    "IBM Plex Mono", "SF Mono", SFMono-Regular, "Berkeley Mono", ui-monospace,
    Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;

  --dgm-bg: #f4f4ec;            /* fanfold stock, warm off-white */
  --dgm-panel: #dfe9d8;         /* the printed band */
  --dgm-panel-soft: rgba(223, 233, 216, 0.72);
  --dgm-border: #8b9c85;
  --dgm-fg: #17201c;            /* never quite black; ribbon on paper */
  --dgm-muted: #5a6a60;
  --dgm-accent: #14713d;
  --dgm-accent-soft: rgba(20, 113, 61, 0.12);
  /* A field the cursor has landed on inverts — that is what a 3270 does, and it
     is also the only legible answer for type on the dark scheme's phosphor
     green, which white would sit on at less than 2:1. var() rather than a
     literal so the one declaration follows the ground into both schemes. */
  --dgm-on-accent: var(--dgm-bg);
  --dgm-busy: #8a6212;          /* yellow is illegible on paper; ochre is its ink */
  --dgm-response: #10666a;
  --dgm-fail: #a9261b;
  /* The warning banner is a recessed field with ochre type, not a coloured
     card: on paper the printer had one ribbon and severity was the wording. */
  --dgm-warn-bg: #e9ebe0;
  --dgm-warn-fg: #8a6212;
  --dgm-shadow: rgba(23, 32, 28, 0.14);
}

:root[data-dgm-skin='mainframe'][data-theme='dark'],
:root[data-dgm-skin='mainframe']:not([data-theme='light']) {
  /* Not #000: a CRT face in a lit room is a blue-black, and the whole palette
     is keyed off that cast so the greens do not read as neon. */
  --dgm-bg: #080d0c;
  --dgm-panel: #0e1614;
  --dgm-panel-soft: rgba(14, 22, 20, 0.72);
  --dgm-border: #2a3e39;
  --dgm-fg: #c9d6ce;
  --dgm-muted: #6c918a;
  --dgm-accent: #34d96a;
  --dgm-accent-soft: rgba(52, 217, 106, 0.16);
  --dgm-busy: #e8b646;
  --dgm-response: #45c9c0;
  --dgm-fail: #f2604f;
  --dgm-warn-bg: #0e1614;
  --dgm-warn-fg: #e8b646;
  --dgm-shadow: rgba(0, 0, 0, 0.55);
}

@media (prefers-color-scheme: light) {
  :root[data-dgm-skin='mainframe']:not([data-theme='dark']) {
    --dgm-bg: #f4f4ec;
    --dgm-panel: #dfe9d8;
    --dgm-panel-soft: rgba(223, 233, 216, 0.72);
    --dgm-border: #8b9c85;
    --dgm-fg: #17201c;
    --dgm-muted: #5a6a60;
    --dgm-accent: #14713d;
    --dgm-accent-soft: rgba(20, 113, 61, 0.12);
    --dgm-busy: #8a6212;
    --dgm-response: #10666a;
    --dgm-fail: #a9261b;
    --dgm-warn-bg: #e9ebe0;
    --dgm-warn-fg: #8a6212;
    --dgm-shadow: rgba(23, 32, 28, 0.14);
  }
}

/* The diagram itself is deliberately not in scope. It renders through
   mermaid's own light and dark themes, and restyling nodes and edges to
   phosphor would mean a themeVariables set per skin plus design work on what
   the 3279's semantic colours mean in mermaid's class model. The skin recolours
   the chrome around the picture. */

/* Everything that claims the page is scoped to .dgm-standalone, which only the
   emitted page sets on <body>. This stylesheet is also loaded whole into hosts
   that own the document around the diagram — the VS Code Markdown preview
   contributes it alongside the editor's own — and a bare `body` rule there would
   restyle the reader's prose to match the diagram. The tokens above are safe to
   share; laying out the page is not. */
body.dgm-standalone {
  margin: 0;
  background: var(--dgm-bg);
  color: var(--dgm-fg);
  font: 14px/1.5 var(--dgm-font);
}

.dgm {
  display: flex;
  flex-direction: column;
  padding: 16px;
  box-sizing: border-box;
  gap: 12px;
  /* Not inherited from the host: the runtime's own sizing is in px throughout,
     and an editor's prose font would leave the controls at the wrong scale. */
  font: 14px/1.5 var(--dgm-font);
  color: var(--dgm-fg);
}

/* Filling the window is the standalone page's job, and filling it *exactly* is
   what keeps the narration on screen: a definite height is what the shell rules
   in the layout section need to divide up, so the caption and the transport
   stay put and the diagram scales into what is left rather than growing the
   page out from under them. Embedded, the diagram is one element among others
   and takes the height its content asks for. */
.dgm-standalone .dgm {
  height: 100vh;
  overflow: hidden;
  max-width: 1400px;
  margin: 0 auto;
}

/* --- chrome ------------------------------------------------------------- */

.dgm-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.dgm-heading {
  display: flex;
  align-items: baseline;
  gap: 10px;
  min-width: 0;
}

.dgm-title {
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* The trail of views drilled through. Muted, because it is orientation
   rather than content. */
.dgm-crumb {
  font-size: 12px;
  color: var(--dgm-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dgm-back {
  padding: 4px 10px;
  font-size: 12px;
}

.dgm-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.dgm-btn,
.dgm-select {
  appearance: none;
  border: 1px solid var(--dgm-border);
  background: var(--dgm-panel);
  color: var(--dgm-fg);
  border-radius: 7px;
  padding: 6px 12px;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}

.dgm-btn:hover,
.dgm-select:hover {
  border-color: var(--dgm-accent);
}

.dgm-btn-primary {
  background: var(--dgm-accent);
  border-color: var(--dgm-accent);
  color: var(--dgm-on-accent);
  min-width: 72px;
  font-weight: 600;
}

/* Embed mode. The stage, its narration and the transport survive; the page
   furniture does not, because the embedding page has its own. */
.dgm-embed { min-height: 0; padding: 8px; }
.dgm-embed .dgm-bar,
.dgm-embed .dgm-steps { display: none; }
.dgm-embed .dgm-body { grid-template-columns: minmax(0, 1fr); }
/* The storyboard is content rather than furniture, so it survives embedding —
   but with the step list gone it takes that column instead of a third one. */
.dgm-embed.dgm-has-board .dgm-body {
  grid-template-columns: minmax(0, 1fr) minmax(200px, 320px);
}

/* Inline mode: one diagram among the paragraphs of a document, rather than a
   page of its own. It builds on embed — the step list and the page furniture are
   already gone — and puts back only the two things a reader cannot do without:
   somewhere to press Play, and a way back out of a diagram they drilled into.
   Everything else in the bar belongs to authoring a diagram, not reading one.

   Selecting what stays, rather than what goes, is deliberate: a control added to
   the bar later should have to earn its place in a document explicitly. */
.dgm-inline {
  border: 1px solid var(--dgm-border);
  border-radius: 10px;
  background: var(--dgm-bg);
  padding: 10px 12px;
  gap: 8px;
}

.dgm-inline .dgm-bar { display: flex; }
.dgm-inline .dgm-title { display: none; }
.dgm-inline .dgm-controls > * { display: none; }
.dgm-inline .dgm-controls > .dgm-play,
.dgm-inline .dgm-controls > .dgm-select { display: inline-block; }

/* Back and the scenario picker are left to the runtime, which already hides
   each until it has something to offer — Back until the reader has drilled into
   something, the picker until there is more than one scenario. Both do it by
   writing element.style, which beats the rules above, so a document shows them
   only when they mean something. The breadcrumb stays for the same reason: it
   renders empty until there is a trail to describe. */

.dgm-inline .dgm-stage { min-height: 180px; }

/* Presenter mode. The room is looking at the stage and the presenter is doing
   the narrating, so the step list, the scrubber and most of the transport go
   away: what is left is the diagram, the storyboard beside it, and the current
   step's prose at a size that survives a projector. Following the .dgm-embed
   precedent above — strip chrome by class, never rebuild the page. */
.dgm-present .dgm-steps,
.dgm-present .dgm-foot,
.dgm-present .dgm-crumb { display: none; }

.dgm-present .dgm-body { grid-template-columns: minmax(0, 1fr); }

/* The storyboard survives, because a demo is exactly when someone wants to
   point at what the user sees. */
.dgm-present.dgm-has-board .dgm-body {
  grid-template-columns: minmax(0, 1fr) minmax(240px, 380px);
}

/* Play, the speed cycle and Copy link are things you do while building a
   diagram, not while showing one; they carry .dgm-authoring so this rule names
   them rather than guessing from the shape of their other classes. Exit,
   Restart — taking a demo from the top is presenting, not building — the
   scenario picker and help stay.

   The set is the same now that most of it lives in the tool rail instead of the
   bar: the classes moved with the buttons, so this rule thins the rail rather
   than emptying it. Nothing in the rail section may state a `display` for a
   button, or this would stop being true — see the note there. */
.dgm-present .dgm-authoring { display: none; }

/* Except in a document, where inline mode has already restored Play at
   `.dgm-inline .dgm-controls > .dgm-play` (0,3,0) and outranks the (0,2,0)
   rule above — leaving a continuous-run button in the one mode built to
   advance a beat at a time. Both rules are right on their own; this states
   which wins where they meet. */
.dgm-inline.dgm-present .dgm-controls > .dgm-authoring { display: none; }

/* dgm-nofoot is the mirror image of dgm-authoring, and the pair is why both are
   classes rather than lists of buttons: authoring names what a presentation
   takes away, nofoot names what it has to put back. It is on exactly one
   control, Restart, and it says why that control exists — Home does the same
   thing, so does clicking step 1, and so does dragging the scrub to zero, which
   is what a reader actually reaches for. The two modes below are the two that
   hide `.dgm-foot`: with no scrub on screen there is no other way back to the
   start without a keyboard, and a presenter at a lectern has a pointer.

   (0,2,0) against the (0,1,0) default, so it wins wherever it applies whatever
   the order. It cannot collide with the authoring rule above at the same
   weight: Restart is deliberately not authoring, so no element carries both.

   The reel arm looks redundant and is not quite: `.dgm-reel .dgm-rail` hides
   the whole column further down, so nothing shows there today. The class
   states the reason rather than the mode, and a reel is a mode with no scrub;
   should the reel ever grow a rail, the button that belongs in it is this one. */
.dgm-nofoot { display: none; }
.dgm-present .dgm-nofoot,
.dgm-reel .dgm-nofoot { display: block; }

/* No floor. This once had one, to make the stage big after the chrome went
   away; the shell now hands it everything the caption leaves, and a 60vh
   minimum could only overflow a short window. */
.dgm-present .dgm-stage { min-height: 0; }

/* Long narration must not fight the stage for the window. The caption keeps
   its content height up to a third of the screen and scrolls internally past
   that; it is exempt from the shell's flex squeeze, because when someone is
   presenting, the words being said are the one thing that must never be
   clipped — the diagram scales, the text does not. */
.dgm-present .dgm-caption {
  min-height: 0;
  padding: 16px 20px;
  flex: 0 0 auto;
  max-height: 34vh;
  overflow-y: auto;
}
.dgm-present .dgm-caption-name { font-size: 18px; }
.dgm-present .dgm-caption-desc { font-size: 1.45rem; line-height: 1.45; }

/* Presenting into one pane of a split view is not presenting, so Present asks
   the browser for the screen — and puts this class on unconditionally, granted
   or refused. On the element that *is* the fullscreen element, position: fixed
   resolves against the viewport and z-index is inert in the top layer, so the
   box is identical either way; on a host that refused — a webview, an iframe
   without allowfullscreen, a mode entered with no user gesture behind it — the
   same rule pins the player to the window by hand, minus the browser chrome
   going away. One plain class rule on purpose: a pseudo-class spelling here
   (`:fullscreen`, `:-webkit-full-screen`) invalidates the whole selector list
   in any browser missing it, which is why this used to be three identical
   rules. Fullscreen's own backdrop is black, so the surface paints itself; the
   page's max-width and centring go, and the shell rules then divide the screen
   exactly as they divide a window. The z-index only has to beat the host's
   page: the help sheet (20) and the lightbox (30) are children of the root, so
   they stack inside it either way. */
.dgm.dgm-present-fill {
  background: var(--dgm-bg);
  height: 100%;
  width: 100%;
  max-width: none;
  margin: 0;
  box-sizing: border-box;
  overflow: hidden;
  position: fixed;
  inset: 0;
  z-index: 40;
}

/* Reel mode: the diagram as a vertical story. Entered by URL (`?reel`) like
   embed, never toggled in page; a tap plays exactly one step, the segmented
   bar above the stage says which beat this is, and the caption does the
   narrating at phone size. The chrome goes entirely — a reel is watched, not
   operated — and record --reel photographs this exact layout. */
.dgm-reel .dgm-bar,
.dgm-reel .dgm-steps,
.dgm-reel .dgm-foot,
.dgm-reel .dgm-crumb { display: none; }

/* One column, board or not — a 9:16 frame has no room for a side panel. The
   board keeps its place as content (below the stage, capped) rather than
   furniture. The three-class variant is what outranks the
   .dgm-has-board .dgm-body rule in the layout section, same trick as embed
   and present above. */
.dgm-reel .dgm-body {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  flex: 1 1 0;
  min-height: 0;
}
.dgm-reel.dgm-has-board .dgm-body {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) auto;
}

/* Portrait page: cap the column at 9:16 against the viewport height and let
   the stage take every pixel the caption and bar leave. */
.dgm-standalone .dgm.dgm-reel {
  max-width: min(100vw, 56.25vh);
  height: 100vh;
  overflow: hidden;
}

.dgm-reel .dgm-stage {
  height: 100%;
  min-height: 0;
  box-sizing: border-box;
}

/* The holder's own height and the svg clamp that reads it used to live here.
   They are unscoped now, in the layout section: fitting the diagram to the
   room it has is what every host that bounds the player wants, and the reel
   was only the first one to need it. */

/* The caption is the reel's narration track, at a FIXED height — not a
   minimum. The reel column is viewport-sized, so a caption that grew with
   its text would resize the stage from step to step: notes would be placed
   against the previous step's geometry, the camera's cached keyframes would
   go stale, and a recording's stage would jump between frames. One constant
   allotment keeps every frame's layout identical. */
.dgm-reel .dgm-caption {
  flex: 0 0 auto;
  height: 22vh;
  min-height: 0;
  padding: 18px 20px;
  overflow: hidden;
}
.dgm-reel .dgm-caption-name { font-size: 22px; }
.dgm-reel .dgm-caption-desc { font-size: 1.35rem; line-height: 1.4; }

.dgm-reel .dgm-board-stack {
  max-height: 22vh;
  min-height: 120px;
}

/* The story bar. Hidden everywhere but the reel; the fill widths are inline
   styles the clock writes, deliberately with no transition — the bar has to
   photograph exactly at any paused millisecond. */
.dgm-reelbar { display: none; }

.dgm-reel .dgm-reelbar {
  display: flex;
  gap: 4px;
  padding: 4px 2px;
}

.dgm-reelbar-seg {
  flex: 1;
  height: 3px;
  border-radius: 2px;
  background: var(--dgm-border);
  overflow: hidden;
}

.dgm-reelbar-fill {
  height: 100%;
  width: 0;
  background: var(--dgm-fg);
}

/* Off-screen holding pen for the share link when the clipboard API is not
   available — a page opened from the filesystem is not a secure context. It has
   to be focusable and selectable, so it cannot be display:none. */
.dgm-linkbox {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.dgm-btn.is-on {
  border-color: var(--dgm-accent);
  color: var(--dgm-accent);
}

/* --- focus ---------------------------------------------------------------
   One visible ring for everything reachable by keyboard, including the SVG
   groups that only became focusable because the runtime said so. */

.dgm-btn:focus-visible,
.dgm-select:focus-visible,
.dgm-step:focus-visible,
.dgm-chip:focus-visible,
.dgm-scrub:focus-visible,
.dgm-scrub-mark:focus-visible {
  outline: 2px solid var(--dgm-accent);
  outline-offset: 2px;
}

.dgm-clickable:focus-visible {
  outline: 2px solid var(--dgm-accent);
  outline-offset: 3px;
}

/* --- the settings and shortcuts sheet ------------------------------------ */

.dgm-help {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.35);
  padding: 16px;
}

.dgm-help-panel {
  background: var(--dgm-bg);
  color: var(--dgm-fg);
  border: 1px solid var(--dgm-border);
  border-radius: 12px;
  padding: 18px 20px;
  min-width: 300px;
  max-width: 440px;
  box-shadow: 0 20px 60px var(--dgm-shadow);
}

.dgm-help-title {
  font-weight: 600;
  margin-bottom: 10px;
}

.dgm-help-note {
  font-size: 12px;
  color: var(--dgm-muted);
  margin-bottom: 10px;
  line-height: 1.5;
}

/* The sheet holds two kinds of thing now, so each half is named: a title alone
   cannot say that the menu above the list is a setting and the list below it is
   documentation. Quiet and small — these are signposts between sections, not
   headings competing with the sheet's own. */
.dgm-help-section {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dgm-muted);
  /* No top margin: everything that can stand above this carries its own bottom
     one, so the spacing is owned in one direction throughout the panel. */
  margin: 0 0 8px;
}

/* A <label> wrapping its control, which is why this is a flex row rather than a
   grid like the shortcut list: the name goes left, the control right, and the
   whole row is the click target that focuses it. */
.dgm-help-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 0 0 14px;
  font-size: 13px;
  cursor: pointer;
}

.dgm-help-list {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 6px 16px;
  margin: 0 0 14px;
  font-size: 13px;
}

.dgm-help-list dt {
  font-family: 'SF Mono', ui-monospace, Menlo, Consolas, monospace;
  font-size: 12px;
  color: var(--dgm-accent);
  white-space: nowrap;
}

.dgm-help-list dd {
  margin: 0;
  color: var(--dgm-muted);
}

.dgm-warning {
  background: var(--dgm-warn-bg);
  color: var(--dgm-warn-fg);
  border: 1px solid currentColor;
  border-radius: 8px;
  padding: 8px 12px;
  font-size: 13px;
}

/* --- layout -------------------------------------------------------------
 *
 * The bounded shell. Where a host gives the player root a definite height —
 * the standalone page, a generated site page, the playground pane — the body
 * takes whatever the bar, the caption and the transport leave, the stage
 * stretches into that, and the diagram scales down to fit. Without it a tall
 * diagram grows the stage to its natural height and pushes the narration and
 * the scrubber below the fold, which is the one thing a walkthrough cannot
 * afford. Reel mode has worked this way from the start; the rules are simply
 * not scoped to it any more.
 *
 * None of it fires in a host that owns the document around an auto-height
 * diagram — a VS Code Markdown preview, an inline block in a page: flex-grow
 * has no free space to take, and a percentage height against an auto-height
 * parent is not a length.
 */

/* The rows are left implicit, and that is deliberate rather than an oversight.
   The reel names its rows because it always has one or two of them; here the
   count depends on the mode, and an explicit minmax(0, 1fr) first row would
   only be right when the stage is the sole item. Wherever the narrow layout
   stacks the stage, the storyboard and the step list into one column, a 1fr
   first row is measured after the two auto rows have taken what they need and
   collapses to nothing, which the stage's own min-height then overflows —
   worse than the implicit auto rows, which stretch to share a definite height
   between them. */
.dgm-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 280px;
  gap: 16px;
  align-items: start;
  flex: 1 1 auto;
  min-height: 0;
}

/* With a storyboard the stage still dominates: the panel takes a column of its
   own and the step list keeps the width it always had, so adding a storyboard
   costs the diagram some room rather than rearranging the page. */
.dgm-has-board .dgm-body {
  grid-template-columns: minmax(0, 1fr) minmax(200px, 320px) 280px;
}

@media (max-width: 900px) {
  /* Every two-column variant has to be named here, not just .dgm-body: the
     board rules above are more specific, so a narrow screen would otherwise
     keep squeezing a 240px panel beside the diagram. */
  .dgm-body,
  .dgm-has-board .dgm-body,
  .dgm-embed.dgm-has-board .dgm-body,
  .dgm-present.dgm-has-board .dgm-body,
  .dgm-reel.dgm-has-board .dgm-body {
    grid-template-columns: minmax(0, 1fr);
  }

  /* One column means everything stacks, so a page clipped to the window would
     hide the step list outright. The page scrolls here as it always has; the
     stage takes a fixed share of the window instead, which puts the caption a
     short scroll below the diagram rather than a screenful. A reel is exempt:
     it is a portrait column by construction and owns its own heights.

     Present is exempt for the same kind of reason. The escape hatch is for the
     *scrolling* page, and present is never one: it is a bounded shell either
     way — real fullscreen, or the fixed, inset-0, overflow-hidden
     .dgm-present-fill fallback a phone always lands on, since iOS Safari has
     no element fullscreen. Without the exemption the hatch (0,3,0) simply
     outranks the fill rule (0,2,0), the stage keeps its 55vh, and the rows
     stacked below it push the caption and the storyboard out of the bottom of
     a window that cannot scroll — where the board's absolutely positioned
     crossfade layers paint over the narration on the way past. Exempted,
     present inherits the shell: the body flexes into the height the caption
     leaves, the stage stretches into that, and the svg's max-height fits the
     whole diagram. */
  .dgm-standalone .dgm:not(.dgm-reel):not(.dgm-present) {
    height: auto;
    min-height: 100vh;
    overflow: visible;
  }

  .dgm-standalone .dgm:not(.dgm-reel):not(.dgm-present) .dgm-stage {
    height: 55vh;
  }

  /* Present on a phone: the storyboard becomes a picture-in-picture thumbnail.
     A 380px panel stacked under the diagram was unreadable at this width and
     cost the diagram half the screen; the lightbox is the real full-size look
     at a frame, and the panel's job here is only to say one is available and
     be tappable. So it leaves the grid entirely — the stage gets the column
     back — and parks top-left, across from the minimap's top-right.

     Not scoped to .dgm-standalone: present uses the fill fallback in the
     playground pane and in a VS Code webview too, and those are the hosts most
     likely to be narrow.

     touch-action: none is what lets the swipe-to-dismiss gesture in
     bindBoardGestures see horizontal pointermoves instead of losing them to
     the browser's own scrolling; user-select and user-drag keep a swipe from
     turning into a text selection or a native image drag. */
  .dgm-present .dgm-body { position: relative; }

  .dgm-present .dgm-board {
    position: absolute;
    left: 10px;
    top: 10px;
    width: min(32%, 150px);
    z-index: 6;
    padding: 4px;
    box-shadow: 0 8px 24px var(--dgm-shadow);
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
  }

  .dgm-present .dgm-board-img { -webkit-user-drag: none; }

  /* The title and the caption are panel furniture; at thumbnail size they
     would crowd out the picture they describe. */
  .dgm-present .dgm-board-title,
  .dgm-present .dgm-board-caption { display: none; }

  /* The stack still needs a height of its own — its two crossfade layers are
     absolutely positioned and contribute none — but 180px is a panel's floor,
     not a thumbnail's. A ratio sizes it from the width instead. */
  .dgm-present .dgm-board-stack {
    min-height: 0;
    aspect-ratio: 16 / 10;
  }

  /* A caption-only frame has no picture to preview and cannot open the
     lightbox, so with the caption hidden the thumbnail would be an empty chip
     over the diagram. */
  .dgm-present .dgm-board.is-wordy { display: none; }
}

/* The other escape hatch, and the same bargain as the 900px one above: the
   shell divides a definite height between the bar, the stage, the caption and
   the transport, and below roughly 500px — the stage's own 320px floor plus
   the chrome — there is no height left to divide. A landscape phone, a short
   browser window, a split editor pane: rather than clip the transport off the
   bottom of a page that cannot scroll, the page becomes a scrolling one again
   and the reader gets everything back by scrolling for it.

   Reel and present are exempt for exactly the reasons the width hatch states:
   a reel owns its own heights, and present is a bounded shell either way and
   has no scrollbar to offer. */
@media (max-height: 520px) {
  .dgm-standalone .dgm:not(.dgm-reel):not(.dgm-present) {
    height: auto;
    min-height: 100vh;
    overflow: visible;
  }

  /* And the same second half, for the same reason the width hatch gives: with
     nothing bounding it, a tall diagram grows the stage to its natural height
     — a thousand pixels of sequence diagram above a caption nobody will scroll
     that far to find. A share of the window instead, floored by the stage's own
     320px minimum, keeps the narration a short scroll away. */
  .dgm-standalone .dgm:not(.dgm-reel):not(.dgm-present) .dgm-stage {
    height: 55vh;
  }
}

/* overflow is hidden rather than scrolled: the stage is pannable now, and a
   scrollbar competing with drag-to-pan reads as two ways to do one thing.

   align-self overrides the grid's align-items:start for the stage alone, so
   the diagram claims the whole row while the step list and the storyboard
   still sit at the top of theirs. */
.dgm-stage {
  position: relative;
  background: var(--dgm-panel);
  border: 1px solid var(--dgm-border);
  border-radius: 12px;
  padding: 20px;
  /* The tool rail floats over this edge, and a floating control that covers
     the picture also swallows the clicks meant for it: a node with a
     `click … view` binding near the right edge — examples/03-interaction has
     several — would simply not be reachable. So the strip is reserved rather
     than shared: 42px of rail, 10px of inset and 10px of breath, which is
     enough that a diagram scaled to fit never lands underneath it.

     Only at fit. Zooming or panning can still put a node under the rail, and
     that is the reader's own doing, undone by the same gesture or a
     double-click; the rail's translucency is there to admit it. */
  padding-right: 62px;
  overflow: hidden;
  min-height: 320px;
  align-self: stretch;
  cursor: grab;
  touch-action: none;
}

/* No rail in these three, so no strip to reserve — and an embed or a reel is
   already short of room. Same list, in the same order, as the rule that hides
   the column further down. */
.dgm-inline .dgm-stage,
.dgm-embed .dgm-stage,
.dgm-reel .dgm-stage { padding-right: 20px; }

.dgm-stage:active { cursor: grabbing; }

/* The holder needs a height of its own for the clamp below to mean anything,
   and a diagram smaller than its stage wants to sit in the middle of it
   rather than at the top. */
.dgm-svg-holder {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* max-height is what actually fits a tall diagram into the stage instead of
   letting the stage's overflow:hidden crop it. The runtime writes height:auto
   inline on the svg, so the viewBox ratio does the scaling and the width
   follows from it. */
.dgm-svg-holder svg {
  display: block;
  margin: 0 auto;
  max-height: 100%;
  width: auto;
}

.dgm-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* --- minimap -------------------------------------------------------------
 *
 * The whole diagram in the stage's corner, with a rectangle around the region
 * the stage is showing. On screen only while something has zoomed past fit, so
 * a page at rest — and every recording, which photographs ?embed at rest — is
 * unchanged. The runtime writes the box's exact width and height from the
 * diagram's aspect ratio; the caps here are the share of the stage it fits
 * into, stated twice so a slow first layout is never oversized.
 *
 * The z-index puts it above pills and notes — and above the tool rail, which
 * would otherwise cover the map's own corner on any stage short enough for the
 * map to hit its 40% height cap, and take the clicks meant for it. A pill
 * anchored to a node in the top-right corner, and the rail's topmost buttons,
 * can therefore end up underneath the map — both accepted, and for the same
 * reason: the map is only on screen while something has zoomed past fit, it is
 * the instrument the reader is using at that moment, and it is the smaller
 * claim on that corner.
 *
 * It carries the fit control too, which used to be a rail button. The map is
 * shown exactly while the stage holds less than the whole diagram, which is
 * exactly when fitting means anything, so putting the button inside the box
 * costs it no visibility rules of its own — .is-off below takes it away with
 * everything else.
 */

.dgm-map {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 9;
  max-width: 28%;
  max-height: 40%;
  box-sizing: border-box;
  overflow: hidden;
  border: 1px solid var(--dgm-border);
  border-radius: 8px;
  background: var(--dgm-bg);
  box-shadow: 0 2px 10px var(--dgm-shadow);
  opacity: 0.95;
  cursor: pointer;
  touch-action: none;
}

.dgm-map.is-off { display: none; }

.dgm-map-body {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* No transition on the rectangle: it describes the frame the diagram is drawn
   at, and one that eased into place would lag the thing it describes. */
.dgm-map-rect {
  position: absolute;
  box-sizing: border-box;
  border: 1.5px solid var(--dgm-accent);
  background: var(--dgm-accent-soft);
  pointer-events: none;
}

/* Fit, in the map's bottom-right corner: the corner furthest from the diagram's
   own top-left, and the one the view rectangle is least often parked in.
   Opaque rather than translucent, because it sits on a picture rather than on
   the page's background and a wash over a thumbnail is unreadable — the box
   itself is small enough that a solid 22px square is not much of a claim.

   Deliberately not a .dgm-btn: the rail's button rules are forbidden a
   `display` (see the note in the rail section) and are sized for a 42px
   column, neither of which suits a control living inside a 220px thumbnail.
   The colours are the same tokens all the same, so the two read as one set. */
.dgm-map-fit {
  position: absolute;
  right: 4px;
  bottom: 4px;
  width: 22px;
  height: 22px;
  padding: 0;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  appearance: none;
  border: 1px solid var(--dgm-border);
  border-radius: 6px;
  background: var(--dgm-panel);
  color: var(--dgm-fg);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}

.dgm-map-fit:hover,
.dgm-map-fit:focus-visible {
  border-color: var(--dgm-accent);
  color: var(--dgm-accent);
}

/* 14px inside a 22px box. The glyph is drawn at 16 by icon(), which sets the
   attributes rather than the style, so a rule here overrides them. */
.dgm-map-fit svg {
  display: block;
  width: 14px;
  height: 14px;
}

/* --- the tool rail -------------------------------------------------------
 *
 * Everything the bar used to carry except Play and Present, in one column over
 * the stage's right edge: Restart, Cine, Copy link, help. See buildRail in
 * runtime.js for what each of them is and why the classes stayed with them.
 * Restart is the one that is usually not there — it carries dgm-nofoot, and
 * stands only in the modes with no scrub — so the column is three at rest.
 *
 * Three things were here and are not. Fit moved into the minimap, which appears
 * at exactly the moment fitting means anything. Theme is the page's, not one
 * diagram's, so its control is page chrome. Speed moved into the settings
 * sheet, which is what let this column stop being sized around a word.
 *
 * Vertically centred rather than pinned to a corner, because both corners are
 * taken — the minimap top-right, the presenter storyboard top-left.
 *
 * z-index 8 puts it over the overlay's notes and pills, under the minimap (9),
 * which is the instrument in use whenever it is on screen, and under the help
 * sheet (20) and the lightbox (30) — those are children of the root rather than
 * the stage, so they cover it whatever it says here.
 *
 * Translucent rather than hover-to-reveal: a control nobody can see is a
 * control nobody knows exists. It goes solid on hover or when something inside
 * it takes focus, which is exactly when the diagram behind it stops mattering.
 *
 * 42px wide, stated rather than derived because there is nothing left in here
 * worth measuring: every button is one 16px glyph in the same 32px square this
 * sheet draws every icon target in. 32 of button, 4 of padding each side and a
 * 1px border each side is the 42.
 *
 * It was 56, and the whole of the extra 14 was the speed button: its label was
 * its own value, so the column had to be wide enough for "0.25x" or every
 * target in it would move under the cursor the moment the rate changed — the
 * exact resize .dgm-rail-note still goes out of its way to avoid. Speed is a
 * menu in the settings sheet now, so the column is as wide as a glyph and no
 * wider.
 */
.dgm-rail {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 8;
  width: 42px;
  padding: 4px;
  box-sizing: border-box;
  /* The buttons stack down the column and stretch across it. align-items is
     left out: stretch is already the default, and a rail button that did not
     fill the width would be a target that moves with its label. */
  display: flex;
  flex-direction: column;
  gap: 6px;
  border: 1px solid var(--dgm-border);
  border-radius: 10px;
  /* Opaque by default, so a browser with no backdrop-filter still gets a
     legible panel rather than a wash. The @supports below is what trades that
     for the blur where there is one — stated in this order because a fallback
     that has to be undone is a fallback that works. */
  background: var(--dgm-panel);
  box-shadow: 0 2px 10px var(--dgm-shadow);
  transition: background 0.15s, border-color 0.15s;
}

@supports ((backdrop-filter: blur(8px)) or (-webkit-backdrop-filter: blur(8px))) {
  .dgm-rail {
    background: var(--dgm-panel-soft);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
  }
}

/* (0,2,0), so it wins over the @supports block above wherever it applies
   regardless of source order. */
.dgm-rail:hover,
.dgm-rail:focus-within {
  background: var(--dgm-panel);
  border-color: var(--dgm-accent);
}

/* Nothing at (0,2,0) or above may state a `display` on an element that might
   carry dgm-authoring. `.dgm-present .dgm-authoring { display: none }` is
   (0,2,0) and stands earlier in this sheet, so a `display` here — on
   `.dgm-rail .dgm-btn`, which matches the Copy link button — would quietly
   put it back in the middle of a presentation. The box is sized by
   padding instead: a 16px glyph inside 7px of padding and a 1px border is 32px
   tall, with no flex container needed to centre it. The width comes from the
   column, which the buttons stretch to fill.

   `.dgm-nofoot` states one at (0,2,0) and is allowed to, for the stronger
   version of the same reason: no element carries both classes, and the whole
   point of that one is to be shown by the mode this rule's neighbour hides
   things in. */
.dgm-rail .dgm-btn {
  position: relative;
  min-width: 32px;
  height: 32px;
  /* 7px down each side of a 16px glyph is the 32px height; across, the column
     supplies exactly the 32 the min-width asks for, and icon() centres the
     glyph in it. */
  padding: 7px 2px;
  box-sizing: border-box;
  border-radius: 8px;
  /* Transparent at rest, so the panel behind them is the panel the reader sees:
     the base .dgm-btn paints var(--dgm-panel), which is the rail's own
     background *and* the stage's, so a column of opaque buttons turned the blur
     into a wash nobody could tell from the ground, and left :hover's background
     swap with nothing to swap. The panel treatment comes back on hover and on
     focus, where it is what says which target the press will land on. */
  background: transparent;
  border-color: transparent;
}

.dgm-rail .dgm-btn:hover,
.dgm-rail .dgm-btn:focus-visible {
  background: var(--dgm-panel);
  border-color: var(--dgm-accent);
}

/* is-on is a state rather than a pointer's whereabouts — the camera is
   following, the diagram is zoomed — so it is drawn as one at rest too, and
   more strongly than a hover: the accent tints the box, not just its edge. */
.dgm-rail .dgm-btn.is-on {
  background: var(--dgm-accent-soft);
  border-color: var(--dgm-accent);
  color: var(--dgm-accent);
}

.dgm-rail .dgm-btn svg {
  display: block;
  margin: 0 auto;
}

/* The copy confirmation. Absolutely positioned inside the button, to the left
   of the column: it says a word without the rail changing width, which on a
   button the reader has just clicked would move the next click's target. */
.dgm-rail-note {
  position: absolute;
  right: calc(100% + 10px);
  top: 50%;
  transform: translateY(-50%);
  white-space: nowrap;
  padding: 3px 8px;
  border-radius: 6px;
  background: var(--dgm-fg);
  color: var(--dgm-bg);
  font-size: 11px;
  font-weight: 500;
  line-height: 16px;
  box-shadow: 0 2px 8px var(--dgm-shadow);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s;
}

.dgm-rail-note.is-on { opacity: 1; }

/* Hosts that show no chrome show no rail either, and each for the reason it
   hides the bar: a diagram inline in a document is a reader's, not an author's;
   an embed's page has its own furniture — and every recording photographs
   `?embed`, so a rail here would appear in every GIF the recorder makes; a reel
   is watched rather than operated. Named separately rather than left to
   `.dgm-embed` covering inline, because the inline contract is the one that
   must not move. */
.dgm-inline .dgm-rail,
.dgm-embed .dgm-rail,
.dgm-reel .dgm-rail { display: none; }

/* There is no narrow-screen collapse. There was one, at 520px, because five
   targets stacked over a phone-sized diagram is most of the diagram; three
   42px squares is 62px of a 400px screen, which the stage reserves as padding
   at every width rather than covering the picture at one of them. The column
   that needed hiding is the column that no longer exists. */

/* --- the page's theme control --------------------------------------------
 *
 * Dark and light belong to the page, not to a diagram on it, so this button is
 * page chrome: it sits outside #cinegram — which the player empties on every
 * render — and writes nothing but `data-theme` on the root element, which every
 * player is watching. Three surfaces put it somewhere of their own: the emitted
 * page fixes it to the corner (below), a `cinegram site` page drops it in the
 * header's action row, the playground in its own toolbar. So the button is
 * drawn here and placed there.
 *
 * Sized and coloured like a rail button, because it is the same kind of thing
 * even though it is not in the rail — a 16px glyph inside 7px of padding and a
 * 1px border, on the same tokens. It carries no .dgm-btn class: that one is
 * inside the player's own cascade, where presenter mode and inline mode reach
 * in and hide things, and none of that applies to a control on the page.
 */
.dgm-page-theme {
  appearance: none;
  -webkit-appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 7px;
  box-sizing: border-box;
  border: 1px solid var(--dgm-border);
  border-radius: 8px;
  background: var(--dgm-panel);
  color: var(--dgm-fg);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}

.dgm-page-theme:hover {
  border-color: var(--dgm-accent);
  color: var(--dgm-accent);
}

.dgm-page-theme:focus-visible {
  outline: 2px solid var(--dgm-accent);
  outline-offset: 2px;
}

.dgm-page-theme svg { display: block; }

/* Only the page cinegram emits whole gets the control pinned to the window:
   scoped to .dgm-standalone, so a document that merely loads this stylesheet
   keeps its own layout and its own idea of where a button goes — the same
   arrangement the rest of the page rules are under.
   The z-index clears the rail (8) and the minimap (9), which are the stage's
   own instruments and would otherwise cover a corner of it, and stays under the
   help sheet (20) and the lightbox (30): while one of those is open the page
   behind it is not the thing being operated. */
.dgm-standalone > .dgm-page-theme {
  position: fixed;
  top: 12px;
  right: 12px;
  z-index: 12;
}

/* And the bar gives back the width the fixed control covers. The bar's right
   end is where Play and Present sit, and the page is centred in at most 1400px
   — so on any window narrower than that plus two gutters the control lands on
   top of Present. A sited page is exempt because its control is in the site
   header instead, in the flow, taking its own room. */
.dgm-standalone:not(.dgm-sited) .dgm-bar { padding-right: 44px; }

/* --- storyboard ----------------------------------------------------------
 *
 * What the human sees, beside what the system does. The two image layers are
 * stacked and crossfade by opacity, which is why the stack needs a height of
 * its own — absolutely positioned children contribute none.
 */

/* The panel scrolls in its own column rather than lengthening the page: in a
   bounded shell the page has no scroll left to give it. */
.dgm-board {
  display: flex;
  flex-direction: column;
  gap: 8px;
  border: 1px solid var(--dgm-border);
  border-radius: 12px;
  background: var(--dgm-panel);
  padding: 12px;
  box-sizing: border-box;
  max-height: 100%;
  overflow: auto;
}

/* The swipe's exit and its return. The gesture code writes the transform and
   the opacity inline; these classes own the motion between them, so the
   durations live where every other transition does — including the
   reduced-motion shortening, which is what lets the runtime wait for a
   transitionend instead of second-guessing whether one will come. */
.dgm-board.is-flinging { transition: transform 0.18s ease-in, opacity 0.18s ease-in; }
.dgm-board.is-springing { transition: transform 0.15s ease-out, opacity 0.15s ease-out; }

.dgm-board-title {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--dgm-muted);
}

.dgm-board-stack {
  position: relative;
  min-height: 180px;
  border-radius: 8px;
  overflow: hidden;
  background: var(--dgm-bg);
}

/* A caption-only frame has no picture, so the box that would have held one
   collapses and the words get the room instead. */
.dgm-board.is-wordy .dgm-board-stack {
  min-height: 0;
  background: none;
}

.dgm-board-layer {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.25s ease;
}

.dgm-board-layer.is-front { opacity: 1; }

.dgm-board-img {
  max-width: 100%;
  max-height: 100%;
  display: block;
}

/* The panel is a preview; a frame with a picture invites the full-size look. */
.dgm-board.is-pictorial .dgm-board-stack {
  cursor: zoom-in;
}

.dgm-board-caption {
  font-size: 13px;
  color: var(--dgm-fg);
  white-space: pre-line;
}

.dgm-board.is-wordy .dgm-board-caption {
  font-size: 15px;
  padding: 24px 8px;
  text-align: center;
  color: var(--dgm-muted);
}

/* --- storyboard lightbox --------------------------------------------------
 *
 * Same overlay tier as the help dialog, one layer above it: Esc closes the
 * innermost thing first, and that order is the player's, not the stylesheet's.
 * The image's *layout* size is the fit (so an SVG frame re-rasterises crisp at
 * any viewport); zooming is a transform on top of that, so the wheel gesture
 * never causes layout. No transition on the transform — a cursor-anchored
 * zoom that lags its cursor feels broken.
 */

.dgm-lightbox {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: rgba(0, 0, 0, 0.72);
  overscroll-behavior: contain;
  touch-action: none;
}

.dgm-lightbox:focus {
  outline: none;
}

.dgm-lightbox-img {
  display: block;
  flex: 0 0 auto;
  transform-origin: center center;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  -webkit-user-drag: none;
}

.dgm-lightbox-img.is-panning {
  cursor: grabbing;
}

.dgm-lightbox-caption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 14px 18px;
  text-align: center;
  font-size: 13px;
  color: #fff;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.55));
  white-space: pre-line;
  pointer-events: none;
}

.dgm-lightbox-caption:empty {
  display: none;
}

/* --- step list ---------------------------------------------------------- */

/* overflow was hidden, for the radius; auto clips the same way and adds the
   scrollbar a long walkthrough needs once the shell stops the column growing
   past the window. */
.dgm-steps {
  list-style: none;
  counter-reset: dgm-step;
  margin: 0;
  padding: 0;
  border: 1px solid var(--dgm-border);
  border-radius: 12px;
  max-height: 100%;
  overflow: auto;
}

.dgm-step-row {
  counter-increment: dgm-step;
}

/* A real <button>, so it is in the tab order and answers Enter and Space
   without any of that being reimplemented. The reset undoes what being a
   button brings with it. */
.dgm-step {
  appearance: none;
  width: 100%;
  margin: 0;
  font: inherit;
  color: inherit;
  background: none;
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px 10px 34px;
  position: relative;
  cursor: pointer;
  border: 0;
  border-bottom: 1px solid var(--dgm-border);
  transition: background 0.15s;
}

.dgm-step-row:last-child .dgm-step { border-bottom: 0; }

.dgm-step::before {
  content: counter(dgm-step);
  position: absolute;
  left: 10px;
  top: 10px;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  border: 1px solid var(--dgm-border);
  font-size: 10px;
  line-height: 17px;
  text-align: center;
  color: var(--dgm-muted);
}

.dgm-step:hover { background: var(--dgm-accent-soft); }

.dgm-step.is-active {
  background: var(--dgm-accent-soft);
  box-shadow: inset 3px 0 0 var(--dgm-accent);
}

.dgm-step.is-active::before {
  background: var(--dgm-accent);
  border-color: var(--dgm-accent);
  color: var(--dgm-on-accent);
}

.dgm-step.is-done .dgm-step-name { color: var(--dgm-muted); }

.dgm-step-name { font-size: 13px; }
.dgm-step-time { font-size: 11px; color: var(--dgm-muted); font-variant-numeric: tabular-nums; }

/* --- caption ------------------------------------------------------------ */

/* The narration for the step the clock is inside. It keeps its height when
   empty so the stage does not jump as the caption comes and goes between
   steps. */
.dgm-caption {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-height: 42px;
  box-sizing: border-box;
  padding: 9px 14px;
  border: 1px solid var(--dgm-border);
  border-radius: 10px;
  background: var(--dgm-panel);
  opacity: 0;
  transition: opacity 0.2s ease;
}

.dgm-caption.is-on { opacity: 1; }

/* The same treatment present mode gets, for the same reason: on a bounded page
   a step with a paragraph of narration would otherwise take its height out of
   the stage. The cap keeps the diagram's share of the window predictable, and
   the scrollbar makes the overflow readable instead of clipped — the default
   flex-shrink and the 42px floor above still do the squeezing, this only says
   what happens once they run out.

   Only where the shell bounds the page: a host that owns an auto-height
   document has the room below the diagram and should use it. And only in the
   plain mode — present and reel each state their own caption height above,
   and being written later this rule would otherwise quietly outrank both on
   equal specificity, putting a scrollbar in a recorded reel's frames. */
.dgm-standalone .dgm:not(.dgm-reel):not(.dgm-present) .dgm-caption {
  max-height: 34vh;
  overflow-y: auto;
}

.dgm-caption-name {
  font-weight: 600;
  font-size: 13px;
}

.dgm-caption-desc {
  font-size: 13px;
  color: var(--dgm-muted);
  white-space: pre-line;
}

/* --- transport ---------------------------------------------------------- */

.dgm-foot {
  display: flex;
  align-items: center;
  gap: 12px;
}

.dgm-scrub-wrap {
  position: relative;
  flex: 1;
  display: flex;
  align-items: center;
}

.dgm-scrub {
  flex: 1;
  accent-color: var(--dgm-accent);
}

/* Step boundaries, laid over the scrubber. The container ignores the pointer
   so dragging the range still works everywhere between the ticks; each tick
   takes it back for itself. */
.dgm-scrub-marks {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.dgm-scrub-mark {
  position: absolute;
  top: 50%;
  width: 3px;
  height: 12px;
  margin-left: -1.5px;
  transform: translateY(-50%);
  padding: 0;
  border: 0;
  border-radius: 2px;
  background: var(--dgm-muted);
  opacity: 0.5;
  cursor: pointer;
  pointer-events: auto;
  transition: opacity 0.15s, background 0.15s, height 0.15s;
}

.dgm-scrub-mark:hover,
.dgm-scrub-mark:focus-visible {
  background: var(--dgm-accent);
  opacity: 1;
  height: 17px;
}

.dgm-clock {
  font-size: 12px;
  color: var(--dgm-muted);
  font-variant-numeric: tabular-nums;
  min-width: 96px;
  text-align: right;
}

/* --- animation states --------------------------------------------------- */

.dgm-layer {
  pointer-events: none;
}

/* --dgm-color is the per-track override. The runtime sets it inline on the
   particle group or the node group when a `color:` attribute was compiled in,
   and every rule below reads it through var(--dgm-color, <theme token>) so an
   absent colour falls back to exactly what the theme did before. Because it is
   a custom property it inherits to the shape elements mermaid drew, which is
   where the stroke and fill actually have to land. */

/* The particle itself: an arrowhead, not a dot. A dot has no direction, and
   direction is the one thing the diagram underneath cannot say — the edge is
   drawn once, pointing the way the author declared it, and a response runs
   back along it. Filled and given the corner join so the notch stays crisp at
   this size. */
.dgm-particle-head {
  fill: var(--dgm-color, var(--dgm-accent));
  stroke-linejoin: miter;
}

.dgm-particle-halo {
  fill: var(--dgm-color, var(--dgm-accent));
  opacity: 0.25;
  animation: dgm-pulse 1.1s ease-out infinite;
}

.dgm-particle-label {
  fill: var(--dgm-fg);
  font-size: 11px;
  font-weight: 600;
  paint-order: stroke;
  stroke: var(--dgm-bg);
  stroke-width: 4px;
  stroke-linejoin: round;
}

.dgm-particle-response .dgm-particle-head,
.dgm-particle-response .dgm-particle-halo {
  fill: var(--dgm-color, var(--dgm-response));
}

/* A failing flow reads as failing before it arrives, so the particle carries
   the outcome the whole way rather than only at the ✕. */
.dgm-particle-fail .dgm-particle-head,
.dgm-particle-fail .dgm-particle-halo {
  fill: var(--dgm-color, var(--dgm-fail));
}

/* Only the failing head is outlined, and in the page background: it is the one
   that has to stay legible sitting on top of the ✕ it is about to land on. */
.dgm-particle-fail .dgm-particle-head {
  stroke: var(--dgm-bg);
  stroke-width: 1.5px;
}

@keyframes dgm-pulse {
  0%   { transform: scale(0.7); opacity: 0.35; }
  70%  { transform: scale(1.5); opacity: 0;    }
  100% { transform: scale(1.5); opacity: 0;    }
}

/* --- active edges ------------------------------------------------------- */

/* While a flow track is open its edge is lit. The transition is short and on
   the edge itself rather than on a wrapper, so a scrub that closes the track
   fades the edge back instead of snapping. */

.dgm-flow-active {
  stroke: var(--dgm-color, var(--dgm-accent)) !important;
  stroke-width: 2.6px !important;
  transition: stroke 0.15s ease, stroke-width 0.15s ease;
}

.dgm-flow-response {
  stroke: var(--dgm-color, var(--dgm-response)) !important;
}

/* A flow running against its edge takes that edge's arrowhead off for as long
   as it is open. Nothing is lost — the particle carries its own chevron, and
   the arrowhead comes back the moment the track closes — and it removes the
   one thing on screen that actively disagrees with the animation. `marker-end`
   as a property beats the presentation attribute mermaid wrote; both ends are
   cleared because a path mermaid drew from the far end wears its head at the
   start instead. */
.dgm-flow-reverse {
  marker-end: none;
  marker-start: none;
}

/* Ordered after the style hooks so a failure wins the colour: `status` is
   semantic and `style` is decoration, and the two can appear together. */
.dgm-flow-fail {
  stroke: var(--dgm-color, var(--dgm-fail)) !important;
}

/* The comet. A clone of the edge path with a dash window the runtime slides
   along it, drawn in .dgm-layer so it sits above the edge but below the
   particle that leads it. */
.dgm-trail {
  fill: none;
  stroke: var(--dgm-color, var(--dgm-accent));
  stroke-width: 5px;
  stroke-linecap: round;
  opacity: 0.4;
}

.dgm-trail-response {
  stroke: var(--dgm-color, var(--dgm-response));
}

.dgm-trail-fail {
  stroke: var(--dgm-color, var(--dgm-fail));
}

.dgm-fail-mark circle {
  fill: var(--dgm-bg);
  opacity: 0.92;
}

.dgm-fail-mark line {
  stroke: var(--dgm-fail);
  stroke-width: 3px;
  stroke-linecap: round;
}

/* Node states. Mermaid draws the node body as the shape element inside g.node,
   so the state classes target those descendants rather than the group. */

.dgm-highlight rect,
.dgm-highlight circle,
.dgm-highlight polygon,
.dgm-highlight path {
  stroke: var(--dgm-color, var(--dgm-accent)) !important;
  stroke-width: 3px !important;
  filter: drop-shadow(0 0 8px var(--dgm-color, var(--dgm-accent)));
}

.dgm-busy rect,
.dgm-busy circle,
.dgm-busy polygon,
.dgm-busy path {
  stroke: var(--dgm-color, var(--dgm-busy)) !important;
  stroke-width: 3px !important;
  filter: drop-shadow(0 0 10px var(--dgm-color, var(--dgm-busy)));
}

/* The pulse scales the shape inside the node, never the g.node itself.
   Mermaid positions g.node with a `transform` attribute, and CSS `transform` is
   the same property rather than an addition to it — animating the group throws
   the node to the origin. `transform-box: fill-box` then makes the shape scale
   about its own centre instead of the SVG's. */
.dgm-pulse rect,
.dgm-pulse circle,
.dgm-pulse polygon,
.dgm-pulse path {
  stroke: var(--dgm-color, var(--dgm-accent)) !important;
  stroke-width: 2.5px !important;
  animation: dgm-node-pulse 0.9s ease-in-out infinite;
  transform-box: fill-box;
  transform-origin: center;
}

@keyframes dgm-node-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.05); }
}

/* --- focus -------------------------------------------------------------- */

/* Focus is drawn as what recedes. Desaturating as well as fading is what makes
   it read as "not this" rather than "faint": colour is most of what pulls the
   eye across a diagram. */
.dgm-unfocused {
  opacity: 0.18;
  filter: saturate(0.15);
  transition: opacity 0.25s ease, filter 0.25s ease;
}

/* A hop's destination, in the last moments before the particle lands. It
   animates the glow rather than a transform: mermaid positions g.node with a
   `transform` attribute, and a CSS transform is the same property, so animating
   one here would move the node to the origin. */
.dgm-waypoint {
  animation: dgm-waypoint 0.6s ease-out infinite;
}

.dgm-waypoint rect,
.dgm-waypoint circle,
.dgm-waypoint polygon,
.dgm-waypoint path {
  stroke: var(--dgm-color, var(--dgm-accent)) !important;
  stroke-width: 2.5px !important;
}

@keyframes dgm-waypoint {
  0%   { filter: drop-shadow(0 0 1px var(--dgm-color, var(--dgm-accent))); }
  50%  { filter: drop-shadow(0 0 11px var(--dgm-color, var(--dgm-accent))); }
  100% { filter: drop-shadow(0 0 1px var(--dgm-color, var(--dgm-accent))); }
}

.dgm-dim { opacity: 0.28; }
.dgm-hidden { opacity: 0; }
.dgm-show { opacity: 1; }

/* --- interaction -------------------------------------------------------- */

/* A clickable element advertises itself on hover rather than permanently, so
   the diagram still reads as a diagram at rest. */
.dgm-clickable { cursor: pointer; }

.dgm-clickable:hover rect,
.dgm-clickable:hover circle,
.dgm-clickable:hover polygon,
.dgm-clickable:hover path {
  stroke: var(--dgm-accent) !important;
  stroke-width: 2.5px !important;
  filter: drop-shadow(0 0 6px var(--dgm-accent));
}

/* Collapsed is reveal state, not timeline state: an element waiting to be
   clicked into view. Unlike .dgm-hidden it also stops taking pointer events,
   so an invisible node is not accidentally clickable.

   The unfold is opacity and a blur rather than a scale, for the same reason the
   pulse avoids one: these classes land on g.node, whose `transform` attribute
   is doing the positioning. `display` stays out of it too — a display change
   cannot transition, and would pop. */
.dgm-collapsed {
  opacity: 0;
  filter: blur(3px);
  pointer-events: none;
  transition: opacity 0.3s ease, filter 0.3s ease;
}

/* --- affordance chips --------------------------------------------------- */

/* A reveal nobody can see is a reveal nobody finds: the diagram looks complete
   and the detail behind it is discovered by accident. The chip says how much is
   folded away before the click and what is being undone after. */
.dgm-chip {
  position: absolute;
  transform: translate(-45%, -55%);
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  box-sizing: border-box;
  border: 1px solid var(--dgm-accent);
  border-radius: 999px;
  background: var(--dgm-accent);
  color: var(--dgm-on-accent);
  font: inherit;
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  cursor: pointer;
  pointer-events: auto;
  box-shadow: 0 2px 6px var(--dgm-shadow);
  transition: transform 0.15s ease, background 0.15s ease;
}

.dgm-chip:hover {
  transform: translate(-45%, -55%) scale(1.12);
}

.dgm-chip.is-open {
  background: var(--dgm-bg);
  color: var(--dgm-accent);
}

.dgm-chip-view {
  background: var(--dgm-bg);
  color: var(--dgm-accent);
  font-weight: 600;
}

.dgm-noted rect,
.dgm-noted circle,
.dgm-noted polygon,
.dgm-noted path {
  stroke: var(--dgm-accent) !important;
}

.dgm-highlight,
.dgm-busy,
.dgm-dim,
.dgm-hidden,
.dgm-noted {
  transition: opacity 0.25s ease;
}

/* --- persistent state --------------------------------------------------- */

/* A standing state, as opposed to something happening to the node right now.
   It reads as a quieter, permanent version of a highlight: an outline rather
   than a glow, so an animation can still play over the top of it. */
.dgm-stated rect,
.dgm-stated circle,
.dgm-stated polygon,
.dgm-stated path {
  stroke: var(--dgm-color, var(--dgm-accent)) !important;
  stroke-width: 2px !important;
}

/* Badges and gauge readings. Anchored to the element's top-right corner and
   stacked downwards by the runtime, so a node can carry several without them
   landing on top of each other. */
.dgm-pill {
  position: absolute;
  transform: translate(-30%, -50%);
  display: flex;
  align-items: center;
  gap: 5px;
  max-width: 160px;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--dgm-color, var(--dgm-accent));
  background: var(--dgm-bg);
  color: var(--dgm-color, var(--dgm-accent));
  font-size: 11px;
  font-weight: 600;
  line-height: 16px;
  white-space: nowrap;
  box-shadow: 0 2px 6px var(--dgm-shadow);
  pointer-events: none;
}

.dgm-pill-set {
  background: var(--dgm-color, var(--dgm-accent));
  color: var(--dgm-bg);
}

.dgm-pill-label {
  font-weight: 500;
  opacity: 0.75;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 9px;
}

.dgm-pill-value {
  font-variant-numeric: tabular-nums;
}

/* --- notes -------------------------------------------------------------- */

/* The runtime positions notes by their top-left corner rather than through a
   translate, because it has to clamp them into the stage and push them clear of
   each other — arithmetic that needs the real box, not an offset from one. */
.dgm-note {
  position: absolute;
  /* border-box so the pinned width the runtime writes is the box the layout
     measured, padding included. */
  box-sizing: border-box;
  background: var(--dgm-fg);
  color: var(--dgm-bg);
  border-radius: 8px;
  padding: 6px 10px;
  font-size: 12px;
  line-height: 1.4;
  white-space: pre-line;
  box-shadow: 0 6px 20px var(--dgm-shadow);
  max-width: 240px;
  pointer-events: none;
}

.dgm-note::after {
  content: '';
  position: absolute;
  width: 10px;
  height: 10px;
  background: inherit;
  border-radius: 1px;
}

.dgm-note-above::after { left: 50%; bottom: -5px; transform: translateX(-50%) rotate(45deg); }
.dgm-note-below::after { left: 50%; top: -5px;    transform: translateX(-50%) rotate(45deg); }
.dgm-note-left::after  { top: 50%;  right: -5px;  transform: translateY(-50%) rotate(45deg); }
.dgm-note-right::after { top: 50%;  left: -5px;   transform: translateY(-50%) rotate(45deg); }

/* A note that had to be moved to avoid another one is no longer pointing at
   its target, so it stops pretending to. */
.dgm-note.is-adrift::after { display: none; }

/* --- reduced motion ------------------------------------------------------
 *
 * A reader who asks for reduced motion still wants the diagram — they want it
 * to hold still. Autoplay is already suppressed in the runtime; what is left is
 * the decoration that moves on its own, which goes away entirely, while the
 * transitions that communicate a change of state are only shortened. The
 * particle still travels: that is the content, not an effect.
 */
@media (prefers-reduced-motion: reduce) {
  .dgm-particle-halo,
  .dgm-pulse rect,
  .dgm-pulse circle,
  .dgm-pulse polygon,
  .dgm-pulse path,
  .dgm-waypoint {
    animation: none;
  }

  .dgm-btn,
  .dgm-select,
  .dgm-rail,
  .dgm-rail-note,
  .dgm-map-fit,
  .dgm-board.is-flinging,
  .dgm-board.is-springing,
  .dgm-step,
  .dgm-chip,
  .dgm-caption,
  .dgm-board-layer,
  .dgm-collapsed,
  .dgm-unfocused,
  .dgm-flow-active,
  .dgm-scrub-mark,
  .dgm-highlight,
  .dgm-busy,
  .dgm-dim,
  .dgm-hidden,
  .dgm-noted {
    transition-duration: 0.01ms;
  }
}
