/* ===== DATA DICTIONARY TOURS =====
   Page-specific layer over css/demo.css for salesforce-datadictionary.html.
   The engine (js/cycle.js), the player wiring (js/demo.js, js/demo-nav.js,
   js/modal.js) and the generic window/curtain/transport styling all come from
   the shared build files untouched; everything Salesforce-capture-specific
   lands here instead, so demo.css stays a straight copy of the Fulltrace
   Website build.

   The tours reuse the shared annotation classes where the look is the same
   job (.dash-frame, .dash-badge, .jc-tip, .shot-legend), and add dict-*
   classes only for what these captures need: a light canvas, per-step
   screenshot swaps, the live-search typing stack, spotlight cutouts and the
   simulated click sequences. All four stages sit on one canonical 1467x664
   canvas (top-left anchored crops of the same browser window), so the chain
   hand-off never resizes the frame. */

/* ------------------------------------------------------------ stage sizing
   The captures are 1467x664 (aspect 2.2093), much wider than the Fulltrace
   1010x775 window, so the shared 960px cap renders the Salesforce UI too
   small to read. Wider inline stage, and the same viewport-height guard as
   demo.css so the intro (85% visibility) can always fire. */

.window--dict .shot-stage { width: min(1040px, calc(100vw - var(--demo-gutter))); }

@media (min-width: 700px) {
  .window--dict .shot-stage {
    width: min(1040px, calc(100vw - var(--demo-gutter)), calc((100vh - 130px) * 2.2093));
  }
}

/* Fullscreen: demo.css reads --stage-base with !important, so only the token
   needs overriding, not the rule. 1467 is the capture's own width: a big
   enough screen shows it at exactly 1x, and the other two terms keep it from
   ever upscaling past that. Screens under ~1570px wide never reach the cap,
   so they render as they always did. */
.window--dict.is-modal {
  --stage-base: min(1467px, 100vw, calc((100vh - 104px) * 2.2093));
}

/* ------------------------------------------------------- fullscreen only
   Inline, the widest this stage ever gets is 1040 against captures drawn at
   1467: a 71% render of Salesforce UI that was small to begin with. Full
   screen is the only size these tours are legible at, so that is the only
   way they play now (Jason, 19/08/2026). The player is not removed, just
   withheld: js/modal.js still opens this same window, and .is-modal is what
   lets it through.

   Keyed off has-js because the launcher's link cannot open anything without
   js/modal.js. A reader without scripts keeps the inline player and its
   static first frame, and never sees a button that would do nothing. */
html.has-js .window--dict:not(.is-modal) { display: none; }

/* ------------------------------------------------------------- the frame
   The site's window card draws 1px around chrome and stage together, and
   demo.css drops it when the window goes full screen (border: 0), which
   leaves the demo bleeding into the canvas with nothing to say where it
   stops. Put it back. Chrome and stage are siblings, not nested, so the
   frame comes in two halves: the chrome takes the top and the sides, the
   stage the sides and the bottom, and the chrome's own border-bottom is
   already the join between them.

   Both widths grow by exactly the 2px the borders add, so the stage's
   CONTENT box still resolves to --stage-base and the captures still render
   at their own pixels on a screen big enough for it. */
.window--dict.is-modal .window__chrome--demo {
  width: calc(var(--stage-w) + 2px);
  border: 1px solid var(--card-border);
  border-bottom-color: var(--rule);
  border-radius: 8px 8px 0 0;
}

.window--dict.is-modal .shot-stage {
  width: calc(var(--stage-base) + 2px) !important;
  border: 1px solid var(--card-border);
  border-top: 0;
  border-radius: 0 0 8px 8px;
  overflow: hidden;            /* the capture's square corners, rounded off */
}

/* The curtain overscans by 1px to kill a sub-pixel seam on fractional-DPI
   displays (demo.css), which against a bordered stage means it paints over
   the border for the whole intro and the frame appears only when the black
   lifts. Inside the border instead: the border is itself what covers the
   seam it was overscanning for. */
.window--dict.is-modal .demo-curtain { inset: 0; }

/* ---------------------------------------------------------- the launcher
   Stands where the player used to sit, and is built to read as the same
   object: the window's own card, chrome bar and label, with an invitation
   where the capture would be. Only shown when scripts can honour it (see
   has-js above), so it and the inline player are never both on the page.

   Narrow screens get told rather than offered. Full screen on a phone is
   still a 1467px capture on a 390px screen, which is the unreadable render
   the fullscreen-only rule exists to prevent, so below 700px the button goes
   and a line explaining where to watch takes its place. 700px is demo.css's
   own line between the two hint texts, and puts tablets on the button. */

.demo-launch {
  display: none;
  max-width: 720px;
  margin-inline: auto;
  background: var(--bg-panel);
  border: 1px solid var(--card-border);
  border-radius: 8px;
  overflow: hidden;
}

html.has-js .demo-launch { display: block; }

.demo-launch-chrome {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--rule);
}

.demo-launch-count {
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.04em;
  color: var(--mono-muted);
}

.demo-launch-body { padding: 30px 24px 32px; text-align: center; }

.demo-launch-copy {
  max-width: 54ch;
  margin-inline: auto;
  font-size: 14px;
  line-height: 1.7;
  color: var(--tag-text);
}

.demo-launch-btn {
  display: inline-block;
  margin-top: 22px;
  font-size: 13px;
  padding: 10px 22px;
}

.demo-launch-note {
  margin-top: 12px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--mono-muted);
}

.demo-launch-narrow {
  display: none;
  max-width: 44ch;
  margin: 20px auto 0;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.6;
  color: var(--mono-muted);
}

@media (max-width: 700px) {
  .demo-launch-btn,
  .demo-launch-note,
  .demo-launch-wide { display: none; }
  .demo-launch-narrow { display: block; }
}

/* Welded literal, deliberately NOT a token: sampled from the captures' own
   Lightning page chrome, visible only while a screenshot is still fetching. */
.dict-svg { background: #f3f2f2; }

/* The caption cards ride a 1467-unit canvas against Fulltrace's 1010, so the
   shared 15px lands smaller on the glass; one size up restores parity. */
/* And the fade with it: demo.css's 0.4s is judged against a 1010-unit stage,
   where the card is a small thing appearing. At 1467 it covers enough of the
   screen that the same 0.4s reads as a pop rather than an arrival. */
.window--dict .jc-tip { transition-duration: 0.6s; }

.window--dict .jc-tip-card { font-size: 16.5px; }
.window--dict .jc-tip-head { font-size: 14.5px; }

/* Welded dark, at both themes. demo.css inverts the card against the PAGE
   theme, which is the right read for Fulltrace: its captures are dark VS Code,
   so a light grey card floats above them and a light page gets the dark card
   instead. These captures are white Salesforce UI whatever the page is doing,
   so that light card lands white-on-white and the annotation layer stops
   reading as a layer. Invert against the CAPTURE instead: the dark card
   demo.css already ships for light mode, used at both. Sampled literals like
   the ones they override, so leave them out of any token sweep. */
.window--dict .shot-svg .jc-tip-card {
  color: #f0f1f3;
  background: #454850;
  border-color: rgba(255, 255, 255, 0.12);
}

.window--dict .shot-svg .jc-tip-head { color: #C3BBFB; }

/* --------------------------------------------------------- screenshot swaps
   Same contract as the Fulltrace tabs tour: the base capture is always
   painted, each later step's capture stacks above it and fades in when its
   step lights. The outgoing image holds for 1s while the incoming fades over
   it, so the eye never sees the base through the dissolve.

   The 0.3s below is a beat of stillness at the hand-off, asked for across the
   tours (Jason, 20/08/2026): the card and the frame of the new step land, the
   screen it is about waits, and only then does it change. It belongs on every
   swap that crosses a dot. The sequences that play INSIDE one dot keep their
   own cadence: the typing stack and the zoom stack below take the same 0.3s
   on their first frame only, because that frame is the crossing, and the
   later frames just carry it so their spacing stays as it was. The click-led
   swaps (dependency 3-16, ERD 3-4) are untouched, since their wait is the
   click reading and lengthening it would loosen the cause from the effect. */

.dict-step-img { opacity: 0; transition: opacity 1s ease 1s; }

.dict-stage:has(.jc-step[data-step="2"].is-active) .dict-step-img[data-step-img="2"],
.dict-stage:has(.jc-step[data-step="3"].is-active) .dict-step-img[data-step-img="3"],
.dict-stage:has(.jc-step[data-step="4"].is-active) .dict-step-img[data-step-img="4"],
.dict-stage:has(.jc-step[data-step="5"].is-active) .dict-step-img[data-step-img="5"],
.dict-stage:has(.jc-step[data-step="6"].is-active) .dict-step-img[data-step-img="6"],
.dict-stage:has(.jc-step[data-step="7"].is-active) .dict-step-img[data-step-img="7"],
.dict-stage:has(.jc-step[data-step="8"].is-active) .dict-step-img[data-step-img="8"],
.dict-stage:has(.jc-step[data-step="9"].is-active) .dict-step-img[data-step-img="9"],
.dict-stage:has(.jc-step[data-step="10"].is-active) .dict-step-img[data-step-img="10"],
.dict-stage:has(.jc-step[data-step="11"].is-active) .dict-step-img[data-step-img="11"],
.dict-stage:has(.jc-step[data-step="12"].is-active) .dict-step-img[data-step-img="12"],
.dict-stage:has(.jc-step[data-step="13"].is-active) .dict-step-img[data-step-img="13"],
.dict-stage:has(.jc-step[data-step="14"].is-active) .dict-step-img[data-step-img="14"],
.dict-stage:has(.jc-step[data-step="15"].is-active) .dict-step-img[data-step-img="15"],
.dict-stage:has(.jc-step[data-step="16"].is-active) .dict-step-img[data-step-img="16"] {
  opacity: 1;
  transition-delay: 0.3s;
}

/* Theme-matched floor between the base and the swaps, for the tab-style tours
   (setup, explorer) whose whole screen changes per step: a dissolve passes
   through this panel grey rather than through the base capture. The graph
   tours carry no floor element, so their near-identical frames dissolve into
   each other directly. */
.dict-swap-floor { fill: #f3f2f2; opacity: 0; transition: opacity 0.25s ease; }

.dict-stage:has(.jc-step[data-step="2"].is-active) .dict-swap-floor,
.dict-stage:has(.jc-step[data-step="3"].is-active) .dict-swap-floor,
.dict-stage:has(.jc-step[data-step="4"].is-active) .dict-swap-floor,
.dict-stage:has(.jc-step[data-step="5"].is-active) .dict-swap-floor,
.dict-stage:has(.jc-step[data-step="6"].is-active) .dict-swap-floor {
  opacity: 1;
  transition-delay: 0.3s;      /* with the swap above: grey must not arrive before the picture */
}

/* ------------------------------------------------------- live-search typing
   The explorer's step 5 plays the search narrowing as a typed sequence: four
   captures (nu, num, numbero, numberofe) stack in order, each arriving on its
   own beat and staying (fill both), so the table visibly shrinks 14, 12, 2, 1
   under a single step. Driven by the step class, so stepping away resets the
   stack and the exit fades out on the shared swap timing. */

.dict-type-img { opacity: 0; transition: opacity 1s ease 1s; }

.dict-stage:has(.jc-step[data-step="5"].is-active) .dict-type-img {
  animation: dict-type 0.45s ease 0.3s both;
}

/* 0.3s carried through, so the keystrokes stay 1.5s apart and only the arrival
   of the first one moves. */
.dict-stage:has(.jc-step[data-step="5"].is-active) .dict-type-img[data-type="2"] { animation-delay: 1.8s; }
.dict-stage:has(.jc-step[data-step="5"].is-active) .dict-type-img[data-type="3"] { animation-delay: 3.3s; }
.dict-stage:has(.jc-step[data-step="5"].is-active) .dict-type-img[data-type="4"] { animation-delay: 4.8s; }

@keyframes dict-type {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* ------------------------------------------------------------- spotlights
   A full-canvas scrim with the step's region cut out (fill-rule evenodd in
   the markup, which takes as many cutouts as a step needs: several of these
   light a control and the thing it drives at once, because scrimming the
   payoff to point at the button that summoned it reads backwards). Lighter
   than the Fulltrace 0.86: these captures are white UI, and the surroundings
   should stay readable as context.

   Every step of the explorer, ERD and documentation tours carries one. The
   dependency tour scrims sparingly: its click steps play a cursor across a
   screen that is still arriving, so only the steps whose payoff IS a redrawn
   region (3, 4, 6 and 7) scrim, and each waits for the response before it
   fades in (see the hold-back delays below). */

/* In light mode the shared token resolves --demo-dim to the PAGE background,
   which for Fulltrace's dark VS Code captures is the right way round: a pale
   scrim over a dark screenshot dims it. These captures are white Salesforce
   UI at both themes, so the same token painted near-white over near-white
   dimmed nothing and the spotlight did not read at all (Jason, 20/08/2026).

   A mid grey instead, and only for this window, so the Fulltrace player keeps
   the token it was tuned against. Measured off the rendered frame rather than
   guessed: under the 0.55 below, #54565C takes the capture's white chrome from
   230 down to about 151 and its white panels to about 161, against the 251 of
   whatever the step has cut out. Dark mode's own scrim lands the same chrome
   at 112, so this is a shade lighter than that on purpose: it is the same
   spotlight, read on a light page, and the surroundings still have to stay
   legible as context, which is the whole bargain of the lighter 0.55. */
:root[data-theme="light"] .window--dict { --demo-dim: #54565C; }

.dict-spot {
  fill: var(--demo-dim);
  opacity: 0;
  transition: opacity 0.65s ease;
  pointer-events: none;
}

.jc-step.is-active .dict-spot { opacity: 0.55; }

/* A second, lighter scrim laid inside the first one's cutout, so a step can
   read at three levels rather than two: the framed control at full white, the
   region it acted on under a touch of shade (still legible, plainly the
   result), and everything else under the full 0.55. Its outer edge is the
   cutout it sits in, so the two scrims never stack. */
.jc-step.is-active .dict-spot--soft { opacity: 0.2; }

/* ------------------------------------------------------------ staged shadow
   A blurred stroke drawn on the cutout's own outline and clipped to the scrim,
   so only the half that falls on the dimmed side survives. The lit region then
   reads as lifted off the page rather than punched through it, which the flat
   cutout on its own never quite said. Stroke and blur are in user units, so
   both scale with the stage instead of thinning out at fullscreen. */
.dict-stage-shadow {
  fill: none;
  stroke: #0E0E11;
  stroke-width: 30;
  filter: blur(14px);
  opacity: 0;
  transition: opacity 0.65s ease;
  pointer-events: none;
}

.jc-step.is-active .dict-stage-shadow { opacity: 0.5; }

/* ---------------------------------------------- simulated clicks (dep 2-16)
   The dependency tour plays its interactions rather than describing them:
   each click step acts on the PREVIOUS step's screen. A cursor and ripple
   (.dict-click, one per step, inside its own jc-step group) read first, and
   only then does the response fade in: the inspector filling (3), the map
   refocusing after a double-click (4), the node menu (5), Grouped View (6),
   RecordType then ApexClass clicked open (7, 8), the traversal into a
   class's own perspective and its objects (9, 10), the double-click home to
   the grouped census (11), the menu reopened on the root (12), Metadata
   Overview landing collapsed (13), two tree types unfolding (14, 15), and
   the double-click on a listed class handing back to the graph (16).
   Everything is step-scoped, so stepping away resets a sequence and exits
   ride the shared swap timing. */

/* Step 2's zoom: four captures arriving on their own beats, 6% to 80% (the
   explorer's typing stack, reused for motion). Each frame lands and stays,
   and the last persists through step 3 as the screen its click lands on. */
.dict-zoom-img { opacity: 0; transition: opacity 1s ease 1s; }

[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="2"].is-active) .dict-zoom-img {
  animation: dict-type 0.45s ease 0.3s both;
}

/* Carried through as well, so the four zoom beats stay 1.1s apart. */
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="2"].is-active) .dict-zoom-img[data-zoom="2"] { animation-delay: 1.4s; }
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="2"].is-active) .dict-zoom-img[data-zoom="3"] { animation-delay: 2.5s; }
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="2"].is-active) .dict-zoom-img[data-zoom="4"] { animation-delay: 3.6s; }

[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="3"].is-active) .dict-zoom-img[data-zoom="4"] {
  opacity: 1;
  transition-delay: 0s;
}

/* Each overlay holds through the step after it, as the screen the next
   step's click lands on. Step 11's homecoming is the census again: the same
   file as step 6 in its own data-step-img="11" element, ordered above the
   step-10 fan it has to be seen dissolving over. */
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="4"].is-active) .dict-step-img[data-step-img="3"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="5"].is-active) .dict-step-img[data-step-img="4"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="6"].is-active) .dict-step-img[data-step-img="5"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="7"].is-active) .dict-step-img[data-step-img="6"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="8"].is-active) .dict-step-img[data-step-img="7"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="9"].is-active) .dict-step-img[data-step-img="8"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="10"].is-active) .dict-step-img[data-step-img="9"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="11"].is-active) .dict-step-img[data-step-img="10"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="12"].is-active) .dict-step-img[data-step-img="11"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="13"].is-active) .dict-step-img[data-step-img="12"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="14"].is-active) .dict-step-img[data-step-img="13"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="15"].is-active) .dict-step-img[data-step-img="14"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="16"].is-active) .dict-step-img[data-step-img="15"] {
  opacity: 1;
  transition-delay: 0s;
}

/* The responses arrive after their click has read (the animation's fill
   holds them at 0 through the delay, overriding the generic step rule).
   Doubles land at 2s, singles at 1.5-1.6s. */
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="5"].is-active) .dict-step-img[data-step-img="5"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="12"].is-active) .dict-step-img[data-step-img="12"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="14"].is-active) .dict-step-img[data-step-img="14"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="15"].is-active) .dict-step-img[data-step-img="15"] {
  animation: dict-type 0.5s ease 1.5s both;
}

[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="3"].is-active) .dict-step-img[data-step-img="3"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="6"].is-active) .dict-step-img[data-step-img="6"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="7"].is-active) .dict-step-img[data-step-img="7"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="8"].is-active) .dict-step-img[data-step-img="8"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="10"].is-active) .dict-step-img[data-step-img="10"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="13"].is-active) .dict-step-img[data-step-img="13"] {
  animation: dict-type 0.5s ease 1.6s both;
}

[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="4"].is-active) .dict-step-img[data-step-img="4"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="9"].is-active) .dict-step-img[data-step-img="9"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="11"].is-active) .dict-step-img[data-step-img="11"],
[data-demo-panel="dependencies"] .dict-stage:has(.jc-step[data-step="16"].is-active) .dict-step-img[data-step-img="16"] {
  animation: dict-type 0.5s ease 2s both;
}

/* Frames, badges and scrims on the click steps hold back until the response
   has landed, so nothing rings or shades a screen that is not there yet.
   Enter only. */
[data-demo-panel="dependencies"] .jc-step[data-step="3"].is-active .dash-frame,
[data-demo-panel="dependencies"] .jc-step[data-step="3"].is-active .dash-badge,
[data-demo-panel="dependencies"] .jc-step[data-step="3"].is-active .dash-badge-num,
[data-demo-panel="dependencies"] .jc-step[data-step="3"].is-active .dict-spot,
[data-demo-panel="dependencies"] .jc-step[data-step="5"].is-active .dash-frame,
[data-demo-panel="dependencies"] .jc-step[data-step="5"].is-active .dash-badge,
[data-demo-panel="dependencies"] .jc-step[data-step="5"].is-active .dash-badge-num,
[data-demo-panel="dependencies"] .jc-step[data-step="5"].is-active .dict-spot,
[data-demo-panel="dependencies"] .jc-step[data-step="6"].is-active .dash-frame,
[data-demo-panel="dependencies"] .jc-step[data-step="6"].is-active .dash-badge,
[data-demo-panel="dependencies"] .jc-step[data-step="6"].is-active .dash-badge-num,
[data-demo-panel="dependencies"] .jc-step[data-step="6"].is-active .dict-spot,
[data-demo-panel="dependencies"] .jc-step[data-step="7"].is-active .dash-frame,
[data-demo-panel="dependencies"] .jc-step[data-step="7"].is-active .dash-badge,
[data-demo-panel="dependencies"] .jc-step[data-step="7"].is-active .dash-badge-num,
[data-demo-panel="dependencies"] .jc-step[data-step="7"].is-active .dict-spot,
[data-demo-panel="dependencies"] .jc-step[data-step="8"].is-active .dash-frame,
[data-demo-panel="dependencies"] .jc-step[data-step="8"].is-active .dash-badge,
[data-demo-panel="dependencies"] .jc-step[data-step="8"].is-active .dash-badge-num,
[data-demo-panel="dependencies"] .jc-step[data-step="8"].is-active .dict-spot,
[data-demo-panel="dependencies"] .jc-step[data-step="10"].is-active .dash-frame,
[data-demo-panel="dependencies"] .jc-step[data-step="10"].is-active .dash-badge,
[data-demo-panel="dependencies"] .jc-step[data-step="10"].is-active .dash-badge-num,
[data-demo-panel="dependencies"] .jc-step[data-step="10"].is-active .dict-spot,
[data-demo-panel="dependencies"] .jc-step[data-step="12"].is-active .dash-frame,
[data-demo-panel="dependencies"] .jc-step[data-step="12"].is-active .dash-badge,
[data-demo-panel="dependencies"] .jc-step[data-step="12"].is-active .dash-badge-num,
[data-demo-panel="dependencies"] .jc-step[data-step="12"].is-active .dict-spot,
[data-demo-panel="dependencies"] .jc-step[data-step="13"].is-active .dash-frame,
[data-demo-panel="dependencies"] .jc-step[data-step="13"].is-active .dash-badge,
[data-demo-panel="dependencies"] .jc-step[data-step="13"].is-active .dash-badge-num,
[data-demo-panel="dependencies"] .jc-step[data-step="13"].is-active .dict-spot,
[data-demo-panel="dependencies"] .jc-step[data-step="14"].is-active .dash-frame,
[data-demo-panel="dependencies"] .jc-step[data-step="14"].is-active .dash-badge,
[data-demo-panel="dependencies"] .jc-step[data-step="14"].is-active .dash-badge-num,
[data-demo-panel="dependencies"] .jc-step[data-step="14"].is-active .dict-spot,
[data-demo-panel="dependencies"] .jc-step[data-step="15"].is-active .dash-frame,
[data-demo-panel="dependencies"] .jc-step[data-step="15"].is-active .dash-badge,
[data-demo-panel="dependencies"] .jc-step[data-step="15"].is-active .dash-badge-num,
[data-demo-panel="dependencies"] .jc-step[data-step="15"].is-active .dict-spot { transition-delay: 2.1s; }

[data-demo-panel="dependencies"] .jc-step[data-step="4"].is-active .dash-frame,
[data-demo-panel="dependencies"] .jc-step[data-step="4"].is-active .dash-badge,
[data-demo-panel="dependencies"] .jc-step[data-step="4"].is-active .dash-badge-num,
[data-demo-panel="dependencies"] .jc-step[data-step="4"].is-active .dict-spot { transition-delay: 2.5s; }

[data-demo-panel="dependencies"] .jc-step[data-step="9"].is-active .dash-frame,
[data-demo-panel="dependencies"] .jc-step[data-step="9"].is-active .dash-badge,
[data-demo-panel="dependencies"] .jc-step[data-step="9"].is-active .dash-badge-num,
[data-demo-panel="dependencies"] .jc-step[data-step="9"].is-active .dict-spot,
[data-demo-panel="dependencies"] .jc-step[data-step="11"].is-active .dash-frame,
[data-demo-panel="dependencies"] .jc-step[data-step="11"].is-active .dash-badge,
[data-demo-panel="dependencies"] .jc-step[data-step="11"].is-active .dash-badge-num,
[data-demo-panel="dependencies"] .jc-step[data-step="11"].is-active .dict-spot,
[data-demo-panel="dependencies"] .jc-step[data-step="16"].is-active .dash-frame,
[data-demo-panel="dependencies"] .jc-step[data-step="16"].is-active .dash-badge,
[data-demo-panel="dependencies"] .jc-step[data-step="16"].is-active .dash-badge-num,
[data-demo-panel="dependencies"] .jc-step[data-step="16"].is-active .dict-spot { transition-delay: 2.4s; }

/* Every card waits for the response its step describes, so the words, the
   ring and the shading all land together: painting with the step put them on
   screen while the cursor was still travelling. Each delay matches its own
   step's frame and scrim in the block above, so these three groups track those
   three. Steps 1 and 2 have no click to wait for and paint with the step.
   Enter only: the exit rides the tip's base transition. */
[data-demo-panel="dependencies"] .jc-tip[data-step="3"].is-active,
[data-demo-panel="dependencies"] .jc-tip[data-step="5"].is-active,
[data-demo-panel="dependencies"] .jc-tip[data-step="6"].is-active,
[data-demo-panel="dependencies"] .jc-tip[data-step="7"].is-active,
[data-demo-panel="dependencies"] .jc-tip[data-step="8"].is-active,
[data-demo-panel="dependencies"] .jc-tip[data-step="10"].is-active,
[data-demo-panel="dependencies"] .jc-tip[data-step="12"].is-active,
[data-demo-panel="dependencies"] .jc-tip[data-step="13"].is-active,
[data-demo-panel="dependencies"] .jc-tip[data-step="14"].is-active,
[data-demo-panel="dependencies"] .jc-tip[data-step="15"].is-active { transition-delay: 2.1s; }

[data-demo-panel="dependencies"] .jc-tip[data-step="9"].is-active,
[data-demo-panel="dependencies"] .jc-tip[data-step="11"].is-active,
[data-demo-panel="dependencies"] .jc-tip[data-step="16"].is-active { transition-delay: 2.4s; }

[data-demo-panel="dependencies"] .jc-tip[data-step="4"].is-active { transition-delay: 2.5s; }

/* The ERD's step 3 is a click too, and the glyph rules below are keyed on the
   step rather than the panel, so it inherits their timing for free. What it
   needs of its own is the response and the annotations behind it: the filled
   inspector at 1.6s, then scrim, panel frame and card at 2.1s. */
/* Delayed transitions, NOT animations, and the difference is the whole bug that
   was here: an animation holding an image at 1 is cancelled the moment its step
   loses .is-active, and the value drops to the declared 0 with no transition to
   run, because the declared value never changed. Step 3's capture was vanishing
   in a single frame at the hand-off, uncovering the 63% base for the full
   second step 4's menu took to fade in: two diagrams at two zooms, one of them
   the green unselected Account node. A delayed transition holds the old value
   through the delay just as the animation did, and still fades out properly
   when the step changes. */
[data-demo-panel="erd"] .dict-stage:has(.jc-step[data-step="3"].is-active) .dict-step-img[data-step-img="3"] {
  transition: opacity 0.5s ease 1.6s;
}

/* And step 3's capture holds through step 4, which is the same screen plus a
   menu. Nothing underneath is ever uncovered, so the swap is only the menu
   arriving. Same contract as the dependency tour's held overlays. */
[data-demo-panel="erd"] .dict-stage:has(.jc-step[data-step="4"].is-active) .dict-step-img[data-step-img="3"] {
  opacity: 1;
  transition-delay: 0s;
}

/* Step 4's own capture waits for the right-click to have read. */
[data-demo-panel="erd"] .dict-stage:has(.jc-step[data-step="4"].is-active) .dict-step-img[data-step-img="4"] {
  transition: opacity 0.5s ease 1.6s;
}

/* The right-click itself. The unscoped step-4 rules further down belong to the
   dependency tour, where 4 is a double-click; this is one press, so both the
   glyph and its single pulse are re-declared panel-scoped. */
[data-demo-panel="erd"] .jc-step[data-step="4"].is-active .dict-click {
  animation: dict-click-show 2.4s ease 0.3s both;
}

[data-demo-panel="erd"] .jc-step[data-step="4"].is-active .dict-click-ripple {
  animation: dict-ripple 0.9s ease-out 0.8s;
}

[data-demo-panel="erd"] .jc-step[data-step="3"].is-active .dash-frame,
[data-demo-panel="erd"] .jc-step[data-step="3"].is-active .dash-badge,
[data-demo-panel="erd"] .jc-step[data-step="3"].is-active .dash-badge-num,
[data-demo-panel="erd"] .jc-step[data-step="3"].is-active .dict-spot,
[data-demo-panel="erd"] .jc-tip[data-step="3"].is-active { transition-delay: 2.1s; }

/* Step 4 now plays its right-click rather than presenting the result: cursor at
   0.3s, one pulse at 0.8s, the menu at 1.6s, and only then the frame, the scrim
   and the card. Its annotations used to land at 0 against a screen still
   dissolving, which is why the card read as popping in over a moving picture. */
[data-demo-panel="erd"] .jc-step[data-step="4"].is-active .dash-frame,
[data-demo-panel="erd"] .jc-step[data-step="4"].is-active .dash-badge,
[data-demo-panel="erd"] .jc-step[data-step="4"].is-active .dash-badge-num,
[data-demo-panel="erd"] .jc-step[data-step="4"].is-active .dict-spot,
[data-demo-panel="erd"] .jc-tip[data-step="4"].is-active { transition-delay: 2.1s; }

/* The glyphs. Scoped through the owning step, so the clicks coexist. Click
   semantics per Jason: double-click enters a node's perspective (4, 9, 11)
   or jumps out of the overview's list (16); the node menus (5, 12) are
   right-clicks, which the words carry since the glyph has no separate form
   for it; opening a group, the menu items and the tree rows are single
   clicks. */
.dict-click { opacity: 0; pointer-events: none; }

.jc-step[data-step="5"].is-active .dict-click,
.jc-step[data-step="12"].is-active .dict-click,
.jc-step[data-step="14"].is-active .dict-click,
.jc-step[data-step="15"].is-active .dict-click { animation: dict-click-show 2.2s ease 0.3s both; }
.jc-step[data-step="3"].is-active .dict-click,
.jc-step[data-step="6"].is-active .dict-click,
.jc-step[data-step="7"].is-active .dict-click,
.jc-step[data-step="8"].is-active .dict-click,
.jc-step[data-step="10"].is-active .dict-click,
.jc-step[data-step="13"].is-active .dict-click { animation: dict-click-show 2.4s ease 0.3s both; }
.jc-step[data-step="4"].is-active .dict-click,
.jc-step[data-step="9"].is-active .dict-click,
.jc-step[data-step="11"].is-active .dict-click,
.jc-step[data-step="16"].is-active .dict-click { animation: dict-click-show 2.6s ease 0.3s both; }

/* Literals, not tokens: the cursor sits on the captures' light UI in both
   site themes, so the heading token (near-white in dark mode) would vanish. */
.dict-click-cursor { fill: #1c1e24; stroke: #ffffff; stroke-width: 1px; }

.dict-click-ripple {
  fill: none;
  stroke: var(--demo-accent-line);
  stroke-width: 2.5px;
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
}

/* One measured pulse for a click, two fast ones for a double-click. Entering
   a node's perspective is a double-click (4, 9, 11, 16); opening a group,
   the menus, their items and the tree rows are singles. */
.jc-step[data-step="3"].is-active .dict-click-ripple { animation: dict-ripple 0.9s ease-out 0.8s; }
.jc-step[data-step="4"].is-active .dict-click-ripple,
.jc-step[data-step="9"].is-active .dict-click-ripple,
.jc-step[data-step="11"].is-active .dict-click-ripple,
.jc-step[data-step="16"].is-active .dict-click-ripple { animation: dict-ripple 0.42s ease-out 0.8s 2; }
.jc-step[data-step="5"].is-active .dict-click-ripple,
.jc-step[data-step="6"].is-active .dict-click-ripple,
.jc-step[data-step="7"].is-active .dict-click-ripple,
.jc-step[data-step="8"].is-active .dict-click-ripple,
.jc-step[data-step="10"].is-active .dict-click-ripple,
.jc-step[data-step="12"].is-active .dict-click-ripple,
.jc-step[data-step="13"].is-active .dict-click-ripple,
.jc-step[data-step="14"].is-active .dict-click-ripple,
.jc-step[data-step="15"].is-active .dict-click-ripple { animation: dict-ripple 0.9s ease-out 0.7s; }

/* The explorer's one click, on its own clock. Every rule above opens a step with
   the click that caused it; this one closes a step with the click the NEXT step
   answers, so it has to wait out the card instead of leading it. Step 3's beat is
   8745ms (159 characters at CHAR_MS, inside the stage's dwell floor and ceiling,
   js/cycle.js): the cursor arrives at 5.9s, the ripple pulses at 6.5s, and both
   are gone about 250ms before the swap to the Account-only table starts. Panel
   scoped because the dependency tour owns a step 3 of its own, and re-timed by
   hand if step 3's card is ever re-worded, since the card is what sets the beat. */
[data-demo-panel="explorer"] .jc-step[data-step="3"].is-active .dict-click {
  animation: dict-click-show 2.6s ease 5.9s both;
}

[data-demo-panel="explorer"] .jc-step[data-step="3"].is-active .dict-click-ripple {
  animation: dict-ripple 0.9s ease-out 6.5s;
}

@keyframes dict-click-show {
  0% { opacity: 0; }
  14% { opacity: 1; }
  82% { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes dict-ripple {
  0% { transform: scale(0.3); opacity: 0.9; }
  100% { transform: scale(1.5); opacity: 0; }
}

/* Still mode plays the responses but not the cursors: at a phone's scale the
   glyph is a smudge, and the screens changing carry the point on their own. */
@media (max-width: 700px) {
  .dict-click { animation: none !important; opacity: 0; }
}

/* ----------------------------------------------------------- numbered badges
   The circled numeral exists to tie a step to its region when the words are
   somewhere else. Below 700px they are: demo.css drops the cards and the
   legend under the canvas carries the text, so the numeral is the only thing
   joining a legend line to a part of the screen, and it stays.

   Above 700px nothing needs it. Every card opens with its own number, the
   dependency tour's cards sit against the region they describe, and the other
   three now carry a spotlight on every step: the lit region reads as the
   region without a second numeral hanging off its edge. Dropped everywhere,
   and the frame plus the cutout ring it instead. */
@media (min-width: 701px) {
  .dict-stage .dash-badge-g { display: none; }
}

/* ---------------------------------------------------- later-tour curtains
   Same bargain as the Fulltrace panels in demo.css: the first tour's intro
   teaches the controls, every later tour drops the hint line and the arrow.
   Keyed by panel name because that is how demo.css does it; if the parked
   setup tour returns as the opener, move "explorer" into this list. */

[data-demo-panel="erd"] .demo-curtain-hint,
[data-demo-panel="erd"] .demo-curtain-point,
[data-demo-panel="dependencies"] .demo-curtain-hint,
[data-demo-panel="dependencies"] .demo-curtain-point,
[data-demo-panel="documentation"] .demo-curtain-hint,
[data-demo-panel="documentation"] .demo-curtain-point { display: none; }

/* --------------------------------------------------------- curtain purpose
   One line under the tour's name saying what the LWC is for, slotted in by
   js/demo.js from the stage's data-cycle-sub. Every curtain keeps it, hint
   or no hint: on the later tours it is all the card says beyond the name.
   Dressed like the hint (same type, same muted colour) so the card still
   reads as name then detail, and revealed in the gap between the name (at
   once) and the hint's first word (6s, in the explorer block below), so the
   beats stay separate. The wait before it is deliberate and long, and the
   fade deliberately slow: the name has to be read and finished with before a
   second line arrives, or the two land as one block of text and neither gets
   read. Tuned by eye over three passes, ending here (Jason, 19/08/2026).
   Every stage carrying a data-cycle-sub needs an intro long enough to seat
   it: this line is not finished arriving until 3.6s.

   Slow and late on the way IN only. A transition is read off the state being
   moved TO, so both of those timings belong in the is-title rule: the base
   rule is what plays on the way OUT, when js/cycle.js drops is-title, and it
   carries the name's own 0.45s (TITLE_FADE in js/cycle.js, which is also the
   wait before the black lifts). Leaving the 1.2s on the base rule let this
   line hang over the lifting screen for three quarters of a second after the
   name and the hint had gone. */
.window--dict .demo-curtain-sub {
  margin-top: -6px;           /* the curtain's 14px gap alone reads detached; hug the name */
  font-family: var(--font-mono);
  font-size: 12.5px;
  line-height: 1.5;
  letter-spacing: 0.02em;
  color: var(--mono-muted);
  opacity: 0;
  transition: opacity 0.45s ease;
}

.window--dict .demo-curtain.is-title .demo-curtain-sub {
  opacity: 1;
  transition: opacity 1.2s ease 2.4s;
}

/* ------------------------------------------------------ intro arrow sizing
   demo.css draws the arrow at a fixed 290x176, measured against Fulltrace's
   1.654 stage: ~625px tall inline, and the head lands ~28px clear of the
   45px transport bar. These captures are 2.2093, so the same width is only
   ~516px tall and that same arrow runs 27px INTO the bar, which sits above
   the curtain on z-index 3 and clips the head off.

   Size it off the stage instead of in fixed pixels. The curtain is pinned to
   the stage by inset, so its height never depends on its children and size
   containment is safe on it; cqh then tracks the stage's own height at every
   width, inline and fullscreen alike. Both lengths here are against the
   curtain's CONTENT box, which its own 24px padding puts 48px under the
   stage: 25.6cqh is what leaves the head the same ~28px of air the arrow has
   on Fulltrace, and the width holds the art's 290:176. */

.window--dict .demo-curtain { container-type: size; }

.window--dict .demo-curtain-arrow {
  width: 42.2cqh;
  height: 25.6cqh;
}

/* Where the head LANDS, not just how big the arrow is. The tail hangs off the
   end of the hint sentence, which is fixed-size type, but the arrow's reach is
   the cqh sizing above, so on a bigger stage (the fullscreen modal especially)
   the head slides further left of the transport it points at. Cancel that by
   giving right the same cqh term the reach grows by: the head then lands at
   the same spot relative to the stage's centred transport at every size.

   38.5cqh is the head's horizontal reach back from the box's right edge, with
   the scaleX(-1) rotate(9deg) folded in: (0.95298 x 42.2) - (0.06755 x 25.6),
   the coefficients being cos/sin of 9deg against the head's position in the
   145x88 art. 169px is the anchor's distance right of the stage's centre
   (half the rendered hint line, tied to its 12.5px type) minus a small lean
   toward the buttons, landing the head just right of the dots. Recompute the
   px if the hint's copy or type size changes; the cqh if the art or its
   transform does. */
.window--dict .demo-curtain-arrow {
  right: calc(169px - 38.5cqh);
}

/* Under ~420px of stage the title, the hint and the arrow crowd the bar: the
   card's text block is a fixed size, so it eats a bigger share of a shorter
   stage and the head closes to single figures. The hint line names the
   controls on its own, so the arrow is what goes, the same bargain demo.css
   strikes below 700px. */
@container (max-height: 370px) {
  .window--dict .demo-curtain-point { display: none; }
}

/* ------------------------------------------- explorer: the navigation beat
   Trialled on the explorer tour first, to be widened once it reads right
   (Jason, 19/08/2026). It is the only DD tour that still carries the hint
   line and the arrow, so scoping to its panel changes nothing else; the same
   treatment for the Fulltrace dashboard's tour 01 is a selector away.

   Two changes to how the card lands. The sentence about the controls now
   sits DOWN by the controls rather than centred under the name, out of the
   flex flow so the name and the purpose line stay centred as a pair. And the
   arrow it hangs off is short, because the journey is now short: from the
   end of the sentence to the transport, not across the whole card.

   88px puts the line 43px clear of the 45px bar, about one bar-height: close
   enough that it reads as a label on the transport rather than as a third
   line of the card. The proximity is now what points, which is why the arrow
   is gone here, and that is a measurement rather than a preference.

   The art is a 145x88 viewBox with a stroke-width of 1 and a ~14-unit head,
   so both scale with the box: at 290px wide (the Fulltrace size) the stroke
   renders 2px and the head 28px; at 122px, 0.84px and 11.8px, about the
   floor for a curve that still reads as an arrow; at the 64px this position
   would allow, 0.44px and 6.2px, which is a smudge. And the size is not free
   to choose, because preserveAspectRatio is the engine's markup: the fall
   and the reach are locked at 1.115 to 1.377 of each other, so a legible
   122px arrow must drop 83px, which pins the sentence back at ~150px off the
   floor. Arrow or proximity, not both. To have it back, put bottom at 150px
   and restore width 122px / height 74px / right -9px / top 12px, which lands
   the head ~30px above the bar with the tail at the sentence's end.

   Behind a container query because a bottom-anchored line only works on a
   stage with room for it: the name and the purpose line are centred, so the
   shorter the stage, the closer they sit to a line pinned 88px off the
   floor. Under 400 of curtain (~446 of stage) the treatment lifts and the
   rules above take back over: the line recentres in the flex flow and the
   arrow returns, at the stage-relative cqh sizing that pairing was tuned
   for. Nothing below needs a second set of numbers. */
@container (min-height: 400px) {
  [data-demo-panel="explorer"] .demo-curtain-hint {
    position: absolute;
    left: 24px;
    right: 24px;
    bottom: 88px;
    margin-inline: auto;      /* max-width is set; without this it hugs `left` */
  }

  [data-demo-panel="explorer"] .demo-curtain-point { display: none; }
}

/* The card lands in three beats, and each waits for the one before it to be
   READ rather than merely to have arrived: the name (at once, done by
   0.45s), what the component is for (2.4s, done by 3.6s), and how to drive
   it (6s, the sentence itself taking 2.1s to arrive word by word, so it is
   complete at 8.1s). That is a slow card by design and it costs the whole
   intro: the stage's own data-cycle-intro carries 12500 to seat it, which
   leaves the finished card standing for about four and a half seconds.

   The arrow beats below are dead at any stage tall enough for the low
   sentence (nothing to draw, see the block above) and live only on the short
   ones, where the flow layout and the cqh arrow come back. Trim from the
   tail here first if the intro ever needs to come back down. */
[data-demo-panel="explorer"] .demo-curtain.is-title .demo-curtain-hint-word {
  transition-delay: calc(6s + var(--d, 0ms));
}

[data-demo-panel="explorer"] .demo-curtain.is-title .demo-curtain-point {
  transition-delay: 8.3s;
}

[data-demo-panel="explorer"] .demo-curtain.is-title .demo-curtain-arrow-line {
  animation: demo-arrow-draw 1.8s ease-out 8.4s backwards;
}

[data-demo-panel="explorer"] .demo-curtain.is-title .demo-curtain-arrow-head {
  animation: demo-arrow-tip 0.3s ease 10.1s backwards;
}

[data-demo-panel="explorer"] .demo-curtain.is-title .demo-curtain-arrow {
  animation: demo-point-nudge 1.15s ease-in-out 10.4s infinite;
}

/* -------------------------------------------------------- held/paused cue
   Mirrors the dash-stage rules in demo.css, which are scoped to Fulltrace's
   stages: the held frame stops above the 45px transport bar, and the play
   glyph invites the click while a hover holds the walk. */

.dict-stage::after {
  content: "";
  position: absolute;
  inset: 0 0 45px;
  border: 1px solid color-mix(in srgb, var(--demo-accent-line) 50%, transparent);
  opacity: 0;
  transition: opacity 0.16s ease;
  pointer-events: none;
  z-index: 3;
}

.dict-stage.is-holding::after { opacity: 1; }

.dict-stage.is-holding .demo-hold { opacity: 1; transform: translate(-50%, 0); }

.window--demo .dict-stage.is-holding .cyc-pause { display: none; }
.window--demo .dict-stage.is-holding .cyc-play { display: block; }

/* -------------------------------------------------------------- still mode
   Below 700px the walk plays as swapping stills with the legend carrying the
   words (demo.css hides the tips and transport, and relights only the active
   step's badge, counter-scaled): the scrims and held frame go with them. */
@media (max-width: 700px) {
  .dict-stage::after { display: none; }
  .dict-stage .jc-step.is-active .dict-spot,
  .dict-stage .jc-step.is-active .dict-stage-shadow { opacity: 0; }
}
