/* ── CleanHub Dashboard Case Study ─────────────────────────────────
   Mirrors report.css/mind.css's structure (page-specific layout only;
   skin for shared components — .card, .stats, .annotation-card,
   .section-eyebrow, the heading/body type scale — lives in style.css).
   Hero uses report.css's ch-hero method (height-driven screenshot,
   bleeding past the right edge) since this hero is a wide dashboard
   screenshot, not a phone mockup like 7Mind's. The annotated-screenshot
   blocks (Problem's two "Before" shots, Process's wireframe) reuse
   mind.css's shot-canvas/connector-line system verbatim, dash- renamed,
   not reinvented. */

/* ── HERO ────────────────────────────────────────────────────────── */
/* Structure/behavior mirrors .mind-hero (mind.css) — tucks under the
   fixed nav the same way (negative margin-top cancelling .page's own
   padding-top, padding-top adding it back), same viewport-fit-capped-
   at-746px height, same full-bleed hero (side-pad only, no max-width
   of its own) with .dash-hero-top doing the actual --max-w centering.
   padding-top departs from .mind-hero's own 96px+nav-h figure (this
   hero's own vertical rhythm reads better tighter, at a plain 2x
   nav-h) and the right side differs: a wide dashboard screenshot
   instead of a phone mockup, so it's sized by height and bled past
   the edge (below) instead of mind's fixed-aspect-ratio phone frame. */
.dash-hero {
  /* position: relative (no z-index) — containing block for
     .dash-hero-blob-zone below, same reasoning as .mind-hero's own
     note in mind.css: a child .bg-blob's z-index: -1 needs to resolve
     against .page's ONE shared stacking context, not a local one, so
     nothing here should open its own stacking context. overflow:
     hidden keeps the blob contained AND, since this hero now spans the
     full viewport width (no max-width of its own), doubles as the
     crop for the screenshot's own intentional bleed past the right
     edge below — no separate body-level overflow-x: clip needed. */
  position: relative;
  overflow: hidden;
  margin-top: calc(-1 * var(--nav-h));
  padding: calc(2 * var(--nav-h)) var(--side-pad) 0;
  min-height: min(100vh, calc(746px + var(--nav-h)));
  display: flex;
  flex-direction: column;
}
/* Figma node 1325:38678 ("Rectangle 208") — the same masked/blurred
   .bg-blob component 7Mind's hero uses (see .mind-hero-blob-zone,
   mind.css), just the sage→blue gradient instead of wine→rust and
   positioned toward the top-right instead of bottom. Unlike
   .mind-hero-blob-zone (inset skips the nav-h slice under the nav,
   since that blob is bottom-anchored and never reaches that high
   anyway), this one insets to the true top (0, not var(--nav-h)) —
   this blob deliberately pokes into the hero's top-right corner, so
   clipping its zone at nav-h would cut it off with a hard, visible
   edge floating above a flat gap instead of fading out naturally
   under the (transparent-until-scrolled) nav. */
.dash-hero-blob-zone { position: absolute; inset: 0; overflow: hidden; }
.dash-hero-top {
  max-width: var(--max-w);
  margin: 0 auto;
  width: 100%;
  display: grid;
  grid-template-columns: minmax(0, 500px) 1fr;
  gap: 80px;
  align-items: center;
  flex: 1;
}
/* Even 20px gap across all 3 hero text bits (eyebrow → title →
   subtitle), matching .mind-hero-left's own rhythm (mind.css) instead
   of the old 12px/40px pairing. .dash-tag/.dash-opener both pick up
   the same warm --brown .mind-tag/.mind-opener use. */
.dash-tag { margin-bottom: 20px; color: var(--brown); }
.dash-title { margin-bottom: 20px; }
.dash-opener {
  font-family: var(--font-sans);
  font-size: 20px;
  line-height: 1.5;
  color: var(--brown);
  font-weight: 400;
  max-width: 480px;
}
.dash-hero-right {
  display: flex;
  align-items: center;
  justify-content: center;
}
.dash-screen-frame {
  width: 100%;
  max-width: 560px;
}
/* Same 16px combo (.frame's own 8px default doubled) 7Mind's hero
   mockup uses — .mind-hero-mockup .frame in mind.css: border-radius
   is .frame's own box, --frame-radius matches it on the wrapped img,
   --frame-bleed keeps the halo's outer curve the same 16px past that
   so both arcs share one center point instead of reading as a ring of
   uneven thickness. Scoped to .dash-hero-right — .dash-screen-inner
   also wraps the Outcome section's screenshot elsewhere on the page,
   which keeps the component's plain 8px default. Only at >=1360px —
   below that the frame goes back to the component's own 8px default,
   same as every other breakpoint. */
@media (min-width: 1360px) {
  .dash-hero-right .dash-screen-inner.frame {
    border-radius: 16px;
    --frame-radius: 16px;
    --frame-bleed: 16px;
  }
}

/* ── META ROW ────────────────────────────────────────────────────── */
/* Sibling after </header>, not nested inside it — matches
   .mind-meta-row (mind.css) exactly. */
.dash-meta-row {
  max-width: var(--max-w-outer);
  margin: 64px auto 0;
  width: 100%;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 24px 40px;
  padding: 24px var(--side-pad) 56px;
}
.dash-meta-label { margin-bottom: 8px; }

/* Desktop hero: sized by height, free to bleed past the column/viewport's
   right edge — identical method to .ch-hero (report.css). */
@media (min-width: 1101px) {
  .dash-hero-top { align-items: stretch; }
  .dash-hero-left { align-self: center; }
  .dash-hero-right { position: relative; overflow: visible; }
  /* Scoped to .dash-hero-right — .dash-screen-frame/.dash-screen-inner
     are reused as a plain (non-absolute) media wrapper elsewhere on the
     page (e.g. the Outcome section's screenshot), so these rules must
     not apply globally. */
  .dash-hero-right .dash-screen-frame {
    /* top: 50% + the translateY below centers the frame vertically in
       its cell instead of pinning it to the top — matters once
       max-height (below) caps it shorter than the row itself. */
    position: absolute;
    top: 50%;
    left: 0;
    transform: translateY(-50%);
    height: 100%;
    max-height: 576px;
    width: auto;
    max-width: none;
  }
  .dash-hero-right .dash-screen-inner { height: 100%; width: fit-content; }
  .dash-hero-right .dash-screen-inner img {
    width: auto;
    max-width: none;
    height: 100%;
    object-fit: contain;
  }
}
/* Narrower text column + shorter image below 1360px — keeps the
   screenshot from crowding the text as the row's own available width
   shrinks. Comes after the base >=1101px rule above so its max-height
   wins over that rule's own 576px in the overlapping 1101-1359px
   range. */
@media (min-width: 1101px) and (max-width: 1359px) {
  .dash-hero-top { grid-template-columns: minmax(0, 390px) 1fr; }
  .dash-hero-right .dash-screen-frame { max-height: 432px; }
}

/* ── BODY ────────────────────────────────────────────────────────── */
.dash-body {
  max-width: var(--max-w-outer);
  margin: 0 auto;
  padding: 0 var(--side-pad) 120px;
}
/* position: relative (no z-index) — a containing block for a .bg-blob
   child's top/left%, but its z-index:-1 needs to resolve against
   .page's ONE shared stacking context (style.css), not a context
   scoped to just this section (see .page's own note on why). */
.dash-section { margin-top: 100px; position: relative; }
.dash-section-label { margin-bottom: 12px; }
.dash-h2 { margin-bottom: 24px; }
.dash-prose-col { max-width: var(--content-w); }
.dash-p { margin-bottom: 20px; max-width: var(--content-w); }
.dash-p:last-child { margin-bottom: 0; }

/* Value-loop line (Overview) — set apart from the surrounding prose as
   its own emphasized statement, not just a bold run inside a paragraph. */
.dash-loop-line {
  font-family: var(--font-sans);
  font-size: 22px;
  font-weight: 600;
  color: var(--black);
  letter-spacing: -0.01em;
  margin: 32px 0;
  max-width: var(--content-w);
}

/* ── STATS ───────────────────────────────────────────────────────── */
/* Columns/padding come from the shared .stats component now
   (style.css, auto-fit + minmax(200px)) — only each row's own
   placement stays here. .dash-outcome-stats overrides --stat-gap: 0,
   matching .ch-outcome-metrics — .stat's own border-left dividers are
   the separator for a 4-up row, a gap would double it up. */
.dash-stats {
  max-width: var(--content-w-md);
  margin: 40px auto;
}
.dash-outcome-stats {
  --stat-gap: 0px;
  max-width: var(--content-w-md);
  margin: 40px auto 24px;
}

/* ── SIGNAL CARDS (research findings, Problem section) ─────────────
   New component: a title, a fraction stat beside a supporting line,
   and a customer quote beneath a divider. Built on the shared .card
   shell (style.css) rather than .card--negative/positive — these are
   neutral findings, not problems or wins. */
.dash-signal-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
  max-width: var(--content-w-md);
  margin: 32px auto 40px;
  /* Grid items stretch to the row's tallest card by default — this
     lets each card hug its own content height instead. */
  align-items: start;
}
.dash-signal-card { gap: 16px; }
.dash-signal-row {
  display: flex;
  gap: 12px;
  align-items: flex-start;
}
.dash-signal-frac {
  font-family: var(--font-serif);
  font-size: 32px;
  line-height: 1;
  color: var(--gold);
  flex-shrink: 0;
}
.dash-signal-row p { margin: 0; }
.dash-signal-quote {
  display: flex;
  gap: 8px;
  padding-top: 16px;
  border-top: 1px solid var(--light);
}
.dash-signal-quote-mark {
  font-family: var(--font-serif);
  font-size: 20px;
  line-height: 1;
  color: var(--neutral-border);
  flex-shrink: 0;
}
.dash-signal-quote p {
  margin: 0;
  font-family: var(--font-sans);
  font-size: 13px;
  line-height: 1.5;
  font-style: italic;
  color: var(--mid);
  text-wrap: pretty;
}

/* ── BULLETS (mirrors .mind-bullets, since mind.css isn't loaded here) ── */
.dash-bullets {
  list-style: none;
  margin-top: 12px;
  margin-bottom: 20px;
  max-width: var(--content-w);
}
.dash-bullets li {
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.6;
  color: var(--black);
  font-weight: 400;
  padding: 8px 0 8px 20px;
  position: relative;
}
.dash-bullets li::before {
  content: '•';
  position: absolute;
  left: 0;
  color: var(--black);
}
.dash-bullets li b,
.dash-bullets li strong {
  color: var(--black);
  font-weight: 600;
}

/* ── SUB TITLE (H3, sans) — a lead-in heading for a block below it,
   e.g. "Three decisions came out of that:" ahead of the decisions
   diagram. Bottom-only margin: the block above already carries its own
   trailing spacing (.dash-bullets' 20px, .dash-p's 20px), and the
   diagram/section below supplies its own top margin too. */
.dash-sub-title { margin-top: 0; margin-bottom: 20px; }

/* ── PULL QUOTE (Design Challenge) ──────────────────────────────────
   Hugs its own sentence — mirrors .mind-pull-quote exactly. */
.dash-pull-quote {
  border: 1px solid var(--neutral-border);
  border-radius: 10px;
  padding: 40px 56px;
  margin: 200px auto;
  max-width: var(--content-w-md);
  width: fit-content;
  /* position: relative (no z-index) — a containing block for its
     .bg-blob child's top/left%, but its z-index:-1 needs to resolve
     against .page's ONE shared stacking context (style.css), not a
     context scoped to just this box (see .page's own note on why). */
  position: relative;
}
.dash-pull-quote-label { margin-bottom: 15px; }
/* Layout only — pairs with .h2-section for type (matches .mind-pull-quote/
   .ch-pull-quote now). color is a legitimate override: .h2-section
   defaults to --black, but a pull quote wants the warmer --dark-brown. */
.dash-pull-quote-text {
  color: var(--dark-brown);
  margin: 0;
}

/* ── BEFORE SCROLL (old dashboard, laptop-framed scroll-crop) ───────
   Same scroll-jack crop/scrub mechanic as the Impact Report case
   study's before/after (.ch-annotated-scroll / initAnnotatedScroll in
   report.css / report.js), adapted three ways: a fixed 1440:795
   "laptop screen" ratio instead of a viewport-height box, so it scales
   as one shape at any width rather than just cropping more/less of a
   tall portrait shot; two images side by side with no gap inside that
   frame — a non-scrolling side-nav (fills the frame's height) beside
   the dashboard itself, which scrolls; and a single annotation column
   instead of a left/right split. dashboard.js sizes .dash-before-scroll
   itself (pin duration + scrub distance) at runtime, since the scrub
   distance depends on the frame's rendered width — not fixed across
   breakpoints the way report.js's hand-tuned static height could
   assume for its one specific image. */
/* Below 1500px this is a normal, non-bleeding block — it shrinks in
   step with the page's own padding exactly like every other section,
   same as .dash-before-frame-wrap's max-width simply not being reached
   yet. Only ≥1500px does it bleed past .dash-section's own content
   column (--max-w, 1290px), same escalating idea as report.css's
   .ch-annotated-scroll: the frame can only actually reach its 1290px
   cap once the row around it is wider than 1290 by at least the
   annotation column's un-overlapped width (200px column − 80px overlap
   = 120px), or the flex row would keep shrinking the frame to make
   room for the column beside it instead. */
.dash-before-scroll {
  position: relative;
  margin: 40px 0;
}
@media (min-width: 1500px) {
  .dash-before-scroll { margin-left: -200px; margin-right: -200px; }
}
/* Flattened mobile export, swapped in below 600px (see the 600px query
   at the bottom of this file) instead of the scroll-jack frame — no
   frame/shadow of its own, just the image. */
.dash-before-mobile {
  display: none;
  width: 100%;
  height: auto;
  margin: 40px 0;
}
/* Flex, not grid: the annotation column needs to overlap the frame's
   right edge (negative "gap") rather than sit beside it in its own
   track, and the frame itself caps out at 1290px instead of stretching
   to fill whatever's left — extra room beyond that (plus the column's
   own width) is centered by justify-content once frame-wrap stops
   growing. */
.dash-before-inner {
  position: sticky;
  top: var(--nav-h);
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 24px 0;
}
.dash-before-frame-wrap {
  flex: 1 1 auto;
  min-width: 0;
  max-width: 1290px;
}
.dash-before-frame {
  width: 100%;
  height: auto;
  aspect-ratio: 1440 / 795;
  /* The ratio above sets the box's default/auto height at its current
     (width-capped) width — min-height then only ever pushes that
     TALLER, never overrides it downward, so 1440:795 acts as a floor
     rather than a fixed shape. Same idea as report.css's
     .ch-screen-sticky (height: 80vh; max-height: 1000px) for how far a
     generous viewport is allowed to stretch it, expressed as one
     min-height so it composes with the aspect-ratio's own floor
     instead of fighting it (an explicit height here would just replace
     the ratio outright). Width never grows to compensate — it's set
     entirely by .dash-before-frame-wrap's own flex/max-width sizing,
     which this box's height has no feedback into. */
  min-height: min(80vh, 1000px);
  /* Matches the Figma mockup's own laptop-bezel rounding. */
  --frame-radius: 16px;
}
.dash-before-viewport {
  display: flex;
  height: 100%;
  overflow: hidden;
  border-radius: var(--frame-radius);
  /* Positioned + isolation from .frame's own stacking context — without
     this the viewport (a plain static box) paints BELOW .frame::before's
     glass/blur overlay (an absolutely-positioned, and therefore always-
     stacked-above, sibling) regardless of paint order, washing out the
     side-nav and content underneath it. .frame > img normally gets this
     same z-index: 1 directly; here it has to be set explicitly since the
     images are nested a level deeper. */
  position: relative;
  z-index: 1;
}
/* Fixed-width column (never scrolls) — sized as a fraction of the
   frame's own WIDTH (422px nav : 2622px nav+content combined, the two
   exports' real pixel widths — content is 2200px wide, cropped down
   from its original 2456px to trim blank space on its own right edge),
   not derived from height the way a single height:100%/width:auto
   image would be. That matters once .dash-before-frame's min-height
   makes a taller-than-1440:795 frame: a height-driven nav would grow
   proportionately wider right along with it, when only the extra
   height should change. The literal #ffffff (not a token) is the fill
   for whatever gap opens up between the two images on a frame taller
   than their own combined height. */
.dash-before-sidenav {
  flex: 0 0 calc(422 / 2622 * 100%);
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  background: #ffffff;
  overflow: hidden;
}
.dash-before-sidenav-top,
.dash-before-sidenav-bottom {
  display: block;
  width: 100%;
  height: auto;
  flex-shrink: 0;
}
.dash-before-content {
  flex: 1 1 auto;
  min-width: 0;
  height: 100%;
  overflow: hidden;
  position: relative;
}
.dash-before-content-inner {
  position: absolute;
  top: 0; left: 0; right: 0;
  will-change: transform;
}
.dash-before-content-inner img { display: block; width: 100%; height: auto; }

/* Height is set inline by dashboard.js (matching the frame's own
   rendered height) rather than in CSS — needed so the absolutely
   positioned cards inside still reserve layout space once the column
   drops below the frame at 900px (see .dash-before-inner). Negative
   margin pulls the column 80px left, over the frame's right edge,
   instead of sitting beside it — z-index lifts it above .dash-before-
   viewport (both are z-index: 1 within .frame's isolated context, so
   this needs to actually outrank it, not just tie). */
.dash-before-annotations {
  position: relative;
  z-index: 2;
  flex: 0 0 200px;
  width: 200px;
  margin-left: -80px;
}
/* Visual skin (border/background/text/blur) lives in the shared
   .annotation-card component in style.css — this only positions each
   card against its synced content row, caps its width, and drives its
   reveal. */
.dash-annotation {
  position: absolute;
  left: 0;
  width: 100%;
  max-width: 200px;
  opacity: 0;
  transform: translateX(-20px);
  transition: opacity 0.4s ease, transform 0.4s ease;
  pointer-events: none;
}
.dash-annotation.is-visible { opacity: 1; transform: translateX(0); }

/* No stacking override below 900px — the annotation column's cards are
   positioned absolutely by dashboard.js, measured against the frame's
   own rect (see initBeforeScroll's update()). A column-direction stack
   here would put .dash-before-annotations BELOW the frame instead of
   beside it, which flips that measurement negative and throws every
   card's `top` off, overlapping the frame instead of sitting under it
   — the side-by-side row (and its 80px overlap) stays the same layout
   all the way down instead. */

/* ── DIAGRAM FRAMES (Reorg map, Intent matrix) ──────────────────────
   Flat exports with no shadow of their own — .frame--flush, same
   treatment as 7Mind's IA-tree diagrams. */
.dash-diagram {
  margin: 40px 0;
}
.dash-diagram--capped {
  max-width: 1000px;
  margin-left: auto;
  margin-right: auto;
}
.dash-diagram-image {
  display: block;
  width: 100%;
  height: auto;
  margin: 0;
}

/* ── SCROLL REVEAL ───────────────────────────────────────────────── */
.dash-reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}
.dash-reveal.is-visible { opacity: 1; transform: translateY(0); }

/* ── RESPONSIVE ──────────────────────────────────────────────────── */
/* Hero stacks earlier than the rest of the page's own ≤900px tier
   below — at this image's own wide aspect ratio, the side-by-side
   treatment runs out of room for the text column well before 900px
   does. */
@media (max-width: 1100px) {
  /* Stacked layout: text stays on top (no order flip — the old
     image-first order didn't match the Figma reference, node
     1381:47859's <900/<600/<440 frames all keep the text first) and
     the screenshot moves below it.
     min-height/padding-top match .mind-hero's own stacked-tier
     override (mind.css) in spirit — the 746px cap is non-stacked-only,
     and padding-top keeps the base rule's added var(--nav-h) while the
     rest of it shrinks to a plain 48px. */
  .dash-hero { min-height: auto; padding-top: calc(48px + var(--nav-h)); }
  .dash-hero-top { grid-template-columns: minmax(0, 1fr); gap: 56px; }
  /* min-width: 0 overrides the flex item's default auto min-width —
     below 600px .dash-screen-frame goes back to a wide, non-shrinking
     bleed (see that tier below); without this override here too, it
     would force this whole column wider than the viewport instead of
     letting the frame bleed past its edge on its own. */
  .dash-hero-right { justify-content: flex-start; min-width: 0; }
  /* Down to 600px, the image fills the column's own width instead of
     bleeding past it (below 600px it's back to the desktop hero's own
     "sized by height, bled past the edge" method — the column itself
     is too narrow by then to show the UI at a legible size any other
     way). max-height caps the frame's own box (what .dash-hero-top's
     row height is based on), while .dash-hero's own overflow: hidden
     crops the image itself where it spills past that — no overflow
     needed here, which also means .dash-screen-inner's own glass halo
     (::before, style.css) never gets clipped early on the sides that
     shouldn't be. margin-bottom is a plain, ordinary 56px (matching
     the gap above) — genuine empty space before the hero's own bottom
     edge, not a crop mechanism. */
  .dash-screen-frame {
    width: 100%;
    max-width: none;
    max-height: 458px;
    padding: 8px 8px 0;
    margin-bottom: 56px;
  }
}
@media (max-width: 900px) {
  .dash-meta-row { grid-template-columns: 1fr 1fr; }
  /* Still 3 columns here — only the gap tightens and the row inside
     each card stacks (fraction above claim) to fit the narrower
     column; full 1-column stacking doesn't kick in until 700px below. */
  .dash-signal-cards { gap: 12px; }
  .dash-signal-row { flex-direction: column; gap: 8px; }
  .dash-pull-quote { padding: 32px; }
}
@media (max-width: 700px) {
  /* Full width per card again once stacked, so the 900px row-stack and
     tightened gap above aren't needed any more — back to the base
     values. */
  .dash-signal-cards { grid-template-columns: 1fr; gap: 24px; }
  .dash-signal-row { flex-direction: row; gap: 12px; }
}
@media (max-width: 600px) {
  .dash-meta-row { grid-template-columns: 1fr; }
  /* Back to the desktop hero's own "sized by height, free to bleed
     past the right edge" method (.dash-hero's own overflow: hidden
     crops it) instead of the 1100-900px tier's fill-width-then-crop-
     the-bottom treatment above — at this image's own wide aspect
     ratio, shrinking it to the column's own (now much narrower) width
     would make the UI inside illegible well before phone-mockup-sized
     text would be. */
  .dash-screen-frame {
    width: auto;
    max-width: none;
    height: 370px;
    max-height: none;
    overflow: visible;
    padding: 0;
  }
  .dash-screen-frame .dash-screen-inner { height: 100%; width: fit-content; }
  .dash-screen-frame .dash-screen-inner img {
    width: auto;
    max-width: none;
    height: 100%;
    object-fit: contain;
  }
  .dash-before-scroll { display: none; }
  .dash-before-mobile { display: block; }
  /* Re-anchored for this tier's much narrower frame — the wider
     tiers' own top/right values (set inline, case-study-dashboard.html)
     push the blob almost entirely out of view once the frame itself
     is this narrow, same idea as .mind-hero-blob-zone's own left
     override at ≤600px (mind.css). !important is what lets a
     stylesheet rule win over the inline style. */
  .dash-hero-blob-zone .bg-blob { top: -150px !important; right: -300px !important; }
}
@media (max-width: 440px) {
  /* Smaller still at this tier, matching the Figma <440 frame's own
     value. */
  .dash-screen-frame { height: 300px; }
}
