/* Dame Digital, concept "Assembly". Tokens live on html in starter.css. The brand book's type, self-hosted
   (OFL, WOFF2 from the book's TTFs): Antonio carries the display voice (tall, condensed capitals, as in
   the logotype), Albert Sans the reading voice. */
@font-face { font-family: 'Antonio'; src: url('fonts/Antonio-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Antonio'; src: url('fonts/Antonio-600.woff2') format('woff2'); font-weight: 600; font-display: swap; }
@font-face { font-family: 'Antonio'; src: url('fonts/Antonio-700.woff2') format('woff2'); font-weight: 700; font-display: swap; }
@font-face { font-family: 'Albert Sans'; src: url('fonts/AlbertSans-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Albert Sans'; src: url('fonts/AlbertSans-500.woff2') format('woff2'); font-weight: 500; font-display: swap; }
@font-face { font-family: 'Albert Sans'; src: url('fonts/AlbertSans-700.woff2') format('woff2'); font-weight: 700; font-display: swap; }

html { scroll-behavior: auto; scrollbar-color: #4A8FE0 #0E1116; }
body { font-family: 'Albert Sans', system-ui, sans-serif; font-weight: 400; font-size: 1.125rem; line-height: 1.55; -webkit-font-smoothing: antialiased; }
::selection { background: #7CF3FF; color: #0E1116; }
h1, h2, .wordmark, .name, .nav-mark, .rows, .pipe-labels span, .mail { font-family: 'Antonio', 'Albert Sans', sans-serif; }
.name, .clone, .wordmark { letter-spacing: 0.005em; }
a { color: inherit; text-underline-offset: 0.2em; text-decoration-thickness: 1px; }
a:focus-visible, .go:focus-visible { outline: 2px solid #7CF3FF; outline-offset: 4px; border-radius: 4px; }
p { margin: 0 0 0.8em; }

.skip { position: absolute; left: 1rem; top: -4rem; z-index: 20; background: #7CF3FF; color: #0E1116; padding: 0.6rem 1rem; border-radius: 6px; font-weight: 600; }
.skip:focus { top: 1rem; }

/* Navigation: the direct route, on the first screen and always. */
.nav { position: fixed; inset: 0 0 auto 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; padding: 1.1rem clamp(1rem, 4vw, 3.5rem); pointer-events: none; }
.nav a { pointer-events: auto; text-decoration: none; }
.nav-mark { font-weight: 800; font-size: 0.95rem; letter-spacing: 0.01em; text-transform: uppercase; }
.nav nav { display: flex; gap: clamp(1rem, 2.4vw, 2.2rem); font-size: 0.95rem; font-weight: 500; }
.nav nav a { color: #E7EBF0; padding: 0.3rem 0; background: linear-gradient(#7CF3FF, #7CF3FF) no-repeat 0 100% / 0 1px; transition: background-size 0.3s ease; }
.nav nav a:hover { background-size: 100% 1px; }
.sound { pointer-events: auto; font: inherit; font-weight: 500; color: #7CF3FF; background: none; border: 1px solid rgb(124 243 255 / 0.5); border-radius: 999px; padding: 0.2rem 0.75rem; cursor: pointer; }
.sound[aria-pressed='true'] { background: #7CF3FF; color: #0E1116; }
.sound:focus-visible { outline: 2px solid #7CF3FF; outline-offset: 3px; }
.no-js .sound { display: none; }
@media (max-width: 699px) { .sound { padding: 0.15rem 0.5rem; } }

.pin { padding: 5.5rem clamp(1.2rem, 5vw, 4.5rem) 3rem; }
.stage { display: block; }

/* Hero: the wordmark. With the swarm live, its points draw the letters and the DOM letters step back
   (still in the page for readers, search and the no-script visitor). */
.hero .pin { display: grid; grid-template-rows: 1fr auto; align-items: center; justify-items: start; }
.wordmark { margin: 0; font-weight: 900; text-transform: uppercase; font-size: clamp(3.2rem, 8.1vw, 9.5rem); line-height: 0.95; letter-spacing: -0.02em; color: #E7EBF0; justify-self: center; text-align: center; transition: color 0.8s ease; }
.wordmark span { display: inline-block; }
.swarm-live .wordmark { color: transparent; }
.hero-line { align-self: end; max-width: 30ch; font-size: clamp(1.15rem, 1.6vw, 1.5rem); line-height: 1.35; font-weight: 500; color: #E7EBF0; margin: 0; }
/* r28: the hero holds 0.2 viewport (data-span 1.2), so the line blurs out over its second half, after the
   crisp logo has handed over to its points, and before the Stacked panel rises over it. */
.hero-line.resolve.exit { --from: 0.5; --to: 1; }

/* Stacked: a panel over the swarm, the project names stacking up in rows, Vince's line held on top,
   then a pill-shaped portal opens through the panel onto the swarm. */
.stacked .pin { padding: 0; }
.panel { position: absolute; inset: 0; background: #1A1F27; overflow: clip; display: grid; place-items: center; }
.rows { position: absolute; inset: 0; perspective: 900px; }
.row { position: absolute; left: 0; right: 0; height: var(--rowh); }
.clone { position: absolute; top: 0; left: 0; white-space: nowrap; font-weight: 900; text-transform: uppercase; font-size: var(--rowfs); line-height: 1; letter-spacing: -0.02em; will-change: transform; opacity: 0.2; }
.quote { position: relative; z-index: 2; margin: 0; max-width: min(46rem, 86vw); padding: 0 1rem; }
.quote blockquote { margin: 0; }
.quote p { font-size: clamp(1.35rem, 2.6vw, 2.35rem); line-height: 1.25; font-weight: 600; color: #E7EBF0; text-wrap: balance; text-shadow: 0 0 18px #1A1F27, 0 0 6px #1A1F27; }
.quote figcaption { margin-top: 1.2rem; font-size: 1rem; font-weight: 600; color: #7CF3FF; }
/* r27: while the line is points and the portal opens, the swarm is drawn over the panel (below the nav). */
.swarm-over .stage { z-index: 5; }
/* r27, reduced motion only: the still outline of the pill that draws in as the line fades (app.js). */
.rm-pill { display: none; }
.no-js .stacked .panel, .stacked .panel { min-height: 100vh; min-height: 100svh; }

/* Projects: copy at left, the screenshot at right (the swarm assembles into exactly that box). */
.project .pin { display: grid; grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr); gap: clamp(1.5rem, 4vw, 4rem); align-items: center; min-height: 100vh; min-height: 100svh; box-sizing: border-box; }
.copy { position: relative; z-index: 2; max-width: 34rem; }
.name { font-weight: 900; text-transform: uppercase; font-size: clamp(2.6rem, 6.4vw, 7rem); line-height: 0.95; letter-spacing: -0.025em; margin: 0 0 1.2rem; overflow-wrap: anywhere; }
#wildroserates .name, #design-studio .name { font-size: clamp(2.3rem, 4.9vw, 5.6rem); }
.lede { font-size: clamp(1.25rem, 1.9vw, 1.7rem); line-height: 1.3; font-weight: 600; color: #E7EBF0; max-width: 26ch; }
.copy p:not(.lede) { color: #E7EBF0; max-width: 40ch; }
.go { display: inline-flex; align-items: center; gap: 0.6em; margin-top: 1rem; padding: 0.8rem 1.3rem; border: 1px solid #7CF3FF; border-radius: 999px; color: #7CF3FF; font-weight: 600; text-decoration: none; transition: background 0.25s ease, color 0.25s ease; }
.go::after { content: ''; width: 0.9em; height: 0.9em; background: currentColor; clip-path: polygon(0 45%, 70% 45%, 45% 18%, 55% 8%, 100% 50%, 55% 92%, 45% 82%, 70% 55%, 0 55%); }
.go:hover { background: #7CF3FF; color: #0E1116; }
.shot { margin: 0; aspect-ratio: 1200 / 750; width: 100%; border-radius: 14px; overflow: clip; box-shadow: 0 30px 60px -20px rgb(0 0 0 / 0.6); }
.shot img { display: block; width: 100%; height: 100%; object-fit: cover; }
.swarm-live .shot { box-shadow: none; }
.swarm-live .shot img { opacity: var(--img, 0); }
.stations { list-style: none; padding: 0; margin: 0.4rem 0 0.6rem; display: flex; flex-wrap: wrap; gap: 0.4rem 1.2rem; font-weight: 600; color: #838C98; }
.stations li { transition: color 0.3s ease; }
.stations li.on { color: #7CF3FF; }
.pipe-labels { display: none; position: absolute; inset: 0; pointer-events: none; z-index: 1; opacity: 0; }
.swarm-live .pipe-labels { display: block; }
.pipe-labels span { position: absolute; translate: -50% 0; font-family: 'Antonio', 'Albert Sans', sans-serif; font-weight: 700; font-size: clamp(0.8rem, 1.1vw, 1.1rem); text-transform: uppercase; letter-spacing: 0.04em; color: #838C98; transition: color 0.4s ease, translate 0.4s ease; }
.pipe-labels span.on { color: #E7EBF0; translate: -50% -0.4rem; }
.status { display: inline-block; font-weight: 700; color: #D2C2FF !important; }

/* Kylo: the room is the stage; a phone's screen is the first window onto it. */
.kylo .pin { grid-template-columns: minmax(0, 1fr); align-items: end; justify-items: start; }
.kylo .copy { max-width: 36rem; padding: 1.5rem 0 0; }
/* r22: the copy's scrim is a soft pool behind the copy only (bottom left), so the room reads at its
   baked brightness everywhere else. */
.kylo .pin::before { content: ''; position: absolute; inset: 0; background: radial-gradient(ellipse 62% 86% at 0% 100%, rgb(14 17 22 / 0.88) 0%, rgb(14 17 22 / 0.78) 62%, rgb(14 17 22 / 0) 100%); pointer-events: none; z-index: 1; }
/* r23: on a wide screen that pool darkened the whole left side of the room. It is now sized to the
   copy's own box (25rem by 16.5rem around the copy's middle, from the pin's left padding and the
   bottom), so the room left of and above the copy keeps its baked brightness. The phone keeps its
   band (below). (A pool on the copy itself, as a ::before, cost a 225 ms frame when the copy first
   showed; on the pin it costs nothing measurable.) */
@media (min-width: 700px) {
  .kylo .pin::before { will-change: opacity; background: radial-gradient(ellipse 25rem 16.5rem at calc(clamp(1.2rem, 5vw, 4.5rem) + 13rem) calc(100% - 13.4rem), rgb(14 17 22 / 0.86) 0%, rgb(14 17 22 / 0.82) 64%, rgb(14 17 22 / 0.66) 78%, rgb(14 17 22 / 0.3) 88%, rgb(14 17 22 / 0.08) 96%, rgb(14 17 22 / 0) 100%); }
}
/* r22: a small soft pool behind the nav's links (top right), so they still read over the solid room. */
.kylo .pin::after { content: ''; position: absolute; inset: 0; background: radial-gradient(ellipse 34% 16% at 100% 0%, rgb(14 17 22 / 0.75) 0%, rgb(14 17 22 / 0.5) 50%, rgb(14 17 22 / 0) 100%); pointer-events: none; z-index: 1; }
.phone { position: absolute; inset: 0; width: 100%; height: 100%; display: none; z-index: 1; }
.swarm-live .phone { display: block; }
.phone-surround { fill: #0E1116; }
.phone-body { fill: none; stroke: #E7EBF0; stroke-width: 1.5; }

/* About: pinned; the crews on the swarm, the copy at left (below the crews when narrow). */
.about { position: relative; }
.about .pin { min-height: 100vh; min-height: 100svh; display: grid; align-items: center; justify-items: start; }
.about-inner { position: relative; z-index: 2; text-shadow: 0 0 14px #0E1116, 0 0 4px #0E1116; max-width: 40rem; }
.swarm-live .clause { opacity: var(--c, 1); transition: opacity 0.35s ease; }
.js .about .who { --r: clamp(0, (var(--p) - 0.52) / 0.1, 1); opacity: var(--r); translate: 0 calc((1 - var(--r)) * 1.5rem); }
@media (max-width: 1199px) {
  .about .pin { align-items: end; }
  .about-inner { max-width: 36rem; }
}
.about h2 { font-weight: 800; font-size: clamp(2rem, 3.6vw, 3.4rem); line-height: 1.05; margin: 0 0 1.6rem; }
.builds { margin: 0 0 2rem; }
.builds p { font-size: clamp(1.3rem, 2.2vw, 2rem); line-height: 1.3; font-weight: 600; }
.who { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.7rem; color: #E7EBF0; }
.who li { padding-left: 1.4rem; position: relative; }
.who li::before { content: ''; position: absolute; left: 0; top: 0.62em; width: 0.5rem; height: 0.5rem; border-radius: 50%; background: #7CF3FF; }

/* Contact: the opening's wordmark returns, now in the projects' colours. */
.contact .pin { display: grid; grid-template-rows: 1fr auto; justify-items: center; align-items: center; text-align: center; }
.closing { font-size: clamp(3.2rem, 8.1vw, 9.5rem); }
.reach h2 { font-weight: 700; font-size: clamp(1.4rem, 2.4vw, 2.2rem); margin: 0 0 0.8rem; }
.mail { display: inline-block; font-family: 'Antonio', 'Albert Sans', sans-serif; font-weight: 700; font-size: clamp(1.15rem, 3.3vw, 3rem); color: #7CF3FF; text-decoration: none; background: linear-gradient(#7CF3FF, #7CF3FF) no-repeat 0 100% / 100% 2px; padding-bottom: 0.2em; overflow-wrap: anywhere; }
.mail:hover { color: #E7EBF0; }
.nah { margin-top: 2rem; color: #838C98; font-size: 1rem; }
.company { margin: 1.4rem 0 0; max-width: 46ch; color: #B6BEC9; font-size: 1rem; line-height: 1.5; }

/* Entrances, each project its own (the floor: no two sections share one). All scoped to html.js so
   the page reads whole without script; the reading state is still and sharp. */
.js .project[data-entrance='blur'] .copy { --r: clamp(0, (var(--p) - 0.08) / 0.14, 1); filter: blur(calc((1 - var(--r)) * 16px)); opacity: var(--r); }
.js .project[data-entrance='print'] .copy { --r: clamp(0, (var(--p) - 0.06) / 0.18, 1); clip-path: inset(calc((1 - var(--r)) * 100%) 0 0 0); }
.js .project[data-entrance='wipe'] .copy { --r: clamp(0, (var(--p) - 0.06) / 0.16, 1); clip-path: inset(0 calc((1 - var(--r)) * 100%) 0 0); }
.js .project[data-entrance='rise'] .copy > :not(.name) { --r: clamp(0, (var(--p) - 0.14) / 0.15, 1); opacity: var(--r); translate: 0 calc((1 - var(--r)) * 3rem); }
.js .project .name.reveal { --from: 0.04; --to: 0.24; }
/* r23: the Kylo section is back to r21's 4 viewports of scroll (data-span 5), so the copy reads --p as
   r21 did: in from 1.2 to 1.68 viewports. The name keeps the projects' own reveal (0.16 to 0.96). */
.js .kylo .copy { --r: clamp(0, (var(--p) - 0.3) / 0.12, 1); opacity: var(--r); translate: 0 calc((1 - var(--r)) * 4rem); }
/* The copy's scrim arrives with the copy, so the phone frame and the first of the particle room are not dimmed. */
.js .kylo .pin::before { opacity: clamp(0, (var(--p) - 0.3) / 0.12, 1); }
.js .reach { --r: clamp(0, (var(--p) - 0.35) / 0.2, 1); opacity: var(--r); }
/* Keyboard focus never lands on something still hidden. */
.js .copy:focus-within, .js .reach:focus-within, .js .about-inner:focus-within .who { opacity: 1 !important; filter: none !important; clip-path: none !important; translate: none !important; }

/* Phone and small tablet: the screenshot above, copy below; the swarm fills the same box. */
@media (max-width: 699px) {
  .nav nav { gap: 0.7rem; font-size: 0.8rem; align-items: center; }
  .nav-mark { font-size: 0.72rem; white-space: nowrap; }
  .wordmark span { display: block; }
  .wordmark, .closing { font-size: clamp(2.6rem, 15.5vw, 6rem); line-height: 1; }
  .project .pin { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto; align-content: center; gap: 1.2rem; padding-top: 4.2rem; padding-bottom: 1.5rem; }
  .project .shot { order: -1; aspect-ratio: 390 / 520; width: min(68vw, 42svh); justify-self: center; }
  .name { font-size: clamp(2rem, 11vw, 3.4rem); margin-bottom: 0.6rem; }
  #wildroserates .name, #design-studio .name { font-size: clamp(1.7rem, 8.5vw, 2.8rem); }
  .lede { font-size: 1.1rem; }
  body { font-size: 1rem; }
  .go { padding: 0.65rem 1.1rem; margin-top: 0.4rem; }
  .kylo .pin { grid-template-rows: auto; align-content: end; }
  /* r25: on a phone the copy sits at the bottom, below the sofa, with its scrim rising from the bottom edge.
     The particle room keeps r21's scrim. */
  .kylo .pin::before { background: linear-gradient(to top, rgb(14 17 22 / 0.86) 0, rgb(14 17 22 / 0.86) 17rem, rgb(14 17 22 / 0) 21rem); }
  .kylo .pin::after { background: radial-gradient(ellipse 100% 9% at 100% 0%, rgb(14 17 22 / 0.7) 0%, rgb(14 17 22 / 0) 100%); }
  .pipe-labels span { font-size: 0.72rem; }
}
@media (min-width: 700px) and (max-width: 1023px) {
  .wordmark span { display: block; }
  .wordmark, .closing { font-size: 13vw; line-height: 1; }
  .project .pin { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto; align-content: center; }
  .project .shot { order: -1; width: min(100%, 60svh * 1.6); justify-self: center; }
}

/* Reduced motion: nothing is pinned or scrubbed; every section is a plain screen. */
@media (prefers-reduced-motion: reduce) {
  .js [data-span] { height: auto !important; }
  .js [data-span] > .pin { position: relative !important; height: auto !important; min-height: 100vh; min-height: 100svh; }
  .js .copy, .js .about-inner, .js .about .who, .clause, .js .reach, .js .project .copy > * { opacity: 1 !important; filter: none !important; clip-path: none !important; translate: none !important; scale: none !important; }
  .stacked .panel { position: relative; min-height: 100svh; }
  /* r27: once the panel is a third of the way past, the line fades and the portal's outline draws, still. */
  .js .stacked .rm-pill { display: block; position: absolute; left: 50%; top: 64%; translate: -50% -50%; overflow: visible; pointer-events: none; }
  .rm-pill rect { fill: none; stroke: #7CF3FF; stroke-width: 1.5; stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset 1.1s ease; }
  .rm-past .rm-pill rect { stroke-dashoffset: 0; }
  .js .stacked .quote { transition: opacity 0.8s ease; }
  .js .stacked .rm-past .quote { opacity: 0 !important; }
  /* r22: the finished Kylo room as a still (ARCH's Cycles render of the furnished room). */
  .js .kylo .pin { background: #0E1116 url('room/kylo-room-still-1440.webp') center / cover no-repeat; }
}
@media (prefers-reduced-motion: reduce) and (max-width: 699px) {
  .js .kylo .pin { background-image: url('room/kylo-room-still-390.webp'); }
}
.no-js .stacked .panel { position: relative; }
.stacked .pin { place-items: stretch; }
.hero .pin, .contact .pin { min-height: 100vh; min-height: 100svh; box-sizing: border-box; }
.contact .reach { padding-bottom: 2rem; }

/* The brand-book logo (on-dark version: daylight ink, the violet to blue slit kept). Nav: the horizontal
   logo; hero and contact: the primary logo, whose points the swarm assembles (the img steps back while
   the swarm is live, and is the logo itself without script or under reduced motion). */
.vh { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
.nav-mark img { display: block; height: 30px; width: auto; }
.logo-img { display: block; width: min(60vw, 880px); height: auto; }
/* With the swarm live the points gather into the logo, then the crisp SVG takes over in the same box
   (--logo, set per frame from the scroll); the points keep out of that box themselves (app.js). */
.logo { position: relative; }
.swarm-live .logo-img { opacity: var(--logo, 0); }
/* r11: set in the head before the first paint (page.html) and lifted by app.js as the intro starts, so the
   hero logo is never painted in full and then taken away. Never set without script or under reduced motion. */
.logo-wait [data-word="hero"] .logo-img, .logo-wait [data-word="hero"] .logo-layers { opacity: 0; }
@media (max-width: 699px) {
  .nav-mark img { height: 22px; }
  .logo-img { width: 78vw; }
}

/* r8: the crisp logo in parts. app.js lays five copies of the same SVG over the img, each clipped to
   one part (bar, slit, bowl, the two lines of the wordmark). The clips partition the logo, so at rest
   the parts are the img, pixel for pixel, in its box. They carry the intro, the idle and the contact's
   signature; the img stays in the page as the box everything measures against. Only with the swarm
   live: without script or under reduced motion the img is the logo, still. */
.logo-layers { display: none; position: absolute; pointer-events: none; opacity: var(--logo, 0); }
.swarm-live .logo-layers { display: block; }
/* r13 fix: the hero's layers are sized by layout, never by a pixel measure taken in JS: the h1 is exactly
   the img's box (the img is its only in-flow child, display block), so 100% of it is the img's box at
   every moment, however late the page settles. The glide scales that box by --k (a number, 1 at rest)
   about --ox/--oy (the lockup's ink centre, as a share of the box). At --k 1 it is left 0, top 0, 100%. */
/* r15: during the intro --k stays at the largest size and the glide is a transform on this one layer
   (app.js startHeroDraw), about the same ink centre; will-change only while it draws. */
[data-word="hero"] .logo-layers.drawing { transform-origin: var(--ox, 50%) var(--oy, 50%); will-change: transform; }
[data-word="hero"] .logo-layers { left: calc(var(--ox, 50%) * (1 - var(--k, 1))) !important; top: calc(var(--oy, 50%) * (1 - var(--k, 1))) !important; width: calc(100% * var(--k, 1)) !important; height: calc(100% * var(--k, 1)) !important; }
.swarm-live .logo.layered .logo-img { visibility: hidden; }
/* r11: no pointer parallax; the parts never leave the img's box, so the points hand off from it exactly. */
.lp { position: absolute; inset: 0; }
.lp img { display: block; width: 100%; height: 100%; }
.lp-bar { clip-path: inset(0 89.91% 0 0); --d: 1; }
.lp-slit { clip-path: inset(0 87.84% 0 10.09%); --d: 1.3; }
.lp-bowl { clip-path: inset(0 58.47% 0 12.16%); --d: 0.85; }
.lp-l1 { clip-path: inset(0 0 50% 41.53%); --d: 0.55; }
.lp-l2 { clip-path: inset(50% 0 0 41.53%); --d: 0.5; }
/* A rising line carries its own clip with it, so no other line's ink can enter its window. */
.lp-l1 img { clip-path: inset(0 0 50% 41.53%); }
.lp-l2 img { clip-path: inset(50% 0 0 41.53%); }
/* The slit's light: a lighter tint of its own colours laid on it that breathes (crisp, no blur), and a bright band travelling down it. Both sit
   exactly on the slit's rectangle (x 26.5 to 29.5, y 10 to 90 of the 252.81 x 100 viewBox). */
.lp-glow, .lp-sheen { position: absolute; left: 10.482%; width: 1.187%; top: 10%; height: 80%; }
.lp-glow { background: linear-gradient(to bottom, #B9A6FF, #9CC8FF); z-index: 1; opacity: 0; animation: lp-breathe 5.2s ease-in-out 2.6s infinite; }
.lp-sheen { background: linear-gradient(to bottom, rgb(255 255 255 / 0) 0%, rgb(255 255 255 / 0) 42%, rgb(231 235 240 / 0.85) 50%, rgb(255 255 255 / 0) 58%, rgb(255 255 255 / 0) 100%) no-repeat 0 0 / 100% 300%; background-position-y: 100%; animation: lp-sheen 7s cubic-bezier(0.45, 0, 0.3, 1) 2.9s infinite; }
/* A light passing over the whole logo, masked by the logo's own alpha so it lights ink only. */
.lp-sweep { position: absolute; inset: 0; background: linear-gradient(100deg, rgb(255 255 255 / 0) 44%, rgb(255 255 255 / 0.5) 50%, rgb(255 255 255 / 0) 56%) no-repeat; background-size: 300% 100%; background-position-x: 100%; -webkit-mask: url('logo/primary-on-dark.svg') 0 0 / 100% 100% no-repeat; mask: url('logo/primary-on-dark.svg') 0 0 / 100% 100% no-repeat; animation: lp-sweep 10s cubic-bezier(0.5, 0, 0.3, 1) 2.2s infinite; }
@keyframes lp-breathe { 0%, 100% { opacity: 0; } 50% { opacity: 0.45; } }
@keyframes lp-sheen { 0% { background-position-y: 100%; } 45%, 100% { background-position-y: 0%; } }
@keyframes lp-sweep { 0% { background-position-x: 100%; } 16%, 100% { background-position-x: 0%; } }
/* Hero intro, once on load at the top of the page: the points' logo turns crisp part by part, in place: the
   bar from its foot up, the bowl opening out from the slit, the slit drawn top to bottom, each line from
   its baseline up; then the light passes. The points keep out of the ink only once every part is in. */
.intro .lp-bar img { animation: lp-rise 1s cubic-bezier(0.16, 1, 0.3, 1) 0.15s both; }
.intro .lp-bowl img { animation: lp-bloom 1.1s cubic-bezier(0.33, 1, 0.68, 1) 0.5s both; }
.intro .lp-slit img { animation: lp-draw 0.8s cubic-bezier(0.65, 0, 0.35, 1) 1s both; }
.intro .lp-l1 img { animation: lp-rise-l1 0.9s cubic-bezier(0.16, 1, 0.3, 1) 1.25s both; }
.intro .lp-l2 img { animation: lp-rise-l2 0.9s cubic-bezier(0.16, 1, 0.3, 1) 1.4s both; }
.intro .lp-sweep { animation: lp-sweep 10s cubic-bezier(0.5, 0, 0.3, 1) 2.2s infinite; }
@keyframes lp-rise { from { clip-path: inset(100% 0 0 0); } to { clip-path: inset(0 0 0 0); } }
@keyframes lp-bloom { from { clip-path: circle(0% at 11.5% 50%); } to { clip-path: circle(150% at 11.5% 50%); } }
@keyframes lp-draw { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0 0 0 0); } }
@keyframes lp-rise-l1 { from { clip-path: inset(50% 0 50% 41.53%); } to { clip-path: inset(0 0 50% 41.53%); } }
@keyframes lp-rise-l2 { from { clip-path: inset(100% 0 0 41.53%); } to { clip-path: inset(50% 0 0 41.53%); } }
/* Hero draw-on (app.js startHeroDraw; r8's parts intro above is no longer started). The logo's own
   SVG, inline, over the crisp parts while .drawing; when it ends the SVG steps aside and the crisp parts
   take the idle. No blur, no grain. */
.lp-draw { display: none; position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.drawing .lp-draw { display: block; }
.drawing .lp, .drawing .lp-glow, .drawing .lp-sheen, .drawing .lp-sweep { visibility: hidden; animation: none; }
/* The words are paved, not traced. Each letter's real filled glyph shows through a mask made of its
   strokes' centrelines, stroked a little wider than the glyph's own stroke; each centreline's dash grows
   from its start to its end (r12: many roads at once across both words, each eased like a car, driven by
   app.js roadKeyframes). So the ink is always the true solid letter, clipped to how far its road has been
   paved: no outline phase and no fill phase. The draw's own clock (dw-clock) ends the draw. r14: it runs
   4.7 s, the glide's end (app.js GLIDE_END, kept equal), so the swap to the crisp parts happens with the
   lockup at rest: the swap costs a long frame, and a glide still moving across it froze and snapped.
   r15: 5 s; r16: 5.4 s, the new GLIDE_END. */
.lp-draw { animation: dw-clock 5.4s linear forwards; }
.dw-road { fill: none; stroke: #fff; stroke-width: 5.9; stroke-linecap: butt; stroke-linejoin: round; stroke-dasharray: var(--len, 400) var(--len, 400); stroke-dashoffset: var(--len, 400); }
.dw-slit { clip-path: inset(0 0 100% 0); animation: dw-slit 0.9s cubic-bezier(0.65, 0, 0.35, 1) 3.2s forwards; }
/* Paved: the words are the crisp line parts from here on (the same img the idle uses), the drawn words gone. */
.drawing.paved .lp-l1, .drawing.paved .lp-l2 { visibility: visible; }
.paved .lp-draw > g ~ g { display: none; }
@keyframes dw-clock { to { opacity: 1; } }
@keyframes dw-slit { to { clip-path: inset(0 0 0 0); } }
/* r10: the D mark builds itself alongside the words (app.js startHeroDraw): the stem rises as a solid
   bar from 0.6 s, the bowl sweeps round as a filled dial from 1.4 s, and the slit ignites last, a beam
   slicing down the gap at 3.1 s that blooms (a solid light widening over the gaps and fading, no blur),
   then hands its light straight to the idle light pass, which starts the moment the draw ends. */
.dm-stem { transform-box: fill-box; transform-origin: 50% 100%; transform: scaleY(0); animation: dm-rise 1s cubic-bezier(0.16, 1, 0.3, 1) 0.6s forwards; }
.dm-dial { stroke-dasharray: 125.664 400; stroke-dashoffset: 125.664; animation: dm-sweep 1.5s cubic-bezier(0.45, 0, 0.2, 1) 1.4s forwards; }
.lp-draw .dw-slit { animation: dw-slit 0.45s cubic-bezier(0.7, 0, 0.3, 1) 3.1s forwards; }
.dm-bloom { opacity: 0; transform-box: fill-box; transform-origin: 50% 50%; animation: dm-bloom 0.9s ease-out 3.4s forwards; }
@keyframes dm-rise { to { transform: scaleY(1); } }
@keyframes dm-sweep { to { stroke-dashoffset: 0; } }
@keyframes dm-bloom { 0% { opacity: 0; transform: scaleX(1); } 25% { opacity: 0.9; transform: scaleX(2.6); } 100% { opacity: 0; transform: scaleX(1); } }
/* r14: the hero's first idle light pass waits the idle's usual 2.2 s after the draw ends (it was 0.05 s,
   to take the bloom's light on; the draw now ends at the landing, 0.5 s after the bloom has gone, and a
   pass starting there would read as one more intro beat after the scale has come to rest). */
[data-word="hero"] .lp-sweep { animation-delay: 2.2s; }
/* Contact: an ending rather than an arrival. The logo forms without its slit; once it has settled the
   slit is signed in last, top to bottom, the band of light riding its tip, and a single light passes
   and a glow settles. Then a slower idle than the hero's. Unsigned again when the reader leaves. */
.closing .lp-slit img { clip-path: inset(0 0 100% 0); }
.closing.signed .lp-slit img { animation: lp-draw 1.2s cubic-bezier(0.55, 0, 0.25, 1) 0.2s both; }
.closing .lp-sheen, .closing .lp-glow, .closing .lp-sweep { animation: none; opacity: 0; }
.closing.signed .lp-sheen { opacity: 1; animation: lp-sign 1.2s cubic-bezier(0.55, 0, 0.25, 1) 0.2s both, lp-sheen 11s cubic-bezier(0.45, 0, 0.3, 1) 4s infinite; }
.closing.signed .lp-glow { animation: lp-settle 2.4s ease-out 1.2s both, lp-breathe 7s ease-in-out 3.6s infinite; }
.closing.signed .lp-sweep { opacity: 1; animation: lp-sweep 14s cubic-bezier(0.5, 0, 0.3, 1) 1.5s infinite; }
@keyframes lp-sign { from { background-position-y: 83%; } to { background-position-y: 17%; } }
@keyframes lp-settle { 0% { opacity: 0; } 30% { opacity: 0.8; } 100% { opacity: 0; } }

/* r8: per-project card colours. Once a project's screenshot has fully arrived, app.js marks its section
   .is-set, and drops it again before the screenshot starts to break up; the card's accents ease over
   0.8 s to the project's own tokens and back. Roles: --acc (accent text: button label, status),
   --acc-line (button outline), --acc-fill and --acc-ink (button hover fill and its label), --head
   (the project name), --edge (a 1 px line round the screenshot), --frame (a 3 px frame inside it),
   --muted (the status label). Each project sets --p-<role>; any unset keeps the Dame Digital value.
   Every text pair is 4.5:1 or better on the #0E1116 ground; the darker hexes are only lines and fills. */
@property --acc { syntax: '<color>'; inherits: true; initial-value: #7CF3FF; }
@property --acc-line { syntax: '<color>'; inherits: true; initial-value: #7CF3FF; }
@property --acc-fill { syntax: '<color>'; inherits: true; initial-value: #7CF3FF; }
@property --acc-ink { syntax: '<color>'; inherits: true; initial-value: #0E1116; }
@property --head { syntax: '<color>'; inherits: true; initial-value: #E7EBF0; }
@property --edge { syntax: '<color>'; inherits: true; initial-value: rgb(124 243 255 / 0); }
@property --frame { syntax: '<color>'; inherits: true; initial-value: rgb(124 243 255 / 0); }
@property --muted { syntax: '<color>'; inherits: true; initial-value: #D2C2FF; }
/* r17: every colour that type or a control takes (--acc, --acc-line, --acc-fill, --acc-ink, --head,
   --muted) is the project's own token for as long as the card exists: set statically, never by a scroll
   state and never eased, so the site's cyan and daylight are never seen on a card that has its own
   system. Only the screenshot's own line and frame (--edge, --frame) still arrive with .is-set, and
   they ease in from the project's colour at zero alpha, not from the site's cyan. */
.project { --acc: var(--p-acc, #7CF3FF); --acc-line: var(--p-acc-line, #7CF3FF); --acc-fill: var(--p-acc-fill, #7CF3FF); --acc-ink: var(--p-acc-ink, #0E1116); --head: var(--p-head, #E7EBF0); --muted: var(--p-muted, #D2C2FF);
  --edge: var(--p-edge-off, rgb(124 243 255 / 0)); --frame: var(--p-frame-off, rgb(124 243 255 / 0));
  transition: --edge 0.8s ease, --frame 0.8s ease; }
.project.is-set, .no-js .project { --edge: var(--p-edge, rgb(124 243 255 / 0)); --frame: var(--p-frame, rgb(124 243 255 / 0)); }
.project .name { color: var(--head); }
.project .go { border-color: var(--acc-line); color: var(--acc); }
.project .go:hover { background: var(--acc-fill); color: var(--acc-ink); }
.project .stations li.on { color: var(--acc); }
.project .status { color: var(--muted) !important; }
.project .shot { position: relative; }
.project .shot::after { content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; box-shadow: inset 0 0 0 1px var(--edge), inset 0 0 0 4px var(--frame); }
/* The tokens, read from each project's own repository (sources in notes\BRIEF.md). Design Studio and
   Kylo keep the Dame Digital colours. */
/* Dame, ui-tokens colors.ts: evergreen frame, edge and button; mutedTeal accent text and name;
   hover: evergreen fill, dark-mode text #FAFAF8 (4.7:1). */
#dame { --p-acc: #FAFAF8; --p-acc-line: #2F7D68; --p-acc-fill: #286B59; --p-acc-ink: #FAFAF8; --p-head: #2F7D68; --p-edge: #2F7D68; --p-frame: #2F7D68; --p-edge-off: rgb(47 125 104 / 0); --p-frame-off: rgb(47 125 104 / 0); }
/* r17, Dame as Dame draws itself. colors.ts: evergreen #2F7D68 is the primary (the name, 3.8:1 on the
   ground, large display type only; the button's fill), dark-mode text #FAFAF8 on it (4.7:1).
   radii.ts: button 14. typography.ts: a plain grotesque in sentence case, Plus Jakarta Sans where
   loaded; Albert Sans, already in this site, is the closest face here. */
#dame .name { font-family: 'Albert Sans', sans-serif; font-weight: 700; text-transform: none; letter-spacing: -0.035em; line-height: 1; }
#dame .go { background: #2F7D68; color: #FAFAF8; border-radius: 14px; }
#dame .go:hover { background: #286B59; color: #FAFAF8; }
/* Wild Rose Rates, design tokens.ts: garnetLit edge line (structure only, never a fill), glacier frame,
   brassLit button accent, quartz name, slateLift button fill. */
#wildroserates { --p-acc: #191E24; --p-acc-line: #F8F7F4; --p-acc-fill: #E8D9B4; --p-acc-ink: #191E24; --p-head: #F8F7F4; --p-edge: #CC2A3C; --p-frame: #2F6C85; --p-muted: #CBDCE4; --p-edge-off: rgb(204 42 60 / 0); --p-frame-off: rgb(47 108 133 / 0); }
/* r17, Wild Rose Rates as its own site draws itself (uca\src\design\tokens.ts, uca\src\app\globals.css).
   Type: no webfont by decision; the display face is the system grotesque set tight (--wrr-font-display,
   h1 tracking -0.02em), in sentence case. Ground pair inverted for the dark card: quartz #F8F7F4 name,
   glacierPale #CBDCE4 supporting line. Geometry: radius 2px. The button is the solid primary, quartz
   fill with slate #191E24 ink (15.65:1), brassPale #E8D9B4 (the winning row's band) on hover. Garnet is
   only ever an edge: garnetLit #CC2A3C as the rule down the card's copy and the screenshot's line;
   glacier #2F6C85 the frame; brassLit #D2AE66 the rule under the name, brass being the one warm accent. */
#wildroserates .copy { border-left: 2px solid #CC2A3C; padding-left: clamp(0.9rem, 1.6vw, 1.5rem); }
#wildroserates .name { font-family: 'Segoe UI Variable Display', 'Segoe UI', Inter, system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif; font-weight: 600; text-transform: none; letter-spacing: -0.02em; line-height: 1.02; font-size: clamp(2.2rem, 4.3vw, 4.8rem); margin-bottom: 0.9rem; }
#wildroserates .name::after { content: ''; display: block; width: 2.2em; height: 2px; margin-top: 0.28em; background: #D2AE66; }
#wildroserates .lede { font-family: 'Segoe UI Variable Text', 'Segoe UI', Inter, system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif; font-weight: 400; letter-spacing: -0.005em; color: #CBDCE4; }
#wildroserates .go { font-family: 'Segoe UI Variable Text', 'Segoe UI', Inter, system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif; background: #F8F7F4; color: #191E24; border-radius: 2px; }
#wildroserates .go:hover { background: #E8D9B4; color: #191E24; border-color: #E8D9B4; }
#wildroserates .go:focus-visible { outline-color: #CC2A3C; border-radius: 2px; }
@media (max-width: 699px) { #wildroserates .name { font-size: clamp(1.8rem, 8.6vw, 2.8rem); } }
/* 3DP YYC, brand-package book.css: monochrome by design (no accent): ink frame and accents, muted
   labels, ink fill with paper label on hover. */
#\33 dp-yyc { --p-acc: #EBEBE8; --p-acc-line: #EBEBE8; --p-acc-fill: #EBEBE8; --p-acc-ink: #121212; --p-head: #EBEBE8; --p-edge: #EBEBE8; --p-frame: rgb(235 235 232 / 0); --p-muted: #9A9A94; --p-edge-off: rgb(235 235 232 / 0); --p-frame-off: rgb(235 235 232 / 0); }

/* Copy round 0, Vince 2026-10-06: the hero line is a scroll cue. An instruction, so it is set as a
   label (Antonio, capitals, open tracking), centred at the foot of the hero, glowing in the accent,
   with a chevron that drifts down. The chevron is drawn by CSS so the line's words stay editable. */
.hero-line.scroll-cue {
  justify-self: center; align-self: end; max-width: none; margin: 0 0 clamp(1.25rem, 4vh, 2.5rem);
  display: flex; flex-direction: column; align-items: center; gap: 0.65rem;
  font-family: 'Antonio', 'Albert Sans', sans-serif; font-weight: 600; font-size: clamp(0.85rem, 1vw, 1rem);
  line-height: 1; letter-spacing: 0.24em; text-transform: uppercase; text-indent: 0.24em;
  color: #7CF3FF; text-shadow: 0 0 10px rgb(124 243 255 / 0.55), 0 0 22px rgb(124 243 255 / 0.3);
  animation: cue-glow 2.8s ease-in-out infinite;
}
.hero-line.scroll-cue::after {
  content: ''; width: 0.7rem; height: 0.7rem; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  rotate: 45deg; filter: drop-shadow(0 0 6px rgb(124 243 255 / 0.7));
  animation: cue-drift 2.8s ease-in-out infinite;
}
@keyframes cue-glow { 0%, 100% { text-shadow: 0 0 8px rgb(124 243 255 / 0.4), 0 0 18px rgb(124 243 255 / 0.2); } 50% { text-shadow: 0 0 12px rgb(124 243 255 / 0.75), 0 0 28px rgb(124 243 255 / 0.4); } }
@keyframes cue-drift { 0%, 100% { translate: 0 -2px; opacity: 0.7; } 50% { translate: 0 6px; opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .hero-line.scroll-cue, .hero-line.scroll-cue::after { animation: none; }
}

