/* ==========================================================================
   Bunn Design Co. — case study page components

   Loaded (alongside site.css, studio.css and showcase.css) only by the two
   premium case-study pages: work/northbound.html and work/retro-fitness.html.
   Nothing here redefines a shared component — everything is a new cs-*
   class, plus one size modifier on showcase.css's existing .nb-* device
   mockup (--xl) so the same component that runs small on the homepage can
   run large here instead of a second device-frame system being invented.
   Same tokens, spacing scale, reveal system and easing as everywhere else.
   ========================================================================== */

/* ---------- opening hero, mouse-reactive texture ----------
   hero-texture.css positions .hero__texture and stacks it under .hero >
   .wrap — this page's opening section is .studio-phead, not .hero, so it
   needs the same stacking glue under its own selector. */
.studio-phead > .wrap { position: relative; z-index: 1; }

/* ---------- large device showcase (reuses .nb-* from showcase.css) ----------
   The homepage card runs this component at up to 820px so it fits beside a
   text column in a 2-up grid; here the showcase IS the section, full
   width, so it gets real room. */
.nb-devices-link--xl { max-width: 1180px; }
.nb-devices-link--xl .nb-frame--desktop .nb-frame__viewport {
  height: clamp(440px, 34vw, 640px);
}
.nb-devices-link--xl .nb-frame--phone {
  width: clamp(150px, 15vw, 220px);
}
.cs-showcase { display: flex; justify-content: center; }
/* the homepage's WEBSITE DEMO badge overlays the scrolling screenshot
   itself — fine there, where the frame is small and roughly constant in
   width, but at --xl's much larger, viewport-proportional size the
   screenshot underneath (including each site's own nav logo) scales up
   enough to actually collide with it at some widths — confirmed directly,
   not just suspected: measured overlap with Retro Fitness's own logo at
   820px. Scaling the badge to match didn't fix it (a bigger badge just
   collided more). The real fix is moving it off the screenshot entirely,
   into the frame's own chrome bar (.nb-frame__status below) — that layer
   never scales with or scrolls under the site preview, so it can't
   collide with anything in it at any width. The case-study pages use
   .nb-frame__status instead of .nb-badge for this reason; .nb-badge itself
   is untouched, so the homepage cards render exactly as before. */
.nb-frame__status {
  display: inline-flex; align-items: center; gap: .35rem; flex: none;
  margin-left: .5rem;
  padding: .3rem .65rem .3rem .55rem;
  border-radius: 999px;
  background: var(--paper);
  border: 1px solid var(--about-green-35);
  font-family: var(--display); font-weight: 600; font-size: 10px;
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-70);
  white-space: nowrap;
}
.nb-frame__status-dot { width: 6px; height: 6px; flex: none; border-radius: 50%; background: var(--blue); }
@media (max-width: 560px) {
  .nb-frame__status-label { display: none; } /* dot only once the bar gets tight */
  .nb-frame__status { padding-inline: .4rem; }
}
/* the 440px floor above reads fine once there's real width to go with it,
   but on a narrow phone the frame's own width shrinks to the column while
   that floor stays fixed — measured aspect ratio actually flips to taller
   than wide (0.68) at 390px, reading as a tall card instead of a browser
   window. A lower, narrower floor keeps it landscape at every width. */
@media (max-width: 700px) {
  .nb-devices-link--xl .nb-frame--desktop .nb-frame__viewport { height: clamp(230px, 46vw, 400px); }
  .nb-devices-link--xl .nb-frame--phone { width: clamp(112px, 22vw, 190px); }
}

/* ---------- section scaffolding ---------- */
.cs-sec { padding-block: var(--s-7); }
.cs-sec--tight { padding-block: var(--s-6); }
.cs-sec--flush-top { padding-top: var(--s-3); }
.cs-sec--alt { background: var(--paper-2); }
.cs-sec__head { max-width: 62ch; margin-bottom: var(--s-6); }
.cs-sec__head--center { max-width: 56ch; margin-inline: auto; text-align: center; }
.cs-sec__title { margin-top: var(--s-2); color: var(--ink); }
.cs-sec__lead { margin-top: var(--s-4); font-size: var(--t-lead); color: var(--ink-70); text-wrap: pretty; }

/* section eyebrows read black (not the site-wide grey) here — a static
   label colour, no hover state (no colour shift, no scale). */
.cs-sec__head .eyebrow, .studio-phead__eyebrow { color: var(--ink); }
.cs-sec__head .eyebrow::before, .studio-phead__eyebrow::before { background: var(--ink); }

/* ---------- Northbound-only spacing tightening ----------
   Scoped to .page-nb (set on <body> in work/northbound.html only) so
   retro-fitness.html — which shares every class above — keeps its original
   spacing untouched. The base .cs-sec scale matches about.css/services.css's
   own section padding almost exactly, but this page's sections carry less
   copy than a typical about/services section (a device mockup, a palette,
   a photo grid), so the same generous padding reads as dead air around
   them instead of breathing room. Pulled in across the board, with the
   design-direction section (palette + typography, now caption-free) tightened
   further still since it's the shortest content block on the page. */
.page-nb .cs-sec { padding-block: var(--s-5); }
.page-nb .cs-sec--tight { padding-block: var(--s-4); }
.page-nb .cs-sec--flush-top { padding-top: var(--s-2); }
.page-nb .cs-sec__head { margin-bottom: var(--s-4); }
.page-nb .cs-sec__lead { margin-top: var(--s-3); }
.page-nb .studio-phead { padding-top: var(--s-5); padding-bottom: var(--s-4); }
.page-nb .studio-phead__lead { margin-top: var(--s-4); }
.page-nb .cs__meta { margin-top: var(--s-4); padding-top: var(--s-4); }
.page-nb .cs-showcase { margin-top: var(--s-2); }
.page-nb .nb-devices-link--xl { max-width: 1260px; }
.page-nb .cs-direction { gap: var(--s-5); }
@media (max-width: 780px) {
  .page-nb .cs-sec { padding-block: var(--s-4); }
  .page-nb .cs-sec__head { margin-bottom: var(--s-3); }
}

/* ---------- full-width "important section" screenshots ----------
   A tall, real screenshot cropped to a wide window via object-position
   rather than a second set of purpose-cut images — --pos picks which band
   of the original page shows. Frameless (no browser chrome): these read as
   "a closer look," distinct from the device mockup above. */
.cs-shot {
  width: 100%; overflow: hidden;
  border-radius: 14px; border: 1px solid var(--ink-14);
  background: var(--paper-2);
  box-shadow: 0 24px 56px rgba(17, 17, 17, .08);
}
.cs-shot img {
  display: block; width: 100%;
  height: clamp(320px, 38vw, 580px);
  object-fit: cover; object-position: center var(--pos, top);
}
.cs-shot__cap {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-3);
  margin-top: var(--s-3);
  font-family: var(--display); font-size: var(--t-sm); font-weight: 600; color: var(--ink-55);
}
.cs-shot__cap span:last-child { font-weight: 400; color: var(--ink-55); }
.cs-shot-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-5); }
@media (max-width: 780px) {
  .cs-shot-grid { grid-template-columns: 1fr; }
}

/* ---------- design direction: colour + type ---------- */
.cs-direction { display: grid; grid-template-columns: minmax(0, .8fr) minmax(0, 1fr); gap: var(--s-6); align-items: start; }
@media (max-width: 860px) { .cs-direction { grid-template-columns: 1fr; } }

.cs-swatches { display: flex; flex-wrap: wrap; gap: var(--s-4); }
.cs-swatch { width: clamp(64px, 8vw, 84px); }
.cs-swatch__chip {
  width: 100%; aspect-ratio: 1; border-radius: 12px;
  border: 1px solid rgba(17, 17, 17, .08);
  box-shadow: 0 10px 22px rgba(17, 17, 17, .06);
  transition: border-color .25s ease, box-shadow .25s ease, transform .25s cubic-bezier(.2, .7, .2, 1);
}
.cs-swatch__chip:hover {
  border-color: var(--blue);
  box-shadow: 0 10px 22px rgba(17, 17, 17, .06), 0 0 0 3px var(--about-green-16);
  transform: translateY(-2px);
}
.cs-swatch__name { display: block; margin-top: .6rem; font-family: var(--display); font-weight: 600; font-size: var(--t-xs); color: var(--ink); }
.cs-swatch__hex { display: block; font-family: var(--mono, var(--display)); font-size: 11px; color: var(--ink-55); letter-spacing: .02em; }

/* ---------- design direction, palette-only (Northbound) ----------
   Typography sample dropped per request — just the palette, sized to read
   as the section's whole point rather than a small block inside a wide
   empty one. */
.cs-palette-full { width: 100%; }
.cs-swatches--full { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s-5); }
.cs-swatches--full .cs-swatch { width: 100%; }
.cs-swatches--full .cs-swatch__chip { border-radius: 18px; }
.cs-swatches--full .cs-swatch__name { margin-top: var(--s-3); font-size: var(--t-sm); }
.cs-swatches--full .cs-swatch__hex { font-size: 12px; }
@media (max-width: 700px) {
  .cs-swatches--full { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* type samples get the same card treatment as the swatch chips (padding,
   border, shadow) — plain text lines next to a row of bold colour squares
   read as thin and unbalanced, so each sample earns its own visual weight
   instead of floating loose in the column. */
.cs-type { display: grid; gap: var(--s-3); }
.cs-type__card {
  padding: var(--s-3) var(--s-4);
  background: var(--paper);
  border: 1px solid rgba(17, 17, 17, .08);
  border-radius: 12px;
  box-shadow: 0 10px 22px rgba(17, 17, 17, .06);
}
.cs-type__sample { line-height: 1; letter-spacing: -.02em; color: var(--ink); }
.cs-type__label {
  margin-top: var(--s-3);
  font-family: var(--display); font-size: var(--t-xs); font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-55);
}

/* ---------- feature / decision callouts ---------- */
.cs-features { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-5); }
@media (max-width: 780px) { .cs-features { grid-template-columns: 1fr; } }
.cs-feature__num {
  font-family: var(--display); font-weight: 700; font-size: var(--t-sm); color: var(--blue-ink);
  margin-bottom: var(--s-2);
}
.cs-feature__title { font-family: var(--display); font-weight: 700; font-size: var(--t-h3); letter-spacing: -.02em; }
.cs-feature__body { margin-top: .5rem; color: var(--ink-70); font-size: var(--t-sm); text-wrap: pretty; }

/* ---------- responsive comparison ---------- */
.cs-responsive { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, .42fr); gap: var(--s-5); align-items: end; }
@media (max-width: 780px) { .cs-responsive { grid-template-columns: 1fr; } }
/* width:100% (not just max-width) on both frames — needed because their
   images have such extreme intrinsic aspect ratios (the mobile shot is
   390x10654, ~28:1 tall) that CSS grid's width:auto sizing falls back to
   "natural width implied by the explicit height" when computing the grid
   item's own width, since a percentage width can't resolve during that
   intrinsic-size pass. That collapsed the phone frame to ~19px wide. An
   explicit width sidesteps needing the image's intrinsic ratio at all. */
.cs-responsive__desktop {
  width: 100%;
  border-radius: 14px; border: 1px solid var(--ink-14); overflow: hidden;
  box-shadow: 0 24px 56px rgba(17, 17, 17, .08);
}
.cs-responsive__desktop img { display: block; width: 100%; height: clamp(260px, 30vw, 460px); object-fit: cover; object-position: top; }
.cs-responsive__phone {
  width: 100%; max-width: 260px; margin-inline: auto;
  border-radius: 18px; border: 1px solid var(--ink-14); overflow: hidden;
  box-shadow: 0 24px 56px rgba(17, 17, 17, .08);
}
.cs-responsive__phone img { display: block; width: 100%; height: clamp(360px, 40vw, 520px); object-fit: cover; object-position: top; }

/* ---------- placeholder slot (missing screenshot/info) ---------- */
.cs-placeholder {
  display: flex; align-items: center; justify-content: center; text-align: center;
  width: 100%; height: 100%; min-height: 220px; box-sizing: border-box;
  border: 2px dashed rgba(17, 17, 17, .3); border-radius: 12px;
  color: var(--ink-55); font-family: var(--display); font-size: var(--t-xs); font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; padding: var(--s-3);
}

/* ---------- status label ---------- */
.cs-status {
  display: inline-flex; align-items: center; gap: .45rem;
  font-family: var(--display); font-weight: 600; font-size: var(--t-xs);
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-70);
  border: 1px solid var(--ink-14); border-radius: 999px; padding: .35rem .75rem;
}
.cs-status__dot { width: 6px; height: 6px; border-radius: 50%; background: var(--blue); flex: none; }

/* ---------- portfolio grid ----------
   A bento/mosaic mix of sizes and shapes that interlock — one --big tile,
   two --tall, two --wide, the rest square-ish — rather than a uniform
   grid of identical cards, so the photography itself reads as varied and
   deliberate. grid-auto-flow: dense packs every size around the others
   with no manual row math. No captions: just the photographs, scaling and
   lightly darkening on hover/focus as the only interaction. */
.cs-portfolio {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-auto-rows: clamp(110px, 11vw, 168px);
  grid-auto-flow: dense;
  gap: var(--s-3);
  margin-top: var(--s-2);
}
.cs-portfolio__item--big  { grid-column: span 2; grid-row: span 2; }
.cs-portfolio__item--wide { grid-column: span 2; grid-row: span 1; }
.cs-portfolio__item--tall { grid-column: span 1; grid-row: span 2; }
.cs-portfolio__item {
  margin: 0; position: relative; border-radius: 14px; overflow: hidden;
  box-shadow: 0 18px 40px rgba(17, 17, 17, .08);
}
.cs-portfolio__media { width: 100%; height: 100%; overflow: hidden; background: var(--paper-2); }
/* one transform, composed from three variables — see the same pattern on
   .pf-item in studio.css. --tilt-s is the hover zoom, --tilt-x/--tilt-y the
   cursor-position drift site.js writes onto the hovered [data-tilt] tile, and
   --scroll-s the slow scroll-linked creep studio.js writes onto a
   [data-scroll-scale] tile. Multiplying the two scales rather than having
   two rules set `scale` is what lets all three run at once. */
.cs-portfolio__media img {
  display: block; width: 100%; height: 100%; object-fit: cover;
  transform: translate3d(var(--tilt-x, 0px), var(--tilt-y, 0px), 0)
             scale(calc(var(--tilt-s, 1) * var(--scroll-s, 1)));
  transition: transform var(--dur-1) var(--ease-entry);
}
/* for logo/product art that needs to stay fully visible rather than being
   cropped by object-fit: cover — same idea as site.css's .gitem__media--contain */
.cs-portfolio__media--contain { background: var(--white); }
.cs-portfolio__media--contain img { object-fit: contain; padding: 10%; }

/* ---------- natural-ratio variant ----------
   The default grid locks every row to a fixed unit height (clamp(110px,
   11vw,168px)) so mixed --big/--wide/--tall spans interlock into a bento —
   right for Northbound's landscape photography, but a handful of near-
   square product shots (Retro Fitness's bag/card/signage) either got
   cropped by cover or left with visible white padding under contain.
   This variant instead sizes each item by its own --ar custom property, so
   the box IS the photo's shape: no crop, no letterboxing. */
/* CSS Grid rows share a single height per row line across every column, so
   letting items size purely by their own --ar (mixed aspect ratios) left
   visible empty gaps under the shorter items sharing a row with a taller
   one. Multi-column layout sizes each column independently instead — the
   standard lightweight masonry trick — so heights never have to agree. */
.cs-portfolio--natural {
  display: block; column-count: 3; column-gap: var(--s-3);
}
.cs-portfolio--natural .cs-portfolio__item {
  display: block; width: 100%; break-inside: avoid; margin-bottom: var(--s-3);
}
.cs-portfolio--natural .cs-portfolio__item--big { column-span: all; }
.cs-portfolio--natural .cs-portfolio__media { height: auto; aspect-ratio: var(--ar, 1); }
@media (max-width: 780px) {
  .cs-portfolio--natural { column-count: 2; }
}
@media (max-width: 480px) {
  .cs-portfolio--natural { column-count: 1; }
}
.cs-portfolio__item::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(155deg, rgba(19, 28, 36, .16), rgba(19, 28, 36, 0) 60%);
  opacity: 0; transition: opacity .35s ease;
}
.cs-portfolio__item:hover .cs-portfolio__media img,
.cs-portfolio__item:focus-within .cs-portfolio__media img { --tilt-s: 1.08; }
.cs-portfolio__item:hover::after,
.cs-portfolio__item:focus-within::after { opacity: 1; }

/* ---------- entrance ----------
   A plain, clearly-visible rise and fade — no blur (a paint cost not worth
   paying across 12+ large photographs at once) and no mask wipe (too subtle
   to register as "an animation" on tiles this size in a dense grid). Each
   tile lifts 22px as it fades in, staggered by the --d the group already
   sets, which is the same reveal language as everything else on the site,
   just without the blur step. `translate`, not `transform`, so this can't
   collide with the hover zoom, which rides --tilt-s on the same element. */
.cs-portfolio .cs-portfolio__item.about-reveal {
  opacity: 0; translate: 0 22px; filter: none; transform: none;
  transition: opacity var(--dur-3) var(--ease-entry) var(--d, 0ms),
              translate var(--dur-3) var(--ease-entry) var(--d, 0ms);
}
.cs-portfolio .cs-portfolio__item.about-reveal.is-in { opacity: 1; translate: none; }
.no-js .cs-portfolio__item.about-reveal { opacity: 1; translate: none; }
@media (prefers-reduced-motion: reduce) {
  .cs-portfolio .cs-portfolio__item.about-reveal { opacity: 1; translate: none; }
}

/* below 900px the 4-column combinatorics (big/wide/tall spans dense-packed
   against 3 or 2 columns) can leave an odd cell nothing fits — so only the
   one --big lead tile keeps its span; --wide/--tall fall back to a single
   cell, which always tiles cleanly at any column count. */
@media (max-width: 900px) {
  .cs-portfolio { grid-template-columns: repeat(3, minmax(0, 1fr)); grid-auto-rows: clamp(96px, 15vw, 140px); }
  .cs-portfolio__item--big { grid-column: span 2; grid-row: span 2; }
  .cs-portfolio__item--wide, .cs-portfolio__item--tall { grid-column: span 1; grid-row: span 1; }
}
@media (max-width: 560px) {
  .cs-portfolio { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: clamp(84px, 26vw, 130px); gap: var(--s-2); }
  .cs-portfolio__item--big { grid-column: span 2; grid-row: span 2; }
}
@media (prefers-reduced-motion: reduce) {
  .cs-portfolio__media img { transition: none; }
}
