/* ─── Fonts ─────────────────────────────────────────────────── */
@font-face {
  font-family: 'Karrik';
  src: url('fonts/Karrik-Regular.woff2') format('woff2'),
       url('fonts/Karrik-Regular.woff') format('woff');
  font-weight: 400;
  font-style: normal;
}

@font-face {
  font-family: 'Karrik';
  src: url('fonts/Karrik-Italic.woff2') format('woff2'),
       url('fonts/Karrik-Italic.woff') format('woff');
  font-weight: 400;
  font-style: italic;
}

@font-face {
  font-family: 'Switzer';
  src: url('fonts/Switzer-Medium.woff2') format('woff2'),
       url('fonts/Switzer-Medium.ttf') format('truetype');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

/* ─── Reset & Base ─────────────────────────────────────────── */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

:root {
  --bg: #0d0d0d;
  --text: #000000;
  --muted: #000000;
  --accent: #000000;
  --gap: 24px;
  --font: 'Nanum Myeongjo', 'millesime', 'Cormorant Garamond', serif;
  --engraved: 0px 1px 1px rgba(255,255,255,0.12), 0px 2px 4px rgba(0,0,0,0.7);
}

html {
  scroll-behavior: smooth;
  overflow-x: hidden;   /* vertical-only column — nothing pans sideways */
}

/* While a page is open on top of the grid (any project page, About or
   Contact), the grid itself is held still. The lock has to sit on html, not
   on body: html already carries overflow-x above, so a body-level overflow no
   longer reaches the viewport and would do nothing. Added and removed by
   syncPageScrollLock() in script.js, which also puts the scroll position
   back, so the grid is exactly where it was left. */
html.scroll-locked {
  overflow: hidden;
}

body {
  position: relative;   /* anchors the footer, which is out of flow (see below) */
  background-color: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 19px;
  font-weight: 700;
  letter-spacing: 0em;
  min-height: 100vh;
}

a {
  color: inherit;
  text-decoration: none;
}

/* The floor. position: absolute, not fixed — it sits in the document at the
   canvas origin, so the browser scrolls it natively, on the compositor, with
   no JavaScript in the loop. That is what keeps it smooth during fast scrolls:
   a big image moved by a per-frame transform cannot keep up, but one the
   browser scrolls itself is locked to the content by construction.
   Being absolute it *would* enlarge the scrollable area, so script.js sizes it
   to exactly the canvas (never larger) once per layout — see _bgFit().
   background-size: cover then fits the photo over that box; the photo is a
   little bigger than the canvas, so this is a slight downscale and stays sharp
   — it is never stretched up. */
#bg {
  position: absolute;
  top: 0;
  left: 0;
  /* The picture itself is set by _bgFit() in script.js, which picks the light
     web copy — and a smaller one again on a phone. Naming it here as well
     would make the browser fetch the full-size original before the script
     could get a word in. */
  background-size: cover;
  background-position: center top;
  background-repeat: no-repeat;
  z-index: -1;
}

/* ─── Intro Overlay ─────────────────────────────────────────── */
.intro-overlay {
  position: fixed;
  inset: 0;
  z-index: 500;
  background: #cfc9bd;
  cursor: pointer;
  pointer-events: auto; /* clicking anywhere on the overlay enters */
  transition: opacity 1s ease;
  overflow: hidden;
}

/* ─── Landing: floor background + centered Switzer text menu ─── */
.landing-stack {
  position: absolute;
  inset: 0;
  pointer-events: none; /* floor never blocks; only the menu is interactive */
  /* Own stacking context, so the silhouettes' soft-light blend mixes with the
     floor only — never with the overlay's flat backing colour underneath. */
  isolation: isolate;
}

/* ─── Silhouette layer ───
   Two transparent PNGs — the plate and the objects — soft-light blended into
   the floor, matching how they were composited in Photoshop. Their transparent
   areas let the floor through untouched.
   No opacity here on purpose: soft-light already lets the floor's texture read
   through, and the PNGs carry their own 65% level baked in. Dimming further
   flattens the blend out to almost nothing.
   Purely decorative: pointer-events: none so every click reaches the menu
   underneath. Sizes and offsets are in vw/vh so the shapes keep the same relationship to
   the window on any screen. */
.landing-shape {
  position: absolute;
  height: auto;
  mix-blend-mode: soft-light;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* Plate — large, left edge of the window, running off-screen to the left.
   The numbers below size the PNG's *file*, not the plate you see: the
   65-opacity re-export sits inside more empty padding than the file it
   replaced (the disc fills 80% of the frame's width, where before it filled
   100%), so the element is scaled up and re-offset to cancel that padding out.
   The visible disc therefore lands exactly where it did before — 55vw wide,
   left edge at -18.75vw. Re-export with different padding and these need
   recomputing from the new file's content bounds. */
.landing-plate {
  width: 68.376vw;
  left: -25.417vw;
  /* Anchored to the vertical middle of the window, then pushed up by half the
     PNG's own height — so the disc keeps its place as the window resizes. */
  top: calc(50vh - 34.402vw);
}

/* Objects — scattered down the right-hand side and toward the bottom centre.
   Padding-compensated the same way as the plate above. */
.landing-objects {
  width: 84.509vw;
  left: 28.424vw;
  top: calc(50vh - 59.010vw);
}

.landing-floor {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* Centered vertical stack. A faint dark backing keeps it readable over the
   busy floor without hiding it. */
.landing-menu {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%); /* centered both axes */
  display: flex;
  flex-direction: column;
  align-items: center;       /* name + items share one vertical axis */
  gap: 4px;                  /* tight spacing between the three menu items */
  /* The name used to sit inside this column and is now positioned separately
     (see .landing-title). This padding stands in for the space it occupied —
     the column keeps its old height, so being centred it leaves Portfolio /
     About / Contact on exactly the pixels they were on before. */
  padding-top: calc(1.1 * clamp(15px, 5vw, 46px) + 16px);
  pointer-events: auto;
  text-align: center;
  font-family: 'Switzer', system-ui, sans-serif;
}

/* The name and the menu hold one colour from the moment the page loads —
   no intro fade, no shift through colours. Only a menu word under the mouse
   changes, and only then (see .landing-link:hover below). */
.landing-title,
.landing-link {
  color: #B9D3D0;
}

/* The name is positioned against the window, not the nav: top centre, with the
   nav left centred in the middle of the screen below it. */
.landing-title {
  position: absolute;
  top: 6vh;
  left: 50%;
  transform: translateX(-50%);
  font-family: 'Overpass Mono', ui-monospace, monospace;
  font-weight: 400;               /* Overpass Mono Regular */
  /* vw-scaled so the whole name always fits on one line, never wrapping */
  font-size: clamp(15px, 5vw, 46px);
  letter-spacing: 0.04em;
  /* No text-transform: the name reads in title case, as written in index.html */
  line-height: 1.1;
  white-space: nowrap;
}

.landing-link {
  font-family: 'Overpass Mono', ui-monospace, monospace;
  font-weight: 400;               /* Overpass Mono Regular */
  font-size: clamp(23px, 2.2vw, 28px); /* +3pt (4px) again, from 19/24px */
  letter-spacing: 0.03em;
  line-height: 1.2;               /* tight stacking */
  text-transform: none;           /* title case, same as the heading */
  cursor: pointer;
  text-decoration: none;
  text-shadow: none;    /* flat 2D — overrides the inherited nav a shadow */
  transition: color 200ms ease;
  outline: none;
}
/* Only the word under the mouse lightens, and it eases back when the mouse
   leaves. The name is not a link and never changes. */
.landing-link:hover,
.landing-link:focus-visible {
  color: #F2FAF8;
  text-shadow: none;              /* nothing inherited from `nav a:hover` */
}

/* ─── Intro minimap (persists after entering) ───────────────── */
/* Minimap: thumbnails float directly over the site — no panel, no boxes.
   The container is transparent; its padding is an invisible hover/click area. */
#intro-minimap {
  --mm-backing-opacity: 0.3; /* how visible the dark backing is (tune freely) */
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 90;
  background: none;
  pointer-events: auto; /* whole area is hoverable so all thumbs can brighten */
}

/* Flat dark backing, sized in JS to wrap tightly around the thumbnails. */
#intro-minimap .mm-backing {
  position: absolute;
  background: rgba(0, 0, 0, var(--mm-backing-opacity));
  border-radius: 8px;
  pointer-events: none;
}

#intro-minimap .mm-card {
  position: absolute;
  pointer-events: auto; /* thumbnails are clickable */
  cursor: pointer;
}

/* Real object thumbnails. Off-screen ones are dimmed; on-screen ones and
   anything while hovering the minimap read at full strength. */
#intro-minimap .mm-card--img {
  background-size: cover;
  background-position: center;
  opacity: 0.7;
  transition: opacity 150ms ease;
}
#intro-minimap .mm-card--img.in-view { opacity: 1; }
#intro-minimap:hover .mm-card--img { opacity: 1; }

/* Current-position marker: the visible-screen rectangle. Hairline double edge
   (white inner, dark outer) so a truly thin line reads over any thumbnail. */
#intro-minimap .mm-view {
  position: absolute;
  background: none;
  box-shadow:
    0 0 0 0.5px rgba(255, 255, 255, 0.95),
    0 0 0 1px rgba(0, 0, 0, 0.6);
  pointer-events: none;
}

/* ─── Directional edge arrows ───────────────────────────────── */
.edge-arrow {
  position: fixed;
  z-index: 90;
  font-size: 22px;
  line-height: 1;
  color: rgba(0, 0, 0, 0.55);
  text-shadow:
    -1px -1px 1px rgba(255, 255, 255, 0.4),
    1px 2px 4px rgba(0, 0, 0, 0.5);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.4s ease;
}

.edge-arrow.visible { opacity: 1; }

.edge-arrow--up    { top: 16px;    left: 50%; transform: translateX(-50%); }
.edge-arrow--down  { bottom: 16px; left: 50%; transform: translateX(-50%); }
.edge-arrow--left  { left: 16px;   top: 50%;  transform: translateY(-50%); }
.edge-arrow--right { right: 16px;  top: 50%;  transform: translateY(-50%); }

.intro-overlay.hidden {
  opacity: 0;
}

.intro-text {
  position: absolute;
  top: 50%;
  left: 50%;
  pointer-events: all;
  cursor: pointer;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  transition: transform 0.5s ease, filter 0.5s ease;
}

.intro-text:hover {
  transform: translate(-50%, -50%) scale(1.03);
  filter: blur(0.3px);
}

.intro-name {
  font-family: var(--font);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: #000000;
}

.intro-sub {
  font-family: var(--font);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: #000000;
}

/* ─── Fixed corner name (grid → back to menu) ───────────────── */
/* Corner nav on the portfolio page — Contact / About, stacked top-left, on the
   pixels the name used to occupy. */
.grid-corner {
  position: fixed;
  top: 26px;  /* was 20px + 6px box padding — keeps the text where it was */
  left: 33px; /* was 24px + 9px box padding */
  z-index: 110; /* above the grid, below the landing overlay (500) */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
}

.grid-corner-link {
  font-family: 'Overpass Mono', ui-monospace, monospace;
  font-weight: 400;               /* Overpass Mono Regular */
  font-size: 15px;
  /* The generic `nav a` rule uppercases nav links; these read as written. */
  text-transform: none;
  letter-spacing: 0.04em;
  line-height: 1.25;
  color: #ffffff;                 /* navigation reads white over the floor */
  cursor: pointer;
  text-decoration: none;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
  transition: color 100ms ease;
  outline: none;
}
.grid-corner-link:hover,
.grid-corner-link:focus-visible { color: #ffffff; } /* flat, no shift */

/* ─── Header ────────────────────────────────────────────────── */
header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  display: flex;
  justify-content: center; /* menu centered at top */
  align-items: center;
  padding: 24px 32px;
  background: none;
}

.site-name {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

nav {
  display: flex;
  gap: 32px;
}

nav a {
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(160, 170, 165, 0.4);
  text-shadow:
    -1px -1px 1px rgba(255,255,255,0.35),
    1px 2px 4px rgba(0,0,0,0.9);
  transition: color 0.2s ease, text-shadow 0.2s ease;
  cursor: pointer;
}

nav a:hover {
  color: rgba(180, 190, 185, 0.5);
  text-shadow:
    -1px -1px 1px rgba(255,255,255,0.45),
    1px 2px 5px rgba(0,0,0,1);
}

/* ─── Carved menu items (stacked shadow + highlight, like the name) ─── */
.nav-carved {
  --nav-carved-h: 15px; /* display height (2px taller than the old text menu) */
  position: relative;
  display: inline-block;
  height: var(--nav-carved-h);
  line-height: 0;
  cursor: pointer;
}

.nav-carved img {
  display: block;
  height: 100%;
  width: auto;
}

/* highlight overlays the shadow perfectly (same crop, same size).
   Always fully visible; hover just brightens it for a clickable feel. */
.nav-carved .nav-highlight {
  position: absolute;
  top: 0;
  left: 0;
  opacity: 1;
  transition: filter 150ms ease;
}
.nav-carved:hover .nav-highlight { filter: brightness(1.3); }

/* ─── Grid ──────────────────────────────────────────────────── */
main {
  /* No top padding: it offset the column from the document top, and since the
     column clips its objects (overflow: hidden) that offset showed up as a
     hard horizontal line 80px down the window. The only clipping boundaries
     should be the window's own edges. */
  padding-top: 0;
}

.grid {
  columns: 4;
  column-gap: var(--gap);
  padding: 24px 48px;
}

@media (max-width: 1100px) {
  .grid { columns: 3; }
}

@media (max-width: 700px) {
  .grid { columns: 2; }
}

@media (max-width: 400px) {
  .grid { columns: 1; }
}

/* ─── Vertical column mode ──────────────────────────────────────
   script.js measures the objects in the flow layout above, then adds this
   class: the column becomes one screen-wide track that only scrolls down, and
   every object is absolutely placed and driven by transform. Declared after
   the breakpoints so it wins over them. */
/* Measured but not yet placed in the column. Hidden rather than removed, so
   the flow layout the measurements come from is still laid out. */
.grid--measuring {
  visibility: hidden;
}

.grid--column {
  columns: auto;
  column-gap: normal;
  padding: 0;
  position: relative;
  width: 100%;
  overflow: hidden;   /* objects bounce inside the column, never outside it */
}

.grid--column .card {
  position: absolute;
  top: 0;
  left: 0;
  margin: 0;
  transform-origin: top left;
  /* Promoted to its own compositor layer so script.js's sub-pixel translate3d
     is rendered smoothly instead of snapping to whole pixels. */
  will-change: transform;
  backface-visibility: hidden;
}

/* ─── Section Spacer ────────────────────────────────────────── */
.section-spacer {
  column-span: all;
  height: 80px;
}

/* ─── Project Card ──────────────────────────────────────────── */
.card--custom-width {
  width: var(--card-w) !important;
}
.card--custom-width img {
  width: 100%;
  height: auto;
}
.card--custom-height {
  overflow: visible !important;
}
.card--custom-height img {
  height: var(--card-h) !important;
  width: auto !important;
}
.card--push-down {
  margin-top: 20cm;
}
.card--push-right {
  column-span: all;
  margin-left: auto;
}
.card--right-center {
  column-span: all;
}
.card--full-width {
  column-span: all;
  width: 100%;
}
.card-column-group {
  break-inside: avoid;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
}
.card--portrait {
  overflow: visible !important;
}
/* Two different jobs were being done by one `overflow: hidden`, and they are
   not the same job. The column still clips objects at its walls — that is what
   makes something hide off the page with a sliver showing. But the card's own
   box must not clip, or the hover zoom is sheared off at the picture's edge.
   Hiding at the walls: .grid--column. Growing on hover: here. */
.card.card--unclipped {
  overflow: visible;   /* two classes, so it beats .card's own overflow rule */
}

/* Objects that turn in place. The rotation lives here, not on the card (whose
   transform is its position on the floor) and not on the image (whose
   transform is the hover nudge). */
.card-spin {
  width: 100%;
  height: 100%;
  transform-origin: 50% 50%;
  will-change: transform;
}

.card-layers {
  position: relative;
}
.card-layers img {
  width: 100%;
  height: auto;
  display: block;
}
.card-layers img:not(:first-child) {
  position: absolute;
  top: 0;
  left: 0;
}
.card--featured {
  column-span: all;
  width: 80%;
  margin: 0 0 var(--gap) auto;
}
.card {
  position: relative;
  overflow: hidden;
  cursor: default;
  background-color: transparent;
  margin-bottom: var(--gap);
  break-inside: avoid;
}

.card--on-object {
  cursor: pointer;
}

.card--span-all {
  column-span: all;
}

.card img {
  width: 100%;
  height: auto;
  display: block;
  transition: transform 0.5s ease, filter 0.5s ease;
  filter: brightness(0.92);
}

.card--on-object img {
  transform: scale(1.03);
  filter: brightness(0.6);
}

.card-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 24px 20px;
  opacity: 0;
  transition: opacity 0.3s ease;
}

.card--on-object .card-overlay {
  opacity: 1;
}

.card-title {
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 4px;
}

.card-meta {
  font-size: 11px;
  font-weight: 400;
  color: var(--accent);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* ─── Gallery Modal ──────────────────────────────────────────── */
.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(228, 220, 205, 0.45);
  backdrop-filter: blur(6px);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}

.modal-backdrop.active {
  opacity: 1;
  pointer-events: all;
}

/* ─── Gallery Container (vertically scrollable) ─────────────── */
.gallery-container {
  width: 100%;
  height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  /* Reaching the top or the bottom of a project page ends there: the scroll
     is not passed on to the grid behind it. */
  overscroll-behavior: contain;
  /* No scroll snapping in the project view — the reader stops wherever they
     like, rather than being pulled onto a section boundary. */
  scroll-snap-type: none;
}

.gallery-container.vertical-mode {
  scroll-snap-type: none;
}

.gallery-container.vertical-mode .gallery-strip-section {
  height: auto;
  min-height: unset;
  overflow: visible;
}

.gallery-container::-webkit-scrollbar { display: none; }

/* ─── Book view ──────────────────────────────────────────────── */
/* Used by "What it means to be handled" alone (project.bookView). The whole
   page scrolls as one, under the "other projects" link, which floats in the
   top-left corner with nothing behind it.

   The name and the year/exhibition line sit in normal flow underneath the
   images here, rather than lying over the bottom of them. */
.modal-backdrop.book-view .gallery-heading {
  position: static;
  padding: 24px 48px 0;
  align-self: stretch;
}

/* ─── Section 1: Image Strip ─────────────────────────────────── */
.gallery-strip-section {
  width: 100%;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
}

/* Name + year/exhibition, in the empty band under the images. Absolutely
   positioned on purpose: the strip stays the section's only in-flow child, so
   the images keep the exact position and size they had. */
.gallery-heading {
  position: absolute;
  left: 48px;
  right: 48px;
  bottom: 14px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  pointer-events: none;   /* never blocks dragging the image strip */
}

.gallery-strip--below {
  height: 40vh !important;
  margin-top: 12px;
}

.gallery-strip {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 20px;
  overflow-x: auto;
  overflow-y: hidden;
  width: 100%;
  height: 80vh;
  padding: 0 48px;
  scroll-snap-type: none;
  cursor: grab;
}

.gallery-strip:active { cursor: grabbing; }

.gallery-strip.vertical {
  flex-direction: column;
  height: auto;
  overflow: visible;
  scroll-snap-type: none;
  cursor: default;
  padding: 0;
  gap: 0;
}

.gallery-strip.vertical > img {
  width: 100%;
  height: auto;
  scroll-snap-align: none;
}

.gallery-pair {
  display: flex;
  flex-direction: row;
  width: 100%;
}

.gallery-pair img {
  width: 50% !important;
  height: auto;
  scroll-snap-align: none;
}

.gallery-h-strip {
  display: flex;
  flex-direction: row;
  width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  cursor: grab;
  margin: 1mm 0;
  gap: 2mm;
}

.gallery-h-strip:active { cursor: grabbing; }
.gallery-h-strip::-webkit-scrollbar { display: none; }

.gallery-h-strip img {
  width: auto !important;
  height: 80vh;
  flex-shrink: 0;
  object-fit: contain;
}

.gallery-strip::-webkit-scrollbar { display: none; }

.gallery-photo-credit {
  font-size: 11px;
  color: var(--muted);
  font-style: italic;
  margin-top: 0.5em;
}

.gallery-figure {
  display: flex;
  flex-direction: column;
  height: 100%;
  flex-shrink: 0;
}

.gallery-figure figcaption {
  padding-top: 8px;
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--muted);
  opacity: 0.45;
  font-weight: 300;
  white-space: nowrap;
  flex-shrink: 0;
}

.gallery-strip:not(.vertical) video {
  height: 100%;
  width: auto;
  flex-shrink: 0;
  display: block;
}

.gallery-strip:not(.vertical) img.gallery-img--half {
  height: 50%;
  align-self: center;
}

/* A couple of images on a page can be set a little smaller than the rest.
   They are sized by height, exactly as every other image in the strip is, so
   they keep their own proportions and need no box around them — 90% of the
   height the others get. Two pictures of the same shape therefore come out
   the same size as each other. They sit centred against their neighbours. */
.gallery-strip:not(.vertical) img.gallery-img--narrow {
  height: 63%;            /* 70% of the 90% these two were shown at */
  align-self: center;
}

/* One image can be set further from the one before it than the rest of the
   strip. The row's own gap still applies, so this is the extra on top: 20px
   of gap + 40px here = 60px, three times every other gap. */
.gallery-strip:not(.vertical) img.gallery-img--gap-before {
  margin-left: 40px;
}

.gallery-strip:not(.vertical) img {
  height: 100%;
  width: auto;
  object-fit: contain;
  flex-shrink: 0;
  display: block;
}

.gallery-strip .gallery-figure img {
  height: calc(80vh - 1.8em);
  width: auto;
  object-fit: contain;
  flex-shrink: 0;
  display: block;
}

.gallery-strip-title {
  position: absolute;
  bottom: -0.5em;
  left: 50%;
  transform: translateX(-50%);
  font-size: 11px;
  font-weight: 300;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text);
  opacity: 0.35;
  white-space: nowrap;
  pointer-events: none;
}

.gallery-scroll-hint {
  display: none;
}

.scroll-hint {
  position: fixed;
  color: #c2bdb6;
  font-size: 20px;
  opacity: 0.7;
  pointer-events: none;
  z-index: 300;
  transition: opacity 0.6s ease;
}

.scroll-hint.vertical {
  bottom: 32px;
  left: 50%;
  transform: translateX(-50%);
  animation: bounce-down 2s ease-in-out infinite;
}

.scroll-hint.horizontal {
  right: 32px;
  top: 50%;
  transform: translateY(-50%);
  animation: bounce-right 2s ease-in-out infinite;
}

.scroll-hint.hidden {
  opacity: 0;
}

@keyframes bounce-down {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50% { transform: translateX(-50%) translateY(7px); }
}

@keyframes bounce-right {
  0%, 100% { transform: translateY(-50%) translateX(0); }
  50% { transform: translateY(-50%) translateX(7px); }
}

/* ─── Section 2: Text Info ───────────────────────────────────── */
.gallery-text-section {
  width: 100%;
  /* Only as tall as its own text — it used to be forced to a full 100vh, which
     made it a separate screen. Now that it ends where the words end, scrolling
     to the bottom leaves the images above still in view, with the text sitting
     in the space beneath them. */
  min-height: auto;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: flex-start;      /* bottom-left: hard against the left edge */
  /* 48px left matches the image strip's own left inset, so the text lines up
     with the first image rather than floating in from 10vw. No negative
     margins anywhere — the block sits in normal flow, below the images. */
  padding: 32px 48px 48px;
  gap: 24px;
}

.gallery-artist {
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
}

.gallery-title {
  font-family: 'Overpass Mono', ui-monospace, monospace;
  font-size: 28px;
  font-weight: 400;               /* Overpass Mono Regular */
  letter-spacing: 0em;
  line-height: 1.3;
  color: #ffffff;
}

.gallery-meta {
  font-family: 'Overpass Mono', ui-monospace, monospace;
  font-weight: 400;               /* Overpass Mono Regular */
  font-size: 13px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: #ffffff;
}

.gallery-material {
  font-family: 'Overpass Mono', ui-monospace, monospace;
  font-weight: 400;               /* Overpass Mono Regular */
  font-size: 14px;
  letter-spacing: 0em;
  color: #ffffff;
}

.gallery-desc {
  font-size: 16px;
  line-height: 1.8;
  color: #ffffff;
  max-width: 600px;
  font-weight: 700;
  /* was 0.8 — that faded the colour out; full opacity so the white is true */
  opacity: 1;
}

/* The book page's first line. On a computer it reads as one sentence, with
   the comma shown and the line break switched off; on a phone the switch is
   thrown the other way (see the phone rules at the end of this file), so the
   date drops onto its own line and the comma disappears. Neither element
   exists anywhere else, so this changes nothing on any other page. */
br.desc-break {
  display: none;
}

/* The Estonian description block. It carried a border-top that separated it
   from the English text above, but every project's descriptionET is empty, so
   the rule only ever showed as a stray line under the description — removed. */
#gallery-desc-et {
  padding-top: 24px;
}

.gallery-sale-wrap {
  position: relative;
  display: inline-flex;
  height: 100%;
  flex-shrink: 0;
}

.gallery-sale-wrap img {
  transition: filter 0.3s ease;
}

.gallery-sale-wrap:hover img {
  filter: brightness(0.5);
}

.gallery-sale-tag {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.8rem;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: #fff;
  opacity: 0;
  transition: opacity 0.3s ease;
  pointer-events: none;
}

.gallery-sale-wrap:hover .gallery-sale-tag {
  opacity: 1;
}

/* hidden, kept for JS compatibility */
.gallery-thumbs { display: none; }
.gallery-counter { display: none; }

.modal-close {
  position: fixed;
  top: 28px;                      /* top-left corner of the window */
  left: 32px;
  background: none;
  border: none;
  color: #ffffff;
  font-family: 'Overpass Mono', ui-monospace, monospace;
  font-weight: 400;               /* Overpass Mono Regular */
  font-size: 15px;
  letter-spacing: 0.02em;
  cursor: pointer;
  line-height: 1;
  text-shadow: none;
  filter: none;
  transition: color 0.2s ease;
  z-index: 2000;
  transform: translateZ(0);
  isolation: isolate;
  padding: 8px;
}

.modal-close:hover {
  color: #ffffff;
  text-shadow: none;
}

/* ─── About Modal ───────────────────────────────────────────── */
.about-backdrop {
  position: fixed;
  inset: 0;
  z-index: 600; /* above the landing overlay (500) so links reach it */
  background: rgba(228, 220, 205, 0.45);
  backdrop-filter: blur(6px);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}

.about-backdrop.active {
  opacity: 1;
  pointer-events: all;
}

.about-content {
  max-width: 620px;
  width: 90%;
  padding: 60px 16px;
  overflow-y: auto;
  /* As above: the ends of About and Contact do not scroll the grid behind. */
  overscroll-behavior: contain;
  max-height: 90vh;
  font-size: 17px;
  font-weight: 700;
  line-height: 1.8;
}

.about-content::-webkit-scrollbar { display: none; }

.about-photo-block {
  width: 100%;
  margin-top: 64px;
  margin-left: auto;
  margin-right: auto;
}

.about-photo {
  width: 100%;
  height: auto;
  display: block;
}

.about-photo-credit {
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--muted);
  opacity: 0.45;
  margin-top: 8px;
  font-weight: 300;
  white-space: nowrap;
}

.about-description p {
  font-size: 17px;
  line-height: 1.8;
  color: #ffffff;
  font-weight: 700;
  letter-spacing: 0em;
}


.cv-contact {
  margin-bottom: 40px;
}

.cv-contact p {
  font-size: 17px;
  letter-spacing: 0em;
  color: #ffffff;
  line-height: 1.8;
  font-weight: 700;
}

.cv-content {
  max-width: 860px !important;
}

.cv-section {
  margin-bottom: 44px;
}

.cv-divider {
  margin: 16px 0;
}

.cv-section h3 {
  font-size: 13px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  font-weight: 800;
  margin-bottom: 14px;
  color: #ffffff;
}

.cv-section p {
  font-size: 17px;
  line-height: 1.8;
  color: #ffffff;
  font-weight: 700;
  /* Each entry is two lines now — its name, then where and when. This gap
     sets one entry apart from the next, so the pair reads as a single item. */
  margin-bottom: 14px;
}

.cv-link {
  cursor: pointer;
  /* The entry runs over two lines now. Left inline, the link's box spans both
     of them and its middle falls in the gap between, where a tap misses it.
     As a block it fills the whole entry, so anywhere on either line works.
     The text flows exactly as before — it is the only thing in its paragraph. */
  display: block;
}

.cv-link-underline {
  border-bottom: 1px solid rgba(242, 237, 230, 0.3);
  transition: border-color 0.2s ease;
}

.cv-link:hover .cv-link-underline {
  border-bottom-color: rgba(242, 237, 230, 0.8);
}

/* ─── Footer ────────────────────────────────────────────────── */
footer {
  /* Out of flow: in flow it sat below the column and added ~94px to the page,
     so the column's clip edge stopped short of the document bottom and cut
     objects along an internal line. Absolute keeps it at the very bottom
     without contributing height, letting the column span the full page. */
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 48px 32px 32px;
  text-align: center;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #444;
}

/* Everything below is phone-only and lives at the end of the file on
   purpose: these rules have the same weight as the ones they adjust, so
   they have to come after them to take effect. */

/* ─── Phone: the landing fills the whole screen ──────────────── */
/* Phones only. Nothing in here applies at 768px or wider, where the landing
   is left exactly as it was.

   Three things were leaving pale edges on a phone. The overlay's own backing
   colour (#cfc9bd) shows anywhere the floor photograph does not reach, so the
   photograph is made to cover the screen outright, cropping at the edges
   rather than leaving a gap. 100vh on a phone is the screen *without* the
   browser's bars, so the stack stopped short of them; 100dvh fills the screen
   as it actually is at that moment. And the drift column below is laid out
   1440px wide (see _fitColumnToScreen in script.js), so its box is clipped
   here to stop a phone browser treating the page as wider than the screen and
   shrinking everything into a strip. */
@media (max-width: 767px) {
  /* The page itself must not scroll at all — only the map does.
     body carries min-height: 100vh, and on a phone 100vh means the screen as
     it would be with the browser's bars hidden, which is taller than what you
     can actually see while they are showing. That left the page a few dozen
     pixels of scroll of its own, and a swipe went into collapsing the address
     bar instead of moving the map: the moment that ran out, scrolling stopped
     dead and the composition looked like it ended there. Pinning the page to
     the real height of the screen leaves the gesture nowhere to go but the
     map. */
  html, body {
    max-width: 100%;
    height: 100%;
    min-height: 100%;
    overflow: hidden;
  }

  /* The composition is drawn larger than the screen and panned around inside
     its own window, like a map. Making it a scrolling area of its own is what
     keeps the page itself from ever becoming wider than the screen — the thing
     that put pale strips down the sides of the home page. */
  #phone-stage {
    position: fixed;
    inset: 0;
    /* A fixed box is measured against the screen with the bars hidden, so its
       foot sat behind the address bar and the last stretch of the composition
       could not be scrolled into view. dvh is the screen as it actually is at
       that moment, bars and all. */
    height: 100vh;
    height: 100dvh;
    overflow: auto;
    overscroll-behavior: contain;        /* panning stops at the edges */
    touch-action: pan-x pan-y;           /* a swipe pans, in both directions */
    z-index: 0;                          /* under every control and overlay */
  }

  /* The picture inside that window: as big as the shrunk composition, which is
     what there is to scroll around in. */
  #phone-canvas {
    position: relative;
  }

  /* With a project, About or Contact open, the map behind is held still in
     both directions, not just up and down. */
  html.scroll-locked #phone-stage {
    overflow: hidden;
    touch-action: none;   /* a finger cannot pan it either */
  }

  main {
    overflow: hidden;
  }

  .intro-overlay,
  .landing-stack {
    width: 100vw;
    height: 100vh;      /* for anything without dvh */
    height: 100dvh;
  }

  /* Cover the screen whole: enlarge and crop at the edges, never letterbox. */
  .landing-floor {
    width: 100vw;
    height: 100vh;
    height: 100dvh;
    object-fit: cover;
    object-position: center;
  }

  /* The two faded silhouettes are drawn twice the size on a phone. Scaling
     them about their own centres keeps each one exactly where it is and lets
     it grow outwards, with whatever passes the edge of the screen cropped.
     Nothing else moves: the floor, the name and the menu are untouched. */
  .landing-plate,
  .landing-objects {
    transform: scale(2);
    transform-origin: center;
  }

  /* The book page's date on its own line, with no comma before it. */
  .desc-comma {
    display: none;
  }
  br.desc-break {
    display: inline;
  }

  /* ── Photo credits all one size ──────────────────────────────
     Every credit is written at 11px, but a phone browser does not always draw
     it at 11px. Left to itself, Safari inflates text that sits inside a block
     wider than the screen, and the more the block overhangs the more it
     inflates — so a credit under a wide photograph (962px here) came out
     noticeably bigger than one under a narrow photograph (427px), even though
     both say 11px. Pinning the adjustment to 100% makes every browser draw
     the size that is written, which is the smaller one. */
  .gallery-figure figcaption,
  .gallery-photo-credit,
  .about-photo-credit {
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
    font-size: 11px;
  }

  /* ── Buttons clear of the phone's own furniture ──────────────
     viewport-fit=cover (see index.html) lets the page paint all the way into
     the corners, which also means underneath the status bar and the notch.
     Anything sitting 26-28px from the top then lands inside that band, where a
     tap reaches the phone, not the page — which is why the close button did
     nothing. These two offsets push the controls below whatever the phone
     reserves, and are zero on a phone with nothing to reserve.
     The extra padding gives each control a finger-sized target, pulled back by
     an equal negative margin so the text stays on the pixel it was on.
     touch-action: manipulation drops the browser's wait for a second tap, so a
     single tap acts at once. */
  :root {
    --safe-top: env(safe-area-inset-top, 0px);
    --safe-left: env(safe-area-inset-left, 0px);
    --safe-bottom: env(safe-area-inset-bottom, 0px);
  }

  .modal-close {
    top: calc(28px + var(--safe-top));
    left: calc(32px + var(--safe-left));
    padding: 16px;
    margin: -8px 0 0 -8px;
    touch-action: manipulation;
  }

  .grid-corner {
    top: calc(26px + var(--safe-top));
    left: calc(33px + var(--safe-left));
  }

  .grid-corner-link,
  .landing-link,
  .cv-link {
    touch-action: manipulation;
  }

  /* Finger-sized rows for the corner and landing links, without moving the
     text: the padding grows the hit area, the margin takes the space back. */
  .grid-corner-link {
    padding: 10px 16px;
    margin: -10px -16px;
  }

  /* About and Contact start below whatever the phone reserves at the top, so
     the close button that just moved down is never sitting on the first line
     of the text, and the last line clears the home indicator at the bottom. */
  .about-content {
    padding-top: calc(60px + var(--safe-top));
    padding-bottom: calc(60px + var(--safe-bottom));
  }
}
