/* =============================================================================
   THE RED 4 DIGITAL SITE FRAMEWORK — public stylesheet

   PALETTES ARE PARSED OUT OF THIS FILE by server/lib/theme.ts. That is why
   each one is a flat block of custom properties with no nesting and no
   shorthand: the parser reads `[data-palette="x"] { --token: value; }` and
   nothing cleverer. Adding a palette here makes it appear in the admin with
   no TypeScript change at all — which is the point — but a palette written in
   some other shape will simply not be found.

   Every palette must define EVERY token. A palette that omits one inherits
   whatever the previous one left behind, which shows up as a single wrong
   colour on one page in one season.
   ========================================================================== */

/* ------------------------------------------------------------- palettes -- */

[data-palette="slate"]{
  --bg:#FBFBFC; --surf:#FFFFFF; --surf2:#F1F3F6; --deep:#1B2430;
  --ink:#1C232C; --muted:#5D6874; --line:#E1E5EB;
  --brand:#2C5AA0; --on-brand:#FFFFFF; --brand-soft:#E8EEF8;
  --acc:#D9772E; --acc-ink:#FFFFFF; --acc2:#3E8E7E; --acc2-ink:#FFFFFF;
  --hero-from:rgba(18,26,36,.74); --hero-to:rgba(18,26,36,.10);
}
[data-palette="orchard"]{
  --bg:#FFFDF6; --surf:#FFFFFF; --surf2:#F4F0E2; --deep:#25341F;
  --ink:#26331F; --muted:#61705A; --line:#E4DCC7;
  --brand:#2F7D4F; --on-brand:#FFFFFF; --brand-soft:#E4F0E7;
  --acc:#E39A26; --acc-ink:#3B2905; --acc2:#5E9CC0; --acc2-ink:#12222C;
  --hero-from:rgba(20,34,18,.72); --hero-to:rgba(20,34,18,.06);
}
[data-palette="harbour"]{
  --bg:#F8FAFC; --surf:#FFFFFF; --surf2:#EBF1F5; --deep:#0F2733;
  --ink:#132630; --muted:#556A75; --line:#D8E3EA;
  --brand:#12667F; --on-brand:#FFFFFF; --brand-soft:#DCEEF3;
  --acc:#E8623C; --acc-ink:#FFFFFF; --acc2:#F0B429; --acc2-ink:#33260A;
  --hero-from:rgba(10,32,42,.74); --hero-to:rgba(10,32,42,.08);
}
[data-palette="ember"]{
  --bg:#FFFAF6; --surf:#FFFFFF; --surf2:#F7EBE2; --deep:#2C1A16;
  --ink:#2A1A16; --muted:#6E5A52; --line:#EBDACF;
  --brand:#A8321E; --on-brand:#FFFFFF; --brand-soft:#F9E4DE;
  --acc:#C99A28; --acc-ink:#332703; --acc2:#4A6B5C; --acc2-ink:#FFFFFF;
  --hero-from:rgba(36,18,14,.76); --hero-to:rgba(36,18,14,.08);
}
[data-palette="linen"]{
  --bg:#FCFBF8; --surf:#FFFFFF; --surf2:#F2EFE8; --deep:#232220;
  --ink:#232220; --muted:#6B6862; --line:#E4E0D6;
  --brand:#3A3733; --on-brand:#FFFFFF; --brand-soft:#EDEAE3;
  --acc:#8A7B5F; --acc-ink:#FFFFFF; --acc2:#5C6B66; --acc2-ink:#FFFFFF;
  --hero-from:rgba(24,23,21,.70); --hero-to:rgba(24,23,21,.06);
}
[data-palette="bloom"]{
  --bg:#FFFAFC; --surf:#FFFFFF; --surf2:#F8ECF2; --deep:#301C2A;
  --ink:#2C1B27; --muted:#6E5A66; --line:#EDD9E3;
  --brand:#A6386B; --on-brand:#FFFFFF; --brand-soft:#FAE3EE;
  --acc:#C9A227; --acc-ink:#332803; --acc2:#6E7BA8; --acc2-ink:#FFFFFF;
  --hero-from:rgba(38,18,32,.74); --hero-to:rgba(38,18,32,.08);
}
[data-palette="spring"]{
  --bg:#FBFDF7; --surf:#FFFFFF; --surf2:#ECF5E5; --deep:#22381F;
  --ink:#24381F; --muted:#5C7255; --line:#D9E8CF;
  --brand:#4E9A51; --on-brand:#FFFFFF; --brand-soft:#E6F3E5;
  --acc:#E0728F; --acc-ink:#FFFFFF; --acc2:#8FCBAE; --acc2-ink:#173026;
  --hero-from:rgba(20,40,20,.70); --hero-to:rgba(20,40,20,.06);
}
[data-palette="summer"]{
  --bg:#FFFCF0; --surf:#FFFFFF; --surf2:#FDF2D6; --deep:#33301C;
  --ink:#332F1C; --muted:#6E6A4C; --line:#EDE0B8;
  --brand:#CE7B10; --on-brand:#FFFFFF; --brand-soft:#FDEFD5;
  --acc:#2F7FA0; --acc-ink:#FFFFFF; --acc2:#6FA33A; --acc2-ink:#FFFFFF;
  --hero-from:rgba(48,40,10,.68); --hero-to:rgba(48,40,10,.06);
}
[data-palette="autumn"]{
  --bg:#FDF7EE; --surf:#FFFFFF; --surf2:#F3E6D3; --deep:#33251A;
  --ink:#32251A; --muted:#6F5B46; --line:#E5D1B9;
  --brand:#A9491A; --on-brand:#FFFFFF; --brand-soft:#F8E7DA;
  --acc:#B98C1E; --acc-ink:#33260A; --acc2:#4F6B3C; --acc2-ink:#FFFFFF;
  --hero-from:rgba(44,26,12,.74); --hero-to:rgba(44,26,12,.06);
}
[data-palette="winter"]{
  --bg:#F7F9FB; --surf:#FFFFFF; --surf2:#E7EEF3; --deep:#1E2930;
  --ink:#20282F; --muted:#5D6A73; --line:#D4DEE6;
  --brand:#2F6C8F; --on-brand:#FFFFFF; --brand-soft:#E2EDF3;
  --acc:#B85B3E; --acc-ink:#FFFFFF; --acc2:#7E96A4; --acc2-ink:#111C22;
  --hero-from:rgba(18,30,38,.72); --hero-to:rgba(18,30,38,.08);
}
[data-palette="festive"]{
  --bg:#FBF7F0; --surf:#FFFFFF; --surf2:#EFE6D6; --deep:#17301F;
  --ink:#16301E; --muted:#53705C; --line:#DCCFBA;
  --brand:#96222A; --on-brand:#FFFFFF; --brand-soft:#F7E4E1;
  --acc:#B8901F; --acc-ink:#2E2303; --acc2:#1E5B3E; --acc2-ink:#FFFFFF;
  --hero-from:rgba(12,32,20,.76); --hero-to:rgba(12,32,20,.08);
}
/* ----------------------------------------------------------- font sets -- */
/* --dw display weight · --dt tracking · --ds line height factor
   --dsc display SIZE correction, and this one is measured rather than chosen.

   The headline scale below is one set of clamps for all six pairings, so every
   face was asked to hold the same font-size — and a font-size is not a size.
   Measured in a browser at 100px, cap heights came out:

     Gloock 75 · Instrument Serif 72 · Fraunces 70 · Space Grotesk 70
     · Bricolage Grotesque 66 · Baloo 2 61.6

   That is a 22% spread between the largest and smallest capitals at an
   identical font-size, which is why switching pairing used to change how big
   the page felt as well as how it read. --dsc brings each back to the default
   pairing's cap height. Re-derive it the same way if a pairing is added:
   canvas measureText("H").actualBoundingBoxAscent at a fixed size, divided
   into Bricolage's 66. */

[data-fonts="editorial"]{ --f-display:"Fraunces",Georgia,serif; --f-body:"Figtree",system-ui,sans-serif;
  --dw:600; --dt:-.022em; --dsc:0.94; --ds:1.05; }
[data-fonts="modern"]{ --f-display:"Bricolage Grotesque",system-ui,sans-serif; --f-body:"Work Sans",system-ui,sans-serif;
  --dw:700; --dt:-.032em; --dsc:1.0; --ds:1.0; }
[data-fonts="elegance"]{ --f-display:"Instrument Serif",Georgia,serif; --f-body:"Karla",system-ui,sans-serif;
  --dw:400; --dt:-.012em; --dsc:0.92; --ds:1.12; }
[data-fonts="playful"]{ --f-display:"Baloo 2",system-ui,cursive; --f-body:"Nunito",system-ui,sans-serif;
  --dw:700; --dt:-.012em; --dsc:1.07; --ds:1.06; }
[data-fonts="contrast"]{ --f-display:"Gloock",Georgia,serif; --f-body:"Manrope",system-ui,sans-serif;
  --dw:400; --dt:-.018em; --dsc:0.88; --ds:1.08; }
[data-fonts="grotesque"]{ --f-display:"Space Grotesk",system-ui,sans-serif; --f-body:"Inter",system-ui,sans-serif;
  --dw:600; --dt:-.026em; --dsc:0.94; --ds:1.02; }
/* ---------------------------------------------------------- shape style -- */
/* One switch that decides how the whole site feels, rather than fifty edits. */

[data-shape="soft"]{ --r-sm:8px; --r:14px; --r-lg:20px; --r-pill:999px;
  --shadow:0 12px 30px -18px rgba(15,20,28,.45); --shadow-lg:0 26px 60px -34px rgba(15,20,28,.55); }
[data-shape="sharp"]{ --r-sm:3px; --r:5px; --r-lg:7px; --r-pill:4px;
  --shadow:0 8px 22px -16px rgba(15,20,28,.40); --shadow-lg:0 18px 44px -30px rgba(15,20,28,.5); }
[data-shape="flat"]{ --r-sm:0px; --r:0px; --r-lg:0px; --r-pill:0px;
  --shadow:none; --shadow-lg:none; }
/* ================================================================= base == */

*{box-sizing:border-box}
html{scroll-behavior:smooth}
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{animation-duration:.001ms !important; transition-duration:.001ms !important}
}
body{
  margin:0; background:var(--bg); color:var(--ink);
  font-family:var(--f-body); font-size:17px; line-height:1.65;
  -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
}
/* `height:auto` is not decoration and must not be removed.
   Every <img> here carries width and height attributes so the browser can
   reserve the space before the file arrives — but those attributes also act as
   presentational hints for the CSS width and height properties. With BOTH
   dimensions set, `aspect-ratio` is ignored, which is why the square team
   portraits rendered 900px tall inside a 376px column. Setting height:auto in
   the base leaves one dimension free, so aspect-ratio works everywhere and the
   attributes still do their job. */
img,svg,video{max-width:100%; height:auto; display:block}
a{color:inherit}
button{font:inherit; cursor:pointer; border:none; background:none; color:inherit}
:focus-visible{outline:3px solid var(--acc); outline-offset:3px; border-radius:var(--r-sm)}
h1,h2,h3,h4,.disp,.hero-h{
  font-family:var(--f-display); font-weight:var(--dw);
  letter-spacing:var(--dt); line-height:var(--ds); text-wrap:balance; margin:0;
}
/* .hero-h is a carousel slide's headline. It looks like the h1 and is not
   one, because a page has exactly one h1 and the first slide has it. */
h1,.hero-h{font-size:calc(clamp(2.1rem,1.3rem + 3.2vw,3.9rem) * var(--dsc,1))}
/* A page whose title sits beside a picture rather than across the page —
   an event, where the words share the row and the date is doing some of
   the announcing. */
h1.beside{font-size:calc(clamp(1.9rem,1.2rem + 2.8vw,3.4rem) * var(--dsc,1))}

h2{font-size:calc(clamp(1.65rem,1.1rem + 2.1vw,2.6rem) * var(--dsc,1))}
h3{font-size:calc(clamp(1.2rem,1rem + .8vw,1.55rem) * var(--dsc,1))}
.shell{max-width:1240px; margin:0 auto; padding:0 26px}
.shell.narrow{max-width:820px}
@media (max-width:720px){ .shell{padding:0 18px} body{font-size:16px} }
/* Skip link — the first thing in the tab order, invisible until it matters.
   Keyboard users otherwise tab the whole header on every page. */
.skip{
  position:absolute; left:-9999px; top:0; z-index:200;
  background:var(--deep); color:#fff; padding:12px 18px; border-radius:0 0 var(--r) 0;
}
.skip:focus{left:0}
/* SENTENCE CASE, AND BARELY TRACKED. This was 12px, weight 700, uppercase and
   letter-spaced .16em, above almost every heading on the site. Two things were
   wrong with that. It is the commonest tell of a generated page, so it made
   eight different clients' sites read as the same template; and a label shouted
   in capitals competes with the heading it is meant to be introducing, which is
   the opposite of its job. It still marks itself as a label — brand colour, a
   step down in size, its own line — without announcing that it is one. */
.eyebrow{
  font-weight:600; font-size:.88rem; letter-spacing:.01em;
  color:var(--brand); display:block; margin-bottom:10px;
}
.lede{font-size:1.14rem; color:var(--muted); max-width:62ch; margin:0}
.hint{display:block; font-size:.82rem; color:var(--muted); font-weight:400; margin-top:3px}
section{padding:clamp(48px,6vw,92px) 0}
section.tight{padding:clamp(28px,3vw,44px) 0}
section.tint{background:var(--surf2)}
section.deep{background:var(--deep); color:#fff}
/* TWO BANDS OF THE SAME COLOUR ARE ONE BAND, AND WERE RENDERING WITH A HOLE.
   Each section carries padding top AND bottom so it holds its shape on its
   own. Where the next section is a different ground that is right — the two
   paddings buy the boundary its air. Where it is the SAME ground there is no
   boundary to give air to, and the two simply add up: measured at 173px of
   empty tint between the last word of one and the first of the next, with
   nothing to explain it.
   Collapsing the second one's top leaves a single section's worth of gap,
   which is the same rhythm as anywhere else inside a band.
   ONLY WHERE THE GROUND MATCHES. A tint after a dark band keeps both paddings,
   because there the eye is being shown a join.
   NOT DONE FOR TWO PLAIN SECTIONS, which measure identically — 173px — but
   have no band to have a hole in, so tightening them is a change of taste
   rather than a fix, and it would move every page on every site. */
section.tint + section.tint,
section.deep + section.deep{padding-top:0}
section.deep .lede,section.deep .hint{color:rgba(255,255,255,.72)}
section.deep .eyebrow{color:var(--acc)}
/* A GHOST BUTTON ON A DARK BAND HAD AN INVISIBLE LABEL.
   `.btn.ghost` draws its words in --ink, which is the text colour for a LIGHT
   ground. Measured against --deep it comes out at 1.00-1.02:1 on every one of
   the eleven palettes — not low contrast, the same colour. The 2px outline is
   --line and sits at 9-12:1, so what a visitor actually saw was an outlined
   box with nothing written in it.
   The hero has carried this rule since the beginning and a `deep` section
   never did, so the fault only appeared once a block could be given a dark
   band — a cta with two buttons showed one. Found on sureway, which had
   patched it in its own stylesheet; this is that fix brought home, so the site
   stops carrying a private copy through every release. */
section.deep .btn.ghost{color:#fff; box-shadow:inset 0 0 0 2px rgba(255,255,255,.5)}
section.deep .btn.ghost:hover{box-shadow:inset 0 0 0 2px #fff}
.section-head{max-width:64ch; margin-bottom:clamp(26px,3vw,42px)}
.section-head.center{margin-inline:auto; text-align:center}
/* ---------------------------------------------------------------- buttons */

.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:9px;
  padding:14px 26px; border-radius:var(--r-pill); font-weight:700; font-size:.96rem;
  text-decoration:none; background:var(--brand); color:var(--on-brand);
  transition:filter .18s ease, box-shadow .18s ease, background-color .3s ease;
  box-shadow:var(--shadow);
}
/* IT ANSWERS, IT DOES NOT JUMP. This lifted 2px and grew its shadow, which
   with the card lift below is the pair of gestures every generated page on the
   web is currently wearing. A button darkening under the pointer says the same
   thing — "this one" — without the page moving underneath somebody's hand.
   brightness() rather than a second colour token because it is one line that
   is correct on all eleven palettes and on whatever a client overrides them
   with. Not on the ghost, which has nothing to darken: it answers by taking
   the brand into its outline, which it already did. */
.btn:not(.ghost):hover{filter:brightness(.93)}
.btn.acc{background:var(--acc); color:var(--acc-ink)}
.btn.ghost{background:transparent; color:var(--ink); box-shadow:inset 0 0 0 2px var(--line)}
.btn.ghost:hover{box-shadow:inset 0 0 0 2px var(--brand)}
.btn.sm{padding:10px 18px; font-size:.86rem}
.btn.lg{padding:17px 34px; font-size:1.02rem}
.btn[disabled],.btn.is-disabled{opacity:.5; pointer-events:none}
.btn-row{display:flex; flex-wrap:wrap; gap:12px; margin:0}
.btn-row.center{justify-content:center}
/* ----------------------------------------------------------------- header */

.topline{
  background:var(--deep); color:rgba(255,255,255,.86); text-align:center;
  /* Sentence case, like the eyebrow before it. Small capitals tracked out at
     .18em is the hardest way to read eleven-and-a-half pixels of type, and it
     made the strap so wide that fitting it on a phone needed two media queries
     below — see the note there, which is now about a narrower problem. */
  font-size:12px; font-weight:600; letter-spacing:.01em;
  padding:9px 16px; display:flex; justify-content:center; align-items:center; gap:20px; flex-wrap:wrap;
}
.topline a{color:inherit; text-decoration:none; border-bottom:1px solid rgba(255,255,255,.35)}
.topline a:hover{border-bottom-color:currentColor}
/* The social links, where a site has asked for them there. No underline — they
   are marks rather than words, and a rule under an icon reads as a mistake. */
.topline a.topline-social{
  display:inline-flex; align-items:center; border-bottom:0; opacity:.75;
  transition:opacity .2s ease; padding:0 1px;
}
.topline a.topline-social:hover{opacity:1}
/* ONE ROW ON A PHONE. The strap used to be small capitals letter-spaced at
   .18em, which made it wide enough to wrap and take the telephone number onto a
   second line — two bands of dark before the logo even starts, on the screen
   with the least room to give.

   Sentence case removed most of that width: measured on the demonstration
   strap, these two steps now render 179px and 212px of content against 203px
   and 238px before. They are KEPT rather than deleted because the width they
   were protecting against is a client's strap, not this one, and a longer one
   will still need them.

   If a screen is still too narrow for both, it is the WORDS that give way, not
   the telephone number: the strap is a description somebody can read further
   down, the number is the thing they might act on. */
@media (max-width:700px){
  .topline{flex-wrap:nowrap; gap:10px; font-size:10px; letter-spacing:.05em; padding:7px 12px}
  .topline span{min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
  .topline a{flex:none; white-space:nowrap}
  /* The strip has room for the sentence and the telephone number and nothing
     else at this width — the footer carries the same links. */
  .topline a.topline-social{display:none}
}
/* One tighter step, for 360 and 320 wide handsets — still very much in use.
   Measured, not guessed: at 320 the budget is 292px and this step renders 179,
   so the demonstration strap has room to spare. It earned its place when the
   type was uppercase and the same strap came to 203px; it stays for the client
   who writes a longer one. */
@media (max-width:389px){
  .topline{gap:8px; font-size:9px; letter-spacing:.015em; padding:6px 10px}
}
header.site{
  position:sticky; top:0; z-index:60;
  background:color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter:saturate(1.4) blur(14px);
  border-bottom:1px solid var(--line);
}
.bar{display:flex; align-items:center; gap:22px; padding:12px 0}
.brandmark{display:flex; align-items:center; gap:12px; text-decoration:none; flex:none; min-width:0}
/* The height is a token so the client can set it — see --logo-h on <body>.
   The cap on the width scales with it, because the old fixed 260px was a
   wordmark that stopped growing halfway through the slider. */
.brandmark img{height:var(--logo-h,52px); width:auto; max-width:min(calc(var(--logo-h,52px) * 5), 42vw); object-fit:contain}
.brandmark .name{font-family:var(--f-display); font-weight:var(--dw); font-size:1.32rem; letter-spacing:var(--dt); white-space:nowrap}
.brandmark .tag{display:block; font-size:.72rem; font-weight:600; color:var(--muted); letter-spacing:.04em}
nav.main{display:flex; gap:3px; margin-left:auto; flex-wrap:wrap}
nav.main a{
  text-decoration:none; padding:9px 14px; border-radius:var(--r-pill);
  font-weight:600; font-size:.92rem; transition:background-color .2s ease;
}
nav.main a:hover{background:var(--surf2)}
nav.main a[aria-current]{background:var(--brand-soft); color:var(--brand)}
.hdr-cta{display:flex; align-items:center; gap:10px; flex:none}
/* The mobile menu is a <details>. It opens, closes, takes focus and answers
   the keyboard on its own, with no JavaScript — so the navigation cannot be
   the thing that breaks when a script fails to load. */
.menu{position:relative}
.menu summary{
  list-style:none; width:44px; height:44px; border-radius:var(--r-sm);
  display:none; flex-direction:column; justify-content:center; align-items:center; gap:5px;
  border:1px solid var(--line); cursor:pointer;
}
.menu summary::-webkit-details-marker{display:none}
.menu summary span{display:block; width:20px; height:2px; background:var(--ink); border-radius:2px}
.menu-panel{
  position:absolute; right:0; top:calc(100% + 10px); min-width:240px; z-index:70;
  background:var(--surf); border:1px solid var(--line); border-radius:var(--r);
  box-shadow:var(--shadow-lg); padding:10px;
}
.menu-panel a{display:block; padding:11px 14px; border-radius:var(--r-sm); text-decoration:none; font-weight:600}
.menu-panel a:hover{background:var(--surf2)}
@media (max-width:1024px){
  nav.main{display:none}
  .menu summary{display:flex}
  .hdr-cta{margin-left:auto}
}
@media (max-width:560px){
  .brandmark img{height:calc(var(--logo-h,52px) * .8); max-width:min(calc(var(--logo-h,52px) * 3.5), 50vw)}
  .brandmark .name{font-size:1.08rem}
  .hdr-cta .btn{padding:9px 14px; font-size:.82rem}
}
/* The map on the contact page. A picture from our own server, never an embed —
   see lib/map.ts for why that is a legal decision rather than a technical one. */
.map-card{margin-top:26px}
.map-card img{width:100%; height:auto; border-radius:var(--r-card, 14px); display:block}
.map-card .btn{margin-top:12px}

/* ------------------------------------------------------------------- hero */

.hero{position:relative; color:#fff; isolation:isolate; padding:clamp(74px,10vw,150px) 0}
.hero.plain{color:var(--ink); background:var(--surf2); padding:clamp(48px,6vw,90px) 0}
.hero-bg{position:absolute; inset:0; z-index:-2; overflow:hidden}
.hero-bg img{width:100%; height:100%; object-fit:cover}
/* Two gradients, and the direction is the point.
   The hero's words sit at the LEFT, vertically centred, so the strong half of
   the overlay has to be on the left — a `to top` gradient puts the dark end at
   the bottom, under nothing, and leaves the headline fighting whatever the
   photograph happens to be doing behind it. The second, gentler pass darkens
   the whole frame a little so the overlay holds even against a white sky. */
.hero::after{
  content:""; position:absolute; inset:0; z-index:-1;
  background:
    linear-gradient(to right, var(--hero-from), var(--hero-to) 72%),
    linear-gradient(to top, rgba(8,10,14,.34), rgba(8,10,14,.10));
}
/* On a phone the text is full width, so there is no "left" to shade — the
   overlay becomes vertical, heaviest where the words are. */
@media (max-width:760px){
  .hero::after{
    background:
      linear-gradient(to bottom, var(--hero-from), var(--hero-to) 92%),
      linear-gradient(to top, rgba(8,10,14,.30), rgba(8,10,14,.14));
  }
}
.hero.plain::after{display:none}
.hero h1,.hero .hero-h{max-width:18ch; text-shadow:0 2px 10px rgba(0,0,0,.30)}
.hero .lede{color:rgba(255,255,255,.92); margin-top:18px; font-size:1.2rem; max-width:46ch;
  text-shadow:0 1px 2px rgba(0,0,0,.28)}
.hero.plain .lede{color:var(--muted)}
.hero .btn-row{margin-top:30px}
/* The eyebrow is brand-coloured everywhere else, which is right on a page
   background and unreadable on a dark hero overlay. The plain hero has no
   overlay, so it keeps the brand colour. */
.hero .eyebrow{color:rgba(255,255,255,.82)}
.hero.plain .eyebrow{color:var(--brand)}
/* AND THE SAME FAULT MIRRORED, found while fixing the one above.
   `.hero .btn.ghost` paints its label #fff for the dark overlay a hero
   normally has — but a PLAIN hero has no picture and no overlay, and sits on
   --surf2, which is nearly white. Measured at 1.11:1. The plain hero already
   overrides .lede and .eyebrow for exactly this reason and the button was
   simply missed, so it is white on off-white: the same invisible label, from
   the opposite direction. Specificity does the work — (0,3,0) here against
   (0,2,1) for the rule it is correcting. */
.hero.plain .btn.ghost{color:var(--ink); box-shadow:inset 0 0 0 2px var(--line)}
.hero.plain .btn.ghost:hover{box-shadow:inset 0 0 0 2px var(--brand)}
.hero .btn.ghost{color:#fff; box-shadow:inset 0 0 0 2px rgba(255,255,255,.5); backdrop-filter:blur(3px)}
.hero .btn.ghost:hover{box-shadow:inset 0 0 0 2px #fff}
/* -------------------------------------------------- the hero as a carousel */
/* EVERY FRAME IN THE SAME GRID CELL. The section is then as tall as its
   tallest slide and stays that height as they change — absolute positioning
   would collapse it to nothing, and a caption two lines longer than the last
   would make the whole page jump every six seconds. */
.hero.has-carousel{padding:0; display:grid; overflow:hidden}
.hero.has-carousel > .hero-slide{
  grid-area:1/1; position:relative; padding:clamp(74px,10vw,150px) 0;
  opacity:0; transition:opacity .8s ease; pointer-events:none;
}
.hero.has-carousel > .hero-slide.is-on{opacity:1; pointer-events:auto}
/* The overlay moves from the section onto each frame: a frame that fades makes
   its own stacking context, so the section's ::after would end up on the wrong
   side of the picture it is meant to be darkening. */
.hero.has-carousel::after{display:none}
.hero-slide .hero-bg{position:absolute; inset:0; z-index:0; overflow:hidden}
.hero-slide::after{
  content:""; position:absolute; inset:0; z-index:1;
  background:
    linear-gradient(to right, var(--hero-from), var(--hero-to) 72%),
    linear-gradient(to top, rgba(8,10,14,.34), rgba(8,10,14,.10));
}
@media (max-width:760px){
  .hero-slide::after{
    background:
      linear-gradient(to bottom, var(--hero-from), var(--hero-to) 92%),
      linear-gradient(to top, rgba(8,10,14,.30), rgba(8,10,14,.14));
  }
}
.hero-slide > .shell{position:relative; z-index:2}

/* The controls, sitting on the picture. Hidden with JavaScript off, where the
   first slide is the whole hero and there is nothing to step through. */
.hero-nav{
  grid-area:1/1; align-self:end; justify-self:center; position:relative; z-index:3;
  display:flex; align-items:center; gap:10px; margin-bottom:22px;
  padding:5px 7px; border-radius:var(--r-pill);
  background:rgba(8,10,14,.30); backdrop-filter:blur(6px);
}
html:not(.js) .hero-nav{display:none}
.hero-arrow{
  display:flex; align-items:center; justify-content:center; width:34px; height:34px;
  padding:0; border:0; border-radius:50%; background:none; color:#fff; cursor:pointer;
}
.hero-arrow:hover{background:rgba(255,255,255,.20)}
.hero-dots{display:flex; align-items:center; gap:8px}
.hero-dot{
  width:9px; height:9px; padding:0; border:0; border-radius:50%; cursor:pointer;
  background:rgba(255,255,255,.45); transition:background .2s ease, transform .2s ease;
}
.hero-dot[aria-current]{background:#fff; transform:scale(1.3)}
.hero-arrow:focus-visible,.hero-dot:focus-visible{outline:2px solid #fff; outline-offset:3px}
/* Somebody who has asked their system for less motion gets the pictures, not
   the movement: the script does not advance and the change is not animated. */
@media (prefers-reduced-motion: reduce){
  .hero.has-carousel > .hero-slide{transition:none}
}

/* ------------------------------------------------------------------ cards */

.grid{display:grid; gap:clamp(18px,2.2vw,30px)}
.grid.two{grid-template-columns:repeat(auto-fit,minmax(300px,1fr))}
.grid.three{grid-template-columns:repeat(auto-fit,minmax(268px,1fr))}
.grid.four{grid-template-columns:repeat(auto-fit,minmax(210px,1fr))}
/* THE `cards` GRID HAD NO COLUMNS AT ALL. `.grid` sets display and a gap and
   nothing else, and there has never been a `.grid.cards` rule — so every list
   that asked for one has been a single column of full-width cards, each with a
   four-by-three photograph across the top of it. On a 1240 shell that is an
   800-pixel picture of a town above two lines of text.

   Two per row, and the card lies down: the picture takes a third at the left
   and the words sit beside it. Below 640 it stands back up, because a 200-pixel
   picture next to three words is worse than either. */
.grid.cards{grid-template-columns:repeat(auto-fit,minmax(430px,1fr))}
.grid.cards .card{flex-direction:row; align-items:stretch}
.grid.cards .card-media{flex:0 0 36%; aspect-ratio:auto; align-self:stretch}
.grid.cards .card-media img{height:100%; min-height:170px}
.grid.cards .card-body{flex:1 1 auto; min-width:0}
@media (max-width:640px){
  .grid.cards .card{flex-direction:column}
  .grid.cards .card-media{flex:none; aspect-ratio:16/9}
  .grid.cards .card-media img{min-height:0}
}
.card{
  background:var(--surf); border:1px solid var(--line); border-radius:var(--r-lg);
  overflow:hidden; display:flex; flex-direction:column;
  transition:border-color .2s ease;
}
a.card{text-decoration:none}
/* A grid whose cards are square-cornered whatever shape the theme is in.
   The diary asked for it: a row of dated boxes reads as a listing rather
   than as a set of soft tiles. */
.grid.square .card, .grid.square .card-media{border-radius:0}

/* THE ONE YOU CAN ACTUALLY BUY. A row of dates all looking the same is a row
   somebody has to read through to find the one that is selling; the border and
   the badge do that reading for them. */
/* A DATE SOMEBODY CAN ACTUALLY BUY FOR, and the whole row is arranged so it is
   the one you see first: the brand outline, and the body tinted rather than
   white. The tint is --brand-soft, not a pink typed in here, so it follows the
   palette — this site is pink, a client's might not be, and a hard-coded pink
   would be wrong on the first theme change nobody thought to check. */
.card.lit{border-color:var(--brand); box-shadow:0 0 0 2px var(--brand) inset}
.card.lit .card-body{background:var(--brand-soft)}
/* The badge already has a style of its own further down; on a lit card it
   fills in rather than sitting quietly, because it is the reason it is lit. */
/* WHITE, OUTLINED, BRAND TEXT — the same badge wherever it appears, on the
   picture or in the body of a card with none. Outlined rather than filled: on
   a busy photograph a solid block of brand competes with the image, where a
   white chip reads as a label applied to it. The shadow is what keeps it
   legible over a pale patch. */
/* The same object as the hero's pill: white, brand text, a live dot and the
   same slow ring. They say the same thing in two places, so they should not
   look like two different things. */
.card.lit .badge,
.card-badge .badge{background:var(--surf); color:var(--brand)}
.badge-live{
  display:inline-flex; align-items:center; gap:8px;
  box-shadow:0 4px 18px rgba(0,0,0,.28);
  animation:live-pill 2.6s ease-in-out infinite;
}
.badge-live .live-dot{width:8px; height:8px; flex:0 0 8px}

/* IT BREATHES RATHER THAN BLINKS. A pulse that flashes is an advert and gets
   ignored; a slow swell reads as "this one is live" and survives being looked
   at for a whole minute. Opacity and scale only — both composited, so a page
   of twelve cards costs nothing to animate.

   IT STOPS ENTIRELY FOR ANYONE WHO HAS ASKED FOR LESS MOTION. There is a
   global reduced-motion rule near the top of this file that cuts every
   duration to nothing, and that would leave this mid-swell at whatever opacity
   it happened to hold — so the animation is turned off by name here instead,
   and the badge simply sits still. */
@keyframes badge-pulse{
  0%,100%{ transform:scale(1); box-shadow:0 0 0 0 rgba(0,0,0,0) }
  50%{ transform:scale(1.045); box-shadow:0 0 0 6px color-mix(in srgb, var(--brand) 22%, transparent) }
}
/* THE PULSE IS OPT-IN NOW, not automatic on every lit card. A badge that moves
   means "this one is live"; spending that on every label a card can carry is
   how the signal stops being one. `badge-live` above is how a card asks for it,
   and components.ts puts it there only on a highlighted card. The keyframes
   stay for anything still asking by name. */
@media (prefers-reduced-motion:reduce){
  .card.lit .badge,.card-badge .badge,.badge-live,.badge-live .live-dot{animation:none}
}
/* THE BORDER AND THE TITLE, NOT THE WHOLE CARD.
   Twelve cards each rising 4px and dropping a bigger shadow is the single most
   recognisable thing about a page nobody designed — and it says only "these
   are cards", which the reader already knew. What is actually useful is WHICH
   ONE is under the pointer and WHERE it goes, so the border takes the brand
   and the title underlines itself: the title is the link, so the title is what
   should look like one.
   A `lit` card is already brand-bordered, so only its title answers — correct,
   because the border there is carrying a different message and should not
   flicker between two jobs. */
a.card:hover{border-color:var(--brand)}
a.card:hover h3{text-decoration:underline; text-underline-offset:3px}
/* MIN-HEIGHT:0 OR THE PICTURE DECIDES THE SHAPE, and two browsers disagree
   about which. This is a flex item of .card, so its min-height:auto resolves
   to the image's own min-content height — and Chrome lets a square image push
   this box to square while Safari honours the aspect-ratio and keeps 4:3. The
   same page therefore cropped on a phone and did not on a laptop, which is
   why a crop reported from a phone could not be reproduced. 4:3 is what this
   line asks for, so 4:3 is what both should give. */
.card-media{aspect-ratio:4/3; background:var(--surf2); position:relative; min-height:0}
.card-media img{width:100%; height:100%; object-fit:cover}
/* Across the bottom of the picture, centred, with enough of a scrim behind it
   that white type survives a pale photograph. It is the reason to stop
   scrolling, so it goes where the eye already is. */
.card-badge{
  position:absolute; left:0; right:0; bottom:0; display:flex; justify-content:center;
  padding:10px 12px 12px; pointer-events:none;
  background:linear-gradient(to top, rgba(12,10,14,.72), rgba(12,10,14,0));
}
.card-body{padding:22px; display:flex; flex-direction:column; gap:9px; flex:1}
.card-body h3{margin:0}
.card-body p{margin:0; color:var(--muted); font-size:.95rem}
/* The same element as .eyebrow, in a card — it is rendered from the same
   `eyebrow` field. It follows it into sentence case, because a date set in
   capitals above a card and in ordinary type above a heading is the same date
   dressed two ways on one page. */
.card-meta{font-size:.84rem; color:var(--muted); font-weight:600; letter-spacing:.01em}
.card-foot{margin-top:auto; padding-top:12px; font-weight:700; color:var(--brand)}
.price{font-family:var(--f-display); font-size:1.3rem; font-weight:var(--dw)}
.price s{color:var(--muted); font-size:.9rem; font-weight:400; margin-left:8px}
.badge{
  /* Centred, and hugging its own text rather than stretching the width of the
     card body — a flex child stretches by default, which is how a short pill
     came to be a full-width strip with the words shoved to the left. */
  display:inline-block; align-self:center; text-align:center;
  padding:4px 11px; border-radius:var(--r-pill);
  font-size:.74rem; font-weight:700; letter-spacing:.05em; text-transform:uppercase;
  background:var(--brand-soft); color:var(--brand);
}
.badge.acc{background:var(--acc); color:var(--acc-ink)}
.badge.quiet{background:var(--surf2); color:var(--muted)}
/* ------------------------------------------------------ text and prose --- */

.prose{max-width:68ch}
.prose > * + *{margin-top:1.05em}
.prose h2{margin-top:1.8em}
.prose h3{margin-top:1.5em}
.prose p,.prose li{color:var(--ink)}
.prose a{color:var(--brand); text-underline-offset:3px}
.prose ul,.prose ol{padding-left:1.3em}
.prose li + li{margin-top:.4em}
.prose blockquote{
  margin:1.6em 0; padding:4px 0 4px 22px; border-left:3px solid var(--brand);
  font-family:var(--f-display); font-size:1.22rem; line-height:1.45;
}
.prose img{border-radius:var(--r); margin-block:1.6em}
.prose.centred{margin-inline:auto; text-align:center}
/* --------------------------------------------------- media and galleries - */

figure{margin:0}
.feature-media{border-radius:var(--r-lg); overflow:hidden; background:var(--surf2)}
.feature-media img{width:100%; height:100%; object-fit:cover; aspect-ratio:var(--ratio,16/9)}
/* Crop from the top rather than the middle. A photograph of a show usually
   carries the organiser's banner along the top of the hall, and a centred
   crop cuts it in half — which is the one part of the picture that says
   whose show it is. */
.feature-media.top img{object-position:top}

figcaption{font-size:.85rem; color:var(--muted); margin-top:10px}
.split{display:grid; grid-template-columns:1fr 1fr; gap:clamp(24px,4vw,60px); align-items:center}
/* What it costs to get in, one line per way in. The name on the left, the
   price hard right, so the column of prices reads down as a price list. */
.prices{list-style:none; margin:0; padding:0; display:grid; gap:6px}
.prices li{display:flex; justify-content:space-between; gap:16px; align-items:baseline}
.prices .hint{font-weight:400}
/* The picture gives up a little of its half to the words beside it. */
.split.media-narrow{grid-template-columns:.82fr 1fr}

.split.flip > figure{order:2}
/* THE VARIANT HAS TO BE NAMED HERE OR IT WINS. `.split.media-narrow` is two
   classes and this collapse was one, so on a phone the narrow variant kept its
   two columns: a squashed picture and a column of text beside it, with the
   detail table running off the side of the screen. Specificity, not the
   breakpoint, and it looked like a layout that had simply not been thought
   about. */
@media (max-width:860px){
  .split, .split.media-narrow{grid-template-columns:1fr}
  .split.flip > figure{order:0}
}
.gallery-grid{display:grid; grid-template-columns:repeat(auto-fill,minmax(200px,1fr)); gap:12px}
/* Stretched across the row rather than left-aligned in fixed tracks. A gallery
   page has thirty pictures and fills its rows anyway; a show has four, and
   four tiles hugging the left of a wide page look like a mistake. */
.gallery-grid.fill{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}

.gallery-grid a{display:block; aspect-ratio:1; border-radius:var(--r); overflow:hidden; background:var(--surf2)}
.gallery-grid img{width:100%; height:100%; object-fit:cover; transition:transform .3s ease}
.gallery-grid a:hover img{transform:scale(1.05)}
/* ------------------------------------------------------------------ forms */

.form{display:flex; flex-direction:column; gap:18px; max-width:620px}
.form .row{display:grid; grid-template-columns:1fr 1fr; gap:18px}
@media (max-width:640px){ .form .row{grid-template-columns:1fr} }
.field{display:flex; flex-direction:column; gap:6px}
.field label{font-weight:700; font-size:.9rem}
input[type=text],input[type=email],input[type=tel],input[type=url],input[type=password],
input[type=number],input[type=date],input[type=datetime-local],input[type=search],select,textarea{
  width:100%; padding:13px 15px; font:inherit; font-size:.98rem;
  border:1px solid var(--line); border-radius:var(--r-sm);
  background:var(--surf); color:var(--ink);
}
textarea{resize:vertical; min-height:120px}
input:focus,select:focus,textarea:focus{border-color:var(--brand); outline:none; box-shadow:0 0 0 3px var(--brand-soft)}
.check{display:flex; align-items:flex-start; gap:10px; font-weight:600; font-size:.92rem}
.check input{width:auto; margin-top:3px}
.notice{
  padding:14px 18px; border-radius:var(--r); font-size:.92rem;
  background:var(--brand-soft); border:1px solid var(--line);
}
.notice.warn{background:#FFF4E0; border-color:#EBD5A8; color:#5B4210}
.notice.bad{background:#FDE9E7; border-color:#F0C4BF; color:#7A2418}
.notice.good{background:#E6F4EA; border-color:#BFE0CA; color:#1D5030}
/* ---------------------------------------------------------------- tables */

.table{width:100%; border-collapse:collapse; font-size:.94rem}
.table th,.table td{text-align:left; padding:12px 14px; border-bottom:1px solid var(--line)}
.table th{font-size:.78rem; text-transform:uppercase; letter-spacing:.06em; color:var(--muted)}
.table-wrap{overflow-x:auto}
/* -------------------------------------------------------------------- FAQ */

.faq{border-top:1px solid var(--line); max-width:76ch}
.faq details{border-bottom:1px solid var(--line)}
.faq summary{
  list-style:none; cursor:pointer; padding:20px 40px 20px 0; position:relative;
  font-family:var(--f-display); font-weight:var(--dw); font-size:1.1rem;
}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{
  content:"+"; position:absolute; right:6px; top:50%; transform:translateY(-50%);
  font-size:1.5rem; color:var(--brand); line-height:1;
}
.faq details[open] summary::after{content:"–"}
.faq .answer{padding:0 0 22px; color:var(--muted); max-width:68ch}
/* ------------------------------------------------------------- reviews -- */

.stars{display:inline-flex; gap:2px; color:var(--acc)}
.review{background:var(--surf); border:1px solid var(--line); border-radius:var(--r-lg); padding:26px}
.review blockquote{margin:0 0 14px; font-family:var(--f-display); font-size:1.12rem; line-height:1.5}
.review cite{font-style:normal; font-weight:700; font-size:.9rem}
.review .src{display:block; font-size:.8rem; color:var(--muted); font-weight:400}
/* ------------------------------------------------------------------ people */

.person{text-align:center}
/* A trader with no photograph yet. Holds the same square so the grid does not
   go ragged around the ones that have one. */
.person-blank{aspect-ratio:1; background:var(--surf2); border-radius:var(--r-lg)}

.person img{width:100%; aspect-ratio:1; object-fit:cover; border-radius:var(--r-lg); background:var(--surf2)}
.person h3{margin-top:14px; font-size:1.1rem}
.person .role{color:var(--brand); font-weight:700; font-size:.86rem}
/* A picture opened over the page. Sized to the viewport rather than the file,
   so a 4000px photograph does not need scrolling to see. */
dialog.zoom{border:0; padding:0; background:transparent; max-width:96vw; max-height:96vh; overflow:visible}
dialog.zoom::backdrop{background:rgba(20,12,18,.82)}
dialog.zoom img{display:block; max-width:96vw; max-height:92vh; width:auto; height:auto;
  border-radius:var(--r); background:var(--surf2)}
dialog.zoom form{margin:0}
dialog.zoom button{position:absolute; top:-14px; right:-14px; width:40px; height:40px;
  border:0; border-radius:50%; background:var(--surf); color:var(--ink);
  font-size:1.4rem; line-height:1; cursor:pointer; box-shadow:var(--shadow-lg)}

/* The people at one event, under everything else about it. */
.people-here{border-top:1px solid var(--line); margin-top:56px; padding-top:8px}

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

footer.site{background:var(--deep); color:rgba(255,255,255,.78); padding:clamp(48px,6vw,80px) 0 30px; margin-top:0}
/* Sentence case, same reasoning as .eyebrow and .topline. A footer column
   heading is a label, and a label shouted is still a label. */
/* The footer's name can be a sentence rather than a name — see footerName —
   so it wraps there, where the header's cannot. */
footer.site .brandmark .name{white-space:normal; font-size:1.12rem; line-height:1.25; max-width:22ch}
footer.site h4{color:#fff; font-size:.88rem; letter-spacing:.01em; margin:0 0 14px; font-family:var(--f-body); font-weight:700}
/* THE FOOTER "GRID" HAS NEVER BEEN A GRID.
   
   It asked for `1.6fr repeat(auto-fit,minmax(150px,1fr))`, and that is invalid
   CSS: a track list containing an auto-fill or auto-fit repetition may not
   contain a flexible track anywhere else in the list. Every browser throws the
   whole declaration away, leaving grid-template-columns at its initial value —
   so the footer has been one stacked column on every site built on this
   framework since the day it was written. Checked rather than assumed: the
   declaration is REJECTED when set on an element and read back, and so is
   every variation of it with a fourth track added.
   
   Flexbox does what the grid was asking for and is valid. The brand block
   takes the wide share, the link columns take equal shares of what is left,
   and the credit is pushed to the end by its own auto margin — which is also
   what puts it on the same top line as the mark, instead of on a row of its
   own underneath. */
.f-grid{display:flex; flex-wrap:wrap; align-items:flex-start; gap:clamp(26px,4vw,54px)}
.f-grid > div{flex:1 1 150px; min-width:0}
.f-grid > div:first-child{flex:1.6 1 300px}
.f-grid a{display:block; color:inherit; text-decoration:none; padding:5px 0; font-size:.92rem}
.f-grid a:hover{color:#fff}
.f-addr{font-size:.92rem; max-width:34ch; margin:14px 0 0; line-height:1.7}
.f-social{display:flex; gap:12px; margin-top:20px}
.f-social a{
  width:38px; height:38px; border-radius:var(--r-pill); display:grid; place-items:center;
  background:rgba(255,255,255,.1); transition:background-color .2s ease;
}
.f-social a:hover{background:rgba(255,255,255,.22)}
.f-bot{
  margin-top:40px; padding-top:22px; border-top:1px solid rgba(255,255,255,.14);
  display:flex; flex-wrap:wrap; gap:10px 26px; justify-content:space-between;
  font-size:.8rem; color:rgba(255,255,255,.56);
}
/* The house credit. The R takes the page's own accent so the mark belongs to
   the site rather than sitting on it; the 4 stays Red 4 red, which is the half
   that makes it ours. */
/* The credit goes to the far right, out of the way of the client's own
   columns: it is ours, it belongs at the edge, and left-aligned under the last
   column it read as though it were part of their footer.

   THE LINK'S OWN MARGINS ARE LEFT ALONE. r4-credit.js — the script on
   red4digital.co.uk that turns the mark into the animated canvas — writes
   `margin-left:auto; margin-right:auto` inline on the link and then positions
   the canvas itself. Overriding those margins does move the link, and it also
   pulls the canvas out from under the label: the mark ends up beside its own
   caption rather than under it. So the LINK is left exactly as the script
   wants it, and the paragraph around it is what moves to the right — it is
   shrink-to-fit, so the link's auto margins have nothing left to centre it
   within and the whole thing lands at the edge with its own layout intact. */
.f-by{margin:0 0 0 auto; display:flex; justify-content:flex-end; align-self:flex-start; flex:0 0 auto}
/* THE LABEL GOES ABOVE THE MARK, and that has to be said rather than left to
   luck. Nothing in this stylesheet styles .built-by — it is a block box whose
   label and mark are inline, so whether the mark sits UNDER the words or
   BESIDE them depends entirely on how much room the box happens to have. It
   was stacked while the footer was one wide column and the pair could not fit
   on a line; the moment the credit became a shrink-to-fit item its box was
   exactly wide enough for both, and the mark hopped up beside its own caption.
   A column flex says what is meant and does not depend on arithmetic. */
.f-by .built-by{display:inline-flex; flex-direction:column; align-items:center}
@media (max-width:560px){
  /* Back to the left on a phone, where everything else in the footer starts
     there and a single right-aligned item looks like a mistake — and back to
     some space above it, since it is no longer beside anything. */
  .f-by{justify-content:flex-start; margin:10px 0 0}
}
.built-by:hover{color:#fff}
/* ------------------------------------------------------ the one moment --- */
/* THE PAGE HAS ONE PIECE OF MOTION IT DID NOT HAVE TO EARN, AND THIS IS IT.

   It used to have seventeen. Every section head, card, review, portrait,
   figure, quote and button row faded up as it scrolled into view, which is the
   effect every generated page on the web is currently wearing — so it said
   nothing about the site it was on, and a visitor scrolling a long page watched
   the same 600ms nudge forty times. One arrival, at the top, where somebody is
   actually looking, does the job the other sixteen were diluting.

   NO JAVASCRIPT BEYOND THE `js` CLASS THE HEAD ALREADY SETS. The old reveal
   needed an IntersectionObserver, and an observer is a thing that can fail with
   every element left at opacity 0. This cannot fail into a blank hero: with the
   class absent the rule never applies and the words are simply there.

   OFF BY NAME FOR REDUCED MOTION, not by the global duration cut near the top
   of this file. That cut leaves the delays in place, which would hold the
   button row invisible for its 280ms and then snap it in — worse than the
   animation it was meant to remove. Same reason the badge pulse is named.

   THE CAROUSEL IS LEFT ALONE. A hero that crossfades between pictures already
   has a moment of its own, and two moments in one place is a fight. */
@keyframes hero-in{
  from{ opacity:0; transform:translateY(12px) }
  to{ opacity:1; transform:none }
}
.js .hero:not(.has-carousel) > .shell > *{
  animation:hero-in .55s cubic-bezier(.22,.61,.36,1) both;
}
.js .hero:not(.has-carousel) > .shell > .eyebrow{animation-delay:.04s}
.js .hero:not(.has-carousel) > .shell > h1{animation-delay:.08s}
.js .hero:not(.has-carousel) > .shell > .lede{animation-delay:.18s}
.js .hero:not(.has-carousel) > .shell > .btn-row{animation-delay:.28s}
@media (prefers-reduced-motion:reduce){
  .js .hero:not(.has-carousel) > .shell > *{animation:none}
}
/* -------------------------------------------------------- print ---------- */
@media print{
  header.site,footer.site,.topline,.btn,.menu{display:none}
  body{font-size:12pt; background:#fff; color:#000}
  .shell{max-width:none; padding:0}
}
/* -------------------------------------------------------- breadcrumbs --- */
/* Added after the palettes deliberately: theme.ts parses this file for
   [data-palette] blocks, and keeping ordinary rules below them keeps that
   parse obviously correct. */
.crumbs ol{list-style:none; display:flex; flex-wrap:wrap; gap:8px; margin:0; padding:0; font-size:.85rem; color:var(--muted)}
.crumbs li + li::before{content:"›"; margin-right:8px; opacity:.6}
.crumbs a{color:inherit; text-decoration:none; border-bottom:1px solid transparent}
.crumbs a:hover{border-bottom-color:currentColor}
.crumbs [aria-current]{color:var(--ink); font-weight:600}

/* ------------------------------------------------------- venue detail -- */
/* A definition list rather than paragraphs: "where is it, how do I get
   there, what does parking cost" is a set of questions with answers, and
   marking it up as one is what lets a screen reader jump between them. */
.venue-detail{margin:32px 0 0; display:grid; gap:0}
.venue-detail .vrow{display:grid; grid-template-columns:180px 1fr; gap:24px;
  padding:18px 0; border-top:1px solid var(--line)}
.venue-detail .vrow:last-child{border-bottom:1px solid var(--line)}
.venue-detail dt{font-weight:600; color:var(--muted); font-size:.9rem;
  letter-spacing:.02em; text-transform:uppercase}
.venue-detail dd{margin:0}
.venue-detail dd > :first-child{margin-top:0}
.venue-detail dd > :last-child{margin-bottom:0}
@media (max-width:640px){
  .venue-detail .vrow{grid-template-columns:1fr; gap:6px}
}

/* WHAT IT IS, AND WHERE IT IS, SIDE BY SIDE.
   The description keeps the reading width it had; the venue sits beside it as
   a box rather than under a heading three screens down, because "where is it
   and can I park" is asked at the same moment as "what is it". Below 900px
   the box goes under the words, which is the order somebody reads them in on
   a phone anyway. */
.with-side{display:grid; grid-template-columns:minmax(0,1.65fr) minmax(260px,.85fr);
  gap:clamp(24px,3.5vw,52px); align-items:start; margin-top:44px}
@media (max-width:900px){ .with-side{grid-template-columns:1fr} }

.side-box{background:var(--surf); border:1px solid var(--line); border-radius:var(--r-lg);
  padding:clamp(20px,2.4vw,28px); position:sticky; top:24px}
.side-box > h2{font-size:1.15rem; margin:0}
/* The rows are the venue page's, without the space a page heading wants. */
.side-box .venue-detail{margin-top:14px}
.side-box .venue-detail .vrow{grid-template-columns:1fr; gap:4px; padding:12px 0}
.side-box .venue-detail dt{font-size:.78rem}
.side-box .venue-detail dd{font-size:.95rem}
.side-map{margin:16px 0 0}
.side-map img{width:100%; height:auto; border-radius:var(--r); display:block}
.side-box .btn-row{margin-top:18px; display:flex; gap:8px; flex-wrap:wrap}
@media (max-width:900px){ .side-box{position:static} }

/* The house credit. Only the size and position are decided here — the artwork,
   its animation, the word beneath it and that word's colour are all served from
   red4digital, so one change there reaches every site we have built. */
.built-by {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  text-decoration: none;
  --r4-red: #b91e24;
}
.r4-mark-slot {
  width: var(--r4-mark-h, 76px); height: var(--r4-mark-h, 76px);
  display: block; flex: none;
}
.r4-mark-slot > svg, .r4-mark-slot > span { width: 100%; height: 100%; display: block; }
.r4-mark { height: 100%; width: 100%; display: block; }
.r4-px rect { fill: var(--r4-red); transition: fill 0.2s ease; }
.built-by:hover .r4-px rect { fill: currentColor; }
/* Brand red is drawn for white paper and goes muddy on a dark footer. */
.site-footer .built-by, .footer .built-by { --r4-red: #d2373d; }

/* The credit's wording and its name.
   The label is the site's own footerCredit setting, not a literal — we design
   some sites we do not host, and a stamp claiming otherwise is a lie printed on
   every page. The name shows only on hover, positioned absolutely so revealing
   it cannot nudge the footer about.

   NOTHING HERE UNDERLINES, and it takes this many selectors to say so. The
   footer's own `.site-footer a:hover { text-decoration: underline }` scores
   (0,2,1) and beat a plain `.built-by:hover` at (0,2,0) — so the credit was
   underlined on hover despite a rule that read as though it had settled it.
   Adding the wrapper class takes these to (0,3,0), which wins on merit rather
   than on !important. */
.built-by, .built-by:hover,
.built-by span, .built-by:hover span,
.site-footer .built-by, .site-footer .built-by:hover,
.site-footer .built-by span, .site-footer .built-by:hover span,
.f-by .built-by, .f-by .built-by:hover,
.f-by .built-by span, .f-by .built-by:hover span,
.footer .built-by, .footer .built-by:hover { text-decoration: none; }

.built-by-label {
  font-size: 0.52rem; letter-spacing: 0.13em; text-transform: uppercase;
  line-height: 1; margin-bottom: 7px; white-space: nowrap;
  color: var(--r4-credit-label, #c9a227);
}
.built-by-name {
  position: absolute; left: 50%; bottom: -0.85rem; transform: translate(-50%, 2px);
  font-size: 0.54rem; letter-spacing: 0.11em; text-transform: uppercase;
  line-height: 1; white-space: nowrap; opacity: 0; pointer-events: none;
  transition: opacity 0.18s ease, transform 0.18s ease;
}
.built-by:hover .built-by-name, .built-by:focus-visible .built-by-name {
  opacity: 1; transform: translate(-50%, 0);
}
@media (prefers-reduced-motion: reduce) { .built-by-name { transition: none; } }

/* ---------------------------------------------------------------- tickets --
   The ticket face. Designed for the two places it is actually read: a phone
   held up in a queue, and a sheet of paper folded into quarters. High contrast
   on white in both cases, which is why this ignores the palette — a scanner
   reading a QR through a dark theme at low brightness is a queue that stops.  */

.ticket-face {
  background: #fff; color: #111;
  border: 1px solid #d9d9d9; border-radius: 10px;
  padding: 1.5rem 1.25rem; margin: 1.25rem 0;
  text-align: center; max-width: 22rem;
}
.ticket-face p { margin: 0.35rem 0; }
.ticket-qr { display: block; width: 100%; max-width: 15rem; height: auto; margin: 0 auto 1rem; }
.ticket-number {
  font-size: 1.6rem; font-weight: 700; letter-spacing: 0.02em; line-height: 1;
}
.ticket-face .hint { color: #555; font-size: 0.85rem; margin-top: 0.75rem; }

@media print {
  /* Paper gets the ticket and nothing else. */
  .site-header, .site-footer, .breadcrumbs, .section-head .eyebrow { display: none !important; }
  .ticket-face { border-color: #000; page-break-inside: avoid; }
}

/* ------------------------------------------------------------- ticketing --
   A show's pictures and its venue. `height: auto` on images is load-bearing
   here as everywhere in this stylesheet — see the base rule.                */

.show-hero { margin: 0 0 1.5rem; border-radius: 10px; overflow: hidden; }
.show-gallery {
  display: grid; gap: 0.75rem;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 14rem), 1fr));
}
.show-gallery img { border-radius: 8px; width: 100%; height: auto; display: block; }

.venue-block { display: grid; gap: 1.25rem; }
@media (min-width: 44rem) { .venue-block { grid-template-columns: 3fr 2fr; align-items: start; } }
.venue-map { display: block; margin: 0 0 1.25rem; border-radius: 10px; overflow: hidden; }
.venue-map img { width: 100%; height: auto; display: block; }
.venue-words p { margin: 0 0 0.6rem; }

/* The picture strip in the admin's multi-picture field. */
.media-strip { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0 0 0.5rem; }
.chosen-one { position: relative; display: inline-block; line-height: 0; }
.chosen-one img { border-radius: 6px; object-fit: cover; }
.chosen-one .btn { position: absolute; top: -6px; right: -6px; padding: 0 0.35rem; line-height: 1.4; }
.venue-photo { margin: 0 0 0.9rem; border-radius: 8px; overflow: hidden; }
.venue-photo img { width: 100%; height: auto; display: block; }

/* Who is coming — one block per ticket. */
.attendee { border: 1px solid var(--line, #ddd); border-radius: 8px; padding: 0.85rem 1rem; margin: 0 0 0.85rem; }
.attendee legend { font-size: 0.85rem; font-weight: 600; padding: 0 0.35rem; }

/* A QR code shown in the middle of a sentence, on a confirmation or a wallet
   page. */
.in-prose{
  /* WIDTH SET EXPLICITLY. An SVG carries its own width and height attributes,
     and those are presentational hints for the CSS properties as well — with
     `height:auto` freeing one of them the thing collapsed to the size of its
     viewBox, and a 300px QR square rendered at 22px. Saying the width outright
     is the only version that does not depend on what the file happens to
     declare. Left, not centred: it sits beside the sentence describing it. */
  display:block; width:200px; max-width:100%; height:auto;
  margin:20px 0 24px;
  border:1px solid var(--line); border-radius:var(--r); background:#fff; padding:10px}

/* PAYING, ON OUR OWN PAGE. The total is the largest thing in the box because
   it is the thing being agreed to; the buttons are PayPal's own and are left
   alone, because a buyer recognises them and that recognition is the point. */
.pay-box{background:var(--surf); border:1px solid var(--line); border-radius:var(--r-lg);
  padding:clamp(20px,3vw,30px); margin:26px 0; max-width:460px}
.pay-box .co-lines{margin:0 0 4px}
.pay-box .co-total{margin-bottom:20px}
.pay-box .paymarks{margin-top:14px}
.pay-total{font-family:var(--f-display); font-size:2.1rem; font-weight:var(--dw); margin:0 0 18px}
.pay-total span{font-family:var(--f-body); font-size:.95rem; font-weight:400; color:var(--muted); margin-left:8px}
#paypal-buttons{min-height:52px}
#pay-status:empty{display:none}

/* ============================================================== checkout ==
   THE ONE PAGE WHERE LOOKING UNFAMILIAR COSTS MONEY.

   These buyers have bought this ticket from a Shopify store for years. The
   layout is deliberately the one they know — what you are buying pinned on the
   right, what you have to fill in on the left, a total that answers as you
   choose — because every difference from it is a moment of doubt at the exact
   point where doubt ends the sale.

   It degrades to one column and a form that works. Nothing below is required
   for the page to function; it is required for the page to be believed. */

.checkout{display:grid; grid-template-columns:minmax(0,1fr) 360px; gap:clamp(28px,4vw,56px);
  align-items:start; margin:0 0 10px}
@media (max-width:900px){ .checkout{grid-template-columns:1fr} }

.co-main{display:flex; flex-direction:column; gap:22px; max-width:640px}
.co-step{font-family:var(--f-display); font-weight:var(--dw); letter-spacing:var(--dt);
  font-size:1.28rem; margin:6px 0 0}

/* --- what is for sale ---------------------------------------------------- */

.co-types{list-style:none; margin:0; padding:0; border:1px solid var(--line);
  border-radius:var(--r); overflow:hidden; background:var(--surf)}
.co-type{display:grid; grid-template-columns:1fr auto auto; gap:18px; align-items:center;
  padding:16px 18px; border-bottom:1px solid var(--line)}
.co-type:last-child{border-bottom:none}
.co-type-words{min-width:0}
.co-type-name{margin:0; font-weight:700; font-size:1rem}
.co-type-words .hint{margin-top:2px}
.co-type-price{margin:0; font-weight:700; font-size:1.05rem; white-space:nowrap;
  font-variant-numeric:tabular-nums}
@media (max-width:520px){
  .co-type{grid-template-columns:1fr auto; row-gap:12px}
  .co-type-words{grid-column:1 / -1}
}

/* A stepper, because "how many" is a decision between one and four and a
   number field asks you to type it. The input stays a real number input: it is
   what the form posts, what a keyboard user tabs to, and what works when the
   script does not load. */
.stepper{display:inline-flex; align-items:center; border:1px solid var(--line);
  border-radius:var(--r-pill); background:var(--bg); overflow:hidden}
.stepper-btn{width:38px; height:42px; font-size:1.15rem; font-weight:700; line-height:1;
  color:var(--ink); transition:background-color .15s ease}
.stepper-btn:hover{background:var(--surf2)}
.stepper input[type=number]{width:52px; padding:10px 0; text-align:center; border:none;
  background:transparent; border-radius:0; font-weight:700; font-variant-numeric:tabular-nums;
  -moz-appearance:textfield}
.stepper input[type=number]::-webkit-outer-spin-button,
.stepper input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none; margin:0}
.stepper input:focus{box-shadow:none; outline:2px solid var(--brand); outline-offset:-2px}

/* --- who is buying ------------------------------------------------------- */

.co-fields{display:flex; flex-direction:column; gap:18px}
.co-fields .row{display:grid; grid-template-columns:1fr 1fr; gap:18px}
@media (max-width:520px){ .co-fields .row{grid-template-columns:1fr} }
input[aria-invalid=true]{border-color:#C0392B}
input[aria-invalid=true]:focus{box-shadow:0 0 0 3px #FDE9E7}
.co-bad{margin:0; font-size:.85rem; font-weight:600; color:#9B2C1B}

/* --- the button and the marks under it ----------------------------------- */

.co-go{display:flex; flex-direction:column; align-items:flex-start; gap:14px; margin-top:4px}
/* THE TOTAL AGAIN, BESIDE THE BUTTON, and only on a phone. On a narrow screen
   the summary sits above the ticket picker — where Shopify puts it and where
   these buyers expect it — so by the time somebody has chosen four tickets and
   typed their name, the number they are agreeing to has scrolled off the top.
   Most of these tickets sell on a phone. Hidden where both would be visible at
   once, because two totals on one screen invites the question of whether they
   are the same number. */
.co-total-here{display:none; width:100%; max-width:340px; border-top:none; padding-top:0}
@media (max-width:900px){ .co-total-here{display:flex} }
.co-go .btn{width:100%; max-width:340px}
/* Dimmed, not disabled. A disabled button gives no reason, and the reason is
   the whole point — this one still takes the click and says what is missing. */
.btn.is-waiting{opacity:.55}
.co-safe{margin:0; font-size:.84rem; color:var(--muted); max-width:44ch; line-height:1.55}

.paymarks{display:flex; flex-wrap:wrap; align-items:center; gap:7px}
.paymark{display:inline-flex; border:1px solid var(--line); border-radius:5px; overflow:hidden;
  background:#fff; line-height:0}
.paymark svg{display:block}

/* --- the summary, pinned ------------------------------------------------- */

/* Clear of the sticky header, or the show's name sits behind the logo. The
   header is 116px on the desktop layout and shrinks on scroll; 132px keeps a
   gap at its tallest, which is the moment that matters. */
.co-summary{position:sticky; top:132px}
@media (max-width:900px){ .co-summary{position:static; order:-1} }
.co-summary-inner{background:var(--surf2); border:1px solid var(--line);
  border-radius:var(--r-lg); padding:22px}
.co-show{display:flex; gap:14px; align-items:flex-start; padding-bottom:16px;
  border-bottom:1px solid var(--line)}
.co-thumb{flex:0 0 68px; width:68px; height:68px; border-radius:var(--r-sm); overflow:hidden;
  background:var(--surf)}
.co-thumb img{width:100%; height:100%; object-fit:cover}
.co-show-title{margin:0; font-weight:700; font-size:.95rem; line-height:1.35}
.co-show-when,.co-show-where{margin:2px 0 0; font-size:.85rem; color:var(--muted)}

.co-lines{list-style:none; margin:16px 0; padding:0; display:grid; gap:10px; font-size:.9rem}
.co-lines li{display:grid; grid-template-columns:1fr auto auto; gap:12px; align-items:baseline}
.co-line-qty{color:var(--muted); font-variant-numeric:tabular-nums}
.co-line-cost{font-weight:700; font-variant-numeric:tabular-nums}
.co-empty{display:block !important; color:var(--muted); font-style:italic}

.co-total{display:flex; justify-content:space-between; align-items:baseline; gap:16px;
  margin:0; padding-top:14px; border-top:1px solid var(--line); font-weight:700}
.co-total strong{font-family:var(--f-display); font-size:1.75rem; font-weight:var(--dw);
  letter-spacing:var(--dt); font-variant-numeric:tabular-nums}

.co-note{margin:14px 0 0; font-size:.83rem; color:var(--muted); line-height:1.55}
.co-perk{display:inline-flex; align-items:center; gap:7px; color:var(--brand); font-weight:700;
  font-size:.88rem}


/* The buy button and the reason to come, on one line and vertically centred,
   so a pill and a button of different heights still read as a pair. Wraps
   rather than shrinks: on a narrow phone the button keeps its full width and
   the perk drops beneath it, which is the order they matter in. */
.show-actions{display:flex; flex-wrap:wrap; align-items:center; gap:14px; margin-top:22px}
.show-lead > figure{margin:0}
.show-actions .btn{flex:0 0 auto}
@media (max-width:420px){
  .show-actions{gap:12px}
  .show-actions .btn{width:100%; justify-content:center}
}

.perk{display:inline-flex; align-items:center; gap:9px; margin:0;
  background:var(--acc-soft,var(--surf2)); color:var(--brand);
  border:1px solid var(--brand); border-radius:var(--r-pill);
  padding:9px 18px; font-weight:800; font-size:1rem; letter-spacing:.01em}
.perk span{font-size:1.05em; line-height:1}

/* ======================================================== live tickets ==
   The hero's "Live tickets" pill.

   WHITE ON THE PICTURE, not brand. A hero is already a photograph with a
   headline over it; a coloured chip competes with both, where a white one
   reads as a label laid on top of them. The brand comes back as the text and
   the dot, which is where it is legible.

   IT BREATHES RATHER THAN BLINKS, and for the same reason as the card badge: a
   flash is an advert and gets ignored, a slow swell survives being looked at.
   The dot carries most of the movement, so the words stay readable while it
   does. Off entirely for anyone who has asked for less motion — the global
   reduced-motion rule cuts durations to zero, which would freeze this
   mid-swell, so it is switched off by name instead. */
/* IT IS A .btn FIRST. Sitting in the row with the other two, it takes their
   padding, their type size and their shape from the one place those are
   defined — so a change to the buttons carries it along instead of leaving it
   a slightly different height beside them, which is the kind of thing nobody
   can name but everybody sees. Only the colours and the pulse are its own. */
.live-pill{
  background:#fff; color:var(--brand);
  box-shadow:0 4px 18px rgba(0,0,0,.28);
  animation:live-pill 2.6s ease-in-out infinite;
}
.live-pill:hover{background:#fff; color:var(--brand)}
.live-dot{
  width:9px; height:9px; border-radius:50%; background:var(--brand); flex:0 0 9px;
  animation:live-dot 1.6s ease-in-out infinite;
}
@keyframes live-pill{
  0%,100%{ box-shadow:0 4px 18px rgba(0,0,0,.28), 0 0 0 0 rgba(255,255,255,.55) }
  50%{ box-shadow:0 4px 18px rgba(0,0,0,.28), 0 0 0 9px rgba(255,255,255,0) }
}
@keyframes live-dot{
  0%,100%{ opacity:1; transform:scale(1) }
  50%{ opacity:.35; transform:scale(.8) }
}
@media (prefers-reduced-motion:reduce){
  .live-pill,.live-dot{animation:none}
}

/* ------------------------------------------------------------- lettings --- */
/* THE BUILDING PAGE HAD NO STYLES AT ALL until 1.22.0. The module shipped its
   markup asking for `.ticks`, `.admin-table` and `.table-scroll` — the last two
   live in the ADMIN stylesheet, which a public page never loads — so what a
   visitor got was a naked table, an unstyled bullet list, and a photograph as
   tall as the screen with every word underneath it. These are the styles that
   markup always meant. */

/* The head: a picture at a size that leaves room for the words beside it.
   Sixteen-by-ten because a building is wider than it is tall and a portrait
   crop of a terrace tells you nothing; sticky on a long page so the photograph
   is still there while somebody reads the facts.

   THE PICTURE FINISHES WHERE THE WORDS DO. It is stretched to the row rather
   than left at its own height, so its bottom edge lines up with the last thing
   in the column beside it — the buttons, on a building page. Sixteen-by-ten is
   still the FLOOR and not the size: a grid row is as tall as its tallest item,
   so the picture contributes its own crop before anything stretches it, and a
   column of words shorter than the photograph leaves the photograph alone. */
.letting-intro{display:grid; grid-template-columns:minmax(0,5fr) minmax(0,6fr); gap:clamp(24px,3.5vw,54px); align-items:stretch}
.letting-shot{margin:0; height:100%}
/* The pictures as a carousel, when there is more than one.

   EVERY FRAME IN THE SAME GRID CELL, the same arrangement the hero carousel
   uses and for the same reason: absolute positioning would collapse the box to
   nothing and the words beside it would jump up the page every six seconds.
   The frames are all the same crop, so the box is one picture tall whichever
   is showing. */
.shot-carousel{display:grid; position:relative; height:100%}
.shot-carousel > .shot-slide{
  grid-area:1/1; margin:0; opacity:0; transition:opacity .6s ease; pointer-events:none;
}
.shot-carousel > .shot-slide.is-on{opacity:1; pointer-events:auto}
.shot-carousel > .shot-slide img{
  width:100%; height:100%; aspect-ratio:16/10; object-fit:cover; display:block;
  border-radius:var(--r-lg); box-shadow:var(--shadow);
}
/* A drawing rather than a photograph: shown whole, on a plain ground, because
   a plan cropped to a wide frame loses its title and half its top wall. The
   route decides which pictures these are, from their own proportions. */
.shot-carousel > .shot-slide.is-upright img{
  object-fit:contain; background:var(--surf); padding:10px;
}
/* On the picture, bottom centre, and gone entirely without JavaScript — where
   the first frame is the only one anybody can reach. */
.shot-nav{
  grid-area:1/1; align-self:end; justify-self:center; position:relative; z-index:2;
  display:flex; align-items:center; gap:8px; margin-bottom:14px;
  padding:4px 6px; border-radius:var(--r-pill);
  background:rgba(8,10,14,.42); backdrop-filter:blur(6px);
}
html:not(.js) .shot-nav{display:none}
@media (max-width:900px){
  .shot-carousel > .shot-slide img{aspect-ratio:16/9; height:auto}
}
@media (prefers-reduced-motion: reduce){
  .shot-carousel > .shot-slide{transition:none}
}
/* height with the ratio, not instead of it: the ratio sizes the picture while
   the row is being measured, and the height then fills whatever the row turns
   out to be. object-fit keeps the crop centred through both. */
.letting-shot img{
  width:100%; height:100%; aspect-ratio:16/10; object-fit:cover; display:block;
  border-radius:var(--r-lg); box-shadow:var(--shadow);
}
.letting-words > .eyebrow{display:block; margin-bottom:10px}
.letting-words h1,.letting-words h2{margin:0 0 10px}
/* On the listing the name is a link to the building, and it should read as a
   heading rather than as a link in a sentence — the underline arrives on
   hover, where it says "this is clickable" to somebody who is about to. */
.letting-words h2 a{color:inherit; text-decoration:none}
.letting-words h2 a:hover{text-decoration:underline; text-underline-offset:3px}
/* On the listing the picture is a link to the building, and the link is what
   the image is measured against — so it has to be as tall as the figure or the
   picture goes back to its own height and stops short of the button. */
.letting-shot a{display:block; height:100%}

/* One building after another down the listing, far enough apart to read as two
   things rather than one long one. */
.letting-listing + .letting-listing{margin-top:clamp(36px,5vw,68px); padding-top:clamp(36px,5vw,68px); border-top:1px solid var(--line)}
.letting-words .lede{margin:0 0 14px}
.letting-summary{color:var(--muted); margin:0 0 22px; max-width:52ch}

/* The facts. A description list because that is what it is — four labels and
   four answers — and a grid rather than a row so the fourth does not fall off
   a phone. */
.letting-facts{display:grid; grid-template-columns:repeat(auto-fit,minmax(118px,1fr)); gap:14px; margin:0 0 26px}
.letting-facts > div{
  border:1px solid var(--line); border-radius:var(--r); padding:12px 14px; background:var(--surf);
}
.letting-facts dt{font-size:.74rem; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); font-weight:700}
.letting-facts dd{margin:4px 0 0; font-family:var(--f-display); font-weight:var(--dw); font-size:1.3rem; line-height:1.15}

/* What you get, on the dark ground. Two or three columns so eight or twelve
   lines are a block to take in rather than a column to scroll past, and the
   tick is drawn rather than a bullet — a list of what you get should look like
   things being counted off. */
.letting-gets .ticks,.ticks{list-style:none; margin:0; padding:0}
.ticks{display:grid; grid-template-columns:repeat(auto-fit,minmax(248px,1fr)); gap:12px 30px}
.ticks li{position:relative; padding-left:32px; line-height:1.45}
.ticks li::before{
  content:""; position:absolute; left:0; top:.34em; width:18px; height:10px;
  border-left:2.5px solid var(--acc); border-bottom:2.5px solid var(--acc);
  transform:rotate(-45deg);
}
section.deep .ticks li{color:rgba(255,255,255,.88)}
.letting-gets{margin-top:0}

/* The rooms. Cards, because the price is what somebody is comparing and in a
   seven-column table it was fourth from the left in small type. */
.room-cards{list-style:none; margin:0; padding:0; display:grid; gap:16px;
  grid-template-columns:repeat(auto-fill,minmax(232px,1fr))}
.room-card{
  border:1px solid var(--line); border-radius:var(--r-lg); background:var(--surf);
  padding:18px 18px 16px; display:flex; flex-direction:column; gap:10px;
  transition:border-color .2s ease, box-shadow .2s ease;
}
.room-card:hover{border-color:var(--brand); box-shadow:var(--shadow)}
/* A room that has gone stays on the page — a full house is information — but it
   stops competing with the ones that have not. */
.room-card.state-let,.room-card.state-reserved{background:var(--surf2); opacity:.72}
.room-card.state-let:hover,.room-card.state-reserved:hover{border-color:var(--line); box-shadow:none}
.room-head{display:flex; align-items:center; justify-content:space-between; gap:10px}
.room-head h3{margin:0; font-size:1.06rem}
.room-rent{
  margin:0; font-family:var(--f-display); font-weight:var(--dw);
  font-size:clamp(1.7rem, 1.3rem + 1vw, 2.1rem); line-height:1; color:var(--brand);
}
.room-rent span{display:block; font-family:var(--f-body); font-size:.78rem; font-weight:600; color:var(--muted); margin-top:5px}
.room-spec{list-style:none; margin:0; padding:0; display:flex; flex-wrap:wrap; gap:6px}
.room-spec li{
  font-size:.79rem; font-weight:600; color:var(--ink);
  background:var(--surf2); border-radius:var(--r-pill); padding:4px 10px;
}
.room-card.state-let .room-spec li,.room-card.state-reserved .room-spec li{background:var(--surf)}
.room-spec .room-step-free{background:var(--brand-soft); color:var(--brand)}
.room-card .btn{margin-top:auto; align-self:flex-start}

@media (max-width:820px){
  /* The picture goes back on top, because two columns of nothing is worse than
     one column of something. It keeps the smaller crop. */
  .letting-intro{grid-template-columns:1fr; gap:22px}
  /* One column, so there is nothing beside the picture to match: it goes back
     to being as tall as its own crop rather than as tall as the words. */
  .letting-shot,.letting-shot a,.shot-carousel{height:auto}
  .letting-shot img{aspect-ratio:16/9; height:auto}
}

/* Getting about: one line per journey, the icon carrying the mode so the row
   can be read without reading it. A grid rather than a flex row because four
   journeys of different lengths should still line up in columns.

   IN A PANEL OF ITS OWN COLOUR. White cards on a white page are a hairline
   border apart from the page, and "how far is the station" is the question
   every enquiry opens with — it has earned a block somebody cannot scroll past
   without noticing. The panel takes the soft brand tint and the cards inside go
   white against it, which is the way round that keeps the words at full
   contrast; the tint is a palette token, so this is the site's own blue, or
   green, or plum, and not a colour chosen here. */
.letting-travel{padding-top:0}

/* The description and the journeys side by side. The prose keeps a reading
   measure — a column of body text 900 pixels wide is hard work whatever else is
   beside it — and the aside takes the rest.

   STICKY, at the height of the header plus a breath: the description runs to
   four headings, and a panel that scrolls away at the first of them is a panel
   somebody has to go back for. It unsticks by itself when the column is shorter
   than the screen, which is what `position:sticky` does and a fixed position
   would not. */
.letting-cols{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,360px); gap:clamp(28px,4vw,64px); align-items:start}
.letting-prose .prose{max-width:62ch}
.letting-aside{position:sticky; top:104px}
@media (max-width:900px){
  /* One column, description first — it is what they came for. The panel stops
     being sticky, because a sticky sidebar in a single column is a block that
     follows you down the page. */
  .letting-cols{grid-template-columns:1fr; gap:34px}
  .letting-aside{position:static}
}
.travel-panel{
  background:var(--brand-soft); border-radius:var(--r-lg);
  padding:clamp(24px,3vw,40px) clamp(20px,3vw,40px);
}
/* The figures with the journeys beside them. The figures take the narrower
   side and go to two columns — four of them in half a page at full size is
   four numbers each on its own line — and the panel takes the rest. */
.facts-pair{display:grid; grid-template-columns:minmax(0,0.85fr) minmax(0,1.15fr); gap:clamp(24px,3.5vw,54px); align-items:start}
.facts-pair .grid.four{grid-template-columns:repeat(2,minmax(0,1fr))}
@media (max-width:900px){
  /* Figures first, then the journeys: the numbers are the headline and the
     journeys are the answer to the next question. */
  .facts-pair{grid-template-columns:1fr; gap:28px}
}

/* Where the buildings disagree about how far anywhere is, each gets its own
   panel with its own name on it, side by side for comparing. */
.travel-many{display:grid; gap:clamp(18px,2.5vw,28px); grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}
.travel-panel .section-head{margin-bottom:22px}
.journeys{list-style:none; margin:0; padding:0; display:grid; gap:14px;
  grid-template-columns:repeat(auto-fit,minmax(236px,1fr))}
.journeys li{
  display:flex; gap:14px; align-items:flex-start;
  border:1px solid transparent; border-radius:var(--r); background:var(--surf); padding:16px 18px;
  box-shadow:var(--shadow);
}
.journey-icon{
  flex:none; width:40px; height:40px; border-radius:var(--r-pill); display:grid; place-items:center;
  background:var(--brand); color:var(--on-brand);
}
.journey-what{display:block; min-width:0}
.journey-what b{display:block; line-height:1.3}
.journey-time{display:block; font-family:var(--f-display); font-weight:var(--dw); font-size:1.12rem; margin-top:3px}
.journey-note{display:block; font-size:.84rem; color:var(--muted); margin-top:2px}

/* What the reviews add up to, beside the hero's buttons. Glass rather than a
   solid chip: it sits on a photograph, and a white box there reads as a second
   button competing with the two that matter. */
.hero-rating{
  display:inline-flex; align-items:center; gap:10px; padding:9px 16px;
  border-radius:var(--r-pill); background:rgba(255,255,255,.14);
  border:1px solid rgba(255,255,255,.28); backdrop-filter:saturate(1.3) blur(6px);
  font-size:.88rem; line-height:1.3;
}
.hero-rating .stars{display:inline-flex; color:#F5B301}
.hero-rating b{font-size:1.02rem}
/* On a hero with no picture the words are ink on a tint, so the glass has to
   stop being glass or it disappears. */
.hero.plain .hero-rating{background:var(--surf); border-color:var(--line)}
@media (max-width:560px){
  /* Under the buttons rather than squeezed beside them. */
  .btn-row .hero-rating{width:100%; justify-content:center}
}

/* A VENUE NAME IS A LABEL, NOT A HEADLINE. The h1 runs to 3.9rem, which is
   right for a page whose title is the thing being said — a show, an article. A
   venue page is reference: the name tells you that you are in the right place,
   and the address, directions and parking under it are what the visitor came
   for. At full size a long venue name filled the width and pushed the address
   off the first screen. Still multiplied by --dsc so it follows the display
   size setting rather than pinning one size. Roughly h2. */
.venue-page h1{font-size:calc(clamp(1.8rem,1.2rem + 1.8vw,2.6rem) * var(--dsc,1))}

/* Room between the two. Headings are margin:0 here and so are .where-shot and
   .where-map, which is right where a parent decides the rhythm — on this page
   nothing was, so the descender of a long name touched the photograph. */
.venue-page .where-pair{margin-top:clamp(20px,2.5vw,32px)}

/* Where it all is, with a room beside it. Both sides are pictures of the same
   height, so the band reads as one thing rather than a map that happens to
   have a photograph next to it. */
.where-pair{display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:clamp(18px,2.5vw,30px); align-items:start}
.where-pair.alone{grid-template-columns:minmax(0,1fr); max-width:768px}
.where-map,.where-shot{margin:0}
.where-map img,.where-shot img{
  width:100%; aspect-ratio:3/2; object-fit:cover; display:block;
  border-radius:var(--r-lg); border:1px solid var(--line);
}
.where-map .btn{margin-top:12px}
@media (max-width:760px){
  /* Same trap as .split above: name the variant or it outranks this. */
  .where-pair, .where-pair.alone{grid-template-columns:1fr}
}

/* Panels with a picture beside them. The panels keep three across in the
   narrower column — six of them read as two rows of three — and the picture
   takes the rest. It sticks because it is one image against a block of six:
   unstuck, it is a tall photograph beside a tall list with nothing lining up.
   It stops sticking by itself when the column runs out, which is what
   position:sticky does and a fixed position would not. */
.cards-pair{display:grid; grid-template-columns:minmax(0,2.1fr) minmax(0,1fr); gap:clamp(22px,3vw,44px); align-items:start}
.cards-pair .cards-half{grid-template-columns:repeat(3,minmax(0,1fr))}
.cards-beside{margin:0; position:sticky; top:104px}
.cards-beside img{
  width:100%; aspect-ratio:3/4; object-fit:cover; display:block;
  border-radius:var(--r-lg); box-shadow:var(--shadow);
}
@media (max-width:1100px){
  /* The picture drops under them rather than squeezing the panels to a word a
     line, and stops following. */
  .cards-pair{grid-template-columns:1fr}
  .cards-beside{position:static; max-width:520px}
  .cards-beside img{aspect-ratio:16/9}
}
@media (max-width:760px){
  .cards-pair .cards-half{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}
}
