/* ==========================================================================
   PERRO — Homepage stylesheet
   Departure Mono, #B2C703 ground, 14px uppercase mono type.
   Breakpoints: md 768px / lg 1070px / xl 1250px.
   ========================================================================== */

@font-face {
  font-family: "Departure Mono";
  src: url("../assets/fonts/DepartureMono-Regular.woff2") format("woff2"),
       url("../assets/fonts/DepartureMono-Regular.woff") format("woff");
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

/* ---------- theme ---------- */

:root {
  --color-bg: #1C1917;
  --color-fg: #B2C703;
  /* Fixed brand accents — unlike --color-bg/fg and --footer-bg/fg, these
     never flip with theme (the ticker bar and the saturation knob's
     arc/pointer are deliberately the same regardless of light/dark site
     theme). Named here purely so the value isn't a repeated magic number. */
  --accent-lime: #B2C703;
  --accent-dark: #1C1917;
  /* The site's two recurring easing curves, previously repeated as
     literals. --ease-wipe is the page-transition/theme-wipe curve
     specifically (theme-transition-overlay + main's load fade) — same
     resolved value, just named. */
  --ease-standard: cubic-bezier(0.19, 1, 0.22, 1);
  --ease-wipe: cubic-bezier(0.65, 0, 0.35, 1);
}
:root[data-theme="light"] {
  --color-bg: #B2C703;
  --color-fg: #1C1917;
}

.theme-toggle {
  position: fixed;
  top: 30px;
  right: 16px;
  z-index: 10000;
  background: transparent;
  border: 0;
  padding: 10px;
  margin: -10px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.theme-toggle.is-hidden { display: none; }
.theme-toggle:focus-visible { outline: 2px solid var(--color-fg); outline-offset: 4px; }
@media (min-width: 768px) {
  .theme-toggle { top: 30px; right: 30px; }
}
@media (min-width: 1070px) {
  .theme-toggle { top: 36px; right: 42px; }
}
.theme-toggle__icon {
  width: 22px;
  height: 22px;
  display: block;
  overflow: visible;
}
@media (min-width: 768px) {
  .theme-toggle__icon { width: 18px; height: 18px; }
}
.theme-toggle__fill {
  fill: var(--color-fg);
  transform-origin: 12px 12px;
  transform: scale(0);
  transition: transform 450ms ease;
}
:root[data-theme="light"] .theme-toggle__fill {
  transform: scale(1);
}
.theme-toggle__ring {
  fill: none;
  stroke: var(--color-fg);
  stroke-width: 1.4;
}

.theme-transition-overlay {
  position: fixed;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 0;
  z-index: 20000;
  pointer-events: none;
  transition: height 650ms var(--ease-wipe);
}
.theme-transition-overlay.is-active { height: 100vh; }
.theme-transition-overlay.no-transition { transition: none; }
.page-wipe-arrival .loader-component { display: none; }
@media (prefers-reduced-motion: reduce) {
  .theme-transition-overlay { transition: none; }
}

/* ---------- base ---------- */

*, *::before, *::after { box-sizing: border-box; }

html {
  font-size: 62.5%;
  scroll-behavior: smooth;
  background-color: var(--color-bg);
}

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-fg);
  position: relative;
  font-family: "Departure Mono", monospace;
  font-size: 14px;
  line-height: 21px;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, h4, h5, h6, p { margin: 0; }

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

/* Clears the fixed header + the fixed logo band beneath it (--header-h is
   measured in JS; the clamp mirrors .mobile-logo-bleed's own height formula
   so this stays in sync at every screen size). At 1070px+ .mobile-logo-bleed
   hides on every page (see the rule further down) so no extra clearance is
   needed there either. */
main {
  position: relative;
  opacity: 0;
  padding-top: calc(var(--header-h, 80px) + clamp(90px, 24vw, 150px));
  transition: opacity 550ms var(--ease-wipe);
}
main.is-loaded { opacity: 1; }

/* Testing a repeatable background texture on the main content area only —
   footer keeps its own plain --footer-bg, untouched. A pseudo-element
   rather than main's own background-image, matching how the print-grain
   overlay works (body::after), since that's the only way to give the
   texture its own independent opacity without touching main's real content.

   The source PNG is a low-alpha BLACK grain — reads fine as a subtle
   darkening on the light theme's lime background, but a black-on-black
   texture is close to invisible against the dark theme's near-black
   background no matter the blend mode, since there's no lightness to work
   with. So dark mode inverts it to a light grain instead and switches to a
   lightening blend (screen) rather than a darkening one (multiply). */
main::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  display: none;
  background-image: url("../assets/images/background-texture.webp");
  background-repeat: repeat;
  filter: invert(1);
  mix-blend-mode: screen;
  opacity: 0.06;
}
/* Tied to the same footer switch as the grunge sprite pieces below — both
   are "the images"; body::after's print-grain stays on regardless, since
   that's the site's base noise, not part of what the switch controls. */
:root[data-tex="on"] main::before {
  display: block;
}
:root[data-theme="light"] main::before {
  filter: none;
  mix-blend-mode: multiply;
  opacity: 0.15;
}
@media (prefers-reduced-motion: reduce) {
  main { transition: none; }
}
@media (min-width: 1070px) {
  main { padding-top: 0; }
}

/* ---------- grunge sprite textures (.tex) ---------- */
/* Decorative only — every piece is aria-hidden in the markup and carries
   no meaning. Sources are alpha-only WebP masks (see
   scripts/crop-sprites.py); background-color paints the shape, so the
   same file works in both themes and only --tex-ink/--tex-blend/
   --tex-opacity need to flip — same reasoning as main::before's grain
   layer just above: light ink + screen lifts off the dark theme's
   near-black bg, dark ink + multiply sits into the light theme's lime bg.

   Each piece is z-index:-1 so it paints behind its anchor's real content
   (text, nav links, logo) without needing to be careful about DOM order.
   .tex-anchor's isolation keeps that negative z-index contained to just
   that one anchor instead of leaking into the page's wider stacking order. */
.tex-anchor {
  position: relative;
  isolation: isolate;
}
/* The two anchors whose pieces bleed past their own edge (barbed-wire,
   halftone-patch, grid-smudge) clip locally rather than via overflow-x on
   body/html — the sticky header logo (.sticky-home) relies on the
   viewport itself being the scrolling element, and forcing a non-visible
   overflow-x anywhere up that chain flips overflow-y to auto too and
   breaks that. Clipping at the row itself avoids touching the page's
   scroll container at all, and still reads as edge-bleed since these full-
   width rows already sit within ~30px of the real viewport edge. */
.hero-top-divider,
.pre-footer-divider {
  overflow: hidden;
}
/* .tex[aria-hidden] rather than plain .tex: every piece already carries
   aria-hidden="true" by the "decorative only" contract above, and the
   extra attribute selector bumps specificity past the grid's own
   ".row > *" utility (position:relative; width:100%) — same specificity,
   later in the cascade, so a plain .tex would silently lose position:
   absolute and each placement's width for any piece that's a direct
   child of a .row (e.g. tex-pos-hero inside .hero-top-divider). */
.tex[aria-hidden] {
  position: absolute;
  z-index: -1;
  display: block;
  pointer-events: none;
  -webkit-mask-image: var(--tex-src);
  mask-image: var(--tex-src);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
  background-color: var(--tex-ink);
  mix-blend-mode: var(--tex-blend);
  opacity: var(--tex-opacity);
}
/* Off by default. Needs to beat .tex[aria-hidden]'s own display:block
   (0,2,0) — :root:not([data-tex="on"]) .tex[aria-hidden] is 0,4,0, so it
   wins regardless of source order; drops out entirely once data-tex="on"
   is set, falling back to the base rule's display:block. */
:root:not([data-tex="on"]) .tex[aria-hidden] { display: none; }

:root:not([data-theme="light"]) .tex {
  --tex-ink: #EDEAE3;
  --tex-blend: screen;
  --tex-opacity: 0.08;
}
:root[data-theme="light"] .tex {
  --tex-ink: #1C1917;
  --tex-blend: multiply;
  --tex-opacity: 0.12;
}
/* halftone-patch reads best large and very faint, more ambient wash than
   legible mark. Matched to the theme rules' own specificity
   (:root(:not)[data-theme] .tex) above — a plain .tex--faint class loses
   the cascade to that more specific selector regardless of source order. */
:root:not([data-theme="light"]) .tex--faint { --tex-opacity: 0.045; }
:root[data-theme="light"] .tex--faint { --tex-opacity: 0.07; }
/* one piece (crosshair) tinted with the site accent instead of neutral
   ink, kept low so it stays texture rather than a second logo mark */
:root:not([data-theme="light"]) .tex--accent { --tex-ink: var(--color-fg); --tex-opacity: 0.06; }
:root[data-theme="light"] .tex--accent { --tex-ink: var(--color-fg); --tex-opacity: 0.06; }

/* shapes — mask source + native aspect ratio only; size/position/rotation
   come from the placement classes below so the same shape can be reused
   at different scales */
.tex-barcode    { --tex-src: url("../assets/textures/barcode-00240.webp");      aspect-ratio: 249 / 93; }
.tex-crosshair  { --tex-src: url("../assets/textures/crosshair.webp");          aspect-ratio: 113 / 143; }
.tex-halftone   { --tex-src: url("../assets/textures/halftone-patch.webp");     aspect-ratio: 260 / 124; }
.tex-barbedwire { --tex-src: url("../assets/textures/barbed-wire.webp");        aspect-ratio: 444 / 63; }
.tex-xxxx       { --tex-src: url("../assets/textures/xxxx.webp");               aspect-ratio: 305 / 64; }
.tex-gridsmudge { --tex-src: url("../assets/textures/grid-smudge.webp");        aspect-ratio: 260 / 138; }
.tex-scratch    { --tex-src: url("../assets/textures/scratch-lines.webp");      aspect-ratio: 415 / 71; }
.tex-sparkle    { --tex-src: url("../assets/textures/sparkle-crosshairs.webp"); aspect-ratio: 141 / 106; }

/* page-background placement — asymmetric, never mirrored left/right, kept
   clear of copy: the hero row's own margin, the info column's empty space
   below its shorter content, the spacer row before the slider, and the
   pre-footer divider's big empty gap.

   Every rule here is compounded as .tex.tex-pos-* (not just .tex-pos-*):
   these set `width`, which the grid's ".row > *" utility (width:100%,
   same 0,1,0 specificity, later in the cascade) would otherwise win for
   any piece that's a direct child of a .row, same issue as .tex[aria-
   hidden] above. */
/* top:70px rather than a small offset — .hero-top-divider's own box
   starts at the very top of the page (its pt-80 padding is what pushes
   the real divider-dot/text down), and this piece's absolute `top` isn't
   affected by that padding, so a small offset here was landing inside the
   padding gap under the fixed header (57px tall) instead of the visible
   content below it. */
.tex.tex-pos-hero {
  top: 70px;
  left: -16px;
  width: 72px;
  transform: rotate(-4deg);
}
/* Placed in the narrow real gap between the IG link and the rotation-grid
   photo below it, not near the column's own bottom edge — the rotation
   grid's first two tiles use large negative top margins (.minus-180) to
   overlap up into this column at desktop widths, so most of the column's
   apparent empty space below the text is actually covered by a photo. */
.tex.tex-pos-info {
  left: 2px;
  top: 215px;
  width: 34px;
  transform: rotate(6deg);
}
/* a second, smaller barbed-wire scrap, opposite corner from the crosshair
   so the two don't read as a mirrored pair */
.tex.tex-pos-info-top {
  right: 4%;
  top: 2px;
  width: 58px;
  transform: rotate(3deg);
}
/* right-aligned rather than left: at desktop widths this spacer row
   collapses to 0 height and sits right where the rotation grid's first
   two photos overlap (see tex-pos-info above) — those photos only reach
   to about the row's horizontal midpoint, so the right-hand side (roughly
   matching the gif-slot column above) is the one part of this row that's
   actually clear. */
.tex.tex-pos-spacer {
  top: -4px;
  right: 2%;
  width: 180px;
  transform: rotate(18deg);
}
/* a third, small barbed-wire scrap sharing the spacer row with xxxx, far
   enough right that the two never touch */
.tex.tex-pos-spacer-wire {
  top: 2px;
  right: 20%;
  width: 54px;
  transform: rotate(-5deg);
}
.tex.tex-pos-prefooter-a {
  bottom: -30px;
  right: -70px;
  width: 300px;
  transform: rotate(-2deg);
}
.tex.tex-pos-prefooter-b {
  top: -18px;
  left: -40px;
  width: 160px;
  transform: rotate(5deg);
}
@media (max-width: 767.98px) {
  /* below ~768px: drop about half the background pieces, scale the rest down.
     Compounded as .tex.tex-pos-* here too — display:none needs to beat the
     base .tex[aria-hidden] rule's display:block (0,2,0 attribute
     selector), which a plain 0,1,0 class here would lose. */
  .tex.tex-pos-info,
  .tex.tex-pos-info-top,
  .tex.tex-pos-spacer-wire,
  .tex.tex-pos-prefooter-b { display: none; }
  .tex.tex-pos-hero { width: 56px; left: -10px; }
  /* mobile stacks the info column's IG link directly above this spacer
     row with almost no gap — nudge down for clearance from it */
  .tex.tex-pos-spacer { width: 110px; top: 10px; }
  .tex.tex-pos-prefooter-a { width: 200px; right: -40px; bottom: -18px; }
}

/* footer placement — a torn band along the top edge, a small mark in the
   free top corner, and the barcode near the copyright line. Kept off the
   saturation knob (bottom-left) and the copyright text itself
   (bottom-right) at every width. footer isn't a .row, so these don't
   strictly need the compound bump, but it's kept for consistency with
   the rules above. */
.tex.tex-pos-footer-band {
  top: -4px;
  left: 4%;
  width: 92%;
  transform: rotate(-0.6deg);
}
.tex.tex-pos-footer-corner {
  top: 18px;
  left: 6%;
  width: 60px;
  transform: rotate(4deg);
}
.tex.tex-pos-footer-barcode {
  right: 10px;
  bottom: 32px;
  width: 84px;
  z-index: 1; /* footer's own child, not a .tex-anchor descendant — sits above footer-black, below the credit line/knob (z-index:2) */
  transform: rotate(-2deg);
}
@media (max-width: 767.98px) {
  /* footer keeps only the band + barcode below ~768px */
  .tex.tex-pos-footer-corner { display: none; }
  .tex.tex-pos-footer-band { width: 96%; }
  .tex.tex-pos-footer-barcode { width: 64px; bottom: 60px; right: 8px; }
}

/* ---------- footer texture switch ---------- */
/* A minimal track+thumb switch, sharing the saturation knob's own visual
   language: a dim var(--footer-fg) outline at rest, the same hardcoded
   brand lime (#B2C703) used by the knob's arc when "on". Textures are off
   by default (no data-tex attribute); data-tex="on" both shows every
   .tex piece (see the base rule near main::before) and slides the thumb. */
.footer-controls {
  position: absolute;
  left: 10px;
  bottom: 10px;
  z-index: 2;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 14px;
}
.tex-switch {
  background: transparent;
  border: 0;
  padding: 8px;
  margin: -8px;
  display: flex;
  align-items: center;
  cursor: pointer;
}
.tex-switch:focus-visible { outline: 2px solid var(--footer-fg); outline-offset: 4px; }
.tex-switch__track {
  width: 68px;
  height: 34px;
  display: block;
  overflow: visible;
}
.tex-switch__track rect {
  fill: none;
  stroke: var(--footer-fg);
  stroke-opacity: 0.25;
  stroke-width: 2;
}
/* footer-fg in both states, not the knob arc's hardcoded lime — footer-bg
   IS that lime in the default dark-site theme, so a lime-filled thumb
   would sit invisibly on top of a lime background. footer-fg always
   contrasts against footer-bg by construction (the pair inverts together
   per theme), so it's the one color guaranteed to show in both themes;
   on/off reads through opacity + slide position instead of a hue change. */
.tex-switch__thumb {
  fill: var(--footer-fg);
  fill-opacity: 0.35;
  transition: transform 200ms ease, fill-opacity 200ms ease;
}
.tex-switch[aria-checked="true"] .tex-switch__thumb {
  fill-opacity: 1;
  transform: translateX(19px);
}
@media (prefers-reduced-motion: reduce) {
  .tex-switch__thumb { transition: none; }
}

button {
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  cursor: pointer;
}
button:focus { outline: 0; }

img { max-width: 100%; }

.visibility-hidden {
  visibility: hidden;
  position: absolute;
}

/* Visually hidden but still in the accessibility tree and reachable —
   unlike .visibility-hidden above, which removes it from both. Used for
   a real <h1> that documents each page's own visual wordmark without
   changing anything on screen. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------- grid (only what the page uses) ---------- */

.container-fluid {
  width: 100%;
  margin-right: auto;
  margin-left: auto;
}

.row {
  display: flex;
  flex-wrap: wrap;
  margin-right: -15px;
  margin-left: -15px;
}
.row > *,
.mobile-slider .mobile-slide {
  position: relative;
  width: 100%;
  padding-right: 15px;
  padding-left: 15px;
}

.col-12 { flex: 0 0 100%; max-width: 100%; }

@media (min-width: 768px) {
  .col-md-4  { flex: 0 0 33.333333%; max-width: 33.333333%; }
  .col-md-5  { flex: 0 0 41.666667%; max-width: 41.666667%; }
  .col-md-6  { flex: 0 0 50%;        max-width: 50%; }
  .col-md-7  { flex: 0 0 58.333333%; max-width: 58.333333%; }
  .col-md-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
}
@media (min-width: 1070px) {
  .col-lg-3  { flex: 0 0 25%;        max-width: 25%; }
  .col-lg-4  { flex: 0 0 33.333333%; max-width: 33.333333%; }
  .col-lg-5  { flex: 0 0 41.666667%; max-width: 41.666667%; }
  .col-lg-6  { flex: 0 0 50%;        max-width: 50%; }
  .col-lg-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
}
@media (min-width: 1250px) {
  .col-xl-9 { flex: 0 0 75%; max-width: 75%; }
}

/* ---------- display / flex helpers ---------- */

.d-flex { display: flex; }
.d-none { display: none; }
.flex-column { flex-direction: column; }
.justify-content-center { justify-content: center; }
.align-items-center { align-items: center; }
.position-relative { position: relative; }
.text-center { text-align: center; }
.break-lg { display: none; }
@media (min-width: 1070px) {
  .break-lg { display: inline; }
}
.ml-auto { margin-left: auto; }
.gap-1 { gap: 4px; }
.w-100 { width: 100%; }
.h-100 { height: 100%; }
.cover { width: 100%; height: 100%; object-fit: cover; }

@media (min-width: 768px) {
  .d-md-flex { display: flex; }
  .d-md-none { display: none; }
  .ml-auto-md { margin-left: auto; }
}

/* ---------- spacing utilities (px values match class names) ---------- */

.px-15 { padding-left: 15px; padding-right: 15px; }
.px-30 { padding-left: 30px; padding-right: 30px; }
.pt-15 { padding-top: 15px; }
.pt-20 { padding-top: 20px; }
.pt-30 { padding-top: 30px; }
.pt-35 { padding-top: 35px; }
.pt-80 { padding-top: 80px; }
.pb-15 { padding-bottom: 15px; }
.pb-30 { padding-bottom: 30px; }
.pl-30 { padding-left: 30px; }

@media (min-width: 768px) {
  .px-md-30 { padding-left: 30px; padding-right: 30px; }
  .pt-md-0 { padding-top: 0; }
  .pt-md-30 { padding-top: 30px; }
  .pt-md-33 { padding-top: 33px; }
  .pt-md-40 { padding-top: 40px; }
  .pt-md-100 { padding-top: 100px; }
  .pb-md-30 { padding-bottom: 30px; }
  .pl-md-0 { padding-left: 0; }
  .pl-md-50 { padding-left: 50px; }
}
@media (min-width: 1070px) {
  .px-lg-30 { padding-left: 30px; padding-right: 30px; }
  .pt-lg-0 { padding-top: 0; }
  .pt-lg-40 { padding-top: 40px; }
  .pt-lg-135 { padding-top: 135px; }
  .pl-lg-15 { padding-left: 15px; }
}

@media (min-width: 1070px) {
  .minus-80 { margin-top: -110px; }
  .minus-180 { margin-top: -100px; }
}
@media (min-width: 1250px) {
  .minus-180 { margin-top: -180px; }
}

/* ---------- loader ---------- */

.loader-component {
  width: 100%;
  height: 100%;
  position: fixed;
  background: var(--color-bg);
  left: 0;
  top: 0;
  z-index: 2090;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  transition: opacity 0.6s ease;
}
.loader-component.is-hidden {
  opacity: 0;
  pointer-events: none;
}

/* ---------- header ---------- */

header {
  position: fixed;
  width: 100%;
  top: 0;
  left: 0;
  z-index: 9;
  padding-top: 30px;
}
@media (min-width: 1070px) {
  header { padding-top: 36px; }
  header .navbar { padding: 0 42px; }
}
header .navbar .nav-container {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  margin: 0;
  padding: 0;
}
@media (min-width: 1070px) {
  header .navbar .nav-container { gap: 9px; }
}
header .navbar .nav-container .nav-item .nav-link {
  text-decoration: none;
  color: var(--color-fg);
}

.menu-dots-btn {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 44px;
  height: 26px;
  padding: 0;
  border: 1.5px solid var(--color-fg);
  border-radius: 6px;
  background: transparent;
  cursor: pointer;
  transition: background 300ms var(--ease-standard), border-color 300ms var(--ease-standard);
}
/* The same three bars form both icons. Interrupted transitions reverse
   from their current transforms without timers or icon swaps. */
.menu-dots-btn span {
  position: absolute;
  display: block;
  top: calc(50% - 1px);
  left: calc(50% - 7px);
  width: 14px;
  height: 2px;
  border-radius: 1px;
  background: var(--color-fg);
  transform-origin: center;
  transition: transform 420ms cubic-bezier(0.22, 1, 0.36, 1), opacity 220ms ease, background 150ms ease;
  pointer-events: none;
}
.menu-dots-btn span:nth-child(1) { transform: translateY(-4px) rotate(0deg); }
.menu-dots-btn span:nth-child(2) { transform: scaleX(1); }
.menu-dots-btn span:nth-child(3) { transform: translateY(4px) rotate(0deg); }
.menu-dots-btn.show {
  background: transparent;
  border-color: var(--color-fg);
}
.menu-dots-btn.show span { background: var(--color-fg); }
.menu-dots-btn.show span:nth-child(1) { transform: translateY(0) rotate(45deg); }
.menu-dots-btn.show span:nth-child(2) { transform: scaleX(0); opacity: 0; }
.menu-dots-btn.show span:nth-child(3) { transform: translateY(0) rotate(-45deg); }
/* Retain the visible 44 × 26 button; extend its touch area vertically. */
.menu-dots-btn::before { content: ""; position: absolute; inset: -10px -1.5px; }
.menu-dots-btn:focus-visible { outline: 2px solid var(--color-fg); outline-offset: 4px; }
@media (prefers-reduced-motion: reduce) {
  .menu-dots-btn, .menu-dots-btn span { transition: none; }
}
@media (hover: hover) {
  .menu-dots-btn:hover {
    background: var(--color-fg);
    border-color: transparent;
  }
  .menu-dots-btn:hover span {
    background: var(--color-bg);
  }
  .menu-dots-btn.show:hover {
    background: transparent;
    border-color: var(--color-fg);
  }
  .menu-dots-btn.show:hover span {
    background: var(--color-fg);
  }
}
@media (min-width: 768px) {
  .menu-dots-btn { display: none; }
}

/* Fixed under the header like the theme toggle and the mobile menu button,
   so it never scrolls away below 1070px, where it hides — on the homepage
   because the sticky hero logo takes over further down the page. */
.mobile-logo-bleed {
  position: fixed;
  top: var(--header-h, 80px);
  left: 0;
  z-index: 8;
  width: 100%;
  overflow: hidden;
  height: 24vw;
  min-height: 90px;
  max-height: 150px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.mobile-logo-bleed img {
  width: 104%;
  max-width: none;
  height: auto;
  flex-shrink: 0;
}
@media (min-width: 1070px) {
  .mobile-logo-bleed { display: none; }
}

/* Between mobile (where the fixed logo band was tuned to fit) and 1070px
   (where the hero logo takes over), the band's own width grows enough that
   the logo image's fixed aspect ratio makes it taller than the 150px cap —
   so it was getting clipped top and bottom by this box's overflow:hidden,
   sitting right up against the header with no breathing room. Raise the cap
   so the full logo fits, and nudge it down slightly for clearance — main's
   own clearance padding (above) is widened to match so nothing overlaps
   below it either. */
@media (min-width: 768px) and (max-width: 1069.98px) {
  .mobile-logo-bleed {
    top: calc(var(--header-h, 80px) + 16px);
    max-height: 190px;
  }
  main { padding-top: calc(var(--header-h, 80px) + 16px + clamp(90px, 24vw, 190px)); }
}

@media (max-width: 767.98px) {
  .hero-top-divider.pt-80 { padding-top: 60px; }
  .pre-footer-divider.pt-80 { padding-top: 40px; }
}

/* menu.html/faq.html have no hero logo to take over at 1070px+ like the
   homepage does, so — unlike .mobile-logo-bleed above — they get a second,
   separate logo image (same theme-swapped source, see home.js) that's
   static rather than fixed, sitting inside the page's own content column
   instead of floating above it. Hidden below 1070px since .mobile-logo-bleed
   already covers that range there. */
.page-content-col__logo { display: none; }
@media (min-width: 1070px) {
  .page-content-col__logo {
    display: block;
    margin-bottom: 40px;
  }
  .page-content-col__logo img {
    display: block;
    width: 100%;
    max-width: 320px;
    height: auto;
  }
}

/* Shared centred content column for menu.html and faq.html — their old
   col-md-9/col-lg-8/col-xl-7-style classes were never actually defined
   anywhere in this grid, so both silently rendered at the full row width
   (col-12) above mobile. A real max-width instead, centred by the row's
   own justify-content: center, and shared so both pages stay consistent. */
@media (min-width: 768px) {
  .page-content-col { max-width: 720px; }
}

header .submenu {
  position: absolute;
  top: calc(100% + 14px);
  left: 50%;
  min-width: 200px;
  background-color: var(--color-bg);
  padding: 15px 24px;
  opacity: 0;
  pointer-events: none;
  z-index: 1;
  transform: translate(-50%, -10px);
  transition: opacity 0.3s ease, transform 0.35s var(--ease-standard);
}
header .submenu.show {
  opacity: 1;
  pointer-events: all;
  transform: translate(-50%, 0);
}
header .submenu > div { gap: 0; }
header .submenu a {
  display: inline-block;
  padding: 11px 16px;
}
@media (min-width: 768px) {
  header .submenu { display: none; }
}

.submenu-toggle {
  position: fixed;
  top: 0;
  left: 0;
  pointer-events: none;
  width: 100%;
  opacity: 0;
  height: 100svh;
}
.submenu-toggle.show { pointer-events: all; }
@media (min-width: 768px) {
  .submenu-toggle { display: none; }
}

/* ---------- dividers ---------- */

.divider-dot {
  width: 100%;
  height: 1px;
  background-image: linear-gradient(to right, var(--color-fg) 50%, rgba(255, 255, 255, 0) 0%);
  background-position: bottom;
  background-size: 4px 1px;
  background-repeat: repeat-x;
}

/* ---------- imagery ---------- */

.img-wrapper {
  width: 100%;
  display: block;
  position: relative;
}
.img-wrapper :is(img, video) {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Crops 5px off the right edge of the animated gif-slot loop specifically
   (originally a GIF, now a muted looping <video> — the selector covers
   both) — widening the image past its wrapper's right edge and letting
   .asp-1's own overflow:hidden clip it, rather than touching the shared
   .img-wrapper img rule every other photo on the site also uses.
   Specificity needs to beat the later .asp-1 img rule (same "class +
   element" weight) below, which would otherwise reset this back to
   width:100% at 768px+ — and the site-wide `img { max-width: 100% }`
   reset needs overriding too, or it clamps this straight back down
   regardless of the width set here. */
.gif-slot .img-wrapper :is(img, video) {
  width: calc(100% + 5px);
  max-width: none;
}

.asp-1 { overflow: hidden; }
.asp-2 { overflow: hidden; }
@media (min-width: 768px) {
  .asp-1 { aspect-ratio: 0.735; }
  .asp-2 { aspect-ratio: 1.42; }
  .asp-1 img, .asp-2 img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
}

/* Desktop image rotation — two stacked layers per position, crossfaded by
   js/image-rotation.js. Fixed aspect-ratio on .asp-1/.asp-2 above means the
   box never resizes; only the layers' opacity ever animates. */
.rotate-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--rotate-fade-ms, 900ms) ease;
  pointer-events: none;
}
.rotate-layer.is-visible { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .rotate-layer { transition: none; }
}

.hover-food .img-wrapper :is(img, video) {
  transition: transform 0.3s cubic-bezier(0.38, 0.005, 0.215, 1);
}

/* At desktop widths this wrapper is display:contents (invisible to the box
   model), so its children remain direct flex items of .row — the 3
   independently-rotating grid tiles handled by js/image-rotation.js. On
   mobile the whole thing is replaced by the swipeable galleries below. */
.mobile-slider { display: contents; }
@media (max-width: 767.98px) {
  .mobile-slider { display: none; }
}

/* ---------- mobile image sliders (portrait gallery, landscape gallery) ---------
   Populated by js/mobile-image-slider.js from the same manifest.json the
   desktop rotation uses. Desktop-hidden via .d-md-none; on mobile the two
   galleries plus the GIF are reordered below the info column via `order`. */
.mobile-portrait-slider,
.mobile-landscape-slider {
  display: none;
}
@media (max-width: 767.98px) {
  .row > .gif-slot { order: 3; }
  .row > .mobile-portrait-slider { order: 1; }
  .row > .mobile-landscape-slider { order: 2; }

  .mobile-portrait-slider,
  .mobile-landscape-slider {
    display: block;
    width: 100%;
    padding-top: 30px;
  }
  /* Editorial offset: the top image is centred at its original (asymmetric)
     width; the middle (landscape) image runs fully edge-to-edge; the GIF
     keeps the doubled, symmetric gutter. Vertical gaps between the three
     are 25% more than the base 30px (37.5px).
     Previous versions, in case we want to go back:
       — before the full-bleed middle image: .mobile-landscape-slider { padding-left: 60px; padding-right: 15px; padding-top: 30px; }
         and .row > .gif-slot had padding-top: 30px (from the .pt-30 utility, unoverridden).
       — before centring the top image: .mobile-portrait-slider { padding-left: 15px; padding-right: 60px; } (hugged the left edge) */
  .mobile-portrait-slider { padding-left: 37.5px; padding-right: 37.5px; }
  .mobile-landscape-slider {
    /* Negative margin cancels the row's own 15px structural gutter (from
       .container-fluid.px-30's 30px minus .row's -15px) so this reaches
       the true viewport edge, not just its own zeroed padding. The extra
       30px of width makes up for what the negative margins pull it past —
       margin doesn't add to the box's own width, only its position. */
    width: calc(100% + 30px);
    padding-left: 0;
    padding-right: 0;
    margin-left: -15px;
    margin-right: -15px;
    padding-top: 37.5px;
  }
  .row > .gif-slot { padding-left: 45px; padding-right: 45px; padding-top: 37.5px; }
  .mobile-slider-scroll {
    width: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .mobile-slider-scroll::-webkit-scrollbar {
    display: none;
  }
  .mobile-slider-track {
    display: flex;
  }
  .mobile-slider-item {
    flex: 0 0 100%;
    scroll-snap-align: start;
    scroll-snap-stop: always;
    overflow: hidden;
  }
  .mobile-portrait-slider .mobile-slider-item { aspect-ratio: 0.735; }
  .mobile-landscape-slider .mobile-slider-item { aspect-ratio: 1.42; }
  .mobile-slider-item img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    -webkit-user-drag: none;
    user-select: none;
  }
  .mobile-slider-scroll { cursor: grab; }
  .mobile-slider-scroll.is-dragging {
    cursor: grabbing;
    scroll-snap-type: none;
  }
}
@media (min-width: 1070px) {
  .hover-food:hover .img-wrapper :is(img, video) { transform: scale(1.05); }
}

/* ---------- scroll-in animations ---------- */

.animate-on-scroll {
  opacity: 0;
  will-change: scroll-position;
}
.animate-on-scroll.animate {
  animation-name: fade-in;
  animation-fill-mode: forwards;
  animation-duration: 1s;
  transition-timing-function: var(--ease-standard);
}
@media (max-width: 1069.98px) {
  .animate-on-scroll.animate { animation-duration: 0.8s; }
}

.zoom-out { pointer-events: none; }
.zoom-out img:not(.rotate-layer) { opacity: 0; transform: scale(1.05); }
.zoom-out.animate { pointer-events: auto; }
.zoom-out.animate img:not(.rotate-layer) {
  transform: scale(1);
  animation-name: zoom-out;
  animation-fill-mode: forwards;
  transition-timing-function: cubic-bezier(0.38, 0.005, 0.215, 1);
  animation-duration: 0.8s;
}
/* The rotating positions still get the scale-in reveal (matching every other
   .zoom-out tile), just without the opacity fight against .rotate-layer's
   own crossfade opacity. */
.zoom-out.img-rotate .img-wrapper { transform: scale(1.05); }
.zoom-out.img-rotate.animate .img-wrapper {
  transform: scale(1);
  transition: transform 0.8s cubic-bezier(0.38, 0.005, 0.215, 1);
}

@keyframes fade-in {
  0% { opacity: 0; -webkit-backface-visibility: hidden; }
  to { opacity: 1; -webkit-backface-visibility: hidden; }
}
@keyframes zoom-out {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

/* ---------- sticky hero logo ---------- */

.container-logo {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  pointer-events: none;
  display: none;
  z-index: 99;
}
@media (min-width: 1070px) {
  .container-logo { display: block; }
}
.container-logo .sticky-home {
  position: sticky;
  top: 0;
  display: flex;
  justify-content: center;
  transition: padding-top 0.4s var(--ease-standard);
  padding-top: 415px;
}
.container-logo .sticky-home.is-pinned {
  padding-top: 260px;
}
.pr-perro-logo {
  will-change: scroll-position;
  width: 100%;
  height: auto;
}

/* Mobile: the logo strip is not fixed below 1070px (container-logo
   is display:none) — the logo simply isn't rendered there. */

/* ---------- status ticker ---------- */

.ticker-bar {
  width: 100%;
  height: 36px;
  overflow: hidden;
  background: #0A0A0A;
  display: flex;
  align-items: center;
}

.ticker-bar__track {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  will-change: transform;
  animation-name: ticker-scroll;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

@keyframes ticker-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .ticker-bar__track { animation-play-state: paused; }
}

.ticker-unit {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 0 24px;
  font-family: "Departure Mono", monospace;
  font-size: 14px;
  line-height: 21px;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  color: var(--accent-lime);
  white-space: nowrap;
}

.ticker-play {
  display: inline-block;
}
.ticker-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent-lime);
  animation-name: ticker-blink;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  animation-duration: 1s;
}
.ticker-dot--static {
  animation: none;
  opacity: 1;
}
@keyframes ticker-blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.15; }
}

/* ---------- footer ---------- */

footer { position: relative; }

@media (max-width: 1069.98px) {
  footer { height: auto !important; }
}

/* The footer always shows the inverse of the current site theme: dark-site
   -> light footer, light-site -> dark footer (matching the main site's own
   dark palette one-for-one). */
footer {
  --footer-bg: #B2C703;
  --footer-fg: #1C1917;
}
:root[data-theme="light"] footer {
  --footer-bg: #1C1917;
  --footer-fg: #B2C703;
}

.footer-black {
  background: var(--footer-bg);
  color: var(--footer-fg);
  padding-top: clamp(40px, 4.5vw, 75px);
  overflow: hidden;
  position: relative;
  isolation: isolate;
}

.footer-nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(30px, 9vw, 160px);
  padding: 0 30px;
  text-transform: none;
  font-size: clamp(14px, 1.5vw, 24px);
  line-height: 1.5;
}
.footer-nav a { color: var(--footer-fg); }
@media (min-width: 1070px) {
  .footer-nav {
    gap: clamp(24px, 7vw, 120px);
    font-size: clamp(14px, 1.25vw, 20px);
  }
}
@media (max-width: 767.98px) {
  .footer-nav {
    flex-wrap: nowrap;
    gap: clamp(22px, 7vw, 30px);
    padding: 0 16px;
    font-size: 15px;
  }
}

.footer-logo-giant {
  position: relative;
  margin-top: clamp(45px, calc(7vw + 5px), 128px);
  padding-bottom: clamp(60px, 14vw, 250px);
}
@media (max-width: 767.98px) {
  .footer-logo-giant {
    margin-top: 64px;
    padding-bottom: 150px;
  }
}
.footer-logo-giant img {
  display: block;
  width: 106%;
  max-width: none;
  margin-left: -3%;
  height: auto;
  filter: none;
}
.footer-logo-giant.is-waving img { filter: url(#logo-ripple-filter); }

.saturation-knob {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 8px;
}
/* A plain wrapper carries the enlarged hit area — SVG root elements don't
   reliably render/hit-test ::before in every browser, so the trick (same
   one .menu-dots-btn::before uses) goes on this div instead of the <svg>. */
.saturation-knob__hit {
  position: relative;
  display: flex;
  touch-action: none;
}
.saturation-knob__hit::before {
  content: "";
  position: absolute;
  inset: -16px;
}
.saturation-knob__dial {
  width: 40px;
  height: 40px;
  cursor: ns-resize;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}
.saturation-knob__track {
  fill: none;
  stroke: var(--footer-fg);
  stroke-opacity: 0.25;
  stroke-width: 2;
}
.saturation-knob__arc {
  fill: none;
  stroke: var(--accent-lime);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 2 3;
}
.saturation-knob__pointer {
  stroke: var(--accent-dark);
  stroke-width: 1.5;
  stroke-linecap: round;
}
.saturation-knob__value {
  font-family: "Departure Mono", monospace;
  font-size: 11px;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--footer-fg);
}

@media (max-width: 767.98px) {
  /* the wrapper's own flex/positioning is only for desktop's "sit next to
     the knob" layout — at mobile the knob and switch split apart (knob
     shifts up, switch drops to the credit line), so each escapes to
     <footer>'s own positioning context individually, same trick
     .footer-credit uses below for its year/studio spans. */
  .footer-controls {
    position: static;
    display: contents;
  }
  .saturation-knob {
    position: absolute;
    left: 50%;
    bottom: 90px;
    transform: translateX(-50%);
  }
  .saturation-knob__dial { width: 60px; height: 60px; }
  .saturation-knob__value { font-size: 15px; }
  .tex-switch {
    position: absolute;
    left: 50%;
    bottom: 16px;
    transform: translateX(-50%);
  }
  /* CSS transforms on SVG shape children resolve in the viewBox's own user
     units, not rendered CSS pixels — the desktop translateX(19px) already
     holds correctly at this larger rendered size, no override needed. */
  .tex-switch__track { width: 46px; height: 23px; }
}

/* ---------- footer credit ---------- */

.footer-credit {
  position: absolute;
  right: 10px;
  bottom: 10px;
  z-index: 2;
  display: flex;
  gap: 6px;
  font-family: "Departure Mono", monospace;
  font-size: 11px;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--footer-fg);
}
.footer-credit__studio { color: var(--footer-fg); }

@media (max-width: 767.98px) {
  .footer-credit {
    display: contents;
  }
  .footer-credit__year,
  .footer-credit__studio {
    position: absolute;
    bottom: 16px;
    font-size: 13px;
  }
  .footer-credit__year { left: 16px; }
  .footer-credit__studio { right: 16px; }
}

/* ---------- ascii footer logo ---------- */

.ascii {
  font-family: "Departure Mono", monospace;
  text-transform: uppercase;
  font-size: 14px;
  line-height: 18.2px;
  letter-spacing: -0.01em;
  width: 100%;
  margin: 0;
  overflow: visible;
}
@media (min-width: 768px) {
  .ascii {
    font-size: 20px;
    line-height: 26px;
  }
}
@media (min-width: 1070px) {
  .ascii {
    font-size: 25px;
    line-height: 32.5px;
  }
}
@media (max-width: 767.98px) {
  .ascii {
    margin-left: -3px;
    text-align: center;
  }
}
/* ---------- idle ascii flame overlay ---------- */

.container-pre {
  position: fixed;
  z-index: 100;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background-color: var(--color-bg);
  transition: opacity 3s var(--ease-standard);
  opacity: 0;
  pointer-events: none;
}
.container-pre pre {
  background-color: var(--color-bg);
  color: var(--color-fg);
  font-family: "Departure Mono", monospace;
  font-size: 14px;
  line-height: 14px;
  margin: 0;
  position: absolute;
  width: 100%;
  top: 50px;
  bottom: calc(var(--idle-logo-h, 0px) + 10px);
  left: 0;
  pointer-events: none;
}
.container-pre.show {
  transition: opacity 1.5s var(--ease-standard);
  pointer-events: all;
  opacity: 1;
}
.container-pre.show pre { pointer-events: none; }

.ascii-idle-logo {
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 104%;
  max-width: none;
  height: auto;
  pointer-events: none;
}

/* ---------- static print grain, shared by every page ---------- */

body::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 9999; /* Above page artwork; below theme controls and transitions. */
  pointer-events: none;
  opacity: 0.85;
  mix-blend-mode: soft-light;
  background-image: url("../assets/images/print-grain.svg");
  /* Tiled at one viewport-height per repeat, not stretched to the full
     scrollable page — rendering the SVG turbulence filter across an entire
     multi-thousand-pixel-tall page in one stretch is expensive enough to
     make the browser drop paint on long pages (a blank flat-colour band
     appears partway down). Tiling keeps each render the cost of one
     viewport while still scrolling with the page instead of sitting fixed
     on top of it. */
  background-size: 100% 100vh;
  background-repeat: repeat-y;
}

@media print {
  body::after { display: none; }
}

/* Content-level way home for the interior pages, separate from navigation. */
.page-top-divider { padding-top: 100px; }
.page-home-link {
  display: flex;
  align-items: center;
  width: fit-content;
  min-height: 44px;
  margin-bottom: 24px;
  color: var(--color-fg);
}
.page-home-link:hover { text-decoration: underline; text-underline-offset: 5px; }
.page-home-link:focus-visible { outline: 2px solid currentColor; outline-offset: 4px; }

/* ---------- menu page ---------- */

.menu-image-col { text-align: center; }

.menu-image {
  display: inline-block;
  width: 100%;
  max-width: 900px;
  height: auto;
  box-shadow: 0 4px 40px rgba(0, 0, 0, 0.35);
  /* Starts hidden — the src attribute in the HTML is a bundled fallback
     that's often stale, so js/menu-upload.js only reveals this once it's
     confirmed (and, for the live preview, actually finished loading) what
     should really be shown. Without this, the stale fallback flashes on
     screen for the fraction of a second the fetch takes. */
  opacity: 0;
  transition: opacity 300ms ease;
}
.menu-image.is-ready { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .menu-image { transition: none; }
}

/* ---------- faq page ---------- */

.page-faq p { line-height: 1.6; }
