/* Every style for samkarpvideo.com. One file, shared by every page.
   Sections below are in the order they appear on screen. To find a rule,
   search for its section banner rather than reading top to bottom. */

/* ============================================================
   TYPEFACES — self hosted
   ============================================================
   These used to arrive from fonts.googleapis.com via a <link> in every page
   head. Three round trips to two third-party hosts sat on the critical path
   before a heading could render in the right face, and every visitor handed
   Google their IP address to read a page about video production in central
   Pennsylvania. CLAUDE.md already called for self-hosted @font-face; this is
   that, finally done.

   All families here are OFL licensed, so hosting them is allowed.

   The unicode-range values are Google's own subset boundaries, kept as they
   are so a page of plain English still pulls only the latin files and
   latin-ext stays unrequested until something needs it. Vietnamese and the
   other subsets Google offers are not carried at all.

   font-display:swap throughout, so first paint behaves as it always has.

   BIG SHOULDERS DISPLAY replaced Archivo Black as the heading face. It is a
   variable font, so one file per subset covers every weight and the range is
   declared rather than one file per step. Only the display face is preloaded,
   because it sets the largest contentful paint on every page.

   BARLOW replaced Archivo for everything else. It is NOT variable on Google
   Fonts, so 400 and 600 are two separate files per subset. That is the whole
   reason this block went from four files to six.

   WHAT IT COSTS. On a plain English page only the latin files are fetched:
   80.4 KB now against 53.5 KB before, so about 27 KB more on the critical
   path. Two thirds of that is Barlow being static. If that ever matters more
   than the type does, the cheapest fix is dropping the 600 and letting the
   small caps labels sit at 400, not swapping the face again. */

@font-face{
  font-family:"Big Shoulders Display";
  font-style:normal;
  font-weight:400 900;
  font-display:swap;
  src:url("fonts/big-shoulders-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face{
  font-family:"Big Shoulders Display";
  font-style:normal;
  font-weight:400 900;
  font-display:swap;
  src:url("fonts/big-shoulders-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:"Barlow";
  font-style:normal;
  font-weight:400;
  font-display:swap;
  src:url("fonts/barlow-400-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face{
  font-family:"Barlow";
  font-style:normal;
  font-weight:400;
  font-display:swap;
  src:url("fonts/barlow-400-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:"Barlow";
  font-style:normal;
  font-weight:600;
  font-display:swap;
  src:url("fonts/barlow-600-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face{
  font-family:"Barlow";
  font-style:normal;
  font-weight:600;
  font-display:swap;
  src:url("fonts/barlow-600-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ============================================================
   TOKENS — Umber
   ============================================================ */
/* CARBON. The ground was a warm near-black, and the whole palette sat in the
   same family as the footage: golden hour, brick, wood interiors and a red car
   all printed on a page tinted the same way. Nothing clashed, and nothing
   separated either, so the work blended into the site instead of being framed
   by it. This ground is neutral and deeper, which is the entire point: every
   warm thing in a frame warms up against it, and the red car reads redder.

   It is the deepest of the five that were compared, at 0.27% luminance against
   0.58% for the old one, and that has a cost worth knowing about. There is
   almost nothing left underneath, so --surface-2 and --muted can no longer
   separate a panel from the page by brightness alone. The borders and the
   spacing do that work now, which is why the tiles gained a radius in the same
   pass. If a future surface looks like it is floating in nothing, add a rule to
   it rather than trying to lift it off the ground.

   Measured, not guessed. Every pair that carries text clears WCAG AA: body
   17.2:1, muted prose 6.3:1, placeholders 4.9:1, the button label 15.3:1, error
   text 7.4:1 on the page and 7.2:1 on a panel. */
:root{
  --bg:#08090A;
  --fg:#EDEEEF;
  --muted:#121314;
  --muted-fg:#8E9294;
  --accent:#E0E2E3;
  --accent-fg:#08090A;
  --border:#26282A;
  --surface-2:#0D0E10;   /* .panel behind the contact form, and the closing plate on the homepage */
  --faint:#383B3D;
  --placeholder:#7A7E80;  /* form placeholders only. 4.9:1 on --bg, clears AA */
  /* The one colour on this site that is allowed to mean something rather than
     just belong. Terracotta rather than a signal red, which would be the only
     saturated thing on a near-monochrome page and would read as an alarm on a
     form where the visitor has simply left a box empty. Warmed slightly against
     the cooler ground so it still reads as a deliberate colour rather than as a
     stray. Measured 7.4:1 on --bg and 7.2:1 on --surface-2, so it clears AA
     everywhere it is used, and it is far enough from --accent that the two
     never read as the same state. */
  --warn:#E0876A;

  /* THREE FACES, THREE JOBS. --font-display used to do two of these at once:
     it was the heading face and, through body's font-family, the default face
     for everything that did not opt out. That meant the buttons, the 404 link
     list, the contact page's email address and phone number, the text a visitor
     types into the form, and the phone call bar were all set in whatever the
     headings were set in. Changing the heading face changed all of it, which is
     not what "display face" means anywhere else.
       --font-display  headings, and only headings
       --font-ui       the page default: chrome, controls, typed input
       --font-body     running prose and the small caps labels
     The split was made first, against the old faces, so that this change could
     land on the headings and stop there. It does: pointing --font-display
     somewhere else moves four elements on the homepage, three on services, two
     on contact and one on 404, and every one of them is a heading. */
  --font-display:"Big Shoulders Display",system-ui,sans-serif;
  --font-ui:"Barlow",system-ui,sans-serif;
  --font-body:"Barlow",system-ui,sans-serif;

  /* THE SHAPE SCALE. Two values, and a rule about which gets which: anything a
     visitor touches takes --radius-control, anything that holds content takes
     --radius-surface. The surface is the larger of the two because a big box
     needs more radius than a small one to read as equally rounded.
     There used to be no scale at all. Every corner on the site was square, but
     square by default rather than by decision: `border-radius:0` is also what a
     browser does when nobody chooses, and there were exactly two radius
     declarations in the whole stylesheet, both of them zero. The shape now says
     something on purpose. Do not introduce a third value without a reason. */
  --radius-control:5px;
  --radius-surface:6px;

  --nav-h:72px;
  /* What the footer costs a page that is trying to fit in one screen. Read by
     the contact layout, which sizes itself to what the nav and the footer
     leave behind. A measurement of the built footer at desktop, not a value
     the footer is held to, so check it here if that block ever grows a row. */
  --footer-h:101px;
  --dur:250ms;

  /* Gap between a content column and the rail beside it. Scales with the window
     so the two columns do not crowd each other at 1024 or drift apart at 1600. */
  --gap-rail:clamp(40px,4vw,72px);
  /* Between form fields, and between the two halves of the name/email row, so
     the row reads as part of the same rhythm rather than a nested group. */
  --gap-field:36px;

  /* PAGE GUTTER. One value, read by every container that touches the screen
     edge: .shell, .nav__inner, .hero__content, .hero__base. It used to be
     written out four times with a 768px override each, which is what made the
     full-bleed work tiles below awkward to reach for: a tile can only break out
     of the shell if it knows exactly how far the shell is holding it in.
     The safe-area variants exist because viewport-fit=cover puts the notch and
     the home indicator inside the viewport, so anything sitting at an edge has
     to add that inset back itself. The 0px fallback is load-bearing: a
     declaration containing an unsupported env() is dropped whole, which would
     take the base padding with it. */
  --gutter:20px;
  --gutter-l:calc(var(--gutter) + env(safe-area-inset-left,0px));
  --gutter-r:calc(var(--gutter) + env(safe-area-inset-right,0px));

  /* Height of the fixed contact bar, and the space the page reserves for it.
     Both are zero above the bar's breakpoint, so every rule that reads them is
     inert on a desktop without needing its own media query. */
  --callbar-h:60px;
  --callbar-space:0px;

  /* The page is dark by design, so tell the browser. Without this the parts it
     draws itself stay light: the textarea resize grip, scrollbars inside the
     field, the autofill wash Chrome paints over a filled input, and the date
     and select popups if either ever lands here. Those are the only surfaces
     this touches; nothing in the stylesheet below changes. */
  color-scheme:dark;
}

/* The two token overrides that make the site mobile-specific rather than
   desktop-shrunk. Everything downstream reads the tokens, so the breakpoints
   live here once instead of being repeated per component.
   768px is the gutter step. The contact bar is phones only: at tablet width
   there is room for the page's own buttons and a bar pinned over the content
   would be in the way rather than in reach. */
@media(min-width:768px){ :root{ --gutter:40px; } }
@media(max-width:767px){
  :root{ --callbar-space:calc(var(--callbar-h) + env(safe-area-inset-bottom,0px)); }
}

*,*::before,*::after{box-sizing:border-box;}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth;}
body{
  margin:0;
  background:var(--bg);
  color:var(--fg);
  /* The page default, not the heading face. Headings ask for --font-display by
     name below; everything else that has no opinion lands here. */
  font-family:var(--font-ui);
  font-weight:400;
  letter-spacing:0;
  overflow-x:hidden;
  /* Hold the footer to the bottom of the window on pages too short to push it
     there. Nothing did this before, so 404 ended its content around 700px into
     a 1000px window, drew the footer immediately after it, and left roughly
     300px of bare page underneath: the single most broken looking thing on the
     site, and on the one page a visitor only ever reaches by accident. About is
     short enough to be one closing block away from the same fault.
     dvh rather than the svh the hero uses, and the difference is which way each
     one has to fail. The hero is sized to fill and must not overshoot, so it
     takes the smallest viewport. This is a floor, and the risk is a footer left
     hanging when a mobile toolbar retracts and the viewport grows, so it takes
     the dynamic one. */
  min-height:100dvh;
  display:flex;
  flex-direction:column;
}
/* Takes the slack, so the footer is pushed down rather than floated up. Its own
   rule, deliberately separate from the overflow-x one further up, so the two
   concerns can be edited without meeting. */
main{flex:1 0 auto;}
::selection{background:var(--accent);color:var(--accent-fg);}
img,video{display:block;max-width:100%;}
a{color:inherit;text-decoration:none;}
button{font:inherit;color:inherit;}

:focus-visible{outline:2px solid var(--accent);outline-offset:3px;}

/* Nothing uses this yet. Kept on purpose: it is the correct way to put text on
   the page for screen readers only, and the obvious substitute when you need
   one in a hurry is display:none, which hides it from screen readers too. */
.sr-only{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;
}
.skip-link{
  position:absolute;left:8px;top:-60px;z-index:200;
  background:var(--accent);color:var(--accent-fg);
  padding:12px 20px;font-family:var(--font-ui);
  font-weight:600;
  text-transform:uppercase;letter-spacing:0.08em;font-size:14px;
  transition:top 150ms;
}
.skip-link:focus{top:8px;}

.noise{
  position:fixed;inset:0;z-index:100;pointer-events:none;
  opacity:0.025;mix-blend-mode:overlay;
}

/* ============================================================
   TYPE
   ============================================================ */
.display{
  /* Named rather than inherited. This is the one place the display face is
     meant to arrive, so it asks for it directly instead of relying on body. */
  font-family:var(--font-display);
  /* Big Shoulders is a variable font and its 400 is light. Archivo Black was a
     single black weight, so no rule on this site ever had to name one. This is
     the weight the headings were compared at. */
  font-weight:800;
  text-transform:uppercase;
  /* Was -0.03em, cut for Archivo Black, which is wide and needs pulling in. A
     condensed face is already tight and the same value closed the counters up.
     Big Shoulders is spaced correctly as drawn, so nothing is taken off it. */
  letter-spacing:0;
  line-height:0.85;
  margin:0;
}
/* Running copy switches to the lighter companion face. */
.lede,.body-text,.svc__body,.step__body,.work__note,.form__status,.form__note{
  font-family:var(--font-body);
  font-weight:400;
}
/* The same small label as .line__k, .wordmark__role and the hero kit: body face
   at 600, wide tracking, muted. Those three always said so; this one never did,
   because the page default used to be Archivo Black and a heavy face at 400 was
   already doing the job by accident. Barlow 400 is not, so the rule now states
   what it always meant. */
.eyebrow{
  font-family:var(--font-body);
  font-weight:600;
  text-transform:uppercase;
  font-size:0.6875rem;
  letter-spacing:0.24em;
  color:var(--muted-fg);
  margin:0 0 28px;
}

/* THE WAY BACK, on 404. Four routes out, given the weight of an actual offer
   rather than of a sentence someone forgot to style.
   Each one gets its own line and its own rule, which is the same construction
   the contact page uses for its direct lines, so the page a visitor lands on by
   mistake is built out of the same parts as the page they were looking for.
   Held to a column rather than run across the band: a stack of four is read at
   a glance, and a row of four small links at the far left of a 1520px page
   trails off into empty space on the right. */
.lost{
  list-style:none;padding:0;
  margin:36px 0 0;
  max-width:420px;
}
.lost li{border-top:1px solid var(--border);}
.lost li:last-child{border-bottom:1px solid var(--border);}
.lost a{
  display:flex;align-items:center;justify-content:space-between;
  padding:18px 2px;
  font-family:var(--font-ui);
  font-weight:600;
  text-transform:uppercase;
  font-size:0.9375rem;
  letter-spacing:0.06em;
  color:var(--fg);
  text-decoration:none;
  transition:color var(--dur), padding-left var(--dur);
}
/* Generated rather than marked up, which is a presentation decision and not an
   accessibility one. Worth being accurate about: CSS content is announced by
   most screen readers, so this is not hidden from anybody and "Work, right
   arrow" is what some visitors will hear. That is acceptable for a glyph whose
   whole meaning is "this goes somewhere", and it stays out of the markup where
   a maintainer would have to copy it four times. */
.lost a::after{
  content:"\2192";
  font-family:var(--font-body);
  color:var(--faint);
  transition:color var(--dur), transform var(--dur);
}
@media(hover:hover){
  .lost a:hover{color:var(--accent);padding-left:10px;}
  .lost a:hover::after{color:var(--accent);transform:translateX(4px);}
}
/* No focus rule here on purpose: the global :focus-visible near the top of this
   file already draws the accent outline at the same offset, and restating it
   per component is how a stylesheet ends up with four versions of one ring. */
.lede{
  font-size:clamp(1.0625rem,1.7vw,1.3125rem);
  line-height:1.45;
  color:var(--muted-fg);
  max-width:46ch;
  margin:0;
}
.body-text{
  font-size:clamp(1.0625rem,1.4vw,1.1875rem);
  line-height:1.6;
  color:var(--muted-fg);
  max-width:62ch;
}
.body-text + .body-text{margin-top:1.4em;}
/* A LINK INSIDE A SENTENCE, which this site had no style for until the About
   page needed one. The reset near the top sets a{color:inherit} and strips the
   underline, which is right for nav, buttons and tiles, where the shape of the
   thing already says it is a link. In running prose nothing else says it, so an
   unstyled link there is invisible: same colour, same weight, no rule under it.
   Underline rather than a colour swap on its own, because colour alone is not a
   sufficient distinction (WCAG 1.4.1) and --accent against --muted-fg is a
   quiet difference to begin with. The offset keeps the rule off the descenders
   so it reads as a link and not as strikethrough on a "g" or a "y". */
.body-link{
  color:var(--fg);
  text-decoration:underline;
  text-decoration-color:var(--faint);
  text-underline-offset:0.18em;
  text-decoration-thickness:1px;
  transition:color var(--dur),text-decoration-color var(--dur);
}
.body-link:hover,.body-link:focus-visible{
  color:var(--accent);
  text-decoration-color:var(--accent);
}

/* ============================================================
   LAYOUT
   ============================================================ */
/* Every page sets viewport-fit=cover, which draws the page under the notch and
   the home indicator instead of letting the browser letterbox around them. That
   is what makes the hero video reach the edges, but it means anything sitting
   at an edge has to add the inset back itself or a phone held sideways will
   crop it. The fallback of 0px is there because a declaration containing an
   unsupported env() is dropped whole, which would take the base padding with
   it. Containers that touch an edge: .shell, .nav__inner, .hero__content,
   .hero__base. */
.shell{
  width:100%;
  max-width:1600px;
  margin-inline:auto;
  padding-inline:var(--gutter-l) var(--gutter-r);
}

/* The work reel breaks out of the shell to the full width of the window, and a
   100vw box is wider than the page's content box by exactly the width of the
   scrollbar, which would otherwise add a horizontal scrollbar to every page.
   clip rather than hidden on purpose: overflow:hidden makes this a scroll
   container, which would break the sticky nav above it and the scroll snapping
   the hero relies on. clip trims the overflow without creating one. */
main{overflow-x:clip;}

/* 88px was the desktop rhythm paid at phone scale, where it read as four
   screens of mostly empty page. A phone wants a tighter beat than a 1440px
   monitor: the eye travels a shorter distance, so the same gap reads as a
   longer pause. Desktop is untouched below. */
.band{padding-block:72px;}
@media(min-width:768px){ .band{padding-block:136px;} }
.band--rule{border-top:1px solid var(--border);}
/* Services only. The page is meant to open straight into the first tile rather
   than under a tall masthead, so the space above its heading is cut right back
   and the heading itself sits tighter to the tiles. Bottom padding is left
   alone, since that is what separates the last tile from the footer.
   Cut back a second time once the stack started snapping. A page that loads at
   scroll zero has to already be at a snap position or it corrects itself the
   moment it appears, and the only snap position near the top is the first
   tile's centre. Every pixel above that tile is a pixel the page has to travel
   on load, so the padding here is deliberately small. Growing it again brings
   the load jump back. */
/* The 8px this replaces was bought by the snapping, which is gone: every pixel
   above the first tile used to be a pixel the page jumped through on load, so
   the masthead was pushed up until it was almost touching the nav. Nothing
   fights it now, and 8px of air over a page's own title does not read as
   opening hard on the work, it reads as a heading that failed to clear the bar
   above it. There is still room to spare: at 1600x1000 the first tile now runs
   262 to 747, so it is comfortably whole on the first screen with the masthead
   given proper space above it. */
.band--tight{padding-block-start:32px;padding-block-end:48px;}
@media(min-width:768px){ .band--tight{padding-block-start:48px;padding-block-end:72px;} }
/* Sits directly under a band that has already paid for the space between them.
   Without this the two paddings stack and leave a canyon. */
.band--flush{padding-block-start:0;}

/* SECTION RHYTHM. The canyon above is not specific to one pair of bands: any
   two adjacent bands both pay for the gap between them, so the visitor gets
   double. Measured at 1440px before these rules existed, the homepage had 273px
   of empty page between the last work tile and the panel under it.
   .band--flush solved it by hand in one place. These two rules solve it
   everywhere, so a page added later gets the right rhythm without anyone
   remembering to reach for a modifier.
   Two cases, because a plain join and a ruled join want different things. */

/* Plain join: the band above already paid. Add nothing. */
.band + .band:not(.band--rule){padding-block-start:0;}

/* Ruled join: the band above pays for the gap down to the line, and this pays
   for the gap from the line to its own content. Deliberately smaller than a
   full band pad. Zero here would sit the text on the rule. */
.band + .band--rule{padding-block-start:48px;}
@media(min-width:768px){ .band + .band--rule{padding-block-start:72px;} }
/* Follows the hero, which is already a full screen tall. A band's usual top
   padding on top of that means a visitor scrolls a whole screen and then some
   before the first heading appears. Bottom padding is untouched, since that is
   what holds the grid off the section below it. */
.band--lift{padding-block-start:32px;}
/* Was 64px, which was the right air over a 72px display heading. The band opens
   on a slate rule now — see .slate — and a hairline does not need as much room
   above it as a word set in Archivo Black does. The 20px this gives back is
   spent below, where it buys the thumbnail strip more of the first screen. */
@media(min-width:768px){ .band--lift{padding-block-start:44px;} }
/* The hero cue scrolls this band to the top of the window, where the sticky
   nav is already sitting. With the old padding the heading cleared it by
   accident; at this height it would land underneath. Holding the scroll short
   by the height of the nav puts the heading back in view, and fixes any jump
   link that targets this section for the same reason. */
.band--lift{scroll-margin-top:var(--nav-h);}
/* The band is shorter than the window it lands in — 735px inside 900px at
   1440 — and the scroll stops with its top under the nav, so the leftover
   space at the foot was showing the top sliver of the closing section's
   footage. A stop that rests on one section should not have the next one
   peeking into it, so the band is given the rest of the screen. The content
   stays where it was; only the dark below the thumbnail strip grows.
   Held to the pointer devices and widths that actually stop here: a phone
   scrolls straight through, and this much empty space under a 350px band
   would be a hole in the page rather than a clean edge. svh, not vh, so a
   mobile browser's shrinking toolbar cannot make it overshoot. */
/* CENTRED IN THE SCREEN IT CLAIMS, not parked at the top of it. The rule above
   is right that the band has to own the whole screen, but block layout puts
   every pixel of the leftover at the foot: at 1600x1000 that measured 301px of
   bare page under the thumbnail strip, which is the one thing on this site that
   read as unfinished rather than as space. The reel cannot simply grow into it
   either, since --reel-w is capped to keep the neighbours wide enough to be
   read as footage, and at a 16:9 centre that cap is what binds first on any
   normal desktop window.
   So the slack is split instead of dumped. Same band, same height, same clean
   edge against the closing section; the reel now sits in the middle of its own
   screen and the space reads as letterboxing, which is the right gesture for a
   page whose job is footage.
   flex-direction:column keeps the children full width, so the reel's
   margin-inline breakout to 100vw still resolves against this box exactly as it
   did under block layout. Verified at 1600, 1440 and 1280. */
@media (pointer:fine) and (min-width:768px){
  .band--lift{
    min-height:calc(100svh - var(--nav-h));
    display:flex;
    flex-direction:column;
    justify-content:center;
    /* .band's 136px foot is sized for a band that ends where its content ends.
       This one does not: it runs to the bottom of the screen either way, so
       that figure stops being air under the content and becomes 92px of extra
       weight on one side of a box that is supposed to be centred. Matched to
       the 44px head instead, which leaves the centring to do the work. */
    padding-block-end:44px;
  }
}
/* A scroll in the hero finishes the trip down to this band, landing where the
   cue button lands because both are the band's top held short by the
   scroll-margin above.
   These rules serve trackpads and nothing else. A trackpad sends a continuous
   stream of small deltas, which is exactly what snapping is built for: it only
   ever finishes the short remainder of a gesture the visitor already made, and
   it reads as one motion. A mouse wheel is the opposite, a single large delta
   with nothing around it, and Chrome answers that by jumping straight to the
   next snap point at a speed no stylesheet can change. So site.js takes wheel
   events away before they reach this and animates them itself.
   Do not widen this to cover the mouse. Six attempts to make one mechanism
   serve both inputs are in the history and every one of them was right on one
   device and wrong on the other.

   TWO RANGES, not one. "hero" is the original and covers the hero and the top
   of the work band. "page" adds a third stop at the foot of the document, so a
   trackpad reaches the closing screen the same way the wheel animation does.
   site.js decides which is safe to arm; see the note there for why "page" is
   held to pointer devices and to windows tall enough to hold the work band. */
html[data-snap="hero"],
html[data-snap="page"]{ scroll-snap-type:y mandatory; }
.hero{ scroll-snap-align:start; scroll-margin-top:var(--nav-h); }
.band--lift{ scroll-snap-align:start; }
/* The last stop is the foot of the document rather than the top of .closer,
   because on most desktop windows .closer's top cannot reach the top of the
   window: the page runs out of scroll first. At 1440x900 its top sits at 1636
   and the furthest the page scrolls is 1568. Aligning to the end instead puts
   the stop at the very bottom, where .closer, .how and the footer are all in
   view at once — which is what those two sections were sized to do anyway.
   On .footer for the same reason tile-snapping.md gives: it is the last thing
   in the document and it is small, and a snap target has to be small. */
html[data-snap="page"] .footer{ scroll-snap-align:end; }
.band--tight .eyebrow{margin-bottom:18px;}
/* 28px sat a 61px display heading almost on the lid of the first tile, which
   made the masthead read as part of the stack rather than as the thing
   introducing it. A heading needs more room beneath it than the gap between two
   tiles, or the hierarchy inverts. */
.band--tight .heading{margin-bottom:44px;}
/* Do not put scroll-snap-align on this band. It wraps the whole services stack
   and runs to about 2280px, far taller than any window, and a snap area larger
   than the viewport makes every scroll position that keeps the viewport inside
   it a valid place to rest. Aligning it does not add one snap point at the top,
   it declares the entire stack one legal resting zone and switches mandatory
   snapping off across most of the page. A snap target has to be small.
   Harmless today, since the services page no longer snaps, and left here
   because this is where the mistake gets made. See docs\tile-snapping.md. */

/* ============================================================
   NAV
   ============================================================ */
.nav{
  position:sticky;top:0;z-index:80;
  background:var(--bg);
  border-bottom:1px solid var(--border);
}
.nav__inner{
  height:var(--nav-h);
  display:flex;align-items:center;justify-content:space-between;gap:16px;
  max-width:1600px;margin-inline:auto;
  padding-inline:var(--gutter-l) var(--gutter-r);
}

/* Two lines: the name at display scale, the discipline under it in the small
   caps the rest of the site uses for labels. It used to be one run reading
   "SAM KARP | VIDEO PRODUCTION", set at a single size, so the pipe was doing
   the work of a line break and the name carried no more weight than the words
   after it.
   The clamp that used to live here scaled hard to keep three words from pushing
   the menu button off a 360px phone. That pressure is gone: both lines are far
   shorter than the old single run, so the name can go up a size rather than
   down. */
.wordmark{
  display:inline-flex;flex-direction:column;
  /* Each line sized to its own text rather than stretched to the width of the
     other one, which is what a column flex container does by default. */
  align-items:flex-start;
  gap:7px;
  text-transform:uppercase;
  line-height:1;white-space:nowrap;
}
/* Flat, not clamped. A wordmark is a mark: it should be the same size on every
   screen rather than breathing with the viewport like body copy.
   24px is a floor, not a preference. The role line under it measures 126.3px,
   and the name runs 5.57px wide per px of font size, so anything under 22.7px
   leaves the sub-line wider than the name above it. 24px puts the name at
   133.6px, comfortably the wider of the two, and the whole block is still
   narrower than the single-line version it replaces: that ran about 230px on a
   320px phone, this one is 134px. */
.wordmark__name{
  font-size:1.5rem;
  /* Barlow 400 at this size reads as body copy, not as a mark, and -0.04em was
     closing up a face that is not here any more. */
  font-weight:600;
  letter-spacing:-0.01em;
  line-height:1;
}
/* The site's standard small label, the same one .eyebrow and the work captions
   and the hero kit all use: body face at 600, wide tracking, muted. A step down
   in size from those because it sits under something rather than over it.
   Tracking is trimmed to 0.16em rather than the usual 0.2em for one reason: at
   0.2em this line measures wider than the name above it, and a sub-line wider
   than the thing it belongs to reads as two separate items rather than one
   lockup. */
.wordmark__role{
  font-family:var(--font-body);font-weight:600;
  font-size:0.625rem;
  letter-spacing:0.16em;
  color:var(--muted-fg);
  line-height:1;
}
.nav__links{display:none;gap:32px;align-items:center;}
@media(min-width:900px){ .nav__links{display:flex;} }
.nav__link{
  font-family:var(--font-body);font-weight:600;
  text-transform:uppercase;
  font-size:0.75rem;letter-spacing:0.16em;
  color:var(--muted-fg);
  padding:4px 0;
  border-bottom:1px solid transparent;
  transition:color var(--dur),border-color var(--dur);
}
.nav__link:hover{color:var(--fg);}
.nav__link[aria-current="page"]{color:var(--fg);border-bottom-color:var(--accent);}

.nav__toggle{
  display:inline-flex;align-items:center;justify-content:center;
  min-width:44px;min-height:44px;padding:0 14px;
  background:none;border:1px solid var(--border);
  font-family:var(--font-body);font-weight:600;
  text-transform:uppercase;font-size:0.6875rem;letter-spacing:0.14em;
  cursor:pointer;
  transition:border-color var(--dur),color var(--dur);
}
.nav__toggle:hover{border-color:var(--accent);color:var(--accent);}
@media(min-width:900px){ .nav__toggle{display:none;} }

/* A panel over the page, not a list that pushes it down. Opening a menu used to
   shove the whole document south and leave the visitor looking at the same page
   they were already on, wearing a stack of links as a hat. Taking the screen for
   as long as it is open is what a phone expects, and it buys the room for the
   thing a visitor on a phone actually wants: a number they can tap.
   Sits below the nav's z-index and starts at its bottom edge, so the Close
   button stays visible and in the same place the Menu button was.
   visibility rather than display, for the reason the lightbox gives at length:
   display:none cannot be transitioned, while visibility already keeps the panel
   out of the tab order and off the accessibility tree while it is closed. */
.drawer{
  position:fixed;
  inset:var(--nav-h) 0 0 0;
  z-index:70;
  background:var(--bg);
  display:flex;flex-direction:column;
  overflow-y:auto;
  /* A scroll that reaches the end of the panel must not carry on into the page
     underneath it, which is still sitting there at full height. */
  overscroll-behavior:contain;
  opacity:0;visibility:hidden;
  translate:0 -6px;
  transition:opacity var(--dur),translate var(--dur),
             visibility 0s linear var(--dur);
}
.drawer[data-open="true"]{
  opacity:1;visibility:visible;translate:none;
  transition:opacity var(--dur),translate var(--dur),visibility 0s linear 0s;
}
@media(min-width:900px){ .drawer{display:none;} }

.drawer__links{display:flex;flex-direction:column;}
.drawer__link{
  display:block;
  padding:22px var(--gutter-r) 22px var(--gutter-l);
  border-top:1px solid var(--border);
  text-transform:uppercase;
  font-size:clamp(1.5rem,7vw,2rem);
  font-weight:600;
  letter-spacing:-0.01em;line-height:1;
  transition:color var(--dur);
}
/* The nav's own bottom border is directly above this one. Two hairlines with
   nothing between them read as a single thick rule drawn by mistake. */
.drawer__links .drawer__link:first-child{border-top:0;}
.drawer__link:hover,.drawer__link:focus{color:var(--accent);}

/* Pushed to the foot of the panel by margin-top:auto, so it sits at the bottom
   of the screen under four links or under six. */
.drawer__direct{
  margin-top:auto;
  border-top:1px solid var(--border);
  padding:20px var(--gutter-r)
    calc(24px + env(safe-area-inset-bottom,0px))
    var(--gutter-l);
  display:flex;flex-direction:column;
}
.drawer__contact{
  display:flex;align-items:center;
  min-height:48px;
  font-weight:600;
  text-transform:uppercase;
  font-size:1.0625rem;letter-spacing:0;
  color:var(--fg);
  transition:color var(--dur);
}
.drawer__contact:hover,.drawer__contact:focus{color:var(--accent);}

/* ============================================================
   HERO — the footage is the point; chrome stays out of its way
   ============================================================ */
.hero{
  position:relative;
  min-height:calc(100svh - var(--nav-h));
  display:flex;flex-direction:column;justify-content:space-between;
  overflow:hidden;
}
/* The contact bar is on screen from the first paint on a phone, so the hero has
   to end above it or its own base strip sits underneath one. Both tokens are
   zero above the bar's breakpoint, which is why this only needs saying here. */
@media(max-width:767px){
  .hero{min-height:calc(100svh - var(--nav-h) - var(--callbar-space));}
}
.hero__media{
  position:absolute;inset:0;
  background:var(--muted);
  transform-origin:center;
  will-change:transform;
}
.hero__media video{width:100%;height:100%;object-fit:cover;}
.hero__scrim{
  position:absolute;inset:0;
  background:
    linear-gradient(to bottom,rgba(19,17,16,0.86) 0%,rgba(19,17,16,0.30) 42%,rgba(19,17,16,0.02) 66%,rgba(19,17,16,0.62) 100%),
    linear-gradient(to right,rgba(19,17,16,0.55) 0%,rgba(19,17,16,0.10) 52%,rgba(19,17,16,0) 100%);
}
/* Follows the split composition above: the copy holds the two ends of the frame
   and the shot holds the middle, so the scrim is dark at both ends and clears
   right out between them. That middle band, roughly 42% to 60%, is the only
   part of the hero with nothing laid over it at all, and it is deliberately the
   part with the subject in it.
   The sideways gradient is dropped here. It exists to back left-aligned type by
   weighting the left of the frame, and against centred type it just tilts the
   image for no reason. */
@media(max-width:767px){
  .hero__scrim{
    background:linear-gradient(to bottom,
      rgba(19,17,16,0.84) 0%,
      rgba(19,17,16,0.60) 26%,
      rgba(19,17,16,0.10) 42%,
      rgba(19,17,16,0.10) 60%,
      rgba(19,17,16,0.52) 78%,
      rgba(19,17,16,0.88) 100%);
  }
}
.hero__content{
  position:relative;z-index:2;
  width:100%;max-width:1600px;margin-inline:auto;
  padding:56px var(--gutter-r) 0 var(--gutter-l);
}
@media(min-width:768px){ .hero__content{padding-top:72px;} }
.hero__title{
  font-size:clamp(2.75rem,9vw,9rem);
  margin-bottom:28px;
  /* THE BREAK IS THE POINT. "Every shot" over "moves" is the composition the
     hero was built around: two short stacked lines holding the left of the
     frame while the shot keeps the rest. 16ch was the figure that produced it
     in Archivo Black, which is wide. Big Shoulders is condensed enough that the
     whole line fits inside the same box, and a single line reads as a caption
     across the top rather than as a title.
     11ch rather than a <br>, so the break stays a consequence of the column
     width and a longer headline still wraps sensibly instead of breaking in a
     hardcoded place. Measured across 1600, 1440, 1280, 1024, 820, 390 and 360:
     anything from 9ch to 13ch gives two lines at every one of them and 8ch
     tips to three, so this sits in the middle of the band rather than on its
     edge. Re-measure if the headline text or the display face changes. */
  max-width:11ch;
}
/* MOBILE HERO: centred, and split top and bottom.
   The clamp above is inert on a phone. 9vw at 390px is 35px, under the 2.75rem
   floor, so every screen from 320px to about 489px got exactly 44px: not a
   responsive headline, just the smallest value a desktop scale bottomed out at.
   Making it bigger was not the answer either. A 74px three-line stack works on
   a 1440px canvas because the headline sits in the left third with the car
   beside it; a phone has no horizontal room for that arrangement, so the type
   and the footage end up fighting over the same pixels whatever the size.
   So the two are separated vertically instead. The headline holds the top, the
   lede drops to the foot, and the shot keeps the whole middle of the frame with
   nothing over it. Both are centred, which is what lets the split read as a
   composition rather than as two things that failed to meet.
   Everything here is off by 768px, where the desktop arrangement takes over
   unchanged. */
@media(max-width:767px){
  /* Fills the space between the nav and the base strip, so the two auto margins
     below have something to push against. */
  .hero__content{
    flex:1;
    display:flex;flex-direction:column;
    padding-top:40px;padding-bottom:28px;
    text-align:center;
  }
  .hero__title{
    /* Sized so "EVERY SHOT" always fits one line and the break lands before
       "MOVES". The text runs 6.86px wide per px of font size, so the ceiling is
       the column width over 6.86: 46.6px at 360px, 51px at 390px. 13vw cleared
       390px but came to 46.8px at 360px and tipped to three lines. 12.5vw sits
       under the ceiling at every width from 320px up.
       The cap is set against the other side of the breakpoint rather than by
       eye: the desktop rule is 9vw, so it hands over at 69px at 768px, and
       4.25rem lands this at 68px just before it. */
    font-size:clamp(2.5rem,12.5vw,4.25rem);
    max-width:none;
    margin:0 auto;
  }
  /* The whole reason the lede is at the bottom. auto here eats the free space
     above it, which pins it just over the base strip and leaves the middle of
     the frame empty. */
  .hero__foot{
    margin-top:auto;
    align-items:center;
    gap:0;
  }
  .hero__foot .lede{
    max-width:32ch;
    margin-inline:auto;
    /* Off --muted-fg here and only here. Measured against the composited frame,
       the muted tone fell under the 4.5:1 that body text needs wherever the clip
       runs bright, and it has to clear that everywhere rather than on average.
       The shadow is the one .work__play already uses for type sitting on
       footage with no scrim of its own. */
    color:var(--fg);
    text-shadow:0 1px 6px rgba(19,17,16,0.85);
  }
  /* The base strip is centred to match, but that rule cannot live here: both
     .hero__base-inner and .hero__kit are declared further down the file, and at
     equal specificity the later declaration wins whether or not this one is
     inside a media query. It sits with them instead, under .hero__kit. */
}
.hero__foot{
  display:flex;flex-direction:column;align-items:flex-start;
  gap:28px;
}
/* No button in the hero on a phone. Full width it became a solid bone block
   sitting across the middle of the frame, which is the one part of the shot the
   scrim deliberately leaves bright: the hero is the only place on the site where
   the footage is the argument rather than the illustration, and the button was
   parked on top of it.
   Nothing is lost by removing it. The contact bar carries the same label at the
   foot of the same screen and never scrolls away, so the action is more
   available than it was, not less. Leaving both would have put "Get a quote"
   on screen twice at once.
   Desktop keeps its button: there the hero content sits in the left third and
   the button lands on the scrim's dark side, clear of the subject. */
@media(max-width:767px){ .hero__foot .btn{display:none;} }

/* Anchors the bottom of the frame so the hero isn't all weight up top. */
.hero__base{
  position:relative;z-index:2;
  width:100%;max-width:1600px;margin-inline:auto;
  /* Bottom inset as well as the sides: this strip is the one thing on the site
     that sits hard against the bottom of the screen, which is exactly where the
     home indicator sits on a phone without a home button. */
  padding:0 var(--gutter-r)
    calc(24px + env(safe-area-inset-bottom,0px))
    var(--gutter-l);
}
@media(min-width:768px){
  .hero__base{padding-bottom:calc(32px + env(safe-area-inset-bottom,0px));}
}
/* The contact bar sits below this now and pays the bottom inset itself, so
   adding it here as well would push the kit strip up off the foot of the frame
   by the height of a home indicator for no reason. */
@media(max-width:767px){ .hero__base{padding-bottom:20px;} }
.hero__base-inner{
  display:flex;flex-wrap:wrap;gap:16px 32px;
  align-items:center;justify-content:space-between;
  border-top:1px solid rgba(217,207,192,0.28);
  padding-top:18px;
}
.hero__kit{
  display:flex;flex-wrap:wrap;gap:14px 26px;
  list-style:none;margin:0;padding:0;
}
/* The four items measure 292.5px of text between them, so on a 375px phone the
   335px of usable width leaves 42.5px for the three gaps: 14px each. At the
   desktop 26px the row needed 370px, wrapped, and left "Part 107" alone on a
   second line, which is the orphan the strip is least able to carry.
   12px clears it from 375px up, which covers every current iPhone and most
   Android. Narrower than that (360px Galaxy, 320px SE) it still wraps, and it is
   left to: the alternative is cutting an item or pulling the letter-spacing in
   from the 0.2em that this strip shares with the work captions and the hero cue,
   and a tidy second line is cheaper than either. Measured, not estimated: a
   first pass at 18px was guessed from character counts and was 11px short. */
/* Centred with the rest of the hero. Left at space-between, the strip is the
   one thing still hanging off the left edge, and it gives away that the frame
   above it was centred by a rule rather than on purpose. Has to be declared
   here rather than up with the other mobile hero rules: these two selectors are
   defined below that block, and at equal specificity the later declaration wins
   regardless of the media query. */
@media(max-width:767px){
  .hero__kit{gap:10px 12px;justify-content:center;}
  .hero__base-inner{justify-content:center;}
}
.hero__kit li{
  font-family:var(--font-body);font-weight:600;
  text-transform:uppercase;font-size:0.6875rem;letter-spacing:0.2em;
  color:var(--fg);opacity:0.82;
}
.hero__cue{
  display:inline-flex;align-items:center;gap:10px;
  min-height:44px;padding:0;
  background:none;border:0;cursor:pointer;
  font-family:var(--font-body);font-weight:600;
  text-transform:uppercase;font-size:0.6875rem;letter-spacing:0.2em;
  color:var(--fg);opacity:0.72;
  transition:opacity var(--dur);
}
.hero__cue:hover{opacity:1;}
.hero__cue svg{width:12px;height:16px;flex:none;}
@media(max-width:520px){ .hero__cue{display:none;} }

/* ============================================================
   BUTTONS
   ============================================================ */
.btn{
  display:inline-flex;align-items:center;justify-content:center;
  height:56px;padding-inline:32px;
  font-family:var(--font-ui);
  font-weight:600;
  text-transform:uppercase;letter-spacing:0.02em;font-size:0.9375rem;
  border:1px solid transparent;border-radius:var(--radius-control);cursor:pointer;
  /* TACTILE, because the old button was not. Its only response to being touched
     was a background change from bone to a slightly whiter bone, which is close
     to invisible, and :active was not styled at all, so pressing it did nothing.
     The complaint was that the buttons felt bland. At rest they are not: a bone
     plate at 15:1 on near-black is the highest contrast thing on the page. They
     felt dead, which is a different problem and this is its fix.
     A lit top edge so the plate has a light side, a lift and a real shadow on
     hover, and a press that pushes it down and pulls the shadow inside. Only
     transform and box-shadow animate, both compositor properties. */
  box-shadow:inset 0 1px 0 rgba(255,255,255,0.26), 0 1px 2px rgba(0,0,0,0.5);
  transition:background var(--dur),color var(--dur),border-color var(--dur),
             transform 140ms cubic-bezier(0.16,1,0.3,1),box-shadow 140ms ease;
}
.btn:hover{
  transform:translateY(-1px);
  box-shadow:inset 0 1px 0 rgba(255,255,255,0.34), 0 8px 22px rgba(0,0,0,0.5);
}
.btn:active{
  transform:translateY(1px);
  box-shadow:inset 0 2px 5px rgba(0,0,0,0.3);
}
/* The lift is the part that has to go. The colour change stays, because without
   it a reduced-motion visitor gets no hover feedback at all. */
@media(prefers-reduced-motion:reduce){
  .btn,.btn:hover,.btn:active{transform:none;}
}
.btn--accent{background:var(--accent);color:var(--accent-fg);}
.btn--accent:hover{background:var(--fg);}
/* Unused. Every button on the site is btn--accent. This is the quiet second
   style if one is ever needed; audited 15 August 2026 and kept deliberately. */
.btn--outline{border-color:var(--border);color:var(--fg);}
.btn--outline:hover{border-color:var(--accent);color:var(--accent);}
.btn--lg{height:68px;padding-inline:40px;font-size:clamp(0.9375rem,1.6vw,1.125rem);}

/* Full width on a phone. An auto-width button sitting at the left margin is the
   single clearest tell that a layout was drawn for a mouse and then made
   narrower: on a desktop the button is sized by its label because there is a
   row to share, and on a phone there is no row, so the same rule leaves a small
   target floating in a wide empty band.
   The lg variant loses its extra height here as well. Large-versus-regular is a
   hierarchy device that works when two buttons can be seen at once and means
   nothing when both run the full width of the screen one after the other. */
@media(max-width:767px){
  .btn{width:100%;}
  .btn--lg{height:56px;padding-inline:32px;}
}

/* ============================================================
   WORK — video-forward tiles, restrained hover
   ============================================================ */
.work-grid{
  display:grid;grid-template-columns:1fr;gap:48px;
}
@media(min-width:900px){
  .work-grid{grid-template-columns:repeat(2,1fr);gap:72px 40px;}
}

/* FULL BLEED ON PHONES. The tiles are the product on a site whose job is making
   footage look expensive, and inside the shell's gutters they were rendering
   350x197 on a 390px screen: a 16:9 landscape crop, inset on both sides, inside
   a portrait viewport. That is a thumbnail in a list, not a piece of work.
   Running them to both edges is worth more than the 40px of width it recovers.
   An inset frame reads as a card in a feed; an edge-to-edge frame reads as a
   film still, and the tile stops being a link to the work and starts being the
   work. The crop is untouched at 16/9, so nothing is trimmed off the sides of a
   rolling shot to buy the height.
   Negative margin rather than a wider grid, so the tiles break out of the shell
   without the shell or the heading above them having to know about it. */
@media(max-width:767px){
  .work-grid{
    margin-inline:calc(-1 * var(--gutter-l)) calc(-1 * var(--gutter-r));
  }
  /* Both of these used to be held off the tile edge by a 16px clamp, which was
     fine while the shell's gutter was holding the tile off the screen. Now the
     tile edge IS the screen edge, so they take the page gutter instead and the
     caption lines up with every other line of text on the page rather than
     sitting 4px inside it. */
  .work__strip{margin-inline:var(--gutter-l) var(--gutter-r);}
  .work__play{right:var(--gutter-r);}
}

/* The whole tile grows on hover rather than the footage growing inside a still
   frame. A transform, so the space the tile occupies never changes and the
   neighbours cannot be pushed around. Growth is past full size rather than up
   from a shrunk resting state, the way the service blocks work: those are
   always showing one playing block, while here the untouched state is what a
   visitor sees most of the time and a grid resting at 96% just looks small.

   This used to be a subgrid so that captions sitting under the frames would
   line up across a row. The captions are on the tiles now, so a tile is one
   16:9 frame and nothing else: every tile in a row is the same height by
   construction and there is nothing left to align. The subgrid, the row spans
   and their fallback all went with it. */
.work{
  display:block;position:relative;
  transition:transform var(--dur);
}
@media(hover:hover){
  .work:hover,
  .work:focus-within{ transform:scale(1.015); }
}
/* An odd number of tiles leaves the last one alone on its row. Rather than
   stand it beside a hole, it takes the full width and a wider crop, which
   closes the section off instead of letting it trail away. At an even count
   the rule matches nothing, so three, four, five, six or seven pieces all lay
   out without an edit here. */
@media(min-width:900px){
  .work:last-child:nth-child(odd){grid-column:1 / -1;}
  .work:last-child:nth-child(odd) .work__frame{aspect-ratio:21/9;}
}

/* The section arrives rather than simply being present. Driven by the scroll
   position itself, so there is no observer to wire up and no state to hold,
   and a browser without it just shows the tiles, which is the right fallback:
   a reveal that depends on script can leave the work invisible if the script
   never runs.
   It animates `translate` rather than `transform` deliberately. `transform` is
   already spoken for by the hover scale above and the two would overwrite each
   other; the standalone property composes with it instead. */
@supports (animation-timeline:view()){
  @media(prefers-reduced-motion:no-preference){
    .work{
      animation:work-rise linear both;
      animation-timeline:view();
      animation-range:entry 0% entry 45%;
    }
  }
}
@keyframes work-rise{
  from{opacity:0;translate:0 18px;}
  to{opacity:1;translate:none;}
}

/* The button covers the whole card so the entire tile is clickable, while the
   <h3> stays outside it and keeps its heading semantics for screen readers. */
.work__open{
  position:absolute;inset:0;z-index:2;
  background:none;border:0;padding:0;
  cursor:pointer;
}
/* Keyboard focus, and it has to stay: closing the lightbox hands focus back to
   the tile it was opened from (site.js), which Chrome treats as keyboard focus
   and rings accordingly. That is the ring appearing "on its own" after watching
   something, not a stray border.
   Held at 2px rather than the 4px it used to sit at. The tiles carry no border
   of their own now, so a ring standing off in the 40px gutter read as a box
   floating beside the work instead of as the tile being selected. Outside the
   frame rather than inset on purpose: bone against the near-black page is about
   13:1, while the same line laid over a bright sky would not clear 3:1. */
.work__open:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}

/* A dark circle with a white triangle is player chrome borrowed from every
   video site there is, and it was the only round thing on the page. This is
   the same shape language as .btn instead: hard corners, accent fill, display
   type in caps. It sits in a corner rather than over the middle of the shot. */
/* Was a filled accent block in the bottom left, which is now where the strip
   lives and was in any case the loudest thing in a section built on restraint.
   It reads as the hero's cue instead: same 11px uppercase at 0.2em, same
   opacity-not-fill treatment, sitting in the top corner away from the type. */
.work__play{
  position:absolute;z-index:2;
  top:clamp(16px,1.6vw,22px);right:clamp(16px,1.6vw,22px);
  display:inline-flex;align-items:center;gap:9px;
  background:none;color:var(--fg);
  font-family:var(--font-body);font-weight:600;
  text-transform:uppercase;letter-spacing:0.2em;
  font-size:0.6875rem;line-height:1;
  padding:0;
  /* The shot behind this corner is whatever it is, and unlike the strip there
     is no scrim up here to sit on. */
  text-shadow:0 1px 4px rgba(19,17,16,0.9);
  pointer-events:none;
  transition:opacity var(--dur), transform var(--dur);
}
.work__play svg{width:9px;height:11px;flex:none;}

/* Desktop: tiles stay clean at rest, the cue fades in on hover. */
@media(hover:hover){
  .work__play{opacity:0;transform:translateY(-4px);}
  .work:hover .work__play,
  .work:focus-within .work__play{opacity:0.9;transform:translateY(0);}
}

/* Touch: there is no hover to reveal anything, so the cue is always there. */
@media(hover:none){
  .work__play{opacity:0.85;}
}
/* No border. A hairline box drawn around a video still reads as an empty slot
   waiting to be filled, and footage this good supplies its own edge. The rule
   that used to run around the frame now runs under it, above the caption,
   where it does a second job: see .work__meta. */
.work__frame{
  position:relative;
  aspect-ratio:16/9;
  background:var(--muted);
  border-radius:var(--radius-surface);
  overflow:hidden;
  /* DO NOT REMOVE THIS BORDER, and do not give it a visible colour.
     overflow:hidden clips children to the PADDING box, so a border puts a band
     of painted colour outside everything the frame clips. Without one, the
     outermost pixel of a tile is raw footage, and scaling the tile on hover
     rasterises this box at a fractional size and antialiases that edge: bright
     footage blended with a near-black page, which reads as a thin grey line
     around the whole tile.
     The services blocks never showed it because .svc has carried a 1px border
     all along. This frame did too, until the border was dropped for looking
     like an empty slot around the footage. Dropping it is what exposed the
     edge; painting it in the page's own colour keeps the tile borderless to
     look at while giving the clip an edge to end on.
     A box-shadow does not substitute. Inset shadows paint beneath child
     content, so the video covers them, and an outer shadow sits on the wrong
     side of the boundary. It has to be a border.
     Costs nothing: box-sizing is border-box site-wide, so the tile does not
     change size, and one pixel comes off each edge of the shot.
     None of this reproduces in headless Chromium, which rasterises on the CPU,
     so it cannot be caught from a screenshot here.
     Painting it in --bg was an attempt to make the edge disappear and it did
     not hold. So the edge is owned instead: on hover the same border lights up
     in the accent tint the strip and the hero's base rule already use, and the
     tile reads as outlined on purpose. A hairline that is meant to be there
     cannot look like a rendering fault, and any fringe the compositor still
     produces lands underneath a line that is brighter than it. */
  border:1px solid var(--bg);
  transition:border-color var(--dur);
}
/* A rim light rather than a hairline. The bright edge is the border itself, and
   the two shadows are its falloff: a tight one to keep the edge from ending
   abruptly, then a wide soft one that lets the tile sit off the page. Warm bone
   throughout, the same colour the strip and the hero rule use, so nothing new
   enters the palette.
   Kept to the accent rather than pure white, and to a wide soft falloff rather
   than a tight bright one, because the neon-glow version of this reads cheap
   immediately on a dark page. */
@media(hover:hover){
  .work:hover .work__frame,
  .work:focus-within .work__frame{
    border-color:rgba(217,207,192,0.92);
    box-shadow:
      0 0 0 1px rgba(217,207,192,0.22),
      0 0 34px 2px rgba(217,207,192,0.16);
  }
}
.work__frame video,
.work__frame img{
  width:100%;height:100%;object-fit:cover;
  /* Slower than the tile's own growth on purpose. Two rates instead of one put
     a little parallax between the frame and the shot inside it, which is what
     stops the hover reading as one flat thing being scaled up. */
  transition:transform 600ms cubic-bezier(0.16,1,0.3,1);
}
@media(hover:hover){
  .work:hover .work__frame video,
  .work:hover .work__frame img,
  .work:focus-within .work__frame video,
  .work:focus-within .work__frame img{transform:scale(1.05);}
}
/* Not in use yet. It labels a work tile that has no footage behind it, and the
   fourth tile on the home page is still a placeholder, so this is waiting on
   that rather than left over from something. */
.work__empty{
  position:absolute;inset:0;
  display:flex;align-items:center;justify-content:center;
  font-family:var(--font-body);font-weight:600;
  text-transform:uppercase;font-size:0.6875rem;letter-spacing:0.22em;
  color:var(--faint);
}
/* The client and the year sit at opposite ends of a column that runs to about
   740px at desktop, and with nothing between them they read as two unrelated
   labels rather than one caption. The rule inherited from the frame spans the
   full width and ties them back together. It is also the tile's hover cue now,
   in place of the border that used to change colour around the frame: a line
   sweeping to accent across the whole width says "this one" more quietly than
   recolouring the headline did. */
/* Enough dimming at the foot of the frame to carry the strip, and nothing above
   it. The shot keeps its own brightness everywhere the type is not. */
.work__scrim{
  position:absolute;inset:0;z-index:1;
  pointer-events:none;
  background:linear-gradient(to top,
    rgba(19,17,16,0.86) 0%,
    rgba(19,17,16,0.52) 15%,
    rgba(19,17,16,0.12) 34%,
    rgba(19,17,16,0) 52%);
}

/* The hero's base strip, brought down to tile scale. Same hairline tinted with
   the accent rather than the grey border, same 18px under it, same 11px
   uppercase at 0.2em, and the same trick of ranking things by opacity instead
   of by size: the hero runs its kit list at 0.82 and its cue at 0.72, so the
   piece name here sits at full strength and the client and year sit back.
   Everything is one size on purpose. Two sizes is what made the old captions
   look busy. */
.work__strip{
  position:absolute;left:0;right:0;bottom:0;z-index:2;
  margin:0 clamp(16px,1.6vw,22px) clamp(16px,1.6vw,20px);
  display:flex;flex-wrap:wrap;
  align-items:baseline;justify-content:space-between;gap:6px 24px;
  border-top:1px solid rgba(217,207,192,0.28);
  padding-top:16px;
  pointer-events:none;
  transition:border-color var(--dur);
}
.work:hover .work__strip,
.work:focus-within .work__strip{border-top-color:rgba(217,207,192,0.65);}
/* Piece name on the left, the context it needs on the right. Set as one run
   they blurred into a single line of small caps, because opacity alone is not
   enough separation when two different kinds of information sit shoulder to
   shoulder at the same size. Splitting them across the strip is what the hero
   does with its kit list and its cue. */
.work__idents{
  display:flex;flex-wrap:wrap;align-items:baseline;
  justify-content:flex-end;gap:4px 20px;
  min-width:0;
}
/* Still an <h3>, so the section keeps its heading outline and the script still
   reads it for the lightbox label. Only the styling has stopped shouting. */
.work__title,
.work__client,
.work__year{
  font-family:var(--font-body);font-weight:600;
  text-transform:uppercase;font-size:0.6875rem;letter-spacing:0.2em;
  line-height:1.2;
  color:var(--fg);
  margin:0;
}
.work__client,
.work__year{opacity:0.72;transition:opacity var(--dur);}
.work:hover .work__client,
.work:hover .work__year,
.work:focus-within .work__client,
.work:focus-within .work__year{opacity:0.95;}

/* ============================================================
   SLATE — a section's top edge, not a heading
   ============================================================ */
/* A hairline with small caps at both ends. Deliberately the same construction
   as .hero__base-inner, down to the rule's colour and the 18px above the text,
   because the point is the rhyme: the hero closes on this row and the section
   under it opens on the same one.
   It is not a heading and must not grow into one. Anything that needs display
   type wants .heading instead. The two ends are a label and a fact, both short
   enough to sit on one line at 900px; a third item would wrap and the rule
   would stop reading as an edge. */
.slate{
  display:flex;flex-wrap:wrap;gap:16px 32px;
  align-items:center;justify-content:space-between;
  border-top:1px solid rgba(217,207,192,0.28);
  padding-top:18px;
  margin-bottom:28px;
}
/* .eyebrow ships a 28px bottom margin for sitting over a heading. In here it is
   one half of a rule, so it is reset to nothing and .slate owns the spacing. */
.slate__k{margin:0;color:var(--fg);opacity:0.72;}
@media(max-width:767px){
  /* The right-hand fact is the first thing to go on a phone. Both ends on one
     line need about 520px and below that they stack, which turns a one-line
     edge into a two-line block and costs the tiles their head start. */
  .slate__k + .slate__k{display:none;}
  .slate{margin-bottom:28px;}
}

/* ============================================================
   WORK REEL — one piece centred, its neighbours at the edges
   ============================================================ */
/* THE COUNT IS NOWHERE IN HERE. Every rule below addresses a piece by its
   distance from the centre, never by its position in a list, so there are only
   ever five slots to style however many articles the markup holds: the centre,
   two neighbours, and two parking bays just out of frame that a piece slides
   through on its way in or out. Adding a tenth job is a copy-paste in
   index.html and nothing else.

   That is the whole reason this replaced a grid. A grid has to be told how many
   columns it has, which is what made an odd number of tiles a special case
   needing its own rule, and what made a stage with a four-across strip under it
   go wrong the moment a fifth piece arrived.

   Everything hangs off data-ready, which site.js sets. Blocked script leaves it
   "false" and the track falls back to the plain stacked tiles below, at full
   size, so the work is still all there. */

/* ---------- fallback: no script, no reel ---------- */
.reel__track{
  display:grid;grid-template-columns:1fr;gap:48px;
}
@media(min-width:900px){
  .reel[data-ready="false"] .reel__track{grid-template-columns:repeat(2,1fr);gap:72px 40px;}
}
@media(max-width:767px){
  .reel[data-ready="false"] .reel__track{
    margin-inline:calc(-1 * var(--gutter-l)) calc(-1 * var(--gutter-r));
  }
}

/* ---------- the reel ---------- */
.reel[data-ready="true"]{
  position:relative;
  /* OUT TO THE WINDOW, not to the shell. Clipped at the shell the neighbours
     ended on a hard vertical edge with a strip of empty page beyond it, which
     reads as a picture that has been cut off rather than a row that carries on.
     Cut by the window instead, there is nothing past the edge to argue with and
     the same clip reads as "there is more this way".
     calc(50% - 50vw) is the standard breakout: half the parent's width back,
     half the viewport's width out. The clip on <main> below is what stops the
     100vw box from adding a horizontal scrollbar. */
  margin-inline:calc(50% - 50vw);
  overflow:hidden;

  /* What the window has left once the nav, this band's top padding and the
     slate have taken theirs, plus air at the foot so the reel is not sitting on
     the bottom edge of the screen. */
  --reel-room:calc(100svh - var(--nav-h) - 165px);
  /* Bounded by the window's WIDTH and its HEIGHT, because 16:9 means a wide
     short window cannot take a wide centre: at 1366x768 a 64vw centre would be
     874px across and 492 tall, and the room is 530. min() picks whichever
     constraint bites first and the centre is never the thing that overflows. */
  /* The cap is what decides how much neighbour is left over. The reel is as
     wide as the shell, 1600px, so a 1180px centre leaves 182px a side once the
     gap is paid, and at 90% scale that is a dim vertical strip you cannot read
     as footage. At 1040 each side shows about 250px, which is a quarter of a
     16:9 frame: enough to see what the next piece is of. */
  --reel-w:min(58vw, calc(var(--reel-room) * 16 / 9), 1040px);
  --reel-gap:clamp(16px,2vw,28px);
  --reel-step:calc(var(--reel-w) + var(--reel-gap));
}
/* Phones get a centre that is nearly the whole screen with the neighbours
   showing as slivers. Enough to say the row keeps going, not enough to take
   width off the footage, which is the only thing worth looking at on a 390px
   screen. */
/* THE SLIVER IS ARITHMETIC, not a guess. What shows of a neighbour is
     (viewport - centre) / 2  -  gap  -  (centre * the scale it is shrunk by)
   At 84vw with a 10px gap on a 390px phone that came to 11px, which is a line
   down the edge of the screen rather than a piece of footage, and it read as a
   rendering fault. 76vw with a smaller gap and less shrink gives about 33px:
   still a sliver, but one you can see is a picture. The cost is 30px off the
   centre, which is the trade this layout is: a centre that runs the full width
   of the screen cannot also have neighbours beside it. */
@media(max-width:767px){
  .reel[data-ready="true"]{
    --reel-w:76vw;
    --reel-gap:8px;
    --reel-room:none;
  }
  .reel[data-ready="true"] .work[data-slot="-1"],
  .reel[data-ready="true"] .work[data-slot="1"]{
    transform:translateX(calc(-50% + (var(--reel-step) * var(--reel-dir,1)))) scale(0.96);
  }
  .reel[data-ready="true"] .work[data-slot="-1"]{--reel-dir:-1;}
  .reel[data-ready="true"] .work[data-slot="1"]{--reel-dir:1;}

  /* Client and year come off the caption on a phone. The strip was written for
     a 740px tile; at 296px the title and the two idents cannot share a line, so
     they wrapped to three and filled the bottom half of a 166px frame, running
     straight through the play button sitting in the middle of it. The title
     alone fits on one line and leaves the picture visible, which is the whole
     job of the centre. Nothing is lost to a screen reader: the caption is still
     in the DOM and the button is labelled with the title. */
  .reel[data-ready="true"] .work__idents{display:none;}
}

.reel[data-ready="true"] .reel__track{
  display:block;
  position:relative;
  height:calc(var(--reel-w) * 9 / 16);
  /* Horizontal drags are the reel's; vertical ones belong to the page. Without
     this a swipe down the page that starts on the reel gets swallowed. */
  touch-action:pan-y;
}

.reel[data-ready="true"] .reel__track > .work{
  position:absolute;top:0;left:50%;
  width:var(--reel-w);
  /* transform and opacity only, so a move is composited and never reflows the
     page. There is a video playing inside the thing being moved. */
  transition:transform 460ms cubic-bezier(0.16,1,0.3,1),opacity 460ms ease;
  /* The view() reveal on .work animates opacity and would fight the slot
     opacities below on anything that scrolled into view mid-move. */
  animation:none;
}

/* THE FIVE SLOTS. Written out rather than calculated from a --slot number so
   that what each position does is readable here instead of assembled at
   runtime. site.js writes data-slot and nothing else. */
.reel[data-ready="true"] .work[data-slot="-2"]{
  transform:translateX(calc(-50% - (2 * var(--reel-step)))) scale(0.94);
  opacity:0;pointer-events:none;
}
.reel[data-ready="true"] .work[data-slot="-1"]{
  transform:translateX(calc(-50% - var(--reel-step))) scale(0.94);
  opacity:0.52;z-index:1;
}
.reel[data-ready="true"] .work[data-slot="0"]{
  transform:translateX(-50%) scale(1);
  opacity:1;z-index:2;
}
.reel[data-ready="true"] .work[data-slot="1"]{
  transform:translateX(calc(-50% + var(--reel-step))) scale(0.94);
  opacity:0.52;z-index:1;
}
.reel[data-ready="true"] .work[data-slot="2"]{
  transform:translateX(calc(-50% + (2 * var(--reel-step)))) scale(0.94);
  opacity:0;pointer-events:none;
}

/* NO RIM IN THE REEL. .work__frame lights its border and throws a soft bone
   glow on hover, which was built for a tile sitting whole on the page. A
   neighbour here is scaled down and cut off by the window, so the rim traced
   three sides of a shape whose fourth side is a clip: a bright line that ran
   off the screen and stopped. It read as a rendering fault, which is exactly
   what it looked like.
   The centre does not want one either. It is already the subject; outlining it
   says "selected" about the only thing that could not be anything else. */
.reel[data-ready="true"] .work .work__frame,
.reel[data-ready="true"] .work:hover .work__frame,
.reel[data-ready="true"] .work:focus-within .work__frame{
  border-color:var(--bg);
  box-shadow:none;
}

/* Reaching for a neighbour brings it forward instead. Opacity and a little
   scale, both of which the piece is already animating, so the hover borrows the
   motion the reel is made of rather than introducing a new effect. */
@media(hover:hover){
  .reel[data-ready="true"] .work[data-slot="-1"]:hover,
  .reel[data-ready="true"] .work[data-slot="1"]:hover{opacity:0.88;}
  .reel[data-ready="true"] .work[data-slot="-1"]:hover{
    transform:translateX(calc(-50% - var(--reel-step))) scale(0.97);
  }
  .reel[data-ready="true"] .work[data-slot="1"]:hover{
    transform:translateX(calc(-50% + var(--reel-step))) scale(0.97);
  }
}
/* Keyboard focus still needs a visible marker, and with the rim gone it has to
   come from somewhere. A ring on the button rather than the frame, so it sits
   inside the clip and cannot be the thing that runs off the edge. */
.reel[data-ready="true"] .work .work__open:focus-visible{
  outline:2px solid var(--accent);
  outline-offset:-4px;
}

/* The caption belongs to the piece being shown. On a neighbour it is small
   enough to be unreadable and busy enough to matter, so it goes. */
.reel[data-ready="true"] .work:not([data-slot="0"]) .work__strip,
.reel[data-ready="true"] .work:not([data-slot="0"]) .work__scrim{
  opacity:0;
  transition:opacity var(--dur);
}
/* The corner "Watch" badge is replaced by the button in the middle. */
.reel[data-ready="true"] .work__play{display:none;}

/* The hover rim on .work__frame marks a clickable edge, which is right for a
   neighbour and wrong for the centre: the centre's affordance is the play
   button, and a rim around a piece that is already the subject just makes it
   look selected twice. */
.reel[data-ready="true"] .work[data-slot="0"] .work__frame{
  border-color:var(--bg);
  box-shadow:none;
}

/* ---------- the play button ---------- */
/* A span, not a button. The real control is the full-cover .work__open beneath
   it, which already carries the label and the keyboard behaviour; this is what
   that control looks like. Two buttons stacked on the same rectangle is how you
   get a tab stop that does nothing.

   Square, because the site is square. Every other edge on this page is a right
   angle and a circle here would be the one round thing on it. */
.reel__play{
  position:absolute;left:50%;top:50%;z-index:3;
  transform:translate(-50%,-50%);
  display:flex;align-items:center;justify-content:center;
  width:clamp(62px,6.2vw,88px);
  height:clamp(62px,6.2vw,88px);
  border:1px solid rgba(217,207,192,0.72);
  background:rgba(19,17,16,0.58);
  color:var(--fg);
  pointer-events:none;
  transition:background-color var(--dur),border-color var(--dur),color var(--dur);
}
.reel__play svg{
  width:30%;height:auto;
  /* A triangle's mass sits left of its bounding box's centre, so centring the
     box leaves it looking shifted. This is the optical correction. */
  translate:9% 0;
}
/* Driven from the tile, since the span cannot be hovered itself. Filling it
   with the accent on hover rather than glowing it: a solid bone square reads as
   a button being pressed, a halo reads as a stock template. */
@media(hover:hover){
  .reel[data-ready="true"] .work[data-slot="0"]:hover .reel__play{
    background:var(--accent);border-color:var(--accent);color:var(--accent-fg);
  }
}
.reel[data-ready="true"] .work[data-slot="0"] .work__open:focus-visible ~ .reel__play,
.reel[data-ready="true"] .work[data-slot="0"]:focus-within .reel__play{
  background:var(--accent);border-color:var(--accent);color:var(--accent-fg);
}
/* Pressed. One pixel, the same tactile nudge the buttons elsewhere use. */
.reel[data-ready="true"] .work[data-slot="0"] .work__open:active ~ .reel__play{
  transform:translate(-50%,calc(-50% + 1px));
}
/* Only the centre offers to play. A neighbour's job is to come to the middle. */
.reel[data-ready="true"] .work:not([data-slot="0"]) .reel__play{
  opacity:0;
}

/* ---------- saying out loud that this is a carousel ----------
   The peeking neighbours imply it, and an implication is not a control. These
   two are the explicit version: arrows that say the row moves, and a rank of
   ticks that says how far along it is and how much there is. Between them a
   visitor who has not touched anything yet can already tell what this is. */
.reel__nav{
  position:absolute;top:50%;z-index:4;
  transform:translateY(-50%);
  display:flex;align-items:center;justify-content:center;
  width:clamp(44px,3.4vw,54px);
  height:clamp(60px,4.6vw,74px);
  border:1px solid rgba(217,207,192,0.5);
  background:rgba(19,17,16,0.62);
  color:var(--fg);
  cursor:pointer;
  transition:background-color var(--dur),border-color var(--dur),color var(--dur);
}
.reel__nav:hover{
  background:var(--accent);border-color:var(--accent);color:var(--accent-fg);
}
.reel__nav:focus-visible{outline:2px solid var(--accent);outline-offset:3px;}
.reel__nav:active{transform:translateY(calc(-50% + 1px));}
.reel__nav svg{width:11px;height:17px;flex:none;}
/* Held off the window's edge rather than the centre's, so they sit over the
   neighbours and read as belonging to the row rather than to the middle piece. */
.reel__nav--prev{left:clamp(12px,2vw,32px);}
.reel__nav--next{right:clamp(12px,2vw,32px);}

.reel__ticks{
  display:flex;justify-content:center;align-items:center;
  gap:10px;
  margin-top:22px;
}
.reel__tick{
  width:26px;height:2px;
  background:rgba(217,207,192,0.22);
  transition:background-color var(--dur),transform var(--dur);
}
.reel__tick[data-on="true"]{
  background:var(--accent);
  transform:scaleY(2);
}
@media(max-width:767px){
  .reel__ticks{gap:8px;margin-top:16px;}
  .reel__tick{width:18px;}
  /* The arrows are for pointers. A phone has the neighbours themselves and a
     swipe, and two 54px controls parked over a 296px centre would cover the
     footage they are meant to be helping you get to. */
  .reel__nav{display:none;}
}

/* ---------- the player, in the centre's own box ---------- */
/* No reshaping and no overlay. The centre is already 16:9, so the video opens
   at exactly the size and position of the preview it replaces and nothing on
   the page moves. That is the whole reason the centre is 16:9 rather than the
   wider crop this section used to run. */
.reel__player{
  position:absolute;inset:0;z-index:5;
  background:#000;
}
.reel__player iframe{display:block;width:100%;height:100%;border:0;}
/* While a video is playing the neighbours stop asking for attention. They are
   still there and still clickable, which is how you leave without hunting for
   the close button. */
.reel[data-ready="true"][data-watching="true"] .work:not([data-slot="0"]){
  opacity:0.16;
}

/* ============================================================
   LIGHTBOX — full video with sound, one instance reused
   ============================================================ */
/* Closed state is visibility:hidden, NOT the hidden attribute. visibility
   already keeps it out of the tab order and off the accessibility tree, and
   unlike display:none it can be transitioned. Adding hidden back would make
   the overlay snap open with no fade. */
.lightbox{
  position:fixed;inset:0;z-index:100;
  display:flex;align-items:center;justify-content:center;
  padding:20px;
  /* Keeps a scroll gesture that reaches the end of this overlay from carrying
     on into the page underneath it. */
  overscroll-behavior:contain;
  background:rgba(19,17,16,0.92);
  opacity:0;visibility:hidden;
  /* visibility is not smoothly animatable, so it is switched with a 0s step.
     On close that step is delayed until the fade has finished; on open it fires
     immediately, because focus() cannot move to a visibility:hidden element and
     the close button is focused in the same tick the overlay opens. */
  transition:opacity var(--dur), visibility 0s linear var(--dur);
}
.lightbox[data-open="true"]{
  opacity:1;visibility:visible;
  transition:opacity var(--dur), visibility 0s linear 0s;
}
.lightbox__inner{
  position:relative;
  width:100%;max-width:1200px;
  /* A 16:9 stage plus the bar under it is taller than a phone held sideways,
     which is how people watch video. Left uncapped it pushes Close off the
     bottom of the screen, and a phone has no Escape key to fall back on.
     Capping the width against the height keeps the whole overlay reachable.
     The dvh line wins where supported, so a mobile URL bar cannot overstate
     the space available. */
  max-width:min(1200px, calc((100vh - 110px) * 16 / 9));
  max-width:min(1200px, calc((100dvh - 110px) * 16 / 9));
}
.lightbox__stage{
  position:relative;
  aspect-ratio:16/9;
  background:#000;
  border:1px solid var(--border);
  border-radius:var(--radius-surface);
  /* The others already clipped their children. This one did not, and its iframe
     is inset:0, so the video would have squared off the corner the border just
     rounded. */
  overflow:hidden;
}
.lightbox__stage iframe{
  position:absolute;inset:0;
  width:100%;height:100%;border:0;
}
.lightbox__bar{
  display:flex;justify-content:space-between;align-items:center;gap:16px;
  margin-top:14px;
  font-family:var(--font-body);font-weight:600;
  text-transform:uppercase;font-size:0.6875rem;letter-spacing:0.18em;
}
/* Both controls in this bar were 12px tall. Close matters most: it is the only
   visible way out of the overlay, and hunting for a 12px strip is exactly the
   wrong experience on a phone held sideways. Same trick as the footer, with a
   little horizontal room as well; the negative margins keep the bar's height
   and the two items' positions unchanged. */
.lightbox__fallback{
  color:var(--muted-fg);text-decoration:none;
  display:inline-flex;align-items:center;
  min-height:24px;
  padding:6px 8px;margin:-6px -8px;
}
.lightbox__fallback:hover{color:var(--accent);}
.lightbox__close{
  background:none;border:0;cursor:pointer;
  display:inline-flex;align-items:center;
  min-height:24px;
  padding:6px 8px;margin:-6px -8px;
  font-family:var(--font-body);font-weight:600;
  text-transform:uppercase;font-size:0.6875rem;letter-spacing:0.18em;
  color:var(--fg);
}
.lightbox__close:hover{color:var(--accent);}

/* ============================================================
   HEADINGS
   ============================================================ */
.heading{
  font-size:clamp(2.25rem,6vw,4.5rem);
  margin-bottom:44px;
}
.heading--split{
  display:flex;flex-wrap:wrap;align-items:flex-end;
  justify-content:space-between;gap:20px;margin-bottom:44px;
}
.heading--split .display{font-size:clamp(2.25rem,6vw,4.5rem);}

/* ============================================================
   SERVICES
   ============================================================ */
.stack{
  display:grid;grid-template-columns:1fr;
  /* The tiles carry no border any more, so this gap is the only thing telling
     one from the next: it stopped being a margin and became the edge. 20px was
     sized for tiles that outlined themselves and left them looking stacked
     rather than separate once the outlines went.
     There is a ceiling on it. The scroll picker chooses the tile covering the
     middle of the window, so a gap is a band where the answer is none of them
     and every clip sits dark at once. The note by the phone rules has the
     history: 20px was a flicker nobody caught and 56px was a visible dead
     stretch. 28 is inside that, and against a 478px tile it is well under a
     tenth of the travel. */
  gap:28px;
}

/* The services stack used to snap, so every scroll finished on a block rather
   than between two. It was lovely on a trackpad and wrong on a mouse wheel, and
   it is gone. The rules, the reasoning and the several traps involved are in
   docs\tile-snapping.md, which is enough to put it back in about a minute.
   Read that before reinventing it: most of the obvious approaches were tried. */

/* ============================================================
   CLOSING CALL TO ACTION
   ============================================================ */
/* This was a heading and a button sitting alone on a rule, which left the foot
   of the page looking like it had run out rather than ended. Boxed, with a line
   of copy under the heading and the direct contact details tucked under the
   button, it carries the same weight as the tiles above it. Bordered rather
   than filled, to match those tiles. */
.cta{
  display:grid;grid-template-columns:1fr;gap:28px;
  border:1px solid var(--border);
  padding:36px 24px;
}
@media(min-width:900px){
  .cta{
    /* The button column takes only what it needs, so the copy keeps the rest
       however long it gets. */
    grid-template-columns:minmax(0,1fr) auto;
    align-items:center;
    gap:48px;
    padding:56px 48px;
  }
}
/* A section drawn as one framed surface. The call to action inside it gives up
   its own border and padding and is divided off with a hairline instead, so the
   two read as one design rather than a box sitting inside another box. */
.panel{
  border:1px solid var(--border);
  border-radius:var(--radius-surface);
  background:var(--surface-2);
  padding:36px 24px;
}
@media(min-width:900px){ .panel{padding:56px 48px;} }
.panel .heading{margin-bottom:36px;}
.panel .cta{
  border:0;
  padding:48px 0 0;
  margin-top:48px;
  border-top:1px solid var(--border);
}
.cta__title{margin:0 0 16px;}
.cta__lede{margin-bottom:0;max-width:44ch;}
.cta__act{display:grid;gap:18px;justify-items:start;}
@media(min-width:900px){
  .cta__act{justify-items:end;text-align:right;}
}
.cta__direct{
  margin:0;
  font-size:0.9375rem;
  color:var(--muted-fg);
  display:flex;flex-wrap:wrap;gap:4px 16px;
}
/* The two direct lines beside the services CTA. Same reasoning as the drawer:
   they are the alternative to the button, so they hold their own weight rather
   than borrowing it from whatever the page default happens to be. */
.cta__direct a{color:var(--fg);font-weight:600;}
.svc{
  position:relative;
  isolation:isolate;
  overflow:hidden;
  border:1px solid var(--border);
  border-radius:var(--radius-surface);
  padding:36px 24px;
  display:grid;grid-template-columns:1fr;gap:20px;
}
@media(min-width:1400px){
  .svc{
    /* The copy is beside the footage here, not on top of it.

       It was on top for a long time and the whole file remembers the cost: a
       scrim heavy enough to carry a paragraph, a raised body colour, a text
       shadow, and a contrast note admitting the page still did not pass. All of
       that was one layer being asked to do two jobs at once, set the mood and
       hold the words, so it had to be sized for the harder job across the whole
       frame. Shaping that layer instead of flattening it only moved the problem
       into view: you could see the shape.

       Splitting the jobs retires the argument. The clip gets its own frame and
       runs at very close to full brightness, because nothing is written on it.
       The copy gets a panel and is simply legible, with no shadow propping it
       up and no measurement to apologise for. This is the conclusion the phone
       layout reached first, further down this file, for the same reason.

       One column still, not two. The media is absolutely positioned and never
       claims a cell, so the text children keep filling the single column the
       way every note below assumes. What reserves the space for the clip is
       this block's right padding, not a grid track. */
    /* Declared once and used twice, by the padding here and by the inset on
       .svc__media. They have to agree or the copy runs under the picture, so
       they are not allowed to be two numbers. */
    --svc-media:52%;
    grid-template-columns:minmax(0,1fr);
    background:var(--surface-2);
    /* Two groups, not four evenly spaced items.

       Equal gaps everywhere was the previous answer and it measured beautifully
       and composed badly: 73px between every pair, which says a name, a
       sentence, an action and four keywords are four peers of equal weight.
       They are not. The name and the sentence are one statement; the link and
       the terms under it are what you do about it and what it covers. Spacing
       them identically flattens that, and a column where everything is equally
       important reads as nothing being important.

       So the leftover height is paid out in one place instead of three. The
       statement sits at the top, the action is anchored at the foot, and the
       gap between them is whatever the tile has spare. Flex rather than grid
       purely because margin-top:auto on a flex child does this in one line;
       in grid, auto margins move an item inside its own row and cannot push
       the rows apart.

       Auto placement is still what lays this out, so the constraint the old
       note protected holds: a tile works with three children or five, and no
       row is named anywhere. */
    display:flex;
    flex-direction:column;
    align-items:flex-start;
    gap:26px;
    padding:56px calc(var(--svc-media) + 40px) 56px 40px;
    /* Height is set by the footage, not by the copy, and now there is an actual
       target rather than a feel: the clip owns a fixed share of the width, so
       the tile height that shows it at 16/9 is a straight calculation. At 54%
       of a 1360px block the clip is 704 wide, which wants 396 of height. The
       clamp is built around that, so a visitor at a common desktop width sees
       these shots at very close to the ratio they were shot in instead of a
       crop nobody chose.
       It drifts squarer as the window narrows, because the clip's width falls
       with the block while the floor holds. That is what sets the breakpoint
       below: under 1200px the split stops being the right shape and the tile
       stacks instead. */
    min-height:clamp(420px,29.9vw,478px);
    /* No border. There are two edges here where there used to be one: the clip
       carries its own around the mount, and a second line drawn around the
       whole tile put a box inside a box, so the eye read the outer rectangle
       first, which is the one part of this composition that means nothing. The
       panel says where the tile is and does not need outlining to do it.

       The resting scale, on the other hand, is back, after being dropped and
       then tried on the mount instead. Worth recording why, because the
       reasoning that removed it was wrong in a specific way.

       It was dropped for moving the page's outer margin: three tiles at 96% and
       one at full size means the left and right edges of the stack step in and
       out as you scroll. That is a real objection to moving page structure. It
       is not a real objection here, because a tile in a list is a card, not
       structure, and a card growing when it becomes active is ordinary and
       legible. What actually made the old version look broken was the border
       that used to sit on this rule: a crisp outline makes a 4% width change
       read as a misaligned grid. Take the outline away and the same movement
       reads as the tile coming forward, which is what it always meant.

       Moving it onto the mount kept the margins perfectly straight and lost the
       point. The tile is what the visitor is reading, copy included, so the
       tile is what should come forward.

       0.97 rather than the old 0.96: same gesture, 23px of travel per side
       instead of 30, which is enough to feel and not enough to notice as
       geometry. It compounds with the push-in on the shot inside the mount, so
       the tile grows 3% while the picture within it grows 6%. Two rates, the
       same device .work__frame uses, and the reason this reads as depth rather
       than as one flat thing being scaled. */
    border:0;
    transform:scale(0.97);
    transition:transform var(--dur), box-shadow var(--dur);
  }
  .svc[data-playing="true"]{ transform:scale(1); }
}
/* A link, not a button, and that is a deliberate demotion.

   This page had five filled accent rectangles on it: one per tile and one in
   the closing block. A beige box repeated down the left margin four times is
   the clearest tell that a page was assembled from a template rather than
   designed, and on a site whose whole job is making footage look expensive it
   was the cheapest thing on the screen. The rule is one filled button per page.
   The closing block keeps it, because that is the page's actual ask.

   The action still belongs on every tile, so it stays as type. Accent colour
   does the pointing, and the hairline underneath gives it the same defined edge
   every other element on this site ends on. It reads as a considered link
   instead of shouting four times before the visitor has finished reading.

   justify-self because a grid item otherwise fills its column, which on a
   button was a 600px wide rectangle and on a link would be a 600px underline */
.svc__cta{
  justify-self:start;
  color:var(--accent);
  font-family:var(--font-body);
  font-weight:600;
  text-transform:uppercase;
  font-size:0.8125rem;
  letter-spacing:0.15em;
  padding-bottom:7px;
  border-bottom:1px solid rgba(217,207,192,0.34);
  transition:color var(--dur),border-color var(--dur);
}
.svc__cta:hover{ color:var(--fg); border-bottom-color:var(--fg); }

/* Media sits behind everything in the block. It is position:absolute, so it
   drops out of flow and never claims a grid cell; the two text divs stay
   direct children and keep filling the two columns. */
.svc__media{
  position:absolute;inset:0;z-index:-1;
  background:var(--muted);
}
.svc__media video{
  width:100%;height:100%;object-fit:cover;
  display:block;
}
/* Scrim, built from two stacked layers. ::after is what the playing block is
   left with; ::before is the extra dimming every other block carries, and fades
   to nothing when a block takes its turn. Both layers are the same near black,
   so they composite to a plain darker layer, and only opacity animates, which
   every browser interpolates the same way.
   This rule sets up the two layers and deliberately gives neither of them a
   background. Each of the three width bands paints its own, because each one
   puts the copy somewhere different and so needs a different shape:

     <768px       clip full bleed, name laid into its foot   bottom-up gradient
     768-1399px   clip stacked above the copy, name in foot  bottom-up, in px
     >=1400px     copy beside the clip, nothing written on it  flat, and faint

   There used to be a pair of default gradients here, left over from when the
   copy sat on top of the footage at every width. All three bands override them
   now, so they were painting nothing anywhere and only surviving as a
   description of a layout the page had stopped using. The long note that went
   with them, about a 0.63 alpha floor and where the text column ended, went the
   same way: no band puts body copy on a clip any more, so there is no floor
   left to hold.

   Nothing is inherited on purpose. A band added later that forgets to set a
   background gets no scrim at all, which is obvious on sight, rather than
   quietly picking up a shape drawn for a layout it is not using. */
.svc__media::after{
  content:"";
  position:absolute;inset:0;
}
.svc__media::before{
  content:"";
  position:absolute;inset:0;
  transition:opacity var(--dur);
}
/* The clip becomes a picture mounted in the tile, not a panel butted onto one.

   Nothing is written on it any more, so almost all of the scrim goes. What is
   left of ::after is a whisper, enough to seat the footage on a dark page
   rather than have it glare out of one. ::before keeps the one job worth
   keeping: the tile whose clip is running is the bright one and the rest sit
   back, which is the signal the scroll picker exists to give.

   The inset is the part that does the work. Run edge to edge, the clip and the
   panel are two flat rectangles of very different brightness meeting on a
   straight line, and no treatment of that line fixes it, because the line is
   not the problem: having two rectangles is. Both were tried. A hairline at the
   join, dark or bone, is invisible against bright footage and the tile still
   reads as a video with some words beside it. Pulled in on three sides, the
   panel closes around the picture instead and the tile becomes one object with
   something mounted in it. That is the whole difference between this looking
   placed and looking pasted on.

   The border is not decoration. .work__frame earlier in this file has the long
   version: overflow:hidden clips to the padding box, so without a border the
   outermost pixel of a frame is raw footage, and this tile scales from 0.96 to
   1 when it takes its turn, which rasterises that edge at a fractional size and
   antialiases bright footage against a dark panel into what looks like a grey
   line. That note also warns the effect does not reproduce in headless
   Chromium, so it cannot be caught in a screenshot here and has to be reasoned
   about instead. Bone at low alpha rather than the panel colour, because a
   mount reads better with an edge, and a line that is meant to be there cannot
   be mistaken for a rendering fault.

   overflow:hidden is load bearing too: the playing clip pushes in to 1.03, and
   without it that growth spills over the mount and onto the panel. */
@media(min-width:1400px){
  .svc__media{
    /* 28 top and bottom, 40 on the outer edge, and the asymmetry is the point.
       40 is where the copy's own margin sits, so the picture and the text line
       up on the page's outer edge and the stack has one straight margin down
       each side. Matching 28 there instead would set every second tile 12px
       further out than the one above it, which on a column of alternating
       tiles reads as a wobble rather than as a rhythm.
       Vertically there is nothing to line up with, so the picture keeps the
       tighter inset and stands a little taller than the copy beside it, which
       is what stops the two halves looking like equal columns. */
    inset:28px 40px 28px calc(100% - var(--svc-media));
    overflow:hidden;
    border:1px solid rgba(217,207,192,0.22);
    /* The mount does not scale. It was tried, as the alternative to scaling the
       tile, and the note on .svc says why the tile won it back. Two scales at
       once would compound into about 9% of growth on the picture, which stops
       being a tile coming forward and starts being a zoom. The growth inside
       the mount is the video push-in below, and that is enough. */
    transition:border-color var(--dur), box-shadow var(--dur);
  }
  /* Every other tile turns around. Four identical left-copy right-clip rows is
     the layout equivalent of the button that used to sit in all four of them:
     correct, consistent, and the reason the page read as assembled rather than
     composed. Alternating gives the stack a rhythm to travel down and lets each
     service own its own frame instead of filling in the same template.
     Mirrored, not restyled: same insets, same column width, same everything,
     swapped. The copy stays left aligned in its column at both ends, because
     ragged-left body text is harder to read and this is not the place to prove
     a point about symmetry. */
  .svc:nth-child(even){
    padding:56px 40px 56px calc(var(--svc-media) + 40px);
  }
  .svc:nth-child(even) .svc__media{
    inset:28px calc(100% - var(--svc-media)) 28px 40px;
  }
  .svc__media::after{ background:rgba(19,17,16,0.06); }
  .svc__media::before{ background:rgba(19,17,16,0.42); }
  /* The rim light moves onto the mount, because the mount is now the only edge
     in the tile and the picture is the thing that is live. Same bone, same idea
     as .work__frame, dialled down: that one announces a tile the pointer is
     on, this one marks a tile the page chose while the visitor is reading, so
     it should be noticed rather than obeyed.
     The blur is held to 22px on purpose. .svc still clips to its own box and
     the mount sits 28px inside it, so anything wider than that gets cut off
     square along the tile edge, which looks exactly like the rendering fault
     the border further up exists to avoid. */
  .svc[data-playing="true"]{ box-shadow:none; }
  .svc[data-playing="true"] .svc__media{
    border-color:rgba(217,207,192,0.62);
    box-shadow:
      0 0 0 1px rgba(217,207,192,0.18),
      0 0 22px 1px rgba(217,207,192,0.14);
  }
  /* The two-rate move the work tiles use. The tile grows from 0.96 to 1 when it
     takes its turn; if the picture inside grew at exactly that rate the whole
     thing would read as one flat object being scaled. A slower, smaller push-in
     on the footage puts a little parallax between the frame and the shot, and
     it is the closest thing on this page to a camera move, which is the right
     association for the one tile that is actually playing. Kept small: this
     compounds with the tile's own 4%. */
  .svc__media video{
    transform:scale(1);
    transition:transform 700ms cubic-bezier(0.16,1,0.3,1);
  }
  .svc[data-playing="true"] .svc__media video{ transform:scale(1.03); }
}
.svc[data-playing="true"] .svc__media::before{ opacity:0; }

/* Everything the copy was wearing to survive being on top of footage comes off
   at the width where it no longer is. The shadow, the raised body colour and
   the apology in the note beside it were all one workaround, and the layout
   retired the problem they were working around. Body copy goes back to
   --muted-fg, which is what it is on every other page, and measures 6.4:1 on
   --surface-2. Below 900px the copy is still over a clip, so the base rules
   stay exactly as they were. */
@media(min-width:768px){
  .svc__body,
  .svc__tags li{ text-shadow:none; }
  .svc__body{ color:var(--muted-fg); }
}
/* The title is the one piece of copy still lying on footage between 768 and
   1199, laid into the foot of the clip the way a phone lays it, so it keeps the
   shadow there and only loses it once it moves onto the panel. */
@media(min-width:1400px){
  .svc__title{ text-shadow:none; }
  /* What anchors the second group. Everything from the link down is pushed to
     the foot of the panel, so the tile reads as statement, air, action. */
  .svc__cta{ margin-top:auto; }
}

/* The same rim light the work tiles use, on the same rule: the block whose
   footage is actually running gets the bright edge and the bloom. Work marks
   that with hover because a pointer is how you choose a tile there; here the
   scroll picker chooses, so it hangs off data-playing instead. Same idea, same
   colour, same values, different trigger, because each section decides what is
   live in its own way.
   Deliberately not on hover as well. Hovering an unplayed block already has a
   signal, the dimming lifting half way, and a second block wearing a rim while
   another one is playing would give the stack two focal points at once.
   The border is the one .svc has always carried, so nothing moves: only its
   colour changes. The blocks below rest at 96%, which opens up the 20px stack
   gap enough that the bloom does not crowd its neighbours. */
.svc[data-playing="true"]{
  border-color:rgba(217,207,192,0.92);
  box-shadow:
    0 0 0 1px rgba(217,207,192,0.22),
    0 0 34px 2px rgba(217,207,192,0.16);
}
/* Touch has no hover query wrapping it, and the blocks do not scale there, so
   the transition has to be stated for that case too or the rim would snap on. */
@media(hover:none){
  .svc{ transition:border-color var(--dur), box-shadow var(--dur); }
}

/* The hover hint that used to live here has gone with the thing it hinted at.
   It lifted a resting block's dim half way when the pointer crossed it, as a
   preview of the state clicking would produce. That was a good way to signal
   the click, and site.js now explains why the click itself is gone.
   It is not kept as a plain hover flourish. A block is not interactive any
   more, and lighting one under the pointer would say it is, which is the same
   false promise the pointer cursor was rejected for. It would also give the
   stack two lit blocks at once, the one the page chose and the one the pointer
   happens to be over, and the note by the rim light is explicit that the page
   should only ever have one focal point. The one interactive thing in a block,
   the quote link, has its own hover and that is where the response belongs. */

/* A shadow tight enough to darken only the pixels immediately behind a letter.
   The playing scrim was deliberately lightened past the point where body copy
   clears 4.5:1 against a bright frame, because the footage mattered more than
   the measurement. This buys back the readability locally instead of dimming
   the whole picture again. It is worth being honest about the limit: the
   contrast standard does not credit shadows, so this makes the text easier to
   read without making the page pass. Darkening the scrim is the only thing
   that would, and that is Sam's call, not a default to quietly restore. */
.svc__title,
.svc__body,
.svc__tags li{ text-shadow:0 1px 3px rgba(19,17,16,0.85); }

.svc__title{
  font-size:clamp(1.75rem,4vw,3rem);
  margin:0;
}
.svc__body{
  font-size:clamp(1.0625rem,1.4vw,1.1875rem);
  line-height:1.6;
  /* Full --fg rather than --muted-fg. Muted text over a scrimmed bright frame
     measures about 2.7:1, which fails. This is the contrast fix, not a style
     preference, so do not revert it to match the other pages. */
  color:var(--fg);
  /* No bottom margin. The gap between body and tags is a grid gap now, and a
     margin here would be added on top of it and break the even spacing. */
  max-width:56ch;margin:0;
}
/* Metadata, not chrome. Boxed, these were sixteen outlined pills stacked down
   one page, and four of them sat in every tile directly under a fifth outlined
   thing, the quote button. That is a lot of borders competing with the only
   part of the page anyone came to look at.
   Unboxed they do the same job for nothing: these are terms a visitor scans,
   not controls they press, and nothing about a rounded outline was telling them
   otherwise. The rule above the row is what replaces the boxes, and it earns
   its place twice, closing the copy column off at the foot so the text block
   has a base instead of trailing away. */
.svc__tags{display:flex;flex-wrap:wrap;gap:10px 24px;list-style:none;padding:0;margin:0;}
.svc__tags li{
  font-family:var(--font-body);font-weight:600;
  text-transform:uppercase;font-size:0.625rem;letter-spacing:0.16em;
  color:var(--muted-fg);
}

/* THE BAND IN BETWEEN. Wide enough that a phone's full bleed would look like a
   mistake, too narrow to stand a paragraph beside a clip.

   The measurement is what settles it. The split holds the clip near 16/9 down
   to about 1200px, and then falls apart quickly: 1.71 at 1280, 1.53 at 1150,
   1.30 at 1024, 1.09 at 920, where cover is discarding 39% of the frame width
   on a page whose entire job is showing the frame. The copy column is running
   out at the same time, 338px at 1024, which is not a column, it is a gutter.

   So this band stacks, for the reason the phone stacks. It differs from a phone
   in exactly the three ways it should:
     the tile keeps its border and its inset instead of bleeding to the screen
     the clip gets a full 16/9, because there is room for one here
     the quote button stays, because the fixed call bar that earns its removal
     is phone only. See the max-width:767px on .callbar.
   Those three are also why this is not written as a shared block with the phone
   rules below and a pile of unsets. Two short blocks that each say what they
   mean beat one block that spends half its lines taking itself back. */
@media(min-width:768px) and (max-width:1399px){
  .svc{
    background:var(--surface-2);
    grid-template-columns:minmax(0,1fr);
    /* No horizontal padding on the tile: the clip runs the full width of it and
       the copy is inset by its own padding instead, which is what lets the
       picture reach the border on three sides. */
    padding:0 0 32px;
    gap:18px;
    /* The clip sets the height now, so the desktop floor would only add empty
       panel under the tags. */
    min-height:0;
    align-content:start;
  }
  .svc__media{
    grid-area:1 / 1;
    position:relative;
    z-index:auto;
    inset:auto;
    aspect-ratio:16 / 9;
    border-left:0;
  }
  /* Laid into the foot of its own clip, the same construction the phone uses:
     a hairline, the name under it, sitting on a bottom up scrim. */
  .svc__title{
    grid-area:1 / 1;
    align-self:end;
    z-index:1;
    border-top:1px solid rgba(217,207,192,0.28);
    padding-top:14px;
    margin:0 28px 20px;
  }
  /* Pixels, not percentages, and that is the whole point of restating it here.
     The phone version of this gradient is written in percentages, which is
     right there because the clip is about 160px tall and 34% of it is 55px of
     shading under a name. The same percentages on this band's clip, which runs
     to 674px at 1280, shade 229px: a quarter of the picture darkened to light
     text that occupies the bottom eighty of it. Anchoring the stops in pixels
     keeps the shaded strip the size of the thing it exists to back, whatever
     the clip grows to. */
  .svc__media::after{
    background:linear-gradient(to top,
      rgba(19,17,16,0.88) 0px,
      rgba(19,17,16,0.62) 60px,
      rgba(19,17,16,0.22) 130px,
      rgba(19,17,16,0) 200px);
  }
  /* Flat, and the only job it has left: the tile that is playing is the bright
     one. Same value the desktop split uses so the two bands dim alike. */
  .svc__media::before{ background:rgba(19,17,16,0.42); }
  .svc__body,
  .svc__cta,
  .svc__tags{ margin-inline:28px; }
  .svc__body{ max-width:64ch; }
}

/* FULL BLEED ON PHONES. The same move the work tiles got, for the same reason
   and with the same 16/9 crop, because it is the same problem.

   The desktop block is text laid over footage, and it works there because the
   copy is held to the left 52% and the right half of the frame stays bright.
   A phone has no right half. The text spanned the whole block, so the scrim had
   to be heavy across the whole frame, and the footage became wallpaper behind a
   paragraph: dark, cropped, and carrying nothing. Worse, the block's height was
   set by how long its paragraph ran, so the four clips were cropped to 0.98,
   0.91, 0.86 and 0.85 of a square, each a different accidental slice of a 16:9
   shot. Nobody chose any of those crops.

   So on a phone the clip stops being a background and becomes the thing itself:
   its own frame, edge to edge, at the ratio it was shot in, with the copy under
   it on the page. The block is a piece of footage with a caption, which is what
   the work grid already is.

   Everything below 900px that used to fight the scrim can go with it. The text
   is on --bg now, so the shadow behind it and the raised body colour are both
   solving a problem this layout does not have, and the contrast note further up
   this file stops applying here: the copy simply passes.

   Desktop is untouched. Every rule in this block is inside the phone query. */
@media(max-width:767px){
  /* The separation is 56px of padding on the block rather than 56px of grid
     gap, and the difference is not cosmetic: it looks identical and behaves
     differently.
     site.js picks the block to play by asking which one covers the middle of
     the window, and a real gap is a band where the answer is "none of them".
     At a 20px gap that was a flicker nobody caught. At 56px, with the clips now
     carrying a visible bright/dim state, it became a stretch of scrolling where
     all four sat dark at once. Paying the space as padding makes the blocks
     touch, so the middle of the window is always inside exactly one of them and
     one clip is always lit. */
  .stack{gap:0;}
  .svc{
    border:0;
    padding:0 0 48px;
    /* The media below breaks out of the shell with a negative margin, which a
       clipping ancestor would cut straight back off. */
    overflow:visible;
    gap:16px;
  }
  /* Out of the background and into the flow, sharing one grid cell with the
     title so the name sits on the picture the way a work tile's caption does.
     position:relative rather than static: the two scrim layers are absolutely
     positioned against it, and left static they would find .svc instead and
     cover the copy as well as the picture. */
  /* Slim, the way the desktop block is slim. Desktop never shows these clips at
     16/9 either: the block is as wide as the shell and only as tall as its
     min-height clamp, which measures 2.05 at the 900px breakpoint, 2.36 at
     1024, 2.95 on a laptop and 3.17 once the shell caps out. So a widescreen
     crop is the house style here, not a departure from it, and 2.4 sits inside
     that range rather than beside it.
     It does not go wider, and the reason is the footage rather than the layout.
     cover crops top and bottom, so 2.4 discards 26% of the frame, 2.75 discards
     35% and 3.17 discards 44%. A 1360px block can lose 44% and still have 461px
     of picture left; 390px of phone cannot. Rendered at 2.75 the car's wheels
     sit on the bottom edge with the name strip lying across its body, and at
     3.17 the wheels are gone altogether, on the tile whose whole claim is
     rolling shots. This is the widest crop where all four shots keep their
     subject. Change this one value if the footage ever changes. */
  .svc__media{
    grid-area:1 / 1;
    position:relative;
    z-index:auto;
    aspect-ratio:2.4 / 1;
    margin-inline:calc(-1 * var(--gutter-l)) calc(-1 * var(--gutter-r));
  }
  /* The service name, laid into the foot of its own clip. Same construction as
     .work__strip: a hairline, 16px under it, sitting on a bottom-up scrim. It
     keeps the display face rather than dropping to the work tile's 11px small
     caps, because on the work page the caption is metadata under a piece that
     speaks for itself and here it is the name of the thing being sold. */
  .svc__title{
    grid-area:1 / 1;
    align-self:end;
    z-index:1;
    font-size:clamp(1.375rem,5.5vw,1.625rem);
    border-top:1px solid rgba(217,207,192,0.28);
    padding-top:14px;
    margin-bottom:16px;
  }
  /* Replaced, not removed. The flat wash was there to make a whole paragraph
     readable across the frame; this is the work tile's gradient, which darkens
     only the foot where the name sits and leaves the rest of the shot alone. */
  .svc__media::after{
    background:linear-gradient(to top,
      rgba(19,17,16,0.86) 0%,
      rgba(19,17,16,0.52) 15%,
      rgba(19,17,16,0.12) 34%,
      rgba(19,17,16,0) 52%);
  }
  /* The light layer stays, doing the one job it did that still matters: the
     block whose clip is running is the bright one and the rest sit back. The
     rule that clears it on the playing block is further up and still applies. */
  .svc__media::before{background:rgba(19,17,16,0.45);}
  /* Nothing to sit on once the border is gone. On a phone the clip that is
     moving is already the signal, so the rim light is not replaced. */
  .svc[data-playing="true"]{border-color:transparent;box-shadow:none;}
  /* A phone already carries a "Get a quote" in the fixed bar at the foot of the
     screen. Four more of the same button, one per tile, plus the closing block,
     was the same link six times on one page. The bar is the one that is always
     reachable whatever the visitor has scrolled to, so it is the one that
     stays and the page is left as the services and nothing else.
     Both of these are phone-only. Desktop has no bar, so it keeps its closing
     block, and the per-tile buttons there are part of how a block is spaced:
     see the note by .svc about the button being layout, not decoration. */
  .svc__cta{display:none;}
  .services-outro{display:none;}
  /* Tighter tracking and a tighter row gap so a four term row has a chance of
     fitting on one line at 375px rather than always taking two. There is no
     padding to trim any more: the pills went, here as everywhere else. */
  .svc__tags{gap:8px 16px;}
  .svc__tags li{letter-spacing:0.1em;}
  /* Body and tags are on the page background now, so the shadow behind them is
     solving a problem this layout does not have. The title still sits on
     footage, so it keeps its own. */
  .svc__body,
  .svc__tags li{text-shadow:none;}
  /* .band--tight is 8px above its heading because the stack used to snap and
     every pixel above the first tile was a pixel the page jumped through on
     load. The snapping is gone (see the note by .stack), so the constraint that
     bought that 8px is gone with it, and on a phone it read as the masthead
     being pinned to the underside of the nav. The desktop value is left where
     it is: it is Sam's call whether that page still wants to open hard on the
     first tile now that nothing is fighting it. */
  .band--tight{padding-block-start:32px;}
}

/* ============================================================
   PROCESS — order carries meaning here
   ============================================================ */
/* DELIBERATELY NOT A .band. The band rhythm is built on adjacency: two bands
   sitting next to each other let .band + .band--rule work out the join between
   them, so neither pays for the gap twice. This is a <section> of its own now,
   with the work section above it and the closing plate below, so that selector
   matches nothing and .band's 136px top and bottom would stack against its
   neighbours into a 270px canyon at both ends. It owns its spacing instead.
   The rule at the top is what .band--rule would have given it. */
.how{
  border-top:1px solid var(--border);
  padding-block:40px 52px;
}
/* SIZED AGAINST THE WINDOW'S HEIGHT, not just its width. This block and the ask
   above it are meant to be readable together without scrolling between them, so
   what they can spend is a share of the screen rather than whatever their own
   content asks for. svh in the middle of the clamp does that: on a tall monitor
   these sit at the ceiling, and on a 768px laptop they come down together
   instead of overflowing.
   The heading is capped by height as well, via min(). At 1366x768 the width
   clamp alone gives it 72px, which is a fifth of everything the pair has. */
@media(min-width:768px){
  .how{padding-block:clamp(28px,3.8svh,66px) clamp(30px,4.2svh,80px);}
}
.how .heading{
  font-size:clamp(1.75rem,min(6vw,8.5svh),4.5rem);
  margin-bottom:clamp(20px,3.2svh,48px);
}
/* Same treatment one level down. The step titles are the other thing on this
   block big enough to matter to the total. */
.step__title{font-size:clamp(1.25rem,min(2.8vw,4.4svh),2rem);}

.steps{
  display:grid;grid-template-columns:1fr;gap:40px;
  /* An <ol> now, because the order is the content. The markers go; the sequence
     is carried by the three titles and by reading order. */
  list-style:none;margin:0;padding:0;
}
@media(min-width:768px){ .steps{grid-template-columns:repeat(3,1fr);gap:clamp(40px,4vw,64px);} }
/* The 01/02/03 labels used to carry this rule on their own bottom border. The
   numbers were noise, but the line was doing real work: it gives the three
   steps a shared top edge so they read as one row rather than three loose
   paragraphs. So the line stays and moves onto the step itself.
   Brighter than --border now that the steps sit on the page instead of inside a
   panel: a hairline that read as a divider against --surface-2 almost vanished
   against --bg. Same bone tint the hero's base rule and the slate use. */
.step{
  border-top:1px solid rgba(217,207,192,0.28);
  padding-top:20px;
}
.step__title{
  /* A heading that never carries .display, so it names the face itself. Same
     for any future heading class: if it is a heading, say --font-display here
     rather than letting it fall through to the page default. */
  font-family:var(--font-display);
  font-weight:800;
  text-transform:uppercase;
  font-size:clamp(1.375rem,2.8vw,2rem);
  letter-spacing:0;margin:0 0 14px;
}
.step__body{font-size:1rem;line-height:1.6;color:var(--muted-fg);margin:0;max-width:34ch;}

/* The three arrive in order as the section comes up the screen, which is the
   one place on this page where a stagger is telling the truth: they are a
   sequence, and they say so by landing as one.
   Driven by the scroll position itself, the same mechanism the work tiles used
   before the reel replaced them, so there is no observer to wire up and nothing
   to leave the steps invisible if a script fails to run. */
@supports (animation-timeline:view()){
  @media(prefers-reduced-motion:no-preference){
    .step{
      animation:step-rise linear both;
      animation-timeline:view();
      animation-range:entry 0% entry 42%;
    }
    /* A short offset per column rather than a delay, which a scroll-driven
       animation has no concept of: the range is what moves. */
    .step:nth-child(2){animation-range:entry 4% entry 48%;}
    .step:nth-child(3){animation-range:entry 8% entry 54%;}
  }
}
@keyframes step-rise{
  from{opacity:0;translate:0 16px;}
  to{opacity:1;translate:none;}
}

/* ============================================================
   THE CLOSE — the page ends the way it opened
   ============================================================ */
/* Full width of the window, so the last thing on the page has the same edge to
   edge confidence as the first. Same construction as .hero: a clip layer, a
   scrim over it, content above that.
   isolation:isolate keeps the stacking inside this section, so the scrim cannot
   end up over the sticky nav or under the footage on a browser that resolves
   z-index differently once a transform enters the page. */
.closer{
  position:relative;
  isolation:isolate;
  overflow:hidden;
  background:var(--muted);
  border-top:1px solid var(--border);
  border-bottom:1px solid var(--border);
  /* Height-aware, for the same reason as .how below it: the two share a screen,
     so they are budgeted against the screen. See the note there. */
  padding-block:clamp(24px,4svh,74px);
  /* Shorter than the hero on purpose. The hero takes a whole screen because it
     is the first thing on the page; this is a reprise, and a second full screen
     of footage would read as the page starting again rather than carrying on.
     It also has to leave room for the block underneath it.

     The second term is the floor that keeps the last stop honest. The scroll
     rests at the foot of the document, so the closing screen is this section
     plus .how plus the footer — about 430px of the two below — and on a tall
     window that came to less than the screen. The remainder showed as a dead
     band of leftover work grid above this section: 93px at a 937px window,
     315px at 1297px, a quarter of the screen on anything taller.

     Written as max() against the clamp rather than a min-height media query
     on purpose. It only bites once there is genuinely space going spare, and
     it grows smoothly from there instead of jumping at a breakpoint when
     somebody resizes. The crossover lands near 820px, above the min-height
     itself, because this section's real height is set by its content until a
     window is tall enough for the floor to overtake it. So a 625, 721, 757 or
     800px window renders exactly as it did before, a 14in MacBook at 839 gains
     10px, and only a genuinely tall screen sees the section grow: +86px at
     937, +348px at 1297. */
  min-height:max(clamp(300px,40svh,540px), calc(100svh - 430px));
  display:flex;
  align-items:center;
}
.closer__media{position:absolute;inset:0;z-index:0;}
.closer__media video{width:100%;height:100%;object-fit:cover;}
/* Heavier at the left, where the type is, and clearing towards the right so the
   shot has somewhere to actually be seen. That split is the hero's, for the same
   reason: a scrim strong enough to read a muted-grey lede through, applied
   evenly, darkens the footage until there is no point having it.
   The vertical pass is what carries the base rule at the foot. */
.closer__scrim{
  position:absolute;inset:0;z-index:1;
  background:
    linear-gradient(to right,
      rgba(19,17,16,0.92) 0%,
      rgba(19,17,16,0.78) 34%,
      rgba(19,17,16,0.34) 62%,
      rgba(19,17,16,0.14) 100%),
    /* The foot has to go dark ACROSS THE WHOLE WIDTH, not just under the type
       column. The base rule puts small caps at the right-hand end, which is the
       one part of the frame the horizontal pass above deliberately leaves clear
       so the footage can be seen. Measured against a white backdrop that corner
       came out at 2.99:1, a fail. Taking the bottom stop from 0.55 to 0.88
       fixes it without touching the middle band, which is where the shot
       actually reads. */
    linear-gradient(to bottom,
      rgba(19,17,16,0.30) 0%,
      rgba(19,17,16,0.08) 38%,
      rgba(19,17,16,0.45) 78%,
      rgba(19,17,16,0.88) 100%);
}
.closer__inner{position:relative;z-index:2;width:100%;}
/* The column the type holds. Past this the footage is on its own. */
.closer__say{max-width:min(56%,720px);}
@media(max-width:899px){
  /* No room for a column and a shot side by side. The type takes the width and
     the scrim goes even, which is what the hero does at this size too. */
  .closer__say{max-width:none;}
  /* Heavier than the desktop pass. The type spans the full width here, so there
     is no clear side of the frame to hide the weak contrast in, and the scrim
     has to hold up under the brightest frame a swapped-in clip might have. */
  .closer__scrim{
    background:linear-gradient(to bottom,
      rgba(19,17,16,0.76) 0%,
      rgba(19,17,16,0.86) 55%,
      rgba(19,17,16,0.94) 100%);
  }
}
/* The largest type on the page after the hero, and deliberately so: the ask is
   the second most important thing this page does. Sits between the hero's 144px
   and the 72px every other section heading takes.
   Smaller than the flat version of this block used, because it now shares the
   frame with a clip instead of owning the whole width. */
.closer__title{
  font-size:clamp(1.875rem,min(4.4vw,7.2svh),4rem);
  margin:0 0 clamp(12px,1.8svh,22px);
  max-width:18ch;
}
/* Full strength, not --muted-fg like every other .lede on the site.
   The muted tone is a mid grey, and it reads fine on the page background where
   the other ledes live. Over footage it does not: against a bright frame behind
   a 0.85 scrim it comes out around 2.4:1, which is a fail, and the clip behind
   this block is meant to be swapped. A treatment that only works for the shot
   currently sitting there is not a treatment. At full strength it clears AA
   against anything, including a sky.
   The hierarchy still reads, because the line below it is half the size of the
   headline and set in the body face rather than Archivo Black. */
.closer__lede{
  max-width:38ch;
  margin-bottom:clamp(18px,2.6svh,36px);
  color:var(--fg);
}
/* Same reasoning, one step less severe: .slate__k is --fg held at 0.72, which
   lands near 4.2:1 over a bright frame. The foot of this block sits in the
   darkest part of the scrim, but not by enough to rely on. */
.closer__base .slate__k{opacity:0.88;}

/* Reuses .slate, the hairline-with-small-caps-at-both-ends the hero closes on
   and the reel opens with. Third time it appears on this page, which is the
   point: it is the site's way of drawing an edge.
   Runs the full width rather than stopping at the type column, so the foot of
   this block reads the same as the foot of the hero. */
.closer__base{margin-top:clamp(20px,3.4svh,56px);margin-bottom:0;}
.closer__lines{display:flex;flex-wrap:wrap;gap:6px 26px;}
.closer__lines a{
  color:var(--fg);
  text-decoration:none;
  transition:color var(--dur);
}
.closer__lines a:hover{color:var(--accent);}
.closer__lines a:focus-visible{outline:2px solid var(--accent);outline-offset:3px;}
/* The phone-width rule that drops the second half of a .slate is already global,
   and here it drops "Central Pennsylvania" and keeps the direct lines, which is
   the right half to lose: the footer says the same thing two rows further down. */

/* ============================================================
   ABOUT
   ============================================================ */
.facts{
  display:grid;grid-template-columns:1fr;
  border-top:1px solid var(--border);
}
@media(min-width:640px){ .facts{grid-template-columns:repeat(2,1fr);} }
@media(min-width:1024px){ .facts{grid-template-columns:repeat(4,1fr);} }
.fact{
  padding:24px 24px 24px 0;
  border-bottom:1px solid var(--border);
}
@media(min-width:640px){
  .fact{border-right:1px solid var(--border);padding-left:24px;}
  .fact:first-child{padding-left:0;}
}
.fact__k{
  display:block;font-family:var(--font-body);font-weight:600;
  text-transform:uppercase;font-size:0.625rem;letter-spacing:0.2em;
  color:var(--muted-fg);margin-bottom:10px;
}
.fact__v{
  display:block;text-transform:uppercase;
  font-size:clamp(1rem,1.8vw,1.25rem);font-weight:600;letter-spacing:0;line-height:1.1;
}

/* ============================================================
   CONTACT
   ============================================================ */
/* No footage on this page, by decision. The reasoning is in the comment at the
   head of the contact section in contact.html and is worth reading before
   adding any: two versions carried a clip here and both read as a picture in
   empty space, because a contact page has no claim for one to prove.

   That leaves type, the form and the direct lines. Nothing here is sized to
   fill space, which is also why the band cannot open a hole the way it did with
   a 468px panel standing beside a 228px heading. */

/* MEASURE ---------------------------------------------------
   This page holds a headline, a lede, four fields and three phone numbers. That
   is not enough to fill the 1520px the shell gives it at Sam's 1707 viewport,
   and both attempts to make it fill failed in opposite directions: stretched to
   full width everything went thin and the details box became a huge empty
   rectangle; kept in a column with a rail beside it, the rail ran short and
   left a hole under itself. A side column next to a form is always the shorter
   of the two, so that hole is structural, not a spacing mistake.

   So the measure comes down instead and the leftover space becomes margin,
   which reads as intended rather than as a gap. 1120 is not arbitrary: less the
   80px of shell padding it is 1040, which is exactly where the headline lands
   at its 15ch cap, so the type sets the width of the page.
   The nav and footer keep the full shell on purpose. Insetting the content
   against them is the thing that makes it read as a deliberate column. */
.shell--narrow{max-width:1120px;}

/* CONTACT LAYOUT ------------------------------------------- */
/* Rail on the left, form on the right, the whole page inside one screen. See
   the long note in contact.html for why a rail works here when an earlier one
   did not.
   Below 900 this is a plain stack. .contact-rail goes display:contents so its
   two blocks become grid children in their own right, which is what lets the
   form sit between them: heading, form, direct lines. */
.contact-layout{
  display:grid;
  gap:36px;
  padding-block:36px;
}
.contact-head{order:1;}
.contact-main{order:2;}
.contact-direct{order:3;}
@media(max-width:899px){ .contact-rail{display:contents;} }

@media(min-width:900px){
  .contact-layout{
    /* The form is the reason for the page, so it takes the wider column. */
    grid-template-columns:minmax(0,0.8fr) minmax(0,1fr);
    column-gap:clamp(48px,5vw,88px);
    /* Fills what the nav and the footer leave, so the block sits in the middle
       of the screen rather than hanging off the nav with the footer adrift
       below it. Content taller than this just grows and the page scrolls,
       which is what a short laptop screen gets. */
    min-height:calc(100svh - var(--nav-h) - var(--footer-h));
    align-content:center;
    padding-block:48px;
  }
  /* Stretched to the form's height by the grid's default align-items, which is
     what lets the direct lines sit at its foot. */
  .contact-rail{display:flex;flex-direction:column;gap:48px;}
  /* Pushes the lines to the bottom of the rail, so the column is anchored at
     both ends against the panel beside it instead of stopping halfway down. */
  .contact-direct{margin-top:auto;}
}

.contact-head__title{
  font-size:clamp(3rem,5.5vw,5rem);
  margin-bottom:18px;
}
.contact-head__lede{max-width:24ch;}

.contact-main__form .form{max-width:none;gap:20px;}
.contact-main__form .form__row{gap:20px;}
.contact-main__form .form__send{gap:10px;}
/* The site-wide field size is set for a form running the full width of the
   measure. In a column beside a rail the same type overruns a half-width
   field, so it is capped here. It also buys back the height this page needs. */
.contact-main__form .field__input,
.contact-main__form .field__area{font-size:clamp(1.0625rem,1.5vw,1.25rem);}
.contact-main__form .field__label{margin-bottom:6px;}
.contact-main__form .field__input{height:52px;}
/* Deep enough to still ask for more than "need a video", shallow enough to
   keep the page in one screen. resize:vertical is untouched, so anyone with
   more to say can drag it open. */
.contact-main__form .field__area{min-height:104px;}
/* The site-wide .btn--lg is sized for a button standing alone with room to
   spare. This one is the last thing in a form that is trying to be short. */
.contact-main__form .btn--lg{height:52px;}
/* Trimmed from the shared .panel padding (used as-is on the homepage's "How a
   job runs" block) so the form's own fields, not the frame around them, set
   this page's height. */
.contact-main__form.panel{padding:24px 24px;}
@media(min-width:900px){ .contact-main__form.panel{padding:32px 36px;} }

.form{display:grid;gap:var(--gap-field);max-width:820px;}
/* Name and email are short values. Given a rule each at the form's full width
   they read as an empty ladder, so they share a row once there is room. */
.form__row{display:grid;gap:var(--gap-field);}
@media(min-width:768px){ .form__row{grid-template-columns:1fr 1fr;} }

.field{display:block;}
.field__label{
  display:block;font-family:var(--font-body);font-weight:600;
  text-transform:uppercase;font-size:0.625rem;letter-spacing:0.2em;
  color:var(--muted-fg);margin-bottom:10px;
}
/* No text-transform here on purpose. It only ever changed how the field looked;
   the value posted to Formspree was always whatever the visitor actually typed,
   so an inbox full of normal sentences never matched the shouting on screen.
   The labels above the fields keep their caps, since those are labels. */
.field__input,.field__area{
  width:100%;background:transparent;color:var(--fg);
  border:0;border-bottom:1px solid var(--border);border-radius:0;
  /* What a visitor types, so it follows the UI face rather than the headings.
     A display face is cut for a word at 100px, not for an email address being
     read back at 20px to check it was entered correctly. */
  font-family:var(--font-ui);
  letter-spacing:-0.02em;
  font-size:clamp(1.25rem,3vw,1.75rem);
  padding:14px 0;
}
.field__input{height:66px;}
/* Taller than the inputs by more than its content strictly needs. This is the
   field the actual job goes in, and a wide shallow box tells people to answer
   in one line; the extra depth is the only thing on the page asking them to
   say more than "need a video". The contact page cuts this back for its own
   copy of the form, which has one screen to fit into. */
.field__area{min-height:180px;resize:vertical;line-height:1.3;}
/* --faint sat these at 1.94:1 against the page, measured in Chromium. These are
   set in 28px display type, so WCAG AA wants 3:1 as large text and 4.5:1 to be
   safe at any size the clamp lands on. --placeholder clears 4.5:1 while staying
   well under typed text at 15.7:1, so the field still reads as empty. */
/* The colour is only half of it, and the half that was already right. The other
   half was the face: a placeholder set in Archivo Black at 28px with the
   display tracking is typeset exactly like a typed answer, so "Real estate /
   auto / event" read as a value somebody had already entered rather than as a
   hint about what to enter. Contrast alone cannot fix that, because the reader
   is going by weight and shape before they judge brightness.
   So the hint drops to the body face at a normal weight and loses the negative
   tracking. It stays --placeholder, so every contrast number in the note above
   still holds, and it now looks like an empty field with a suggestion in it. */
.field__input::placeholder,.field__area::placeholder{
  color:var(--placeholder);
  font-family:var(--font-body);
  font-weight:400;
  letter-spacing:0;
}
/* Pointer focus gets the quiet version: the rule under the field turns accent.
   Keyboard focus has to be findable from across the page, so it also thickens
   that rule to 2px and washes the field, which gives the indicator real area
   rather than a one pixel change of colour. The extra pixel is drawn with an
   inset shadow instead of a wider border so the field does not shift by 1px
   when it takes focus. The default outline is replaced here, not discarded. */
.field__input:focus,.field__area:focus{outline:none;border-bottom-color:var(--accent);}
.field__input:focus-visible,.field__area:focus-visible{
  outline:none;
  border-bottom-color:var(--accent);
  box-shadow:inset 0 -2px 0 var(--accent);
  background:var(--muted);
}

/* The button used to sit in a bare div with nothing beside it, on the one
   control on the site where a visitor stops and weighs it up. The note carries
   that moment; the grid gap replaces the margins the two paragraphs used to
   set for themselves, so an empty status line costs nothing. */
.form__send{display:grid;gap:16px;justify-items:start;}
.form__note{
  margin:0;
  font-size:0.9375rem;line-height:1.5;
  color:var(--muted-fg);
  max-width:38ch;
}
/* THE ERROR STATE, which the form did not really have.

   What it had was one line under the send button carrying every message, set at
   13px in the accent bone, sitting directly beneath a 15px helper note in
   --muted-fg. Stacked like that, "Add your name, email, and a few details" read
   as a second helpful aside rather than as the reason nothing had been sent. It
   also could not say which of four boxes was the problem, and it appeared below
   the button, which is past the thing the visitor was looking at.

   So the message moves to the field it is about. The status line stays for the
   things that genuinely belong to the whole form: sending, network failure, and
   the mailto: fallback. */
.field__error{
  margin:8px 0 0;
  font-family:var(--font-body);
  font-size:0.8125rem;line-height:1.4;
  color:var(--warn);
}
/* The rule under the field carries the state too. On its own the message would
   be the only marker, and a visitor scanning a four field form for what to fix
   is looking at the boxes, not reading underneath them. */
.field__input[aria-invalid="true"],
.field__area[aria-invalid="true"]{ border-bottom-color:var(--warn); }
/* Focus still wins. A field that has just been corrected should look focused
   rather than still broken, and the invalid flag is cleared on input anyway. */
.field__input[aria-invalid="true"]:focus,
.field__area[aria-invalid="true"]:focus{ border-bottom-color:var(--accent); }

.form__status{
  font-size:0.8125rem;letter-spacing:0.02em;color:var(--accent);margin:0;
}
/* Failures that are not about one field still have to look unlike the note they
   sit under, or they read as more advice. */
.form__status[data-state="error"]{ color:var(--warn); }
.form__status:empty{display:none;}

/* Pulled off screen rather than hidden, for the reason spelled out beside the
   input in contact.html. The form is a grid, so a field left in normal flow
   would open a 36px gap even at zero height; absolute takes it out of flow and
   the gap closes. */
.form__gotcha{position:absolute;left:-9999px;width:1px;height:1px;opacity:0;}

/* The sent state. The form leaves and the sent panel takes its place in the
   same column, framed the same way, so the result lands exactly where the
   question was and the rail beside it does not move.
   The masthead used to leave too. It was a question then; it is a one-word
   page label now, so only the line under it goes. The direct lines are
   deliberately left standing: somebody who has just written in may still want
   to call, and hiding them would leave the message sitting alone on screen. */
.contact[data-sent="true"] .contact-head__lede,
.contact[data-sent="true"] .contact-main__form{display:none;}
/* Rests 12px low and transparent, and site.js flips data-in once the panel has
   been laid out. `translate` rather than `transform` for the same reason the
   work tiles use it: it composes instead of overwriting. The reduced motion
   block at the end of this file already zeroes every transition duration, so
   this arrives instantly for anyone who asked for less. */
.sent{
  opacity:0;translate:0 12px;
  transition:opacity 400ms cubic-bezier(0.16,1,0.3,1),
             translate 400ms cubic-bezier(0.16,1,0.3,1);
}
.sent[data-in="true"]{opacity:1;translate:none;}
/* Sized to the panel it now sits in rather than to the masthead. This replaces
   the form, not the heading, so matching the h1's clamp would put a 104px word
   inside a framed box built for 20px fields. */
.sent__heading{font-size:clamp(2rem,3.4vw,2.75rem);max-width:15ch;margin-bottom:14px;}
/* The heading takes focus on send, which is what actually announces the change.
   It is not a control, so it gets no focus ring of its own. */
.sent__heading:focus{outline:none;}

/* DIRECT LINES ---------------------------------------------- */
/* Three across the column, same rhythm as the form row above. At 1040 that is
   346 a column, which the 24 character email address clears. */
.lines{
  display:grid;gap:0;grid-template-columns:1fr;
  border-top:1px solid var(--border);
}
@media(min-width:640px){ .lines{grid-template-columns:repeat(3,1fr);} }
.line{
  padding:24px 24px 24px 0;
  border-bottom:1px solid var(--border);
}
@media(min-width:640px){
  .line{border-right:1px solid var(--border);padding-left:24px;}
  .line:first-child{padding-left:0;}
}
.line__k{
  display:block;font-family:var(--font-body);font-weight:600;
  text-transform:uppercase;font-size:0.625rem;letter-spacing:0.2em;
  color:var(--muted-fg);margin-bottom:10px;
}
.line__v{
  display:block;text-transform:uppercase;
  font-size:clamp(0.9375rem,1.6vw,1.125rem);font-weight:600;letter-spacing:0;
  word-break:break-word;transition:color var(--dur);
}
a.line__v:hover{color:var(--accent);}

/* In the rail they stack instead of running three across: the column is half
   the width the three-across rule was measured against, and the 24 character
   email address does not fit a third of it. The vertical rules go with them,
   since a single-column stack has nothing to sit between. */
@media(min-width:900px){
  .contact-direct .lines{grid-template-columns:1fr;}
  .contact-direct .line{border-right:0;padding:14px 0;}
}

/* The About page borrows this block for its three hard facts. It needs nothing
   but room above it: on the contact page the grid follows an eyebrow that
   already sets its own gap, and here it follows a paragraph that does not.
   The values below are the .line paddings, so the grid keeps the contact
   page's rhythm rather than inventing a second one. */
.about__facts{margin-top:44px;}

/* ============================================================
   CONTACT BAR — phones only, and only once the hero has gone by
   ============================================================ */
/* The site sells to people who are standing somewhere with a phone in their
   hand, and for most of them the shortest path to hiring Sam is a phone call.
   That call used to be four screens down the contact page.
   On screen from the first paint, including over the hero. It was briefly held
   back until the hero had scrolled away, on the reasoning that it would compete
   with the hero's own button. The better answer was to take that button out on
   phones and let this be the CTA, which is what happened: with nothing to
   compete with there is nothing to wait for, and the hero got its middle back.
   The reveal machinery went with it, so this is now plain CSS and the script
   knows nothing about it.
   Not on the contact page, where the form it points at is already the page, and
   not on 404. Both simply leave the markup out. */
.callbar{
  position:fixed;
  left:0;right:0;bottom:0;
  z-index:75;
  display:none;
  align-items:stretch;
  /* The gap is the divider. One pixel of the bar's own background showing
     between two filled halves, rather than a border on one of them. */
  gap:1px;
  background:var(--border);
  border-top:1px solid var(--border);
  height:calc(var(--callbar-h) + env(safe-area-inset-bottom,0px));
  padding-bottom:env(safe-area-inset-bottom,0px);
}
@media(max-width:767px){ .callbar{display:flex;} }
.callbar__call,
.callbar__quote{
  flex:1;
  display:inline-flex;align-items:center;justify-content:center;
  font-family:var(--font-ui);
  font-weight:600;
  text-transform:uppercase;letter-spacing:0.02em;
  font-size:0.9375rem;
}
/* Bone on near-black and near-black on bone. Both are the palette's existing
   pairs, so both clear AA by a wide margin without a new colour being invented
   for the bar. */
.callbar__call{background:var(--bg);color:var(--fg);}
.callbar__quote{background:var(--accent);color:var(--accent-fg);}

/* ============================================================
   FOOTER
   ============================================================ */
.footer{border-top:1px solid var(--border);}
/* Reserve the bar's height so the last line of the page is not sitting under
   it. Keyed off the bar actually being in the document, so the contact page and
   404, which leave it out, do not reserve space for something they never show.
   Both tokens are zero above the bar's breakpoint, so this is inert on
   desktop. */
@media(max-width:767px){
  body:has(.callbar) .footer{padding-bottom:var(--callbar-space);}
}
.footer__grid{
  display:flex;flex-wrap:wrap;gap:20px;justify-content:space-between;
  /* No border of its own. It used to sit under a large wordmark and needed a
     line to separate the two; with the wordmark gone that line landed directly
     under the footer's own top border and read as one doubled rule. */
  padding-block:40px 48px;
  font-family:var(--font-body);font-weight:600;
  text-transform:uppercase;font-size:0.6875rem;letter-spacing:0.16em;
  color:var(--muted-fg);
}
.footer__nav{display:flex;flex-wrap:wrap;gap:24px;}
/* The label is set at 11px, which left the clickable box 12px tall: half the
   24px minimum a pointer target is meant to have, and these are thumb targets
   on a phone. The padding grows the box and the matching negative margin takes
   the growth back out of the layout, so the footer still measures and looks
   exactly as it did; only the area that responds to a tap changes. */
.footer__nav a{
  display:inline-flex;align-items:center;
  min-height:24px;
  padding-block:6px;
  margin-block:-6px;
}
.footer__nav a:hover{color:var(--accent);}

/* ============================================================
   REDUCED MOTION
   ============================================================ */
@media(prefers-reduced-motion:reduce){
  html{scroll-behavior:auto;}
  /* Delay is zeroed as well as duration. The lightbox holds visibility until
     its fade finishes, and a delay left standing would keep the closed overlay
     focusable for a quarter second after it went transparent. */
  *,*::before,*::after{transition-duration:1ms !important;transition-delay:0s !important;}
  .hero__media{transform:none !important;}
  .work:hover,
  .work:focus-within{transform:none !important;}
  /* The footage inside the frame grows on hover as well. Transitions are cut
     to 1ms above, so leaving this one standing would not be a slower zoom, it
     would be an instant jump to 105% the moment the pointer arrives. */
  .work__frame video,
  .work__frame img{transform:none !important;}
  /* The service blocks used to rest at 96% and grow on the one that was
     playing, and this rule was here to stop them all sitting shrunk forever
     under an attribute the script never sets. The resting scale is gone now, so
     there is nothing left to undo and the rule went with it. The two below are
     the ones that still have work to do. */
  /* The push-in on the playing clip, for the same reason as .work__frame video
     above. The attribute that drives it is never set under reduced motion, so
     this is belt and braces rather than a live bug, but the two rules should
     not disagree about whether footage is allowed to move. */
  .svc__media video{transform:none !important;}
  /* And the tile itself, which rests at 97% and opens to full size on the one
     that is playing. The attribute that drives it is never set here, so without
     this every tile on the page would sit permanently shrunk with nothing ever
     growing to explain why, and none of them would line up with the heading
     above or the closing block below. */
  .svc{transform:none !important;}
  /* The dimming layer only means something when there is a lit tile to be
     dimmer than, and here there never is: the script does not set the playing
     attribute at all under reduced motion, so without this every clip on the
     page would sit permanently at the resting alpha with nothing to contrast
     against. That is not a signal, it is just a darker page for the people who
     asked for less movement, not less light. So the dim comes off and all four
     clips are shown at full brightness. */
  .svc__media::before{opacity:0 !important;}
  /* The services stack no longer snaps at all, so there is nothing to switch
     off here. If it is ever put back, docs\tile-snapping.md says what this
     block needs, because mandatory snapping repositions the page after someone
     has stopped scrolling and that is exactly what this query exists to stop. */
}
