@import url("styles.css");

/* ============================================================
   index.css — site-specific styles, layered on top of styles.css
   (the shared base imported above).
   ============================================================ */


:root {
    --paper: #f2efe6;
    --ink: #1a1714;
    --ink-soft: rgba(26, 23, 20, 0.62);
    --ink-faint: rgba(26, 23, 20, 0.38);
    --navy: #1f2d4a;

    --serif: ui-serif, "Iowan Old Style", "New York", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
    --sans: "Louis George", -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif;

    --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

/* ============================================================
   Experience — NYC skyline scroll-pinned color → cartoon drain
   ============================================================ */

/* Defensive backstop: any frame the compositor fails to paint in time
   (e.g. fast-scrolling between sections, GPU layer shuffle) falls
   through to the html canvas. Default canvas-white reads as a flashing
   horizontal seam during fast scroll; --ink matches the dominant dark
   tone of surrounding elements so a missed frame is invisible rather
   than a white strip. Purely defensive — if the layout is right, this
   is never seen. */
html {
    background-color: var(--ink);
}

/* Disable browser scroll-snap entirely in the sandbox. Mandatory snap
   from styles.css would prevent stopping mid-skyline; proximity used to
   work but kept snapping aggressively at every section boundary. With
   no scroll-lock anywhere in the sandbox, the page just scrolls freely. */
body {
    scroll-snap-type: none;
}

/* styles.css ships .nav-div with `width: 15vh` (a typo — vw was
   intended). 15vh on a 16:9 viewport ≈ 8.5vw — that's the visual
   proportion the layout was designed around, restated in vw so it's
   robust across aspect ratios. The remaining ~6.5vw gap to the right
   of the nav is intentional and falls through to html's --ink
   backstop, so it reads as part of the dark surround rather than a
   bug. */
.container-about .nav-div {
    width: 8.5vw;
}

/* The section enters at the lock position (image-y = HORIZON_RATIO × H at
   viewport top). Inside the sticky pin:
     phase B (drain, V²/H of scroll)  — clip-path traverses the
       on-screen portion of the drain line ONLY (cbpStart → cbpEnd).
       phase length is V²/H rather than V because the drain line in
       stage coords moves at the same px/scroll rate as the image; a
       V-tall on-screen window of the drain line corresponds to V²/H
       of scroll. This eliminates the "dead presses" where the drain
       line was off-screen above/below the viewport.
     phase C (translate, (1 − HR) × H − V) — image continues scrolling
       until its bottom hits the viewport bottom.
   Section height = phaseB + phaseC + V = V²/H + 0.68·H.
   The exact value depends on V²/H, which CSS calc can't compute
   portably (length × length / length isn't standard). The IIFE's
   setSectionHeight() sets the precise value via inline style at init
   and on resize. The CSS height below is a close fallback used only
   for the brief instant before the script runs — sized slightly too
   tall (V + 0.68·H) so the pin has room to engage; JS immediately
   corrects to the exact value. */
.exp-skyline {
    position: relative;
    width: 100vw;
    /* No background: the color photo and cartoon (both inside the
       sticky pin's stage) ARE the surface during this section. Any
       frame they don't cover falls through to html's --ink backstop.
       Skyline images are preloaded + force-decoded in the IIFE below
       so they're always painted by the time the user scrolls in. */
    height: calc(100vw * 4 / 3 * 0.68 + 100vh);
    /* Sibling sections (.container) are float:left; this needs to float too,
       otherwise earlier sections render behind / get hidden. */
    float: left;
}

.skyline-pin {
    position: sticky;
    top: 0;
    height: 100vh;
    overflow: hidden;
}

/* Stage holds both image layers at full natural display height (100vw wide,
   100vw × 4/3 tall — the color photo is 2928×3904 = 3:4 portrait). JS sets
   transform:translate3d(0, ty, 0) each frame to drive phases A and C. */
.skyline-stage {
    position: absolute;
    top: 0;
    left: 0;
    width: 100vw;
    height: calc(100vw * 4 / 3);
    will-change: transform;
}

.skyline-layer {
    position: absolute;
    inset: 0;
    background-size: 100% 100%;
    background-position: center;
    background-repeat: no-repeat;
}

/* Cartoon sits inside .skyline-stage (sibling of .skyline-color) with
   inset:0, pixel-aligned with the color photo. The stage's transform
   translates both layers together — no JS-driven `top` chase, no
   compositor-lag seam. z-index:1 keeps it below the color layer
   (z-index:2); the color's clip-path drain reveals it. The pin's
   overflow:hidden clips it to the viewport box. */
.skyline-cartoon {
    background-image: url('images/nyc_skyline_cartoon.webp');
    z-index: 1;
}

/* JS sets clip-path: inset(0 0 N% 0) each frame, where N grows from 0 to
   100 during phase B. The color image stays in place — only its bottom
   N% is clipped, exposing the identically-positioned cartoon below. The
   B&W appears to "replace" the color from the bottom up, since both
   images are pixel-aligned. DREAMER (inside this layer) is clipped along
   with the bottom of the color image as the drain rises past it. JS
   writes clip-path on the very first frame, so no static initial value
   is needed here — will-change is the only hint that stays. */
.skyline-color {
    background-image: url('images/nyc_skyline_color.webp');
    z-index: 2;
    will-change: clip-path;
}

/* Shared label styling — large, all-caps, sans-serif, widely tracked.
   ~5vw scales with viewport width; with letter-spacing 0.4em, a 7-letter
   word renders ≈ 35vw wide, roughly 1/3 of the viewport. */
.skyline-label {
    position: absolute;
    font-family: var(--sans);
    font-size: clamp(40px, 5vw, 200px);
    font-weight: 700;
    letter-spacing: 0.4em;
    text-transform: uppercase;
    pointer-events: none;
    white-space: nowrap;
    z-index: 3;
}

/* DREAMER lives inside .skyline-color (which spans the stage via inset:0).
   JS rests its top low in the viewport (~75vh) so the drain line takes a
   while to climb up to it — that wait is the "delay" before DREAMER
   starts moving. Once the line catches its bottom edge, DREAMER rides
   the line up and out of the viewport (clip-path on .skyline-color hides
   it once it's above the visible region). The CSS `top` here is just a
   reasonable pre-JS starting position. */
.skyline-label-dreamer {
    top: calc(100vw * 4 / 3 * 0.32 + 75vh);
    left: 5vw;
    color: #ffffff;
    text-shadow: 0 1px 16px rgba(0, 0, 0, 0.45), 0 0 1px rgba(0, 0, 0, 0.6);
}

/* BUILDER lives in the sticky pin (not the stage). JS animates `top` as a
   function of drain progress: starts at 100vh (just below viewport bottom,
   off-screen) at drain=0, ends at 55vh (left-aligned, slightly below
   vertical center) at drain=1. Stays at 55vh through phase C, so it holds
   its left/below-center position as the cartoon scrolls past behind it.
   CSS top is the drain=0 starting position. */
.skyline-label-builder {
    top: 100vh;
    left: 5vw;
    color: #1a1714;
    text-shadow: 0 1px 12px rgba(255, 255, 255, 0.65);
    will-change: top;
}

/* ============================================================
   BUILDER markers — clickable map pins on the B&W cartoon
   ============================================================ */

/* Markers are siblings of .skyline-color inside .skyline-stage,
   position: absolute relative to the stage. Each marker has inline
   `top: <imageY*100>%` (= imageY × H, since stage height = H) and
   inline `left: <vw>` for its anchor point on the cartoon image. The
   stage's transform translates the markers along with the cartoon
   during phase B and C, so no per-frame JS position update is needed
   — markers ride the cartoon for free. JS only manages `.is-revealed`
   (drain crossed `data-image-y` AND section still in viewport) and
   `.is-expanded` (toggled on click). z-index 4 keeps markers above
   color (z-index 2) and cartoon (z-index 1). The pin's overflow:hidden
   clips markers to the viewport box — when the section is below
   viewport or scrolled past, the marker doesn't bleed onto adjacent
   sections. Initial state is hidden + non-clickable. */
.skyline-marker {
    position: absolute;
    z-index: 4;
    display: inline-flex;
    align-items: flex-start;
    gap: 0.4em;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease-out;
    user-select: none;
    -webkit-user-select: none;
}

/* Card body is descriptive paragraph copy users may want to copy —
   keep it selectable even though the parent .skyline-marker isn't. */
.skyline-marker-card-body {
    user-select: text;
    -webkit-user-select: text;
}

.skyline-marker.is-revealed {
    opacity: 1;
    pointer-events: auto;
}

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

/* Pin is a plain image. Top-aligned with the card so the card's first
   line sits next to the pin (right of it). cursor: pointer to signal
   it's clickable; JS attaches a click listener that toggles the same
   .is-expanded state as the toggle button. */
.skyline-marker-pin {
    width: clamp(28px, 2.6vw, 44px);
    height: auto;
    display: block;
    flex-shrink: 0;
    cursor: pointer;
    filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.35));
}

/* The card is the white rounded box. It contains a toggle button (the
   always-visible primary/secondary label), an X close button (only
   shown when expanded), and a description body (only shown when
   expanded).

   width: fit-content so the card hugs its content (collapsed = just
   the label width). max-width is intentionally tight (~12em collapsed,
   14em expanded) so the body paragraph wraps into multiple lines
   rather than stretching the card horizontally — we want growth to
   happen downward. The viewport-edge cap (calc with --marker-x) is the
   secondary safety so the card never touches the right edge for
   markers placed far right; --marker-x is set inline per marker to
   match its `left`.

   margin-top vertically centers the closed card with the pin. Pin is
   a square (clamp 28-44px); closed card is ~3.25em tall (padding + 2
   stacked label lines + 2px border) — taller than the pin — so we
   lift the card up by half the difference to align centers. Because
   margin-top is fixed (independent of card content height), the card
   top stays in the same screen position when the card expands; the
   pin position is unaffected. */
.skyline-marker-card {
    position: relative;
    background: #ffffff;
    border: 1px solid #1a1714;
    border-radius: 6px;
    padding: 0.45em 0.7em;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
    width: fit-content;
    max-width: min(12em, calc(100vw - var(--marker-x, 50vw) - 70px));
    margin-top: calc((clamp(28px, 2.6vw, 44px) - 3.25em) / 2);
    color: #1a1714;
    transition: box-shadow 160ms ease;
}

/* Hover lift on the closed label only — deepens the shadow so the card
   appears to pop forward. Disabled when expanded so the open card's
   reveal isn't visually competing with a hover state. The pin
   (.skyline-marker-pin) is a separate sibling, so it's unaffected. */
.skyline-marker:not(.is-expanded) .skyline-marker-card:hover {
    box-shadow: 0 8px 22px rgba(0, 0, 0, 0.35);
}

/* When expanded, give the card a slightly wider max-width so the X
   close button (~32px column at top-right) has room to sit beside the
   longest label line without overlapping text. 14em is just enough
   that "Wisconsin Historical" still fits on line 1 with X clearance —
   so the label wrap pattern matches the collapsed state. Short-label
   cards (Google, N+1) just get a slightly wider body wrap area. */
.skyline-marker.is-expanded .skyline-marker-card {
    max-width: min(14em, calc(100vw - var(--marker-x, 50vw) - 70px));
}

.skyline-marker-card-toggle {
    display: inline-flex;
    flex-direction: column;
    background: transparent;
    border: 0;
    padding: 0;
    margin: 0;
    cursor: pointer;
    color: inherit;
    font: inherit;
    text-align: left;
    line-height: 1.15;
}

.skyline-marker-card-toggle:focus-visible {
    outline: 2px solid #1f2d4a;
    outline-offset: 3px;
    border-radius: 2px;
}

.skyline-marker-label-primary {
    font-family: var(--sans);
    font-weight: 700;
    font-size: clamp(14px, 1.3vw, 22px);
    letter-spacing: 0.06em;
    /* text-transform: uppercase; */
}

.skyline-marker-label-secondary {
    font-family: var(--sans);
    font-weight: 400;
    font-size: clamp(11px, 1vw, 16px);
    letter-spacing: 0.04em;
    color: rgba(26, 23, 20, 0.78);
    margin-top: 2px;
}

.skyline-marker-card-body {
    margin: 0.6em 0 0;
    font-family: var(--sans);
    font-size: clamp(13px, 1.05vw, 16px);
    line-height: 1.5;
    color: #1a1714;
}

.skyline-marker-card-body[hidden] {
    display: none;
}

.skyline-marker-card-close {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 28px;
    height: 28px;
    border: 0;
    background: transparent;
    color: #1a1714;
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
    border-radius: 4px;
    padding: 0;
}

.skyline-marker-card-close[hidden] {
    display: none;
}

.skyline-marker-card-close:hover {
    background: rgba(0, 0, 0, 0.08);
}

.skyline-marker-card-close:focus-visible {
    outline: 2px solid #1f2d4a;
    outline-offset: 1px;
}

/* ============================================================
   Polaroid — peek / open / flip
   ============================================================ */

/* Outer wrapper sits as an absolute child of .skyline-color (sibling
   of DREAMER), so the same drain clip-path that erases the color
   image during phase B also erases the polaroid — works for both
   peek and open states automatically.
   `top` is JS-driven each frame in stage/color coords:
     polaroid.top = V - 0.322 * polaroidH + lockOffset
   so the rotated visible peek's bottom-LEFT lands at the viewport
   bottom on screen during phase B (CW 10° around top-center →
   visible peek bottom-LEFT y' ≈ 0.322 * polaroidH below the
   un-rotated top). The rest extends below and is cropped by the
   pin's overflow:hidden. z-index 5 stacks above DREAMER (3). */
.polaroid {
    position: absolute;
    /* `top` is set by JS each frame; CSS provides only a sane fallback */
    top: 60vh;
    right: 14vw;
    width: clamp(260px, 26vw, 420px);
    padding: 0;
    margin: 0;
    border: 0;
    background: transparent;
    cursor: pointer;
    /* CW tilt: viewer's-hand feel. transform-origin at bottom-LEFT
       (the off-screen corner of the card during peek) so peek-state
       rotation AND hover-state straightening both pivot around that
       hidden anchor — the card swings up around the corner, like
       it's pinched there. The JS positioning formula uses the
       0.409*polaroidH factor derived for this pivot (visible peek
       bottom-LEFT y ≈ 0.552W ≈ 0.409*polaroidH). */
    transform: rotate(10deg);
    transform-origin: 0% 100%;
    /* No clip-path on the polaroid: the part of the card below the
       viewport during peek is cropped by .skyline-pin's overflow:hidden
       at the viewport bottom — a screen-space cut, so the rotation
       doesn't expose an internal "half polaroid" cut line. Drain
       consumption is handled by .skyline-color's own clip-path
       (we're a descendant). */
    perspective: 1200px;
    /* Pin the 3D vanishing point to the card's center so the inner
       flip rotates symmetrically — independent of .polaroid's own 2D
       transform-origin. */
    perspective-origin: 50% 50%;
    z-index: 5;
    transition: transform 0.8s var(--ease),
        filter 0.25s ease-out;
    filter: drop-shadow(0 8px 14px rgba(0, 0, 0, 0.28));
    will-change: transform;
}

.polaroid:hover,
.polaroid:focus-visible {
    filter: drop-shadow(0 14px 22px rgba(0, 0, 0, 0.36));
}

.polaroid:focus-visible {
    outline: none;
    outline-offset: 0;
}

/* Peek hover: straightens the card and lifts it. Pivot is bottom-LEFT
   (set on .polaroid base), so the card swings up CCW around its
   hidden bottom-LEFT corner as the angle drops toward 0 — the whole
   top of the card rises into view, like it's hinged at the off-screen
   corner. The small upward translate adds an overall lift on top of
   the swing. Telegraphs "click me to bring me out". The 0.8s transform
   transition on .polaroid carries the motion. */
.polaroid:not(.is-open):hover,
.polaroid:not(.is-open):focus-visible {
    transform: translateY(-1vh) rotate(4deg);
}

/* Open: JS computes the inline transform on click so the polaroid
   lands centered in the viewport (vertically) and at ~75vw (the
   middle of the right half) regardless of drain progress. The
   transform value below is only a fallback — JS overrides it on
   open(). No clip-path needed (overflow:visible by default), so the
   3D flip's perspective parallelogram renders without being cut. */
.polaroid.is-open {
    transform: translate3d(-8vw, 0, 0) rotate(0deg);
    cursor: pointer;
}

.polaroid.is-open:hover,
.polaroid.is-open:focus-visible {
    filter: drop-shadow(0 8px 14px rgba(0, 0, 0, 0.28));
}

/* preserve-3d lives ONLY on this inner element. Combining it with the
   outer 2D transform on the same element would flatten the 3D scene. */
.polaroid-card {
    position: relative;
    width: 100%;
    aspect-ratio: 100 / 135;
    transform-style: preserve-3d;
    transform: rotateY(0deg);
    /* Pivot the 3D flip around the card's center, not its top-left.
       (Default is 50% 50% but the parent's transform-origin: 50% 0%
       can confuse some renderers — set it explicitly.) */
    transform-origin: 50% 50%;
    transition: transform 0.8s var(--ease);
}

.polaroid.is-flipped .polaroid-card {
    transform: rotateY(180deg);
}

/* Hover affordance: when open, hovering nudges the card a few degrees
   on the same Y-axis the flip uses — a small wobble that telegraphs
   flippability. The is-flipped variant mirrors the tilt around 180° so
   the back face wobbles by the same visual amount. */
.polaroid.is-open:hover .polaroid-card,
.polaroid.is-open:focus-visible .polaroid-card {
    transform: rotateY(8deg);
}

.polaroid.is-open.is-flipped:hover .polaroid-card,
.polaroid.is-open.is-flipped:focus-visible .polaroid-card {
    transform: rotateY(172deg);
}

/* Suppress the hover-tilt while the cursor is on the × button.
   (Belt-and-suspenders — the button is now outside .polaroid-card so
   the tilt no longer reaches it, but this also keeps the card itself
   stable while the cursor is on the close affordance.) */
.polaroid.is-open:has(.polaroid-close:hover) .polaroid-card {
    transform: rotateY(0deg);
}

.polaroid.is-open.is-flipped:has(.polaroid-close:hover) .polaroid-card {
    transform: rotateY(180deg);
}

.polaroid-face {
    position: absolute;
    inset: 0;
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
    background: #f7f4ec;
    border-radius: 2px;
    padding: 6% 6% 32% 6%;
    box-sizing: border-box;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18) inset;
    outline: 1px solid rgba(0, 0, 0, 0.06);
    display: flex;
    flex-direction: column;
}

.polaroid-face-back {
    transform: rotateY(180deg);
    padding: 8% 8% 28% 8%;
}

.polaroid-photo {
    flex: 1;
    width: 100%;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06);
}

/* Tag: italic location/year, pinned right under the photo and right-
   aligned to the photo's right edge (padding-right matches the face's
   6% horizontal padding so the closing ")" sits flush under the photo).
   `top: 76.3%` lines the tag-box top up exactly with the photo's bottom
   edge: photo_bottom = face_height − bottom_padding (32% of face
   width) → in face-height units, 1 − 32/135 ≈ 76.3%. */
.polaroid-tag {
    position: absolute;
    left: 0;
    right: 0;
    top: 76.3%;
    padding: 0 6%;
    font-family: var(--sans);
    font-style: italic;
    font-size: clamp(9px, 0.85vw, 12px);
    letter-spacing: 0.02em;
    color: rgba(26, 23, 20, 0.78);
    text-align: right;
    pointer-events: none;
}

/* Caption: project name, centered both axes in the white strip area
   between the photo bottom (76.3%) and the buttons (which sit at
   `bottom: 4%` with height 1.5em). */
.polaroid-caption {
    position: absolute;
    left: 0;
    right: 0;
    top: 76.3%;
    bottom: calc(4% + 1.5em);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 6%;
    font-family: "Architects Daughter", cursive;
    font-size: clamp(11px, 1.05vw, 15px);
    letter-spacing: 0.02em;
    color: rgba(26, 23, 20, 0.88);
    text-align: center;
    pointer-events: none;
}

.polaroid-back-body {
    flex: 1;
    overflow: auto;
    font-family: "Architects Daughter", cursive;
    font-size: clamp(14px, 1.4vw, 20px);
    line-height: 1.5;
    color: rgba(26, 23, 20, 0.88);
    padding-bottom: 1.6em;
}

.polaroid-back-body p {
    margin: 0 0 0.7em;
}

/* Close affordance: single × centered horizontally on the polaroid.
   Lives outside .polaroid-card (sibling, not descendant) so the hover-
   tilt rotateY on the card doesn't drag the button out from under the
   cursor. Hidden during peek because the button sits below the viewport
   bottom (positioned at bottom: 4% of the .polaroid box, which is mostly
   off-screen during peek) — .skyline-pin's overflow:hidden crops it.
   Visible when open because the polaroid is translated fully into the
   viewport. */
.polaroid-close {
    position: absolute;
    bottom: 4%;
    left: 50%;
    transform: translateX(-50%);
    width: 1.5em;
    height: 1.5em;
    border-radius: 50%;
    border: 1px solid rgba(26, 23, 20, 0.25);
    background: rgba(255, 255, 255, 0.7);
    color: var(--ink);
    font-size: 1em;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 0;
    transition: background 0.18s ease-out,
        border-color 0.18s ease-out,
        transform 0.18s ease-out;
}

.polaroid-close:hover {
    background: #ffffff;
    border-color: rgba(26, 23, 20, 0.55);
    transform: translateX(-50%) scale(1.06);
}

.polaroid-close:focus-visible {
    outline: 2px solid #1f2d4a;
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {

    .polaroid,
    .polaroid-card,
    .polaroid-close {
        transition: none;
    }
}

/* ============================================================
   Graduation — static graduation frame
   ============================================================ */

/* Section is exactly 100vh. JS appends frame 0 from the manifest as a
   `.grad-frame.grad-frame-active` <img>; no other frames are inserted
   and there's no scroll lock. The skyline cartoon sits fixed at
   z-index:0 past its phase C, so this section needs z-index:1 +
   background color to paint above it. */
.grad-section {
    position: relative;
    z-index: 1;
    width: 100vw;
    height: 100vh;
    background: white;
    overflow: hidden;
    /* Sibling sections (.container, .exp-skyline) are float:left; mirror
       that to keep float layout intact (per CLAUDE.md scroll architecture). */
    float: left;
}

/* All frames are rendered fully opaque and stacked at the same
   absolute position. Animation switches which frame is visible by
   raising its z-index — there is always an opaque image on top, so
   the section's black background can never leak between frames the
   way it could during an opacity 0→1 transition (compositor layer
   not ready, decode pending, etc.). */
.grad-frame {
    position: absolute;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    object-fit: cover;
    object-position: center;
    pointer-events: none;
    z-index: 1;
}

.grad-frame.grad-frame-active {
    z-index: 2;
}

/* WI capitol — affixed to the bottom-right of the SCHOLAR section,
   anchored at bottom and right with a top breathing margin. Sits
   above the graduation frame stack (z-index 3) so it persists across
   frame switches without being clipped. The image is a cut-out
   silhouette (rembg / U2Net) cropped to its alpha bounding box, so
   `height` controls the silhouette directly with no transparent
   padding. Reduced opacity lets the frames behind read through it. */
.wi-capitol {
    position: absolute;
    right: 0;
    bottom: 0;
    height: 88vh;
    width: auto;
    z-index: 3;
    opacity: 0.25;
    pointer-events: none;
}

/* LIFELONG LEARNER — left-aligned, vertically centered. translateY(-50%)
   pairs with top: 50vh to place the label's visual midline at viewport
   center, independent of font-size (which scales with viewport via the
   clamp on .skyline-label). Color/shadow match BUILDER (dark ink with a
   soft white halo for legibility over the varied graduation photo).
   z-index: 3 from .skyline-label keeps it above the .grad-frame imgs
   that JS appends below it. */
.grad-label {
    top: 50vh;
    left: 5vw;
    transform: translateY(-50%);
    color: #1a1714;
    text-shadow: 0 1px 12px rgba(255, 255, 255, 0.65);
}

/* ============================================================
   Gallery page (gallery.html)
   ============================================================ */

/* Override the global body rules from styles.css (height: 100vh,
   overflow-y: scroll, scroll-snap-type: y mandatory, display: inline-block)
   so this page scrolls naturally. The sandbox already has body
   { scroll-snap-type: none }; we restate it here for clarity since the
   gallery is a different page entirely. */
body.gallery-page {
    height: 100vh;
    overflow-y: auto;
    scroll-snap-type: none;
    display: block;
    background-color: darkslategray;
    color: white;
}

.gallery-header {
    background-color: black;
    padding: 2vh 5vw;
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 2vh;
}

.gallery-logo img {
    width: 8vw;
    min-width: 80px;
    height: auto;
    display: block;
}

.gallery-header nav {
    display: flex;
    gap: 3vw;
    flex-wrap: wrap;
    align-items: center;
    user-select: none;
    -webkit-user-select: none;
}

/* Landing-page nav (sandbox) — same no-text-select treatment as gallery.
   #Intro's "About Me" headline is also a label-like element, not body
   copy, so it shouldn't be highlightable either. */
.nav-bar,
#Intro > h1 {
    user-select: none;
    -webkit-user-select: none;
}

.gallery-header nav a {
    color: white;
    text-decoration: none;
    letter-spacing: 0.1em;
    font-size: 1em;
}

.gallery-header nav a:hover,
.gallery-header nav a[aria-current="page"] {
    color: burlywood;
}

/* CONTACT dropdown in the gallery header. The .nav-contact-* base rules
   live further down (shared with the index2 vertical nav); these scope
   the toggle's typography to the gallery row and put a black panel
   behind the menu so EMAIL/LINKEDIN read on the darkslategray body
   below the header. */
.gallery-header .nav-contact-toggle {
    color: white;
    cursor: pointer;
    letter-spacing: 0.1em;
    font-size: 1em;
}

.gallery-header .nav-contact-toggle:hover {
    color: burlywood;
}

.gallery-header .nav-contact-menu {
    top: calc(100% + 1.5vh);
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    background-color: black;
    padding: 1.5vh 1.5vw;
    min-width: max-content;
    gap: 1.2vh;
    line-height: 1;
    border: 1px solid burlywood;
    border-top: none;
}

.gallery-header .nav-contact-item {
    letter-spacing: 0.1em;
    font-size: clamp(11px, 1.4vh, 14px);
}

.gallery-page main {
    padding: 5vh 5vw 8vh;
}

.gallery-page main h1 {
    font-family: "Retro Signature";
    font-size: clamp(48px, 15vh, 200px);
    text-align: center;
    color: lightslategray;
    margin: 0 0 5vh 0;
}

.gallery-page .gallery {
    width: 100%;
    padding: 0;
}

.gallery-footer {
    background-color: black;
    padding: 5vh 0;
    text-align: center;
}

.gallery-footer a {
    margin: 0 1vw;
}

/* Lightbox overlay. Single fixed-position dialog reused for every slide;
   JS swaps src/caption/counter on open and disables prev/next at ends. */
body.gallery-page.lightbox-open {
    overflow: hidden;
}

.lightbox {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.85);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 100;
}

.lightbox[hidden] {
    display: none;
}

.lightbox-figure {
    margin: 0;
    max-width: 90vw;
    max-height: 90vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2vh;
}

.lightbox-img {
    max-width: 90vw;
    max-height: 80vh;
    width: auto;
    height: auto;
    object-fit: contain;
    box-shadow: 0 4px 30px rgba(0, 0, 0, 0.6);
}

.lightbox-caption {
    color: white;
    font-family: "Louis George";
    text-align: center;
    display: flex;
    flex-direction: column;
    gap: 0.5em;
    font-size: clamp(13px, 1.8vh, 18px);
}

.lightbox-counter {
    color: lightslategray;
    font-size: clamp(11px, 1.4vh, 14px);
    letter-spacing: 0.15em;
}

.lightbox-close,
.lightbox-prev,
.lightbox-next {
    position: absolute;
    background: transparent;
    border: 0;
    color: white;
    cursor: pointer;
    font-family: inherit;
    line-height: 1;
    padding: 1vh 1.5vw;
    transition: color 120ms ease;
}

.lightbox-close:focus,
.lightbox-prev:focus,
.lightbox-next:focus {
    outline: none;
}

.lightbox-close {
    top: 2vh;
    right: 2vw;
    font-size: clamp(28px, 4vh, 44px);
}

.lightbox-prev,
.lightbox-next {
    top: 50%;
    transform: translateY(-50%);
    font-size: clamp(40px, 7vh, 80px);
}

.lightbox-prev {
    left: 2vw;
}

.lightbox-next {
    right: 2vw;
}

.lightbox-close:hover,
.lightbox-prev:not(:disabled):hover,
.lightbox-next:not(:disabled):hover {
    color: burlywood;
}

.lightbox-prev:disabled,
.lightbox-next:disabled {
    visibility: hidden;
}

/* The existing top-nav rules in styles.css target `.nav span`. The GALLERY
   nav item in index.html is now an `<a>`, so mirror the color treatment
   for anchors too. */
.nav a.nav-text {
    color: white;
    text-decoration: none;
}

.nav a.nav-text:hover {
    color: burlywood;
    cursor: pointer;
}

/* CONTACT dropdown — clicking the toggle adds .is-open on the parent
   <li>, which underlines+golds the CONTACT label and reveals the
   EMAIL/LINKEDIN children stacked below.
   The menu is position:absolute so its appearance/disappearance never
   reflows the rest of the nav column (.nav-bar uses justify-content:
   center, so any in-flow growth would shift sibling items upward). */
.nav-contact {
    position: relative;
}

.nav-contact.is-open .nav-contact-toggle {
    color: burlywood;
    text-decoration: underline;
    text-underline-offset: 0.35em;
}

.nav-contact-menu {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    display: none;
    flex-direction: column;
    align-items: center;
    line-height: 2;
}

.nav-contact.is-open .nav-contact-menu {
    display: flex;
}

.nav-contact-item,
.nav-contact-item:link,
.nav-contact-item:visited {
    color: white;
    text-decoration: none;
    font-size: 0.75em;
    cursor: pointer;
}

.nav-contact-item:hover {
    color: burlywood;
}

/* About Me profile photo — hover dims the portrait and reveals two
   social links (LinkedIn left, GitHub right) overlaid on top. The
   wrapper is the hover target; .photos has pointer-events: none in
   styles.css so events fall through to the wrapper, and the absolute-
   positioned anchors sit above with their own pointer-events. */
.profile-photo {
    position: relative;
    height: 30vh;
    width: 30vh;
    border-radius: 50%;
    overflow: hidden;
    display: flex;
    justify-content: center;
    margin: 0 auto;
}

.profile-photo-img {
    height: 100%;
    width: auto;
    transition: filter 0.25s ease-out;
}

/* Darken (rather than fade) on hover — fading to a lower opacity
   blends with the section's white background and reads as "lighter".
   brightness() multiplies pixel values, pulling the image toward
   black so the social icons read clearly against it. */
.profile-photo:hover .profile-photo-img {
    filter: brightness(0.4);
}

/* Both icon anchors share an identical square box (the source PNGs
   have different intrinsic alpha framing, so a percentage `width`
   alone gave different rendered sizes). Locking width AND height,
   plus object-fit: contain on the inner img, guarantees identical
   visual size across both icons. */
.profile-photo-link {
    position: absolute;
    top: 50%;
    width: 6vh;
    height: 6vh;
    transform: translateY(-50%);
    opacity: 0;
    transition: opacity 0.25s ease-out;
    z-index: 2;
}

.profile-photo-link img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
}

.profile-photo-linkedin {
    left: 18%;
}

.profile-photo-github {
    right: 18%;
}

.profile-photo:hover .profile-photo-link {
    opacity: 1;
}

/* Thin white divider centered between the two icons. Vertical span
   stops short of the circle edge so it reads as a separator, not a
   diameter. Fades in with the icons on hover. */
.profile-photo::after {
    content: "";
    position: absolute;
    top: 22%;
    bottom: 22%;
    left: 50%;
    width: 1.5px;
    background: white;
    transform: translateX(-50%);
    opacity: 0;
    transition: opacity 0.25s ease-out;
    z-index: 2;
    pointer-events: none;
}

.profile-photo:hover::after {
    opacity: 1;
}