/* PROVI — shared design system. Used by every page, WebGL or not. */

/* Bruno Ace for display: squared, architectural, and built to be set in
   caps. It ships in ONE weight (400) and has no width axis, so nothing here
   asks it for bold — a synthetic bold would thicken the strokes unevenly and
   fill in its corners, which is most of what gives the face its character.
   Presence comes from size and tracking instead.

   IBM Plex Sans for body. Bruno Ace is geometric and cold; Plex is a
   humanist technical face, so it warms the reading text without competing,
   and its open apertures hold up at the small dim sizes this page uses. */
@import url('https://fonts.googleapis.com/css2?family=Bruno+Ace&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap');

:root{
  --ink:#0A0A0C;
  --ink-2:#141416;
  --ink-3:#1C1C1F;
  --paper:#F2F1EC;
  --paper-dim:#8C8C90;
  --line:rgba(242,241,236,.10);
  --line-strong:rgba(242,241,236,.20);
  /* ---------- the highlight ----------
     THERE ISN'T ONE. No hue anywhere in the interface. The site went gold,
     then magenta, then violet, and every round was an argument about which
     colour to spend against a page made entirely of grey grain. The answer
     was none: emphasis comes from BRIGHTNESS and TEXTURE instead.

     This is also the only version that agrees with the rest of the brand.
     The scene has always been strictly achromatic — every shader is single
     channel, distance falls to true black rather than haze — and the accent
     was the one place the site broke its own rule. Now it does not.

     The tokens keep their names on purpose. Fifty-odd rules across five
     stylesheets reference them, and renaming every one to say "bright" would
     be a large diff that changes nothing about what renders. What changed is
     what they hold.

     Every value is a grey ALREADY IN USE on the site, so the accent tier is
     the top of the existing scale rather than a new one bolted beside it:

       --accent-text  #DEDCD4  14.41:1  the nav wordmark's own brightness
       --accent       #9C9A94   7.03:1  a step above body copy at 5.58
       --accent-dim   #6A6A6F   3.68:1  the resting tone of a nav link,
                                        and above the 3:1 floor for borders */
  --accent:#9C9A94;
  --accent-text:#DEDCD4;
  --accent-dim:#6A6A6F;
  /* THE DIMMEST TEXT TONE THAT STILL PASSES AA. The site had small type set
     in #55555A (2.67:1) and in --accent-dim (3.68:1). --accent-dim is correct
     for BORDERS, where the floor is 3:1, and was never meant to carry text;
     at 11px a nav link in it fails the 4.5:1 body-text requirement outright.
     #85858A measures 5.08:1 on ink, so captions, footers and resting nav
     links stay visibly quieter than body copy without dropping below AA. */
  --paper-faint:#85858A;
  --accent-soft:rgba(242,241,236,.07);
  --accent-glow:rgba(242,241,236,.20);

  /* The film grain, as a token. The same fractal turbulence the .grain
     overlay and the display type already use, hoisted out so any surface can
     carry it. With no colour left to mark something as important, texture is
     the other half of the job.

     SIZE IS THE DIAL, NOT FREQUENCY. The turbulence itself is unchanged; what
     makes the grain coarse is rendering its 120px tile at 480, which scales
     every speck four times. Lowering baseFrequency instead produces large
     smooth blobs that read as camouflage rather than as grain, and thresholding
     the alpha into discrete dots produced specks too sparse to see at all.
     Both were tried. Scaling the same texture is what actually matches the
     grain in the 3D scenes, which is the reference: those are real point
     sprites at 7 to 16 device pixels, not film noise. */

  --font-display:"Bruno Ace", "Helvetica Neue", Arial, sans-serif;
  --font-body:"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif;

  --nav-h:76px;
  --pad:clamp(20px, 5vw, 96px);
  --max:1320px;
}

*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth;background:var(--ink)}
/* Lenis drives its own eased scrollTo every frame; native smooth-scroll
   fights it (each Lenis step gets re-smoothed by the browser on top of
   Lenis's own easing), which is what makes scroll input feel almost
   completely absorbed. Lenis tags <html> with these classes while active. */
html.lenis,html.lenis body{height:auto}
.lenis.lenis-smooth{scroll-behavior:auto!important}
.lenis.lenis-stopped{overflow:hidden}
body{
  background:var(--ink);
  color:var(--paper);
  font-family:var(--font-body);
  font-weight:400;
  line-height:1.6;
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}
img{display:block;max-width:100%}
a{color:inherit;text-decoration:none}
button{font:inherit;background:none;border:none;color:inherit}
ul{list-style:none}

::selection{background:var(--accent);color:var(--ink)}
::-moz-selection{background:var(--accent);color:var(--ink)}

.wrap{max-width:var(--max);margin-inline:auto;padding-inline:var(--pad)}

h1,h2,h3,h4{
  font-family:var(--font-display);
  font-weight:400;              /* Bruno Ace has no other weight */
  letter-spacing:.01em;
  line-height:1.12;
}

a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{
  outline:2px solid var(--accent);
  outline-offset:3px;
}

/* NO GRAIN OVERLAY. The page used to carry a fixed film-grain layer over
   every surface, and a second, coarser one on every accent surface. Both are
   gone: at the sizes they had shipped at they read as haze and dirt on the
   flat dark panels rather than as film, and they sat on top of the one thing
   that should own the texture — the point-grain in the 3D scenes, which is
   real geometry and is untouched. Emphasis is now brightness alone. */

/* ---------- floating nav ----------
   No bar, no background, no border: two pieces of text sitting on top of
   whatever the page is showing. On the homepage that is a moving canyon, so
   anything with a fill would read as a panel bolted over the scene.

   The wordmark starts centred and docks left as the hero clears. It moves on
   `transform` alone rather than by changing `left` or the grid, because those
   two either animate unevenly or not at all. The element stays in normal flow
   at the left padding edge the whole time; the untucked state just shifts it
   by half the viewport less half its own width. */
.pnav{
  position:fixed;inset:0 0 auto 0;z-index:100;
  height:var(--nav-h);
  display:flex;align-items:center;
  padding-inline:var(--pad);
  /* the bar itself must not intercept clicks aimed at the scene behind it */
  pointer-events:none;
}
.pnav-mark{
  pointer-events:auto;
  font-family:var(--font-display);font-weight:400;
  font-size:1rem;letter-spacing:.24em;
  color:#DEDCD4;
  text-shadow:0 0 20px rgba(10,10,12,.85);
  transform:translateX(calc(50vw - var(--pad) - 50%));
  transition:transform 1.1s cubic-bezier(.22,.61,.36,1), color .3s;
}
.pnav-mark:hover{color:#F2F1EC}
.pnav-links{
  margin-left:auto;
  display:flex;align-items:center;gap:clamp(1.25rem,2.6vw,2.4rem);
  opacity:0;transform:translateY(-5px);
  pointer-events:none;
  transition:opacity .7s ease .25s, transform .7s cubic-bezier(.22,.61,.36,1) .25s;
}
.pnav-links a{
  font-family:var(--font-display);font-weight:400;
  font-size:.66rem;letter-spacing:.2em;text-transform:uppercase;
  color:var(--paper-faint);
  text-shadow:0 0 18px rgba(10,10,12,.9);
  transition:color .3s;
}
/* The current page takes the highlight. It was the same paper as every other
   link, so the nav said nothing about where you were. */
.pnav-links a:hover{color:#DEDCD4}
.pnav-links a[aria-current="page"]{color:var(--accent-text)}

/* Docked: wordmark home to the left edge, links faded up on the right.
   Every page except the homepage ships with this class already set, since
   they have no hero to wait on. */
.pnav.is-docked .pnav-mark{transform:translateX(0)}
.pnav.is-docked .pnav-links{opacity:1;transform:none;pointer-events:auto}

@media(prefers-reduced-motion:reduce){
  .pnav-mark,.pnav-links{transition-duration:.001s}
}
@media(max-width:560px){
  .pnav-links{gap:1.1rem}
  .pnav-links a{font-size:.6rem;letter-spacing:.14em}
}
/* Bruno Ace is a wide face and the links are three long words. On a 390px
   phone the wordmark plus MARKETPLACE ran about 353px into 350px of usable
   width, so the mark and the first link collided. Nothing here wraps — the
   bar is a fixed-height flex row — so the type has to come down instead. */
@media(max-width:430px){
  .pnav-mark{font-size:.82rem;letter-spacing:.18em}
  .pnav-links{gap:.8rem}
  .pnav-links a{font-size:.52rem;letter-spacing:.1em}
}

/* ---------- nav scrim, flat pages only ----------
   A scene page floats the wordmark over grain with nothing behind it, which
   is the point. A flat page scrolls paragraphs and cards underneath it, and
   without something between the two the links sit on top of the copy. The
   scrim is a gradient rather than a bar: it has no bottom edge, so the nav
   still reads as floating rather than as a header bolted on.

   Painted on a pseudo-element so it can fade independently of the links, and
   pointer-events stay off the whole bar. */
body.flat .pnav::before{
  content:"";position:absolute;inset:0;
  pointer-events:none;
  background:linear-gradient(180deg,
    rgba(10,10,12,.92) 0%, rgba(10,10,12,.78) 55%, rgba(10,10,12,0) 100%);
  -webkit-backdrop-filter:blur(6px);
  backdrop-filter:blur(6px);
  /* the blur has to stop where the paint does, or it smears a hard-edged
     rectangle of blurred content across the top of the page */
  -webkit-mask-image:linear-gradient(180deg,#000 55%,transparent 100%);
  mask-image:linear-gradient(180deg,#000 55%,transparent 100%);
  opacity:0;
  transition:opacity .35s ease;
}
body.flat .pnav.is-scrolled::before{opacity:1}
/* the mark and links have to sit above their own scrim */
.pnav-mark,.pnav-links{position:relative}

main{padding-top:var(--nav-h);position:relative;z-index:1}

/* ---------- buttons ---------- */
.btn{
  display:inline-flex;align-items:center;gap:.6rem;
  font-size:.86rem;font-weight:600;letter-spacing:.01em;
  padding:.9rem 1.6rem;border-radius:2px;
  cursor:pointer;transition:background .25s,color .25s,border-color .25s;
  position:relative;overflow:hidden;
}
.btn-fill{
  /* A GREY SLAB. With no hue and no texture left, a filled button is marked
     out by brightness and by being the only lit surface on the page.

     A gradient rather than a flat fill, because a light source in a surface
     reads as an object. Paper label on it, checked at both ends:
     #4A4A4E is 7.80:1 and #2C2C30 is 12.30:1, so the text clears AA across
     the whole face with a wide margin. */
  background:linear-gradient(160deg,#4A4A4E 0%,#3E3E42 48%,#2C2C30 100%);
  color:var(--paper);
  position:relative;
  overflow:hidden;
  box-shadow:0 6px 26px -12px var(--accent-glow);
  transition:filter .25s,box-shadow .25s,color .25s,border-color .25s;
}
.btn-fill:hover{
  filter:brightness(1.22);
  box-shadow:0 9px 34px -10px var(--accent-glow);
}
.btn-line{border:1px solid var(--line-strong);color:var(--paper)}
.btn-line:hover{border-color:var(--accent);color:var(--accent-text);background:var(--accent-soft)}

/* ---------- generic page sections (standard pages) ---------- */
.band{padding-block:clamp(4rem,11vh,7rem)}
.band-hairline{border-top:1px solid var(--line)}
.kicker{
  font-size:.76rem;letter-spacing:.14em;text-transform:uppercase;
  color:var(--accent-text);margin-bottom:1.25rem;font-weight:600;
}
/* Caps, because a display face this geometric reads as lettering rather
   than as text once it is set in sentence case. Sizes come down from the
   Archivo settings: Bruno Ace is much wider per character. */
.display{
  font-family:var(--font-display);font-weight:400;
  text-transform:uppercase;
  font-size:clamp(1.6rem,3.4vw,2.5rem);
  letter-spacing:.015em;line-height:1.14;
}
.lede{
  color:var(--paper-dim);font-size:1.05rem;line-height:1.75;
  max-width:38rem;margin-top:1.25rem;
}
.center{text-align:center;margin-inline:auto}

/* ---------- placeholder pages ----------
   Everything except the homepage is a holding screen for now. Sized off the
   viewport rather than a fixed rem value so the word fills the screen the way
   the homepage headline does, and capped so it cannot outgrow an ultrawide. */
.soon{
  min-height:100vh;
  display:flex;flex-direction:column;
  align-items:center;justify-content:center;
  text-align:center;padding:var(--nav-h) var(--pad) 12vh;
}
.soon-word{
  font-family:var(--font-display);font-weight:400;
  text-transform:uppercase;
  font-size:clamp(2.4rem,9vw,7rem);
  letter-spacing:.02em;line-height:1.05;
  color:#DEDCD4;
  text-shadow:0 0 40px rgba(10,10,12,.9);
}
.soon-sub{
  margin-top:1.6rem;
  font-size:.7rem;letter-spacing:.24em;text-transform:uppercase;
  font-family:var(--font-display);
  color:var(--paper-faint);
}
.soon-back{
  margin-top:3rem;
  font-size:.66rem;letter-spacing:.2em;text-transform:uppercase;
  font-family:var(--font-display);
  color:var(--paper-faint);transition:color .3s;
}
.soon-back:hover{color:#DEDCD4}

/* ---------- footer ---------- */
.foot{border-top:1px solid var(--line);padding-block:clamp(3rem,8vh,5rem);position:relative;z-index:1}
.foot-grid{display:flex;flex-wrap:wrap;justify-content:space-between;gap:2.5rem}
.foot-word{font-family:var(--font-display);font-weight:400;font-size:1.05rem;letter-spacing:.08em;margin-bottom:.75rem}
.foot-meta{font-size:.86rem;color:var(--paper-dim);line-height:1.9}
.foot-links{display:flex;flex-direction:column;gap:.6rem}
.foot-links a{font-size:.86rem;color:var(--paper-dim)}
.foot-links a:hover{color:var(--accent-text)}

/* ---------- ambient 3D backdrop canvas (supporting pages) ---------- */
#ambientCanvas{
  position:fixed;inset:0;z-index:0;
  display:block;
}

/* ---------- card grids (templates, work) ---------- */
.card-grid{
  display:grid;grid-template-columns:1fr;gap:1.75rem;
  margin-top:3rem;
}
@media(min-width:700px){.card-grid{grid-template-columns:1fr 1fr}}
.card{
  border:1px solid var(--line);
  background:rgba(20,20,22,.5);
  overflow:hidden;
  transition:border-color .25s,transform .35s;
}
.card:hover{border-color:var(--accent-dim)}
.card:hover .card-title{color:var(--accent-text);transition:color .25s}
.card-media{
  aspect-ratio:16/10;overflow:hidden;background:var(--ink-2);
}
.card-media img{width:100%;height:100%;object-fit:cover;object-position:top;transition:transform .5s ease}
.card:hover .card-media img{transform:scale(1.03)}
.card-body{padding:1.5rem 1.6rem 1.75rem}
.card-tag{font-size:.72rem;letter-spacing:.1em;text-transform:uppercase;color:var(--accent-text)}
.card-title{font-family:var(--font-display);font-weight:400;font-size:1.05rem;letter-spacing:.03em;text-transform:uppercase;margin-top:.5rem}
.card-desc{color:var(--paper-dim);font-size:.92rem;line-height:1.65;margin-top:.6rem}
.card-meta{display:flex;justify-content:space-between;align-items:center;margin-top:1.4rem}
/* The number is what the reader came for on a page that posts prices, so it
   stops being body-coloured and takes the highlight. */
.card-price{font-weight:600;color:var(--accent-text);font-size:1.05rem}

/* ---------- forms ---------- */
.form-shell{max-width:520px;margin-top:2.5rem}
.field{display:block;margin-bottom:1.6rem}
.field span{
  display:block;font-size:.76rem;letter-spacing:.08em;text-transform:uppercase;
  color:var(--paper-dim);margin-bottom:.55rem;
}
.field input,.field textarea,.field select{
  width:100%;background:rgba(20,20,22,.6);border:1px solid var(--line-strong);
  color:var(--paper);font:inherit;font-size:.98rem;padding:.75rem .9rem;
  border-radius:2px;
}
.field select{appearance:none}
.field input:focus,.field textarea:focus,.field select:focus{border-color:var(--accent);outline:none}
.field textarea{resize:vertical;min-height:110px}
.form-note{color:var(--paper-dim);font-size:.86rem;margin-bottom:1.75rem}

/* ---------- dictionary entries (about) ---------- */
.dict-entry{border-top:1px solid var(--line);padding-block:2.25rem}
.dict-entry:last-child{border-bottom:1px solid var(--line)}
.dict-word{
  font-family:var(--font-display);font-weight:400;font-size:1.25rem;
  letter-spacing:.04em;text-transform:uppercase;
  display:flex;align-items:baseline;gap:.75rem;flex-wrap:wrap;
}
.dict-pos{font-size:.82rem;color:var(--paper-dim);font-weight:400;font-style:italic}
.dict-root{color:var(--paper-dim);font-size:.92rem;margin-top:.5rem;line-height:1.7}
.dict-def{margin-top:1rem}
.dict-def ol{margin-left:1.1rem;list-style:decimal}
.dict-def li{color:var(--paper-dim);font-size:.98rem;line-height:1.7;margin-top:.4rem}

/* ---------- stat / spec rows ---------- */
.spec-rows{max-width:640px;margin-top:2.5rem}
.spec-row{
  display:grid;grid-template-columns:9rem 1fr;gap:1.5rem;
  padding-block:1rem;border-top:1px solid var(--line);
}
.spec-row:last-child{border-bottom:1px solid var(--line)}
.spec-row dt{font-size:.76rem;letter-spacing:.08em;text-transform:uppercase;color:var(--accent-text)}
.spec-row dd{font-size:.95rem;line-height:1.6}

@media(prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
}

/* ---------- standing scroll marker ----------
   Bottom-right indicator, shared by the homepage and the About page. Its
   opacity is written per frame by whichever engine owns that page's scroll:
   js/scene.js on the homepage, js/depth-scroll.js on About. Fixed rather
   than absolute so it holds its corner while the track scrolls, and
   z-indexed above the canvases but below the film grain overlay (200). */
.scroll-persist{
  position:fixed;right:clamp(20px,3.5vw,54px);bottom:clamp(18px,4vh,40px);
  z-index:5;opacity:0;
  /* Re-enabled per frame by the driving script only while the marker is
     actually visible, so an invisible button never eats a click. */
  pointer-events:none;cursor:pointer;
  padding:.35rem 0 .35rem .5rem;background:none;border:0;
  font-family:var(--font-display);font-weight:400;
  font-size:.62rem;letter-spacing:.22em;text-transform:uppercase;
  /* Dimmer than --paper-dim (#8C8C90). Bruno Ace sets tight, squared strokes
     that read brighter than their fill at this size, so the marker looked
     white against the canyon at the value the body copy uses. */
  color:var(--paper-faint);
  text-shadow:0 0 18px rgba(10,10,12,.9);
  transition:color .3s;
  will-change:opacity;
}
.scroll-persist:hover{color:var(--accent-text)}
/* No scroll journey to advance, so nothing to point at. */
.webgl-unavailable .scroll-persist{display:none}

/* ---------- drifting grain canvas (supporting pages) ----------
   Behind the page content and above the page background. The homepage does
   this in WebGL inside its own scene; every other page gets it from
   js/particles.js, which exits silently when this element is absent. */
#particleCanvas{
  position:fixed;inset:0;z-index:0;
  display:block;
  pointer-events:none;
}

/* ---------- the coming-soon banner ----------
   Runs on the marketplace and at the foot of the contact page, so it lives
   here rather than in either page's own stylesheet.

   NOT called .soon. That name is taken by the full-screen holding pages
   above (custom.html and work.html still use it), and reusing it merged the
   two components: this banner's subtitle inherited the placeholder's display
   face, centring and uppercase tracking, and this banner's border and
   padding were landing on those pages in return.

   The word is set as large as the viewport will take it and clamped against
   vw, not a fixed size: "COMING SOON" is two words of Bruno Ace tracked
   open, and at a fixed size it either wraps on a phone or looks timid on a
   wide desktop. The band has its own hairlines top and bottom because it is
   an interruption in the page, and it should read as one. */
.cs-banner{
  border-block:1px solid var(--line);
  padding-block:clamp(2.6rem,7vh,4.4rem);
  position:relative;overflow:hidden;
}
.cs-word{
  font-family:var(--font-display);font-weight:400;
  text-transform:uppercase;
  font-size:clamp(2.1rem,10.5vw,7.5rem);
  letter-spacing:.04em;line-height:.98;
  color:var(--accent-text);
  margin:0;
}
/* Sits directly under the word and carries what the word cannot: WHAT is
   coming, and when it is not. */
.cs-sub{
  margin-top:clamp(.8rem,2vh,1.3rem);
  max-width:46rem;
  font-size:clamp(.95rem,1.2vw,1.1rem);
  line-height:1.65;color:var(--paper-dim);
}
.cs-cta{
  margin-top:clamp(1.4rem,3.5vh,2.2rem);
  display:flex;gap:.8rem;flex-wrap:wrap;
}
/* On the contact page the band closes the page rather than opening it, so it
   carries no top rule: the form above it already ends on one. */
.cs-banner--close{border-top:0}

/* ---------- visually hidden ----------
   Text that is read but not seen: a heading that has to name the page for a
   crawler and a screen reader while the design shows something shorter. Not a
   display:none, which would take it out of the accessibility tree too, and
   not an off-screen negative indent, which drags focus rings across the page
   if it ever wraps a focusable element. */
.vh{
  position:absolute;width:1px;height:1px;
  margin:-1px;padding:0;border:0;
  clip-path:inset(50%);overflow:hidden;white-space:nowrap;
}

/* ---------- skip link ----------
   Hidden until it is focused, which is the first thing a keyboard reaches on
   every page. It needs a real background and a z-index above the fixed nav,
   or it lands behind the wordmark and the person tabbing cannot see where
   they are. */
.skip{
  position:fixed;top:0;left:0;z-index:300;
  transform:translateY(-120%);
  padding:.85rem 1.4rem;
  background:var(--ink-2,#141416);
  border:1px solid var(--accent);
  color:var(--paper);
  font-family:var(--font-display);font-size:.62rem;
  letter-spacing:.18em;text-transform:uppercase;
  transition:transform .18s;
}
.skip:focus{transform:none;outline:2px solid var(--accent);outline-offset:2px}
@media(prefers-reduced-motion:reduce){ .skip{transition:none} }
