@font-face {
  font-family: 'ABC Favorit';
  src: url('/fonts/ABCFavorit-Light.woff2') format('woff2');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'ABC Gravity Wide';
  src: url('/fonts/ABCGravity-Wide.woff2') format('woff2'),
       url('/fonts/ABCGravity-Wide.woff') format('woff');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Founders Grotesk';
  src: url('/fonts/FoundersGrotesk-Regular.otf') format('opentype');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

:root { --edge: clamp(28px, 4vw, 48px); }

/* Stops the page underneath from rubber-band scrolling while the nav
   menu is open - without this, a touch drag on the (fixed, on-top)
   overlay can still bounce the document behind it on iOS. */
html.open, body.open { overflow: hidden; }

/* Phones only (480px covers essentially every current phone in portrait,
   even the largest Pro Max/Android models - anything wider is a tablet
   or a resized browser window). 4vw is already below the 28px floor at
   this width, so it never actually controls the value here - halving
   just the vw term alone wouldn't produce any visible change, so the
   floor is halved too. */
@media (max-width: 480px) {
  :root { --edge: clamp(14px, 2vw, 48px); }
}
* { box-sizing: border-box; }
/* -webkit-tap-highlight-color on a/button alone doesn't stop iOS Safari
   applying its own default highlight/flash to an <img> nested inside a
   link (e.g. the works-grid photos) - it needs it too. -webkit-touch-
   callout:none additionally stops the press-and-hold "save/preview"
   affordance on images and links, which can itself add a perceptible
   delay/flash before a tap is treated as a real click. */
a, button, img {
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
}
html, body { margin: 0; padding: 0; height: 100%; background: #000; }
html { scrollbar-width: none; -ms-overflow-style: none; }
html::-webkit-scrollbar { display: none; }

/* Stay hidden until the real fonts are loaded, so there's no visible
   fallback-font flash/swap on arrival - revealed by app.js. */
body { opacity: 0; transition: opacity 0.6s ease; }
body.fonts-ready { opacity: 1; }

.scrollbar-track {
  position: fixed;
  top: 0;
  left: 50%;
  width: 24px;
  height: 100vh;
  z-index: 60;
  pointer-events: none;
}

/* Chrome (this track included) persists across client-side route swaps
   rather than being re-created per route (see app.js) - routes that don't
   want a scrollbar (the works grid) toggle this body class instead of the
   element ever being physically added/removed. */
body.no-scrollbar .scrollbar-track { display: none; }

.scrollbar-thumb {
  position: absolute;
  left: 0;
  width: 100%;
}

/* ---- Home (hero video) ---- */

/* Part of the persistent chrome (present in every page's own markup,
   never torn down/recreated by the router - see #bg/.mark/.overlay) so
   it's already decoding and playing in the background from the moment
   any page loads. Switching to Home just reveals it (display toggle),
   instead of the router creating a fresh <video> on every visit, which
   would force a real decode/buffer restart each time even though the
   file itself was already downloaded. */
#home-video {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 1;
  width: 100vw;
  height: 100dvh;
  object-fit: cover;
}

body.route-home #home-video {
  display: block;
}

.split {
  display: flex;
  min-height: 100vh;
}

/* Only the text side dims when the menu opens - the photo is a real
   descendant of .split now (see .image-half img above), so a filter up
   here would dim it too. In the side-by-side split the photo stays lit
   on purpose, next to the dimmed text; the stacked layout dims it
   separately via .image-half::after (it sits directly above the nav
   overlay there, with nothing separating them). */
.text-half {
  width: 50%;
  background: #000;
  color: rgb(230, 28, 33);
  padding: var(--edge);
  display: flex;
  flex-direction: column;
  transition: filter 0.6s cubic-bezier(0.65, 0, 0.35, 1);
  justify-content: flex-start;
}

body.open .text-half {
  filter: brightness(0);
}

.text-half a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 0.06em;
  text-underline-offset: 0.25em;
}

.image-half {
  width: 50%;
  background: #000;
  position: sticky;
  top: 0;
  height: 100vh;
  align-self: flex-start;
  overflow: hidden;
}

/* Real, visible <img> elements (not canvas-drawn) so the photo scrolls in
   perfect native sync with the text next to it - a canvas repainted each
   frame to track the photo's position can't keep up with the browser's
   own scroll compositor, especially on iOS, and visibly lags behind.
   Stacked absolutely on top of each other and cross-faded via opacity
   (see .is-active) for routes with more than one photo - z-index (see
   app.js) keeps whichever photo is active on top of every photo before
   it, so nothing is ever permanently stuck as the bottom layer. */
.image-half img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  display: block;
  opacity: 0;
  transition: opacity 1.5s ease;
}

.image-half img.is-active { opacity: 1; }

/* Stacked-layout-only dim, applied when the nav menu is open - the photo
   sits directly above the nav overlay with nothing separating them there
   (in the side-by-side split it stays lit on purpose, next to the dimmed
   text - see body.open .split above). Scoped per-breakpoint below since
   each route's stacked threshold differs. */
.image-half::after {
  content: '';
  position: absolute;
  inset: 0;
  background: #000;
  opacity: 0;
  transition: opacity 0.6s cubic-bezier(0.65, 0, 0.35, 1);
  pointer-events: none;
}

/* Built from two plain CSS bars (see .mark-bar below), not a canvas-drawn
   or text glyph - a real "+" made of two rectangles is the same technique
   virtually every site uses for a hamburger-style menu icon, just styled
   as a cross instead of 3 parallel lines. No font, no canvas, no glyph
   metrics - nothing here depends on how a browser measures a character,
   which is what caused the mark's earlier bugs (mispositioning, and once
   going fully invisible on iOS for a reason never pinned down). Rotating
   this container 45deg on open turns the whole cross into an X, since the
   two bars are already crossed at their centers. */
.mark {
  position: fixed;
  top: var(--edge);
  right: var(--edge);
  z-index: 50;
  width: clamp(56px, 6vw, 90px);
  height: clamp(56px, 6vw, 90px);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  transform: rotate(0deg);
  transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}

.mark-bar {
  position: absolute;
  top: 50%;
  left: 50%;
  background: rgb(230, 28, 33);
  transform: translate(-50%, -50%);
}

.mark-bar-h { width: 100%; height: 14%; }
.mark-bar-v { width: 14%; height: 100%; }

body.open .mark { transform: rotate(45deg); }
.mark:focus { outline: none; }

#bg {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  pointer-events: none;
  z-index: 35;
}

.overlay {
  position: fixed;
  inset: 0;
  /* iOS Safari sizes a fixed, inset:0 element against the viewport as if
     its address bar were already collapsed, regardless of whether it's
     currently showing - so the bottom nav link can end up hidden behind
     the (still visible) bar until it auto-hides. dvh tracks the actual
     visible viewport at all times instead - fine here since the overlay
     only appears as a static, momentary menu, never scrolled underneath. */
  height: 100vh;
  height: 100dvh;
  z-index: 40;
  pointer-events: none;
  display: flex;
  align-items: flex-end;
  padding: 0 var(--edge) var(--edge) var(--edge);
  visibility: hidden;
  /* The portrait and nav links used to each have their own
     transform/transition and slide up independently - moved here so the
     whole panel (both, together) is the one thing that moves, instead of
     two things that have to happen to agree on timing. */
  transform: translateY(100%);
  transition: visibility 0s linear 0.65s, transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}

body.open .overlay {
  pointer-events: auto;
  visibility: visible;
  transition-delay: 0s;
  cursor: pointer;
  transform: translateY(0);
}

.overlay nav {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--edge);
}

.overlay nav a {
  display: block;
  font-family: 'ABC Gravity Wide', system-ui, sans-serif;
  text-transform: uppercase;
  color: rgb(230, 28, 33);
  -webkit-text-fill-color: rgb(230, 28, 33);
  text-decoration: none;
  line-height: 0.86;
  letter-spacing: 0;
  font-size: clamp(24px, min(9vw, 10vh), 160px);
  transition: padding-left 0.2s;
}

/* hover:hover excludes touch - a tap doesn't have a real "unhover" the
   way a mouse leaving the link does, so this indent would otherwise get
   stuck on whatever link was last tapped instead of being a hover cue. */
@media (hover: hover) {
  .overlay nav a:hover {
    padding-left: 0.3em;
  }
}

.overlay nav a.current {
  pointer-events: none;
}

.overlay nav a.current::before {
  content: '\25b6\a0';
}

.menu-portrait {
  display: block;
  position: absolute;
  top: var(--edge);
  left: var(--edge);
  width: clamp(200px, min(20vw, 35vh), 320px);
  pointer-events: none;
  translate: 0 0;
  transition: translate 0.2s ease;
}

/* The visible portrait is this real, CSS-masked element (not the <img>
   below) - a plain solid-color box clipped to the dot-pattern PNG's alpha
   channel, recreating the same flat-red tint the old canvas draw produced.
   Being a real element means it moves with the panel exactly like the nav
   text, instead of being redrawn on a separate canvas at RAF cadence,
   which is what let it visibly lag/stretch during scroll on iOS. */
.menu-portrait-fill {
  position: absolute;
  inset: 0;
  background-color: rgb(230, 28, 33);
  -webkit-mask-image: url(/images/dot_portrait_transparent.png);
  mask-image: url(/images/dot_portrait_transparent.png);
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  pointer-events: none;
}

.menu-portrait img {
  display: block;
  width: 100%;
  height: auto;
  opacity: 0;
  pointer-events: none;
}

body.open .menu-portrait {
  pointer-events: auto;
}

body.open .menu-portrait.current {
  pointer-events: none;
}

body.open .menu-portrait:hover {
  translate: 30px 0;
}

/* ---- Work-detail pages (shared by the 5 individual work pages) ---- */

.work-detail-title {
  font-family: 'ABC Gravity Wide', system-ui, sans-serif;
  text-transform: uppercase;
  font-size: clamp(32px, 4vw, 64px);
  line-height: 1;
  margin: 0 0 0.6em;
}

.work-detail-body {
  font-family: 'ABC Favorit', system-ui, sans-serif;
  font-size: clamp(18px, 2.1vw, 26px);
  line-height: 1.5;
  margin: 0 0 0.8em;
}

.work-detail-meta,
.work-detail-tracklist {
  display: block;
  font-family: 'ABC Favorit', system-ui, sans-serif;
  font-size: clamp(16px, 1.4vw, 19px);
  line-height: 1.6;
  opacity: 0.85;
}

.work-detail-meta { margin: 0 0 0.4em; }
.work-detail-tracklist { margin: 1em 0 0; }

.work-detail-track { cursor: pointer; }

.work-detail-video {
  aspect-ratio: 16 / 9;
  margin-top: calc(var(--edge) - 8px);
  position: relative;
  cursor: pointer;
}

.work-detail-video + .work-detail-video { margin-top: var(--edge); }

/* The actual YouTube/Vimeo player - created and loaded up front (see
   app.js), sitting behind the thumbnail until tapped. */
.work-detail-video-player {
  position: absolute;
  inset: 0;
}

.work-detail-video-player iframe {
  width: 100%;
  height: 100%;
  display: block;
  border: 0;
}

/* A real, visible image - not canvas-drawn like work-photo - so it
   scrolls and dims (via .split's filter, since it's a real descendant)
   in perfect native sync with the rest of the page. Absolutely
   positioned (rather than a plain flow block) so it stacks visually
   above the player div that now shares this same box underneath it. */
.work-detail-video-thumb {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.work-detail-play-icon {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 15%;
  max-width: 145px;
  transform: translate(-50%, -50%);
  fill: rgb(230, 28, 33);
  pointer-events: none;
}

.work-detail-video.is-active .work-detail-video-thumb,
.work-detail-video.is-active .work-detail-play-icon {
  display: none;
}

/* ---- Stacked-layout breakpoint, parameterized ----
   Every route with a .split (all except the works grid) collapses to a
   single column below its own breakpoint - but that threshold differs
   per route (1200 default, 1400 Contact, 1800 Concerts) and CSS media
   queries can't reference a variable, so app.js's mount() sets one of
   these body classes to match the current route's config.breakpoint. */
@media (max-width: 1200px) {
  body.bp-1200 .split { flex-direction: column; }
  body.bp-1200 .text-half, body.bp-1200 .image-half { width: 100%; }
  body.bp-1200 .image-half { order: -1; position: relative; height: 60vh; }
  body.bp-1200.open .image-half::after { opacity: 1; }
  body.bp-1200 .scrollbar-track { display: none; }
  body.bp-1200 .overlay nav a:hover { padding-left: 0; }
  body.bp-1200 .menu-portrait:hover { translate: 0 0; }
  /* Match the gap already used between the meta line and the tracklist
     line above it (0.4em + 1em = 1.4em), rather than --edge's own value. */
  body.bp-1200 .work-detail-video { margin-top: 2.2em; }
}

@media (max-width: 1400px) {
  body.bp-1400 .split { flex-direction: column; }
  body.bp-1400 .text-half, body.bp-1400 .image-half { width: 100%; }
  body.bp-1400 .image-half { order: -1; position: relative; height: 60vh; }
  body.bp-1400.open .image-half::after { opacity: 1; }
  body.bp-1400 .scrollbar-track { display: none; }
  body.bp-1400 .overlay nav a:hover { padding-left: 0; }
  body.bp-1400 .menu-portrait:hover { translate: 0 0; }
}

@media (max-width: 1800px) {
  body.bp-1800 .split { flex-direction: column; }
  body.bp-1800 .text-half, body.bp-1800 .image-half { width: 100%; }
  body.bp-1800 .image-half { order: -1; position: relative; height: 60vh; }
  body.bp-1800.open .image-half::after { opacity: 1; }
  body.bp-1800 .scrollbar-track { display: none; }
  body.bp-1800 .overlay nav a:hover { padding-left: 0; }
  body.bp-1800 .menu-portrait:hover { translate: 0 0; }
  body.bp-1800 .concert-table th:nth-child(1), body.bp-1800 .concert-table td:nth-child(1) { width: 16.5%; }
  body.bp-1800 .concert-table th:nth-child(2), body.bp-1800 .concert-table td:nth-child(2) { width: 23.5%; }
  body.bp-1800 .concert-table th:nth-child(3), body.bp-1800 .concert-table td:nth-child(3) { width: 30%; }
  body.bp-1800 .concert-table th:nth-child(4), body.bp-1800 .concert-table td:nth-child(4) { width: 30%; }
}

/* ---- About ---- */

.about-title {
  font-family: 'ABC Gravity Wide', system-ui, sans-serif;
  text-transform: uppercase;
  font-size: clamp(32px, 4vw, 64px);
  line-height: 1;
  margin: 0 0 0.5em;
}

.about-body {
  font-family: 'ABC Favorit', system-ui, sans-serif;
  font-size: clamp(22px, 2.1vw, 32px);
  line-height: 1.4;
  margin: 0 0 1em;
}

.about-body:last-child { margin-bottom: 0; }

body.route-about .text-half a {
  text-underline-offset: 0.2em;
  text-decoration-skip-ink: auto;
}

/* Every route with a .text-half, mobile only - same treatment above
   whatever heading each page starts with (JOSH COHEN, UPCOMING,
   CONTACT, a work-detail title, etc.), leaving the other three sides of
   .text-half's padding untouched. Works index isn't affected - it has
   no .text-half at all. */
@media (max-width: 480px) {
  .text-half {
    padding-top: 30px;
  }
  /* Matches .about-body's own mobile size (its floor throughout this
     width range) on the work-detail pages (and Tutorials, which shares
     the same class) too. */
  .work-detail-body {
    font-size: 22px;
  }
  .work-detail-meta,
  .work-detail-tracklist {
    font-size: 16px;
  }
}

/* ---- Concerts ---- */

.concert-section-label {
  font-family: 'ABC Gravity Wide', system-ui, sans-serif;
  text-transform: uppercase;
  font-size: clamp(32px, 4vw, 64px);
  line-height: 1;
  margin: 1em 0 0.5em;
}

.concert-section-label:first-child { margin-top: 0; }

.concert-meta {
  display: block;
  font-family: 'ABC Favorit', system-ui, sans-serif;
  font-size: clamp(16px, 1.5vw, 21px);
  line-height: 1.5;
}

.concert-table-wrap {
  overflow-x: auto;
  margin-bottom: 1em;
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* iOS only applies native momentum/inertial scrolling to the page's
     own scroll by default - a nested scrollable element like this one
     needs this explicitly, or a swipe stops dead the instant you lift
     your finger instead of gently decelerating like everywhere else. */
  -webkit-overflow-scrolling: touch;
}

.concert-table-wrap::-webkit-scrollbar {
  display: none;
}

.concert-table {
  width: 100%;
  min-width: 700px;
  border-collapse: collapse;
  table-layout: fixed;
  font-family: 'ABC Favorit', system-ui, sans-serif;
  font-size: clamp(16px, 1.75vw, 25px);
  line-height: 1.4;
}

.concert-table th:nth-child(1), .concert-table td:nth-child(1) { width: 22%; }
.concert-table th:nth-child(2), .concert-table td:nth-child(2) { width: 22%; }
.concert-table th:nth-child(3), .concert-table td:nth-child(3) { width: 28%; }
.concert-table th:nth-child(4), .concert-table td:nth-child(4) { width: 28%; }

@media (min-width: 2100px) {
  .concert-table th:nth-child(1), .concert-table td:nth-child(1) { width: 19%; }
  .concert-table th:nth-child(2), .concert-table td:nth-child(2) { width: 25%; }
}

.concert-table td { overflow-wrap: break-word; }

.concert-table th,
.concert-table td {
  text-align: left;
  vertical-align: top;
  padding: 0.6em 0.8em 0.6em 0;
  border-bottom: 1px solid currentColor;
}

.concert-table th {
  font-family: 'ABC Gravity Wide', system-ui, sans-serif;
  text-transform: uppercase;
  font-weight: normal;
  font-size: 0.8em;
  letter-spacing: 0.02em;
  border-bottom-width: 2px;
}

/* ---- Contact ---- */

.contact-title {
  font-family: 'ABC Gravity Wide', system-ui, sans-serif;
  text-transform: uppercase;
  font-size: clamp(32px, 4vw, 64px);
  line-height: 1;
  margin: 0 0 1em;
}

.contact-entry { margin: 0 0 3em; }
.contact-entry:last-child { margin-bottom: 0; }

.contact-label {
  font-family: 'ABC Gravity Wide', system-ui, sans-serif;
  text-transform: uppercase;
  font-size: clamp(18px, 1.8vw, 26px);
  line-height: 1;
  margin: 0 0 0.3em;
}

.contact-detail {
  font-family: 'ABC Favorit', system-ui, sans-serif;
  font-size: clamp(16px, 1.6vw, 22px);
  line-height: 1.5;
  margin: 0;
}

.contact-table {
  border-collapse: collapse;
  font-family: 'ABC Favorit', system-ui, sans-serif;
  font-size: clamp(16px, 1.6vw, 22px);
  line-height: 1.5;
  margin: 0;
}

.contact-table td {
  padding: 0 2em 0.3em 0;
  border: none;
}

/* Below 506px the two columns (Weird Time Signature / All other
   releases) no longer fit side by side - grid + order re-flows the four
   cells into label-then-email pairs stacked one above the other, rather
   than the plain top-to-bottom DOM order (which would read as both
   labels, then both emails). tbody/tr become display:contents so the
   <td>s themselves are the actual grid items order can act on. */
@media (max-width: 506px) {
  .contact-table { display: grid; }
  .contact-table tbody, .contact-table tr { display: contents; }
  .contact-table tr:nth-child(1) td:nth-child(1) { order: 1; }
  .contact-table tr:nth-child(2) td:nth-child(1) { order: 2; }
  .contact-table tr:nth-child(1) td:nth-child(2) { order: 3; margin-top: 0.5em; }
  .contact-table tr:nth-child(2) td:nth-child(2) { order: 4; }
}

.contact-table tr:first-child td {
  padding-bottom: 0.1em;
}

/* The form and the success message occupy the same grid cell (rather
   than one being removed from flow when the other shows), so the
   reserved height is always max(form height, message height) - toggling
   between them never changes this block's height, which would otherwise
   shift everything below it up or down. */
.newsletter-wrap {
  display: grid;
  max-width: 350px;
}

.newsletter-wrap > * {
  grid-area: 1 / 1;
}

.newsletter-form {
  display: flex;
  align-items: baseline;
  gap: 0.5em;
  visibility: visible;
}

.newsletter-form.is-hidden {
  visibility: hidden;
  pointer-events: none;
}

.newsletter-success {
  align-self: center;
  visibility: hidden;
}

.newsletter-success.is-visible {
  visibility: visible;
}

/* The underline lives here, on a wrapper - not on the input itself -
   because Chrome's autofill styling (see :-webkit-autofill below) can
   visually paint over a border set directly on the input. This element
   is never autofilled, so its border can't be touched by that. */
.newsletter-input-wrap {
  flex: 1;
  min-width: 0;
  display: block;
  border-bottom: 1px solid currentColor;
}

.newsletter-form input {
  display: block;
  width: 100%;
  background: transparent;
  border: none;
  color: inherit;
  font-family: 'ABC Favorit', system-ui, sans-serif;
  font-size: clamp(16px, 1.6vw, 22px);
  padding: 0.2em 0;
}

/* Off-screen, not display:none/visibility:hidden - some bots specifically
   check for and skip those, defeating the point of a honeypot field. */
.newsletter-honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Chrome ignores plain background-color/color on an autofilled input -
   -webkit-text-fill-color is the one text-color property it still
   respects, and painting over its own background with a huge inset
   box-shadow (rather than changing background-color, which it blocks)
   is the standard workaround. */
.newsletter-form input:-webkit-autofill,
.newsletter-form input:-webkit-autofill:hover,
.newsletter-form input:-webkit-autofill:focus {
  -webkit-text-fill-color: rgb(230, 28, 33);
  caret-color: rgb(230, 28, 33);
  -webkit-box-shadow: 0 0 0 1000px #000 inset;
  transition: background-color 9999s ease-in-out 0s;
  border: none;
  font-family: 'ABC Favorit', system-ui, sans-serif;
  font-size: clamp(16px, 1.6vw, 22px);
  padding: 0.2em 0;
}

.newsletter-form input::placeholder {
  color: currentColor;
  opacity: 0.6;
}

.newsletter-form input:focus {
  outline: none;
}

.newsletter-form button {
  background: none;
  border: none;
  padding: 0;
  transform: translateY(0.08em);
  color: inherit;
  font-family: 'ABC Favorit', system-ui, sans-serif;
  font-size: clamp(16px, 1.6vw, 22px);
  cursor: pointer;
}

/* ---- Works grid ---- */

/* Every work is a plain, real <img> (not canvas-drawn) - no per-work
   text lives on this page, just a grid of links out to each work's own
   page - so the menu-open dim can be a normal CSS filter, no
   canvas-side fade trick needed. */
.works-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
}

.work-photo-link {
  display: block;
  transition: filter 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}

/* Left column only, matching every other page's split layout - text on
   the left dims to black, the photo on the right stays lit. In the
   stacked layout below there's no left/right left to preserve, so the
   whole grid dims instead. */
body.open .work-photo-link:nth-child(odd) {
  filter: brightness(0);
}

.work-photo-link img {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
}

@media (max-width: 1200px) {
  body.route-works .works-grid { grid-template-columns: 1fr; }
  body.open.route-works .work-photo-link { filter: brightness(0); }
}
