/* ══════════════════════════════════════════════════════════════
   titi.re — SHARED DESIGN SYSTEM (all pages)
   Tokens, nav pill, frost accent, footer, grain, scroll-bar,
   serpent cursor. Page-specific styles stay in each page.
   ══════════════════════════════════════════════════════════════ */

@property --x { syntax: '<length-percentage>'; inherits: true; initial-value: 50%; }
@property --y { syntax: '<length-percentage>'; inherits: true; initial-value: 50%; }
@property --angle { syntax: '<angle>'; inherits: true; initial-value: 0deg; }
@property --speed { syntax: '<number>'; inherits: true; initial-value: 0; }

:root{
  --bg:#080808;--surface:#0e0e0e;--text:#f0f0f0;--muted:#8a8a8a;--dim:#1e1e1e;
  /* The colour an unread word sits at in How It Works. Dark enough that the
     sweep is the event, bright enough that the copy ahead is legibly THERE —
     seeing the sentence coming is the point. */
  --unread-w:#383838;
  --accent:#b8ff00;--accent-dim:#96d400;--accent-warm:#cfb800;
  --success:#22c55e;--warning:#eab308;--danger:#ef4444;
  --fh:'DM Sans',sans-serif;--fs:'DM Serif Display',serif;--fm:'DM Mono',monospace;
  --ink: currentColor; --ink-2: #b8ff00; --ink-3: #96d400;

  /* LINES — three weights, translucent white instead of near-black hex.
     There were eleven different greys doing this job (#141414 through
     #3a3a3a). Each one is opaque, so it sits ON the background rather than
     in front of it: over the frosted panels it reads as a grey smear
     instead of an edge. Translucent white picks up whatever is behind it,
     which is what makes an edge look like an edge. */
  --line:   rgba(255,255,255,.06);   /* hairline dividers, quiet seams */
  --line-2: rgba(255,255,255,.10);   /* default component border */
  --line-3: rgba(255,255,255,.16);   /* raised or interactive surfaces */

  /* SHADOWS — you cannot cast a black shadow on a black background.
     The page is #080808, so a black blur has eight units of headroom to
     darken into and does essentially nothing except haze whatever it
     crosses. The wide layer is the whole problem: at 40px of blur it
     covers enough ground to sit over the blob and the grain, and there it
     stops being a shadow and becomes a grey smear that swims as the blob
     moves behind it. So there is no wide layer here at all.

     What is left is what actually reads as elevation on a dark surface:
     a tight contact shadow tying the element to what it sits on, and a
     lit top edge. Light does the lifting; black only marks the seam. */
  --shadow-1: 0 1px 2px rgba(0,0,0,.5),
              inset 0 1px 0 rgba(255,255,255,.05);
  --shadow-2: 0 2px 6px rgba(0,0,0,.45),
              inset 0 1px 0 rgba(255,255,255,.07);
  --shadow-3: 0 3px 10px rgba(0,0,0,.5),
              0 0 0 1px rgba(255,255,255,.04),
              inset 0 1px 0 rgba(255,255,255,.09);
}

/* SCROLL PROGRESS BAR */
#scroll-bar{
  position:fixed;top:0;left:0;z-index:9999;
  height:1px;background:var(--accent);
  width:0;pointer-events:none;
}

/* FOCUS VISIBLE — keyboard accessibility */
:focus-visible{
  outline:2px solid var(--accent);
  outline-offset:4px;
  border-radius:2px;
  transition:outline-offset .15s ease;
}
a:focus-visible,button:focus-visible{
  outline:2px solid var(--accent);
  outline-offset:4px;
}

/* SKIP LINK — first focusable element on every page, so keyboard and screen
   reader users can bypass the nav and hero instead of tabbing through them on
   every load (WCAG 2.4.1 Bypass Blocks). Off-screen until focused; it must
   stay focusable, so no display:none and no visibility:hidden. */
.skip-link{
  position:fixed;top:12px;left:12px;z-index:100000;
  transform:translateY(-200%);
  padding:12px 20px;border-radius:12px;
  font-family:var(--fm,monospace);font-size:11px;letter-spacing:.14em;
  text-transform:uppercase;text-decoration:none;
  color:var(--bg,#080808);background:var(--accent,#b8ff00);
  box-shadow:var(--shadow-2);
  transition:transform .2s cubic-bezier(.23,1,.32,1);
}
.skip-link:focus{transform:translateY(0)}
/* The landmark takes focus programmatically via tabindex="-1"; it is not a
   control, so it should not draw a focus ring of its own. */
[id="main-content"]:focus,[id="legal-content"]:focus{outline:none}

/* CURSOR VISIBILITY — cursor:none hangs off this class, which site.js only
   adds once the serpent tracker is actually attached. Declared in the
   stylesheet, the system cursor disappeared whether or not the script that
   replaces it ever ran: a blocked CDN, a parse error, or a slow load left the
   page with no visible cursor at all. */
html.has-custom-cursor body{cursor:none}
html.has-custom-cursor .split-wrap{cursor:none}

/* ── NAV — floating frosted pill ───────────────────────────── */
nav{
  position:fixed;top:16px;left:50%;z-index:800;
  display:flex;justify-content:space-between;align-items:center;gap:36px;
  width:max-content;max-width:calc(100vw - 32px);
  padding:13px 30px;
  background:rgba(12,14,8,.5);
  -webkit-backdrop-filter:blur(28px) saturate(200%);backdrop-filter:blur(28px) saturate(200%);
  border:1px solid var(--line-2);border-radius:999px;
  /* The 40px black drop this replaces was the worst offender on the page:
     the nav floats directly over the hero, so the smear tracked the blob. */
  box-shadow:0 2px 8px rgba(0,0,0,.5),
    inset 0 1px 0 rgba(255,255,255,.12),
    inset 0 -1px 0 rgba(0,0,0,.35);
  transform:translateX(-50%);
  transition:transform .4s cubic-bezier(.76,0,.24,1);
}
nav.hidden{transform:translateX(-50%) translateY(-140%)}
.nav-logo{display:flex;align-items:center;color:#ccc}
.nav-logo svg{width:86px;height:16px;display:block}
.nav-toggle{display:none}
.nav-toggle:focus-visible{outline:none}
.nav-toggle-bar{display:block;width:22px;height:2px;background:#fff;transition:transform .25s,opacity .25s,background .25s}
.nav-toggle.open .nav-toggle-bar{background:var(--accent)}
.nav-links{display:flex;gap:36px;list-style:none;align-items:center}
.nav-links a{font-family:var(--fm);font-size:12.5px;letter-spacing:.16em;text-transform:uppercase;color:#aaa;transition:color .3s;line-height:1;display:block}
.nav-links a:hover{color:var(--accent)}
.nav-links a.active{color:var(--accent)}

/* FROSTBITE TEXT EFFECT — ice creeps up the "Services" nav link (unified nav accent) */
.fx-frost {
  color: transparent;
  -webkit-text-fill-color: transparent;
  background:
    radial-gradient(circle at 22% 30%, #fff 0 1.6px, transparent 2.2px),
    radial-gradient(circle at 68% 62%, #fff 0 1.3px, transparent 1.9px),
    linear-gradient(0deg,
      #fff 0%,
      color-mix(in srgb, var(--ink-3) 55%, #fff) 34%,
      color-mix(in srgb, var(--ink-3) 25%, var(--ink)) 60%,
      var(--ink) 100%);
  background-size: 90px 46px, 120px 58px, 100% 260%;
  -webkit-background-clip: text;
  background-clip: text;
  animation: fx-frost 4s ease-in-out infinite alternate;
}
.nav-links li:has(.fx-frost){overflow:visible}
@keyframes fx-frost {
  0% {
    background-position: 0 0, 0 0, 0 0%;
    filter: drop-shadow(0 0 1px color-mix(in srgb, var(--ink-3) 15%, transparent));
  }
  100% {
    background-position: 18px 6px, -22px -8px, 0 100%;
    filter: drop-shadow(0 0 6px color-mix(in srgb, var(--ink-3) 40%, transparent));
  }
}
@media (prefers-reduced-motion: reduce) {
  .fx-frost { animation: none; }
}

/* NAV — mobile menu (hamburger + full-width frosted panel) */
@media(max-width:860px){
  nav{padding:12px 18px;width:calc(100vw - 32px);flex-wrap:wrap;align-items:center;border-radius:24px;gap:0 16px}
  .nav-links{
    position:relative;width:100%;
    flex-direction:column;align-items:flex-start;gap:0;
    padding:0;
    max-height:0;overflow:hidden;
    transition:max-height .25s cubic-bezier(.23,1,.32,1),padding .25s cubic-bezier(.23,1,.32,1);
    pointer-events:none;
    border-top:1px solid transparent;
  }
  .nav-links.open{max-height:600px;padding:12px 0 20px;pointer-events:auto;overflow:visible}
  .nav-links a{
    font-size:clamp(32px,8vw,72px);letter-spacing:.04em;color:#e5e5e5;
    padding:14px 0 14px 26px;display:block;width:100%;
    position:relative;
    transition:color .3s;
  }
  .nav-links a:hover{color:var(--accent)}
  .nav-links a::after{
    content:'';position:absolute;left:0;top:50%;width:8px;height:8px;
    border-radius:50%;background:var(--accent);transform:translate(0,-50%) scale(0);
    opacity:0;transition:transform .3s,opacity .3s;
    box-shadow:0 0 18px 2px rgba(184,255,0,.4);
  }
  .nav-links a:hover::after{opacity:1;transform:translate(0,-50%) scale(1)}
  .nav-links a.active{color:var(--accent)}
  .nav-links a.active::after{opacity:1;transform:translate(0,-50%) scale(1)}
  .nav-toggle{
    display:flex;flex-direction:column;gap:6px;justify-content:center;align-items:center;
    background:none;border:none;cursor:pointer;padding:8px;width:22px;height:14px;flex-shrink:0;z-index:801;
    box-sizing:content-box;
  }
  .nav-toggle.open .nav-toggle-bar:nth-child(1){transform:translateY(4px) rotate(45deg)}
  .nav-toggle.open .nav-toggle-bar:nth-child(2){transform:translateY(-4px) rotate(-45deg)}
}

/* ── FOOTER — bottom bar ───────────────────────────────────── */
#site-footer{padding:0 52px;position:relative}
#site-footer::before{
  content:'';position:absolute;top:0;left:52px;right:52px;
  height:1px;background:var(--line-2);
}
.ft-bar{display:flex;justify-content:space-between;align-items:center;padding:22px 0;font-style:normal}
.ft-copy{
  font-family:var(--fm);font-size:9px;letter-spacing:.18em;color:#9a9a9a;text-transform:uppercase;
  flex-shrink:0;font-style:normal;
}
.ft-nav{display:flex;gap:20px;list-style:none}
.ft-nav a{
  font-family:var(--fm);font-size:9px;letter-spacing:.14em;text-transform:uppercase;
  color:#9a9a9a;transition:color .3s;font-style:normal;
}
.ft-nav a:hover{color:var(--accent)}
.ft-legal{display:flex;gap:20px}
.ft-legal a{
  font-family:var(--fm);font-size:9px;letter-spacing:.14em;text-transform:uppercase;
  color:#9a9a9a;transition:color .3s;font-style:normal;
}
.ft-legal a:hover{color:var(--accent)}
@media(max-width:860px){
  #site-footer{padding:0 28px}
  #site-footer::before{left:28px;right:28px}
  .ft-bar{flex-wrap:wrap;gap:12px}
}
@media(max-width:520px){
  #site-footer{padding:0 20px}
  #site-footer::before{left:20px;right:20px}
  .ft-bar{flex-direction:column;align-items:center;gap:10px;text-align:center}
  .ft-nav{display:none}
  .ft-legal{flex-wrap:wrap;gap:14px;justify-content:center}
}

/* ── FILM GRAIN — fullscreen overlay, CSS-only animated noise ── */
.grain{
  position:fixed;top:-10%;left:-10%;width:120%;height:120%;z-index:700;pointer-events:none;
  opacity:.06;
  background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size:200px 200px;
  animation:grainShift 0.12s steps(1) infinite;
  mix-blend-mode:screen;
}
@keyframes grainShift{
  0%{transform:translate(0,0)}
  25%{transform:translate(-2%,3%)}
  50%{transform:translate(3%,-1%)}
  75%{transform:translate(-1%,-2%)}
  100%{transform:translate(2%,1%)}
}

/* ── CURSOR — Serpent: a chain of segments chasing the pointer ── */
.cur {
  --c: var(--ink-2);   /* cursor primary colour  */
  --c2: var(--ink-3);  /* cursor accent colour   */
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 99999;
}
.cur-hint { display: none; }
.cur-word { display: none; }

/* the follower — positioned from --x/--y, eased with a transition.

   Driven by transform, not left/top. left and top are layout properties: they
   cannot be composited, so eight segments each mid-transition meant layout and
   paint for eight elements on the main thread every frame, for as long as the
   pointer kept moving. will-change:left,top could not help — there is no layer
   to promote a layout property to. transform is composited, so the chase now
   runs off the main thread entirely and stops competing with whatever the page
   is painting underneath it.

   --x/--y are registered as <length-percentage> at the top of this file, so the
   composed transform interpolates rather than snapping. They are only ever read
   here while the dots are visible, which is after the first pointermove has set
   them in px — the 50% initial value never resolves against a 14px dot on screen. */
.cursor {
  position: absolute;
  left: 0;
  top: 0;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--c);
  transform: translate3d(var(--x), var(--y), 0) translate(-50%, -50%);
  transition: transform 0.16s cubic-bezier(0.22, 1, 0.36, 1),
              opacity 0.3s;
  pointer-events: none;
  will-change: transform;
  opacity: 0;
}
.cur.is-active .cursor { opacity: 1; }

/* 06 Serpent — a chain of segments chasing the pointer like a tail */
.cur-snake .cursor {
  width: calc(20px - var(--i) * 2px);
  height: calc(20px - var(--i) * 2px);
  background: color-mix(in srgb, var(--c) calc(100% - var(--i) * 11%), var(--c2));
  /* Two values now, not three — the chase is one transitioned property
     (transform) instead of two (left, top). A third value here would silently
     re-map onto transform again and the tail would lose its stagger. */
  transition-duration: calc(0.1s + var(--i) * 0.05s), 0.3s;
  z-index: calc(10 - var(--i));
}

@media(hover:none){ .cur{display:none!important} }
@media (prefers-reduced-motion: reduce) {
  .cur { display: none !important; }
  .grain { display: none !important; }
}
