/*
  FILE: portfolio.css
  PURPOSE: Landing page V3 (see index.html's own doc comment for the
  full picture). Everything from "Outer two-column layout" onward
  below is an unmodified copy of work.css's own content, kept
  duplicated rather than shared so this experiment can't affect the
  real Work page. Only the new hero section's styles at the top of
  this file are actually new.
*/

body {
  margin: 0;
  font-family: var(--font-body);
  background: var(--color-bg-content); /* per Lucrecia's request, matches the max-width box's own colour, was --color-bg-default */
  color: var(--color-text-dark);
}

/* ================= NEW HERO SECTION ================= */

/* Full viewport, both dimensions -- height:100vh with a 100dvh
   override for mobile browsers (accounts for the address bar showing/
   hiding, same pattern used for .csa-gate__stage on the CSA Website
   project page). position:relative so the background image and the
   overlay card can both be positioned against it. */
.landing-v3-hero {
  position: relative;
  width: 100%;
  height: 100vh;
  height: 100dvh;
  overflow: hidden; /* the breathing animation zooms the image slightly past 100%, this clips that excess at the hero's own edges instead of letting it push the page wider/taller */
  display: flex;
  align-items: center;
  justify-content: center;
}

/* object-fit:cover is what satisfies "show as much of the image as
   possible while always covering the entire background": it scales
   the image (preserving its aspect ratio) to the SMALLEST size that
   still fills the box completely, cropping only whichever dimension
   has to give, and object-position:center (the default) keeps that
   crop centred either way the window resizes. */
.landing-v3-hero__bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  animation: landing-v3-breathe 12s ease-in-out infinite;
}

/* Slow scale pulse ("the camera breathes"). Scaling UP from 1 (never
   down) means it only ever zooms in slightly further than the cover
   fit already guarantees, so there's never a gap at the edges to
   worry about. */
@keyframes landing-v3-breathe {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.06); }
}

@media (prefers-reduced-motion: reduce) {
  .landing-v3-hero__bg {
    animation: none;
  }
}

/* Corner frame: an L-shaped bracket in the bottom-left and top-right
   corners, per Lucrecia's reference designs -- each is just 2 plain
   rectangles (one running along each edge) that overlap at the corner
   itself, rather than one shape, since a straight rectangle is much
   simpler than a clipped/masked L outline for the exact same result.
   Uses the card's own background colour so the two decorative
   elements read as part of the same visual language. Sits on top of
   the (absolutely positioned, but earlier in DOM order) background
   image and under the card, same default stacking as everything else
   here -- no z-index needed. */
.landing-v3-hero__corner {
  position: absolute;
  background: var(--color-bg-nav);
  pointer-events: none;
}

/* bl = bottom-left, tr = top-right; -v = the arm running along the
   vertical (left/right) edge, -h = the arm running along the
   horizontal (top/bottom) edge. 15px thick, per Lucrecia's spec --
   230px up the left/right edge, 350px along the bottom/top edge. */
.landing-v3-hero__corner--bl-v {
  left: 0;
  bottom: 0;
  width: 15px;
  height: 230px;
}

.landing-v3-hero__corner--bl-h {
  left: 0;
  bottom: 0;
  width: 350px;
  height: 15px;
}

.landing-v3-hero__corner--tr-v {
  right: 0;
  top: 0;
  width: 15px;
  height: 230px;
}

.landing-v3-hero__corner--tr-h {
  right: 0;
  top: 0;
  width: 350px;
  height: 15px;
}

@media (max-width: 768px) {
  .landing-v3-hero__corner--bl-v,
  .landing-v3-hero__corner--tr-v {
    width: 8px;
    height: 120px;
  }

  .landing-v3-hero__corner--bl-h,
  .landing-v3-hero__corner--tr-h {
    width: 190px;
    height: 8px;
  }
}

/* 1px divider right at the bottom edge of the header, per Lucrecia's
   request -- same colour as the site's other solid dividers
   (.divider--solid, used between Work sections further down this
   page) for consistency. */
.landing-v3-hero__divider {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--color-text-dark);
}

/* Caption: bottom-left, 40px from both edges, per the brief. White
   text + a soft drop shadow (not a background chip) so it stays
   legible over any part of a photo without covering more of it than
   necessary. Links straight to that image's own project page (see
   portfolio.js), text set to match that project's own name/title on
   the Work page exactly. */
.landing-v3-hero__caption {
  position: absolute;
  left: 40px;
  bottom: 40px;
  font-family: var(--font-body);
  font-size: var(--font-size-body);
  font-weight: var(--weight-bold);
  color: #FFFFFF;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
  text-decoration: none;
}

.landing-v3-hero__caption:hover,
.landing-v3-hero__caption:focus-visible {
  text-decoration: underline;
}

/* Mobile: centred at the bottom instead of bottom-left, per Lucrecia's
   follow-up. Keeps the same 40px clearance from the bottom edge. */
@media (max-width: 768px) {
  .landing-v3-hero__caption {
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    text-align: center;
  }
}

/* Overlay card: solid (not translucent) so it reads clearly over
   whatever part of the photo sits behind it, same border + drop
   shadow as project thumbnails elsewhere on the site. z-index isn't
   needed by default since it comes after the (absolutely positioned)
   image in DOM order and .landing-v3-hero centres it via flex -- once
   pinned (see below) it needs an explicit z-index instead, since
   position:fixed takes it out of that flow. 5px corner radius
   applies throughout, at every width and both states (including once
   morphed into the nav -- .is-nav below doesn't override it, so this
   one rule already covers that too). Mobile is
   smaller still (min() of a 280px target and 100%-40px so very
   narrow phones still keep 20px of clearance each side) -- Lucrecia
   wants more of the background photo visible on mobile, may follow up
   with mobile-specific crops of the images later. */
.landing-v3-hero__card {
  position: relative;
  box-sizing: border-box;
  width: min(280px, calc(100% - 40px));
  background: var(--color-bg-nav); /* per Lucrecia's request, was --color-bg-default; covers the nav-morphed state too, .is-nav doesn't override it */
  border-radius: 5px;
  box-shadow: var(--shadow-image);
  padding: 30px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 30px;
  text-align: center;
}

@media (min-width: 769px) {
  .landing-v3-hero__card {
    width: 320px;
  }
}

/* ---- Scroll-driven morph into the nav (runs at every viewport
   width, see portfolio.js's morph section for how the end position/
   size differs between desktop and mobile). Two states layered on top
   of the rules above:

   .is-pinned: added by JS the moment the user scrolls at all (even
   1px). Switches the card to position:fixed so JS can drive its
   top/left/width/height directly via inline styles as scroll
   progress goes from 0 to 1 -- every value below is later overridden
   inline per-frame, these are just sane fallbacks/starting points.
   Removed again the instant the user scrolls back to the very top,
   which drops the card back into .landing-v3-hero's normal flex-
   centred flow -- this is what makes "scroll back up reverts to the
   original centred floating component" work for free, no reverse
   animation logic needed, it's just the CSS the card had all along.

   .is-nav: added once scroll progress crosses 0.6 (see the JS), i.e.
   only once the card is already most of the way through shrinking
   into a bar. Switches the internal layout from the stacked column
   (name/intro/actions/nav-links) to a single row with the name on the
   left and the nav links on the right -- flex-direction can't be
   smoothly interpolated, so this is a hard swap timed to land after
   most of the visual resize has already happened, to minimise how
   jarring it looks. Also collapses intro/actions out of the layout
   entirely (display:none, not just invisible) so they don't take up
   row space or stay keyboard-focusable once they're not meant to be
   there any more. ---- */
.landing-v3-hero__card.is-pinned {
  position: fixed;
  z-index: 2;
  margin: 0;
  /* Without this, the intro/actions blocks (already opacity:0 by
     --p:0.4, but still display:block/flex and taking up their full
     flex-column space until .is-nav removes them at --p:0.6) would
     force the box to grow tall enough to fit them regardless of the
     shorter height set inline below, leaving a visibly empty gap
     inside the shrinking card. Clipping is harmless here since
     whatever gets cut off is already invisible by the time the
     interpolated height is short enough for this to matter. */
  overflow: hidden;
  /* No transition here (there used to be one): portfolio.js now
     re-reads scroll position and re-applies top/left/width/height on
     every rendered frame, not just on 'scroll' events (which don't
     fire at a fixed rate, and fire noticeably less often in Safari
     than Chrome) -- with every frame already carrying the correct
     value, a transition just meant each frame was chasing the last
     one instead of showing it immediately, which read as lag/drift
     rather than smoothness. See the "Continuous per-frame update"
     comment in portfolio.js for the full reasoning. */
}

.landing-v3-hero__card.is-nav {
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  text-align: left;
  gap: 0;
  padding: 20px 30px;
}

.landing-v3-hero__card.is-nav .landing-v3-hero__intro,
.landing-v3-hero__card.is-nav .landing-v3-hero__actions {
  display: none;
}


/* font-size interpolates continuously with scroll (via --p, the
   morph's 0-1 progress, set inline on the card each scroll frame --
   see portfolio.js). Falls back to the full 50px size when --p isn't
   set at all (mobile, where the morph script doesn't run), since
   calc() with a missing custom property still needs a valid fallback. */
/* Font styles only here -- inherited down by both the full-name spans
   and the initials span below, whichever one is actually showing. */
.landing-v3-hero__name {
  margin: 0;
  /* Melodrama per Lucrecia's request, falls back to the site's own
     heading font if it somehow fails to load. Only the -Variable file
     was uploaded (see assets/fonts/melodrama.css), which covers this
     element's own --weight-semibold (600) within its declared 300-700
     range, no separate static weight needed. */
  font-family: 'Melodrama-Variable', var(--font-heading);
  font-size: calc(var(--font-size-landing-title) - var(--p, 0) * 30px);
  font-weight: var(--weight-semibold);
  line-height: 1.1;
  color: var(--color-text-dark);
  white-space: nowrap;
}

.landing-v3-hero__name-full {
  display: flex;
  flex-direction: column;
}

/* Only shown on mobile once the card is in its nav state, see the
   max-width:768px block near the bottom of this file. */
.landing-v3-hero__name-initials {
  display: none;
}

/* Single line instead of stacked, once the card's switched into its
   row-based nav layout (see .is-nav above). Mobile shows the initials
   span instead of this one at that point (see below), so this has no
   visible effect there, kept unconditional just for consistency. */
.landing-v3-hero__card.is-nav .landing-v3-hero__name-full {
  flex-direction: row;
  gap: 6px;
}

/* Hero state only -- a plain flex child sitting between the name and
   the intro paragraph, so it lands "right below the name" for free
   via the card's own gap:30px, and align-self:stretch spans the full
   content width (the card's own align-items:center would otherwise
   leave it at its intrinsic 0 width like any other empty div),
   automatically respecting the card's 30px padding since stretch only
   ever fills the content box, never the padding itself.

   Once the user starts scrolling, portfolio.js takes over completely
   (position:fixed + inline top/left/width/height, set directly on
   this element, no CSS class needed) to run the two-phase morph into
   a vertical line beside the name -- see the "line morph" section in
   that file. Nothing here needs a nav-state override as a result. */
.landing-v3-hero__line {
  align-self: stretch;
  height: 1px;
  background: var(--color-accent);
  flex-shrink: 0;
  /* No transition here (there used to be one) -- portfolio.js sets
     this element's top/left/width/height fresh on every rendered
     frame once pinned, same reasoning as .is-pinned's own comment in
     this file. Harmless either way in the static/unpinned state
     above, nothing on this rule ever changes while unpinned. */
}

/* Fades out linearly over the first 40% of the scroll morph (opacity
   0 by --p:0.4), well before the layout swap at 0.6 -- by the time
   .is-nav removes these from the flex row entirely they're already
   fully invisible, so that removal doesn't visibly "jump". */
.landing-v3-hero__intro {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--font-size-project-body);
  font-weight: var(--weight-regular);
  line-height: var(--line-height-project-body);
  color: var(--color-text-dark);
  opacity: clamp(0, calc(1 - var(--p, 0) * 2.5), 1);
}

/* Bug fix: the instant the card is pinned, .landing-v3-hero__line
   switches to position:fixed (driven by portfolio.js) so it can
   morph freely -- but that also removes it from the card's flex
   column entirely, so this paragraph (the next flow child after it)
   collapsed upward to fill the vacated space, a visible jump right as
   the transition started. 31px puts that space back: the card's own
   30px flex gap plus the line's own 1px height, i.e. exactly what's
   missing once the line is no longer a flex sibling. */
.landing-v3-hero__card.is-pinned .landing-v3-hero__intro {
  margin-top: 31px;
}

/* The 10px gap between the CTA and the arrow is smaller than the
   30px gap the card uses everywhere else (per the brief), so this
   group needs its own nested gap rather than relying on the card's.
   Same fade-out as .landing-v3-hero__intro above, same reasoning. */
.landing-v3-hero__actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  opacity: clamp(0, calc(1 - var(--p, 0) * 2.5), 1);
}

/* No border, per Lucrecia's request (was a bordered-box treatment
   matching the real landing page's own .landing-cta) -- the hover/
   focus background fill below is still the same. */
.landing-v3-hero__cta {
  font-family: var(--font-body);
  font-size: var(--font-size-body);
  font-weight: var(--weight-medium);
  color: var(--color-text-dark);
  background: transparent;
  border: none;
  padding: 10px 20px;
  text-decoration: none;
  cursor: pointer;
}

.landing-v3-hero__cta:hover,
.landing-v3-hero__cta:focus-visible {
  background: var(--color-bg-accent);
}

.landing-v3-hero__arrow {
  display: flex;
  color: var(--color-text-dark);
  animation: landing-v3-arrow-bounce 1.6s ease-in-out infinite;
}

@keyframes landing-v3-arrow-bounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(8px); }
}

@media (prefers-reduced-motion: reduce) {
  .landing-v3-hero__arrow {
    animation: none;
  }
}

/* Nav links (Work / About me): hidden entirely (display:none, not
   just invisible) until the card switches into its row layout at
   --p:0.6, at which point they appear (display:flex) and fade in
   linearly over the remaining scroll (0.6 to 1.0) -- timed together so
   there's no gap where they'd be invisible but still focusable. 30px
   gap between the two links, matches the reference. */
.landing-v3-hero__nav-links {
  display: none;
}

.landing-v3-hero__card.is-nav .landing-v3-hero__nav-links {
  display: flex;
  align-items: center;
  gap: 30px;
  opacity: clamp(0, calc((var(--p, 0) - 0.6) * 2.5), 1);
}

.landing-v3-hero__nav-link {
  font-family: var(--font-body);
  font-size: var(--font-size-small);
  font-weight: var(--weight-regular);
  color: var(--color-text-dark);
  text-decoration: none;
}

.landing-v3-hero__nav-link:hover,
.landing-v3-hero__nav-link:focus-visible {
  text-decoration: underline;
}

/* Current-page nav link (Work, since this page IS the work content):
   bold + the accent colour, not colour alone, so the current page is
   still distinguishable without relying on colour perception. */
.landing-v3-hero__nav-link--active {
  font-weight: var(--weight-bold);
  color: var(--color-accent);
}

@media (max-width: 768px) {
  /* Card width itself needs no override here any more -- the base
     rule (width: calc(100% - 40px); max-width: 400px;) already is the
     mobile value, now that the fixed 320px lives in its own
     min-width:769px override above instead. */
  .landing-v3-hero__name {
    font-size: 32px; /* scaled down from the 50px desktop size, matches the ratio used for the real landing page's own name at this breakpoint */
  }

  /* Condensed to initials once the mobile nav is stuck, per Lucrecia's
     request -- not enough room next to the nav links at mobile
     widths otherwise. Swapping which span is display:none rather than
     animating anything, same hard-swap approach as .is-nav itself. */
  .landing-v3-hero__card.is-nav .landing-v3-hero__name-full {
    display: none;
  }

  .landing-v3-hero__card.is-nav .landing-v3-hero__name-initials {
    display: inline;
    /* Shrunk from the fixed 32px above -- at that size "LC" doesn't
       fit inside the nav bar's height (69px, minus this state's own
       20px top/bottom padding) once stuck. 20px matches the size
       desktop's own name settles at once fully morphed into its nav. */
    font-size: 20px;
  }
}

/* Anchor target for the CTA/arrow links + smooth scroll, so pressing
   either one glides down into the work content instead of jumping
   instantly. Scoped to this page only (not a global html rule) so it
   doesn't affect anywhere else the browser might jump to on this
   page. */
html {
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

/* ================= WORK PAGE CONTENT (originally duplicated from
   work.css, now diverged -- sidebar nav removed entirely per
   Lucrecia's request, content widened to fill the full 1200px column
   on its own, and every spot that used to follow "the 30px rule"
   (side padding and the general inner-spacing rhythm) is now 60px
   instead, at every width. Bright Little Labs' 4 thumbnails switched
   from 4:3 to 16:10 crops (HTML + this file's aspect-ratio both
   updated), and .project-row__text--default is now a fixed 450px
   instead of a 266px cap, since there's much more room now that
   there's no sidebar to share it with. ================= */

/* ---- Outer layout: no more nav column, .work-content is the only
   child and simply fills the bordered box. ---- */

.work-layout {
  max-width: 1200px;
  margin: 0 auto;
  box-sizing: border-box; /* keeps the outline below flush with the 1200px edge, not 2px wider */
  background: var(--color-bg-content); /* per Lucrecia's request, every breakpoint */
  border: 1px solid var(--color-text-dark);
  /* No bottom border: the footer sits directly below with its own
     border-top (footer.css), so a bottom border here would double up
     with it, 2px within this box's own 1200px width where the two
     overlap, next to the footer's single 1px everywhere wider. */
  border-bottom: none;
}

.work-content {
  box-sizing: border-box;
}

/* ---- Page header: "Work" title in an accent banner ---- */

.work-header {
  position: relative; /* containing block for the absolutely-positioned title below */
  background: transparent; /* was --color-bg-accent, made transparent so it doesn't clash with the stuck nav bar once scrolled past */
  border-bottom: 1px solid var(--color-text-dark);
  height: 230px; /* exact value specified in the brief */
  box-sizing: border-box;
}

.work-header__title {
  position: absolute;
  left: 60px; /* was 30px, bumped along with the rest of the page's side padding */
  bottom: 30px; /* 30px from the bottom edge of the header, per Lucrecia's request */
  margin: 0;
  font-family: var(--font-heading);
  font-size: var(--font-size-h1); /* back to h1, this is the page's only h1 */
  font-weight: var(--weight-medium);
  color: var(--color-text-dark);
}

/* ---- Dividers between/within sections ---- */

.divider--solid {
  border: none;
  margin: 0;
  height: 1px;
  background: var(--color-text-dark);
}

/* Dashed dividers use a repeating gradient (not border-style: dashed)
   for an exact 5px dash / 5px gap, matching the pattern used
   throughout the rest of the site's dashed dividers. */
.divider--dashed-h {
  height: 1px;
  background-image: repeating-linear-gradient(
    to right,
    var(--color-text-dark) 0,
    var(--color-text-dark) 5px,
    transparent 5px,
    transparent 10px
  );
}

.divider--dashed-v {
  width: 1px;
  align-self: stretch; /* stretches to the full height of its flex row */
  background-image: repeating-linear-gradient(
    to bottom,
    var(--color-text-dark) 0,
    var(--color-text-dark) 5px,
    transparent 5px,
    transparent 10px
  );
}

/* Sits between the two Spyscape sub-projects, 60px clear of each (was 30px) */
.divider--dashed-h--section-gap {
  margin: 60px 0;
}

/* ---- Shared project section wrapper ---- */

.project-section {
  padding: 50px 60px; /* side padding was 30px, doubled per Lucrecia's request; vertical 50px untouched, not part of that ask */
  box-sizing: border-box;
}

/* Mobile side padding is its own smaller value (30px), not the 60px
   desktop got doubled to -- 60px was too much clearance on a narrow
   phone width. Declared here (not in the max-width:768px block near
   the bottom of this file) so it stays next to the base rule it
   overrides, equal specificity so source order alone decides, and this
   comes later in the file. */
@media (max-width: 768px) {
  .project-section {
    padding-left: 30px;
    padding-right: 30px;
  }
}

.project-section__heading {
  margin: 0 0 15px;
  font-family: var(--font-heading);
  font-size: var(--font-size-h2);
  font-weight: var(--weight-medium);
  color: var(--color-text-dark);
}

.project-section__subheading,
.project-subheading {
  font-family: var(--font-heading);
  font-size: var(--font-size-h3);
  font-weight: var(--weight-bold);
  color: var(--color-text-dark);
}

.project-section__subheading {
  margin: 0 0 15px;
}

.project-section__intro,
.project-description,
.project-note {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--font-size-body);
  font-weight: var(--weight-regular);
  color: var(--color-text-dark);
}

/* Bright Little Labs' shared intro paragraph only, per Lucrecia's
   request -- .project-description/.project-note above aren't part of
   this, they're separate per-project description paragraphs elsewhere
   on the page that weren't asked for a width cap. */
.project-section__intro {
  max-width: 540px;
}

/* Space before the Bright Little Labs grid. Declared after
   .project-section__intro above (not just alongside it) so its
   margin-bottom actually wins the cascade instead of being reset by
   the base rule's margin: 0 - this was a real bug found while
   testing, moving it fixed a gap that measured 0px instead of the
   intended value (now 40px, was 60px, was 30px before that). */
.project-section__intro--grid-gap {
  margin-bottom: 40px;
}

/* ---- Text-beside-image row: bottom-aligned, per the general rule ---- */

.project-row {
  display: flex;
  align-items: flex-end; /* bottom-aligns the text block with the image */
  gap: 60px; /* the general inner-spacing rule applies to the gap between image and text too (was 30px) */
}

.project-row__image-link {
  display: block;
  flex-shrink: 0;
}

/* Rows without an explicit image size (Sky's 352px max-height is its
   own modifier below) need the image link to actually grow within the
   flex row, otherwise `width: 100%` on the <img> has no definite width
   to resolve against and the browser falls back to the image's raw
   intrinsic pixel size instead (this was a real bug: Spyscape/DFG/The
   Key's images were rendering at full original size until this was
   added). */
.project-row__image-link--default {
  flex: 1;
  min-width: 0;
}

.project-row__image-link:focus-visible {
  outline: 2px solid var(--color-text-dark);
  outline-offset: 2px;
}

/* All row thumbnails (Sky, both Spyscape sub-projects, Design For Good,
   The Key) share this fixed-height sizing. Height stays constant as the
   window narrows, the flex row's shrinking width acts as a mask that
   crops the sides (via object-fit: cover) instead of the whole photo
   scaling down into an illegibly small thumbnail. 352px matches the
   brief's original Sky height, reused here so every row thumbnail lines
   up at the same height. max-width caps it at that height's own 16:10
   shape (352 x 1.6 = 563px), so on wide rows the image doesn't stretch
   into an oddly wide crop. */
.project-row__image {
  display: block;
  width: 100%;
  height: 352px;
  max-width: 563px;
  object-fit: cover;
  border-radius: var(--radius-image);
  box-shadow: var(--shadow-image);
}

.project-row__text {
  display: flex;
  flex-direction: column;
  align-items: flex-start; /* left-aligned to each other, per the general rule */
  gap: 15px; /* per the general rule */
  min-width: 0;
}

.project-row__text--default {
  /* 450px per Lucrecia's request (was a 266px cap). flex-basis:450px
     (not a hard width) with shrink allowed and min-width:0 (from
     .project-row__text above) so it still renders at exactly 450px on
     any realistic desktop width, but won't force the row to overflow
     at the narrower end of the desktop range (~769-900px) where
     450px text + the image's own minimum + the row's 60px gap don't
     all comfortably fit at once. Applies to Sky, Spyscape, Design For
     Good and The Key. */
  flex: 0 1 450px;
}

.project-title {
  margin: 0;
  font-family: var(--font-heading);
  font-size: var(--font-size-h2);
  font-weight: var(--weight-medium);
}

.project-title--sub {
  /* Used for sub-project names nested under a section heading (e.g.
     the two Spyscape sub-projects), one level down from a top-level
     project title. */
  font-size: var(--font-size-h3);
  font-weight: var(--weight-bold);
}

.project-title__link {
  color: var(--color-text-dark);
  text-decoration: none;
}

.project-title__link:hover,
.project-title__link:focus-visible {
  text-decoration: underline;
}

.project-subheading {
  margin: 0;
}

.project-note {
  font-style: italic;
  font-weight: var(--weight-bold);
}

/* ---- Section 3, Bright Little Labs: 2x2 grid ---- */

.project-grid {
  display: grid;
  grid-template-columns: 1fr 1px 1fr;
  grid-template-rows: auto 1px auto;
}

.project-grid__item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 15px;
}

/* Gutter trimmed to 40px (was 60px, was 30px before that) so the
   thumbnails themselves render bigger -- the image fills 100% of its
   grid item's own content box, so a smaller padding here directly
   grows the visible image, not just the gap around it. */
.project-grid__item:nth-child(1) { grid-column: 1; grid-row: 1; padding: 0 40px 40px 0; }
.project-grid__divider-v { grid-column: 2; grid-row: 1 / 4; }
.project-grid__item:nth-child(3) { grid-column: 3; grid-row: 1; padding: 0 0 40px 40px; }
.project-grid__divider-h { grid-column: 1 / 4; grid-row: 2; }
.project-grid__item:nth-child(5) { grid-column: 1; grid-row: 3; padding: 40px 40px 0 0; }
.project-grid__item:nth-child(6) { grid-column: 3; grid-row: 3; padding: 40px 0 0 40px; }

.project-grid__thumbnail-link {
  display: block;
  width: 100%;
}

.project-grid__thumbnail-link:focus-visible {
  outline: 2px solid var(--color-text-dark);
  outline-offset: 2px;
}

/* 16:10 instead of 4:3 for the 4 Bright Little Labs thumbnails on
   desktop, per Lucrecia's request -- HTML sources swapped to match
   (the -thumbnail-16-10.jpg files, not -4-3.jpg). Mobile goes back to
   4:3 (see the max-width:768px override further down), matching every
   other project thumbnail's own mobile treatment on this page (Sky,
   Spyscape, Design For Good, The Key all switch to their dedicated
   4:3 crop below 768px too) -- this grid was the one exception left
   showing 16:10 at every width, not an intentional mobile choice. */
.project-grid__thumbnail {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  border-radius: var(--radius-image);
  box-shadow: var(--shadow-image);
}

/* ---- Section 5, Chabad + Self initiated: two stacked columns ---- */

.project-columns {
  display: flex;
  align-items: stretch;
  gap: 60px; /* the dashed divider sits within this gap, was 30px */
}

.project-column {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 15px;
}

.project-column__image-link {
  display: block;
  /* .project-column above is a column-direction flex container with
     align-items:flex-start, so this link (a flex item) doesn't stretch
     to the column's width on its own -- it would otherwise shrink-to-
     fit around its content, which is exactly the image using
     width:100% below, an indeterminate percentage-of-shrink-to-fit
     loop that resolves to a 0-width box in practice. Explicit width
     here breaks that loop by giving the image something definite to
     size its own 100% against. */
  width: 100%;
  /* .project-column's own gap (15px) already adds space below every
     item in the column, this adds 25px more on top of that just for
     the image, making the image-to-title gap 40px total without
     touching the 15px title/subheading/description gaps below it. */
  margin-bottom: 25px;
}

.project-column__image-link:focus-visible {
  outline: 2px solid var(--color-text-dark);
  outline-offset: 2px;
}

.project-column__image {
  display: block;
  /* Both Chabad and Self initiated use the same 16:10 crop, and the
     two columns always split 50/50 (.project-column above is flex:1
     with no other sizing input), so filling the column's own width
     already keeps both images the same height as each other, no fixed
     height needed. This used to be a fixed 320px height with the width
     derived from a 4:3 ratio, which fit comfortably within either
     column's real width -- once this became 16:10 the derived width
     (512px at 320px tall) no longer fit, so the old max-width:100%
     safety net clamped the box's WIDTH down without touching its fixed
     HEIGHT, silently squashing the image out of its true 16:10 shape.
     width:100% avoids that failure mode entirely, at any column width. */
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 10;
  border-radius: var(--radius-image);
  box-shadow: var(--shadow-image);
}

/* ---- Thumbnail hover state: every project image, row/grid/column ---- */

.project-row__image,
.project-grid__thumbnail,
.project-column__image {
  transition: transform 200ms ease-out, box-shadow 200ms ease-out;
}

.project-row__image-link:hover .project-row__image,
.project-row__image-link:focus-visible .project-row__image,
.project-grid__thumbnail-link:hover .project-grid__thumbnail,
.project-grid__thumbnail-link:focus-visible .project-grid__thumbnail,
.project-column__image-link:hover .project-column__image,
.project-column__image-link:focus-visible .project-column__image {
  transform: scale(1.03);
  box-shadow: var(--shadow-image-hover);
}

/* Below this width, the side-by-side rows, the 2x2 grid and the two
   columns all stack into a single column instead, otherwise the
   fixed-width text column plus a growing image can't fit a narrow
   viewport at all (this was a real bug: .project-row had no mobile
   rule and overflowed the page horizontally). */
@media (max-width: 768px) {
  /* Header is no longer a fixed-height banner on mobile -- the mobile
     nav is stuck at top:20px, height 69px (see MOBILE_END_TOP/
     END_HEIGHT in portfolio.js, nav top was 40px), so its bottom edge
     sits 89px down from the top of the viewport. Lucrecia wants the
     title 30px clear of that, so the header's top padding is
     20 + 69 + 30 = 119px -- the title always ends up in the right
     place because the header only scrolls into view once the nav has
     already finished morphing into its stuck position (you can't see
     the header until you've scrolled past the entire hero). Height is
     auto (was a fixed 60px) since the padding above now does the
     sizing. Declared in this later media query block (not the earlier
     one above), so it actually wins over the unconditional
     .work-header rule declared in between, same cascade-order issue as
     project-section__intro--grid-gap earlier in this file. */
  .work-header {
    height: auto;
    /* Left/right 30px matches .project-section's own mobile side
       padding below (was 60px, Lucrecia's mobile side-padding request
       applies here too so the "Work" title still lines up with the
       project content underneath it). Bottom 10px is the gap Lucrecia
       wants between the title and the divider line below it -- that
       line is just this header's own existing border-bottom, landing
       exactly 10px below the title because of this padding. */
    padding: 119px 30px 10px;
    box-sizing: border-box;
  }

  .work-header__title {
    position: static;
    left: auto;
    bottom: auto;
  }

  .project-row {
    flex-direction: column;
    align-items: flex-start; /* bottom-align only makes sense side by side */
    gap: 30px; /* image-to-text gap, mobile only, was inheriting the desktop 60px */
  }

  .project-row__image-link--default {
    flex: none;
    width: 100%;
  }

  .project-row__text--default {
    flex-basis: auto;
    width: 100%;
    /* The desktop 450px target width doesn't apply once stacked, full
       width here means up to .project-section's own 60px padding, not
       edge to edge. */
    max-width: none;
  }

  /* Chabad/Self initiated image-to-title gap, mobile only: .project-
     column's own 15px gap plus this reduced from 25px to 15px totals
     30px, matching the 30px "text to thumbnail" spacing used elsewhere
     on this page (was 40px total, desktop's 25px is untouched). */
  .project-column__image-link {
    margin-bottom: 15px;
  }

  /* Text stacks above its image on mobile, using Design For Good's
     source order (already text-then-image) as the reference for every
     project, regardless of which side its image sits on at desktop
     width. Pushing the image to the end covers both .project-row and
     .project-column, since text is always the other child in each. */
  .project-row__image-link,
  .project-column__image-link {
    order: 1;
  }

  .project-grid {
    display: flex;
    flex-direction: column;
    gap: 40px; /* between thumbnails, mobile only, was 60px */
  }

  /* Image-to-title gap within each Bright Little Labs grid item, mobile
     only, per Lucrecia's request (was 30px, before that 15px). */
  .project-grid__item {
    gap: 20px;
  }

  /* Same specificity as the desktop nth-child rules, so padding
     resets cleanly (spacing now comes from the flex gap instead). */
  .project-grid__item:nth-child(1),
  .project-grid__item:nth-child(3),
  .project-grid__item:nth-child(5),
  .project-grid__item:nth-child(6) {
    padding: 0;
  }

  .project-grid__divider-v,
  .project-grid__divider-h {
    display: none;
  }

  .project-columns {
    flex-direction: column;
  }

  .divider--dashed-v {
    display: none;
  }

  /* Chabad Islington and Self initiated still need a divider between
     them once stacked, just solid and horizontal instead of dashed and
     vertical. More specific than the rule above, so it wins over the
     generic "hide all dashed-v dividers" reset without touching the
     Bright Little Labs grid's own vertical divider. */
  .project-columns .divider--dashed-v {
    display: block;
    /* Full viewport width, ignoring .project-section's own 30px side
       padding, per Lucrecia's request (was 100% of the padded column,
       stopping short of the true edges). Negative margin cancels that
       padding out on both sides. */
    width: calc(100% + 60px);
    margin-left: -30px;
    margin-right: -30px;
    height: 1px;
    align-self: auto;
    background-image: none;
    background: var(--color-text-dark);
  }

  /* No override needed here any more: the desktop rule above already
     uses width:100%/height:auto now too (see its own comment), so
     stacking on mobile just naturally continues using the column's
     own full width at every breakpoint. */

  /* The desktop fixed height (352px) doesn't scale down with the
     column's shrinking width once stacked, so thumbnails were
     rendering at whatever odd ratio 352px happened to be relative to
     a narrow mobile width, not a deliberate crop. Mobile uses a
     dedicated 4:3 asset for these thumbnails (see the <picture>
     elements in the HTML), matched here by a real 4:3 box instead of
     the desktop's fixed height. */
  .project-row__image {
    height: auto;
    width: 100%;
    max-width: none;
    aspect-ratio: 4 / 3;
  }

  /* Bright Little Labs grid: back to 4:3 on mobile, matching every
     other project thumbnail on this page (see .project-row__image
     just above). The desktop rule's 16:10 was applying at every width
     since it had no mobile override, this fixes that. */
  .project-grid__thumbnail {
    aspect-ratio: 4 / 3;
  }

  /* Chabad + Self initiated: same fix, now that their <picture>
     elements switch to the dedicated 4:3 crop on mobile too (see the
     HTML), the box needs to match instead of forcing the desktop
     16:10 shape onto it. */
  .project-column__image {
    aspect-ratio: 4 / 3;
  }
}
