/* =========================================================
   Lajos Tamás Jakab — Portfolio
   Landing page styles
   Design source: Figma "Portfolio_website" — Landing page (node 199:523)
   =========================================================

   BREAKPOINTS — the one set every stylesheet in this project uses.
   Declared here because style.css loads first on every page.

     desktop   > 900px    full design layout
     tablet   <= 900px    multi-column grids collapse (4->2, 2->1), the
                          nav becomes the hamburger overlay
     mobile   <= 600px    everything is a single column, --gutter stops
                          scaling and pins to 24px, type steps down

   These two numbers are it. style.css, project.css and about.css all
   break at exactly 900px and 600px, so the nav, the landing grids and
   the project grids always agree on where "tablet" and "mobile" begin.
   (Before this was consolidated the three files broke at 1023/900/767,
   1360/900/560 and 900/560 respectively, which is why at ~780px the nav
   was still in its desktop form while project grids had already
   collapsed to 2 columns.)

   COMPONENT-SPECIFIC EXCEPTIONS — a component may add ONE extra
   breakpoint of its own when a real measured constraint forces it, but
   it must be commented with the reason at the @media rule. Current
   exceptions, both at 1360px (the viewport width at which the 1194px
   container finally reaches its full design width, so both are really
   "the design width no longer fits"):
     - project.css  .ia-row      — 3 fixed-width boxes stop fitting
     - about.css    .about-grid  — the 4-col photo grid's text column
                                   drops under ~274px and stops reading

   CONTAINER — one shared centered measure, --container-max (1194px),
   used by .site-header, .work-section, .contact-section, .wheel-stage,
   .project-container and .about-container so every page's content sits
   on the same left and right edge. Two ways to apply it, both landing
   on an identical (100% - 1194px) / 2 inset once the viewport is wide
   enough:
     - block containers:  max-width: calc(var(--container-max) + 2 * var(--gutter));
                          margin-inline: auto; padding-inline: var(--gutter);
     - full-bleed bars:   padding-inline: max(var(--gutter), calc((100% - var(--container-max)) / 2));
   The second form is for elements whose own background must still span
   the full viewport (the sticky header), where a max-width would cut
   the background short.
   ========================================================= */

:root {
  --color-bg: #1a1818;
  --color-text: #ede5e5;
  --color-white: #ffffff;
  --color-icon-bg: #ede5e5;
  --color-icon-fg: #1a1818;

  --font-main: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  /* 1194px, not the 1192px this used to say: 1194 is the number the
     project and about pages were already hardcoding as a literal, and
     the landing grids divide into it exactly (4 columns + 3x32px gaps
     = 4 x 274.5px, the design's 274px card). The old 1192 token was
     referenced by nothing at all, so nothing depended on it. */
  --container-max: 1194px;
  --gutter: clamp(24px, 6vw, 124px);

  --radius-lg: 16px;
  --radius-pill: 999px;
}

* { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
}

/* Gentle opening animation: a quick, subtle fade on every page load so a
   project page (reached from the WORK grid or a SPIN wheel card) settles
   in instead of just popping into view. Kept short and understated on
   purpose — this should read as the page "arriving", not as a showy
   transition.
   opacity-only, deliberately: an earlier version also animated
   `transform: translateY(...)` on body. Any element with a computed
   transform other than `none` — even mid-animation, even a momentary
   no-op one — becomes the containing block for its position:fixed
   descendants, so for the animation's full 0.45s .nav-menu's fixed
   inset:0 resolved against body's animated box instead of the viewport
   (offsetParent became BODY, height ~4210px instead of 900px, and the
   page gained real horizontal scroll). Dropping the fill-mode only fixed
   the state *after* the animation finished — the transform was still
   live for the 450ms the animation was running, and on a backgrounded
   tab (rAF throttled) it can sit part-way through indefinitely. Animating
   opacity only means body's transform is `none` for the animation's
   entire lifetime, so this containing-block bug can't happen at all. */
@keyframes page-enter {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

body {
  animation: page-enter 0.45s ease-out;
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-main);
  font-weight: 500;
  line-height: normal;
  -webkit-font-smoothing: antialiased;
}

/* Reduced motion is honoured across the whole site, not just the page
   fade. The four things that actually move here:
     - body's page-enter fade (below)
     - .spin-arrow's `bob`, which loops forever — the worst offender,
       since an infinite animation never stops asking for attention
     - html's smooth scrolling, which turns every anchor jump and every
       "back to top" into a long animated travel
     - the SPIN wheel's 3.2s spin with motion blur, and the three
       Mercedes Aura videos: both live in main.js, which reads the same
       query (the wheel jumps straight to its landed state; the videos
       never autoplay or even download). */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  body {
    animation: none;
  }

  .spin-arrow {
    animation: none;
  }
}

img { max-width: 100%; display: block; }

/* Every paragraph's spacing is set deliberately via an explicit `gap`
   on its flex container — the browser's default ~1em p margin doesn't
   collapse inside a flex container the way it would in normal block
   flow, so left alone it stacks on top of that gap and produces
   inconsistent spacing between adjacent text blocks. */
p { margin: 0; }

/* =========================
   Line breaking
   -------------------------
   The browser's default greedy wrap fills each line to the brim and dumps
   whatever is left on the last one, which is what produced the stranded
   single words all over the site — "flow.", "them", "software.", "(P1)" —
   and first lines noticeably shorter than the ones under them.

   Two different tools, because they solve different halves of it:

     text-wrap: pretty   looks ahead so the last line is never left with a
                         lone word. Correct for running copy: it only
                         adjusts the tail, so long paragraphs keep their
                         even colour.
     text-wrap: balance  evens the length of every line in the block. Right
                         for short display text, wrong for long copy — and
                         it self-limits anyway, since browsers stop
                         balancing past about six lines.

   Headings and short labels get `balance`; running copy gets `pretty`.
   The narrow 274px columns override to `balance` in project.css — see the
   note there for why `pretty` cannot help at that measure.

   Chosen over nudging letter-spacing to force better breaks: a spacing
   tweak only holds at the one width it was tuned for, and every other
   viewport re-wraps somewhere else. These two properties re-solve the
   break at every width instead. Both degrade to the old greedy wrap on a
   browser that lacks them, so nothing breaks. */
h1, h2, h3 {
  text-wrap: balance;
}

p, li {
  text-wrap: pretty;
}

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

button {
  font-family: inherit;
  cursor: pointer;
}

section {
  padding-inline: var(--gutter);
}

.section-title {
  font-weight: 500;
  font-size: clamp(48px, 9vw, 128px);
  color: var(--color-white);
  margin: 0;
  line-height: 1;
  word-break: break-word;
}

/* =========================
   Skip link
   -------------------------
   First focusable element on the page, so a keyboard or screen-reader
   user can jump past the header/nav straight to the content instead of
   tabbing through it on every page.

   Hidden with the clip-path technique rather than display:none or
   visibility:hidden — both of those would also remove it from the focus
   order, which defeats the point. On :focus it switches to position:
   fixed so it lands in the same top-left spot whatever the scroll
   position, above the sticky header's z-index:30. No transition: it must
   appear instantly on focus, and animating it in would be one more thing
   to have to suppress for reduced motion.
   ========================= */

.skip-link {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link:focus {
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: 100;
  width: auto;
  height: auto;
  margin: 0;
  padding: 12px 20px;
  overflow: visible;
  clip-path: none;
  border-radius: var(--radius-pill);
  background: var(--color-icon-bg);
  color: var(--color-bg);
  font-size: 16px;
  font-weight: 500;
  text-decoration: none;
}

/* The landed SPIN card receives focus when the wheel stops (see
   main.js), so it needs a focus ring that reads on top of a photo —
   the brightness bump the cards already carry for hover is far too
   subtle to serve as a keyboard focus indicator on its own. */
.wheel-card:focus-visible {
  outline: 3px solid var(--color-white);
  outline-offset: 4px;
}

/* =========================
   Header
   ========================= */

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding-block: 24px;
  /* Full-bleed form of the shared container (see the CONTAINER note at
     the top of this file). The header's own background has to reach both
     viewport edges — it's the sticky bar everything scrolls under — so
     it can't take a max-width; instead the padding grows past --gutter
     once the viewport exceeds the 1194px measure, landing the logo and
     the nav on exactly the same left/right edge as the page content
     below. Previously this was a flat `var(--gutter)`, which at 1920px
     put the logo at x=115 while about/project content started at x=355 —
     the nav sat 240px to the left of the content it belongs to. */
  padding-inline: max(var(--gutter), calc((100% - var(--container-max)) / 2));
  position: sticky;
  top: 0;
  z-index: 30;
  background: var(--color-bg);
}

/* Anchor-jump targets that land directly under the sticky header (WORK,
   CONTACT — reached via #work/#contact from any page, including a
   cross-page link from about.html) need breathing room so their heading
   doesn't sit flush against the header edge. scroll-margin-top offsets
   the landing position by the header's own height plus a little extra
   air, for both the smooth scroll-behavior animation and a plain
   same-page hash jump. The header's real height is set as a CSS
   variable by JS (site.js) since it varies with the logo/gutter clamp
   sizes across breakpoints. */
#work,
#contact {
  scroll-margin-top: calc(var(--header-height, 88px) + 24px);
}

/* A project page's CLOSE pill returns to the exact card the user opened
   (id="work-<slug>", see main.js), not just the top of #work — same
   clearance as the section anchors above so the card doesn't land flush
   under the sticky header. */
.project-card {
  scroll-margin-top: calc(var(--header-height, 88px) + 24px);
}

.logo {
  margin: 0;
  font-size: clamp(20px, 2.2vw, 32px);
  font-weight: 500;
  white-space: nowrap;
  transition: opacity 0.2s ease;
}

.logo:hover,
.logo:focus-visible {
  opacity: 0.7;
}

.nav-menu {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 32px;
  font-size: 20px;
}

.nav-link {
  white-space: nowrap;
  transition: opacity 0.2s ease;
}

.nav-link:hover,
.nav-link:focus-visible {
  opacity: 0.7;
}

.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 5px;
  width: 32px;
  height: 32px;
  background: none;
  border: none;
  padding: 0;
  z-index: 20;
}

.nav-toggle span {
  display: block;
  width: 24px;
  height: 2px;
  background: var(--color-text);
  transition: transform 0.25s ease, opacity 0.25s ease;
}

/* =========================
   SPIN section
   ========================= */

.spin-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 32px;
  padding-top: clamp(24px, 4vw, 40px);
  padding-bottom: 0;
  overflow: hidden;
}

/* SPIN / subtitle / arrow, 32px apart — measured between the glyphs, not
   between the boxes.

   A line box is always taller than the ink inside it, and by a different
   amount for each of these three, so a flat `gap` that looks right on
   paper does not look even on screen. Measured in Plus Jakarta Sans at
   1280px: SPIN's box carries 9.5px of empty space below its cap line
   (line-height:1 sits well inside this font's 1.26em natural line), and
   the subtitle's carries 6.5px above its ascenders. The arrow needs no
   correction — its stroke, round caps included, fills its 16x16 viewBox
   edge to edge, so its box IS its ink.

   So each text element gives back its own dead space as a negative
   margin, and then the single 32px gap is true for all three. The trims
   are ratios of the element's own font-size (9.5/128, 6.5/20), so they
   hold at every clamp step and at the smaller mobile subtitle size
   rather than only at the width they were measured on. --spin-size
   exists so the SPIN trim and SPIN's font-size cannot drift apart.

   .spin-section's own 32px gap already carries this on to the wheel. */
.spin-info {
  --spin-size: clamp(56px, 12vw, 128px);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 32px;
  text-align: center;
}

.spin-title-wrap {
  margin: 0;
  line-height: 1;
  margin-bottom: calc(-0.074 * var(--spin-size));
}

.spin-title {
  margin: 0;
  padding: 0;
  background: none;
  border: none;
  color: inherit;
  font-family: inherit;
  font-weight: 500;
  font-size: var(--spin-size);
  line-height: 1;
  cursor: pointer;
}

.spin-title:disabled {
  opacity: 0.6;
  cursor: default;
}

/* Exactly one line tall, always — the subtitle swaps between a short idle
   label and "opening <project name>…", and the longer project names wrap
   to two lines, which must not shove the wheel down mid-message.

   An earlier version reserved two lines (min-height: 2.8em). That held
   the wheel still, but it also meant the one-line resting state — what
   the page shows almost all of the time — carried a whole empty line
   below the text, which is what made the gap down to the arrow read so
   much wider than the gap up to SPIN.

   Fixing the height at one line and centring the content instead gets
   both: nothing below ever moves, and a second line grows symmetrically
   into the 32px gaps above and below (14px into each, so it never
   touches SPIN or the arrow) rather than pushing them apart.
   align-content, not place-content: the latter would also centre the
   column track, which sizes it to max-content and stops the text
   wrapping at all. line-height is pinned rather than left at `normal`
   so `1.4em` is exactly one line — `normal` varies by font and platform. */
.spin-subtitle {
  margin: 0;
  font-size: 20px;
  white-space: pre-wrap;
  line-height: 1.4;
  height: 1.4em;
  display: grid;
  align-content: center;
  /* Own dead space handed back, per the note on .spin-info above: 6.5px
     above the ascenders and 0.5px below the descenders, at 20px. */
  margin-top: -0.325em;
  margin-bottom: -0.025em;
}

.spin-arrow {
  display: flex;
  color: var(--color-text);
  animation: bob 2s ease-in-out infinite;
}

@keyframes bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(6px); }
}

/* The container bleeds edge-to-edge so there's always dead space around
   the design-width stage for a rotated card's corners to swing into
   before the true clip boundary — the stage below keeps the original
   1191px composition untouched. Bleeding via a percentage of the
   section's own box (not 100vw) keeps it centred regardless of
   scrollbar width, which 100vw does not account for. */
.wheel-container {
  position: relative;
  /* Establishes its own stacking context (z-index needs a value, not
     auto, to do this) so the wheel cards' inline z-index — main.js sets
     each card's z-index up to 1000 to layer them front-to-back around
     the wheel — stays scoped inside this container instead of being
     compared directly against .site-header's z-index:30 at the page's
     root stacking context. Without this, a front-facing card (z-index
     near 1000) paints straight over the sticky header, which is why the
     header looked "unfilled"/see-through above the SPIN section: the
     opaque background was there, a card was just drawn on top of it. */
  z-index: 1;
  width: calc(100% + 2 * var(--gutter));
  margin-inline: calc(-1 * var(--gutter));
  overflow: hidden;
}

.wheel-stage {
  position: relative;
  width: 100%;
  /* The shared 1194px measure rather than the Figma frame's own 1191px:
     3px wider, but it puts the stage's left edge on the same number as
     the WORK cards directly below it. At 1920px the 1191px stage started
     at x=357 against the grid's x=355 — close enough to look like a
     misalignment rather than a decision. The composition is driven by
     aspect-ratio below, so it just scales by 0.25%. */
  max-width: var(--container-max);
  margin-inline: auto;
  /* Extra headroom above the design's own 1191x517 box: mid-spin, a
     card near the front slot is rotated just enough that its corners
     swing above y=0 before easing back to flat — margin-top (set by
     JS, scaled with the stage) gives that swing somewhere to go instead
     of being clipped by the container's top edge. */
  margin-top: 0px;
  aspect-ratio: 1191 / 517;
}

.wheel {
  position: absolute;
  inset: 0;
}

.wheel-card {
  position: absolute;
  left: var(--x, 50%);
  top: var(--y, 0%);
  width: 25.7%;
  aspect-ratio: 306 / 172;
  transform: translate(-50%, 0) rotate(var(--rot, 0deg));
  border-radius: var(--wheel-card-radius, var(--radius-lg));
  overflow: hidden;
  display: block;
  box-shadow: 0 20px 40px rgba(0,0,0,0.35);
  opacity: var(--op, 1);
  filter: blur(var(--blur, 0px));
  will-change: left, top, transform, filter;
}

.wheel-card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.wheel-card:hover,
.wheel-card:focus-visible {
  filter: blur(var(--blur, 0px)) brightness(1.08);
}

.wheel-container.is-spinning .wheel-card {
  pointer-events: none;
}

/* Matches the Figma "Divider" layer's own box: 1440x185 against a
   1440-wide page and the 517-tall wheel-stage (185/517 ≈ 34% height,
   measuring from the stage since that 185px band sits entirely within
   the stage's own bottom portion) — full page width, not the narrower
   1191px stage. That's why this is a sibling of .wheel-stage (a child
   of .wheel-container) rather than nested inside it: .wheel-stage caps
   out at 1191px and centers with ~125px of empty space on each side at
   wide viewports, and a card rotated near a side angle can swing its
   corner past the stage's own edge into exactly that gap — sized to
   the stage it left that gap uncovered and a corner would poke through
   with a hard, unfaded edge. Sized to the full-width container instead,
   the same plain bottom fade reaches all the way to the container's own
   clip edge, so nothing between the stage and that edge goes unfaded. */
.wheel-fade {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 34%;
  background: linear-gradient(to bottom, rgba(26,24,24,0) 0%, var(--color-bg) 55%);
  pointer-events: none;
  /* main.js gives each wheel card an inline z-index up to 1000 to layer
     them front-to-back around the wheel (see .wheel-container's z-index
     comment above) — this fade sits in that same stacking context as a
     later sibling of .wheel-stage, so without its own z-index above that
     range a front-facing card simply painted over it and the fade never
     showed. */
  z-index: 1001;
}

/* =========================
   WORK section
   ========================= */

.work-section {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(32px, 5vw, 80px);
  margin-top: clamp(48px, 8vw, 120px);
  /* Shared container (see CONTAINER at the top of this file). The
     `section` rule already supplies padding-inline: var(--gutter);
     these two lines add the cap. Without it the grid ran the full
     viewport width minus gutters — 1675px at 1920px — while the SPIN
     wheel directly above it stayed at its ~1191px design width, so the
     wheel read as a narrow island floating 242px inside the cards. */
  max-width: calc(var(--container-max) + 2 * var(--gutter));
  margin-inline: auto;
}

.project-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 32px;
  width: 100%;
}

.project-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.project-image {
  position: relative;
  width: 100%;
  aspect-ratio: 580 / 326;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: #2a2828;
}

.project-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.4s ease;
}

/* Hover effects are gated behind a real hovering pointer.

   Unguarded, they cost a tap: a touch browser has no hover, so it emulates
   one — the first tap on a card resolves the :hover state instead of
   activating the link, and only a second tap follows it. On a grid of six
   cards that reads as the site ignoring you. (It also leaves the state stuck
   afterwards, which is why a card's arrow icon sits visible on a phone long
   after the tap.) `hover: hover` is false on touch, so those devices get the
   resting appearance and a single tap navigates.

   :focus-visible stays outside the guard — it is keyboard-only and has
   nothing to do with the pointer. */
@media (hover: hover) {
  .project-card:hover .project-image img {
    transform: scale(1.04);
  }
}

.project-card:focus-visible .project-image img {
  transform: scale(1.04);
}

.project-link-icon {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 50.7px;
  height: 50.7px;
  border-radius: var(--radius-pill);
  background: var(--color-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.2s ease;
}

@media (hover: hover) {
  .project-card:hover .project-link-icon {
    opacity: 1;
  }
}

.project-card:focus-visible .project-link-icon {
  opacity: 1;
}

.project-link-icon img { width: 15px; height: 15px; }

.project-info {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.project-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}

.project-name {
  margin: 0;
  font-size: 20px;
}

.project-year {
  margin: 0;
  font-size: 16px;
  text-align: right;
  white-space: nowrap;
}

.project-tags {
  margin: 0;
  font-size: 16px;
}

/* =========================
   CONTACT section
   ========================= */

.contact-section {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(32px, 5vw, 80px);
  margin-top: clamp(48px, 8vw, 120px);
  margin-bottom: clamp(48px, 8vw, 120px);
  /* Shared container — same as .work-section above. */
  max-width: calc(var(--container-max) + 2 * var(--gutter));
  margin-inline: auto;
}

.contact-info {
  display: grid;
  /* minmax(0, 1fr), not a bare 1fr: a bare 1fr track still carries an
     implicit min-width:auto, i.e. its content's min-content size, so the
     four tracks only distribute space evenly while every card's longest
     unbreakable text run happens to be shorter than an equal share. The
     Instagram card's copy has a &nbsp; joining "amateur photography!" —
     the longest unbreakable run of the four — so once the viewport got
     tight enough for that run to exceed 274.5px (measured: a visibly
     wider third column at 1000px, 215px against the other three's 184px)
     that one track alone refused to shrink with its siblings. minmax(0,…)
     removes the auto floor so every track truly divides the row equally,
     same fix as project.css's [[portfolio-design-rules]] flex min-width
     traps, just the grid-track equivalent. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 32px;
  width: 100%;
}

.contact-method {
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.contact-icon {
  width: 100%;
  /* Capped at the design's own 274px column. Uncapped, this square grew
     with its grid column while the icon inside stayed a fixed 64px: 375px
     at the 2-column breakpoint and 537px at 1 column, i.e. a 537x537 slab
     of cream holding one small icon, three times down the page — and
     these are the highest-contrast elements on a dark page, so at that
     size they swallowed the section. Capping keeps the icon-to-tile
     proportion at the design's 64/274 all the way down, since below
     274px the tile and icon shrink together. The 4-column desktop layout
     is untouched: with the container fix above, a desktop column is
     (1194 - 3x32) / 4 = 274.5px, so the cap is already a no-op there. */
  max-width: 274px;
  /* Explicit, though flex's default stretch would also start here once
     max-width binds — states the intent that a capped tile hugs the
     left edge of its column rather than centering in it. */
  align-self: flex-start;
  aspect-ratio: 1 / 1;
  background: var(--color-icon-bg);
  border-radius: var(--radius-lg);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.25s ease;
}

/* Same guard as the project cards above: these tiles are links too, and an
   emulated hover on the first tap would cost them a tap in the same way. */
@media (hover: hover) {
  .contact-method:hover .contact-icon {
    transform: translateY(-4px);
  }
}

.contact-method:focus-visible .contact-icon {
  transform: translateY(-4px);
}

/* Sized as a proportion of the tile rather than a flat 64px, so the
   icon-to-tile relationship stays at the design's 64/274 = 23.36% at
   every tile size. The tile is square (aspect-ratio above) and has a
   definite height, so the percentage resolves against it directly.
   A flat 64px was right only at the design's own 274px tile: once the
   tile is capped at 274px it can still come out *smaller* than that in
   a 4-column layout on a mid-size screen (170px at a 905px viewport),
   where a fixed 64px icon jumps to 37% of the tile and reads as
   oversized. max-height keeps 64px as the ceiling so the icon never
   exceeds the design size even if a tile is ever wider. */
.contact-icon img {
  width: auto;
  height: 23.36%;
  max-height: 64px;
  max-width: 80%;
  object-fit: contain;
}

.contact-text {
  margin: 0;
  font-size: 20px;
  line-height: 1.5;
  /* One short sentence in a 274px column — balance evens all three lines
     rather than only rescuing the last, which is what `pretty` would do. */
  text-wrap: balance;
}

/* =========================
   404 page
   -------------------------
   No project.css on this page (it isn't a project), so .project-footer
   and .footer-pill — reused verbatim from the project pages' footer
   markup — get their own definitions here, scoped under .not-found-page
   so they can't leak onto (or clash with) project.css's sticky/scrimmed
   versions of the same class names on other pages. This page is short
   and never scrolls past its own content, so the footer just sits at
   the bottom of the flex column instead of sticking with a scrim.
   ========================= */

.not-found-page {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

.not-found-container {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding-block: clamp(48px, 8vw, 120px);
}

.not-found-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  max-width: 560px;
}

.not-found-text {
  margin: 0;
  font-size: 20px;
  line-height: 1.5;
}

.not-found-page .project-footer {
  display: flex;
  align-items: center;
  justify-content: center;
  padding-inline: max(var(--gutter), calc((100% - var(--container-max)) / 2));
  padding-block: 24px;
}

.not-found-page .footer-pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  border-radius: var(--radius-pill);
  border: none;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: inherit;
  font-weight: 500;
  font-size: 20px;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
  transition: transform 0.2s ease, background 0.2s ease, color 0.2s ease;
}

.not-found-page .footer-pill:hover,
.not-found-page .footer-pill:focus-visible {
  transform: translateY(-3px);
  background: #2c2929;
}

/* =========================
   Responsive
   ========================= */

/* ---------- tablet: <= 900px ---------- */
@media (max-width: 900px) {
  .project-grid {
    grid-template-columns: 1fr;
  }

  /* Was its own 1023px breakpoint; folded into the shared tablet tier.
     minmax(0, 1fr) for the same reason as the desktop rule above. */
  .contact-info {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .site-header {
    padding-block: 20px;
  }

  /* The nav becomes the hamburger overlay at the same width the grids
     collapse, rather than at its own old 767px. It still physically fits
     as a row down to ~600px, but a desktop-style nav sitting above a
     page whose every grid has already reflowed was the most visible
     symptom of the three stylesheets disagreeing. */
  .nav-toggle { display: flex; }

  .nav-menu {
    position: fixed;
    inset: 0;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 40px;
    font-size: 28px;
    background: var(--color-bg);
    transform: translateX(100%);
    transition: transform 0.3s ease;
    z-index: 10;
  }

  .nav-menu.is-open {
    transform: translateX(0);
  }

  .nav-toggle[aria-expanded="true"] span:nth-child(1) {
    transform: translateY(7px) rotate(45deg);
  }
  .nav-toggle[aria-expanded="true"] span:nth-child(2) {
    opacity: 0;
  }
  .nav-toggle[aria-expanded="true"] span:nth-child(3) {
    transform: translateY(-7px) rotate(-45deg);
  }
}

/* ---------- mobile: <= 600px ---------- */
@media (max-width: 600px) {
  /* --gutter stops scaling here rather than at the tablet boundary: at
     600px the clamp is already down to 36px, so pinning it to 24px is a
     12px step. Pinning it at 900px instead would have dropped it from
     54px to 24px in one jump, which reads as the page lurching sideways
     at the breakpoint. */
  :root { --gutter: 24px; }

  .spin-subtitle { font-size: 16px; }

  .contact-info {
    grid-template-columns: 1fr;
  }
}
