/* ═══════════════════════════════════════════════════════════════════
   Arcos Interiors — landing / hero
   Palette sampled from the ADF brand poster.
   Type system mirrors the reference: Didone display + humanist sans
   + script accent.
   ═══════════════════════════════════════════════════════════════════ */

/* ── the palette ──────────────────────────────────────────────────────
   Warm paper, deep brown, charcoal. The site was built dark and this is
   the same site read the other way up, so the token NAMES are the ones
   the whole stylesheet already speaks — --black is still the page, --ink
   is still the raised surface, --bone is still the strongest text. Only
   what they hold has changed. Renaming ninety-odd call sites to say
   "paper" instead of "black" would be a large diff that changed nothing
   anybody can see.

   Two things did not flip, and both for the same reason. Anything that
   sits ON a photograph keeps its dark scrim and its light type: a
   picture is not a theme, and white text over a bright interior needs
   the same darkening whichever way the page around it is going.
   Anything that is a shadow got much weaker — a shadow on paper is a
   hint, where on near-black it had to be a statement to be seen at all.
   ─────────────────────────────────────────────────────────────────── */

:root{
  /* ── brand ──────────────────────────────────────────────────────── */
  /* The ramp is inverted from the dark build. -lt and -hi used to mean
     "lighter, so it reads against black"; on paper an accent has to
     darken to be read, so -hi is now the strongest rather than the
     palest. Every step below is AA or better as small text on --black. */
  --bronze:       #7A5530;   /* 6.6:1 on paper — numerals, small marks   */
  --bronze-lt:    #8B6239;   /* 5.6:1 — labels, eyebrows, the brand tone */
  --bronze-hi:    #653D1D;   /* 8.9:1 — the strongest accent             */
  --bronze-dp:    #4A2E14;   /* deepest, for fills and marker ink        */
  /* The green out of the logo mark, and the only place on the page it is
     not the bronze doing the accenting. Every line set in the script
     face takes --olive.

     It is a chosen swatch, not a measured one: the mark itself samples
     at #B1D35B and this sits just under it. Against paper it carries
     1.75:1, so it lands as a tint on the page rather than as type — a
     mark you see before you read. --olive-hi is the same hue taken down
     to where it can be read at any size, for anything that has to be. */
  --olive:        #AFCC58;   /* 1.75:1 on paper, 1.62:1 on --ink         */
  --olive-lt:     #6E8529;   /* 4.0:1 — a middle step                    */
  --olive-hi:     #54661F;   /* 6.2:1 on paper, 5.7:1 on --ink           */

  /* ── neutrals ───────────────────────────────────────────────────── */
  --black:        #FDFBF7;   /* the page — warm off-white                */
  --ink:          #F5F2EE;   /* raised surface, alternate section — beige*/
  --sage:         #F4F7EB;   /* the third section tone — pale green      */
  --bone:         #4A3B29;   /* headings, high contrast — 10.2:1         */
  --bone-dim:     #333333;   /* body — 12.2:1                            */
  --bone-faint:   #7A7268;   /* quiet labels, captions — 4.6:1           */
  --hair:         rgba(74,59,41,.16);

  /* A shadow on paper. One value, so nothing invents its own. */
  --lift:         rgba(74,59,41,.10);
  --lift-deep:    rgba(74,59,41,.16);

  /* ── on a photograph ────────────────────────────────────────────── */
  /* The page turned over; the pictures did not. A caption on a lit
     interior, a play button on a video still, the scrim that holds
     either of them legible — all of that has to stay light-on-dark
     whichever way the page around it goes, because it is sitting on an
     image and an image has its own brightness. These are the tokens for
     that, and the only ones in the file that still read like the old
     build. Using --bone there instead would put deep brown type on a
     photograph and lose it. */
  --on-media:      #F4F1EC;
  --on-media-dim:  rgba(244,241,236,.74);
  --media-scrim:   rgba(12,12,14,.55);
  --media-veil:    rgba(12,12,14,.86);
  /* the bronze, lightened, for accents carried on an image */
  --on-media-accent: #D9AE84;
  --on-media-hair: rgba(244,241,236,.26);

  /* ── type ───────────────────────────────────────────────────────── */
  --display: "Marcellus", "Trajan Pro", "Optima", Palatino, Georgia, serif;
  /* high-contrast cut, for display type set large enough to show the
     thick/thin modulation — the small sizes stay on Marcellus */
  --display-hi: "Playfair Display", "Marcellus", Didot, Georgia, serif;
  --sans:    "Anek Bangla", ui-sans-serif, system-ui, -apple-system, sans-serif;
  --script:  "Alex Brush", "Snell Roundhand", "Apple Chancery", cursive;

  /* ── the scale ──────────────────────────────────────────────────── */
  /* Every size on the site is a step on one scale built from φ, 1.618.
     Nothing sets a font-size of its own any more; if a thing needs a
     size it takes the step nearest the job.

     The step ratio is √φ (1.272), not φ itself, so every SECOND step is
     exactly golden: body × φ = sub, sub × φ = head, head × φ = display.
     A whole φ per step is too coarse to build a page on — it goes 16px,
     26px, 42px with nothing usable in between, and this site carries a
     lot of small type that needs the room. The half-step is what makes
     the ratio wearable; the golden intervals are all still in there.

                     step  ×base   at 16px
       --fs-mark      -3   0.486     7.8px
       --fs-micro     -2   0.618     9.9px   φ⁻¹
       --fs-fine      -1   0.786    12.6px
       --fs-body       0   1.000    16.0px   ← the anchor
       --fs-lede       1   1.272    20.4px
       --fs-sub        2   1.618    25.9px   φ
       --fs-title      3   2.058    32.9px
       --fs-head       4   2.618    41.9px   φ²
       --fs-major      5   3.330    53.3px
       --fs-display    6   4.236    67.8px   φ³
       --fs-hero       7   5.389    86.2px
       --fs-mega       8   6.854   109.7px   φ⁴

     The fluid tokens are a clamp between two steps of that same scale:
     the floor is the step the size lands on at 375px, the ceiling the
     step it lands on at 1440px. So the page is a pure golden scale at
     both ends of the range and only interpolating in between — which is
     the honest compromise, because fluid type and a fixed ratio cannot
     both hold at every width.

     The four smallest are fixed. They sit at the legibility floor
     already and have nowhere to shrink to. --fs-body is fixed at 1rem
     for the same reason from the other side: anything under 16px in a
     form field makes iOS zoom the page on focus. */
  --fs-mark:    0.486rem;
  --fs-micro:   0.618rem;
  --fs-fine:    0.786rem;
  --fs-body:    1rem;
  --fs-lede:    clamp(1.000rem, 0.904rem + 0.41vw, 1.272rem);
  --fs-sub:     clamp(1.272rem, 1.150rem + 0.52vw, 1.618rem);
  --fs-title:   clamp(1.618rem, 1.463rem + 0.66vw, 2.058rem);
  --fs-head:    clamp(1.618rem, 1.266rem + 1.50vw, 2.618rem);
  --fs-major:   clamp(2.058rem, 1.610rem + 1.91vw, 3.330rem);
  --fs-display: clamp(2.618rem, 2.048rem + 2.43vw, 4.236rem);
  --fs-hero:    clamp(2.618rem, 1.643rem + 4.16vw, 5.388rem);
  --fs-mega:    clamp(3.330rem, 2.089rem + 5.29vw, 6.854rem);

  /* ── motion (strong curves — CSS defaults are too weak) ─────────── */
  --ease-out:    cubic-bezier(.23,1,.32,1);
  --ease-in-out: cubic-bezier(.77,0,.175,1);
  --ease-drawer: cubic-bezier(.32,.72,0,1);

  /* ── rhythm ─────────────────────────────────────────────────────── */
  --gut:  clamp(1.25rem, 4vw, 3.5rem);
  /* The bar's height, and the offset every page lays itself out
     against — so lowering it lifts the content of the whole site by the
     same amount, which is the point. The floor is what the 34px mark
     and a 44px touch target need with air left over. */
  --navh: clamp(60px, 7vh, 82px);

  /* ── layers ─────────────────────────────────────────────────────── */
  --z-watermark: -1;
  --z-fab:       90;
  --z-nav:      100;
  /* Above the drawer at 200, because it covers the whole page including
     the chrome — it is the first thing painted and the only thing on
     screen while it is up. */
  --z-loader:  1000;

  /* Logo watermark strength. Single knob — raise toward .12 for a more
     present mark, drop toward .04 to make it near-subliminal. */
  --wm: .07;
}

/* ── reset ────────────────────────────────────────────────────────── */
*,*::before,*::after{ box-sizing:border-box; margin:0; padding:0 }

html{ -webkit-text-size-adjust:100% }

/* anchor jumps glide rather than cut — but never against a user who has
   asked for less motion */
@media (prefers-reduced-motion:no-preference){
  html{ scroll-behavior:smooth }
}
/* On every [id], not only on :target. The clearance is the same either
   way for a native jump, but js/anchor.js reads this value back with
   getComputedStyle to work out where to scroll — and on :target alone it
   reads 0 until the fragment already points at the element, which is one
   step too late to be useful. */
[id]{ scroll-margin-block:clamp(2rem,10vh,6rem) }

body{
  min-height:100dvh;
  background:var(--black);
  color:var(--bone);
  font-family:var(--sans);
  font-size:var(--fs-body);
  line-height:1.55;
  font-weight:300;
  overflow-x:hidden;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
}

img,svg,video{ display:block; max-width:100% }
a{ color:inherit; text-decoration:none }
button{ font:inherit; color:inherit; background:none; border:0 }

/* Selection is the highlighter, on every page. The about page marks six
   phrases with a green wash and the reader can do the same to any line
   on the site — same colour, same idea, and it puts the brand green
   somewhere every page can reach it without spending a headline on it.

   A wash with the type left dark, not a solid with the type flipped
   white: it was solid bronze on #fff, which at this saturation dropped a
   block of paint over whatever was being read. */
::selection{ background:rgba(175,204,88,.6); color:var(--bone) }
::selection{ background:color-mix(in srgb, var(--olive) 60%, transparent) }

/* Read by a screen reader, never painted. Lives here rather than in a
   section stylesheet because the shared footer uses it and not every
   page loads every section's CSS — the one page that didn't rendered the
   hidden heading as a line of text across the bottom of the screen. */
.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;
}

/* Bronze, like every other focus ring on the site. This one was olive
   — the only green left in the interface, and it turned up on whatever
   you happened to click. */
:focus-visible{
  outline:2px solid var(--bronze-hi);
  outline-offset:3px;
  border-radius:2px;
}

/* ═══ nav ═════════════════════════════════════════════════════════ */
/* A mark, a hairline, a wordmark, and the control that opens everything
   else. There is no link in the bar at all: navigation lives in the
   drawer, and a header that carries one lone shortcut next to a menu
   button is a header that has not decided what it is.
   ═══════════════════════════════════════════════════════════════════ */
.nav{
  position:fixed; top:0; left:0; right:0; z-index:var(--z-nav);
  height:var(--navh);
  padding:0 var(--gut);
  padding-top:env(safe-area-inset-top);
  display:flex; align-items:center; justify-content:space-between;
  gap:1.5rem;
}
/* The bar carries no plate of its own, but it cannot be wholly bare
   either: the gallery further down runs pale interiors under it and bone
   type on a lit ceiling is unreadable. A short gradient, fading out well
   before the bar's own height, is enough to hold the type without ever
   reading as a bar. pointer-events:none so it never eats a click meant
   for the hero. */
/* Nothing on paper. The bar is deep brown on warm off-white and needs
   no help; a veil there only puts a pale fog over whatever the page has
   at the top of it. It appears — dark, as it always was — only while
   js/chrome.js says the bar is over footage. */
.nav::before{
  content:""; position:absolute; inset:0 0 auto; height:200%;
  background:none;
  pointer-events:none;
  transition:background .4s var(--ease-out);
}
.is-over-media .nav::before{
  background:linear-gradient(180deg,
    rgba(12,12,14,.62) 0%,
    rgba(12,12,14,.26) 45%,
    rgba(12,12,14,0)   100%);
}
.nav > *{ position:relative }   /* above that gradient */

/* ── the bar's own ground ─────────────────────────────────────────── */
/* The bar used to leave on the way down and come back on the way up. It
   stays now, which means the page can no longer be allowed to run
   through it: a wordmark sitting on top of a heading is the one thing a
   permanent header must not do.

   So on paper it carries the page's own colour. At rest that is
   invisible — it is the same warm off-white the page already is — and
   the moment anything scrolls up behind it, it is a plate. The hairline
   arrives with the scroll rather than sitting there from the first
   paint: at the top there is nothing to divide, and a rule drawn across
   the page before it has been scrolled reads as a bar the design does
   not want. Once type is passing underneath, that edge is what tells
   you the type is passing underneath rather than simply stopping.

   Over footage neither applies. The gradient above is what holds the
   chrome there, and a plate would put a band across the film. */
.nav{
  background:var(--black);
  transition:background-color .4s var(--ease-out),
             box-shadow .3s var(--ease-out);
}
.is-scrolled .nav{ box-shadow:0 1px 0 var(--hair) }
.is-over-media .nav{ background:transparent; box-shadow:none }

@media (prefers-reduced-motion:reduce){
  .nav{ transition:none }
}

/* ── brand: the monogram, ruled off from the wordmark ─────────────── */
/* The mark is the logo cropped to the glyph alone — assets/img/logo-mark
   .png. The firm's name used to sit under it inside the artwork, which
   is why the artwork is not used whole: a wordmark baked into a picture
   cannot be set in the site's own face, cannot be spaced to match, and
   goes soft on a retina screen. It is typed here instead. */
/* Mark and name on one line, with nothing between them: the monogram's
   right edge meets the A. logo-mark.png carries no transparent margin —
   its ink runs to all four edges of the 504x240 canvas — so gap:0 here
   really is zero, and any value would be real space. */
.brand{
  display:flex; align-items:center;
  flex:0 0 auto; min-height:44px;
  gap:0;
}

/* -- the lockup ---------------------------------------------------- */
/* The name over the discipline, both ranged to the same right edge. It
   is the shape a printed identity takes: one word large enough to be
   the name, and under it the trade, small and opened right up so that
   the two lines end together rather than merely stacking.

   align-items:flex-end is what makes it a lockup rather than two
   labels — the second line is tracked wide enough that its right edge
   lands under the first, and that shared edge is the whole device. */
.brand__lockup{
  display:flex; flex-direction:column;
  align-items:flex-end;
  gap:.1em;
  line-height:1;
}
.brand__name{
  font-family:var(--display); font-weight:400;
  /* Full capitals rather than small caps: it is one word now instead of
     three, so it can carry the size, and small caps under a line of
     tracked-out capitals reads as a mistake in the lockup rather than
     as a second tier. */
  font-size:var(--fs-lede);
  line-height:1;
  text-transform:uppercase;
  letter-spacing:.2em;
  margin-right:-.2em;          /* the tracking's trailing space, removed */
  white-space:nowrap;
  color:var(--bone);
  transition:color .3s var(--ease-out);
}
.brand__sub{
  font-family:var(--sans);
  font-size:var(--fs-micro);
  font-weight:400;
  letter-spacing:.34em;
  /* the tracking pushes a trailing space past the right edge; this
     pulls the line back so both edges align */
  margin-right:-.34em;
  text-transform:uppercase;
  color:var(--bone-dim);
  white-space:nowrap;
  transition:color .3s var(--ease-out);
}
.brand__mark{
  height:clamp(24px,3.2vw,34px);
  width:auto;
  flex:0 0 auto;
  transition:opacity .3s var(--ease-out);
}
/* The rule is the whole lockup: it gives the mark something to sit
   against and stops the two halves reading as one crowded object. */
.brand__rule{
  flex:0 0 auto;
  width:1px; height:clamp(20px,2.6vw,28px);
  background:var(--hair);
}
@media (hover:hover) and (pointer:fine){
  .brand:hover .brand__name{ color:var(--bronze-lt) }
  .brand:hover .brand__sub{ color:var(--bronze-lt) }
  .brand:hover .brand__mark{ opacity:.82 }
}

/* Nothing is dropped on a narrow phone. This used to hide the name below
   420px, from when it ran along one line as ARCOS DESIGN FIRM and did
   crowd the burger. Stacked, it costs nothing: the name is 67px over a
   subtitle that is already 64px, so the brand measures the same either
   way — and hiding only the name left the phone showing DESIGN FIRM with
   no firm named above it, which is worse than either whole. (The rule
   also hid a .brand__rule that no page has ever had in its markup.) */

/* ── burger: a word and two hairlines ─────────────────────────────── */
/* The lines alone were legible as a control but silent about what they
   open. The word carries that, and flips to CLOSE while the drawer is
   up — see js/menu.js. */
.burger{
  flex:0 0 auto;
  min-width:44px; min-height:44px;
  display:flex; flex-direction:row; align-items:center;
  justify-content:flex-end;
  gap:clamp(.65rem,1.4vw,1.05rem);
  cursor:pointer;
}
.burger__label{
  font-size:var(--fs-micro);
  letter-spacing:.24em; text-transform:uppercase;
  white-space:nowrap;
  color:var(--bone);
  transition:color .3s var(--ease-out);
}
.burger__lines{
  display:flex; flex-direction:column; align-items:flex-end;
  gap:8px;
}
.burger__lines span{
  display:block; height:1px; background:var(--bone);
  /* shorter than they were: the word now carries part of the width, and
     the pair together should not outrun the wordmark opposite */
  width:clamp(34px,3.6vw,54px);
  transition:width .3s var(--ease-out), background .3s var(--ease-out);
}
@media (hover:hover) and (pointer:fine){
  .burger:hover .burger__lines span:first-child{ width:clamp(24px,2.6vw,38px) }
  .burger:hover .burger__lines span{ background:var(--bronze-lt) }
  .burger:hover .burger__label{ color:var(--bronze-lt) }
}

/* Narrow phones: the lines alone. The word is the first thing that can
   go — a burger at this size is understood without it, and the space it
   frees keeps the mark opposite from crowding. */
@media (max-width:420px){
  .burger__label{ display:none }
}

/* ═══ buttons ═════════════════════════════════════════════════════ */
.btn{
  position:relative; display:inline-flex; align-items:center; gap:.6rem;
  /* The left padding is not symmetrical any more. It is 2.6rem because
     the fill's resting state is a dot sitting in that space — see THE
     HOVER BUTTON below. Take the dot away and this goes back to 1.5. */
  min-height:44px; padding:.7rem 1.5rem .7rem 2.6rem;
  font-size:var(--fs-fine); letter-spacing:.18em; text-transform:uppercase; font-weight:400;
  border:1px solid var(--hair); border-radius:999px;
  overflow:hidden; cursor:pointer;
  transition:transform .16s var(--ease-out), border-color .3s var(--ease-out), color .3s var(--ease-out);
}
.btn > *{ position:relative; z-index:1 }
.btn svg{ width:15px; height:15px }
.btn::before{
  content:""; position:absolute; inset:0; z-index:0;
  background:linear-gradient(100deg,var(--bronze),var(--bronze-dp));
}
.btn:active{ transform:scale(.97) }
@media (hover:hover) and (pointer:fine){
  .btn:hover{ border-color:transparent; color:#fff }
}

.btn--solid{
  border-color:transparent;
  background:linear-gradient(100deg,var(--bronze),var(--bronze-dp));
  color:#fff;
}
/* The wipe on hover is a lift in the same bronze, not a change of
   colour: this button used to go olive under the cursor, which read as
   a different button rather than as the same one being pressed.

   Which way the lift runs matters now that the fill is a circle rather
   than a full-width wipe. It used to be bronze-hi → bronze, darker at
   the left and lighter at the right, and against a ground running the
   other way that made the fill darker than the button exactly where it
   starts — so the whole gesture opened invisibly and the dot at rest was
   a hole rather than a mark. Turned around, it is lighter than the
   ground the whole way across and lightest at the origin.

   bronze-lt is as far as it goes: white on #8B6239 is 5.4:1, and the
   label here is .74rem, which is small text and wants 4.5. */
.btn--solid::before{ background:linear-gradient(100deg,var(--bronze-lt),var(--bronze)) }

/* ═══ THE HOVER BUTTON ════════════════════════════════════════════ */
/* Every pill on the site presses the same way: a dot on the left blooms
   out until it has flooded the button, and the label rides across on the
   front of it — out to the right, and back in from the left in white.
   Seven buttons carry it. Start a project on the home page; The full
   scope of services, See it built and Tell us your story on the about
   page; and WhatsApp, Call the studio and Send message on the contact
   page. The site has no others — services, projects and the four project
   pages ask with the round .fab in the corner, which is a disc with a
   turning ring and a different component entirely.

   ── the fill ──────────────────────────────────────────────────────
   .btn::before was already here doing this job, and the only thing that
   changed is the shape it is clipped to. It was inset(0 100% 0 0) →
   inset(0), a straight edge crossing the button left to right. It is now
   a circle grown from a point, which is the same journey in the same
   direction and a different thing to watch: a straight edge is a shutter
   and a circle is ink going into paper.

   circle() rather than a scaled element, which is how this effect is
   usually built. A dot scaled fifty times is a dot rasterised at eight
   pixels and then blown up, and its edge arrives soft and stepped at
   exactly the moment the eye is following it; a clip-path is resolved at
   the size it is drawn, so the front stays crisp the whole way across.
   It also needs no extra element, and it stays on the compositor.

   --hb-r, with 145% behind it. The radius has to reach the far corner
   from an origin that is not the centre, and a percentage on circle()
   resolves against sqrt(w²+h²)/sqrt(2) instead — the two do not track,
   and across the five buttons here the figure that just covers runs from
   124% to 135%, climbing with every pixel a button is wider than it is
   tall. Any single percentage is therefore the worst case plus a margin,
   and everything past the corner is time at the end of the transition
   when the button is already full: invisible on the way in, and a pause
   before anything moves on the way out.

   So js/hoverbtn.js measures each button and sets --hb-r to the exact
   distance in pixels, re-measuring when the webfonts land and on resize.
   The 145% is what holds until it does, and on a page where it does
   not — over-covering, but never short, which is the failure that
   shows.

   ── the curve ─────────────────────────────────────────────────────
   Neither of the page's own. This was written with --ease-drawer and
   before that --ease-out, and both were wrong in the same way: they are
   arrival curves, front-loaded, and what is arriving here is not an
   element but an edge. Worse, the coverage runs ahead of the radius
   anyway — the fill is a disc, so its area goes up with the square of
   how far the front has travelled, and the last third of the button is
   swallowed in a moment however evenly the radius grows.

   Measured on the narrowest pill, percentage of the button flooded:

              --ease-out   --ease-drawer   (.4,0,.2,1)
      10%          52%           40%            17%
      20%          78%           75%            27%
      30%          93%           93%            49%
      40%         100%          100%            72%
      60%         100%          100%            96%

   The site's own curve is three quarters done in a fifth of its
   duration. There was no bloom to watch — the button simply turned
   bronze, and the 520ms it was given were spent holding still.

   cubic-bezier(.4,0,.2,1) is the standard premium curve and it is very
   nearly linear through the first half, which is what a spreading front
   needs. 480ms, because the flood now finishes around 340 and the rest
   is the corners catching up.

   ── the label ─────────────────────────────────────────────────────
   Two copies. js/hoverbtn.js wraps what the markup already has — a
   <span> and an arrow — into .hb__face, clones it as .hb__face--in, and
   the two cross: the first leaves to the right and fades, the second
   arrives from the left and lands. Both travel with the fill rather than
   against it, and the direction is the one the arrow in them points.

   The incoming copy is 90ms behind, so it is still on its way in as the
   bronze reaches it and it is never seen as white type on paper.

   position:absolute with padding:inherit, not a centred overlay. An
   absolutely positioned child is laid out against the padding box, so
   inheriting the button's own padding puts the second copy's first
   letter on exactly the same pixel as the first copy's. Centre it
   instead and the label lands a few pixels left of where it started,
   which reads as the button being slightly out of register with itself.

   ── without the script ────────────────────────────────────────────
   .hb is added by js/hoverbtn.js and everything below hangs off it, so a
   page where that file does not run keeps a working button with a
   blooming fill and one label that does not travel. The script also
   returns on a touch screen — there is no hover to answer — and under
   reduced motion, where the rule at the foot of this block puts the fill
   back to a plain instant cover. */
.btn::before,
.ab__link::before,
.ab__cta::before{
  clip-path:circle(.28rem at 1.3rem 50%);
  transition:clip-path .48s cubic-bezier(.4,0,.2,1);
}
@media (hover:hover) and (pointer:fine){
  .btn:hover::before,
  .ab__link:hover::before,
  .ab__cta:hover::before{ clip-path:circle(var(--hb-r,145%) at 1.3rem 50%) }
}

/* The stage. .btn already clips; .ab__cta did not, and a bloom that is
   not clipped is a bronze circle sitting on the page. */
.hb{ position:relative; overflow:hidden }

/* .hb-scoped, and that is load-bearing rather than tidy. Both .btn and
   .ab__cta carry a `> *{ position:relative }` rule to lift their contents
   off the fill, and a bare .hb__face--in ties with it on specificity —
   so on css/about-page.css, which loads after this file, that rule won
   and the second copy was laid out in flow instead of over the first.
   The button came out at twice its width with its label sitting in the
   left half. One class deeper settles it wherever the sheets land. */
.hb .hb__face{
  position:relative; z-index:1;
  display:inline-flex; align-items:center;
  gap:inherit;                 /* .6rem on .btn, .7rem on .ab__cta */
  transition:transform .32s var(--ease-out), opacity .24s var(--ease-out);
}
.hb .hb__face--in{
  position:absolute; inset:0;
  padding:inherit;
  justify-content:flex-start;
  transform:translateX(-.9rem);
  opacity:0;
  color:#fff;
  transition:transform .4s var(--ease-out) .09s,
             opacity .3s var(--ease-out) .09s;
}
@media (hover:hover) and (pointer:fine){
  .hb:hover .hb__face{ transform:translateX(.9rem); opacity:0 }
  .hb:hover .hb__face--in{ transform:translateX(0); opacity:1 }
  /* Two buttons nudged their own arrow four pixels right on hover, which
     was this same gesture at the only scale that was available before.
     The whole face travels now, and the two together read as the arrow
     sliding out from under its own label. */
  .hb:hover .hb__face svg{ transform:none }
}

/* Reduced motion: the button still has to answer the pointer, so it
   still fills — it just does not perform the crossing. The fill is a
   cover rather than a bloom, and the second copy never moves, which is
   also what it looks like when js/hoverbtn.js has returned early. */
@media (prefers-reduced-motion:reduce){
  .btn::before,
  .ab__link::before,
  .ab__cta::before{ transition:none }
  .btn:hover::before,
  .ab__link:hover::before,
  .ab__cta:hover::before{ clip-path:inset(0) }
  .hb .hb__face,
  .hb .hb__face--in{ transition:none }
}

/* ── the light set ────────────────────────────────────────────────── */
/* Everything fixed on the page, told it is over a photograph. Added by
   js/chrome.js; without the script nothing here ever applies and the
   chrome stays brown on paper, which is correct everywhere except the
   first five screens of the home page. */
.is-over-media .brand__name,
.is-over-media .burger__label{ color:var(--on-media) }
.is-over-media .brand__sub{ color:var(--on-media-dim) }
.is-over-media .brand__rule{ background:rgba(244,241,236,.34) }
.is-over-media .burger__lines span{ background:var(--on-media) }
.is-over-media .brand__mark{ filter:drop-shadow(0 1px 3px rgba(0,0,0,.45)) }

/* ── the identity, whole, over the hero ───────────────────────────── */
/* Against the footage the brand is not a header item, it is the title
   card: twice the size, and set the way the identity is actually drawn
   in assets/img/logo.png — the monogram with the name beneath it rather
   than ranged alongside. The two-line lockup is kept, so the name is
   still the site's own face; it is the arrangement that changes.

   Ranged left, not centred. The printed logo is built on a centre axis,
   but it is drawn on its own white square — here it is one element in a
   composition where the eyebrow, the title and the lede are all hard
   left on the same gutter. Centring the lockup over that puts the
   monogram fifteen pixels inboard of a column of type that is otherwise
   dead straight, which is the kind of near-miss that reads as a mistake
   rather than as a decision. Flush left, the mark, both lines of the
   name and the whole hero share one edge.

   align-self:start rather than a taller bar. The stack is about twice
   the bar's height and has to hang below it, and growing --navh instead
   would take the burger down with it and move the one control in the
   chrome that should never move. The bar stays exactly the height it is
   everywhere else; only the brand leaves it.

   Scoped to .is-over-hero, NOT to .is-over-media, and the difference
   matters. Two separate things were being asked of one class: "you are
   on a picture, switch to the light set" and "you are on the opening
   footage, become the title card". They are not the same instruction.
   Every full-bleed picture on the site wants the first — a project
   plate, a project hero — and exactly one place wants the second.

   While they shared a class, scrolling a project plate under the bar
   grew the wordmark to twice its size in the middle of a page that has
   no hero to be a title card for. js/chrome.js now adds .is-over-hero
   only for [data-media-bg="hero"], which is the home page's opening
   track and nothing else. The light set below stays on .is-over-media
   and still applies everywhere. */
/* ── over the footage ─────────────────────────────────────────────
   The one place the brand changes shape: on the home page's opening
   track it becomes the mark, large, with the name set small underneath
   it. Everywhere else — every other page, and this page the moment the
   footage has scrolled past — it is the row in the bar, unchanged.

   Scoped to .is-over-hero, which js/chrome.js adds only for
   [data-media-bg="hero"]. A project plate scrolling under the bar gets
   .is-over-media and the light colour set, but never this.

   The stack hangs below the bar rather than growing it. --navh is the
   offset every page on the site lays itself out against, so raising it
   to fit one screen's logo would move the content of every other page
   down with it. align-self:start keeps the bar exactly 68px and lets
   only the brand leave it; the burger, which is the one control in the
   chrome that should never move, stays where it always is.

   Centred on the mark, not ranged left. The two lines are now a caption
   to the monogram rather than a column of type in their own right, and
   a caption belongs on its subject's axis — the earlier objection to
   centring applied when the name was set large enough to read as its
   own column, which is exactly what it has stopped being. */
.is-over-hero .brand{
  flex-direction:column;
  align-items:center;
  align-self:start;
  gap:.4rem;
  padding-top:clamp(.6rem,1.4vh,1rem);
}

/* Roughly double the 34px the bar carries, which is the whole point of
   the treatment: on the opening screen the mark is the identity and the
   words only confirm it. */
.is-over-hero .brand__mark{
  height:clamp(42px,5.4vw,62px);
  /* the shadow scales with the mark — a 3px blur under a 62px glyph is
     a hairline that does nothing */
  filter:drop-shadow(0 2px 9px rgba(0,0,0,.5));
}

/* Centred, so the shared right edge the bar's lockup uses is replaced
   by a centre axis. */
.is-over-hero .brand__lockup{
  align-items:center;
  gap:.5em;
}

/* Small, and off the display serif onto the sans. Marcellus at eleven
   pixels has nothing left to show — its modulation and serifs fall
   below a pixel and it reads as a slightly muddier sans — so the sans,
   tracked wide, is the version of small caps that survives the size.

   The two tracking values are picked so that five letters and eleven
   come out to nearly the same measure, which is what closes the block
   under the mark instead of leaving two lines of unrelated widths. */
.is-over-hero .brand__name{
  font-family:var(--sans);
  font-weight:500;
  font-size:var(--fs-body);
  letter-spacing:.32em;
  /* the tracking adds a trailing space after the last letter; without
     this the centred line sits half a space left of the axis */
  margin-right:-.32em;
  text-shadow:0 1px 6px rgba(0,0,0,.45);
}
.is-over-hero .brand__sub{
  font-size:var(--fs-micro);
  letter-spacing:.2em;
  margin-right:-.2em;
  text-shadow:0 1px 5px rgba(0,0,0,.45);
}

/* Eased rather than snapped: the colour set below already crosses over
   .4s at the hero's edge, and a lockup that jumps size while the tone
   fades reads as two separate events. Layout properties, so this is a
   relayout of one element — once, at a boundary the page is already
   redrawing. */
.brand{ transition:gap .35s var(--ease-out), padding-top .35s var(--ease-out) }
.brand__mark{ transition:opacity .3s var(--ease-out), height .35s var(--ease-out) }
.brand__name,
.brand__sub{ transition:color .3s var(--ease-out), font-size .35s var(--ease-out) }

@media (prefers-reduced-motion:reduce){
  .brand,
  .brand__mark,
  .brand__name,
  .brand__sub{ transition:none }
}

/* Over footage the seal needs separating from whatever is behind it,
   and it used to be done with a wide soft shadow — 5px of blur under
   the ring and 6px under the disc. At this size that is not a lit edge,
   it is a smear: the type is nine pixels tall, so a halo half its own
   height reads as the letter being out of focus rather than raised off
   the picture. The comment on .fab__ch below made exactly this argument
   for the paper state and then the media state did it anyway.

   Tight and dark instead. One or two pixels of offset, no spread worth
   the name, and the opacity carrying the contrast that the radius used
   to. It separates just as well and stays sharp. */
.is-over-media .fab__ch{
  color:var(--on-media-accent);
  text-shadow:0 1px 2px rgba(0,0,0,.8);
}
.is-over-media .fab__disc{
  color:var(--on-media);
  border-color:rgba(244,241,236,.62);
  filter:drop-shadow(0 1px 2px rgba(0,0,0,.62));
}

/* ═══ persistent contact seal ═════════════════════════════════════ */
/* Fixed bottom-right for the whole session. Sits under the nav in the
   layer stack so the menu overlay covers it, and clear of the
   right-hand progress rail (vertically centred) and the scroll cue
   (bottom-centre).

   A seal, not a pill. The words run around a ring that turns; the disc
   and the arrow inside it stand still, because a target that moves is a
   target that is harder to hit. js/fab.js builds the ring and drives
   the rotation — the rest of the behaviour is here.

   Three transforms, three elements, on purpose: js/transformation.js
   parks and reveals the anchor, the hover lift is on the disc, and the
   rotation is on the ring. Stacked on one element they would overwrite
   each other. */
.fab{
  position:fixed; z-index:var(--z-fab);
  right:max(var(--gut), env(safe-area-inset-right));
  bottom:calc(clamp(1rem,3.5vh,1.75rem) + env(safe-area-inset-bottom));

  /* --fab: the diameter, and the one number the rest is derived from —
     js/fab.js reads it too, so the ring is laid out to whatever the
     stylesheet says at that breakpoint. */
  --fab:128px;
  display:grid; place-items:center;
  width:var(--fab); height:var(--fab);
  border-radius:50%;
  color:var(--bone);

  /* Parked out of view. JS adds .is-in once the page has settled so the
     seal doesn't compete with the hero title on first paint.
     Never scale from 0 — it would read as appearing out of nothing. */
  opacity:0; transform:translateY(10px) scale(.95);
  pointer-events:none;
  /* exit timing (fast) — the enter override below slows it down */
  transition:opacity .18s var(--ease-out), transform .18s var(--ease-out);
}
.fab.is-in{
  opacity:1; transform:none; pointer-events:auto;
  transition-duration:.34s, .34s;
}
/* Out over the footer, and only there.

   This rule has been removed and restored once. It originally hid the
   seal over the enquiry CTA as well, which was too eager — the button
   was disappearing halfway down a page it was meant to sit on, so all of
   it went. What is left is the case that was always right: the footer
   carries every number, every address and every social the seal is a
   shortcut to, and a floating shortcut parked on top of its own
   destination reads as a bug. It also lands exactly on the sign-off.

   The enquiry block is no longer watched — see js/fab.js. */
.fab.is-out{
  opacity:0; transform:translateY(10px) scale(.95); pointer-events:none;
  transition-duration:.18s;
}

/* ── the ring ─────────────────────────────────────────────────────── */
/* Spans the whole button; every character is absolutely placed against
   its centre by js/fab.js. Empty until the script runs, and nothing is
   lost if it never does — the disc and the arrow are the button, the
   ring is the invitation. */
.fab__ring{
  position:absolute; inset:0;

  /* No will-change here, deliberately. The ring turns continuously, and
     the hint promoted it to a layer that is rasterised once and then
     re-used for every frame of the rotation — so the glyphs were being
     painted at one angle and shown at all the others, which is its own
     kind of soft. The browser still gives an animating transform a
     layer when it needs one; it just re-rasterises it, and nine-pixel
     type is small enough to need that. */
}
.fab__ch{
  position:absolute; left:50%; top:50%;
  font-family:var(--sans);
  font-size:var(--fs-micro);
  /* Each glyph is individually rotated, which costs subpixel
     antialiasing — the browser falls back to greyscale, and greyscale
     eats about a quarter-pixel off every stem. A step of weight puts it
     back without changing the size or the colour. */
  font-weight:600;
  letter-spacing:.04em;
  text-transform:uppercase;
  color:var(--bronze-lt);
  /* No shadow on paper: brown type on warm off-white is already legible,
     and a glow under it at this size reads as a blurred letter rather
     than a lit one. The halo comes back — dark, under light type — only
     over the footage. */
  text-shadow:none;
  transition:color .3s var(--ease-out);
}

/* ── the inner circle ─────────────────────────────────────────────── */
/* Drawn, not filled: a hairline and the arrow, with the page showing
   through. The ring of type around it is already the weight — a solid
   bronze coin inside it made two objects competing for the same middle,
   and the seal read as a badge stuck onto the page rather than struck
   into it.

   Unfilled costs the legibility a fill was buying, since this sits over
   the hero footage. Bought back with shadow instead of ground: a drop
   under the rule and under the glyph, which darkens what is behind them
   without putting a colour in front of it. */
.fab__disc{
  position:relative;
  display:grid; place-items:center;
  width:calc(var(--fab) * .48); height:calc(var(--fab) * .48);
  border-radius:50%;
  background:none;
  border:1px solid rgba(139,98,57,.5);
  color:var(--bone);
  filter:none;
  transition:transform .3s var(--ease-out), border-color .3s var(--ease-out);
}
.fab__icon{ width:16px; height:16px; display:block }

@media (hover:hover) and (pointer:fine){
  .fab.is-in:hover .fab__disc{
    transform:scale(1.08);
    border-color:var(--bronze-hi);
  }
  .fab.is-in:hover .fab__ch{ color:var(--bronze-hi) }
}
/* Press feedback on the disc, which is the part that reads as the
   button — and it must not fight the hover scale above. */
.fab.is-in:active .fab__disc{ transform:scale(.95); transition-duration:.14s }

.fab:focus-visible{ outline:2px solid var(--bronze-hi); outline-offset:6px }

/* narrow phones: a smaller seal, not a different one — every size below
   is derived from --fab, so this is the only number that changes */
@media (max-width:420px){
  .fab{ --fab:104px }
  .fab__ch{ font-size:var(--fs-mark) }
  .fab__icon{ width:14px; height:14px }
}

/* landscape phones have almost no vertical room — shrink, don't remove */
@media (max-height:480px) and (orientation:landscape){
  .fab{ --fab:96px }
  .fab__ch{ font-size:var(--fs-mark) }
}

/* No script, no ring — the disc and the arrow still read as a button,
   and aria-label still says what it is. */
.no-js .fab{ opacity:1; transform:none; pointer-events:auto }

/* ═══ logo watermark ══════════════════════════════════════════════ */
/* Applied to a section that is position:relative and creates its own
   stacking context. z-index:-1 keeps it behind the section's own copy
   without escaping behind the page background. */
.wm{ position:relative; overflow:hidden }
.wm::before{
  content:""; position:absolute; z-index:var(--z-watermark);
  pointer-events:none;
  opacity:var(--wm);

  /* Cropped to the AD monogram only. Two reasons: the logo's own wordmark
     reads "Arcos Design Firm", which would contradict the site wordmark,
     and a second legible wordmark competes with the body copy behind it.
     A watermark should be a mark.

     The crop is done with background-size/position rather than clip-path
     so the box itself is exactly the visible mark — positioning each
     instance stays predictable. Values map the monogram's pixel box
     (x 180–415, y 125–255 of the 568×439 source) onto the element.
     Source aspect is preserved, so nothing is stretched.

     To use the FULL logo instead, replace the background line with:
       background:url("../assets/img/logo.png") no-repeat center/contain;
     and swap every --wm-ar below back to 568/439. */
  background:url("../assets/img/logo.png") no-repeat 54% 40% / 232% 318%;
  aspect-ratio:var(--wm-ar, 245/138);

  /* the source is a transparent PNG, so no plate shows behind it */
}


/* ═══ shared type ═════════════════════════════════════════════════ */
.eyebrow{
  display:flex; align-items:center; gap:.85rem;
  font-size:var(--fs-micro); letter-spacing:.32em; text-transform:uppercase;
  color:var(--bronze-lt);
  margin-bottom:clamp(.9rem,2.4vh,1.6rem);
}
.eyebrow__rule{ width:clamp(28px,5vw,64px); height:1px; background:var(--bronze) }
/* Green on the hero's own eyebrow, and nowhere else. The rule is a
   hairline, so on paper the brand green would be a 1.75:1 thread nobody
   sees — but here it sits on near-black at better than 13:1, a few
   inches under the green wedge in the monogram, and the two finally
   answer each other.

   Scoped to .tf__intro, not to .is-over-media alone. That class is on
   the root, so on its own it would also turn the eyebrow above the
   reviews green — a rule eight thousand pixels down the page, painted a
   colour it can only hold while it is nowhere near the fold, and back
   to bronze by the time anyone sees it. It never shows, which is
   exactly why it would have gone unnoticed. */
.is-over-media .tf__intro .eyebrow__rule{ background:var(--olive) }

.display{
  font-family:var(--display);
  font-weight:400;
  font-optical-sizing:auto;
  line-height:.86;
  letter-spacing:-.015em;
  text-transform:uppercase;
}
.display em{
  font-style:normal;
  background:linear-gradient(180deg,var(--bronze-hi) 0%,var(--bronze) 52%,var(--bronze-dp) 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
}

/* ── word-by-word fill ────────────────────────────────────────────── */
/* Any [data-fill] paragraph is split into words by js/intro.js and lit
   as it scrolls. Shared by the collage sections, so the reading pace
   feels the same wherever the device is used. */
[data-fill] .w{
  color:rgba(74,59,41,.17);
  /* .32s before. The word is the smallest unit the fill has, and at a
     third of a second each one was still arriving after the eye had
     reached the next two. */
  transition:color .22s var(--ease-out);
}
[data-fill] .w.on{ color:var(--bone) }

/* line masks, so display type can rise out of nothing */
.line{ display:block; overflow:hidden; padding-bottom:.06em; margin-bottom:-.06em }
.line__i{ display:block; will-change:transform }

/* ═══ reduced motion ══════════════════════════════════════════════ */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.01ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.01ms !important;
  }
}

/* ── a written line ───────────────────────────────────────────────── */
/* The <svg> js/signature.js puts in place of a line of type. Global
   because two pages carry one — the aside on the home page and the
   tagline that closes the about page — and it is the element itself
   that is the same, not the block it sits in. Sized in em by the
   script, so whatever font-size the host paragraph was given goes on
   scaling the drawing of the words exactly as it scaled the words. */
.sig{
  display:block;
  height:auto;             /* the viewBox keeps the ratio */
  max-width:100%;          /* a narrow column crops nothing, it shrinks */
  /* The fills are currentColor, and this stands in for a line of type —
     so it takes the colour that line was given rather than naming one of
     its own. It was pinned to --bone, which held the drawing in brown
     after the hand it replaces went green. */
  color:inherit;
  overflow:visible;
}

/* ═══════════════════════════════════════════════════════════════════
   The cursor ring

   A soft circle that follows the pointer a beat behind it. Sage, not
   bronze and not blue: the bronze is already doing every accent on this
   site and a brown ring would read as one more piece of the furniture,
   while blue belongs to nothing here at all. This is the olive out of
   the logo mark, desaturated and lightened until it is barely a colour
   — present enough to be seen moving, quiet enough that it never
   competes with a photograph it happens to be crossing.

   The native cursor is hidden and this replaces it. Two things make
   that safe rather than merely fashionable:

   The hiding is gated behind .has-ring, which js/cursor.js puts on
   <html> only once the ring actually exists. Written as a bare
   `cursor:none` in the stylesheet it would apply on any page where the
   script failed to load, and the reader would be left steering a page
   with nothing on screen at all — the one failure mode worth designing
   around here.

   And the ring tracks tightly. It still trails, because the trail is
   the effect, but at a rate that has it under the pointer within a few
   frames of stopping. A soft follow is charming on an ornament and
   unusable on the thing you aim with; see EASE in js/cursor.js.
   ═══════════════════════════════════════════════════════════════════ */

/* !important because the site sets cursor:pointer, :crosshair, :zoom-out
   and :progress in a dozen places, and every one of them would beat a
   plain rule here. The whole point is that there is exactly one pointer
   on screen now, so nothing may opt back in. */
@media (hover:hover) and (pointer:fine){
  html.has-ring,
  html.has-ring *{ cursor:none !important }
}
:root{
  /* The one knob. Both values are the same hue; the fill is the ring's
     own colour at a fraction of its strength. */
  --cursor-line: rgba(138,148,116,.78);
  --cursor-fill: rgba(168,177,146,.14);

  /* And two that are not a colour so much as a guarantee. A single
     translucent line has no contrast it can count on: it is crossing
     photographs, and somewhere in this portfolio is a sage-green
     kitchen and a wall of olive panelling that it will vanish into
     completely. A dark hairline outside it and a light one inside mean
     one of the three edges always separates, whatever is behind. Both
     are weak enough to read as depth rather than as a second ring. */
  --cursor-edge-dark:  rgba(24,20,15,.34);
  --cursor-edge-light: rgba(255,255,255,.30);
}

.cursor{
  position:fixed;
  top:0; left:0;
  z-index:400;              /* over the page and the seal, under the
                               lightbox at 500 and the loader at 1000 */
  width:30px; height:30px;
  margin:-15px 0 0 -15px;   /* centred on the point, not hung off it */
  border-radius:50%;
  border:1px solid var(--cursor-line);
  background:var(--cursor-fill);
  box-shadow:0 0 0 1px var(--cursor-edge-dark),
             inset 0 0 0 1px var(--cursor-edge-light);
  /* Never eat a click, a hover or a text selection. Without this the
     ring would sit between the reader and every link on the site. */
  pointer-events:none;
  opacity:0;
  /* Only the size and the fade transition. The position is written every
     frame by js/cursor.js and must never be eased twice. */
  transition:opacity .3s var(--ease-out),
             width .3s var(--ease-out),
             height .3s var(--ease-out),
             margin .3s var(--ease-out),
             border-color .3s var(--ease-out),
             border-width .3s var(--ease-out),
             box-shadow .3s var(--ease-out),
             background-color .3s var(--ease-out);
  will-change:transform;
}

/* Shown once the pointer has actually moved — see the note in
   js/cursor.js about why it does not appear at load. */
.cursor.is-live{ opacity:1 }

/* Opened up over anything that answers a pointer. The ring is the only
   part of the page that can say "this is a control" without drawing
   anything on the control itself. */
/* This used to open the ring and fade it at the same time — 52px across
   and the line dropped from .62 to .42. The reasoning was that a bigger
   ring needs less weight to carry, which is true on paper and wrong on
   a photograph: almost everything worth hovering on this site is a
   picture, so the state that mattered most was the one that could least
   be seen. It opens and firms up now. Two pixels of line rather than
   one, close to opaque, and the edges either side of it stronger to
   match. */
.cursor.is-over{
  width:52px; height:52px;
  margin:-26px 0 0 -26px;
  border-width:2px;
  border-color:rgba(146,158,120,.95);
  background:rgba(168,177,146,.17);
  box-shadow:0 0 0 1px rgba(24,20,15,.42),
             inset 0 0 0 1px rgba(255,255,255,.38);
}

/* Pressed. A cursor that does not answer a click feels dead now that it
   is the only cursor — this is the whole of the click feedback the
   pointer itself gives. */
.cursor.is-down{
  width:22px; height:22px;
  margin:-11px 0 0 -11px;
  background:rgba(168,177,146,.30);
  border-color:rgba(146,158,120,.98);
}

/* Off entirely where it would be wrong or pointless: no pointer to
   follow on a touch screen, and nothing to gain from a trailing circle
   for a reader who has asked for less motion. Both are handled in the
   script as well — this is the half that costs nothing if the script
   never runs. */
@media (hover:none), (pointer:coarse){
  .cursor{ display:none }
}
@media (prefers-reduced-motion:reduce){
  .cursor{ display:none }
}

/* ═══════════════════════════════════════════════════════════════════
   The curtain, between pages

   Opening a project plays the same move the loading screen plays on the
   way out: a curtain travelling upward, 0.8s on ease-in-out, with the
   picture behind it drifting toward you as it goes. See the long note
   in css/loader.css for why that curve — briefly, a curtain is heavy
   and hung, so it takes a moment to start, gathers, and arrives without
   slamming.

   The difference is that a loading screen only ever leaves, while this
   has to arrive first. So the motion is split across the navigation:
   the curtain rises to cover the page you are leaving, the browser
   changes document underneath it, and it keeps rising off the page you
   have arrived at. Both halves travel the same direction at the same
   speed, so what the reader sees is one continuous sweep with a new
   page behind it — not two animations that happen to meet.

   Ground and timings are taken from the loader rather than reinvented:
   #2A2621 is the tone it sits on before its photograph decodes, and the
   0.8s / ease-in-out pairing is the same decision. If one moves, both
   should.
   ═══════════════════════════════════════════════════════════════════ */
.curtain{
  position:fixed;
  inset:0;
  z-index:900;              /* over the lightbox at 500, under the
                               loading screen at 1000, which owns the
                               first paint on the home page */
  background:#2A2621;
  pointer-events:none;      /* never eats a click, even mid-sweep */
  /* Parked fully open below the fold. inset() measures from each edge,
     so 100% from the top is a curtain with no height, waiting at the
     bottom of the screen. */
  clip-path:inset(100% 0 0 0);
  visibility:hidden;
}

/* ── rising to cover, on the way out ─────────────────────────────── */
.curtain.is-closing{
  visibility:visible;
  clip-path:inset(0 0 0 0);
  transition:clip-path .55s var(--ease-in-out);
}

/* ── still rising, on the way in ─────────────────────────────────── */
/* Set covering with no transition on the first frame, then released —
   see js/curtain.js, which has to force a reflow between the two or the
   browser collapses them into no animation at all. */
.curtain.is-covering{
  visibility:visible;
  clip-path:inset(0 0 0 0);
}
.curtain.is-opening{
  visibility:visible;
  clip-path:inset(0 0 100% 0);
  transition:clip-path .8s var(--ease-in-out);
}

/* A reader who has asked for less motion gets the new page, not the
   performance of arriving at it. The script returns early too; this is
   the half that holds if it ever does not. */
@media (prefers-reduced-motion:reduce){
  .curtain{ display:none }
}
