/* ============================================================
   Photography Draft 2026
   Companion stylesheet for photography-draft.php
   Inherits CSS variables from theme style.css
   Breakpoints follow the canonical reference in plugin
   assets/base-sections.css (1200 / 768 / 480, compound height rules)
   ============================================================ */

.photography-draft {
    /* Per-page accent (PROJECT.md §2.4), matching the live photography page's
       cobalt. Routed through the semantic tokens rather than painting the raw
       pigment onto the controls, so anything accent-coloured added later
       follows automatically.

       Scoped to this container rather than :root deliberately: header-branded.php
       re-declares --highlight-color in an inline :root that loads AFTER
       wp_head(), so a :root here would lose to it. Declaring on the element
       beats the inherited value whatever the ancestor's specificity. */
    --highlight-color: var(--cr-cobalt);
    --link-hover-color: var(--cr-cobalt);

    background: var(--main-content-bg);
    /* `clip`, not `hidden`. overflow-x: hidden forces the other axis to
       compute as `auto`, which quietly makes this element a scroll container
       — an extra scrollport the browser manages, with its own scroll
       anchoring, wrapped around the whole page. `clip` cuts off horizontal
       overflow without any of that. */
    overflow-x: clip;
}


/* ============================================================
   INTRO
   Uses the shared .strategy-intro / .strategy-intro-body structure, so
   everything typographic — serif face, 1.875rem stepping to 1.25rem on
   mobile, centred, var(--text-color), and the opacity/filter transition
   the scroll fade rides on — comes from base-sections.css. Nothing about
   the type is restated here.

   All this file adds is the 66vh box the text centres inside.
   ============================================================ */

.photo-draft-intro {
    min-height: 66vh;
    display: flex;
    align-items: center;
    justify-content: center;

    /* The vertical padding is header clearance, and it has to be symmetric.
       66vh is a min-height, so when the copy is taller than the box the box
       grows and the text fills it edge to edge — starting at y=0, which on
       this page is behind the fixed header. Equal top and bottom padding
       gives the text a floor it can't rise above while leaving the box's
       centre exactly where it was, so nothing moves at sizes where the copy
       already fits. Twice --header-height, so the overflowing case clears
       the header with air to spare rather than tucking right under it;
       --header-height is header-branded.php's own variable, the same one
       the toolbar below pins against. */
    padding: calc(var(--header-height) * 2) 2vw;
}

/* base-sections.css gives .strategy-intro `padding: 200px 0 6rem` for its
   usual job as a section lead-in. Here the 66vh box does the spacing, so
   that padding has to go or the text sits low rather than centred. This also
   clears the 3rem padding/margin the tablet breakpoint adds for the divider
   below, since that would push the text off centre at those sizes. */
.photo-draft-intro .strategy-intro {
    padding: 0;
    margin: 0;
}

/* Below 1200px base-sections.css draws a 50%-width hairline under
   .strategy-intro to separate it from the section that follows. On this page
   the intro is followed by a lot of deliberate air and a scattered grid, so
   the rule reads as a stray mark rather than a divider. Scoped to this
   page's wrapper, so the shared component keeps it everywhere else. */
.photo-draft-intro .strategy-intro::after {
    display: none;
}

/* photograph-page.css sets this for the same block on the live photography
   page, but that stylesheet only loads on page-photography.php. */
.photo-draft-intro h3 {
    text-wrap: balance;
    margin-bottom: 2rem;
}

@media (max-width: 768px), ((max-width: 1200px) and (max-height: 768px)) {
    .photo-draft-intro {
        /* Where the copy actually outgrows 66vh. The header is taller here
           than --header-height alone describes — this breakpoint gives it
           `height: calc(48px + env(safe-area-inset-top))` — so the inset is
           added back on top of the doubled variable rather than trusting it. */
        padding: calc(var(--header-height) * 2 + env(safe-area-inset-top, 0px)) 4vw;
    }
}


/* ============================================================
   FILTER BAR
   Sits between the intro and the grid: the three views on the left,
   the standing hint on the right, both on one line at the page's own
   2vw / 4vw gutters.

   Legitima throughout, matching the page's serif voice, stepping down a
   notch at each breakpoint: 1.125rem on desktop, 1rem on tablet, 0.875rem
   on mobile. Sized in literal rem, never an --fs-* token — that scale is
   drawn for Innovator Grotesk and runs large on Legitima. Every part is
   nowrap: the spec is a single line, so the copy on either side is what
   will give if it grows.
   ============================================================ */

.photo-draft-toolbar {
    /* Pinned just under the fixed header once the intro has scrolled past.
       --header-height (calc(48px + 2vw)) and the 1000 the header sits at are
       both from header-branded.php's inline block, so this tracks the header
       at every width rather than guessing an offset.

       Sticky works here despite main.photography-draft's overflow-x: clip:
       `clip` on one axis leaves the other visible and creates no scroll
       container, so this still resolves against the viewport. That is the
       same property of `clip` the wrapper chose it for. */
    position: sticky;
    top: var(--header-height);
    z-index: 1001;

    /* Deliberately transparent: cards pass behind this bar in full view.
       Nothing to remove here — no background and no backdrop-filter is the
       intended look, so don't "fix" the legibility by adding one back. */

    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
    /* No padding-top — the bar sits hard against the header when pinned. */
    padding: 0 2vw 1rem;
    font-family: var(--serif-font), serif;
    /* Literal rem, not an --fs-* token: that scale is Innovator Grotesk's
       and reads oversized on Legitima. */
    font-size: 1.125rem;
    color: var(--text-color);
}

.photo-filter {
    display: flex;
    align-items: baseline;
    gap: 0.5ch;
    white-space: nowrap;
}

.photo-filter-sep {
    /* Decorative, and deliberately lighter than either state of the words
       it divides, so it reads as punctuation rather than a fourth option. */
    opacity: 0.4;
}

.photo-filter-option {
    /* <button> reset, matching .wc-expand's — same reasoning, a control that
       has to look like plain type. */
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    color: inherit;
    cursor: pointer;

    /* Holds the button at the width of its own SELECTED form, so choosing a
       view doesn't shove the words after it sideways. The visible span and a
       hidden 600-weight copy of the same label share one grid cell; the cell
       takes the wider of the two, which is always the bold one. Hence
       data-label on the element — ::after can only read an attribute. */
    display: inline-grid;
    font-weight: 400;
}

.photo-filter-option > span,
.photo-filter-option::after {
    grid-area: 1 / 1;
}

.photo-filter-option::after {
    content: attr(data-label);
    font-weight: 600;
    visibility: hidden;
    /* Belt and braces: never let the ghost affect the line box. */
    height: 0;
    overflow: hidden;
}

.photo-filter-option.is-selected {
    font-weight: 600;
}

.photo-filter-option:hover {
    color: var(--link-hover-color);
}

.photo-draft-hint {
    /* base-sections.css already gives every `p` weight 400 and no margin,
       so neither is restated here. font-family and font-size ARE, and both
       are load-bearing: that global rule sets the sans face at --fs-base,
       and would otherwise beat inheritance and leave the hint in Innovator
       Grotesk at a size that never steps with the rest of the row. */
    font-family: inherit;
    font-size: inherit;
    font-style: italic;
    /* Standing instruction rather than part of the page's voice, so it sits
       back from the filter it shares the row with. The token carries its own
       dark-scheme value, so this needs no prefers-color-scheme block. */
    color: var(--text-color-muted);
    white-space: nowrap;
    text-align: right;
}

/* A card the current view excludes. display:none rather than a fade: these
   are grid items, so anything short of removing them from layout leaves a
   hole where the card was. The half-drop and the mobile left/right pinning
   both compensate — see the `of` selectors below. */
.photo-card-hidden {
    display: none;
}

/* Tablet and below: the switch moves to the right and the hint to the left.
   row-reverse rather than reordering the markup, so the filter stays first in
   the DOM and tab order still reaches it before anything below the bar. */
@media (max-width: 1200px) {
    .photo-draft-toolbar {
        flex-direction: row-reverse;
        font-size: 1rem;
    }

    .photo-draft-hint {
        text-align: left;
    }
}

@media (max-width: 768px), ((max-width: 1200px) and (max-height: 768px)) {
    .photo-draft-toolbar {
        padding-left: 4vw;
        padding-right: 4vw;
        font-size: 0.875rem;
    }
}


/* ============================================================
   THE GRID
   Plain (non-`dense`) grid, so DOM order == visual order, and the reading
   and tab order follow what's actually on screen. (The gallery template
   shuffles the cards on every load by moving the elements themselves, for
   the same reason — CSS `order` would have split the two apart.) Grid
   rather than flex-wrap because the column width is then exact by
   construction, with no `calc(100% - n*gap)/n` basis to round badly and
   drop a card onto the next row.

   Column count is a single token, set once per breakpoint.

   HALF-DROP: alternate columns are pushed down by --half-drop, the way
   a half-drop textile repeat staggers its tiles. Cards centre on their
   grid line first (align-items: center), then the drop is applied as a
   transform on top of that — a transform rather than a margin so the
   offset is purely visual and doesn't inflate the row it sits in.
   ============================================================ */

.photo-floating-grid {
    --cols: 3;
    /* Half the row gap. Keeping that ratio means every column holds the same
       vertical rhythm, just phase-shifted — a dropped card and the undropped
       one below it stay exactly as far apart as two undropped ones. */
    --half-drop: 10vw;

    display: grid;
    grid-template-columns: repeat(var(--cols), 1fr);
    align-items: center;
    column-gap: 6vw;
    row-gap: 20vw;
    margin: 0 auto;
    /* The gap before the contact block lives HERE rather than on the
       contact section's top padding, and it is the same --close-gap the
       contact section pads its bottom with, so the page closes
       symmetrically. Putting it on the grid keeps it out of the contact
       section's box, which is what its full-bleed gradient is sized
       from — top padding there stretched the green ramp over dead air. */
    padding: 2rem 2vw var(--close-gap);
}

/* One grid item == one column slot. The photo inside is smaller than
   the slot; the empty remainder is what creates the organic scatter. */
.photo-card {
    min-width: 0;
    --drop: 0px;
    transform: translateY(var(--drop));
    transition: opacity 0.3s ease, filter 0.3s ease;
}

/* 3 columns: drop the middle one. nth-child is modulo the column count, so
   this selects a whole column down the page, not just the first row — which
   is also why the pattern has to be restated per breakpoint.

   Every drop rule is wrapped in :where() to hold it at the same specificity
   as the bare `.photo-card` reset above. Without that the resets below would
   lose on specificity despite coming later, and this 3-column stagger would
   leak into the 2-column and single-column layouts on top of theirs. */
/* Counted over the VISIBLE cards, not all of them. Filtering removes grid
   items from layout but not from the DOM, and a bare :nth-child() keeps
   counting the removed ones — so after a filter the drop landed on cards
   by their original position and the stagger came apart. `of S` counts
   within the matching set, which is exactly the set the grid is laying out.
   Unsupported in a very old browser means the rule drops and the grid sits
   flat: the page still works, it just loses the stagger. */
.photo-card:where(:nth-child(3n+2 of .photo-card:not(.photo-card-hidden))) {
    --drop: var(--half-drop);
}

@media (max-width: 1200px) {
    .photo-floating-grid { --cols: 2; }

    /* 2 columns: drop the right-hand one. */
    .photo-card { --drop: 0px; }
    .photo-card:where(:nth-child(2n of .photo-card:not(.photo-card-hidden))) { --drop: var(--half-drop); }
}

@media (max-width: 768px), ((max-width: 1200px) and (max-height: 768px)) {
    .photo-floating-grid {
        --cols: 1;
        row-gap: 16vw;
        padding-left: 4vw;
        padding-right: 4vw;
    }
    /* Nothing to restate for the bottom padding: --close-gap drops to the
       mobile figure with the contact section's rules at the end of this
       file, and the padding-bottom above follows it. */

    /* Single column — nothing to stagger against. */
    .photo-card { --drop: 0px; }
}


/* ============================================================
   CARD GEOMETRY
   --card-scale (0.66–1) and --card-offset (0 … 1 - scale) are
   assigned per card, per page load, by the inline randomizer the
   plugin's photo-floating-gallery.php emits after the grid.
   Because offset never exceeds (1 - scale), width + margin-left
   never exceeds 100% — a card cannot leave its column by
   construction, with no clamping needed.
   The fallbacks (1 / 0) render a full-width left-aligned grid if
   that script never runs.

   Both are then ANIMATED, not fixed for the life of the page:
   watercolor-reveal.js drives them to 1 / 0 across a reveal, so the
   card grows into its column as the photograph dissolves off it, and
   back to the rolled pair as the photograph floods in. Deliberately
   not a CSS transition — the reveal's clock is trimmed per click (it
   stops when the last visible pixel has changed), and the growth has
   to land on that same clock, alongside the caption fade.

   HOVER runs the same travel on the same clock and leaves the photograph
   alone, so a pointer resting on a card fills the column, at the speed a
   click would, without revealing anything. "The same clock" means the
   TRIMMED one the growth actually runs on and not the nominal four
   seconds — the JS works it out per card, for a press in the middle.
   It is driven from the same place for that reason, and because the two
   have to agree about who owns the card's size at any moment. The
   un-reveal owns it outright: a card shrinks back to its rolled pair as
   the photograph floods in, whether or not the pointer is still on it,
   because that shrink is part of the transition being watched. The hover
   growth is then spent, and the pointer has to leave and come back
   before it will grow again — otherwise the card would snap straight
   back up the moment the photograph landed. A CSS transition on a hover
   class could not see any of that, and the reveal's inline styles would
   outrank it in any case.

   The column-containment argument survives the animation: both vars
   move on one eased progress g, so scale + offset traces
   s + o + g(1 - s - o), which is monotonic in g and reaches exactly 1.
   A growing card still cannot cross into the next column.

   Since this is width and margin — real layout, not a transform — a
   card growing re-measures its grid row, and the page below it shifts
   for the duration of the reveal.
   ============================================================ */

.wc-stage {
    position: relative;
    overflow: hidden;
    width: calc(var(--card-scale, 1) * 100%);
    margin-left: calc(var(--card-offset, 0) * 100%);
    /* --card-ratio is set inline per card from the image's real dimensions,
       so the box exactly matches its aspect ratio — no cropping needed. */
    aspect-ratio: var(--card-ratio, 4/5);
    cursor: crosshair;
    outline-offset: 4px;
    /* Same shadow as the homepage reviews section. On the stage rather than
       on the media inside it: `overflow: hidden` clips an element's children,
       not its own box-shadow, so this renders — the previous attempt put it on
       the image, where the stage's overflow swallowed it on all four sides. */
    /* --wc-inv-scale is 1 in the grid and 1/scale while the card is expanded,
       published per frame by watercolor-reveal.js. Dividing by it holds the
       shadow's apparent size as the object grows, instead of a hairline lift
       swelling into a three-pixel one. */
    box-shadow: 0 calc(8px * var(--wc-inv-scale, 1))
                  calc(25px * var(--wc-inv-scale, 1)) rgba(0, 0, 0, 0.2);
}

/* A black shadow is invisible on a near-black ground, so it flips to white —
   reading as a soft lift off the page rather than a cast shadow. */
@media (prefers-color-scheme: dark) {
    .wc-stage {
        box-shadow: 0 calc(8px * var(--wc-inv-scale, 1))
                      calc(25px * var(--wc-inv-scale, 1)) rgba(255, 255, 255, 0.2);
    }
}

/* Video cards are non-interactive — nothing to click, so no crosshair. */
.photo-card-video .wc-stage {
    cursor: default;
}

.wc-layer {
    position: absolute;
    inset: 0;
}

/* Static photo shown while a card has no live WebGL context: before its
   first activation, and again after teardown while offscreen. The canvas
   paints fully opaque over it once active, and the JS removes it entirely
   on the first successful texture upload. */
.wc-poster {
    background-size: cover;
    background-position: center;
}

.wc-gl {
    width: 100%;
    height: 100%;
    display: block;
    /* The canvas covers the whole card and sits on top, so it would swallow
       every click on the "read more" link underneath it. It's decorative —
       let clicks fall through. The reveal listener is on .wc-stage, and it
       reads coordinates from the stage's own rect, so nothing depends on
       which layer was actually hit. */
    pointer-events: none;
}


/* ============================================================
   REVEALED TEXT
   No background — the watercolor dissolve exposes the page itself.
   The border lives on the same element as the text, so it fades in
   and out in lockstep with it.

   While hidden this layer is `inert` (toggled by watercolor-reveal.js),
   which keeps the invisible "read more" link out of the tab order and
   un-clickable — opacity: 0 alone leaves it both focusable and hittable.
   That tracks what is VISIBLE, not what was asked for: inert comes off the
   moment anything is going to be shown and only goes back on once the
   caption has finished leaving, so both controls stay live for the whole of
   a fade in either direction.

   The layer is also the one part of the card that must NOT be laid out at
   the card's animated size — see the geometry note on .wc-text below.
   ============================================================ */

.wc-text {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    padding: 1vw;
    opacity: 0;
    /* Held at its apparent width while the card is expanded, for the same
       reason as the shadow above. Everything else about this layer — the type,
       the padding — is left to zoom with the object, because that is what it
       is: one object changing size. The rule is the exception because a
       hairline is a hairline at any size; at 3.5x it reads as a frame. */
    border: calc(1px * var(--wc-inv-scale, 1)) solid var(--text-color);

    /* Opaque, so an expanded card reads as an object rather than a hole.
       On the grid this changes nothing: the card sits directly on this same
       colour, which is exactly what the dissolve used to expose. Lifted over
       the dimmed page it is the difference between a card and a window. */
    background: var(--main-content-bg);

    /* Laid out at the size the card ENDS at, then scaled back down onto the
       size it currently is.

       As a plain inset:0 layer this box was --card-scale of the slot, so it
       widened on every frame of the growth and the caption rewrapped the whole
       way in — fixed type in a moving measure. The stage is exactly
       --card-scale of the slot, so 100% / --card-scale is exactly the slot: a
       constant, whatever the animation is doing. The line breaks are therefore
       computed once and cannot change, and the transform maps that fixed box
       precisely back onto whatever the stage currently is.

       Nothing about the resting revealed state moves: at --card-scale 1 the
       width is 100% and the transform is the identity. Only the transition
       differs — the padding and border ride the zoom in with the photograph
       rather than sitting at final size on a smaller card.

       right/bottom are released because width and height are driving instead;
       leaving inset:0 to over-constrain the box works but says the opposite of
       what's meant. Hit-testing follows the transform, so the two controls
       stay clickable at every point in the growth. */
    right: auto;
    bottom: auto;
    width: calc(100% / var(--card-scale, 1));
    height: calc(100% / var(--card-scale, 1));
    transform: scale(var(--card-scale, 1));
    transform-origin: top left;
}

/* Article body prose, matching `.story-content-inner p` in
   story-templates.css exactly — same serif, same 1.5 / 1.25 / 1.125rem
   steps at the same breakpoints. Fixed at each step and deliberately
   independent of --card-scale, so copy reads identically whatever size
   its card happened to roll. The responsive steps live with the rest of
   the breakpoints at the foot of this file. */
.wc-text-body {
    font-family: var(--serif-font);
    font-size: 1.5rem;
    line-height: 1.4;
    color: var(--text-color);
}

/* Bottom row of the card: expand on the left, read more on the right.
   space-between puts expand hard left when it's the only one there, which
   is the common case — read more is opt-in per card in the CMS. */
.wc-card-actions {
    margin-top: auto;
    width: 100%;
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
}

/* One rule for both controls — they're the same thing wearing different
   labels. Fixed size, deliberately not tied to --card-scale. */
.wc-read-more,
.wc-expand {
    font-family: var(--primary-font);
    font-size: var(--fs-base);
    font-weight: 600;
    color: var(--text-color);
    text-decoration: none;
    /* <button> resets, so .wc-expand matches the anchor exactly */
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
}

.wc-read-more:hover,
.wc-expand:hover {
    color: var(--link-hover-color);
}

/* Belt and braces: keeps read more right-aligned even if expand is absent. */
.wc-read-more {
    margin-left: auto;
}


/* ============================================================
   VIDEO CARDS
   Deferred load (preload="none" + data-src), poster cross-fades out
   once the video can play through.
   ============================================================ */

.photo-card-video .wc-poster-img,
.photo-card-video video {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    /* contain, not cover — the box already matches the media's own aspect
       ratio (via --card-ratio), so this is just a safety net against crop */
    object-fit: contain;
}

.photo-card-video video {
    opacity: 0;
    transition: opacity 0.4s ease;
}

.photo-card-video.video-loaded video {
    opacity: 1;
}

.photo-card-video .wc-poster-img {
    transition: opacity 0.4s ease;
}

.photo-card-video.video-loaded .wc-poster-img {
    opacity: 0;
}


/* ============================================================
   MOBILE — single column.
   Scale stays random, but the horizontal offset is pinned so cards
   alternate flush-left / flush-right down the page instead of
   drifting to arbitrary positions in a very wide column.
   ============================================================ */

@media (max-width: 768px), ((max-width: 1200px) and (max-height: 768px)) {
    .photo-card:nth-child(odd of .photo-card:not(.photo-card-hidden)) .wc-stage {
        margin-left: 0;
    }

    .photo-card:nth-child(even of .photo-card:not(.photo-card-hidden)) .wc-stage {
        margin-left: calc((1 - var(--card-scale, 1)) * 100%);
    }
}


/* ============================================================
   LIGHTBOX FIGURE
   The backdrop and close button come from the shared .photo-lightbox
   component in the plugin's base-sections.css. What's added here is the
   frame the watercolor canvas draws into.

   The figure takes the photo's own aspect ratio and is sized to the
   largest box of that ratio that fits inside 96vw x 96vh — so the canvas
   matches the image exactly, the shader's cover-fit crops nothing, and
   the effect's geometry is identical to a card's.
   ============================================================ */

/* The shared component fades the whole overlay's opacity — and the canvas is
   inside it, so the photo was being faded out by CSS while the watercolor
   dissolve had barely started. Two exits at once, the quicker one winning.

   So here the backdrop colour moves onto a ::before that fades by itself and
   the container stays fully opaque. The photo is then dissolved by the shader
   alone, against a wash that fades over exactly the same window. Both use the
   duration and curve watercolor-reveal.js publishes from its own constants. */
.photography-draft .photo-lightbox,
.photography-draft .photo-lightbox.active {
    background: transparent;
    opacity: 1;
}

.photography-draft .photo-lightbox::before {
    content: "";
    position: absolute;
    inset: 0;
    background: var(--lightbox-bg);
    opacity: 0;
    transition: opacity var(--lightbox-fade) var(--lightbox-ease);
}

.photography-draft .photo-lightbox.active::before {
    opacity: 1;
}

/* Absolute ::before, positioned figure, both z-index auto — so they paint in
   DOM order and the photo sits above the wash. */
.photo-lightbox-figure {
    position: relative;
    --lb-ratio: 1.5;
    aspect-ratio: var(--lb-ratio);
    width: min(96vw, calc(96vh * var(--lb-ratio)));
    max-width: 96vw;
    max-height: 96vh;
    cursor: crosshair;

    /* The same frame a card wears, so the FALLBACK path looks like the rest of
       the page rather than a bare photograph on a wash. Fixed values, not
       counter-scaled: this box no longer grows out of anything — an expand is
       the card itself scaling up now, and this overlay is only reached when
       there is no card context to scale (reduced motion, or WebGL gone).

       outline rather than border: it takes no part in layout whatever the
       box-sizing, so the aspect ratio, the 96vw/96vh fit and the canvas that
       fills this box are all left exactly as they were. The negative offset
       pulls it inside the edge so it reads as a border. */
    outline: 1px solid var(--text-color);
    outline-offset: -1px;
    box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
}

/* Matching .wc-stage: a black shadow is invisible on a near-black ground, so
   it flips to white and reads as a lift off the page. */
@media (prefers-color-scheme: dark) {
    .photo-lightbox-figure {
        box-shadow: 0 8px 25px rgba(255, 255, 255, 0.2);
    }
}

/* EXPANDED. The card doesn't hand its photograph to an overlay — it becomes
   the expanded view itself, scaled and translated to the middle of the
   viewport by a transform on its .wc-stage.

   All this rule does is lift it over the backdrop wash (z-index 10000), which
   is borrowed to dim the grid behind. The transform stays on the stage rather
   than the card because .photo-card already carries the half-drop translate,
   and because the card's layout box must be left exactly where it is: the slot
   never collapses, nothing reflows, and no placeholder is needed.

   Nothing clips it. main.photography-draft has overflow-x: clip, but it spans
   the full page width and the expanded box is 96vw centred, so it fits; and
   `clip` on one axis leaves overflow-y visible, so the vertical overflow is
   fine too. Verified against the real page at 1440x900 before this was built. */
.photo-card.photo-card-expanded {
    position: relative;
    z-index: 10001;
}

/* The overlay reduced to the wash alone, for a card expanding over it. The
   figure inside is the fallback path's picture; an expanding card IS the
   picture, so it would be a second copy of the same photograph. */
.photo-lightbox.photo-lightbox-bare .photo-lightbox-figure {
    display: none;
}

/* Inside the figure this is the no-WebGL / reduced-motion fallback, so it
   fills the frame rather than sizing itself the way it does standalone on
   the live photography page. */
.photo-lightbox-figure .photo-lightbox-image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    max-width: none;
    max-height: none;
    object-fit: contain;
}

.photo-lightbox-gl {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
}

/* Exactly one of the two is ever live. */
.photo-lightbox-figure.wc-gl-active .photo-lightbox-image {
    visibility: hidden;
}

.photo-lightbox-figure:not(.wc-gl-active) .photo-lightbox-gl {
    display: none;
}


/* ============================================================
   CARD TEXT — the article prose scale, copied from
   `.story-content-inner p:not(.story-meta)` in story-templates.css.
   ============================================================ */

/* Covers tablet and mobile both: the mobile query's compound clause sits
   inside max-width 1200 too, so a landscape phone matches this as well. */
@media (max-width: 1200px) {
    .wc-text { padding: 2vw; }
    .wc-text-body { font-size: 1.25rem; }
}

@media (max-width: 480px), ((max-width: 1200px) and (max-height: 480px)) {
    .wc-text-body { font-size: 1.125rem; }

    /* The two action labels drop one notch on the token scale — on a phone
       the card is narrow enough that --fs-base crowds the bottom row. */
    .wc-read-more,
    .wc-expand { font-size: var(--fs-sm); }
}


/* ============================================================
   CONTACT SECTION — this page's breathing room
   Twice the shared figure from template-parts/contact-section.css
   on the BOTTOM, and nothing on the top: the scattered cards want
   more air before the close than the homepage's tighter sections
   do, and the grid above supplies the air on its side of the seam.

   --close-gap is a scoped local (PROJECT.md §3, exception 3), not
   a design token. It exists so "the same amount" is structural —
   the grid's padding-bottom and this section's read one number per
   breakpoint instead of two hand-matched calcs that drift.

   It lives on the BODY because .contact-section is a SIBLING of
   .photography-draft rather than a descendant (the part is included
   after </main> so its full-bleed gradient escapes the padded,
   clipped wrapper) — the body is the only ancestor both share.
   Matching on the body also scopes the whole block to this page,
   which matters because the part is shared with the homepage, and
   branded pages emit no body_class() (PROJECT.md §6).

   Both breakpoints are restated: the scoped rule outspecifies the
   shared mobile figure, so leaving it out would ship the desktop
   padding to phones.
   ============================================================ */

body:has(.photography-draft) {
    --close-gap: calc(4vw + 80px + 160px);
}

body:has(.photography-draft) .contact-section {
    padding: 0 0 var(--close-gap);
}

@media (max-width: 768px), ((max-width: 1200px) and (max-height: 768px)), (max-width: 480px) {
    /* The shorter mobile footer, doubled the same way. The grid's query
       above stops at the two clauses this shares with it — a viewport
       narrow enough for the 480 clause matches max-width 768 anyway. */
    body:has(.photography-draft) {
        --close-gap: calc(4vw + 70px + 160px);
    }

    body:has(.photography-draft) .contact-section {
        padding: 0 0 var(--close-gap);
    }
}
