@import url("tokens.css");

/* Airlock product page — layout, type, bands.
   Every colour is a var() into tokens.css. No hex literal appears in this file. */

:root {
  --measure: 68ch;
  --gutter: clamp(1rem, 4vw, 3rem);
  /* Named so css/scroll.css can cancel it and put it back: the title card
     full-bleeds to the section's edges and then restores the band's own
     padding inside itself. */
  --band-pad: clamp(3rem, 8vw, 6rem);
  /* The #cost heading's size, stated once. TWO stylesheets paint that
     sentence: css/scroll.css for the <h2> in its own block, and
     css/cinematic.css for the dark panel carrying the same sentence on the
     capture's plane while the act runs. They must be the same size or the
     slide and the block read as two different headings — and a clamp()
     copied into two files is a drift waiting to happen. Declared here rather
     than in tokens.css, which is colour only and is parsed as such by
     tools/check_contrast.py. */
  --cost-heading-size: clamp(1.875rem, 2vw + 1.5rem, 3rem);
}

*, *::before, *::after { box-sizing: border-box; }
img { max-width: 100%; height: auto; }
body {
  margin: 0;
  background: var(--canvas);
  color: var(--body);
  font: 400 clamp(1rem, 0.4vw + 0.95rem, 1.125rem)/1.6 system-ui, -apple-system, sans-serif;
}
h1, h2, h3 { color: var(--ink); line-height: 1.15; text-wrap: balance; }
h1 { font-size: clamp(2rem, 4vw + 1rem, 3.5rem); margin: 0 0 1rem; }
h2 { font-size: clamp(1.5rem, 2vw + 1rem, 2.25rem); margin: 0 0 1rem; }
h3 { font-size: clamp(1.125rem, 1vw + 0.875rem, 1.375rem); margin: 0 0 0.5rem; }
p { margin: 0 0 1rem; }

.band { padding: var(--band-pad) var(--gutter); }

/* Task 12 · the band rhythm around the laptop act.
   A full-bleed stage does not need a band's worth of air on either side of it;
   read at full height the standard rhythm left a dead strip above the plate
   and a wider one below it, which is what the owner was seeing. These are
   unconditional rather than keyed to .cin--live, so the tightened spacing is
   the same page whether or not js/cinematic.js ran, and no reflow can happen
   after first paint. */
.band--stage { padding-bottom: clamp(1.25rem, 3vw, 2rem); }
.band--stage .price-line { margin-bottom: clamp(0.5rem, 1.5vw, 1rem); }
.band--after-stage { padding-top: clamp(1.75rem, 4vw, 2.75rem); }
.band--canvas  { background: var(--canvas); }
.band--quiet   { background: var(--surface-subtle); }
.band--capture { background: var(--surface-capture); color: var(--on-dark-muted); }
.band--capture h1, .band--capture h2 { color: var(--on-dark); }
.band > .inner { max-width: 72rem; margin-inline: auto; }
.measure { max-width: var(--measure); }

.btn {
  display: inline-block; padding: 0.75rem 1.5rem; border-radius: 0.375rem;
  font-weight: 600; text-decoration: none;
  background: var(--navy); color: var(--on-primary); border: 2px solid var(--navy);
}
.btn:hover { background: var(--navy-hover); border-color: var(--navy-hover); }
.btn--ghost { background: transparent; color: var(--teal-text); border-color: var(--border-strong); }
.band--capture .btn--ghost { color: var(--on-dark); border-color: var(--on-dark-muted); }
a:focus-visible, .btn:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }

.badge {
  display: inline-block; padding: 0.25rem 0.625rem; border-radius: 0.25rem;
  font: 700 0.8125rem/1.3 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.04em; border: 2px solid currentColor;
  /* letter-spacing is applied after the LAST character too, so the chip
     carried 0.52px of empty track between the final glyph and its right
     border that the left side did not have — measured at 4x as a stray mark
     after BROWSER ONLY and after ORM ENFORCED. Taking the same amount off the
     right padding puts the ink back in the middle of the chip.
     CSS only, and deliberately so: the badge strings are compared byte for
     byte against services/badges.py by tools/check_badges.py, so not one
     character or space inside the span may move. */
  padding-right: calc(0.625rem - 0.04em);
}
.badge--browser { color: var(--brass-text); background: var(--brass-soft); }
.badge--orm     { color: var(--teal-text);  background: var(--teal-soft); }

.grid { display: grid; gap: clamp(1rem, 3vw, 2rem); }
@media (min-width: 48rem) { .grid--3 { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 48rem) { .grid--2 { grid-template-columns: repeat(2, 1fr); } }

/* §9 both columns are identical in size, colour, and prominence. */
.scope-col {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 0.5rem; padding: clamp(1.25rem, 3vw, 2rem);
}

/* The capture card carried a --surface-capture bed. Two reasons it is gone:
   the owner asked for it, and it was failing contrast. figcaption is --slate,
   and --slate on --surface-capture measures 2.95:1 — under the 4.5 this page
   requires — so every caption inside one of these cards was failing while the
   same colour passes on the bands the cards sit in.

   The hairline that replaced the bed is gone too — the owner marked it three
   times, on #whats-inside, #in-action and the dark-band before/after pair —
   so a capture now sits directly against its band with no border of its own. */
.capture { border-radius: 0.5rem; padding: 0.75rem; margin: 0; }
.capture img { display: block; width: 100%; height: auto; border-radius: 0.25rem; }
figcaption { color: var(--slate); font-size: 0.9375rem; margin-top: 0.5rem; }
.band--capture figcaption { color: var(--on-dark-muted); }

table { border-collapse: collapse; width: 100%; }
th, td { text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--border); }
details { border-bottom: 1px solid var(--border); padding: 1rem 0; }
summary { cursor: pointer; font-weight: 600; color: var(--ink); }

/* ---- additions: page furniture, all colours still var() ---- */

a { color: var(--text-link); }
.band--capture a { color: var(--on-dark); }
.band--capture h3 { color: var(--on-dark); }
.band--capture strong { color: var(--on-dark); }
.band--capture th { color: var(--on-dark); }
code, .mono {
  font: 0.9em/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  background: var(--navy-soft); color: var(--ink);
  padding: 0.1em 0.35em; border-radius: 0.2rem;
}
.band--capture code { background: var(--code-surface); color: var(--on-dark); }

.eyebrow {
  font: 600 0.875rem/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--on-dark-muted); margin: 0 0 1rem;
}
.band--quiet .eyebrow, .band--canvas .eyebrow { color: var(--slate); }

.actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 1.5rem 0 0.75rem; }
.price-line { font-size: 0.9375rem; color: var(--on-dark-muted); margin: 0 0 2rem; }
.band--quiet .price-line, .band--canvas .price-line { color: var(--slate); }

.span-all { grid-column: 1 / -1; }
.stack > * + * { margin-top: clamp(1.5rem, 4vw, 2.5rem); }

.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 0.5rem; padding: clamp(1rem, 2.5vw, 1.5rem);
}
.card p:last-child { margin-bottom: 0; }

.step { border-left: 4px solid var(--brass); padding-left: clamp(0.75rem, 2vw, 1.25rem); }
.step-n {
  display: inline-block; min-width: 1.75rem;
  font: 700 0.875rem/1.75rem ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--brass-text);
}

.menu-list { list-style: none; padding: 0; margin: 0; }
.menu-list > li { border-top: 1px solid var(--border); padding: 1rem 0; }
.menu-list > li:last-child { border-bottom: 1px solid var(--border); }
.menu-name { color: var(--ink); }

.callout {
  background: var(--warning-soft); color: var(--body);
  border: 2px solid var(--warning); border-radius: 0.5rem;
  padding: clamp(1rem, 3vw, 1.5rem); margin: 0 0 2rem;
}
.callout h3 { color: var(--warning); margin-top: 0; }
.callout p:last-child { margin-bottom: 0; }

.scope-col ul { padding-left: 1.15rem; margin: 0; }
.scope-col li + li { margin-top: 0.875rem; }

details p:last-child { margin-bottom: 0; }
details[open] summary { margin-bottom: 0.75rem; }

/* ---- Task 10 · captures as columns, with an inline zoom ----
   The grid and the expanded state are page structure, so they live here and
   survive js/zoom.js being absent: with no JavaScript every capture is still
   visible and legible in its column, and only the zoom is missing. Nothing
   below hides anything — the widest state a figure can reach is a full grid
   row, and its collapsed state is a smaller box, never a hidden one. */

.gallery { display: grid; gap: clamp(0.75rem, 2vw, 1.25rem); align-items: start; }
@media (min-width: 48rem) { .gallery { grid-template-columns: repeat(2, 1fr); } }
.gallery__item[data-expanded] { grid-column: 1 / -1; }
.gallery__item { margin: 0; }

/* The zoom control is built by js/zoom.js, so it never exists as a dead
   button on a page whose JavaScript did not load. Nothing here hides
   anything: no opacity, no visibility, no off-screen transform. */
.capture__zoom {
  display: block; position: relative; width: 100%;
  margin: 0; padding: 0; border: 0; background: none;
  color: inherit; font: inherit; cursor: zoom-in; border-radius: 0.25rem;
}
.gallery__item[data-expanded] .capture__zoom { cursor: zoom-out; }
.capture__zoom:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 3px; }
/* The visible affordance is an icon now, and the words moved to the button's
   aria-label. The pill is sized to a 44x44 target rather than shrunk to the
   glyph: the icon got smaller, the thing you aim at did not. (The whole
   capture is the button, so the real hit area is larger still — this is the
   affordance, and it is held to the same floor.) */
.capture__hint {
  position: absolute; top: 0.5rem; right: 0.5rem;
  min-width: 44px; min-height: 44px;
  display: grid; place-items: center;
  background: var(--navy); color: var(--on-primary);
  border: 1px solid var(--on-dark-muted); border-radius: 0.25rem;
}
.capture__hint svg { display: block; }

/* ---- Task 13 · #two-modes click-to-animate before/after ----
   Built entirely by js/compare.js: the .compare class on the gallery, the
   toggle button and the inline opacity values below are ALL written from
   script. Nothing here is unconditional — a page whose JavaScript never runs,
   or whose js/compare.js 404s, keeps the plain two-column comparison exactly
   as Task 10 left it: both figures in normal flow, both images visible, both
   captions readable, nothing overlapped and nothing hidden by this file.

   Scoped to #two-modes-gallery by id, not by .gallery, so the other capture
   rows on the page (Task 10's screenshot galleries, lines ~359-431) are
   untouched — they never see a .compare class because nothing ever adds one
   to them. */
#two-modes-gallery.compare { position: relative; }

/* Both figures are pinned to the SAME grid area — grid-column/grid-row 1 —
   which is what makes the overlap exact. Measured, not assumed: an
   absolutely-positioned grid item with no explicit grid-column falls back to
   the whole grid container as its containing block (confirmed by an early
   version of this rule, which stretched "after" to the full two-column
   width). Pinning both to grid area 1/1 makes that area their shared
   containing block instead, so `inset: 0` fills it exactly — no transform,
   no measurement taken in script, correct at any viewport width including
   below 48rem where the gallery is already a single column (page.css:172),
   with no resize listener needed either way.

   "before" stays a normal (non-absolute) grid item — it is what gives the
   merged frame its box and its height, and it is where js/compare.js puts
   the toggle button, so it keeps a positioning context of its own regardless
   of which capture is currently the visible one. "after" is the one pulled
   out of flow and placed exactly over it. */
#two-modes-gallery.compare .gallery__item[data-compare-role="before"],
#two-modes-gallery.compare .gallery__item[data-compare-role="after"] {
  /* Both ends stated explicitly — "1" alone leaves grid-column-end: auto,
     which measured as an INDEFINITE placement: the browser fell back to the
     whole grid container as the containing block instead of the single
     track, stretching "after" across both columns. Only resolved once both
     lines were explicit, confirmed by reading the computed grid-column-end
     back out after the fix. */
  grid-column: 1 / 2; grid-row: 1 / 2;
}
#two-modes-gallery.compare .gallery__item[data-compare-role="before"] {
  position: relative;
}
#two-modes-gallery.compare .gallery__item[data-compare-role="after"] {
  position: absolute; inset: 0; margin: 0;
}
/* js/zoom.js's own [data-expanded] rule (page.css:173) spans a figure across
   the full grid row and has lower specificity than the pinned rule above, so
   without this it could never win while compare is active. Restoring the
   span here, at equal specificity to the pin and later in the cascade, is
   what lets the zoom control still expand "after" while compare is active —
   the two features stay independent rather than one silently overriding the
   other. */
#two-modes-gallery.compare .gallery__item[data-compare-role="after"][data-expanded] {
  grid-column: 1 / -1;
}

/* NO TRANSITION IS DECLARED HERE, deliberately. There used to be one, on
   .gallery__item — the <figure> — whose opacity never changes by design:
   js/compare.js fades a .compare__layer div it builds inside each figure, and
   no CSS rule anywhere matched that class. The result measured as one
   distinct opacity state per click, the final one, arriving at t=1ms. The
   cross-fade is an anime.js tween now (js/compare.js), which declares its own
   start value with the tween `from` parameter, so no rule here — and no
   resting inline style — has to hold a hidden state for it to animate out of.
   The position above is still a fixed value, set once, never touched per
   frame. */

/* Built by js/compare.js, never authored in index.html, for the same reason
   js/zoom.js builds .capture__zoom in script: a page whose JavaScript never
   arrives must never show a control that cannot do anything. Top-left, so it
   never collides with .capture__hint's top-right zoom icon on the same
   figure. Text-labelled rather than icon-only — an icon-only control here
   would repeat the defect the zoom control was sent back for. */
.compare__toggle {
  position: absolute; top: 0.5rem; left: 0.5rem; z-index: 3;
  display: inline-flex; align-items: center;
  min-height: 44px; padding: 0.5rem 0.85rem;
  background: var(--navy); color: var(--on-primary);
  border: 1px solid var(--on-dark-muted); border-radius: 0.25rem;
  font: inherit; font-weight: 600; font-size: 0.9rem; cursor: pointer;
}
.compare__toggle:hover { background: var(--navy-hover); }
.compare__toggle:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 3px; }

/* A live region for the toggle's own announcement, and nothing else on the
   page currently needs one, so this is scoped narrowly rather than promoted
   to a page-wide utility. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* ---- Task 10 · fine print ----
   Smaller and denser, never shorter. 15px computed, --body for text, and the
   contrast is whatever --body already clears on canvas (8.64) and subtle. */

.band--fineprint { padding-block: clamp(2rem, 4.5vw, 3.25rem); }
.band--fineprint h2 { font-size: clamp(1.25rem, 1.2vw + 0.9rem, 1.625rem); margin-bottom: 0.75rem; }

.fineprint { font-size: 0.9375rem; line-height: 1.45; color: var(--body); }
.fineprint.grid { gap: clamp(0.75rem, 2vw, 1.25rem); }
.fineprint h3 { font-size: 1rem; margin-bottom: 0.35rem; }
.fineprint p { margin: 0 0 0.65rem; max-width: var(--measure); }
.fineprint p:last-child { margin-bottom: 0; }
.fineprint .scope-col { padding: clamp(0.875rem, 2vw, 1.25rem); }
.fineprint ul { padding-left: 1.05rem; }
.fineprint li + li { margin-top: 0.45rem; }
.fineprint th, .fineprint td { padding: 0.4rem 0.6rem; }
.callout.fineprint { margin-bottom: 1.25rem; }

/* Two independent items, so they can sit side by side instead of stacked. */
.fineprint__cols { column-gap: clamp(1.5rem, 4vw, 2.5rem); }
@media (min-width: 48rem) {
  .fineprint__cols { columns: 2; }
  .fineprint__cols > p { break-inside: avoid; max-width: none; }
}

/* ---- surviving a WordPress theme ----------------------------------------
   THE BUG THIS FIXES, because it will look like over-engineering otherwise.
   On the live site every heading rendered white and every paragraph rendered
   dark slate on the navy bands — the page's dark surfaces painting correctly
   underneath the THEME's light-mode text colours. It reads as "dark mode and
   light mode mixed up" and it is neither: it is the cascade.

   The cause is that .band--capture set `color` on the SECTION and let it
   inherit. An inherited value is only used when no declaration matches the
   element at all, so a theme's plain `p { color: #333 }` — specificity 0,0,1,
   the weakest possible rule — beats it. Headings were unaffected only because
   lines 53 and 117 name them explicitly.

   So every element that carries text in a band now names its own colour
   rather than inheriting one. The doubled class is deliberate, not a typo:
   .band.band--capture.band--capture is 0,3,1, which clears the 0,1,1 that
   theme and page-builder content wrappers (.entry-content p,
   .elementor-widget-container p) are written at. It is the smallest weapon
   that actually wins; !important would win too and would take the page's own
   overrides down with it.

   Standalone, none of this changes a pixel — the colours are the same tokens
   the inherited rule resolved to. It only matters where another stylesheet is
   in the room. */

.band.band--capture.band--capture p,
.band.band--capture.band--capture li,
.band.band--capture.band--capture dt,
.band.band--capture.band--capture dd,
.band.band--capture.band--capture td,
.band.band--capture.band--capture summary,
.band.band--capture.band--capture caption,
.band.band--capture.band--capture blockquote,
.band.band--capture.band--capture small { color: var(--on-dark-muted); }

.band.band--capture.band--capture h1,
.band.band--capture.band--capture h2,
.band.band--capture.band--capture h3,
.band.band--capture.band--capture strong,
.band.band--capture.band--capture th,
.band.band--capture.band--capture summary strong { color: var(--on-dark); }

/* The light bands need the same protection in the other direction: a theme
   that paints light text for its own dark surfaces would put it on our ivory
   and quiet bands, which is the identical failure with the colours swapped. */
.band.band--canvas.band--canvas p,
.band.band--canvas.band--canvas li,
.band.band--canvas.band--canvas dd,
.band.band--canvas.band--canvas td,
.band.band--canvas.band--canvas summary,
.band.band--quiet.band--quiet p,
.band.band--quiet.band--quiet li,
.band.band--quiet.band--quiet dd,
.band.band--quiet.band--quiet td,
.band.band--quiet.band--quiet summary { color: var(--body); }

.band.band--canvas.band--canvas h1,
.band.band--canvas.band--canvas h2,
.band.band--canvas.band--canvas h3,
.band.band--quiet.band--quiet h1,
.band.band--quiet.band--quiet h2,
.band.band--quiet.band--quiet h3 { color: var(--ink); }

/* The cream strip between two navy bands on the live site: the bands are flush
   by design and carry their own padding, so a theme's block margin opens a gap
   that the body's --canvas shows through. Bands butt up against each other. */
.band.band { margin: 0; }
