/* ═══════════════════════════════════════════════════════════════════════════
   THE DECK

   The tour renders the real dashboards on a plane in space and moves a camera
   around them. Nothing here is a screenshot: the sidebars, tables, pills and
   bars are the same elements the app ships, so the tour cannot drift out of
   date the way captured footage does, and every figure stays sharp at any zoom.

   Structure, and why it nests this way:

     .deck-viewport   owns the perspective. The camera lives here.
       .deck          preserve-3d. This is what rotates and translates.
         .deck-fit    overflow:hidden + fixed aspect. The screen bezel.
           .deck-ui   the dashboard at its true 1240px width, scaled to fit.
           .deck-sweep  scan line, injected by tour.js
         .dl          callouts. Siblings of .deck-fit, NOT children, because
                      overflow:hidden flattens 3D and they need real depth.

   .deck-ui is held at 1240px and scaled rather than allowed to reflow. At a
   container's natural width the dashboard would collapse into its own narrow
   breakpoint, and a tour of the product should show the layout a teacher
   actually gets on a laptop.

   Motion is applied by tour.js and only inside a desktop matchMedia. Every
   hidden state in this file is a starting position for an animation that
   ALWAYS runs; nothing is left invisible if scripting never arrives.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════════
   SEAMS

   Four sticky scenes paint an opaque ground so the page cannot be seen sliding
   underneath them while they are held. That is still required — but each was
   painting a flat fill that ended on a razor edge, and two of them were not
   even the same color (#030305 against #050508). Scrolling from one scene to
   the next therefore dragged a visible horizontal line up the screen.

   Two changes, no change to what the grounds are FOR:

     --tour-bg      one color, so no boundary can be a color step.
     --stage-ground a gradient that is fully opaque across the middle and
                    fades out over the top and bottom 7vh.

   The fade is the whole point. While a scene is held, its edges sit against
   the viewport edges where nothing is read, so the opaque middle still does
   its job. During the handoff the departing edge dissolves into the ambient
   field and the aurora behind it instead of cutting them off, which is what
   turns the transition from an edge into a blend.

   A background-image gradient rather than a mask, deliberately: a mask would
   take the scene's own text with it and fade the copy at the edges too.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  /* Tracks the page token rather than restating it, so the ground and the
     body behind it can never drift to two different darks. */
  --tour-bg: var(--bg, #050508);
  --stage-ground: linear-gradient(180deg,
      transparent 0,
      var(--tour-bg) 7vh,
      var(--tour-bg) calc(100% - 7vh),
      transparent 100%);
}

/* ─── Stage ──────────────────────────────────────────────────────────────── */

.tour-stage {
  display: grid;
  grid-template-columns: minmax(280px, 350px) 1fr;
  gap: clamp(2rem, 4vw, 4rem);
  align-items: center;
  width: min(1560px, 100%);
  margin: 0 auto;
  padding: 0 clamp(1.25rem, 3vw, 2.5rem);
  min-height: 100vh;
}

/* Mirrored stage. The same machinery, sides swapped, so the eye is not asked
   to track the identical composition for the whole experience. The deck's
   resting rotation is mirrored with it by tour.js, so a flipped deck still
   turns to face the copy rather than away from it. */
.tour-stage[data-layout="flip"] { grid-template-columns: 1fr minmax(280px, 350px); }
.tour-stage[data-layout="flip"] .tour-caps { order: 2; }
.tour-stage[data-layout="flip"] .deck-viewport { order: 1; }

@media (max-width: 900px) {
  .tour-stage[data-layout="flip"] { grid-template-columns: 1fr; }
  .tour-stage[data-layout="flip"] .tour-caps { order: 1; }
  .tour-stage[data-layout="flip"] .deck-viewport { order: 2; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   KINETIC TYPE

   No interface at all. One statement, filling the screen, arriving a word at
   a time as the page is scrolled.

   These exist because an experience where every scene is "copy left, product
   right" reads as a template no matter how good each scene is. Dropping the
   product entirely for fifteen seconds resets the eye and gives the argument
   somewhere to land — and it is the only moment in the tour where the writing
   is the whole design.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Held with CSS `sticky`, NOT with a ScrollTrigger pin.

   A pin lifts the element out of flow and relies on an injected spacer to
   reserve the room it used to occupy. Get that measurement wrong — because a
   webfont landed late, because another pin above it resized, because a
   refresh fired mid-scroll — and the statement stays fixed on screen while
   the next section scrolls up underneath it. That is exactly the "text
   overlapping the dashboard" failure.

   Sticky cannot do that. It is bounded by its own parent: the moment
   .type-scene's bottom edge arrives, the stage releases, with no spacer and
   no arithmetic involved. The scroll timeline below still scrubs the words,
   it just no longer owns the layout. */
.type-scene {
  position: relative;
  min-height: 190vh;
}

.type-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  display: grid;
  align-content: center;
  justify-items: center;
  text-align: center;
  padding: 4rem clamp(1.5rem, 7vw, 7rem);
  /* Opaque across the middle, so nothing can read through it while it is
     held; feathered at the edges, so it does not leave one when it goes. */
  background-color: transparent;
  background-image: var(--stage-ground);
  overflow: hidden;
}

@media (max-width: 900px) {
  .type-scene { min-height: 0; }
  .type-stage { position: static; height: auto; min-height: 78vh; }
}

.type-eyebrow {
  font-family: var(--mono);
  font-size: 0.56rem;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--cyan);
  margin-bottom: 2.2rem;
}

.type-line {
  font-family: var(--heading);
  font-size: clamp(2rem, 7.2vw, 6rem);
  line-height: 1.0;
  letter-spacing: -0.045em;
  max-width: 17ch;
  margin: 0 auto;
}

/* Wrapped by tour.js. Each word is its own compositing layer so the blur and
   lift can differ per word without the browser re-rasterising the sentence. */
.type-line .w { display: inline-block; will-change: transform, opacity, filter; }
.type-line .hl {
  background: linear-gradient(135deg, var(--cyan), var(--violet));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}

.type-sub {
  margin-top: 2.2rem;
  color: var(--text2);
  font-size: 1.02rem;
  line-height: 1.65;
  max-width: 46ch;
}

/* The counter-line. Set as a statement rather than a footnote, because it is
   the reply to the headline above it, not a caption for it. */
.type-answer {
  font-family: var(--heading);
  font-size: clamp(1.15rem, 2.2vw, 1.75rem);
  letter-spacing: -0.025em;
  line-height: 1.25;
  color: var(--text);
  max-width: 24ch;
}

/* A hairline that draws itself under the statement as the words land. */
.type-rule {
  margin-top: 2.6rem;
  width: min(420px, 60vw);
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--cyan), var(--violet), transparent);
  transform: scaleX(0);
  transform-origin: 50% 50%;
}

@media (prefers-reduced-motion: reduce) {
  .type-line .w { opacity: 1 !important; filter: none !important; transform: none !important; }
  .type-rule { transform: scaleX(1); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE INTRODUCTION

   The mark's first appearance. It has been withheld from the welcome screen
   and from every screen of the argument, so that it arrives here as the reply
   to a problem the visitor now believes in rather than as a letterhead they
   scrolled past on the way in.
   ═══════════════════════════════════════════════════════════════════════════ */

/* 210vh. Was 300 when this scene carried a written turn ahead of the name;
   with the turn gone, holding the original length would have left most of a
   viewport at the end where scrolling did nothing — the exact defect this
   file has already been through twice. */
.intro-scene { position: relative; min-height: 210vh; }

.intro-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  display: grid;
  place-items: center;
  text-align: center;
  padding: 4rem clamp(1.5rem, 6vw, 6rem);
  background-image: var(--stage-ground);
  overflow: hidden;
}

/* The bloom.

   Driven by --intro-glow, which the reveal timeline writes as the wordmark
   assembles. A custom property rather than opacity on the element, because
   the same number also opens the gradient's radius — light that only fades up
   reads as a layer being turned on, where light that spreads as it brightens
   reads as something happening in the room.

   Default 0. If the timeline never runs, this is simply the flat ground it
   has always been. */
.intro-stage {
  --intro-glow: 0;
}

.intro-stage::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(
      ellipse calc(55% + var(--intro-glow) * 22%) calc(48% + var(--intro-glow) * 20%)
      at 50% 46%,
      rgba(33, 216, 252, calc(0.12 + var(--intro-glow) * 0.20)),
      transparent 68%),
    radial-gradient(
      ellipse 40% 34% at 50% 44%,
      rgba(139, 92, 246, calc(var(--intro-glow) * 0.16)),
      transparent 70%);
}

.intro-inner { position: relative; z-index: 2; }

/* The .intro-bridge / .ib-sm / .ib-lg rules that lived here styled a written
   turn between the globe and the name. The turn was removed — the globe's
   conclusion now fades to an empty screen and the logo answers it directly —
   so the rules went with it rather than being left behind to be rediscovered
   later as styles with no markup. */

/* ─── The ring ────────────────────────────────────────────────────────────── */
/* A single expanding circle behind the mark at the instant it lands.

   The ring and the mark share one grid cell rather than the ring being
   absolutely placed at a measured offset. The logo is rendered at height:auto,
   so any hardcoded `top` would be a guess about its aspect ratio — and a
   guess that goes wrong puts a circle visibly off-center behind the one
   element on the page that must not look misaligned. Sharing a cell is
   concentric by construction, whatever shape the file turns out to be. */
.intro-crest {
  position: relative;
  display: grid;
  place-items: center;
  width: max-content;
  margin: 0 auto 2rem;
}

.intro-crest > * { grid-area: 1 / 1; }

.intro-ring {
  width: clamp(96px, 11vw, 150px);
  height: clamp(96px, 11vw, 150px);
  border: 1px solid var(--cyan);
  border-radius: 50%;
  opacity: 0;
  pointer-events: none;
}

/* Each character of the wordmark is its own layer so the name can assemble
   rather than fade. inline-block is what makes a transform apply at all. */
.intro-name .c {
  display: inline-block;
  will-change: transform, opacity;
}

/* ─── The pauses ──────────────────────────────────────────────────────────── */
/* One word of a .start-sec headline. inline-block for the same reason as
   above: yPercent and scale do nothing to an inline box.

   The <em> stays INSIDE its .sw rather than being replaced by it, so the
   gradient still has one element to clip across the whole phrase. Wrapping
   each word in its own gradient would restart the ramp at every space. */
.start-sec .sw,
.chapter-lede .sw {
  display: inline-block;
  will-change: transform, opacity;
}

.start-sec .sw-em,
.chapter-lede .sw-em { transform-origin: 50% 80%; }

/* The tiles carry a blur and a rotation while they arrive, and a rotated box
   with a backdrop behind it shows its own edge stepping unless the browser is
   told to keep it on its own layer. */
.fact, .start-step { will-change: transform, opacity, filter; }

.intro-mark {
  width: clamp(96px, 11vw, 150px);
  height: auto;
  display: block;
  /* The margin moved to .intro-crest, which is now what occupies the flow. */
  margin: 0;
  filter: drop-shadow(0 0 54px rgba(33, 216, 252, 0.55));
}

.intro-eyebrow {
  font-family: var(--mono);
  font-size: clamp(0.72rem, 1.2vw, 0.95rem);
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: var(--text2);
  margin-bottom: 1.3rem;
}

.intro-name {
  font-family: var(--heading);
  font-size: clamp(2.6rem, 8vw, 6.4rem);
  line-height: 0.98;
  letter-spacing: -0.05em;
  margin-bottom: 1.6rem;
}

/* Sized for a definition, not for a slogan.

   The old tagline was five words and sat at 22ch / 2.3rem. This line is a
   sentence naming three subjects and a grade band, and at those values it
   stacked into five enormous lines that pushed the wordmark off the optical
   center. Wider measure, smaller type: it is the explanation under the name,
   and it should not compete with it. */
.intro-tagline {
  font-family: var(--heading);
  font-size: clamp(1.05rem, 1.9vw, 1.5rem);
  line-height: 1.4;
  letter-spacing: -0.02em;
  max-width: 46ch;
  text-wrap: balance;
  margin: 0 auto;
  background: linear-gradient(135deg, var(--cyan), var(--violet));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
  padding: 0 0.06em;
}

@media (max-width: 900px) {
  .intro-scene { min-height: 0; }
  .intro-stage { position: static; height: auto; min-height: 84vh; }
}

@media (prefers-reduced-motion: reduce) {
  .intro-scene { min-height: 0; }
  .intro-stage { position: static; height: auto; min-height: 80vh; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   AMBIENT FIELD

   A calm wallpaper behind the whole experience: a very faint hex weave and a
   handful of slowly drifting points, fixed to the viewport so it never
   restarts or jumps as sections change.

   CSS only — no canvas, no rAF, no scroll listener. The honeycomb on the
   opening screen is a canvas because it reacts to the pointer and pulses;
   this one only has to breathe, and paying for a render loop across six
   thousand vertical pixels to achieve that would be the exact trade this
   project has been avoiding.

   It sits at z-index 0 behind everything. The sticky statement screens
   (evidence, type, globe, welcome) paint their own opaque grounds over it on
   purpose — those moments have dedicated visuals and do not want a second
   texture competing. Everywhere else, this is what is behind the deck.
   ═══════════════════════════════════════════════════════════════════════════ */

.tour-ambient {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}

/* The weave. Three hairline gradients at 60° to each other read as a hex
   field at this opacity without a single image request. */
.tour-ambient::before {
  content: '';
  position: absolute;
  inset: -10%;
  background-image:
    repeating-linear-gradient(60deg, rgba(255,255,255,.016) 0 1px, transparent 1px 58px),
    repeating-linear-gradient(-60deg, rgba(255,255,255,.016) 0 1px, transparent 1px 58px),
    repeating-linear-gradient(0deg, rgba(255,255,255,.012) 0 1px, transparent 1px 50px);
  mask-image: radial-gradient(ellipse 80% 70% at 50% 50%, #000 20%, transparent 88%);
  -webkit-mask-image: radial-gradient(ellipse 80% 70% at 50% 50%, #000 20%, transparent 88%);
  animation: ambientDrift 90s linear infinite;
}

@keyframes ambientDrift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-58px, -100px, 0); }
}

.tour-ambient i {
  position: absolute;
  width: 3px; height: 3px;
  border-radius: 50%;
  background: var(--cyan);
  opacity: 0;
  animation: ambientFloat var(--dur, 22s) ease-in-out infinite;
  animation-delay: var(--del, 0s);
}
.tour-ambient i:nth-child(even) { background: var(--violet); }
.tour-ambient i:nth-child(3n) { background: var(--lime); width: 2px; height: 2px; }

@keyframes ambientFloat {
  0%   { opacity: 0; transform: translateY(20px) scale(.7); }
  25%  { opacity: .38; }
  60%  { opacity: .22; transform: translateY(-24px) scale(1); }
  100% { opacity: 0; transform: translateY(-60px) scale(.7); }
}

/* Content sits above it. Sections that paint their own ground already do so
   opaquely, so this only affects the ones that were showing bare body. */
.chapter-sec, .start-sec, .tour-end, .type-scene, .evi-scene, .globe-scene, .tour-hero {
  position: relative;
  z-index: 1;
}

@media (prefers-reduced-motion: reduce) {
  .tour-ambient::before { animation: none; }
  .tour-ambient i { animation: none; opacity: .18; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE EVIDENCE

   Three findings, one per screen, each one a single enormous figure with its
   claim under it and its source under that.

   Deliberately typographic rather than a chart. A chart invites the reader to
   interrogate an axis; a figure this size is simply read. And because each one
   carries a visible citation, the sequence earns the right to be this
   emphatic — the number does the arguing, not the adjectives around it.
   ═══════════════════════════════════════════════════════════════════════════ */

.evi-scene { position: relative; }

.evi-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  display: grid;
  place-items: center;
  overflow: hidden;
  padding: 4rem clamp(1.5rem, 6vw, 6rem);
  background-image: var(--stage-ground);
}

/* A faint counter that persists across all three, so the sequence reads as
   evidence being laid out rather than three unrelated slides. */
.evi-rail {
  position: absolute;
  left: 50%;
  bottom: clamp(2rem, 6vh, 4rem);
  transform: translateX(-50%);
  display: flex;
  gap: 0.5rem;
}
.evi-rail i {
  width: 26px; height: 2px; border-radius: 2px;
  background: rgba(255, 255, 255, 0.14);
  transition: background 0.4s ease, width 0.4s ease;
}
.evi-rail i.on { background: var(--cyan); width: 42px; }

/* No single measure on the block. Each role gets the width that suits it —
   a short headline reads badly at 46ch and a supporting sentence reads badly
   at 24ch. One narrow column for all of them is what made this feel like a
   cramped card rather than a statement. */
/* All four share one cell, so each arrives dead center. The first two are then
   parked left and right by tour.js and STAY there while the third lands
   between them — so by the third scroll all three findings are on screen at
   once and the case is visibly cumulative rather than a slideshow that forgets
   what it just said.

   Width is capped so a parked card at 42% scale clears the centerd one. */
.evi {
  grid-area: 1 / 1;
  text-align: center;
  width: min(560px, 90%);
  justify-self: center;
}

/* ─── "Did you know?" ────────────────────────────────────────────────────
   The hinge between the welcome and the evidence. Its whole job is to turn
   three statistics into something the reader leans toward rather than reads
   past, so it is set larger than anything around it and arrives one word at
   a time from three different directions.

   The question mark is its own element and lands last, with a glow — it is
   the punctuation the whole section turns on. */

.dyk {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0 0.28em;
  font-family: var(--heading);
  font-size: clamp(3rem, 11vw, 9rem);
  /* Not 0.98. At this size a line-height under 1 makes the line box shorter
     than the glyphs themselves, and the stage's overflow:hidden then crops
     whatever pokes out — which at 9rem is the top of the question mark. */
  line-height: 1.08;
  letter-spacing: -0.05em;
}

/* The padding is doing real work, not spacing: an inline-block is clipped to
   its own box, and a tall glyph at this size overhangs it. */
.dyk-w {
  display: inline-block;
  padding: 0.06em 0.02em 0.1em;
  will-change: transform, opacity, filter;
}

.dyk-w em {
  font-style: normal;
  display: inline-block;
  /* background-clip:text paints into this element's box, so the box has to be
     bigger than the glyph or the gradient is trimmed at the edges. The right
     margin gives that padding back to the layout; the left one deliberately
     does not — cancelling both sides pulled the mark hard against "know". */
  padding: 0 0.08em;
  margin: 0 -0.08em 0 0.05em;
  background: linear-gradient(140deg, var(--cyan), var(--violet));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 0 30px rgba(33, 216, 252, 0.5));
}

@media (prefers-reduced-motion: reduce) {
  .dyk-w { opacity: 1 !important; transform: none !important; filter: none !important; }
}

.evi-tag {
  font-family: var(--mono);
  font-size: 0.58rem;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--cyan);
  margin-bottom: 2.4rem;
}

/* The run-up. Each finding is one sentence with the figure inside it, so the
   opening clause sits above the number and the rest below — the statistic
   becomes the loudest word in the sentence rather than a separate object with
   a caption attached. */
.evi-lead-in {
  margin: 0 auto 0.5rem;
  max-width: 26ch;
  font-family: var(--heading);
  /* Loud enough to be read as the opening of the sentence rather than a label
     above it — at supporting weight the eye jumped straight to the figure and
     the clause was lost. */
  font-size: clamp(1.5rem, 3vw, 2.5rem);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.032em;
  color: var(--text);
}

/* The figure. Sized to be the only thing on screen worth looking at. */
.evi-num {
  font-family: var(--heading);
  font-size: clamp(4.5rem, 15vw, 13rem);
  line-height: 0.9;
  letter-spacing: -0.055em;
  font-variant-numeric: tabular-nums;
  background: linear-gradient(150deg, var(--cyan), var(--violet) 70%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}

/* The unit is painted, not clipped.

   background-clip:text paints the PARENT's background and clips it to the
   text. A descendant inherits -webkit-text-fill-color:transparent but has no
   background of its own to clip, so it renders as nothing — which is why the
   percent sign disappeared. Giving it a solid fill both fixes that and reads
   better: the unit should be quieter than the number it qualifies. */
.evi-num em {
  font-style: normal;
  font-size: 0.4em;
  letter-spacing: -0.01em;
  -webkit-text-fill-color: var(--cyan);
  color: var(--cyan);
  margin-left: 0.04em;
}

.evi-claim {
  margin: 2.2rem auto 0;
  max-width: 26ch;
  font-family: var(--heading);
  font-size: clamp(1.15rem, 2.4vw, 1.9rem);
  line-height: 1.28;
  letter-spacing: -0.028em;
  color: var(--text);
  text-wrap: balance;
}

/* Ranges must never break. "ages 5–8" wrapping to "5-" / "8" reads as a typo
   and undermines the figure it belongs to — which on this page is the entire
   point of the screen. Applied to the span, not the paragraph, so the rest of
   the line still wraps normally. */
.nb { white-space: nowrap; }

/* The turn — the line the figure exists to set up.

   Sized close to the claim above it rather than treated as a caption: the
   statistic gets attention, but this is the sentence a reader should leave
   with, and at supporting-text size it was being skipped. */
.evi-punch {
  margin: 1.7rem auto 0;
  max-width: 28ch;
  font-family: var(--heading);
  font-size: clamp(1.25rem, 2.15vw, 1.75rem);
  line-height: 1.26;
  letter-spacing: -0.028em;
  color: var(--text);
  text-wrap: balance;
}

/* Our reading of a statistic, never the statistic itself.

   Renamed from the unused .evi-say, which described nothing. This class now
   has one job and it is a factual one: it holds the sentence that goes BEYOND
   what a cited source measured, placed below the citation so the two can
   never be read as one claim. Quieter and narrower than the claim above it,
   because interpretation should not carry the visual authority of evidence.

   If a line in here could be mistaken for something the source found, it
   belongs in .evi-claim with a citation, or it does not belong at all. */
.evi-note {
  margin: 1.4rem auto 0;
  max-width: 48ch;
  color: var(--text2);
  font-size: clamp(0.95rem, 1.5vw, 1.1rem);
  line-height: 1.7;
}

/* Marks a citation that has not been confirmed yet. Deliberately visible
   rather than silent: an unsourced figure on a page aimed at district buyers
   is a liability, and this should be impossible to ship by accident. */
.evi-src[data-src-check] {
  color: var(--amber);
  border-top-color: rgba(240, 180, 41, 0.35);
}

/* A second beat on the same screen: the sentence lands, then this arrives to
   turn it. Held back by the timeline rather than shown with everything else,
   because a fact that is already on screen cannot be a reveal. */
.evi-reveal {
  margin: 1.8rem auto 0;
  max-width: 30ch;
  padding: 1rem 1.3rem;
  border-radius: 14px;
  border: 1px solid rgba(240, 180, 41, 0.32);
  background: rgba(240, 180, 41, 0.06);
  font-family: var(--heading);
  font-size: clamp(1.02rem, 1.75vw, 1.35rem);
  line-height: 1.3;
  letter-spacing: -0.022em;
  color: var(--text);
}

/* Small but never hidden, and set off by a rule so it reads as a citation
   rather than as another line of the paragraph above it. */
.evi-src {
  margin: 2.6rem auto 0;
  padding-top: 1.2rem;
  max-width: 34ch;
  border-top: 1px solid var(--glass-border);
  font-family: var(--mono);
  font-size: 0.56rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text3);
}

/* A statement screen: no figure, no citation, one sentence at full height.
   Used for the leads that punctuate the evidence rather than form part of it. */
.evi-statement {
  margin: 0 auto;
  max-width: 20ch;
  font-family: var(--heading);
  font-size: clamp(1.9rem, 5.4vw, 4.4rem);
  line-height: 1.08;
  letter-spacing: -0.042em;
  text-wrap: balance;
}
.evi-statement em {
  font-style: normal;
  background: linear-gradient(135deg, var(--cyan), var(--violet));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
  padding: 0 0.05em; margin: 0 -0.05em;
}

/* The convergence: three separate assertions, arriving one at a time, each
   one its own line. Set as a list rather than a paragraph because they are
   three findings restated — not a sentence that happens to have commas. */
/* ─── The gap ────────────────────────────────────────────────────────────
   Three assertions using the whole screen, not a column down the middle.

   The findings are narrow on purpose — a statistic wants a tight measure. The
   conclusion is the opposite: it is the widest idea in the sequence and was
   being squeezed into 24 characters, which read as a receipt rather than a
   verdict.

   So each line takes its own alignment, its own weight and its own size, and
   enters from its own side. Left, center, right — the eye travels across the
   screen instead of straight down it, and the three read as three statements
   rather than one wrapped sentence. */

.evi.evi-wide { width: min(1180px, 94%); }

.evi-conv-lines {
  display: grid;
  gap: clamp(1.1rem, 3.2vh, 2.4rem);
  width: 100%;
  max-width: none;
  margin: 0;
}

.evi-conv-line {
  font-family: var(--heading);
  line-height: 1.02;
  letter-spacing: -0.042em;
  margin: 0;
}

/* Light, wide, left — the setup. */
.evi-conv-line.g-a {
  justify-self: start;
  text-align: left;
  font-size: clamp(1.5rem, 4.4vw, 3.5rem);
  font-weight: 500;
  color: var(--text2);
  max-width: 20ch;
}

/* Centered and quieter — the middle beat, and the pivot. */
.evi-conv-line.g-b {
  justify-self: center;
  text-align: center;
  font-size: clamp(1.35rem, 3.6vw, 2.9rem);
  font-weight: 600;
  max-width: 24ch;
}

/* Heavy, right, largest — the verdict, and the last thing read. */
.evi-conv-line.g-c {
  justify-self: end;
  text-align: right;
  font-size: clamp(1.75rem, 5.4vw, 4.4rem);
  font-weight: 800;
  max-width: 18ch;
}

.evi-conv-line em {
  font-style: normal;
  background: linear-gradient(135deg, var(--cyan), var(--violet));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
  padding: 0 0.06em; margin: 0 -0.02em 0 0.02em;
}

/* A hairline under the last line, drawn from the right, so the section closes
   rather than simply stopping. */
.evi-gap-rule {
  justify-self: end;
  width: min(420px, 60%);
  height: 1px;
  margin-top: 0.4rem;
  background: linear-gradient(90deg, transparent, var(--cyan), var(--violet));
  transform: scaleX(0);
  transform-origin: 100% 50%;
}

@media (max-width: 900px) {
  .evi-conv-line.g-a, .evi-conv-line.g-b, .evi-conv-line.g-c {
    justify-self: start; text-align: left; max-width: none;
  }
  .evi-gap-rule { justify-self: start; transform-origin: 0 50%; }
}

.evi-conv {
  margin: 0 auto;
  max-width: 19ch;
  font-family: var(--heading);
  font-size: clamp(1.6rem, 4.2vw, 3.4rem);
  line-height: 1.14;
  letter-spacing: -0.038em;
  text-wrap: balance;
}
.evi-conv em {
  font-style: normal;
  background: linear-gradient(135deg, var(--cyan), var(--violet));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}
.evi-then {
  margin: 2.2rem auto 0;
  padding-top: 1.6rem;
  border-top: 1px solid var(--glass-border);
  color: var(--text2);
  font-size: clamp(1rem, 1.7vw, 1.2rem);
  line-height: 1.68;
  max-width: 48ch;
}

@media (max-width: 900px) {
  .evi-scene { min-height: 0 !important; }
  .evi-stage { position: static; height: auto; min-height: 86vh; }
  .evi { margin-bottom: 3rem; }
  .evi-rail { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .evi-scene { min-height: 0 !important; }
  .evi-stage { position: static; height: auto; min-height: 80vh; }
  .evi { margin-bottom: 3rem; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE GLOBE

   The one place in the experience that is pure spectacle — and it earns it,
   because it is carrying the single sentence the whole argument rests on.

   Built in SVG rather than WebGL. A wireframe sphere is a few dozen ellipses;
   a canvas would mean a renderer, a raf loop, a device-pixel-ratio problem and
   a black rectangle wherever WebGL is unavailable. SVG strokes stay razor
   sharp at any size, scrub perfectly in both directions, cost almost nothing
   to paint, and degrade to a static globe if scripting never arrives.

   The rotation is real, not decorative: the longitude ellipses have their rx
   driven by scroll, so the sphere genuinely appears to turn as the visitor
   moves. Scrolling back turns it back.
   ═══════════════════════════════════════════════════════════════════════════ */

/* 280vh, not 340. The answer beat moved to its own screen, and leaving the
   original length behind would have left roughly half a viewport at the end
   where scrolling did nothing. */
.globe-scene { position: relative; min-height: 280vh; }

.globe-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  display: grid;
  place-items: center;
  overflow: hidden;
  /* Was #030305 — two shades darker than every neighbor, which made the
     boundary a step you could see even before it was an edge. */
  background-image: var(--stage-ground);
}

/* Ground glow, so the sphere sits in space rather than on a flat panel. */
.globe-stage::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse 60% 50% at 50% 50%, rgba(33, 216, 252, 0.09), transparent 68%);
}

.globe-wrap {
  grid-area: 1 / 1;
  width: min(76vh, 88vw);
  aspect-ratio: 1;
  position: relative;
}

.globe-wrap svg { width: 100%; height: 100%; overflow: visible; display: block; }

/* Every stroke is drawn by animating dashoffset from 1 to 0. pathLength="1"
   normalises each shape to a single unit so one tween value works for all of
   them regardless of their real perimeter. */
.g-line {
  fill: none;
  stroke: rgba(33, 216, 252, 0.42);
  stroke-width: 0.9;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}

.g-rim {
  stroke: rgba(33, 216, 252, 0.75);
  stroke-width: 1.4;
  filter: drop-shadow(0 0 10px rgba(33, 216, 252, 0.55));
}

.g-lon { stroke: rgba(139, 92, 246, 0.4); }

.g-node {
  fill: #21d8fc;
  opacity: 0;
  filter: drop-shadow(0 0 6px rgba(33, 216, 252, 0.9));
}
.g-node.is-violet { fill: #8b5cf6; filter: drop-shadow(0 0 6px rgba(139, 92, 246, 0.9)); }
.g-node.is-lime { fill: #10b981; filter: drop-shadow(0 0 6px rgba(16, 185, 129, 0.9)); }

/* ─── Copy over the globe ────────────────────────────────────────────────── */

.globe-copy {
  grid-area: 1 / 1;
  position: relative;
  z-index: 3;
  text-align: center;
  padding: 0 clamp(1.25rem, 6vw, 6rem);
  width: 100%;
}

.globe-line {
  font-family: var(--heading);
  font-size: clamp(2rem, 6.4vw, 5.2rem);
  line-height: 1.02;
  letter-spacing: -0.045em;
  max-width: 15ch;
  margin: 0 auto;
  /* A legibility floor: white type over a bright wireframe needs its own
     ground or the strokes read straight through the counters. */
  text-shadow: 0 4px 40px rgba(3, 3, 5, 0.95), 0 2px 14px rgba(3, 3, 5, 0.9);
}

.globe-line .w { display: inline-block; will-change: transform, opacity, filter; }

/* The highlight words carry no text fill at all — only a gradient clipped to
   their glyphs. That makes the legibility shadow above actively harmful: with
   nothing filling the letterforms, the shadow paints OVER the gradient rather
   than behind it, and two near-black stops at .95 and .9 alpha turn the two
   biggest words in the sentence to mud. So the shadow is dropped here, and the
   gradient carries its own contrast instead: both stops move to their light
   tints, which clears 7:1 on this background where violet-500 sat near 4.8:1.

   A drop-shadow filter is not the alternative it looks like — tour.js animates
   `filter` on these same words, and its inline blur(0px) would erase it. */
.globe-line .hl {
  background: linear-gradient(135deg, #5ee3fd, #a78bfa);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
  text-shadow: none;
}

/* ─── The answer ─────────────────────────────────────────────────────────── */
/* The payoff. Deliberately a separate visual object rather than another line
   of the same paragraph: the headline is the problem, this is the reply, and
   they should not look like the same voice. */

.globe-answer {
  margin-top: clamp(1.6rem, 4vh, 3rem);
  display: inline-flex;
  align-items: center;
  gap: 1rem;
  padding: 1rem 1.8rem 1rem 1.2rem;
  border-radius: 100px;
  background: rgba(6, 6, 12, 0.72);
  border: 1px solid rgba(33, 216, 252, 0.32);
  box-shadow: 0 24px 70px -24px rgba(0, 0, 0, 1), 0 0 50px -20px rgba(33, 216, 252, 0.55);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}

.globe-answer img {
  width: 42px; height: 42px; object-fit: contain; flex: none;
  filter: drop-shadow(0 0 18px rgba(33, 216, 252, 0.6));
}

.globe-answer strong {
  font-family: var(--heading);
  font-size: clamp(1.05rem, 2.3vw, 1.7rem);
  letter-spacing: -0.025em;
  line-height: 1.2;
  font-weight: 700;
}

@media (max-width: 900px) {
  .globe-scene { min-height: 0; }
  .globe-stage { position: static; height: auto; min-height: 90vh; padding: 4rem 0; }
  .globe-wrap { width: min(62vh, 86vw); }
  .globe-answer { padding: 0.9rem 1.3rem 0.9rem 1rem; }
}

@media (prefers-reduced-motion: reduce) {
  .globe-scene { min-height: 0; }
  .globe-stage { position: static; height: auto; min-height: 80vh; }
  .g-line { stroke-dashoffset: 0; }
  .g-node { opacity: 0.9; }
  .globe-line .w { opacity: 1 !important; filter: none !important; transform: none !important; }
}

/* ─── Caption column ─────────────────────────────────────────────────────── */

/* SAFE DEFAULT: captions flow one after another and are all readable.
   Stacking them into a single cell is only correct while something is
   actively hiding all but one of them — if the engine never runs, that same
   rule turns the column into four paragraphs printed on top of each other.

   tour.js adds .js-motion to <html> once it has successfully taken over, so
   the overlapping layout can never exist without the thing that manages it. */
.tour-caps { position: relative; z-index: 20; }
.tour-caps > .cap + .cap { margin-top: 2.25rem; }

.js-motion .tour-caps { display: grid; }
.js-motion .tour-caps > .cap { grid-area: 1 / 1; align-self: center; margin-top: 0; }

/* Above the callouts. A label floating off the edge of the deck must never
   land on top of the sentence it is illustrating. */
.tour-caps .cap { position: relative; z-index: 2; }

.cap-step {
  font-family: var(--mono);
  font-size: 0.56rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--cyan);
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 1rem;
}

.cap-step::before {
  content: '';
  width: 5px; height: 5px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 12px currentColor;
}

.cap h3 {
  font-family: var(--heading);
  font-size: clamp(1.4rem, 2.2vw, 2rem);
  line-height: 1.12;
  letter-spacing: -0.03em;
  margin-bottom: 0.85rem;
}

.cap h3 em {
  font-style: normal;
  background: linear-gradient(135deg, var(--cyan), var(--violet));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}

.cap p { color: var(--text2); font-size: 0.94rem; line-height: 1.62; }
.cap p + p { margin-top: 0.7rem; }

/* Bullets, for the beats that enumerate what a tab does. Lines, not boxes —
   a bulleted card inside a caption column competes with the deck. */
.cap-list { list-style: none; margin: 1rem 0 0; padding: 0; }
.cap-list li {
  position: relative;
  padding: 0.36rem 0 0.36rem 1.15rem;
  font-size: 0.87rem;
  color: var(--text2);
  line-height: 1.5;
}
.cap-list li::before {
  content: '';
  position: absolute; left: 0; top: 0.92rem;
  width: 5px; height: 5px; border-radius: 1px;
  background: var(--cyan);
  transform: rotate(45deg);
  opacity: 0.85;
}
.cap-list strong { color: var(--text); font-weight: 600; }

.cap-note {
  margin-top: 1rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--glass-border);
  font-size: 0.82rem;
  color: var(--text3);
}

/* The reveal caption. Larger than a beat, because it is the answer the four
   preceding beats were building toward, and the only place in the opening
   where the product is named as the resolution rather than the host. */
.cap-reveal .cap-step { color: var(--lime); }
.cap-reveal h3 { font-size: clamp(1.9rem, 3.1vw, 2.8rem); margin-bottom: 0.7rem; }
.cap-reveal p { font-size: 1.02rem; }
.cap-reveal p em {
  font-style: normal;
  color: var(--text);
  font-weight: 600;
}

/* ─── Progress rail ──────────────────────────────────────────────────────── */

.tour-rail { display: flex; align-items: center; gap: 0.8rem; margin-bottom: 1.8rem; }
.tour-rail-track { flex: 1; height: 2px; border-radius: 2px; background: rgba(255,255,255,0.09); overflow: hidden; }
.tour-rail-fill { display: block; height: 100%; width: 0; border-radius: 2px; background: linear-gradient(90deg, var(--cyan), var(--violet)); }
.tour-rail-idx { font-family: var(--mono); font-size: 0.58rem; letter-spacing: 0.14em; color: var(--text3); font-variant-numeric: tabular-nums; }
.tour-rail-idx em { font-style: normal; color: var(--text); }

/* ─── Stage atmosphere ───────────────────────────────────────────────────── */
/* tour.js writes these while a chapter is pinned. They drive paint only —
   two soft lights and a grid offset — so the stage responds to how fast and
   which way the visitor is scrolling without ever contending for a transform
   the timelines own. Everything reverses for free, because it is derived from
   scroll progress rather than played.

   Both layers sit behind the content on negative z-index inside an isolated
   stacking context, so they cannot come between the deck and the captions. */

.tour-stage {
  --tour-velocity: 0;
  --tour-light-a: 24%;
  --tour-light-b: 78%;
  --tour-grid-x: 0px;
  --tour-grid-y: 0px;
  isolation: isolate;
}

.tour-stage::before,
.tour-stage::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.6s ease;
}

/* Two lights that travel with progress and warm slightly with speed. */
.tour-stage::before {
  z-index: -2;
  background:
    radial-gradient(circle at var(--tour-light-a) 45%,
      rgba(33, 216, 252, calc(0.05 + var(--tour-velocity) * 0.04)), transparent 34%),
    radial-gradient(circle at var(--tour-light-b) 62%,
      rgba(139, 92, 246, calc(0.055 + var(--tour-velocity) * 0.035)), transparent 38%);
}

/* A fine grid that drifts against the scroll direction, so the stage reads as
   ground the deck is sitting on rather than a flat backdrop. */
.tour-stage::after {
  z-index: -1;
  inset: 4% 1%;
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.018) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.014) 1px, transparent 1px);
  background-size: 46px 46px;
  background-position: var(--tour-grid-x) var(--tour-grid-y);
  mask-image: radial-gradient(ellipse 70% 60% at 50% 50%, #000 30%, transparent 78%);
  -webkit-mask-image: radial-gradient(ellipse 70% 60% at 50% 50%, #000 30%, transparent 78%);
}

.tour-stage.is-active::before { opacity: 1; }
.tour-stage.is-active::after { opacity: 0.85; }

/* Atmosphere is the first thing to go: it is the least load-bearing part of
   the page and the most expensive to paint continuously. */
@media (max-width: 900px), (prefers-reduced-motion: reduce) {
  .tour-stage::before,
  .tour-stage::after { display: none; }
}

/* ─── Viewport and camera ────────────────────────────────────────────────── */

.deck-viewport { perspective: 2200px; perspective-origin: 50% 45%; position: relative; }

.deck {
  position: relative;
  transform-style: preserve-3d;
  /* The resting pose. tour.js animates away from and back to this; if the
     script never runs, this is what a visitor sees, and it reads as an
     intentional angled product shot rather than a broken animation. */
  transform: rotateX(4deg) rotateY(-9deg) rotateZ(0.4deg);
}

/* Contact shadow, so the plane sits in the scene instead of floating on it. */
.deck::after {
  content: '';
  position: absolute; left: 6%; right: 6%; bottom: -7%; height: 60px;
  background: radial-gradient(ellipse 50% 100% at 50% 0%, rgba(0,0,0,0.72), transparent 70%);
  filter: blur(26px);
  transform: translateZ(-70px);
  pointer-events: none;
}

.deck-fit {
  position: relative;
  aspect-ratio: 1240 / 800;
  overflow: hidden;
  border-radius: 16px;
  background: #07090d;
  border: 1px solid rgba(255,255,255,0.11);
  box-shadow:
    0 60px 120px -40px rgba(0,0,0,0.95),
    0 0 0 1px rgba(255,255,255,0.04) inset,
    0 1px 0 rgba(255,255,255,0.07) inset;
}

/* A screen is lit from somewhere. Without this the plane reads as a flat
   rectangle of color no matter how far it rotates. */
.deck-fit::after {
  content: '';
  position: absolute; inset: 0;
  pointer-events: none; z-index: 6;
  background: linear-gradient(114deg,
    rgba(255,255,255,0.05) 0%, rgba(255,255,255,0) 34%, rgba(0,0,0,0.16) 100%);
}

.deck-ui {
  width: 1240px;
  transform-origin: top left;
  transform: scale(var(--deck-s, 0.56));
}

/* Scan line. Injected by tour.js on every panel change — cheap, and it tells
   the eye something was swapped rather than merely faded. */
.deck-sweep {
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 34%;
  z-index: 5;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(180deg,
    transparent 0%,
    rgba(33,216,252,0.05) 55%,
    rgba(33,216,252,0.16) 88%,
    rgba(33,216,252,0.55) 99%,
    transparent 100%);
}

/* ─── Depth layers ───────────────────────────────────────────────────────── */

/* Callouts sit ABOVE the deck, not beside or on it.

   Every other position has a failure: outside the left or right edge they land
   in the gutter or on the caption column; inside the edge they cover the
   dashboard they are annotating. Above the frame there is nothing to collide
   with — the stage is centerd in the viewport and the deck is shorter than it,
   so that space is always empty.

   The inline left/right on each callout is overridden rather than removed, so
   the markup keeps a record of which side each one was authored for. */
.dl {
  position: absolute;
  bottom: calc(100% + 0.85rem);
  left: auto !important;
  right: 0 !important;
  top: auto !important;
  transform-style: preserve-3d;
  pointer-events: none;
  z-index: 7;
}

/* Flipped stages read right-to-left, so the annotation follows. */
.tour-stage[data-layout="flip"] .dl { right: auto !important; left: 0 !important; }

@media (max-width: 900px) { .dl { display: none; } }

.callout {
  display: inline-flex; align-items: center; gap: 0.55rem;
  padding: 0.58rem 0.92rem;
  border-radius: 11px;
  background: rgba(10,10,18,0.9);
  border: 1px solid rgba(33,216,252,0.34);
  box-shadow: 0 22px 50px -18px rgba(0,0,0,0.95), 0 0 30px -12px rgba(33,216,252,0.5);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  font-size: 0.8rem; font-weight: 600; color: #ededf2; white-space: nowrap;
}
.callout i { color: #21d8fc; font-size: 0.76rem; }
.callout.is-amber { border-color: rgba(240,180,41,0.4); }
.callout.is-amber i { color: #f0b429; }
.callout.is-lime { border-color: rgba(16,185,129,0.4); }
.callout.is-lime i { color: #10b981; }

/* ─── Focus ring ─────────────────────────────────────────────────────────── */
/* The stand-in for a cursor. A tour needs to say "look here" without a fake
   mouse pointer, which always reads as a recording of someone else's screen.

   Sized in the dashboard's own 1240px coordinate space and scaled down with
   it, so the border and glow are specified oversized to survive the reduction:
   at --deck-s ≈ 0.56 a 1.5px border renders sub-pixel and disappears. */
.focus-ring {
  position: absolute;
  border-radius: 14px;
  border: 3px solid #21d8fc;
  box-shadow: 0 0 0 7px rgba(33,216,252,0.12), 0 0 60px rgba(33,216,252,0.45);
  pointer-events: none;
  opacity: 0;
  z-index: 4;
}


/* ═══════════════════════════════════════════════════════════════════════════
   THE PRODUCT, SCOPED

   The shipped dashboards, namespaced under .ngd so the product's palette
   cannot leak into the marketing page around it. Class names follow the app's
   own, so a change to the real dashboard is brought across by copying the rule
   rather than translating it.

   Four skins, because there genuinely are four. These are not four opinions
   about one design system — they are what each audience is served today:

     .skin-teacher   emerald on near-black       app/public/teachers
     .skin-student   violet on deep navy, glass  app/public/students
     .skin-admin     neon cyan/lime/magenta      app/public/school-admin
     .skin-k5        LIGHT, playful              student-dashboard-k5

   The K-5 skin being light is not an inconsistency to fix. A dashboard for a
   six-year-old should not look like a SOC console.

   Type is the product's own stack (Plus Jakarta Sans / Instrument Sans /
   DM Mono), self-hosted in /fonts. Marketing chrome outside the deck keeps the
   brand stack.
   ═══════════════════════════════════════════════════════════════════════════ */

.ngd {
  position: relative;               /* containing block for the focus ring */
  display: grid;
  grid-template-columns: 248px 1fr;
  min-height: 800px;
  font-family: var(--font-app-body, 'Instrument Sans', system-ui, sans-serif);
  line-height: 1.55;
  --d-bg: #07090d;
  --d-surface: #0c0f15;
  --d-raised: #12161e;
  --d-line: rgba(255,255,255,0.06);
  --d-text: #eaecf0;
  --d-text2: rgba(234,236,240,0.58);
  --d-text3: rgba(234,236,240,0.32);
  --d-accent: #10b981;
  --d-a2: #f472b6;
  --d-a3: #fbbf24;
  --d-a4: #38bdf8;
  --d-a5: #a78bfa;
  background: var(--d-bg);
  color: var(--d-text);
}

/* Real palettes, lifted from each shipped dashboard's :root. */
.ngd.skin-teacher { --d-bg:#07090d; --d-surface:#0c0f15; --d-raised:#12161e; --d-accent:#10b981; --d-a2:#f472b6; --d-a3:#fbbf24; --d-a4:#38bdf8; --d-a5:#a78bfa; }
.ngd.skin-student { --d-bg:#070a16; --d-surface:rgba(12,16,40,.55); --d-raised:rgba(18,24,54,.6); --d-line:rgba(140,120,255,.12); --d-text:#e2e8f0; --d-text2:#8b95b8; --d-text3:#5c6888; --d-accent:#8b5cf6; --d-a2:#6366f1; --d-a3:#f59e0b; --d-a4:#3b82f6; --d-a5:#10b981; }
.ngd.skin-admin   { --d-bg:#08080c; --d-surface:#0e0e14; --d-raised:#15151f; --d-line:rgba(255,255,255,.07); --d-text:#ededf2; --d-text2:rgba(237,237,242,.52); --d-text3:rgba(237,237,242,.28); --d-accent:#00f0ff; --d-a2:#b4ff39; --d-a3:#febc2e; --d-a4:#ff2d78; --d-a5:#8b5cf6; }
.ngd.skin-k5      { --d-bg:#f0f4ff; --d-surface:#ffffff; --d-raised:#f8faff; --d-line:rgba(148,163,184,.18); --d-text:#1e293b; --d-text2:#475569; --d-text3:#64748b; --d-accent:#06b6d4; --d-a2:#8b5cf6; --d-a3:#fbbf24; --d-a4:#ec4899; --d-a5:#22c55e; }
.ngd.skin-k5 { font-family: 'Space Grotesk', var(--font-app-body, system-ui), sans-serif; }

/* The opener has no sidebar: it is not a dashboard, it is the answer to "what
   is this product", and giving it app chrome would imply a screen that does
   not exist. */
.ngd.no-side { grid-template-columns: 1fr; }

/* Belt and braces. The two-column shell only makes sense when there is a
   sidebar to put in the first column; without one, `main` lands in a 248px
   track and the whole dashboard wraps into a gutter. Forgetting .no-side is
   an easy mistake to make and an ugly one to look at, so the layout defends
   itself rather than relying on the class being remembered. */
.ngd:not(:has(.app-side)) { grid-template-columns: 1fr; }
.ngd.skin-brand {
  --d-bg: #07080f; --d-surface: rgba(255,255,255,.032); --d-raised: rgba(255,255,255,.055);
  --d-line: rgba(255,255,255,.075);
  --d-text: #ededf2; --d-text2: rgba(237,237,242,.55); --d-text3: rgba(237,237,242,.28);
  --d-accent: #21d8fc; --d-a2: #8b5cf6; --d-a3: #f0b429; --d-a4: #10b981; --d-a5: #8b5cf6;
}

/* ─── Opener: the three subjects, and what is delivered ──────────────────── */

.ngd .pillar {
  text-align: center;
  padding: 1.25rem 1rem;
  border-radius: 15px;
  border: 1px solid var(--d-line);
  background: var(--d-surface);
}
.ngd .pillar .pico {
  width: 50px; height: 50px; margin: 0 auto 0.7rem;
  border-radius: 15px; display: grid; place-items: center; font-size: 1.15rem;
  background: color-mix(in srgb, var(--pc, var(--d-accent)) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--pc, var(--d-accent)) 34%, transparent);
  color: var(--pc, var(--d-accent));
}
.ngd .pillar h4 {
  font-family: var(--font-app-display, system-ui), sans-serif;
  font-size: 0.92rem; font-weight: 700; margin-bottom: 0.3rem; letter-spacing: -0.01em;
}
.ngd .pillar p { font-size: 0.76rem; color: var(--d-text2); line-height: 1.5; }

/* ─── The reveal ─────────────────────────────────────────────────────────── */
/* Where the three pillars stop being an argument and become the product. This
   is the first time the name is explained rather than merely displayed, so it
   is given the weight of an answer: mark, name, and the one-line claim, on the
   spot the pillars just vacated. */

.ngd .reveal {
  text-align: center;
  padding: 1.6rem 1.2rem 1.4rem;
  border-radius: 18px;
  border: 1px solid color-mix(in srgb, var(--d-accent) 30%, transparent);
  background:
    radial-gradient(ellipse 70% 100% at 50% 0%, color-mix(in srgb, var(--d-accent) 12%, transparent), transparent 70%),
    var(--d-surface);
}
.ngd .reveal img {
  width: 62px; height: 62px; object-fit: contain;
  margin: 0 auto 0.9rem; display: block;
  filter: drop-shadow(0 0 26px color-mix(in srgb, var(--d-accent) 55%, transparent));
}
.ngd .reveal-eyebrow {
  font-family: var(--font-app-mono, monospace);
  font-size: 0.54rem; letter-spacing: 0.34em; text-transform: uppercase;
  color: var(--d-text2); margin-bottom: 0.6rem;
}
.ngd .reveal-name {
  font-family: var(--font-app-display, system-ui), sans-serif;
  font-size: 1.9rem; font-weight: 800; letter-spacing: -0.035em; line-height: 1;
}
.ngd .reveal-sub {
  margin-top: 0.7rem;
  font-size: 0.9rem;
  color: var(--d-text2);
  max-width: 44ch;
  margin-left: auto; margin-right: auto;
}

/* The seam where three subjects become one pathway. */
.ngd .merge {
  margin-top: 0.85rem;
  border-radius: 14px;
  border: 1px solid color-mix(in srgb, var(--d-accent) 24%, transparent);
  background: linear-gradient(135deg,
    color-mix(in srgb, var(--d-accent) 8%, transparent),
    color-mix(in srgb, var(--d-a2) 7%, transparent));
  padding: 1rem 1.15rem;
  display: flex; align-items: center; gap: 1rem;
}
.ngd .merge .mtag {
  font-family: var(--font-app-mono, monospace);
  font-size: 0.52rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--d-accent); white-space: nowrap;
}
.ngd .merge .mline { flex: 1; height: 1px; background: linear-gradient(90deg, var(--d-accent), var(--d-a2)); opacity: 0.5; }
.ngd .merge .mout { font-family: var(--font-app-display, system-ui), sans-serif; font-size: 0.92rem; font-weight: 700; }

.ngd .deliver { display: flex; gap: 0.85rem; align-items: flex-start; padding: 0.8rem 0; border-bottom: 1px solid var(--d-line); }
.ngd .deliver:last-child { border-bottom: none; }
.ngd .deliver .dico {
  width: 32px; height: 32px; flex: none; border-radius: 10px;
  display: grid; place-items: center; font-size: 0.8rem;
  background: color-mix(in srgb, var(--dc, var(--d-accent)) 13%, transparent);
  color: var(--dc, var(--d-accent));
}
.ngd .deliver .t { font-size: 0.86rem; font-weight: 600; }
.ngd .deliver .s { font-size: 0.76rem; color: var(--d-text2); margin-top: 0.14rem; line-height: 1.5; }

/* ─── Sidebar ────────────────────────────────────────────────────────────── */
/* The real dashboards are sidebar apps, not tab bars. That is lucky for a
   tour: the nav IS the table of contents, so lighting one item at a time is
   both the chapter structure and an honest depiction of the product. */

.ngd .app-side {
  background: var(--d-surface);
  border-right: 1px solid var(--d-line);
  padding: 1.35rem 0.75rem 1rem;
  display: flex; flex-direction: column;
}

.ngd .app-brand {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0 0.7rem 1.15rem;
  margin-bottom: 0.9rem;
  border-bottom: 1px solid var(--d-line);
}
.ngd .app-brand img { width: 26px; height: 26px; object-fit: contain; }
.ngd .app-brand b {
  font-family: var(--font-app-display, system-ui), sans-serif;
  font-size: 0.9rem; letter-spacing: -0.01em; font-weight: 700;
}
.ngd .app-brand span {
  display: block;
  font-family: var(--font-app-mono, monospace);
  font-size: 0.5rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--d-text3); margin-top: 0.12rem;
}

.ngd .nav-item {
  display: flex; align-items: center; gap: 0.8rem;
  padding: 0.62rem 0.85rem;
  border-radius: 8px;
  color: var(--d-text2);
  font-size: 0.85rem; font-weight: 500;
  position: relative;
  border: 1px solid transparent;
}
.ngd .nav-item .ico { width: 17px; text-align: center; font-size: 0.82rem; color: var(--d-text3); flex: none; }
.ngd .nav-item.on { color: var(--d-accent); }
.ngd .nav-item.on .ico { color: var(--d-accent); }

/* A real element, not the ::before the shipped CSS uses, so the timeline can
   tween it. Same 3px bar, same glow. */
.ngd .nav-ind {
  /* Centered with a negative margin rather than translateY(-50%): the timeline
     tweens scaleY on this element, and GSAP owns the transform once it does —
     a CSS translate here would be overwritten on the first frame. */
  position: absolute; left: -0.75rem; top: 50%; margin-top: -10px;
  width: 3px; height: 20px; border-radius: 0 3px 3px 0;
  background: var(--d-accent);
  box-shadow: 0 0 10px var(--d-accent);
  opacity: 0;
}
.ngd .nav-item.on .nav-ind { opacity: 1; }
.ngd .nav-badge {
  margin-left: auto;
  font-family: var(--font-app-mono, monospace);
  font-size: 0.55rem;
  padding: 0.1rem 0.36rem; border-radius: 100px;
  background: color-mix(in srgb, var(--d-accent) 18%, transparent);
  color: var(--d-accent);
}
.ngd .nav-badge.pilot { background: rgba(251,191,36,.16); color: #fbbf24; }
.ngd .nav-sep { height: 1px; background: var(--d-line); margin: 0.6rem 0.5rem; }

/* ─── Main ───────────────────────────────────────────────────────────────── */

.ngd .app-main { padding: 1.5rem 1.75rem 2rem; min-width: 0; }

.ngd .app-top { display: flex; justify-content: space-between; align-items: flex-end; gap: 1rem; margin-bottom: 1.5rem; }
.ngd .app-title {
  font-family: var(--font-app-display, system-ui), sans-serif;
  font-size: 1.45rem; letter-spacing: -0.025em; font-weight: 700;
}
.ngd .app-title span { display: block; font-family: inherit; font-size: 0.82rem; font-weight: 400; color: var(--d-text2); margin-top: 0.2rem; letter-spacing: 0; }
.ngd .btn {
  font-size: 0.8rem; font-weight: 600;
  padding: 0.55rem 1rem; border-radius: 9px;
  border: 1px solid var(--d-line); background: var(--d-raised); color: var(--d-text);
  display: inline-flex; align-items: center; gap: 0.45rem;
}
.ngd .btn.primary { background: var(--d-accent); color: #06121a; border-color: transparent; font-weight: 700; }
.ngd.skin-admin .btn.primary { color: #04060a; }

/* Panels stack in one cell so the deck never changes height mid-scroll; a
   reflow inside a pinned frame reads as the page glitching. */
.ngd .panel-wrap { display: grid; }
.ngd .panel { grid-area: 1 / 1; }

.ngd .grid { display: grid; gap: 0.85rem; }
.ngd .g2 { grid-template-columns: repeat(2, 1fr); }
.ngd .g3 { grid-template-columns: repeat(3, 1fr); }
.ngd .g4 { grid-template-columns: repeat(4, 1fr); }

.ngd .card {
  background: var(--d-surface);
  border: 1px solid var(--d-line);
  border-radius: 14px;
  padding: 1.05rem 1.1rem;
}
.ngd.skin-student .card { backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); }
.ngd.skin-k5 .card { box-shadow: 0 4px 12px rgba(0,0,0,.07); border-radius: 16px; }
.ngd .card h3 {
  font-family: var(--font-app-display, system-ui), sans-serif;
  font-size: 0.92rem; font-weight: 700; letter-spacing: -0.01em; margin-bottom: 0.8rem;
}

.ngd .stat { font-family: var(--font-app-display, system-ui), sans-serif; font-size: 1.9rem; line-height: 1; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; font-weight: 700; margin-bottom: 0.3rem; }
.ngd .stat.c1 { color: var(--d-accent); }
.ngd .stat.c2 { color: var(--d-a2); }
.ngd .stat.c3 { color: var(--d-a3); }
.ngd .stat.c4 { color: var(--d-a4); }
.ngd .muted { color: var(--d-text2); font-size: 0.8rem; }
.ngd .tiny { font-family: var(--font-app-mono, monospace); font-size: 0.55rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--d-text3); }

/* ─── Rows, tables, pills ────────────────────────────────────────────────── */

.ngd .row { display: flex; justify-content: space-between; align-items: center; gap: 0.9rem; padding: 0.7rem 0; border-bottom: 1px solid var(--d-line); }
.ngd .row:last-child { border-bottom: none; }
.ngd .row .t { font-weight: 600; font-size: 0.86rem; }
.ngd .row .s { color: var(--d-text2); font-size: 0.76rem; margin-top: 0.12rem; }

.ngd table { width: 100%; border-collapse: collapse; font-size: 0.83rem; }
.ngd th { text-align: left; font-family: var(--font-app-mono, monospace); font-size: 0.54rem; letter-spacing: 0.13em; text-transform: uppercase; color: var(--d-text3); font-weight: 400; padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--d-line); }
.ngd td { padding: 0.62rem 0.6rem; border-bottom: 1px solid var(--d-line); }
.ngd tr:last-child td { border-bottom: none; }
.ngd .name { font-weight: 600; }

.ngd .pill { font-family: var(--font-app-mono, monospace); font-size: 0.54rem; letter-spacing: 0.1em; text-transform: uppercase; padding: 0.18rem 0.5rem; border-radius: 100px; white-space: nowrap; }
.ngd .pill.good { background: rgba(16,185,129,.15); color: #10b981; }
.ngd .pill.info { background: rgba(56,189,248,.14); color: #38bdf8; }
.ngd .pill.warn { background: rgba(251,191,36,.16); color: #fbbf24; }
.ngd .pill.bad  { background: rgba(244,114,182,.15); color: #f472b6; }

.ngd .bar { display: block; height: 5px; border-radius: 3px; background: rgba(255,255,255,0.08); overflow: hidden; min-width: 64px; }
.ngd.skin-k5 .bar { background: rgba(148,163,184,.2); height: 7px; }
.ngd .bar i { display: block; height: 100%; border-radius: 3px; background: linear-gradient(90deg, var(--d-accent), var(--d-a5)); }
.ngd .flag { color: var(--d-a3); font-size: 0.74rem; margin-top: 0.2rem; }

.ngd .mile { display: grid; grid-template-columns: 1fr auto auto auto; gap: 0.9rem; padding: 0.58rem 0; border-bottom: 1px solid var(--d-line); font-size: 0.81rem; align-items: center; }
.ngd .mile:last-child { border-bottom: none; }
.ngd .mile .lab { font-weight: 600; }
.ngd .mile .n { font-family: var(--font-app-mono, monospace); font-size: 0.7rem; color: var(--d-text2); text-align: right; min-width: 58px; }
.ngd .mile .n b { color: var(--d-a3); }
.ngd .mile.pending { opacity: 0.42; }

/* ─── Metric tiles ───────────────────────────────────────────────────────── */
/* The teacher and admin dashboards do not use bare numbers on a card — they
   use a tile with an accent edge, an icon, a sparkline and a delta. Copying
   that shape matters more than it sounds: a plain figure reads as a slide,
   and this deck has to pass for the product. Class names follow the app's
   (metric-tile, tile-icon, tile-value, tile-label, tile-chart). */

.ngd .metrics-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.8rem; }

.ngd .metric-tile {
  position: relative;
  border-radius: 14px;
  border: 1px solid var(--d-line);
  background: var(--d-surface);
  padding: 0.95rem 1rem;
  overflow: hidden;
}
.ngd .tile-accent {
  position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: var(--tc, var(--d-accent));
}
.ngd .tile-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.75rem; }
.ngd .tile-icon {
  width: 30px; height: 30px; border-radius: 9px;
  display: grid; place-items: center; font-size: 0.76rem;
  background: color-mix(in srgb, var(--tc, var(--d-accent)) 15%, transparent);
  color: var(--tc, var(--d-accent));
}
.ngd .tile-delta {
  font-family: var(--font-app-mono, monospace);
  font-size: 0.5rem; letter-spacing: 0.08em;
  padding: 0.16rem 0.4rem; border-radius: 100px;
  background: rgba(16, 185, 129, 0.14); color: #10b981;
}
.ngd .tile-delta.flat { background: rgba(255, 255, 255, 0.06); color: var(--d-text3); }
.ngd .tile-value {
  font-family: var(--font-app-display, system-ui), sans-serif;
  font-size: 1.7rem; font-weight: 700; letter-spacing: -0.032em; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.ngd .tile-label {
  font-family: var(--font-app-mono, monospace);
  font-size: 0.5rem; letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--d-text2); margin-top: 0.38rem;
}
/* A sparkline made of bars rather than a path — no measuring, no library,
   and it reads correctly at deck scale. */
.ngd .tile-chart { display: flex; align-items: flex-end; gap: 2px; height: 20px; margin-top: 0.7rem; }
.ngd .tile-chart i {
  flex: 1; border-radius: 1px;
  background: color-mix(in srgb, var(--tc, var(--d-accent)) 42%, transparent);
}

/* ─── Resource cards ─────────────────────────────────────────────────────── */

.ngd .resources-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.7rem; }
.ngd .resource-card {
  display: flex; gap: 0.75rem; align-items: flex-start;
  border-radius: 12px;
  border: 1px solid var(--d-line);
  background: var(--d-surface);
  padding: 0.85rem 0.9rem;
}
.ngd .rc-icon {
  width: 32px; height: 32px; flex: none; border-radius: 10px;
  display: grid; place-items: center; font-size: 0.8rem;
  background: color-mix(in srgb, var(--tc, var(--d-accent)) 14%, transparent);
  color: var(--tc, var(--d-accent));
}
.ngd .rc-t { font-size: 0.82rem; font-weight: 600; line-height: 1.3; }
.ngd .rc-s { font-size: 0.7rem; color: var(--d-text2); margin-top: 0.25rem; line-height: 1.5; }
.ngd .rc-tag {
  display: inline-block; margin-top: 0.45rem;
  font-family: var(--font-app-mono, monospace);
  font-size: 0.48rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--d-text3);
}

@media (max-width: 900px) {
  .ngd .metrics-grid { grid-template-columns: repeat(2, 1fr); }
  .ngd .resources-grid { grid-template-columns: 1fr; }
}

/* ─── Course cards ───────────────────────────────────────────────────────── */
/* The app presents courses as a thumbnail grid, not a list of rows. A list
   was quicker to build here and immediately read as a different product —
   which defeats the point, because this deck has to stand in for a live demo.

   The thumbnail is a gradient plate with a category glyph, sized at the exact
   16:9 the real artwork uses. Dropping the real images in later is a src swap
   with no layout shift. */

.ngd .course-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.8rem; }
.ngd .course-grid.g2up { grid-template-columns: repeat(2, 1fr); }

.ngd .course-card {
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid var(--d-line);
  background: var(--d-surface);
}

.ngd .course-thumb {
  position: relative;
  aspect-ratio: 16 / 9;
  display: grid;
  place-items: center;
  font-size: 1.6rem;
  background:
    radial-gradient(ellipse 80% 90% at 30% 15%, color-mix(in srgb, var(--tc, #21d8fc) 34%, transparent), transparent 70%),
    linear-gradient(140deg, color-mix(in srgb, var(--tc, #21d8fc) 16%, transparent), rgba(0, 0, 0, .35));
  border-bottom: 1px solid var(--d-line);
}
.ngd .course-thumb .cband {
  position: absolute; top: 0.45rem; left: 0.45rem;
  font-family: var(--font-app-mono, monospace);
  font-size: 0.48rem; letter-spacing: 0.1em; text-transform: uppercase;
  padding: 0.16rem 0.42rem; border-radius: 100px;
  background: rgba(0, 0, 0, 0.55); color: var(--d-text);
}
.ngd .course-thumb .clock {
  position: absolute; inset: 0; display: grid; place-items: center;
  background: rgba(4, 6, 14, 0.55); font-size: 0.9rem; color: var(--d-text3);
}

.ngd .course-body { padding: 0.65rem 0.75rem 0.75rem; }
.ngd .course-title { font-weight: 600; font-size: 0.8rem; line-height: 1.28; }
.ngd .course-meta {
  font-family: var(--font-app-mono, monospace);
  font-size: 0.58rem; letter-spacing: 0.06em;
  color: var(--d-text2); margin-top: 0.3rem;
}
.ngd .course-card .bar { margin-top: 0.5rem; min-width: 0; }

/* The empty state, because a teacher evaluating this will ask what a student
   sees before anything is assigned. */
.ngd .course-empty {
  grid-column: 1 / -1;
  border: 1px dashed var(--d-line);
  border-radius: 12px;
  padding: 1.1rem;
  text-align: center;
  color: var(--d-text2);
  font-size: 0.8rem;
}

/* ─── Skills surfaces ────────────────────────────────────────────────────── */
/* Modelled on the skills dashboard in BehindTheScenes/Mockup Examples:
   a row of stat pills, domain mastery bars, and a radar against class average. */

.ngd .pill-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.6rem; }
.ngd .stat-pill {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.65rem 0.75rem;
  border-radius: 11px;
  border: 1px solid var(--d-line);
  background: var(--d-surface);
}
.ngd .sp-icon {
  width: 30px; height: 30px; flex: none;
  border-radius: 9px; display: grid; place-items: center; font-size: 0.75rem;
  background: color-mix(in srgb, var(--pc, var(--d-accent)) 15%, transparent);
  color: var(--pc, var(--d-accent));
}
.ngd .sp-val {
  font-family: var(--font-app-display, system-ui), sans-serif;
  font-size: 1.1rem; font-weight: 700; line-height: 1; letter-spacing: -0.02em;
}
.ngd .sp-label {
  font-family: var(--font-app-mono, monospace);
  font-size: 0.5rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--d-text2); margin-top: 0.22rem;
}

.ngd .radar-wrap { display: grid; grid-template-columns: 210px 1fr; gap: 1rem; align-items: center; }
.ngd .radar { width: 100%; display: block; }
.ngd .radar .web { fill: none; stroke: var(--d-line); stroke-width: 1; }
.ngd .radar .spoke { stroke: var(--d-line); stroke-width: 1; }
.ngd .radar .avg { fill: none; stroke: var(--d-text3); stroke-width: 1.2; stroke-dasharray: 4 4; }
.ngd .radar .you {
  fill: color-mix(in srgb, var(--d-accent) 24%, transparent);
  stroke: var(--d-accent); stroke-width: 1.8;
}
.ngd .radar text {
  font-family: var(--font-app-mono, monospace);
  font-size: 7px; letter-spacing: 0.08em; fill: var(--d-text3); text-transform: uppercase;
}

.ngd .legend { display: flex; gap: 1rem; margin-top: 0.7rem; }
.ngd .legend span { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.72rem; color: var(--d-text2); }
.ngd .legend i { width: 14px; height: 2px; border-radius: 2px; background: var(--d-accent); }
.ngd .legend i.dash { background: repeating-linear-gradient(90deg, var(--d-text3) 0 4px, transparent 4px 8px); }

/* ─── Class wall ─────────────────────────────────────────────────────────── */

.ngd .wall { display: grid; gap: 0.55rem; }
.ngd .wall-post {
  display: flex; align-items: flex-start; gap: 0.7rem;
  padding: 0.7rem 0.8rem;
  border-radius: 11px;
  border: 1px solid var(--d-line);
  background: var(--d-surface);
}
.ngd .wall-av {
  width: 32px; height: 32px; flex: none;
  border-radius: 50%; display: grid; place-items: center;
  font-family: var(--font-app-display, system-ui), sans-serif;
  font-weight: 700; font-size: 0.7rem; color: #06121a;
  background: linear-gradient(140deg, var(--ac, #21d8fc), color-mix(in srgb, var(--ac, #21d8fc) 40%, #8b5cf6));
}
.ngd .wall-body { flex: 1; min-width: 0; }
.ngd .wall-t { font-size: 0.82rem; font-weight: 600; }
.ngd .wall-t b { font-weight: 700; }
.ngd .wall-s { font-family: var(--font-app-mono, monospace); font-size: 0.56rem; letter-spacing: 0.1em; color: var(--d-text3); margin-top: 0.22rem; text-transform: uppercase; }
.ngd .wall-react {
  display: inline-flex; align-items: center; gap: 0.3rem;
  padding: 0.2rem 0.5rem; border-radius: 100px;
  background: rgba(255, 255, 255, 0.05);
  font-size: 0.66rem; color: var(--d-text2);
}

@media (max-width: 900px) {
  .ngd .course-grid, .ngd .course-grid.g2up { grid-template-columns: 1fr; }
  .ngd .pill-row { grid-template-columns: repeat(2, 1fr); }
  .ngd .radar-wrap { grid-template-columns: 1fr; }
}

/* ─── Student surfaces ───────────────────────────────────────────────────── */

.ngd .hero-xp { display: flex; align-items: center; gap: 1.1rem; }
.ngd .ring {
  width: 70px; height: 70px; flex: none; border-radius: 50%;
  display: grid; place-items: center; position: relative;
  font-family: var(--font-app-display, system-ui), sans-serif; font-weight: 700;
  background: conic-gradient(var(--d-accent) 0.75turn, rgba(255,255,255,.07) 0.75turn);
}
.ngd .ring::after { content: ''; position: absolute; inset: 6px; border-radius: 50%; background: var(--d-bg); }
.ngd .ring span { position: relative; z-index: 1; text-align: center; line-height: 1.05; }
.ngd .ring small { display: block; font-size: 0.5rem; opacity: 0.6; font-family: var(--font-app-mono, monospace); }

.ngd .badge { border-radius: 13px; border: 1px solid var(--d-line); background: var(--d-surface); padding: 0.85rem 0.6rem; text-align: center; }
.ngd .badge .medal { width: 40px; height: 40px; margin: 0 auto 0.5rem; border-radius: 12px; display: grid; place-items: center; font-size: 1.05rem; background: color-mix(in srgb, var(--d-a3) 18%, transparent); border: 1px solid color-mix(in srgb, var(--d-a3) 32%, transparent); }
.ngd .badge .bname { font-size: 0.73rem; font-weight: 600; line-height: 1.25; }
.ngd .badge .brar { font-family: var(--font-app-mono, monospace); font-size: 0.47rem; letter-spacing: 0.13em; text-transform: uppercase; color: var(--d-text3); margin-top: 0.25rem; }
.ngd .badge.rare { border-color: color-mix(in srgb, var(--d-a3) 42%, transparent); }
.ngd .badge.rare .brar { color: var(--d-a3); }
.ngd .badge.locked { opacity: 0.28; }

.ngd .feed { display: flex; align-items: flex-start; gap: 0.7rem; padding: 0.6rem 0; border-bottom: 1px solid var(--d-line); }
.ngd .feed:last-child { border-bottom: none; }
.ngd .feed .ico2 { width: 26px; height: 26px; flex: none; border-radius: 8px; display: grid; place-items: center; font-size: 0.75rem; background: rgba(255,255,255,0.05); }
.ngd.skin-k5 .feed .ico2 { background: rgba(6,182,212,.1); }
.ngd .feed .t { font-size: 0.81rem; font-weight: 600; }
.ngd .feed .s { font-size: 0.71rem; color: var(--d-text2); margin-top: 0.08rem; }

/* ─── The lab ────────────────────────────────────────────────────────────── */

.ngd .lab-shell { display: grid; grid-template-columns: 1.05fr 0.95fr; gap: 0.9rem; align-items: start; }

/* The five lab states share one box.
   Without this the stack was only as tall as whichever state happened to be
   tallest, and every shorter one floated in the middle of it while the frame
   around them resized between beats. Fixing the height means the terminal
   stays exactly where it was as the student's attempts progress, which is the
   whole point — you are meant to be watching one screen change, not five
   screens take turns. */
.ngd .lab-shell .panel-wrap {
  align-content: start;
  min-height: 268px;
}
.ngd .lab-shell .panel { align-self: start; }
.ngd .layer { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0.8rem; border-radius: 9px; margin-bottom: 0.32rem; border: 1px solid var(--d-line); background: var(--d-surface); }
.ngd .layer:last-child { margin-bottom: 0; }
.ngd .layer .n2 { font-family: var(--font-app-mono, monospace); font-size: 0.58rem; color: var(--d-text3); width: 1rem; }
.ngd .layer .lname { font-weight: 600; font-size: 0.82rem; flex: 1; }
.ngd .layer .lstate { font-family: var(--font-app-mono, monospace); font-size: 0.52rem; letter-spacing: 0.11em; text-transform: uppercase; color: var(--d-text3); }
.ngd .layer.done { border-color: rgba(16,185,129,.28); background: rgba(16,185,129,.06); }
.ngd .layer.done .lstate { color: #10b981; }
.ngd .layer.here { border-color: color-mix(in srgb, var(--d-a3) 45%, transparent); background: color-mix(in srgb, var(--d-a3) 8%, transparent); }
.ngd .layer.here .lstate { color: var(--d-a3); }
.ngd .layer.locked { opacity: 0.4; }

/* ─── The packet ─────────────────────────────────────────────────────────
   A dot that climbs the layer stack, stalls at the one the student is stuck
   on, and finally gets through.

   This is the only animation in the tour that is also the lesson. A viewer who
   watches it learns why the fault is at layer 3 — the packet crosses Physical
   and Data Link without trouble and dies at Network, twice, until the routing
   table is fixed. That is a better argument for "labs teach by doing" than any
   sentence next to it. */

.ngd .lab-layers { position: relative; padding-left: 28px; }

.ngd .lab-rail {
  position: absolute;
  left: 9px; top: 4px; bottom: 4px;
  width: 2px;
  border-radius: 2px;
  background: linear-gradient(180deg,
    rgba(255,255,255,.04), rgba(255,255,255,.16) 35%, rgba(255,255,255,.16) 65%, rgba(255,255,255,.04));
}

.ngd .lab-packet {
  position: absolute;
  left: 4px; top: 0;
  width: 12px; height: 12px;
  border-radius: 50%;
  background: var(--d-accent);
  box-shadow: 0 0 14px var(--d-accent);
  opacity: 0;
  z-index: 2;
}

/* A short trail, so direction of travel is readable at a glance. */
.ngd .lab-packet::after {
  content: '';
  position: absolute;
  left: 4px; bottom: 100%;
  width: 4px; height: 26px;
  border-radius: 4px;
  background: linear-gradient(180deg, transparent, currentColor);
  color: inherit;
  opacity: 0.5;
}

.ngd .objective { border-radius: 11px; border: 1px solid color-mix(in srgb, var(--d-accent) 24%, transparent); background: color-mix(in srgb, var(--d-accent) 6%, transparent); padding: 0.85rem; margin-bottom: 0.7rem; }
.ngd .objective .lbl { font-family: var(--font-app-mono, monospace); font-size: 0.52rem; letter-spacing: 0.15em; text-transform: uppercase; color: var(--d-accent); margin-bottom: 0.42rem; }
.ngd .objective p { font-size: 0.81rem; color: var(--d-text2); }
.ngd .choice { display: flex; align-items: center; gap: 0.55rem; padding: 0.5rem 0.7rem; border-radius: 8px; border: 1px solid var(--d-line); font-size: 0.78rem; margin-bottom: 0.36rem; }
.ngd .choice.right { border-color: rgba(16,185,129,.45); background: rgba(16,185,129,.08); color: #10b981; }
.ngd .choice.wrong { opacity: 0.45; text-decoration: line-through; }

/* ═══════════════════════════════════════════════════════════════════════════
   CHAPTER NAVIGATION

   A guided experience that can only be consumed in order is not guided, it is
   a queue. Someone who came to see teacher visibility should not have to
   scroll through a networking lab to reach it.

   A rail of stops on the left edge: current one lit, all of them reachable.
   Collapsed to dots by default so it never competes with the deck, expanding
   to labels on hover or keyboard focus.

   Everything here is a real <button> in a <nav>, so it is tabbable and
   announced. The rail is also the progress indicator, which is why there is
   no separate one.
   ═══════════════════════════════════════════════════════════════════════════ */

.tour-nav {
  position: fixed;
  left: 1.1rem;
  top: 50%;
  /* Centered with a margin rather than translateY(-50%). A transform on a
     fixed element makes it a containing block for its own descendants and,
     more importantly here, some browsers drop it out of the viewport-fixed
     layer entirely once a pinned ancestor starts compositing — which is
     exactly the "it does not stay on screen" symptom. */
  margin-top: -160px;
  z-index: 9500;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.7rem 0.6rem;
  border-radius: 18px;
  /* Opaque enough to read against a light K-5 deck as well as a dark one. */
  background: rgba(6, 6, 11, 0.86);
  border: 1px solid rgba(255, 255, 255, 0.09);
  box-shadow: 0 20px 50px -22px rgba(0, 0, 0, 0.95);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  transition: background 0.3s, border-color 0.3s;
}

.tour-nav:hover,
.tour-nav:focus-within {
  background: rgba(6, 6, 11, 0.95);
  border-color: rgba(33, 216, 252, 0.28);
}

.tn-stop {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.32rem 0.4rem;
  background: none;
  border: none;
  cursor: pointer;
  font-family: inherit;
  /* Was --text3 at 24% — legible in isolation, invisible in practice. */
  color: rgba(237, 237, 242, 0.55);
  border-radius: 8px;
  transition: color 0.22s, background 0.22s;
}

.tn-stop:hover { color: var(--text, #ededf2); background: rgba(255,255,255,.05); }
.tn-stop:focus-visible { outline: 2px solid var(--cyan, #21d8fc); outline-offset: 1px; }

/* The dot. Grows into a bar on the active stop, so position in the sequence
   is legible at a glance without reading anything. */
.tn-dot {
  flex: none;
  width: 7px; height: 7px;
  border-radius: 100px;
  background: currentColor;
  opacity: 0.5;
  transition: height 0.3s cubic-bezier(.22,1,.36,1), opacity 0.22s, background 0.22s;
}

.tn-label {
  font-family: var(--mono, monospace);
  font-size: 0.52rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
  max-width: 0;
  overflow: hidden;
  opacity: 0;
  transition: max-width 0.34s cubic-bezier(.22,1,.36,1), opacity 0.26s;
}

.tour-nav:hover .tn-label,
.tour-nav:focus-within .tn-label { max-width: 190px; opacity: 1; }

.tn-stop.is-current { color: var(--cyan, #21d8fc); }
.tn-stop.is-current .tn-dot { height: 20px; opacity: 1; box-shadow: 0 0 12px currentColor; }
.tn-stop.is-done .tn-dot { opacity: 0.75; }

/* Skip. Appears only while a pinned sequence is actually holding the page,
   because that is the only time there is anything to skip. */
.tn-skip {
  position: fixed;
  left: 1.1rem;
  bottom: 1.4rem;
  z-index: 880;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.55rem 0.9rem;
  border-radius: 100px;
  background: rgba(8, 8, 14, 0.82);
  border: 1px solid var(--glass-border, rgba(255,255,255,.06));
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  color: var(--text2, rgba(237,237,242,.52));
  font-family: var(--body, sans-serif);
  font-size: 0.76rem;
  font-weight: 600;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transform: translateY(8px);
  transition: opacity 0.3s, transform 0.3s, color 0.2s, border-color 0.2s;
}

.tn-skip.on { opacity: 1; pointer-events: auto; transform: none; }
.tn-skip:hover { color: var(--text, #ededf2); border-color: rgba(33,216,252,.4); }
.tn-skip:focus-visible { outline: 2px solid var(--cyan, #21d8fc); outline-offset: 2px; }
.tn-skip svg { width: 12px; height: 12px; stroke: currentColor; fill: none; stroke-width: 2; }

/* On a phone the rail would sit on top of the content. A bottom bar instead:
   a thin progress line plus the current chapter name, tappable to open the
   full list. */
@media (max-width: 900px) {
  .tour-nav {
    left: 0; right: 0; top: auto; bottom: 0;
    transform: none;
    flex-direction: row;
    justify-content: center;
    gap: 0.35rem;
    border-radius: 0;
    padding: 0.7rem 0.9rem;
    border-top: 1px solid var(--glass-border, rgba(255,255,255,.06));
    background: rgba(8, 8, 14, 0.92);
    overflow-x: auto;
  }
  .tn-label { display: none; }
  .tn-dot { width: 9px; height: 9px; }
  .tn-stop.is-current .tn-dot { height: 9px; width: 26px; }
  .tn-skip { bottom: 4.2rem; left: auto; right: 1rem; }
}

@media (prefers-reduced-motion: reduce) {
  .tour-nav, .tn-stop, .tn-dot, .tn-label, .tn-skip { transition: none; }
}

/* ─── Sound control ──────────────────────────────────────────────────────── */
/* Bottom-right: clear of the injected site header at the top, clear of the
   exit control, and diagonally opposite the skip button so the four corners
   are never fighting. Always visible — a sound that is playing must always
   have an obvious way to stop it. */

.tour-sound {
  position: fixed;
  right: 1.2rem;
  bottom: 1.4rem;
  z-index: 9500;
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.55rem 0.95rem;
  border-radius: 100px;
  background: rgba(6, 6, 11, 0.86);
  border: 1px solid rgba(255, 255, 255, 0.09);
  box-shadow: 0 18px 40px -20px rgba(0, 0, 0, 0.95);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  color: rgba(237, 237, 242, 0.55);
  font-family: var(--body, sans-serif);
  font-size: 0.74rem;
  font-weight: 600;
  cursor: pointer;
  transition: color 0.22s, border-color 0.22s, background 0.22s;
}

.tour-sound:hover { color: var(--text, #ededf2); border-color: rgba(33, 216, 252, 0.35); }
.tour-sound:focus-visible { outline: 2px solid var(--cyan, #21d8fc); outline-offset: 2px; }
.tour-sound.on { color: var(--cyan, #21d8fc); border-color: rgba(33, 216, 252, 0.35); }

/* Wanted, but the browser has not permitted playback yet — which on a direct
   visit is every moment before the first click. Amber and gently pulsing, so
   it reads as "press me" rather than as "playing". */
.tour-sound.pending {
  color: var(--amber, #f0b429);
  border-color: rgba(240, 180, 41, 0.4);
  animation: tsPending 2.4s ease-in-out infinite;
}
@keyframes tsPending {
  0%, 100% { border-color: rgba(240, 180, 41, 0.28); }
  50%      { border-color: rgba(240, 180, 41, 0.7); }
}
.tour-sound.pending .ts-bars i { height: 4px; }
@media (prefers-reduced-motion: reduce) { .tour-sound.pending { animation: none; } }

/* Four bars. Flat when off, moving when on — the state is legible without
   reading the label, which matters for a control someone reaches for in a
   hurry because sound started in a quiet room. */
.ts-bars { display: flex; align-items: flex-end; gap: 2px; height: 12px; }
.ts-bars i {
  width: 2px;
  height: 3px;
  border-radius: 1px;
  background: currentColor;
  transition: height 0.25s;
}
.tour-sound.on .ts-bars i { animation: tsBar 1.05s ease-in-out infinite; }
.tour-sound.on .ts-bars i:nth-child(1) { animation-delay: -0.5s; }
.tour-sound.on .ts-bars i:nth-child(2) { animation-delay: -0.2s; }
.tour-sound.on .ts-bars i:nth-child(3) { animation-delay: -0.75s; }
.tour-sound.on .ts-bars i:nth-child(4) { animation-delay: -0.35s; }

@keyframes tsBar {
  0%, 100% { height: 3px; }
  50%      { height: 12px; }
}

/* Introduced once, at the end of the opening sequence.
   Someone about to have audio start should be shown where the off switch is
   before they need it, not left hunting for it while a room turns to look.
   It draws attention to itself for four seconds and then never again. */
body.hint-sound .tour-sound {
  animation: tsHint 1.5s cubic-bezier(.22,1,.36,1) 2;
  border-color: rgba(33, 216, 252, 0.6);
  color: var(--cyan, #21d8fc);
}

@keyframes tsHint {
  0%, 100% { box-shadow: 0 18px 40px -20px rgba(0,0,0,.95), 0 0 0 0 rgba(33,216,252,.5); }
  40%      { box-shadow: 0 18px 40px -20px rgba(0,0,0,.95), 0 0 0 12px rgba(33,216,252,0); }
}

/* A one-off label, so the pulse says what it is rather than just blinking. */
body.hint-sound .tour-sound::after {
  content: 'Sound is optional';
  position: absolute;
  right: calc(100% + 0.7rem);
  white-space: nowrap;
  font-family: var(--mono, monospace);
  font-size: 0.5rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--cyan, #21d8fc);
  opacity: 0;
  animation: tsHintLabel 4.2s ease forwards;
}

@keyframes tsHintLabel {
  0%   { opacity: 0; transform: translateX(6px); }
  12%  { opacity: 1; transform: none; }
  78%  { opacity: 1; }
  100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .tour-sound.on .ts-bars i { animation: none; height: 8px; }
  .tour-sound, .ts-bars i { transition: none; }
  body.hint-sound .tour-sound { animation: none; }
  body.hint-sound .tour-sound::after { animation: none; opacity: 1; }
}

/* Above the mobile chapter bar. */
@media (max-width: 900px) {
  .tour-sound { bottom: 4.2rem; }
  .ts-label { display: none; }
}

/* ─── Signal field ───────────────────────────────────────────────────────── */
/* The opening. Faint points of light resolving out of the dark, so the first
   screen reads as entering an environment rather than loading a page. Pure
   decoration: it sits behind everything, ignores pointer events, and is the
   first thing dropped under reduced motion. */

.signal-field { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.signal-field i {
  position: absolute;
  width: 3px; height: 3px;
  border-radius: 50%;
  background: var(--cyan, #21d8fc);
  opacity: 0;
  animation: sigFloat var(--dur, 9s) ease-in-out infinite;
  animation-delay: var(--del, 0s);
}
.signal-field i:nth-child(even) { background: var(--violet, #8b5cf6); }
.signal-field i:nth-child(3n) { background: var(--lime, #10b981); width: 2px; height: 2px; }

@keyframes sigFloat {
  0%   { opacity: 0; transform: translateY(14px) scale(0.6); }
  22%  { opacity: 0.55; }
  55%  { opacity: 0.28; transform: translateY(-10px) scale(1); }
  100% { opacity: 0; transform: translateY(-26px) scale(0.7); }
}

/* Thin connecting lines, so the points read as a system rather than dust. */
.signal-field b {
  position: absolute;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(33,216,252,.22), transparent);
  opacity: 0;
  animation: sigLine var(--dur, 11s) ease-in-out infinite;
  animation-delay: var(--del, 0s);
}
@keyframes sigLine {
  0%, 100% { opacity: 0; transform: scaleX(0.3); }
  40%, 60% { opacity: 1; transform: scaleX(1); }
}

@media (prefers-reduced-motion: reduce) {
  .signal-field { display: none; }
}

/* ─── Getting started ────────────────────────────────────────────────────── */
/* Onboarding gets its own moment rather than being buried in the admin
   dashboard. It is the objection that kills deals — "we do not have time to
   roll this out" — so it is answered in the open. Not pinned: five short steps
   read faster as a list than as five held beats. */

.start-steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 0; margin-top: 2.6rem; }
.start-step {
  position: relative;
  padding: 1.5rem 1.3rem;
  border: 1px solid var(--glass-border);
  background: var(--glass);
}
.start-step:first-child { border-radius: 16px 0 0 16px; }
.start-step:last-child { border-radius: 0 16px 16px 0; }
.start-step + .start-step { margin-left: -1px; }
.start-step .sn {
  font-family: var(--mono);
  font-size: 0.62rem; letter-spacing: 0.16em;
  color: var(--cyan); margin-bottom: 0.8rem;
}
.start-step h4 { font-family: var(--heading); font-size: 1rem; letter-spacing: -0.015em; margin-bottom: 0.4rem; }
.start-step p { font-size: 0.84rem; color: var(--text2); line-height: 1.55; }

/* The connecting thread. Reads as a sequence rather than five separate cards. */
.start-step::after {
  content: '';
  position: absolute; top: 2.15rem; right: -5px;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--cyan); opacity: 0.5; z-index: 2;
}
.start-step:last-child::after { display: none; }

@media (max-width: 820px) {
  .start-steps { grid-template-columns: 1fr; }
  .start-step { border-radius: 0 !important; margin-left: 0; }
  .start-step + .start-step { margin-top: -1px; }
  .start-step:first-child { border-radius: 16px 16px 0 0 !important; }
  .start-step:last-child { border-radius: 0 0 16px 16px !important; }
  .start-step::after { display: none; }
}

/* ─── Catalog facts ──────────────────────────────────────────────────────── */

.grid-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1rem;
  margin-top: 2.6rem;
}

.fact {
  padding: 1.5rem 1.4rem;
  border-radius: 16px;
  border: 1px solid var(--glass-border);
  background: var(--glass);
}
.fact-n {
  font-family: var(--heading);
  font-size: clamp(2.6rem, 5vw, 3.8rem);
  line-height: 1;
  letter-spacing: -0.045em;
  font-variant-numeric: tabular-nums;
  background: linear-gradient(140deg, var(--cyan), var(--violet));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}
/* Solid fill on the unit, for the same reason the evidence figures need it:
   a descendant of a background-clip:text element has no background to clip. */
.fact-n em {
  font-style: normal;
  font-size: 0.5em;
  -webkit-text-fill-color: var(--cyan);
  color: var(--cyan);
}
.fact-l {
  margin-top: 0.7rem;
  font-family: var(--heading);
  font-size: 1rem;
  letter-spacing: -0.015em;
}
.fact p { margin-top: 0.4rem; color: var(--text2); font-size: 0.86rem; line-height: 1.55; }

.frameworks {
  margin-top: 2rem;
  padding: 1.4rem 1.5rem;
  border-radius: 16px;
  border: 1px solid rgba(33, 216, 252, 0.2);
  background: linear-gradient(135deg, rgba(33, 216, 252, 0.05), rgba(139, 92, 246, 0.04));
}
.fw-label {
  font-family: var(--mono);
  font-size: 0.55rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--cyan);
  margin-bottom: 1rem;
}
.fw-list { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.fw {
  display: inline-flex; align-items: center; gap: 0.5rem;
  padding: 0.5rem 0.9rem; border-radius: 100px;
  border: 1px solid var(--glass-border); background: rgba(255, 255, 255, 0.03);
  font-size: 0.84rem; color: var(--text2);
}
.fw i { font-size: 0.7rem; color: var(--lime); }

/* ─── Lab interior ───────────────────────────────────────────────────────── */
/* The lab is the product. It gets a real frame rather than a card, because the
   moment the tour enters it the visitor should feel they have gone inside
   something rather than scrolled to the next panel. */

.ngd .lab-frame {
  border-radius: 14px;
  border: 1px solid color-mix(in srgb, var(--d-accent) 26%, transparent);
  background: linear-gradient(180deg, color-mix(in srgb, var(--d-accent) 5%, transparent), transparent 55%), var(--d-surface);
  overflow: hidden;
}
.ngd .lab-bar {
  display: flex; align-items: center; gap: 0.7rem;
  padding: 0.6rem 0.9rem;
  border-bottom: 1px solid var(--d-line);
  font-family: var(--font-app-mono, monospace);
  font-size: 0.55rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--d-text3);
}
.ngd .lab-bar .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--d-accent); box-shadow: 0 0 8px var(--d-accent); }
.ngd .lab-bar .sp { flex: 1; }
.ngd .lab-body { padding: 1rem; }

/* Terminal output. Monospace, because a 9-12 lab genuinely puts a student in
   front of one and the tour should not soften that. */
.ngd .term {
  font-family: var(--font-app-mono, monospace);
  font-size: 0.7rem; line-height: 1.85;
  background: rgba(0,0,0,.35);
  border: 1px solid var(--d-line);
  border-radius: 9px;
  padding: 0.7rem 0.85rem;
}
.ngd .term div { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ngd .term .pr { color: var(--d-accent); }
.ngd .term .ok { color: #10b981; }
.ngd .term .no { color: #ef4444; }
.ngd .term .dim2 { color: var(--d-text3); }

.ngd .hint {
  display: flex; gap: 0.65rem; align-items: flex-start;
  border-radius: 10px;
  border: 1px solid color-mix(in srgb, var(--d-a3) 34%, transparent);
  background: color-mix(in srgb, var(--d-a3) 8%, transparent);
  padding: 0.7rem 0.85rem;
  font-size: 0.79rem; line-height: 1.5;
}
.ngd .hint i { color: var(--d-a3); margin-top: 0.15rem; }

.ngd .solved {
  display: flex; align-items: center; gap: 0.8rem;
  border-radius: 11px;
  border: 1px solid rgba(16,185,129,.4);
  background: rgba(16,185,129,.08);
  padding: 0.85rem 1rem;
}
.ngd .solved .big { font-family: var(--font-app-display, system-ui), sans-serif; font-size: 1.1rem; font-weight: 700; color: #10b981; }

/* Reward chips that fly out on completion. */
.ngd .reward { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.7rem; }
.ngd .reward span {
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.4rem 0.7rem; border-radius: 100px;
  border: 1px solid var(--d-line); background: var(--d-raised);
  font-size: 0.76rem; font-weight: 600;
}

/* ─── Grade-band characters ──────────────────────────────────────────────── */
/* The three character designs, one per band. They carry the "grows up with
   them" argument faster than any copy can: an eight-year-old and a sixteen-
   year-old should not be looking at the same mascot.

   Used in exactly one place in the whole experience. The combined
   grade-banded.png shows the same three designs together, so using both would
   be the same artwork twice. */

.ngd .charwrap { display: grid; grid-template-columns: 250px 1fr; gap: 1.3rem; align-items: center; }
.ngd .char { position: relative; }

/* The three characters are not the same shape — one is square, two are 2:3
   portrait — so the box is fixed and the artwork is fitted inside it rather
   than the box taking the artwork's shape. object-fit:contain keeps every
   one of them whole and correctly proportioned; height caps the tall ones so
   a portrait figure cannot push the panel past the frame. */
.ngd .char img {
  display: block;
  width: 100%;
  max-height: 340px;
  object-fit: contain;
  object-position: center bottom;
  filter: drop-shadow(0 22px 40px rgba(0,0,0,.55));
}
.ngd.skin-k5 .char img { filter: drop-shadow(0 18px 34px rgba(30,41,59,.28)); }
.ngd .char figcaption {
  margin-top: 0.5rem; text-align: center;
  font-family: var(--font-app-mono, monospace);
  font-size: 0.5rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--d-text3);
}
/* A ground for the figure to stand on, so it is not floating in the panel. */
.ngd .char::after {
  content: '';
  position: absolute; left: 12%; right: 12%; bottom: 1.4rem; height: 22px;
  border-radius: 50%;
  background: radial-gradient(ellipse 50% 100% at 50% 50%, color-mix(in srgb, var(--cc, #21d8fc) 26%, transparent), transparent 72%);
  filter: blur(9px);
  z-index: -1;
}

@media (max-width: 900px) {
  .ngd .charwrap { grid-template-columns: 1fr; }
  .ngd .char img { max-width: 220px; margin: 0 auto; }
}

/* ─── Compliance artwork ─────────────────────────────────────────────────── */

/* The compliance artwork is 2:3 portrait, so it gets a narrow column and a
   height cap rather than a wide one that would make it tower over the text
   beside it. */
.ngd .compliance { display: grid; grid-template-columns: 150px 1fr; gap: 1.2rem; align-items: center; }
.ngd .compliance img {
  display: block;
  width: 100%;
  max-height: 210px;
  object-fit: contain;
  border-radius: 10px;
}
@media (max-width: 900px) { .ngd .compliance { grid-template-columns: 1fr; } }

/* ─── Topic chips ────────────────────────────────────────────────────────── */
/* The things a student is already dealing with, before any of it is taught.
   Scattered on purpose: the argument is that they arrive unconnected. */

.ngd .topics { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.ngd .topic {
  display: inline-flex; align-items: center; gap: 0.45rem;
  padding: 0.5rem 0.85rem; border-radius: 100px;
  border: 1px solid var(--d-line); background: var(--d-surface);
  font-size: 0.8rem; color: var(--d-text2);
}
.ngd .topic i { font-size: 0.72rem; color: var(--tc, var(--d-accent)); }
.ngd .topic.hot { border-color: color-mix(in srgb, var(--tc, var(--d-accent)) 40%, transparent); color: var(--d-text); }

/* ─── Learning signals ───────────────────────────────────────────────────── */
/* The four numbers the lab produced. They enter from the side the lab was on,
   so the visitor sees the data arrive rather than the panel change. */

.ngd .signals { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.6rem; }
.ngd .signal {
  border-radius: 12px;
  border: 1px solid color-mix(in srgb, var(--d-a3) 30%, transparent);
  background: color-mix(in srgb, var(--d-a3) 7%, transparent);
  padding: 0.8rem 0.7rem;
  text-align: center;
}
.ngd .signal .sv {
  font-family: var(--font-app-display, system-ui), sans-serif;
  font-size: 1.35rem; font-weight: 700; letter-spacing: -0.025em;
  color: var(--d-a3); line-height: 1;
}
.ngd .signal .sl {
  font-family: var(--font-app-mono, monospace);
  font-size: 0.5rem; letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--d-text2); margin-top: 0.35rem;
}

/* ─── What is held ───────────────────────────────────────────────────────── */

.ngd .held { display: flex; align-items: center; gap: 0.6rem; padding: 0.5rem 0; font-size: 0.83rem; }
.ngd .held i { color: #10b981; font-size: 0.75rem; }
.ngd .nope { display: flex; align-items: center; gap: 0.6rem; padding: 0.5rem 0; font-size: 0.83rem; color: var(--d-text3); }
.ngd .nope i { color: #ef4444; font-size: 0.75rem; }
.ngd .nope span { text-decoration: line-through; }

/* ─── Curriculum bands ───────────────────────────────────────────────────── */

.ngd .band { border-radius: 13px; border: 1px solid var(--d-line); background: var(--d-surface); padding: 0.95rem; }
.ngd .band-tag { font-family: var(--font-app-mono, monospace); font-size: 0.52rem; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 0.6rem; }
.ngd .band-k .band-tag { color: #22c55e; } .ngd .band-k { border-color: rgba(34,197,94,.3); }
.ngd .band-m .band-tag { color: #38bdf8; } .ngd .band-m { border-color: rgba(56,189,248,.3); }
.ngd .band-h .band-tag { color: #a78bfa; } .ngd .band-h { border-color: rgba(167,139,250,.32); }
.ngd .band h4 { font-family: var(--font-app-display, system-ui), sans-serif; font-size: 0.88rem; font-weight: 700; margin-bottom: 0.45rem; }
.ngd .band ul { list-style: none; margin: 0; padding: 0; }
.ngd .band li { font-size: 0.74rem; color: var(--d-text2); padding: 0.24rem 0 0.24rem 0.8rem; position: relative; }
.ngd .band li::before { content: ''; position: absolute; left: 0; top: 0.72rem; width: 4px; height: 4px; border-radius: 50%; background: currentColor; opacity: 0.5; }

/* ─── Small screens ──────────────────────────────────────────────────────── */
/* Below 900px nothing is pinned and nothing is scrubbed, so the deck must be a
   good static image on its own: square on, unscaled, captions above it in
   document order. The sidebar collapses because a 248px rail beside a phone
   viewport leaves nothing for the content. */

@media (max-width: 900px) {
  .tour-stage { grid-template-columns: 1fr; min-height: 0; gap: 1.6rem; padding-top: 2.5rem; padding-bottom: 2.5rem; }
  .deck-viewport { perspective: none; }
  .deck { transform: none; }
  .deck::after, .deck-sweep, .dl { display: none; }
  .deck-fit { aspect-ratio: auto; overflow: visible; }
  .deck-ui { width: 100%; transform: none; }
  .tour-caps { display: block; }
  .cap { margin-bottom: 2.25rem; }
  .cap:last-child { margin-bottom: 0; }
  .tour-rail { display: none; }

  .ngd { grid-template-columns: 1fr; min-height: 0; }
  .ngd .app-side { display: none; }
  .ngd .app-main { padding: 1rem; }
  .ngd .g2, .ngd .g3, .ngd .g4 { grid-template-columns: 1fr; }
  .ngd .lab-shell { grid-template-columns: 1fr; }
  .ngd .app-title { font-size: 1.1rem; }
  .ngd .stat { font-size: 1.5rem; }
  .ngd table { font-size: 0.74rem; }
  .ngd .mile { grid-template-columns: 1fr auto; gap: 0.4rem; }
}

@media (prefers-reduced-motion: reduce) {
  .deck { transform: rotateX(3deg) rotateY(-6deg); }
  .tour-caps { display: block; }
  .cap { margin-bottom: 2.25rem; }
  .focus-ring, .deck-sweep { display: none; }
}
