/*
 * case-study.css - shared styles for all case study pages
 *
 * Each page sets its own accent via:
 *   :root { --action-primary: ...; --action-primary-hover: ...; --action-primary-subtle: ...; }
 *
 * Page-specific overrides (accent, chip variant, per-project media modules)
 * live in a small <style> block at the top of each page.
 */

/* ── Reset ────────────────────────────────────────────────────────────────── */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
/* clip, not hidden. A browser cannot honour overflow-x:hidden alongside
   overflow-y:visible, so it computes overflow-y to auto and body becomes a
   scroll container. position:sticky in the subtree then resolves against a
   content-height body and never engages - which is why the case-study
   sidebar had never actually stuck on any page. overflow-x:clip suppresses
   horizontal overflow without creating a scroll container. */
html{scroll-behavior:smooth;overflow-x:clip}
body{
  font-family:var(--font-sans);
  background:var(--surface-default);
  color:var(--text-primary);
  -webkit-font-smoothing:antialiased;
  line-height:1.5;
  min-height:100vh;
  overflow-x:clip;
}
img{display:block;max-width:100%}
button{cursor:pointer;font-family:inherit}

/* ── Focus ────────────────────────────────────────────────────────────────── */
/* This sheet had no focus rule of any kind, so five case studies fell back to
   the user-agent ring while index.html carried a hand-tuned one. The plugins
   page had since grown its own. That is the inconsistency, so the ring lives
   here now and every page inherits the same one.

   Two rings, not one. A single accent ring is invisible against an accent
   surface - the selected rail item, the active tab, a tinted card - and a
   single neutral ring disappears against the page. The inner ring is the
   accent and the outer is the page colour, so whichever surface sits behind
   the control, one of the two is always in contrast with it. 1.4.11 asks for
   3:1 on the indicator; --action-primary is 6.81:1 (plugins) to 9.19:1
   (myverint) on white and 4.19:1 to 5.13:1 on the dark page, measured per
   project accent.

   :focus-visible, not :focus, so a mouse click on a card does not leave a
   ring behind it. */
:where(a,button,summary,[tabindex]:not([tabindex="-1"]),input,select,textarea):focus-visible{
  outline:2px solid var(--action-primary);
  outline-offset:2px;
  box-shadow:0 0 0 6px var(--surface-default);
  border-radius:var(--radius-sm);
}
/* The spread has to exceed offset + width or the outer ring hides underneath
   the outline: offset 2 plus width 2 reaches 4px, so a 4px spread would sit
   exactly beneath it and paint nothing. 6px leaves a visible 4-6px band. */
/* Controls that already carry their own radius keep it - the ring follows the
   shape rather than squaring off a pill or a circle. */
.vs-item:focus-visible,.pf-step:focus-visible,.sc-tab:focus-visible,
.chip:focus-visible,.back-nav:focus-visible,.sb-cta:focus-visible{border-radius:inherit}
#scroll-top:focus-visible,.pf-d-close:focus-visible{border-radius:50%}
/* .vs-btn sits ON the video, so the outer ring has to go. It is an opaque 6px
   band of --surface-default, which would paint a white donut over the footage
   in light mode. That button already brings its own contrast - fixed dark
   chrome and a white outline, for the same reason - so it needs the shape
   only, not the second ring. */
.vs-btn:focus-visible{border-radius:50%;box-shadow:none}

/* ── Ambient field ────────────────────────────────────────────────────────── */
/* One fixed, page-wide layer: a dot lattice plus two radial washes drifting
   in the project accent. Fixed to the viewport rather than bounded per
   section - masking it per section makes the texture pulse on and off at
   every boundary. body paints the canvas, so a fixed child at z-index 0
   sits above it and below the content.

   Everything here reads var(--action-primary), so each page's accent flows
   through from its own :root block. Add
     <div class="cs-field" aria-hidden="true"><i></i><i></i></div>
   as the first child of <body>, and give the page's own content wrapper
   position:relative;z-index:1.

   Light and dark are not the same treatment. Glow is additive: on near-black
   there is headroom to add light, so accent dots read as luminosity. On white
   a dark dot subtracts light and reads as dirt, so light mode uses
   near-neutral dots at a third the strength. */
:root{
  --cs-dot:    color-mix(in srgb, var(--text-primary) 9%, transparent);
  --cs-wash-a: color-mix(in srgb, var(--action-primary) 10%, transparent);
  --cs-wash-b: color-mix(in srgb, var(--action-primary) 8%,  transparent);
}
/* 20/15 in dark, not the 22/17 myverint shipped with. Once this is shared, the
   cap has to hold for the lightest accent on the site, and green carries the
   most luminance of any hue (0.7152 of the coefficient). Copilot's #34d399
   lifted the near-black canvas far enough that --text-secondary fell to
   4.39:1 where both washes overlap - a fail. 20/15 puts the worst case at
   4.69:1 and costs nothing visible on the other four. */
[data-theme="dark"]{
  --cs-dot:    color-mix(in srgb, var(--action-primary) 28%, transparent);
  --cs-wash-a: color-mix(in srgb, var(--action-primary) 20%, transparent);
  --cs-wash-b: color-mix(in srgb, var(--action-primary) 15%, transparent);
}
.cs-field{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden}
.cs-field::before{content:"";position:absolute;inset:0;
  background-image:radial-gradient(circle,var(--cs-dot) 1px,transparent 1px);
  background-size:22px 22px}
/* will-change promotes each wash to its own compositor layer, so the drift is
   a GPU matrix transform on a cached texture rather than a repaint of a
   1100px-square gradient every frame. These animate forever, which is exactly
   the case will-change is for - it is a hint to keep, not one to clean up.

   Worth knowing: this field runs in every OPEN project window, so three
   windows open at once means six of these animating. Closing windows you are
   not reading is the cheapest performance win available on this site.

   A note on an optimisation NOT taken: shrinking these elements and scaling
   them up further looks like a big win on paper, since a smooth low-opacity
   gradient has no detail to lose and the texture would be 72% smaller. It
   probably saves nothing. Chromium rasterises a layer with an accelerated
   transform animation at the animation's MAXIMUM scale, so a 34vmax element
   animated to 2.29x rasterises at the same 78vmax as a 64vmax element animated
   to 1.22x. Do not make that change without profiling it first. */
.cs-field i{position:absolute;display:block;border-radius:50%;
  will-change:transform}
/* Paused on two triggers, set by two different owners, which is why they are
   two attributes rather than one - they would otherwise fight over the same
   value and whichever wrote last would win.

   data-cs-hidden: set by this page's own case-study.js on visibilitychange.
     An animation on a hidden tab is usually throttled by the browser anyway,
     but "usually" depends on whether the compositor considers the layer
     visible, and these are position:fixed inside an iframe.

   data-cs-idle:   set by the PARENT page on every window raise, on any project
     window that is not the front-most one. A covered window is still composited
     - the browser has no idea you cannot see it - so without this, three open
     windows meant six washes drifting forever and five of them for nobody. */
html[data-cs-hidden] .cs-field i,
html[data-cs-idle]   .cs-field i{animation-play-state:paused}
.cs-field i:nth-child(1){width:64vmax;height:64vmax;top:-22vmax;right:-16vmax;
  background:radial-gradient(circle closest-side,var(--cs-wash-a),transparent 70%);
  animation:csFieldA 17s ease-in-out infinite alternate}
.cs-field i:nth-child(2){width:56vmax;height:56vmax;bottom:-20vmax;left:-16vmax;
  background:radial-gradient(circle closest-side,var(--cs-wash-b),transparent 70%);
  animation:csFieldB 23s ease-in-out infinite alternate}
/* Presence comes from travel and size, not opacity. Opacity is capped by
   contrast: in light mode 10%/8% keeps --text-secondary at 5.00:1 even where
   both washes overlap, and 12% already drops it to 4.49:1 and fails. Motion
   amplitude costs nothing, so the drift is large and both washes sit in
   opposite corners, away from the text column. */
@keyframes csFieldA{from{transform:translate3d(0,0,0) scale(1)}
                      to{transform:translate3d(-16vmax,10vmax,0) scale(1.22)}}
@keyframes csFieldB{from{transform:translate3d(0,0,0) scale(1.14)}
                      to{transform:translate3d(14vmax,-12vmax,0) scale(.94)}}
@media(prefers-reduced-motion:reduce){
  .cs-field i{animation:none;will-change:auto}
}
/* The field is behind everything, so anything that must sit above it needs a
   stacking context of its own. .cs-body gets its z-index in the Layout block
   below, alongside its width - one declaration per selector per context. */
.back-nav,#scroll-top{z-index:3}

/* ── Inline media ─────────────────────────────────────────────────────────── */
/* What replaces the carousel. Media sits beside the claim it evidences rather
   than being front-loaded in a tray where slides 2-4 are never seen.

   <figure class="cs-media">
     <video class="cs-media-vid" data-src="videos/x.mp4"
            poster="images/x-poster.webp" muted loop playsinline
            preload="none" aria-label="..."></video>
     <figcaption>What this shows and why it matters</figcaption>
   </figure>

   data-src rather than src: nothing is fetched until the figure is near the
   viewport. Use <img> in place of <video> for stills - same frame, same
   caption treatment. Full width only; there was a side-by-side variant here
   that no page ever used, so it went. */
.cs-media{margin:26px 0 0;position:relative}
.cs-media > video,
.cs-media > img{
  width:100%;height:auto;display:block;
  border-radius:var(--radius-lg);
  border:1px solid var(--border-subtle);
  background:var(--surface-raised);
  /* Shadows are not symmetric between themes. On white a shadow must be
     short and faint or it reads as a grey smear; on near-black it needs
     depth to register at all. */
  box-shadow:0 18px 40px rgba(18,18,19,.16);
}
[data-theme="dark"] .cs-media > video,
[data-theme="dark"] .cs-media > img{
  box-shadow:0 48px 96px rgba(0,0,0,.8);
}
.cs-media figcaption{
  margin-top:12px;
  font-size:13px;line-height:1.6;
  /* secondary, not tertiary: dark --text-tertiary is 4.45:1 on
     --surface-default and 4.02:1 on --surface-raised, so it fails AA at this
     size. --text-secondary is 8.41:1 on the same surface. */
  color:var(--text-secondary);
  text-wrap:pretty;
  /* Full width of the figure, no measure cap. The 62ch cap is the right rule
     for a column of body copy, but a caption is anchored to the thing above
     it - stopping it two thirds of the way across a wide figure reads as a
     ragged edge rather than as a comfortable line length. */
}

/* The carousel, the placeholder SVG styling and both lightboxes were removed
   in Sept 2026 - roughly 350 lines. Every case study places its media inline
   now, via .cs-media above or a module of its own. Checked before deleting:
   no page carries .carousel-*, .lbx-*, .vlbx-* or .cs-placeholder-img markup,
   and the JS that drove them went with it. */

/* ── Layout ───────────────────────────────────────────────────────────────── */
/* 1280, not 1100. The window these pages open in is min(1320px, 100vw-300px),
   so the old 1100 cap left ~220px of the frame permanently empty. It was only
   held at 1100 to stay flush with the 1040px carousel; the carousels are gone.
   minmax(0,1fr) rather than 1fr so a wide child cannot blow the column out. */
.cs-body{
  display:grid;
  grid-template-columns:200px minmax(0,1fr);
  gap:56px;
  padding:48px 44px 120px;
  max-width:1280px;
  width:100%;
  margin:0 auto;
  box-sizing:border-box;
  position:relative;   /* above the ambient field */
  z-index:1;
}

/* ── Sidebar ──────────────────────────────────────────────────────────────── */
/* A sticky grid item with align-self:start and no height cap has no travel
   room once its content is taller than the viewport, so it scrolls away like
   a static block. The max-height plus its own scroll area is what actually
   keeps it pinned. Needs overflow-x:clip on html/body to work at all - see
   the reset at the top of this file. */
.sidebar{
  position:sticky;top:28px;
  height:fit-content;
  max-height:calc(100vh - 56px);
  overflow-y:auto;
  scrollbar-width:thin;
  align-self:start;
  display:flex;flex-direction:column;gap:20px;
}
.chip{
  display:inline-flex;align-items:center;align-self:flex-start;
  font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  padding:4px 10px;border-radius:6px;
  white-space:nowrap;flex-shrink:0;line-height:1;
}
.sb-fields{
  display:flex;flex-direction:column;gap:16px;
  padding-left:14px;
  border-left:2px solid var(--action-primary);
}
.sb-label{font-size:9px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--text-tertiary);margin-bottom:3px}
.sb-value{font-size:13px;font-weight:500;color:var(--text-secondary);line-height:1.5;text-wrap:pretty}
.sb-div{height:1px;background:var(--border-subtle)}
.sb-nut{font-size:13px;color:var(--text-secondary);line-height:1.8;font-style:italic;text-wrap:pretty}

/* ── Content ──────────────────────────────────────────────────────────────── */
.cs-content{display:flex;flex-direction:column;gap:52px}

/* ── Hero ─────────────────────────────────────────────────────────────────── */
.cs-hero{padding-bottom:36px;border-bottom:1px solid var(--border-subtle)}
.cs-hero h1{
  font-size:38px;font-weight:900;
  letter-spacing:-1px;line-height:1.15;
  color:var(--text-primary);
}
.cs-hero-accent{color:var(--action-primary)}
/* 17.5px at 650px, not 16px at 580px. What read as "too narrow" was the
   ratio rather than the width: the h1 fills the 880px column and the subtitle
   stopped at 580, so it looked unrelated to the line above it.
   Widening the box alone would have broken the measure - 580px at 16px is
   already about 72 characters a line, the top of the 50-75 range this file
   works to. Lifting the size and the width together lands at 74, so the block
   is 12% wider and reads as part of the hero instead of a caption under it. */
.cs-hero-sub{
  margin-top:16px;
  font-size:17.5px;color:var(--text-secondary);
  line-height:1.7;max-width:650px;
  text-wrap:pretty;
}

/* ── Sections ─────────────────────────────────────────────────────────────── */
.cs-section-num{margin-bottom:10px}
.cs-sec-title{
  font-size:26px;font-weight:900;
  letter-spacing:-.5px;
  color:var(--action-primary);
  margin-bottom:16px;
}
/* Body copy is primary, not secondary. This is the reading text of the whole
   page and it was sitting one step down from the headings, which made every
   section read as caption rather than prose.
   The hero subtitle deliberately stays secondary - it is the one line that
   should sit back, directly under the h1.

   DIRECT CHILDREN ONLY. As a descendant selector this reached every <p>
   inside any module a section contained - 17 of them across the site - and
   at (0,1,1) it beat any single-class rule those modules used, silently, on
   colour, size, line-height and margin. That is the specificity trap in
   CLAUDE.md, and it is why module paragraphs needed two classes to override
   what should never have applied to them. Scoped to `>`, section body copy
   still gets styled and module internals are left alone. Verified first:
   all 17 nested paragraphs already set their own colour, size and leading. */
.cs-section > p{
  font-size:16px;color:var(--text-primary);
  line-height:1.85;text-wrap:pretty;margin-bottom:12px;
}
.cs-section > p:last-child{margin-bottom:0}
.cs-section > p strong{color:var(--text-primary);font-weight:700}


/* ══ Before/after comparison, one seam ═════════════════════════════════════
   SHARED, because two case studies now use it: the LUX light/dark seam and
   the Copilot wrap-up before/after. It was written inline on the LUX page
   first; this is that code moved, not a second copy of it.

   What stayed behind on LUX is everything specific to what LUX compares: the
   token chips, their leader lines, the hotspot dots and the gutter that makes
   room for them. What moved here is the mechanism and the box.

   Two things are parameters rather than values, because they were LUX's
   answers and not the component's:
     --cmp-ar   the aspect ratio of the pair of images
     --cmp-gut  side padding, for a page that needs room beside the frame
   A page that needs neither sets neither.

.cmp{margin-top:22px;--p:50%;--cmp-gut:0%}
.cmp-stage{position:relative;padding:0 var(--cmp-gut)}
.cmp-frame{position:relative;overflow:hidden;border-radius:var(--radius-lg);
  border:1px solid var(--border-subtle);background:var(--surface-sunken);
  /* Per page: the two images decide this, not the component. */
  aspect-ratio:var(--cmp-ar, 16/9);
  /* 18px/40px light, 26px/52px dark. Shadows are not symmetric between
     themes, and the dark one was 48/96 until it was measured: a 96px blur on
     an 800x450 box is a large paint region, and it sits under a child whose
     clip-path changes on every frame of a drag. 52px still reads as depth on
     near-black. */
  box-shadow:0 18px 40px rgba(18,18,19,.16)}
[data-theme="dark"] .cmp-frame{box-shadow:0 26px 52px rgba(0,0,0,.72)}

.cmp-side{position:absolute;inset:0}
.cmp-side img{width:100%;height:100%;display:block;object-fit:cover}
/* One side sits behind at full width; the other is clipped from the right, so
   the seam is at --p and the clipped side occupies the left.

   .cmp-clip, not .cmp-dark. The rule was named for what LUX happens to put in
   it, and Copilot puts "before" there instead - a class called cmp-dark
   holding a screenshot of an empty text field is a comment that lies. The
   page keeps its own semantic class alongside this one. */
.cmp-clip{clip-path:inset(0 calc(100% - var(--p)) 0 0)}

/* The input is the control. Full size and invisible, so a press anywhere in
   the frame jumps the seam there and then drags - which the old version could
   not do, it needed the handle grabbed. */
.cmp-input{position:absolute;inset:0;z-index:4;width:100%;height:100%;margin:0;
  opacity:0;cursor:ew-resize;-webkit-appearance:none;appearance:none;
  background:transparent;
  /* pan-y, stated rather than left to the default: the input covers a tall
     image, so a vertical swipe that starts on it has to scroll the page while
     horizontal drags still move the seam. */
  touch-action:pan-y}
.cmp-input::-webkit-slider-thumb{-webkit-appearance:none;width:44px;height:100vh}
.cmp-input::-moz-range-thumb{width:44px;height:100vh;border:0;background:transparent}

.cmp-handle{position:absolute;top:0;bottom:0;left:var(--p);z-index:3;
  width:44px;margin-left:-22px;pointer-events:none;
  display:flex;flex-direction:column;align-items:center}
.cmp-line{flex:1;width:2px;background:rgba(255,255,255,.72);
  box-shadow:0 0 6px rgba(0,0,0,.45)}
/* position:relative so the ::after ring below can inset against it. */
.cmp-pill{position:relative;flex-shrink:0;width:36px;height:36px;border-radius:50%;
  background:rgba(20,20,24,.95);border:2px solid rgba(255,255,255,.55);
  color:#fff;display:flex;align-items:center;justify-content:center;
  box-shadow:0 4px 16px rgba(0,0,0,.5)}
.cmp-pill svg{width:14px;height:14px;display:block}
/* The pulse invites the drag, and it stops once the thing has been used -
   an affordance hint that keeps pulsing is just noise.

   It is a ring on a pseudo-element animating transform and opacity, NOT a
   box-shadow animation. The box-shadow version spread a 16px shadow out and
   back forever, and box-shadow is painted: every frame invalidated a region
   around the handle, which sits on top of a large image whose clip-path the
   seam is also changing. Transform and opacity composite instead, so the ring
   is a GPU matrix on a cached 36px texture and touches no paint at all. Same
   animation to look at, and it was one of only two infinite box-shadow
   animations on the site. */
.cmp-pill::after{content:"";position:absolute;inset:-2px;border-radius:50%;
  border:2px solid rgba(255,255,255,.55);pointer-events:none;opacity:0}
.cmp:not(.cmp-used) .cmp-pill::after{
  animation:cmpPulse 1.9s ease-out infinite}
@keyframes cmpPulse{
  0%  {transform:scale(1);   opacity:.55}
  70%,100%{transform:scale(1.95); opacity:0}
}
@media(prefers-reduced-motion:reduce){
  .cmp:not(.cmp-used) .cmp-pill::after{animation:none;opacity:0}
}
/* The input is invisible, so its focus ring would be too. Put it on the pill. */
.cmp-input:focus-visible~.cmp-handle .cmp-pill{
  outline:3px solid var(--action-primary);outline-offset:3px}

.cmp-hint{margin-top:10px;text-align:center;font-size:11.5px;
  color:var(--text-tertiary);letter-spacing:.03em}

/* ── side labels ───────────────────────────────────────────────────────────
   Not on LUX, which names its halves through the token chips instead. Placed
   top-left and top-right and pinned to the FRAME rather than to the seam, so
   they never slide under the handle or collide with each other as it moves.
   Fixed dark chrome on purpose: a label sits on whichever image is behind it,
   so it cannot take its colour from the document theme, the same reasoning as
   the handle and the phone bezel. */
.cmp-label{position:absolute;top:10px;z-index:2;pointer-events:none;
  padding:4px 10px;border-radius:var(--radius-full);
  background:rgba(16,17,19,.82);color:#fff;
  font-size:10px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;
  line-height:1.3}
.cmp-label-a{left:10px}
.cmp-label-b{right:10px}

/* ── Plugin blocks: removed ────────────────────────────────────────────────
   .plugins-list, .plugin-block, .plugin-head, .plugin-num, .plugin-tag,
   .plugin-title and .plugin-desc lived here, 54 lines of them. Only the
   Copilot page ever used them - the Plugins case study, despite the name,
   uses the .vs reel - and the Copilot page's three cards were replaced by the
   interactive player, which says the same three things by showing them.
   Nothing on the site referenced these afterwards, so they went with the
   markup rather than being left behind for the next dead-code scan to find.
   In git if they are wanted back.                                          */

/* ── Stat cards ───────────────────────────────────────────────────────────── */
.stat-grid{
  display:grid;
  grid-template-columns:repeat(3,1fr);
  gap:16px;margin-top:28px;
}
.stat-card{
  background:var(--surface-default);
  border:1px solid var(--border-subtle);
  border-radius:16px;
  padding:28px 20px 0;
  display:flex;flex-direction:column;align-items:center;
  text-align:center;overflow:hidden;
  box-shadow:0 1px 6px rgba(0,0,0,.04);
}
[data-theme="dark"] .stat-card{box-shadow:none}
.stat-icon{color:var(--action-primary);margin-bottom:14px}
.stat-icon svg{width:30px;height:30px;display:block}
.stat-num{
  font-size:48px;font-weight:900;
  color:var(--action-primary);
  letter-spacing:-2.5px;line-height:1;margin-bottom:8px;
}
.stat-title{font-size:14px;font-weight:700;color:var(--text-primary);margin-bottom:6px}
.stat-desc{font-size:12px;color:var(--text-secondary);line-height:1.6;padding:0 6px 20px;flex:1}

/* ── "What I'd do differently" ───────────────────────────────────────────── */
/* Margin notes, not cards and not alerts. A filled circle with "!" is an error
   affordance, and a reflection is not an error; numbered chips imply a sequence
   these do not have. A short uppercase label in the margin says what kind of
   lesson it is and gets out of the way.

   Three levels, three colours: the label carries the accent, which ties it to
   the section title, the lesson is primary, the body is secondary. Before,
   the label and the body were both secondary, so there were only two levels
   and the block read as flat grey.

   Contrast on the washed ambient background, worst case with both washes
   overlapping: label 6.73:1 light and 4.89:1 dark, lesson 13.71:1 and 9.37:1,
   body 5.00:1 and 6.87:1.

   Markup:
     <div class="diff-item">
       <div class="diff-marg">Process</div>
       <div class="diff-body">
         <p class="diff-lesson">The lesson, in one sentence.</p>
         <p class="diff-text">What happened and what it cost.</p>
       </div>
     </div>                                                                  */
.diff-item{
  display:grid;grid-template-columns:7.5em minmax(0,1fr);
  gap:0 2.4em;
  /* baseline, not a hand-tuned padding-top, so the label's baseline sits on
     the lesson's first baseline whatever either font-size becomes */
  align-items:baseline;
  padding:2.2em 0;
}
/* The rule goes BETWEEN items, via the adjacent sibling combinator.
   :first-of-type does not work here: .cs-section-num is the first div child of
   the section, so the first .diff-item is the second div and never matched the
   reset - which is why a divider kept appearing directly under the section
   heading. */
.diff-item + .diff-item{border-top:1px solid var(--border-subtle)}
.diff-item:first-of-type{padding-top:0;margin-top:1.4em}
.diff-item:last-of-type{padding-bottom:.4em}
.diff-marg{
  font-size:.75rem;font-weight:700;letter-spacing:.11em;
  text-transform:uppercase;color:var(--action-primary);
  line-height:1.35;
}
/* the plain accent is 3.52:1 on the dark wash and fails; the hover step is the
   lighter of the pair in dark mode and clears it */
[data-theme="dark"] .diff-marg{color:var(--action-primary-hover)}
.diff-body{min-width:0}
/* Two classes here are now belt and braces rather than a requirement:
   .cs-section is scoped to `> p`, so it no longer reaches inside this module.
   Left as-is because they work; new modules only need one class. */
.diff-body .diff-lesson{
  font-size:1.1875rem;line-height:1.35;font-weight:700;
  letter-spacing:-.014em;color:var(--text-primary);
  margin:0 0 .6em;text-wrap:nowrap;
}
.diff-body .diff-text{
  font-size:.9375rem;line-height:1.75;color:var(--text-secondary);
  margin:0;max-width:64ch;text-wrap:pretty;
}
@media(max-width:760px){
  .diff-item{grid-template-columns:1fr;gap:.55em 0;padding:1.7em 0}
  .diff-item:first-of-type{margin-top:1.1em}
  /* wrapping the lesson is fine once it has the full width */
  .diff-body .diff-lesson{font-size:1.0625rem;text-wrap:pretty}
}

/* ── AI card ──────────────────────────────────────────────────────────────── */
.ai-card{
  display:flex;align-items:flex-start;gap:20px;
  margin-top:24px;
  padding:22px 24px;
  background:var(--surface-raised,#F5F5F5);
  border:1px solid var(--border-subtle);
  border-radius:16px;
}
.ai-icon{flex-shrink:0;margin-top:2px;width:40px;height:auto;display:block}
.ai-icon svg{width:40px;height:auto;display:block}
.ai-card-body{flex:1}
.ai-card-label{
  font-size:10px;font-weight:800;letter-spacing:.12em;
  text-transform:uppercase;color:var(--text-primary);
  margin-bottom:10px;
}
.ai-card-text{font-size:14.5px;color:var(--text-secondary);line-height:1.85;text-wrap:pretty}

/* ── Scroll reveal ────────────────────────────────────────────────────────── */
@media(prefers-reduced-motion:no-preference){
  .cs-reveal{opacity:0;transform:translateY(16px);transition:opacity .45s ease-out,transform .45s ease-out}
  .cs-reveal.is-visible{opacity:1;transform:translateY(0)}
}

/* ── Back nav (mobile only) ───────────────────────────────────────────────── */
.back-nav{
  display:none;align-items:center;gap:6px;
  padding:14px 18px;
  font-size:13px;font-weight:600;
  color:var(--text-secondary);text-decoration:none;
  border-bottom:1px solid var(--border-subtle);
  background:var(--surface-default);
}
.back-nav svg{flex-shrink:0;color:var(--text-tertiary)}
.back-nav:hover{color:var(--text-primary)}

/* ── Scroll to top ────────────────────────────────────────────────────────── */
#scroll-top{
  position:fixed;bottom:28px;right:28px;
  width:38px;height:38px;border-radius:50%;
  background:var(--action-primary);color:var(--text-on-accent);
  border:none;cursor:pointer;
  display:flex;align-items:center;justify-content:center;
  box-shadow:0 4px 16px rgba(0,0,0,.18);
  opacity:0;transform:translateY(10px);
  transition:opacity .22s,transform .22s,background .15s;
  pointer-events:none;z-index:500;
}
#scroll-top.show{opacity:1;transform:translateY(0);pointer-events:auto}
#scroll-top:hover{background:var(--action-primary-hover)}
/* The ink is --text-on-accent, not #fff, and that is not cosmetic. In dark
   every project accent is a BRIGHT colour - that is the whole point of the
   per-page dark override - so a white arrow on it landed between 1.92:1
   (copilot green) and 3.34:1 (plugins magenta). None of the five reached the
   3:1 that 1.4.11 asks of an icon.
   myverint, lux and copilot each carried their own `#scroll-top{color:#1a1a1a}`
   patch; plugins and agent-factory never got one, so the arrow was invisible
   on two of the five pages. The token replaces all three page-level copies:
   #fff on the light accents (5.48:1 to 9.19:1) and #121213 on the dark ones
   (5.61:1 to 9.74:1). */

/* ── Responsive ───────────────────────────────────────────────────────────── */
/* These pages are read inside a RESIZABLE WINDOW, not only on a device, and
   main.js lets a project window go down to 800px wide. So the breakpoints have
   to be driven by how much room the layout actually needs, not by phone sizes.

   Measured, with .cs-body at 200px sidebar + 56px gap + 88px padding:

     window   content   myverint description column
      1320      976 px      440 px
      1220      876 px      340 px
      1140      796 px      260 px   <- the floor for a readable column
      1090      746 px      210 px   <- one word per line
       900      556 px       20 px
       860      516 px      -20 px   <- overflows, and the overflow is
                                        unreachable because we clip x to keep
                                        position:sticky working

   The modules used to flip at 900px, which left 800-1140 broken. They flip at
   1140 now, and the page itself goes single column at 1000 so the content gets
   the sidebar's width back before things get tight. */
@media(max-width:1000px){
  .cs-body{grid-template-columns:1fr;gap:28px;padding:32px 24px 90px}
  /* ── The meta block becomes one card ──────────────────────────────────
     On mobile this stack used to carry four separate rules: the accent bar
     down the left of the fields, the .sb-div above the nutshell, the
     sidebar's own border-bottom, and the back-nav border right above it.
     Four horizontal lines in 300px of vertical space reads as debris, and
     none of them was doing work that spacing could not do.

     One tinted card instead. The tint and the hairline are both mixed from
     --action-primary, so the container carries the project colour rather
     than being neutral chrome, and hierarchy inside comes from type weight
     instead of rules. */
  .sidebar{
    position:static;top:auto;height:auto;max-height:none;overflow:visible;
    gap:14px;
    padding:20px;
    margin-bottom:4px;
    border:1px solid color-mix(in srgb, var(--action-primary) 14%, var(--border-subtle));
    border-radius:var(--radius-xl);
    background:color-mix(in srgb, var(--action-primary) 4%, var(--surface-raised));
  }
  [data-theme="dark"] .sidebar{
    background:color-mix(in srgb, var(--action-primary) 8%, var(--surface-raised));
  }
  .sb-fields{
    display:grid;grid-template-columns:1fr 1fr;gap:16px 20px;
    padding-left:0;border-left:0;         /* the accent bar goes */
  }
  /* Labels step up from tertiary because tertiary fails AA in dark mode
     (4.02:1 on --surface-raised); secondary is 8.41:1. Values step up to
     primary so label and value separate on weight, not on a divider. */
  .sb-label{color:var(--text-secondary);font-size:10px;margin-bottom:4px}
  .sb-value{color:var(--text-primary);font-size:14px;font-weight:600}
  .sb-div{display:none}
  .sb-nut{margin:0;font-size:13.5px;line-height:1.7}
}
/* Genuinely small screens: the type scale steps down and the mobile back
   nav appears. Not tied to the window resize above - a 900px window on a
   desktop should not get phone-sized type. */
@media(max-width:720px){
  .back-nav{display:flex}
  .cs-body{gap:28px;padding:24px 20px 80px}
  .cs-hero h1{font-size:24px}
  /* Primary, not secondary, and only here. On desktop the lede is 17.5px
     against 16px body, so size carries the hierarchy and --text-secondary
     reads as a deliberate step down. At 720 the lede drops to 16 and the body
     to 15, the size gap closes to a point, and colour is the only thing left
     saying which is which - at which point the lede is the QUIETER of the two,
     which is backwards. #d0d3d6 is 12.92:1 on the dark page so nothing was
     failing; it just looked washed out next to white body copy. */
  .cs-hero-sub{font-size:16px;max-width:none;color:var(--text-primary)}
  .cs-section > p{font-size:15px}
  .ai-card-text{font-size:15px}
  .stat-grid{grid-template-columns:1fr 1fr}
  .stat-grid .stat-card:last-child{grid-column:1/-1}
  .stat-num{font-size:36px}
}
@media(max-width:480px){
  .cs-body{padding:20px 18px 80px}
  .stat-grid{grid-template-columns:1fr}
  .stat-grid .stat-card:last-child{grid-column:auto}
  .stat-num{font-size:32px}
  .ai-card{flex-direction:column;gap:10px}
  .cs-section > p{font-size:14.5px}
  .ai-card-text{font-size:14.5px}
}

/* ── Mobile type floor ─────────────────────────────────────────────────────
   LAST in the file on purpose. The first version of this sat above the
   responsive ladder and did nothing: at 360px both it and the
   @media(max-width:1000px) block apply, the selectors are the same weight,
   and the later one wins - so .sb-label stayed at the 10px the 1000px block
   sets. A narrower breakpoint has to come after a wider one, or it is decided
   by line number rather than by intent.

   All-caps tracked labels at 9px are a fine desktop detail and a bad phone
   one. No guideline forces this - 1.4.4 is about zoom, not size - it is here
   because the smallest type on five case studies was 9px and these pages get
   read on a phone as often as not.                                          */
@media(max-width:720px){
  .sb-label{font-size:10.5px}
  .chip{font-size:10.5px}
  .ai-card-label{font-size:10.5px}
  .cs-section-num,.diff-marg{font-size:10.5px}
}
