{"$schema":"https://ui.artbloom.tech/schema/registry-item.json","name":"riot","type":"registry:page","title":"RIOT","description":"A type-first kinetic manifesto for a design festival where Geist's weight axis is the instrument: a per-character load reveal and weight-on-hover headline, marquee ribbons whose speed and skew couple to scroll velocity, scroll-reactive programme rows and a pausable testimonial carousel. A two-colour riso-poster palette; no image assets; reduced-motion respected.","author":"@artbloom","dependencies":[],"registryDependencies":[],"files":[{"path":"app/riot/page.tsx","target":"app/riot/page.tsx","content":"\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\n\n/**\n * RIOT — a kinetic typographic manifesto for a design festival and the studio\n * behind it. Type is the whole design: Geist's weight axis is the instrument.\n *\n * Deliberate against the usual tells: the base is warm bone paper (not a tinted\n * near-black, not the cream-serif-terracotta editorial cliché), the register is\n * a two-colour riso gig-poster — electric cobalt as the signature with vermilion\n * only ever on paper (never on black), a grotesk display face (never a serif).\n * No tracked-caps eyebrows (the big caps are display, not label chrome), no\n * middle-dot metas (an asterisk motif separates the ticker), no arrow links, no\n * glassmorphism. One bold move — kinetic type — everything around it kept quiet.\n *\n * Signature moments:\n *  - the headline reveals per-character on load and animates its variable weight\n *    per-character on hover (heavier is wider in Geist, so weight carries width);\n *    the word \"quiet\" is set thin on purpose.\n *  - the marquee ribbons run at different speeds and *couple to scroll velocity* —\n *    scroll faster and they accelerate and skew. All motion is off under\n *    prefers-reduced-motion.\n */\n\ntype RTStyle = React.CSSProperties & Record<string, string | number>\n\nconst TICKER = [\n  \"RIOT 07\",\n  \"14–17 May\",\n  \"The Forge, Manchester\",\n  \"120 speakers, four stages\",\n  \"Early tickets close Friday\",\n  \"Make something loud\",\n]\n\nconst HERO_LINES: { t: string; quiet?: boolean }[][] = [\n  [{ t: \"NOBODY\" }],\n  [{ t: \"REMEMBERS\" }],\n  [{ t: \"THE\" }, { t: \"QUIET\", quiet: true }],\n  [{ t: \"ONES.\" }],\n]\n\nconst SESSIONS = [\n  { name: \"The death of the safe choice\", format: \"Keynote\", field: \"Identity\", room: \"Main stage\" },\n  { name: \"Set it twice as big\", format: \"Workshop\", field: \"Type\", room: \"Studio 2\" },\n  { name: \"Riso till the ink runs out\", format: \"Print lab\", field: \"Hands-on\", room: \"The Press Room\" },\n  { name: \"Break the grid on purpose\", format: \"Talk\", field: \"Layout\", room: \"Main stage\" },\n  { name: \"Make the motion mean something\", format: \"Talk\", field: \"Motion\", room: \"Studio 1\" },\n  { name: \"Arguing with clients, and winning\", format: \"Panel\", field: \"Practice\", room: \"Main stage\" },\n]\n\nconst PARTNERS = [\n  \"PULP\",\n  \"VERSO\",\n  \"COARSE\",\n  \"NOON\",\n  \"GRAIN\",\n  \"OFFCUT\",\n  \"RIOT RECORDS\",\n  \"THE FORGE\",\n  \"BOLD MATTER\",\n  \"STUDIO NEON\",\n]\n\nconst WORKS = [\n  { t: \"Festival identity 07\", k: \"Brand\" },\n  { t: \"Riso poster series\", k: \"Print\" },\n  { t: \"Stage motion package\", k: \"Motion\" },\n  { t: \"The programme, in print\", k: \"Editorial\" },\n  { t: \"Type specimen zine\", k: \"Type\" },\n]\n\nconst STATS = [\n  { v: 9400, suffix: \"+\", label: \"troublemakers through the doors\" },\n  { v: 120, suffix: \"\", label: \"speakers across four stages\" },\n  { v: 42, suffix: \"\", label: \"talks, labs and workshops\" },\n  { v: 4, suffix: \"\", label: \"days in one very loud room\" },\n]\n\nconst TESTIMONIALS = [\n  {\n    q: \"I walked in with a house style and left having burned it down. Best decision I made all year.\",\n    who: \"Priya Nair\",\n    role: \"Studio lead, Pulp\",\n  },\n  {\n    q: \"The only festival where the argument in the hallway beat the keynote — and the keynote was extraordinary.\",\n    who: \"Marcus Dodd\",\n    role: \"Art director\",\n  },\n  {\n    q: \"Loud, generous, a little unhinged. I've been to fifty of these. It's the one I re-book before I've even left.\",\n    who: \"Lena Kessler\",\n    role: \"Type designer\",\n  },\n]\n\n/** Reveal once when scrolled into view. Reduced-motion shows instantly. */\nfunction useReveal<T extends HTMLElement>(threshold = 0.25) {\n  const ref = useRef<T>(null)\n  const [shown, setShown] = useState(false)\n  useEffect(() => {\n    const node = ref.current\n    if (!node) return\n    if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n      setShown(true)\n      return\n    }\n    const io = new IntersectionObserver(\n      ([e]) => {\n        if (e.isIntersecting) {\n          setShown(true)\n          io.disconnect()\n        }\n      },\n      { threshold },\n    )\n    io.observe(node)\n    return () => io.disconnect()\n  }, [threshold])\n  return [ref, shown] as const\n}\n\n/** One big count-up stat, triggered when it scrolls into view. */\nfunction Stat({ v, suffix, label }: { v: number; suffix: string; label: string }) {\n  const [ref, shown] = useReveal<HTMLDivElement>(0.5)\n  const n = useCountUp(v, shown)\n  return (\n    <div className=\"rt-stat\" ref={ref}>\n      <span className=\"rt-stat-num\">\n        {n.toLocaleString(\"en-US\")}\n        {suffix}\n      </span>\n      <span className=\"rt-stat-label\">{label}</span>\n    </div>\n  )\n}\n\n/** Count from 0 to target once `run` flips true. Reduced-motion jumps to target. */\nfunction useCountUp(target: number, run: boolean, dur = 1500) {\n  const [n, setN] = useState(0)\n  useEffect(() => {\n    if (!run) return\n    if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n      setN(target)\n      return\n    }\n    let raf = 0\n    let start = 0\n    const step = (t: number) => {\n      if (!start) start = t\n      const p = Math.min(1, (t - start) / dur)\n      const eased = 1 - Math.pow(1 - p, 3)\n      setN(Math.round(eased * target))\n      if (p < 1) raf = requestAnimationFrame(step)\n    }\n    raf = requestAnimationFrame(step)\n    return () => cancelAnimationFrame(raf)\n  }, [target, run, dur])\n  return n\n}\n\n/**\n * A looping ribbon whose speed and skew are coupled to scroll velocity via the\n * shared refs. Two identical segments wrap seamlessly. Static under reduced motion.\n */\nfunction Ribbon({\n  children,\n  speed,\n  dir,\n  velMagRef,\n  velSignedRef,\n  reduced,\n  className,\n}: {\n  children: React.ReactNode\n  speed: number\n  dir: 1 | -1\n  velMagRef: React.MutableRefObject<number>\n  velSignedRef: React.MutableRefObject<number>\n  reduced: boolean\n  className?: string\n}) {\n  const trackRef = useRef<HTMLDivElement>(null)\n  const segRef = useRef<HTMLDivElement>(null)\n  useEffect(() => {\n    if (reduced || window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return\n    const track = trackRef.current\n    const seg = segRef.current\n    if (!track || !seg) return\n    let raf = 0\n    let last = performance.now()\n    let off = 0\n    const loop = (t: number) => {\n      const dt = Math.min(64, t - last) || 16\n      last = t\n      const w = seg.offsetWidth || 1\n      const boost = 1 + velMagRef.current * 4.5\n      off = (off + (speed / 1000) * dt * boost) % w\n      const base = dir < 0 ? -off : off - w\n      const skew = Math.max(-9, Math.min(9, velSignedRef.current * 2.6))\n      track.style.transform = `translate3d(${base.toFixed(2)}px,0,0) skewX(${skew.toFixed(2)}deg)`\n      raf = requestAnimationFrame(loop)\n    }\n    raf = requestAnimationFrame(loop)\n    return () => cancelAnimationFrame(raf)\n  }, [reduced, speed, dir, velMagRef, velSignedRef])\n  return (\n    <div className={`rt-ribbon ${className || \"\"}`}>\n      <div className=\"rt-ribbon-track\" ref={trackRef}>\n        <div className=\"rt-ribbon-seg\" ref={segRef}>\n          {children}\n        </div>\n        <div className=\"rt-ribbon-seg\" aria-hidden=\"true\">\n          {children}\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport default function Riot() {\n  const [reduced, setReduced] = useState(false)\n  const [loaded, setLoaded] = useState(false)\n\n  const velMagRef = useRef(0)\n  const velSignedRef = useRef(0)\n  const rowsRef = useRef<(HTMLElement | null)[]>([])\n\n  const [ti, setTi] = useState(0)\n  const [paused, setPaused] = useState(false)\n  const [signed, setSigned] = useState(false)\n\n  const [maniRef, maniShown] = useReveal<HTMLDivElement>(0.3)\n\n  useEffect(() => {\n    setLoaded(true)\n  }, [])\n\n  useEffect(() => {\n    if (paused) return\n    if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return\n    const id = window.setInterval(() => setTi((i) => (i + 1) % TESTIMONIALS.length), 5200)\n    return () => window.clearInterval(id)\n  }, [paused])\n\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n    const sync = () => setReduced(mq.matches)\n    sync()\n    mq.addEventListener(\"change\", sync)\n    if (mq.matches) {\n      return () => mq.removeEventListener(\"change\", sync)\n    }\n\n    let raf = 0\n    let lastY = window.scrollY\n    let lastT = performance.now()\n    const loop = (t: number) => {\n      const dt = Math.min(64, t - lastT) || 16\n      lastT = t\n      const y = window.scrollY\n      const inst = (y - lastY) / dt // px per ms\n      lastY = y\n      velSignedRef.current += (inst - velSignedRef.current) * 0.12\n      const mag = Math.min(1, Math.abs(velSignedRef.current) / 3)\n      velMagRef.current += (mag - velMagRef.current) * 0.15\n\n      const rows = rowsRef.current\n      if (rows.length) {\n        const vh = window.innerHeight\n        for (const el of rows) {\n          if (!el) continue\n          const r = el.getBoundingClientRect()\n          const d = (r.top + r.height / 2 - vh / 2) / vh\n          const cl = Math.max(-1, Math.min(1, d))\n          const skew = Math.max(-5, Math.min(5, velSignedRef.current * 3.2))\n          const scale = 1 - Math.min(0.05, Math.abs(cl) * 0.05)\n          el.style.transform = `skewX(${skew.toFixed(2)}deg) scale(${scale.toFixed(3)})`\n        }\n      }\n      raf = requestAnimationFrame(loop)\n    }\n    raf = requestAnimationFrame(loop)\n    return () => {\n      mq.removeEventListener(\"change\", sync)\n      cancelAnimationFrame(raf)\n    }\n  }, [])\n\n  return (\n    <div className=\"riot-root\">\n      <style>{css}</style>\n\n      {/* TICKER — announcement marquee, scrolls away above the sticky nav */}\n      <div className=\"rt-ticker\" aria-hidden=\"true\">\n        <div className={`rt-ticker-track ${reduced ? \"\" : \"is-run\"}`}>\n          {[0, 1].map((seg) => (\n            <div className=\"rt-ticker-seg\" key={seg}>\n              {TICKER.map((item) => (\n                <span className=\"rt-ti-item\" key={item}>\n                  <span className=\"rt-ti-text\">{item}</span>\n                  <span className=\"rt-ast\" aria-hidden=\"true\">\n                    ✳\n                  </span>\n                </span>\n              ))}\n            </div>\n          ))}\n        </div>\n      </div>\n\n      {/* NAV — sticky */}\n      <header className=\"rt-nav\">\n        <a className=\"rt-word\" href=\"#top\">\n          RIOT\n        </a>\n        <nav className=\"rt-nav-links\" aria-label=\"Primary\">\n          <a href=\"#programme\">Programme</a>\n          <a href=\"#work\">Studio</a>\n          <a href=\"#manifesto\">Manifesto</a>\n          <a className=\"rt-btn rt-btn-sm\" href=\"#tickets\">\n            Get tickets\n          </a>\n        </nav>\n      </header>\n\n      {/* HERO — the centerpiece manifesto headline */}\n      <section className=\"rt-hero\" id=\"top\">\n        <div className=\"rt-hero-inner\">\n          <p className=\"rt-stamp\">Edition 07 / May 2026 / Manchester</p>\n          <h1 className={`rt-hero-title ${loaded ? \"rt-in\" : \"\"}`} aria-label=\"Nobody remembers the quiet ones.\">\n            <span aria-hidden=\"true\">\n              {(() => {\n                let ci = 0\n                return HERO_LINES.map((line, li) => (\n                  <span className=\"rt-line\" key={li}>\n                    {line.map((word, wi) => (\n                      <span className=\"rt-wordgrp\" key={wi}>\n                        {Array.from(word.t).map((c, k) => (\n                          <span\n                            className={`rt-ch ${word.quiet ? \"rt-ch-quiet\" : \"\"}`}\n                            style={{ \"--i\": ci++ } as RTStyle}\n                            key={k}\n                          >\n                            {c}\n                          </span>\n                        ))}\n                      </span>\n                    ))}\n                  </span>\n                ))\n              })()}\n            </span>\n          </h1>\n          <p className=\"rt-lede\">\n            RIOT is a four-day design festival and the studio behind it — talks, type, print and a lot of noise,\n            for people who refuse to blend in.\n          </p>\n          <div className=\"rt-hero-cta\">\n            <a className=\"rt-btn rt-btn-lg\" href=\"#tickets\">\n              Get tickets\n            </a>\n            <p className=\"rt-hero-note\">14–17 May at The Forge, Manchester. Early birds close Friday.</p>\n          </div>\n        </div>\n      </section>\n\n      {/* PROGRAMME — full-bleed lineup stack, rows skew/scale on scroll velocity */}\n      <section className=\"rt-prog\" id=\"programme\">\n        <div className=\"rt-prog-head\">\n          <h2 className=\"rt-h2\">The programme</h2>\n          <p className=\"rt-prog-sub\">\n            Six of the loudest sessions from RIOT 07. The full schedule lands with your ticket.\n          </p>\n        </div>\n        <div className=\"rt-prog-list\">\n          {SESSIONS.map((s, i) => (\n            <article\n              className=\"rt-row\"\n              key={s.name}\n              ref={(el) => {\n                rowsRef.current[i] = el\n              }}\n            >\n              <div className=\"rt-row-main\">\n                <span className=\"rt-row-fmt\">{s.format}</span>\n                <h3 className=\"rt-row-name\">{s.name}</h3>\n              </div>\n              <div className=\"rt-row-meta\">\n                <span className=\"rt-row-field\">{s.field}</span>\n                <span className=\"rt-row-room\">{s.room}</span>\n              </div>\n            </article>\n          ))}\n        </div>\n      </section>\n\n      {/* RIBBONS — speed + skew couple to scroll velocity */}\n      <section className=\"rt-ribbons\" id=\"work\" aria-label=\"In motion\">\n        <Ribbon\n          speed={90}\n          dir={-1}\n          velMagRef={velMagRef}\n          velSignedRef={velSignedRef}\n          reduced={reduced}\n          className=\"rt-ribbon-word\"\n        >\n          {Array.from({ length: 4 }).map((_, i) => (\n            <span className=\"rt-rib-word\" key={i}>\n              LOUDER <span className=\"rt-rib-star\">✳</span> BIGGER <span className=\"rt-rib-star\">✳</span> RUDER{\" \"}\n              <span className=\"rt-rib-star\">✳</span>{\" \"}\n            </span>\n          ))}\n        </Ribbon>\n\n        <Ribbon\n          speed={55}\n          dir={1}\n          velMagRef={velMagRef}\n          velSignedRef={velSignedRef}\n          reduced={reduced}\n          className=\"rt-ribbon-logos\"\n        >\n          {Array.from({ length: 3 }).map((_, rep) => (\n            <span className=\"rt-rib-logos-grp\" key={rep}>\n              {PARTNERS.map((p) => (\n                <span className=\"rt-rib-logo\" key={p}>\n                  {p}\n                </span>\n              ))}\n            </span>\n          ))}\n        </Ribbon>\n\n        <Ribbon\n          speed={34}\n          dir={-1}\n          velMagRef={velMagRef}\n          velSignedRef={velSignedRef}\n          reduced={reduced}\n          className=\"rt-ribbon-work\"\n        >\n          {Array.from({ length: 3 }).map((_, rep) => (\n            <span className=\"rt-rib-work-grp\" key={rep}>\n              {WORKS.map((w, i) => (\n                <span className={`rt-tile rt-tile-${i % 4}`} key={`${rep}-${w.t}`}>\n                  <span className=\"rt-tile-k\">{w.k}</span>\n                  <span className=\"rt-tile-t\">{w.t}</span>\n                </span>\n              ))}\n            </span>\n          ))}\n        </Ribbon>\n      </section>\n\n      {/* STATS + VOICES */}\n      <section className=\"rt-stats-wrap\">\n        <div className=\"rt-stats\">\n          {STATS.map((s) => (\n            <Stat key={s.label} v={s.v} suffix={s.suffix} label={s.label} />\n          ))}\n        </div>\n\n        <div\n          className=\"rt-voices\"\n          onMouseEnter={() => setPaused(true)}\n          onMouseLeave={() => setPaused(false)}\n          onFocusCapture={() => setPaused(true)}\n          onBlurCapture={() => setPaused(false)}\n        >\n          <div className=\"rt-voices-quote\" aria-live=\"polite\">\n            <blockquote key={ti} className=\"rt-voices-q\">\n              <p>{TESTIMONIALS[ti].q}</p>\n              <cite>\n                <span className=\"rt-voices-who\">{TESTIMONIALS[ti].who}</span>\n                <span className=\"rt-voices-role\">{TESTIMONIALS[ti].role}</span>\n              </cite>\n            </blockquote>\n          </div>\n          <div className=\"rt-voices-nav\">\n            <button\n              type=\"button\"\n              className=\"rt-voices-arrow\"\n              aria-label=\"Previous quote\"\n              onClick={() => setTi((i) => (i - 1 + TESTIMONIALS.length) % TESTIMONIALS.length)}\n            >\n              ‹\n            </button>\n            <div className=\"rt-dots\" role=\"tablist\" aria-label=\"Choose a quote\">\n              {TESTIMONIALS.map((t, i) => (\n                <button\n                  key={t.who}\n                  type=\"button\"\n                  role=\"tab\"\n                  aria-selected={i === ti}\n                  aria-label={`Quote ${i + 1} of ${TESTIMONIALS.length}`}\n                  className={`rt-dot ${i === ti ? \"is-on\" : \"\"}`}\n                  onClick={() => setTi(i)}\n                />\n              ))}\n            </div>\n            <button\n              type=\"button\"\n              className=\"rt-voices-arrow\"\n              aria-label=\"Next quote\"\n              onClick={() => setTi((i) => (i + 1) % TESTIMONIALS.length)}\n            >\n              ›\n            </button>\n          </div>\n        </div>\n      </section>\n\n      {/* MANIFESTO — cobalt field, the edition statement */}\n      <section className=\"rt-mani\" id=\"manifesto\" ref={maniRef}>\n        <span className=\"rt-mani-ed\" aria-hidden=\"true\">\n          07\n        </span>\n        <div className={`rt-mani-inner ${maniShown ? \"rt-in\" : \"\"}`}>\n          <p className=\"rt-mani-kick\">What RIOT believes</p>\n          <p className=\"rt-mani-text\">\n            Design should pick a side. Safe is the expensive option. If nobody argues, you didn&apos;t say\n            anything. We&apos;d rather be <span className=\"rt-mani-hot\">wrong and loud</span> than right and quiet.\n          </p>\n        </div>\n      </section>\n\n      {/* TICKETS — CTA */}\n      <section className=\"rt-tickets\" id=\"tickets\">\n        <div className=\"rt-tickets-inner\">\n          <h2 className=\"rt-tickets-title\">Tickets for RIOT 07 are live</h2>\n          <p className=\"rt-tickets-sub\">\n            Four days, one pass. Early-bird pricing holds until Friday, then it goes up and we don&apos;t feel bad\n            about it.\n          </p>\n          {signed ? (\n            <p className=\"rt-tickets-done\" role=\"status\">\n              See you in May. Check your inbox for the reservation.\n            </p>\n          ) : (\n            <form\n              className=\"rt-form\"\n              onSubmit={(e) => {\n                e.preventDefault()\n                setSigned(true)\n              }}\n            >\n              <label className=\"rt-visually-hidden\" htmlFor=\"rt-email\">\n                Email address\n              </label>\n              <input\n                id=\"rt-email\"\n                className=\"rt-input\"\n                type=\"email\"\n                required\n                placeholder=\"you@studio.com\"\n                autoComplete=\"email\"\n              />\n              <button className=\"rt-btn rt-btn-lg rt-form-btn\" type=\"submit\">\n                Hold my spot\n              </button>\n            </form>\n          )}\n          <p className=\"rt-tickets-note\">Group rates for studios of five or more. Students get in for a tenner.</p>\n        </div>\n      </section>\n\n      {/* FOOTER */}\n      <footer className=\"rt-footer\">\n        <div className=\"rt-footer-cols\">\n          <div className=\"rt-footer-col\">\n            <h3 className=\"rt-footer-h\">Festival</h3>\n            <a href=\"#programme\">Programme</a>\n            <a href=\"#tickets\">Tickets</a>\n            <a href=\"#manifesto\">Manifesto</a>\n          </div>\n          <div className=\"rt-footer-col\">\n            <h3 className=\"rt-footer-h\">Studio</h3>\n            <a href=\"#work\">Work</a>\n            <a href=\"#tickets\">Hire us</a>\n            <a href=\"#top\">About</a>\n          </div>\n          <div className=\"rt-footer-col\">\n            <h3 className=\"rt-footer-h\">Follow</h3>\n            <a href=\"#top\">Instagram</a>\n            <a href=\"#top\">Newsletter</a>\n            <a href=\"#top\">Say hello</a>\n          </div>\n        </div>\n        <p className=\"rt-footer-line\">\n          RIOT is a design festival, and a studio, based wherever there&apos;s an argument worth having.\n        </p>\n        <div className=\"rt-footer-word\" aria-hidden=\"true\">\n          RIOT\n        </div>\n      </footer>\n\n    </div>\n  )\n}\n\nconst css = `\n.riot-root {\n  --paper: #eee7d9;\n  --paper-hi: #f7f3ea;\n  --ink: #141414;\n  --blue: #2b34ff;\n  --blue-deep: #1c25e6;\n  --hot: #ff3a1f;\n  --line: rgba(20, 20, 20, 0.16);\n  --line-soft: rgba(20, 20, 20, 0.09);\n  background: var(--paper);\n  color: var(--ink);\n  font-family: var(--font-inter), system-ui, sans-serif;\n  -webkit-font-smoothing: antialiased;\n  line-height: 1.55;\n  overflow-x: hidden;\n}\n.riot-root *,\n.riot-root *::before,\n.riot-root *::after { box-sizing: border-box; }\n.riot-root ::selection { background: var(--blue); color: var(--paper-hi); }\n.riot-root a { color: inherit; }\n.riot-root a:focus-visible,\n.riot-root button:focus-visible,\n.riot-root input:focus-visible {\n  outline: 3px solid var(--blue);\n  outline-offset: 3px;\n}\n\n.rt-word {\n  font-family: var(--font-geist), system-ui, sans-serif;\n  font-weight: 900;\n  font-variation-settings: \"wght\" 900;\n  letter-spacing: -0.04em;\n  text-decoration: none;\n  font-size: 1.5rem;\n  line-height: 1;\n}\n\n/* BUTTONS */\n.rt-btn {\n  display: inline-flex; align-items: center; justify-content: center;\n  background: var(--blue); color: var(--paper-hi);\n  font-family: var(--font-geist), system-ui, sans-serif;\n  font-weight: 700; font-variation-settings: \"wght\" 700;\n  border: none; cursor: pointer; text-decoration: none;\n  border-radius: 2px; letter-spacing: -0.01em;\n  padding: 0.72rem 1.1rem; font-size: 0.98rem;\n  transition: transform 0.28s cubic-bezier(0.2, 0.8, 0.2, 1), background 0.28s;\n}\n.rt-btn:hover { transform: translateY(-2px); background: var(--blue-deep); }\n.rt-btn:active { transform: translateY(0); }\n.rt-btn-lg { padding: 1rem 1.7rem; font-size: 1.12rem; }\n.rt-btn-sm { padding: 0.5rem 0.95rem; font-size: 0.9rem; }\n\n/* TICKER */\n.rt-ticker {\n  background: var(--blue); color: var(--paper-hi);\n  overflow: hidden; border-bottom: 2px solid var(--ink);\n}\n.rt-ticker-track { display: flex; width: max-content; }\n.rt-ticker-track.is-run { animation: rtMarquee 26s linear infinite; }\n.rt-ticker-seg { display: flex; flex: 0 0 auto; }\n.rt-ti-item {\n  display: inline-flex; align-items: center; gap: 1.1rem;\n  padding: 0.5rem 1.1rem;\n  font-family: var(--font-geist-mono), ui-monospace, monospace;\n  font-size: 0.82rem; white-space: nowrap;\n}\n.rt-ti-text { opacity: 0.96; }\n.rt-ast { color: var(--paper-hi); font-size: 0.7rem; opacity: 0.85; }\n@keyframes rtMarquee { from { transform: translate3d(0,0,0); } to { transform: translate3d(-50%,0,0); } }\n\n/* NAV */\n.rt-nav {\n  position: sticky; top: 0; z-index: 60;\n  display: flex; align-items: center; justify-content: space-between;\n  gap: 1rem;\n  padding: 0.9rem clamp(1.1rem, 4vw, 3rem);\n  background: var(--paper);\n  border-bottom: 2px solid var(--ink);\n}\n.rt-nav-links { display: flex; align-items: center; gap: clamp(1rem, 2.4vw, 2rem); }\n.rt-nav-links a {\n  text-decoration: none; font-size: 0.98rem; font-weight: 500;\n  color: var(--ink);\n}\n.rt-nav-links a:not(.rt-btn) { position: relative; }\n.rt-nav-links a:not(.rt-btn)::after {\n  content: \"\"; position: absolute; left: 0; right: 100%; bottom: -3px;\n  height: 2px; background: var(--blue); transition: right 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);\n}\n.rt-nav-links a:not(.rt-btn):hover::after { right: 0; }\n@media (max-width: 620px) { .rt-nav-links a:not(.rt-btn) { display: none; } }\n\n/* HERO */\n.rt-hero {\n  padding: clamp(2.5rem, 7vw, 6rem) clamp(1.1rem, 4vw, 3rem) clamp(3rem, 8vw, 6rem);\n  border-bottom: 2px solid var(--ink);\n}\n.rt-hero-inner { max-width: min(96vw, 90rem); margin: 0 auto; }\n.rt-stamp {\n  font-family: var(--font-geist-mono), ui-monospace, monospace;\n  font-size: 0.9rem; color: var(--ink); opacity: 0.7;\n  margin: 0 0 clamp(1.2rem, 3vw, 2.4rem);\n}\n.rt-hero-title {\n  margin: 0;\n  font-family: var(--font-geist), system-ui, sans-serif;\n  font-weight: 850;\n  line-height: 0.86;\n  letter-spacing: -0.045em;\n  text-transform: none;\n  font-size: clamp(3.4rem, 15.5vw, 15rem);\n}\n.rt-line { display: flex; flex-wrap: wrap; gap: 0.26em; align-items: baseline; }\n.rt-wordgrp { display: inline-flex; }\n.rt-ch {\n  display: inline-block;\n  font-variation-settings: \"wght\" 850;\n  font-weight: 850;\n  color: var(--ink);\n  opacity: 0;\n  transform: translateY(0.55em);\n  transition: font-variation-settings 0.35s cubic-bezier(0.2, 0.8, 0.2, 1), color 0.35s;\n  will-change: font-variation-settings;\n}\n.rt-hero-title.rt-in .rt-ch {\n  animation: rtCharIn 0.72s cubic-bezier(0.16, 1, 0.3, 1) both;\n  animation-delay: calc(var(--i) * 0.028s);\n}\n.rt-ch:hover { font-variation-settings: \"wght\" 900; color: var(--blue); }\n.rt-ch-quiet {\n  font-variation-settings: \"wght\" 220;\n  font-weight: 220;\n  color: var(--blue);\n  font-style: italic;\n}\n.rt-ch-quiet:hover { font-variation-settings: \"wght\" 900; color: var(--ink); }\n@keyframes rtCharIn { from { opacity: 0; transform: translateY(0.6em); } to { opacity: 1; transform: none; } }\n\n.rt-lede {\n  max-width: 42rem;\n  margin: clamp(1.6rem, 4vw, 3rem) 0 0;\n  font-size: clamp(1.1rem, 1.9vw, 1.5rem);\n  line-height: 1.4;\n  color: var(--ink);\n}\n.rt-hero-cta {\n  display: flex; align-items: center; flex-wrap: wrap; gap: 1.2rem;\n  margin-top: clamp(1.6rem, 3.5vw, 2.6rem);\n}\n.rt-hero-note {\n  margin: 0; font-size: 0.98rem; opacity: 0.72; max-width: 22rem;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .rt-ticker-track.is-run { animation: none; }\n  .rt-hero-title.rt-in .rt-ch { animation: none; }\n  .rt-ch { opacity: 1; transform: none; transition: none; }\n  .rt-nav-links a:not(.rt-btn)::after { transition: none; }\n  .rt-btn { transition: none; }\n}\n\n/* PROGRAMME */\n.rt-prog { padding: clamp(3.5rem, 8vw, 7rem) 0 0; }\n.rt-prog-head {\n  max-width: min(96vw, 90rem); margin: 0 auto;\n  padding: 0 clamp(1.1rem, 4vw, 3rem) clamp(1.5rem, 4vw, 2.8rem);\n}\n.rt-h2 {\n  font-family: var(--font-geist), system-ui, sans-serif;\n  font-weight: 800; font-variation-settings: \"wght\" 800;\n  font-size: clamp(2rem, 5.5vw, 4rem); line-height: 0.95;\n  letter-spacing: -0.04em; margin: 0 0 0.7rem;\n}\n.rt-prog-sub { margin: 0; max-width: 34rem; font-size: 1.05rem; opacity: 0.75; }\n.rt-prog-list { border-top: 2px solid var(--ink); }\n.rt-row {\n  display: flex; align-items: center; justify-content: space-between; gap: 1.5rem;\n  padding: clamp(1.3rem, 3.4vw, 2.7rem) clamp(1.1rem, 4vw, 3rem);\n  border-bottom: 2px solid var(--ink);\n  transform-origin: left center; will-change: transform;\n  transition: background 0.3s, color 0.3s;\n}\n.rt-row:hover { background: var(--ink); color: var(--paper); }\n.rt-row-main { display: flex; align-items: baseline; gap: clamp(0.8rem, 2vw, 1.8rem); min-width: 0; }\n.rt-row-fmt {\n  flex: 0 0 auto;\n  font-family: var(--font-geist-mono), ui-monospace, monospace;\n  font-size: 0.85rem; opacity: 0.7; padding-top: 0.4em;\n}\n.rt-row-name {\n  margin: 0;\n  font-family: var(--font-geist), system-ui, sans-serif;\n  font-weight: 750; font-variation-settings: \"wght\" 750;\n  font-size: clamp(1.6rem, 5.2vw, 4.4rem); line-height: 0.98;\n  letter-spacing: -0.035em;\n}\n.rt-row:hover .rt-row-name { font-variation-settings: \"wght\" 880; }\n.rt-row-meta {\n  flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end;\n  gap: 0.15rem; text-align: right;\n}\n.rt-row-field {\n  font-family: var(--font-geist), system-ui, sans-serif;\n  font-weight: 700; font-variation-settings: \"wght\" 700;\n  font-size: clamp(1rem, 1.6vw, 1.25rem); color: var(--blue); letter-spacing: -0.01em;\n}\n.rt-row:hover .rt-row-field { color: var(--hot); }\n.rt-row-room { font-size: 0.9rem; opacity: 0.65; }\n@media (max-width: 620px) {\n  .rt-row { flex-direction: column; align-items: flex-start; gap: 0.5rem; }\n  .rt-row-meta { flex-direction: row; align-items: baseline; gap: 0.8rem; }\n}\n@media (prefers-reduced-motion: reduce) {\n  .rt-row { transform: none !important; transition: background 0.3s, color 0.3s; }\n}\n\n/* RIBBONS */\n.rt-ribbons { border-top: 2px solid var(--ink); }\n.rt-ribbon { overflow: hidden; border-bottom: 2px solid var(--ink); }\n.rt-ribbon-track { display: flex; width: max-content; will-change: transform; }\n.rt-ribbon-seg { display: flex; flex: 0 0 auto; align-items: center; }\n\n.rt-ribbon-word { background: var(--paper); }\n.rt-rib-word {\n  display: inline-block; white-space: nowrap;\n  font-family: var(--font-geist), system-ui, sans-serif;\n  font-weight: 900; font-variation-settings: \"wght\" 900;\n  font-size: clamp(2.4rem, 7vw, 6rem); line-height: 1.1;\n  letter-spacing: -0.04em; padding: 0.35em 0;\n  color: var(--ink);\n}\n.rt-rib-star { color: var(--hot); }\n\n.rt-ribbon-logos { background: var(--blue); }\n.rt-rib-logos-grp { display: inline-flex; align-items: center; }\n.rt-rib-logo {\n  display: inline-block; white-space: nowrap;\n  font-family: var(--font-geist), system-ui, sans-serif;\n  font-weight: 800; font-variation-settings: \"wght\" 800;\n  font-size: clamp(1.3rem, 3vw, 2.4rem); letter-spacing: -0.02em;\n  color: var(--paper-hi); opacity: 0.92;\n  padding: 0.7rem clamp(1.2rem, 3vw, 2.6rem);\n}\n\n.rt-ribbon-work { background: var(--paper-hi); }\n.rt-rib-work-grp { display: inline-flex; align-items: stretch; }\n.rt-tile {\n  display: inline-flex; flex-direction: column; justify-content: space-between;\n  white-space: normal;\n  width: clamp(13rem, 22vw, 18rem); min-height: clamp(8rem, 13vw, 11rem);\n  margin: clamp(1rem, 2vw, 1.6rem); padding: 1.1rem 1.2rem;\n  border: 2px solid var(--ink); border-radius: 2px;\n}\n.rt-tile-0 { background: var(--blue); color: var(--paper-hi); border-color: var(--blue); }\n.rt-tile-1 { background: var(--paper); color: var(--ink); }\n.rt-tile-2 { background: var(--ink); color: var(--paper); border-color: var(--ink); }\n.rt-tile-3 { background: var(--hot); color: var(--ink); border-color: var(--hot); }\n.rt-tile-k {\n  font-family: var(--font-geist-mono), ui-monospace, monospace;\n  font-size: 0.78rem; opacity: 0.85;\n}\n.rt-tile-t {\n  font-family: var(--font-geist), system-ui, sans-serif;\n  font-weight: 750; font-variation-settings: \"wght\" 750;\n  font-size: clamp(1.1rem, 1.7vw, 1.4rem); line-height: 1.05; letter-spacing: -0.02em;\n}\n\n/* STATS + VOICES */\n.rt-stats-wrap { border-bottom: 2px solid var(--ink); }\n.rt-stats {\n  display: grid; grid-template-columns: repeat(4, 1fr);\n}\n.rt-stat {\n  padding: clamp(2rem, 5vw, 4rem) clamp(1.1rem, 2.5vw, 2rem);\n  border-right: 2px solid var(--ink); border-bottom: 2px solid var(--ink);\n  display: flex; flex-direction: column; gap: 0.5rem;\n}\n.rt-stat:last-child { border-right: none; }\n.rt-stat-num {\n  font-family: var(--font-geist), system-ui, sans-serif;\n  font-weight: 850; font-variation-settings: \"wght\" 850;\n  font-size: clamp(2.6rem, 7vw, 5.5rem); line-height: 0.9;\n  letter-spacing: -0.05em; font-variant-numeric: tabular-nums;\n}\n.rt-stat:nth-child(2) .rt-stat-num { color: var(--blue); }\n.rt-stat:nth-child(4) .rt-stat-num { color: var(--hot); }\n.rt-stat-label { font-size: 0.95rem; opacity: 0.72; line-height: 1.3; max-width: 15rem; }\n@media (max-width: 760px) {\n  .rt-stats { grid-template-columns: repeat(2, 1fr); }\n  .rt-stat:nth-child(2n) { border-right: none; }\n}\n@media (max-width: 440px) {\n  .rt-stats { grid-template-columns: 1fr; }\n  .rt-stat { border-right: none; }\n}\n\n.rt-voices {\n  max-width: min(96vw, 66rem); margin: 0 auto;\n  padding: clamp(2.5rem, 7vw, 6rem) clamp(1.1rem, 4vw, 3rem);\n  display: flex; flex-direction: column; gap: clamp(1.5rem, 4vw, 2.5rem);\n}\n.rt-voices-quote { min-height: clamp(9rem, 22vw, 12rem); display: flex; }\n.rt-voices-q { margin: 0; animation: rtFade 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; }\n.rt-voices-q p {\n  margin: 0 0 1.4rem;\n  font-family: var(--font-geist), system-ui, sans-serif;\n  font-weight: 500; font-variation-settings: \"wght\" 500;\n  font-size: clamp(1.4rem, 3.6vw, 2.6rem); line-height: 1.18; letter-spacing: -0.025em;\n}\n.rt-voices-q cite { font-style: normal; display: flex; flex-direction: column; gap: 0.1rem; }\n.rt-voices-who { font-weight: 700; font-size: 1.05rem; }\n.rt-voices-role { opacity: 0.65; font-size: 0.95rem; }\n.rt-voices-nav { display: flex; align-items: center; gap: 1.1rem; }\n.rt-voices-arrow {\n  width: 2.6rem; height: 2.6rem; flex: 0 0 auto;\n  background: var(--paper); color: var(--ink);\n  border: 2px solid var(--ink); border-radius: 2px; cursor: pointer;\n  font-size: 1.5rem; line-height: 1; display: grid; place-items: center;\n  transition: background 0.25s, color 0.25s;\n}\n.rt-voices-arrow:hover { background: var(--ink); color: var(--paper); }\n.rt-dots { display: flex; gap: 0.5rem; }\n.rt-dot {\n  width: 0.85rem; height: 0.85rem; padding: 0; border-radius: 999px;\n  border: 2px solid var(--ink); background: transparent; cursor: pointer;\n  transition: background 0.25s;\n}\n.rt-dot.is-on { background: var(--blue); border-color: var(--blue); }\n@keyframes rtFade { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }\n@media (prefers-reduced-motion: reduce) {\n  .rt-voices-q { animation: none; }\n  .rt-voices-arrow { transition: none; }\n}\n\n/* MANIFESTO */\n.rt-mani {\n  position: relative; overflow: hidden;\n  background: var(--blue); color: var(--paper-hi);\n  border-bottom: 2px solid var(--ink);\n  padding: clamp(3.5rem, 10vw, 8rem) clamp(1.1rem, 4vw, 3rem);\n}\n.rt-mani-ed {\n  position: absolute; right: -0.08em; bottom: -0.32em; margin: 0;\n  font-family: var(--font-geist), system-ui, sans-serif;\n  font-weight: 900; font-variation-settings: \"wght\" 900;\n  font-size: clamp(16rem, 42vw, 42rem); line-height: 0.7;\n  color: var(--blue-deep); letter-spacing: -0.06em; pointer-events: none;\n  z-index: 0;\n}\n.rt-mani-inner {\n  position: relative; z-index: 1; max-width: min(96vw, 62rem); margin: 0 auto;\n  opacity: 0; transform: translateY(24px);\n  transition: opacity 0.9s cubic-bezier(0.16, 1, 0.3, 1), transform 0.9s cubic-bezier(0.16, 1, 0.3, 1);\n}\n.rt-mani-inner.rt-in { opacity: 1; transform: none; }\n.rt-mani-kick {\n  font-family: var(--font-geist-mono), ui-monospace, monospace;\n  font-size: 0.9rem; opacity: 0.8; margin: 0 0 1.4rem;\n}\n.rt-mani-text {\n  margin: 0;\n  font-family: var(--font-geist), system-ui, sans-serif;\n  font-weight: 650; font-variation-settings: \"wght\" 650;\n  font-size: clamp(1.7rem, 5vw, 4rem); line-height: 1.08; letter-spacing: -0.035em;\n}\n.rt-mani-hot { color: var(--hot); }\n\n/* TICKETS */\n.rt-tickets {\n  border-bottom: 2px solid var(--ink);\n  padding: clamp(3.5rem, 9vw, 7rem) clamp(1.1rem, 4vw, 3rem);\n}\n.rt-tickets-inner { max-width: min(96vw, 56rem); margin: 0 auto; text-align: center; }\n.rt-tickets-title {\n  margin: 0 0 1rem;\n  font-family: var(--font-geist), system-ui, sans-serif;\n  font-weight: 850; font-variation-settings: \"wght\" 850;\n  font-size: clamp(2.2rem, 7vw, 5.5rem); line-height: 0.92; letter-spacing: -0.045em;\n}\n.rt-tickets-sub { margin: 0 auto 2.2rem; max-width: 38rem; font-size: 1.1rem; opacity: 0.78; }\n.rt-form {\n  display: flex; gap: 0.7rem; max-width: 30rem; margin: 0 auto; flex-wrap: wrap;\n}\n.rt-input {\n  flex: 1 1 14rem; min-width: 0;\n  padding: 1rem 1.1rem; font-size: 1.05rem;\n  background: var(--paper-hi); color: var(--ink);\n  border: 2px solid var(--ink); border-radius: 2px;\n  font-family: inherit;\n}\n.rt-input::placeholder { color: rgba(20, 20, 20, 0.45); }\n.rt-form-btn { flex: 0 0 auto; }\n.rt-tickets-done {\n  font-family: var(--font-geist), system-ui, sans-serif;\n  font-weight: 700; font-variation-settings: \"wght\" 700;\n  font-size: 1.3rem; color: var(--blue); margin: 0.5rem 0 0;\n}\n.rt-tickets-note { margin: 1.6rem 0 0; font-size: 0.9rem; opacity: 0.6; }\n.rt-visually-hidden {\n  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;\n  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;\n}\n\n/* FOOTER */\n.rt-footer { padding: clamp(3rem, 6vw, 5rem) clamp(1.1rem, 4vw, 3rem) 0; overflow: hidden; }\n.rt-footer-cols {\n  display: flex; flex-wrap: wrap; gap: clamp(2rem, 8vw, 6rem);\n  max-width: min(96vw, 90rem); margin: 0 auto;\n}\n.rt-footer-col { display: flex; flex-direction: column; gap: 0.55rem; }\n.rt-footer-h {\n  margin: 0 0 0.5rem;\n  font-family: var(--font-geist-mono), ui-monospace, monospace;\n  font-size: 0.85rem; font-weight: 500; opacity: 0.6; text-transform: none;\n}\n.rt-footer-col a { text-decoration: none; font-weight: 600; font-size: 1.05rem; width: fit-content; }\n.rt-footer-col a:hover { color: var(--blue); }\n.rt-footer-line {\n  max-width: 34rem; margin: clamp(2.5rem, 6vw, 4rem) auto clamp(1.5rem, 4vw, 2.5rem) 0;\n  font-size: 1.05rem; opacity: 0.7;\n}\n.rt-footer-word {\n  font-family: var(--font-geist), system-ui, sans-serif;\n  font-weight: 900; font-variation-settings: \"wght\" 900;\n  font-size: clamp(6rem, 27vw, 26rem); line-height: 0.72;\n  letter-spacing: -0.05em; color: var(--ink);\n  margin: 0 0 -0.14em; user-select: none;\n}\n\n`\n","type":"registry:page"}],"meta":{"kind":"templates","categories":["agency","events"],"docs":"https://ui.artbloom.tech/artbloom/templates/riot"}}