/* =====================================================================
   Open Air Gallery — site stylesheet

   Round three, and the canvas is flipped (CLAUDE.md, "white ground, his
   teal, one pop colour"). White is the ground, ink is the type, the mint
   is the loud colour a band is painted in, and the coral is the one accent
   that pops against it. Black is kept for three things and no more: the
   marquee strip, the scrim over a photograph, and the footer. Every colour
   is named once in :root and used through var() everywhere else, so a bare
   hex outside this block is a bug.

   Contrast, computed in docs/contrast.py and asserted at build time:
     --ink on --paper       19.8:1  (AAA)
     --muted on --paper      7.8:1
     --dim on --paper        5.2:1  (labels, captions, small print)
     --ink on --mint        13.8:1  (the mint band, and the mint button)
     --ink on --pop          6.0:1  (the coral button)
     --on-ink on --ink      17.8:1  (type on one of the three black bands)
     --on-ink-mute on --ink 11.7:1  (the footer's second voice)
     --mint on --ink        13.8:1  (the accent where the ground is black)
   Three rules that go with it: the mint never carries type on white (1.5:1
   — it is paint, and the type on it is ink), the coral carries type only
   at 24px and up (3.4:1 is large-text contrast and nothing else), and
   mint never sits on --wall.
   ===================================================================== */
:root{
  /* The ground, and the ink on it. --ink is still the black the three dark
     bands are painted in — the marquee, the scrim over a photograph and the
     footer — and on the flipped canvas it is the type as well. --paper-2
     and --paper-3 are the two steps off white a panel and a well take. */
  --paper:#FFFFFF; --paper-2:#F6F5F1; --paper-3:#EDEBE5;
  --ink:#0A0A0B; --ink-2:#141416; --ink-3:#1A1A1D;
  --muted:#55524C; --dim:#6E6C67;
  /* Type on one of the three black bands: the footer, the marquee and the
     caption over a photograph. The page is white, but those are not. */
  --on-ink:#F4F3EF; --on-ink-mute:#C9C7C1;
  --mint:#71EEB8; --mint-deep:#55B38A; --wall:#E9E7E2;
  /* One step between the two mints, for the places where mint has to be
     shaded against itself rather than against the ink: the roller lap
     marks and the bristle tones in Splat. It never carries type. */
  --mint-mid:#63D0A0;
  /* --- round three: the second paint (owner, 2026-09-19) -------------
     "Add in multi-colour mixing and an on-screen effect that matches
     that." So every paint verb on this site now carries two paints, and
     where one lands on the other they mix the way paint mixes rather than
     stacking the way vector does. --pop is the coral the round-three
     palette names, the one colour that pops against his mint; it is paint
     and never a foreground, so no contrast pair is claimed for it and it
     never carries type.

     --mix is what the two make: the channel-wise product of --mint and
     --pop, which is what a multiply blend computes at the overlap
     (0x71*0xFF, 0xEE*0x4F, 0xB8*0x2E, each over 255) — the brown two
     complements make on a wall. It is named here so the mixing has a
     value that can be looked up and checked rather than only observed,
     and so a still of an overlap can be compared against it.

     --pop-mid and --pop-deep are the coral's two shades, doing for it what
     --mint-mid and --mint-deep do for the mint: the lap marks between them
     and the darker rim round a wet edge. --pop-wet is the pale coral a
     loaded bristle carries. */
  --pop:#FF4F2E; --pop-mid:#E8451F; --pop-deep:#A8300F;
  /* The rim of a wet edge is darker than the paint's own shade: --mint-deep
     and --pop-deep are tones the design uses beside their paint, and a rim
     that is only one step down does not read as an edge at all. These two
     are the edge, and they are paint and never type. */
  --mint-rim:#2C7452; --pop-rim:#7A2007;
  /* The wet highlight on a fresh throw: white, because a gloss is the light
     and not the paint. Under the multiply that mixes the throw into the
     stroke it disappears, which is what happens to a highlight when more
     paint goes over it. */
  --gloss:rgba(255,255,255,.55); --gloss-0:rgba(255,255,255,0);
  /* The vein. Two wet paints dragged through each other do not average to a
     third colour — they marble, and the lightest thing in a marbled edge is
     the thin cream line where the two have pulled apart and the ground is
     showing through the film. That is this, and it is warm rather than
     white because it is paint and not light. */
  --cream:#FFF1E4;
  --pop-wet:rgba(255,170,146,.72); --pop-wet-0:rgba(255,170,146,0);
  --mix:#714A21;
  /* The warm white the paint mechanics marble with: their veins go black
     without it. It is theirs, it carries no type, and it is declared here
     because :root is the one place a hex may live. */
  --cream:#FFF1E4;
  /* A hairline is ink on the white page and paper on a black band, so
     there are two pairs of them and no rule uses the wrong one. */
  --line:rgba(10,10,11,.14); --line-2:rgba(10,10,11,.28);
  --line-on-ink:rgba(244,243,239,.14); --line-on-ink-2:rgba(244,243,239,.26);
  /* Stencil's overspray is the paint that misses the cut, so it is the
     colour of the paint: ink where the figure is ink on the white page,
     and paper where the figure sits on a photograph or a black band. */
  --spray-halo:rgba(10,10,11,.55);
  --mint-glow:rgba(113,238,184,.14); --shade:rgba(10,10,11,.72);
  /* The ambient breath, and the two stops that make it soft. The core is
     the ceiling the owner's "tasteful" sets: mint at 9%, never more, and
     the softness is the gradient's own falloff — nothing on this site is
     ever blurred with a filter. --glow-0 is the same mint at nothing, so
     the fade ends in the colour it started as rather than in grey. */
  --glow:rgba(113,238,184,.09); --glow-soft:rgba(113,238,184,.035);
  --glow-0:rgba(113,238,184,0);
  /* The wet edge the brush leaves on the rule it has just painted: the pale
     mint of a loaded bristle, over the flat mint of the dry rule, so "wet"
     reads as a lighter ridge rather than more of the same green. */
  --mint-wet:rgba(168,247,214,.72); --mint-wet-0:rgba(168,247,214,0);
  /* Primer: the coat a wall waits under before the mural goes on. Twelve
     per cent of the photograph's colour left, the contrast pulled almost
     flat and the whole thing lifted toward --wall, which is a whitewashed
     wall and not a grey one — hence the sepia at the end. It is a filter
     rather than a colour because it has to be the photograph, faintly,
     rather than a sheet laid over it: the mural is still there under the
     primer, the way it is on a wall that has been painted out. */
  --primer:saturate(.12) contrast(.45) brightness(1.5) sepia(.08);
  --scrim:rgba(10,10,11,.88); --scrim-soft:rgba(10,10,11,.45);
  /* Ink on the one light panel: the ghost of a buff job across its corner,
     its hairlines, and the muted grey that stands in for --muted there.
     --ink-mute on --wall is 7.4:1. --mint on --wall is 1.16:1, which is why
     PLAN.md §2 says the accent never lands on this colour: on the band it
     appears the one way it may, as ink on mint. */
  --ghost:rgba(10,10,11,.10); --ink-line:rgba(10,10,11,.22); --ink-mute:#4B4842;
  /* Stencil: the card the feet are sprayed through. Matte board, lit from
     the top edge, a shade off --wall so it reads as something laid over the
     page rather than part of it; a cut edge of ink around it and the shadow
     of a hand holding it a little off the wall. It carries no type, so it
     answers to none of the contrast rules above — it is a prop. */
  /* A run of paint: the wet mint of a loaded tin, thinning as it runs.
     Tip's run is this; Letter's runs take the coral of the feet instead
     (site.js hands each its --drip). */
  --drip:rgba(113,238,184,.9);
  --drip-0:rgba(113,238,184,.3);
  /* Tip: how much of the pool is left at the far ends of the wall. It is a
     mask and nothing else, so only the alpha of it counts — the paint under
     it is the mint gradient, and this is how thin the paint runs out there. */
  --pool-thin:rgba(244,243,239,.42);
  --matte:#9A9284; --matte-2:#7F776A;
  --matte-edge:rgba(10,10,11,.45); --st-shade:rgba(10,10,11,.55);
  --st-cut:rgba(10,10,11,.55);
  --radius:4px;                 /* near-square — signwriting, not SaaS */
  --max:1240px; --nav-h:72px;
  /* Five faces, used deliberately — the reference-site lesson Ephraim asked for
     (CLAUDE.md, round three: "multiple typefaces — four or five"). Anton
     is the headline, condensed and shouted; Archivo carries the width axis
     the dimension figures and the wordmark need; Fraunces is what you
     actually read; Inter is the interface; and Permanent Marker is his own
     hand, for an aside. Every fallback is metric-near, so a 7rem headline
     does not reflow the page while the face is still in flight. */
  --font-tall:"Anton","Arial Narrow",Impact,"Haettenschweiler",sans-serif;
  --font-display:"Archivo","Helvetica Neue",Helvetica,Arial,"Liberation Sans",sans-serif;
  --font-serif:"Fraunces",Georgia,"Times New Roman",serif;
  --font-body:"Inter",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  --font-marker:"Permanent Marker","Marker Felt","Comic Sans MS",cursive;
  --ease:cubic-bezier(.16,1,.3,1); --ease-roll:cubic-bezier(.62,.03,.2,1);
  /* The reel (Home): the card's lift off the moving picture, and the
     shade the rail of names sits on at the foot of the stage. */
  --reel-shadow:0 26px 60px rgba(10,10,11,.38),0 4px 14px rgba(10,10,11,.22);
  --reel-foot:linear-gradient(to top,rgba(10,10,11,.72),rgba(10,10,11,0));
}

/* The four Google faces come down in one request (build.py FONTS). The
   marker does not: Permanent Marker is already in the repo as a build
   input for the Wash wall, so it is served from here rather than asked of
   a fifth family on a CDN. Apache 2.0, attribution in site/fonts/. */
@font-face{
  font-family:"Permanent Marker";
  src:url(../fonts/PermanentMarker.woff2) format("woff2");
  font-weight:400;font-style:normal;font-display:swap;
}

*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0;background:var(--paper);color:var(--ink);
  font-family:var(--font-body);font-size:16px;line-height:1.65;
  font-variant-numeric:tabular-nums;-webkit-font-smoothing:antialiased;
}
img,picture,video,svg{max-width:100%;display:block}
a{color:inherit;text-decoration:none}
ul{list-style:none;margin:0;padding:0}
:focus-visible{outline:2px solid var(--mint);outline-offset:3px}

/* --- type -------------------------------------------------------------
   Four jobs, five faces, and a class for each so a heading never has to
   name a font file:

     .tall    Anton, caps, line-height .9 — every headline on the site
     .wide    Archivo at wdth 125 / wght 900 — the wordmark, the dimension
              figures, and a lead that is shouted rather than read
     .serif   Fraunces — the copy a visitor actually reads
     (Inter)  the interface: buttons, labels, captions, the form
     .marker  Permanent Marker — an aside in his own hand, never small

   h1 and h2 are .tall by default, because on this site they always are. */
h1,h2,h3,h4{margin:0;line-height:1.1;font-weight:400}
h1,h2,.tall{
  font-family:var(--font-tall);font-weight:400;text-transform:uppercase;
  letter-spacing:.005em;line-height:.9;
}
.wide{
  font-family:var(--font-display);font-weight:900;
  font-variation-settings:'wdth' 125,'wght' 900;
  text-transform:uppercase;letter-spacing:.02em;
}
.serif{font-family:var(--font-serif);font-weight:400;letter-spacing:0;text-transform:none}
.marker{font-family:var(--font-marker);font-weight:400;text-transform:none;letter-spacing:0}
h1{font-size:clamp(3rem,8.6vw,7rem)}
h2{font-size:clamp(2.4rem,6vw,4.6rem)}
h3{font-family:var(--font-body);font-weight:700;font-size:1.2rem;letter-spacing:-.01em;line-height:1.3}
h4{font-family:var(--font-body);font-weight:700;font-size:.82rem;letter-spacing:.18em;text-transform:uppercase;color:var(--dim)}
p{margin:0 0 1em}
b,strong{font-weight:700}
small{font-size:.82rem}
.muted{color:var(--muted)}
.lead{font-size:1.1rem;color:var(--muted);max-width:62ch}
/* The aside in his own hand, and the one place the coral carries type. So
   it is never small: --pop on --paper is 3.4:1, which is large-text
   contrast and nothing else, and 1.5rem is the floor everywhere it lands. */
.eyebrow{
  display:block;font-family:var(--font-marker);font-weight:400;
  font-size:clamp(1.5rem,2.2vw,1.75rem);line-height:1.15;
  letter-spacing:0;text-transform:none;
  color:var(--pop);margin-bottom:.6rem;
}

/* --- shell ----------------------------------------------------------- */
.wrap{width:min(var(--max),calc(100% - 2rem));margin-inline:auto}
section{padding:clamp(4.5rem,10vw,8rem) 0;position:relative}
.section-head{max-width:760px;margin-bottom:clamp(2rem,5vw,3.5rem)}
.section-head .lead{margin-top:1.1rem}
.alt{background:var(--paper-2)}
.skip{position:absolute;left:-999px;top:0;background:var(--mint);color:var(--ink);padding:.5rem 1rem;z-index:1000;font-weight:700}
.skip:focus{left:1rem;top:1rem}
/* The drawn props a page lends its own elements — today the roller every
   card borrows (build.py roller_sprite()). Absolutely positioned and zero
   sized, so a <symbol> in the document can never take part in the layout. */
.propdefs{position:absolute;width:0;height:0;overflow:hidden}

/* --- buttons --------------------------------------------------------- */
/* A button is a painted sign: the headline face, caps, a 2px edge and
   almost no radius. The coral button carries ink rather than white — white
   on coral is 3.4:1 and a button label is not large type. */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:.5rem;
  padding:.8rem 1.4rem;border-radius:var(--radius);
  font-family:var(--font-tall);font-weight:400;text-transform:uppercase;
  font-size:1.05rem;letter-spacing:.05em;line-height:1.1;
  border:2px solid transparent;cursor:pointer;white-space:nowrap;
  transition:background .2s,border-color .2s,color .2s,transform .2s var(--ease);
}
.btn-mint{background:var(--mint);border-color:var(--mint);color:var(--ink)}
.btn-mint:hover{background:var(--ink);border-color:var(--ink);color:var(--mint)}
.btn-pop{background:var(--pop);border-color:var(--pop);color:var(--ink)}
.btn-pop:hover{background:var(--ink);border-color:var(--ink);color:var(--pop)}
.btn-ghost{background:transparent;border-color:var(--ink);color:var(--ink)}
.btn-ghost:hover{background:var(--ink);color:var(--paper)}
/* The same ghost on one of the black bands. */
.on-ink .btn-ghost{border-color:var(--on-ink);color:var(--on-ink)}
.on-ink .btn-ghost:hover{background:var(--on-ink);color:var(--ink)}
.page-hero.has-media .btn-ghost{border-color:var(--on-ink);color:var(--on-ink)}
.page-hero.has-media .btn-ghost:hover{background:var(--on-ink);color:var(--ink)}
.btn-sm{padding:.5rem 1rem;font-size:.9rem}
.btn svg{width:17px;height:17px;flex:none}
.btn-row{display:flex;gap:.7rem;flex-wrap:wrap}
@media (prefers-reduced-motion:no-preference){
  .btn:hover{transform:translateY(-1px)}
}

/* --- breakpoints: base, 640, 960, 1240 ------------------------------- */
@media (min-width:640px){
  .wrap{width:min(var(--max),calc(100% - 3rem))}
  .lead{font-size:1.15rem}
}
@media (min-width:960px){
  body{font-size:17px}
  .wrap{width:min(var(--max),calc(100% - 4rem))}
}
@media (min-width:1240px){
  .wrap{width:var(--max)}
}

/* =====================================================================
   Nav — the menu runs across the top of the screen (Overall Murals).

   Ephraim's own reference, in the owner's words (CLAUDE.md, round three):
   "the menu runs across the top of the screen, not tucked on the right — a
   full horizontal nav beside the wordmark, visible at every width (no
   hamburger)." So there is no hamburger and no panel: the wordmark is on
   the left, the five links run across the middle in the headline face, and
   the consult button is on the right.

   "At every width" is the part that costs something. On a phone the bar
   wraps to two rows: the wordmark and the button on the first, the five
   links on the second — and all five fit on that row, down to a 360px
   screen, with nothing scrolled, faded or hidden behind a control, which
   is the whole point of the reference. Below 520px the row is set a touch
   smaller and tighter and spread edge to edge (see the phone block).

   White glass on a 2px ink hairline, sticky rather than fixed: the page
   below it is white now, so the bar is a painted sign at the top of the
   page rather than something floating over a photograph, and the hero
   starts under it instead of behind it.
   ===================================================================== */
.nav{
  position:sticky;top:0;z-index:100;
  background:rgba(255,255,255,.88);
  backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
  border-bottom:2px solid var(--ink);
}
.nav .wrap{display:flex;align-items:center;gap:clamp(.8rem,2vw,2.5rem);min-height:var(--nav-h)}

.brand{
  display:grid;flex:none;font-family:var(--font-display);
  font-variation-settings:'wdth' 125,'wght' 900;font-weight:900;
  font-size:.95rem;line-height:1;letter-spacing:.03em;
  text-transform:uppercase;color:var(--ink);
}

/* The row itself. margin-inline:auto is what centres it between the
   wordmark and the button without either of them having to be the same
   width as the other. */
.nav-bar{margin-inline:auto;min-width:0}
.nav-links{display:flex;align-items:center;gap:.1rem;list-style:none;margin:0;padding:0}
.nav-links a{
  display:block;font-family:var(--font-tall);font-weight:400;
  text-transform:uppercase;font-size:1.05rem;letter-spacing:.04em;
  padding:.6rem .7rem;color:var(--ink);white-space:nowrap;
  transition:color .2s;
}
.nav-links a:hover{color:var(--pop)}
/* The page you are on, in the one accent that is allowed to shout. It is
   1.05rem, which is under the coral's 24px floor for type on white — so the
   current page is marked twice, in colour and with a painted underline, and
   the colour is never the only signal. */
.nav-links a.active{color:var(--pop)}
/* The underline hangs into the link's own bottom padding (the negative
   margin gives back exactly its 5px), so the current link is the same
   height as the others and its word sits on the same line as theirs. */
.nav-links a.active::after{content:"";display:block;height:3px;margin:2px 0 -5px;background:var(--pop)}
.nav-cta{flex:none}

/* The phone. The links take a row of their own under the wordmark and the
   button. They used to run off the right edge and scroll under a fade, which
   at 390 cut CONTACT in half; now the row fits. overflow-x:auto stays only
   as a guard for a visitor who has set a large minimum font size — at the
   default size there is nothing to scroll, and so no fade either. */
@media (max-width:959px){
  .nav .wrap{flex-wrap:wrap;gap:.6rem 1rem;padding-top:.7rem;padding-bottom:.1rem}
  .nav-cta{order:2;margin-left:auto}
  .nav-bar{
    order:3;width:100%;
    overflow-x:auto;overscroll-behavior-x:contain;
    -webkit-overflow-scrolling:touch;scrollbar-width:none;
  }
  .nav-bar::-webkit-scrollbar{display:none}
  .nav-links{gap:0;padding-bottom:.45rem;width:max-content}
  .nav-links a{font-size:.95rem;padding:.5rem .55rem}
  .nav-links li:first-child a{padding-left:0}
}
/* A narrow phone (360–520). The five links at .95rem with .04em tracking
   need 367px and a 360 screen gives them 328, so here the row is spread
   edge to edge (space-between, the end links flush with the gutters), the
   tracking drops to .025em and the size follows the screen from .95rem at
   ~400px down to 13.9px at 360 — still Anton, still uppercase, and every
   link keeps a tap target over 40px tall. */
@media (max-width:520px){
  .nav-links{width:100%;justify-content:space-between}
  .nav-links a{font-size:clamp(.82rem,3.85vw,.95rem);letter-spacing:.025em;padding:.6rem .25rem}
  .nav-links li:last-child a{padding-right:0}
}
@media (min-width:960px){
  .brand{font-size:1.05rem}
  .nav-links a{font-size:1.15rem;padding:.7rem .85rem}
}

/* --- page hero -------------------------------------------------------- */
/* The bar is sticky, so it takes its own room at the top of the document
   and the hero starts under it rather than behind it. */
.page-hero{padding:clamp(3.5rem,9vw,7rem) 0;position:relative;isolation:isolate;overflow:hidden}
.page-hero .hero-media{position:absolute;inset:0;z-index:-2}
.page-hero .hero-media img{width:100%;height:100%;object-fit:cover}
/* The hero clip sits over the photograph and only plays where motion is
   welcome: no script, reduced motion or data-saver leaves the still. */
.hero-video{display:none}
html.motion .hero-video{display:block;position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
@media (prefers-reduced-motion:reduce){html.motion .hero-video{display:none}}
/* The scrim used to finish in --ink because the page under it was ink. It
   is white now, so a shade that fades to black would end in a hard edge:
   this one is even, dark at both ends and lightest through the middle,
   which is the mockup's. */
.hero-shade{position:absolute;inset:0;z-index:-1;background:linear-gradient(180deg,var(--shade) 0%,var(--scrim-soft) 46%,var(--shade) 100%)}
.hero-inner{max-width:900px}
.page-hero .lead{margin-top:1.3rem;font-family:var(--font-serif);color:var(--muted);opacity:1}
.crumbs{display:flex;gap:.5rem;align-items:center;font-size:.8rem;letter-spacing:.06em;text-transform:uppercase;color:var(--dim);margin-bottom:1.2rem}
.crumbs a:hover{color:var(--ink)}

/* A hero with a photograph under it is the exception: there the words are
   the light ones and they carry the ink halo that keeps small type off a
   lit-up mural. A hero without one is the white page, and its words are
   ink with nothing behind them. */
.page-hero.has-media .lead{color:var(--on-ink);opacity:.86}
.page-hero.has-media .crumbs{color:var(--on-ink-mute)}
.page-hero.has-media .crumbs a:hover{color:var(--on-ink)}

/* --- the tin on the hero's floor, and the rule it fills ----------------
   build.py tip_can() and tip_rule(); the mechanic is under TIP, below.

   These are the parts that are in every render, script or none: a tin (a
   photograph of a Ronan Bulletin Enamel can since owner note 13) standing
   in the bottom-left corner of the hero's words, and a finished
   mint rule across the top of the section under it. --tip-w is the tin's
   size and the whole of its geometry — the box is square, the hero reserves
   exactly that much room under the words for it, and the two fractions the
   mechanic hangs off (build.py CAN_TIP_PIVOT and CAN_TIP_POUR) are fractions
   of the same box. The tin is absolutely positioned, so a tin that lies
   right out of its box once it is over can never move anything; the hero
   clips, which is where a pour that ran too far would stop. */
.page-hero.has-tip{--tip-w:150px}
@media (max-width:700px){.page-hero.has-tip{--tip-w:96px}}
.page-hero.has-tip .hero-inner{position:relative;padding-bottom:calc(var(--tip-w) + .8rem)}
.tip{position:absolute;left:0;bottom:0;width:var(--tip-w);height:var(--tip-w)}
.tip-can{display:block;width:100%;height:100%}
/* The drawing lies out of its own box at seventy-six degrees, so it is the
   one prop on the site that is not clipped to its element. */
.tip-can svg{display:block;width:100%;height:100%;overflow:visible}
/* The tin is a photograph now (owner note 13: a can of Ronan Bulletin
   Enamel, the old label; build.py tip_can()). It stands in the square box
   at the box's full height, bottom-left, so its right side is at
   CAN_TIP_PIVOT's x. The image is never a drag source and never the event
   target: the span is the control, the picture is what it looks like. Over
   it, three things that are paint and shade and not the can: the mint in
   the open rim, the lens over the lip (only while it pours) and a soft
   floor shadow. Their boxes are fractions of the same square, measured on
   the cut-out (349 x 474), so they sit on the rim at any --tip-w. */
.tip-photo{position:relative}
.tip-photo picture,.tip-photo img{display:block;height:100%;width:auto}
.tip-photo img{-webkit-user-drag:none;user-select:none;-webkit-user-select:none;pointer-events:none}
.tip-photo .can-paint{
  position:absolute;left:9.3%;top:2.4%;width:55.3%;height:5.6%;border-radius:50%;
  background:
    radial-gradient(38% 46% at 40% 34%,var(--gloss) 0,var(--gloss-0) 100%),
    radial-gradient(closest-side,var(--mint) 0,var(--mint) 70%,var(--mint-deep) 100%);
  box-shadow:inset 0 1px 0 var(--mint-rim);
}
.tip-photo .can-shadow{
  position:absolute;left:1%;bottom:-3.5%;width:72%;height:8%;border-radius:50%;
  background:radial-gradient(closest-side,var(--shade) 0,transparent 100%);
}
/* The tin is a picture until site.js makes it a control: no script, nothing
   to press. */
.tip-can[role="button"]{cursor:pointer;-webkit-tap-highlight-color:transparent}
.tip-can[role="button"]:focus-visible{outline:2px solid var(--mint);outline-offset:4px;border-radius:var(--radius)}

.tiprule{position:relative;height:2px;margin-bottom:clamp(1.6rem,3.6vw,2.4rem)}
.tiprule-line{
  position:absolute;left:0;right:0;top:0;height:2px;
  background:var(--mint);transform-origin:var(--tip-x,0%) center;
}

/* --- the lettered band ------------------------------------------------
   build.py spray_band(): a word set in the wordmark's own face at the size
   of a headline, with room either side of it for a brush to travel.

   The word is the only thing in the band with a height. Everything the
   mechanic adds — the brush, the wet band behind it and the runs — is
   absolutely positioned inside the stage, and the stage shrink-wraps the
   word, so the band is exactly as tall with Letter as it is without it.
   The band clips: a brush coming in from the left is off the page, not off
   the end of the document.

   The brush itself is in the server HTML and is shown by nothing but
   html.motion, the way Brush's brush is — markup a crawler can see and no
   visitor without motion ever does.                                      */
.spray-band{position:relative;overflow:hidden;padding:clamp(2.2rem,5.5vw,4rem) 0}
.spray-stage{position:relative;display:inline-block;max-width:100%}
.spray-word{
  display:grid;margin:0;
  font-family:var(--font-display);font-variation-settings:'wdth' 125,'wght' 800;
  font-size:clamp(2.6rem,12vw,7.2rem);line-height:.92;letter-spacing:.02em;
  text-transform:uppercase;color:var(--paper);
}
.spray-track{display:none}

/* --- a band painted in one stroke -------------------------------------
   build.py stroke_band() and highlight(); the drawing is strokes.py.

   "Maybe make the teal a brush stroke — so it looks like he painted on the
   screen" (owner, 2026-09-19). So a mint band is never a background colour
   and never a shape with a clean edge: it is one swept stroke behind the
   words, drawn at a viewBox the section stretches, and the section itself
   has no background at all.

   Two things follow. The stroke must not be clipped — the drips run out of
   the bottom of the drawing and down the white page below it, which is the
   whole point of a drip — so neither the svg nor the band clips, and the
   band's own padding is what keeps the words off the ragged edges. And the
   words sit above it in the ordinary .wrap: the paint is at z-index 0 and
   aria-hidden, so a screen reader hears a section and not a stroke.        */
.band{
  position:relative;color:var(--ink);text-align:center;
  padding:clamp(5rem,11vw,8.5rem) 0 clamp(5.5rem,12vw,9.5rem);
}
/* The stroke is painted past both ends of its box and leans a degree or
   two, so it is wider than the page. overflow-x:clip takes that off the
   document without taking the drips with it: clip on one axis leaves the
   other visible, where hidden or auto would have forced a scrollport and
   cut the runs off at the bottom edge. */
.stroke-wrap{position:absolute;inset:0;z-index:0;pointer-events:none;overflow-x:clip;overflow-y:visible}
.stroke{width:100%;height:100%;display:block;transform-origin:50% 50%;overflow:visible}
.band > .wrap{position:relative;z-index:1}

/* Words on a stroke: the paint is drawn behind the letters rather than
   under them, a little larger than the line in both directions, so it runs
   past the ends the way a highlight laid with a brush does. The span makes
   its own stacking context, which is what keeps a z-index of -1 inside the
   headline instead of behind the photograph the headline is on. */
.hl{position:relative;display:inline-block;isolation:isolate;padding:0 .12em;color:var(--ink)}
.hl .hl-stroke{position:absolute;inset:-9% -2%;width:104%;height:118%;z-index:-1;overflow:visible}
.hl em{position:relative;font-style:normal}

/* --- two colours the paint mechanics' own blocks cannot supply ---------
   The lettered wordmark and the wall's sound toggle are drawn by blocks
   that belong to the paint mechanics (LETTER and WALL, below), and those
   blocks are not edited here. Both name a colour that was written for a
   dark page: the wordmark is --paper, which is now the ground it stands
   on, and the toggle is --muted, which is now ink. They are recoloured
   from here, scoped rather than shouted — no !important, just a selector
   that says where it applies.                                            */
.spray-band .spray-word{color:var(--ink)}
body .paint-snd{color:var(--on-ink-mute);border-color:var(--line-on-ink)}

/* --- closing call to action ------------------------------------------- */
.cta{border:1px solid var(--line);border-radius:var(--radius);background:var(--paper-2);padding:clamp(1.8rem,5vw,3.4rem)}
.cta h2{margin-bottom:.7rem}
.cta p{color:var(--muted);max-width:60ch}
.cta .btn-row{margin-top:1.6rem}

/* --- footer -----------------------------------------------------------
   One of the three black bands the flipped canvas keeps, and the last
   thing on every page: the wordmark and how to reach him on the left, the
   work and the company beside it, headings in the headline face. */
footer{background:var(--ink);color:var(--on-ink);padding:clamp(3.4rem,7vw,5rem) 0 1.8rem}
footer .brand{color:var(--on-ink)}
footer h4{
  font-family:var(--font-tall);font-weight:400;text-transform:uppercase;
  font-size:1.35rem;letter-spacing:.04em;color:var(--on-ink);
}
.foot-grid{display:grid;gap:2.4rem}
.foot-brand p{color:var(--on-ink-mute);max-width:36ch;margin:1.1rem 0}
.foot-contact li{margin-bottom:.4rem}
.foot-contact a{display:inline-flex;align-items:center;gap:.55rem;color:var(--on-ink-mute);transition:color .2s}
.foot-contact a:hover{color:var(--mint)}
.foot-contact svg{width:16px;height:16px;flex:none;color:var(--on-ink-mute)}
footer h4{margin-bottom:1rem}
footer .foot-grid ul li{margin-bottom:.5rem}
footer .foot-grid ul a{color:var(--on-ink-mute);transition:color .2s}
footer .foot-grid ul a:hover{color:var(--on-ink)}
.foot-bottom{display:flex;flex-wrap:wrap;justify-content:space-between;gap:.6rem 1.6rem;margin-top:clamp(2.2rem,5vw,3.2rem);padding-top:1.4rem;border-top:1px solid var(--line-on-ink);color:var(--on-ink-mute);font-size:.85rem}
.foot-credit{flex-basis:100%;font-size:.72rem;opacity:.8}
.foot-credit a{color:inherit;text-decoration:underline;text-underline-offset:2px}

@media (min-width:640px){.foot-grid{grid-template-columns:1fr 1fr}.foot-brand{grid-column:1 / -1}}
@media (min-width:960px){.foot-grid{grid-template-columns:1.5fr 1fr 1fr}.foot-brand{grid-column:auto}}
/* Five columns since the search pages (2026-10-11): the brand, the work, the
   services, the cities and the company. */
@media (min-width:960px){.foot-grid{grid-template-columns:1.4fr 1fr 1fr 1fr 1fr}}

/* =====================================================================
   MOTION — Roll · Scale · Cure

   The contract, and it is not negotiable: the pages render complete without
   JavaScript. Everything that hides anything lives under html.motion, which
   the inline bootstrap in <head> adds only when a script runs and the visitor
   has not asked for reduced motion — and every hidden thing carries
   `animation:oaSelf 0s linear 2.8s forwards`, so a page whose script never
   arrives still shows all of its text 2.8 seconds later. One observer adds
   .in. No counters, no scores, no labels: the murals do the talking.

     Roll   a reveal arrives like a roller pass — a clip-path wipe
            left-to-right with the last 18px of rise.
     Roll it in
            the same pass, over a card's photograph: it waits under
            primer and is rolled in as it arrives, one card 120ms behind
            the last, or at once if the pointer reaches it first. It
            stays painted. See ROLL IT IN, below.
     Scale  the dimension figures widen into place, wdth 100 -> 125.
     Cure   one slow mint sheen down the rule under a figure as it lands,
            once, never repeated.
     Brush  the mint rule his three stages sit on is painted in by a brush
            riding its leading edge as the visitor scrolls, and un-painted
            as they scroll back up.
     Stencil a card of matte board, cut to the figure's own glyphs, comes
            down over a figure as it arrives; the numbers are sprayed
            through the cut and the card lifts off, leaving the overspray.
     Tip    the tin on the Services hero goes over when it is clicked or
            dragged past forty degrees, pours a sheet of mint down the
            hero, pools at its foot and drips through onto the section
            below, where it becomes that section's rule. See TIP, below.
   ===================================================================== */

/* --- Roll ------------------------------------------------------------- */
/* The wipe is a mask, not a clip-path. clip-path on the observed element
   collapses its intersection rectangle to nothing, so the observer that is
   supposed to reveal it never fires and the section sits there until the
   2.8s self-reveal bails it out. A mask is paint-only: the observer still
   sees the full box. The 88% stop leaves a soft wet edge on the roller. */
html.motion .rv{
  opacity:0;transform:translateY(18px);
  -webkit-mask-image:linear-gradient(90deg,var(--paper) 88%,transparent);
          mask-image:linear-gradient(90deg,var(--paper) 88%,transparent);
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-size:0% 100%;mask-size:0% 100%;
  transition:opacity .5s var(--ease),transform .8s var(--ease),
             -webkit-mask-size .8s var(--ease-roll),mask-size .8s var(--ease-roll);
  animation:oaSelf 0s linear 2.8s forwards;
}
html.motion .rv.rv.in{opacity:1;transform:none;-webkit-mask-size:130% 100%;mask-size:130% 100%;animation:none}
.rv.in{opacity:1;transform:none;animation:none}
@keyframes oaSelf{to{opacity:1;transform:none;-webkit-mask-size:130% 100%;mask-size:130% 100%}}
/* A roller does not repaint the whole wall at once. */
html.motion .rv-d1{transition-delay:.08s}
html.motion .rv-d2{transition-delay:.16s}
html.motion .rv-d3{transition-delay:.24s}

/* --- Scale ------------------------------------------------------------ */
/* The figures are never hidden — they arrive narrow and widen out to their
   real width, so a visitor who never gets the script still reads 81' x 80'
   at full width. The type itself is set with the component. */
.dims{position:relative}
html.motion .dims{
  font-variation-settings:'wdth' 100,'wght' 800;
  transition:font-variation-settings .9s var(--ease);
  animation:oaDims 0s linear 2.8s forwards;
}
html.motion .dims.dims.in{font-variation-settings:'wdth' 125,'wght' 800;animation:none}
@keyframes oaDims{to{font-variation-settings:'wdth' 125,'wght' 800}}

/* --- Cure ------------------------------------------------------------- */
/* One pass of mint down the rule, then it is done for good: .in is added
   once and the observer stops watching. */
/* The sheen is a background travelling inside the rule, not an element
   sliding along it. It used to be a 40%-wide box translated 250% of its own
   width, which worked on a card — .pcard clips — and on a project page, where
   the figures sit in an unclipped .wrap, pushed the document 150px wider than
   the window and gave the whole page a horizontal scrollbar. A background can
   be positioned outside its box; it cannot overflow it. */
html.motion .dims.in::before{
  content:"";position:absolute;left:0;right:0;bottom:-.16em;height:1px;
  background:linear-gradient(90deg,transparent,var(--mint),transparent) no-repeat;
  background-size:40% 100%;
  animation:oaCure 1.8s var(--ease) var(--st-cure,.4s) 1 both;pointer-events:none;
}
@keyframes oaCure{
  from{background-position:-45% 0;opacity:0}
  18%{opacity:1}
  to{background-position:145% 0;opacity:0}
}

/* --- Brush ------------------------------------------------------------ */
/* The seventh verb, and the second that runs both ways. The owner: "Add an
   animated paintbrush in more places on his site." So the mint rule his three
   stages sit on is not drawn any more, it is painted: a brush rides the
   leading edge as the visitor scrolls down and retreats as they scroll back
   up, and each stage takes its Cure sheen as the brush passes it.

   The rule, its track and the brush are all in the server HTML and only the
   motion is gated, which keeps the promise the rest of this file keeps: with
   no script — and for a visitor who asked for reduced motion, who never gets
   html.motion — the rule is simply a finished mint rule and there is no
   brush. The track is absolutely positioned and its own height, with the line
   on its bottom edge sitting over the top hairline of the stage list: it adds
   nothing to the layout, and because it clips, a brush at the end of the rule
   can never widen the document (the lesson written up under Cure). --paint,
   0 to 1, is the one number site.js writes; --bw is how big the brush is
   drawn and --bh how far it stands above the line, which is what the track
   has to be tall enough to hold. The brush is the photographed sash brush
   (.pbrush, below), hung off the middle of its chisel edge, which is what
   puts its bristles on the line at any size. */
.beatrule{position:relative;--bw:72px;--bh:calc(var(--bw) * .78)}
@media (max-width:700px){.beatrule{--bw:50px}}
.beatrule-track{
  position:absolute;left:0;right:0;top:calc(var(--bh) * -1);
  height:calc(var(--bh) + 6px);overflow:hidden;pointer-events:none;
}
.beatrule-line{
  position:absolute;left:0;right:0;top:var(--bh);height:2px;
  background:var(--mint);transform-origin:left center;
}
.beatrule-pos{display:none}
html.motion .beatrule-line{transform:scaleX(var(--paint,0))}
/* The positioner is the width of the track, so one translateX in per cent of
   itself carries the brush and its wet edge along the whole rule. It lifts
   off over the last stretch rather than being sliced by the track's edge. */
html.motion .beatrule-pos{
  display:block;position:absolute;left:0;top:0;width:100%;height:100%;
  transform:translateX(calc(var(--paint,0) * 100%));
  opacity:clamp(0,calc((1 - var(--paint,0)) * 12),1);
}
/* The wet edge behind the brush is the coral (owner, 2026-09-19): the rule
   he is painting is mint, and the paint that is still wet on it carries the
   second colour, so the one place on the page where the brush is actually
   touching the wall has both paints in it. It is a ridge of wet paint, not
   a glow: a 4px band with a hard right edge where the bristles are.

   It is hung off the positioner's left edge and pulled back by its own
   width, the way Roll hangs .pcard-wet off the roller -- right:0 put it at
   the far end of a box that is itself the width of the whole rule, which
   is a per cent of a rule past the end of the track that clips it, so the
   wet edge was never on screen at all. It is now where the bristles are. */
html.motion .beatrule-wet{
  position:absolute;left:0;top:calc(var(--bh) - 1px);height:4px;
  width:calc(var(--bw) * 1.2);margin-left:calc(var(--bw) * -1.2);
  border-radius:2px;
  background:linear-gradient(90deg,var(--pop-wet-0),var(--pop-wet));
}
/* The brush is the photograph (owner, 2026-10-08: "there's still a generic
   paint brush"): .beatrule-brush is a point on the line at the bristles,
   and the .pbrush hangs off it by its own chisel and is turned about it,
   36 degrees off the level, so the hairs are on the leading end of the
   paint at any --bw. It is drawn longer than the round brush was (--bw
   went from 64/44 to 72/50 with it, 2026-10-08) because a photograph of a
   flat brush needs the size to read as one; turned, it still stands
   inside the track's --bh. */
html.motion .beatrule-brush{
  position:absolute;left:0;top:calc(var(--bh) + 1px);width:0;height:0;
}
html.motion .beatrule-brush .pbrush{--pb-l:calc(var(--bw) * 1.15);--pb-r:-36deg}
/* The fallback, art.py brush_rule_svg(): a 200 x 200 box whose contact
   point is 26% across and 83% down. */
html.motion .beatrule-brush > svg{
  position:absolute;left:0;top:0;display:block;width:var(--bw);height:var(--bw);
  margin-left:calc(var(--bw) * -.26);margin-top:calc(var(--bw) * -.83);
}
/* Cure, fired by the brush rather than by arriving on screen: the same sheen
   travelling the same way, under the stage's number, every time the brush
   goes past it in either direction. .cured is the state — the brush is past
   this stage — and .curing is the pass that just happened; site.js takes it
   off and puts it back so the sheen runs again on a repaint. */
html.motion .beat h3{position:relative}
html.motion .beats-full .beat h3{--cy:2.1rem;--cw:2.6rem}
html.motion .beat.curing h3::after{
  content:"";position:absolute;left:0;top:var(--cy,5px);
  width:var(--cw,34px);height:1px;
  background:linear-gradient(90deg,transparent,var(--mint),transparent) no-repeat;
  background-size:60% 100%;
  animation:oaCure 1.4s var(--ease) 1 both;pointer-events:none;
}

/* =====================================================================
   LIVE — the figures roll

   The sixth verb, and the first that runs both ways. The owner: "Try to
   animate the numbers on his site ... I'd like him to see motion as he
   scrolls up and down from them." So every figure on the site is a
   mechanical counter: each digit a column of 0-9 that rolls up to its
   value as the figure comes on screen and back to 0 as it leaves, every
   time, rather than the once-and-done of Roll and Cure.

   The figure in the server HTML is the figure: site.js leaves the real
   characters in place as .roll-g and lays the columns over them, so the
   box is the text's own box and nothing can shift. .roll-g goes
   transparent rather than hidden — it is still the text a screen reader
   reads and a visitor selects, and the columns over it are aria-hidden.

   The geometry, once: the window is one line tall plus a sixth of an em
   of air at each end, pulled back by an equal negative margin so the line
   box is unchanged. Each wheel is a grid of ten cells one window high, so
   the roll is translateY of a whole number of windows and the digit lands
   exactly where the text's digit was — the air is what stops a tall glyph
   being clipped at the top of its window. Nothing here exists without
   html.motion: with no script the figure is the text and that is all.
   ===================================================================== */
html.motion .roll{position:relative;display:inline-block;font-variant-numeric:tabular-nums}
html.motion .roll-g{color:transparent}
html.motion .roll-w{position:absolute;inset:0;display:flex;align-items:stretch;overflow:hidden}
html.motion .roll-x{flex:none;display:flex;align-items:center}
html.motion .roll-d{flex:none;position:relative;overflow:hidden;height:calc(100% + .34em);margin:-.17em 0}
html.motion .roll-c{
  display:grid;grid-auto-rows:100%;height:100%;
  transform:translateY(calc(var(--d,0) * -100%));
  transition:transform var(--rt,.6s) cubic-bezier(.22,.78,.26,1) var(--rd,0s);
}
html.motion .roll-c > span{display:flex;align-items:center;justify-content:center}
/* .in is added on the way in and taken away on the way out: one declaration
   is the whole of both directions. */
html.motion .roll.in .roll-c{--d:var(--v)}

/* --- Stencil ----------------------------------------------------------
   The eighth verb, and the one that happens to the hook itself. A figure
   does not simply arrive any more: a stencil card comes down over it, the
   numbers are sprayed through the cut, the card lifts off, and the
   overspray stays on the wall.

   The cut is the figure's own glyphs and never a drawing of digits.
   site.js measures every run of text on the line where it actually sits
   and lays it into an SVG <mask> in the same font at the same size, cut a
   hair wide the way a stencil is cut. The card is built at the moment it
   is wanted and removed when it is gone.

   It composes with the two verbs already on a figure rather than
   replacing either. Scale's grow is snapped to its end value under the
   card — which is where it was going anyway, and what keeps the cut true
   to the glyphs it was measured from — Live's columns roll under the card
   from the moment it starts down, and the spray is the moment they land.
   Cure is not retimed by hand: the figure carries --st-cure, so the sheen
   sets off as the card is lifted and, where no stencil ever runs, at the
   .4s it has always run at.

   Nothing here can move a box: the card and the halo are absolutely
   positioned inside a figure that never clips them. And nothing here
   exists without html.motion — site.js builds no card, no halo and no
   filter at all, so the no-script page is the page it has always been. */
html.motion .st-card,html.motion .st-halo{position:absolute;pointer-events:none;-webkit-user-select:none;user-select:none}

/* The card. It waits a card and a half above the figure and comes down
   the way a hand lays one on a wall; it goes back up faster than it came,
   which is the difference between placing a stencil and pulling one off. */
html.motion .st-card{
  z-index:1;display:block;opacity:0;
  transform:translate3d(0,-130%,0);
  box-shadow:0 14px 28px var(--st-shade);
}
html.motion .st-card svg{display:block;width:100%;height:100%}
html.motion .st-card .st-s0{stop-color:var(--matte)}
html.motion .st-card .st-s1{stop-color:var(--matte-2)}
html.motion .st-card .st-edge{fill:none;stroke:var(--matte-edge);stroke-width:1}
/* The cut itself. A card is held a little off the wall, so the shape of
   the figure is always a shade darker than the wall around it — which is
   also what makes the cut legible over a photograph rather than over ink. */
html.motion .st-card .st-cut{fill:var(--st-cut);transition:opacity .2s linear}
/* And it goes the moment the paint does. The card is still flat on the
   wall then and the paint has filled the cut anyway, so nothing is lost —
   what it saves is the lift: a shadow travelling up with the card would
   read as a second figure sliding off the first. */
html.motion .st-spray .st-cut{opacity:0}
html.motion .st-in .st-card{
  opacity:1;transform:none;
  transition:transform .18s cubic-bezier(.2,.86,.34,1),opacity .1s linear;
}
html.motion .st-lift .st-card{
  opacity:0;transform:translate3d(0,-150%,0);
  transition:transform .22s cubic-bezier(.45,0,.78,.4),opacity .22s ease-in .05s;
}

/* The figure under it. The width axis goes straight to where Scale was
   taking it, because the card is over the figure while it happens; the
   rolling columns are held out of sight until the paint goes on. The
   animation is this section's share of the 2.8s bail-out every hidden
   thing in this file carries: if the sequence never finishes, the figure
   comes back by itself rather than staying under a card for good. */
html.motion .dims.st{font-variation-settings:'wdth' 125,'wght' 800;transition:none}
html.motion .st .roll-w{opacity:0;transition:opacity .12s linear;animation:oaStFail 0s linear 2.8s forwards}
html.motion .st-spray .roll-w{opacity:1;transition:opacity .2s var(--ease)}
@keyframes oaStFail{to{opacity:1}}
/* The mint prime marks and the times sign are painted already: they sit
   over the card rather than being sprayed through it, and they take no
   overspray. The rolling columns sit over it too — what the cut shows is
   the wall until the can is opened. */
html.motion .roll,html.motion .dims .f,html.motion .dims .x,html.motion .stat-n .f{position:relative;z-index:2}
html.motion .stat-n{position:relative}

/* The overspray. A copy of the figure with no ink in it at all: what
   paints is the shadow, six to ten pixels of --paper spreading outside
   the glyphs. It is displaced by the tags' own spray filter for as long
   as the can is open — site.js puts that on and takes it off — and by
   nothing at all once it has dried, which is how a halo stays on the page
   with no live feTurbulence anywhere on it. */
html.motion .st-halo{inset:0;z-index:3;opacity:0;transition:opacity .42s var(--ease)}
html.motion .st-halo .roll-w{display:none}
html.motion .st-halo .roll-g{color:transparent;text-shadow:0 0 5px var(--spray-halo),0 0 11px var(--spray-halo)}
html.motion .st-halo .f,html.motion .st-halo .x{color:transparent;text-shadow:none}
html.motion .st-spray .st-halo{opacity:.35;transition-duration:.2s}
html.motion .st-done .st-halo{opacity:.12}
/* A zero-height inline-block sits its bottom edge on the baseline it is
   standing on, which is the only way to ask the page where a baseline is.
   site.js puts one in, reads it and takes it out again in the same tick. */
html.motion .st-probe{display:inline-block;width:0;height:0;vertical-align:baseline}
html.motion .st-defs{position:absolute;width:0;height:0;overflow:hidden}

/* --- Letter -----------------------------------------------------------
   Owner, 2026-09-19: "Is there any way to have a can of spray paint spray
   'Open Air Gallery' underneath the hero as a section builder?" — and on
   2026-10-01: "Change spray can into quality paint brush, and use better
   paint fx." So the band build.py lays down is lettered rather than
   printed: a double-loaded sash brush (art.py brush_write_svg()) comes
   down onto the first line, pulls it left to right, lifts, comes back and
   pulls the second. Each line arrives through a torn, bristled edge, with
   the wet double load — mint along the tops of the letters, coral along
   their feet, --mix where the two were worked together — riding right
   behind the hairs and drying back to the letters' own ink a brush-width
   behind. Two or three runs come off the heaviest letters once the brush
   has gone past them.

   The brush and the wet edge are in step by construction rather than by a
   number passed between them. One animation on the band, on one clock,
   moves a handful of registered custom properties: --lt-x and --lt-y are
   where the bristles touch, --lt-e1 and --lt-e2 are where each line's
   paint has reached. While a line is being written its edge and --lt-x
   take the same values at the same keyframe offsets, so at every frame
   the chisel is on the edge — whatever the easing, whatever the size.
   site.js only measures the word (the --lt-s/f/y lengths the keyframes
   are written in) and says go.

   It writes once on the way in, like Roll; a hover on a fine pointer and a
   tap on a coarse one write it again. Nothing in it can move a box: the
   brush, the wet clone and the runs are all absolutely positioned inside a
   stage that shrink-wraps the word, and a mask changes no metric of the
   text at all. And nothing in it survives without html.motion — the wet
   clone and the runs are site.js's and are never built, the brush is
   display:none, and the word is simply the word.                       */
@property --lt-x{syntax:'<length>';inherits:true;initial-value:0px}
@property --lt-y{syntax:'<length>';inherits:true;initial-value:0px}
@property --lt-e1{syntax:'<length>';inherits:true;initial-value:0px}
@property --lt-e2{syntax:'<length>';inherits:true;initial-value:0px}
@property --lt-l{syntax:'<number>';inherits:true;initial-value:0}
@property --lt-a{syntax:'<number>';inherits:true;initial-value:0}
@property --lt-d1{syntax:'<number>';inherits:true;initial-value:0}
@property --lt-d2{syntax:'<number>';inherits:true;initial-value:0}
/* The band clips sideways only: a brush coming in from the left is off
   the page, but its handle may lean up over the foot of the hero, because
   on the first line it is taller than the band's own top padding. clip on
   one axis leaves the other visible without making a scrollport. */
html.motion .spray-band{
  overflow:visible;overflow-x:clip;
  --sp-t:3.6s;--lt-bw:3em;--lt-ew:.62em;--lt-ww:1.7em;--lt-lead:2.2em;--lt-lift:.3em;
  --lt-s1:0px;--lt-f1:0px;--lt-s2:0px;--lt-f2:0px;--lt-y1:0px;--lt-y2:0px;
}

/* Each line waits behind an edge that is nowhere yet. Two mask layers: the
   bristled edge site.js draws (alpha only — streaks of uneven length that
   run out through their own gradients, a third of them broken where hairs
   have run dry), hung so its leading tip is the line's --lt-e; and a solid
   body behind it, up to half the edge's width back. Until site.js has
   drawn the edge the fallback is a plain feather, so no frame ever shows
   the letters before the brush gets to them.

   The word carries this section's share of the 2.8s bail-out every hidden
   thing in this file carries: if site.js never arrives, the edges run out
   past the word and it paints itself in. site.js calls it off the moment
   it binds (.lt-ready), the way .rollin and .tipready call off theirs. */
html.motion .spray-band[data-spray] .spray-word > span{
  --lt-e:var(--lt-e1);
  -webkit-mask-image:var(--lt-edge,linear-gradient(90deg,var(--ink) 50%,transparent)),linear-gradient(var(--ink),var(--ink));
          mask-image:var(--lt-edge,linear-gradient(90deg,var(--ink) 50%,transparent)),linear-gradient(var(--ink),var(--ink));
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-size:var(--lt-ew) 100%,max(0px,calc(var(--lt-e) - var(--lt-ew) * .5)) 100%;
          mask-size:var(--lt-ew) 100%,max(0px,calc(var(--lt-e) - var(--lt-ew) * .5)) 100%;
  -webkit-mask-position:calc(var(--lt-e) - var(--lt-ew)) 0,0 0;
          mask-position:calc(var(--lt-e) - var(--lt-ew)) 0,0 0;
}
html.motion .spray-band[data-spray] .spray-word > span:nth-child(2){--lt-e:var(--lt-e2)}
html.motion .spray-band[data-spray] .spray-word{animation:oaLetterFail 0s linear 2.8s forwards}
@keyframes oaLetterFail{to{--lt-e1:100000px;--lt-e2:100000px}}
html.motion .spray-band.lt-ready .spray-word{animation:none}
/* Dry: the masks come off outright rather than being left at their end
   values, so a settled band carries nothing at all on its type. */
html.motion .spray-band.spray-done .spray-word > span{-webkit-mask-image:none;mask-image:none}
html.motion .spray-band.spray-done .spray-wet{display:none}

/* The wet band: a clone of the word with no ink of its own, whose lines
   are filled with the double load and clipped to their glyphs. The load is
   hung so it ends at the edge — strongest right behind the hairs, fading
   back to nothing — so the letters are wet and coloured just behind the
   brush and their own ink a brush-width back. It wears the same masks as
   the word, so its leading edge is torn by the same bristles. */
html.motion .spray-wet{
  position:absolute;inset:0;z-index:1;pointer-events:none;
  -webkit-user-select:none;user-select:none;
}
html.motion .spray-band .spray-wet > span{
  color:transparent;
  background-image:var(--lt-wet,none);background-repeat:no-repeat;
  background-size:var(--lt-ww) 100%;
  background-position:calc(var(--lt-e) - var(--lt-ww)) 0;
  -webkit-background-clip:text;background-clip:text;
  opacity:var(--lt-d1);
}
html.motion .spray-band .spray-wet > span:nth-child(2){opacity:var(--lt-d2)}

/* The clock. 14-50% is the first line and 63-96% the second; between
   them the brush lifts (--lt-l) and goes back. --lt-e1 is held at the
   start of its line until the brush lands there and at the end of it from
   then on, so the paint is never ahead of the hairs. */
html.motion .spray-band.spray-go{animation:oaLetter var(--sp-t) linear both}
@keyframes oaLetter{
  0%{--lt-x:calc(var(--lt-s1) - var(--lt-lead));--lt-y:var(--lt-y1);--lt-l:1;--lt-a:0;
     --lt-e1:var(--lt-s1);--lt-e2:var(--lt-s2);--lt-d1:1;--lt-d2:1}
  6%{--lt-a:1}
  14%{--lt-x:var(--lt-s1);--lt-l:0;--lt-e1:var(--lt-s1)}
  50%{--lt-x:var(--lt-f1);--lt-y:var(--lt-y1);--lt-l:0;--lt-e1:var(--lt-f1);--lt-d1:1}
  56%{--lt-l:1}
  63%{--lt-x:var(--lt-s2);--lt-y:var(--lt-y2);--lt-l:0;--lt-e2:var(--lt-s2);--lt-d1:0}
  96%{--lt-x:var(--lt-f2);--lt-e2:var(--lt-f2);--lt-a:1;--lt-l:0;--lt-d2:1}
  100%{--lt-x:calc(var(--lt-f2) + var(--lt-lead) * .5);--lt-y:var(--lt-y2);--lt-l:1;--lt-a:0;
       --lt-e1:var(--lt-f1);--lt-e2:var(--lt-f2);--lt-d1:0;--lt-d2:.4}
}

/* The brush. The track is the stage; the one moving box is .spray-pos, a
   point at the bristles carried by --lt-x and --lt-y and lifted by --lt-l.
   The drawing hangs off that point by art.py's BRUSH_WRITE_TIP — 15%
   across and 80% down its 280 x 160 box — so the chisel is on the wet
   edge at any size, and it tips back a few degrees as it lifts, the way
   a wrist does. It exists only while the brush is out. */
/* The track is laid out (but hidden) under html.motion from the start, so
   the brush's lazy photograph is fetched as the band comes near rather than
   on the first frame of the stroke; outside html.motion it is display:none
   and never fetched at all. */
html.motion .spray-track{
  display:block;position:absolute;left:0;top:0;width:100%;height:100%;
  z-index:2;pointer-events:none;visibility:hidden;
}
html.motion .spray-band.spray-go .spray-track{visibility:visible}
html.motion .spray-pos{
  position:absolute;left:0;top:0;width:0;height:0;will-change:transform;
  transform:translate3d(var(--lt-x),calc(var(--lt-y) - var(--lt-l) * var(--lt-lift)),0);
  opacity:var(--lt-a);
}
html.motion .spray-brush{
  position:absolute;left:0;top:0;width:0;height:0;
  transform:rotate(calc(var(--lt-l) * -7deg));
}
/* The photographed brush (owner, 2026-10-08), hung off the middle of its
   chisel and turned 25 degrees off the level — art.py's BRUSH_WRITE_DEG, 65
   off the upright — so the handle leans forward over the letters still to
   come and the hairs drag back over the paint just laid. Its length is the
   drawn brush's: .91 of --lt-bw. */
html.motion .spray-brush .pbrush{--pb-l:calc(var(--lt-bw) * .91);--pb-r:-25deg}
/* The fallback, art.py brush_write_svg(): a 280 x 160 box whose chisel is
   at BRUSH_WRITE_TIP, 15% across and 80% down. */
html.motion .spray-brush > svg{
  position:absolute;left:0;top:0;display:block;overflow:visible;
  width:var(--lt-bw);height:calc(var(--lt-bw) * .5714);
  margin-left:calc(var(--lt-bw) * -.15);margin-top:calc(var(--lt-bw) * -.4571);
}

/* --- The photo brush ---------------------------------------------------
   One photograph of a real sash brush (build.py photo_brush(), cut out by
   tools/make-paintbrush.py; docs/images.md "The brush"), lying side-on with
   its tip to the left. The box is hung so the middle of the chisel edge
   (--pb-tx, --pb-ty: build.py PB_TIP, checked at build time) is on the
   point its parent stands at, and turned about that point by --pb-r. Its
   size is --pb-l, the brush's length.

   The paint is not in the photograph. .pbrush-load multiplies a tint from
   the tokens into the hairs, so the bristles' own texture shows through
   it, and .pbrush-wet is the opaque lip of wet paint on the chisel with a
   gloss on it. Both are masked twice: to the bristles (paintbrush-bristles,
   cut from the same matte, so no paint is ever on the ferrule or the wood)
   and to how far up the hairs the paint has wicked. The box is its own
   stacking context (the transform makes it one, isolation says so), so the
   multiply is into the photograph and never into the page behind it. */
.pbrush{
  --pb-ar:0.2138; --pb-tx:0.011; --pb-ty:0.498;
  --pb-l:64px;--pb-r:0deg;
  position:absolute;left:0;top:0;display:block;isolation:isolate;
  width:var(--pb-l);height:calc(var(--pb-l) * var(--pb-ar));
  margin-left:calc(var(--pb-l) * var(--pb-tx) * -1);
  margin-top:calc(var(--pb-l) * var(--pb-ar) * var(--pb-ty) * -1);
  transform-origin:calc(var(--pb-tx) * 100%) calc(var(--pb-ty) * 100%);
  transform:rotate(var(--pb-r));
  filter:drop-shadow(0 calc(var(--pb-l) * .02) calc(var(--pb-l) * .02) var(--line-2));
}
.pbrush picture,.pbrush img{display:block;width:100%;height:100%}
.pbrush img{pointer-events:none;-webkit-user-drag:none;user-select:none;-webkit-user-select:none}
.pbrush-load,.pbrush-wet{
  position:absolute;inset:0;pointer-events:none;
  -webkit-mask-image:url(../assets/img/paintbrush-bristles.png),var(--pb-reach);
          mask-image:url(../assets/img/paintbrush-bristles.png),var(--pb-reach);
  -webkit-mask-size:100% 100%;mask-size:100% 100%;
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-composite:source-in;mask-composite:intersect;
}
.pbrush-load{mix-blend-mode:multiply;background:var(--pb-load)}
/* Letter: double-loaded — the mint corner along the top of the stroke, the
   coral along the feet, the two worked together into --mix through the
   middle; wicked a third of the way up the hairs, with the wet lip at the
   chisel in the same three paints. */
.pbrush-write{
  --pb-load:linear-gradient(180deg,var(--mint) 0 36%,var(--mix) 46% 54%,var(--pop) 64% 100%);
  --pb-reach:linear-gradient(90deg,var(--ink) 0 11%,transparent 22%);
}
.pbrush-write .pbrush-wet{opacity:.8;
  background:
    linear-gradient(180deg,var(--gloss-0) 8%,var(--gloss) 16%,var(--gloss-0) 24%),
    linear-gradient(180deg,var(--mint) 0 36%,var(--mix) 46% 54%,var(--pop-mid) 64% 100%);
  --pb-reach:linear-gradient(90deg,var(--ink) 0 3.5%,transparent 10%);
}
/* Brush: the mint the rule is painted in, and the coral wet edge on the
   very tips of the hairs, where they touch. */
.pbrush-rule{
  --pb-load:linear-gradient(90deg,var(--pop) 0 3%,var(--mint) 6% 100%);
  --pb-reach:linear-gradient(90deg,var(--ink) 0 10%,transparent 20%);
}
.pbrush-rule .pbrush-wet{
  opacity:.85;
  background:linear-gradient(90deg,var(--pop-mid) 0 2.2%,var(--mint) 4%);
  --pb-reach:linear-gradient(90deg,var(--ink) 0 4%,transparent 11%);
}

/* The runs off the lettering are the coral of the feet (site.js gives
   each one its --drip), wet: a bead with a gloss on its shoulder and a
   darker rim underneath, so it reads as paint and not as a dot. */
html.motion .spray-band .spray-drip{--drip-0:var(--pop-wet-0)}
html.motion .spray-band .spray-bead{
  background:radial-gradient(circle at 34% 30%,var(--gloss) 0 16%,var(--drip) 30%);
  box-shadow:inset 0 calc(var(--dw) * -.28) 0 var(--pop-rim);
}

/* The runs. Each one is a body of paint scaled down from the letter it
   came off and a bead carried to the end of it — two transforms on the
   same clock rather than a height, so a drip costs a layout nothing.
   site.js sets where it starts, how long it runs and when: after the
   brush. Tip's run through onto the next section is this same markup. */
html.motion .spray-drip{
  position:absolute;z-index:1;pointer-events:none;
  width:var(--dw);height:var(--dh);opacity:0;
}
html.motion .spray-drip.run{animation:oaSprayWet .18s linear both}
html.motion .spray-run{
  position:absolute;inset:0;transform:scaleY(0);transform-origin:top center;
  border-radius:0 0 var(--dw) var(--dw);
  background:linear-gradient(180deg,var(--drip-0),var(--drip) 55%);
}
html.motion .spray-bead{
  position:absolute;left:50%;top:0;
  width:calc(var(--dw) * 1.7);height:calc(var(--dw) * 1.7);
  margin-left:calc(var(--dw) * -.85);border-radius:50%;
  background:var(--drip);transform:translateY(-50%);
}
html.motion .spray-drip.run .spray-run{animation:oaSprayRun var(--dt) cubic-bezier(.34,.06,.28,1) both}
html.motion .spray-drip.run .spray-bead{animation:oaSprayBead var(--dt) cubic-bezier(.34,.06,.28,1) both}
@keyframes oaSprayWet{to{opacity:1}}
@keyframes oaSprayRun{from{transform:scaleY(0)}to{transform:scaleY(1)}}
@keyframes oaSprayBead{from{transform:translateY(-50%)}to{transform:translateY(calc(var(--dh) - 50%))}}

/* --- the cards --------------------------------------------------------
   "Add more motion to the cards." Three things, composed on top of the
   hover the card already had rather than in place of it:

     drift  the photograph lags its frame as the card rides up the screen.
            The picture is 12% taller than the frame and hung 6% above it,
            so it can move 6% either way and still cover — no gap, ever,
            and the frame's box does not change.
     rise   the card comes up 18px into place and settles back down as it
            leaves, both ways, off .in. Roll still runs once: .rolled is
            added on the first crossing and is what holds the wipe open
            afterwards, so the reveal is not replayed on every pass.
     tilt   four degrees toward the pointer, desktop only, reset on leave.

   Every one of them is a custom property on one transform, so the hover
   lift, the tilt and the rise never fight over the same declaration.     */
html.motion .rv.rolled{opacity:1;-webkit-mask-size:130% 100%;mask-size:130% 100%;animation:none}
html.motion .pcard[data-live].rv{
  transform:perspective(900px) translate3d(0,var(--lift,18px),0)
            rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg));
  transition:border-color .25s,transform .55s var(--ease);
}
html.motion .pcard[data-live].rv.in{--lift:0px;will-change:transform}
html.motion .pcard[data-live].rv.in:hover,
html.motion .pcard[data-live].rv.in:focus-visible{--lift:-3px}
/* Under the pointer the card has to keep up with it. */
html.motion .pcard[data-live].rv.is-tilt{transition:border-color .25s,transform .1s linear}
html.motion .pcard[data-live] .pcard-img{position:relative;isolation:isolate}
html.motion .pcard[data-live] .pcard-img picture{
  position:absolute;left:0;right:0;top:-6%;height:112%;
  transform:translate3d(0,calc(var(--drift,0) * 6%),0);
}

/* --- the ambient glow --------------------------------------------------
   "an ambient glow in places that's tasteful." Tasteful is written down
   here as four rules, and the code keeps all four: mint at 9% at its very
   brightest and nothing over that; the softness is the gradient's own
   falloff, because a filter blur on page content is not something this
   site does; the core never sits under type and never crosses a
   photograph — the four cores land on empty canvas, and the one under the
   call to action is behind an opaque panel, so only the halo escaping
   round it is ever seen; and one glow is lit at a time, which site.js
   enforces by lighting only the one nearest the middle of the screen.

   The box is the section, exactly, so the light can never widen the
   document or reach for a scrollbar. --g is the scroll value site.js
   writes, --gx/--gy place the core, and the breath is on ::before so the
   six-second idle and the scroll value never fight over one property.  */
.glow{position:absolute;inset:0;pointer-events:none}
html.motion .glow{
  z-index:-1;overflow:hidden;
  opacity:var(--g,.45);transition:opacity .3s linear;
}
html.motion .glow::before{
  content:"";position:absolute;inset:0;
  background:radial-gradient(26% 46% at var(--gx,50%) var(--gy,50%),
             var(--glow) 0%,var(--glow-soft) 46%,var(--glow-0) 100%);
  animation:oaBreath 6s ease-in-out infinite;
}
@keyframes oaBreath{
  0%,100%{opacity:.74;transform:scale(.96)}
  50%{opacity:1;transform:scale(1.04)}
}
/* A section holding a glow has to be its own stacking context, or z-index:-1
   drops the light behind the section's background — which on the two .alt
   bands means behind --ink-2, where nothing can be seen of it. Isolated, the
   negative layer lands where it is meant to: above the panel's own
   background, below every word and photograph on it. */
html.motion .stats-band,html.motion .pair-band,
html.motion .cta-wrap,html.motion .pintro{isolation:isolate}
/* Where each core sits: off the type, off the photographs, and far enough
   apart down the page that two are never on one screen anyway. */
html.motion .stats-band .glow{--gx:68%;--gy:76%}
html.motion .pair-band .glow{--gx:76%;--gy:26%}
html.motion .cta-wrap .glow{--gy:82%}
html.motion .pintro .glow{--gx:74%;--gy:20%}

/* =====================================================================
   ROLL IT IN — the murals go up under a roller

   Concept 2, and Roll's second use: the first is a reveal that arrives on
   its own, this one is a reveal the visitor lays down. Every photograph on
   a card waits under primer, and it is rolled in under a single pass that
   leaves it painted for good.

   Three layers inside a box that already clips, and none of them changes
   the box:

     the primer   a filter on the photograph itself (--primer), under
                  html.motion and only until the card is .painted. It is
                  on the img from the very first paint — html.motion is
                  set by the bootstrap in <head>, before the body is
                  parsed — so there is no frame of full colour to flash.
                  The 2.8s oaPrimed is the same bail-out every hidden
                  thing on this site carries: if site.js never arrives the
                  primer lifts by itself rather than stranding the murals
                  under it. site.js adds .rollin the moment it runs, which
                  calls the bail-out off.
     the paint    a second, unprimed copy of the <picture>, cloned by
                  site.js and masked exactly the way .rv is masked — a
                  mask, not a clip-path, for the same reason as Roll. Its
                  mask grows from 0 to 116% of the card, and because the
                  gradient goes opaque to 86% of itself the painted edge
                  is at .86 x 1.16 = 100% of the card at the end of the
                  pass, to the pixel. When the pass finishes the primer
                  comes off the photograph underneath and the copy is
                  removed in the same frame: the same pixels, one layer.
     the roller   art.py's, at --rw, riding that edge. The track is the
                  width of the card and translates from 0 to 100% of
                  itself over the same 600ms with the same easing, so the
                  nap and the painted edge stay together without a number
                  passing between them. The nap is the track's left edge —
                  the two margins are art.py's contact point — and the wet
                  sheen is the strip immediately behind it.

   Every card rolls itself in as it arrives, desktop and phone alike, 120ms
   behind the one before it and the first of a run after a 350ms grace: a
   portfolio can never sit whitewashed waiting for a hover. The hover is the
   shortcut — a card the pointer reaches before the stagger does paints at
   once, under the roller cursor — which also means the cursor is a passing
   thing, handed back as soon as the last card lands. Either way it is once
   and for good: nothing here un-paints on the way back up.              */
html.motion .pcard{--rw:96px;--roll-t:.6s}
@media (max-width:700px){html.motion .pcard{--rw:68px}}

html.motion .pcard-img picture:not(.pcard-paint) img{
  filter:var(--primer);
  animation:oaPrimed 0s linear 2.8s forwards;
}
html.motion.rollin .pcard-img picture:not(.pcard-paint) img{animation:none}
html.motion .pcard.painted .pcard-img img{filter:none;animation:none}
@keyframes oaPrimed{to{filter:none}}

html.motion .pcard-paint{
  z-index:1;
  -webkit-mask-image:linear-gradient(90deg,var(--paper) 86%,transparent);
          mask-image:linear-gradient(90deg,var(--paper) 86%,transparent);
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-position:0 0;mask-position:0 0;
  -webkit-mask-size:0% 100%;mask-size:0% 100%;
  transition:-webkit-mask-size var(--roll-t) var(--ease-roll),
                     mask-size var(--roll-t) var(--ease-roll);
}
html.motion .pcard.rolling .pcard-paint{-webkit-mask-size:116% 100%;mask-size:116% 100%}

/* The track: the whole width of the card, carried across it by one
   translate. Nothing in it is ever measured — the roller is hung off the
   track's left edge by art.py's two fractions, so it sits on the wet edge
   at any --rw. It fades in as the pass starts and lifts off as it ends,
   on its own linear clock, so the fade never fights the easing. */
.pcard-roll{display:none}
html.motion .pcard-roll{
  display:block;position:absolute;left:0;top:0;width:100%;height:100%;
  z-index:2;pointer-events:none;opacity:0;
  transform:translateX(0);
  transition:transform var(--roll-t) var(--ease-roll);
}
html.motion .pcard.rolling .pcard-roll{
  transform:translateX(100%);
  animation:oaRollOff var(--roll-t) linear both;
}
html.motion .pcard.painted .pcard-roll{display:none}
@keyframes oaRollOff{0%{opacity:0}10%{opacity:1}84%{opacity:1}100%{opacity:0}}
html.motion .pcard-roller{
  position:absolute;left:0;top:50%;width:var(--rw);height:var(--rw);
  margin-left:calc(var(--rw) * -.698);
  margin-top:calc(var(--rw) * -.669);
}
html.motion .pcard-roller svg{display:block;width:100%;height:100%;overflow:visible}
/* The paint that is still wet, directly behind the nap. Faint: this is a
   sheen on a photograph, not a bar of mint across it. */
html.motion .pcard-wet{
  position:absolute;top:0;bottom:0;left:0;
  width:calc(var(--rw) * 1.1);margin-left:calc(var(--rw) * -1.1);
  background:linear-gradient(90deg,var(--mint-wet-0),var(--mint-wet));
  opacity:.42;
}

/* --- and the way out -------------------------------------------------- */
/* html.motion is already withheld when the visitor asks for reduced motion.
   This is the second lock: if the class ever arrives anyway, nothing here
   moves and nothing here hides. */
@media (prefers-reduced-motion:reduce){
  html.motion .rv,html.motion .rv.in{opacity:1;transform:none;-webkit-mask-image:none;mask-image:none;transition:none;animation:none}
  html.motion .dims{font-variation-settings:'wdth' 125,'wght' 800;transition:none;animation:none}
  html.motion .dims.in::before{content:none;animation:none}
  /* The columns are never built under reduced motion — site.js returns
     before it touches the DOM. If they ever are, the figure is the text. */
  html.motion .roll-g{color:inherit}
  html.motion .roll-w{display:none}
  /* Stencil is built by site.js and site.js returns before it touches the
     DOM here, so there is no card and no overspray to hold still. This
     says it a second time, in case the class ever arrives anyway. */
  html.motion .st-card,html.motion .st-halo{display:none}
  html.motion .st .roll-w{opacity:1;animation:none}
  html.motion .pcard[data-live].rv{transform:none;transition:none}
  html.motion .pcard[data-live] .pcard-img picture{position:static;height:100%;transform:none}
  /* Roll it in: no primer, and the roller was never there. */
  html.motion .pcard-img img{filter:none;animation:none}
  html.motion .pcard-roll{display:none}
  /* Reduced motion gets no glow at all, rather than a still one. The
     bootstrap withholds html.motion from a visitor who has asked for
     reduced motion, so there was never going to be a glow to hold still:
     this says the same thing twice, on purpose. */
  html.motion .glow{display:none}
  /* Letter: the wordmark is the word, and there was never a brush.
     site.js builds no wet band and no runs here; this says it again. */
  html.motion .spray-word,html.motion .spray-word > span{-webkit-mask-image:none;mask-image:none;animation:none}
  html.motion .spray-track,html.motion .spray-wet,html.motion .spray-drip{display:none}
  /* Brush: the rule is painted and the brush was never there. */
  html.motion .beatrule-line{transform:none}
  html.motion .beatrule-pos{display:none}
  html.motion .beat.curing h3::after{content:none;animation:none}
  html{scroll-behavior:auto}
}

/* =====================================================================
   SPLAT — paint thrown from a button, and the brush between pages

   The fifth verb, and the only one a visitor sets off on purpose: a click on
   a button-styled link, anywhere on the site, bursts a mint splat out of the
   exact point the pointer hit and then a roller pass covers the screen. The
   same roller, with no splat in front of it, is the page transition for the
   nav, the wordmark and the footer's own links. Nothing here is
   load-bearing. The overlay does not exist in the server HTML; site.js
   builds it, only under html.motion, and only for a plain left click on a
   same-origin link — every other click is an ordinary click on an ordinary
   link, and with no script, or with reduced motion asked for, so is every
   click. The layer is pointer-events:none and aria-hidden, so nothing here
   is ever in the way of the page or of a screen reader.

   Two paints, and they mix. Round three (owner, 2026-09-19): the blob
   alternates his mint and the coral, the stroke behind it always carries
   the other one — as its body, and as lap marks of the first bleeding back
   into it — and the blob multiplies into the stroke where they overlap, so
   the overlap is --mix and not one shape lying on another. No sound, no
   counter, no label.
   ===================================================================== */
html.motion .splat-layer{
  position:fixed;inset:0;z-index:2000;pointer-events:none;overflow:hidden;
  /* The layer is its own compositing group, and it has to be said out
     loud: the splat multiplies into the stroke under it, and a blend that
     reached past this layer would multiply the page itself. z-index 2000
     on a fixed box already makes a stacking context; isolation says why. */
  isolation:isolate;
}
/* The stroke is a viewport-sized sheet at the origin; the throw is a box
   of its own, carrying the left/top site.js measured for it inline, which
   is why that rule cannot be written here.

   max-width is undone on purpose. The reset at the top of this file gives
   every svg max-width:100%, which is right for a drawing in a column and
   wrong for a throw: the box is 680px wide, so on a 390 phone the cap
   squeezed it to 390 and the paint came out at 57% of the size it was
   asked for. Nothing here is in a column — the layer is the viewport. */
html.motion .splat-layer svg{position:absolute;left:0;top:0;max-width:none}
/* The two paints, and the only place either of them is chosen. Every fill
   below is --pnt, --pnt-deep or --pnt-wet, and these two classes are what
   they resolve to: site.js puts .sp-a or .sp-b on the <svg> and nothing
   else in this block knows which paint is in the brush. The stroke always
   carries the other one from the splat it follows, so the frame the
   visitor sees has both. */
html.motion .sp-a{--pnt:var(--mint);--pnt-mid:var(--mint-mid);--pnt-deep:var(--mint-deep);--pnt-rim:var(--mint-rim);
  --pnt-wet:var(--mint-wet);--pnt-wet-0:var(--mint-wet-0);
  --oth:var(--pop);--oth-0:var(--pop-wet-0)}
html.motion .sp-b{--pnt:var(--pop);--pnt-mid:var(--pop-mid);--pnt-deep:var(--pop-deep);--pnt-rim:var(--pop-rim);
  --pnt-wet:var(--pop-wet);--pnt-wet-0:var(--pop-wet-0);
  --oth:var(--mint);--oth-0:var(--glow-0)}
html.motion .sp-ink{fill:var(--pnt)}
html.motion .sp-rimc{flood-color:var(--pnt-rim)}
html.motion .sp-gloss-a{stop-color:var(--gloss)}
html.motion .sp-gloss-b{stop-color:var(--gloss-0)}

/* The burst, and where it is animated. site.js gives the throw its own box
   — 680x640 of viewBox with the click point at --ox/--oy inside it — and
   places that box by left/top, so the one animated property is a transform
   on the <svg> element itself. That is the whole of the performance story:
   an SVG group whose transform animates is re-rasterised every frame, and
   five filtered layers over a 470px throw cost ~66ms a frame on a machine
   with no GPU, which is four frames for the entire pop. An <svg> is an
   ordinary box; with will-change the compositor rasters it once and moves
   it, and the filters inside never run again. --sr is the variant's turn,
   --s0/--s1 the scale it pops between (0.667-based on a phone, which is the
   1.4x the round-three note asks for against the desktop 2.1x).

   Round three: 260ms, not 120. "Increase time on transitions so he can
   actually enjoy it." Round four (2026-10-01): 360ms, with the sweep at
   1.4s — "slow down the paint page transition." */
html.motion .sp-splat{
  z-index:2;
  transform-origin:var(--ox) var(--oy);will-change:transform;
  transform:rotate(var(--sr)) scale(var(--s0));
  animation:oaSplatPop .36s cubic-bezier(.34,1.42,.64,1) both;
}
@keyframes oaSplatPop{
  from{transform:rotate(var(--sr)) scale(var(--s0));opacity:.4}
  to{transform:rotate(var(--sr)) scale(var(--s1));opacity:1}
}
html.motion .sp-stroke{z-index:1}
/* The leading edge, on its own sheet over the mix zone. Same keyframes,
   same timeline, so it is the same stroke — and the ragged thing is what
   the visitor sees at the boundary rather than the clip that made it. */
html.motion .sp-stroke-top{z-index:4;pointer-events:none}
html.motion .sp-stroke-top.go .sp-sweep{
  will-change:transform;
  animation:oaSplatSweep 1.4s cubic-bezier(.42,.02,.48,.98) both;
}

/* --- the mix zone: where the stroke has gone over the throw ----------
   Round three, second pass (owner): "mixing must marble, not multiply."
   A multiply is physically true and visually dead — two paints average to
   one flat brown and the frame loses both of them. Wet paint dragged
   through wet paint does not average: it marbles, the two colours stay
   themselves in interleaved swirls, and the lightest thing in it is the
   thin cream vein where they have pulled apart.

   So there is no blend mode anywhere in this block. The mix zone is a
   third copy of the throw, painted in a gradient that runs pnt / oth /
   pnt / cream / oth across it and then pushed about by turbulence at low
   frequency and high scale — which is what turns a banded gradient into
   swirls — masked back to the throw's own silhouette and given the same
   rim. It sits over the plain throw and is revealed exactly as far as the
   stroke has travelled.

   The reveal is an inset clip rather than a mask or a blend, and it can be
   because the stroke's leading edge is affine in the animation's eased
   progress and so is the inset: site.js computes the two ends of it,
   --i0 and --i1, from the side the stroke comes in on, and the clip is
   interpolated on the same timeline with the same easing. Nothing here is
   re-filtered as it travels: the art is static and only the clip moves. */
html.motion .sp-marble{
  z-index:3;
  transform-origin:var(--ox) var(--oy);
  clip-path:var(--i0);
}
html.motion .sp-marble.go{animation:oaSplatMix 1.4s cubic-bezier(.42,.02,.48,.98) both}
@keyframes oaSplatMix{from{clip-path:var(--i0)}to{clip-path:var(--i1)}}
html.motion .sp-mmask{mask-type:alpha}
html.motion .sp-mp{stop-color:var(--pnt)}
html.motion .sp-mo{stop-color:var(--oth)}
html.motion .sp-mc{stop-color:var(--cream)}
html.motion .sp-vein{fill:none;stroke:var(--cream);stroke-linecap:round;opacity:.52}
html.motion .sp-vein-2{fill:none;stroke:var(--mix);stroke-linecap:round;opacity:.28}

/* The bleed: a thin ring of thinner paint just outside the rim, and
   nothing at all inside it — paint wicking a few millimetres into the wall
   and stopping, which is what stops the blob reading as a vector shape cut
   out with scissors. A blurred copy of the whole throw was a glow. */
html.motion .sp-halo{opacity:.55}
/* The grain: fractal noise through luminanceToAlpha, kept inside the
   paint's own alpha, at a tenth and a bit. Tooth, not texture. */
html.motion .sp-grain{opacity:.22}
/* The gloss: a crescent along the top-left shoulder of the body, where a
   wet throw catches the light — blurred, and running out at both ends, so
   it is a highlight and not a shape laid on one. */
html.motion .sp-gloss{opacity:.8}

/* The runs. Round three draws them as filled, tapered paths that thin as
   they fall and bead at the end, rather than as a stroked line of one
   width — a run of paint has a shoulder and a tip. They are revealed by an
   inset clip travelling down the path's own bounding box, and the bead
   arrives once the run has reached it. */
html.motion .sp-drip{
  fill:var(--pnt);clip-path:inset(0 0 100% 0);
  animation:oaSplatDrip .42s var(--ease-roll) both;
}
@keyframes oaSplatDrip{from{clip-path:inset(0 0 100% 0)}to{clip-path:inset(0 0 -4% 0)}}
html.motion .sp-bead{
  fill:var(--pnt);transform-box:fill-box;transform-origin:50% 50%;
  animation:oaSplatBead .2s cubic-bezier(.34,1.5,.64,1) .36s both;
}
@keyframes oaSplatBead{from{transform:scale(0)}to{transform:scale(1)}}

/* The stroke. A brush pass across the whole viewport, from the side the
   link was on — and, from the phone menu, down it: site.js builds that one
   in a box laid on its side and stands it up with a rotate on .sp-mirror, so
   the body, the laps, the bristles and the animation below are the same for
   both. The only thing that moves is one translateX in the group's frame.

   The body is a plain rect with a vertical lap-mark gradient and carries no
   filter at all, because a filter on a full-viewport rect is a
   full-viewport repaint every frame. The displacement is spent where it
   shows: on the dry-brush streaks and the bristle fingers near the leading
   edge, and on the drops flung off them. will-change is set only while the
   pass is running. */
html.motion .sp-body{fill:url(#oaLap)}
html.motion .sp-lap-a{stop-color:var(--pnt)}
html.motion .sp-lap-b{stop-color:var(--pnt-mid)}
html.motion .sp-lap-c{stop-color:var(--pnt-deep)}
/* The other paint, carried across in the same pass as lap marks: the band
   itself in that colour, and a narrower core of the two paints mixed where
   the reload dragged through what was already on the nap. Both are laid on
   normally — there is no blend mode left anywhere in this file, because a
   multiply is what made the overlaps flat. */
html.motion .sp-lapo{fill:var(--oth)}
html.motion .sp-lapo-mix{fill:var(--mix)}
/* Dry brush: the streaks the nap leaves behind the leading edge once the
   load starts to go. Deep paint and pale paint, both at low alpha, so the
   body is broken up without a hole in it — a hole would be the page
   showing through a transition whose whole job is to cover it. */
html.motion .sp-dry0{fill:var(--pnt-deep)}
html.motion .sp-dry1{fill:var(--pnt-wet)}
html.motion .sp-dry2{fill:var(--pnt-mid)}
/* The wet edge: the pale, bleeding line right behind the bristles, blurred
   and then pushed about by the same turbulence the bristles get. Crossing
   the splat it is what makes the overlap read as wet rather than as two
   shapes meeting. */
html.motion .sp-wet{fill:var(--pnt-wet);opacity:.55}
html.motion .sp-t0{fill:var(--pnt)}
html.motion .sp-t1{fill:var(--pnt-mid)}
html.motion .sp-t2{fill:var(--pnt-deep)}
html.motion .sp-sweep{
  transform-box:view-box;transform-origin:0 0;
  transform:translateX(var(--tx0));
}
html.motion .sp-stroke.go .sp-sweep{
  will-change:transform;
  animation:oaSplatSweep 1.4s cubic-bezier(.42,.02,.48,.98) both;
}
@keyframes oaSplatSweep{
  from{transform:translateX(var(--tx0))}
  to{transform:translateX(var(--tx1))}
}

/* The second lock, as everywhere else in this file: if html.motion ever
   arrives on a machine asking for reduced motion, there is no paint. */
@media (prefers-reduced-motion:reduce){
  html.motion .splat-layer{display:none}
}

/* =====================================================================
   THE DIMENSIONS HOOK

   The measurements are the headline. Archivo's width axis is what makes
   that work: at wdth 125 the figures are physically wide, so 81' x 80'
   occupies the page the way the wall occupies the street. Mint touches the
   prime marks and the times sign, and nothing else.
   ===================================================================== */
.dims{
  display:block;font-family:var(--font-display);
  font-variation-settings:'wdth' 125,'wght' 800;
  font-size:clamp(3.4rem,11vw,9rem);line-height:.82;letter-spacing:-.02em;
  color:var(--ink);
}
/* Where a figure sits on a photograph or a black band it is the other way
   round — light type, a light hairline under it, and a light overspray. */
.pcard .dims,.page-hero .dims,.on-ink .dims{color:var(--on-ink);--spray-halo:rgba(244,243,239,.6)}
.pcard .dims::after,.page-hero .dims::after,.on-ink .dims::after{background:var(--line-on-ink-2)}
.dims .f,.dims .x{font-size:.38em;color:var(--mint);vertical-align:.34em}
.dims .x{margin:0 .12em}
/* The rule Cure sweeps. */
.dims::after{content:"";position:absolute;left:0;right:0;bottom:-.16em;height:1px;background:var(--line)}
.dims-sm{font-size:clamp(1.5rem,4.6vw,2.2rem)}
/* A card sets the figures at 2.2rem, where .38em prime marks land near 13px and
   read as dots rather than marks. They carry the only mint on a card, so they
   have to be legible: .46em at card size only — the project-page figures are
   three times as big and stay as specified. The vertical-align is retuned from
   .34em to .28em because it is relative to the mark's own size, and .28 x .46
   is the same optical lift off the baseline as .34 x .38 was. */
.dims-sm .f,.dims-sm .x{font-size:.46em;vertical-align:.28em}
/* A wall known by its area: the unit sits after the figure, a hair clear. */
.dims-area .x{margin:0 .08em 0 0}
.dims-area .f{margin-left:.22em;white-space:nowrap}

/* --- a wall we have no feet for --------------------------------------- */
/* The measurement is the hook, so the space it would have taken is not left
   as a hole: it says "Size on request" (owner, 2026-10-03), quietly — small,
   tracked and uppercase in the body face, like a card's place line, so a
   wall without feet never shouts louder than one with them. --dim on the
   white page (5.2:1), --on-ink-mute on a card's dark scrim. It is a note and
   not a number: nothing counts it, sprays it or rolls it, because Live,
   Stencil and Cure bind to .dims and .stat-n alone. */
.feet-note{
  display:block;font-family:var(--font-body);font-weight:600;
  font-size:.8rem;line-height:1.3;letter-spacing:.16em;text-transform:uppercase;
  color:var(--dim);
}
.pintro .feet-note{margin:1.2rem 0 1.6rem}
.pcard .feet-note-sm{
  order:2;margin-top:.4rem;color:var(--on-ink-mute);
  font-size:.72rem;
}
.pcard-lg .feet-note-sm{font-size:.76rem}
.pnav .pnav-d.pnav-note{
  font-family:var(--font-body);font-variation-settings:normal;font-weight:600;
  font-size:.74rem;letter-spacing:.16em;text-transform:uppercase;color:var(--dim);
}

/* --- project card ----------------------------------------------------- */
/* The card is the link. Photo at 4:3, the figures over a scrim at the foot,
   a hairline that goes mint when you reach it. */
.pcard{position:relative;display:block;overflow:hidden;border:1px solid var(--line);border-radius:var(--radius);background:var(--paper-3);transition:border-color .25s,transform .35s var(--ease)}
.pcard:hover,.pcard:focus-visible{border-color:var(--mint)}
.pcard-img{aspect-ratio:4/3;background:var(--paper-3)}
.pcard-img picture,.pcard-img img{display:block;width:100%;height:100%}
.pcard-img img{object-fit:cover;transition:transform .7s var(--ease)}
.pcard:hover .pcard-img img{transform:scale(1.04)}
.pcard-body{position:absolute;inset:auto 0 0 0;padding:1.1rem 1.15rem 1.15rem;background:linear-gradient(0deg,var(--scrim) 0%,var(--scrim-soft) 58%,transparent 100%)}
.pcard h3{margin-top:.55rem;font-size:1.02rem}
.pcard .place{display:block;margin-top:.2rem;font-size:.76rem;letter-spacing:.16em;text-transform:uppercase;color:var(--on-ink-mute)}

.pgrid{display:grid;gap:1rem;grid-template-columns:1fr}
@media (min-width:640px){.pgrid{grid-template-columns:repeat(2,1fr);gap:1.2rem}}
@media (min-width:960px){.pgrid{grid-template-columns:repeat(3,1fr)}}
/* The Work grid's lead card (work_index()) spans what the remainder needs, so
   no row under it ends on a lone card. Its picture is cut wider to match:
   one row of three is 4/3 a column, so a card two wide is 8/3. */
@media (min-width:640px) and (max-width:959.98px){
  .pgrid .pcard.lead-t{grid-column:span 2}
  .pgrid .pcard.lead-t .pcard-img{aspect-ratio:8/3}
}
@media (min-width:960px){
  .pgrid .pcard.lead-3{grid-column:span 3}
  .pgrid .pcard.lead-3 .pcard-img{aspect-ratio:21/8}
  .pgrid .pcard.lead-2{grid-column:span 2}
  .pgrid .pcard.lead-2 .pcard-img{aspect-ratio:8/3}
}
/* Public works on /work is the words alone (public_works(media=False)). */
.pw.pw-solo .pw-grid{grid-template-columns:1fr}
.pw.pw-solo .pw-words{padding-inline:0}
@media (prefers-reduced-motion:no-preference){.pcard:hover{transform:translateY(-3px)}}

/* =====================================================================
   HOME

   Four blocks between the hero and the call to action: what the work adds
   up to, six of the twelve walls, the two Rochester commissions side by
   side, and Ephraim's three stages in his own words. Nothing here invents a
   component the rest of the site does not already have — the card is the
   card, the figures are the figures.
   ===================================================================== */

/* The hero gets room for a mural rather than just room for the words. The
   photograph is the page's largest paint, so the block is sized off the
   viewport and the words sit on its floor. */
.page-hero.tall{min-height:clamp(560px,86vh,880px);display:flex;align-items:flex-end;padding-bottom:clamp(3rem,7vw,5.5rem)}
/* The standard shade is lightest at 38% down, which on a tall hero is exactly
   where the eyebrow lands — and a mint eyebrow over a lit-up mural is the one
   contrast failure PLAN.md §9(e) names. So the tall hero keeps the top of the
   photograph clear and puts the words on a floor of near-black instead. */
.page-hero.tall .hero-shade{background:linear-gradient(180deg,var(--scrim-soft) 0%,transparent 18%,transparent 30%,var(--scrim) 50%,var(--ink) 100%)}
/* And the last few per cent: a mint eyebrow is small type, and small type over
   a photograph of a flower mural needs its own edge. An ink halo does that
   without drawing a box round the words. */
.page-hero.has-media .hero-inner .eyebrow,.page-hero.has-media .hero-inner .lead,.page-hero.has-media .hero-inner .crumbs{text-shadow:0 1px 14px var(--ink)}
.page-hero.tall .wrap{width:min(var(--max),calc(100% - 2rem))}
.page-hero .hero-media picture{width:100%;height:100%}
/* A photograph held at its own width (page_hero(cap=True)) sits on ink. */
.page-hero.media-capped{background:var(--ink)}
@media (min-width:640px){.page-hero.tall .wrap{width:min(var(--max),calc(100% - 3rem))}}
@media (min-width:960px){.page-hero.tall .wrap{width:min(var(--max),calc(100% - 4rem))}}
@media (min-width:1240px){.page-hero.tall .wrap{width:var(--max)}}

/* --- the scale statement --------------------------------------------- */
/* Every figure in this row is projects.py adding itself up. The prime mark
   is the same mint mark it is on a card, so the row reads as measurements
   rather than as marketing. */
.stats{display:grid;grid-template-columns:repeat(2,1fr);gap:1.6rem 1.2rem}
.stat{border-top:1px solid var(--line-2);padding-top:1rem}
.stat-n{
  display:block;font-family:var(--font-display);
  font-variation-settings:'wdth' 118,'wght' 800;
  font-size:clamp(2.1rem,5.4vw,3.2rem);line-height:.9;letter-spacing:-.02em;
  color:var(--ink);
}
.stat-n .f{font-size:.42em;color:var(--mint);vertical-align:.3em}
/* A stat that is a name (the dolphins), in the figure's face at a size a
   phrase fits. Not .stat-n, so nothing rolls or sprays it. */
.stat-w{
  display:block;font-family:var(--font-display);
  font-variation-settings:'wdth' 125,'wght' 900;
  font-size:clamp(1.5rem,3.4vw,2.2rem);line-height:.95;letter-spacing:-.01em;
  color:var(--ink);
}
/* 100,000+ — the + is a mark on the figure and never wraps off it. */
.stat-n{white-space:nowrap}
.stat-l{display:block;margin-top:.5rem;font-size:.76rem;letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
@media (min-width:960px){.stats{grid-template-columns:repeat(3,1fr);gap:1.6rem}}

/* --- the end of a card row -------------------------------------------- */
.row-end{margin-top:clamp(1.6rem,4vw,2.4rem)}

/* --- the two Rochester walls ------------------------------------------ */
/* Same card, more of it: the pair is the only place two photographs get half
   the page each — two civil-rights portraits in one city, side by side. */
.pgrid.pair{grid-template-columns:1fr}
@media (min-width:640px){.pgrid.pair{grid-template-columns:repeat(2,1fr)}}
.pcard-lg .pcard-img{aspect-ratio:3/2}
.pcard-lg .pcard-body{padding:1.4rem 1.4rem 1.5rem}
.pcard-lg .dims-sm{font-size:clamp(2rem,5.4vw,3.1rem)}
.pcard-lg h3{font-size:1.15rem;margin-top:.7rem}

/* --- his three stages -------------------------------------------------- */
.beats{list-style:none;margin:0;padding:0;display:grid;gap:2.2rem}
.beat{border-top:1px solid var(--line-2);padding-top:1.5rem}
.beat h3{
  font-family:var(--font-display);text-transform:uppercase;
  font-variation-settings:'wdth' 100,'wght' 800;
  font-size:1.18rem;letter-spacing:.01em;margin-bottom:.9rem;
}
.beat h3::before{content:"";display:block;width:34px;height:2px;background:var(--mint);margin-bottom:.9rem}
.beat blockquote{margin:0}
.beat blockquote p{margin:0;color:var(--ink);opacity:.86;max-width:52ch}
@media (min-width:960px){.beats{grid-template-columns:repeat(3,1fr);gap:2rem}}

/* =====================================================================
   HOME — the mockup's spine (research/mock-approved.html)

   The approved mockup is the specification, section for section: a hero
   that is the headline and nothing else, a band painted in one stroke, two
   rows of wall-and-words, the brand wall and the faces (the other agent's
   components, rendered here if they have landed), the black marquee strip,
   the grid of walls, the public-works row, and the closing band.

   The blocks the live site already had and the mockup does not draw — the
   sprayed wordmark, the measure of it, the two Rochester walls, his three
   stages and the graffiti-removal band — are all still on the page and in
   the same order they were: the mockup adds sections, it does not delete
   copy. The one figure the owner struck out stays struck out — there is no
   wall count anywhere on this page.
   ===================================================================== */

/* --- the hero: one headline, on the wall ------------------------------ */
/* A hero with a photograph is one of the three black bands, so its words
   are the light ones; page_hero() marks it .has-media so nothing has to
   guess. Home's is centred on the picture, which is the mockup. */
.page-hero.has-media{color:var(--on-ink)}
.page-hero.has-media h1,.page-hero.has-media .lead{color:var(--on-ink)}
.page-hero.hero-mid{
  display:flex;align-items:center;justify-content:center;text-align:center;
  padding:0;min-height:clamp(520px,86vh,860px);
}
.page-hero.hero-mid .hero-inner{max-width:none;margin-inline:auto}
.page-hero.hero-mid .hero-shade{background:linear-gradient(180deg,var(--scrim-soft) 0%,var(--shade) 100%)}
.page-hero.hero-mid h1{font-size:clamp(3rem,10.4vw,10.4rem);margin:0}
/* The marker sits a little off level, the way a hand writes it. */
.page-hero.hero-mid .eyebrow{
  font-size:clamp(1.5rem,2.4vw,2.1rem);margin-bottom:1rem;
  transform:rotate(-3deg);
}

/* --- the words inside a painted band ---------------------------------- */
.band h2{font-size:clamp(2.6rem,7vw,6.5rem);margin:0 0 1.2rem}
/* The lead in a band is shouted rather than read: Archivo at its widest,
   caps, and short lines. The paragraph under it is the one you read. */
.band-lead{
  max-width:52rem;margin:0 auto 1.1rem;
  font-size:clamp(.95rem,1.35vw,1.2rem);line-height:1.4;color:var(--ink);
}
.band-copy{max-width:44rem;margin:0 auto;font-size:1.08rem;line-height:1.6;color:var(--ink)}

/* --- a wall down one half of the page, the words down the other -------- */
.rows{display:grid;grid-template-columns:1fr;padding:0}
/* The box is a shape, not whatever the photograph that arrived happens to
   be: a rendition whose encoded aspect is half a pixel off its neighbour
   would otherwise make the row a different height in two renders of the
   same document. The picture covers it. */
.rows-media{position:relative;aspect-ratio:4/3;background:var(--paper-3)}
.rows-media picture,.rows-media img{display:block;width:100%;height:100%}
.rows-media img{object-fit:cover}
.rows-words{
  display:flex;flex-direction:column;justify-content:center;
  padding:clamp(2.8rem,8vw,5.5rem) clamp(1rem,5vw,4rem);
}
.rows-words h2{font-size:clamp(3.2rem,7.4vw,7rem);margin:0 0 .5rem}
.rows-words .serif{font-size:1.05rem;line-height:1.65;color:var(--muted);max-width:33rem;margin:0 0 1rem}
.rows-words .serif:last-child{margin-bottom:0}
/* The measurement in a row is the mockup's: Archivo at its widest, with the
   prime marks and the times sign in the coral instead of the mint, because
   on white the mint is paint and not a colour type can be set in. */
.dims-row{font-size:clamp(2.1rem,4.4vw,2.9rem);line-height:.9;margin:0 0 1.4rem}
.dims .f,.dims .x{color:var(--pop)}
.pcard .dims .f,.pcard .dims .x,.page-hero .dims .f,.page-hero .dims .x,
.on-ink .dims .f,.on-ink .dims .x{color:var(--mint)}
.dims-row::after{display:none}
/* The places a public-works row lists are not a measurement, so they are
   not .dims — but they are set in the same face, and at the size the coral
   is allowed to carry type at. */
.places{font-size:clamp(1.5rem,2.2vw,1.9rem);line-height:1.2;color:var(--pop);margin:0 0 1.1rem}
.note{font-size:clamp(1.5rem,2vw,1.6rem);line-height:1.2;color:var(--pop);margin:.4rem 0 0}
@media (min-width:860px){
  .rows{grid-template-columns:1fr 1fr}
  .rows-media{aspect-ratio:auto;min-height:min(64vh,580px)}
}

/* --- the marquee strip ------------------------------------------------- */
/* Type, a black ground and a coral bar between the words: the second of the
   three black bands. It runs off both edges because a strip that fits is a
   list, and it clips rather than scrolling the page. */
.mq{
  background:var(--ink);color:var(--on-ink);overflow:hidden;white-space:nowrap;
  padding:.75rem 0;font-family:var(--font-tall);text-transform:uppercase;
  font-size:clamp(1.25rem,2.5vw,2.2rem);letter-spacing:.04em;line-height:1.25;
}
.mq span{padding:0 .5em}
.mq b{color:var(--pop);font-weight:400}

/* --- a section head, and the aside under one -------------------------- */
.section-head h2{font-size:clamp(2.6rem,6.2vw,5.4rem)}
.section-head .lead{font-size:1.06rem;color:var(--muted);margin-top:1rem}
.sub{
  font-family:var(--font-marker);font-weight:400;text-transform:none;
  font-size:clamp(1.5rem,2.2vw,1.8rem);line-height:1.2;
  color:var(--pop);margin:.2rem 0 1.1rem;
}
.shout{color:var(--pop)}
.walls-band h2{font-size:clamp(3.2rem,8vw,8rem)}

/* --- the card, as the mockup draws it --------------------------------- */
/* The title is the headline and the measurement is the line under it, in
   the wordmark's face and in mint — the one place on the page mint carries
   type, and it may because the caption sits on a black scrim. */
.pcard-body{display:flex;flex-direction:column}
.pcard h3{order:1;margin:0;font-family:var(--font-tall);font-weight:400;text-transform:uppercase;font-size:clamp(1.5rem,2.3vw,2.1rem);line-height:.95;letter-spacing:.01em;color:var(--on-ink)}
.pcard .dims-sm{
  order:2;margin-top:.3rem;font-family:var(--font-display);font-weight:900;
  font-variation-settings:'wdth' 125,'wght' 900;
  font-size:.92rem;line-height:1.2;letter-spacing:.06em;color:var(--mint);
}
.pcard .dims-sm .f,.pcard .dims-sm .x{font-size:.92em;vertical-align:0}
.pcard .place{order:3;margin-top:.35rem}
.pcard-lg .dims-sm{font-size:1.05rem}

/* --- the measure of it ------------------------------------------------- */
.stats-band .stat{border-top:2px solid var(--ink)}
.stat-n{font-variation-settings:'wdth' 125,'wght' 900;color:var(--ink)}
.stat-n .f{color:var(--pop)}
.stat-l{color:var(--muted)}

/* --- the closing band -------------------------------------------------- */
.cta{border:0;border-radius:0;background:none;padding:0;text-align:center}
.cta h2{font-size:clamp(3rem,9vw,9.2rem);margin:0 0 .3rem}
.cta .eyebrow{display:block;transform:rotate(-2deg);margin-bottom:.9rem}
.cta .serif{max-width:46ch;margin:0 auto 1.7rem;color:var(--ink);font-size:1.05rem;line-height:1.6}
.cta .btn-row{justify-content:center;margin-top:0}

/* =====================================================================
   THE CHROME EVERY OTHER PAGE WEARS

   Home is the mockup; these are the parts of it the rest of the site is
   built out of, so that a page whose body has not been rewritten yet still
   looks like it belongs to the same site. Nothing here is page content —
   it is the hero, the section head, the small headings, the chips and the
   panels, restyled to the system the mockup sets:

     a headline is Anton, a measurement is Archivo at wdth 125, an aside is
     the marker in coral at 24px and up, and reading copy is Fraunces.
   ===================================================================== */

/* --- the hero on the white page --------------------------------------- */
.page-hero h1{font-size:clamp(2.8rem,7.6vw,6.6rem)}
.page-hero .hero-inner .eyebrow{margin-bottom:.4rem}
.page-hero .lead{max-width:56ch;font-size:1.08rem;line-height:1.6}
/* A hero with nothing behind it does not need a page's worth of room under
   the words; the section that follows brings its own. */
.page-hero:not(.has-media):not([data-wall]){padding-bottom:clamp(2rem,4vw,3rem)}
/* The blank wall is white now, like the rest of the page. It is still a
   wall: the canvas the visitor paints on is the same element, and the
   three paints the mechanics cycle include the ink that shows on it. */
.page-hero.blank .btn-row{margin-top:1.8rem}

/* --- section heads, and the small headings under them -----------------
   Anton's caps rise out of their own line box at line-height .9, so a
   heading set tight under an aside will climb into it: the marker above a
   section head gets the room back. */
.section-head .eyebrow{margin-bottom:1rem}
/* Every heading on the site is the headline face now, at the size its job
   asks for: a section head shouts, a stage name and a card title speak. */
h3{font-family:var(--font-tall);font-weight:400;text-transform:uppercase;font-size:1.3rem;letter-spacing:.02em;line-height:1}
h4{font-family:var(--font-tall);font-weight:400;font-size:1.15rem;letter-spacing:.04em;color:var(--ink)}
.beat h3{font-family:var(--font-tall);font-variation-settings:normal;font-size:1.45rem;letter-spacing:.02em}
.beats-full .beat h3::before{font-variation-settings:'wdth' 125,'wght' 900;color:var(--pop)}
.city b{font-variation-settings:'wdth' 125,'wght' 900;font-size:1.1rem}
.svc-h{color:var(--pop)}
.pull{border-left-width:4px}
.pull p{font-family:var(--font-serif)}

/* --- the panels ------------------------------------------------------- */
/* Cards, wells and the form all sit on the page rather than on a dark
   canvas, so their edge is the ink hairline and their ground is the page. */
.contact-card,.pnav a,.figframe,.form-status{border-width:1px}
.field input,.field select,.field textarea{border-width:2px;border-radius:var(--radius)}
.field label{color:var(--muted)}
.tags li{border-width:2px;border-color:var(--line-2);color:var(--muted)}

/* =====================================================================
   WORK — the index

   One grid of twelve with four chips over it, and the filtering is CSS.
   The chips are ordinary anchors pointing at four empty targets that sit
   just above the grid; :target then hides the cards that do not match.
   No script is involved, so the filter survives a blocked or broken
   site.js, and with nothing targeted every wall is on screen — which is
   the state the page should open in anyway.
   ===================================================================== */
/* The hero already ends on a paragraph; a second full section pad under it
   just leaves a hole above the chips. */
.work{padding-top:clamp(2.5rem,6vw,4rem)}
.ftarget{display:block;height:0;scroll-margin-top:calc(var(--nav-h) + 1.5rem)}
.filters{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:clamp(1.6rem,4vw,2.4rem)}
/* Square, a 2px edge and the headline face: the same sign-painting the
   buttons are, at the size of a label. */
.chip{
  display:inline-flex;align-items:center;gap:.5rem;
  padding:.5rem 1rem;border:2px solid var(--line-2);border-radius:var(--radius);
  font-family:var(--font-tall);font-weight:400;text-transform:uppercase;
  font-size:1rem;letter-spacing:.04em;color:var(--ink);
  transition:border-color .2s,color .2s,background .2s;
}
.chip:hover{border-color:var(--ink);color:var(--ink)}
.chip .ct{font-size:.74rem;color:var(--dim);font-variant-numeric:tabular-nums}
/* The chip that is doing the filtering, and All when nothing else is. */
.chip[href="#f-all"],
#f-brand:target ~ .filters .chip[href="#f-brand"],
#f-portrait:target ~ .filters .chip[href="#f-portrait"],
#f-civic:target ~ .filters .chip[href="#f-civic"]{background:var(--mint);border-color:var(--mint);color:var(--ink)}
.chip[href="#f-all"] .ct,
#f-brand:target ~ .filters .chip[href="#f-brand"] .ct,
#f-portrait:target ~ .filters .chip[href="#f-portrait"] .ct,
#f-civic:target ~ .filters .chip[href="#f-civic"] .ct{color:var(--ink);opacity:.62}
#f-brand:target ~ .filters .chip[href="#f-all"],
#f-portrait:target ~ .filters .chip[href="#f-all"],
#f-civic:target ~ .filters .chip[href="#f-all"]{background:transparent;border-color:var(--line-2);color:var(--muted)}
#f-brand:target ~ .filters .chip[href="#f-all"] .ct,
#f-portrait:target ~ .filters .chip[href="#f-all"] .ct,
#f-civic:target ~ .filters .chip[href="#f-all"] .ct{color:var(--dim);opacity:1}
/* and the filtering itself */
#f-brand:target ~ .pgrid .pcard:not(.cat-brand),
#f-portrait:target ~ .pgrid .pcard:not(.cat-portrait),
#f-civic:target ~ .pgrid .pcard:not(.cat-civic){display:none}

/* =====================================================================
   A PROJECT PAGE

   The wall at full bleed with a six-foot figure standing on it, then the
   measurements set large, then what is known about the wall and nothing
   that is not.
   ===================================================================== */
.phero{padding:0}
.scale{position:relative;isolation:isolate}
/* The height is written out rather than left to aspect-ratio with a
   max-height on the same box: given both, the browser keeps the ratio by
   *narrowing* the box, and the full-bleed hero came out 1280px wide in a
   1440px window with ink down the right-hand side. A height in vh and vw
   says what is meant — as tall as the ratio allows, never taller than the
   window — and the box stays the full width of the page. */
.scale-media{position:relative;height:min(68vh,120vw);overflow:hidden;background:var(--paper-3)}
.scale-media picture,.scale-media img{width:100%;height:100%;object-fit:cover}
@media (min-width:640px){.scale-media{height:min(74vh,80vw)}}
@media (min-width:960px){.scale-media{height:min(80vh,56.25vw)}}

/* The baseline the figure stands on, and the figure. --ft is the wall's
   width in feet, written on the element by build.py; she is two feet wide
   and six tall, so 2/ft of the picture's width at an aspect ratio of 1:3
   makes her exactly (6 / ft) of it high. On Gucci that is a thirtieth of
   the frame, which is the entire point of putting her there. --fx is where
   she stands, 0 to 1; step 14 gives her the drag. */
.scale-base{position:absolute;left:0;right:0;bottom:var(--base,7%);height:1px;background:var(--mint);opacity:.5}
.fig{
  position:absolute;bottom:var(--base,7%);
  --fw:calc(200% / var(--ft, 40));
  left:calc(var(--fx,.12) * (100% - var(--fw)));
  width:var(--fw);min-width:9px;aspect-ratio:1/3;
  padding:0;border:0;background:none;color:var(--mint);
  filter:drop-shadow(0 1px 3px var(--ink));
}
.fig-svg{width:100%;height:100%}
.fig-cap{
  position:absolute;left:50%;bottom:calc(100% + .45rem);transform:translateX(-50%);
  font-size:.66rem;letter-spacing:.14em;text-transform:uppercase;color:var(--mint);
  white-space:nowrap;text-shadow:0 1px 8px var(--ink);
}

/* --- the wall going up ------------------------------------------------ */
/* The portrait reel, stood up in a phone-shaped well beside one sentence.
   The well is an aspect-ratio box and the clip is 720x1280 with both
   attributes on the element, so the section reserves its own room in every
   render and the poster, the clip and the empty box are the same shape.
   site.js plays it only while it is on screen; with no script the element
   keeps its controls and the visitor plays it, which is why the frame is
   sized the same either way. */
.progress{padding:clamp(3rem,7vw,5.5rem) 0 clamp(1rem,3vw,2rem)}
.progress-grid{display:grid;gap:clamp(1.6rem,4vw,2.6rem);align-items:center;justify-items:start}
.progress-phone{
  margin:0;position:relative;width:min(300px,74vw);aspect-ratio:9/16;
  border-radius:26px;overflow:hidden;background:var(--ink);
  border:1px solid var(--line);box-shadow:0 22px 56px rgba(10,10,11,.20);
}
.progress-video{display:block;width:100%;height:100%;object-fit:cover;background:var(--ink)}
.progress-words h2{font-size:clamp(2.2rem,5vw,3.6rem)}
.progress-words p{margin:1rem 0 0;max-width:34ch;color:var(--muted)}
@media (min-width:860px){
  .progress-grid{grid-template-columns:auto minmax(0,1fr);gap:clamp(2.4rem,5vw,4.4rem);align-items:center}
}

/* --- the whole film --------------------------------------------------- */
/* A 16:9 well holding the film itself. The element is in the server HTML with
   its own controls and its poster, so with no script it is a film a visitor
   can play; under html.motion site.js hides those controls, lays a button the
   width of the picture over the poster, and gives them back the moment it is
   pressed. The well is aspect-ratio 16/9 in every one of those states, so the
   section reserves the same room whatever happens in it. */
.film{padding:clamp(1rem,3vw,2rem) 0 clamp(3rem,7vw,4.5rem)}
.film .section-head{margin-bottom:clamp(1.4rem,3vw,2rem)}
.film-frame{
  position:relative;margin:0;aspect-ratio:var(--film-ar,16/9);overflow:hidden;
  border-radius:var(--radius);background:var(--ink);border:1px solid var(--line);
}
/* A film shot portrait is shown portrait. The frame takes the film's own shape
   from --film-ar, so object-fit:cover has nothing to crop and nothing to
   stretch; a tall one is held to a column of readable width and centred rather
   than running the width of the page, where it would be four screens deep. */
.film-portrait{width:min(520px,82vw);margin-inline:auto;border-radius:26px}
/* Wider than the portrait hero's 420px on purpose: on a page where the
   hero is already playing this film muted, the band under it is the one
   a visitor came to watch, and two frames the same size read as the same
   thing printed twice. */
.film-video{display:block;width:100%;height:100%;object-fit:cover;background:var(--ink)}
.film-play{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:2;
  gap:.6em;box-shadow:0 10px 40px rgba(10,10,11,.45);
}
.film-play svg{width:1.05em;height:1.05em;flex:none}
.film .credit{margin:.9rem 0 0}

/* --- the words under the wall ----------------------------------------- */
.pintro{padding-top:clamp(2.5rem,6vw,4rem)}
.pintro .dims{margin:1.4rem 0 1.8rem}
.pstory{max-width:58ch}
.pmeta{display:flex;flex-wrap:wrap;gap:1.6rem 2.6rem;margin:2rem 0 0;padding-top:1.5rem;border-top:1px solid var(--line)}
.pmeta dt{font-size:.74rem;letter-spacing:.18em;text-transform:uppercase;color:var(--dim)}
.pmeta dd{margin:.35rem 0 0;font-weight:600}
.credit{margin-top:1.4rem;font-size:.86rem;color:var(--dim)}

/* --- previous and next ------------------------------------------------- */
.pnav-wrap{padding-top:0}
.pnav{display:grid;gap:.8rem;border-top:1px solid var(--line);padding-top:1.6rem}
.pnav a{display:grid;gap:.4rem;padding:1.2rem 1.3rem;border:1px solid var(--line);border-radius:var(--radius);background:var(--paper-2);transition:border-color .25s}
.pnav a:hover,.pnav a:focus-visible{border-color:var(--mint)}
.pnav-next{text-align:right}
.pnav-k{display:flex;align-items:center;gap:.4rem;font-size:.74rem;letter-spacing:.18em;text-transform:uppercase;color:var(--dim)}
.pnav-next .pnav-k{flex-direction:row-reverse}
.pnav-k svg{width:15px;height:15px}
.pnav-t{font-weight:700;font-size:1.05rem}
.pnav-d{font-family:var(--font-display);font-variation-settings:'wdth' 118,'wght' 800;font-size:1.3rem;color:var(--ink)}
@media (min-width:640px){.pnav{grid-template-columns:1fr 1fr;gap:1rem}}

/* --- press: what other people published about a wall (press.py) -------
   A plain list of outlets' own headlines, off-site in a new tab. The rows
   are hairline-ruled like .pnav's top edge; the outlet is the small caps
   that sit over everything else here, the headline is the serif the story
   is set in, and the only colour is the mint the hover already uses and
   the coral play mark on a video. Nothing moves but the arrow. */
.press{padding-bottom:clamp(2.5rem,6vw,4rem)}
.press-list{list-style:none;margin:clamp(1.6rem,4vw,2.4rem) 0 0;padding:0;border-top:1px solid var(--line-2)}
.press-list li{border-bottom:1px solid var(--line)}
.press-row{display:grid;gap:.35rem;padding:1.15rem 0;color:var(--ink);text-decoration:none}
.press-o{font-size:.74rem;letter-spacing:.18em;text-transform:uppercase;font-weight:700;color:var(--dim)}
.press-t{font-size:clamp(1.08rem,1.6vw,1.28rem);line-height:1.35;text-wrap:pretty}
.press-play{display:inline-block;margin-right:.5em;color:var(--pop-deep);font-size:.7em;vertical-align:.12em}
.press-d{display:flex;align-items:center;gap:.6rem;font-size:.85rem;color:var(--muted)}
.press-d svg{width:16px;height:16px;flex:none;transition:transform .25s var(--ease)}
.press-row:hover .press-t,.press-row:focus-visible .press-t{text-decoration:underline;text-decoration-color:var(--mint);text-decoration-thickness:2px;text-underline-offset:.22em}
.press-row:hover .press-d svg,.press-row:focus-visible .press-d svg{transform:translateX(4px)}
@media (min-width:860px){
  .press-row{grid-template-columns:11rem 1fr auto;align-items:baseline;gap:1.6rem}
  .press-d{justify-content:flex-end;white-space:nowrap}
}

/* =====================================================================
   SWIPE — the gallery rail

   With no script the rail is a native scroll-snap strip you can drag with
   a finger or reach with the keyboard, so the arrows and the dots never
   render: they live behind html.js because they are enhancement, and a
   control that cannot do anything should not be on the page at all.
   ===================================================================== */
.swipe{position:relative}
.swipe-rail{display:flex;gap:1rem;overflow-x:auto;scroll-snap-type:x mandatory;scroll-behavior:smooth;
  padding-bottom:1rem;scrollbar-width:none;-ms-overflow-style:none}
.swipe-rail::-webkit-scrollbar{display:none}
.swipe-rail:focus-visible{outline:2px solid var(--mint);outline-offset:4px;border-radius:var(--radius)}
html.js .swipe-rail{cursor:grab}
html.js .swipe-rail.dragging{cursor:grabbing;scroll-behavior:auto;scroll-snap-type:none;user-select:none}
.swipe-slide{flex:0 0 calc(min(92%,var(--slide,900px)) * var(--k,1));scroll-snap-align:center}
.swipe-slide:first-child{scroll-snap-align:start}
.swipe-slide:last-child{scroll-snap-align:end}
.swipe-slide>*{width:100%;margin:0}
.swipe-slide img{width:100%;height:auto;border-radius:var(--radius)}
.swipe.fits .swipe-dots,.swipe.fits .swipe-arw,.swipe.fits .swipe-hint{display:none}
.swipe-arw{display:none;position:absolute;top:calc(50% - 1rem);translate:0 -50%;z-index:4;
  width:46px;height:46px;border-radius:50%;place-items:center;cursor:pointer;
  background:var(--paper);border:2px solid var(--ink);color:var(--ink);
  transition:background .2s,border-color .2s,color .2s,opacity .2s}
html.js .swipe-arw{display:grid}
.swipe-arw svg{width:20px;height:20px}
.swipe-arw:hover{background:var(--mint);border-color:var(--mint);color:var(--ink)}
.swipe-arw.prev{left:2px}
.swipe-arw.next{right:2px}
.swipe-arw[disabled]{opacity:0;pointer-events:none}
@media (hover:none){.swipe-arw{display:none!important}}
.swipe-dots{display:none;justify-content:center;gap:.4rem;margin-top:.2rem}
html.js .swipe-dots{display:flex}
.swipe-dot{width:8px;height:8px;padding:0;border:0;border-radius:999px;background:var(--line-2);cursor:pointer;transition:width .3s var(--ease),background .3s}
.swipe-dot.is-on{width:26px;background:var(--ink)}
.swipe-hint{display:none;position:absolute;right:.2rem;top:-1.9rem;font-size:.68rem;letter-spacing:.16em;text-transform:uppercase;color:var(--dim)}
@media (hover:none){html.js .swipe-hint{display:block}}

/* =====================================================================
   ABOUT

   Two halves and a photograph, twice: Ephraim beside his own paragraph,
   the crew beside a picture of the lift. Then his three stages at full
   width — on Home they are three columns of quotation, here they are the
   page — and the cities the roster has actually reached.
   ===================================================================== */
.duo{display:grid;gap:clamp(1.8rem,5vw,3.2rem);align-items:center}
@media (min-width:960px){
  .duo{grid-template-columns:5fr 7fr}
  .duo.wide{grid-template-columns:7fr 5fr}
}

/* The crew row on About: the crew's paragraph first, and the founder small
   beside it (owner, 2026-10-01: the company is the expert, so About leads
   with the company, the crew and the process, and the founder is one small
   figure after them). The portrait is a cut-out with a real alpha channel, so
   it wants no frame and no crop; the hairline under it is the mint baseline
   the scale figure stands on. */
.crew-row{display:grid;gap:clamp(1.6rem,4vw,3rem);align-items:end}
@media (min-width:860px){.crew-row{grid-template-columns:1fr auto}}
.founder{margin:0;display:flex;gap:1.1rem;align-items:flex-end}
.founder picture{position:relative;display:block;flex:0 0 auto;width:140px}
.founder img{width:100%;height:auto;display:block}
.founder picture::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;background:var(--mint);opacity:.5}
.founder figcaption{display:flex;flex-direction:column;gap:.2rem;padding-bottom:.4rem}
.founder figcaption b{font-size:1.05rem}
.founder figcaption span{font-size:.85rem;color:var(--dim)}
.founder .founder-cta{margin-top:.7rem}
@media (min-width:640px){.founder picture{width:180px}}

/* Every other photograph on this site is a photograph of a wall, so it gets
   the hairline and the near-square corner the cards already use. */
.figframe{margin:0;border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;background:var(--paper-3)}
.figframe img{width:100%;height:auto}
.figcap{padding:.7rem .9rem .8rem;font-size:.8rem;letter-spacing:.06em;color:var(--dim);background:var(--paper-2);border-top:1px solid var(--line)}

/* A pull paragraph: a mint rule down the side, larger than body copy, and
   a small source line underneath. Never a person's words with their name
   under them (owner, 2026-10-01: the company is the expert). */
.pull{margin:1.4rem 0 0;border-left:2px solid var(--mint);padding-left:clamp(1rem,3vw,1.6rem)}
.pull p{font-size:1.08rem;line-height:1.6;color:var(--ink);opacity:.9;max-width:56ch}
.pull cite{display:block;margin-top:.9rem;font-style:normal;font-size:.74rem;letter-spacing:.18em;text-transform:uppercase;color:var(--dim)}
@media (min-width:640px){.pull p{font-size:1.15rem}}

/* --- the three stages, at length -------------------------------------- */
/* Home runs them as three columns. About gives each one a row of its own,
   the stage number in mint where the short mint rule sits on Home, and the
   quotation at a reading measure beside it. */
.beats-full{counter-reset:beat}
@media (min-width:960px){.beats-full{grid-template-columns:1fr;gap:3rem}}
.beats-full .beat{counter-increment:beat;padding-top:1.8rem}
@media (min-width:860px){
  .beats-full .beat{display:grid;grid-template-columns:minmax(0,16rem) minmax(0,1fr);gap:2.6rem;align-items:start}
}
.beats-full .beat h3{font-size:clamp(1.3rem,3vw,1.9rem);margin-bottom:0}
.beats-full .beat h3::before{
  content:"0" counter(beat);display:block;width:auto;height:auto;background:none;
  font-family:var(--font-display);font-variation-settings:'wdth' 125,'wght' 800;
  font-size:1.5rem;letter-spacing:-.02em;color:var(--pop);margin-bottom:.45rem;
}
.beats-full .beat blockquote p{max-width:62ch;font-size:1.02rem;margin-top:.9rem}
@media (min-width:860px){.beats-full .beat blockquote p{margin-top:0}}

/* --- where the walls are ---------------------------------------------- */
.cities{display:grid;grid-template-columns:repeat(2,1fr);gap:1.4rem 1.2rem}
@media (min-width:640px){.cities{grid-template-columns:repeat(4,1fr)}}
.city{border-top:1px solid var(--line-2);padding-top:.9rem}
.city b{display:block;font-family:var(--font-display);text-transform:uppercase;font-variation-settings:'wdth' 100,'wght' 800;font-size:1.05rem;letter-spacing:.01em;font-weight:400}
.city span{display:block;margin-top:.3rem;font-size:.74rem;letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}

/* =====================================================================
   SERVICES

   Four blocks: the walls, the signs, the paint, and the cleaning side of
   the business that has a page of its own. Nothing new is invented here —
   the quotation, the stat row and the two-column split are the components
   Home and About already use.
   ===================================================================== */
/* The stat row sits in half a page here rather than the full width, so it
   stays two across at every size instead of opening out to four. */
@media (min-width:960px){.stats-2{grid-template-columns:repeat(2,1fr)}}

.tags{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.4rem}
.tags li{border:1px solid var(--line-2);border-radius:999px;padding:.45rem .95rem;font-size:.86rem;font-weight:600;color:var(--muted)}

/* A label over a quotation, in the display face so it reads as a stage name
   rather than as a sentence. */
.svc-h{
  font-family:var(--font-display);text-transform:uppercase;font-weight:400;
  font-variation-settings:'wdth' 100,'wght' 800;
  font-size:1.05rem;letter-spacing:.02em;margin-top:2.2rem;
}

/* =====================================================================
   CONTACT

   The form on the left, four cards on the right, and every one of them is
   real without JavaScript: the fields are server-rendered, the two link
   cards are ordinary links, and the note under the button carries the
   address to write to when nothing else works.
   ===================================================================== */
.contact-grid{display:grid;gap:clamp(2rem,5vw,3.2rem);align-items:start}
@media (min-width:960px){.contact-grid{grid-template-columns:7fr 5fr}}

.form{display:grid;gap:1.1rem;margin-top:1.8rem}
.form .row{display:grid;grid-template-columns:1fr;gap:1.1rem}
@media (min-width:560px){.form .row{grid-template-columns:1fr 1fr}}
.field{display:grid;gap:.45rem}
.field label{font-size:.72rem;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:var(--muted)}
.field input,.field textarea,.field select{
  width:100%;background:var(--paper);border:1px solid var(--line-2);color:var(--ink);
  padding:.85rem 1rem;border-radius:var(--radius);font:inherit;font-size:1rem;
  transition:border-color .2s,box-shadow .2s;
}
.field input::placeholder,.field textarea::placeholder{color:var(--dim)}
.field input:focus,.field textarea:focus,.field select:focus{outline:none;border-color:var(--mint);box-shadow:0 0 0 3px var(--mint-glow)}
.field textarea{min-height:150px;resize:vertical}
.field option{background:var(--paper);color:var(--ink)}
.field .hint{font-size:.8rem;line-height:1.5;color:var(--dim)}
/* The chevron is drawn with a border rather than an inline SVG: a background
   image would have to name a colour in hex, and the only hexes on this site
   live in :root. */
.sel{position:relative;display:block}
.sel select{appearance:none;-webkit-appearance:none;padding-right:2.6rem}
.sel::after{
  content:"";position:absolute;right:1.1rem;top:50%;width:.46rem;height:.46rem;
  border-right:2px solid var(--muted);border-bottom:2px solid var(--muted);
  transform:translateY(-70%) rotate(45deg);pointer-events:none;
}
/* The honeypot: off the screen, out of the tab order, never shown to anybody
   who is not a robot. display:none would keep some bots from filling it. */
.gotcha{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden}
.form-note{margin:0;font-size:.86rem;line-height:1.6;color:var(--dim)}
.form-note a{color:var(--ink);text-decoration:underline;text-underline-offset:2px}
.form-status{display:none;padding:1rem 1.2rem;border:1px solid var(--line-2);border-radius:var(--radius);background:var(--paper-2)}
.form-status.on{display:block}
.form-status a{color:var(--ink);text-decoration:underline;text-underline-offset:2px}

.contact-side{display:grid;gap:1rem}
.contact-card{display:flex;gap:1rem;align-items:flex-start;padding:1.4rem;background:var(--paper-2);border:1px solid var(--line);border-radius:var(--radius);transition:border-color .2s}
a.contact-card:hover,a.contact-card:focus-visible{border-color:var(--mint)}
.contact-card .ic{flex:none;display:grid;place-items:center;width:42px;height:42px;border:1px solid var(--line-2);border-radius:var(--radius);background:var(--paper-3);color:var(--ink)}
.contact-card .ic svg{width:20px;height:20px}
.contact-card b{display:block;margin-bottom:.25rem;font-size:.95rem}
.contact-card span{display:block;color:var(--muted);font-size:.9rem;line-height:1.6;word-break:break-word}

/* =====================================================================
   GRAFFITI REMOVAL

   Since 2026-10-08 the page follows the live Wix page section by section
   (owner: "match more closely to the graffiti removal page on
   openairgallery.art"): an inset photograph hero with the headline on the
   left and the sentence and button on the right; a centred heading over
   three outlined service cards (title, picture, copy, button); a centred
   two-line heading over the three steps; and a left-aligned "Book a free
   consult" over the enquiry form, which ends the page. build.py, GRAFFITI
   REMOVAL, lists what stands in for each Wix stock picture. The
   before/after waits for real photographs and renders nothing until then.
   ===================================================================== */

/* --- the hero: inset, held at the photograph's own width --------------- */
.gr-hero{
  --gr-inset:clamp(1rem,3.2vw,2.8rem);
  width:min(var(--gr-cap,1240px),calc(100% - 2 * var(--gr-inset)));
  margin:var(--gr-inset) auto 0;border-radius:var(--radius);
  min-height:clamp(520px,80vh,860px);display:flex;align-items:center;
}
.gr-hero .wrap{width:calc(100% - clamp(2rem,9vw,9rem))}
.gr-hero .hero-inner{max-width:none;display:grid;grid-template-columns:1fr;column-gap:3rem}
.gr-hero .hero-inner > *{grid-column:1}
.gr-hero .hero-inner > .eyebrow:empty{display:none}
.gr-hero h1{font-size:clamp(3rem,7.2vw,6.6rem);line-height:.92}
.gr-h1-pre{display:block}
.gr-hero .lead{margin-top:.9rem;font-style:italic;font-size:clamp(1rem,1.3vw,1.15rem)}
.gr-hero-side{margin-top:1.8rem;max-width:30rem}
.gr-hero-side p{margin:0;color:var(--on-ink);font-size:1.05rem;line-height:1.6;text-shadow:0 1px 14px var(--ink)}
.gr-hero .btn-row{margin-top:1.3rem}
@media (min-width:900px){
  .gr-hero .hero-inner{grid-template-columns:minmax(0,1.35fr) minmax(0,1fr)}
  .gr-hero-side{grid-column:2 !important;grid-row:1 / span 5;align-self:end;justify-self:end;
    margin:0;text-align:right}
  .gr-hero-side .btn-row{justify-content:flex-end}
}

/* --- the headings ------------------------------------------------------ */
.gr-title{text-align:center;font-size:clamp(2.4rem,5.4vw,4.8rem);margin:0 auto}
.gr-title-left{font-size:clamp(2.6rem,6vw,5.2rem);margin:0;max-width:12ch}
.gr-kicker{display:block;margin-bottom:.35rem;font-family:var(--font-display);font-weight:800;
  font-size:clamp(1.15rem,2vw,1.8rem);line-height:1.2;letter-spacing:-.005em;text-transform:none}
.gr-pop{font-style:normal;color:var(--pop)}
.gr-pop-ink{font-style:normal;font-family:var(--font-marker);font-weight:400}

/* --- the three services ------------------------------------------------ */
.gr-cards{list-style:none;margin:clamp(2rem,4.4vw,3.2rem) 0 0;padding:0;
  display:grid;gap:clamp(1rem,1.6vw,1.4rem);grid-template-columns:1fr}
@media (min-width:900px){.gr-cards{grid-template-columns:repeat(3,minmax(0,1fr))}}
.gr-card{display:flex;flex-direction:column;align-items:flex-start;
  padding:clamp(1.2rem,2.2vw,1.6rem);background:var(--paper);
  border:1px solid var(--mint-deep);border-radius:var(--radius)}
.gr-card h3{margin:0 0 1rem;font-size:clamp(34px,3.2vw,46px);line-height:.95;color:var(--ink)}
.gr-card h3 .gr-pop{display:block}
.gr-media{position:relative;width:100%;aspect-ratio:16/10;overflow:hidden;
  border-radius:var(--radius);margin-bottom:1.3rem;background:var(--paper-2)}
.gr-media picture,.gr-media img{display:block;width:100%;height:100%;object-fit:cover}
.gr-media-type{background:var(--paper);border:1px solid var(--line)}
.gr-media-type .stroke-wrap{inset:10% -4% 14%}
.gr-num{line-height:1;color:var(--ink)}
.gr-media-type .gr-num{position:absolute;inset:0;display:grid;place-items:center;
  font-size:clamp(64px,7vw,104px);z-index:1}
.gr-media-photo .gr-num{position:absolute;left:.7rem;top:.7rem;padding:.15rem .6rem;
  background:var(--mint);border-radius:var(--radius);font-size:1.6rem}
.gr-card p{margin:0 0 1.5rem;color:var(--ink);font-size:16px;line-height:1.65;flex:1}

/* --- the three steps, on the painted band ------------------------------ */
.gr-process .gr-title{margin-bottom:clamp(2rem,4.4vw,3.2rem)}
.gr-steps{list-style:none;margin:0;padding:0;display:grid;gap:clamp(1rem,1.6vw,1.4rem);
  grid-template-columns:1fr;text-align:left}
@media (min-width:900px){.gr-steps{grid-template-columns:repeat(3,minmax(0,1fr))}}
.gr-step{display:flex;flex-direction:column;align-items:flex-start;background:var(--paper);
  border-radius:var(--radius);padding:clamp(1.4rem,2.4vw,2rem);box-shadow:0 18px 40px -28px var(--ink)}
.gr-step-n{font-size:clamp(56px,5vw,76px);line-height:1;color:var(--ink);margin-bottom:.6rem}
.gr-step h3{margin:0 0 .8rem;font-size:clamp(30px,2.8vw,40px);line-height:.95;color:var(--ink)}
.gr-step p{margin:0 0 1.4rem;color:var(--ink);font-size:16px;line-height:1.65;flex:1}

/* --- the consult and its form ------------------------------------------ */
.gr-form{margin-top:clamp(1.8rem,4vw,2.8rem)}

/* --- before / after ---------------------------------------------------- */
/* Two pictures, side by side and labelled. That is the whole component with
   no script, and it is honest: both walls are real content in the server
   HTML and both are readable at once.
   With a script the pair stacks into one frame under a mint divider — the
   same two layers, one on top of the other. Everything that does the
   stacking is scoped to html.motion, so the reduced-motion render and the
   no-script render are the same page, and the drag is never the only way to
   see either state. */
.ba{margin:0}
.ba-stage{display:grid;gap:1rem;grid-template-columns:1fr}
@media (min-width:640px){.ba-stage{grid-template-columns:1fr 1fr}}
/* The after layer is first in the DOM because the before has to stack on top
   of it under html.motion. Side by side, the pair reads the other way round —
   before, then after — so the grid puts it back. */
.ba-before{order:-1}
.ba-layer{
  position:relative;overflow:hidden;line-height:0;
  border:1px solid var(--line);border-radius:var(--radius);background:var(--paper-2);
}
.ba-layer picture{display:block;height:100%}
.ba-layer svg,.ba-layer img{display:block;width:100%;height:auto;aspect-ratio:16/9;object-fit:cover}
.ba-lab{
  position:absolute;left:.7rem;bottom:.7rem;z-index:2;
  padding:.3rem .7rem;border-radius:999px;
  background:var(--scrim);border:1px solid var(--line-2);
  font-family:var(--font-body);font-size:.7rem;font-weight:700;line-height:1.4;
  letter-spacing:.18em;text-transform:uppercase;color:var(--on-ink);
}
.ba-handle{display:none}
.ba-cap{margin-top:.85rem;font-size:.86rem;line-height:1.5;color:var(--muted)}
.ba-hint{display:none;color:var(--on-ink)}

html.motion .ba-stage{
  display:block;position:relative;aspect-ratio:16/9;overflow:hidden;
  border:1px solid var(--line);border-radius:var(--radius);background:var(--paper-2);
  touch-action:pan-y;cursor:ew-resize;
}
html.motion .ba-layer{position:absolute;inset:0;border:0;border-radius:0;background:none}
html.motion .ba-layer svg,html.motion .ba-layer img{width:100%;height:100%;aspect-ratio:auto}
/* The before is clipped, not moved: a clip is paint-only, so the divider can
   sit anywhere without either picture ever changing size. --bx is written by
   site.js in one rAF and starts, in the stylesheet, at half way. */
html.motion .ba-before{clip-path:inset(0 calc(100% - var(--bx,50%)) 0 0)}
html.motion .ba-after .ba-lab{left:auto;right:.7rem}
html.motion .ba-hint{display:inline}
html.motion .ba-handle{
  display:block;position:absolute;top:0;bottom:0;z-index:3;
  left:var(--bx,50%);translate:-50% 0;width:44px;padding:0;
  background:none;border:0;cursor:ew-resize;touch-action:none;
}
/* The mint line itself, drawn inside the button so the grab area is a finger
   wide and the divider is one pixel. */
html.motion .ba-handle::before{
  content:"";position:absolute;top:0;bottom:0;left:calc(50% - 1px);width:2px;
  background:var(--mint);box-shadow:0 0 0 1px var(--shade);
}
html.motion .ba-handle:focus-visible{outline:2px solid var(--mint);outline-offset:2px;border-radius:var(--radius)}
.ba-grip{
  position:absolute;top:50%;left:50%;translate:-50% -50%;
  display:flex;align-items:center;justify-content:center;
  width:44px;height:44px;border-radius:50%;
  background:var(--mint);color:var(--ink);
}
.ba-grip svg{width:15px;height:15px;margin:0 -3px}
@media (prefers-reduced-motion:no-preference){
  html.motion .ba-handle:hover .ba-grip,html.motion .ba-stage.is-live .ba-grip{transform:scale(1.06)}
  .ba-grip{transition:transform .2s var(--ease)}
}

/* =====================================================================
   THE GRAFFITI-REMOVAL BAND AND STRIP

   The other half of the business, advertised on the pages of the first —
   the shape DroneGodMax uses for Green Flash (gf_band()/gf_strip()), with
   this site's content and this site's palette rule.

   The band is the ONE light block on a dark site: a scrubbed --wall panel
   with near-black type and a diagonal ghost of overpaint fading out of the
   top-left corner. That is the pitch in one image, and it is why it may
   only happen once. Mint never sits on --wall (1.16:1), so on this panel
   the accent appears the only way it is allowed to — ink on mint, in the
   eyebrow pill and in the primary button, both 13.8:1.
   ===================================================================== */
.gr-band-wrap{padding-top:0}
.gr-band{
  position:relative;isolation:isolate;overflow:hidden;
  border-radius:var(--radius);
  background:var(--wall);color:var(--ink);
  padding:clamp(1.8rem,5vw,3.4rem);
  display:grid;gap:clamp(1.8rem,4vw,3rem);align-items:center;
}
/* The ghost of the last buff job: whoever painted this wall out did not
   match the colour, and the patch is still there in a raking light. */
.gr-band::before{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:linear-gradient(115deg,var(--ghost),transparent 42%);
}
@media (min-width:960px){
  .gr-band{grid-template-columns:minmax(0,1fr) minmax(0,1.05fr)}
}
.gr-band h2{margin-bottom:1rem}
.gr-band .lead{color:var(--ink-mute);opacity:1;max-width:56ch}
.gr-band .btn-row{margin-top:1.8rem}
/* The eyebrow is mint everywhere else on the site; here it is the mint. */
/* The eyebrow was a mint pill here because mint could not carry type on
   this panel and ink on mint could. It is the marker now, and on the
   scrubbed panel the marker is ink: --pop on --wall is under the floor the
   coral is allowed to carry type at, and this is the one block on the site
   whose ground is neither the page nor the paint. */
.gr-band .eyebrow{
  color:var(--ink);background:none;padding:0;margin-bottom:.9rem;
}
.gr-band .tags{margin-top:1.4rem}
.gr-band .tags li{border-color:var(--ink-line);color:var(--ink-mute)}
.gr-band .btn-ghost{border-color:var(--ink-line);color:var(--ink)}
.gr-band .btn-ghost:hover{border-color:var(--ink);color:var(--ink)}
/* The photograph: his own, from the Wix graffiti page, with a one-line
   caption under it. */
/* Words first in the document, photograph first on the page — at every width. */
.gr-band-photo{margin:0;min-width:0;order:-1}
.gr-band-photo picture,.gr-band-photo img{display:block;width:100%;height:auto}
.gr-band-photo img{border-radius:var(--radius);aspect-ratio:3/2;object-fit:cover}
.gr-band-photo figcaption{margin-top:.75rem;font-size:.86rem;line-height:1.5;color:var(--ink-mute)}

/* --- the slim one-liner ------------------------------------------------ */
/* Work, Services and About get the offer in one line, on the dark canvas
   they already are: the light panel is Home's, once. */
.gr-strip-wrap{padding-top:0}
.gr-strip{
  display:grid;gap:1.2rem;align-items:center;
  padding:1.3rem clamp(1.2rem,3vw,1.8rem);
  border:1px solid var(--line-2);border-radius:var(--radius);
  background:linear-gradient(90deg,var(--mint-glow),transparent 55%),var(--paper-2);
}
.gr-strip p{margin:0;color:var(--muted);font-size:.98rem;line-height:1.6}
.gr-strip p b{color:var(--ink)}
.gr-strip-links{display:flex;gap:.5rem;flex-wrap:wrap}
@media (min-width:860px){.gr-strip{grid-template-columns:1fr auto;gap:1.8rem}}

/* =====================================================================
   SCALE — the six-foot figure, dragged

   The static scale bar is the component: she is server-rendered at
   --fx:.12, sized (6 / ft) of the picture, with a real "6 ft" caption, and
   that is a complete and honest answer to "how big is this wall" with no
   script at all. Everything below only lets a visitor walk her along it.

   So it is all scoped to html.motion, which the bootstrap withholds when
   the visitor has asked for reduced motion — the reduced-motion render is
   the static one, and site.js binds no listeners there either. No counter,
   no percentage, no readout: the answer is how small she looks.
   ===================================================================== */
/* pan-y, not none: a finger must still be able to scroll the page past a
   wall that fills it. A horizontal drag reaches the handler regardless. */
html.motion .scale{touch-action:pan-y}
html.motion .fig{cursor:grab;touch-action:none}
html.motion .scale.is-live .fig{cursor:grabbing}
/* Keyboard steps and the tilt settle; a drag does not, because a figure that
   lags the finger holding it does not feel like a figure. */
html.motion .fig{transition:left .18s var(--ease)}
html.motion .scale.is-live .fig,html.motion .scale.is-tilting .fig{transition:none}
html.motion .fig:focus-visible{outline:2px solid var(--mint);outline-offset:3px;border-radius:2px}

/* The hint is an instruction for something only the script can do, so it is
   not in the document until the script could do it — and it goes as soon as
   the visitor has done it once. */
.scale-hint{
  display:none;position:absolute;right:1rem;bottom:calc(var(--base,7%) + .6rem);
  font-size:.66rem;letter-spacing:.14em;text-transform:uppercase;color:var(--mint);
  text-shadow:0 1px 8px var(--ink);pointer-events:none;
}
html.motion .scale-hint{display:block}
html.motion .scale.is-used .scale-hint{opacity:0}
@media (prefers-reduced-motion:no-preference){
  html.motion .scale-hint{transition:opacity .4s var(--ease)}
}
@media (prefers-reduced-motion:reduce){
  html.motion .fig{transition:none}
  html.motion .scale-hint{display:none}
}

/* "Save our contact" — the vCard button, on the contact sidebar and beside
   the founder on About. */
.contact-card-cta .mt{margin-top:.75rem}
.contact-card-cta .btn{width:auto}
.btn svg{width:1.05em;height:1.05em;margin-right:.45em;vertical-align:-.18em}

/* =====================================================================
   Wall — the blank wall you can paint (site.js, "Wall").

   One element, built by the script and not in the document without it: a
   canvas the size of the hero. (It had a sound toggle until 2026-10-08; the
   wall is silent now and .paint-snd below is the tin's on /services.) So
   this whole block describes things that do not exist under reduced motion
   or with no script, and the hero there is the hero it always was.

   The canvas goes at z-index -1 inside .page-hero's own stacking context —
   the place the hero's shade sits on a hero that has a photograph — so the
   paint is over the background and under every word, and it is
   pointer-events:none: the listeners are on the hero, and a link in the
   hero is still a link. Neither element is in flow, so painting the wall
   cannot move anything on the page.
   ===================================================================== */
.paint-wall{
  position:absolute;inset:0;width:100%;height:100%;
  z-index:-1;pointer-events:none;
}
/* Only while a stroke is actually being laid: a plain vertical swipe has
   already been let through to the page by then. */
html.motion .page-hero[data-wall].is-painting{touch-action:none}

/* The kit's sound toggle (oagKit.sndToggle), in the corner of a hero and
   absolutely positioned so it costs the hero no height. Muted is the
   default and the state is shared. Since 2026-10-08 only the tin on
   /services puts one up. */
.paint-snd{
  position:absolute;z-index:2;
  right:clamp(1rem,4vw,2.4rem);bottom:clamp(.9rem,2.6vw,1.6rem);
  display:inline-flex;align-items:center;gap:.45rem;
  padding:.5rem .8rem;border:1px solid var(--line);border-radius:var(--radius);
  background:var(--ink-2);color:var(--muted);cursor:pointer;
  font-family:var(--font-body);font-weight:700;font-size:.72rem;
  letter-spacing:.12em;text-transform:uppercase;line-height:1;
  transition:color .2s,border-color .2s;
}
.paint-snd:hover{color:var(--paper);border-color:var(--line-2)}
.paint-snd svg{width:15px;height:15px;display:block}
.paint-snd .paint-snd-on{display:none}
.paint-snd[aria-pressed="true"]{color:var(--mint);border-color:var(--mint-deep)}
.paint-snd[aria-pressed="true"] .paint-snd-on{display:block}
.paint-snd[aria-pressed="true"] .paint-snd-off{display:none}
@media (max-width:520px){
  .paint-snd .paint-snd-t{display:none}
  .paint-snd{padding:.5rem .55rem}
}
@media (prefers-reduced-motion:reduce){
  .paint-snd{transition:none}
}

/* The 404's hero is the wall and nothing else, so it is given the height of
   one: enough blank to be worth a brush, and the words sit in the top of
   it the way they do on every other hero. */
.page-hero.blank{min-height:clamp(420px,60vh,600px)}
.page-hero.blank .btn-row{margin-top:1.8rem}

/* =====================================================================
   TIP — the tin you knock over (site.js, "Tip").

   Owner, 2026-09-19: "Maybe a paint bucket you click and it spills."

   The tin and the rule are in the server HTML and are finished there (see
   the block under `page hero`, above). Everything in here is the mechanic,
   and every selector in it is under html.motion: the sheet, the pool and
   the run are built by site.js and exist nowhere else, and a visitor who
   asked for reduced motion never gets html.motion at all, so there is
   nothing to hold still — the tin stands up and the rule is mint.

   The geometry is build.py's (the photograph's — art.py's same-named
   constants are the drawn tin's, kept as the fallback), named once here,
   checked against build.py at build time, and read back out of here by
   site.js rather than written a third time:
     --tip-pvx/--tip-pvy   CAN_TIP_PIVOT, the edge of the base it rolls on
     --tip-px/--tip-py     CAN_TIP_POUR, where the paint leaves it at rest
     --tip-deg             CAN_TIP_DEG, where it settles once it is over
   --tip-x is where it lands (site.js measures it): a per cent of the hero for the pool, and a
   per cent of the rule for the rule, which are different boxes and so are
   two values of the same name on two elements.

   No filter is used anywhere in it. The pour is a canvas drawn at the
   pixel, the pool is a height and a wobble, and the only filters on this page are
   the ones inside the drawing of the tin, which are as static as the
   drawing is.                                                          */
html.motion .page-hero.has-tip{
  --tip-pvx:0.724; --tip-pvy:0.96;
  --tip-px:1.583; --tip-py:0.752;
  --tip-deg:76deg;
  --pool-h:14px;
  --tip-x:20%;
}
@media (max-width:700px){html.motion .page-hero.has-tip{--pool-h:10px}}

/* --- the tin going over ----------------------------------------------
   One rotation about one point. The drag writes --tipdeg and holds the
   tin wherever the pointer has carried it; let go past forty degrees and
   oaTipOver takes it the rest of the way from exactly there, overshoots
   past its resting angle and settles back on it — a tin has weight, and
   a tin that arrives at its angle and stops has none. */
html.motion .tip-can{
  transform-origin:calc(var(--tip-pvx) * 100%) calc(var(--tip-pvy) * 100%);
  transform:rotate(0deg);
  transition:transform .55s var(--ease);
}
html.motion .page-hero.has-tip.is-dragging .tip-can{
  transform:rotate(var(--tipdeg,0deg));transition:none;animation:none;
}
html.motion .page-hero.has-tip.tipping .tip-can{
  animation:oaTipOver 1.15s cubic-bezier(.3,.7,.4,1) both;
}
html.motion .page-hero.has-tip.righting .tip-can{
  animation:oaTipUp .78s cubic-bezier(.3,.8,.35,1) both;
}
@keyframes oaTipOver{
  0%{transform:rotate(var(--tipdeg,0deg))}
  46%{transform:rotate(92deg)}
  66%{transform:rotate(66deg)}
  84%{transform:rotate(81deg)}
  100%{transform:rotate(var(--tip-deg))}
}
@keyframes oaTipUp{
  0%{transform:rotate(var(--tip-deg))}
  56%{transform:rotate(-7deg)}
  80%{transform:rotate(3deg)}
  100%{transform:rotate(0deg)}
}
/* A tin on its side casts no shadow on the floor it has left. */
html.motion .tip-can .can-shadow{transition:opacity .4s var(--ease),transform .4s var(--ease)}
html.motion .page-hero.has-tip.tipping .tip-can .can-shadow,
html.motion .page-hero.has-tip.is-dragging .tip-can .can-shadow{opacity:.15}

/* The paint inside. It is level whatever the tin does, so as the tin turns
   it turns the other way by the same amount; then it leans to the lip and
   drains, and the lens of paint over the lip comes up as it goes. */
html.motion .tip-can .can-paint{
  transform-box:view-box;transform-origin:100px 62px;
  transition:transform .5s var(--ease),opacity .5s var(--ease);
}
html.motion .page-hero.has-tip.tipping .tip-can .can-paint{
  transform:rotate(calc(var(--tip-deg) * -1)) translateX(16px);
}
html.motion .page-hero.has-tip.pouring .tip-can .can-paint{
  transform:rotate(calc(var(--tip-deg) * -1)) translateX(30px) scaleX(.16);
  opacity:.85;transition-duration:1.26s;
}
/* The photograph's paint is a box laid over the rim, not a group inside a
   drawing, so it does not counter-rotate: turned level it would stand out
   past a rim that is now on its side. It runs to the lip instead — drawn
   toward the right-hand edge it pours from — and is gone by the time the
   sheet has reached the floor. Later in the file than the drawn tin's
   rules and as specific, so these are the ones that hold. */
html.motion .tip-photo .can-paint{
  transform-box:border-box;transform-origin:100% 50%;
  transition:transform .5s var(--ease),opacity .5s var(--ease);
}
html.motion .page-hero.has-tip.tipping .tip-photo .can-paint{transform:scaleX(.72)}
html.motion .page-hero.has-tip.pouring .tip-photo .can-paint{
  transform:scaleX(.1);opacity:0;transition-duration:1.26s;
}

/* --- the pour --------------------------------------------------------
   One canvas, drawn by site.js frame by frame while the tin is over: the
   stream let go from the lip and falling under gravity, the meniscus on
   the rim, the drips at the end, and the swell and ripples where it lands
   in the pool. site.js sets its box in px inside the tin's own box, with
   its bottom floored to the hero's bottom edge, because the hero is a
   scrollport and a canvas a hair below it is scrollable overflow. It is
   over the tin (the meniscus sits on the lip) and under nothing else. */
html.motion .tip-pour{position:absolute;display:block;pointer-events:none;z-index:1}

/* --- the pool --------------------------------------------------------
   A height and a wobble, exactly as the concept says, along the bottom
   edge of the hero. It sits at z-index -1 inside .page-hero's own
   stacking context — the layer the hero's shade sits in — so the paint is
   over the photograph and under every word, and it is pointer-events
   none. On a phone --slosh leans it toward the low side: half a degree at
   full tilt, which on a hero's width is a few pixels of surface either
   way, which is what a shallow pool of paint does.

   The pool is two boxes, not one, and the reason is the lesson written up
   under Cure: .page-hero is overflow:hidden, which makes it a scrollport,
   and anything that leaves its padding box — a meniscus two pixels proud
   of the water, a surface leaning two per cent past the ends — becomes
   scrollable overflow that a scrollIntoView or a focus() can then scroll
   the hero's own words by. So the outer box is fixed, sits exactly on the
   hero's bottom edge and clips; the water inside it is the thing with the
   animated height, and the meniscus and the lean are the water's. */
html.motion .tip-pool{
  position:absolute;left:0;right:0;bottom:0;height:calc(var(--pool-h) * 3);
  z-index:-1;pointer-events:none;overflow:hidden;
}
/* The water. It is deepest where the sheet came down and runs thin toward
   the ends of the wall, which is what paint on a floor does and is also
   what keeps a band of mint across the foot of the hero from reading as a
   second rule. It clips, so the meniscus inside it is gone the moment the
   water is gone and there is never a mint hairline on an untipped hero. */
html.motion .tip-pool-body{
  position:absolute;left:-2%;right:-2%;bottom:0;height:0;overflow:hidden;
  background:linear-gradient(180deg,var(--mint-wet) 0,var(--mint) 20%,var(--mint-deep) 100%);
  -webkit-mask-image:radial-gradient(140% 100% at var(--tip-x) 100%,var(--paper) 26%,var(--pool-thin) 100%);
          mask-image:radial-gradient(140% 100% at var(--tip-x) 100%,var(--paper) 26%,var(--pool-thin) 100%);
  transform-origin:50% 100%;
  transform:rotate(calc(var(--slosh,0) * .5deg));
  transition:height .59s var(--ease-roll),transform .3s linear;
}
html.motion .page-hero.has-tip.pooled .tip-pool-body{height:var(--pool-h)}
html.motion .page-hero.has-tip.righting .tip-pool-body{height:0;transition-duration:.7s}
/* The meniscus, at the waterline and inside it: it thickens and thins
   rather than rising, because a surface that rose would be clipped — and
   a pool whose surface left its own box is the overflow story again. */
/* Round three: the pour is mint, and the meniscus picks up the coral where
   the pool meets the run that goes through to the rule — --tip-x is that
   point on both boxes, so the coral is brightest exactly where the paint is
   leaving and thins away along the surface. Two background layers: the
   coral rim over the pale mint the surface always carried. */
html.motion .tip-pool-lip{
  position:absolute;left:0;right:0;top:0;height:4px;border-radius:50%;
  transform-origin:50% 0;
  background:
    radial-gradient(22% 260% at var(--tip-x) 60%,var(--pop) 0,var(--pop-wet) 34%,var(--pop-wet-0) 100%),
    linear-gradient(90deg,var(--mint-wet-0),var(--mint-wet) 28%,
             var(--mint-wet) 72%,var(--mint-wet-0));
}
html.motion .page-hero.has-tip.pooled .tip-pool-lip{animation:oaTipWobble 2.6s ease-in-out infinite}
@keyframes oaTipWobble{
  0%,100%{transform:translateY(0) scaleY(1)}
  28%{transform:translateY(.7px) scaleY(1.7)}
  55%{transform:translateY(0) scaleY(.7)}
  80%{transform:translateY(.4px) scaleY(1.3)}
}
/* --- the run onto the section below ----------------------------------
   Spray's drip, markup and keyframes and all: a run of paint is a run of
   paint and this file already draws one. It hangs from the top edge of
   the section, which is the hero's bottom edge, so the pool and the run
   meet across a boundary the hero's own overflow would otherwise cut.
   .dry is the last of it going into the rule as the pool drains. */
html.motion .tip-drop.dry .spray-run{animation:oaTipDryRun .5s var(--ease) both}
html.motion .tip-drop.dry .spray-bead{animation:oaTipDryBead .5s var(--ease) both}
@keyframes oaTipDryRun{from{transform:scaleY(1)}to{transform:scaleY(0)}}
@keyframes oaTipDryBead{
  from{transform:translateY(calc(var(--dh) - 50%));opacity:1}
  to{transform:translateY(calc(var(--dh) - 50%));opacity:0}
}

/* --- the rule it becomes ---------------------------------------------
   The rule fills outward from the point the run came through — --tip-x
   is its transform-origin — and then takes the Cure sheen, which is the
   same pass of mint the figures take, at the same speed, off the same
   keyframes. .cured is the state and .curing the pass: the rule is
   painted once and stays painted, because paint does not come off a wall
   because the tin stood back up.

   The bail-out here is the only one in this file that runs the other way.
   Everything else hidden under html.motion reveals itself after 2.8s in
   case the script never lands; a rule that did that would be mint before
   the visitor had touched the tin, which is the mechanic given away. So
   it is 2.8s for a page whose site.js never arrives, and site.js calls it
   off the moment it binds — html.tipready, exactly as .rollin calls off
   the primer's. */
html.motion .tiprule-line{
  transform:scaleX(var(--tip-fill,0));
  transition:transform 1.2s var(--ease-roll);
  animation:oaTipFail 0s linear 2.8s forwards;
}
@keyframes oaTipFail{to{transform:none}}
/* Owner's call (2026-09-19): the section must never sit without its rule.
   Once site.js has bound, the rule still paints itself -- just late enough
   (8s) that a visitor who reaches for the tin gets the pour first. */
html.tipready .tiprule-line{animation:oaTipFail 0s linear 8s forwards}
html.motion .tiprule.filling .tiprule-line{--tip-fill:1}
html.motion .tiprule.curing::after{
  content:"";position:absolute;left:0;right:0;top:0;height:2px;pointer-events:none;
  background:linear-gradient(90deg,transparent,var(--mint-wet),transparent) no-repeat;
  background-size:40% 100%;
  animation:oaCure 1.8s var(--ease) 1 both;
}

@media (prefers-reduced-motion:reduce){
  /* Tip: the tin stands up, the rule is mint, and there is no sheet, no
     pool and no run — site.js builds none of them here and never gives
     the tin a role. This says it a second time, in case the class ever
     arrives anyway. */
  html.motion .tip-can{transform:none;transition:none;animation:none}
  html.motion .tip-can .can-paint,html.motion .tip-can .can-shadow{transform:none;opacity:1}
  html.motion .tip-pour,html.motion .tip-pool,html.motion .tip-drop{display:none}
  html.motion .tiprule-line{transform:none;animation:none}
  html.motion .tiprule.curing::after{content:none;animation:none}
}

/* =====================================================================
   PAGES — the inner pages of the round-three redesign.

   Everything below this line belongs to the pages agent and to the inner
   pages only: the "Painted for" wall, the faces strand, Public works, the
   Services diamond board, and the round-three dress on /work, the project
   pages, /about, /graffiti-removal, /contact and /404.

   The sitewide skin — the tokens, the five faces, .tall / .wide / .serif /
   .marker / .pop, the nav, the footer, the brush-stroke bands and Home —
   is the skin agent's and is above this line. Nothing here redefines a
   token or a face; it only uses them. Where a rule needs a value the skin
   has not shipped yet, it comes through a var() with a fallback, so this
   block is correct both before and after that lands.
   ===================================================================== */

/* --- the "Painted for" wall ----------------------------------------
   The clean version (owner, 2026-10-01: "something more clean"). An even
   grid — five across, three on a phone — ruled with hairlines between the
   cells and none round the outside, on white, with a lot of air. The marks
   are the brands' own files in the brands' own colours (brands.py), so
   nothing here paints them; each is set to its own optical height (h / hm),
   never to the box, and nothing happens on hover.

   The rules are drawn by each cell's top and left edge, and the frame hides
   the first row's top and the first column's left by pulling the list a
   pixel up and left under overflow:hidden — so only the lines *between*
   cells show, at any count. The list is flex rather than grid so a short
   last row is centred instead of leaving a hole at its end. */
.brandwall{padding:104px 0 112px;background:var(--paper,#fff)}
.bw-head{display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;margin:0 0 56px}
.bw-kick{margin:0;font-family:var(--font-body);font-weight:600;font-size:12px;
  letter-spacing:.22em;text-transform:uppercase;color:var(--muted)}
.bw-title{margin:0;font-family:var(--font-serif);font-weight:400;text-transform:none;
  letter-spacing:-.01em;line-height:1.05;font-size:clamp(30px,3.2vw,44px);color:var(--ink)}
.bw-frame{overflow:hidden}
.bgrid{list-style:none;padding:0;margin:-1px 0 0 -1px;width:calc(100% + 1px);
  display:flex;flex-wrap:wrap;justify-content:center}
.bcell{--cols:5;flex:0 0 calc(100% / var(--cols));aspect-ratio:2/1;
  border-top:1px solid var(--line);border-left:1px solid var(--line);
  display:flex;align-items:center;justify-content:center;padding:16px;text-align:center}
.bmark{display:block;max-width:100%}
.bcell .bmark{height:var(--bh);width:auto;max-width:70%;object-fit:contain}
.bword{line-height:1.05;font-size:15px;letter-spacing:.02em}
/* The four that are still type are set one way, so they read as four
   lockups of the same kind rather than four headlines in four faces: the
   display face, uppercase, tracked, two words stacked as a logo stacks. */
.bcell .bword,.bcell .bword.wide,.bcell .bword.tall,.bcell .bword.serif{
  font-family:var(--font-display);font-variation-settings:'wdth' 112,'wght' 760;
  font-weight:760;font-style:normal;text-transform:uppercase;
  font-size:16px;letter-spacing:.14em;line-height:1.18;width:min-content;white-space:normal}
@media (max-width:760px){
  .brandwall{padding:64px 0 72px}
  .bw-head{margin:0 0 36px;gap:10px}
  .bw-kick{font-size:11px}
  .bcell{--cols:3;aspect-ratio:1.35/1;padding:8px}
  .bcell .bmark{height:calc(var(--bhm,var(--bh)) * .8);max-width:86%}
  .bcell .bword,.bcell .bword.wide,.bcell .bword.tall,.bcell .bword.serif{font-size:11px;letter-spacing:.12em}
}
/* The heading the faces strand still wears (it was this wall's). */
.bhead{display:flex;align-items:baseline;flex-wrap:wrap;gap:10px 22px;margin:0 0 34px}
.bhead h2{font-size:clamp(40px,5.2vw,84px);margin:0;color:var(--ink,#0A0A0B)}
.bhead .marker{color:var(--pop,#FF4F2E);font-size:clamp(20px,2.2vw,26px)}

/* --- the faces strand ----------------------------------------------
   Five names, set in the serif, big enough to be the section. The dot
   between them is a separator and not content, so it is drawn by the
   stylesheet and never read out; the last name does not get one. Kylie
   has no wall photograph yet, so she is a span and not a link — she is
   the same size as the others because the strand is about the people,
   not about which pages we happen to have.                             */
.faces{padding:78px 0 84px;background:var(--paper,#fff)}
.faces-lead{max-width:56ch;margin:0 0 30px;font-size:18px;line-height:1.55;color:var(--muted-ink,#3a3940)}
.frow{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px 26px;
  font-family:var(--face-serif,Fraunces),Georgia,serif;font-style:italic;
  font-size:clamp(28px,3.4vw,52px);line-height:1.15;color:var(--ink,#0A0A0B)}
.frow .face{color:inherit;text-decoration:none}
.frow a.face{text-decoration:underline;text-decoration-color:transparent;text-underline-offset:.14em;
  text-decoration-thickness:.05em;transition:text-decoration-color .18s ease,color .18s ease}
.frow a.face:hover,.frow a.face:focus-visible{color:var(--pop,#FF4F2E);text-decoration-color:currentColor}
.frow .face:not(:nth-last-child(2))::after{content:"\00b7";color:var(--pop,#FF4F2E);margin-left:26px}
.frow .face-more{font-style:normal;font-size:clamp(17px,1.7vw,24px);color:var(--pop,#FF4F2E);align-self:center}
@media (max-width:760px){
  .faces{padding:48px 0 52px}
  .faces-lead{font-size:16px;margin-bottom:22px}
  .frow{gap:6px 18px}
  .frow .face:not(:nth-last-child(2))::after{margin-left:18px}
}

/* --- public works ---------------------------------------------------
   A full-bleed pair: the Flower City wall on one side, the strand on the other. It
   is deliberately not a grid of cards — a card implies a roster, and the
   whole point of this block is that the roster is not the work. The
   marker line is the promise and it is the last thing read.            */
.pw{padding:0;background:var(--paper,#fff);border-top:2px solid var(--ink,#0A0A0B)}
.pw-grid{display:grid;grid-template-columns:1fr 1fr;align-items:stretch}
.pw-media{margin:0;position:relative;min-height:clamp(340px,42vw,560px);overflow:hidden;background:var(--ink-3,#1A1A1D)}
.pw-media picture,.pw-media img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.pw-words{padding:clamp(40px,5vw,80px) clamp(22px,4vw,64px);display:flex;flex-direction:column;justify-content:center}
.pw-words h2{font-size:clamp(46px,6vw,104px);margin:0 0 16px;color:var(--ink,#0A0A0B)}
.pw-words h2 em{font-style:normal;color:var(--pop,#FF4F2E)}
/* Same rule for the reel's heading: the emphasised words are the coral, upright.
   An <em> left to the browser is an italic of a face that has no italic. */
.progress-words h2 em{font-style:normal;color:var(--pop,#FF4F2E)}
.progress-words h2{color:var(--ink,#0A0A0B)}
.progress-words p{color:var(--muted,#55555C)}
.pw-places{font-size:clamp(15px,1.5vw,20px);letter-spacing:.06em;color:var(--pop,#FF4F2E);margin:0 0 18px}
.pw-words p.serif{font-size:17.5px;line-height:1.58;max-width:52ch;margin:0 0 14px;color:var(--muted-ink,#2b2a2e)}
.pw-note{font-size:clamp(19px,2vw,26px);color:var(--pop,#FF4F2E);margin:10px 0 0;transform:rotate(-1.4deg);transform-origin:left center}
/* The one link inside this section's copy — the wall it names. Site-wide,
   a{text-decoration:none}, which is right for cards and nav and wrong for a
   name in the middle of a sentence: it would read as body text. Underlined
   in the mint the rest of the section does not use, coral when reached. */
.pw-words p a{
  text-decoration:underline;text-decoration-color:var(--mint,#71EEB8);
  text-decoration-thickness:.09em;text-underline-offset:.16em;
  transition:color .18s ease,text-decoration-color .18s ease;
}
.pw-words p a:hover,.pw-words p a:focus-visible{color:var(--pop,#FF4F2E);text-decoration-color:currentColor}
.pw-words .row-end{margin-top:26px}
/* The walls the strand is framed for (build.py PW_TO_COME): a line while
   they have no photograph, a strip of captioned pictures once they do. */
.pw-words p.pw-next{font-size:15.5px;color:var(--muted);border-left:2px solid var(--mint);padding-left:14px}
.pw-strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));border-top:2px solid var(--ink)}
.pw-shot{margin:0;position:relative;aspect-ratio:4/3;overflow:hidden;background:var(--ink-3)}
.pw-shot picture,.pw-shot img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.pw-shot figcaption{position:absolute;left:0;right:0;bottom:0;padding:28px 16px 12px;color:var(--on-ink);font-size:14px;background:linear-gradient(transparent,var(--shade))}
@media (max-width:760px){
  .pw-grid{grid-template-columns:1fr}
  .pw-media{min-height:260px}
  .pw-words{padding:38px 18px 44px}
  .pw-words p.serif{font-size:16px}
}

/* --- the round-three dress on the inner pages ------------------------
   Only the classes these pages own. The nav, the footer, the hero and
   Home are the skin agent's; nothing below reaches them.               */

/* /work — the filter chips. Square, ruled, set in the wide face: a
   reference-site control rather than a pill, and no counts on them.    */
.work .filters{gap:8px;margin-bottom:clamp(26px,4vw,40px)}
.work .filters .chip{
  border-radius:0;border:1px solid var(--ink,#0A0A0B);background:transparent;
  color:var(--ink,#0A0A0B);padding:.62rem 1.05rem;
  font-family:var(--face-wide,ArchivoW),Archivo,sans-serif;font-weight:900;font-stretch:125%;
  text-transform:uppercase;font-size:12.5px;letter-spacing:.07em;
}
.work .filters .chip:hover{background:var(--ink,#0A0A0B);color:var(--paper,#fff);border-color:var(--ink,#0A0A0B)}
.chip[href="#f-all"],
#f-brand:target ~ .filters .chip[href="#f-brand"],
#f-portrait:target ~ .filters .chip[href="#f-portrait"],
#f-civic:target ~ .filters .chip[href="#f-civic"]{border-color:var(--ink,#0A0A0B)}
#f-brand:target ~ .filters .chip[href="#f-all"],
#f-portrait:target ~ .filters .chip[href="#f-all"],
#f-civic:target ~ .filters .chip[href="#f-all"]{border-color:var(--ink,#0A0A0B);color:var(--ink,#0A0A0B)}

/* A wall's measurements — the hook, set physically wide, with the prime
   marks and the times sign in the pop colour and nothing else.          */
/* The face, the width axis and the sizes are the component's own and are
   left alone — Scale and Stencil animate them. What changes on white is
   the colour: ink figures, and the prime marks and the times sign in the
   pop colour rather than the mint, which is the one accent the round-three
   palette gives this hook.                                              */
.pintro .dims{color:var(--ink,#0A0A0B)}
.pintro .dims .f,.pintro .dims .x,
.pgrid .pcard-body .dims .f,.pgrid .pcard-body .dims .x{color:var(--pop,#FF4F2E)}
.pintro .dims::after{background:var(--rule-ink,rgba(10,10,11,.18))}

/* A project page's words: the title tall, the story in the serif, the
   facts in the wide face underneath.                                    */
.pintro h1{font-size:clamp(52px,8vw,128px);margin:.1em 0 0;color:var(--ink,#0A0A0B)}
.pintro .pstory{font-size:19px;line-height:1.6;max-width:58ch}
.pintro .pmeta dt{font-family:var(--face-wide,ArchivoW),Archivo,sans-serif;font-weight:900;
  font-stretch:125%;text-transform:uppercase;font-size:11.5px;letter-spacing:.09em;color:var(--pop,#FF4F2E)}
.pintro .pmeta dd{font-family:var(--face-serif,Fraunces),Georgia,serif;font-size:17px}

/* The stage lists on /about and /graffiti-removal: stage names tall, the
   sentences under them in the serif whether they are his or ours.       */
.beats-full .beat h3,.beats-4 .beat h3,.beats-gr .beat h3{
  font-family:var(--face-tall,Anton),Impact,sans-serif;text-transform:uppercase;
  line-height:.95;letter-spacing:.01em;font-size:clamp(24px,2.4vw,34px);font-weight:400;
}
.beats-full .beat p,.beats-4 .beat p,.beats-gr .beat p{
  font-family:var(--face-serif,Fraunces),Georgia,serif;font-size:17px;line-height:1.55}

/* /about — the cities: a list of names, in the wide face on a ruled row. */
.cities .city b{
  font-family:var(--face-wide,ArchivoW),Archivo,sans-serif;font-weight:900;font-stretch:125%;
  text-transform:uppercase;letter-spacing:.03em}
.cities .city span{font-family:var(--face-serif,Fraunces),Georgia,serif;font-style:italic}

/* /graffiti-removal — the before/after caption, in his hand.            */
.ba-cap{font-family:var(--face-serif,Fraunces),Georgia,serif;font-size:16px}
.ba-cap .ba-hint{font-family:var(--face-marker,Marker),cursive;color:var(--pop,#FF4F2E);font-size:19px}

/* /contact — the form on white, with the aside in marker beside it.     */
.contact-aside{font-size:clamp(19px,2vw,26px);color:var(--pop,#FF4F2E);
  margin:-.2em 0 1.4rem;transform:rotate(-1.6deg);transform-origin:left center}
.form label{font-family:var(--face-wide,ArchivoW),Archivo,sans-serif;font-weight:900;
  font-stretch:125%;text-transform:uppercase;font-size:11.5px;letter-spacing:.09em;color:var(--ink,#0A0A0B)}
.form input,.form textarea,.form select{
  background:var(--paper,#fff);color:var(--ink,#0A0A0B);
  border:1px solid var(--ink,#0A0A0B);border-radius:0}
.form input::placeholder,.form textarea::placeholder{color:var(--dim,#6E6C67)}
.form input:focus,.form textarea:focus,.form select:focus{outline:3px solid var(--mint,#71EEB8);outline-offset:1px}
.form .hint,.form-note{font-family:var(--face-serif,Fraunces),Georgia,serif;font-size:15px}
.contact-side .contact-card{background:var(--paper,#fff);border:1px solid var(--ink,#0A0A0B);border-radius:0}
.contact-side .contact-card b{font-family:var(--face-tall,Anton),Impact,sans-serif;font-weight:400;
  text-transform:uppercase;font-size:21px;letter-spacing:.03em;color:var(--ink,#0A0A0B)}
.contact-side .contact-card span{font-family:var(--face-serif,Fraunces),Georgia,serif;font-size:15.5px}
.contact-card-cta{background:var(--mint,#71EEB8)}

/* /404 — a blank wall, and the copy says so.                            */
.page-hero.blank .hero-inner .lead{font-family:var(--face-serif,Fraunces),Georgia,serif}

/* The inner pages' own surfaces on the white ground. Every one of these
   is a panel the dark canvas lifted off the page with --ink-2; on white a
   panel is white with an ink hairline round it, and the type in it is ink.
   Only selectors that exist on /work, a project page, /about, /services,
   /graffiti-removal, /contact and /404 are listed — the sitewide .alt,
   the nav, the footer and Home stay the skin agent's.                   */
.pnav a,.figframe,.figcap,.form-status,.swipe-arw,.ba,.ba-stage{
  background:var(--paper,#fff);border-color:var(--rule-ink,rgba(10,10,11,.18));color:var(--ink,#0A0A0B)}
.pnav a{color:var(--ink,#0A0A0B)}
.pnav a:hover{border-color:var(--pop,#FF4F2E)}
.pnav .pnav-k{color:var(--pop,#FF4F2E)}
.pnav .pnav-t{color:var(--ink,#0A0A0B)}
.pnav .pnav-d{color:var(--ink,#0A0A0B)}
.figcap{color:var(--dim,#6E6C67);border-top-color:var(--rule-ink,rgba(10,10,11,.18))}
.pintro .pull p,.beats-full .beat blockquote p,.beats-4 .beat blockquote p,
.beats-gr .beat blockquote p,.duo .pull p{color:var(--ink,#0A0A0B);opacity:1}
.contact-side .contact-card .ic{background:var(--paper,#fff);border-color:var(--rule-ink,rgba(10,10,11,.18));color:var(--ink,#0A0A0B)}
.contact-card-cta,.contact-card-cta .ic{background:var(--mint,#71EEB8)}
.field option{background:var(--paper,#fff);color:var(--ink,#0A0A0B)}
.cities .city{border-color:var(--rule-ink,rgba(10,10,11,.18))}
.ba-lab{color:var(--ink,#0A0A0B)}
.swipe-dot{background:var(--rule-ink,rgba(10,10,11,.18))}
.swipe-dot[aria-current="true"]{background:var(--ink,#0A0A0B)}

/* `.wide` is the round-three type utility (the expanded face), and the
   layout modifier on a two-column row used to be called the same thing —
   so `<div class="duo wide">` was setting a whole column of serif copy in
   the display face and shouting it in capitals. The modifier is renamed
   `duo-wide` in the markup; this is its rule, unchanged.                */
@media (min-width:860px){.duo.duo-wide{grid-template-columns:7fr 5fr}}

/* The graffiti-removal one-liner, which is on /work, /services and
   /about. Ink on white with a rule round it, not a lifted dark panel.   */
.gr-strip{background:var(--paper,#fff);border-color:var(--rule-ink,rgba(10,10,11,.18))}
.gr-strip p{color:var(--ink,#0A0A0B)}
.gr-strip p b{color:var(--pop,#FF4F2E);font-family:var(--face-wide,ArchivoW),Archivo,sans-serif;
  font-weight:900;font-stretch:125%;text-transform:uppercase;letter-spacing:.04em;font-size:.92em}

/* --- the Services bands -----------------------------------------------
   One service to a screen (owner, 2026-10-01: "each service corresponding
   with a screen-wide example"). A full-bleed photograph of a real wall,
   the words down one side on the scrim the heroes already use, the side
   alternating band to band. No motion of its own: the words take the .rv
   reveal every section takes, and the button takes Splat like every .btn.
   The photograph is an <img> under object-fit, so the band is the same
   document with a script and without one.                              */
.svc-head{padding:clamp(56px,7vw,92px) 0 clamp(40px,5vw,64px);text-align:center;background:var(--paper)}
.svc-head .marker{color:var(--pop);font-size:clamp(1.5rem,2.2vw,1.75rem);margin:0 0 6px}
.svc-head h2{font-size:clamp(38px,5.6vw,84px);line-height:.95;margin:0;color:var(--ink)}
.svc-head h2 em{font-style:normal;color:var(--pop)}

.svc-band{position:relative;isolation:isolate;overflow:hidden;background:var(--ink);
  color:var(--on-ink);min-height:clamp(560px,86vh,920px);
  display:flex;align-items:flex-end;padding:clamp(3rem,7vw,5.5rem) 0}
.svc-band + .svc-band{border-top:6px solid var(--paper)}
.svc-media{position:absolute;inset:0;z-index:-2}
.svc-media picture,.svc-media img{display:block;width:100%;height:100%}
.svc-media img{object-fit:cover;object-position:50% 50%}
/* The scrim is darkest under the words and gone by two thirds of the way
   across, so the wall is the band and the words sit in its shadow.      */
.svc-shade{position:absolute;inset:0;z-index:-1;
  background:linear-gradient(90deg,var(--scrim) 0%,var(--scrim) 22%,var(--scrim-soft) 52%,transparent 78%),
             linear-gradient(0deg,var(--scrim-soft) 0%,transparent 42%)}
.svc-band.flip .svc-shade{
  background:linear-gradient(270deg,var(--scrim) 0%,var(--scrim) 22%,var(--scrim-soft) 52%,transparent 78%),
             linear-gradient(0deg,var(--scrim-soft) 0%,transparent 42%)}
.svc-words{max-width:34rem}
.svc-band.flip .svc-words{margin-left:auto}
.svc-num{display:block;font-size:clamp(2.4rem,4.4vw,3.6rem);line-height:.8;color:var(--pop);margin:0 0 .5rem}
.svc-words h2{font-size:clamp(3rem,7vw,6.4rem);line-height:.92;margin:0 0 1rem;color:var(--on-ink)}
.svc-words p{font-size:clamp(1.05rem,1.4vw,1.2rem);line-height:1.6;color:var(--on-ink);max-width:31rem;margin:0 0 1.6rem}
.svc-cap{display:flex;align-items:center;gap:.4rem .9rem;flex-wrap:wrap;margin-top:1.7rem;
  font-family:var(--font-serif);font-style:italic;font-size:1rem;color:var(--on-ink-mute)}
.svc-cap a{color:inherit;text-decoration:underline;text-underline-offset:3px}
.svc-cap a:hover{color:var(--mint)}
/* A phone stacks the words at the foot of the photograph, so the scrim
   comes up from the bottom on both sides of the alternation.            */
@media (max-width:760px){
  .svc-band{min-height:82vh;min-height:82svh;padding:2.6rem 0 2.4rem}
  .svc-band .svc-shade,.svc-band.flip .svc-shade{
    background:linear-gradient(0deg,var(--scrim) 0%,var(--scrim) 34%,var(--scrim-soft) 64%,transparent 88%)}
  .svc-band.flip .svc-words{margin-left:0}
  .svc-words p{margin-bottom:1.3rem}
  .svc-cap{margin-top:1.3rem;font-size:.95rem}
}

/* /services — the figures over the tin's rule.                         */
.svc-rule{background:var(--paper,#fff)}
.stats-3{display:grid;gap:1.2rem;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));
  list-style:none;margin:clamp(1.6rem,4vw,2.6rem) 0 0;padding:0}

/* --- a project that opens on a portrait film ---------------------------- */
/* Two of the walls in the drop of 2026-09-25 were filmed standing up, and a
   2160x3840 film in a full-bleed hero is nine tenths cropped away to leave a
   letterbox of the middle of it. So the film keeps its own shape, in a column
   beside the words. The photograph is under it exactly as it is in
   page_hero(), and it is .hero-video on top, so no-script, reduced motion and
   data-saver behave here the way they do everywhere else on the site. */
.thero{position:relative;padding:clamp(2.2rem,5vw,3.6rem) 0 clamp(.5rem,2vw,1.4rem)}
.thero-grid{display:grid;gap:clamp(1.6rem,4vw,2.6rem);align-items:center;justify-items:center}
.thero-words{text-align:center}
.thero-words h1{margin:0;font-size:clamp(2.4rem,8vw,4.6rem);line-height:.95}
.thero-film{margin:0;width:min(420px,80vw)}
.thero-media{
  position:relative;aspect-ratio:var(--film-ar,9/16);overflow:hidden;
  border-radius:26px;background:var(--ink);border:1px solid var(--line);
  box-shadow:0 22px 56px rgba(10,10,11,.20);
}
.thero-media picture{display:block;width:100%;height:100%}
.thero-media img{display:block;width:100%;height:100%;object-fit:cover}
@media (min-width:860px){
  .thero-grid{grid-template-columns:minmax(0,1fr) auto;gap:clamp(2.4rem,5vw,4.4rem);justify-items:start}
  .thero-words{text-align:left}
}

/* --- from the feed ------------------------------------------------------ */
/* His reels, in phone frames. The frame is the clip's own aspect ratio and
   never 9/16 by decree: these are screen recordings and one of them is wider
   than it is tall, so forcing them into a phone rectangle would crop away the
   interface that is the whole reason for showing them as reels. */
.feed{padding:clamp(3rem,7vw,5rem) 0}
.feed-strip{
  list-style:none;margin:clamp(1.6rem,4vw,2.4rem) 0 0;padding:0;
  display:grid;gap:clamp(2rem,5vw,3rem);justify-items:center;
}
.feed-item{display:grid;gap:1.1rem;justify-items:center;text-align:center;max-width:34rem}
.feed-phone{
  margin:0;position:relative;width:min(300px,74vw);
  aspect-ratio:var(--film-ar,9/16);border-radius:26px;overflow:hidden;
  background:var(--ink);border:1px solid var(--line);
  box-shadow:0 22px 56px rgba(10,10,11,.20);
}
.feed-video{display:block;width:100%;height:100%;object-fit:cover;background:var(--ink)}
.feed-phone .film-play{font-size:.82rem;padding:.55rem .9rem;gap:.4em}
.feed-words h3{margin:0;font-size:1.2rem}
.feed-words p{margin:.55rem auto 0;color:var(--muted);max-width:32ch}
@media (min-width:760px){
  .feed-strip{grid-template-columns:repeat(2,minmax(0,1fr));align-items:start}
}

/* --- the reel (Home) ---------------------------------------------------- */
/* Owner, 2026-10-03: "make the page scroll like a movie, seamlessly blending
   multiple projects, say a card floats over the seams". build.py
   scroll_reel() writes one list of walls; this is its two renders.

   Without html.motion — no script, or reduced motion asked for — it is a
   plain stack: each wall's poster frame full-width at 16:9 and its white card
   under it, overlapping the foot of the picture. The stage (the clips and
   the rail of names) is display:none, and the clips are preload="none", so
   nothing plays and nothing is fetched beyond the posters.

   Under html.motion every wall is a screen of scroll (--reel-per of one,
   tunable) and the stage is pinned over the list: sticky, a screen tall,
   with the list pulled up under it by its own height and padded at the foot
   by one more screen, so the pin lasts exactly until the last wall's move is
   finished. The cards ride the list, centred on the seam each one belongs
   to; site.js adds a translate so they rise faster than the scroll. If site.js
   never binds (html.reel-on), each poster comes in after 2.8s and fills its
   own screen, so the page is still a run of full-bleed walls with their
   cards; the box is the same size either way — nothing moves when it binds. */
.reel{padding:clamp(3rem,7vw,5rem) 0;--reel-h:100vh;--reel-per:1}
@supports (height:100svh){.reel{--reel-h:100svh}}
.reel .section-head{margin-bottom:clamp(1.6rem,4vw,2.6rem)}
.reel-stage,.reel-rail{display:none}
.reel-list{list-style:none;margin:0;padding:0;display:grid;gap:clamp(2.4rem,6vw,4rem)}
.reel-wall{position:relative;margin:0}
.reel-still{margin:0;background:var(--ink)}
.reel-still img{display:block;width:100%;height:auto;aspect-ratio:16/9;object-fit:cover}
.reel-card{
  position:relative;z-index:1;box-sizing:border-box;
  width:min(28rem,calc(100% - 2rem));
  margin:-3.2rem 0 0 max(1rem,calc((100% - var(--max)) / 2));
  padding:1.4rem 1.6rem 1.6rem;background:var(--paper);box-shadow:var(--reel-shadow);
  display:flex;flex-direction:column;align-items:flex-start;
}
.reel-n{margin:0 0 .35rem;color:var(--pop);font-size:1.25rem;line-height:1}
.reel-name{
  margin:0;font-family:var(--font-tall);font-weight:400;text-transform:uppercase;
  font-size:clamp(1.9rem,3.4vw,2.8rem);line-height:.95;letter-spacing:.01em;color:var(--ink);
}
.reel-card .dims-sm{margin:.45rem 0 .15rem}
.reel-card .feet-note{margin:.6rem 0 .1rem}
.reel-city{
  margin:.45rem 0 0;font-size:.78rem;font-weight:600;line-height:1.3;
  letter-spacing:.16em;text-transform:uppercase;color:var(--dim);
}
.reel-go{margin-top:1.15rem}

html.motion .reel{padding-bottom:0}
html.motion .reel-track{position:relative}
html.motion .reel-stage{
  display:block;position:sticky;top:0;z-index:0;
  height:var(--reel-h);overflow:hidden;background:var(--ink);
}
html.motion .reel-screens{position:absolute;inset:0}
/* One clip per wall, stacked in the order of the walls so the one coming in
   at a seam is always over the one in front; site.js sets each one's
   opacity. A window standing up keeps most of the picture's width and lets
   the ink show above and below it (74% of the height), rather than cutting
   a landscape clip down to a sliver. */
html.motion .reel-video{
  position:absolute;left:0;top:0;width:100%;height:100%;display:block;
  object-fit:cover;opacity:0;background:transparent;
}
@media (orientation:portrait){html.motion .reel-video{top:13%;height:74%}}
html.motion .reel-dip{position:absolute;inset:0;background:var(--ink);opacity:0;pointer-events:none}
html.motion .reel-list{
  display:block;position:relative;z-index:1;
  margin-top:calc(-1 * var(--reel-h));padding-bottom:var(--reel-h);
}
html.motion .reel-wall{height:calc(var(--reel-per) * var(--reel-h))}
/* Under motion the posters wait out of the box (display:none, so a lazy
   image is never fetched) for the 2.8s every hidden thing here waits: by then
   site.js has bound and they are never needed; if it has not, they come in
   and the band is a run of full-bleed walls with their cards. */
html.motion .reel-still{position:absolute;inset:0;display:none;animation:oaReelBail 0s linear 2.8s forwards}
@keyframes oaReelBail{to{display:block}}
html.motion .reel-still img{height:100%;aspect-ratio:auto}
html.motion .reel-card{
  position:absolute;left:50%;top:calc(var(--reel-h) * .5);margin:0;
  transform:translate3d(-50%,-50%,0);
}
/* site.js has the clips: the posters stand down (and, being lazy and now
   display:none, are never fetched), and the rail comes up. The drift is the
   "constantly move" half of the ask: a slow scale and pan on the clips' box,
   on the compositor, on top of the clips' own motion. */
html.reel-on .reel-still{display:none;animation:none}
html.reel-on .reel-card{will-change:transform}
html.reel-on .reel-screens{animation:oaReelDrift 26s ease-in-out infinite alternate}
@keyframes oaReelDrift{
  from{transform:scale(1.04) translate3d(-.9%,.5%,0)}
  to{transform:scale(1.11) translate3d(.9%,-.7%,0)}
}
html.reel-on .reel-rail{
  display:block;position:absolute;left:0;right:0;bottom:0;
  padding:3.6rem max(1rem,calc((100% - var(--max)) / 2)) 1.1rem;
  background:var(--reel-foot);color:var(--on-ink-mute);
}
.reel-rail ol{
  list-style:none;margin:0;padding:0;display:flex;justify-content:space-between;gap:1rem;
  font-family:var(--font-body);font-weight:600;font-size:.66rem;line-height:1.2;
  letter-spacing:.16em;text-transform:uppercase;
}
.reel-rail li{position:relative;white-space:nowrap;transition:color .35s var(--ease)}
.reel-rail li.on{color:var(--on-ink)}
.reel-rail li.on::before{
  content:"";position:absolute;left:0;right:0;top:-.7rem;height:3px;background:var(--pop);
}
.reel-bar{display:block;position:relative;height:2px;margin-top:.75rem;background:var(--line-on-ink-2);overflow:hidden}
.reel-bar i{position:absolute;inset:0;background:var(--mint);transform-origin:0 50%;transform:scaleX(var(--rp,0))}
@media (max-width:759px){
  .reel-rail li{display:none}
  .reel-rail li.on{display:block}
  .reel-rail li.on::before{right:auto;width:2.4rem}
}

/* The reel's extras on Home: the row photographs drift inside their frames
   with the scroll (--par from site.js, -1 to 1), on the same terms as a
   card's. Under html.motion only, and data-par is only on Home's rows. */
html.motion [data-par]{overflow:hidden}
html.motion [data-par] img{transform:translate3d(0,calc(var(--par,0) * 5%),0) scale(1.12)}

/* =====================================================================
   SEARCH PAGES (owner, 2026-10-11): the city hubs (/murals/<city>), the
   service pages (/services/<service>) and the FAQ. Built from the parts
   the site already has (the hero, the card grid, the press list, the
   stages, the closing band); this is only the reading measure, the links
   inside running text, and the questions. Tokens only.
   ===================================================================== */
.copy{max-width:68ch}
.copy .serif,.copy p.serif{line-height:1.6}
.copy p+p{margin-top:1rem}
.copy a,.link-line a,.faq-a a{
  text-decoration:underline;text-decoration-color:var(--mint-deep);
  text-decoration-thickness:2px;text-underline-offset:.2em}
.copy a:hover,.link-line a:hover,.faq-a a:hover{text-decoration-color:var(--pop)}
.link-line{margin:clamp(1.6rem,4vw,2.4rem) 0 0;font-size:.95rem;line-height:1.7;color:var(--muted);max-width:80ch}
.hub .pgrid+.link-line{margin-top:clamp(1.8rem,4vw,2.6rem)}
.cities-links a{display:block;color:var(--ink)}
.cities-links a:hover b{color:var(--pop-deep)}
/* The FAQ: native <details>, so it opens with no script at all. */
.faq{max-width:900px;border-top:1px solid var(--line-2)}
.faq details{border-bottom:1px solid var(--line)}
.faq summary{
  display:flex;align-items:baseline;justify-content:space-between;gap:1.2rem;
  padding:1.2rem 0;cursor:pointer;list-style:none;color:var(--ink)}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{
  content:"+";flex:none;font-family:var(--font-display);font-variation-settings:'wdth' 100,'wght' 700;
  font-size:1.5rem;line-height:1;color:var(--pop-deep)}
.faq details[open] summary::after{content:"\2212"}
.faq-q{
  font-family:var(--font-display);text-transform:none;font-variation-settings:'wdth' 100,'wght' 700;
  font-size:clamp(1.05rem,1.7vw,1.25rem);line-height:1.3;letter-spacing:0}
.faq summary:hover .faq-q{text-decoration:underline;text-decoration-color:var(--mint);text-decoration-thickness:2px;text-underline-offset:.22em}
.faq summary:focus-visible{outline:2px solid var(--ink);outline-offset:4px}
.faq-a{padding:0 0 1.4rem;max-width:68ch}
.faq-a p{font-size:1.04rem;line-height:1.6;margin:0}
/* A service page's running copy: sub-heads at a reading size, not a hoarding's. */
.svc-copy-group+.svc-copy-group{margin-top:clamp(2.6rem,6vw,4rem)}
.svc-copy h2{font-size:clamp(2rem,4.4vw,3.4rem);margin-bottom:1.1rem}
.svc-copy p{font-size:1.08rem;line-height:1.65;color:var(--ink)}
.svc-copy p b{font-family:var(--font-display);font-variation-settings:'wdth' 100,'wght' 700;font-weight:400}
/* The text link under a Services band's button, to the service's own page. */
.svc-also{margin:1rem 0 0;font-size:.92rem;color:var(--on-ink-mute)}
.svc-also a{color:var(--on-ink);text-decoration:underline;text-decoration-color:var(--mint);text-decoration-thickness:2px;text-underline-offset:.2em}
.svc-also a:hover{color:var(--mint)}
