/* ==========================================================================
   Case study pages — the layout grid, then the components the copy needs.
   Every case study is assembled from these; nothing is page-specific.

   SCOPE: everything that changes shared layout is behind `.cs-page`, the class
   on <body> of the five project pages. The home page, the projects index and
   about/contact are deliberately untouched and still run the site's fluid
   --gutter and the --rail offset.
   ========================================================================== */

/* --- The container ------------------------------------------------------- */
/* Side padding is the site's own --gutter: 20px on a phone, 40px from 1000px
   up. It was pinned to a flat 40px originally, which held on desktop but spent
   80px of a 375px screen — a fifth of the viewport — on margin.

   Content still stops growing at 1920px; past that the page centres and the
   extra space falls outside the padding, so the padding never grows into a
   margin. box-sizing is border-box globally, so max-width 2000 − 2×40 = 1920
   of content at the cap. */
.cs-page {
  /* How far a label sits from the thing it labels, whenever the two are
     stacked rather than side by side. Used in two places that must agree: the
     row gap when .cs-row collapses on a narrow screen, and the space under the
     top rail — where "Case study" is stacked above the project title at every
     width, not just narrow ones. Naming it is the point; as two literals they
     drifted to 16px and 40px.

     --sp-6, matching the row gap .cs-col falls back to below 62rem. At --sp-4
     it was tighter than the gap between the content rows underneath it, so on a
     narrow screen the back link, the depth label and the title read as one
     undifferentiated stack instead of three steps. A label should not sit
     closer to its neighbour than two body rows sit to each other. */
  --cs-label-gap: var(--sp-6);
}

.cs-page .container {
  max-width: calc(1920px + 2 * 40px);
  padding-inline: var(--gutter);
}

/* --- The section row ----------------------------------------------------- */
/* label | content, 1fr 4fr with NO column gap, so the content column starts at
   exactly 20% of the net width and the label column is the full 20% to write
   in. This replaces the `margin-left: var(--rail)` offset the case studies used
   to carry: same 20% line, but now the label has a real cell to sit in. */
.cs-row {
  display: grid;
  grid-template-columns: 1fr 4fr;
  column-gap: 0;
}

/* A row with no label — a full-bleed figure, say — still starts on the line. */
.cs-row > .cs-col:only-child {
  grid-column: 2;
}

.cs-row + .cs-row {
  margin-top: var(--space-block);
}

/* Keeps a long label off the content line it is labelling */
.cs-row__label {
  padding-right: var(--sp-6);
}

/* --- The content column -------------------------------------------------- */
/* Six columns and a 32px gutter. Everything inside a section lands on this one
   grid, which is what makes a paragraph, a headline and a comparison table
   share the same left and right edges. */
.cs-col {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  /* Split deliberately. 32px is structural — it is the six-column gutter that
     every edge in a section lands on — while the vertical rhythm is free to be
     looser, so the space under a headline reads as separation rather than as
     the same gap turned sideways. */
  row-gap: var(--grid-row-gap);
  column-gap: var(--grid-gutter);
  align-content: start;
}

/* Default: full width of the six. Anything narrower opts in below. */
.cs-col > * {
  grid-column: 1 / -1;
}

/* Headline: four of six. Deliberately narrower than the two paragraphs
   beneath it, and balanced so a two-line headline breaks into even lines
   instead of one long line and one orphan. */
.cs-col > .cs-section__title {
  grid-column: span 4;
  max-width: none;
  text-wrap: balance;
}

/* Body paragraphs: three of six each, so a pair sits side by side — with the
   text itself at 70% of that span. The ~30% to the right of every paragraph is
   the point, not an unfinished column. */
.cs-section__body {
  display: contents;
}

.cs-col > .cs-section__body > p {
  grid-column: span 3;
  width: 70%;
  max-width: none;
  margin-top: 0;
}

/* An explicit body column, for the sections whose two sides are not a matched
   pair of paragraphs — prose on the left, a lead-in and a list on the right.
   Same span and same 70% measure as a loose paragraph, so the text edges sit
   exactly where they do in every other section; the only difference is that
   this one stacks internally instead of flowing on to the next row. */
.cs-col > .cs-section__body > .cs-body-col {
  grid-column: span 3;
  width: 70%;
}

/* Prose spacing, not the 40px grid row gap: these are paragraphs inside one
   column, not two things the grid is separating. */
.cs-body-col > * + * {
  margin-top: var(--sp-6);
}

/* A lead-in and the list it introduces are one thought: 16 between them, not
   the 24 between paragraphs, so the colon reads as attached to its items. */
.cs-body-col > p + .cs-bullets {
  margin-top: var(--sp-4);
}

/* 16 between items. At 12 it was barely more than the leading inside a
   two-line item (~10px), so the wrapped lines didn't read as belonging to
   their own bullet. */
.cs-bullets {
  display: grid;
  gap: var(--sp-4);
}

.cs-bullets li {
  display: flex;
  gap: 0.5em;
}

/* A short drawn rule, centred on the first line. It used to be an em-dash
   glyph: long, heavy, and pushing the text 28px in from a column that is only
   ~380px wide. The copy no longer uses em dashes either, so the old reason
   for it is gone. The x of .cs-table__list is not reusable here — there it
   means "missing from this option". */
.cs-bullets li::before {
  content: "";
  flex: none;
  width: 0.5em;
  height: 1.5px;
  margin-top: calc(0.5lh - 0.75px);
  border-radius: 1px;
  background: var(--color-muted);
}

/* --- Head ---------------------------------------------------------------- */

.cs-head {
  padding-top: clamp(var(--sp-10), 7vw, var(--sp-20));
  /* The head is a band too — it just happens to be the same colour as the page.
     It needs the bottom padding so its cover figure isn't flush against the
     first coloured band below it. */
  padding-bottom: var(--cs-band-pad);
}

/* Back link, then the depth label directly above the project title. The grid
   comes from .cs-row, which is one column at every width since the rail went
   (pages.css), so the two stack in source order.

   The back link is navigation, not a label: it gets the 40 step above the
   depth label, so "Case study" stays with the title it introduces
   (--cs-label-gap below it) instead of the three reading as one stack. Two
   classes, so it outranks the .cs-row row-gap that pages.css sets later. */
.cs-head__top {
  align-items: baseline;
  margin-bottom: var(--cs-label-gap);
}

.cs-row.cs-head__top {
  row-gap: var(--sp-10);
}

.back-link {
  display: inline-flex;
  /* Centre, for the same reason .link-arrow does — see base.css */
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  color: var(--link-ink);
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease);
}

/* One arrow asset for the whole site, so the back link flips the right-pointing
   one rather than carrying a second path that could drift from it. */
.back-link .arrow {
  transform: scaleX(-1);
}

.back-link:hover,
.back-link:focus-visible {
  color: var(--link-hover);
}

/* The flip is restated, or hover would drop it and the arrow would snap round
   mid-nudge. Inside a scaleX(-1) frame a positive translate reads as leftward,
   which is the direction this one points. Same function list either side, so
   the two interpolate. */
.back-link:hover .arrow,
.back-link:focus-visible .arrow {
  transform: scaleX(-1) translateX(0.28em);
}

/* --- Overview ------------------------------------------------------------- */
/* The page's opening block, and its own component rather than a section with an
   empty label: it has no label column, but it does start on the same line the
   sections do.

   That indent is what makes the alignment work rather than just the margin
   look right. --rail is 20% of the container's content box, which is the same
   box a section's 1fr/4fr split measures against — so the Overview's six
   columns and a section's six columns come out identical, and the h1, the lede
   and every section headline below share the same left and right edges. Run it
   at the full net width instead and it is both too wide to read and out of step
   with everything under it.

   Bigger title, lede-sized copy, and 20px more air under the title than a
   section headline gets — the values are the ones the hand-built pages used. */

.cs-overview,
.cs-overview__cover {
  margin-left: var(--rail);
}

.cs-overview {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  row-gap: var(--grid-row-gap);
  column-gap: var(--grid-gutter);
  align-content: start;
}

.cs-overview > * {
  grid-column: 1 / -1;
}

.cs-overview__title {
  grid-column: span 4;
  max-width: none;
  margin-bottom: 20px;
  text-wrap: balance;
}

/* display: contents so the leads are grid items of .cs-overview itself */
.cs-overview__intro {
  display: contents;
}

/* Body size, not lead size: the overview reads as the opening of the copy
   rather than a pull-quote above it. */
.cs-overview__intro > p {
  grid-column: span 3;
  width: 70%;
  margin-top: 0;
  font-size: var(--fs-body);
}

.cs-overview__cover {
  margin-top: var(--space-block);

  /* A tinted cover sits on the cover ground, not the screenshot tint. */
  --figure-tint: var(--cover-ground);
}

/* The cover's caption is centred, where a caption inside a section stays
   ranged left on the text column. The cover is the one figure that is not in
   the six-column body: it runs the full width from the rail out, with no
   column edge for a caption to range against, so left-aligning it parks a
   short line under a very wide plate. Centred, it reads as belonging to the
   picture — the same reasoning as the per-shot captions inside a stack.
   Direct children only, so a nested shot caption keeps its own rule. */
.cs-overview__cover > .figure > figcaption {
  text-align: center;
}

/* --- Facts strip ---------------------------------------------------------- */
/* Company / When / Timeframe / Tools, on the page's own ground rather than a
   fill. The container draws only its top and left edges and each cell draws
   its right and bottom — so every line is drawn exactly once, with no doubling
   at the outer edges, and it stays correct at any column count. */

.facts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  border-top: 1px solid var(--color-line);
  border-left: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

@media (min-width: 62rem) {
  .facts {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

.facts__cell {
  padding: var(--sp-4) var(--sp-5);
  background: transparent;
  border-right: 1px solid var(--color-line);
  border-bottom: 1px solid var(--color-line);
}

.facts dt {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  color: var(--color-muted);
}

.facts dd {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-small);
  line-height: 1.4;
}

/* --- Team and focus ------------------------------------------------------- */

/* --- Sections ------------------------------------------------------------ */

/* Sections are alternating coloured bands rather than a single sheet with big
   gaps. Each carries --cs-band-pad top and bottom, so the space between two
   blocks of content is twice that, and the two grounds do the separating.

   Counted from the END (nth-last-of-type): the last section — the one the
   white footer sits on — is always the tint, whatever the page's length, so
   the footer never meets a white section. The top takes whichever ground
   falls to it; a white first section is fine, because the head ends on the
   project's coloured cover. The bands are their own near-neutral ground,
   --ground-band, not the home page's light blue, which competed with other
   brands' screenshots. (.cs-head is an <article>, so it isn't counted.) */
.cs-section {
  padding-block: var(--cs-band-pad);
}

.cs-section:nth-last-of-type(odd) {
  background: var(--ground-band);

  /* --figure-tint IS --ground-band, so a tinted plate on this band would
     vanish into it. Flip the two on tinted bands: the plate takes the page
     colour, and a transparent screenshot still sits on something. */
  --figure-tint: var(--color-light);
}



/* --- Figures ------------------------------------------------------------- */

.cs-figure {
  margin: 0;
}

.cs-figure--plain .figure__plate {
  background: transparent;
  box-shadow: none;
}

/* For exports with a transparent background: the plate supplies the ground and
   the corner, so both stay editable. The radius has to be set on the <img> too
   — the plate clips with overflow: hidden, and a child that fills its parent
   exactly will still square off the corners on some engines. */
.cs-figure--tint .figure__plate {
  background: var(--figure-tint);
  border-radius: var(--figure-tint-radius);
  /* No lift. The tint already separates the shot from the page, and a shadow
     under a soft grey plate reads as a second, competing edge. */
  box-shadow: none;
}

.cs-figure--tint .figure__plate img {
  border-radius: var(--figure-tint-radius);
}

/* --- Composed screen stack ------------------------------------------------- */
/* Track 1: several screens exported separately and composed here, rather than
   one flattened file. A flattened composition is only ever sharp at the size it
   was flattened for; separate exports let the browser pick the resolution and
   let the spacing scale with the image.

   The spacing is PERCENTAGES on purpose, and this is the one place on the site
   where the fixed 16/24/40/56/80 scale does not apply. That scale positions
   things on the page, where 40 is 40 at every width. Here the margin and the
   gap belong to the picture: they have to hold the same ratio to the screen at
   335px wide as at 1536, and no fixed value can do that. At the 1536 cap these
   resolve to ~103px; at a 1440 viewport ~73px; on a phone ~22px.

   The two numbers are one ratio expressed against two different boxes. The
   brief's ratio is 7.75% OF A SCREEN. The padding is set against the PLATE, so
   it has to be restated: p = 7.75 / (100 + 2 x 7.75) = 6.71% of the plate makes
   the margin 7.75% of the screen that remains. The gap is a margin-top on the
   image, which resolves against the stack's own content box — the screen width
   — so it takes the ratio as given.

   margin-top and not `gap`: a percentage row-gap resolves against the content
   box's BLOCK size, which is indefinite on an auto-height column, so it would
   collapse to zero. */

.figure__stack {
  padding: 6.71%;

  /* The container context lives HERE, not on .figure__shot, and that is not a
     detail. An element that establishes a container does not resolve its own
     cq units against itself — they resolve against the nearest ANCESTOR
     container. Put container-type on the shot and its own box-shadow would
     find no container above it and fall back to the viewport, which is exactly
     the thing we are avoiding.

     The stack's content box is the shot width — the shots are 100% of it — so
     1cqw here IS 1% of a screen. Same reasoning as the 7.75% spacing: the
     measurement belongs to the picture, not the page. */
  container-type: inline-size;
}

/* The gap is between ITEMS, not between screens: a caption belongs to the
   screen above it, so screen-plus-caption is the unit the rhythm separates.
   Without this the caption would float midway between two screens. */
.figure__stack .figure__shot-item {
  margin: 0;
}

.figure__stack .figure__shot-item + .figure__shot-item {
  margin-top: 7.75%;
}

/* A per-shot caption inherits the mono face, size and colour from
   `.figure figcaption` in base.css — it is the same object as any other
   caption, just nested — and overrides two things.

   The offset is a fixed token rather than a percentage: the 7.75% belongs to
   the picture and has to scale with it, but this is the gap between a line of
   type and the thing it labels, which is typographic. 16 rather than the 12
   other captions take, because this one sits INSIDE the plate and needs to
   separate from the screen above it rather than just trail it.

   Centred rather than ranged left, so the caption reads as belonging to the
   screen it sits under. The figure's own caption below the plate stays left,
   on the text column — that difference is what distinguishes the two. */
.figure__stack .figure__shot-item > figcaption {
  margin-top: var(--sp-4);
  text-align: center;
}

/* The wrapper owns the corner and the hairline, not the file. Clipping here
   rather than baking a radius into the export is what lets the corner change
   without a re-export — and these exports carry a few px of their own
   background past the screen's edge, so a radius baked in would have rounded
   that margin rather than the screen. overflow: clip would be tidier but the
   hidden fallback is needed for older Safari, and there is nothing to scroll. */
.figure__shot {
  border-radius: var(--radius-lg);
  overflow: hidden;
  /* --color-chip, not --color-line. The hairline weight the rest of the site
     uses is too strong here: it competes with the screen's own chrome instead
     of just separating it from the plate. The chip is an ALPHA, so it paints a
     different hex on each band and keeps the same strength on both — a fixed
     grey would drift. */
  border: 1px solid var(--color-chip);
  /* So the border eats into the wrapper rather than widening it — the 7.75%
     margin and gap then measure to the hairline, which is the visible edge. */
  box-sizing: border-box;

  /* Track 1 only. These screens are exported flat, so the lift is CSS; the
     older flattened compositions carry their own shadow in the file and would
     double up.

     cqw, not px: measured at a 3873px reference screen, 50px blur and 30px
     spread are 1.29% and 0.775% of a screen. A screen renders anywhere from
     ~290 to ~1330 CSS px here, so a fixed px shadow would be heavy on a phone
     and invisible at the cap. Percentages are not available to box-shadow,
     which is what container units are for.

     The max() floor is for phones. Below about a 310px screen the proportional
     value falls under ~4px of blur, which stops reading as a lift and starts
     reading as a smudge — a shadow needs a few pixels to be a shadow at all.
     2.4 and not 2.5 for the spread: 4 x (0.775 / 1.29) = 2.4, so both floors
     engage at the SAME screen width and the blur:spread ratio survives the
     crossover. Above ~310px neither floor is reached and the scaling is purely
     proportional.

     Where cq units are unsupported the whole declaration is dropped and the
     screens simply sit flat — the same Safari 16 floor the site already has. */
  box-shadow: 0 0 max(4px, 1.29cqw) max(2.4px, 0.775cqw)
    color-mix(in srgb, var(--color-dark) 12%, transparent);
}

.figure__shot img {
  display: block;
  width: 100%;
  height: auto;
}

/* A flattened export whose shadow is baked in and whose silhouette is not a
   rectangle — a card stack, panels at different depths. It keeps the plate
   room and the caption slot and loses everything that assumes a box: the
   hairline would frame the transparent bounding box rather than the artwork,
   the radius and the clip would cut the shadow's falloff square, and a second
   lift would sit under the one already in the file. */
.figure__shot--bare {
  border: 0;
  border-radius: 0;
  overflow: visible;
  box-shadow: none;
}

/* --- Screens side by side ------------------------------------------------- */
/* Narrow panels exported one per file and laid across: the per-issue drawers.
   The plate, the 6.71% room, the tint, the corner and the caption are all the
   stack's — the row sits inside .figure__stack > .figure__shot-item like any
   other group. Only the axis and the phone behaviour are new here.

   The desktop gap is a fixed 30px, not a percentage: the three panels already
   scale with the plate, and the gap between them is the one thing the brief
   pins.

   max-width is 1x of the export. The drawers are 460px in the design and
   exported at 906px, so 453px is the widest a drawer should ever render —
   3 x 453 + 2 x 30 = 1419. In practice the stack padding keeps them under it:
   at the 1536 plate cap the row is ~1330 wide and a drawer ~423, which is
   2.14 device pixels per CSS pixel. The cap is there so a wider container
   later cannot quietly upscale them. */
.figure__row {
  /* The page's full-width screen, as height over width: the 2880 x 1580
     Security tab screens (Findings, Scan history). At 100cqw of a stack,
     that is how tall they render — what both rows below size against. */
  --row-anchor: calc(1580 / 2880);
  --row-gap: 30px;
  display: flex;
  gap: var(--row-gap);
  max-width: calc(3 * 453px + 2 * var(--row-gap));
  margin-inline: auto;
}

.figure__row-item {
  flex: 1 1 0;
  min-width: 0;
  /* Each ITEM is the container, so 1cqw inside it is 1% of a drawer. See the
     stack's note on why the container must be the shot's parent, not the shot. */
  container-type: inline-size;
}

/* The lift, restated for a drawer. The stack's 1.29 / 0.775 are percentages
   of a 1440-wide design screen; a drawer is 460 wide in the same design and
   renders at the same scale, so the same physical shadow is 1440/460 = 3.13x
   those numbers in drawer terms. Same floors, so the phone crossover is
   unchanged. Blur + spread = 6.47% of a drawer, which the phone track uses. */
.figure__row .figure__shot {
  box-shadow: 0 0 max(4px, 4.04cqw) max(2.4px, 2.43cqw)
    color-mix(in srgb, var(--color-dark) 12%, transparent);
}

/* Phones: a horizontal snap scroller inside the plate.

   Nothing is meant to be read at this size — no screen on the page is, on a
   phone. What matters is that the cards sit in PROPORTION to the wide screens
   around them. So a card's height is anchored to a full-width screen in the
   same stack: 90% of the height a 2880 x 1580 export has at 100cqw (Scan
   history, directly above). Width follows from the card's own aspect ratio,
   which the component writes on the row as --card-aspect, so a row of phone
   screens or any other tall panels takes the same rule with no new numbers.

   The gap is two-thirds of a card, not a fixed step. At this card size a
   24px gap lets the third card show ~80% of itself, which reads as clipped
   rather than as a peek. At 2/3 the third card shows ~40% at every phone
   width, because both terms scale with the stack.

   The track runs out through the stack's side padding to the PLATE edge, so a
   card slides under the plate's rounded edge rather than stopping short of
   it. Its own inline padding puts the room back, so the first card rests on
   the stack's usual inset and the last one snaps to the same inset on the
   right. All three are 7.75cqw: the stack is the container, 1cqw is 1% of its
   content box, and 7.75% of that IS the 6.71%-of-plate padding restated.

   overflow-x: auto forces overflow-y to clip too. The block padding is the
   lift's reach — blur + spread, floors included — and the matching negative
   margin gives it back, so the shadow has room without the plate growing. */
@media (max-width: 47.999rem) {
  /* The figure is a grid item, and a grid item's automatic minimum is its
     min-content — which for a scroller is the whole track. Without this the
     row widens the page instead of scrolling. */
  .cs-figure--row {
    min-width: 0;
  }

  .figure__row {
    --card-h: calc(0.9 * var(--row-anchor) * 100cqw);
    --card-w: min(calc(var(--card-h) * var(--card-aspect)), 453px);
    --row-gap: calc(var(--card-w) * 2 / 3);
    --reach: calc(max(4px, 0.0404 * var(--card-w)) + max(2.4px, 0.0243 * var(--card-w)));
    max-width: none;
    margin: calc(-1 * var(--reach)) -7.75cqw;
    padding: var(--reach) 7.75cqw;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: 7.75cqw;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }

  .figure__row::-webkit-scrollbar {
    display: none;
  }

  .figure__row-item {
    flex: 0 0 var(--card-w);
    scroll-snap-align: start;
  }
}

/* --- Device screens ------------------------------------------------------- */
/* A row whose files are a whole device: transparent exports of a phone
   outline with its own very rounded silhouette (figure_row(device=True)).

   Everything that assumes a rectangle goes: the hairline would frame the
   transparent bounding box, the 4px radius and the clip would cut the
   outline's own corners square, and a box-shadow would sit under the box,
   not the phone. The lift becomes filter: drop-shadow, which follows the
   alpha. A filter is not clipped by its own element, and the stack's
   6.71% room is wider than its reach at every width, so the plate's
   overflow: hidden never reaches it either.

   Matching the other Track 1 lifts. drop-shadow has no spread, so the
   box-shadow numbers cannot be copied; they are fitted instead. Restated for
   a phone first: the stack's 1.29 / 0.775 are % of a 1440 design screen, and
   a phone is 417 wide in the same design, so blur b = 4.46% and spread
   s = 2.68% of a phone (x 1440/417). A box-shadow's falloff at distance d
   from the edge is a * Phi((s - d) / (b/2)), with a = 12%. A drop-shadow's
   is a' * Phi(-d / (B/2)). Equal darkness AT the edge (d = 0) gives
   a' = 1.77a = 21%; equal half-strength point (d = s) gives B = 2.08b =
   9.28% of a phone. Floors follow the same 2.08x: 4px of blur -> 8.3px.
   Same colour, same strength where the eye reads it; the far tail is a
   touch longer, which is the shape of a drop-shadow. */
/* Desktop: sized by HEIGHT. A phone is as tall as --device-height percent of
   the Security tab screens in the same section — the one value to tune by
   eye; 85 sits the phones just under them. Width follows the file's own
   aspect ratio (--card-aspect, written by the component), so the 834 x 1752
   exports are never stretched, and the pair centres with its 30px gap.
   Both rows' stacks are the same width, so 100cqw here is exactly the width
   those screens render at — the anchor needs no measuring.

   No 1x cap is needed: even at the 1536 plate cap a phone comes out ~295px
   wide, well under the 417 that would start upscaling a 2x export. */
.figure__row--device {
  --device-height: 85;  /* phones: its own value, in the rule further down */
  --card-h: calc(var(--device-height) / 100 * var(--row-anchor) * 100cqw);
  --card-w: calc(var(--card-h) * var(--card-aspect));
  max-width: none;
  justify-content: center;
}

.figure__row--device .figure__row-item {
  flex: 0 0 var(--card-w);
}

.figure__shot--device {
  border: 0;
  border-radius: 0;
  overflow: visible;
}

.figure__row .figure__shot--device {
  box-shadow: none;
  filter: drop-shadow(0 0 max(8.3px, 9.28cqw)
    color-mix(in srgb, var(--color-dark) 21%, transparent));
}

/* Phones: no carousel, and sized by HEIGHT too, against the same Security
   tab screens — with its own value, because what matters here is the drawers
   row just above. Those are 90% of a tab screen's height; at 115 the phones
   are ~1.3x a drawer's height and about a drawer's width, so they read as a
   little larger, not as a different scale. Width from the file's aspect.

   The gap is two-thirds of a phone, the drawers' rule, so it opens up with
   the phones instead of staying a hairline between them. The pair centres.
   Undo the drawers' scroller first. */
@media (max-width: 47.999rem) {
  .figure__row.figure__row--device {
    --device-height: 115;
    --row-gap: calc(var(--card-w) * 2 / 3);
    max-width: none;
    margin: 0 auto;
    padding: 0;
    overflow: visible;
    scroll-snap-type: none;
  }

  .figure__row--device .figure__row-item {
    scroll-snap-align: none;
  }
}

/* --- The comparison / data table ------------------------------------------ */
/* ONE component for every set of parallel items: competitor cards, numbered
   specs, explored options, impact figures, design versions. They were five
   near-identical blocks of CSS; changing the border colour meant five edits.

   Hairline box: the container draws top + left, each cell draws right + bottom.
   Every line is therefore drawn exactly once, it stays correct at any column
   count, and a part-filled last row simply closes itself and stops — which is
   what lets three versions sit in a two-column table with no dangling edge.

   No gap: the shared borders are the separation. The column count comes from
   build/components.py, which caps at three across. */

.cs-table {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
  border-top: 1px solid var(--color-line);
  border-left: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

@media (min-width: 62rem) {
  .cs-table--2 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .cs-table--3 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.cs-table__cell {
  padding: var(--sp-6);
  background: transparent;
  border-right: 1px solid var(--color-line);
  border-bottom: 1px solid var(--color-line);
}

/* Fields carry their own top margin so any combination of them stacks with the
   right rhythm — but the first one in a cell has nothing to sit under. Without
   this, a cell that opens on a name instead of a kicker would be pushed down by
   a margin meant to separate it from something that isn't there. */
.cs-table__cell > *:first-child {
  margin-top: 0;
}

.cs-table__kicker {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  color: var(--color-muted);
}

/* The one that marks the direction actually taken forward */
.cs-table__kicker.is-accent {
  color: var(--color-accent);
}

.cs-table__name {
  margin-top: var(--sp-3);
  font-size: var(--fs-h3);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-normal);
}

/* A display figure stands in for the name */
.cs-table__value {
  /* The top step, level with the page title: the figure stands in for a name
     and is the loudest thing in its table. */
  font-size: var(--type-6);
  font-weight: var(--fw-medium);
  line-height: 1;
  letter-spacing: var(--ls-tight);
}

.cs-table__desc {
  margin-top: var(--sp-3);
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--color-muted);
  max-width: 22ch;
}

/* For a phrase that should hold one line rather than break to the measure */
.cs-table__desc--wide {
  max-width: none;
}

/* The direction that was taken forward reads at full strength; the ones that
   were not stay muted, so the cell says which won without a label saying so. */
.cs-table__cell.is-chosen .cs-table__desc {
  color: var(--color-text);
}

.cs-table__body {
  margin-top: var(--sp-3);
  font-size: var(--fs-small);
  line-height: 1.5;
}

.cs-table__body > p + p {
  margin-top: var(--sp-3);
}

.cs-table__list {
  margin-top: var(--sp-4);
  display: grid;
  gap: var(--sp-2);
  font-size: var(--fs-small);
  line-height: 1.5;
}

.cs-table__list li {
  display: flex;
  gap: var(--sp-3);
}

.cs-table__list li::before {
  content: "\00d7";
  flex: none;
  color: var(--color-muted);
  line-height: 1.5;
}

.pull-quote {
  margin-top: var(--sp-5);
  padding-left: var(--sp-5);
  border-left: 1px solid var(--color-line-strong);
  font-size: var(--fs-body);
  line-height: 1.45;
}

/* --- Learnings ------------------------------------------------------------ */

.learnings {
  display: grid;
  gap: 0;
}

.learning {
  padding-block: var(--sp-6);
  border-top: 1px solid var(--color-line);
  max-width: 62ch;
}

/* The group closes itself. It used to borrow the .cs-next hairline that sat
   immediately below it; that block is its own band now, so there is nothing
   underneath to close on. Only the ruled variant — .learnings--split draws no
   rules at all, which is why it is excluded rather than reset afterwards. */
.learnings:not(.learnings--split) {
  border-bottom: 1px solid var(--color-line);
}

/* Split variant: titled takeaways side by side with no rules at all, for pages
   whose learnings are written as headline + note rather than as a list.
   Always two columns — each one is half the content column, the same width a
   body paragraph's three-of-six span gets, so the 70% fill below lines the
   takeaways up with the body copy above them instead of running wider. */
.learnings--split .learning {
  border-top: 0;
  padding-block: 0;
  max-width: none;
  width: 70%;
}

@media (min-width: 62rem) {
  .learnings--split {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--grid-gutter);
    row-gap: var(--space-block);
  }

  /* Three takeaways in two columns. Filling column-first puts 1 and 2 down the
     left and 3 at the top of the right, rather than the 1-2 across / 3-orphan
     that normal row flow would give. Two explicit rows are what keep a third
     item from opening a third column. */
  .learnings--fill-column {
    grid-auto-flow: column;
    grid-template-rows: repeat(2, auto);
  }
}

@media (max-width: 61.999rem) {
  .learnings--split {
    /* Reset the column flow, or a one-column grid would spread the items
       sideways into three implicit columns instead of stacking them. */
    grid-auto-flow: row;
    row-gap: var(--space-block);
  }

  .learnings--split .learning {
    width: 100%;
  }
}

.learning__title {
  font-size: var(--fs-h3);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-normal);
  margin-bottom: var(--sp-2);
}

/* A takeaway that needs a second beat — a rule, then what qualifies it. Same
   step .cs-table__body uses between its paragraphs, so the two components
   break a paragraph the same way. */
.learning p + p {
  margin-top: var(--sp-3);
}

/* --- Next project --------------------------------------------------------- */
/* Its own band, on the page's alternating grounds, laid on the same .cs-row
   every section uses: the label in the rail, the content on the line. It used
   to be a ruled block riding along inside the last section's container, which
   meant the sneak-peek pages (no sections at all) closed differently from the
   case studies. Now every project closes identically.

   No hairline any more: the band edge does that work, the same way it does
   between any two sections on the page.

   The link is the footer's contact-link behaviour at headline size — underlined
   at rest, with the arrow sliding out of zero width on hover. Because the rule
   is a border on the link itself it extends to cover the arrow, so the
   underline grows rather than a second one appearing beside it. */

.cs-next__link {
  display: inline-flex;
  align-items: center;
  font-size: var(--fs-h2);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-tight);
  line-height: 1.15;
  padding-bottom: 0.08em;
  border-bottom: 1px solid currentColor;
  color: var(--link-ink);
  transition: color var(--dur-fast) var(--ease);
}

.cs-next__link:hover,
.cs-next__link:focus-visible {
  color: var(--link-hover);
}

/* em-based throughout, so the arrow tracks --fs-h2 as it scales between 24 and
   36px instead of being a fixed size that only looks right at one width. */
.cs-next__icon {
  display: inline-flex;
  align-items: center;
  width: 0;
  margin-left: 0;
  opacity: 0;
  overflow: hidden;
  transition: width var(--dur) var(--ease), margin-left var(--dur) var(--ease),
    opacity var(--dur) var(--ease);
}

.cs-next__link:hover .cs-next__icon,
.cs-next__link:focus-visible .cs-next__icon {
  width: 0.7em;
  margin-left: 0.34em;
  opacity: 1;
}

.cs-next__icon svg {
  flex: none;
  width: 0.7em;
  height: 0.7em;
}

@media (prefers-reduced-motion: reduce) {
  .cs-next__icon {
    transition: none;
  }
}

/* --- Narrow screens ------------------------------------------------------- */
/* One collapse point for the whole layout, matching the breakpoint the rest of
   the site uses to drop its rail. Below it there is no spare column to give
   away, so every constraint that buys white space is lifted at once: the label
   moves above its content, the six columns become one, paragraphs stack at
   full width, and the headline takes the whole row. The comparison tables keep
   their border treatment and just stack — that is why the hairline box is
   drawn as container-edges plus cell-edges, so a one-column stack closes
   itself with no doubled lines. */

@media (max-width: 61.999rem) {
  .cs-row {
    grid-template-columns: 1fr;
    row-gap: var(--cs-label-gap);
  }

  .cs-row > .cs-col:only-child {
    grid-column: 1;
  }

  .cs-col {
    grid-template-columns: 1fr;
    gap: var(--sp-6);
  }

  .cs-col > .cs-section__title {
    grid-column: 1 / -1;
  }

  .cs-col > .cs-section__body > p,
  .cs-col > .cs-section__body > .cs-body-col {
    grid-column: 1 / -1;
    width: 100%;
  }

  .cs-overview,
  .cs-overview__cover {
    margin-left: 0;
  }

  .cs-overview {
    grid-template-columns: 1fr;
    row-gap: var(--sp-6);
  }

  .cs-overview__title {
    grid-column: 1 / -1;
  }

  .cs-overview__intro > p {
    grid-column: 1 / -1;
    width: 100%;
  }
}

/* --- Lightbox ------------------------------------------------------------- */
/* Added by assets/js/lightbox.js — the figure is a plain image without it. */

/* The plate carries this for a single-image figure; each image carries it in a
   composed stack, where the images are the targets rather than the plate. */
.figure__plate.is-zoomable,
.figure__shot img.is-zoomable {
  cursor: zoom-in;
}

/* THE fix for the stuck cursor. `display: flex` below is an author rule, and an
   author rule beats the browser's own [hidden] { display: none } however
   specific it is — so setting .hidden on the overlay used to change nothing:
   it stayed a full-screen invisible layer, holding its zoom-out cursor over the
   page and swallowing every click until a reload. */
.lightbox[hidden] {
  display: none;
}

/* --lb-gutter is the one safe margin, everywhere the lightbox meets the screen
   edge. --lb-bar is the band kept clear above and below the image: a gutter,
   the 44px close button, a gutter. The hint sits in the bottom one, so at fit
   neither control is ever on top of the picture. */
.lightbox {
  --lb-gutter: var(--sp-4);
  --lb-bar: calc(2 * var(--lb-gutter) + 2.75rem);
  position: fixed;
  inset: 0;
  z-index: 200;
  overflow: hidden;
  /* Pinch and drag anywhere on the overlay go to lightbox.js, not to the
     browser's own page zoom — including beside a small image. */
  touch-action: none;
  background: var(--color-overlay);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
  cursor: zoom-out;
}

@media (min-width: 48rem) {
  .lightbox {
    --lb-gutter: var(--sp-6);
  }
}

.lightbox.is-open {
  opacity: 1;
}

/* Not painted. It is the box the image is fitted into, and lightbox.js
   measures it, so the margins are decided here in CSS rather than as numbers
   in the script. */
.lightbox__stage {
  position: absolute;
  inset: var(--lb-bar) var(--lb-gutter);
  pointer-events: none;
}

/* Placed and sized by lightbox.js to the image's own fitted rectangle — never
   a box with the picture letterboxed inside it, which is what used to leave
   white bands either side of a tall screen. The entrance scale lives here, so
   it never fights the zoom transform on the image. */
.lightbox__frame {
  position: absolute;
  z-index: 1;
  visibility: hidden;
  transform: scale(0.985);
  transition: transform var(--dur) var(--ease);
}

.lightbox__frame.is-placed {
  visibility: visible;
}

.lightbox.is-open .lightbox__frame {
  transform: none;
}

/* Fills the frame exactly; the zoom is a transform on top of that, written by
   lightbox.js. */
.lightbox__img {
  display: block;
  width: 100%;
  height: 100%;
  max-width: none;
  /* The ground the case-study screenshots already stand on, so a transparent
     export reads the same here as on the page. Not --color-plate: that is
     literal white, and white is what made this look like a panel. */
  background: var(--ground-band);
  border-radius: var(--radius-lg);
  cursor: zoom-in;
  transform-origin: center center;
  /* No will-change: transform. It pins the layer's raster at the scale it
     was first painted — the fit — so a zoomed screenshot was a blown-up
     thumbnail, text soft at the native ceiling. Without it the browser
     re-rasters at the zoom the image is actually shown at. */
  user-select: none;
  -webkit-user-drag: none;
}

.lightbox__img.is-zoomed {
  cursor: grab;
}

/* Already at native size when fitted: nothing to zoom into. */
.lightbox__img.is-fixed {
  cursor: default;
}

.lightbox__img.is-panning {
  cursor: grabbing;
}

/* Says the zoom exists, once. It sits in the band reserved below the image, so
   at fit it is on the overlay, and it may wrap rather than run off a narrow
   screen — it used to be nowrap, and its zoomed readout was wider than a
   320px phone. lightbox.js retires it after the first zoom, for the session. */
.lightbox__hint {
  position: fixed;
  z-index: 2;
  /* Centred by insets, not left: 50% + translate — that leaves a fixed box
     only the right half of the screen to size into, and it wrapped at 375. */
  left: var(--lb-gutter);
  right: var(--lb-gutter);
  bottom: var(--lb-gutter);
  box-sizing: border-box;
  width: fit-content;
  min-height: 2.75rem;
  display: flex;
  align-items: center;
  margin: 0 auto;
  padding: var(--sp-2) var(--sp-4);
  font-family: var(--font-mono);
  font-size: var(--fs-caption);
  letter-spacing: 0.01em;
  text-align: center;
  color: color-mix(in srgb, var(--color-light) 72%, transparent);
  background: color-mix(in srgb, var(--color-dark) 55%, transparent);
  border-radius: var(--radius-pill);
  pointer-events: none;
  transition: opacity var(--dur) var(--ease);
}

.lightbox__hint.is-dismissed {
  opacity: 0;
}

/* display: flex above would otherwise beat the browser's own [hidden] — the
   same trap as .lightbox[hidden]. */
.lightbox__hint[hidden] {
  display: none;
}

@media (prefers-reduced-motion: reduce) {
  .lightbox__hint {
    transition: none;
  }
}

/* In the top band, so at fit it is on the overlay, never the image. Zoomed,
   the image can reach under it, which is why it carries a ground of its own
   — the overlay colour, not transparent — and an explicit z-index rather
   than relying on coming after the frame in the markup. */
.lightbox__close {
  position: fixed;
  z-index: 3;
  top: var(--lb-gutter);
  right: var(--lb-gutter);
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border: 1px solid color-mix(in srgb, var(--color-light) 35%, transparent);
  border-radius: var(--radius-sm);
  color: var(--color-light);
  background: var(--color-overlay);
  transition: border-color var(--dur) var(--ease),
    background-color var(--dur) var(--ease);
}

.lightbox__close:hover,
.lightbox__close:focus-visible {
  border-color: var(--color-light);
  /* Mixed into the overlay rather than into transparent, so the hover state
     stays opaque over the image too. */
  background: color-mix(in srgb, var(--color-light) 12%, var(--color-overlay));
}
