/* ===========================================================================
   gregmulvey.com - base.css
   The visual layer: components 01 to 18 of _design/component-spec.md (v2.1).
   Sits on top of gregmulvey-tokens.css, which is the ONLY source of color,
   type, spacing, radius and motion. No hex values here, ever.

   Literal numbers below are limited to values Design documented in the spec
   (header 76px, logo 26px/22px, door-two button 36px, label column 150px/120px,
   evidence caption column 140px, field height 46px, bar height/z-index from
   component 18, breakpoints 500/720/900/1100). Anything else is a token.
   =========================================================================== */

*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
img, svg { display: block; max-width: 100%; }
/* height: auto is LOAD-BEARING, not tidiness. The width/height attributes on
   every filled slot become CSS PRESENTATIONAL HINTS, so an img carrying
   height="3000" computes height: 3000px -- and aspect-ratio only applies when
   one dimension is auto. Without this, .gallery img rendered 279x3000 instead
   of 279x349 and the door-two hero ran 1152x1333 instead of 1152x648. It could
   not bite before 2026-09-10 because the build contained no img tags at all;
   the first real photo batch exposed it. Keep the attributes (they reserve the
   box and stop layout shift) and let CSS own the ratio. */
img { height: auto; }
figure { margin: 0; }

/* ---------- CONTAINERS ---------- */
.wrap {
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.wrap--narrow { max-width: var(--container-narrow); }
.wrap--mid    { max-width: var(--container-mid); }

/* Full-width band. Sits OUTSIDE .wrap in the markup and carries its own .wrap. */
.band { width: 100%; }
.band--raised, .surface-raised { background: var(--surface-raised); }

/* ---------- SKIP LINK ---------- */
.skip-link {
  position: absolute; left: -9999px; top: 0;
  background: var(--color-ink-900); color: var(--color-paper);
  padding: var(--space-3) var(--space-4); z-index: 1000;
}
.skip-link:focus { left: var(--space-4); top: var(--space-4); }

/* ---------- VISUALLY HIDDEN ----------
   Real DOM text that is read by assistive tech and crawlers but not painted.
   Used for the brand name in the header mark and the proof-strip heading, both
   per ruling 21 (2026-09-10). Not display:none, which would remove it from the
   accessibility tree and from the text a crawler counts. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

/* ===========================================================================
   01 HEADER. Not sticky on either door.
   =========================================================================== */
.site-header {
  border-bottom: var(--border-hairline);
  background: var(--surface-page);
}
.site-header .wrap {
  display: grid; grid-template-columns: auto 1fr auto;
  align-items: center;
  min-height: 76px;
  padding-block: var(--space-4);
}
.site-header .site-nav { grid-column: 2; justify-self: end; }
/* padding-block, not a taller SVG: the lockup size is fixed by the brand system,
   but the LINK around it has to be a real target. 22px of mark on the phone plus
   space-3 top and bottom clears the 44px comfort target (WCAG 2.2 SC 2.5.8 asks
   only 24, and the audit measured this at 148x22). Same technique as .site-nav a
   and .site-footer__legal a. Ruled by Greg 2026-09-10. */
.site-logo { display: flex; align-items: center; padding-block: var(--space-3); }
.site-logo svg { height: 26px; width: auto; }
.site-logo .mark-ink { fill: var(--color-ink-900); }
.site-logo .mark-dot { fill: var(--accent-500); }

.site-nav ul {
  display: flex; flex-wrap: wrap; gap: var(--space-6);
  list-style: none; margin: 0; padding: 0;
}
/* Colour, underline and current-page rules live in the tokens (.site-nav a). */
.site-nav a {
  font-size: var(--size-body-sm);
  display: inline-block;
  padding-block: var(--space-3);   /* 44px target */
}

/* Door two carries the one ink-outline booking button, at the right of the nav. */
.site-header__book {
  grid-column: 3; margin-left: var(--space-6);
  height: 36px; padding-inline: var(--space-4);
  white-space: nowrap;
}

@media (max-width: 1023px) {
  /* Phone and tablet (breakpoint raised from 720 to 1023 on 2026-09-30: the
     desktop grid cannot fit six links between 721 and 1023 and wrapped the nav
     around the logo). The door-two button sits in the LOGO row so the wrapped
     nav below it stays two rows. Every nav target keeps its 44px height.
     row-gap 0 everywhere: the 44px targets already carry the spacing, and the
     two gaps together made the header 217px on a phone. The logo never
     shrinks; the button takes only the width it needs, at the right. */
  .site-header .wrap {
    grid-template-columns: auto auto;
    justify-content: space-between;
    row-gap: 0;
  }
  .site-logo { grid-column: 1; grid-row: 1; flex-shrink: 0; }
  .site-logo svg { height: 22px; flex-shrink: 0; }
  .site-header__book {
    grid-column: 2; grid-row: 1; justify-self: end; margin-left: var(--space-3);
    height: 36px; padding-inline: var(--space-3); font-size: var(--size-body-sm);
  }
  .site-header .site-nav { grid-column: 1 / -1; grid-row: 2; justify-self: start; }
  .site-nav ul { gap: var(--space-5); row-gap: 0; }
}

/* ===========================================================================
   02 FOOTER. Dark, stacked mark. Three columns at 1024 and up, brand over two
   link columns at 640 to 1023, stacked under 640. Ruled 2026-09-29.
   =========================================================================== */
.site-footer {
  border-top: var(--border-hairline);
  background: var(--surface-dark);
  color: var(--text-on-dark);
  padding-block: var(--section-pad-y);
  margin-top: var(--space-9);
}
.site-footer__mark { display: block; width: 132px; margin-bottom: var(--space-6); }
.site-footer__mark .mark-ink { fill: var(--color-paper); }
.site-footer__mark .mark-dot { fill: var(--accent-500); }
.site-footer p {
  font-size: var(--size-body-sm);
  line-height: var(--leading-lede);
  color: var(--text-muted-on-dark);
  max-width: var(--measure-caption);
  margin: 0 0 var(--space-6);
}
/* Social icons, directly under the phone number (2026-09-29). The sentence's
   muted colour at rest, the nav's lighter ink on hover, no brand colours. Each
   link is a 44px square around a 22px glyph, and the row is pulled left by the
   11px of padding so the first glyph lines up with the text above it. */
.site-footer .site-footer__contact { margin-bottom: var(--space-2); }
.site-footer__social {
  list-style: none; padding: 0;
  display: flex;
  margin: 0 0 var(--space-6) -11px;
}
.site-footer .site-footer__social a {
  display: flex; align-items: center; justify-content: center;
  width: 44px; height: 44px;
  color: var(--text-muted-on-dark);
  text-decoration: none;
}
.site-footer .site-footer__social a:hover { color: var(--text-on-dark); }
.site-footer__social svg { display: block; }
/* The Chamber partner mark (session z ruling 1). Full colour straight onto the
   dark surface, no knockout. Capped at 100px at every breakpoint (Greg,
   2026-09-29; was 200px) so it reads smaller than the 132px wordmark, height
   proportional. The 282 file serves 1x and 2x, the 564 serves 3x. */
.site-footer__partner { margin: 0 0 var(--space-6); }
.site-footer__partner img {
  display: block;
  width: 100%;
  max-width: 100px;
  height: auto;
  margin-bottom: var(--space-4);
}
.site-footer__partner p { margin: 0; }
.site-footer .site-nav ul { display: block; }
/* The grid. Order in the markup is the phone order: brand, Site, Photo and
   video, bottom row. */
.site-footer__grid { display: grid; grid-template-columns: minmax(0, 1fr); column-gap: var(--space-7); }
.site-footer__brand > :last-child { margin-bottom: 0; }
.site-footer__col { margin-top: var(--space-7); }
/* Small and quiet: the sentence's size and colour, so the headings label the
   columns without competing with the links under them. */
.site-footer__heading {
  font-size: var(--size-body-sm);
  line-height: var(--leading-lede);
  font-weight: var(--weight-regular);
  letter-spacing: normal;
  color: var(--text-muted-on-dark);
  margin: 0 0 var(--space-2);
}
.site-footer__bottom {
  grid-column: 1 / -1;
  display: flex; flex-direction: column; align-items: flex-start;
  margin-top: var(--space-7);
  padding-top: var(--space-4);
  border-top: var(--border-on-dark);
}
.site-footer__bottom p { margin: 0; max-width: none; }
@media (min-width: 640px) {
  .site-footer__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .site-footer__brand { grid-column: 1 / -1; }
  .site-footer__bottom { flex-direction: row; justify-content: space-between; align-items: center; gap: var(--space-5); }
}
@media (min-width: 1024px) {
  .site-footer__grid { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr); }
  .site-footer__brand { grid-column: auto; }
  .site-footer__col { margin-top: 0; }
}
.site-footer .site-nav a {
  color: var(--text-on-dark);
  padding-block: var(--space-3);
}
.site-footer :focus-visible { outline-color: var(--focus-ring-on-dark); }
/* The one legal link. Sits under the footer nav, quieter than a nav item, but
   still a 44px target on the phone: it is the link somebody hunts for when they
   want to know whether they may use a photograph, and a hunted-for link must be
   easy to hit. Muted colour, underlined so it reads as a link without it. */
.site-footer__legal {
  margin: var(--space-5) 0 0;
  font-size: var(--size-body-sm);
}
.site-footer__legal a {
  color: var(--text-muted-on-dark);
  text-decoration: underline;
  text-underline-offset: 0.12em;
  display: inline-block;
  padding-block: var(--space-3);
}
.site-footer__legal a:hover { color: var(--text-on-dark); }

/* ===========================================================================
   DOOR ONE PAGE GRAMMAR
   Reading column centred at --container-narrow inside the 1200 wrap.
   =========================================================================== */
.door-one main > .wrap > section:not(.section--wide, .section--mid, .hero--framed),
.door-one main > .band > .wrap > section:not(.section--wide, .section--mid, .hero--framed),
.door-one main > .wrap > .bridge-strip {
  max-width: var(--container-narrow);
  margin-inline: auto;
}

.door-one main .section--mid { max-width: var(--container-mid); margin-inline: auto; }

/* Neutral pages (contact, thanks, 404) belong to neither door grammar. They keep
   the reading column and take no door rules: no bottom bar, no hero image, no
   label grid. */
.door-neutral main > .wrap > section,
.door-neutral main > .wrap.prose {
  max-width: var(--container-narrow);
  margin-inline: auto;
}
/* Same escape hatch door one has. The reading column is a rule about PROSE; a
   row of three cards is not prose, and squeezed into 720px the third card's
   title wraps while the other two do not, which reads as a hierarchy the chooser
   is explicitly not supposed to have. 960px is the token's own card-row width. */
.door-neutral main > .wrap > section.section--mid {
  max-width: var(--container-mid);
}
.door-neutral main > .wrap > section { padding-block: var(--section-pad-y); }
.door-neutral main > .wrap.prose { padding-block: var(--section-pad-y-lg); }

/* THE HANGING H2 IS GONE. Ruled by Greg 2026-09-14.
   It pulled one h2 per door-one page 240px out of the reading column. On the
   page it read as a mistake rather than as emphasis: it was the only element
   breaking the left edge, and the h2 did not even share an edge with its own
   body copy. The tell is that it was noticed as misalignment before it was
   noticed as design. Door one is a single narrative column at 68ch, and one
   column means one left edge. Emphasis on a section is available through
   SPACE, not position.

   Removed on all SEVEN pages that carried it, not the four first counted:
   home, /how-i-work/, /about/, /stories/, and the three story pages. The class
   went with it, so no dead marker survives in the markup.

   To restore: put back a @media (min-width: 1280px) block setting
   margin-left: calc((var(--container-narrow) - var(--container-max)) / 2) and
   width: calc(100% + ((var(--container-max) - var(--container-narrow)) / 2))
   on .door-one .h2--hanging, and re-add the class in build.py. Note the
   breakpoint must stay at 1280: at 1100 the heading clipped off the viewport. */

/* ===========================================================================
   DOOR TWO PAGE GRAMMAR
   One grid for the whole page body: a 150px label column and the text column.
   h2 hangs in the label column by default; sections whose content IS a label
   list move the h2 into the text column (.section--labelled).
   =========================================================================== */
/* .prose caps the SECTION on door one, where the section is the reading column.
   On door two the section is the full grid and the measure belongs to the text. */
.door-two main > .wrap > section.prose,
.door-two main > .band > .wrap > section.prose { max-width: none; }

@media (min-width: 900px) {
  .door-two main > .wrap > section:not(.hero, .section--split),
  .door-two main > .band > .wrap > section:not(.hero, .section--split) {
    display: grid;
    grid-template-columns: 150px minmax(0, 1fr);
    column-gap: var(--space-5);
  }
  .door-two main section > * { grid-column: 2; }
  /* The h2 hangs across both columns, so it opens at the container edge and, in
     a labelled section, sits directly over its own label column. */
  .door-two main section > h2 { grid-column: 1 / -1; }
  .door-two main section > .full,
  .door-two main section > .card-grid,
  .door-two main section > .gallery,
  .door-two main section > .who-grid,
  .door-two main section > .package-grid,
  .door-two main section > .label-list,
  .door-two main section > .figure-trio,
  .door-two main section > .figure-pair,
  .door-two main section > .figure-set,
  .door-two main section > .proof-grid,
  .door-two main section > .justified-strip,
  .door-two main section > .list-two-col,
  .door-two main section > .evidence { grid-column: 1 / -1; }
  /* .list-two-col is a ul, and the rule below caps every section-level ul at
     the narrative measure. A two-column scan list is not narrative. */
  .door-two main section > ul.list-two-col { max-width: none; }

  /* A raised section that puts a figure beside its text at 1280. */
  .door-two main > .wrap > section.section--split,
  .door-two main > .band > .wrap > section.section--split {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 300px;
    column-gap: var(--space-7);
  }
  .door-two .section--split > h2 { grid-column: 1 / -1; }
  .door-two .section--split > div { grid-column: 1; }
  .door-two .section--split__figure { grid-column: 2; grid-row: 2; }
}
.door-two main section > p,
.door-two main section > ul,
.door-two main section > ol { max-width: var(--measure-narrative); }

/* ===========================================================================
   03 HERO, DOOR ONE
   =========================================================================== */
.door-one .hero h1 { margin: 0; }
.door-one .hero .lede { margin-top: var(--space-5); }
/* Home, ruled 2026-09-30: the H1 is the name at h2 size, the public line
   follows as the display line, then the lede. Nothing between them. */
.door-one .hero .hero__name { font-size: var(--size-h2); line-height: var(--leading-h2); letter-spacing: var(--tracking-h2); }
.door-one .hero .hero__name + .display { margin-top: var(--space-4); }
/* Home and /about/, ruled 2026-09-30: the hero takes the first screen from
   721px up so no photograph shows above the fold at 1440x900 or 768x1024
   (tokens rule 8). The 76px is the header's min-height. */
@media (min-width: 721px) {
  .door-one .hero--full {
    box-sizing: border-box; min-height: calc(100svh - 76px);
    display: flex; flex-direction: column; justify-content: center;
  }
}

/* DOOR ONE WITH A HERO IMAGE. Added 2026-09-11 at Greg's request for
   /stories/mike-d/, and it is a deliberate exception to the door-one rule that
   the retainer spine is text-first with no imagery above the fold. It applies
   ONLY to a .hero--framed section, so every other door-one hero is untouched
   and still text-only.
   The section opts out of the narrow reading column above so the frame can run
   the full wrap width the way the door-two heroes do, and the text underneath
   is put back on the reading measure by hand: a 1152px-wide lede would be
   unreadable. */
.door-one .hero--framed { max-width: none; }
.door-one .hero--framed .hero__frame { width: 100%; margin-block-end: var(--space-6); }
/* display: block is LOAD-BEARING and is the whole reason this block exists.
   gregmulvey-tokens.css carries ".door-one .hero img, picture, video
   { display: none }" commented "enforcement, not styling": the design system
   actively PREVENTS an image in a door-one hero rather than merely declining
   to style one. That guard stays in place for every other door-one hero. This
   rule is a deliberate, narrow carve-out for .hero--framed only, and it wins
   on specificity (three classes and an element against two and an element)
   because base.css also loads after the tokens.
   Symptom if it is ever removed: the img computes display:none, the frame
   measures 1152x0, the browser never fetches the file, and the page renders a
   tall blank gap where the photograph should be. That is not a broken path or
   a bad build; it is this guard doing its job. */
.door-one .hero--framed .hero__frame > img,
.door-one .hero--framed .hero__frame > .media-placeholder {
  display: block;
  aspect-ratio: 16 / 9; object-fit: cover; width: 100%; border-radius: 0;
}
.door-one .hero--framed > :not(.hero__frame) { max-width: var(--container-narrow); }
@media (max-width: 720px) {
  /* Same edge-to-edge bleed the door-two heroes take on a phone. */
  .door-one .hero--framed .hero__frame { margin-inline: calc(-1 * var(--gutter)); width: auto; }
}
.hero__cta {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-5);
  margin-top: var(--space-6);
}

/* ===========================================================================
   04 PROOF STRIP
   =========================================================================== */
.proof-strip { display: grid; grid-template-columns: 300px 1fr 1fr 1fr; gap: var(--space-7); }
.proof-strip__cell + .proof-strip__cell { border-left: var(--border-default); padding-left: var(--space-6); }
/* The proof-strip photo is a direct child of the grid, not wrapped in a figure and
   not inside .evidence, so the tokens' photographs rule does not reach it and it was
   the one square-cornered photograph on the home page. Same --radius-sm as every other. */
.proof-strip img { border-radius: var(--radius-sm); }
.proof-strip h3 { font-size: var(--size-h3); font-weight: var(--weight-semibold); margin: 0 0 var(--space-3); }
.proof-strip p { font-size: var(--size-body-sm); color: var(--text-secondary); margin: 0; }
.proof-strip p + p { margin-top: var(--space-3); }
.proof-strip p.proof-strip__numbers {
  font-size: var(--size-h2); line-height: var(--leading-h2);
  letter-spacing: var(--tracking-h2); font-weight: var(--weight-black);
  color: var(--text-primary);
  margin: 0 0 var(--space-3);
}
@media (max-width: 900px) {
  .proof-strip { grid-template-columns: 1fr; gap: var(--space-6); }
  .proof-strip__cell + .proof-strip__cell {
    border-left: 0; padding-left: 0;
    border-top: var(--border-default); padding-top: var(--space-6);
  }
}

/* ===========================================================================
   05 STORY CARD / STORY GRID
   Base rules are in the tokens; only the caption and hook spacing live here.
   =========================================================================== */
.story-card h3 { margin: var(--space-4) 0 0; }
.story-card p { font-size: var(--size-body-sm); color: var(--text-secondary); margin: var(--space-2) 0 0; }
.story-card .caption { margin-top: var(--space-2); }
@media (max-width: 500px) {
  .story-card h3 { margin-top: 0; }
  .story-card > .media-placeholder,
  .story-card > img { grid-row: span 3; }
}

.story-grid[data-count="1"] { grid-template-columns: minmax(0, 320px); margin-inline: 0; }
.story-grid[data-count="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* The attribute rule outranks the tokens' phone rule (.story-grid, one class),
   so a two-card grid stayed two columns at 390 and overflowed (2026-09-30). */
@media (max-width: 500px) { .story-grid[data-count="2"] { grid-template-columns: minmax(0, 1fr); } }

/* ===========================================================================
   06 EVIDENCE FIGURE. Caption below by default, beside at 1100px and wider.
   =========================================================================== */
.evidence { max-width: var(--container-narrow); margin-block: var(--space-7); }
.evidence .caption { margin-top: var(--space-2); }
.evidence--mid  { max-width: var(--container-mid); margin-inline: auto; width: 100%; }
.evidence--band { max-width: none; }

/* A client's mark, shown as the vector it is rather than as a photograph of
   it. The file lives in /assets/work/ and is NOT a manifest slot: it is the
   client's trademark, so it gets no ImageObject and no copyright claim. */
.logo-panel { max-width: var(--container-mid); margin: var(--space-7) auto; }
.logo-panel__stage { background: var(--surface-dark); display: grid; place-items: center; padding: var(--space-9) var(--space-6); }
.logo-panel__stage img { width: min(100%, 480px); height: auto; }
.logo-panel .caption { margin-top: var(--space-2); }
@media (max-width: 500px) { .logo-panel__stage { padding: var(--space-7) var(--space-5); } }

/* A click-to-load video. The poster is an ordinary manifest slot served from
   /media/, and the whole frame is one button: nothing is requested from Vimeo
   until it is pressed, so the page makes no third-party call and sets no
   cookie on load. On press, video_facade_script swaps the frame's contents for
   the player iframe at the same 16:9 box, so nothing below it moves. */
.video__frame { position: relative; aspect-ratio: 16 / 9; background: var(--surface-dark); }
.video__frame > img,
.video__frame > iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.video__play {
  position: absolute; inset: 0; width: 100%; height: 100%;
  display: grid; place-items: center;
  padding: 0; border: 0; background: none; cursor: pointer;
}
.video__play svg {
  width: 72px; height: 72px; border-radius: 50%;
  background: var(--surface-dark); color: var(--text-on-dark);
  box-shadow: var(--shadow-md);
  transition: transform var(--dur-fast) var(--ease-standard);
}
.video__play:hover svg { transform: scale(1.08); }
.video__play:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: var(--focus-ring-offset); }
@media (max-width: 500px) { .video__play svg { width: 56px; height: 56px; } }
/* A row of smaller players under a featured one. Three across the mid
   column, one per row at 700 and under, where a third of a phone is too
   small to judge a spot by. */
.video-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-5); max-width: var(--container-mid); margin: var(--space-7) auto; }
/* Sized to its count, no empty cells (video_grid_blocks): two sit two across;
   a single video never reaches the grid, it renders as a full-width player. */
.video-grid[data-count="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.video-grid .video { margin: 0; }
.video-grid .caption { margin-top: var(--space-2); }
/* Bottom-left on the small tiles: several of these posters are title cards,
   and a centred button lands on the wordmark. */
.video--small .video__play { place-items: end start; padding: var(--space-3); }
.video--small .video__play svg { width: 48px; height: 48px; }
/* The count selector outranks a bare .video-grid, so the phone rule names it too. */
@media (max-width: 700px) { .video-grid, .video-grid[data-count="2"] { grid-template-columns: 1fr; } }
@media (prefers-reduced-motion: reduce) { .video__play svg { transition: none; } }
@media (min-width: 1100px) {
  /* The caption hangs in the right margin so the photo keeps its full measure. */
  .evidence--beside {
    display: grid; grid-template-columns: minmax(0, 1fr) 140px; gap: var(--space-5);
    max-width: calc(var(--container-narrow) + 140px + var(--space-5));
    width: calc(100% + 140px + var(--space-5));
  }
  .evidence--beside .caption { border-top: var(--border-hairline); padding-top: var(--space-3); margin-top: 0; }
}
/* Evidence pair, and any trio of figures: three across at every width. */
/* A captioned proof strip. The same grid TEMPLATE as .door-two .gallery
   (tokens.css: auto-fit, minmax(240px, 1fr), gap --space-3) but NOT the same
   result: auto-fit collapses empty tracks, so the cell width depends on how
   many frames the grid holds, and build.py computes each grid's sizes string
   from its count (autofit_sizes). Two other differences from .gallery: no
   3-column rule at 500px (this stays one column), and captions stay visible,
   because a proof frame that cannot name the organization is not proof. */
.proof-grid { display: grid; gap: var(--space-3); grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.proof-grid figure { margin: 0; }
/* A source narrower than its cell (the 346px June 2026 Neighbors cover)
   otherwise stops at its natural width and sits shorter than its row. */
.proof-grid img { width: 100%; }
.proof-grid .caption { margin-top: var(--space-2); }

/* A list that runs two columns at desktop and one on the phone. Used for the
   "events I photograph" and "what comes back" blocks, which are scan lists
   rather than prose and should not be held to the narrative measure. */
.list-two-col { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--space-6); row-gap: var(--space-2); margin: 0; padding-left: var(--space-5); }
@media (max-width: 720px) { .list-two-col { grid-template-columns: 1fr; } }

/* A ruled, published price line. The class is what lets forbidden-strings.sh
   tell a price the brief cleared from a price that leaked, and the script
   refuses it on any page other than the two it is ruled for. */
.price-ruled { max-width: var(--measure-narrative); }

.figure-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-5); margin-block: var(--space-7); }
.figure-trio { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-5); margin-block: var(--space-7); }
.figure-trio img { width: 100%; }
.figure-quad { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-5); margin-block: var(--space-7); }
/* A figure group that carries ONE caption for the whole set instead of one
   per frame (eight product shots do not need eight labels). */
.figure-set { margin-block: var(--space-7); }
.figure-set > [class^="figure-"] { margin-block: 0 var(--space-2); }
/* A JUSTIFIED STRIP: frames of different shapes, uncropped, one height per row.
   Each figure grows in proportion to its slot's ratio, so a 5:4 frame beside a
   3:2 frame lands at the same height with no crop and no letterbox. Built for
   the LPD Halloween composites (change list 06), whose people and titles run
   to the edges, so any shared crop cut someone. build.py's justified_strip()
   computes each frame's sizes from the same ratios and the 12px gap below;
   change one and the other is wrong. Stacks full width at 720 and under. */
.justified-strip { display: grid; gap: var(--space-5); margin-block: var(--space-7); }
.justified-strip__row { display: flex; gap: var(--space-3); align-items: flex-start; }
.justified-strip__row > figure { flex: 1 1 0; min-width: 0; }
.justified-strip__row > figure[data-ratio="5:4"] { flex-grow: 1.25; }
.justified-strip__row > figure[data-ratio="4:3"] { flex-grow: 1.3333; }
.justified-strip__row > figure[data-ratio="3:2"] { flex-grow: 1.5; }
.justified-strip__row > figure[data-ratio="4:5"] { flex-grow: 0.8; }
.justified-strip__row > figure[data-ratio="3:4"] { flex-grow: 0.75; }
.justified-strip__row > figure[data-ratio="2:3"] { flex-grow: 0.6667; }
.justified-strip img { width: 100%; }
@media (max-width: 720px) {
  .justified-strip__row { flex-direction: column; gap: var(--space-5); }
  .justified-strip__row > figure { flex: none; width: 100%; }
}
/* A trio stays three across at every width, per the ruling. A quad is not a trio:
   it halves rather than shrink four frames past reading size. */
@media (max-width: 500px) {
  .figure-trio { gap: var(--space-2); }
  .figure-pair { gap: var(--space-3); }
  .figure-quad { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); }
}

/* Three cards, one per audience. Three across at desktop; on the phone they take
   the same thumbnail-left grammar as the story card, so three fit one screen. */
.who-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-5); }
/* Service cards run on the gallery's four-across track so a section with two of
   them keeps the same card size as a section with seven. */
.card-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-5); }
@media (max-width: 900px) { .card-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* Five cards (the business page) as three over two, centred, so the fifth is
   never alone on a row (2026-09-30). Six tracks: each card spans two. */
@media (min-width: 501px) {
  .card-grid--five { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .card-grid--five > * { grid-column: span 2; }
  .card-grid--five > :nth-child(4) { grid-column: 2 / span 2; }
  .card-grid--five > :nth-child(5) { grid-column: 4 / span 2; }
}
.who-card { display: block; }
.who-card h3 { margin: var(--space-4) 0 0; }
.who-card p { font-size: var(--size-body-sm); color: var(--text-secondary); margin: var(--space-2) 0 0; }
/* Extra frames under a card's text (the business cards, list-05 follow-up 5).
   Stacked at the card's width; on the phone, where the card is a 96px
   thumbnail row, they sit two across in the text column instead. */
.who-card__more { display: grid; gap: var(--space-3); margin-top: var(--space-4); }
@media (max-width: 500px) {
  .who-grid, .card-grid { grid-template-columns: 1fr; gap: var(--space-6); }
  .who-card { display: grid; grid-template-columns: 96px 1fr; gap: var(--space-4); align-items: start; }
  .who-card > img, .who-card > .media-placeholder { grid-row: span 2; }
  .who-card h3 { margin-top: 0; }
  .who-card__more { grid-column: 2; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); margin-top: 0; }
}

/* Component 06b. The /contact/ chooser. Three imageless cards, equal weight and
   no primary, because the page's only job is to let the visitor pick: nothing
   here may look more clickable than its neighbours. Deliberately NOT .who-card,
   whose phone rule reserves a 96px thumbnail column these cards have no image to
   fill. Stacks at 700px rather than 500px, because three cards of prose at a
   third of a tablet width read as columns of confetti. */
.chooser-grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-5); margin-top: var(--space-6);
}
@media (max-width: 700px) { .chooser-grid { grid-template-columns: 1fr; } }
.chooser-card {
  display: block; padding: var(--space-5);
  background: var(--surface-card);
  border: var(--border-default); border-radius: var(--radius-lg);
  text-decoration: none;
  transition: border-color var(--dur-fast);
}
.chooser-card:hover { border-color: var(--color-ink-900); }
.chooser-card h2 { font-size: var(--size-h3); margin: 0; }
.chooser-card p {
  font-size: var(--size-body-sm); color: var(--text-secondary);
  margin: var(--space-3) 0 0;
}
.chooser-card__go {
  display: inline-block; margin-top: var(--space-4);
  font-size: var(--size-body-sm); font-weight: var(--weight-medium);
  text-decoration: underline;
}

/* ===========================================================================
   07 PULL QUOTE
   =========================================================================== */
.pullquote { max-width: 40ch; }
.pullquote + .pullquote__attribution {
  margin-top: var(--space-4);
  margin-left: calc(var(--space-5) + 3px);
}

/* ===========================================================================
   08 METHODOLOGY LIST / 12 LOGISTICS BLOCK
   One treatment for label-colon content across both doors.
   =========================================================================== */
/* Methodology steps keep the drafted markup exactly (<p><strong>Label.</strong>
   text</p>); the grid on the wrapper puts the label in its own column and the
   anonymous text run beside it. No new elements, per component 08. */
@media (min-width: 721px) {
  .step-list p:has(> strong) {
    display: grid; grid-template-columns: 150px minmax(0, 1fr);
    column-gap: var(--space-5);
    max-width: none;
  }
  .step-list p > strong { grid-column: 1; }
}

.label-list { margin: 0; }
.label-list dt { font-weight: var(--weight-bold); color: var(--text-primary); }
.label-list dd { margin: 0; max-width: var(--measure-narrative); }
@media (min-width: 721px) {
  .label-list { display: grid; grid-template-columns: 150px minmax(0, 1fr); column-gap: var(--space-5); }
  .label-list dt, .label-list dd { padding-block: var(--space-4); }
  .label-list--logistics { grid-template-columns: 120px minmax(0, 1fr); }
  .label-list--ruled > dt, .label-list--ruled > dd { border-top: var(--border-hairline); }
  .label-list--ruled > dt:first-of-type, .label-list--ruled > dt:first-of-type + dd { border-top: 0; }
}
@media (max-width: 720px) {
  .label-list dt { display: inline; margin-inline-end: .3em; }
  .label-list dd { display: inline; }
  .label-list dd::after { content: ""; display: block; height: var(--space-5); }
}

/* ===========================================================================
   09 HERO, DOOR TWO. 16:9 frame, square corners, H1 below.
   =========================================================================== */
.door-two .hero { display: block; padding-block: 0; }
.door-two .hero__frame { width: 100%; }
.door-two .hero__band { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--space-3); }
.door-two .hero__band[data-frames="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.door-two .hero__band[data-frames="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* Two-frame bands only line up when both slots declare the SAME ratio: the
   columns are equal, so equal ratios give equal heights and object-fit: cover
   crops each frame to fit. The business band is 5:4 + 5:4 for that reason
   (list-05 follow-up 3); 3:2 beside 1:1 left a gap under the shorter frame. */
.door-two .hero__band > img,
.door-two .hero__band > .media-placeholder { object-fit: cover; border-radius: 0; }
/* THE RATIO COMES FROM THE MANIFEST, NOT FROM HERE. These two selectors carry
   three classes and outrank img[data-ratio] (one class, one attribute), so
   hard-coding 16:9 meant a 3:2 slot in a band rendered 16:9 and the contact
   sheet's ratio check failed it the moment the slot was filled. 16:9 is now
   only the fallback for an element that declares nothing. Every slot in a band
   or a frame before 2026-09-11 declared 16:9, so nothing already on the site
   changes shape. */
.door-two .hero__band > img:not([data-ratio]),
.door-two .hero__band > .media-placeholder:not([data-ratio]) { aspect-ratio: 16 / 9; }
@media (max-width: 720px) {
  .door-two .hero__band,
  .door-two .hero__band[data-frames="4"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Five frames in two columns leave one alone; it spans the row (2026-09-30). */
  .door-two .hero__band > :nth-child(5):last-child { grid-column: 1 / -1; }
}
.door-two .hero__frame > img,
.door-two .hero__frame > .media-placeholder { object-fit: cover; border-radius: 0; width: 100%; }
.door-two .hero__frame > img:not([data-ratio]),
.door-two .hero__frame > .media-placeholder:not([data-ratio]) { aspect-ratio: 16 / 9; }
.door-two .hero__text {
  display: grid; grid-template-columns: 5fr 7fr; gap: var(--space-7);
  padding-block: var(--space-7);
}
.door-two .hero__text h1 { margin: 0; }
.door-two .hero__text .lede { margin: 0; }
.door-two .hero__cta { margin-top: var(--space-6); }
@media (max-width: 900px) {
  .door-two .hero__text { grid-template-columns: 1fr; gap: var(--space-5); }
}
@media (max-width: 720px) {
  /* Phone: image full bleed, then h1, lede, cta stacked. */
  .door-two .hero__frame { margin-inline: calc(-1 * var(--gutter)); width: auto; }
}

/* ===========================================================================
   10 GALLERY GRID. Grid itself is in the tokens (.door-two .gallery).
   =========================================================================== */
.gallery > img { aspect-ratio: 4 / 5; object-fit: cover; width: 100%; }

/* ===========================================================================
   11 PACKAGE CARDS. Accent-free: ink outline on every tier, border weight on
   the middle tier only, no fill anywhere.
   =========================================================================== */
.package-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-5); }
.package-card {
  display: flex; flex-direction: column; gap: var(--space-4);
  border: var(--border-default); border-radius: var(--radius-lg);
  padding: var(--space-6);
  background: var(--surface-card);
}
.package-card--featured { border: var(--border-strong); }
.package-card h3 { margin: 0; }
.package-card .price {
  font-size: var(--size-h2); font-weight: var(--weight-black);
  letter-spacing: var(--tracking-h2); color: var(--text-primary);
  margin: 0;
}
.package-card p { font-size: var(--size-body-sm); color: var(--text-secondary); margin: 0; }
.package-card .btn { margin-top: auto; align-self: flex-start; }
@media (max-width: 900px) { .package-grid { grid-template-columns: 1fr; } }

/* ===========================================================================
   13 UP-LINK STRIP / BRIDGE STRIP
   =========================================================================== */
.bridge-strip, .upsell-strip {
  border-top: var(--border-hairline);
  padding-block: var(--space-7);
  font-size: var(--size-body-sm);
  color: var(--text-muted);
  max-width: var(--container-narrow);
}
.bridge-strip p, .upsell-strip p { margin: 0; }
.bridge-strip a {
  color: var(--color-ink-900);
  text-decoration-color: var(--color-ink-300);
}
.bridge-strip a:hover { text-decoration-color: var(--color-ink-900); }

/* ===========================================================================
   14 FORM SHELL. The native panel around the Sprout embed.
   =========================================================================== */
.form-panel {
  background: var(--surface-card);
  border: var(--border-default);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  max-width: var(--container-narrow);
}
.form-panel .eyebrow { display: block; margin-bottom: var(--space-5); }
.form-panel__note { font-size: var(--size-body-sm); color: var(--text-muted); margin: 0; }
.form-panel .alert--success { margin-top: var(--space-5); }

/* The Sprout embed. The iframe points at api.sproutstudio.com and posts its own
   content height back, which build.py's per-panel listener writes on as an
   inline height in px. Until that first message lands there is no height at all,
   so min-height holds the panel open and the CTAs above it land somewhere real
   instead of on a collapsed box. The inline height wins the moment it arrives,
   so this never fights it. Keep min-height BELOW the shortest form's natural
   height or it will pad every panel with dead space. */
.form-panel--embed iframe {
  display: block;
  width: 100%;
  min-height: 620px;
  border: 0;
}
.form-panel--embed .form-panel__note { margin-top: var(--space-5); }
/* Phone (2026-09-30, ui.md H8 / ux.md M4): the panel's 32px padding plus
   Sprout's own inset squeezed the fields to about 230px at 390. Tighter panel
   padding on the embed only gives the iframe 318px of the 342px wrap. */
@media (max-width: 500px) {
  .form-panel--embed { padding: var(--space-4) var(--space-3); }
}

/* ===========================================================================
   17 MOTION. Two motions on the site, both in the tokens: the link hover and
   the button hover. No scroll motion anywhere.
   =========================================================================== */

/* ===========================================================================
   18 PHONE BOTTOM CTA BAR
   Phone only, door-two service and package pages plus the hub. Never door one,
   contact, thanks, or blog. Values the tokens do not have, per Design's note:
   the z-index (10), the bar height (48px = --space-7, used via the token), and
   the safe-area inset (an env() value, not a token).
   =========================================================================== */
.cta-bar { display: none; }
@media (max-width: 500px) {
  .door-two.has-cta-bar .cta-bar {
    display: block;
    position: fixed; inset: auto 0 0 0; z-index: 10;
    background: var(--surface-page);
    border-top: var(--border-hairline);
    padding: 2px var(--gutter) calc(2px + env(safe-area-inset-bottom));
    opacity: 0; visibility: hidden; pointer-events: none;
    transition: opacity var(--dur-fast) var(--ease-standard), visibility var(--dur-fast);
  }
  .door-two.has-cta-bar .cta-bar[data-shown="1"] { opacity: 1; visibility: visible; pointer-events: auto; }
  .door-two.has-cta-bar .cta-bar .btn {
    width: 100%; justify-content: center;
    padding-block: var(--space-3); line-height: 1.25;
  }
  /* Pad <main> so the bar never covers the end of the page. */
  .door-two.has-cta-bar main { padding-bottom: calc(var(--space-7) + env(safe-area-inset-bottom)); }
}
@media (prefers-reduced-motion: reduce) {
  .cta-bar { transition: none !important; }
}

/* ===========================================================================
   PHONE, UNDER 500px. Story cards, galleries and figure trios.
   (.story-card / .story-grid / .gallery overrides are in the tokens.)
   =========================================================================== */
@media (max-width: 500px) {
  .door-two .gallery, .story-grid, .who-grid, .card-grid { grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); }
  .door-two .gallery { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .story-grid, .who-grid, .card-grid { grid-template-columns: minmax(0, 1fr); }
  .figure-trio { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .proof-strip p.proof-strip__numbers { font-size: var(--size-h2); }
  /* The section mark opens each phone section above its h2, where the hanging
     h2 cannot exist. Drawn as a pseudo-element so it stays out of the markup
     and applies everywhere; .section-mark in the tokens is the same 40x2 rule
     for anywhere it has to be an element. The display headline takes no mark. */
  main section > h2:not(.display)::before {
    content: ""; display: block;
    width: 40px; height: 2px;
    background: var(--color-ink-900);
    margin-bottom: var(--space-5);
  }
  .surface-dark main section > h2:not(.display)::before { background: var(--color-paper); }
}

/* ===========================================================================
   STAGING ONLY. Everything below is stripped by the launch toggle and never
   appears in the production build.
   =========================================================================== */

.status-banner {
  background: var(--color-warning-bg);
  border-bottom: 1px solid var(--color-warning-border);
  color: var(--color-warning);
  font-size: var(--size-caption);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-caption);
  padding: var(--space-2) var(--gutter);
  display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-5);
  align-items: center;
}
.status-banner strong { font-weight: var(--weight-bold); }
.status-banner a { color: var(--color-warning); text-decoration-color: var(--color-warning); }
.status-banner .status-banner__badge {
  text-transform: uppercase; font-size: var(--size-eyebrow);
  border: 1px solid currentColor; border-radius: var(--radius-sm);
  padding: 0 var(--space-2);
}

/* MEDIA PLACEHOLDER. Keeps slot id, description and ratio. Collapses at launch. */
.media-placeholder {
  background: var(--color-ink-050);
  min-width: 0; overflow: hidden;
  border: 1px solid var(--color-ink-200);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center;
  padding: var(--space-5);
  color: var(--text-muted);
  width: 100%;
}
.media-placeholder__id { font-family: var(--font-mono); font-size: var(--size-caption); color: var(--text-secondary); overflow-wrap: anywhere; }
.media-placeholder__desc { font-size: var(--size-caption); max-width: var(--measure-caption); margin-block-start: var(--space-2); overflow-wrap: anywhere; }
.media-placeholder__ratio { font-size: var(--size-eyebrow); text-transform: uppercase; letter-spacing: var(--tracking-eyebrow); margin-block-start: var(--space-3); }
.media-placeholder[data-ratio="16:9"]   { aspect-ratio: 16 / 9; }
.media-placeholder[data-ratio="4:5"]    { aspect-ratio: 4 / 5; }
.media-placeholder[data-ratio="1:1"]    { aspect-ratio: 1 / 1; }
.media-placeholder[data-ratio="3:2"]    { aspect-ratio: 3 / 2; }
.media-placeholder[data-ratio="1.91:1"] { aspect-ratio: 1.91 / 1; }
.media-placeholder[data-ratio="2:3"]    { aspect-ratio: 2 / 3; }
.media-placeholder[data-ratio="3:4"]    { aspect-ratio: 3 / 4; }
.media-placeholder[data-ratio="5:4"]    { aspect-ratio: 5 / 4; }
.media-placeholder[data-ratio="4:3"]    { aspect-ratio: 4 / 3; }

/* The SAME ratios, applied to real images. Do not delete this as a duplicate
   of the placeholder rules above: a placeholder and the photograph that
   replaces it are different elements, and until the first card and process
   slots were filled only the placeholder was ever held to shape.
   .gallery > img had its own aspect-ratio rule, which is why the twelve-tile
   galleries always looked right and hid the hole. Everything else relied on
   the source file happening to match the slot: the square who-cards looked
   correct only because their sources were square, and the first 5:4 landscape
   dropped into a 1:1 who-card rendered at 1.25 and pulled the row ragged.
   object-fit: cover is what build.py's focal hint (object-position) needs in
   order to do anything at all, so the two belong together. */
img[data-ratio="16:9"]   { aspect-ratio: 16 / 9; object-fit: cover; }
img[data-ratio="4:5"]    { aspect-ratio: 4 / 5;  object-fit: cover; }
img[data-ratio="1:1"]    { aspect-ratio: 1 / 1;  object-fit: cover; }
img[data-ratio="3:2"]    { aspect-ratio: 3 / 2;  object-fit: cover; }
img[data-ratio="1.91:1"] { aspect-ratio: 1.91 / 1; object-fit: cover; }
img[data-ratio="2:3"]    { aspect-ratio: 2 / 3; object-fit: cover; }
img[data-ratio="5:4"]    { aspect-ratio: 5 / 4; object-fit: cover; }
img[data-ratio="4:3"]    { aspect-ratio: 4 / 3; object-fit: cover; }
img[data-ratio="3:4"]    { aspect-ratio: 3 / 4; object-fit: cover; }
/* A looping slot (loop_html) takes the same box as the still it replaces. */
video.media-loop { display: block; width: 100%; height: auto; }
video.media-loop[data-ratio="1:1"] { aspect-ratio: 1 / 1; object-fit: cover; }
/* Tight slots (thumbnails, three-across trios) drop the description so the
   placeholder still reads at small sizes. */
.story-card .media-placeholder,
.figure-trio .media-placeholder,
.gallery .media-placeholder { padding: var(--space-2); }
.story-card .media-placeholder__desc,
.gallery .media-placeholder__desc { display: none; }
[data-staging="0"] .media-placeholder { display: none; }

/* /_status/ DASHBOARD */
.status-table { width: 100%; border-collapse: collapse; font-size: var(--size-body-sm); }
.status-table th, .status-table td {
  text-align: left; padding: var(--space-3) var(--space-4);
  border-bottom: var(--border-hairline); vertical-align: top;
}
.status-table th { font-size: var(--size-caption); text-transform: uppercase; letter-spacing: var(--tracking-eyebrow); color: var(--text-muted); }
.status-pill {
  display: inline-block; font-size: var(--size-caption); font-weight: var(--weight-semibold);
  padding: 2px var(--space-3); border-radius: var(--radius-full); border: 1px solid transparent;
}
.status-pill--final       { background: var(--color-success-bg); border-color: var(--color-success-border); color: var(--color-success); }
.status-pill--interim     { background: var(--color-warning-bg); border-color: var(--color-warning-border); color: var(--color-warning); }
.status-pill--needs-copy  { background: var(--color-error-bg); border-color: var(--color-error-border); color: var(--color-error); }
.status-pill--not-built   { background: var(--color-info-bg); border-color: var(--color-info-border); color: var(--color-info); }
.status-check-list { margin: 0; padding-left: var(--space-5); font-size: var(--size-caption); color: var(--text-muted); }

/* NEEDS-COPY STUB PANEL (door-two P1 pages before copy lands) */
.needs-copy-panel {
  border: 1px dashed var(--color-ink-300);
  padding: var(--space-6);
  color: var(--text-muted);
  text-align: center;
}

/* NEEDS-GREG PANEL (publish-blocking placeholder inside body copy) */
.needs-greg-panel {
  display: inline-block;
  background: var(--color-warning-bg);
  border: 1px dashed var(--color-warning-border);
  color: var(--color-warning);
  font-size: var(--size-caption);
  font-weight: var(--weight-semibold);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
}

/* CONTACT SHEET (staging only). One row per page, both widths side by side. */
.sheet-row { padding-block: var(--space-6); border-top: var(--border-hairline); }
.sheet-row h2 { font-size: var(--size-h3); margin: 0 0 var(--space-4); }
.sheet-row h2 .caption { display: inline; font-weight: var(--weight-regular); }
.sheet-shots { display: grid; grid-template-columns: 180px 1fr; gap: var(--space-5); align-items: start; }
.sheet-shots img { width: 100%; height: auto; border: var(--border-default); border-radius: var(--radius-sm); }
.sheet-shots figcaption { margin-top: var(--space-2); }
@media (max-width: 720px) { .sheet-shots { grid-template-columns: 1fr; } }
