/* ============================================================
   DONATO MILLWORK — Main Stylesheet
   ============================================================
   Full-viewport spreadsheet. All chrome (logo, nav, RFQ) lives in
   the top band and aligns with the table's column grid. Project
   rows extend to the right edge of the screen; a photo preview
   sits behind the rows on the right so the last-column data and
   the project description bleed over the image — matching HV.
   ============================================================ */

@font-face {
  font-family: 'Denim';
  src: url('../fonts/DenimINKVF.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Mono';
  src: url('../fonts/IBM_Plex_Mono/IBMPlexMono-Regular.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* ─── RESET ─── */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
a                      { color: inherit; text-decoration: none; }
button, input, select, textarea {
  font: inherit;
  color: inherit;
  background: transparent;
  border: none;
  outline: none;
}

/* ─── ROOT ─── */
:root {
  --font-main:  'Denim', ui-sans-serif, system-ui, sans-serif;
  --font-mono:  'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --pad-x:      20px;
  --pad-top:    20px;
  /* Chrome row renders at exactly one table-row height (8pt × 1.6
     line-height ≈ 17px) so it sits flush against the spreadsheet
     below with no vertical gap. */
  --chrome-h:   17px;
  --table-size: 8pt;

  /* Column grid — extends full viewport width. Col 1 sized so the
     logo ends right at its edge; the # column (col 2) then sits
     immediately after with just the standard grid gap, so "01",
     "02" etc. read as directly following the logo. `#` widened to
     72px so the expanded "number" label in the header row has room
     before "year" starts. Title column absorbs the rest.
     Order: (close) # year title client typologies location hardware materials
     Client is a fixed 100px — short names ("private", "molteni&c")
     don't need a full 1fr slot, and narrowing it pulls the
     typologies column (and everything right of it) noticeably
     leftward so the row doesn't feel cramped now that there are
     9 columns instead of 8. */
  --pf-cols: 120px 72px 52px minmax(100px, 1fr) 100px 1fr 1fr 1fr 1.2fr;
  --pf-gap:  14px;
}

html, body {
  width:  100%;
  height: 100%;
  overflow: hidden;
  background: #fff;
  color:      #000;
  font-family: var(--font-mono);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ─── CHROME (logo / nav / RFQ) ─── */
/* Fixed grid row at the top that shares the table's --pf-cols
   template, so logo / nav / rfq sit exactly above their column
   counterparts. Empty <span>s fill the skipped cells to preserve
   the grid alignment. */
.pf-chrome {
  position:              fixed;
  top:                   var(--pad-top);
  left:                  var(--pad-x);
  right:                 var(--pad-x);
  z-index:               250;   /* above .pf-lightbox (200) so logo stays visible when the photo is expanded */
  display:               grid;
  grid-template-columns: var(--pf-cols);
  gap:                   var(--pf-gap);
  align-items:           baseline;
  font-family:           var(--font-mono);
  font-size:             var(--table-size);
  line-height:           1.35;           /* matches .pf-row rhythm */
  pointer-events:        none; /* empty cells shouldn't block pointer events */
}
.pf-chrome > * { pointer-events: auto; }
.pf-chrome > span { min-width: 0; }

.logo {
  /* Right-aligned in col 1 so the end of "donato millwork" lines up
     flush with col 1's right edge. With col 1 sized to match the
     logo's natural width, the # column (col 2) starts immediately
     after a single grid gap from the logo. */
  grid-column:  1 / 2;
  justify-self: end;
  white-space:  nowrap;
  cursor:       pointer;
  opacity:      0; /* intro fades it in */
  color:        #000;
}

/* Close link — absolutely positioned just to the left of its anchor
   element (the body paragraph or the first form label). `right: 100%`
   pins close's right edge to the anchor's left edge, so close sits
   immediately adjacent to the content instead of being glued to a
   column boundary. Small gap via margin-right. `top: 0` aligns
   close's first character with the anchor's first line. */
.pf-inline-close {
  position:     absolute;
  right:        100%;
  top:          0;
  /* Horizontal gap between close and body — roomier than the single
     row-height gap, mirrors the total padding between chrome top and
     body top. */
  margin-right: calc(var(--chrome-h) * 2);
  color:        #000;
  cursor:       pointer;
  transition:   color 0.18s ease;
  white-space:  nowrap;
}
.pf-inline-close:hover { color: #bbb; }

.nav {
  /* Nav auto-places into the title column (col 4) — two empty spans
     before it claim cols 2 and 3, so nav lands at col 4. No explicit
     grid-column here because combining one with the auto-placed
     sibling spans triggers a Safari grid quirk that wraps the chrome
     onto two visual rows. Auto-flow keeps everything on row 1. */
  white-space:    nowrap;
  pointer-events: auto;
  opacity:        0;
}
.nav-link {
  color:      #000;           /* black by default */
  cursor:     pointer;
  transition: color 0.18s ease;
}
.nav-link:hover             { color: #bbb; } /* grey on hover */
.nav-link.active-page       { color: #000; }

/* Phone — always visible in the chrome, click-to-call.
   Desktop-only margin-right mirrors the logo's left slack: logo
   sits flush-right in a 120px col 1, so its left edge ends up
   ~(120px − logo_width) from the viewport edge. Adding that same
   amount to the phone's right margin gives both chrome anchors
   matching viewport gutters. Scoped to desktop (min-width: 768)
   because on mobile the phone sits in col 2 of a 2-col grid with
   justify-self: end — the extra margin shoved it visibly left. */
.pf-phone {
  grid-column:  9 / 10;
  justify-self: end;
  color:        #000;
  cursor:       pointer;
  white-space:  nowrap;
  transition:   color 0.18s ease;
}
@media (min-width: 768px) {
  .pf-phone {
    margin-right: calc(120px - var(--logo-width, 120px));
  }
}
.pf-phone:hover { color: #bbb; }

/* Studio tagline — starts at the client column (col 5) and runs to
   the hardware column (col 8), same baseline as logo / nav / phone.
   Starting at the client column (instead of typologies) pulls the
   tagline leftward so it reads closer to the nav and leaves a more
   generous gap before the phone. Hidden when the user clicks into
   the about page (the about page shows the full studio description,
   so the tagline would be redundant there). */
.pf-tagline {
  grid-column: 5 / 9;
  color:       #000;
  white-space: nowrap;
}
body.about-open   .pf-tagline,
body.contact-open .pf-tagline { visibility: hidden; }
.rfq-cta[hidden] { display: none; }
/* Chrome RFQ is mobile-only — the mobile media query below promotes
   it to `display: block` when a project is open. */
.pf-chrome-rfq { display: none; }

/* Responsive chrome — graceful fallbacks when the desktop window is
   squished. The tagline is ~590px of nowrap text; with grid column
   math it starts overflowing into the phone at ~1400px. Drop the
   phone there. At ~1080 the tagline itself runs short on its own —
   drop it too so the logo + nav read cleanly. Mobile's ≤767 media
   query takes over below that. */
@media (max-width: 1400px) and (min-width: 768px) {
  .pf-phone { display: none; }
}
@media (max-width: 1080px) and (min-width: 768px) {
  .pf-tagline { display: none; }
}
@keyframes rfq-pulse {
  0%, 100% { color: #000; }
  50%      { color: #bbb; }
}

/* ─── HOME INTRO DESCRIPTION ─── */
.home-desc {
  position:    fixed;
  inset:       0;
  z-index:     90;
  display:     flex;
  justify-content: center;
  align-items:     center;
  padding:     var(--pad-x);
  pointer-events: none;
  transition:  opacity 0.3s ease;
  font-family: var(--font-main);
}
#descText {
  font-size:   14px;
  line-height: 1.35;
  text-align:  center;
  max-width:   min(720px, 72vw);
}
.home-desc.hidden,
body.page-open .home-desc { opacity: 0; pointer-events: none; }

/* ─── PAGES ─── */
.page {
  position: fixed;
  inset:    0;
  z-index:  50;
  opacity:  0;
  pointer-events: none;
  transition: opacity 0.3s ease;
  overflow:  hidden;
  background: #fff;   /* solid backdrop so mix-blend-mode has something to invert against */
  /* +2 × chrome-h: one row for the chrome band, one row of breathing
     space below it before the column-letter header strip. */
  padding:   calc(var(--pad-top) + 2 * var(--chrome-h)) var(--pad-x) var(--pad-x);
}
.page.active {
  opacity:        1;
  pointer-events: auto;
}

/* About / contact inherit .page's base top padding so "close" + body
   content land on the same Y as the portfolio's header row (n y t t
   l h) — one row higher than they previously sat. */

/* ─── TABLE (full viewport width) ─── */
.pf-table {
  position:    relative;            /* anchor for absolute description overlay */
  width:       100%;
  font-family: var(--font-mono);
  font-size:   var(--table-size);
  line-height: 1.1;
  color:       #000;
}

/* Column-letter header strip. Inherits table's color.
   position: sticky so the header stays pinned at the top of the
   picker's scroll area as the user scrolls through rows — labels
   remain visible. Mobile hides the header entirely (via the mobile
   @media display: none) so sticky is a no-op there. */
.pf-header {
  display:               grid;
  grid-template-columns: var(--pf-cols);
  gap:                   var(--pf-gap);
  color:                 inherit;
  /* Small padding-bottom breaks the header away from the first row
     by a few pixels without letting rows show through the gap under
     the sticky header (padding keeps the header's white background
     extended into that gap). */
  padding:               0 0 0.3em;
  cursor:                default;
  position:              sticky;
  top:                   0;
  background:            #fff;
  z-index:               5;
  /* Fixed order so when .pf-table becomes a flex column (needed for
     the order-based open-project layout below), the header always
     stays at the visual top, ahead of the re-ordered selected row. */
  order:                 -3;
}
.pf-header > span {
  white-space:   nowrap;
  overflow:      visible; /* full labels can spill into adjacent empty cell on hover */
}

/* .pf-table becomes a flex column on both platforms so the opened-
   project layout can use `order:` to lift the selected row + its
   description above any other rows (including ones the user scrolled
   past to click). Previously mobile-only; unifying removes the issue
   where clicking row 28 on desktop would leave the description
   floating way below the visible scroll window. */
.pf-table {
  display:        flex;
  flex-direction: column;
}
.pf-row.is-selected { order: -2; }
#pfDescription      { order: -1; }

/* Shared picker styles — applies to both mobile (6-row cap) and
   desktop (8-row cap). JS publishes --browse-max-height on
   #portfolioLeft regardless of project count, so the picker is
   always a fixed-size window for consistent layout proportions.
   min-height paired with max-height forces exactly that size even
   when there are fewer rows than the threshold. The :not(.has-
   selection) gate auto-drops the cap when a project is opened so
   the expanded view flows naturally. Bottom mask-image fades the
   edge so rows dissolve rather than clipping against a hard line.
   Scrollbar hidden for a cleaner editorial look.
   Touch/snap behaviour (-webkit-overflow-scrolling, scroll-snap)
   lives inside the mobile @media — snap fights mouse-wheel on
   desktop. */
#portfolioLeft:not(.has-selection) {
  max-height:         var(--browse-max-height, none);
  min-height:         var(--browse-max-height, 0);
  overflow-y:         auto;
  scrollbar-width:    none;
  -ms-overflow-style: none;
  mask-image: linear-gradient(
    to bottom,
    #000 0,
    #000 calc(100% - 0.7em),
    transparent 100%
  );
  -webkit-mask-image: linear-gradient(
    to bottom,
    #000 0,
    #000 calc(100% - 0.7em),
    transparent 100%
  );
}
#portfolioLeft:not(.has-selection)::-webkit-scrollbar { display: none; }

/* Rows — inherit table's #000 color. */
.pf-row {
  display:               grid;
  grid-template-columns: var(--pf-cols);
  gap:                   var(--pf-gap);
  align-items:           baseline;
  padding:               0;
  line-height:           1.35;
  color:                 inherit;
  cursor:                pointer;
}
.pf-row > span {
  white-space:   nowrap;
  overflow:      hidden;
  text-overflow: ellipsis;
}
/* Picker highlight: top-of-scroll row is the "focused" project,
   other rows fade to grey. Same treatment on both platforms.
   :not(:hover) exclusion so hovering a dimmed row brings it back
   to full opacity (the hover rule below sets the hovered row to
   opacity: 1, but the has-highlight rule has higher specificity
   thanks to the #portfolioLeft ID and would otherwise win). */
#portfolioLeft.has-highlight .pf-row:not(.is-highlighted):not(.is-selected):not(:hover) {
  opacity: 0.3;
}
/* Hover: non-hovered rows fade via opacity (kept consistent with the
   selection state's fade). */
.pf-table:has(.pf-row:hover) .pf-row          { opacity: 0.3; }
.pf-table:has(.pf-row:hover) .pf-row:hover    { opacity: 1;   }

/* Rows collapse to zero height + fade when a project is opened —
   both platforms now, so opening a scrolled-down row on desktop
   lifts it to the top via order: -2 with nothing visible between
   it and the header. Transition on opacity/max-height gives a
   quick animated collapse; is-opening / is-closing classes below
   freeze the transitions during the class swap burst so the state
   change paints cleanly in one frame. */
.pf-row {
  overflow:    hidden;
  max-height:  3em;
  flex-shrink: 0;
  transition:  opacity 0.2s ease, max-height 0.25s ease, margin 0.25s ease;
}
#portfolioLeft.has-selection .pf-row:not(.is-selected) {
  opacity:        0;
  max-height:     0;
  margin-top:     0;
  margin-bottom:  0;
  pointer-events: none;
}
/* Freeze row transitions during open/close state-change burst. */
#portfolioLeft.is-opening .pf-row,
#portfolioLeft.is-opening .pf-row *,
#portfolioLeft.is-closing .pf-row,
#portfolioLeft.is-closing .pf-row * {
  transition: none !important;
}
/* Selected row + header inherit the table's #000 color. (Used to be
   white with mix-blend-mode: difference for the photo-behind-text
   inversion; removed since no part of any photo ever renders behind
   text in this layout.) */
/* Close label on a selected project row — width matches the logo's
   rendered pixel width (set in JS via --logo-width). With the span
   right-aligned in col 1 (justify-self: end) and its text left-
   aligned inside, "close" renders starting at the same X as the
   logo's first character. */
.pf-close {
  visibility:   hidden;
  cursor:       pointer;
  justify-self: end;
  width:        var(--logo-width, 102px);
  text-align:   left;
}
.pf-row.is-selected .pf-close { visibility: visible; }

/* Inline description — sits in the flex flow (order: -1) directly
   below the selected row (order: -2) on both platforms. Previously
   this was absolute-positioned on desktop with JS setting `top` to
   the clicked row's bottom edge, which meant a click on a scrolled-
   down row (e.g. project 28) placed the description far below the
   visible viewport. With flex ordering the selected row and its
   description always anchor at the top of the picker, independent
   of scroll position. */
.pf-description {
  position:              static;
  display:               grid;
  grid-template-columns: var(--pf-cols);
  gap:                   var(--pf-gap);
  align-items:           start;           /* text + photo both anchor to the top of the row */
  padding:               0;
  margin-top:            2em;             /* breathing between title row and description */
  font-size:             var(--table-size);
  line-height:           1.35;            /* matches .pf-row so description lines sit on the row grid */
}

/* Text column — short desc + "+info" toggle + long desc stacked.
   Sits at grid cols 4-5 (title + client). Col 6 (typologies) is the
   breathing gutter between this block and the photo at col 7+.
   width: 120% pushes the text's right edge into that gutter so
   copy wraps later (wider line) without changing the grid cell
   assignment — the photo at col 7+ is unaffected. */
.pf-description .pf-desc-text {
  grid-column:    4 / 6;
  display:        flex;
  flex-direction: column;
  gap:            0.3em;
  width:          150%;
  pointer-events: auto;
}
/* Short description — stacks inside .pf-desc-text */
.pf-description .pf-desc-short { white-space: pre-wrap; }

/* "view more" toggle — mobile-only. Hidden on desktop (the full
   long description isn't needed there; the page has room for it
   to read in one go if ever desired). Inline at the end of the
   short description on mobile — handled by mobile media query. */
.pf-description .pf-info-toggle { display: none; }

/* Long description.
   - Mobile: hidden by default, toggled by "view more".
   - Desktop: always visible (no toggle exists there; there's room
     to read the full thing inline with the photo). */
.pf-description .pf-desc-long {
  white-space: pre-wrap;
  margin-top:  0.3em;
}
.pf-description .pf-desc-long[hidden] { display: block; }
@media (max-width: 767px) {
  .pf-description .pf-desc-long[hidden] { display: none; }
}

/* Inline "request a quote →" link at the end of the project's text
   block — the one persistent, highly visible CTA. Pulses between
   black and grey to draw the eye, with extra top margin so it
   reads as separate from the description. */
.pf-description .pf-desc-rfq {
  align-self:  start;
  cursor:      pointer;
  color:       inherit;
  margin-top:  1.6em;
  animation:   rfq-pulse 1.4s ease-in-out infinite;
}
/* Specificity needs to beat `.pf-table.has-selection .pf-row:not(.is-selected)`
   so covered rows white-out instead of showing their grey fallback. */
.pf-table.has-selection .pf-row.is-covered,
.pf-row.is-covered { color: #fff; }

/* ─── INLINE PROJECT PHOTO + NAVIGATION ─── */
/* Photo spans grid columns 4 / -1 — the full right side of the
   spreadsheet — so it can render as large as the viewport allows.
   Image uses max-width: 100% + max-height: 65vh so it fits without
   scrolling. The nav row beneath matches the rendered image width
   via JS. */
.pf-detail-photo {
  /* Shifted one column right (was 6 / -1) so col 6 (typologies) acts
     as an empty gutter between the description text and the photo —
     prevents the two from reading as glued together. Photo now
     occupies location → materials only. */
  grid-column:    7 / -1;
  pointer-events: auto;
  display:        flex;
  flex-direction: column;
  align-items:    flex-start;     /* children shrink to natural width */
}
.pf-detail-photo img {
  display:           block;
  width:             auto;
  height:            auto;
  max-width:         100%;
  max-height:        65vh;
  object-fit:        contain;
  image-orientation: from-image;
  user-select:       none;
  -webkit-user-drag: none;
  color:             initial;     /* opt out of the text blend */
  mix-blend-mode:    normal;
}

/* Horizontal photo slider (replaces single-img swap).
   Frame: visible window, clips the track. Track: holds all slides
   side-by-side; transform: translateX moves between photos. Slide:
   one slot per photo, full frame width so snap distances are
   uniform regardless of image orientation. The transition runs on
   release; .is-dragging pins it off so live drag tracks the finger. */
.pf-photo-frame {
  width:    100%;
  overflow: hidden;
}
.pf-photo-track {
  display:    flex;
  /* Visible breathing room between slides — appears during the swipe
     transition so adjacent photos are clearly distinct from each other
     instead of butting up edge-to-edge. JS reads this gap from
     getComputedStyle so the snap distance stays in sync. */
  gap:        6vw;
  transition: transform 0.42s cubic-bezier(0.32, 0.72, 0, 1);
  will-change: transform;
}
.pf-photo-track.is-dragging {
  transition: none;
}
.pf-photo-slide {
  flex-shrink: 0;
  width:       100%;
  display:     flex;
  align-items: flex-start;
  justify-content: flex-start;
}

/* Custom text-cursor — the label "expand" follows the mouse while
   the native cursor is hidden. Positioned via --cursor-x / --cursor-y
   custom properties set from a mousemove listener on .pf-detail-photo.
   mix-blend-mode: difference makes the label invert against whatever
   photo area is beneath it — white text over dark wood reads white,
   over a white wall reads black. Desktop-only: on touch devices,
   there's no cursor to follow, so the rule is scoped to (hover: hover)
   which also restores a default pointer idiom for non-hover devices. */
.pf-detail-photo { position: relative; }
@media (hover: hover) {
  .pf-detail-photo img       { cursor: none; }
  .pf-detail-photo .pf-photo-cursor {
    position:        absolute;
    top:             0;
    left:            0;
    transform:       translate(var(--cursor-x, -9999px), var(--cursor-y, -9999px)) translate(0.6em, 0.4em);
    font-family:     var(--font-mono);
    font-size:       var(--table-size);
    color:           #fff;
    mix-blend-mode:  difference;
    pointer-events:  none;
    letter-spacing:  0.02em;
    white-space:     nowrap;
    z-index:         2;
    opacity:         0;
    transition:      opacity 0.12s ease;
    will-change:     transform;
  }
  .pf-detail-photo.is-cursor-active .pf-photo-cursor { opacity: 1; }
}
/* Desktop landscape photos: the container extends to the chrome's
   right edge (viewport - pad-x), but the phone has an extra
   margin-right = calc(120px - --logo-width) so its right edge sits
   slightly inside that. Cap the landscape image's max-width by the
   same amount so the photo's right edge mirrors the phone's right
   edge — the gutter on the right matches the gutter on the left
   between the viewport edge and the logo's first character.
   Portrait images don't need this — they're naturally narrow.
   Scoped to desktop (mobile has its own full-width photo rules). */
@media (min-width: 768px) {
  /* The reduction is `120px - --logo-width` so the photo's right edge
     mirrors the phone's right edge in the chrome bar above. When the
     logo measures wider than 120px (e.g., on desktop where the rendered
     "donato millwork" is ~145px), 120 - 145 = -25 — without clamping,
     calc(100% - -25px) resolves to 100% + 25px, allowing the image to
     overflow its slide. max(0px, …) clamps the reduction at zero so
     overlong logos collapse to "no reduction" instead of overflow.
     Applied to all images (no longer .is-portrait-gated) so the slider
     swipes between orientations without per-photo width recalculation. */
  .pf-detail-photo img {
    max-width: calc(100% - max(0px, 120px - var(--logo-width, 120px)));
  }
}
.pf-photo-nav {
  display:         flex;
  justify-content: space-between;
  align-items:     baseline;
  margin-top:      0.6em;
  font-size:       var(--table-size);
  line-height:     1.1;
  color:           #000;
  /* width is set in JS to match the rendered image width so prev /
     counter / next line up with the image edges, regardless of
     portrait vs landscape aspect. */
}
.pf-photo-nav a {
  cursor:       pointer;
  white-space:  nowrap;
  transition:   opacity 0.15s ease;
  color:        #000;
}
.pf-photo-nav a:hover          { opacity: 0.5; }
.pf-photo-nav .pf-photo-counter { opacity: 0.5; }

/* Desktop arrow-key hint — first-time prompt nudging the user toward
   keyboard navigation since the prev/next buttons are subtle. Sits in
   the same band as the photo counter (centered in the nav row),
   replacing the prev/counter/next text while it's shown. The
   .is-hinting class on the nav hides those texts via visibility:hidden
   so the layout space is preserved; once the hint fades out and the
   class is removed, the nav contents reappear with no layout shift. */
.pf-photo-nav { position: relative; }
.pf-photo-nav.is-hinting > a,
.pf-photo-nav.is-hinting > .pf-photo-counter { visibility: hidden; }
.pf-arrow-hint {
  position:       absolute;
  top:            0;
  left:           50%;
  transform:      translateX(-50%);
  color:          #000;
  font-family:    var(--font-mono);
  font-size:      var(--table-size);
  line-height:    1.1;
  white-space:    nowrap;
  pointer-events: none;
  transition:     opacity 0.2s ease;
  animation:      arrow-hint-blink 1.4s ease-in-out infinite;
  z-index:        3;
}
.pf-arrow-hint.is-fading { opacity: 0; }
@keyframes arrow-hint-blink {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.3; }
}
/* Mobile shows the swipe hint instead — hide arrow hint there. */
@media (max-width: 767px) {
  .pf-arrow-hint { display: none; }
  .pf-photo-nav.is-hinting > a,
  .pf-photo-nav.is-hinting > .pf-photo-counter { visibility: visible; }
}

/* ─── PHOTO LIGHTBOX (click-to-expand) ─── */
/* Full-viewport overlay. Click the inline project photo to open;
   click anywhere on the lightbox or press Escape to close. */
.pf-lightbox {
  position:        fixed;
  inset:           0;
  z-index:         200;
  background:      #fff;
  display:         flex;
  align-items:     center;
  justify-content: center;
}
.pf-lightbox[hidden] { display: none; }
.pf-lightbox-img {
  /* Reserve a chrome-height buffer at top + bottom (4em each ≈ space
     for logo / close label) so the lightbox image never paints into
     the same band as the chrome bar. Previously max-height: 95vh —
     on iPad portrait that left only ~2.5vh top margin, which the
     "close" label and logo overlap visually. With the fixed em buffer
     the image stays centred and chrome elements always have room. */
  max-width:         95vw;
  max-height:        calc(100vh - 8em);
  width:             auto;
  height:            auto;
  object-fit:        contain;
  image-orientation: from-image;
  user-select:       none;
  -webkit-user-drag: none;
}
/* "close" label — follows the cursor on desktop via --cursor-x / --cursor-y
   set from a mousemove listener on .pf-lightbox. Native cursor is hidden
   so only the text reads. mix-blend-mode: difference inverts it live
   against the photo beneath. Touch devices fall back to a static
   top-right label via the @media (hover: none) branch. */
.pf-lightbox-close {
  position:        absolute;
  top:             var(--pad-top);
  right:           var(--pad-x);
  font-family:     var(--font-mono);
  font-size:       var(--table-size);
  color:           #fff;
  mix-blend-mode:  difference;
  pointer-events:  none;
  z-index:         2;
}
@media (hover: hover) {
  .pf-lightbox      { cursor: none; }
  .pf-lightbox-img  { cursor: none; }
  .pf-lightbox-close {
    top:       0;
    right:     auto;
    left:      0;
    transform: translate(var(--cursor-x, -9999px), var(--cursor-y, -9999px)) translate(0.6em, 0.4em);
    will-change: transform;
  }
}

/* While the lightbox is open, hide the nav, request-a-quote, tagline,
   and the project description (mix-blend-mode was making it peek
   through the white lightbox backdrop). Only the logo and the
   expanded photo remain visible. */
body.lightbox-open .nav,
body.lightbox-open .rfq-cta,
body.lightbox-open .pf-chrome-rfq,
body.lightbox-open .pf-tagline,
body.lightbox-open .pf-phone,
body.lightbox-open .pf-description {
  visibility: hidden;
}

/* Logo over the lightbox: invert its colour live against whatever's
   behind it. Without this, a black-textured photo (or a placeholder
   black image) renders the black logo invisibly. mix-blend-mode:
   difference flips it to white over dark, black over light, so the
   logo stays readable regardless of which photo is expanded. */
body.lightbox-open .logo {
  color:          #fff;
  mix-blend-mode: difference;
}

/* ─── INTRO PREVIEW (row-by-row photo flash) ─── */
/* Fixed panel on the right. During the first-visit intro, each
   project's hero photo flashes here as its row reveals, so the
   user subconsciously learns that rows are clickable = photos.
   Hidden outside the intro sequence. */
.intro-preview {
  position:       fixed;
  top:            5vh;
  right:          var(--pad-x);
  width:          32vw;
  height:         90vh;
  z-index:        60;
  pointer-events: none;
  opacity:        0;
  transition:     opacity 0.25s ease;
}
.intro-preview.is-visible { opacity: 1; }

/* ─── INTRO HINT ("select a project to view →") ─── */
/* Sits near the bottom-left of the viewport so it doesn't overlap
   the first spreadsheet row. Fades in once after the stagger +
   typewriter finish, then fades out a few seconds later.
   The picker is always active (JS sets --browse-max-height
   regardless of project count), so on desktop the hint always
   positions below the 8-row picker + header row (~14em offset
   after chrome clearance). The mobile variant is handled inside
   the mobile @media block. */
body:has(#portfolioLeft[style*="--browse-max-height"]) .pf-intro-hint {
  top:    calc(var(--pad-top) + 2 * var(--chrome-h) + 14em);
  bottom: auto;
}
.pf-intro-hint {
  position:       fixed;
  bottom:         calc(var(--pad-x) + var(--chrome-h));
  left:           calc(var(--pad-x) + 120px + 72px + 52px + 3 * 14px);
  z-index:        60;
  font-family:    var(--font-mono);
  font-size:      var(--table-size);
  color:          #000;
  /* opacity fade — starts transparent, fades in black on intro
     end, then back to transparent. Via opacity not color so the
     hint is genuinely invisible when hidden (would otherwise paint
     as a white-coloured string over project photos on open). */
  opacity:        0;
  pointer-events: none;
  transition:     opacity 0.6s ease;
}
.pf-intro-hint.is-visible { opacity: 1; }
.intro-preview-img {
  width:             100%;
  height:            100%;
  object-fit:        contain;
  object-position:   top right;
  image-orientation: from-image;
  user-select:       none;
}

/* ─── ABOUT PAGE ─── */
/* Content aligns with the about nav link — body paragraph starts at
   grid column 5 (typologies column) so the left margin of the text
   matches the about button above it. Close lives at col 4 (one cell
   to the left of the body). */
.pf-about-block {
  display:               grid;
  grid-template-columns: var(--pf-cols);
  gap:                   var(--pf-gap);
  padding:               0;
  color:                 #000;
  line-height:           1.35;
}
.pf-about-body {
  grid-column: 4 / 7;        /* start at title col to align under the nav */
  white-space: pre-wrap;
  position:    relative;     /* anchor for the absolute .pf-inline-close */
}
.pf-about-label { grid-column: 4 / 5; color: #000; }
.pf-about-value { grid-column: 5 / 7; color: #000; }

/* ─── CONTACT FORM ROWS ─── */
/* Matches about — form fields align under the contact nav link. */
.pf-form-row {
  display:               grid;
  grid-template-columns: var(--pf-cols);
  gap:                   var(--pf-gap);
  align-items:           baseline;
  padding:               0;
  color:                 #000;
  line-height:           1.35;
}
.pf-form-row .pf-form-label {
  grid-column: 4 / 5;
  color:       #000;
  position:    relative;     /* anchor for the absolute .pf-inline-close on first row */
}
.pf-form-row .pf-form-input,
.pf-form-row .pf-form-select,
.pf-form-row .pf-form-textarea {
  /* Cols 5-7 (client + typologies + location) = ~3-column-wide
     input area. Previously spanned 5 / -1 (all the way to the
     materials column), which made the textarea look like a huge
     empty box. Tighter width reads more like a form and less like
     a billboard. */
  grid-column:  5 / 8;
  font-family:  var(--font-mono);
  font-size:    var(--table-size);
  color:        #000;
  padding:      0;
  width:        100%;
}
.pf-form-row .pf-form-textarea {
  min-height:  5em;
  resize:      vertical;
  line-height: 1.35;
}
.pf-form-row input::placeholder,
.pf-form-row textarea::placeholder,
.pf-form-row select:invalid { color: #bbb; }
.pf-form-send {
  grid-column:  5 / -1;
  justify-self: start;
  margin-top:   0.8em;
  font-family:  var(--font-mono);
  font-size:    var(--table-size);
  color:        #000;
  cursor:       pointer;
  padding:      0;
}
.pf-form-send:hover { color: #bbb; }

/* ============================================================
   MOBILE (max-width: 767px)
   ============================================================
   Strip the spreadsheet down to # + title only, stack the chrome,
   and give the photo a sane max-height so description + nav are
   visible above and below without relying on the text-invert
   effect to "show through" the image. Tap a photo to expand into
   the same lightbox used on desktop.
   ============================================================ */
@media (max-width: 767px) {

  :root {
    /* Col 1 = 70px: enough for "typologies" (the longest metadata
       label) with a small breathing margin. Matches the metadata
       footer's label column so the project title in col 2 lands at
       the same X as the metadata values, while pulling everything
       noticeably left for a more comfortable layout. */
    --pf-cols: 70px 1fr auto;
    --pf-gap:  10px;
  }

  /* Desktop's hover-fade (`.pf-table:has(.pf-row:hover) .pf-row {
     opacity: 0.3 }`) leaks onto mobile because iOS's :hover state
     sticks to the last-tapped element until a different element is
     tapped. That was dimming every row to 0.3 whenever a row had a
     stale :hover — including the highlighted one, which read as
     "darker grey" instead of solid black. Force opacity:1 on mobile
     to short-circuit the desktop rule entirely. */
  .pf-table:has(.pf-row:hover) .pf-row { opacity: 1 !important; }

  /* Allow the page to scroll on mobile — opened projects can exceed
     one viewport once the photo + description are rendered. */
  .page { overflow-y: auto; }

  /* Top-align the portfolio content in both browse and selected
     states. The page's padding-top (pad-top + 2 × chrome-h) already
     puts the first row one empty line below the chrome — the HV-
     style "proper" feel — so no extra flex-centering or per-state
     margin-top is needed. Title of a selected project lands at the
     same Y as the highlighted row, because both are the first child
     of #portfolioLeft and #portfolioLeft sits at the top of the
     content area. */
  #page-portfolio {
    min-height:      100vh;
    display:         flex;
    flex-direction:  column;
  }

  /* Mobile-only touch + snap niceties for the picker — they're
     scoped here because scroll-snap on desktop fights mouse-wheel
     scrolling, and -webkit-overflow-scrolling / overscroll-behavior
     are touch-device specific. The base picker styles (max-height,
     overflow, scrollbar-hide, mask fade) live outside the media
     query so desktop with > 8 projects gets the same scrolling
     behaviour. */
  #portfolioLeft:not(.has-selection) {
    -webkit-overflow-scrolling: touch;
    overscroll-behavior:        contain;
    scroll-snap-type:           y mandatory;
  }
  #portfolioLeft:not(.has-selection) .pf-row { scroll-snap-align: start; }

  /* Kill the default iOS tap-highlight flash on rows — on taps that
     land on a row but resolve to a "view" action (generous right-
     side zone), the default highlight briefly flashes the tapped
     row's text, even though the click handler opens a different
     project. transparent tap-highlight = no flash, handler decides. */
  .pf-row,
  .pf-row * { -webkit-tap-highlight-color: transparent; }

  /* ── Chrome: single row. Logo left, about/contact nav right. Phone
     removed on mobile (still on the contact page). ── */
  .pf-chrome {
    grid-template-columns: auto auto;
    grid-auto-rows:        auto;
    row-gap:               0.35em;
  }
  .pf-chrome > span { display: none; }
  .logo { grid-column: 1; grid-row: 1; justify-self: start; }
  .nav  { display: block; grid-column: 2; grid-row: 1; justify-self: end; }
  .pf-phone   { display: none; }
  .pf-tagline { display: none; }

  /* Chrome RFQ — mobile-only. Hidden by default; swaps in for the
     about/contact nav in the top-right when a project is open. Uses
     the same grid slot as the nav so the layout doesn't shift. */
  .pf-chrome-rfq {
    display:      none;
    grid-column:  2;
    grid-row:     1;
    justify-self: end;
    color:        #000;
    cursor:       pointer;
    animation:    rfq-pulse 1.4s ease-in-out infinite;
  }
  body:has(#portfolioLeft.has-selection) .nav           { display: none; }
  body:has(#portfolioLeft.has-selection) .pf-chrome-rfq { display: block; }
  /* The chrome RFQ follows the has-selection state on the portfolio
     page, but with { keepProject: true } on the RFQ click the
     project stays selected underneath contact/about — so the
     has-selection rule above was still forcing display: block on
     those pages. !important overrides it. Same for about. */
  body.contact-open .pf-chrome-rfq,
  body.about-open   .pf-chrome-rfq { display: none !important; }

  /* Inline RFQ in the description is redundant when the chrome RFQ
     is visible — hide it. */
  body:has(#portfolioLeft.has-selection) .pf-description .pf-desc-rfq {
    display: none;
  }

  /* ── Table: only # and title columns are visible. Header hidden. ── */
  .pf-header { display: none; }
  .pf-row .pf-close,
  .pf-row .pf-year,
  .pf-row .pf-client,
  .pf-row .pf-typologies,
  .pf-row .pf-location,
  .pf-row .pf-hardware,
  .pf-row .pf-materials { display: none; }

  /* Rows between old and new focus get faded during a tap-triggered
     scroll so they don't "fly past" visibly. Class is applied by
     scrollToTopOfRow and removed after the scroll settles. */
  .pf-row.is-in-transit {
    opacity: 0;
  }
  /* Selected row keeps its natural height while the FLIP animation
     translates it from the old to new position. */
  .pf-row.is-selected { will-change: transform; }
  /* Description is a flex sibling, not a .pf-row — exempt it from
     the max-height clamp. */
  #pfDescription { max-height: none; overflow: visible; }

  /* ── Row action slot ("view" / "close") lives on column 3, right-
     aligned. Empty text ⇒ no visible affordance. ── */
  .pf-row .pf-action {
    display:      block;
    grid-column:  3;
    justify-self: end;
    color:        #000;
    cursor:       pointer;
    padding-left: 12px;
  }
  .pf-row .pf-action:empty { display: none; }

  /* ── Highlight state (first tap): greys all rows except the
     highlighted one so the chosen project reads as focused. ── */
  #portfolioLeft.has-highlight .pf-row:not(.is-highlighted):not(.is-selected),
  #portfolioLeft.has-highlight .pf-row:not(.is-highlighted):not(.is-selected) .pf-num,
  #portfolioLeft.has-highlight .pf-row:not(.is-highlighted):not(.is-selected) .pf-title {
    color: #bbb;
  }

  /* Grey every non-selected row (including ones "above" the selected
     index) once a project is open — only the selected row reads as
     current. (Flex column + order rules live outside this media
     query now so the same pattern applies on desktop.) */
  #portfolioLeft.has-selection .pf-row:not(.is-selected),
  #portfolioLeft.has-selection .pf-row:not(.is-selected) .pf-num,
  #portfolioLeft.has-selection .pf-row:not(.is-selected) .pf-title {
    color: #bbb;
  }

  /* Editorial flow on mobile: short desc → photo → long desc → (no
     inline RFQ — that moved to the bottom pinned bar). Dissolve
     .pf-desc-text with display:contents so its children become
     siblings of .pf-detail-photo in the pf-description flex.
     Everything starts at the row's left edge (same X as the row
     number) — the title still aligns with the metadata value column
     via the row grid, but body text flows from the left margin. */
  .pf-description {
    position:        static;
    display:         flex;
    flex-direction:  column;
    width:           100%;
    /* One line-height between the title and the description — a
       single blank line, enough to read as a paragraph break without
       the double-space feel. */
    margin-top:      1em;
    /* Fade the description + photo + RFQ in once the row collapse
       has settled. Short-list mode (≤ 6 projects) still runs a 0.35s
       FLIP slide-up, but carousel mode pins the title at the same
       Y it had in browse — there's no movement to wait for there.
       0.15s delay splits the difference: in carousel mode it just
       lets the picker-collapse paint first; in short-list mode the
       reveal still starts before the FLIP finishes, which reads
       fine because the slide-up and the fade-in are different axes.
       Animation fires fresh every time .pf-description is inserted
       into the DOM, which happens on every project open. */
    animation:       pf-desc-reveal 0.3s ease 0.15s both;
  }
  @keyframes pf-desc-reveal {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  .pf-description .pf-desc-text    { display: contents; }
  /* No margin-top on the short description — the .pf-description
     container's own margin-top already provides ~one line of gap
     between the row title and the first line of text. */
  .pf-description .pf-desc-short   { order: 1; display: block; width: 100%; margin-top: 0; max-width: none; }
  /* Grey "view more" to match the metadata labels (name, year, …) —
     reads as a secondary affordance, not a primary link. */
  /* Inline at the end of the short-desc paragraph (injected into the
     span in JS). Mobile-only: desktop hides this via the global
     rule (display: none). Grey so it reads as an affordance, not
     body text. */
  .pf-description .pf-info-toggle  { display: inline; opacity: 1; color: #bbb; }
  .pf-description .pf-detail-photo { order: 3; display: block; width: 100%; margin-top: 1em; position: relative; }
  .pf-description .pf-desc-long    { order: 4; display: block; width: 100%; margin-top: 1em; max-width: none; }
  /* RFQ sits between the photo and the pinned metadata — left-aligned,
     editorial-flow position. Pulses via the global .pf-desc-rfq rule. */
  .pf-description .pf-desc-rfq     { order: 5; display: inline-block; align-self: flex-start; margin-top: 1.2em; max-width: none; }

  /* View more / view less: expanding the long description hides the
     photo so the text can take the full screen (HV pattern). Driven
     by the long desc's [hidden] state via :has(). */
  .pf-description:has(.pf-desc-long:not([hidden])) .pf-detail-photo { display: none; }

  /* Photos capped at 55vh (JS), left-aligned. Small margin-top
     nudges the photo below the intro sentence without gluing it
     to the text; margin-bottom leaves room before the RFQ. */
  .pf-description .pf-detail-photo {
    margin-top:    2.5vh;
    margin-bottom: 5vh;
  }

  /* Prev/next arrows are redundant on touch — users swipe. Counter
     sits on a line of its own below the photo, right-aligned. With
     the slider, the frame's height is the same regardless of which
     photo is showing (all slides share the JS-set max-height), so
     a single static layout works for both portrait and landscape —
     no per-orientation absolute positioning needed. */
  .pf-detail-photo .pf-photo-prev,
  .pf-detail-photo .pf-photo-next { display: none; }
  .pf-detail-photo .pf-photo-nav {
    position:    static;
    width:       100% !important;
    margin:      2.5vh 0 0;        /* substantial gap from photo so the counter reads as its own caption line */
    padding:     0;
    background:  transparent;
    color:       #bbb;
    font-size:   var(--table-size);
    line-height: 1.1;
    display:     flex;
    justify-content: flex-end;      /* counter aligns right */
  }
  .pf-detail-photo .pf-photo-counter { opacity: 1; color: inherit; }

  /* First-time swipe hint — sits at the bottom-left of the photo
     block, blinks to draw the eye, fades on first touch. Single
     layout for all orientations now that the slider gives us a
     consistent frame height. */
  .pf-swipe-hint {
    position:       absolute;
    color:          #000;
    font-family:    var(--font-mono);
    font-size:      var(--table-size);
    line-height:    1.1;
    white-space:    nowrap;
    pointer-events: none;
    transition:     opacity 0.4s ease;
    animation:      swipe-hint-blink 1.2s ease-in-out infinite;
  }
  .pf-detail-photo .pf-swipe-hint {
    bottom:    0;
    left:      0;
    right:     auto;
    top:       auto;
    transform: none;
  }
  .pf-swipe-hint.is-fading { opacity: 0; }
  @keyframes swipe-hint-blink {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.2; }
  }

  /* ── About / Contact: body + labels start flush-left with the
     "donato millwork" logo. Label sits in col 1 (100px), value in
     col 2 on the same line. Close pins to the top-right corner —
     more intuitive as a dismiss affordance. ── */
  .pf-about-block,
  .pf-form-row {
    position:              relative;     /* containing block for close */
    grid-template-columns: 100px 1fr;
    column-gap:            var(--pf-gap);
    row-gap:               0.2em;
    align-items:           baseline;     /* label baseline matches value */
  }
  .pf-about-body,
  .pf-form-send { grid-column: 1 / -1; }
  .pf-about-label,
  .pf-form-row .pf-form-label { grid-column: 1; }
  .pf-about-value,
  .pf-form-row .pf-form-input,
  .pf-form-row .pf-form-select,
  .pf-form-row .pf-form-textarea { grid-column: 2; }

  /* Close — top-right of the intro block. Re-anchored from body
     (position: relative on desktop) to the parent block.
     The body reserves padding-right equal to "close" + a char of
     breathing room so text wraps *before* the close button — close
     effectively becomes the paragraph's right margin. */
  .pf-about-body {
    position:      static;
    padding-right: 6ch;
  }
  .pf-inline-close {
    position:     absolute;
    right:        0;
    top:          0;
    left:         auto;
    margin:       0;
    display:      inline-block;
  }

  /* ── Contact page (mobile) ──
     Two tweaks when body.contact-open:
       1. Swap the "about, contact" nav in the top-right for the
          phone number — on the contact page, calling is the obvious
          action, and the nav links are redundant with the big
          contact form sitting right there.
       2. Drop the close button from the intro paragraph's top-right
          down to the bottom of the form, on the same row as the
          send button. Achieved by making .pf-about-block static on
          contact so the absolutely-positioned close bubbles up to
          #contactLeft (.pf-table, position: relative) and sits at
          its bottom-right edge — same y-axis as the send button
          that lives at the end of the form, same horizontal anchor
          (right edge) as it had at the top. ── */
  body.contact-open .nav { display: none; }
  body.contact-open .pf-phone {
    display:      block;
    grid-column:  2;
    grid-row:     1;
    justify-self: end;
  }
  body.contact-open .pf-about-block { position: static; }
  body.contact-open .pf-about-body  { padding-right: 0; }
  body.contact-open .pf-inline-close {
    top:     auto;
    bottom:  0;
    /* Close on the left, send on the right (justify-self: end on
       .pf-form-send below) — the natural reading flow on mobile is
       left-to-right, and the user's thumb path after filling the
       form lands on the right edge. Putting send there makes it the
       obvious, accidental-tap-resistant target. */
    left:    0;
    right:   auto;
    /* Lift above the submit .pf-form-row, which is position: relative
       and later in DOM — without this z-index, the row's full-width
       grid (even its empty right-side cell) paints on top of close
       and swallows the tap. */
    z-index: 10;
  }
  body.contact-open .pf-form-send { justify-self: end; }

  /* About page (mobile) — close button position mirrors contact:
     drops out of the body paragraph's top-right corner and anchors
     to the bottom-right of #aboutLeft (.pf-table) so it sits in a
     consistent place regardless of which page you're on. Same
     reasoning as contact-open above. */
  body.about-open .pf-about-block { position: static; }
  body.about-open .pf-about-body  { padding-right: 0; }
  body.about-open .pf-inline-close {
    top:     auto;
    bottom:  0;
    right:   0;
    z-index: 10;
  }

  /* ── Intro preview on mobile: flash the first 6 projects' hero
     portraits during the stagger reveal, in the same photo position
     users see when a project is actually opened. Anchored by both
     top (below the chrome + 6-row picker) and bottom (clear of the
     fixed metadata bar) so the preview auto-scales to fit between
     them on any viewport height — shorter phones get a shorter
     preview instead of letting it bleed into the metadata below. ── */
  .intro-preview {
    display:   block;
    /* Same container height as original (top + bottom sum = 29em)
       so the photo size is unchanged. Shifted 3em downward by
       raising top 12 → 15em and dropping bottom 17 → 14em. Bottom
       14em keeps a ~3px clearance above the meta top. */
    top:       calc(var(--pad-top) + 2 * var(--chrome-h) + 15em);
    bottom:    14em;
    right:     auto;
    left:      var(--pad-x);
    width:     auto;
    height:    auto;
    max-width: calc(100% - 2 * var(--pad-x));
  }
  .intro-preview-img {
    width:           auto;
    height:          auto;
    max-width:       100%;
    max-height:      100%;
    object-fit:      contain;
    object-position: top left;
  }

  /* ── Intro hint: sits ~2 line-heights below the picker's bottom
     edge on mobile. 8 rows × 1.35em line-height ≈ 10.8em of picker
     content after the 54px chrome clearance; +13em total puts the
     hint roughly two lines below the 8th row. ── */
  .pf-intro-hint {
    top:    calc(var(--pad-top) + 2 * var(--chrome-h) + 13em);
    bottom: auto;
    left:   var(--pad-x);
  }

  /* ── Mobile metadata footer (HV-style) ──
     Pinned to the viewport bottom regardless of page state so it
     never moves when the user flips between photos of different
     aspect ratios or opens/closes a project. Label column matches
     --pf-cols col 1 so the values sit at the same X as project
     titles in the rows. ── */
  .pf-mobile-meta {
    display:               none;
    position:              fixed;
    left:                  var(--pad-x);
    right:                 var(--pad-x);
    bottom:                var(--pad-x, 1em);
    grid-template-columns: 70px 1fr;
    column-gap:            10px;
    row-gap:               0.15em;
    background:            #fff;
    font-family:           var(--font-mono);
    font-size:             var(--table-size);
    line-height:           1.35;
    z-index:               50;
  }
  .pf-mobile-meta.is-visible { display: grid; }
  /* Hide the credit-card on about / contact even when a project
     is technically still selected underneath (e.g., RFQ flow that
     keeps the project state via keepProject: true). The metadata
     is portfolio-scoped — it shouldn't paint over the contact form
     or the about copy. Used to be implicit when the element lived
     inside #page-portfolio (which got hidden on those pages), but
     now that it's hoisted to body level for proper position-fixed
     behaviour, we need an explicit hide rule. */
  body.about-open   .pf-mobile-meta,
  body.contact-open .pf-mobile-meta { display: none; }
  .pf-mobile-meta-label { color: #bbb; }
  .pf-mobile-meta-value { color: #000; }
  /* Hotlinks inside the metadata (client, photographer) render as
     plain text — the global a reset already strips underline and
     inherits color, but re-asserting here keeps the metadata read
     entirely by the label/value color rules. */
  .pf-mobile-meta-value a {
    color:           inherit;
    text-decoration: none;
  }

  /* Reserve bottom space ONLY when the long description is expanded —
     that's the only state where content can grow taller than the
     viewport and need to scroll past the fixed metadata footer. In
     the default state (photo + short desc), 22em of phantom scroll
     creates an awkward "drag down to nothing" experience, especially
     on tall mobile viewports like iPad mini portrait. :has() lets
     us toggle the padding from CSS without touching JS. */
  #page-portfolio:has(.pf-desc-long:not([hidden])) { padding-bottom: 22em; }
}

/* ─── MOBILE METADATA FOOTER ───
   Hidden on desktop. The mobile media query above turns it on and
   lays it out as an HV-style label/value grid beneath the expanded
   photo + description. */
.pf-mobile-meta { display: none; }

/* ─── MOBILE ROW ACTION ("view" / "close") ───
   Right-aligned cell inside each row, mobile-only. Hidden on desktop;
   when empty on mobile it also collapses so the row looks clean. */
.pf-action { display: none; }

/* ─── MESSAGE (info / success) ─── */
.pf-message {
  grid-column: 2 / -1;
  padding:     0.8em 0;
  color:       #000;
  font-family: var(--font-mono);
  font-size:   var(--table-size);
}
