/* Public site CSS — merged 2026-09-13 from public-layout.css + public-media.css
   + public-portfolio.css (all three were always loaded together — see
   CLAUDE.md's CSS file structure note). Organized top-down: Composition
   (layout shapes reused by more than one component), Components in page
   order (top to bottom, matching index.php's own DOM), then Exceptions
   (state-driven overrides — .hide, [aria-disabled], .needs-truncation,
   etc.). Pure reorganization — no selectors/values changed from the three
   source files, only where each rule physically lives and how it's
   commented. */

/* ===== Composition (shared layout patterns) ===== */

/* Shared header-bar / footer-bar row shape. NOTE: .footer-content also gets
   its own, later override under "footer" below — that block's `padding`
   shorthand deliberately wins over this one's `padding-bottom` for
   .footer-content specifically (never touches .splash-top-bar). Keep that
   override physically after this block. */
.public .splash-top-bar,
.public .footer-content {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    padding: var(--base10);
    padding-bottom: 0;
}

/* Nav — the same php/navigation.php include renders this in both the header
   (#splash) and the footer, so it belongs here, not under one page
   position. */
.public .site-nav {
    padding: var(--base10);
}

.public .site-nav-list {
    list-style: none;
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--base4) var(--base8);
    align-items: center;
    justify-content: space-between;
}

/* Visually a logo/icon, not a text link — narrow exception, same footing as
   the cursor/focus-outline carve-outs (CLAUDE.md's strip-down notes). */
.public .top-link {
    color: inherit;
    text-decoration: none;
}

/* Inline SVG standing in for a text glyph — sized/baseline-aligned to read
   as part of a line of text, not centered-in-a-button the way
   .toggle-icon/.nav-icon/.open-tag-icon are (below). */
.public .icon {
    display: inline-flex;
    /* -0.125em corrects a baseline-height SVG sitting a hair high next to
       text with a real descender. No effect once nested in .icon-group
       below — vertical-align doesn't apply to flex items. */
    vertical-align: -0.125em;
}

/* Bundles several .icon instances into one unit with one collective
   accessible name (role="img" + aria-label on the group; each inner .icon
   stays aria-hidden). */
.public .icon-group {
    display: inline-flex;
    vertical-align: -0.125em;
    gap: var(--base1);
    /* "No/cancel" cursor, not dimmed opacity — tried, reverted (opacity read
       as "faded," not "inert"). Nothing here is focusable; the group's own
       aria-label already carries the real meaning regardless of hover. */
    cursor: not-allowed;
}

.public .icon svg {
    width: auto;
    height: 1em;
    fill: currentColor;
}

/* Shared section scaffold — backs renderSectionHeader()/renderSectionContent()/
   renderSectionArrow() (php/utilities.php), called by hidden-page, project,
   work, and info alike, so none of it is tied to one page-order slot below. */
.public section {
    padding-top: var(--base8);
    padding-bottom: var(--base24);
}

.public .section-header {
    padding: var(--base3) var(--base10);
}

/* Shared with .listing-year (below) — same color, kept as one declaration
   deliberately (don't split it, the value has to stay identical). Size/
   contrast reasoning: CLAUDE.md's "Fourth strip-down exception." */
.public .section-number,
.public .listing-year {
    color: var(--color-gray);
}

/* Renders inline inside the <h2> (renderSectionHeader()) — fixes an old bug
   where a flex-sibling number stayed pinned next to line 1 of a two-line
   title only. Letting it wrap as part of the same text run fixes that for
   any title length. */
.public .section-number {
    margin-left: 1px;
    font-size: 0.5em;
    vertical-align: text-top;
}

.public .section-content {
    display: flex;
    flex-direction: column;
    gap: var(--base4);
    padding: var(--base3) var(--base10) var(--base3) var(--base12);
}

.public .section-content p {
    max-width: var(--reading-measure);
}

.public .section-arrow {
    padding: var(--base6) 0 var(--base6) var(--base16);
}

.public .section-header,
.public .section-arrow,
.public .section-content,
.public .table-of-contents,
.public .wall-illustration {
    max-width: 800px;
    margin-left: auto;
    margin-right: auto;
}

/* Grows-left / fixed-right row shape — content differs per caller (lightbox
   footer, grid figcaption), this shape doesn't. */
.media-item-footer {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--base3);
}

/* Uniform grid-cell rhythm: same 2:1 box regardless of source proportions,
   cropped to fill (cover) — unlike the lightbox's true-proportion "contain"
   treatment below. The placeholder (also an <img>) matches this selector on
   its own, so an empty project doesn't break the grid's rhythm. */
.public .thumbnail-media img,
.public .thumbnail-media video,
.public .project-media-expand img,
.public .project-media-expand video {
    aspect-ratio: calc(3840/2160);
    width: 100%;
    object-fit: cover;
}

/* Stretched to fill exactly, not cropped — keeps a placeholder visually
   distinct from an actual (just oddly cropped) upload. */
.public .thumbnail-media img.media-placeholder {
    width: 100%;
    height: 100%;
}

/* Icon-only button base shape — shared by the video toggle (thumbnail/grid
   corner overlay, lightbox persistent cluster) and the lightbox's own
   Previous/Next. 44px matches this site's touch-target convention
   (admin.css), not just WCAG's 24x24 AA floor. */
.video-toggle-button,
.lightbox-nav-button {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    border-radius: var(--public-button-radius);
}

/* Position override for the toggle button wherever it sits as a corner
   overlay — the project media grid AND the Work/thumbnail grid alike (two
   different page positions, one shared placement rule). */
.public .project-media .video-toggle-button,
.public .projects-grid .video-toggle-button {
    position: absolute;
    bottom: var(--base2);
    left: var(--base2);
    border-radius: var(--public-button-radius);
}

/* Inline SVG (not <img>) — media/icon_play.svg, icon_pause.svg, read and
   embedded by getToggleIconSvg() (php/utilities.php). fill: currentColor is
   what lets Windows Forced Colors Mode recolor these along with the rest of
   the page's text; an <img>-referenced SVG can't be remapped that way. See
   ARCHITECTURE.md pattern 12 for the full reasoning. */
.toggle-icon,
.nav-icon,
.open-tag-icon {
    display: flex;
}

.toggle-icon svg,
.nav-icon svg,
.open-tag-icon svg {
    width: auto;
    height: 1em;
    fill: currentColor;
}

/* "Currently open" icon contrast technique — see ARCHITECTURE.md pattern 10a
   for the full reasoning (why difference + grayscale, and the real tradeoff
   it accepts). */
.open-tag-icon svg {
    mix-blend-mode: difference;
}

.open-tag-icon {
    -webkit-backdrop-filter: grayscale(1);
    backdrop-filter: grayscale(1);
}

/* Baseline-aligned pair: caption-text + caption-cue. */
.public .caption-content {
    display: flex;
    align-items: baseline;
    gap: var(--base2);
    min-width: 0;
    flex: 1;
}

/* "Currently open" corner tag — truly-generic mechanics (display, position,
   z-index) live in foundation.css; this is the public-only visual treatment
   (near-white icon on arbitrary photo content, unlike admin's plain
   black-on-white — see CLAUDE.md's strip-down notes, "third exception," and
   ARCHITECTURE.md pattern 10a). Padding matches the real toggle button's own
   padding (foundation.css) so both corners read as a deliberate pair. */
.public .open-tag {
    display: flex;
    top: var(--base2);
    right: var(--base2);
    padding: var(--base1);
    color: #eee;
}

/* ===== Components, in page order ===== */

/* -- splash / nav -- */

.public #splash {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    min-height: 100svh;
    padding: 0;
}

.public .splash-content {
    display: flex;
    flex-direction: column;
    flex: 1;
    align-items: center;
    justify-content: center;
    padding: var(--base10);
    padding-top: 0;
}

/* -- hidden page -- */
/* No rules of its own — entirely the shared section scaffold in Composition,
   above. */

/* -- project + lightbox -- */

.public .project-outro {
    margin-top: var(--base12);
}

.public .project-media-grid {
    list-style: none;
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 0;
}

.public .project-media-item {
    display: flex;
    flex-direction: column;
    scroll-margin-bottom: var(--base4);
    position: relative; /* for z-index */
    isolation: isolate; /* scopes the open-tag icon's blend-mode/backdrop-filter to just this item */
}

.public .project-media-item:focus-within {
    z-index: 1;
}

.public .project-media {
    display: flex;
    flex-direction: column;
    position: relative; /* for z-index */
}

.public button.project-media-expand {
    padding: 0;
}

.public .project-media-expand {
    width: 100%;
    height: 100%;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
}

.public .project-caption {
    display: flex;
    padding: var(--base2) var(--base14) var(--base4);
}

.public .project-caption .caption-text {
    flex: 1;
    min-width: 0;
}

.public .lightbox-caption .caption-text {
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    overflow: hidden;
    min-width: 0;
    max-width: none;
    flex: 1;
    /* Same value the expanded state's column-width uses (Exceptions, below)
       — expanding never jumps to a different measure. */
    max-width: var(--reading-measure);
}

.public .lightbox-caption .caption-cue {
    display: none;
    flex-shrink: 0;
}

.public #lightbox {
    position: relative;
    display: flex;
    flex-direction: column;
    /* Fixed height so the footer's on-screen position doesn't jump with
       whatever the active media's own rendered height happens to be —
       .lightbox-media-area (below) reserves the leftover space above it. */
    height: 97svh;
    padding: 0;
}

.public .lightbox-media-area {
    flex: 1;
    min-height: 0;
    display: flex;
    justify-content: center;
}

.public .instructions {}

/* Instructions are currently disabled — demoing the persistent nav cluster
   instead (see ARCHITECTURE.md, "Usage Instructions: Temporarily Disabled")
   — but this pointer-type toggle stays live for whenever they're restored.
   Don't swap to opacity/visibility without also checking
   setupLightboxInstructionsDescribedBy() (js/public-main.js), which mirrors
   this same query. */
@media (pointer: fine) {
    .public .instructions-touch { display: none; }
}

@media (pointer: coarse) {
    .public .instructions-keyboard { display: none; }
}

.public .lightbox-item {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    padding-bottom: 0;
}

/* Direct child only (>) — the boundary panel (below) is also a
   .lightbox-item, but its content is a whole grid of thumbnails several
   levels deep, not a single image/video this sizing is meant for. */
.public .lightbox-item > img,
.public .lightbox-item > video {
    display: block;
    /* contain, not cover — the lightbox shows media at true, uncropped
       proportions, letterboxing instead of cropping. */
    object-fit: contain;
    max-width: 100%;
    max-height: 100%;
}

.public .lightbox-footer {
    display: flex;
    flex-wrap: wrap;
    /* flex-start, not flex-end — an expanded multi-column caption is much
       taller than the counter/instructions; flex-end sank them to its
       bottom edge instead of aligning with the top. */
    align-items: flex-start;
    row-gap: var(--base6);
    column-gap: var(--base10);
    padding: var(--base4) var(--base10);
}

.public .lightbox-instructions {
    display: flex;
    gap: var(--base1);
}

.public .lightbox-caption-area {
    flex: 1;
}

.public .lightbox-meta {
    display: flex;
    align-items: baseline;
    gap: var(--base4);
}

.public .lightbox-nav-controls {
    display: flex;
    align-items: center;
    gap: var(--base1);
    flex-shrink: 0;
}

.public .lightbox-caption {
    scroll-margin-bottom: var(--base4);
}

.public .lightbox-counter {
    font-variant-numeric: tabular-nums;
}

.public .lightbox-item.lightbox-boundary {
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

.public .lightbox-boundary-header {
    padding: var(--base2);
}

/* Header + grid wrapped together so they read as one unit — align-items:
   flex-start lines both up on the same left edge, and the shrink-to-fit
   sizing below reserves room for the header so the grid can't overflow past
   it. Row counts are hardcoded per breakpoint for this site's fixed project
   count. See ARCHITECTURE.md's "Boundary Panel" section for the full
   redesign history and the math this replaced. */
.public .lightbox-boundary-content {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    min-height: 0;
    max-width: 66%;
/*    max-height: calc(66% - var(--base24));*/
/*    aspect-ratio: calc(3840/2160 * 3/3);*/
}

.public .lightbox-boundary .projects-grid {
    width: 100%;
    grid-auto-rows: auto;
}

/* Same breakpoint the Work-grid itself changes column count at — restated
   here since .thumbnail-item carries no media query of its own. */
@media (max-width: 1280px) {
    .public .lightbox-boundary-content {
        aspect-ratio: calc(3840 / 2160 / 2);
        max-height: calc(98% - var(--base24));
/*        max-width: 90%;*/
    }
}

/* Breakpoint: project + lightbox stacks/resizes below 550px in portrait. */
@media (max-width: 550px) and (orientation: portrait) {
    .public .project-media-grid {
        grid-template-columns: repeat(1, 1fr);
    }
    .public .project-caption {
        padding: var(--base5) var(--base10);
        font-size: 0.8em;
    }
    .public #lightbox {
/*        height: 75svh;*/
    }
    .public .lightbox-footer {
        flex-direction: column;
    }
}

/* -- work section + portfolio grid -- */

.public .portfolio-index {
    display: flex;
    flex-wrap: wrap;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--base12);
}

.public .table-of-contents {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--base12) var(--base24);
    padding: 0 var(--base6) var(--base10) var(--base10);
    align-self: center;
}

.public .toc-column {
    display: flex;
    flex-direction: column;
    gap: var(--base6);
}

.public .toc-list {
    list-style: none;
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--base6);
    padding-left: var(--base2);
}

.public .listing-item {
    display: flex;
    align-items: baseline;
}

.public .listing-year {
    flex-shrink: 0;
    align-self: flex-start;
    font-size: 0.66em;
    margin-left: 2px;
}

.public .projects-grid {
    list-style: none;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0;
    flex: 1;
}

.public .thumbnail-item {
    position: relative; /* needed for open-tag */
    isolation: isolate; /* scopes the open-tag icon's blend-mode/backdrop-filter to just this thumbnail */
}

.public .thumbnail-item:focus-within {
    z-index: 1; /* focus outline up front */
}

.public .thumbnail-media {
    width: 100%;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Thumbnails: 3 across down to here, then 2. No min-width — pure imagery,
   unlike admin's own cards, so nothing forces an earlier break. */
@media (max-width: 1024px) {
    .public .projects-grid {
        grid-template-columns: repeat(2, 1fr);
    }
}

/* Below this, TOC and thumbnails stack instead of sitting side by side. Type
   sizes (h2/h3/.toc-list) also step up here — same content, comfortably
   larger now that it's reading as a full-width column instead of a third of
   a wide one. */
@media (max-width: 768px) {
    .public .portfolio-index {
        align-items: stretch;
    }

    .public .table-of-contents {
        max-width: none;
    }

    .public h2 {
        font-size: 2.33em;
    }

    .public h3 {
        font-size: 1.66em;
    }

    .public .toc-list {
        font-size: 1.3em;
    }
}

/* -- info -- */
/* No rules of its own — entirely the shared section scaffold in Composition,
   above. */

/* -- 404 -- */
/* render404() (php/utilities.php) is its own small, standalone document —
   not part of index.php's shared template — so it gets its own component
   entry here rather than a slot in that page's own top-to-bottom order.
   Same shape as #splash above: min-height on the flex column (100vh, with
   the 100svh override for mobile browser chrome) plus flex: 1 on the section
   that should grow, so the nav below it is pushed to the bottom of the
   viewport on short content without any hand-tracked height math. Centering
   is vertical only here — .section-header/.section-content already center
   themselves horizontally via their own max-width + margin: auto (Composition,
   above), so this only needs to distribute them within the column's cross axis. */
.public .not-found-shell {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    min-height: 100svh;
}

.public #not-found {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
}

/* -- footer -- */

/* Footer-specific additions on top of the shared row shape (Composition,
   above) — .footer-content styling lives in two places by design under this
   convention. Order matters: this block's own `padding` shorthand
   deliberately overrides the shared block's `padding-bottom` for
   .footer-content specifically — keep this physically after Composition. */
.public .footer-content {
    font-size: calc(var(--base4) * 0.55);
    font-family: var(--Space-Grotesk);
    gap: var(--base2) var(--base4);
    padding: var(--base10);
    padding-top: 0;
    flex-wrap: wrap;
}

/* ===== Exceptions (state overrides) ===== */

/* Same 0.5-opacity "inert" convention as admin.css's own disabled state —
   WCAG 1.4.11 doesn't require a contrast minimum for inactive controls, and
   reduced opacity is its own suggested way to signal that. cursor:
   not-allowed is the same real affordance exception cursor: pointer already
   is (foundation.css). */
.lightbox-nav-button[aria-disabled="true"],
.video-toggle-button[aria-disabled="true"] {
    opacity: 0.5;
    cursor: not-allowed;
}

.public .lightbox-caption.needs-truncation .caption-cue {
    display: inline;
}

/* :focus-visible, not :focus — this reveal is for sighted keyboard/switch
   users; a screen reader already gets aria-label="No caption" regardless. */
.public .lightbox-caption[data-has-caption="0"] .caption-text {
    opacity: 0;
}

.public .lightbox-caption[data-has-caption="0"] .caption-text:focus-visible {
    opacity: 1;
}

.public .lightbox-caption.needs-truncation .caption-text {
    cursor: pointer;
}

.public .lightbox-caption.needs-truncation .caption-text:focus {
    /* .caption-text is display:-webkit-box unconditionally (required for
       -webkit-line-clamp) — has to reset to a normal block box for columns
       to render at all. */
    display: block;
    -webkit-line-clamp: unset;
    overflow: visible;
}

/* Also styles the always-multi-column .project-caption .caption-text
   (Components, above) — same reading measure, so expanding never jumps to a
   different column width. Kept as one declaration deliberately; don't split
   it, the values have to stay identical. */
.public .project-caption .caption-text,
.public .lightbox-caption.needs-truncation .caption-text:focus {
    max-width: none;
    column-width: var(--reading-measure);
    column-gap: var(--base8);
}

.public .lightbox-caption.needs-truncation .caption-text:focus ~ .caption-cue {
    display: none;
}

/* Sighted half of "this doesn't count right now" on the boundary panel — the
   non-visual half is the counter's aria-label, swapped in JS
   (displayLightboxItem(), js/public-main.js). Same 0.5 opacity as the
   dimmed nav buttons above (WCAG 1.4.1). */
.public .lightbox-counter.frozen {
    opacity: 0.5;
}

/* ===== Info Section: Wall Illustration ===== */
.public .wall-illustration {
    padding: var(--base8) var(--base10);
}

.public .info-illustration {
    display: block;
    width: 280px;
    height: auto;
/*    margin: auto;*/
    margin-left: 70px;
}

@media (max-width: 600px) {
    .public .info-illustration {
        margin-left: 0;
    }
}