{"$schema":"https://ui.artbloom.tech/schema/registry-item.json","name":"solstice","type":"registry:page","title":"Solstice","description":"A cinematic desert-resort stay: a video hero (with a Ken-Burns still fallback) behind a persistent booking bar, parallax room reveals, two-image hover-swap suite cards and an editorial sense-of-place narrative. Ships a looping hero video, seven art-directed property renders; motion respects reduced-motion.","author":"@artbloom","dependencies":[],"registryDependencies":[],"files":[{"path":"app/solstice/page.tsx","target":"app/solstice/page.tsx","content":"\"use client\"\n\nimport { useEffect, useMemo, useRef, useState } from \"react\"\n\n/**\n * Solstice — an atmosphere-led landing for a boutique high-desert resort, built\n * to the cinematic-immersive bar in DESIGN.md.\n *\n * Deliberate against the tells that got earlier work rejected: the base is a deep\n * dusk indigo warmed toward charred clay (not a brown-tinted fake black, not the\n * cream+serif+terracotta cliché), the single accent is an ember pulled straight\n * from the sunset in the photography (never acid green or vermilion), there are no\n * tracked-uppercase eyebrows, no middle-dot metas, no em-dash fragment labels, no\n * arrow links, and no 01/02/03 numbering — the suites are a set, not a sequence.\n *\n * The one bold move is the booking affordance: it lives inside the hero, then\n * detaches to dock as a sticky reservation bar once you scroll past the fold, both\n * driven by the same reservation state. The hero itself is a slow ken-burns\n * crossfade between two shots of the same place, day into dusk. All motion is\n * real, scroll-driven, and gated on prefers-reduced-motion.\n */\n\nconst SUITES = [\n  {\n    name: \"The Ochre Room\",\n    view: \"Canyon-facing\",\n    img: \"/solstice/room1.png\",\n    alt: \"A sand-toned desert-modern suite with a low linen bed and a deep arched window onto an ochre canyon at golden hour\",\n    swap: \"/solstice/pool.png\",\n    swapAlt: \"The rooftop infinity pool the Ochre Room opens onto, glowing amber at sunset\",\n    copy: \"A plastered room the colour of the canyon it faces, with an arched window wide enough to lose an afternoon in. Wakes gold, holds the heat, cools to indigo.\",\n    tone: \"#8a6a4a\",\n  },\n  {\n    name: \"Dune House\",\n    view: \"Private courtyard\",\n    img: \"/solstice/room2.png\",\n    alt: \"A warm minimal desert suite with a sunken plaster lounge, sculptural fireplace, and a walled courtyard with a plunge pool\",\n    swap: \"/solstice/spa.png\",\n    swapAlt: \"The domed hammam Dune House guests have on private request, lit by a single skylight\",\n    copy: \"A sunken lounge around a low fire, a walled courtyard, a plunge pool that no one else can see. Made for the guests who came here to disappear for a while.\",\n    tone: \"#6f5540\",\n  },\n]\n\nconst EXPERIENCES = [\n  {\n    name: \"The long table\",\n    img: \"/solstice/dining.png\",\n    alt: \"A long communal dining table set on warm sand at dusk under strings of small warm lights, with an open fire\",\n    copy: \"One seating, one fire, one menu that changes with what the high desert gives up that week. Dinner runs until the last story does.\",\n  },\n  {\n    name: \"The rooftop water\",\n    img: \"/solstice/pool.png\",\n    alt: \"A rooftop infinity pool at sunset, its edge dissolving into a vast ochre canyon and mesa landscape\",\n    copy: \"An infinity edge that gives out onto forty miles of nothing. Best an hour before sundown, when the water turns the same colour as the sky.\",\n  },\n  {\n    name: \"The hammam\",\n    img: \"/solstice/spa.png\",\n    alt: \"A domed desert spa hammam with a single skylight casting a warm beam onto a heated stone platform\",\n    copy: \"A domed room, a single shaft of light, heated stone underfoot. Salt, steam, and an hour where the only thing asked of you is to lie still.\",\n  },\n]\n\nconst GALLERY = [\n  { img: \"/solstice/hero.png\", alt: \"The resort's earth pavilions against the canyon wall at golden hour\", span: \"wide\" },\n  { img: \"/solstice/dining.png\", alt: \"Candlelit desert dining at dusk\", span: \"tall\" },\n  { img: \"/solstice/room1.png\", alt: \"The Ochre Room interior in warm light\", span: \"small\" },\n  { img: \"/solstice/spa.png\", alt: \"The domed hammam under its skylight\", span: \"small\" },\n  { img: \"/solstice/hero2.png\", alt: \"The resort at blue hour, windows glowing amber\", span: \"wide\" },\n  { img: \"/solstice/pool.png\", alt: \"The rooftop pool at sunset over the mesa\", span: \"tall\" },\n]\n\nconst FAQS = [\n  {\n    q: \"How do I get there?\",\n    a: \"We're a two-hour drive from the nearest regional airport, the last forty minutes on graded desert road. Send us your flights and we'll arrange the transfer — a single vehicle, met at arrivals, water and cold towels waiting.\",\n  },\n  {\n    q: \"When is the desert at its best?\",\n    a: \"October through April, when the days run warm and the nights turn cold enough for a fire. High summer is fierce and beautiful and half-price, if you know what you're walking into.\",\n  },\n  {\n    q: \"Is the resort suitable for children?\",\n    a: \"The main house is adults-only after seven. Dune House and the two garden suites welcome families, and we keep a short list of guides who are wonderful with older children.\",\n  },\n  {\n    q: \"Can you host a wedding?\",\n    a: \"We take a handful each year and only ever one at a time — the whole property, yours for three nights. The long table seats forty; the terrace holds a hundred under the stars.\",\n  },\n]\n\n/** Reveal children once, when they scroll into view. Reduced-motion shows instantly. */\nfunction useReveal<T extends HTMLElement>() {\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: 0.2 },\n    )\n    io.observe(node)\n    return () => io.disconnect()\n  }, [])\n  return [ref, shown] as const\n}\n\nexport default function Solstice() {\n  const [scrollY, setScrollY] = useState(0)\n  const [reduced, setReduced] = useState(false)\n  const [docked, setDocked] = useState(false)\n  // A hero video is optional: it fades in over the ken-burns stills once it can\n  // play, and if the file is absent (onError) the stills stay as the fallback.\n  const [videoReady, setVideoReady] = useState(false)\n  const videoRef = useRef<HTMLVideoElement>(null)\n\n  // Shared reservation state — the hero card and the docked bar are two views of it.\n  const today = useMemo(() => new Date().toISOString().slice(0, 10), [])\n  const [arrive, setArrive] = useState(\"\")\n  const [depart, setDepart] = useState(\"\")\n  const [guests, setGuests] = useState(2)\n  const [openFaq, setOpenFaq] = useState<number | null>(0)\n\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n    setReduced(mq.matches)\n    const onMq = () => setReduced(mq.matches)\n    mq.addEventListener(\"change\", onMq)\n\n    let raf = 0\n    const onScroll = () => {\n      if (raf) return\n      raf = requestAnimationFrame(() => {\n        const y = window.scrollY\n        setScrollY(y)\n        setDocked(y > window.innerHeight * 0.82)\n        raf = 0\n      })\n    }\n    window.addEventListener(\"scroll\", onScroll, { passive: true })\n    onScroll()\n    return () => {\n      mq.removeEventListener(\"change\", onMq)\n      window.removeEventListener(\"scroll\", onScroll)\n      if (raf) cancelAnimationFrame(raf)\n    }\n  }, [])\n\n  // The hero video usually loads before React attaches onCanPlay, so the event\n  // is missed and the layer stays at opacity 0 over the stills. Check readyState\n  // on mount and kick off muted playback imperatively (always autoplay-allowed).\n  useEffect(() => {\n    const v = videoRef.current\n    if (!v || reduced) return\n    if (v.readyState >= 3) setVideoReady(true)\n    const p = v.play()\n    if (p && typeof p.catch === \"function\") p.catch(() => {})\n  }, [reduced])\n\n  const par = (rate: number) => (reduced ? 0 : scrollY * rate)\n\n  const [placeRef, placeShown] = useReveal<HTMLDivElement>()\n  const [suitesRef, suitesShown] = useReveal<HTMLDivElement>()\n\n  const scrollToBook = () => {\n    document.getElementById(\"reserve\")?.scrollIntoView({\n      behavior: reduced ? \"auto\" : \"smooth\",\n      block: \"center\",\n    })\n  }\n\n  return (\n    <div className=\"solstice-root\">\n      <style>{css}</style>\n\n      {/* Docked reservation bar — detaches from the hero once past the fold */}\n      <div className={`so-dock ${docked ? \"is-docked\" : \"\"}`} aria-hidden={!docked}>\n        <a className=\"so-dock-mark\" href=\"#top\">\n          Solstice\n        </a>\n        <form\n          className=\"so-dock-form\"\n          onSubmit={(e) => {\n            e.preventDefault()\n            scrollToBook()\n          }}\n        >\n          <label className=\"so-field so-field-sm\">\n            <span>Arrive</span>\n            <input\n              type=\"date\"\n              min={today}\n              value={arrive}\n              onChange={(e) => setArrive(e.target.value)}\n              tabIndex={docked ? 0 : -1}\n            />\n          </label>\n          <label className=\"so-field so-field-sm\">\n            <span>Depart</span>\n            <input\n              type=\"date\"\n              min={arrive || today}\n              value={depart}\n              onChange={(e) => setDepart(e.target.value)}\n              tabIndex={docked ? 0 : -1}\n            />\n          </label>\n          <label className=\"so-field so-field-sm\">\n            <span>Guests</span>\n            <select\n              value={guests}\n              onChange={(e) => setGuests(Number(e.target.value))}\n              tabIndex={docked ? 0 : -1}\n            >\n              {[1, 2, 3, 4].map((n) => (\n                <option key={n} value={n}>\n                  {n} {n === 1 ? \"guest\" : \"guests\"}\n                </option>\n              ))}\n            </select>\n          </label>\n          <button className=\"so-cta so-cta-sm\" type=\"submit\" tabIndex={docked ? 0 : -1}>\n            Check availability\n          </button>\n        </form>\n      </div>\n\n      <header className=\"so-nav\">\n        <a className=\"so-mark\" href=\"#top\">\n          Solstice\n        </a>\n        <nav className=\"so-links\">\n          <a href=\"#place\">The place</a>\n          <a href=\"#suites\">Suites</a>\n          <a href=\"#reserve\" className=\"so-nav-book\">\n            Reserve\n          </a>\n        </nav>\n      </header>\n\n      {/* HERO — ken-burns crossfade between day and dusk, booking card set into it */}\n      <section className=\"so-hero\" id=\"top\">\n        <div className={`so-hero-stage ${reduced ? \"is-static\" : \"\"}`} aria-hidden=\"true\">\n          <div className=\"so-hero-layer so-hero-a\" />\n          <div className=\"so-hero-layer so-hero-b\" />\n        </div>\n        {!reduced && (\n          <video\n            ref={videoRef}\n            className={`so-hero-video ${videoReady ? \"is-ready\" : \"\"}`}\n            src=\"/solstice/hero.mp4\"\n            poster=\"/solstice/hero.png\"\n            autoPlay\n            muted\n            loop\n            playsInline\n            preload=\"auto\"\n            aria-hidden=\"true\"\n            onCanPlay={() => setVideoReady(true)}\n            onError={() => setVideoReady(false)}\n          />\n        )}\n        <div className=\"so-hero-scrim\" />\n        <img className=\"so-hero-alt\" src=\"/solstice/hero.png\" alt=\"A boutique high-desert resort of earth-toned pavilions against an ochre canyon at golden hour\" />\n\n        <div className=\"so-hero-inner\" style={{ transform: `translate3d(0, ${par(-0.08)}px, 0)` }}>\n          <p className=\"so-kicker\">A high-desert retreat, forty miles past the last town</p>\n          <h1 className=\"so-title\">\n            Where the desert\n            <br />\n            keeps its quiet\n          </h1>\n          <p className=\"so-lede\">\n            Eleven rooms cut into a canyon that has stayed dark and silent for a very long time.\n            You are welcome to do the same.\n          </p>\n        </div>\n\n        {/* Booking card — the affordance that later docks to the top */}\n        <form\n          className={`so-book ${docked ? \"is-lifted\" : \"\"}`}\n          onSubmit={(e) => {\n            e.preventDefault()\n            scrollToBook()\n          }}\n          aria-label=\"Check room availability\"\n        >\n          <label className=\"so-field\">\n            <span>Arrive</span>\n            <input type=\"date\" min={today} value={arrive} onChange={(e) => setArrive(e.target.value)} />\n          </label>\n          <label className=\"so-field\">\n            <span>Depart</span>\n            <input\n              type=\"date\"\n              min={arrive || today}\n              value={depart}\n              onChange={(e) => setDepart(e.target.value)}\n            />\n          </label>\n          <label className=\"so-field\">\n            <span>Guests</span>\n            <select value={guests} onChange={(e) => setGuests(Number(e.target.value))}>\n              {[1, 2, 3, 4].map((n) => (\n                <option key={n} value={n}>\n                  {n} {n === 1 ? \"guest\" : \"guests\"}\n                </option>\n              ))}\n            </select>\n          </label>\n          <button className=\"so-cta\" type=\"submit\">\n            Check availability\n          </button>\n        </form>\n\n        <div className=\"so-scrollcue\" aria-hidden=\"true\">\n          <span />\n        </div>\n      </section>\n\n      {/* SENSE OF PLACE — signature shot, caption drifts slower than the image */}\n      <section className=\"so-place\" id=\"place\" ref={placeRef}>\n        <div\n          className=\"so-place-img\"\n          style={{ transform: `translate3d(0, ${par(0.07)}px, 0) scale(1.06)` }}\n        />\n        <div className=\"so-place-scrim\" />\n        <div className={`so-place-body ${placeShown ? \"is-shown\" : \"\"}`} style={{ transform: `translate3d(0, ${par(-0.05)}px, 0)` }}>\n          <p className=\"so-place-caption\">\n            The canyon was here for six million years before the first wall went up. We tried to\n            build something that would apologise for the interruption.\n          </p>\n          <p className=\"so-place-sub\">\n            Eleven rooms, one long table, a pool cut into the roof. Rammed earth the colour of the\n            cliffs, so that from a mile out you would struggle to say where the land ends and the\n            rooms begin. No screens in the common rooms. No music you did not bring yourself.\n          </p>\n        </div>\n      </section>\n\n      {/* SUITES — named categories, two-image hover swap */}\n      <section className=\"so-suites\" id=\"suites\" ref={suitesRef}>\n        <div className=\"so-section-head\">\n          <h2 className=\"so-h2\">Eleven ways to stay</h2>\n          <p className=\"so-section-sub\">\n            Every room faces the canyon or a courtyard of its own. Hover to see where each one leads.\n          </p>\n        </div>\n        <div className={`so-suite-grid ${suitesShown ? \"is-shown\" : \"\"}`}>\n          {SUITES.map((s) => (\n            <article className=\"so-suite\" key={s.name} style={{ [\"--tone\" as string]: s.tone }}>\n              <div className=\"so-suite-media\">\n                <img className=\"so-suite-img so-suite-base\" src={s.img} alt={s.alt} loading=\"lazy\" />\n                <img className=\"so-suite-img so-suite-swap\" src={s.swap} alt={s.swapAlt} loading=\"lazy\" />\n              </div>\n              <div className=\"so-suite-body\">\n                <div className=\"so-suite-line\">\n                  <h3 className=\"so-suite-name\">{s.name}</h3>\n                  <span className=\"so-suite-view\">{s.view}</span>\n                </div>\n                <p className=\"so-suite-copy\">{s.copy}</p>\n              </div>\n            </article>\n          ))}\n        </div>\n      </section>\n\n      {/* EXPERIENCES — image blocks, reveal on scroll, alternating sides */}\n      <section className=\"so-exp\">\n        <div className=\"so-section-head\">\n          <h2 className=\"so-h2\">What the days are made of</h2>\n          <p className=\"so-section-sub\">Nothing scheduled. Everything available.</p>\n        </div>\n        <div className=\"so-exp-list\">\n          {EXPERIENCES.map((x, i) => (\n            <ExpRow key={x.name} x={x} flip={i % 2 === 1} par={par} />\n          ))}\n        </div>\n      </section>\n\n      {/* MID-PAGE CTA BANNER */}\n      <section className=\"so-banner\">\n        <div className=\"so-banner-img\" style={{ transform: `translate3d(0, ${par(0.06)}px, 0) scale(1.08)` }} />\n        <div className=\"so-banner-scrim\" />\n        <div className=\"so-banner-inner\">\n          <h2 className=\"so-banner-title\">The desert is quietest in the cold months</h2>\n          <p className=\"so-banner-sub\">\n            October through April, the fires are lit by five and the sky is yours by nine.\n          </p>\n          <button className=\"so-cta so-cta-lg\" type=\"button\" onClick={scrollToBook}>\n            Find your nights\n          </button>\n        </div>\n      </section>\n\n      {/* MEETINGS / EVENTS / WEDDINGS */}\n      <section className=\"so-events\">\n        <div className=\"so-events-grid\">\n          <div className=\"so-events-copy\">\n            <h2 className=\"so-h2\">Take the whole place</h2>\n            <p className=\"so-section-sub so-events-sub\">\n              A handful of times a year we close the gate and hand Solstice to one gathering. A\n              wedding, a company off-site, a reunion of people who owe each other a long weekend.\n            </p>\n            <ul className=\"so-events-facts\">\n              <li>\n                <span className=\"so-fact-num\">11</span>\n                <span className=\"so-fact-label\">rooms, sleeping twenty-six</span>\n              </li>\n              <li>\n                <span className=\"so-fact-num\">40</span>\n                <span className=\"so-fact-label\">at the long table, a hundred on the terrace</span>\n              </li>\n              <li>\n                <span className=\"so-fact-num\">3</span>\n                <span className=\"so-fact-label\">nights, minimum, for a full buyout</span>\n              </li>\n            </ul>\n            <button className=\"so-text-cta\" type=\"button\" onClick={scrollToBook}>\n              Ask about a buyout\n            </button>\n          </div>\n          <div className=\"so-events-media\">\n            <img src=\"/solstice/dining.png\" alt=\"The long communal table lit for an evening event under strings of warm light\" loading=\"lazy\" />\n          </div>\n        </div>\n      </section>\n\n      {/* GALLERY — small masonry */}\n      <section className=\"so-gallery\">\n        <div className=\"so-section-head\">\n          <h2 className=\"so-h2\">The property</h2>\n          <p className=\"so-section-sub\">A few frames. The rest we save for when you arrive.</p>\n        </div>\n        <div className=\"so-masonry\">\n          {GALLERY.map((g) => (\n            <figure className={`so-tile so-tile-${g.span}`} key={g.img + g.alt}>\n              <img src={g.img} alt={g.alt} loading=\"lazy\" />\n            </figure>\n          ))}\n        </div>\n      </section>\n\n      {/* FAQ + RESERVE */}\n      <section className=\"so-faq\" id=\"reserve\">\n        <div className=\"so-faq-grid\">\n          <div className=\"so-faq-left\">\n            <h2 className=\"so-h2\">Before you come</h2>\n            <p className=\"so-section-sub\">\n              A few things worth knowing. For anything else, a real person answers within a day.\n            </p>\n            <div className=\"so-reserve-card\">\n              <p className=\"so-reserve-line\">Ready when you are.</p>\n              <button className=\"so-cta so-cta-lg\" type=\"button\" onClick={() => document.getElementById(\"top\")?.scrollIntoView({ behavior: reduced ? \"auto\" : \"smooth\" })}>\n                Check availability\n              </button>\n              <p className=\"so-reserve-note\">No deposit to enquire. We hold rooms for two days while you decide.</p>\n            </div>\n          </div>\n          <ul className=\"so-faq-list\">\n            {FAQS.map((f, i) => (\n              <li key={f.q} className={`so-faq-item ${openFaq === i ? \"is-open\" : \"\"}`}>\n                <button\n                  className=\"so-faq-q\"\n                  aria-expanded={openFaq === i}\n                  onClick={() => setOpenFaq(openFaq === i ? null : i)}\n                >\n                  <span>{f.q}</span>\n                  <span className=\"so-faq-icon\" aria-hidden=\"true\" />\n                </button>\n                <div className=\"so-faq-a\">\n                  <p>{f.a}</p>\n                </div>\n              </li>\n            ))}\n          </ul>\n        </div>\n      </section>\n\n      <footer className=\"so-footer\">\n        <div className=\"so-footer-top\">\n          <div>\n            <span className=\"so-mark\">Solstice</span>\n            <p className=\"so-footer-line\">A boutique retreat in the high desert.</p>\n          </div>\n          <div className=\"so-footer-cols\">\n            <div className=\"so-footer-col\">\n              <h4>Find us</h4>\n              <p>Cañon del Sol, off Route 12</p>\n              <p>Reachable by transfer only</p>\n            </div>\n            <div className=\"so-footer-col\">\n              <h4>Hours</h4>\n              <p>Reception around the clock</p>\n              <p>The long table seats at eight</p>\n            </div>\n            <div className=\"so-footer-col\">\n              <h4>Reach a person</h4>\n              <p>stay@solstice.example</p>\n              <p>Replies within a day</p>\n            </div>\n          </div>\n        </div>\n        <div className=\"so-footer-fine\">\n          <span>Open October through April, and the brave weeks either side.</span>\n        </div>\n      </footer>\n    </div>\n  )\n}\n\n/** One experience block — image and copy, revealed on scroll, image parallaxed. */\nfunction ExpRow({\n  x,\n  flip,\n  par,\n}: {\n  x: (typeof EXPERIENCES)[number]\n  flip: boolean\n  par: (rate: number) => number\n}) {\n  const [ref, shown] = useReveal<HTMLDivElement>()\n  return (\n    <article ref={ref} className={`so-exp-row ${flip ? \"is-flip\" : \"\"} ${shown ? \"is-shown\" : \"\"}`}>\n      <div className=\"so-exp-media\">\n        <img\n          src={x.img}\n          alt={x.alt}\n          loading=\"lazy\"\n          style={{ transform: `translate3d(0, ${par(0.04)}px, 0) scale(1.06)` }}\n        />\n      </div>\n      <div className=\"so-exp-copy\">\n        <h3 className=\"so-exp-name\">{x.name}</h3>\n        <p className=\"so-exp-text\">{x.copy}</p>\n      </div>\n    </article>\n  )\n}\n\nconst css = `\n.solstice-root {\n  --dusk: #16121c;\n  --dusk-2: #1b1620;\n  --clay: #251d18;\n  --sand: #ece0cf;\n  --sand-mute: #a99e8e;\n  --ember: #db7440;\n  --ember-soft: #e69463;\n  --line: rgba(236, 224, 207, 0.14);\n  background: var(--dusk);\n  color: var(--sand);\n  font-family: var(--font-geist), system-ui, sans-serif;\n  -webkit-font-smoothing: antialiased;\n  line-height: 1.6;\n  overflow-x: hidden;\n}\n.solstice-root *,\n.solstice-root *::before,\n.solstice-root *::after { box-sizing: border-box; }\n\n.so-mark {\n  font-family: var(--font-fraunces), Georgia, serif;\n  font-weight: 500;\n  letter-spacing: 0.01em;\n  color: var(--sand);\n  text-decoration: none;\n  font-size: 1.3rem;\n  font-variation-settings: \"opsz\" 40, \"SOFT\" 40;\n}\n\n/* NAV */\n.so-nav {\n  position: absolute; top: 0; left: 0; right: 0; z-index: 40;\n  display: flex; align-items: center; justify-content: space-between;\n  padding: 1.5rem clamp(1.25rem, 5vw, 4rem);\n}\n.so-links { display: flex; align-items: center; gap: clamp(1rem, 2.6vw, 2.6rem); }\n.so-links a { color: var(--sand); text-decoration: none; font-size: 0.95rem; opacity: 0.82; transition: opacity 0.35s; }\n.so-links a:hover { opacity: 1; }\n.so-nav-book {\n  border: 1px solid rgba(236, 224, 207, 0.4);\n  border-radius: 999px; padding: 0.5rem 1.15rem; opacity: 1 !important;\n}\n.so-nav-book:hover { background: var(--sand); color: var(--dusk) !important; }\n.solstice-root a:focus-visible,\n.solstice-root button:focus-visible,\n.solstice-root input:focus-visible,\n.solstice-root select:focus-visible {\n  outline: 2px solid var(--ember-soft); outline-offset: 3px; border-radius: 4px;\n}\n@media (max-width: 680px) { .so-links a:not(.so-nav-book) { display: none; } }\n\n/* HERO */\n.so-hero {\n  position: relative; min-height: 100svh; overflow: hidden;\n  display: flex; flex-direction: column; justify-content: space-between;\n  gap: clamp(2rem, 6vh, 4rem); box-sizing: border-box;\n  padding: clamp(7rem, 22vh, 12rem) clamp(1.25rem, 6vw, 6rem) clamp(2.5rem, 8vh, 5rem);\n}\n.so-hero-stage { position: absolute; inset: 0; }\n.so-hero-layer { position: absolute; inset: 0; background-size: cover; background-position: center; will-change: transform, opacity; }\n.so-hero-a { background-image: url(/solstice/hero.png); animation: soFadeA 18s ease-in-out infinite, soKen 18s ease-in-out infinite; }\n.so-hero-b { background-image: url(/solstice/hero2.png); animation: soFadeB 18s ease-in-out infinite, soKen 18s ease-in-out infinite; }\n/* Static fallback image, only shown when motion is reduced */\n.so-hero-alt { display: none; }\n.so-hero-stage.is-static { display: none; }\n.so-hero-video {\n  position: absolute; inset: 0; width: 100%; height: 100%;\n  object-fit: cover; opacity: 0; z-index: 1;\n  transition: opacity 1.1s ease;\n}\n.so-hero-video.is-ready { opacity: 1; }\n\n@keyframes soFadeA { 0%, 42% { opacity: 1; } 50%, 92% { opacity: 0; } 100% { opacity: 1; } }\n@keyframes soFadeB { 0%, 42% { opacity: 0; } 50%, 92% { opacity: 1; } 100% { opacity: 0; } }\n@keyframes soKen { 0% { transform: scale(1.04) translate3d(0, 0, 0); } 50% { transform: scale(1.12) translate3d(-1.5%, -1.5%, 0); } 100% { transform: scale(1.04) translate3d(0, 0, 0); } }\n\n.so-hero-scrim {\n  position: absolute; inset: 0; z-index: 2;\n  background:\n    linear-gradient(to top, var(--dusk) 1%, rgba(23, 19, 32, 0.1) 42%, rgba(23, 19, 32, 0.5) 100%),\n    linear-gradient(to right, rgba(23, 19, 32, 0.55), transparent 55%);\n}\n.so-hero-inner {\n  position: relative; max-width: 44rem; will-change: transform; z-index: 2;\n}\n.so-kicker {\n  color: var(--ember-soft); font-size: 1rem; margin: 0 0 1.1rem;\n  font-family: var(--font-instrument), Georgia, serif; font-style: italic;\n  letter-spacing: 0.01em;\n}\n.so-title {\n  font-family: var(--font-fraunces), Georgia, serif;\n  font-weight: 420;\n  font-size: clamp(3rem, 9.5vw, 8rem);\n  line-height: 0.96; letter-spacing: -0.02em; margin: 0;\n  font-variation-settings: \"opsz\" 144, \"SOFT\" 60, \"WONK\" 0;\n}\n.so-lede {\n  font-size: clamp(1.05rem, 1.5vw, 1.3rem); color: var(--sand); opacity: 0.88;\n  max-width: 33rem; margin: 1.6rem 0 0;\n}\n\n/* HERO BOOKING CARD */\n.so-book {\n  position: relative; z-index: 3; width: 100%;\n  display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 0.5rem;\n  align-items: end; max-width: 52rem;\n  background: rgba(30, 24, 38, 0.92);\n  border: 1px solid var(--line); border-radius: 16px;\n  padding: 1rem 1rem;\n  box-shadow: 0 30px 70px -30px rgba(0, 0, 0, 0.7);\n  transition: opacity 0.5s ease, transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);\n}\n.so-book.is-lifted { opacity: 0; transform: translateY(14px); pointer-events: none; }\n.so-field { display: flex; flex-direction: column; gap: 0.35rem; min-width: 0; }\n.so-field > span {\n  font-size: 0.72rem; letter-spacing: 0.04em; color: var(--sand-mute);\n  text-transform: uppercase; padding-left: 0.15rem;\n}\n.so-field input,\n.so-field select {\n  font-family: var(--font-geist), system-ui, sans-serif;\n  background: rgba(23, 19, 32, 0.8); color: var(--sand);\n  border: 1px solid var(--line); border-radius: 9px;\n  padding: 0.7rem 0.75rem; font-size: 0.95rem; width: 100%;\n  color-scheme: dark;\n}\n.so-field input:hover, .so-field select:hover { border-color: rgba(236, 224, 207, 0.3); }\n.so-cta {\n  display: inline-flex; align-items: center; justify-content: center;\n  background: var(--ember); color: #1a0f07;\n  font-family: var(--font-geist), system-ui, sans-serif; font-weight: 600;\n  text-decoration: none; padding: 0.75rem 1.5rem; border: none;\n  border-radius: 9px; cursor: pointer; white-space: nowrap; font-size: 0.95rem;\n  transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.4s, background 0.3s;\n}\n.so-cta:hover { transform: translateY(-2px); background: var(--ember-soft); box-shadow: 0 16px 34px -14px rgba(219, 116, 64, 0.6); }\n\n/* DOCKED RESERVATION BAR */\n.so-dock {\n  position: fixed; top: 0; left: 0; right: 0; z-index: 60;\n  display: flex; align-items: center; gap: clamp(0.75rem, 2vw, 1.75rem);\n  padding: 0.7rem clamp(1rem, 4vw, 2.5rem);\n  background: rgba(23, 19, 32, 0.96);\n  border-bottom: 1px solid var(--line);\n  transform: translateY(-100%);\n  transition: transform 0.55s cubic-bezier(0.16, 1, 0.3, 1);\n  box-shadow: 0 18px 44px -26px rgba(0, 0, 0, 0.8);\n}\n.so-dock.is-docked { transform: translateY(0); }\n.so-dock-mark {\n  font-family: var(--font-fraunces), Georgia, serif; font-weight: 500;\n  color: var(--sand); text-decoration: none; font-size: 1.15rem; flex-shrink: 0;\n}\n.so-dock-form { display: flex; align-items: end; gap: 0.6rem; margin-left: auto; flex-wrap: wrap; justify-content: flex-end; }\n.so-field-sm > span { font-size: 0.62rem; margin-bottom: -0.1rem; }\n.so-field-sm input, .so-field-sm select { padding: 0.5rem 0.6rem; font-size: 0.88rem; }\n.so-cta-sm { padding: 0.6rem 1.1rem; font-size: 0.9rem; }\n@media (max-width: 720px) {\n  .so-dock-mark { display: none; }\n  .so-field-sm { flex: 1 1 30%; }\n  .so-book { grid-template-columns: 1fr 1fr; }\n  .so-book .so-cta { grid-column: 1 / -1; }\n}\n\n/* SCROLL CUE */\n.so-scrollcue { position: absolute; left: 50%; bottom: 1.4rem; transform: translateX(-50%); z-index: 3; }\n.so-scrollcue span {\n  display: block; width: 1px; height: 44px;\n  background: linear-gradient(to bottom, transparent, var(--sand));\n  animation: socue 2.6s ease-in-out infinite;\n}\n@keyframes socue { 0%, 100% { opacity: 0.2; transform: scaleY(0.55); transform-origin: top; } 50% { opacity: 0.9; transform: scaleY(1); } }\n\n/* SHARED SECTION HEADS */\n.so-section-head { max-width: 44rem; padding: 0 clamp(1.25rem, 6vw, 6rem); margin: 0 auto clamp(2.5rem, 5vw, 4rem); }\n.so-h2 {\n  font-family: var(--font-fraunces), Georgia, serif; font-weight: 440;\n  font-size: clamp(2rem, 5.2vw, 3.7rem); line-height: 1.0; letter-spacing: -0.02em; margin: 0 0 1rem;\n  font-variation-settings: \"opsz\" 120, \"SOFT\" 40;\n}\n.so-section-sub { color: var(--sand-mute); font-size: 1.1rem; margin: 0; max-width: 36rem; }\n\n/* SENSE OF PLACE */\n.so-place { position: relative; min-height: 108vh; display: grid; align-items: center; overflow: hidden; }\n.so-place-img { position: absolute; inset: -6% 0; background: url(/solstice/hero2.png) center/cover no-repeat; will-change: transform; }\n.so-place-scrim { position: absolute; inset: 0; background: linear-gradient(to right, rgba(23, 19, 32, 0.9) 0%, rgba(23, 19, 32, 0.6) 45%, rgba(23, 19, 32, 0.2) 100%); }\n.so-place-body {\n  position: relative; max-width: 40rem; padding: 0 clamp(1.25rem, 6vw, 6rem); will-change: transform;\n  opacity: 0; transform: translateY(28px);\n  transition: opacity 1s cubic-bezier(0.16, 1, 0.3, 1), transform 1s cubic-bezier(0.16, 1, 0.3, 1);\n}\n.so-place-body.is-shown { opacity: 1; transform: none; }\n.so-place-caption {\n  font-family: var(--font-fraunces), Georgia, serif; font-weight: 380;\n  font-size: clamp(1.6rem, 3.6vw, 3rem); line-height: 1.24; letter-spacing: -0.015em; margin: 0 0 1.6rem;\n  font-variation-settings: \"opsz\" 144, \"SOFT\" 80;\n}\n.so-place-sub { color: var(--sand); opacity: 0.82; font-size: 1.08rem; margin: 0; max-width: 32rem; }\n\n/* SUITES */\n.so-suites { padding: clamp(4.5rem, 10vw, 8rem) 0; background: var(--dusk); }\n.so-suite-grid {\n  display: grid; grid-template-columns: repeat(2, 1fr); gap: clamp(1.25rem, 3vw, 2.5rem);\n  padding: 0 clamp(1.25rem, 6vw, 6rem); max-width: 88rem; margin: 0 auto;\n}\n.so-suite {\n  opacity: 0; transform: translateY(30px);\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.so-suite-grid.is-shown .so-suite { opacity: 1; transform: none; }\n.so-suite-grid.is-shown .so-suite:nth-child(2) { transition-delay: 0.12s; }\n.so-suite-media {\n  position: relative; aspect-ratio: 3 / 2; border-radius: 14px; overflow: hidden;\n  border: 1px solid var(--line); background: var(--tone);\n}\n.so-suite-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }\n.so-suite-swap { opacity: 0; transform: scale(1.05); transition: opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1), transform 0.8s cubic-bezier(0.16, 1, 0.3, 1); }\n.so-suite-base { transition: opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1), transform 6s ease-out; }\n.so-suite:hover .so-suite-swap { opacity: 1; transform: scale(1); }\n.so-suite:hover .so-suite-base { opacity: 0; }\n.so-suite-body { padding: 1.3rem 0.4rem 0; }\n.so-suite-line { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }\n.so-suite-name { font-family: var(--font-fraunces), Georgia, serif; font-weight: 480; font-size: clamp(1.4rem, 2.4vw, 1.9rem); letter-spacing: -0.01em; margin: 0; }\n.so-suite-view { color: var(--ember-soft); font-size: 0.85rem; font-family: var(--font-instrument), Georgia, serif; font-style: italic; }\n.so-suite-copy { color: var(--sand); opacity: 0.78; font-size: 1rem; margin: 0.7rem 0 0; }\n@media (max-width: 720px) { .so-suite-grid { grid-template-columns: 1fr; } }\n\n/* EXPERIENCES */\n.so-exp { padding: clamp(4.5rem, 10vw, 8rem) 0; background: var(--dusk-2); }\n.so-exp-list { display: flex; flex-direction: column; gap: clamp(3rem, 7vw, 6rem); max-width: 80rem; margin: 0 auto; padding: 0 clamp(1.25rem, 6vw, 6rem); }\n.so-exp-row {\n  display: grid; grid-template-columns: 1.15fr 1fr; gap: clamp(1.5rem, 4vw, 4rem); align-items: center;\n  opacity: 0; transform: translateY(36px);\n  transition: opacity 1s cubic-bezier(0.16, 1, 0.3, 1), transform 1s cubic-bezier(0.16, 1, 0.3, 1);\n}\n.so-exp-row.is-shown { opacity: 1; transform: none; }\n.so-exp-row.is-flip .so-exp-media { order: 2; }\n.so-exp-media { border-radius: 14px; overflow: hidden; border: 1px solid var(--line); aspect-ratio: 3 / 2; }\n.so-exp-media img { width: 100%; height: 100%; object-fit: cover; will-change: transform; }\n.so-exp-name { font-family: var(--font-fraunces), Georgia, serif; font-weight: 460; font-size: clamp(1.7rem, 3.4vw, 2.7rem); letter-spacing: -0.02em; margin: 0 0 1rem; line-height: 1.05; }\n.so-exp-text { color: var(--sand); opacity: 0.82; font-size: 1.1rem; margin: 0; }\n@media (max-width: 760px) {\n  .so-exp-row, .so-exp-row.is-flip { grid-template-columns: 1fr; }\n  .so-exp-row.is-flip .so-exp-media { order: 0; }\n}\n\n/* MID BANNER */\n.so-banner { position: relative; min-height: 76vh; display: grid; place-items: center; overflow: hidden; }\n.so-banner-img { position: absolute; inset: -6% 0; background: url(/solstice/pool.png) center/cover no-repeat; will-change: transform; }\n.so-banner-scrim { position: absolute; inset: 0; background: radial-gradient(ellipse at center, rgba(23, 19, 32, 0.4), rgba(23, 19, 32, 0.85)); }\n.so-banner-inner { position: relative; text-align: center; max-width: 42rem; padding: 0 clamp(1.5rem, 6vw, 4rem); }\n.so-banner-title { font-family: var(--font-fraunces), Georgia, serif; font-weight: 440; font-size: clamp(2rem, 5.4vw, 4rem); line-height: 1.04; letter-spacing: -0.02em; margin: 0 0 1.1rem; }\n.so-banner-sub { color: var(--sand); opacity: 0.88; font-size: 1.15rem; margin: 0 auto 2rem; max-width: 32rem; }\n.so-cta-lg { padding: 1rem 2rem; font-size: 1.05rem; }\n\n/* EVENTS */\n.so-events { padding: clamp(4.5rem, 10vw, 8rem) clamp(1.25rem, 6vw, 6rem); background: var(--dusk); }\n.so-events-grid { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2rem, 5vw, 4.5rem); align-items: center; max-width: 80rem; margin: 0 auto; }\n.so-events-sub { margin-bottom: 2.2rem; }\n.so-events-facts { list-style: none; padding: 0; margin: 0 0 2.2rem; display: flex; flex-direction: column; gap: 1.1rem; }\n.so-events-facts li { display: flex; align-items: baseline; gap: 1.1rem; border-top: 1px solid var(--line); padding-top: 1.1rem; }\n.so-fact-num { font-family: var(--font-fraunces), Georgia, serif; font-weight: 460; font-size: 2.2rem; color: var(--ember-soft); font-variant-numeric: tabular-nums; min-width: 2.6rem; }\n.so-fact-label { color: var(--sand); opacity: 0.82; font-size: 1.02rem; }\n.so-events-media { border-radius: 14px; overflow: hidden; border: 1px solid var(--line); aspect-ratio: 4 / 5; }\n.so-events-media img { width: 100%; height: 100%; object-fit: cover; }\n.so-text-cta {\n  background: none; border: none; color: var(--ember-soft); cursor: pointer;\n  font-family: var(--font-geist), system-ui, sans-serif; font-size: 1.02rem; font-weight: 500;\n  padding: 0; position: relative;\n}\n.so-text-cta::after { content: \"\"; position: absolute; left: 0; right: 0; bottom: -3px; height: 1px; background: var(--ember-soft); transform: scaleX(0.35); transform-origin: left; transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1); }\n.so-text-cta:hover::after { transform: scaleX(1); }\n@media (max-width: 760px) { .so-events-grid { grid-template-columns: 1fr; } .so-events-media { order: -1; aspect-ratio: 3 / 2; } }\n\n/* GALLERY MASONRY */\n.so-gallery { padding: clamp(4.5rem, 10vw, 8rem) 0; background: var(--dusk-2); }\n.so-masonry {\n  display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-rows: 12rem; gap: 1rem;\n  padding: 0 clamp(1.25rem, 6vw, 6rem); max-width: 88rem; margin: 0 auto;\n}\n.so-tile { margin: 0; border-radius: 12px; overflow: hidden; border: 1px solid var(--line); }\n.so-tile img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1); }\n.so-tile:hover img { transform: scale(1.05); }\n.so-tile-wide { grid-column: span 2; grid-row: span 2; }\n.so-tile-tall { grid-column: span 1; grid-row: span 2; }\n.so-tile-small { grid-column: span 1; grid-row: span 1; }\n@media (max-width: 760px) {\n  .so-masonry { grid-template-columns: repeat(2, 1fr); grid-auto-rows: 9rem; }\n  .so-tile-wide { grid-column: span 2; grid-row: span 2; }\n  .so-tile-tall { grid-row: span 2; }\n}\n\n/* FAQ + RESERVE */\n.so-faq { padding: clamp(4.5rem, 10vw, 8rem) clamp(1.25rem, 6vw, 6rem); background: var(--dusk); }\n.so-faq-grid { display: grid; grid-template-columns: 0.85fr 1.15fr; gap: clamp(2rem, 6vw, 5rem); max-width: 80rem; margin: 0 auto; align-items: start; }\n.so-reserve-card { margin-top: 2rem; padding: 1.8rem; background: var(--clay); border: 1px solid var(--line); border-radius: 16px; }\n.so-reserve-line { font-family: var(--font-fraunces), Georgia, serif; font-size: 1.5rem; margin: 0 0 1.2rem; font-weight: 440; }\n.so-reserve-note { color: var(--sand-mute); font-size: 0.9rem; margin: 1.2rem 0 0; }\n.so-faq-list { list-style: none; padding: 0; margin: 0; }\n.so-faq-item { border-bottom: 1px solid var(--line); }\n.so-faq-item:first-child { border-top: 1px solid var(--line); }\n.so-faq-q {\n  width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 1rem;\n  background: none; border: none; cursor: pointer; text-align: left;\n  color: var(--sand); font-family: var(--font-fraunces), Georgia, serif; font-weight: 440;\n  font-size: clamp(1.15rem, 2vw, 1.5rem); letter-spacing: -0.01em;\n  padding: 1.4rem 0;\n}\n.so-faq-icon { position: relative; width: 16px; height: 16px; flex-shrink: 0; }\n.so-faq-icon::before, .so-faq-icon::after { content: \"\"; position: absolute; background: var(--ember-soft); transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1); }\n.so-faq-icon::before { top: 50%; left: 0; right: 0; height: 1.5px; transform: translateY(-50%); }\n.so-faq-icon::after { left: 50%; top: 0; bottom: 0; width: 1.5px; transform: translateX(-50%); }\n.so-faq-item.is-open .so-faq-icon::after { transform: translateX(-50%) scaleY(0); }\n.so-faq-a { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.5s cubic-bezier(0.16, 1, 0.3, 1); }\n.so-faq-item.is-open .so-faq-a { grid-template-rows: 1fr; }\n.so-faq-a > p { overflow: hidden; margin: 0; color: var(--sand); opacity: 0.8; font-size: 1.05rem; padding-right: 2rem; }\n.so-faq-item.is-open .so-faq-a > p { padding-bottom: 1.4rem; }\n@media (max-width: 760px) { .so-faq-grid { grid-template-columns: 1fr; } }\n\n/* FOOTER */\n.so-footer { background: var(--clay); border-top: 1px solid var(--line); padding: clamp(3rem, 6vw, 5rem) clamp(1.25rem, 6vw, 6rem); }\n.so-footer-top { display: flex; justify-content: space-between; gap: 2rem; flex-wrap: wrap; }\n.so-footer-line { color: var(--sand-mute); margin: 0.4rem 0 0; }\n.so-footer-cols { display: flex; gap: clamp(1.5rem, 5vw, 4rem); flex-wrap: wrap; }\n.so-footer-col h4 { font-family: var(--font-geist), system-ui, sans-serif; font-weight: 600; font-size: 0.8rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--sand-mute); margin: 0 0 0.7rem; }\n.so-footer-col p { color: var(--sand); opacity: 0.82; margin: 0.2rem 0; font-size: 0.95rem; }\n.so-footer-fine { margin-top: clamp(2.5rem, 5vw, 4rem); padding-top: 1.5rem; border-top: 1px solid var(--line); color: var(--sand-mute); font-size: 0.88rem; }\n\n@media (prefers-reduced-motion: reduce) {\n  .so-hero-stage { display: none; }\n  .so-hero-alt { display: block; position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }\n  .so-scrollcue span { animation: none; opacity: 0.4; }\n  .so-hero-a, .so-hero-b { animation: none; }\n  .so-place-body, .so-suite, .so-exp-row { opacity: 1 !important; transform: none !important; transition: none; }\n  .so-place-img, .so-banner-img, .so-exp-media img, .so-suite-base { transform: none !important; }\n  .so-dock { transition: none; }\n  .so-book { transition: opacity 0.2s ease; }\n  .so-faq-a, .so-suite-swap, .so-suite-base, .so-tile img, .so-cta, .so-text-cta::after { transition: none; }\n}\n`\n","type":"registry:page"}],"assets":[{"url":"https://ui.artbloom.tech/r/asset/templates/solstice/assets/hero.mp4","target":"public/solstice/hero.mp4","bytes":1410367},{"url":"https://ui.artbloom.tech/r/asset/templates/solstice/assets/hero.png","target":"public/solstice/hero.png","bytes":2576553},{"url":"https://ui.artbloom.tech/r/asset/templates/solstice/assets/hero2.png","target":"public/solstice/hero2.png","bytes":1974453},{"url":"https://ui.artbloom.tech/r/asset/templates/solstice/assets/room1.png","target":"public/solstice/room1.png","bytes":2346381},{"url":"https://ui.artbloom.tech/r/asset/templates/solstice/assets/room2.png","target":"public/solstice/room2.png","bytes":2202647},{"url":"https://ui.artbloom.tech/r/asset/templates/solstice/assets/pool.png","target":"public/solstice/pool.png","bytes":2158096},{"url":"https://ui.artbloom.tech/r/asset/templates/solstice/assets/spa.png","target":"public/solstice/spa.png","bytes":2190286},{"url":"https://ui.artbloom.tech/r/asset/templates/solstice/assets/dining.png","target":"public/solstice/dining.png","bytes":2311538}],"meta":{"kind":"templates","categories":["experiences"],"docs":"https://ui.artbloom.tech/artbloom/templates/solstice"}}