﻿/* =========================================================================
   dialed DIGITAL: Newcastle NSW
   -------------------------------------------------------------------------
   Ground is roasted-dark rather than neutral black, so the warm neutrals in
   the portfolio work sit in the same family as the site itself.
   The reticle is the signature device, taken straight from the logo mark.

   PLACEHOLDERS TO REPLACE: search for "TODO" in this project:
     assets/js/site.js   â†’ LINKS constant (Instagram, Facebook, form URL)
     work.html           â†’ live URLs for Self / Chloe-Grace
     index.html          â†’ testimonial quotes + DM screenshots
   ========================================================================= */

:root{
  --void:#141010;          /* page ground, warm near-black */
  --ink:#1C1716;           /* raised surfaces, cards */
  --ink-2:#272120;         /* hover surfaces */
  --paper:#EFE9E1;         /* light sections */
  --paper-2:#E4DCD1;

  --bone:#F2EDE7;
  --bone-dim:rgba(242,237,231,.66);
  --bone-faint:rgba(242,237,231,.38);
  --oat:#C9BCB0;

  --signal:#E0A029;        /* the accent. one colour, used sparingly */
  --signal-deep:#B87D18;

  --rule-dark:rgba(242,237,231,.14);
  --rule-light:rgba(20,16,16,.16);

  --display:"Archivo","Helvetica Neue",Helvetica,Arial,sans-serif;
  --body:"Instrument Sans",-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
  --mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;

  --gut:clamp(1.25rem,5vw,4.5rem);
  --max:1280px;
  --rhythm:clamp(4.5rem,11vw,9rem);
  --ease:cubic-bezier(.22,.61,.36,1);
}

*,*::before,*::after{box-sizing:border-box}

html{
  background:var(--void);
  color:var(--bone);
  font-family:var(--body);
  -webkit-text-size-adjust:100%;
  scroll-behavior:smooth;
  scroll-padding-top:5.5rem;
}
body{
  margin:0;
  font-size:clamp(1rem,.97rem + .16vw,1.0625rem);
  line-height:1.72;
  overflow-x:hidden;
  -webkit-font-smoothing:antialiased;
}
img{
  max-width:100%;display:block;height:auto;
  /* Deterrents only. Stops drag to desktop and the iOS long press save
     sheet. Anyone who opens devtools or the network tab still has the file:
     nothing served to a browser can actually be withheld from it. */
  -webkit-user-drag:none;user-drag:none;
  -webkit-touch-callout:none;
  user-select:none;-webkit-user-select:none;
}
a{color:inherit;text-decoration:none}
::selection{background:var(--signal);color:var(--void)}
:focus-visible{outline:2px solid var(--signal);outline-offset:3px}

/* ---------- type ---------- */

h1,h2,h3{
  font-family:var(--display);
  font-weight:600;
  line-height:1.02;
  letter-spacing:-.032em;
  margin:0;
}
h1{font-size:clamp(2.6rem,1.5rem + 5.4vw,6rem)}
h2{font-size:clamp(1.95rem,1.25rem + 3.1vw,3.9rem)}
h3{font-size:clamp(1.15rem,1.05rem + .5vw,1.5rem);letter-spacing:-.018em}

p{margin:0 0 1.15em;max-width:62ch}
.lede{font-size:clamp(1.06rem,1rem + .45vw,1.3rem);line-height:1.6;color:var(--bone-dim);max-width:54ch}
.measure{max-width:58ch}

/* Mono utility face carries every label, figure and piece of metadata.
   It is the "instrument readout" voice of the brand. */
.eyebrow{
  font-family:var(--mono);
  font-size:.7rem;
  font-weight:500;
  letter-spacing:.2em;
  text-transform:uppercase;
  color:var(--signal);
  margin:0 0 1.25rem;
  display:flex;
  align-items:center;
  gap:.7rem;
}
.eyebrow::before{
  content:"";
  width:9px;height:9px;
  flex:none;
  background:
    linear-gradient(var(--signal),var(--signal)) center/100% 1px no-repeat,
    linear-gradient(var(--signal),var(--signal)) center/1px 100% no-repeat;
  transform:scale(0);
  transition:transform .5s var(--ease) .1s;
}
.is-in .eyebrow::before,.eyebrow.is-in::before{transform:scale(1)}

.wrap{max-width:var(--max);margin:0 auto;padding:0 var(--gut)}
.section{padding:var(--rhythm) 0}
.section--tight{padding:calc(var(--rhythm) * .62) 0}

/* Two dark sections in a row double their padding at the seam. Collapse it,
   so the rhythm only reads as a full gap where the background changes.

   Both halves have to be dark. The old pair of rules tested only the second
   half and tried to restore the padding with a following .band-light + rule,
   but that rule was a class less specific and always lost, so every dark
   section after a light band lost its top padding and sat flush against the
   divider. One selector, stating the actual condition, cannot drift. */
.section:not(.band-light) + .section:not(.band-light){padding-top:0}

.skip-link{position:absolute;left:-9999px;top:0;z-index:300;background:var(--signal);color:var(--void);padding:.85rem 1.3rem;font-family:var(--mono);font-size:.75rem;letter-spacing:.12em;text-transform:uppercase}
.skip-link:focus{left:0}

/* ---------- light band ---------- */

.band-light{background:var(--paper);color:var(--void)}
.band-light .lede{color:rgba(20,16,16,.7)}
.band-light .eyebrow{color:var(--signal-deep)}
.band-light .eyebrow::before{background:
  linear-gradient(var(--signal-deep),var(--signal-deep)) center/100% 1px no-repeat,
  linear-gradient(var(--signal-deep),var(--signal-deep)) center/1px 100% no-repeat}

/* ---------- buttons ---------- */

.btn{
  display:inline-flex;align-items:center;gap:.6rem;
  font-family:var(--mono);
  font-size:.775rem;font-weight:500;
  letter-spacing:.13em;text-transform:uppercase;
  padding:.95rem 1.6rem;
  border:1px solid var(--signal);
  background:var(--signal);color:var(--void);
  position:relative;
  transition:background .28s var(--ease),color .28s var(--ease),transform .28s var(--ease);
}
.btn:hover{background:var(--signal-deep);border-color:var(--signal-deep);transform:translateY(-2px)}
.btn--ghost{background:transparent;color:var(--bone);border-color:var(--rule-dark)}
.btn--ghost:hover{background:transparent;border-color:var(--signal);color:var(--signal);transform:translateY(-2px)}
.band-light .btn--ghost{color:var(--void);border-color:var(--rule-light)}
.band-light .btn--ghost:hover{color:var(--signal-deep);border-color:var(--signal-deep)}
.btn--sm{padding:.68rem 1.15rem;font-size:.7rem}

.textlink{
  font-family:var(--mono);font-size:.78rem;letter-spacing:.11em;text-transform:uppercase;
  border-bottom:1px solid var(--rule-dark);padding-bottom:.3rem;
  transition:border-color .25s var(--ease),color .25s var(--ease);
}
.textlink:hover{color:var(--signal);border-color:var(--signal)}
.band-light .textlink{border-color:var(--rule-light)}

/* ---------- masthead ---------- */

.masthead{
  position:sticky;top:0;z-index:100;
  background:rgba(20,16,16,.82);
  backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
  border-bottom:1px solid transparent;
  transition:border-color .3s var(--ease),background .3s var(--ease);
}
.masthead.is-scrolled{border-bottom-color:var(--rule-dark)}
/* Three zones: links left, wordmark centred, CTA right. Both outer columns
   are equal fractions, so the wordmark sits on the page centre rather than
   drifting with the width of the links beside it. */
.masthead__inner{
  display:grid;
  grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);
  align-items:center;gap:1.5rem;min-height:4.9rem;
}
/* Icon and CTA share the right hand column. */
.masthead__right{display:flex;align-items:center;gap:clamp(.7rem,1.6vw,1.1rem);justify-self:end}

/* Instagram mark, drawn rather than fetched, so it costs no request and
   inherits the text colour. Stroked to match the hairline language of the
   rest of the site instead of arriving as a filled block. */
.iglink{
  display:inline-flex;align-items:center;justify-content:center;
  color:var(--bone-dim);flex:none;
  transition:color .25s var(--ease),transform .25s var(--ease);
}
.iglink svg{width:22px;height:22px;fill:none;stroke:currentColor;stroke-width:1.6}
.iglink svg .ig-dot{fill:currentColor;stroke:none}
.iglink:hover{color:var(--signal);transform:translateY(-1px)}
.band-light .iglink{color:rgba(20,16,16,.55)}
.band-light .iglink:hover{color:var(--signal-deep)}

.foot__social{margin:1.1rem 0 0}
.foot__social .iglink svg{width:24px;height:24px}
.brand{flex:none;display:block;transition:opacity .35s var(--ease),transform .35s var(--ease)}

/* On the homepage the hero opens with the wordmark, so the masthead holds its
   own back until that has scrolled away. Two of the same logo on screen at
   once was the thing that read as cluttered. The .js guard means a browser
   with scripting off simply shows it the whole time. */
.js .masthead .brand{opacity:0;visibility:hidden;transform:translateY(-6px)}
.js .masthead.is-past-hero .brand{opacity:1;visibility:visible;transform:none}
.brand img{width:clamp(168px,19vw,224px)}

.nav{display:flex;align-items:center;gap:clamp(1.1rem,2.4vw,2.1rem)}
.nav a:not(.btn){
  font-family:var(--mono);font-size:.76rem;letter-spacing:.13em;text-transform:uppercase;
  color:var(--bone-dim);position:relative;padding:.3rem 0;
  transition:color .25s var(--ease);
}
.nav a:not(.btn):hover,.nav a[aria-current="page"]{color:var(--bone)}
.nav a:not(.btn)::after{
  content:"";position:absolute;left:0;right:0;bottom:-2px;height:1px;
  background:var(--signal);transform:scaleX(0);transform-origin:left;
  transition:transform .3s var(--ease);
}
.nav a:not(.btn):hover::after,.nav a[aria-current="page"]::after{transform:scaleX(1)}

.nav-toggle{
  display:none;margin-left:auto;
  font-family:var(--mono);font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;
  background:none;border:1px solid var(--rule-dark);color:var(--bone);
  padding:.6rem 1rem;cursor:pointer;
}

/* ---------- wider than the measure ----------
   Two places run wider than the 1280px the rest of the page uses. Text
   sections deliberately do not: body copy needs 55 to 75 characters a line to
   stay readable, so the thesis, services, process and testimonials keep the
   contained measure. The hero was tried this way too and reverted: above
   about 1600px a void opens between the headline and the crosshair. */

/* The ticker. Label keeps its gutter, the rail runs off the right edge so
   logos leave the screen rather than stopping short of it. */
.clients .wrap{max-width:none}
.clients .marquee{margin-right:calc(var(--gut) * -1)}

/* The work grid. Wider rather than full bleed: these screenshots are the
   strongest thing on the page and 551px each was selling them short. */
#work > .wrap{max-width:1440px}

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

/* Top padding is lighter than it was: the wordmark now opens the hero, so the
   old 8.5rem of air above it pushed the meta strip well below the fold. */
.hero{position:relative;padding:clamp(2.75rem,6vw,5rem) 0 clamp(3.5rem,8vw,6rem);overflow:hidden}

/* Ambient grid, very faint. Reads as instrument backing, not decoration. */
.hero::before{
  content:"";position:absolute;inset:0;pointer-events:none;
  background-image:
    linear-gradient(var(--rule-dark) 1px,transparent 1px),
    linear-gradient(90deg,var(--rule-dark) 1px,transparent 1px);
  background-size:clamp(48px,7vw,84px) clamp(48px,7vw,84px);
  opacity:.4;
  mask-image:radial-gradient(ellipse 78% 72% at 50% 42%,#000 20%,transparent 78%);
  -webkit-mask-image:radial-gradient(ellipse 78% 72% at 50% 42%,#000 20%,transparent 78%);
}
.hero__inner{position:relative;z-index:2}

/* Two columns from 900px up: copy left, crosshair mark right. The meta strip
   spans both. Below that the mark drops out entirely rather than stacking -
   a shrunken crosshair above the headline reads as clip art. */
.hero__copy{min-width:0}
.hero__mark{display:none}
@media (min-width:900px){
  .hero__inner{
    display:grid;
    grid-template-columns:minmax(0,1fr) clamp(200px,20vw,300px);
    column-gap:clamp(2rem,4vw,3.5rem);
    align-items:start;
  }
  .hero__copy{grid-column:1;grid-row:1}
  .hero__meta{grid-column:1/-1}
  .hero__mark{
    display:block;grid-column:2;grid-row:1;position:relative;
    /* Pulled up so it sits against the wordmark rather than floating level
       with the headline. */
    margin-top:-1.5rem;
    opacity:.16;
    transition:opacity .4s var(--ease);
    animation:mark-drift 14s var(--ease) infinite;
  }
  .hero__mark img{width:100%;height:auto}

  /* The amber state. The PNG is bone, so the lit layer is the same artwork
     used as a mask over the accent colour rather than a filter Ã¢â‚¬â€ that gives
     the true --signal value, not an approximation of it. Where masks aren't
     supported the layer simply never paints and the bone mark stays. */
  .hero__mark::after{
    content:"";position:absolute;inset:0;
    background:var(--signal);
    -webkit-mask:url("../img/mark-bone.png") center/contain no-repeat;
    mask:url("../img/mark-bone.png") center/contain no-repeat;
    opacity:0;transition:opacity .35s var(--ease);
  }
  .hero__mark.is-lit{opacity:1;animation-play-state:paused}
  .hero__mark.is-lit::after{opacity:1}
}
@media (prefers-reduced-motion:reduce){
  .hero__mark{animation:none}
}
@keyframes mark-drift{
  0%,100%{transform:translateY(-.6rem)}
  50%{transform:translateY(.6rem)}
}

/* Amber accent word. Used sparingly, on one word in a headline, to land the
   point the sentence turns on. */
.mark{color:var(--signal)}

/* Wider than the 15ch this used to be. With the mark taking a column on the
   right, 15ch stacked the headline into a narrow tower against the left edge.
   Set below the global h1 scale too: the wordmark above it is now the loudest
   thing in the hero, so the sentence supports it rather than competing. */
/* The hero wordmark. The opening statement, so deliberately larger than the
   masthead one it stands in for. */
.hero__wordmark{width:clamp(250px,40vw,520px);margin:1.3rem 0 1.5rem}

.hero h1{max-width:26ch;font-size:clamp(1.7rem,1.15rem + 2.2vw,3.1rem)}
.hero h1 .mark{color:var(--signal);position:relative;white-space:nowrap}
.hero .lede{margin-top:1.7rem}
.hero__actions{display:flex;flex-wrap:wrap;gap:.85rem;margin-top:2.4rem}
.hero__meta{
  margin-top:3.2rem;padding-top:1.5rem;border-top:1px solid var(--rule-dark);
  display:flex;flex-wrap:wrap;gap:2.4rem;
  font-family:var(--mono);font-size:.73rem;letter-spacing:.1em;text-transform:uppercase;
  color:var(--bone-faint);
}
.hero__meta b{display:block;font-family:var(--display);color:var(--bone);font-weight:600;font-size:1.5rem;letter-spacing:-.025em;text-transform:none;margin-bottom:.25rem}

/* The meta strip is written for the dark ground. On a paper band the bone
   text disappeared entirely, so the three colours all have to flip. */
.band-light .hero__meta{border-top-color:var(--rule-light);color:rgba(20,16,16,.55)}
.band-light .hero__meta b{color:var(--void)}

/* The reticle. Follows the pointer with lag across the hero, and locks on
   when it passes over anything marked [data-lock]. Pointer devices only. */
/* Sits above the copy: it locks onto the headline, so it has to be able to
   draw over it. Hairline strokes, so the text stays readable underneath. */
.reticle{
  display:block;   /* the inline critical CSS hides this until the sheet lands */
  position:absolute;top:0;left:0;z-index:3;
  width:132px;height:132px;margin:-66px 0 0 -66px;
  pointer-events:none;opacity:0;
  transition:opacity .5s var(--ease);
  will-change:transform;
}
.reticle.is-live{opacity:1}
.reticle circle{fill:none;stroke:var(--signal);stroke-width:1;opacity:.5}
.reticle line{stroke:var(--signal);stroke-width:1;opacity:.5}
.reticle .dot{fill:var(--signal);opacity:0;transition:opacity .3s var(--ease)}
.reticle .ring{transition:transform .45s var(--ease),opacity .3s var(--ease);transform-origin:center}
.reticle circle,.reticle line,.reticle .dot{transition:stroke .3s var(--ease),fill .3s var(--ease),opacity .3s var(--ease)}
/* On lock it contracts and flips to bone: the target word is amber, so an
   amber reticle would vanish into it at exactly the moment it should read. */
.reticle.is-locked .ring{transform:scale(.78)}
.reticle.is-locked circle,.reticle.is-locked line{stroke:var(--bone);opacity:.95}
.reticle.is-locked .dot{opacity:1;fill:var(--bone)}

/* Locked on the crosshair mark instead of the headline word. Here the accent
   is the point, so the reticle stays amber and burns brighter rather than
   flipping to bone. */
.reticle.is-locked--mark circle,.reticle.is-locked--mark line{stroke:var(--signal);opacity:1}
.reticle.is-locked--mark .dot{fill:var(--signal)}

/* ---------- byline ----------
   A face against the first person argument on the homepage. Square with a
   hairline rather than the usual round avatar: nothing else on this site has
   a border radius, and a circle would be the only soft edge on the page.

   Uses a dedicated square file rather than cropping the tall portrait with
   object-fit. Cover on a square can only trim that frame vertically, so the
   full width stays in shot and the face ends up a speck at this size. */

/* ---------- portrait ----------
   Centred under the page title on the About page, with the name beneath it.
   The frame follows the file's own ratio so nothing is cropped. */

.byline{margin:0}
/* Text left, portrait right, under the page title. */
.intro{
  display:grid;
  grid-template-columns:minmax(0,1fr) clamp(230px,26vw,320px);
  gap:clamp(2rem,5vw,4rem);
  align-items:start;
  margin-top:clamp(2rem,4vw,3rem);
}
/* No leading gap when the title is inside the grid: the portrait lines up
   with the top of the heading. */
.intro--flush{margin-top:0}
@media (max-width:860px){.intro{grid-template-columns:1fr}}
.byline img{
  width:100%;height:auto;
  aspect-ratio:573/986;object-fit:cover;
  border:1px solid var(--rule-dark);
}
.byline figcaption{margin-top:1rem}
.byline b{
  display:block;font-family:var(--display);font-weight:600;
  font-size:clamp(1.25rem,1.1rem + .5vw,1.6rem);
  letter-spacing:-.02em;color:var(--bone);margin-bottom:.35rem;
}
.band-light .byline img{border-color:var(--rule-light)}
.band-light .byline b{color:var(--void)}

/* ---------- work ---------- */

.work-grid{display:grid;gap:clamp(1.1rem,2.4vw,1.9rem);grid-template-columns:repeat(2,1fr);margin-top:3rem}
.work-card{
  position:relative;display:block;
  background:var(--ink);border:1px solid var(--rule-dark);
  overflow:hidden;
  transition:border-color .35s var(--ease),transform .45s var(--ease);
}
.band-light .work-card{background:var(--paper-2);border-color:var(--rule-light)}
.work-card:hover,.work-card:focus-within{border-color:var(--signal);transform:translateY(-4px)}

/* The card used to be one big <a>. It can't be, now that the shot holds its
   own link to the live site and links cannot nest. So the title carries the
   case study link and stretches over the body only, and the two destinations
   stay separate: shot goes to the site, title goes to the write-up. */
.work-card__body{position:relative}
.work-card__link::after{content:"";position:absolute;inset:0;z-index:1}
.work-card__link:hover{color:var(--signal)}
.work-card__shot{aspect-ratio:16/11;overflow:hidden;background:var(--ink-2);position:relative}
.work-card__shot img{width:100%;height:100%;object-fit:cover;transition:transform .8s var(--ease)}
.work-card:hover .work-card__shot img{transform:scale(1.04)}

/* ---------- portfolio shot ----------
   A full-page capture of the site inside a fixed window. Moving the pointer
   down the card scrubs down the page, so you drive it rather than waiting for
   an animation to play. site.js sets the transform.

   Scrubbing rather than hijacking the wheel is deliberate: a wheel handler
   here would stop the page dead every time someone scrolled past the card,
   which is the single most irritating thing an embedded preview can do.

   Transform only, so it composites on the GPU. Still no iframes. */

.shot--scroll .shot__page{
  position:absolute;top:0;left:0;
  width:100%;height:auto;
  min-height:100%;object-fit:cover;   /* short captures still fill the window */
  transform:translateY(0);
  transition:transform .55s var(--ease);
  will-change:transform;
}

/* While scrubbing the image should track the pointer immediately. The short
   duration is damping, not animation: it smooths the jitter of a hand without
   introducing lag you can feel. Leaving the card eases back to the top. */
.shot--scroll.is-scrubbing .shot__page{transition:transform .1s linear}

/* The click target. A corner chip rather than a full veil: the whole point is
   seeing the page, so covering it would defeat the exercise. */
.shot__veil{
  position:absolute;right:.7rem;bottom:.7rem;z-index:2;
  font-family:var(--mono);font-size:.6rem;font-weight:500;
  letter-spacing:.15em;text-transform:uppercase;
  color:var(--bone);
  background:rgba(20,16,16,.86);
  border:1px solid var(--signal);
  padding:.45rem .8rem;
  opacity:0;transform:translateY(6px);
  transition:opacity .3s var(--ease),transform .3s var(--ease),
             background .25s var(--ease),color .25s var(--ease);
}
.work-card:hover .shot__veil,
.shot__veil:focus-visible{opacity:1;transform:none}
.shot__veil:hover{background:var(--signal);color:var(--void)}

/* Touch has no pointer to scrub with, so the chip has to stay visible to be
   findable at all. */
@media (hover:none){
  .shot__veil{opacity:1;transform:none}
}
@media (prefers-reduced-motion:reduce){
  .shot--scroll .shot__page,.shot__veil{transition:none}
}

/* ---------- enquiry form ----------
   Lives on the light band, so every control is drawn against paper rather
   than the dark ground. Inputs are underlines rather than boxes: the site is
   built out of hairlines and this keeps the form in the same language. */

.form{margin-top:clamp(2rem,4vw,3rem);max-width:760px}
.form__row{display:grid;grid-template-columns:1fr 1fr;gap:clamp(1rem,2.5vw,1.8rem)}
@media (max-width:620px){.form__row{grid-template-columns:1fr}}

.field{display:flex;flex-direction:column;margin-bottom:clamp(1.2rem,2.5vw,1.7rem);min-width:0}
.field--full{margin-bottom:clamp(1.2rem,2.5vw,1.7rem)}

.field label,.field legend{
  font-family:var(--mono);font-size:.68rem;font-weight:500;
  letter-spacing:.16em;text-transform:uppercase;
  color:rgba(20,16,16,.62);
  margin-bottom:.55rem;padding:0;
}
.req{color:var(--signal-deep);letter-spacing:.16em}
.hint{color:rgba(20,16,16,.4);letter-spacing:.16em}

.field input,.field select,.field textarea{
  font-family:var(--body);font-size:1rem;color:var(--void);
  background:transparent;
  border:0;border-bottom:1px solid var(--rule-light);
  border-radius:0;                       /* iOS rounds these by default */
  padding:.6rem 0;
  width:100%;
  transition:border-color .25s var(--ease);
  -webkit-appearance:none;appearance:none;
}
.field textarea{resize:vertical;min-height:6rem;line-height:1.6}
.field select{
  /* the arrow, drawn rather than imported */
  background-image:linear-gradient(45deg,transparent 50%,rgba(20,16,16,.55) 50%),
                   linear-gradient(135deg,rgba(20,16,16,.55) 50%,transparent 50%);
  background-position:right 6px center,right 1px center;
  background-size:5px 5px,5px 5px;
  background-repeat:no-repeat;
  padding-right:1.4rem;
}
.field input::placeholder,.field textarea::placeholder{color:rgba(20,16,16,.34)}
.field input:focus,.field select:focus,.field textarea:focus{
  outline:none;border-bottom-color:var(--signal-deep);
}
.field input:focus-visible,.field select:focus-visible,.field textarea:focus-visible{
  outline:2px solid var(--signal-deep);outline-offset:3px;
}

fieldset.field{border:0;padding:0;margin-inline:0}

.choices{display:flex;flex-wrap:wrap;gap:.6rem .9rem}
.choices label{
  display:inline-flex;align-items:center;gap:.5rem;
  margin:0;padding:.5rem .85rem;
  border:1px solid var(--rule-light);
  font-size:.78rem;letter-spacing:.06em;
  color:rgba(20,16,16,.75);
  cursor:pointer;
  transition:border-color .2s var(--ease),background .2s var(--ease);
}
.choices label:hover{border-color:var(--signal-deep)}
.choices input{accent-color:var(--signal-deep);width:auto;margin:0}
.choices label:has(input:checked){border-color:var(--signal-deep);background:rgba(224,160,41,.1)}

.form__foot{display:flex;align-items:center;gap:1.2rem;flex-wrap:wrap;margin-top:.4rem}
.form__note{font-size:.86rem;color:rgba(20,16,16,.6);margin:0;max-width:32ch}

/* The honeypot. Off screen rather than display:none, because some bots skip
   anything that is not rendered. */
.form__trap{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}

/* ---------- enquiry dialog ----------
   Paper panel on a dimmed page. Carries .band-light so every form control
   inside picks up the light band colours it was designed against. */

.modal{
  position:fixed;inset:0;margin:auto;
  width:min(760px,calc(100vw - 2rem));
  max-height:min(88vh,900px);
  overflow:auto;overscroll-behavior:contain;
  padding:0;
  background:var(--paper);color:var(--void);
  border:1px solid var(--signal);
}
.modal::backdrop{background:rgba(10,8,8,.74)}
.modal__body{padding:clamp(1.4rem,4vw,2.6rem)}
.modal__body .form{margin-top:clamp(1.4rem,3vw,2rem)}

/* The dialog is its own stacking context, so the close button anchors to the
   scrolling panel rather than the viewport. */
.modal__close{
  position:sticky;top:0;float:right;z-index:2;
  background:none;border:0;color:rgba(20,16,16,.45);
  font-size:1.7rem;line-height:1;padding:.5rem .7rem;cursor:pointer;
  transition:color .25s var(--ease);
}
.modal__close:hover{color:var(--void)}

.modal[open]{animation:modal-in .28s var(--ease)}
.modal[open]::backdrop{animation:modal-fade .28s var(--ease)}
@keyframes modal-in{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
@keyframes modal-fade{from{opacity:0}to{opacity:1}}

@media (prefers-reduced-motion:reduce){
  .modal[open],.modal[open]::backdrop{animation:none}
}

/* ---------- launch offer nudge ----------
   Appears once the work section has been read, then stays out of the way.

   Corner panel, not a centre-screen modal. A full interstitial converts a
   little better and annoys a lot more, and on mobile Google treats content
   covered by an interstitial as an intrusive pattern, which would be a strange
   thing to ship on a site that sells on-page SEO. This takes a corner, never
   blocks the page, and can be dismissed with the button or Escape. */

.nudge{
  position:fixed;right:clamp(1rem,3vw,2rem);bottom:clamp(1rem,3vw,2rem);z-index:120;
  width:min(340px,calc(100vw - 2rem));
  background:var(--ink);border:1px solid var(--signal);
  padding:1.4rem 1.5rem 1.5rem;
  box-shadow:0 18px 50px rgba(0,0,0,.5);
  opacity:0;visibility:hidden;transform:translateY(14px);
  transition:opacity .4s var(--ease),transform .4s var(--ease),visibility .4s;
}
.nudge.is-up{opacity:1;visibility:visible;transform:none}

.nudge .eyebrow{margin-bottom:.7rem}
.nudge__line{font-family:var(--display);font-size:1.35rem;font-weight:600;letter-spacing:-.02em;margin:0 0 .5rem;line-height:1.15}
.nudge__line b{color:var(--signal);font-weight:600}
.nudge__sub{font-size:.9rem;color:var(--bone-dim);margin:0 0 1.2rem;max-width:none}

.nudge__close{
  position:absolute;top:.45rem;right:.55rem;
  background:none;border:0;color:var(--bone-faint);
  font-size:1.35rem;line-height:1;padding:.35rem .5rem;cursor:pointer;
  transition:color .25s var(--ease);
}
.nudge__close:hover{color:var(--bone)}

/* On a phone it spans the width as a slim card at the bottom. Still a banner
   using a reasonable amount of the screen, not a takeover. */
@media (max-width:600px){
  .nudge{left:1rem;right:1rem;bottom:1rem;width:auto;padding:1.1rem 1.2rem 1.2rem}
  .nudge__sub{display:none}
}

@media (prefers-reduced-motion:reduce){
  .nudge{transition:opacity .01s,visibility .01s;transform:none}
}

/* ---------- client credits ----------
   A slow horizontal ticker under the hero. Every mark is pre-keyed to bone on
   an identical canvas by tools/build_logos.py, so nothing here needs per-logo
   tweaking and no client's brand colour competes with the accent.

   Only three clients, so site.js clones the row until it is wider than the
   rail, then duplicates the whole thing once. The animation runs to exactly
   -50%, which lands the copy precisely where the original began and makes the
   loop seamless. Spacing is padding on each item rather than a gap, because
   gap leaves one fewer space than items and -50% would drift. */

.clients{padding:clamp(1.8rem,4vw,2.8rem) 0;border-top:1px solid var(--rule-dark)}
.clients__inner{display:flex;align-items:center;gap:clamp(1rem,3vw,2.4rem);flex-wrap:wrap}
.clients__label{
  margin:0;flex:none;
  font-family:var(--mono);font-size:.68rem;font-weight:500;
  letter-spacing:.2em;text-transform:uppercase;color:var(--bone-faint);
}

.marquee{
  position:relative;overflow:hidden;
  flex:1 1 260px;min-width:0;
  /* Fade both ends so logos enter and leave rather than being guillotined by
     the edge of the rail. */
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);
}
.marquee__track{
  display:flex;align-items:center;
  list-style:none;margin:0;padding:0;
  width:max-content;
  animation:marquee var(--marquee-dur,45s) linear infinite;
}
.marquee__track li{flex:none;padding-right:clamp(2.2rem,6vw,5rem)}
.marquee__track img{
  width:clamp(148px,17vw,208px);height:auto;
  opacity:.88;
  transition:opacity .3s var(--ease);
}
.marquee__track img:hover{opacity:1}

/* Hovering holds it still so a logo can actually be looked at, and it stops
   entirely while off screen rather than animating to nobody. */
.marquee:hover .marquee__track,
.marquee.is-idle .marquee__track{animation-play-state:paused}

@keyframes marquee{
  from{transform:translate3d(0,0,0)}
  to{transform:translate3d(-50%,0,0)}
}

@media (prefers-reduced-motion:reduce){
  .marquee__track{animation:none;flex-wrap:wrap}
  .marquee{-webkit-mask-image:none;mask-image:none}
}

/* Screenshot placeholder: replace the div with an <img> when shots land. */
.shot-placeholder{
  position:absolute;inset:0;display:grid;place-content:center;gap:.5rem;text-align:center;
  font-family:var(--mono);font-size:.7rem;letter-spacing:.14em;text-transform:uppercase;
  color:var(--bone-faint);
  background:
    repeating-linear-gradient(135deg,transparent 0 11px,rgba(242,237,231,.035) 11px 22px),
    var(--ink-2);
}
.band-light .shot-placeholder{color:rgba(20,16,16,.4);background:
  repeating-linear-gradient(135deg,transparent 0 11px,rgba(20,16,16,.04) 11px 22px),var(--paper-2)}

.work-card__body{padding:1.5rem clamp(1.2rem,2vw,1.75rem) 1.7rem}
.work-card__body h3{margin-bottom:.35rem}
.work-card__kind{font-family:var(--mono);font-size:.68rem;letter-spacing:.15em;text-transform:uppercase;color:var(--signal);margin:0 0 .9rem}
.band-light .work-card__kind{color:var(--signal-deep)}
.work-card__body p{font-size:.94rem;color:var(--bone-dim);margin:0}
.band-light .work-card__body p{color:rgba(20,16,16,.66)}
.work-card__tags{display:flex;flex-wrap:wrap;gap:.4rem;margin-top:1.1rem}
.tag{font-family:var(--mono);font-size:.64rem;letter-spacing:.1em;text-transform:uppercase;
  border:1px solid var(--rule-dark);padding:.3rem .55rem;color:var(--bone-faint)}
.band-light .tag{border-color:var(--rule-light);color:rgba(20,16,16,.5)}

/* ---------- services ---------- */

.svc-list{margin-top:3rem;border-top:1px solid var(--rule-dark)}
.band-light .svc-list{border-color:var(--rule-light)}
.svc{
  display:grid;grid-template-columns:auto 1fr auto;gap:clamp(1.1rem,3vw,2.6rem);align-items:start;
  padding:clamp(1.6rem,3vw,2.4rem) 0;border-bottom:1px solid var(--rule-dark);
  position:relative;
}
.band-light .svc{border-color:var(--rule-light)}
.svc__idx{font-family:var(--mono);font-size:.72rem;letter-spacing:.1em;color:var(--signal);padding-top:.45rem}
.band-light .svc__idx{color:var(--signal-deep)}
.svc__main h3{margin-bottom:.55rem}
.svc__main p{margin:0;font-size:.96rem;color:var(--bone-dim);max-width:56ch}
.band-light .svc__main p{color:rgba(20,16,16,.66)}
.svc__inc{font-family:var(--mono);font-size:.68rem;letter-spacing:.1em;text-transform:uppercase;color:var(--bone-faint);margin-top:.9rem;display:block}
.band-light .svc__inc{color:rgba(20,16,16,.48)}
.svc__lead{font-family:var(--mono);font-size:.66rem;letter-spacing:.14em;text-transform:uppercase;
  color:var(--void);background:var(--signal);padding:.3rem .6rem;white-space:nowrap;align-self:start;margin-top:.35rem}

/* ---------- offer ---------- */

.offer{
  border:1px solid var(--signal);
  padding:clamp(1.8rem,4vw,3.2rem);
  display:grid;grid-template-columns:1fr auto;gap:2rem;align-items:center;
  position:relative;
  background:linear-gradient(180deg,rgba(224,160,41,.06),transparent);
}
.offer h2{font-size:clamp(1.6rem,1.2rem + 2vw,2.6rem);margin-bottom:.7rem}
.offer p{margin:0;color:var(--bone-dim);font-size:.97rem}
.offer__price{display:flex;align-items:baseline;gap:.7rem;margin-bottom:.9rem;font-family:var(--display)}
.offer__was{font-size:1.4rem;color:var(--bone-faint);text-decoration:line-through}
.offer__now{font-size:clamp(2.4rem,2rem + 3vw,4rem);font-weight:700;letter-spacing:-.04em;color:var(--signal);line-height:1}
.offer__terms{font-family:var(--mono);font-size:.66rem;letter-spacing:.09em;text-transform:uppercase;color:var(--bone-faint);margin-top:1.2rem;display:block}

/* Launch price on the web design row of the homepage service list. */
.svc__promo{margin:.7rem 0 0;font-family:var(--mono);font-size:.74rem;letter-spacing:.08em;text-transform:uppercase;color:var(--bone-dim)}
.svc__promo b{color:var(--void);background:var(--signal);padding:.2rem .45rem;font-weight:500;margin-right:.4rem}
.svc__promo s{opacity:.6;margin-right:.2rem}

/* ---------- testimonials ---------- */

/* Four quotes in one row of equal boxes on desktop. auto-fit left three and
   a lone orphan, so the counts are explicit: 4, then 2x2, then 1. */
.quotes{display:grid;grid-template-columns:1fr;gap:clamp(1rem,2.2vw,1.6rem);margin-top:3rem}
@media (min-width:700px){.quotes{grid-template-columns:repeat(2,1fr)}}
@media (min-width:1080px){.quotes{grid-template-columns:repeat(4,1fr)}}
/* White cards with an amber border, so the quotes lift off the paper band
   and read as people talking, not more body copy. */
.quote{margin:0;background:#fff;color:var(--void);border:2px solid rgba(224,160,41,.5);padding:clamp(1.4rem,2.6vw,2rem);display:flex;flex-direction:column}
.quote blockquote{margin:0 0 1.4rem;font-size:1.02rem;line-height:1.62}
.quote blockquote{font-weight:500}
.quote blockquote::before{content:"";display:block;width:22px;height:1px;background:var(--signal);margin-bottom:1.1rem}
.quote figcaption{margin-top:auto;font-family:var(--mono);font-size:.7rem;letter-spacing:.11em;text-transform:uppercase;color:rgba(20,16,16,.62)}
.quote figcaption b{display:block;color:var(--void);font-weight:500;margin-bottom:.2rem}
.band-light .quote figcaption b{color:var(--void)}

/* ---------- process ---------- */

/* Five steps do not divide evenly, so auto-fit kept stranding the fifth on a
   row of its own at middling widths. Explicit counts instead: five across on
   a wide screen, a deliberate three-and-two in the middle, one column on a
   phone. Never a lone orphan. */
.steps{display:grid;grid-template-columns:1fr;gap:clamp(1.2rem,2.2vw,1.7rem);margin-top:3rem}
@media (min-width:700px){.steps{grid-template-columns:repeat(3,1fr)}}
@media (min-width:1180px){.steps{grid-template-columns:repeat(5,1fr)}}
/* Boxed since the steps lost their descriptions: a hairline top rule alone
   left five short labels floating with nothing to hold them. Full border,
   real padding, and the name set large enough to read as the content rather
   than a caption. */
.step{
  position:relative;
  display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;
  padding:clamp(1.3rem,2.4vw,1.8rem);
  border:1px solid var(--rule-dark);
  transition:border-color .3s var(--ease),transform .3s var(--ease);
}
.band-light .step{border-color:var(--rule-light)}
.step:hover{border-color:var(--signal);transform:translateY(-3px)}
.band-light .step:hover{border-color:var(--signal-deep)}

.step__n{
  font-family:var(--mono);font-size:.72rem;font-weight:500;
  letter-spacing:.18em;color:var(--signal);
  display:block;margin-bottom:.7rem;
}
.band-light .step__n{color:var(--signal-deep)}

.step h3{
  font-size:clamp(1.15rem,1rem + .6vw,1.5rem);
  line-height:1.15;margin:0;
}
.step p{font-size:.92rem;color:var(--bone-dim);margin:0}
.band-light .step p{color:rgba(20,16,16,.66)}

/* ---------- contact ---------- */

.contact-grid{display:grid;grid-template-columns:1.1fr .9fr;gap:clamp(2rem,5vw,4.5rem);align-items:start}
.contact-list{list-style:none;margin:2rem 0 0;padding:0;border-top:1px solid var(--rule-dark)}
.band-light .contact-list{border-color:var(--rule-light)}
.contact-list li{border-bottom:1px solid var(--rule-dark);padding:1.15rem 0;display:flex;gap:1.5rem;align-items:baseline}
.band-light .contact-list li{border-color:var(--rule-light)}
.contact-list dt,.contact-list .k{font-family:var(--mono);font-size:.68rem;letter-spacing:.14em;text-transform:uppercase;color:var(--bone-faint);flex:none;width:5.5rem}
.band-light .contact-list .k{color:rgba(20,16,16,.5)}
.contact-list a{border-bottom:1px solid transparent;transition:border-color .25s,color .25s}
.contact-list a:hover{color:var(--signal);border-color:var(--signal)}

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

.foot{border-top:1px solid var(--rule-dark);padding:clamp(3rem,6vw,4.5rem) 0 2.2rem;background:var(--void);color:var(--bone)}
.foot__top{display:grid;grid-template-columns:1.2fr 1fr 1fr;gap:2.5rem}
.foot__brand img{width:170px;margin-bottom:1.2rem}
.foot h4{font-family:var(--mono);font-size:.68rem;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--bone-faint);margin:0 0 1.1rem}
.foot ul{list-style:none;margin:0;padding:0;display:grid;gap:.7rem}
.foot a{font-size:.92rem;color:var(--bone-dim);transition:color .25s}
.foot a:hover{color:var(--signal)}
.foot__base{margin-top:3.2rem;padding-top:1.5rem;border-top:1px solid var(--rule-dark);
  display:flex;flex-wrap:wrap;gap:1rem;justify-content:space-between;
  font-family:var(--mono);font-size:.68rem;letter-spacing:.1em;text-transform:uppercase;color:var(--bone-faint)}

/* ---------- scroll dial ---------- */
/* Thin readout down the right edge. The tick is scroll position: the page
   itself is the thing being dialled in. */
.dial{display:block;   /* likewise hidden by the inline critical CSS */
  position:fixed;right:clamp(.6rem,1.6vw,1.4rem);top:50%;transform:translateY(-50%);
  z-index:90;height:min(42vh,300px);width:1px;
  /* Neutral rather than bone-tinted: it passes over both bands as you scroll. */
  background:rgba(140,120,108,.34);
  pointer-events:none;opacity:0;transition:opacity .5s var(--ease)}
.dial.is-live{opacity:1}
.dial__tick{position:absolute;left:-5px;width:11px;height:1px;background:var(--signal);top:0;will-change:transform}
.dial__tick::after{content:"";position:absolute;left:4px;top:-1.5px;width:3px;height:3px;border-radius:50%;background:var(--signal)}

/* ---------- reveal ---------- */

.reveal{opacity:0;transform:translateY(18px);transition:opacity .75s var(--ease),transform .75s var(--ease)}
.reveal.is-in{opacity:1;transform:none}

/* ---------- responsive ---------- */

@media (max-width:900px){
  .work-grid{grid-template-columns:1fr}
  .contact-grid{grid-template-columns:1fr}
  .foot__top{grid-template-columns:1fr 1fr}
  .offer{grid-template-columns:1fr}
  .svc{grid-template-columns:auto 1fr}
  .svc__lead{grid-column:2;justify-self:start}
}

@media (max-width:760px){
  .nav-toggle{display:block;margin-left:0}

  /* Back to a single bar with a drop-down beneath it. The CTA lives in the
     drop-down rather than the bar: at 375px the brand, the Menu button and a
     full-width button do not fit on one line. */
  .masthead__inner{
    display:flex;flex-wrap:wrap;align-items:center;gap:1rem;
  }
  .brand{order:1;margin-right:auto}
  .nav-toggle{order:2}
  .nav{order:3}
  /* The icon and CTA travel together into the drop-down. Ordering and the
     open state have to target the wrapper, not the button inside it. */
  .masthead__right{order:4;justify-self:auto}

  .nav,.masthead__right{
    width:100%;
    display:none;                 /* out of the flow until the menu opens */
  }
  .nav.is-open,.nav.is-open ~ .masthead__right{display:flex}
  .nav.is-open ~ .masthead__right{
    justify-content:center;align-items:center;gap:1.2rem;margin-bottom:1.2rem;
  }
  .nav{
    flex-direction:column;align-items:stretch;gap:0;margin:0;
    padding:.2rem 0 0;
  }
  .nav a:not(.btn){padding:1rem 0;border-bottom:1px solid var(--rule-dark);font-size:.85rem}
  .nav a:not(.btn)::after{display:none}
  .dial{display:none}
  .hero__meta{gap:1.6rem 2rem}
  .foot__top{grid-template-columns:1fr}
}

@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{animation-duration:.01ms !important;transition-duration:.01ms !important}
  .reveal{opacity:1;transform:none}
  .reticle,.dial{display:none}
}
