  /* BD Plakatbau (Büro Destruct / Typedifferent.com) — user-provided font
     file, fonts/BD_Plakatbau.otf. Free-tier license: personal/non-commercial
     use is unrestricted, but the foundry's EULA (fonts/ — see the .otf's
     original zip) asks that commercial use be cleared with them first and
     that Büro Destruct be credited as the font's designer wherever design
     credits are shown. This site promotes paid photography work, so that's
     worth acting on, not just noting here. No build step needed for this —
     @font-face works directly off a local file path, unlike the Google
     Fonts families elsewhere in this project which need their own <link>
     in each page's <head>. */
  @font-face{
    font-family:'BD Plakatbau';
    src:url('fonts/BD_Plakatbau.otf') format('opentype');
    font-weight:400;
    font-style:normal;
    font-display:swap;
  }
  :root{
    --dawn-gold:#e5e1d8;
    --dawn-gold-dim:#d2cdc5;
    --dusk-ember:#d2cdc5;
    --dusk-ember-dim:#b3ada5;
    --twilight-violet:#b3ada5;
    --twilight-violet-dim:#b3ada5;
    --night-void:#b3ada5;
    --night-void-2:#b3ada5;
    --stage-cyan:#b3ada5;
    --stage-magenta:#b3ada5;
    --paper:#f4f3ef;
    --paper-glass:rgba(244,243,239,.82);
    /* was #b3ada5 — only ~2:1 contrast against --paper, well under the
       WCAG AA minimum (4.5:1) for the site's default body text color.
       This warm dark gray keeps the same desaturated family but actually
       reads (~7.7:1). */
    --ink:#4a453f;
    /* Brand mark's own face (.brand, footer .fbrand) — BD Terminal,
       loaded via the Adobe Fonts kit <link> in each page's <head>
       (use.typekit.net/wum5vzv.css). Anton kept as fallback in case the
       Typekit request fails; separate identity decision from section
       headlines. */
    --font-display:'bd-terminal-vf', 'Anton', sans-serif;
    /* .sec-title ("Outdoor", "Portraits", "In the studio"...) — Rubik
       Glitch Regular, from Google Fonts (family=Rubik+Glitch, loaded in
       every page's <head> alongside Anton/Space Mono/Bricolage
       Grotesque). Only ships as weight 400, so .sec-title's font-weight
       matches that below. Falls back to Brush Up, then Damson Black
       (the two previous picks) if the Google Fonts request fails. */
    --font-section:'Rubik Glitch', 'brush-up', 'damson', sans-serif;
    /* Cabinet Grotesk replaces Fraunces: taste-skill flags Fraunces as one
       of the two most-overused "AI creative brief" serifs, and this is a
       sans display font from its own recommended pool — closer to the
       "tech but elegant" nightlife/multimedia-creator vibe than an
       editorial serif, while still supporting italic for the same emphasis
       moves (.mini-hero-title, .sec-caption) the site already relies on */
    --font-body:'Cabinet Grotesk', sans-serif;
    /* Space Mono replaces JetBrains Mono for every label/eyebrow/timestamp
       on the site — JetBrains Mono reads as a neutral code-editor font;
       Space Mono has real character (its quirky 'a' and 'g') while staying
       just as legible at label sizes, and its slightly retro-tech feel
       still fits the nightlife/multimedia-creator identity. */
    --font-mono:'Space Mono', monospace;
    /* shared strong ease-out curve for entering/exiting UI elements — built-in
       CSS easings are too weak to feel intentional (Emil Kowalski animation
       standards); reused everywhere instead of one-off cubic-beziers */
    --ease-out-strong:cubic-bezier(.16,1,.3,1);
    /* grain texture used by .lava-mass::after. feColorMatrix desaturates
       the raw turbulence: feTurbulence generates each RGBA channel
       independently by default, so without it this is literally colored
       static — blended on top of the lava blobs that showed up as random
       hue fringing ("chromatic aberration"). Coarser/higher-octave than
       .hero-grain's own copy — bigger grain clumps with more fractal detail
       help mask the feDisplacementMap's own smooth "wave" banding instead
       of a fine, mechanically-even speckle. */
    --grain-noise-coarse-svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.55' numOctaves='4' stitchTiles='stitch' result='t'/%3E%3CfeColorMatrix in='t' type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
    --grain-dark:#1c1a17;
    --grain-mid:#57514a;
    /* subtle halo behind white text sitting on .lava-mass (header/nav,
       section titles, captions...) so it stays legible over the lava's own
       pale areas without looking like a hard outline — see the header
       comment further down for why this replaced mix-blend-mode:difference. */
    --text-halo:0 1px 2px rgba(28,26,23,.35), 0 0 6px rgba(28,26,23,.22);
  }
  *{margin:0;padding:0;box-sizing:border-box;}
  html{scroll-behavior:smooth;}
  body{
    font-family:var(--font-body);
    background:var(--paper-glass);
    color:var(--ink);
    overflow-x:hidden;
  }
  ::selection{background:var(--stage-magenta);color:white;}

  /* ---------- casual download deterrence, not real protection: blocks
     drag-to-desktop, the mobile long-press "save image" menu, and text-
     selection dragging a photo along with it. Doesn't stop anyone who
     opens dev tools, views page source, or just takes a screenshot — there
     is no client-side technique that can. */
  img, video{
    -webkit-user-drag:none;
    user-drag:none;
    -webkit-touch-callout:none;
    user-select:none;
    -webkit-user-select:none;
  }

  /* ---------- lava-mass: a single, page-wide layered grunge-lava background
     — one continuous texture behind header, every content section, AND the
     footer, instead of separate per-section islands. Fixed behind the whole
     page (tattoo.html / nightlife.html / contacto.html only). Three real
     layers, in paint order:
       1. base — solid pale (var(--paper)) background-color, the same tone
          the rest of the page already sits on.
       2. .lava-mass-blobs — two heavily blurred radial-gradient blobs (dark
          near-black "hot spot", medium-dark secondary mass), each
          independently drifting via its own CSS transform keyframes. The
          whole wrapper is ALSO run through an SVG filter (#lava-filter-page,
          defined inline in each page's HTML, not in this stylesheet)
          combining an animated feTurbulence with feDisplacementMap, so the
          blob edges visibly churn and warp over time instead of sliding as
          rigid circles — this is what earns the word "lava" instead of
          "gradient".
       3. .lava-mass::after — the fine feTurbulence grain/stipple already
          used elsewhere in this file, blended on top and slowly panned via
          background-position, independent of the blob motion.
     Colors are pale base / dark blobs — matching the rest of the page's
     existing light --ink-on-paper palette, so content sections no longer
     need their own separate dark background or text-color override. ---------- */
  .lava-mass{
    position:fixed;
    inset:0;
    z-index:-1;
    overflow:hidden;
    pointer-events:none;
    background:var(--paper);
    /* base/settle state for the page-exit/page-enter flash below — same
       "ease back down" duration+curve as .hero-bg img's own settle */
    filter:brightness(1);
    transition:filter .45s ease-out;
  }
  .lava-mass-blobs{
    position:absolute;
    inset:0;
    filter:url(#lava-filter-page);
  }
  .lava-mass-blobs::before,
  .lava-mass-blobs::after{
    content:'';
    position:absolute;
    border-radius:50%;
    will-change:transform;
  }
  .lava-mass-blobs::before{
    width:90vmax;height:90vmax;
    top:-30%;left:-25%;
    filter:blur(110px);
    /* several intermediate stops instead of a single hard 0%-to-transparent
       falloff — a plain two-stop radial-gradient has a near-linear alpha
       ramp that reads as a distinct "ring" once feDisplacementMap warps it;
       staggering the falloff rate breaks that ring into a softer, more
       irregular gradient before the turbulence even touches it */
    background:radial-gradient(circle, var(--grain-dark) 0%, rgba(28,26,23,.75) 22%, rgba(28,26,23,.4) 40%, rgba(28,26,23,.12) 55%, rgba(28,26,23,0) 68%);
    animation:lava-drift-a 52s ease-in-out infinite;
  }
  .lava-mass-blobs::after{
    width:65vmax;height:65vmax;
    bottom:-20%;right:-15%;
    filter:blur(100px);
    background:radial-gradient(circle, var(--grain-mid) 0%, rgba(87,81,74,.7) 25%, rgba(87,81,74,.35) 45%, rgba(87,81,74,0) 70%);
    opacity:.85;
    animation:lava-drift-b 41s ease-in-out infinite;
  }
  /* mobile: drop the feDisplacementMap warp entirely and shrink the blur
     radius. The live turbulence-driven displacement is the single most
     expensive thing running on this page — it recomputes a full-viewport
     pixel warp every frame — and combined with a 100-110px blur it was
     enough to make scrolling feel heavy on weaker phone GPUs. The blobs
     still drift and blur, just without the organic edge-churn, which is a
     small enough visual trade for a background that's supposed to be a
     backdrop, not the thing fighting you for frame budget. */
  @media (max-width:768px){
    .lava-mass-blobs{filter:none;}
    .lava-mass-blobs::before{filter:blur(48px);}
    .lava-mass-blobs::after{filter:blur(44px);}
  }
  @keyframes lava-drift-a{
    0%{transform:translate(0,0) scale(1);}
    22%{transform:translate(14vw,10vh) scale(1.1);}
    48%{transform:translate(6vw,22vh) scale(.94);}
    71%{transform:translate(-12vw,9vh) scale(1.06);}
    100%{transform:translate(0,0) scale(1);}
  }
  @keyframes lava-drift-b{
    0%{transform:translate(0,0) scale(1);}
    30%{transform:translate(-16vw,-12vh) scale(1.16);}
    63%{transform:translate(-7vw,-22vh) scale(.88);}
    100%{transform:translate(0,0) scale(1);}
  }
  .lava-mass::after{
    content:'';
    position:absolute;
    inset:-10%;
    pointer-events:none;
    background-image:var(--grain-noise-coarse-svg);
    background-size:90px 90px;
    /* soft-light instead of overlay: overlay pivots hard at 50% luminance
       per channel, which traced a visible ring/"wave" along the blob
       gradient's own 50%-luma contour once warped — soft-light has no such
       discontinuity but still reveals grain differently in light vs dark
       areas */
    mix-blend-mode:soft-light;
    opacity:.7;
    animation:lava-grain-pan 55s linear infinite;
  }
  @keyframes lava-grain-pan{
    from{background-position:0 0;}
    to{background-position:135px 90px;}
  }
  @media (prefers-reduced-motion: reduce){
    .lava-mass-blobs::before,
    .lava-mass-blobs::after,
    .lava-mass::after{animation:none;}
  }

  /* ---------- rail (signature element: sun-to-stage-light travel) ---------- */
  /* ---------- minimal page-opener: small type hierarchy + one wide random photo,
     replaces the old oversized section-divider title on tattoo.html/nightlife.html ---------- */
  .mini-hero{
    padding-top:140px;
    padding-bottom:24px;
  }
  .mini-hero-text{
    max-width:640px;
    margin-bottom:24px;
  }
  .mini-hero-eyebrow{
    /* an <h1> now (each page's only one) but styled identically to before
       — explicit weight/margin reset so it doesn't pick up the browser's
       default bold-and-spaced h1 styling. */
    font-family:var(--font-mono);
    font-weight:400;
    font-size:12px;
    letter-spacing:.15em;
    text-transform:uppercase;
    color:var(--ink);
    opacity:.9;
    margin:0 0 16px;
  }
  .mini-hero-title{
    font-family:var(--font-section);
    font-weight:400;
    font-size:clamp(28px,4vw,42px);
    line-height:1.15;
    margin-bottom:16px;
  }
  .mini-hero-caption{
    max-width:480px;
    font-size:15px;
    line-height:1.6;
    opacity:.8;
  }
  @media (max-width:640px){
    .mini-hero{padding-top:110px;}
  }

  /* ---------- bio (contacto.html only) ---------- */
  .bio{
    display:flex;
    align-items:center;
    gap:60px;
    padding-bottom:60px;
  }
  .bio-media{
    flex:0 0 34%;
  }
  .bio-photo{
    position:relative;
    aspect-ratio:4/5;
  }
  /* the jpg is the full photo, the png the same shot cut out to a
     transparent-background silhouette, stacked on top of it. Both start
     heavily blurred into an indistinct haze; hovering drops the blur on
     both at once, so the portrait reads as unlocking into focus rather than
     just "an image sharpening". */
  .bio-photo-base,
  .bio-photo-cutout{
    position:absolute;
    inset:0;
    width:100%;
    height:100%;
    object-fit:cover;
    filter:blur(28px);
    transition:filter .5s var(--ease-out-strong);
    will-change:filter;
  }
  .bio-photo-base{
    z-index:1;
    /* dissolves toward the edges instead of ending in a hard photo
       rectangle — same ellipse-mask trick .hero-bg uses to blend into the
       lava field — and sits at partial opacity so it reads as a soft glow
       behind the cutout rather than a second, competing photo. Small solid
       core (15%) means almost the whole layer is already fading out. */
    -webkit-mask-image:radial-gradient(ellipse 60% 60% at 50% 42%, #000 15%, transparent 100%);
    mask-image:radial-gradient(ellipse 60% 60% at 50% 42%, #000 15%, transparent 100%);
    opacity:.7;
  }
  .bio-photo-cutout{
    z-index:2;
    /* two masks intersected: the wide radial keeps the top/sides mostly
       solid while softening the rectangle's corners, the linear one fades
       out the last stretch at the bottom so the frame's hard crop (right
       where a hand/arm gets cut off) reads as a dissolve instead of an
       edge. -webkit-mask-composite's single value covers this since
       there's only one layer being composited onto the base layer. */
    -webkit-mask-image:radial-gradient(ellipse 88% 88% at 50% 45%, #000 68%, transparent 100%), linear-gradient(to bottom, #000 62%, transparent 96%);
    mask-image:radial-gradient(ellipse 88% 88% at 50% 45%, #000 68%, transparent 100%), linear-gradient(to bottom, #000 62%, transparent 96%);
    -webkit-mask-composite:source-in;
    mask-composite:intersect;
    transform:scale(1);
    transition:filter .5s var(--ease-out-strong), transform .5s var(--ease-out-strong);
    will-change:filter, transform;
  }
  .bio-photo:hover .bio-photo-base,
  .bio-photo:hover .bio-photo-cutout{filter:blur(0);}
  /* "selection" zoom on just the sharp cutout layer on hover — the soft
     jpg backdrop stays put, so the pop reads as the subject stepping
     forward instead of the whole composition growing */
  .bio-photo:hover .bio-photo-cutout{transform:scale(1.06);}

  .bio-text{flex:1 1 0;}
  .bio-text .sec-title{margin-bottom:20px;}
  .bio-text p{
    font-size:16px;
    line-height:1.7;
    margin-bottom:18px;
    max-width:560px;
  }
  .bio-text p:last-child{margin-bottom:0;}
  .bio-cta{
    font-style:italic;
    font-weight:500;
    opacity:.9;
  }
  @media (max-width:820px){
    .bio{flex-direction:column;align-items:stretch;gap:32px;}
    .bio-media{flex:none;width:100%;max-width:320px;margin:0 auto;}
  }

  /* ---------- contacto ---------- */
  .contacto{
    min-height:70vh;
    display:flex;
    flex-direction:column;
    justify-content:center;
  }
  .contacto .sec-title{margin-bottom:24px;}

  .rail{
    position:fixed;
    right:18px;
    top:6vh;
    height:88vh;
    width:2px;
    background:linear-gradient(to bottom,
      var(--dawn-gold) 0%,
      var(--dusk-ember) 22%,
      var(--twilight-violet) 45%,
      var(--night-void) 68%,
      var(--night-void) 100%);
    z-index:60;
    opacity:0.55;
  }
  .rail-dot{
    position:absolute;
    left:50%;
    top:0%;
    width:14px;height:14px;
    border-radius:50%;
    transform:translate(-50%,-50%);
    background:var(--dawn-gold);
    box-shadow:0 0 14px 4px var(--dawn-gold);
    transition:background .4s ease, box-shadow .4s ease;
  }
  .rail-label{
    position:absolute;
    right:14px;
    font-family:var(--font-mono);
    font-size:10px;
    letter-spacing:.05em;
    color:var(--ink);
    opacity:.45;
    writing-mode:vertical-rl;
    white-space:nowrap;
  }
  @media (max-width:820px){ .rail{display:none;} }

  /* ---------- nav ---------- */
  header{
    position:fixed;top:0;left:0;right:0;
    display:flex;justify-content:space-between;align-items:center;
    padding:22px 32px;
    z-index:50;
  }
  /* Contrast against whatever's behind (photo, lava, logo carousel) used to
     come from mix-blend-mode:difference on each text element. Dropped
     everywhere on the site: it depends on compositor/blend-mode support
     that isn't reliable across every mobile browser — specifically,
     Instagram's in-app browser (WKWebView with its own restrictions) was
     seen rendering this text as flat white with no inversion, which reads
     fine over the lava's dark blobs but washes out to near-invisible over
     its pale areas. text-shadow has no such dependency — universally
     supported — so a dark halo behind fixed-white text does the same job:
     the shadow all but disappears over dark backdrops (nothing to show
     against) and becomes a visible outline over pale ones, keeping the
     letterforms legible either way. */
  .brand{
    font-family:var(--font-display);
    font-size:20px;
    letter-spacing:.04em;
    color:white;
    white-space:nowrap;
    text-shadow:var(--text-halo);
    text-decoration:none;
    transition:opacity 220ms var(--ease-out-strong), transform 220ms var(--ease-out-strong);
  }
  html.brand-hidden .brand{
    opacity:0;
    transform:translateY(-10px);
    pointer-events:none;
  }
  nav{display:flex;align-items:center;}
  nav a{
    font-family:var(--font-mono);
    font-size:11px;
    letter-spacing:.08em;
    text-transform:uppercase;
    color:white;
    text-decoration:none;
    margin-left:24px;
    opacity:1;
    display:inline-block;
    text-shadow:var(--text-halo);
    transition:opacity .2s ease, transform 160ms var(--ease-out-strong);
  }
  nav a:hover{opacity:.5;}
  nav a:active{transform:scale(.94);}

  /* mobile hamburger: hidden on desktop; header can't fit brand + 4 links +
     lang switch on one line below ~768px, so those collapse into a
     slide-in drawer instead of wrapping/overlapping */
  .nav-toggle{
    display:none;
    flex-direction:column;
    align-items:center;
    justify-content:center;
    gap:5px;
    width:38px;
    height:38px;
    background:rgba(10,9,8,.4);
    border-radius:50%;
    border:none;
    padding:0;
    cursor:pointer;
    z-index:60;
  }
  /* a solid backdrop instead of the header's usual per-element
     difference+contrast trick: those three bars are thin enough (2px)
     that over a busy photo (index.html's hero, tattoo/nightlife gallery
     rows) each one sits on different pixels and differences to a
     different tone, so the icon reads as broken/flickery instead of one
     shape — a real backdrop guarantees the same plain white regardless
     of what's behind it. */
  .nav-toggle span{
    display:block;
    width:16px;
    height:2px;
    background:white;
    transition:transform .25s var(--ease-out-strong), opacity .2s ease;
  }
  html.nav-open .nav-toggle span:nth-child(1){transform:translateY(7px) rotate(45deg);}
  html.nav-open .nav-toggle span:nth-child(2){opacity:0;}
  html.nav-open .nav-toggle span:nth-child(3){transform:translateY(-7px) rotate(-45deg);}
  .nav-scrim{
    position:fixed;inset:0;
    background:rgba(10,9,8,.5);
    z-index:55;
    opacity:0;
    pointer-events:none;
    transition:opacity .26s ease;
  }
  html.nav-open .nav-scrim{opacity:1;pointer-events:auto;}

  /* ---------- language switch: EN/ES toggle living inside header's nav,
     same text-shadow halo as nav a above ---------- */
  .lang-switch{
    display:flex;
    align-items:center;
    gap:8px;
    margin-left:24px;
    padding-left:24px;
    border-left:1px solid rgba(255,255,255,.3);
  }
  .lang-switch button{
    font-family:var(--font-mono);
    font-size:11px;
    letter-spacing:.08em;
    color:white;
    background:none;
    border:none;
    padding:0;
    cursor:pointer;
    opacity:.4;
    text-shadow:var(--text-halo);
    transition:opacity .2s ease, transform 160ms var(--ease-out-strong);
  }
  .lang-switch button.active{opacity:1;}
  .lang-switch button:hover{opacity:.75;}
  .lang-switch button:active{transform:scale(.9);}

  @media (max-width:768px){
    header{padding:18px 20px;}
    .nav-toggle{display:flex;}
    nav{
      position:fixed;top:0;right:0;
      height:100vh;
      width:min(78vw, 320px);
      flex-direction:column;
      align-items:flex-start;
      justify-content:center;
      padding:32px;
      background:rgba(20,18,16,.97);
      transform:translateX(100%);
      transition:transform .26s var(--ease-out-strong);
      will-change:transform;
      z-index:56;
    }
    html.nav-open nav{transform:translateX(0);}
    nav a{
      mix-blend-mode:normal;
      filter:none;
      color:var(--paper);
      margin-left:0;
      width:100%;
      font-size:16px;
      padding:14px 0;
      border-bottom:1px solid rgba(244,243,239,.12);
      opacity:0;
      transform:translateX(14px);
      transition:opacity .28s ease, transform .28s var(--ease-out-strong);
    }
    html.nav-open nav a{opacity:1;transform:translateX(0);}
    nav a:nth-child(1){transition-delay:.05s;}
    nav a:nth-child(2){transition-delay:.09s;}
    nav a:nth-child(3){transition-delay:.13s;}
    nav a:nth-child(4){transition-delay:.17s;}
    .lang-switch{
      mix-blend-mode:normal;
      margin-left:0;
      margin-top:20px;
      padding-left:0;
      border-left:none;
      opacity:0;
      transform:translateX(14px);
      transition:opacity .28s ease, transform .28s var(--ease-out-strong);
      transition-delay:.21s;
    }
    html.nav-open .lang-switch{opacity:1;transform:translateX(0);}
    .lang-switch button{color:var(--paper);filter:none;}
  }

  section{
    position:relative;
    padding:90px 8vw 70px;
    overflow:hidden;
  }
  @media (max-width:640px){
    section{padding:56px 6vw 44px;}
  }

  /* ---------- hero / masthead: full-bleed monochrome photo, swapped live by
     hovering "Nightlife" / "Tattoo & Piercing" in the top nav. No copy at all
     except the giant "by juanpi" signature bottom-right. ---------- */
  .hero{
    position:relative;
    min-height:100vh;
    display:flex;
    flex-direction:column;
    justify-content:flex-end;
    padding-bottom:64px;
    overflow:hidden;
    color:var(--paper);
    /* solid, dark neutral backdrop — the lava layer is gone from this page,
       so this is what the 65%-opacity hero photo blends against instead */
    background:#141210;
  }
  .hero-bg{
    position:absolute;
    inset:0;
    z-index:2;
    overflow:hidden;
    background:var(--dawn-gold-dim);
    /* the whole photo+scrim+grain stack fades to transparent toward the
       edges, "dissolving" into the lava field behind it instead of sitting
       in a hard rectangle */
    -webkit-mask-image:radial-gradient(ellipse 78% 78% at 50% 42%, #000 56%, transparent 100%);
    mask-image:radial-gradient(ellipse 78% 78% at 50% 42%, #000 56%, transparent 100%);
    /* full-bleed (100% of the viewport), but held at 65% opacity as a whole
       — grunge treatment included — so the moving lava gradient blends
       through the photo itself rather than sitting only around its edges.
       This lives on the wrapper (not the <img>), so it stays fixed through
       every later hover-triggered image swap. */
    opacity:.65;
    will-change:opacity;
  }
  .hero-bg img{
    position:absolute;inset:0;
    width:100%;height:100%;
    object-fit:cover;
    display:block;
    /* dissolving-grunge / photocopy treatment: pushed hard toward pure
       black-and-white so midtones collapse into a graphic, almost-binary
       print rather than a normal photograph */
    filter:grayscale(1) contrast(2.2) brightness(1.1);
    transition:opacity .35s var(--ease-out-strong), filter .45s ease-out;
    will-change:opacity, filter;
  }
  /* light-flash swap: brightness spikes to near-white (masking the <img>'s
     src change), then eases back down to the normal grunge exposure once
     the new photo is in — see swapHeroImage() in index.html. */
  .hero-bg img.flash-out{
    filter:grayscale(1) contrast(1.1) brightness(7);
    /* ease-out, not ease-in: an outgoing/exiting state should still start
       moving immediately, not delay the moment being watched */
    transition:filter .22s var(--ease-out-strong);
  }
  .hero-bg img.flash-in{
    filter:grayscale(1) contrast(1.1) brightness(7);
    transition:none;
  }
  .hero-scrim{
    position:absolute;inset:0;
    background:linear-gradient(180deg, rgba(10,9,7,.1) 0%, rgba(10,9,7,.3) 58%, rgba(10,9,7,.66) 100%);
  }
  .hero-halftone{
    position:absolute;inset:0;
    background-image:radial-gradient(circle, rgba(10,9,7,.95) 34%, transparent 36%);
    background-size:5px 5px;
    opacity:.24;
    mix-blend-mode:multiply;
    pointer-events:none;
  }
  .hero-grain{
    position:absolute;inset:0;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.3' numOctaves='3' stitchTiles='stitch' result='t'/%3E%3CfeColorMatrix in='t' type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
    background-size:110px 110px;
    opacity:.45;
    mix-blend-mode:hard-light;
    pointer-events:none;
  }
  .hero-signature{
    position:relative;
    z-index:2;
    align-self:center;
    text-align:center;
    color:var(--paper);
  }
  .hero-signature-img{
    /* the real signature graphic (traspaso/logos/firma.png — a
       transparent-background brush/ink mark) replaces the typed "by
       juanpi". "screen" blend mode drops its dark tones out entirely —
       letting the photo/lava behind show straight through — while the
       saturated red ink lightens into a translucent wash on top; no
       isolation on any ancestor, so it blends against the whole stack:
       lava particles (z-index:1) behind the photo (z-index:2) behind this. */
    display:block;
    width:min(38vw, 360px);
    height:auto;
    margin:0 auto;
    mix-blend-mode:screen;
    will-change:transform;
  }
  .hero-sublinks{
    /* independent of .hero-signature on purpose: the signature sits at the
       bottom of the hero (.hero's own justify-content:flex-end), but these
       links center in the full viewport regardless of where the signature
       lands — position:absolute inset:0 + flex centering breaks them out
       of .hero's column flow instead of stacking underneath the image. */
    position:absolute;
    inset:0;
    z-index:2;
    display:flex;
    justify-content:center;
    align-items:center;
    gap:22px;
    font-family:var(--font-mono);
    font-size:11px;
    letter-spacing:.12em;
    text-transform:uppercase;
  }
  .hero-sublink{
    color:var(--paper);
    text-decoration:none;
    opacity:.9;
    cursor:pointer;
    display:inline-block;
    text-align:center;
    line-height:1.5;
    transition:opacity .2s ease, transform 160ms var(--ease-out-strong);
  }
  .hero-sublink:hover{opacity:.6;}
  .hero-sublink:active{transform:scale(.94);}
  @media (max-width:640px){
    .hero-signature-img{width:min(58vw, 300px);}
  }

  /* ---------- section header pattern ---------- */
  .sec-head{
    display:flex;
    justify-content:space-between;
    align-items:flex-end;
    gap:24px;
    margin-bottom:56px;
    flex-wrap:wrap;
  }
  @media (max-width:640px){
    .sec-head{margin-bottom:32px;}
  }
  .timestamp{
    font-family:var(--font-mono);
    font-size:13px;
    letter-spacing:.08em;
    color:var(--ink);
    opacity:.9;
  }
  .sec-title{
    font-family:var(--font-section);
    font-weight:400;
    text-transform:uppercase;
    letter-spacing:-.01em;
    font-size:clamp(40px,7vw,88px);
    line-height:.9;
  }
  .sec-caption{
    max-width:340px;
    font-size:15px;
    font-style:italic;
    font-weight:300;
    line-height:1.5;
    opacity:.85;
  }

  /* ---------- contrast for body text over .lava-mass: same text-shadow
     halo as the nav (see header comments above) extended to the rest of
     the page's copy. Plain --ink text was a flat dark gray that
     disappeared whenever one of the lava blobs' own mid-gray blur passed
     behind it; color:white + a dark halo stays legible over both the dark
     blobs and the pale base tone, with no dependency on blend-mode
     compositor support (see the header comment for why that broke in
     Instagram's in-app browser specifically). Excludes .mailbox-form (sits
     inside its own near-opaque card, already has guaranteed contrast) and
     anything on index.html (no .lava-mass there — the hero photo's own
     text already has its own treatment). */
  .sec-title,
  .sec-caption,
  .timestamp,
  .media-block-label,
  .mini-hero-eyebrow,
  .mini-hero-title,
  .mini-hero-caption,
  .bio-text p,
  .bio-cta,
  footer .fbrand,
  footer a,
  footer [data-i18n="footer_role"]{
    color:white;
    text-shadow:var(--text-halo);
  }

  /* ---------- blurred-noise look for the big section titles (OUTDOOR, IN
     THE STUDIO, MERCH...) — layered on top of the shared halo above (same
     specificity, wins on source order). #text-noise-blur (defined inline
     at the top of the page, same pattern as #lava-filter-page) feeds a
     Gaussian blur through fine-grain turbulence so the letters look like
     they're dissolving into static/grain instead of just going soft. */
  .sec-title{
    position:relative;
    color:rgba(255,255,255,.85);
    text-shadow:var(--text-halo);
    filter:url(#text-noise-blur);
  }
  /* a small in-focus core in the middle of the noise: a crisp duplicate of
     the same string (mirrored into data-text by i18n.js, see there) drawn
     on top with no filter, masked down to a small center patch so only
     that little core reads sharp while the rest stays under the blur/noise
     above. Kept deliberately tight/subtle per design direction ("very
     slightly" in focus) rather than a hard sharp/blurry split. */
  .sec-title::before{
    content:attr(data-text);
    position:absolute;
    inset:0;
    filter:none;
    -webkit-mask-image:radial-gradient(circle at 50% 50%, black 0%, black 18%, transparent 55%);
    mask-image:radial-gradient(circle at 50% 50%, black 0%, black 18%, transparent 55%);
    pointer-events:none;
  }

  /* ---------- text reveal (tattoo.html / nightlife.html / contacto.html):
     same paused-then-running pattern as the gallery cascade further down —
     an IntersectionObserver in each page's <script> adds .in-view once the
     element scrolls in, gated behind html.js-ready so this degrades to
     always-visible, non-animated text if JS never runs or the user prefers
     reduced motion. Slides in from the side instead of rising from below —
     each element gets .reveal-from-left/.reveal-from-right in JS, alternating
     by its position in page order, so consecutive blocks visibly alternate
     direction as you scroll down instead of all sliding the same way. What
     makes it read as dynamic instead of flat is the stagger: each heading/
     caption/paragraph also gets its own animation-delay, set in JS from its
     position among sibling text in the same container, so a block like
     eyebrow/title/caption reveals as a quick cascade rather than all at once. */
  html.js-ready .mini-hero-eyebrow,
  html.js-ready .mini-hero-title,
  html.js-ready .mini-hero-caption,
  html.js-ready .timestamp,
  html.js-ready .sec-title,
  html.js-ready .sec-caption,
  html.js-ready .bio-text p{
    animation-duration:.85s;
    animation-timing-function:var(--ease-out-strong);
    animation-fill-mode:forwards;
    animation-play-state:paused;
  }
  html.js-ready .reveal-from-left{animation-name:text-reveal-in-left;}
  html.js-ready .reveal-from-right{animation-name:text-reveal-in-right;}
  html.js-ready .mini-hero-eyebrow.in-view,
  html.js-ready .mini-hero-title.in-view,
  html.js-ready .mini-hero-caption.in-view,
  html.js-ready .timestamp.in-view,
  html.js-ready .sec-title.in-view,
  html.js-ready .sec-caption.in-view,
  html.js-ready .bio-text p.in-view{
    animation-play-state:running;
  }
  @keyframes text-reveal-in-left{
    0%{opacity:0;translate:-100vw 0;}
    100%{opacity:1;translate:0 0;}
  }
  @keyframes text-reveal-in-right{
    0%{opacity:0;translate:100vw 0;}
    100%{opacity:1;translate:0 0;}
  }

  /* ---------- media block: featured grid (random pick) + horizontal scroll for the rest ---------- */
  .media-block{margin-bottom:44px;}
  .media-block:last-child{margin-bottom:0;}
  @media (max-width:640px){
    .media-block{margin-bottom:28px;}
  }
  .media-block-label{
    font-family:var(--font-mono);
    font-size:12px;
    letter-spacing:.1em;
    text-transform:uppercase;
    color:var(--ink);
    opacity:.9;
    margin-bottom:14px;
  }
  .featured-grid{
    display:grid;
    grid-template-columns:repeat(4,1fr);
    gap:14px;
    margin-bottom:16px;
  }
  /* each slot is 3:4 or 4:3; a "stacked" slot holds two landscape panes
     stacked edge-to-edge inside a 3:4 slot so nothing gets left empty */
  .featured-slot{
    position:relative;
    display:flex;
    flex-direction:column;
    gap:8px;
  }
  .featured-slot.aspect-portrait{aspect-ratio:3/4;}
  .featured-slot.aspect-landscape{aspect-ratio:4/3;}
  .featured-slot .gallery-item{
    flex:1 1 0;
    min-height:0;
    width:100%;
    aspect-ratio:auto;
  }
  @media (max-width:980px){
    .featured-grid{grid-template-columns:repeat(2,1fr);}
  }
  @media (max-width:480px){
    .featured-grid{grid-template-columns:1fr 1fr;gap:8px;}
    .featured-slot{gap:6px;}
  }

  /* ---------- horizontal scrolling rows (object-fit:cover, no distortion) ---------- */
  .row-wrap{position:relative;}
  .hscroll{
    display:flex;
    gap:14px;
    overflow-x:auto;
    overflow-y:hidden;
    padding-bottom:12px;
    -webkit-overflow-scrolling:touch;
    scrollbar-width:thin;
    scrollbar-color:var(--dawn-gold-dim) transparent;
    cursor:grab;
    will-change:scroll-position;
    transform:translateZ(0);
  }
  .hscroll.dragging{cursor:grabbing;}
  .hscroll::-webkit-scrollbar{height:6px;}
  .hscroll::-webkit-scrollbar-thumb{background:var(--dawn-gold-dim);border-radius:3px;}
  .hscroll::-webkit-scrollbar-track{background:transparent;}

  .gallery-item{
    position:relative;
    flex:0 0 clamp(170px,18vw,260px);
    aspect-ratio:4/3;
    overflow:hidden;
    background:var(--dawn-gold-dim);
    cursor:pointer;
    /* small, consistent radius — overflow:hidden clips the photo itself to
       match, no extra element needed. Vignette is the third (inset) shadow
       tacked onto the same box-shadow list rather than a new layer: a
       soft, wide inset darkens just the corners/edges, kept subtle since
       it runs on every thumbnail in the grid at once. Both cheap, static,
       paint-once box-shadow — no extra compositor layer beyond what the
       hover transition below already needs. */
    border-radius:14px;
    box-shadow:0 10px 26px rgba(20,15,8,.18), 0 2px 6px rgba(20,15,8,.10);
    /* vignette via mask-image instead of a shadow: an ellipse radial-gradient
       (matches .hero-bg's own mask pattern elsewhere in this file) actually
       fades the photo's own pixels to transparent toward the edges, rather
       than just tinting them darker — reads as a real soft vignette. Pure
       CSS gradient, no image to decode, cheapest possible mask source. */
    -webkit-mask-image:radial-gradient(ellipse at center, #000 72%, transparent 100%);
    mask-image:radial-gradient(ellipse at center, #000 72%, transparent 100%);
    /* only transform is animated here — box-shadow triggers paint, not just
       the compositor, and this hover fires at browsing frequency across a
       whole grid of photos, so the shadow stays fixed and the lift alone
       carries the "elevation" feeling */
    transition:transform .2s var(--ease-out-strong);
    /* opacity added alongside transform: the scroll-reveal cascade
       (gallery-fade-in, further down) animates this same element's opacity,
       so the compositor needs the hint for both properties, not just the
       hover lift's transform */
    will-change:transform, opacity;
  }
  .gallery-item:hover{
    transform:translateY(-4px) scale(1.02) translateZ(0);
    z-index:2;
  }
  .gallery-item:active{transform:translateY(-2px) scale(.98) translateZ(0);}
  .gallery-item:focus-visible{outline:2px solid var(--ink);outline-offset:3px;}
  .gallery-item img,
  .gallery-item video{
    position:absolute;
    inset:0;
    width:100%;
    height:100%;
    object-fit:cover;
    display:block;
    pointer-events:none;
    transition:transform .3s var(--ease-out-strong);
    /* opacity added alongside transform: the page-exit flash (further down)
       fades these to 0 instead of blowing them out with the background */
    will-change:transform, opacity;
  }
  .gallery-item:hover img,
  .gallery-item:hover video{
    transform:scale(1.08);
  }
  /* brief tactile "press" feedback on click, before the lightbox opens */
  .gallery-item.pulsing{
    transform:scale(.95);
    transition:transform .12s ease;
    box-shadow:0 6px 14px rgba(20,15,8,.22);
  }
  .gallery-item::after{
    content:'';
    position:absolute;inset:0;
    background:linear-gradient(to top, rgba(0,0,0,.28), transparent 45%);
    opacity:0;
    transition:opacity .2s ease;
  }
  .gallery-item:hover::after{opacity:1;}

  /* ---------- gallery scroll-reveal + ambient float (tattoo.html /
     nightlife.html only). A true scroll-triggered cascade needs JS — there's
     no reliable cross-browser pure-CSS way to react to "this item just
     entered the viewport" yet (animation-timeline:view() isn't there on
     Safari/Firefox) — so each .gallery-item starts hidden and only reveals
     once an IntersectionObserver in the page's <script> adds .in-view to it,
     with a per-item transition-delay assigned there too (search "cascade
     reveal"). All of this is gated behind html.js-ready, set by a tiny
     synchronous script right after <body> (skipped entirely under
     prefers-reduced-motion): if JS never runs, .gallery-item simply falls
     back to its normal fully-visible, non-animated state instead of staying
     invisible forever.
     Reveal (fade-in) and float are two @keyframes animations on the same
     element, gated together by one animation-play-state (paused until
     .in-view, then running) — the fade plays once (animation-fill-mode:
     forwards holds its end state) and the float keeps looping after it.
     Both are declared through the longhand animation-* properties, never
     the `animation` shorthand, specifically so that a later, more specific
     rule (the nth-child ones below) can set animation-name/duration without
     silently resetting animation-play-state back to its default — the
     shorthand would reset every sub-property it doesn't mention, including
     play-state, which is exactly the property this whole gating trick
     depends on. translate/scale/rotate are the standalone CSS properties,
     not the `transform` shorthand, so none of this fights the existing
     transform-based hover/press feedback on the same elements. */
  html.js-ready .gallery-item{
    animation-play-state:paused, paused;
  }
  html.js-ready .gallery-item.in-view{
    animation-play-state:running, running;
  }
  /* four variants, each pairing its own fade-in duration with its own float
     — duration varies per photo (not a flat speed for every image) and is
     short on purpose (0.6-1.05s: fast enough that the fade doesn't linger),
     while the float keeps its own distinct resting tilt, drift direction
     and amplitude per photo. Modulus 4 here is deliberately different from
     the modulus 3 (and 5) used for the scatter offsets below, so "which way
     a photo tilts" and "how far it's nudged off-grid" never fall into the
     same repeating group. */
  html.js-ready .gallery-item:nth-child(4n+1){
    animation-name:gallery-fade-in, gallery-float-a;
    animation-duration:.75s, 11s;
    animation-timing-function:var(--ease-out-strong), ease-in-out;
    animation-iteration-count:1, infinite;
    animation-fill-mode:forwards, none;
  }
  html.js-ready .gallery-item:nth-child(4n+2){
    animation-name:gallery-fade-in, gallery-float-b;
    animation-duration:.95s, 13.5s;
    animation-timing-function:var(--ease-out-strong), ease-in-out;
    animation-iteration-count:1, infinite;
    animation-fill-mode:forwards, none;
  }
  html.js-ready .gallery-item:nth-child(4n+3){
    animation-name:gallery-fade-in, gallery-float-c;
    animation-duration:.6s, 9.5s;
    animation-timing-function:var(--ease-out-strong), ease-in-out;
    animation-iteration-count:1, infinite;
    animation-fill-mode:forwards, none;
  }
  html.js-ready .gallery-item:nth-child(4n){
    animation-name:gallery-fade-in, gallery-float-d;
    animation-duration:1.05s, 15s;
    animation-timing-function:var(--ease-out-strong), ease-in-out;
    animation-iteration-count:1, infinite;
    animation-fill-mode:forwards, none;
  }

  @keyframes gallery-fade-in{
    /* opacity front-loaded to the first ~38% of the run instead of tracking
       scale/translate the whole way — the photo snaps to fully visible
       quickly (a harder, more definite appearance) while the scale/translate
       settle keeps easing smoothly for the rest of the duration; opacity
       needs no further keyframe after 38% since it has nothing left to
       interpolate toward. */
    0%{opacity:0;scale:.82;translate:0 36px;}
    38%{opacity:1;}
    100%{scale:1;translate:0 0;}
  }
  @keyframes gallery-float-a{
    0%,100%{translate:0 0;rotate:-.55deg;}
    50%{translate:.4px -3px;rotate:-.8deg;}
  }
  @keyframes gallery-float-b{
    0%,100%{translate:0 0;rotate:.45deg;}
    50%{translate:-.4px -2.4px;rotate:.75deg;}
  }
  @keyframes gallery-float-c{
    0%,100%{translate:0 0;rotate:-.25deg;}
    50%{translate:.3px -3.6px;rotate:.15deg;}
  }
  @keyframes gallery-float-d{
    0%,100%{translate:0 0;rotate:.3deg;}
    50%{translate:-.3px -2px;rotate:-.1deg;}
  }

  /* ---------- asymmetric scatter: breaks the featured-grid's 4-column rows
     and the hscroll strip's single-line alignment, so photos read as loose,
     water-scattered debris rather than snapped to a grid. Small margin
     offsets (nth-child mod 3) plus a couple of aspect-ratio variants
     (mod 5, so it never lines up with the margin groups) on the hscroll
     rows; margin offsets only (mod 3) on the featured-grid, since that grid
     already carries portrait/landscape variety via .aspect-portrait /
     .aspect-landscape. Kept small on purpose — organic, not misaligned. */
  .hscroll{padding-top:14px;}
  .hscroll .gallery-item:nth-child(3n+2){margin-top:12px;}
  .hscroll .gallery-item:nth-child(3n){margin-top:-8px;margin-left:6px;}
  .hscroll .gallery-item:nth-child(5n+2){aspect-ratio:3/4;}
  .hscroll .gallery-item:nth-child(5n+4){aspect-ratio:1/1;}
  .featured-slot:nth-child(3n+2){margin-top:16px;}
  .featured-slot:nth-child(3n){margin-top:-10px;}

  .row-nav{
    position:absolute;top:50%;
    transform:translateY(-50%);
    width:38px;height:38px;
    display:flex;align-items:center;justify-content:center;
    border:1px solid var(--dawn-gold-dim);
    background:var(--paper-glass);
    color:var(--ink);
    font-family:var(--font-mono);
    font-size:20px;
    line-height:1;
    cursor:pointer;
    z-index:5;
    opacity:.85;
    transition:opacity .2s ease, transform 160ms var(--ease-out-strong);
  }
  .row-nav:hover{opacity:1;}
  .row-nav:active{transform:translateY(-50%) scale(.9);}
  .row-nav.prev{left:-8px;}
  .row-nav.next{right:-8px;}

  @media (max-width:640px){
    .gallery-item{flex-basis:150px;}
    .row-nav{display:none;}
  }

  /* ---------- footer client / brand logo carousel (compact, discreet) ----------
     pure-CSS keyframe loop: the track holds the logo set twice back-to-back and
     translateX's by exactly -50% of its own (content-driven) width, so the loop
     is seamless and never depends on JS measuring pixel widths before images
     have finished loading. */
  .footer-logos{
    width:100%;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:8px;
    margin-top:2px;
  }
  .footer-logos-label{
    font-family:var(--font-mono);
    font-size:11px;
    letter-spacing:.14em;
    text-transform:uppercase;
    opacity:.55;
  }
  .footer-logo-row-wrap{
    max-width:520px;
    width:100%;
    overflow:hidden;
    -webkit-mask-image:linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
    mask-image:linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
  }
  @keyframes footer-logo-scroll{
    from{transform:translateX(0) translateZ(0);}
    to{transform:translateX(-50%) translateZ(0);}
  }
  .footer-logo-track{
    display:flex;
    width:max-content;
    animation:footer-logo-scroll 60s linear infinite;
    will-change:transform;
  }
  .footer-logo-track:hover{animation-play-state:paused;}
  .footer-logo-row{
    display:flex;
    align-items:center;
    gap:32px;
    padding:4px 16px;
  }
  .footer-logo-item{
    flex:0 0 auto;
    height:24px;
    display:flex;align-items:center;justify-content:center;
    opacity:.5;
    filter:grayscale(1);
    box-shadow:0 4px 12px rgba(20,15,8,.14);
    transition:opacity .2s ease, filter .2s ease;
  }
  .footer-logo-item:hover{opacity:.9;filter:grayscale(0);}
  .footer-logo-item img{max-height:100%;max-width:100px;object-fit:contain;display:block;}
  @media (prefers-reduced-motion: reduce){
    .footer-logo-track{animation:none;}
    .footer-logo-row-wrap{overflow-x:auto;}
  }

  /* ---------- Instagram reminder pop-up (floating, dismissible, low-key) ---------- */
  .ig-popup{
    position:fixed;
    left:24px;bottom:24px;
    max-width:300px;
    display:flex;
    gap:14px;
    align-items:flex-start;
    padding:18px 20px;
    /* near-opaque card, not the barely-there glass this used to be: that
       version depended on mix-blend-mode:difference to keep white content
       legible against whatever showed through, which doesn't render
       reliably in every mobile browser (see the header comment for the
       Instagram in-app browser specifics) — ironic for the one component
       whose whole job is getting people into Instagram. A solid pale card
       with plain --ink text needs no such trick. */
    background:rgba(244,243,239,.94);
    border-radius:16px;
    backdrop-filter:blur(20px);
    -webkit-backdrop-filter:blur(20px);
    border:1px solid rgba(28,26,23,.12);
    box-shadow:0 18px 44px rgba(20,15,8,.22);
    /* above the lightbox (200) so the Instagram nudge stays reachable even
       while a photo is open, instead of being trapped behind the backdrop */
    z-index:220;
    opacity:0;
    transform:translateY(16px);
    pointer-events:none;
    transition:opacity .28s var(--ease-out-strong), transform .28s var(--ease-out-strong);
  }
  .ig-popup.show{opacity:1;transform:translateY(0);pointer-events:auto;}
  .ig-popup-icon{
    flex:0 0 auto;
    width:34px;height:34px;
    display:flex;align-items:center;justify-content:center;
    border:1px solid var(--ink);
    border-radius:9px;
    color:var(--ink);
  }
  .ig-popup-icon svg{width:18px;height:18px;display:block;}
  .ig-popup-body{flex:1 1 auto;}
  .ig-popup-text{
    font-size:13.5px;
    font-style:italic;
    line-height:1.4;
    color:var(--ink);
    margin-bottom:10px;
  }
  .ig-popup-link{
    display:inline-flex;
    align-items:center;
    gap:6px;
    font-family:var(--font-mono);
    font-size:11px;
    letter-spacing:.06em;
    text-transform:uppercase;
    color:var(--ink);
    text-decoration:none;
    border-bottom:1px solid var(--ink);
    padding-bottom:2px;
    transition:opacity .2s ease, transform 160ms var(--ease-out-strong);
  }
  .ig-popup-link:hover{opacity:.6;}
  .ig-popup-link:active{transform:scale(.95);}
  .ig-popup-close{
    position:absolute;
    top:8px;right:10px;
    width:22px;height:22px;
    display:flex;align-items:center;justify-content:center;
    background:transparent;
    border:none;
    color:var(--ink);
    opacity:.75;
    font-size:16px;
    line-height:1;
    cursor:pointer;
    transition:opacity .2s ease, transform 160ms var(--ease-out-strong);
  }
  .ig-popup-close:hover{opacity:1;}
  .ig-popup-close:active{transform:scale(.85);}
  @media (max-width:640px){
    .ig-popup{left:16px;right:16px;bottom:16px;max-width:none;}
  }

  /* ---------- page-transition flash: reuses the exact filter recipe
     .hero-bg img.flash-out/.flash-in already use for the hover-triggered
     hero photo swap (see that block for the original), scaled up to the
     whole page instead of one image. No overlay element, no backdrop-filter
     — just a brightness ramp on whatever already serves as each page's
     background (.lava-mass on tattoo/nightlife/contacto, .hero-bg img on
     index), while any gallery photo already revealed (.in-view) fades its
     own opacity to 0 instead of blowing out with the background, so it
     dissolves rather than flashing white on top of the flash. html.page-exit
     fires on internal-link click (see the IIFE
     near the top of each page's <script>); html.page-enter is added
     synchronously before first paint on every load and removed a frame
     later, so every page arrives white and eases down — same "instant
     snap to the blown-out state, then ease back down" trick as the hero's
     own .flash-in. Cheaper than the old whip-pan panel: no new DOM node,
     no backdrop-filter, filter is compositor-friendly and this only ever
     runs once per navigation, never in a loop. */
  html.page-exit .lava-mass{
    filter:brightness(7);
    transition:filter .26s var(--ease-out-strong);
  }
  html.page-enter .lava-mass{
    filter:brightness(7);
    transition:none;
  }
  html.page-exit .hero-bg img{
    filter:grayscale(1) contrast(1.1) brightness(7);
    transition:filter .26s var(--ease-out-strong);
  }
  html.page-enter .hero-bg img{
    filter:grayscale(1) contrast(1.1) brightness(7);
    transition:none;
  }
  html.page-exit .gallery-item.in-view img,
  html.page-exit .gallery-item.in-view video{
    opacity:0;
    transition:opacity .22s var(--ease-out-strong);
  }

  /* ---------- lightbox ---------- */
  .lightbox{
    position:fixed;inset:0;
    background:rgba(10,9,7,.94);
    display:flex;align-items:center;justify-content:center;
    z-index:200;
    opacity:0;
    pointer-events:none;
    transition:opacity .25s ease;
    padding:6vh 6vw;
    /* its own compositor layer so opening/closing never forces the footer,
       its logo carousel animation, or anything else on the page to repaint */
    will-change:opacity, transform;
    transform:translateZ(0);
  }
  .lightbox.open{opacity:1;pointer-events:auto;}
  .lightbox-content{max-width:90vw;max-height:88vh;}
  .lightbox-content img,
  .lightbox-content video{
    max-width:90vw;
    max-height:88vh;
    width:auto;height:auto;
    display:block;
    box-shadow:0 20px 60px rgba(0,0,0,.5);
  }
  .lightbox-close{
    position:absolute;top:22px;right:28px;
    width:44px;height:44px;
    display:flex;align-items:center;justify-content:center;
    border:1px solid var(--dawn-gold-dim);
    background:transparent;
    color:var(--paper);
    font-size:22px;
    font-family:var(--font-mono);
    cursor:pointer;
    z-index:210;
    transition:transform 160ms var(--ease-out-strong), opacity .2s ease;
  }
  .lightbox-close:hover{opacity:.7;}
  .lightbox-close:active{transform:scale(.9);}

  /* ---------- inline contact mailbox (tattoo.html / nightlife.html) ----------
     A bordered card with boxed fields, not just underlines: against the
     busy .lava-mass grain a plain underline-only input didn't read as an
     actual input at a glance, so the field boundary now has to be visible
     on its own, without relying on hover/focus to reveal it. */
  .mailbox-form{
    max-width:640px;
    display:flex;
    flex-direction:column;
    gap:20px;
    margin-top:8px;
    padding:32px;
    border:1px solid var(--ink);
    background:rgba(244,243,239,.92);
  }
  .mailbox-row{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:20px;
  }
  .mailbox-form label{
    display:flex;
    flex-direction:column;
    gap:8px;
    font-family:var(--font-mono);
    font-size:11px;
    letter-spacing:.08em;
    text-transform:uppercase;
    opacity:.9;
  }
  .mailbox-form input,
  .mailbox-form textarea{
    font-family:var(--font-body);
    font-size:15px;
    color:var(--ink);
    background:var(--paper);
    border:1px solid var(--dawn-gold-dim);
    padding:10px 12px;
    resize:vertical;
    transition:border-color .2s ease, background .2s ease;
  }
  .mailbox-form input:focus,
  .mailbox-form textarea:focus{outline:none;border-color:var(--ink);background:#fff;}
  .mailbox-submit{
    align-self:flex-start;
    font-family:var(--font-mono);
    font-size:12px;
    letter-spacing:.08em;
    text-transform:uppercase;
    color:var(--paper);
    background:var(--ink);
    border:1px solid var(--ink);
    padding:14px 26px;
    cursor:pointer;
    transition:background .2s ease, color .2s ease, transform 160ms var(--ease-out-strong);
  }
  .mailbox-submit:hover{background:transparent;color:var(--ink);}
  .mailbox-submit:active{transform:scale(.97);}
  .mailbox-direct{
    margin-top:2px;
    font-family:var(--font-mono);
    font-size:12px;
    opacity:.9;
  }
  .mailbox-direct a{color:var(--ink);border-bottom:1px solid var(--dawn-gold-dim);}
  @media (max-width:640px){
    .mailbox-row{grid-template-columns:1fr;}
    .mailbox-form{padding:22px;}
  }

  footer{
    /* no background of its own — same as every other section, this lets
       .lava-mass's texture run continuous underneath instead of the footer
       reading as a separate flat panel glued to the bottom of the page */
    color:var(--ink);
    padding:36px 8vw 24px;
    display:flex;
    flex-direction:column;
    gap:16px;
    font-family:var(--font-mono);
    font-size:12px;
    border-top:1px solid rgba(210,205,197,.4);
  }
  .footer-top{
    display:flex;
    justify-content:space-between;
    flex-wrap:wrap;
    gap:24px;
    width:100%;
  }
  footer .fbrand{
    font-family:var(--font-display);
    font-size:22px;
    color:var(--ink);
    margin-bottom:8px;
  }
  footer a{color:var(--ink);}
  .footer-social{
    display:flex;
    gap:16px;
    align-items:center;
  }
  .footer-social a{
    display:inline-flex;
    opacity:.75;
    transition:opacity .2s ease, transform 160ms var(--ease-out-strong);
  }
  .footer-social a:hover{opacity:1;}
  .footer-social a:active{transform:scale(.9);}
