
/* ============================================================
   BAR SCENE SOCIALS — v3
   A hand-made zine pinned to a bar wall, printed loud. Black and white
   grounds, three electric accents — orange, green, red — and nothing else.
   Not a ticketing platform.
   ============================================================ */

:root{
  color-scheme: dark;

  /* --- ground: the print is inverted — black stock, white ink -----
     The token names still mean exactly what they always meant. "Paper"
     is the surface you print on; "ink" is the mark you put on it. Only
     the colours swapped, so every component built on those two ideas
     inverts correctly without being rewritten.
     Four steps of ground, darkest to lightest:
       --ink-deep  closing bands + footer   (deeper than the page)
       --paper     the page itself
       --paper-2   raised: cards, panels, the lifted band
       --paper-3   the one quiet band, a step further up
     Keep the spread this tight — big jumps between bands read as a
     theme bug rather than a section change. */
  --paper:      #111110;   /* the page ground — near-black, never pure #000 */
  --paper-2:    #191918;   /* raised surface: cards, panels, lifted band */
  --paper-3:    #232321;   /* the one quiet band */
  --ink:        #F8F8F6;   /* the mark on that ground: type, rules, keylines */
  --ink-deep:   #080807;   /* below the ground — closing bands, footer */
  --ink-2:      #2C2C29;   /* a flat field for placeholder photo blocks */
  --stone:      #7A7A74;   /* the default placeholder photo field */

  /* --- accents: orange, green, red. Nothing else. --------- */
  --orange:     #FF4D14;   /* THE accent — CTAs, marks, links, live state.
                              5.7:1 on the black ground, so it is safe as
                              type here in a way it never was on white. */
  --orange-on:  #FF8A4D;   /* small type on black where --orange is too hot */
  --orange-d:   #C93A08;   /* the dark sibling — only for the handful of
                              surfaces that are still light (button hover,
                              the ticket stock, the taped polaroid) */
  --orange-lt:  #FF9152;   /* ticket stock */
  --green:      #00C24E;   /* electric — dark grounds and large elements */
  --green-d:    #00873A;   /* the deep green field on photo blocks */
  --green-on:   var(--green);  /* green type. Swapped to --green-d inside
                                  .on-white, where --green is 2.24:1 and
                                  --green-d is 4.36. Same pattern as
                                  --orange-on. */
  --violet:     #C13BFF;   /* neon purple — the Halloween words, "Brady",
                              and the header pill. Specified by the
                              designer, so it never moves; whatever it
                              sits on moves instead.
                              It is the hardest ink here because its
                              luminance is .19 — the MIDDLE of the range.
                              White only has to out-run the
                              bright end and --ink-deep only the dark one,
                              but violet is squeezed from both, and there
                              is no plate strength that darkens a
                              highlight past it without the ground
                              passing through its own value on the way.
                              When violet fails over a picture the fix is
                              the crop or the plate GEOMETRY, not more
                              alpha. See --hero-pos on haunted-bar-hop. */
  --red:        #E8202A;
  --red-d:      #B8141C;
  --red-on:     #FF5A62;   /* small red type — --red is 4.20:1 on the page
                              ground and 22px script needs 4.5. */
  --red-btn:    #D01820;   /* a solid red button: white on --red is exactly
                              4.50:1 and 13px bold needs 4.5 with no margin.
                              This gives 5.48 and still reads 3.45:1 as a
                              shape against the page. */

  /* --- sticker inks: the collage layer on the calendar ------------
     Other people's events only, and never orange. "If it's orange, go"
     is the whole promise of Happening Now, and it only holds while
     orange means us — a gold strip and a blue sticker sitting next to
     the painted square are what make that square read as ours.

     These live on the white calendar sheet (--ink, #F8F8F6), not on the
     page ground, so they are measured against near-black type:
       gold  9.6:1   navy 14.7:1 (white on it)   sky 12.8:1   lime 8.6:1
     Mixed from the style guide's warm/community + risograph palettes;
     the powder blue is nudged one step deeper than the extracted
     #CFE3EE so its edge survives on a near-white sheet. */
  --stick-gold:  #F2B01E;   /* Octoberfest — beer gold, not a brand */
  --stick-navy:  #12284B;   /* the playoffs run */
  --stick-sky:   #BEDDEE;
  --stick-lime:  #00C24E;
  --stick-blush: #F9D7D6;   /* the default, if a tone is ever missed */
  --stick-edge:  #FFFDF7;   /* the die-cut border every sticker carries */

  /* --- pinned: type sitting ON an accent or a light surface -------
     This must never follow --ink. White on --orange measures 3.3:1 and
     fails AA — electric orange is mid-luminance, so anything on an
     orange surface takes near-black type instead. Same for the white
     hover fills and the polaroid frame. */
  --on-accent:  #111110;

  /* --- functional ---------------------------------------- */
  --text:       var(--ink);
  --text-2:     rgba(248,248,246,.74);   /* 9.9:1 */
  --text-3:     rgba(248,248,246,.58);   /* 6.4:1 */
  --rule:       rgba(248,248,246,.16);
  --rule-hard:  rgba(248,248,246,.74);

  /* --- type -------------------------------------------- */
  /* --- type ---------------------------------------------------------
     Jomhuria + Yellowtail. The Figma frame paired Jomhuria with Brush
     Script MT; see --script below for why the script face changed.

     METRIC WARNING. Jomhuria is an Arabic-first display face: its Latin
     cap height is only .3907em, against Anton's .8594em. Every display
     font-size in this file is therefore ~2.2x its old Anton value, and
     every display line-height is ~/2.2, purely to hold the SAME optical
     size the site already had. Do not "tidy" these numbers back down.

     Both faces are self-hosted from /fonts — see css/fonts.css. */
  --display: 'Jomhuria', 'Arial Narrow', sans-serif;
  /* Yellowtail, NOT Brush Script MT. Brush Script MT is a macOS system
     font — it does not exist on iOS, Android or stock Windows, so the
     stack used to render one face on a Mac and a different one on a
     phone. It is Monotype-licensed and cannot legally be self-hosted,
     so the fallback became the face: Yellowtail is the closest free
     match (same slant, weight and stroke contrast) and it is what every
     non-Mac visitor was already seeing. Now everyone sees it. */
  --script:  'Yellowtail', cursive;
  --body:    'Space Grotesk', ui-sans-serif, system-ui, sans-serif;

  /* headline pairing, measured off the Figma frame (node 2072-5) */
  --hl-track:  .07em;   /* sans tracking; script keeps 0 */
  --hl-lead:   .46;     /* line-height in Jomhuria units */
  --hl-script: .775em;  /* script size, relative to the headline size.
                           .86 was tuned to Brush Script MT (cap .6494em);
                           Yellowtail's cap is .7207em, 11% larger, so the
                           ratio drops to keep the same optical pairing. */

  /* --- motion (restrained on purpose) ------------------ */
  --ease: cubic-bezier(.16,1,.3,1);
  --quart: cubic-bezier(.25,1,.5,1);

  --shell: 1360px;
  /* The header's real rendered height, not a round number — it is driven
     by whatever its tallest child is at each width (the CTA pill, then
     the nav marks once they show up), so it has to track that exactly or
     content sits under a gap or gets clipped by the fixed bar above it.
     Measured, not guessed, at the same 640px/1100px breakpoints the
     header's own padding rules already use:
       <=640px   68px  (.hdr__in gets 11px vertical padding here)
       641-1100  72px  (default 13px padding, nav hidden behind Menu)
       >=1101px  80px  (the nav marks are the tallest child)
     This was 65px flat before — already 7px short of the 68-72px real
     height pre-existing this change, which the nav marks would only
     have made worse. */
  --hdr-h: 68px;
}
@media (min-width:641px){  :root{ --hdr-h: 72px; } }
@media (min-width:1101px){ :root{ --hdr-h: 80px; } }

*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%; scroll-behavior:smooth}
body{
  margin:0; background:var(--paper); color:var(--text-2);
  /* THE DEFAULT READING SIZE, and it is the same value .lede uses — one
     size and one colour for running copy everywhere instead of the 14 /
     15 / 15.5 / 16-19 / 17 / 18 / 20px spread this had. Everything that
     is NOT running copy (captions, chips, kickers, ticket notes, the
     nav) declares its own size and is unaffected; the audit before this
     change found exactly nine elements on the whole site inheriting the
     old 17px, all of them list items.

     --text-2 rather than --text: prose sits a step back from the
     headings and labels, which is what makes a heading read as a
     heading without needing more size. */
  font:400 clamp(18px,1.55vw,22px)/1.6 var(--body);
  -webkit-font-smoothing:antialiased;
}
/* …which means anything that used to rely on inheriting the BRIGHT ink
   has to say so now. These are labels and names, not prose. */
.h4{color:var(--text)}
.stops span{color:var(--text)}
::selection{background:var(--orange); color:var(--on-accent)}
a{color:inherit}
/* height:auto is REQUIRED now that every <img> carries width/height
   attributes. The attributes are there so the browser can reserve the
   right box before the file arrives — no reflow as each picture lands —
   but with max-width:100% constraining the WIDTH and the height still
   pinned to the attribute, a shrunk image would keep its full intrinsic
   height and stretch. `auto` hands the height back to the intrinsic
   ratio the attributes declare. Every image that needs to fill a box
   instead sets its own height (hero media, event thumbnails, the
   calendar pins and card shot) and those win on specificity. */
img{max-width:100%; height:auto; display:block}
:focus-visible{
  outline:3px solid var(--orange); outline-offset:3px; border-radius:2px;
}

/* ============================================================
   TYPE
   ============================================================ */
.d1,.d2,.d3,.event__title{
  font-family:var(--display); font-weight:400; text-transform:uppercase;
  line-height:var(--hl-lead); letter-spacing:var(--hl-track); margin:0;
  text-wrap:balance;
}
/* sizes are the old Anton values x2.1996 — same cap height on screen.
   .d1 is the exception: a straight x2.2 gave 458px, and at that size the
   longest headline line ("12 BARS OF", 3.284em of advance with tracking)
   needs 1505px of column and overflowed every container from 1440px up.
   380px is the largest that fits the widened hero, and 26.5vw keeps the
   narrow end fitting too. */
/* One continuous ramp from 320px to 1440px, rather than a desktop clamp
   plus a much smaller mobile override. The two ramps met badly: .d1 was
   74px at 561px wide and 51px at 560px, a 23px CLIFF at the breakpoint,
   and the mobile arm was far more timid than it needed to be.

   The real constraint was measured, not guessed. The widest unbreakable
   line in any lockup is "MORE THAN", which costs 3.162px of width per 1px
   of font-size once .04em of tracking is counted. On a 375px phone the
   hero has 339px of room, so it fits up to 107px — the old ramp was
   serving 51. These curves are set to land near 85% of that ceiling at
   every width, then hit the same 190/130/68 maximums as before.

     calc(A + B*vw) form, so the slope is continuous and there is no
     breakpoint for the size to jump at. */
.d1{font-size:clamp(72px, calc(54.8px + 9.39vw), 190px)}
/* .d2 and .d3 are one step now. They had grown apart — .d3 topped out at
   68px against .d2's 130 — which meant a section head like "THREE DAYS A
   YEAR" read as a caption next to "MORE THAN A BAR CRAWL" on the same
   page. Two display sizes below the hero was one more than the site
   actually needs. The class names both stay because 40-odd headlines use
   them, but there is only one size to maintain. */
.d2,.d3{font-size:clamp(50px, calc(38px + 6.39vw), 130px)}
.h4{
  font-family:var(--display); font-weight:400; text-transform:uppercase;
  font-size:clamp(23px,2.55vw,33px); line-height:.464; letter-spacing:.03em; margin:0;
}

/* Narrow screens. Jomhuria is ~1.5x wider than Anton at a matched cap
   height, so the desktop ramp's minimum (128px on .d1) puts "SHAMROCK"
   at 415px on a 375px phone. Headlines therefore have to run a little
   shorter here than they did before — there is no size that both
   matches the old cap height AND fits. Tracking eases to .04em to buy
   back some of the width rather than shrinking the type further. */
@media (max-width:560px){
  /* Tracking still eases here — it buys back width at the size the ramp
     above now asks for. The font-size overrides are gone; the ramp is
     continuous and does not need a second, smaller arm. */
  .d1,.d2,.d3{letter-spacing:.04em}
}

/* --- the script half of a headline pairing -------------------------
   <h1 class="d1">Haunted bar <em class="scr">Crawl</em></h1>
   Smaller in font-size than the sans, but Yellowtail's cap is 1.84x
   Jomhuria's, so it still reads slightly TALLER — which is the whole
   effect. Tracking stays at 0: this face connects, and any letter-
   spacing at all breaks the joins between glyphs. */
.scr{
  font-family:var(--script); font-size:var(--hl-script);
  /* the script word is always set with a capital, whatever the source
     copy does — the script capitals carry the swash that makes the
     pairing read. Single words in practice, so capitalize == first letter. */
  letter-spacing:0; text-transform:capitalize; font-style:normal;
}
/* The script face overhangs its own advance box — the "C" reaches left
   of the origin and 3.4px above the line box at a 160px headline. A .fx
   headline paints its fill as a background clipped to the text, and a
   background only paints inside the element's box, so those overhangs get
   sliced flat. Giving the span its own padded paint box fixes it: vertical
   padding on an INLINE costs nothing in layout, and the horizontal padding
   is cancelled by an equal negative margin. The .04em left over on the
   right is the join spacing to the next word. */
.fx .scr{
  /* display:inline-block is doing real work here, not layout work. A .fx
     headline paints its fill as a background clipped to its text, and a
     background only paints inside the element's box — so the script's
     overhangs (the C reaches above the line box, descenders reach below)
     were sliced flat. An inline-block is an atomic box the parent's text
     clip cannot reach, so the span is left to paint itself, with padding
     that gives those overhangs room. That also avoids the parent and the
     span BOTH painting these glyphs, which multiplied the noise twice and
     made the script word read darker than the sans beside it.
     Every bit of padding is cancelled by an equal negative margin, so the
     line box and the baseline are exactly where they were; the .04em left
     over on the right is the join spacing to the next word. */
  display:inline-block;
  /* Vertical padding is tied to --hl-lead AND to the script face's own
     metrics. The content box of an inline-block IS its line box, so the
     baseline sits at (lead - (asc+desc))/2 + asc from the top. Yellowtail
     is .97 ascent / .30 descent, so at lead .46 the baseline lands .565em
     down — below the box bottom — and the ink runs .174em above the top
     and .406em below it across the words this site actually sets.
     Padding is those numbers plus a little, cancelled by an equal
     negative margin so layout does not move.
     Re-measure whenever --hl-lead, --hl-script or the script FACE
     changes; swapping Brush Script MT for Yellowtail moved every one of
     these values. The check is ink-vs-paint-box on every script word. */
  /* Right padding raised .13em -> .26em. Yellowtail's terminals overhang
     the advance width, and on the longest words the tail was being sliced:
     "Crawl" measured -9.7px past the fill box and "Get" -5.8px. This was
     always true — the earlier audit missed it because it measured the
     SOURCE text rather than the rendered text, and .scr is
     text-transform:capitalize, so every word was measured a cap short.
     The right margin moves with it (-.09em -> -.22em) so the net .04em of
     join spacing to the next word, and the line box, are unchanged. */
  padding:.23em .26em .48em .13em;
  margin:-.23em -.22em -.48em -.13em;
  background-image:inherit; background-blend-mode:multiply;
  -webkit-background-clip:text; background-clip:text;
}

/* ============================================================
   HEADLINE TREATMENT — noise in the letterform, glow off the edge
   Ported from the Figma frame: a same-colour drop shadow plus a
   wider, softer same-colour bloom (Figma's layer blur), and a fine
   noise field multiplied into the fill.

   The fill is painted as a background clipped to the glyphs, so the
   text itself is transparent — anything that needs a solid colour
   (a link underline, a ::selection) has to be set separately.
   ============================================================ */
.fx{
  /* Paint-box padding for the SANS text. A .fx headline paints its fill as
     a background clipped to its text, and a background only paints inside
     the element's box — but a block's box is its line boxes, and at
     --hl-lead .46 the first line's cap sits .0507em ABOVE the box top
     (baseline = (lead - 1.0)/2 + .61, cap = .3907 above that). It was being
     sliced flat across the top. The bottom pad covers descending
     punctuation — "So, are we Crawling?" has a comma.
     Cancelled by an equal negative margin so layout does not move; the
     handful of headlines carrying an inline margin-top subtract --fx-pt
     themselves. Script words are NOT covered by this — they are
     inline-blocks that paint their own box (see .fx .scr). */
  --fx-pt:  .09em;
  --fx-pb:  .06em;
  padding-top:var(--fx-pt); padding-bottom:var(--fx-pb);
  margin-top:calc(-1 * var(--fx-pt)); margin-bottom:calc(-1 * var(--fx-pb));

  --fx-c:   var(--ink);                 /* the fill */
  --fx-n:   .25;                        /* noise strength — Figma: 000000 @ 25% */
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='.25'/%3E%3C/svg%3E"),
    linear-gradient(var(--fx-c),var(--fx-c));
  background-blend-mode:multiply;
  -webkit-background-clip:text; background-clip:text;
  color:transparent;
  /* Figma stacks a Layer blur here, and it WAS implemented — removed
     2026-09-04. At mobile headline sizes it resolved to a sub-pixel
     radius (~0.3px on a 104px .d1), which browsers cannot render as a
     blur; it just forced the glyphs onto a resampled raster layer and
     read as a smear rather than a softened edge.

     The halo itself is --neon now (see the block by .ink--white), shared
     with the inked runs so a split headline and a plain one light the
     same way. It stays wrapped in --fx-filter because the neon keyframes
     animate that variable, and .fx--split empties it. */
  --fx-filter:var(--neon);
  filter:var(--fx-filter);
}

/* ============================================================
   NEON FLICKER  —  event heroes only
   The glow already reads as a lit tube, so the headline behaves like
   one: mostly steady, then a short stutter where it drops out and
   catches again. Two things change on the dead frames — grayscale(1)
   kills the colour and the filter drops the two drop-shadows, so the
   glow goes with it.

   Done entirely on `filter`, one animatable property. The alternative
   was animating --fx-c, which needs @property to be interpolable and
   forces every colourway to carry a second "lit colour" token. Filter
   lists of different shapes cannot interpolate, so the frames snap on
   their own — which is what a failing tube does anyway.

   Deliberately scoped with a class, not `.hero .fx`: the event CARD
   titles share the same colourways and must stay steady.
   ============================================================ */
@keyframes bss-neon{
  0%, 86%, 100%          { filter:var(--fx-filter) }
  86.8%                  { filter:grayscale(1) brightness(.66) }
  87.6%                  { filter:var(--fx-filter) }
  88.9%                  { filter:grayscale(1) brightness(.66) }
  90.2%                  { filter:var(--fx-filter) }
  90.9%                  { filter:grayscale(1) brightness(.66) }
  92%                    { filter:var(--fx-filter) }
}
.fx--neon{ animation:bss-neon 5.5s var(--quart) infinite }
/* stagger so two lit headlines on one screen never blink in unison.
   Scoped away from .fx--lit: these are animation-delay longhands at
   (0,2,0) and would otherwise override the delay inside the two-animation
   shorthand below, starting the ambient loop before the tube has struck. */
.fx--green.fx--neon:not(.fx--lit){ animation-delay:-1.7s }
.fx--red.fx--neon:not(.fx--lit)  { animation-delay:-3.1s }

/* ── IGNITION ────────────────────────────────────────────────
   The ambient loop puts its stutter at 86% of a 5.5s cycle, so a headline
   you have just landed on sits dead still for four seconds before it does
   anything. .fx--lit is added by JS the moment a neon headline is first in
   view — on land for a hero, on scroll for the closing line — and the tube
   strikes straight away: three fast misfires, two slower ones, then it
   holds. Only then does the ambient loop take over.

   Two animations on one property. Where both are running the LAST in the
   list wins, so the loop's .92s delay is what hands over cleanly at the
   moment the strike ends on its settled state. If JS never runs, nothing
   gets .fx--lit and the ambient loop alone is the (working) fallback. */
/* Two misfires, then it holds. This was six — three fast and three slow —
   which read as a fault rather than a tube striking. The duration comes
   down with the count so the strike still lands inside the first second. */
/* IT ARRIVES LIT. The first frame used to be
   `grayscale(1) brightness(.32)` — a deliberately dead tube, struck a
   moment later — and on the page it read as a rendering fault rather
   than a flourish. Three things compounded:

   · On a .fx--split lockup (which is every event name on the site) the
     parent's --fx-filter is EMPTY, so its resting filter is `none`. This
     keyframe therefore does not dim a glow — it greyscales and darkens
     the whole rendered subtree, the .ink runs' own neon and dark halos
     included. Grey, blurred, dim: the whole lockup at once.
   · .fx--lit is added by JS on first view, so the dead frame lands when
     bss.js boots — by which time the hero PHOTOGRAPH is often still
     decoding, so it shows up as grey type on a black rectangle with
     nothing else on screen to explain it.
   · It held for 14% of .62s, which is long enough to read as a frame
     that should not be there.

   So the sign is lit from 0% and misfires twice AFTER you have already
   seen it working, which is the same character without the dead start.
   The ambient loop's own stutter (86.8-92% of bss-neon, above) is
   untouched — that one is mid-cycle and reads correctly. */
@keyframes bss-neon-on{
  0%    { filter:var(--fx-filter) }
  7%    { filter:grayscale(1) brightness(.46) }
  15%   { filter:var(--fx-filter) }
  28%   { filter:grayscale(1) brightness(.56) }
  44%   { filter:var(--fx-filter) }
  100%  { filter:var(--fx-filter) }
}
.fx--lit{
  animation:
    bss-neon-on .62s var(--quart) both,
    bss-neon 5.5s var(--quart) .62s infinite;
}

@media (prefers-reduced-motion: reduce){
  .fx--neon, .fx--lit{ animation:none }
}
/* ── A SECOND INK INSIDE ONE HEADLINE ────────────────────────
   A .fx headline paints ONE fill, clipped to its own text. To print a
   word in a different colour it needs its own paint box, exactly like
   .scr does — and that means the same trap: an inline-block paints only
   inside its box, so it carries padding to keep ascenders and
   descenders inside the fill, cancelled by an equal negative margin.

   The glow is set here too, not inherited. The parent's drop-shadow
   still applies to the whole subtree (it is one filter over the rendered
   result), so the word ends up with its own tight halo inside the
   headline's wider one — which is how a real sign with two tube colours
   actually bleeds.

   Works on a plain <span> and on .scr. On .scr it has to beat
   `background-image:inherit`, hence the .fx .scr.ink selector below. */
.fx .ink{
  display:inline-block;
  /* An inline-block is an atomic box, which is exactly why it can paint its
     own fill — but it also means a multi-word run will not break. "See you
     on" set as one 620px box inside a 597px headline and hung 23px past it.
     max-width lets the run wrap internally again; the paint box still
     surrounds every line it takes.

     content-box is load-bearing, against the global border-box. These
     headlines are grid items under justify-items:center, so most of them
     are sized BY their own contents — the parent's width IS the span's
     max-content, padding included. Measured against the border box, 100%
     then hands the text less room than it needs by exactly the horizontal
     padding, and it wraps for no reason: "12 BARS OF" broke to two lines
     and the Christmas lockup went from two lines to three. Measured
     against the content box, a self-sized parent leaves the run on one
     line, and a parent with a real constraint of its own (the 9ch cap on
     "See you on Brady") still forces the wrap. */
  box-sizing:content-box;
  max-width:100%;
  /* Bottom padding is .14em, not the .08em it looks like it needs. The ink
     only descends .007em below the baseline, but --hl-lead is .46, so the
     line box is far shorter than the glyphs and the baseline sits within a
     pixel of the box bottom before any padding at all. Measured at 190px
     the fill was running 7px short. Cancelled by an equal negative margin,
     so the line box and baseline do not move. */
  padding:.10em .06em .14em .06em;
  margin:-.10em -.06em -.14em -.06em;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='.25'/%3E%3C/svg%3E"),
    linear-gradient(var(--ink-c),var(--ink-c));
  background-blend-mode:multiply;
  -webkit-background-clip:text; background-clip:text;
  color:transparent;
  filter:var(--neon);
}
/* When .ink lands ON a script word, .fx .ink and .fx .scr are both (0,2,0)
   and whichever is declared later wins the padding — which meant "Shuffle"
   took the SANS paint box (.10em/.08em) instead of the script one
   (.23em/.48em). Yellowtail's ascenders and descenders promptly fell
   outside the fill and were sliced off: measured -3.2px at the top and
   -13.1px at the bottom. This rule is (0,3,0) and restates the script box.
   Any future ink on a .scr inherits the fix. */
.fx .scr.ink{
  padding:.23em .26em .48em .13em;
  margin:-.23em -.22em -.48em -.13em;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='.25'/%3E%3C/svg%3E"),
    linear-gradient(var(--ink-c),var(--ink-c));
}
/* Every colourway is now ONE value. The glow derives from it — see --neon
   below — so a new ink is a single line and can never have a halo that
   disagrees with its fill. */
.ink--violet{--ink-c:var(--violet)}
.ink--green {--ink-c:var(--green)}
.ink--orange{--ink-c:var(--orange)}
.ink--red   {--ink-c:var(--red)}
.ink--white {--ink-c:var(--ink); --neon-lift:.55}

/* ── THE NEON ────────────────────────────────────────────────────────
   This was two drop-shadows — one at 90% alpha, one at 40%, and nothing
   in between. Two stops is a step, not a falloff: it read as a sticker
   with a coloured outline rather than as a lit tube, because a real sign
   does not have an edge where its glow stops.

   Four stops, on a roughly geometric radius progression (.006 / .011 /
   .025 / .053em) with alpha falling away much faster than radius grows.
   That ratio is the whole trick — light falls off with distance, so a
   glow that keeps its density as it widens looks like fog, and one that
   loses it looks like light.

   The three bloom radii were halved from .022/.05/.105. The ALPHAS did
   not move with them, deliberately: radius is how much glow there is,
   alpha is how dense it is, and only the first was too much — halving
   both would have taken the sign out altogether. The hot core stayed
   put; at .003em it would be sub-pixel below hero sizes, and it is the
   tube rather than the glow.

   The first stop is the tube itself: a LIGHTENED tint of the ink, one
   pixel wide at a hero size. Real neon blows its core out toward white
   and saturates outward from there, and that single hot line is most of
   what separates "glowing" from "coloured". --neon-lift tunes how far
   toward white it goes; white ink lifts less, having less room.

   saturate/brightness lead the chain so they act on the FILL only —
   filters apply in order, so anything after a drop-shadow would grade
   the shadows too and muddy the falloff.

   Derived from --ink-c with color-mix, so the five colourways above are
   one line each and a sixth is free. The @supports below keeps the old
   two-stop version for anything without color-mix. */
.fx .ink, .fx{
  --neon:
    saturate(1.14) brightness(1.03)
    drop-shadow(0 0 calc(.006em * var(--neon-k,1)) color-mix(in srgb, var(--ink-c,var(--fx-c)) calc(100% - var(--neon-lift,.72) * 100%), #FFFFFF))
    drop-shadow(0 0 calc(.011em * var(--neon-k,1)) color-mix(in srgb, var(--ink-c,var(--fx-c)) 90%, transparent))
    drop-shadow(0 0 calc(.025em * var(--neon-k,1)) color-mix(in srgb, var(--ink-c,var(--fx-c)) 48%, transparent))
    drop-shadow(0 0 calc(.053em * var(--neon-k,1)) color-mix(in srgb, var(--ink-c,var(--fx-c)) 20%, transparent));
}
@supports not (color: color-mix(in srgb, red, blue)){
  .fx .ink, .fx{
    --neon:drop-shadow(0 0 .017em var(--ink-c,var(--fx-c)))
           drop-shadow(0 0 .043em var(--ink-c,var(--fx-c)));
  }
}

/* ── "ON BRADY" — the tagline under the Haunted Bar Hop mark ─────────
   Part of the mark, not a caption: it ships inside every Haunted lockup
   on the site, so it is inside the heading element rather than a
   sibling, and it inherits whatever alignment that lockup has (centred
   in the heroes and on the cards, left in the section headlines).

   Sized in em, not vw, because it has to hold its proportion to the
   word above it across a 190px hero and a 61px card thumbnail — a vw
   clamp would make it near-equal to the card title and a third the
   hero's. The clamp's floor and ceiling are the two ends of that range:
   below 15px this face stops being readable, above 34px it starts
   competing with "Hop" rather than sitting under it.

   display:block (beating .fx .ink's inline-block, same specificity,
   declared later) puts it on its own line without a <br> — a <br> would
   have opened a full line box at the headline's own 190px strut. It
   restates its own paint box instead of inheriting the script one,
   because those paddings are in em of the SCRIPT's size, and this runs
   at a fifth of it. */
/* TWO elements, and both are load-bearing.

   .hl__on-line is a plain block that carries no text of its own. It
   exists to open a line and to zero the STRUT: a line box is at least as
   tall as its block container's font-size x line-height, and inside a
   headline that is .46 x 190px = 87px of empty height under a 34px
   tagline. line-height:0 on the container removes it, so the line box
   is exactly the inline-block inside it. Its margin is in the PARENT's
   em, which is the point — the gap then scales with the lockup, from a
   190px hero down to a 61px card thumbnail, instead of with the
   tagline's own clamped size.

   .hl__on stays an ATOMIC INLINE. A .fx headline paints its fill as a
   background clipped to text, and that clip reaches every glyph in the
   subtree except inside an atomic inline. That is the whole reason
   .fx .ink and .fx .scr are inline-block. Set to display:block this
   briefly stopped being atomic and the parent painted "On Brady" in the
   headline's orange, unrotated, while the span painted it again in its
   own colour at -2deg on top — two copies, slightly offset. Anything
   carrying its own .ink must stay an atomic inline. */
/* NO LONGER HERO-SCOPED. "On Brady" used to run at .22em BELOW the lockup
   everywhere, and the heroes alone pulled it up a hair. The owners asked
   for it larger and slightly overlapping the word above — and once the
   hero moved, a section lockup with the tagline floating 40px underneath
   read as a different mark, not the same one at a smaller size. One ratio
   now serves all four instances (both heroes at .d1, the homepage "next
   up" lockup and the tickets-page lockup at .d2).

   Both terms are in the PARENT's em, which is the whole point: the
   overlap is a fixed FRACTION of the cap band, so it holds from a 190px
   hero down to a 90px phone headline without a breakpoint. Measured at
   19.6% of cap at 1280, 20.7% at 375, 22.1% at 1600.

   -.34em is MEASURED, and the thing to measure is the INK, not the boxes.
   Both runs carry large paint-box padding (the script's is .23em/.48em of
   its own size), so their getBoundingClientRects overlap by 50-odd pixels
   while the letterforms still sit APART. The number that matters is canvas
   actualBoundingBox against the cap band of the line above.

   The owners asked for the script to JUST overlap the lockup — touching
   the letterforms, not riding up into them. The whole usable window is
   about a tenth of an em, so this was chosen by rendering with the neon
   flicker frozen and reading each one:
     -.27  clear of the letters; reads as "sitting below"
     -.29  the O and B ascenders just break the bottom edge  <-- this
     -.34  rides ~35% up the cap band; legible but noticeably crossed
     -.37  the "O" is already being swallowed by the R of BAR
     -.42  unreadable; the script disappears into the letterforms
   Do not nudge this without re-rendering — and freeze `.fx--neon` when you
   do, or the capture lands on a grey dead frame and you will misjudge it.

   Legibility over the letters comes from .hl__on's own text-shadow (it is
   painted text, not a background-clip fill — see below), which is why the
   script survives crossing a lit violet cap at all.

   The clamp floor (30px) matters on phones: below it the script would
   drop under the .d1 ramp's own minimum and the tuck would invert. */
.fx .hl__on-line{display:block; line-height:0; margin-top:-.29em}
/* PLAIN PAINTED TEXT, not an .ink. Every other run in a headline paints
   itself as a background clipped to its own glyphs; this one paints its
   colour directly and takes a text-shadow instead of a drop-shadow halo.

   That is not a simplification for its own sake — it is the fix for a
   ghosting bug. This is the only run on the site that is both
   background-clip:text AND transformed (the -2deg tilt), and the two do
   not survive a parent transform together: hovering an event card, which
   animates translateY on .event__title, repainted the clipped fill from
   the element's untransformed origin and left a second "On Brady" lying
   across "HAUNTED". The stylesheet already carried the warning — "keep
   the headline out of transforms" — and this run was breaking it.

   Dropping the clip costs nothing here. The paint box padding existed to
   stop the fill slicing the script's overhangs, and painted text has no
   fill box to slice; the noise texture is invisible below ~40px; and
   card titles already switch their glow off at this size.

   display:inline-block is still load-bearing. It keeps the run atomic so
   the PARENT's clip cannot reach in and paint these glyphs a second time
   in the headline's own colour — which is a different double-render, and
   the one that shipped when this was display:block. */
.fx .hl__on{
  display:inline-block;
  /* PAINT ORDER, and it is not the DOM order that decides it.
     .hl__on-line is the LAST child of the headline, so the obvious
     assumption is that "On Brady" paints on top. It does not. Every .ink
     run carries `filter:var(--neon)`, and a filter makes a NON-POSITIONED
     element into a stacking context — which CSS 2.1 Appendix E paints in
     step 8, "as if it created a new stacking context with z-index:0".
     A plain static inline-block like this one paints back in step 6, with
     the ordinary inline content. Step 6 is before step 8, so the lit
     letterforms were painting straight over the script no matter where it
     sat in the markup, and the overlap read as "tucked behind".

     position:relative + z-index:1 moves it to step 9 (positive z-index),
     above the .ink runs. It stays an inline-block, so it is still an
     ATOMIC inline and the parent's background-clip:text cannot reach in
     and paint these glyphs a second time — which is the other bug this
     rule has to keep avoiding, see below. */
  position:relative; z-index:1;
  /* global now — see the note on .hl__on-line above */
  background-image:none;
  -webkit-background-clip:border-box; background-clip:border-box;
  color:var(--orange); -webkit-text-fill-color:var(--orange);
  font-size:clamp(30px, .40em, 74px);
  line-height:1;
  margin:0;
  padding:.16em .1em .3em .1em;
  text-transform:none;
  /* NO TRANSFORM. It carried a -2deg tilt and that is what put a second
     copy of "On Brady" across "HAUNTED" on the heroes — and only on the
     heroes, which is the tell: .fx--neon animates `filter` on the parent
     continuously, and a transformed child inside an animating-filter
     subtree gets rasterised from the wrong origin. The section lockups,
     which carry no .fx--neon, never showed it.

     Dropping the clip (below) was not enough on its own, because the
     hazard is the transform, not the clip. The stylesheet's own rule
     says it: keep the headline out of transforms. A 2deg tilt is not
     worth a ghost. */
  /* the halo, same argument as .hero--shot .fx .ink — a tight near-opaque
     core that IS the background at the stroke edge, then two softer
     radii. Flat px, because this run is clamped to 15-34px and an em
     radius at the bottom of that range is sub-pixel. */
  text-shadow:
    0 0 3px  rgba(6,6,5,1),
    0 0 7px  rgba(6,6,5,.95),
    0 0 16px rgba(6,6,5,.8);
}
/* THE DARK BACKING, on the card titles now as well.

   These used to carry `filter:none` — no neon glow, because at 30-92px
   a bloom is just a smudge, and no dark either, because the scrim under
   them was at 86% and nothing needed it. The scrim is at 15% now, so the
   dark moves here: the same device .hero--shot uses, for the same
   reason, and it is the reason a 15% wash is survivable at all.

   No colour glow, only dark — the smudge argument still holds at this
   size. Flat px radii, not em: these titles clamp from 30px to 92px and
   an em radius at the bottom of that range is sub-pixel. Sub-opaque
   throughout, so the falloff has no edge in it and reads as a shadow
   behind the letters rather than a keyline drawn round them.

   Measured with BSSAudit() on this wash: the worst painted edge on the
   homepage row is "Christmas" at 4.75:1, against the site's 3.0 floor —
   comfortably better than the 3.14 it managed on its own bespoke 85%
   plate, because a halo tracks the glyph and a plate cannot. */
.event__title .ink{
  /* FIVE layers here, where the hero gets two, and the difference is
     AREA. Filter cost is superlinear in the area rasterised, and the
     hero lockup is 190px against these titles' 30-92px — so layers that
     cost the hero 450ms of first paint cost these almost nothing, and
     they can have as many as they need.

     They need five. Laddered at 25%: three layers left "Hop" at 2.41 and
     "Christmas" at 2.94, four left "Hop" at 2.81, five clears the whole
     row with nothing under 3.4. Cut to one and it collapses to
     1.09-2.07, because a single sub-opaque layer cannot become a ground
     however wide it is — it is the stacking that approaches opacity.

     Unlike the hero these carry no colour glow, so this is the only
     filter on them and there is no bloom for the dark to push outward. */
  filter:
    drop-shadow(0 0 2px  rgba(6,6,5,.50))
    drop-shadow(0 0 4px  rgba(6,6,5,.50))
    drop-shadow(0 0 8px  rgba(6,6,5,.48))
    drop-shadow(0 0 14px rgba(6,6,5,.44))
    drop-shadow(0 0 24px rgba(6,6,5,.35));
}

/* ── THE DARK HALO — legibility that lives on the type ───────────────
   The heroes carry no scrim at all now, so a neon word can land on a
   blown-out highlight with nothing between them. Rather than dim the
   photograph to fix that, each glyph gets its own dark backing: two
   tight shadows at 0 offset, drawn BEFORE the colour glows so the bloom
   still reads outside them rather than being ringed by black.

   This is not a trick — it is how a neon sign photographs. The tube is
   bright, its backing board is dark, and the bloom sits outside both.
   Measured as an effective background (the halo is what actually
   surrounds each stroke), it holds the violet lockup over the brightest
   part of a frame, which no plate could do without greying the picture.

   TWO RULES ON THE SHAPE OF IT, both learned by shipping it wrong:
   · NOTHING FULLY OPAQUE. A tight shadow at alpha 1 is a crisp dark rim,
     and it reads as a black keyline drawn around the letters rather than
     as a shadow behind them. Every layer here is sub-opaque so the
     falloff is a gradient with no edge in it.
   · THE DARK MUST STAY INSIDE THE COLOUR. Chained drop-shadows each cast
     from the RESULT of the last, so a wide dark layer pushes the colour
     bloom outward and leaves a black band sitting between the glyph and
     its own glow — which is what "the glow looks off" was. The dark now
     totals ~10px at a 175px lockup and the neon blooms to ~19px past it,
     so the tube reads lit again.

   Only on .hero--shot: the cards and bands still have real scrims,
   where a halo would only muddy already-passing type.

   Every radius carries a px floor via max(). Pure em radii scale with
   the word, which is right for the glow and wrong for the halo: on a
   175px lockup .012em is 2.1px and does its job, but on the 34px "On
   Brady" line it is 0.4px — invisible, and that line sits on the same
   photograph. The floor is what the small type needs; the em term is
   what keeps the big type from wearing a hard outline. */
/* ONE dark layer, and the colour glow left at the site default.

   A hero lockup and a section lockup are the same object and have to look
   like it. They did not: the hero was carrying two dark layers plus a
   widened bloom (.04em/.105em against the standard .034em/.085em), and
   because chained drop-shadows each cast from the RESULT of the last, the
   dark was pushing the neon outward into a soft purple cloud while the
   section headline beside it stayed tight. Two lockups, two different
   signs.

   So the colour radii are now exactly `.fx .ink`'s — restated rather than
   inherited, because a filter list replaces wholesale — and the dark is a
   single 4.5px/.024em layer, which at a 190px lockup sits well inside the
   16px bloom and cannot be seen. It is there purely as the effective
   background at the stroke edge.

   That makes the DARKNESS THE OVERLAY'S JOB. The two are coupled and
   there is no third option: at the 50% the brief asks for, the worst
   painted edge on the homepage lockup reads 1.99:1; at 62% it reads
   2.65 and every other hero clears 3:1. The number to move is the
   overlay on .hero--shot > .hero__scrim, not this. */
/* THE DARK BACKING GOES LAST. This is the whole reason a hero lockup
   used to look like a different sign from the section headline below it,
   and it took three passes to find because the glow itself was never the
   problem — the ORDER was.

   Chained drop-shadows each cast from the RESULT of the last. With the
   dark layer first, every colour stop after it was pushed outward by the
   dark's own radius, so the bloom stopped hugging the glyph and became a
   detached ring floating 6px off it. A ring reads as a big soft halo; a
   bloom that starts at the letter reads as a lit tube. The hero's colour
   radii were actually SMALLER than the section's by then and it still
   looked like more glow, which is what finally gave it away.

   Moved to the end, the dark casts from (glyph + finished glow) and
   paints behind all of it: the neon is now pixel-identical to every
   other lockup on the site, and the dark is a soft aura on the
   photograph rather than a spacer inside the type. That is also how a
   real sign sits on a wall.

   No --neon-k here any more either. Halving the radii was compensating
   for the offset, and with the offset gone the heroes just use the same
   glow as everything else. */
/* THREE dark layers now, not one, because the scrim under them came down
   from 50% to 25% to match the event cards and this is where that
   darkness went.

   The order is still the thing that matters: var(--neon) first, dark
   LAST. Chained drop-shadows each cast from the RESULT of the last, so a
   dark layer placed before the colour pushes the bloom outward and
   leaves a black band between the glyph and its own glow — the bug
   written up at length above. Cast last, all three paint behind the
   finished neon and none of them touch it.

   A tight near-opaque core that IS the effective background at the
   stroke edge, then two softer radii so the falloff is a gradient with
   no edge in it. Nothing fully opaque, or it reads as a keyline drawn
   round the letters. Every radius keeps a px floor via max(): a pure em
   radius is right for the glow and wrong for the halo, because at the
   bottom of the clamp .016em is sub-pixel and the small type sits on the
   same photograph as the large. */
/* ONE dark layer, and the layer COUNT is a performance number.

   Chained drop-shadows each rasterise and blur the RESULT of the last,
   across the whole subtree — and this subtree is a 190px lockup, so the
   cost is superlinear in both the number of layers and the area.
   Measured on the Haunted page, first paint against layer count:

     no ink filter at all ............ 64-100ms
     --neon + ONE dark layer ........ 148-208ms
     --neon + THREE dark layers ..... 608-668ms

   Three cost about 450ms of blank screen on every navigation, which was
   most of "the site feels laggy between pages". So it is one layer, made
   denser and tighter than the .92/10px it used to be rather than
   repeated: a tight near-opaque layer is what sets the effective
   background at the stroke edge, and that is the whole job. The two
   softer radii were buying feathering, not contrast.

   NOT A TEXT-SHADOW, and this was tried. text-shadow would cost nothing
   here — it paints from the glyph outline with no subtree raster — but
   an .ink is background-clip:text with a transparent fill, and the
   shadow ends up painting OVER the clipped background instead of behind
   it: the lockup rendered as black letters inside a violet glow, 1.15:1.
   .hl__on can use a text-shadow because it paints its colour directly.
   An .ink cannot.

   TWO layers, not one, and not three. One cannot reach the floor at any
   radius — laddered at 10/14/18/24/30px it plateaus at 2.1-2.3 for the
   violet, because a single sub-opaque layer cannot become a ground no
   matter how wide it is; it is the STACKING that approaches opacity.
   Two gets there, and stops one rasterisation short of the three that
   cost 450ms. */
.hero--shot .fx .ink{
  filter:
    var(--neon)
    drop-shadow(0 0 max(4px,  .022em) rgba(6,6,5,.97))
    drop-shadow(0 0 max(12px, .06em)  rgba(6,6,5,.92));
}

/* A .fx headline's drop-shadow is ONE filter over its whole subtree, so it
   cannot be scoped away from a differently-inked word: "Haunted" in violet
   was rendering a violet halo sitting inside the headline's orange one, and
   the glow read as the wrong colour.

   .fx--split says every visible run in this headline carries its own .ink,
   so the parent contributes no halo at all and each word glows in its own
   colour. It works by emptying --fx-filter rather than setting
   filter:none, because the neon keyframes animate var(--fx-filter) too —
   this way the flicker still dims the whole lockup, it just has no colour
   of its own to add. */
.fx--split{--fx-filter:none}

/* per-event colourways — orange is the house accent, the other two
   belong to their event and are never used off it */
.fx--orange{
  --fx-c:var(--orange);
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='.25'/%3E%3C/svg%3E"),
    linear-gradient(var(--fx-c),var(--fx-c));
}
.fx--green{
  --fx-c:var(--green);
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='.25'/%3E%3C/svg%3E"),
    linear-gradient(var(--fx-c),var(--fx-c));
}
.fx--red{
  --fx-c:var(--red);
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='.25'/%3E%3C/svg%3E"),
    linear-gradient(var(--fx-c),var(--fx-c));
}
/* Safari repaints background-clip:text badly inside a transform or a
   will-change layer; keep the headline out of both. */
@supports not ((-webkit-background-clip:text) or (background-clip:text)){
  .fx{color:var(--fx-c); background-image:none}
}
.kicker{
  font:500 11.5px/1.3 var(--body); text-transform:uppercase;
  letter-spacing:.24em; color:var(--text-3); margin:0;
}
.kicker--paper{color:rgba(255,255,255,.72)}
.lede{font-size:clamp(18px,1.55vw,22px); line-height:1.6; color:var(--text-2); max-width:34ch; margin:0}
.body p{margin:0 0 1.05em; max-width:62ch}
.body p:last-child{margin-bottom:0}
.small{font-size:14px; line-height:1.6; color:var(--text-2)}
.tiny{font:500 11px/1.4 var(--body); text-transform:uppercase; letter-spacing:.18em; color:var(--text-3)}


/* ============================================================
   TEXTURE — the thing that makes it feel printed
   ============================================================ */
.grain{position:relative; isolation:isolate}
.grain::after{
  content:""; position:absolute; inset:0; z-index:2; pointer-events:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='.55'/%3E%3C/svg%3E");
  /* overlay, not multiply: multiplying noise into a near-black ground
     produces nothing at all. Overlay lifts and drops around the mid
     point, so the tooth survives on black. */
  opacity:.26; mix-blend-mode:overlay;
}
.grain--soft::after{opacity:.14}
.grain--dark::after{mix-blend-mode:overlay; opacity:.3}

/* ============================================================
   TORN EDGE
   Sits on the seam where a light field meets a dark one. The SVG is
   filled with the colour of the field it belongs to, so the sheet reads
   as torn and overlapping rather than as a decorative border.
   --tear-fill says which field; .tear--up flips it for a bottom edge.
   Negative margins pull the seam over the join so there is no hairline.
   ============================================================ */
.tear{
  display:block; width:100%; height:clamp(14px,1.8vw,26px); line-height:0;
  position:relative; z-index:1; pointer-events:none;
  color:var(--tear-fill, var(--paper));
}
.tear svg{display:block; width:100%; height:100%; fill:currentColor}
.tear--down{margin-bottom:-1px}
.tear--up{margin-top:-1px; transform:rotate(180deg)}
/* the fixed header tears into the page rather than ending on a straight
   edge. It hangs below the bar, so it must not intercept clicks (it is
   already pointer-events:none) and it hides while the header is
   transparent over a hero — a torn edge with nothing behind it reads as
   a stray shape rather than a torn sheet. */
.tear--hdr{
  position:absolute; left:0; right:0; top:100%;
  height:clamp(9px,1.1vw,15px);
  /* No shadow. The torn edge reads by contrast against whatever is
     scrolling underneath — a photo, the white calendar, a card. Sitting
     over a flat --paper section it is invisible, which is what a torn
     sheet on matching paper actually looks like. */
  z-index:1;
  transition:opacity .3s var(--quart);
}
/* flat edge against the bar, torn edge hanging into the page */
.tear--hdr svg{transform:scaleY(-1)}
.hdr--over .tear--hdr{opacity:0}

/* ============================================================
   ENTRANCE REVEAL
   Deliberately quiet — a 14px rise and a fade, once, on the way in. The
   hidden state is scoped to html.has-js, which the script only adds when
   it is actually going to run: no JS, or prefers-reduced-motion, and the
   page renders fully visible with no transition at all.
   Only transform and opacity animate, so this stays on the compositor.
   ============================================================ */
.has-js [data-reveal] > *,
.has-js [data-reveal]:not(:has(> *)){
  opacity:0;
  transform:translate3d(var(--tx,0px),calc(14px + var(--ty,0px)),0)
            rotate(var(--rot,0deg)) scale(var(--sc,1));
  transition:opacity .62s var(--ease), transform .62s var(--ease);
  will-change:opacity, transform;
}
/* This used to be `transform:none`, which flattened anything that carried a
   resting rotation OR a hover transform of its own — and because this rule
   is (0,3,0) it beat every `.thing:hover{transform:…}` on the site. Two
   things were silently broken by it: the polaroids rendered dead straight
   (their .tilt-* wiped the moment they revealed), and the ticket inside a
   reveal group never tilted on hover while identical tickets outside one
   did.

   So the transform is now composed entirely from three variables that any
   descendant is free to set — --tx/--ty (offset), --rot (angle), --sc (scale) —
   and interaction states set THOSE rather than fighting for `transform`.
   Specificity stops mattering: whichever rule wins still reads the same
   three values. Anything that sets none of them resolves to the identity
   and behaves exactly as before. */
.has-js [data-reveal] > .is-in,
.has-js [data-reveal].is-in:not(:has(> *)){
  opacity:1;
  transform:translate3d(var(--tx,0px),var(--ty,0px),0)
            rotate(var(--rot,0deg)) scale(var(--sc,1));
}
/* once it has landed, stop advertising the layer */
.has-js [data-reveal] > .is-in{will-change:auto}

@media (prefers-reduced-motion: reduce){
  .has-js [data-reveal] > *,
  .has-js [data-reveal]:not(:has(> *)){
    opacity:1 !important; transform:none !important; transition:none !important;
  }
  *,*::before,*::after{
    animation-duration:.001ms !important; animation-iteration-count:1 !important;
    transition-duration:.001ms !important; scroll-behavior:auto !important;
  }
}

/* ============================================================
   FULL-BLEED HERO
   Picture edge to edge, header riding over it, everything centred.
   ============================================================ */
.hero{
  position:relative; isolation:isolate; overflow:hidden;
  min-height:100vh; min-height:100svh;
  display:grid; place-items:center; text-align:center;
}
.hero > .hero__media{position:absolute; inset:0; z-index:0; place-self:stretch}
/* the "photo goes here" marker sits in a corner, not behind the headline */
.hero > .hero__media .photo__cap{
  position:absolute; left:22px; bottom:20px; z-index:3; text-align:left;
  max-width:22ch; opacity:.4; padding:0;
}
@media (max-width:640px){ .hero > .hero__media .photo__cap{left:18px; bottom:16px} }
.hero__media img{width:100%; height:100%; object-fit:cover}

/* ── REAL PHOTOGRAPHY IN A HERO ──────────────────────────────
   A daylight source frame is graded toward dusk so a sunny picture
   can still sit under an October headline. The halftone stays but is
   dialled right down — print texture over a photo, not a riso wash.

   The night gradient stays underneath as the fallback, so a hero whose
   image is missing or still loading looks like the placeholder design
   rather than a black box.

   NOTE the specificity trap in the README: the scrim override below
   matches `.hero > .hero__scrim` at (0,2,0) and only wins by being
   declared later. Keep it after that rule. */
.hero > .hero__media--shot{
  overflow:hidden;
  /* THE THING BEHIND THE PHOTOGRAPH, and it is on screen more often than
     it looks. This was the full orange -> red -> black gradient, written
     when the heroes had no pictures and that gradient WAS the hero. Now
     every one of them carries a 300-500KB photograph, and this paints for
     the frame or two before it decodes — so navigating between pages
     flashed bright orange and then cut to a dark photo. That is the page
     flicker.

     Kept as a gradient rather than flat, because a genuinely missing
     image should still look composed, but pulled down to the page's own
     ground: any gap now reads as the site continuing, not as a flash. */
  background:linear-gradient(152deg,#231009 0%,#170c0a 46%,var(--ink-deep) 100%);
}
.hero__media--shot img{
  width:100%; height:100%; object-fit:cover; display:block;
  /* IN COLOUR. These were fully desaturated and pulled down two stops;
     the pictures are good enough to run as pictures, so the grade is now
     barely there — a touch of saturation and contrast, nothing else.

     Everything the old grade was secretly doing for legibility has moved
     onto the TYPE — see the dark halo on .hero--shot .fx .ink. That is
     the whole trade: give the letterforms their own backing and leave
     the photograph alone.

     --hero-b is still exposed because exposure belongs to the PICTURE,
     not the component. It now defaults to 1 (untouched) and a frame that
     is genuinely over-bright can dial itself back without every other
     hero being pulled down to suit it. */
  filter:saturate(1.06) contrast(1.03) brightness(var(--hero-b, 1));
  /* --hero-pos is the crop. It matters far more now than it did when
     every hero was desaturated and two stops down, because WHERE the
     bright part of a picture lands is now the whole legibility argument:
     the plate can only be so strong before the photograph is grey again,
     so the honest fix for a hot subject is to crop it out from under the
     words rather than to bury it. Portrait sources especially — a 2:3
     frame in a 16:10 hero throws away 60% of its height, and which 40%
     you keep is a composition decision, not a default. */
  object-position:50% var(--hero-pos, 50%);
  /* if the file is missing or still loading, the browser paints the alt
     text across the hero. Hide it visually — the gradient underneath is
     the intended fallback. Screen readers still get the alt attribute. */
  color:transparent; font-size:0;
}
/* warm dusk wash — marries the frame to the palette without hiding it.
   Halved now that the picture is in colour: at the old strength it was
   tinting a colour photograph orange rather than warming a grey one. */
.hero__media--shot::before{
  content:""; position:absolute; inset:0; z-index:1; pointer-events:none;
  background:linear-gradient(158deg,
    rgba(255,77,20,.06) 0%, rgba(232,32,42,.04) 45%, rgba(17,17,16,.10) 100%);
  mix-blend-mode:multiply;
}
/* print texture over photography, low (style guide calls for 8-15%) */
.hero__media--shot::after{
  content:""; position:absolute; inset:0; z-index:2; pointer-events:none;
  background-image:radial-gradient(rgba(17,17,16,.3) 1.4px, transparent 1.5px);
  background-size:7px 7px; mix-blend-mode:multiply; opacity:.09;
}
/* guarantees the centred type stays legible whatever picture lands here */
.hero > .hero__scrim{
  position:absolute; inset:0; z-index:1; place-self:stretch;
  /* Two layers. The linear wash is the old full-height scrim. The radial
     is new: a soft plate centred on the headline, because a headline in
     --orange / --red measures 1.3:1 against the bright sky in the Brady
     Street picture and only ~2.8:1 against a green placeholder field.
     Darkening the whole photo to fix that killed the picture; darkening
     only the middle keeps the edges readable as an image. */
  background:
    radial-gradient(125% 82% at 50% 42%,
      rgba(8,8,7,.78) 0%, rgba(8,8,7,.60) 46%, rgba(8,8,7,0) 88%),
    linear-gradient(180deg,rgba(10,10,9,.44) 0%,rgba(10,10,9,.24) 42%,rgba(10,10,9,.58) 100%);
}
/* ── NO OVERLAY ON A HERO CARRYING A REAL PICTURE ────────────────────
   This element used to be the whole legibility system: a 125%x82%
   radial at .77 over a full-height wash, on top of a desaturated,
   two-stops-down image. It came down in stages — first to a 13% wash
   plus a plate cut to the type — and is now off entirely. The picture
   is the picture.

   Legibility moved onto the TYPE instead. See .hero--shot .fx .ink: a
   tight dark halo is drawn behind each glyph's own colour, which is how
   a neon tube reads against its own backing board in a real photograph.
   That protects the letterforms without touching a single pixel of the
   photograph, which no amount of scrim tuning can do.

   What is left is two flat layers and no plate:

   · a flat 38% black across the whole frame. Even, deliberately — it is
     an exposure decision applied to the picture, not a shape cut around
     the words, so the photograph reads the same in the corners as it
     does behind the lockup.

     38 AND NOT THE CARDS' 25, and the gap is a performance decision
     rather than a design one. The darkness a mid-luminance ink needs has
     to come from either the scrim or the halo, and on a 190px lockup
     halo layers are expensive: 25% needs three of them to clear 3:1 and
     three cost ~450ms of first paint. 38% clears it with two. Laddered
     with two layers, the violet reads 2.69 / 2.91 / 3.11 at 25 / 32 /
     38, so 38 is the first step that passes. The cards sit at 25 because
     their titles are 30-92px, where three layers cost nothing.
   · a shallow gradient over the top of that. Not a photo treatment but
     chrome: the fixed header floats transparently over the first 60px
     of every hero and its links have to sit on something predictable.

     THIS ONE WENT UP WHEN THE FLAT WASH CAME DOWN, and the arithmetic is
     the point. The two layers composite, so what the header actually
     sits on is 1-(1-top)(1-flat). At the old .34 over a .50 flat that
     was .67. Leaving .34 over the lighter flat would have dropped it to
     .505 — and the Shamrock hero has a lit television in its top-left
     corner, directly behind the logo, where 13px nav links need 4.5:1
     and would have had about 3.6. Solving 1-(1-x)(1-.38)=.67 gives
     x=.47, so the top stop is .47 and the 9% stop scales with it. The
     header is protected exactly as well as it was at 50%; the picture
     below it is not touched.

   IT IS NO LONGER A HERO-ONLY NUMBER, and the note that used to say so
   has been rewritten rather than deleted, because the reason it gave was
   sound at the time. It said the cards "have no room to plate only the
   type, so the two were never the same instrument". That was true while
   the cards' protection was a 86% scrim on .event__shot::before. It is
   not true any more: the cards now carry the same dark halo on their own
   ink that the heroes do, which needs no room at all, so both surfaces
   are running the same instrument and 25% is deliberately the same
   number on each. A hero and a card of the same event should not be two
   different exposures of it.

   The halo on the type is what makes that survivable, and it is doing
   more of the work than it used to. History worth keeping: at 20% the
   halo alone put the worst painted edge at 2.17-2.71 across the four
   heroes; 50% cleared 3:1 outright with the halo as the belt rather than
   the braces. 38% sits between those, and the halo on
   .hero--shot .fx .ink went from one layer to two to take the
   difference back — see the note there for why not three. */
.hero--shot > .hero__scrim{
  background:
    linear-gradient(180deg,
      rgba(8,8,7,.47) 0%, rgba(8,8,7,.20) 9%, rgba(8,8,7,0) 18%),
    linear-gradient(rgba(8,8,7,.38), rgba(8,8,7,.38));
}
/* No per-ink variants any more. --red and --violet each needed their own
   deeper scrim when the scrim was the only protection; the halo on the
   ink is per-word by construction, which is a far smaller instrument. */
.hero__inner{
  /* 1000px was an Anton-era measure. Jomhuria is ~1.5x wider at the same
     cap height, so "SHAMROCK" ran 541px past the old column. The sub-copy
     is unaffected — .hero__sub caps itself at 46ch. */
  position:relative; z-index:2; width:100%; max-width:1320px; place-self:center;
  padding:calc(var(--hdr-h) + clamp(40px,7vh,88px)) 28px clamp(44px,7vh,84px);
  display:grid; justify-items:center; gap:clamp(18px,2.6vh,30px);
  color:var(--ink);
}
.hero__inner .kicker{color:rgba(255,255,255,.86)}
/* A headline carrying .fx paints its own fill through background-clip:text,
   so it must keep color:transparent. This rule is (0,2,0) and would beat
   .fx at (0,1,0) — the same specificity trap `.photo` hits elsewhere in
   this file. Any NEW rule that sets `color` on .d1/.d2/.d3 needs the same
   :not(.fx) guard, or the headline silently goes solid. */
.hero__inner .d1:not(.fx),.hero__inner .d2:not(.fx){color:var(--ink)}
/* The date line used to be a 12px tracked-out body label — a caption under
   a 190px headline, which made the one piece of hard information on the
   page the smallest thing on it. It now takes the .h4 display face (the
   "THREE DAYS A YEAR" setting), sans only: the script would fight the
   lockup directly above it. Line-height is .95 rather than .464 because
   this wraps to two lines on narrow screens and .h4's tight leading is
   drawn for single-line headings. */
.hero__meta{
  font-family:var(--display); font-weight:400; text-transform:uppercase;
  font-size:clamp(26px,2.9vw,40px); line-height:.95; letter-spacing:.05em;
  color:rgba(255,255,255,.94); margin:0;
}
/* The separators get their own element because Jomhuria's middle dot is
   wrong for this job twice over: the face has a very small cap height
   relative to its em (~.39), so a dot positioned for a normal x-height
   floats well above the cap band — and at 40px it comes out as a heavy
   blob. Set in the body face at a fraction of the size and nudged to the
   optical centre of the caps, it reads as punctuation rather than as a
   fourth word. */
.hero__meta .sep{
  font-family:var(--body); font-size:.34em; letter-spacing:0;
  /* Measured, not guessed: at a 37px meta the cap band's optical centre
     sits 7.2px above the baseline and this dot's ink centre sits 3.65px,
     so it needs .0957em of the META size lifting — which is .28em of its
     own. Both terms are proportional, so it holds at every clamp step. */
  vertical-align:.28em;
  margin:0 .5em;
  opacity:.62;
}
/* The plain text on a photographic hero gets the same treatment the
   headline does, by the only means available to it: a text-shadow.
   .hero__meta and the ticket's note are ordinary text, not
   background-clip fills, so a shadow works where a drop-shadow filter
   would also blur the glyphs. Three radii — a tight near-opaque core
   that IS the effective background at the stroke edge, then two softer
   ones so it reads as a glow rather than an outline.

   Only inside .hero--shot. On the paper heroes there is no picture and
   this would just be a smudge. */
.hero--shot .hero__meta,
.hero--shot .hero__inner .ticket__note{
  text-shadow:
    0 0 4px rgba(6,6,5,.98),
    0 0 10px rgba(6,6,5,.92),
    0 0 22px rgba(6,6,5,.75);
}
@media (max-width:640px){ .hero__inner{padding-left:18px; padding-right:18px} }

/* ── THE FONT GATE ───────────────────────────────────────────────────
   The hero's content waits for the real typefaces, then fades in.

   This is not polish, it is a correctness fix. Every display measure on
   this site is tuned to JOMHURIA'S metrics — --hl-lead .46, the
   --fx-pt/--fx-pb paint-box padding, the script's -.29em tuck, and the
   negative margins that cancel all of it. Those numbers are meaningless
   against the fallback (Arial Narrow, with a cap height more than twice
   Jomhuria's), so for the moment before the woff2 lands the lockup does
   not merely look wrong, it COLLIDES: on the Shamrock hero "Shuffle"
   lands on top of "SHAMROCK 18". font-display:swap is what asks for
   that frame, and it is the right setting for body copy — text you can
   read in the wrong face beats text you cannot read at all. A display
   lockup whose layout is metric-tuned is the exception.

   So the gate is on .hero__inner rather than on .fx: .fx and .fx--lit
   already own their `animation` property for the neon, and a second
   animation there would fight the tube striking. Fading the hero's whole
   content block also keeps the lockup, the meta line and the ticket
   arriving together rather than in pieces.

   opacity only, never visibility or display: the box still lays out, so
   nothing below the hero moves when it appears. The class is removed by
   the guard in every page's <head> on document.fonts.ready, with a 1.5s
   cap so a font that never arrives cannot leave an empty hero.

   .pagehead is gated with it. Only four pages carry a photographic hero;
   the other five open on a .pagehead instead, and its headline is the
   same metric-tuned display type with the same script tuck in it. Same
   problem, same fix. */
.hero__inner,
.pagehead{transition:opacity .42s var(--ease)}
.fonts-pending .hero__inner,
.fonts-pending .pagehead{opacity:0}
@media (prefers-reduced-motion:reduce){
  .hero__inner,
  .pagehead{transition:none}
}

/* centred page heads on the pages that keep a paper hero */
.pagehead{text-align:center; display:grid; justify-items:center; gap:clamp(14px,2vw,22px)}
.pagehead .lede{max-width:52ch}

/* --- slim head: utility pages put the content first ------------------
   Calendar, tickets, about and contact used the same full centred hero as
   the event pages, which pushed the thing people actually came for (the
   calendar grid, the ticket list, the form) most of a viewport down. Here
   the headline drops to .d3 scale, left-aligns, and the lede paragraph is
   dropped, so the content starts just under the kicker rule. Event pages
   keep the tall centred hero — this is deliberately only for the four. */
.pagehead--slim{
  text-align:left; display:flex; align-items:flex-end;
  justify-content:space-between; flex-wrap:wrap;
  gap:clamp(16px,3vw,44px);
}
.pagehead--slim > :first-child{margin:0}
/* The exception to the merge above. These five heads were deliberately
   dropped to a smaller scale so the calendar, ticket list and form start
   near the top of the page instead of a viewport down; they keep the old
   .d3 ramp while every other headline moves up. */
.pagehead--slim .d3{font-size:clamp(34px, calc(30.1px + 2.63vw), 68px)}
/* the head is now folded into the first content section rather than sitting
   in a band of its own, so it owns the gap to the content below it */
.pagehead--slim{margin-bottom:clamp(26px,3.4vw,52px)}
/* Folding the head into whatever section came first meant it inherited that
   section's top padding — .band--tight on two pages, the default .band on
   three, which put the five menu headlines at two different heights (125px
   vs 173px). Normalise on the section that actually hosts a head so the set
   lines up whichever band it landed in. */
.band:has(> .shell > .pagehead--slim),
.band:has(> .shell--tight > .pagehead--slim){
  padding-top:clamp(40px,4.6vw,72px);
}
.pagehead--slim .taped{flex:0 0 auto; width:clamp(124px,13vw,168px)}
@media (max-width:640px){
  .pagehead--slim{gap:18px}
  .pagehead--slim .taped{width:150px}
}

/* ============================================================
   SHELL & GRID
   ============================================================ */
.shell{max-width:var(--shell); margin:0 auto; padding:0 28px}
/* the closing bands put a .d1 in here; 1000px was an Anton-era measure
   and clipped "So, are we Crawling?" by 102px */
.shell--tight{max-width:1180px}
@media (max-width:640px){ .shell{padding:0 18px} }

section{position:relative}
.band{padding:clamp(56px,7.5vw,120px) 0}
.band--tight{padding:clamp(36px,4.2vw,64px) 0}
/* --paper2 and --grey used to step the ground up through --paper-2 and
   --paper-3 for visual rhythm between sections; Tanner asked for one flat
   dark gray everywhere except image sections (2026-09-02), so both classes
   now resolve to --paper, same as the unmodified .band. Kept as separate
   classes rather than stripped from markup, in case the step returns later.
   --ink-deep (.band--ink, .ftr) is deliberately UNCHANGED — the closing CTA
   and footer stay the darkest ground on purpose, confirmed with Tanner.
   --paper-2/--paper-3 tokens themselves are untouched: cards, the price
   panel, the drawer-link hover and .photo--grey still use them to read as
   raised OVER this new flat ground — only full-bleed section backgrounds
   were flattened. */
.band--paper2{background:var(--paper)}
.band--ink{background:var(--ink-deep); color:var(--ink)}
.band--ink .lede,.band--ink .small{color:rgba(255,255,255,.7)}
.band--ink .kicker{color:rgba(255,255,255,.66)}
/* NOTE: .band--ink .tiny / .ftr .tiny used to be grouped into the
   .band--orange rule below, which meant every small label on a dark band
   was silently painted with an orange background — a full-width orange
   stripe behind the block-level ones. They only ever wanted a colour. */
.band--ink .tiny,.ftr .tiny{color:rgba(255,255,255,.72)}.band--orange .lede{color:rgba(17,17,16,.86)}
.band--orange .kicker{color:rgba(17,17,16,.84)}
.band--orange .btn{border-color:var(--on-accent); color:var(--on-accent)}
.band--orange .btn:hover{background:var(--on-accent); color:var(--orange)}
.band--grey{background:var(--paper)}

/* ============================================================
   ON-WHITE — the calendar prints on white stock
   The token names already mean the right thing: --paper is "the surface
   you print on", --ink is "the mark on it". So this is a straight value
   swap, not a repaint, and every component built on those two inverts on
   its own. Three things do NOT come along for the ride:
     · --on-accent stays near-black. It is the colour of type sitting ON
       an accent, and orange is mid-luminance — white on --orange is
       3.3:1 and fails whichever way round the page is.
     · --orange-on is swapped for --orange-d. #FF8A4D is tuned for small
       type on black; on white it measures under 3:1, --orange-d gives 4.9.
     · anything painting type on a solid --ink fill (buttons, active tabs)
       has to take --paper instead, or it lands black-on-black.
   ============================================================ */
.on-white{
  --paper:      #F8F8F6;
  --paper-2:    #EFEFEB;
  --paper-3:    #E4E4DF;
  --ink:        #111110;
  --ink-deep:   #FFFFFF;
  --ink-2:      #DCDCD6;
  --stone:      #C9C9C2;
  --text:       var(--ink);
  --text-2:     rgba(17,17,16,.74);
  --text-3:     rgba(17,17,16,.62);   /* .56 measured 4.24:1 — under 4.5 for 11px labels */
  --rule:       rgba(17,17,16,.16);
  --rule-hard:  rgba(17,17,16,.72);
  --orange-on:  var(--orange-d);
  --green-on:   var(--green-d);
  background:var(--paper); color:var(--text);
}
/* the grain is tuned to lift a near-black ground; on white it only muddies */
.on-white.grain::after,.on-white .grain::after{display:none}

/* type on a solid --ink fill flips with it */
.on-white .btn:hover{background:var(--ink); color:var(--paper)}
.on-white .btn--paper{border-color:rgba(17,17,16,.5); color:var(--ink)}
.on-white .btn--paper:hover{background:var(--ink); border-color:var(--ink); color:var(--paper)}

/* the sheet is redundant once the whole band is white — keep its spacing,
   drop the panel so it does not read as a black box (--ink is now black) */
.on-white .sheet{background:transparent; color:inherit; padding:0}

/* a white headline glow is invisible on white; invert the halo */
/* NO GLOW ON A WHITE GROUND. Inside .on-white the ink token has already
   inverted to near-black, so --neon was deriving a dark halo from it —
   technically consistent, and wrong. The glow is a neon-sign device: a
   tube emits light onto a dark surround. Black type on white emits
   nothing, and a dark halo under it just reads as a printing fault.

   `none` rather than a tuned-down version, because there is no correct
   amount of glow here — the effect does not belong on this ground at
   all. Setting --neon empties both users of it: .fx's own --fx-filter
   and the filter on each .ink run. */
.on-white .fx,.on-white .fx .ink{--neon:none}
/* --orange-lt (#FF8A4D) is tuned for small type on black and measures
   2.3:1 on white. --orange-d is 4.9:1. Body links inside a white band take
   the dark orange; the underline is kept because colour alone is not a
   sufficient affordance for a link inside a paragraph. */
.on-white .lede a,.on-white .small a,.on-white p a{
  color:var(--orange-d); text-decoration:underline;
  text-underline-offset:.16em; text-decoration-thickness:1px;
}
.on-white .kicker--paper{color:rgba(17,17,16,.62)}
/* defensive: if a band--ink ever lands inside an on-white scope, its
   hardcoded rgba(255,255,255,…) text would go white-on-white */
.on-white .band--ink .lede,.on-white .band--ink .small{color:var(--text-2)}
.on-white .band--ink .kicker,.on-white .band--ink .tiny,
.on-white .band--ink .ticket__note{color:var(--text-3)}
.on-white .sticker--ghost-paper{border-color:rgba(17,17,16,.4); color:var(--ink)}
/* A low-opacity WHITE fill does nothing on a white ground — the chip
   vanishes and the composited fill actually darkens slightly, dropping
   --orange-d to 4.50:1. Give it a near-white fill and a hairline so it
   still reads as a tag: 4.94:1. */
.on-white .sticker{
  background:rgba(255,255,255,.72); color:var(--orange-d);
  box-shadow:inset 0 0 0 1px rgba(17,17,16,.14);
}
.on-white .sticker--ghost,.on-white .sticker--ghost-paper{background:transparent}

.cols{display:grid; gap:clamp(24px,3vw,48px)}
.c-2{grid-template-columns:repeat(2,minmax(0,1fr))}
.c-3{grid-template-columns:repeat(3,minmax(0,1fr))}
.c-4{grid-template-columns:repeat(4,minmax(0,1fr))}
/* asymmetric splits — never 50/50 unless it earns it */
.c-wide-left{grid-template-columns:minmax(0,1.62fr) minmax(0,1fr)}
.c-wide-right{grid-template-columns:minmax(0,1fr) minmax(0,1.62fr)}
.c-offset{grid-template-columns:minmax(0,.42fr) minmax(0,1fr)}
/* c-offset pairs a narrow label column with a wide content column. The
   numbered eyebrows that filled that label column are gone, leaving six of
   these grids with a single child — which would otherwise be squeezed into
   the .42fr column. Span it instead. Self-healing: put a label back and the
   two-column layout returns on its own. */
.c-offset > :only-child{grid-column:1 / -1}

/* ── THE CTA PAIRING: copy left, one ticket right ───────────────────────
   Measured before changing anything, at 1440px: the grid is 1304 wide, the
   copy stops at 573, and the 220px ticket sat left-aligned in a 481px
   column — leaving a 207px void mid-row and a 261px orphan to its right,
   with the ticket vertically centred against a taller text block so no two
   edges in the section lined up. That is the "something feels off".

   Two shared edges fix it. The ticket hangs on the grid's right edge, which
   is also the shell's right margin and the edge the header button sits on;
   and the row is bottom-aligned so the ticket's caption closes on the same
   line as the last line of copy. The headline keeps the top-left corner,
   the ticket takes the bottom-right. */
.cols--cta{align-items:end}
.cols--cta > .pitch{justify-self:end; width:100%}

/* ── THE PITCH ───────────────────────────────────────────────
   The right column used to be a lone ticket floating in a 260px
   orphan of empty grid. It now carries the actual Haunted Bar Hop
   card — the same component the homepage and About use, so the thing
   being sold is SHOWN rather than described — and the ticket is
   tucked into its bottom-right corner, overlapping the frame and
   hanging off the edge like a stub pushed under a poster.

   The overlap is the point: two flat elements side by side read as a
   layout, one object sitting on another reads as a physical thing. */
.pitch{position:relative; width:100%}
.pitch__card{display:block; text-decoration:none}
/* .pitch .pitch__ticket, not .pitch__ticket: .ticket sets width:100% and
   its own max-width clamp, and it is declared LATER in this file, so a
   single-class override here loses on source order regardless of intent. */
.pitch .pitch__ticket{
  position:absolute;
  /* Hangs BELOW the card, not past its right edge. An absolutely
     positioned child still counts toward document scrollWidth, and a
     right-hang of 24-31px on a card already sitting on the shell's right
     margin pushed the page 2-7px wide at some widths. Vertical overlap
     plus the rotation carries the same "sitting on top of it" read with
     no horizontal cost — the tilt itself only reaches ~4px past the
     edge, which the shell padding absorbs. */
  right:0; bottom:-11%;
  width:min(46%, 200px); max-width:none;
  --rot:-5deg;
  z-index:3;
  filter:drop-shadow(0 10px 22px rgba(0,0,0,.55));
}
/* straightens and lifts further than a loose ticket does, because this
   one is pinned under something and has to look like it is coming out */
@media (hover:hover){
  .pitch .pitch__ticket:hover{--rot:-1.5deg; --ty:-7px}
}
/* the stub slides out from under the card as the section arrives.
   `backwards` and NOT `both`: the fill must not survive the animation,
   or it would pin `transform` and the variable-driven hover above would
   stop working — the end state is already the element's resting style. */
@keyframes bss-tuck{
  from{ opacity:0; transform:translate3d(0,26px,0) rotate(-12deg) }
  to  { opacity:1; transform:translate3d(0,0,0) rotate(-5deg) }
}
.pitch.is-in .pitch__ticket{ animation:bss-tuck .74s var(--quart) .26s backwards }
@media (prefers-reduced-motion: reduce){
  .pitch.is-in .pitch__ticket{animation:none}
}

@media (max-width:900px){
  .cols--cta{align-items:start}
  .cols--cta > .pitch{justify-self:stretch}
}
@media (max-width:520px){
  /* at phone width the overlap crowds the meta row — the stub drops
     below the card and sits square */
  .pitch .pitch__ticket{
    position:static; width:clamp(150px,52%,210px);
    margin:14px 0 0 auto; display:block;
    --rot:-3deg; filter:none;
  }
}
@media (max-width:900px){
  .c-3,.c-4{grid-template-columns:repeat(2,minmax(0,1fr))}
  .c-2,.c-wide-left,.c-wide-right,.c-offset{grid-template-columns:minmax(0,1fr)}
}
@media (max-width:560px){ .c-3,.c-4{grid-template-columns:minmax(0,1fr)} }

.stack{display:grid; gap:18px; align-content:start}
.stack--lg{gap:30px}
.row{display:flex; gap:16px; flex-wrap:wrap; align-items:center}
.row--between{justify-content:space-between}
.row--end{justify-content:flex-end}


/* ============================================================
   HEADER — sticky, paper, one hairline
   ============================================================ */
/* The header is injected by JS, so between first paint and boot the bar is
   simply absent — on a fast refresh you see the hero slide up under nothing
   and then the bar drop in. body already reserves the height, so this is
   not a layout shift, it is a missing surface. Painting the empty holder as
   the bar means the correct block of colour is there on the very first
   frame and the real header replaces it in place.

   :empty stops applying the moment innerHTML lands, so there is nothing to
   clean up and no risk of a stray bar if boot never runs. */
#chrome-header:empty{
  position:fixed; top:0; left:0; right:0; z-index:80;
  height:var(--hdr-h); background:var(--paper);
}
body.has-hero #chrome-header:empty{background:transparent}

.hdr{
  position:fixed; top:0; left:0; right:0; z-index:80;
  background:var(--paper);
  transition:background .3s var(--quart);
}
/* .tear--hdr hangs below the bar, so the header cannot clip it */
.hdr{overflow:visible}
/* pages whose hero is a full-bleed image: the bar rides over the picture until
   you scroll, the way the Shamrock site does it */
.hdr--over{background:transparent}
.hdr--over .nav > a,.hdr--over .nav button{color:rgba(255,255,255,.85)}
.hdr--over .nav > a:hover,.hdr--over .nav button:hover{color:var(--ink)}
.hdr--over .nav > a.is-active{color:var(--ink)}
/* Floating over a hero the three marks lose their glow — a white
   drop-shadow on white type over a photograph is a smudge, not a lift —
   and drop a step in opacity so they read as chrome sitting on the
   picture rather than type printed into it. */
.hdr--over .nav a.nav__mark{color:rgba(255,255,255,.85); filter:none}
.hdr--over .nav a.nav__mark:hover,
.hdr--over .nav a.nav__mark.is-active{color:var(--ink); filter:none}
.hdr--over .nav a.nav__mark::after{background:var(--ink)}
/* the dropdown is its own white panel — it never inherits the over-hero colours */
.hdr--over .more__menu a{color:var(--ink)}
.hdr--over .nav .soon{color:rgba(255,255,255,.6)}
.hdr--over .burger{border-color:var(--ink); color:var(--ink)}
.hdr--over .burger:hover{background:var(--ink); color:var(--on-accent)}
/* everything below the header needs the height back, except full-bleed heroes
   which deliberately run underneath it */
body{padding-top:var(--hdr-h)}
body.has-hero{padding-top:0}
.hdr__in{
  max-width:var(--shell); margin:0 auto; padding:13px 28px;
  display:flex; align-items:center; gap:26px;
}
@media (max-width:640px){ .hdr__in{padding:11px 18px; gap:14px} }

/* ── THE LOGO ────────────────────────────────────────────────────────
   images/bss-logo.svg is a single-colour "BAR SCENE / SOCIALS" wordmark.
   It is painted as a MASK with the colour supplied by the element, not
   as an <img>, for one reason: the same artwork has to be white in the
   header, white floating over a hero photo, white in the footer, and
   currentColor inside the loader. A mask gives all four from one file
   with no recoloured copies to keep in step, and `color` is inherited,
   so any container can retint it without touching this rule.

   Sized by width alone — aspect-ratio holds the height, so no placement
   can squash the mark by getting one of the two numbers wrong. The
   mask is centred and `contain`, which means an element wider or taller
   than 592:197 letterboxes the artwork rather than distorting it. */
.logo{
  display:block; width:100%; aspect-ratio:592 / 197;
  background-color:currentColor;
  -webkit-mask:url("../images/bss-logo.svg") center / contain no-repeat;
          mask:url("../images/bss-logo.svg") center / contain no-repeat;
}

/* The header mark. 126px wide reads as ~42px tall, which fits inside the
   80px bar with its 13px padding and still sits taller than the 33px
   wordmark it replaces. It steps down with --hdr-h rather than scaling
   with the viewport: chrome holds its size while content scales. */
.lockup{display:flex; align-items:center; text-decoration:none; flex:none; color:var(--ink)}
.lockup__logo{width:126px}
.lockup:hover{color:var(--orange)}
@media (max-width:1100px){ .lockup__logo{width:116px} }
@media (max-width:640px){  .lockup__logo{width:104px} }
@media (max-width:380px){  .lockup__logo{width:92px} }

.nav{display:flex; align-items:center; gap:2px; margin-left:auto; flex-wrap:wrap}
.nav a,.nav button{
  font:500 13.5px/1 var(--body); letter-spacing:.005em;
  color:var(--text-2); text-decoration:none; background:none; border:0;
  padding:11px 12px; cursor:pointer; position:relative; border-radius:999px;
  transition:color .25s var(--quart);
}
.nav a:hover,.nav button:hover{color:var(--ink)}
.nav a::after{
  content:""; position:absolute; left:11px; right:11px; bottom:6px; height:2px;
  background:var(--orange); transform:scaleX(0); transform-origin:left;
  transition:transform .3s var(--ease);
}
.nav a:hover::after{transform:scaleX(1)}
.nav a.is-active{color:var(--ink); font-weight:700}
.nav a.is-active::after{transform:scaleX(1)}
.nav .soon{font-size:9.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--text-3); margin-left:6px; vertical-align:1px}

/* ── THE THREE EVENT LINKS — a wordmark, not a menu item ─────────────
   The brand notes rule out a literal per-event icon (no pumpkin, no
   tree, no shamrock), so these three read as marks by TYPE: the display
   face, uppercase, at wordmark size next to 13.5px nav items.

   All three are flat white. They previously carried each event's
   identity colour, which put three more hues in a bar that now also
   holds the real logo — the header was doing the colour work the pages
   are supposed to do. White also settles the contrast question for
   good: --red measured 4.20:1 on this ground, clearing the 3:1
   large-text bar but nothing more, whereas white is 17.9:1 and stays
   legal at any size the header is ever set at.

   Sized in EM terms 28px looks modest, but Jomhuria's cap is only
   ~.39em — it reads as roughly a 15-16px cap height, about 1.3x the
   plain nav item's own cap, which is the honest amount of "larger"
   available before the header outgrows itself.

   Fixed, not vw-clamped: nothing else in this header scales with
   viewport width, so a clamp here would make --hdr-h correct at some
   widths and short at others, instead of the one constant every page
   relies on. */
.nav a.nav__mark{
  font-family:var(--display); font-weight:400; text-transform:uppercase;
  font-size:28px; line-height:1; letter-spacing:var(--hl-track);
  color:var(--ink); padding:11px 14px;
}
/* No colour change on hover — discolouring is the opposite of a logo
   staying recognisable. The lift is a soft white glow instead, cheap
   and layout-safe since drop-shadow never touches box size. Higher
   specificity than the plain-link rules above (three classes vs two)
   so it wins without `!important`. */
.nav a.nav__mark:hover,
.nav a.nav__mark.is-active{
  color:var(--ink);
  filter:drop-shadow(0 0 7px rgba(248,248,246,.55));
}
.nav a.nav__mark::after{background:var(--ink)}
/* The "Soon" tag sat on the text baseline, which worked next to 13.5px
   body type and reads as a stray fragment next to a 28px cap. Pinned to
   the corner instead, like a sticker on a printed mark. */
.nav a.nav__mark{padding-top:15px}
.nav a.nav__mark .soon{
  position:absolute; top:2px; right:8px;
  font-size:8.5px; letter-spacing:.09em; margin-left:0; vertical-align:0;
}
@media (max-width:1100px){ .nav{display:none} .hdr__cta{margin-left:auto} }

.more{position:relative}
.more__menu{
  /* above .tear--hdr, which hangs off the header at z-index 1 and was
     painting across the top of this panel */
  z-index:4;
  position:absolute; right:0; top:calc(100% + 9px); min-width:216px;
  background:var(--paper-2); border:1px solid var(--rule); border-radius:14px; padding:8px;
  opacity:0; visibility:hidden; transform:translateY(-5px);
  transition:opacity .22s var(--quart), transform .22s var(--quart), visibility .22s;
  box-shadow:0 14px 34px rgba(0,0,0,.6), 0 2px 6px rgba(0,0,0,.4);
}
.more.is-open .more__menu{opacity:1; visibility:visible; transform:translateY(0)}
.more__menu a{display:block; padding:10px 13px; letter-spacing:.02em; border-radius:8px; font-weight:500; text-transform:none; font-size:13.5px}
.more__menu a:hover{background:var(--paper-3); color:var(--ink)}
.more__menu a::after{display:none}

/* phone bar carries three things: mark, ticket, Menu. 375px minus 36px of
   padding leaves 339 — the ticket is sized to what is actually left over. */
@media (max-width:760px){
  .hdr__in{gap:10px; padding-left:14px; padding-right:14px}
  .lockup svg{width:32px; height:32px}
}
@media (max-width:400px){
}
.burger{display:none}
@media (max-width:1100px){
  /* same metrics as .btn, so Menu and the drawer's Close are one button
     in two states rather than two sizes */
  .burger{
    display:block; background:none; border:1.5px solid var(--ink); border-radius:999px;
    padding:15px 26px; cursor:pointer;
    font:700 13px/1 var(--body); letter-spacing:.01em;
  }
  .burger:hover{background:var(--ink); color:var(--paper)}
}
.drawer{
  position:fixed; inset:0; z-index:90; background:var(--paper);
  padding:22px 22px 40px; overflow:auto;
  opacity:0; visibility:hidden; transition:opacity .3s var(--quart), visibility .3s;
}
.drawer.is-open{opacity:1; visibility:visible}
.drawer a{
  display:block; text-decoration:none; font-family:var(--display);
  text-transform:uppercase; font-size:clamp(62px,15.4vw,101px); line-height:.477;
  letter-spacing:.03em;
  padding:9px 0; border-bottom:1px solid var(--rule);
}
.drawer a .soon{font:500 11px/1 var(--body); letter-spacing:.16em; color:var(--text-3); vertical-align:8px; margin-left:8px}

/* ============================================================
   THE TICKET — Figma component 2081-85
   2:1 stock, so it reads as an object rather than a bar. The soft
   shadow and the tilt-on-hover come from the design's second variant.
   ============================================================ */
/* No drop shadow. The ticket is printed stock lying on the page, not a
   card floating above it — the shadow was the one glassy thing left on a
   site whose whole register is flat print. The tilt still carries the
   hover, so the affordance survives. */
.ticket{
  display:inline-block; width:100%; max-width:clamp(170px,19vw,220px); border:0; padding:0;
  background:none; cursor:pointer; text-decoration:none; text-align:center;
  /* Same three interaction variables the reveal composes, so a ticket
     tilts identically whether or not it sits inside a [data-reveal]. */
  /* Same interaction variables the reveal composes, so a ticket tilts
     identically whether or not it sits inside a [data-reveal].
     --slant is a per-ticket resting angle assigned once in JS; --sway is
     the continuous drift, registered with @property below so it can
     actually animate. Adding them inside one rotate() keeps hover, reveal
     and the idle motion on the same transform instead of three rules
     fighting over it. */
  transform:translate3d(var(--tx,0px),var(--ty,0px),0)
            rotate(calc(var(--rot,0deg) + var(--slant,0deg) + var(--sway,0deg)))
            scale(var(--sc,1));
  transition:transform .34s var(--ease);
}
/* @property is what makes --sway animatable at all: a plain custom
   property jumps between keyframes instead of interpolating. Unsupported
   browsers simply never move the ticket, which is the correct fallback for
   an idle flourish. */
@property --sway{ syntax:'<angle>'; inherits:false; initial-value:0deg }
@keyframes bss-sway{
  0%,100% { --sway:-0.9deg }
  50%     { --sway: 0.9deg }
}
.ticket{ animation:bss-sway var(--sway-dur,7s) var(--quart) infinite; }
/* the tucked stub already has a strong resting angle and a job to do on
   reveal — leave it still rather than stacking a second motion on it */
.pitch .pitch__ticket{animation:none}
@media (prefers-reduced-motion: reduce){ .ticket{animation:none} }

.ticket:hover{--rot:-3.2deg; --ty:-3px}
.ticket:active{--rot:-1.4deg; --ty:0px}
.ticket svg{display:block; width:100%; height:auto; overflow:visible}
.ticket__note{
  display:block; margin-top:11px; font:500 11px/1.4 var(--body);
  text-transform:uppercase; letter-spacing:.16em; color:var(--text-3);
}
.band--ink .ticket__note,.ftr .ticket__note,.hero .ticket__note,
.drawer .ticket__note{color:rgba(255,255,255,.82)}
.drawer .ticket__note{color:var(--text-3)}
.ticket--sm{max-width:clamp(150px,16vw,184px)}
.ticket[data-state="sold"]{cursor:not-allowed; filter:none}
.ticket[data-state="sold"]:hover{--rot:0deg; --ty:0px; filter:none}
.ticket-row{display:flex; gap:26px; align-items:center; flex-wrap:wrap}

/* ============================================================
   BUTTONS, CHIPS, LABELS
   Pills and solid fills — modern controls under the printed
   artwork. No rotation and no fake paper here: these are the bits
   people click, and they should feel crisp.
   ============================================================ */
.btn{
  display:inline-flex; align-items:center; gap:9px;
  font:700 13px/1 var(--body); letter-spacing:.01em;
  padding:15px 26px; border-radius:999px; text-decoration:none;
  border:1.5px solid var(--ink); background:transparent; color:var(--ink);
  cursor:pointer; white-space:nowrap;
  transform:translate3d(var(--tx,0px),var(--ty,0px),0) rotate(var(--rot,0deg)) scale(var(--sc,1));
  transition:background .22s var(--quart), color .22s var(--quart),
             border-color .22s var(--quart), transform .22s var(--ease);
}
.btn:hover{background:var(--ink); color:var(--on-accent); --ty:-2px}
.btn:active{--ty:0px}
.btn--orange{background:var(--orange); border-color:var(--orange); color:var(--on-accent)}
.btn--orange:hover{background:var(--ink); border-color:var(--ink); color:var(--orange-d)}
/* The header CTA. Solid white with near-black type, and NO size modifier:
   .burger (the Menu button) is deliberately built on .btn's own metrics —
   15px/26px padding, 13px/1 — so the two read as one button in two states.
   A .btn--sm here made the pair mismatch. Hover drains the fill out to the
   outlined state Menu already sits in. */
.btn--white{background:var(--ink); border-color:var(--ink); color:#141412}
.btn--white:hover{background:transparent; border-color:var(--ink); color:var(--ink)}
.btn--red{background:var(--red-btn); border-color:var(--red-btn); color:#FFFFFF}
.btn--red:hover{background:var(--ink); border-color:var(--ink); color:var(--red-on)}
.btn--paper{border-color:rgba(255,255,255,.55); color:var(--ink)}
.btn--paper:hover{background:var(--ink); border-color:var(--ink); color:var(--on-accent)}

/* chips / labels — pill, solid, level. The old rotated die-cut sticker read
   as sloppy at 10px; the printed character lives in the artwork instead. */
/* A quiet chip, not an orange blob: the fill is a low-opacity white so it
   reads as a frosted tag on whatever it sits on, and the accent moves to
   the type. --orange-on rather than --orange because this is 11px text —
   #FF4D14 measures 4.1:1 on the composited fill and #FF8A4D gives 5.9:1. */
.sticker{
  display:inline-flex; align-items:center; gap:7px;
  font:700 11px/1 var(--body); letter-spacing:.05em;
  padding:8px 14px; border-radius:999px;
  background:rgba(248,248,246,.12); color:var(--orange-on); border:0;
}
/* live state gets a pulse dot — the one place a tiny bit of motion earns itself */
.sticker--live::before{
  content:""; width:7px; height:7px; border-radius:999px;
  background:var(--orange); flex:none;
  animation:bss-pulse 2.4s var(--quart) infinite;
}
@keyframes bss-pulse{
  0%,100%{ box-shadow:0 0 0 0 rgba(255,77,20,.55) }
  55%    { box-shadow:0 0 0 5px rgba(255,77,20,0) }
}
@media (prefers-reduced-motion:reduce){
  .sticker--live::before{animation:none}
}
/* A chip standing ON a photograph gets a SOLID fill. The default is
   rgba(248,248,246,.12) — a translucent white, which is fine over a known
   page ground and a liability over an unknown picture: it lifts whatever
   is behind it 12% toward white, so a bright frame pushes the chip up
   into --orange-on's own luminance instead of away from it. Measured on
   the Haunted hero, "Going fast — 640 left" read 3.08:1 against a 4.5
   floor for 11px type, and the chip's own fill was what put it there.
   #2A2A28 is exactly what the translucent version composites to over
   --paper, so nothing changes anywhere it already looked right; it just
   stops depending on the picture. */
.hero__inner .sticker,
.band--shot .shell .sticker{background:#2A2A28}
/* The ghost variants keep their keyline but NOT their transparency here.
   "Not on sale yet" is a hollow chip with a white hairline, which was
   fine over a scrimmed photo and is not fine over an unscrimmed one:
   measured 1.48:1 on the Shamrock hero and 2.44 on the Christmas one,
   against a 4.5 floor for 11px. The fill is the same #2A2A28; the
   keyline is what still says "ghost". */
.band--shot .shell .sticker--ghost{background:transparent}
.sticker--ghost{background:transparent; color:var(--ink); box-shadow:inset 0 0 0 1.5px var(--rule-hard)}
.sticker--ghost-paper{background:transparent; color:var(--ink); box-shadow:inset 0 0 0 1.5px rgba(255,255,255,.5)}

/* ============================================================
   PHOTO — no real imagery yet, so: riso colour fields
   Honest placeholders that still carry the print texture.
   ============================================================ */
.photo{
  position:relative; overflow:hidden; background:var(--stone);
  display:grid; place-items:center; isolation:isolate; min-height:130px;
}
.photo::before{
  content:""; position:absolute; inset:0;
  background-image:radial-gradient(rgba(17,17,16,.34) 1.4px, transparent 1.5px);
  background-size:7px 7px; mix-blend-mode:multiply; opacity:.9;
}
.photo::after{
  content:""; position:absolute; inset:0; z-index:2; pointer-events:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='.55'/%3E%3C/svg%3E");
  opacity:.24; mix-blend-mode:multiply;
}
.photo__cap{
  position:relative; z-index:3; font:500 10px/1.4 var(--body);
  text-transform:uppercase; letter-spacing:.19em; color:#FFFFFF; text-shadow:0 1px 3px rgba(17,17,16,.55);
  text-align:center; padding:14px; max-width:24ch;
}
.photo--orange{background:var(--orange)}
.photo--orange .photo__cap{color:var(--on-accent); text-shadow:none}
.photo--green{background:var(--green-d)}
.photo--ink{background:var(--ink-2)}
.photo--grey{background:var(--paper-3)}
.photo--grey .photo__cap{color:rgba(248,248,246,.82)}
.photo--red{background:var(--red-d)}

/* Pinned, not tokenised — same reasoning as .taped. This is a stand-in for
   a physical printed thing, so it keeps its own colours whichever way the
   page is themed. It USED to end on var(--ink-deep), which inverts to
   #FFFFFF inside .on-white: dropped into a white band the gradient ran
   orange → red → white and the white .photo__cap on top measured 1.06:1. */
.photo--night{background:linear-gradient(152deg,#FF4D14 0%,#E8202A 46%,#080807 100%)}

/* A real photograph inside a .photo frame. Same monochrome grade the
   event cards and the section backgrounds already use, so a printed
   snapshot pinned to the wall belongs to the same set as everything
   else — the colour budget on this site is spent on ink, never on
   photography. Kept a touch brighter than the card grade (.82 vs .66)
   because nothing is printed on top of a polaroid; the picture is the
   whole content, and there is no headline that needs a floor.
   Sits under .photo::before (the halftone) and ::after (the print
   noise), which is what keeps it in the same material as its
   colour-field siblings rather than reading as a pasted-in JPEG. */
.photo > img{
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  z-index:0; display:block; color:transparent; font-size:0;
  filter:saturate(1.04) contrast(1.02);
}
/* A photo frame carrying a real picture must not also paint a colour
   field behind it — a slow or failed image would flash the wrong hue,
   and the halftone reads differently over grey than over the picture.
   Two selectors, not one: .taped .photo further down sets its own grey
   at the same (0,2,0) specificity and is declared later, so the plain
   rule alone loses inside a polaroid. Same value both times. */
.photo:has(> img),
.taped .photo:has(> img){background:#141412}
.ar-1,.ar-43,.ar-34,.ar-169{min-height:0}
.ar-1{aspect-ratio:1/1}
.ar-43{aspect-ratio:4/3}
.ar-34{aspect-ratio:3/4}
.ar-169{aspect-ratio:16/9}

/* taped-down polaroid */
/* the polaroid is a physical white object taped to a black wall — it is
   one of the few surfaces that does NOT invert, so its frame and its
   handwriting are pinned rather than tokenised */
.taped{
  position:relative; background:#F8F8F6; padding:11px 11px 40px;
  box-shadow:4px 6px 0 rgba(0,0,0,.55);
  /* The rotation is a variable, not a literal, so the reveal above can
     compose it into its own transform instead of overwriting it. */
  --tilt:0deg;                        /* resting angle */
  --rot:var(--tilt);                  /* what actually gets drawn */
  /* Centre pivot is the safe default — see the media query below. */
  transform-origin:50% 50%;
  transform:translate3d(0,var(--ty,0px),0) rotate(var(--rot)) scale(var(--sc,1));
}
.taped::before{
  content:""; position:absolute; top:-11px; left:50%; width:78px; height:23px;
  transform:translateX(-50%) rotate(-3.5deg);
  background:rgba(207,227,238,.72); border-left:1px dashed rgba(17,17,16,.16); border-right:1px dashed rgba(17,17,16,.16);
}
/* A polaroid can be a link — the Featured Bars strip on the homepage is
   four of them. Nothing about the object changes; it just stops being
   inert. */
a.taped{display:block; text-decoration:none; cursor:pointer}
a.taped:focus-visible{outline:3px solid var(--orange); outline-offset:4px}

/* The caption is the bar's name now, not a two-word neighbourhood, and
   "Water Street Brewery" is three times the length the 18px flat size
   was set for. Sized against the polaroid instead of fixed: 8.2cqw on a
   ~290px card is ~24px, and it steps down as the strip narrows rather
   than wrapping into the frame. The bottom padding grows to match, and
   the whole run is capped at two lines. */
.taped{container-type:inline-size}
.taped__cap{
  position:absolute; left:7px; right:7px; bottom:9px; text-align:center;
  font-family:var(--script); font-weight:400; color:rgba(17,17,16,.78);
  font-size:clamp(13px, 6.4cqw, 22px); line-height:1.05;
}
/* Every polaroid prints on one mid-grey field. They were four different
   accent colours, which sat four more hues next to the event photography
   and left the wall reading as a swatch chart. Grey lets the taped frame,
   the tilt and the handwriting do the work. --stone is the palette's
   existing placeholder grey, so this is the token, not a new colour.
   The caption goes to ink: white on --stone measures 2.6:1. */
/* Pinned rather than tokenised, same as .taped's own white frame and
   .photo--night: a polaroid is a physical object on the wall and keeps its
   colour whichever way the page is themed.

   The exact value is set by the caption, not by taste. --stone (#7A7A74)
   was the obvious token to reuse, but at 10px uppercase the label needs
   4.5:1 and --stone tops out at 4.38:1 with PURE BLACK on it — there is no
   ink dark enough for it. #8A8A83 is the same mid-grey a step lighter and
   measures 5.31:1. The caption also loses its drop shadow, which existed
   to lift white type off a colour field it no longer sits on. */
.taped .photo{background:#8A8A83}
.taped .photo__cap{color:#141412; text-shadow:none}

.tilt-1{--tilt:-2deg} .tilt-2{--tilt:1.6deg}
.tilt-3{--tilt:-1.1deg} .tilt-4{--tilt:2.4deg}

/* HOVER — the photo is straightened and picked up, not swung.
   The first attempt rotated each polaroid THROUGH upright to a mirrored
   angle. Read badly in a row of four: sweeping ~3° about the centre is a
   lot of travel, and crossing the axis meant a card you were leaving and a
   card you were arriving at were moving in opposite directions at once,
   which is what made it feel clunky. Straightening to 0 halves the travel,
   always moves the same way, and matches what a hand does to a crooked
   photo. The lift and the 1.5% scale carry the rest, so the near-flat
   .tilt-3 still has a visible response.

   The hard offset shadow deliberately does NOT move. Leaving it planted
   while the card rises is the depth cue — the gap between photo and
   shadow opens. Animating the offset made it look like the card was
   sliding across the wall instead. */
.taped:hover{
  --rot:0deg;
  --ty:-6px;
  --sc:1.015;
  z-index:2;                          /* over its neighbours mid-swing */
}
/* Quick in, softer out, so sweeping across a row does not leave four cards
   slowly unwinding behind the cursor. --taped-dur is read by whichever
   transition rule wins, so this works inside a reveal group and outside. */
.taped{--taped-dur:.44s}
.taped:hover{--taped-dur:.26s}
.taped,
.has-js [data-reveal] > .taped,
.has-js [data-reveal] > .taped.is-in{
  transition:opacity .62s var(--ease),
             transform var(--taped-dur) cubic-bezier(.22,.68,.24,1);
}
/* Pivot at the tape rather than the centre: a photo stuck to a wall swings
   from its tape, and rotating about the middle is what made the hover read
   as the card twisting in place.

   Only above the grid collapse, though. Pivoting near the top puts almost
   the whole height below the pivot, which roughly DOUBLES how far the
   bottom corners swing sideways — and once .c-wide-right collapses, the
   polaroid on tickets.html becomes 745px wide, where 2deg of resting tilt
   threw its corner 4px past the viewport and gave the page a horizontal
   scrollbar. Below 901px the photos are wide enough that the arc reads as
   a skew anyway, and nothing there has a pointer to hover with. */
@media (min-width:901px){
  .taped{transform-origin:50% 4%}
}
@media (prefers-reduced-motion: reduce){
  .taped:hover{--rot:var(--tilt); --ty:0px; --sc:1}
}

/* ============================================================
   COLLAGE GRID — thin ink lines, unequal cells
   ============================================================ */

/* ============================================================
   CARDS
   ============================================================ */
.card{background:var(--paper-2); border:1px solid var(--rule-hard); display:block; text-decoration:none; position:relative}
.card--pad{padding:clamp(20px,2.2vw,30px)}
a.card{
  /* Composed from the shared interaction variables, so the lift survives
     when the card is a direct child of a [data-reveal] group — which it is
     on featured-bars.html. */
  transform:translate3d(var(--tx,0px),var(--ty,0px),0) rotate(var(--rot,0deg)) scale(var(--sc,1));
  transition:transform .3s var(--ease), box-shadow .3s var(--ease);
}
a.card:hover{--tx:-3px; --ty:-3px; box-shadow:6px 7px 0 var(--ink)}
.card__body{padding:clamp(18px,2vw,26px)}

/* event card — the three-up */
.event{display:grid; grid-template-rows:auto 1fr; text-decoration:none; position:relative}
.event__meta{display:flex; justify-content:space-between; gap:12px; align-items:baseline; padding-top:14px}
/* The date under a card is the one hard fact on it and was set at caption
   size (11px). Tracking eases as it grows — .18em is drawn for 11px and
   reads as a gap at 14. */
.event__meta .tiny{font-size:14px; letter-spacing:.13em}
.event__meta .sticker{font-size:12.5px; padding:9px 15px}
.event .small{font-size:16px}
/* --- the name sits IN the thumbnail, in the event's own colour --------
   The card used to repeat the event name below the picture; it now reads
   once, centred on the shot. The thumbnail is sized as the query container
   so the title scales to the CARD, not the viewport — these cards appear
   at ~780px and ~476px wide in the same row on the homepage, so a vw-based
   clamp would size one of them wrong. */
.event__shot{container-type:inline-size}
.event__shot > img{
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  z-index:0; display:block;
  /* IN COLOUR, like every other picture on the site now. The cards used
     to be the strictest case: fully desaturated and down to brightness
     .66, set by measurement because the title sits directly on the
     picture with nowhere else to go.

     A card cannot use the hero's trick of plating only the type — the
     title is centred on a 16:9 thumbnail and its box IS most of the
     card. So the card keeps a real scrim (::before, below) and the
     picture keeps its colour: the same total darkness, arriving as one
     neutral wash instead of as a desaturation plus two stops. */
  filter:saturate(1.34) contrast(1.05);
}
/* .photo::before normally carries the halftone; on a shot thumbnail it is
   the scrim instead, because a coloured name over a photograph needs a
   darker floor than the halftone alone provides. ::after (noise) is
   untouched, so the print texture survives. */
.event__shot::before{
  z-index:2; mix-blend-mode:normal; opacity:1;
  /* background-size MUST be reset. .photo::before is the 7px halftone dot
     screen, so it ships `background-size:7px 7px` — inherit that here and
     the scrim gradient tiles into a 7px grid instead of covering, which
     reads as a checkered pattern over the photo. */
  background-size:100% 100%;
  /* Set by measurement, and re-set when the pictures went back to colour.
     It used to lean on the image grade — brightness(.66) did roughly half
     the work and this scrim the rest. With the grade gone the scrim has
     to carry all of it, so the alphas rose by about what the grade gave
     up: the composited card now sits at mean luminance .025, a hair off
     the .028 it measured while it was greyscale. The same darkness,
     arriving as a neutral wash over a colour photograph instead of as a
     desaturation plus two stops.

     THIS IS NOW A 25% WASH, and the darkness has moved onto the type.
     At .86/.74/.60 the scrim was doing its job and hiding the
     photographs while it did it — three real crowd shots reduced to a
     texture behind a word. The note above is still true as far as it
     goes: a card cannot plate only its type with a TIGHTER ELLIPSE,
     because the title's box is most of the card. But it can put the
     dark on the glyphs, which is exactly what .hero--shot already does
     further up this file, and which needs no room at all.

     So: the wash is the lightest thing that still separates a picture
     from the paper it sits on, and legibility comes from the halo on
     .event__title .ink below. It started at 15% and came up to 25% by
     eye — 15% read as almost no wash at all, which left the brightest
     frame in the row (Shamrock, mean luminance .123 against Christmas's
     .064) sitting too close in value to the paper around it. */
  background-image:linear-gradient(rgba(8,8,7,.25), rgba(8,8,7,.25));
}

/* The per-ink deepenings are GONE, and deliberately so.

   They existed because the darkness was the scrim's job: violet and red
   are MID-luminance inks (.19 and .18), squeezed from both ends by a
   blown highlight and a lit mid-tone alike, so each needed the whole
   card taken down another step. With the dark now cast from the glyphs
   themselves, the ink's luminance no longer argues with the picture's —
   the effective background at every stroke edge is the halo, and the
   halo is the same for all four inks. One wash, one halo, four cards. */
.event__title{
  position:relative; z-index:3; text-align:center;
  padding-left:.4em; padding-right:.4em;
  /* sized against the CARD, not the viewport, so the two very different
     card widths in the homepage row set the same optical size. Raised from
     11cqw/60 — the name is the card's headline and was reading as a label
     on it. */
  /* Raised again (13.5cqw -> 15.6cqw) with the rest of the homepage type.
     Still sized in cqw against the CARD, so the wide card and the two
     narrow ones in the same row land on the same optical size. */
  font-size:clamp(30px, 15.6cqw, 92px);
}

/* Locked cards grey out the PICTURE, not the card. The filter used to sit
   on .photo, which also desaturated the coloured title inside it. */
.event__shot::after{display:none}   /* no print-noise field on the thumbnails */

/* Hover: the picture pushes in a hair and the name lifts. Transform only,
   so it stays on the compositor. .photo already clips, so the scale is
   contained.

   The scrim used to lift to opacity .82 on hover as well — "the picture
   opens up". Measured over the glyphs, that took every card title below
   its floor while the pointer was on it: Haunted 3.31 -> 2.36 with 13.7%
   of painted pixels under 3:1, Shamrock's "Shuffle" 3.48 -> 2.84,
   Christmas 2.57 -> 2.00. A hover state is exactly when someone is
   reading the card, so it is the worst moment to thin the plate. The
   scale and the title lift carry the interaction on their own. */
.event__shot > img{transition:transform .55s var(--ease), filter .55s var(--ease)}
.event:hover .event__shot > img,
.event:focus-visible .event__shot > img{transform:scale(1.05)}
.event__title{transition:transform .45s var(--ease)}
.event:hover .event__title,
.event:focus-visible .event__title{transform:translateY(-3px)}
@media (prefers-reduced-motion: reduce){
  .event__shot > img, .event__title, .event__shot::before{transition:none}
  .event:hover .event__shot > img{transform:none}
  .event:hover .event__title{transform:none}
}
/* Locked cards used to be the DESATURATED ones, back when the on-sale card
   was the only one carrying colour. Now every card is monochrome, so the
   difference has to be light, not saturation — a locked picture is simply
   further down. The hatch overlay and the ghost chip carry the rest. */
/* A locked card greys out the PICTURE by a touch of desaturation only —
   enough to read as "not yet", not enough to undo the colour. */
.event--locked .event__shot > img{filter:saturate(1.12) contrast(1.03) brightness(.94)}
.event--locked .event__shot{background:var(--ink-2)}


/* locked overlay hatch */

/* inclusions — four grouped things, riso listing style */
/* The numerals and the rules are gone — with them removed the group reads
   as two headed lists rather than a numbered table, so the heading has to
   carry the hierarchy instead. Spacing does the separating now. */
/* ============================================================
   SECTION PHOTOGRAPHY — a picture behind a whole band
   A photograph as the ground for a section, rather than as a card
   or a hero. Same monochrome-plus-scrim discipline the event cards
   use, because the same problem applies: coloured type over a
   photograph needs a floor it can rely on, whatever the picture is.
   The band's own tone shows through the sides so the section still
   belongs to the page rather than floating on an image.
   ============================================================ */
/* --shot-b is the exposure, exposed as a variable because exposure is a
   property of the PICTURE, not of the component. It now defaults to 1 —
   untouched — and a frame that is genuinely over-bright dials itself
   back inline rather than every band being dimmed to suit it. */
.band--shot{position:relative; isolation:isolate; overflow:hidden; --shot-b:1}
.band--shot > .band__bg{
  position:absolute; inset:0; z-index:0; pointer-events:none;
}
.band--shot > .band__bg img{
  width:100%; height:100%; object-fit:cover; display:block;
  filter:saturate(1.05) contrast(1.03) brightness(var(--shot-b));
  color:transparent; font-size:0;
}
/* THE PLATE, and the one place on the site it is still heavy.

   A band is not a hero. The hero could drop to a 13% wash because its
   type occupies a fixed box in the middle of the frame that a plate can be
   cut to. Here a whole section of 16px body copy sits edge to edge, and
   body copy needs 4.5:1, not 3:1 — there is no sub-region to plate,
   because the type IS the region.

   So the picture keeps its colour (that is the part the grade above gave
   back) but not its brightness. Read it as a dark colour photograph
   behind the copy rather than a photograph you are meant to look at;
   the heroes, cards and polaroids are where the pictures get to speak. */
.band--shot > .band__bg::after{
  content:""; position:absolute; inset:0;
  background:
    linear-gradient(180deg, rgba(17,17,16,.86) 0%, rgba(17,17,16,.76) 42%, rgba(17,17,16,.90) 100%),
    radial-gradient(120% 90% at 50% 45%, rgba(17,17,16,.20) 0%, rgba(17,17,16,.52) 100%);
}
/* --violet is the least forgiving ink over a picture — mid-luminance, so
   both ends of the range close on it. Only bands that actually carry a
   violet word take the deeper plate. */
.band--shot:has(.ink--violet) > .band__bg::after{
  background:
    linear-gradient(180deg, rgba(17,17,16,.90) 0%, rgba(17,17,16,.84) 42%, rgba(17,17,16,.93) 100%),
    radial-gradient(120% 90% at 50% 45%, rgba(17,17,16,.32) 0%, rgba(17,17,16,.60) 100%);
}
/* print texture over the picture, same 7px screen as everywhere else */
.band--shot > .band__bg::before{
  content:""; position:absolute; inset:0; z-index:1;
  background-image:radial-gradient(rgba(17,17,16,.34) 1.4px, transparent 1.5px);
  background-size:7px 7px; mix-blend-mode:multiply; opacity:.5;
}
.band--shot > .shell{position:relative; z-index:1}

/* A 2x2 quadrant, not a column. Four headed groups of three stacked
   vertically ran nearly 1,900px on desktop — the reader scrolled past four
   near-identical blocks and the fact that they are FOUR PARTS OF ONE THING
   never landed. Side by side, the whole offer is one glance, and the
   headings stop competing with each other for the same vertical axis.

   The heading also comes down hard. At 92px it was the same size as the
   page's own section headline, so "THE KIT" read as a new chapter rather
   than a column label; the list under it is the content. A hairline over
   each cell gives the quadrant its structure without drawing a box. */
.incl{
  display:grid;
  grid-template-columns:repeat(2, minmax(0,1fr));
  gap:clamp(30px,4vw,58px) clamp(28px,4vw,64px);
}
.incl__row{
  display:block;
  /* The orange rule above each group and the orange dash before each item
     are both gone. Between them they put six orange marks into a block
     whose only real content is four headings and twelve lines — the
     colour was doing structural work the headings already do, and on a
     photographic band it read as scaffolding rather than design.
     Spacing separates the groups now, which is what the rest of the site
     does. */
  padding-top:clamp(10px,1.2vw,16px);
}
.incl__row > div > .h4{
  font-size:clamp(34px,3.8vw,54px);
  margin-bottom:clamp(14px,1.5vw,20px);
}
.incl ul{margin:0; padding:0; list-style:none; display:grid; gap:clamp(11px,1.2vw,15px)}
/* The items are the thing people actually scan, so they are set at reading
   size rather than caption size — 15.5px was a footnote under a 44px
   heading. With the dash gone they sit flush; the indent went with it. */
/* Size inherits the default; the COLOUR is restated. .on-white sets its
   own `color`, so anything relying on inheritance picks up that solid
   near-black instead of the prose tone — --text-2 is the same token in
   both grounds and inverts with them. */
.incl li{color:var(--text-2); max-width:46ch}
@media (max-width:720px){
  .incl{grid-template-columns:minmax(0,1fr); gap:clamp(24px,5vw,34px)}
}

/* numbered fact list */
/* A plain bulleted list. It used to be a two-column grid — a 52px column
   of display-face numerals against a hairline-separated row per item —
   which is a table pretending to be a list: the numbers carried no
   meaning (nothing refers to "rule 03") and the rules made five short
   sentences look like a specification. Real <li> markers now, so the
   list is a list to a screen reader as well as to the eye. */
/* A plain bulleted list. It used to be a two-column grid — a 52px column
   of display-face numerals against a hairline-separated row per item —
   which is a table pretending to be a list: the numbers carried no
   meaning (nothing refers to "rule 03") and the rules made five short
   sentences look like a specification. Real <li> markers now, so it is a
   list to a screen reader as well as to the eye.

   NOT display:grid on the <ul>, however tempting for the gap. A grid
   container blockifies its children, which drops display:list-item and
   takes the markers with it. Margins do the spacing instead. */
.facts{
  list-style:disc; margin:0; padding:0 0 0 1.15em;
}
.facts li{
  color:var(--text-2); max-width:64ch;
  margin:0 0 clamp(10px,1.1vw,14px);
}
.facts li:last-child{margin-bottom:0}
.facts li::marker{color:var(--orange-on)}
/* on the deep band the copy lifts a step, same as every other run there */
.band--ink .facts li{color:rgba(255,255,255,.68)}
.band--ink .facts li::marker{color:var(--orange-lt)}

/* big stat — .stat and .stat__l went with the old metrics strip; only
   .stat__n survives, on the two charity blocks that still set a figure in
   display type. */
/* a straight x2.2 gave 211px, which put the widest stat ("24k", 1.05em of
   advance) 37px into the neighbouring column of the 4-up grid at every
   viewport over 1000px. 13vw/172px is the largest that fits the column. */
.stat__n{font-family:var(--display); font-size:clamp(101px,13vw,172px); line-height:.373; letter-spacing:.02em}

/* ============================================================
   THE DROP — first-load curtain
   A handful of tickets fall past the screen and the site is there.
   Three rules keep it from being the thing everyone hates about
   intros:
     · it never blocks. The page is fully built and readable behind
       it; the curtain is pointer-events:none the moment it starts
       fading, and it removes itself on a timer that cannot hang.
     · it runs ONCE per session (sessionStorage), so navigating the
       site is not a series of curtains.
     · prefers-reduced-motion and no-JS never see it at all.
   ============================================================ */
.drop{
  position:fixed; inset:0; z-index:200;
  background:var(--ink-deep);
  overflow:hidden;
  transition:opacity .5s var(--quart);
}
.drop[data-done]{opacity:0; pointer-events:none}
.drop__t{
  position:absolute; top:0; left:0;
  width:var(--w); will-change:transform;
  animation:bss-drop var(--dur) cubic-bezier(.42,0,.72,1) var(--delay) forwards;
  transform:translate3d(var(--x),-140%,0) rotate(var(--r0));
}
.drop__t svg{display:block; width:100%; height:auto}
@keyframes bss-drop{
  from{ transform:translate3d(var(--x),-140%,0) rotate(var(--r0)) }
  to  { transform:translate3d(calc(var(--x) + var(--drift)),108vh,0) rotate(var(--r1)) }
}
/* the mark sits over the fall so the load has a subject, not just debris */
.drop__mark{
  position:absolute; inset:0; display:grid; place-items:center;
  color:var(--ink); pointer-events:none;
}
.drop__logo{width:clamp(148px,20vw,248px); opacity:.92}

/* ============================================================
   THE WASH
   A colour that arrives at one edge and is gone by the middle,
   between two hairlines of the same ink. It started as a one-off on
   the "ours" row of the listing and turned out to be the most useful
   thing on the page: it marks a block as belonging to an event
   without boxing it, without a fill that fights the photography, and
   without another border.

   One component, three inks. --wash sets the channel triplet so every
   stop can carry its own alpha; --wash-from and --wash-to move the
   fade so a wash can enter from the left, the right, or the top.
   ============================================================ */
.wash{
  position:relative;
  --wash:255,77,20;
  background-image:linear-gradient(var(--wash-angle,90deg),
    rgba(var(--wash),.13) 0%, rgba(var(--wash),.05) 42%, rgba(var(--wash),0) 78%);
}
/* Hairlines top and bottom are not optional — they are what stops the fade
   ending in a soft edge and reading as a smudge. Part of the component. */
.wash{
  border-top:1px solid rgba(var(--wash),.55);
  border-bottom:1px solid rgba(var(--wash),.55);
}
.wash--orange{--wash:255,77,20}
.wash--green {--wash:0,194,78}
.wash--red   {--wash:232,32,42}
/* enters from the right instead — for blocks whose content sits left */
.wash--rtl{--wash-angle:270deg}
/* The vertical variant is gone. It was used twice and both times it read
   as a big tinted rectangle with a hard edge across the page rather than
   as a wash — a pale mint block behind the charity figure, and a muddy
   brown one behind the price stubs. The horizontal fade between two
   hairlines is the treatment that works, so it is the only one. */
/* Bleeds past the column edge and pays the padding back, so the colour
   runs to where the eye reads the margin rather than stopping on the text
   block. Lifted off .listing__row--ours, which is where this started. */
.wash--bleed{
  margin-inline:calc(-1 * clamp(14px,2vw,32px));
  padding-inline:clamp(14px,2vw,32px);
}
/* vertical room, for washes on a content block rather than a table row */
.wash--pad{padding-block:clamp(20px,2.6vw,38px)}

/* ============================================================
   THE LISTING — what else is on this month
   Was four identical bordered rectangles with identical internal
   layouts and equal spacing: the SaaS card grid, and the one thing
   the art direction is explicitly not. A gig guide torn out of a
   zine has no boxes in it — it has a hairline between entries, a
   big date in the margin, and one entry circled.

   So: rules, not borders. The date becomes display type in the left
   margin at ~7x the meta line, which is the hierarchy the boxes were
   faking with a border. And the one that is OURS breaks the grid
   instead of merely changing border colour — it bleeds past the
   column edge and carries a wash that fades out across the row.
   ============================================================ */
.listing{
  list-style:none; margin:0; padding:0;
  border-top:1px solid var(--rule);
}
.listing__row{
  position:relative;
  display:grid;
  grid-template-columns:minmax(72px,9%) minmax(0,1fr) auto;
  gap:clamp(16px,3vw,44px);
  align-items:baseline;
  padding:clamp(20px,2.8vw,34px) 0;
  border-bottom:1px solid var(--rule);
  /* the nudge is --tx, not a literal transform: these rows are direct
     children of a [data-reveal] group, whose finished state owns
     `transform` at (0,3,0) and would clobber a plain transform outright.
     Same reason .taped and .ticket compose theirs from variables. */
  transform:translate3d(var(--tx,0px),var(--ty,0px),0);
}
/* and the timing has to be reclaimed at matching specificity, or the
   hover inherits the reveal's slow .62s entrance curve */
.listing__row,
.has-js [data-reveal] > .listing__row{
  transition:opacity .62s var(--ease), transform .42s var(--quart);
}
/* the accent rule that wipes in under a hovered row. A second line laid
   over the border rather than a colour change on it, so it can animate
   from the left the way a pen would. */
.listing__row::after{
  content:""; position:absolute; left:0; right:0; bottom:-1px; height:1px;
  background:var(--orange); transform:scaleX(0); transform-origin:left center;
  transition:transform .55s var(--quart);
}
@media (hover:hover){
  .listing__row:hover{--tx:8px}
  .listing__row:hover::after{transform:scaleX(1)}
}
.listing__day{
  font-family:var(--display); font-weight:400;
  font-size:clamp(52px,6.4vw,96px); line-height:.62; letter-spacing:.02em;
  color:var(--text-3);
  font-variant-numeric:tabular-nums;
}
.listing__body{display:grid; gap:6px; align-content:start}
.listing__when{font:500 11px/1.4 var(--body); letter-spacing:.18em;
  text-transform:uppercase; color:var(--text-3); margin:0}
.listing__name{
  font-family:var(--display); font-weight:400; text-transform:uppercase;
  font-size:clamp(26px,2.9vw,40px); line-height:.62; letter-spacing:.03em;
  margin:0; color:var(--text);
}
.listing__where{font:400 15px/1.5 var(--body); color:var(--text-2); margin:0}
.listing__act{justify-self:end; align-self:center}

/* ours: bleeds out of the column and takes the accent */

/* --orange-on, not --orange. This row now carries an orange wash, which
   tints the white paper under the numeral to #f9e2d9 and drops --orange
   from 3.12:1 to 2.67 — under the 3.0 floor for large text. --orange-on
   resolves to --orange-d inside .on-white and measures 4.6. */
.listing__row--ours .listing__day{color:var(--orange-on)}
.listing__row--ours .listing__name{color:var(--text)}
.listing__row--ours::after{transform:scaleX(1); opacity:.55}

@media (max-width:720px){
  /* the action drops under the entry rather than squeezing a third
     column into a phone; the date keeps its margin position */
  .listing__row{grid-template-columns:minmax(56px,auto) minmax(0,1fr); row-gap:16px}
  .listing__act{grid-column:2; justify-self:start}
  .listing__day{font-size:clamp(44px,13vw,64px)}
}

/* ============================================================
   PRICE STUBS
   White paper torn out and laid on the page. They were dark panels
   with a border, which is the card grid the art direction keeps
   rejecting; the site already has a paper register (the polaroid,
   the calendar sheet, the white bands) and this belongs to it.

   .on-white rides along in the markup, so every token inside flips
   and the type, the chip and the rules all take their dark values
   without a single override here. Same swap the calendar uses.
   ============================================================ */
.stubs{
  display:grid; grid-template-columns:1.18fr 1fr;
  gap:clamp(16px,2.4vw,34px); align-items:end;
}
.stub{
  position:relative; display:block; text-decoration:none;
  background:var(--paper); color:var(--text);
  padding:clamp(26px,3vw,38px) clamp(22px,2.6vw,32px) clamp(28px,3.2vw,40px);
  /* Torn on all four edges. A deterministic polygon rather than an image:
     it scales with the card, costs no request, and never tiles visibly.
     Point density is split between the long and short edges so the teeth
     stay about the same size on a card of this proportion. */
  clip-path:polygon(0.00% 1.37%, 3.85% 0.00%, 7.69% 0.18%, 11.54% 0.69%, 15.38% 0.00%, 19.23% 0.08%, 23.08% 0.57%, 26.92% 1.19%, 30.77% 0.00%, 34.62% 1.20%, 38.46% 0.90%, 42.31% 1.31%, 46.15% 1.61%, 50.00% 0.80%, 53.85% 1.45%, 57.69% 1.55%, 61.54% 0.42%, 65.38% 0.46%, 69.23% 0.31%, 73.08% 0.65%, 76.92% 0.81%, 80.77% 0.26%, 84.62% 0.69%, 88.46% 1.35%, 92.31% 1.34%, 96.15% 0.31%, 100.00% 0.00%, 100.00% 4.55%, 99.34% 9.09%, 98.51% 13.64%, 100.00% 18.18%, 99.72% 22.73%, 99.34% 27.27%, 99.97% 31.82%, 98.72% 36.36%, 100.00% 40.91%, 99.48% 45.45%, 99.59% 50.00%, 100.00% 54.55%, 100.00% 59.09%, 100.00% 63.64%, 98.38% 68.18%, 99.46% 72.73%, 99.47% 77.27%, 98.24% 81.82%, 100.00% 86.36%, 100.00% 90.91%, 98.99% 95.45%, 100.00% 98.55%, 96.15% 98.76%, 92.31% 99.08%, 88.46% 98.88%, 84.62% 98.59%, 80.77% 99.34%, 76.92% 100.00%, 73.08% 99.99%, 69.23% 100.00%, 65.38% 99.66%, 61.54% 98.62%, 57.69% 100.00%, 53.85% 99.72%, 50.00% 99.56%, 46.15% 98.60%, 42.31% 99.36%, 38.46% 99.03%, 34.62% 99.92%, 30.77% 98.73%, 26.92% 98.87%, 23.08% 100.00%, 19.23% 98.45%, 15.38% 99.30%, 11.54% 99.22%, 7.69% 99.34%, 3.85% 99.24%, 0.85% 100.00%, 1.56% 95.45%, 0.53% 90.91%, 0.04% 86.36%, 1.46% 81.82%, 0.00% 77.27%, 1.78% 72.73%, 1.28% 68.18%, 0.99% 63.64%, 0.53% 59.09%, 0.00% 54.55%, 0.27% 50.00%, 0.32% 45.45%, 1.53% 40.91%, 1.47% 36.36%, 1.05% 31.82%, 1.36% 27.27%, 0.00% 22.73%, 1.63% 18.18%, 0.78% 13.64%, 1.43% 9.09%, 0.19% 4.55%);
  transform:translate3d(0,var(--ty,0px),0);
  transition:transform .34s var(--ease);
}
.stub__tier{font:600 clamp(15px,1.55vw,21px)/1.15 var(--body); letter-spacing:.13em;
  text-transform:uppercase; color:var(--text); margin:0}
.stub__head{display:flex; justify-content:space-between; align-items:center; gap:12px}
.stub__amt{
  font-family:var(--display); font-weight:400;
  font-size:clamp(54px,7.1vw,96px); line-height:.38; letter-spacing:.02em;
  margin:clamp(10px,1.4vw,16px) 0 0 -.06em;   /* the $ hangs into the padding */
  color:var(--ink);
}
.stub__amt sup{font-size:.36em; vertical-align:.9em; letter-spacing:0; margin-right:.04em}
.stub__ea{font:500 12px/1 var(--body); letter-spacing:.16em;
  text-transform:uppercase; color:var(--text-3); margin-left:.5em; vertical-align:.42em}
.stub__note{font:400 15px/1.5 var(--body); color:var(--text-2); margin:clamp(10px,1.2vw,16px) 0 0}
.stub .ticket{margin-top:clamp(14px,1.8vw,22px); pointer-events:none}

/* the recommendation is larger and sits lower; the accent is a torn strip
   of colour across the head rather than a rule on a box */
.stub--pick .stub__amt{font-size:clamp(60px,7.9vw,108px)}
.stub--pick::before{
  content:""; position:absolute; left:0; right:0; top:0;
  height:clamp(7px,.8vw,10px); background:var(--orange);
}

/* ONE hover for the whole card. The ticket used to be its own link inside
   the card, so the card lifted and then the ticket tilted separately as
   the cursor crossed it — two animations for one target. The ticket is now
   inert markup (pointer-events:none above) and the card drives both. */
@media (hover:hover){
  .stub:hover{--ty:-5px}
  .stub:hover .ticket{--rot:-3.4deg; --ty:-3px}
}
.stub:focus-visible{outline:3px solid var(--orange); outline-offset:4px}

@media (max-width:720px){
  .stubs{grid-template-columns:minmax(0,1fr); align-items:stretch}
}

/* ============================================================
   RISO CALENDAR
   ============================================================ */
/* --- the white sheet ------------------------------------------------
   The calendar prints on white stock, not on the page ground — it is the
   one surface on the site that inverts. Everything inside it therefore has
   to stop inheriting the dark-ground colours, including the .band--ink
   overrides further down this file, which is why .sheet re-states them. */
.sheet{
  background:var(--ink); color:var(--on-accent);
  padding:clamp(16px,2.2vw,30px);
}
.sheet .kicker,.sheet .tiny{color:rgba(17,17,16,.62)}

.cal{
  width:100%; border-collapse:collapse; table-layout:fixed;
  --cal-pad-x:8px; --cal-pad-y:9px;
  /* How far a label is pushed down in a square that has a picture in it.
     Deliberately SHORTER than the picture: the label is meant to come
     down over the bottom of the photograph like a caption stuck on it,
     which is both what a collage does and what stops a square from
     reading as three things stacked with air between them. */
  --pinband:58px;
  /* Space held at the foot of every square for the tape lanes. Sized to
     the tallest case — a two-line gold strip on lane 1 over a thin navy
     one on lane 0 — because a strip is positioned from the bottom and
     ignores padding, so this is the only thing keeping a label off it. */
  --laneband:86px;
  /* pins are authored at a desktop size and scaled from there, so one
     number per breakpoint moves all of them together */
  --pin-scale:1;
}
.cal th{
  font:500 10px/1 var(--body); text-transform:uppercase; letter-spacing:.19em;
  color:var(--text-3); padding:0 0 12px; text-align:left; font-weight:500;
}
/* Taller than it was, and with a floor under it. The month used to hold
   one painted square and a lot of white; it now holds a sticker stacked
   over two strip lanes.

   The bottom padding is the important part. A strip is positioned from
   the PADDING box, so it ignores padding and stays pinned to the bottom
   of the square — which means padding-bottom is free space that the
   sticker above cannot grow into, and the cell grows downward instead of
   the two colliding. Without it, Oct 4 (a sticker AND both lanes) put
   "Landmark CU Live" underneath the Octoberfest tape. The value is the
   two lanes plus a little air: 7 + 26 + 23 = 56, rounded up. */
.cal td{
  border-top:1px solid var(--rule); height:clamp(230px,15.5vw,240px);
  vertical-align:top;
  padding:var(--cal-pad-y) var(--cal-pad-x) var(--laneband);
  position:relative; overflow:visible;
}
/* display:block on purpose. As an inline span the numeral's line box was
   set by the CELL's inherited line-height rather than its own, so it
   occupied 29px in flow while reading as 15px — and every label below it
   inherited that 14px of invisible slack. As a block it is exactly its
   own line-height, which is what --pinband is measured against. */
.cal .dnum{
  display:block; font-family:var(--display); font-size:33px;
  letter-spacing:.03em; color:var(--text-3); line-height:.455;
}
.cal td.has .dnum{color:var(--ink)}
.cal .evt{
  display:block; margin-top:7px; font:500 11px/1.35 var(--body);
  text-transform:uppercase; letter-spacing:.07em; color:var(--ink); text-decoration:none;
}
.cal .evt em{display:block; font-style:normal; color:var(--text-3); letter-spacing:.13em; font-size:9.5px; margin-top:2px}
.cal .evt--ours{color:var(--orange); font-weight:700}
/* the hand-drawn "must go" ring — other people's events only */
.cal .must{position:absolute; left:2%; top:6%; width:96%; height:88%; pointer-events:none; overflow:visible}
.cal .must path{fill:none; stroke:var(--ink); stroke-width:2.4; stroke-linecap:round; opacity:.85}

/* ── THE COLLAGE LAYER ───────────────────────────────────────────────
   Somebody else's event is a sticker, not a table row: a small label
   with a die-cut border, tilted a degree or two and dropped a hair off
   the page, the way one actually sits on a calendar pinned to a wall.

   The tilt is authored per event in bss.js and pinned, for the same
   reason the torn edge is pinned — an angle that re-rolls on every
   navigation reads as a rendering bug rather than a hand.

   transform-origin sits at the left edge, not the centre: these are
   narrow cells, and rotating about the middle walks the right-hand end
   of a sticker into the next square. */
.cal .stick{
  /* Sized to its own words, but capped at roughly its own square. A long
     name STACKS — the same treatment the name on our own date gets —
     rather than running out across the week. Allowed to run wide it was
     legible as a line and illegible as a grid: three squares of pale
     blue lying over other people's dates read as a banner across the
     calendar rather than as a note on one day. */
  position:relative; z-index:2; display:block; margin-top:8px;
  width:max-content; max-width:116%;
  padding:5px 7px 6px; border:2px solid var(--stick-edge); border-radius:3px;
  transform:rotate(var(--tilt,0deg)); transform-origin:8% 50%;
  box-shadow:0 1px 0 rgba(17,17,16,.22), 0 3px 7px rgba(17,17,16,.13);
}
.cal .stick b{
  display:block; font:700 14px/1.12 var(--body);
  text-transform:uppercase; letter-spacing:.03em;
}
/* The venue goes into the handwriting — short, an aside, which is the
   only thing the script face is for. The NAME stays in label type on
   purpose: the one name on this calendar written by hand is Haunted Bar
   Hop, and it stops being a signal the moment four others join it. */
.cal .stick em{
  display:block; font-style:normal; font-family:var(--script);
  font-size:17px; line-height:1.05; margin-top:3px;
  color:rgba(17,17,16,.74);
}
.cal .stick--sky  {background:var(--stick-sky);   color:var(--on-accent)}
.cal .stick--lime {background:var(--stick-lime);  color:var(--on-accent)}
.cal .stick--gold {background:var(--stick-gold);  color:var(--on-accent)}
.cal .stick--blush{background:var(--stick-blush); color:var(--on-accent)}
.cal .stick--navy {background:var(--stick-navy);  color:#FFFDF7}
.cal .stick--navy em{color:rgba(255,253,247,.82)}

/* ── THE PASTED-ON PICTURES ──────────────────────────────────────────
   Real die-cut stickers, laid in the square at an angle. Each one is
   authored with its own x / y / size / rotation in MONTH (bss.js) so no
   two sit the same way — a uniform grid of pictures is a sticker SHEET,
   and the thing being imitated is a sticker sheet that has been used.

   THE DAY NUMERAL ALWAYS WINS. It is the one piece of information a
   calendar exists to carry, and it is drawn at a fixed 33px at every
   width — so as the column narrows the numeral does NOT, and a pin
   placed by percentage walks straight into it. Two defences, because
   one is not enough:

     1. Geometry. Every pin is authored at x >= 34%, and below 760px
        the percentage is abandoned altogether for centred-under-the-
        numeral (see the narrow-column block further down), which is the
        only placement that still clears a 45px column.
     2. Order. The numeral is z-index 6, above every pin, every label
        and every strip, so even a pin dropped in at a bad x stays
        behind the date rather than on top of it.

   The drop shadow does the separating, not the die-cut border: these
   sit on a near-white sheet and three of the four are outlined in
   white, so their own edge disappears into the paper. */
.cal .pin{
  position:absolute; z-index:1;
  left:var(--sx); top:var(--sy);
  width:calc(var(--ss) * var(--pin-scale)); height:auto;
  /* The sheet-wide `img{max-width:100%}` applies here too, and a pin's
     containing block is ONE SQUARE — so every picture was being quietly
     clamped to its column the moment the authored size passed it, which
     is most of them below about 1200px and all of them on a phone. That
     is not a safety net for these: overrunning the square is the whole
     idea, and the clamp was what made a 99px sticker render at 48px. */
  max-width:none;
  transform:rotate(var(--tilt,0deg)); transform-origin:50% 88%;
  filter:drop-shadow(0 2px 3px rgba(17,17,16,.30));
  pointer-events:auto;
}
/* Anchored to the bottom of the square instead of hanging from the top.
   This is what sets the Brewers glove down ON the playoff strip: --sy is
   read as a distance up from the bottom rule, small enough that the
   glove and the tape overlap, so the two read as one object rather than
   a picture that happens to be in the same week.

   z-index 4 puts it above the strips (3) — a glove the tape was drawn
   over would look like a printing mistake rather than a sticker. */
.cal .pin--bottom{top:auto; bottom:var(--sy); z-index:4}
/* Hung off the right edge of its square instead of the left, so it
   overruns backwards. The only way a mark wider than a column can sit
   in the last column of a week without running off the sheet. */
.cal .pin--right{left:auto; right:var(--sx)}

/* The loose tickets: the homepage curtain's own silhouette, at confetti
   size. A <span> wrapping an <svg>, so it needs its width stated and its
   line box killed; everything else it inherits from .pin. */
.cal .pin--mark{line-height:0}
.cal .pin--mark svg{display:block; width:100%; height:auto}

/* A label in a square that has a picture starts below the picture. */
.cal td.has-pin .stick{margin-top:var(--pinband)}
/* Above the pins, the labels and the tape — see the note above.

   NO PLATE, NO SHADOW, NOTHING. The date is plain type in plain grey,
   and the only thing keeping it clear of the collage is z-index.

   Three attempts at a paper plate behind these all failed, and they all
   failed for the SAME reason, which the note further up this file
   already spells out under "NO GLOW ON A WHITE GROUND": inside
   .on-white the --ink token has inverted to near-black. Both calendars
   sit inside .on-white. So every "plate in the sheet's own colour"
   written here was deriving a BLACK halo — first blurred, then as eight
   hard offsets, then blurred again and softer. Technically consistent,
   and wrong every time. A token that means "the mark" is not the token
   for "the ground"; on this sheet the ground is --paper.

   It is also unnecessary. Measured across the breakpoints, exactly two
   numerals on the grid are ever touched by anything — the 5th and the
   15th, grazed at the edge by the photograph in the square before them,
   around 1024px — and both stay legible on their own, because they are
   drawn above the picture rather than under it. */
.cal .dnum{position:relative; z-index:6}

/* ── HOVER: KNOCK THE STICKER CROOKED ───────────────────────────────
   One rotation and nothing else. The angle is relative to the sticker's
   OWN rest angle, so one authored at +10deg and one at -18deg both turn
   by the same amount from where they actually sit.

   Pointer devices only — :hover on a touch screen latches after a tap
   and the sticker would simply stay askew. */
@media (hover:hover) and (prefers-reduced-motion:no-preference){
  .cal .pin,
  .cal .stick{transition:transform .22s var(--ease)}
  .cal .pin:hover,
  .cal .stick:hover{transform:rotate(calc(var(--tilt,0deg) + 7deg)); z-index:7}
}

/* ── MULTI-DAY STRIPS ────────────────────────────────────────────────
   A run of days is one piece of tape laid across the squares, not the
   same event copied into each of them.

   Width is counted in columns. An absolutely positioned box resolves its
   percentages against its containing block, which for these is the
   CELL'S PADDING BOX — one whole square, padding included — not the
   content box inside it. So `--n * 100%` is already --n squares, and
   `left:0` is already the square's left edge. (Adding the cell padding
   back on top, which looks like the obvious fix, over-runs every strip
   by one padding per column: the 7-column one ended up 106px past the
   right edge of the table.) The 3px each end keeps two consecutive runs
   from touching.

   --lane stacks strips that share a week row. Lane 0 rides the bottom
   rule; each lane up clears the one below it. */
.cal .span{
  position:absolute; z-index:3; display:flex; align-items:center; gap:7px;
  left:3px;
  width:calc(var(--n) * 100% - 6px);
  bottom:calc(7px + var(--lane) * 30px);
  height:29px; padding:0 11px; border-radius:2px;
  transform:rotate(var(--tilt,0deg));
  box-shadow:0 1px 0 rgba(17,17,16,.18), 0 2px 6px rgba(17,17,16,.12);
  white-space:nowrap; overflow:hidden;
}
.cal .span b{
  font:700 13.5px/1 var(--body); text-transform:uppercase; letter-spacing:.06em;
}
.cal .span em{
  font-style:normal; font-family:var(--script); font-size:16.5px;
  line-height:1; opacity:.8;
}
/* The playoffs are a fortnight of background, not a night out. Half the
   height of a real event strip so it never competes with one. */
.cal .span--thin{height:24px; padding:0 9px; gap:5px}
.cal .span--thin b{font-size:12px}
.cal .span--thin em{font-size:15px}
/* Two lines instead of one. Used where a run is too few squares wide to
   set its name and its venue side by side — see the Oktoberfest span in
   bss.js. The strip grows upward from the lane it sits on, which is why
   --laneband is sized off this case and not off a plain strip. */
.cal .span--stack{
  display:flex; flex-direction:column; align-items:flex-start;
  justify-content:center; gap:1px;
  height:auto; min-height:29px; padding:6px 11px 7px; white-space:normal;
}
.cal .span--stack b{line-height:1.08}
.cal .span--stack em{line-height:1.04; opacity:.86}
/* A strip with something sitting on its opening holds that space clear
   rather than running its name underneath. --lead is the mark's own
   authored width, passed in from MONTH, so a 92px glove and a 118px
   lockup each reserve the right amount; multiplying by the same
   --pin-scale the pin uses keeps the two locked together at every
   breakpoint. A percentage would not track, because a label's width is
   roughly constant in px while the column it sits in is not. */
.cal .span--lead{padding-left:calc(var(--pin-scale) * var(--lead,92px) + 14px)}
.cal .span--gold{background:var(--stick-gold); color:var(--on-accent)}
.cal .span--navy{background:var(--stick-navy); color:#FFFDF7}
.cal .span--sky {background:var(--stick-sky);  color:var(--on-accent)}
.cal .span--lime{background:var(--stick-lime); color:var(--on-accent)}




/* --pinband tracks the picture: it is the picture's own height less the
   overlap we want the label to take, so the caption keeps landing in the
   same place on the photograph as both scale down. */
@media (max-width:1100px){
  .cal{--pin-scale:.92; --pinband:51px}
}
@media (max-width:900px){
  /* Both venue lines come off here. A label is capped to about its own
     square now, so at this width "Landmark CU Live" breaks after "CU"
     and a two-line name becomes a four-line block. The name is the half
     that identifies the night; the venue is in the listing below. */
  .cal .evt em,
  .cal .stick em{display:none}
  .cal{--pin-scale:.82; --pinband:44px}
  .cal td{height:clamp(208px,22vw,226px)}
}
@media (max-width:760px){
  .cal th{font-size:8.5px; letter-spacing:.08em}
  .cal .evt{font-size:9.5px; letter-spacing:.03em}
  .cal{--cal-pad-x:4px; --cal-pad-y:6px; --pin-scale:.66; --pinband:66px; --laneband:66px}
  .cal td{height:clamp(208px,45vw,232px)}
  /* A column is about 45px wide here and the numeral is still 33px, so
     there is no horizontal room beside it at any x — the authored
     percentage is dropped and every pin centres UNDER the date instead.
     left is computed rather than using translateX so the transform
     stays pure rotation and the wobble keyframes keep working. */
  .cal .pin:not(.pin--bottom){left:calc(50% - (var(--ss) * var(--pin-scale)) / 2)}
  /* only the ones hanging from the top have to clear the numeral */
  .cal .pin:not(.pin--bottom){
    /* 32px, and every part of that number was measured rather than
       guessed. The numeral's own line box is 15px, but Jomhuria's
       glyphs overflow it and the ink actually ends at 26.7px; and a
       square rotated by 10deg has a bounding box ~16% taller than
       itself, which grows UPWARD from where `top` puts it. 16px put all
       four pictures across their dates and 24px still grazed them. */
    top:calc(32px + var(--sy) * .3);
  }
  /* A square is about 45px wide down here. The tilt is what tips a
     sticker into its neighbour at that size, so it comes off; the
     die-cut border and the shadow still carry the sticker read. */
  .cal .stick{margin-top:5px; padding:4px 6px 5px; border-width:1.5px}

  .cal td.has-pin .stick{margin-top:var(--pinband)}
  .cal .stick b{font-size:12.5px; letter-spacing:.02em; line-height:1.1}
  /* Strips have to wrap down here — "Milwaukee Octoberfest" is wider
     than the two squares it covers — so they stop being fixed-height. */
  .cal .span{
    height:auto; min-height:19px; padding:3px 6px; gap:5px;
    white-space:normal; transform:none; bottom:calc(6px + var(--lane) * 30px);
  }
  .cal .span b{font-size:11.5px; letter-spacing:.03em; line-height:1.12}
  .cal .span--thin{height:auto; min-height:17px; padding:2px 5px}
  /* The tape keeps its NAME here and loses only its handwriting. What
     made this impossible before was the reserved opening: a mark lying
     on the tape took 80 of the 145px the navy run has down here, and the
     name had nowhere to go. The marks now rest on the tape's TOP EDGE
     instead (see .pin--bottom below), which is the same thing the
     Oktoberfest lockup already did, so the opening is not needed and the
     full width of the tape is free to carry the words. */
  .cal .span--lead{padding-left:0}
  .cal .span--marked em{display:none}
  .cal .span--stack{min-height:0; padding:3px 6px}
  /* Both tape marks sit ABOVE their strip down here rather than across
     it, so the words underneath stay readable end to end. They still
     touch the tape they belong to — they just touch its top edge.

     Measured, because the numbers here are unforgiving: the navy run is
     139px wide at a 375px viewport, "MLB PLAYOFFS" takes 85 of it, and
     the glove is 74 — so the two cannot share the tape at any placement,
     and the glove has to come off it.

     It also hangs off the LEFT end rather than sitting inside Oct 1.
     A 74px sticker does not fit a 48px square, and overrunning to the
     right puts it into Oct 2, where it lands on the gold tape's own name
     and on the Oktoberfest lockup. To the left is the empty square
     before the 1st, so that is the way it overruns — half on the start
     of the line, half off it. */
  .cal .pin--bottom{bottom:27px}
  .cal .pin--bottom:not(.pin--right){left:-52%}
  /* The gold tape carries one line here rather than two, so it is about
     22px shorter than on a desktop, and the lockup riding its top edge
     comes down to match. */
  .cal .pin--right{bottom:60px}
  /* The two tape marks are the one thing that cannot keep its full size
     here. They sit at opposite ends of the same corner of the grid — the
     glove at the start of the navy run, the lockup at the end of the
     gold one — and a 48px column leaves about 145px between them for
     215px of sticker, so at full size they landed on each other. The
     free-standing pictures keep the full .66 and go on overrunning. */
  .cal .pin--bottom{--pin-scale:.44}
}

/* the calendar also runs on the dark band on the homepage */
.band--ink .cal th{color:rgba(255,255,255,.62)}
.band--ink .cal td{border-top-color:rgba(255,255,255,.2)}
.band--ink .cal td:not(.ours) .dnum{color:rgba(255,255,255,.55)}
.band--ink .cal td.has:not(.ours) .dnum{color:var(--ink)}
.band--ink .cal td:not(.ours) .evt{color:var(--ink)}
.band--ink .cal td:not(.ours) .evt em{color:rgba(255,255,255,.6)}
.band--ink .cal .must path{stroke:rgba(255,255,255,.8)}

/* Calendar internals on the white sheet. These MUST stay below the
   `.band--ink .cal *` rules above: the homepage calendar sits inside a
   .band--ink section, and at equal specificity source order decides. The
   doubled `.sheet.sheet` also lifts them so a tie is impossible if
   anything is reordered later.

   Every one of them is scoped `td:not(.ours)`. Both blocks THEME the
   calendar for the ground it is sitting on, and our own date is not
   themed — it is a solid orange block with near-black type wherever it
   appears. Excluding it here is the fix; out-specifying it from the
   .ours rules below is not, because `.sheet.sheet .cal .dnum` is (0,4,0)
   and beat `.cal td.ours .dnum` at (0,3,1) — which is exactly how the
   day numeral ended up at 2.32:1 on the orange. */
.sheet.sheet .cal th{color:rgba(17,17,16,.6)}
.sheet.sheet .cal td{border-top-color:rgba(17,17,16,.2)}
/* #90908E is rgba(17,17,16,.45) already blended onto the sheet: the same
   colour it has always rendered as, but opaque, so the plate behind it
   cannot show through the strokes. */
.sheet.sheet .cal td:not(.ours) .dnum{color:#90908E}
.sheet.sheet .cal td.has:not(.ours) .dnum{color:var(--on-accent)}
.sheet.sheet .cal td:not(.ours) .evt{color:var(--on-accent)}
.sheet.sheet .cal td:not(.ours) .evt em{color:rgba(17,17,16,.58)}
.sheet.sheet .cal .must path{stroke:rgba(17,17,16,.8)}
/* ── OUR DATE ────────────────────────────────────────────────────────
   The one square on the calendar that is ours gets painted rather than
   annotated. A ring said "worth going to", which is the right note for
   somebody else's show and much too polite for the only event on the
   page that we run.

   The paint is a pseudo-element, not a background on the <td>: a table
   cell cannot be rotated (transform on a table-cell is undefined in
   practice and collapses the row), and the ~1.4deg tilt is what makes
   it read as something laid ON the grid rather than a filled table
   cell. Same reason the polaroids tilt.

   TYPE ON ORANGE IS NEAR-BLACK, ALWAYS. --orange is mid-luminance:
   white on it measures 3.3:1 and fails, --on-accent measures 5.7:1.
   This is the site-wide rule, and this cell is not an exception. */
.cal td.ours{overflow:visible}
/* ── THE BOX ─────────────────────────────────────────────────────────
   The orange field, its halftone and the card are all this one box, and
   it is the same shape at every width — hung off the RIGHT edge of the
   square, starting BELOW the numeral, and never narrower than --oc-min.

   That last part is the whole point. The box used to be the square, and
   a square is whatever 1/7th of the shell happens to be: 194px at 1440
   and 100px at 761, which is not enough to hold a picture, a name, a
   venue line and a pill. Between 760 and 1024 the card was 100-138px
   wide with the photograph crushed to 64px, and at exactly 760 it jumped
   from 100px to 242px, because the phone rules widened it and nothing
   else did.

   So it widens wherever the column is too narrow for it, and overruns
   LEFT into the empty end of the month. Right-anchored because the 31st
   is a Saturday with nothing to its right but sheet edge; below the
   numeral because that is what keeps the 29th and the 30th from having
   their dates covered when it overruns across them. */
.cal td.ours{
  --oc-min:188px;
  --oc-box:max(calc(100% - 6px), var(--oc-min));
}
.cal td.ours::before,
.cal td.ours::after,
.cal td.ours .ourcard{
  position:absolute; inset:32px 3px 4px auto; width:var(--oc-box);
  transform:rotate(-1.4deg);
}
.cal td.ours::before{
  content:""; background:var(--orange); z-index:0;
}
/* the same 6px screen the rest of the print carries, so the block is a
   printed field rather than a flat swatch */
.cal td.ours::after{
  content:""; z-index:1; pointer-events:none;
  background-image:radial-gradient(rgba(17,17,16,.30) 1.3px, transparent 1.4px);
  background-size:6px 6px; mix-blend-mode:multiply; opacity:.55;
}
.cal td.ours .dnum{
  position:relative; z-index:6;
  color:var(--on-accent); font-size:clamp(38px,4.4vw,52px); line-height:.42;
}

/* ── OUR DATE, AS A CARD ─────────────────────────────────────────────
   The square used to be a coloured field with a line of bold type at the
   top and two thirds of nothing under it. It is now a miniature of the
   homepage event card, in the same order: picture, name, the line under
   it, a status pill. The grid around it is a collage of photographs and
   stickers; the one square that is OURS was the only thing on it still
   made of type alone.

   It lies in exactly the box the orange field and its halftone occupy —
   same inset, same 1.4deg — so the picture sits INSIDE the orange
   instead of replacing it. Both reasons matter: "if it's orange, go" is
   the page's whole promise, and near-black on --orange is 5.7:1, so
   every word in here passes with no scrim, no plate and no halo. A
   photograph behind the type would have needed the entire hero
   apparatus to hold a 194px square.

   The top padding is the numeral's seat. .dnum is the cell's, not the
   card's, and it draws above at z-index 6. */
.cal td.ours .ourcard{
  z-index:2; transform-origin:50% 50%;
  display:grid; grid-template-rows:minmax(0,1fr) auto auto;
  padding:11px 11px 12px; overflow:hidden;
  text-decoration:none; color:var(--on-accent);
  /* the name is sized against the CARD, not the viewport — same device
     .event__title uses, and the reason the lockup now breaks to the same
     two lines at every width instead of one line at some and two at
     others */
  container-type:inline-size;
}
/* object-position, not a crop: every Haunted frame in the repo is 2:3
   portrait and the crawlers sit in the BOTTOM half of it — the same
   reason the hero runs --hero-pos. A centred crop in a slot this wide
   throws the subjects away and keeps a ceiling. */
.cal td.ours .ourcard__shot{
  display:block; position:relative; overflow:hidden; min-height:0;
  margin-bottom:8px; background:var(--ink-2);
}
.cal td.ours .ourcard__shot img{
  display:block; width:100%; height:100%;
  object-fit:cover; object-position:50% 74%;
}
/* The display face, which is what the rest of the site sets a name in.
   Jomhuria's Latin cap height is .3907em, so these numbers look about
   2.2x too large and are not — see the type note at the top of the
   file. */
.cal td.ours .ourcard__name{
  display:block; font-family:var(--display); font-weight:400;
  font-size:clamp(23px,25cqw,40px); line-height:.46;
  letter-spacing:.02em; text-transform:uppercase; color:var(--on-accent);
}
.cal td.ours .ourcard__meta{
  display:block; margin-top:6px;
  font:500 9.5px/1.2 var(--body); text-transform:uppercase;
  letter-spacing:.11em; color:rgba(17,17,16,.86);
}
/* The pill inverts — near-black ground, light orange type — which is the
   same move the homepage makes with .sticker--live, and the only way to
   put a second orange on an orange field and still see it. 8.2:1. */
.cal td.ours .ourcard__chip{
  justify-self:start; margin-top:7px;
  background:var(--on-accent); color:var(--orange-lt);
  font:700 8.5px/1 var(--body); text-transform:uppercase;
  letter-spacing:.12em; padding:5px 8px 4px; border-radius:999px;
}
.cal td.ours .ourcard:hover .ourcard__name{text-decoration:underline}
.cal td.ours .ourcard:hover .ourcard__shot img{transform:scale(1.04)}
.cal td.ours .ourcard__shot img{transition:transform .5s var(--ease)}
@media (prefers-reduced-motion:reduce){
  .cal td.ours .ourcard__shot img{transition:none}
}

/* ── OUR CARD, ON A PHONE ───────────────────────────────────
   A square is 48px wide down there and the card cannot live inside one:
   at desktop proportions the name broke to "HAU / BAR", the venue line
   and the pill were both cut off, and the whole thing ran off the right
   of the sheet.

   So it stops pretending to be a square and becomes a card properly —
   about two and a half columns wide, hung off the RIGHT edge of the 31st
   and overrunning LEFT across the empty end of the month. Right, because
   the 31st is a Saturday and there is nothing to its right but sheet
   edge.

   It also starts BELOW the numeral band instead of at the top of the
   cell, and that is what makes the overrun safe: the 29th and the 30th
   keep their own numerals, because the card never reaches up into the
   row they sit in. The orange field and its halftone come with it — all
   three are the same box.

   THIS BLOCK HAS TO SIT AFTER THE RULES ABOVE. It first went in with the
   other narrow-column rules a few hundred lines earlier, where
   `inset:auto ... auto` lost to the desktop `inset:4px 3px` at identical
   specificity — and lost SILENTLY, because `width` is not set in the
   desktop rule. So the card came out 238% wide and still pinned by its
   LEFT edge; over-constrained, `right` was ignored, and it ran straight
   off the sheet. */
@media (max-width:900px){
  /* the sheet itself is narrower here, so the floor comes down with it —
     188px against a 96px column was two thirds of the week */
  .cal td.ours{--oc-min:152px}
}
@media (max-width:760px){
  /* 86px is about 1.9 columns at a 48px square — the 31st plus a small
     overhang onto the 30th, rather than the 2.5 columns 118px reached
     back across. Laddered at 118/104/94/86/78 and nothing clips at any of
     them: the name is sized in cqw so it shrinks with the card and stays
     on two lines, and the venue line and the pill both still fit. This is
     the narrowest step that keeps the photograph a useful size. */
  .cal td.ours{--oc-min:86px}
  .cal td.ours .ourcard{padding:9px 9px 10px}
  .cal td.ours .ourcard__shot{margin-bottom:7px}
  .cal td.ours .ourcard__meta{font-size:8.5px; letter-spacing:.08em; margin-top:5px}
  .cal td.ours .ourcard__chip{font-size:7.5px; padding:4px 6px 3px; margin-top:6px}
  /* The loose ticket the widened card now lands on. One stays — the one
     in the row above — which is enough to read as confetti. */
  .cal .pin--crowded{display:none}
}

/* The doodled arrows. Positioned against the cell and allowed to hang
   outside it — see the note on CALARROWS in bss.js. */
.cal__arrow{position:absolute; z-index:3; pointer-events:none; overflow:visible}
.cal__arrow path{
  fill:none; stroke:var(--on-accent); stroke-width:3.4;
  stroke-linecap:round; stroke-linejoin:round;
}
.cal__arrow--in{left:-46%; top:6%; width:58%; height:auto}
.cal__arrow--down{right:9%; top:-30%; width:20%; height:auto}
/* Below 760px a cell is ~48px wide and the arrows are bigger than the
   thing they point at. The paint carries it on its own down there. */
@media (max-width:760px){ .cal__arrow{display:none} }


/* ============================================================
   FORM BITS
   ============================================================ */
.field{
  width:100%; font:400 15px/1.3 var(--body); padding:16px 18px;
  background:var(--paper-2); color:var(--ink);
  border:1.5px solid var(--rule); border-radius:12px;
  transition:border-color .2s var(--quart), box-shadow .2s var(--quart);
  -webkit-appearance:none; appearance:none;
}
.field::placeholder{color:var(--text-3)}
.field:hover{border-color:rgba(248,248,246,.34)}
.field:focus{
  outline:none; border-color:var(--orange);
  box-shadow:0 0 0 4px rgba(255,77,20,.2);
}
select.field{
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23F8F8F6' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat; background-position:right 18px center; padding-right:44px;
}
textarea.field{min-height:140px; line-height:1.55; resize:vertical}
.ftr .field{background:rgba(255,255,255,.07); border-color:rgba(255,255,255,.28); color:var(--ink)}
.ftr .field::placeholder{color:rgba(255,255,255,.5)}
.ftr .field:focus{border-color:var(--orange); box-shadow:0 0 0 4px rgba(255,77,20,.28)}
.capture{display:flex; gap:12px; flex-wrap:wrap}
.capture .field{flex:1; min-width:220px}

/* ============================================================
   FOOTER
   ============================================================ */
.ftr{background:var(--ink-deep); color:var(--ink)}
.ftr a{color:var(--ink); text-decoration:none}
.ftr a:hover{color:var(--orange-lt)}
.ftr__grid{display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:clamp(20px,3vw,44px); padding-bottom:44px}
@media (max-width:860px){ .ftr__grid{grid-template-columns:repeat(2,minmax(0,1fr))} }
@media (max-width:480px){ .ftr__grid{grid-template-columns:minmax(0,1fr)} }
.ftr__h{font:700 10.5px/1 var(--body); text-transform:uppercase; letter-spacing:.2em; color:rgba(255,255,255,.6); margin:0 0 12px}
/* the signup is the last thing in the footer now that the fine-print row is
   gone, so it is centred and owns the closing space */
.ftr__signup{
  border-top:1px solid rgba(255,255,255,.18);
  padding:clamp(30px,4vw,48px) 0 clamp(34px,4.4vw,54px);
  display:grid; justify-items:center; text-align:center; gap:6px;
}
.ftr__signup .small{color:rgba(255,255,255,.6); margin:0}
.ftr__signup .h4{margin:0}
.ftr__signup .capture{margin-top:clamp(14px,1.8vw,22px); justify-content:center; width:100%; max-width:520px}

/* ── THE SIGN-OFF ────────────────────────────────────────────────────
   Last thing on every page, and the one place the logo has nothing to
   compete with — so it runs at wordmark scale rather than the chrome
   scale it takes in the header, and the tagline sits under it in the
   script face the way a signature sits under a printed mark.

   Deliberately no rule above it: the signup already closes with one,
   and a second hairline 40px later reads as a stray table border. The
   space does the separating instead. */
.ftr__signoff{
  display:grid; justify-items:center; gap:clamp(10px,1.4vw,16px);
  padding:clamp(34px,4.4vw,58px) 0 clamp(30px,3.6vw,46px);
  border-top:1px solid rgba(255,255,255,.12);
}
.ftr__logo{width:clamp(184px,26vw,272px); color:var(--ink); opacity:.94}
.ftr__tag{
  font-family:var(--script); font-weight:400;
  font-size:clamp(21px,2.6vw,30px); line-height:1.2;
  color:var(--orange-on); margin:0; text-align:center;
}
/* ============================================================
   THE PRESS
   Hover said "you can act on this"; nothing said "it happened".
   Every interactive object on the site had a hover and only two
   had a :active, so a click landed with no acknowledgement and
   the whole thing felt soft.

   One gesture, everywhere: the object gives. It drops whatever
   lift the hover gave it and compresses a hair. Written through
   the same --tx/--ty/--rot/--sc variables the reveal composes,
   so it survives inside a [data-reveal] group — a plain
   `transform` here would be beaten by the reveal's own rule at
   (0,3,0), which is the trap that silently killed the polaroid
   tilts and the in-group ticket hover before.

   Fast on the way in, slower back out: --press-dur is read by
   each component's own transition so the give is instant and the
   release settles. That asymmetry is most of what "snappy" is.
   ============================================================ */
.btn:active        { --ty:0px; --sc:.965 }
a.card:active      { --tx:0px; --ty:0px; --sc:.985; box-shadow:2px 2px 0 var(--ink) }
.taped:active      { --ty:-2px; --sc:.99 }
.stub:active       { --ty:-1px; --sc:.988 }
.event:active      { --sc:.988 }
.listing__row:active { --tx:4px }
.nav a:active,
.more__menu a:active { --sc:.96 }
/* .ticket keeps its own, richer press — it is the signature object and
   already lands flat from its tilt. This only adds the compression. */
.ticket:active     { --rot:-1.4deg; --ty:0px; --sc:.975 }
/* The press has to beat the hover, and both are (0,2,0) at most, so
   source order does it — these all sit after their hover rules. */
.btn,a.card,.taped,.stub,.event,.ticket{ --press-dur:.09s }
.btn:active,a.card:active,.taped:active,.stub:active,
.event:active,.ticket:active{ transition-duration:var(--press-dur) }

/* ============================================================
   BODY LINKS
   Inline links inside running copy were carrying colour alone,
   which is both the weakest affordance available and a WCAG
   1.4.1 problem — colour cannot be the only signal. They get a
   drawn rule that wipes in from the left on hover, which is also
   the one hover on the site that reads as writing rather than as
   a UI state.
   ============================================================ */
/* Every selector here excludes .btn. This rule is for links inside running
   copy; a pill button is a different object and gets wrecked by it — the
   padding-bottom:.06em overrides the button's own 15px bottom padding (at
   13px type that computes to 0.78px, so the pill loses 14px of height and
   the label sits on the floor of it) and the gradient paints a 1px rule
   straight across the fill. Measured on the footer's Instagram button: 32px
   tall against the header button's 46px, same classes. Anywhere a .btn ends
   up inside .body / .lede / .small / .ftr__signup, this would bite again. */
.body a:not(.btn), .lede a:not(.btn), .small a:not(.btn), .ftr__signup a:not(.btn){
  position:relative; text-decoration:none;
  background-image:linear-gradient(currentColor,currentColor);
  background-repeat:no-repeat;
  background-position:0 100%;
  background-size:100% 1px;
  padding-bottom:.06em;
  transition:background-size .32s var(--ease), color .2s var(--quart);
}
@media (hover:hover){
  .body a:not(.btn):hover, .lede a:not(.btn):hover,
  .small a:not(.btn):hover, .ftr__signup a:not(.btn):hover{
    background-size:100% 2px;
  }
}
.body a:not(.btn):focus-visible, .lede a:not(.btn):focus-visible,
.small a:not(.btn):focus-visible, .ftr__signup a:not(.btn):focus-visible{background-size:100% 2px}
/* the footer's own links already sit on their own line and take the
   accent on hover; keep that and let the rule follow the colour */
.ftr a:not(.btn):hover{background-size:100% 2px}

/* ============================================================
   THE CURSOR
   One drawn object per page — ghost, clover, snowflake, or the
   default sparkle — turning slowly, opening up on anything you
   can act on. Built in bss.js; everything visual is here.

   THREE NESTED ELEMENTS, ONE JOB EACH. .cur is moved by the rAF
   loop, .cur__scale takes the hover growth, .cur__spin takes the
   rotation. They all write `transform`, and a CSS animation beats
   a plain declaration on the same element — put two of these jobs
   on one node and the hover silently stops working. This is the
   same trap the polaroid tilts hit.
   ============================================================ */
.cur{
  position:fixed; left:0; top:0; z-index:210;   /* over .drop (200) */
  width:30px; height:30px; margin:-15px 0 0 -15px;
  pointer-events:none;                          /* never eat a click */
  opacity:0; transition:opacity .2s var(--quart);
  will-change:transform;
  color:var(--cur-c, var(--orange));
}
.cur.is-on{opacity:1}
/* Over a text field the drawn cursor gets out of the way and the real
   I-beam comes back — see the .has-cur rules below. A missing caret is
   a genuine cost; a missing doodle is not. */
.cur.is-field{opacity:0}
.cur__scale,.cur__spin{display:block; width:100%; height:100%}
.cur__scale{transition:transform .3s var(--ease)}
.cur__spin{animation:cur-spin var(--cur-dur,14s) linear infinite}
.cur svg{
  display:block; width:100%; height:100%;
  /* reads on both grounds: the fill carries it on the dark pages, the
     shadow gives it an edge inside the white bands */
  filter:drop-shadow(0 1px 2px rgba(6,6,5,.5)) drop-shadow(0 0 1px rgba(6,6,5,.5));
}
/* the open-up. Faster spin AND bigger, which is the whole gesture */
.cur.is-hot{--cur-dur:2.1s}
.cur.is-hot .cur__scale{transform:scale(1.62)}
@keyframes cur-spin{ to{ transform:rotate(360deg) } }

/* Per page. The cursor takes the event's own ink, so it belongs to the
   page it is on rather than being one more thing in orange. */
body[data-page="halloween"] .cur{--cur-c:var(--violet)}
body[data-page="christmas"] .cur{--cur-c:var(--red)}
body[data-page="shamrock"]  .cur{--cur-c:var(--green)}

/* The native cursor is hidden ONLY once the drawn one is really on
   screen — .has-cur is added on the first mousemove, not at boot. A
   browser that never fires one keeps its arrow.
   Fields keep theirs: `cursor:auto` here beats the inherited none. */
@media (pointer: fine){
  html.has-cur, html.has-cur *{cursor:none}
  html.has-cur input,
  html.has-cur textarea,
  html.has-cur select{cursor:auto}
}
/* Reduced motion keeps the object and drops the movement: no spin, no
   growth, and the follow snaps rather than glides (bss.js reads the same
   query for the lerp). */
@media (prefers-reduced-motion: reduce){
  .cur__spin{animation:none}
  .cur.is-hot .cur__scale{transform:none}
}


/* ============================================================
   MOTION — the global reduced-motion stop.
   The site now carries three deliberate effects (the entrance reveal,
   the neon hero flicker, the live-chip pulse); this kills all of them,
   plus every transition, for anyone who asks for reduced motion.
   ============================================================ */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation:none !important; transition:none !important}
  html{scroll-behavior:auto}
}



/* Route stop list. Grid, not CSS columns: with the route map gone this
   runs the full width of the shell, and multi-column would order the
   stops down one side and back up the other — a numbered route has to
   read 01, 02, 03 left to right. Grid also keeps every row on the same
   baseline across columns, which columns cannot guarantee, and it does
   not interact with the reveal's transform the way break-inside does. */
.stops{
  list-style:none; margin:0; padding:0;
  display:grid; grid-template-columns:repeat(3,minmax(0,1fr));
  column-gap:clamp(24px,3vw,44px);
  border-top:1px solid var(--rule);
}
@media (max-width:900px){ .stops{grid-template-columns:repeat(2,minmax(0,1fr))} }
@media (max-width:560px){ .stops{grid-template-columns:minmax(0,1fr)} }
.stops li{
  display:grid; grid-template-columns:32px minmax(0,1fr); gap:12px;
  align-items:baseline;
  padding:13px 0; border-bottom:1px solid var(--rule); font-size:15px;
}
/* Both lists reserve a column for a leading numeral. Where the numeral is
   gone the column has to go with it, or every row sits indented behind an
   empty gutter. Written as :has() rather than a modifier class so the
   markup stays plain and a numeral coming back needs no CSS change —
   .listing still carries one on Happening Now, where it is a real date. */
.listing__row:not(:has(.listing__day)){grid-template-columns:minmax(0,1fr) auto}
.stops li:not(:has(b)){grid-template-columns:minmax(0,1fr)}
.stops b{font-family:var(--display); font-weight:400; color:var(--orange-on); font-size:33px; line-height:.614; letter-spacing:.03em}
.stops em{font-style:normal; color:var(--text-3); display:block; font-size:12.5px; letter-spacing:.04em}

/* ============================================================
   HOMEPAGE TYPE STEP-UP
   "We would like all of the text to be bigger on the homepage."

   Scoped to body[data-page="home"] on purpose, and deliberately NOT a
   change to the site-wide scale. Every other page is a reading page —
   the route list, the ticket table, the calendar — and was measured at
   the current sizes. The homepage is a poster, and the running copy on
   it was set at the same size as the small print on Contact.

   Everything here moves by roughly the same ~14%, so the relationships
   between the sizes are unchanged; the whole page just sits one step
   closer. The DISPLAY type is untouched — it is already at the top of
   its ramp and moving it would break the lockups' measured wrap points.
   ============================================================ */
body[data-page="home"]{ font-size:clamp(20px,1.78vw,25px) }
body[data-page="home"] .lede{ font-size:clamp(20px,1.78vw,25px) }
body[data-page="home"] .small{ font-size:16px }
/* .tiny and .sticker are tracked-out caption faces; the tracking has to
   ease as they grow or the letters read as separate marks rather than a
   word. Same reason .event__meta .tiny drops from .18em to .13em. */
body[data-page="home"] .tiny{ font-size:13px; letter-spacing:.15em }
body[data-page="home"] .sticker{ font-size:12.5px; padding:9px 15px }
body[data-page="home"] .incl__row > div > .h4{ font-size:clamp(38px,4.3vw,62px) }
/* The calendar sheet keeps its own scale — it is a dense grid where a
   14% step costs a column, and it is the one thing on this page that is
   a table rather than a poster. */
body[data-page="home"] .sheet .kicker{ font-size:11.5px }
