{"$schema":"https://ui.artbloom.tech/schema/registry-item.json","name":"monolith-launch","type":"registry:page","title":"Monolith Launch","description":"A four-page product launch site built entirely from type — no images, no icons, no fonts to fetch and nothing to license. Display headline, a looping spec strip, four claims each backed by a number, the full specification table and a reservation form that admits it has no backend.","author":"@artbloom","dependencies":[],"registryDependencies":[],"files":[{"path":"app/monolith-launch/layout.tsx","target":"app/monolith-launch/layout.tsx","content":"import type { Metadata } from \"next\"\n\nimport { SiteFooter } from \"./components/site-footer\"\nimport { SiteHeader } from \"./components/site-header\"\nimport { brand, hero } from \"./data/product\"\n\nimport \"./monolith-launch.css\"\n\nexport const metadata: Metadata = {\n  title: {\n    default: `${brand.product} — ${brand.descriptor}`,\n    template: `%s — ${brand.name}`,\n  },\n  description: hero.lede,\n}\n\n/**\n * The template shell.\n *\n * A nested layout, so it renders no `<html>` or `<body>` — the app's root layout\n * owns those. `.monoSite` is the single wrapper every style in\n * `monolith-launch.css` hangs off, which is what keeps this sheet from touching\n * anything else in the app.\n *\n * The `<noscript>` block matters: scroll reveals start at `opacity: 0` in CSS so\n * they cannot flash in before hydration, which means without JavaScript they\n * would never appear at all. This turns them on for that reader instead of\n * showing them an empty page.\n */\nexport default function MonolithLaunchLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <div className=\"monoSite\">\n      <noscript>\n        <style\n          dangerouslySetInnerHTML={{\n            __html: \".monoReveal{opacity:1;transform:none}.monoTickerTrack{animation:none}\",\n          }}\n        />\n      </noscript>\n      <SiteHeader />\n      <main>{children}</main>\n      <SiteFooter />\n    </div>\n  )\n}\n","type":"registry:page"},{"path":"app/monolith-launch/monolith-launch.css","target":"app/monolith-launch/monolith-launch.css","content":"/*\n * monolith-launch — a single-product launch site built almost entirely from type.\n *\n * Nothing here can leak. Every class name carries a `mono` prefix and every\n * bare-element rule is scoped under `.monoSite`. A template that ships names\n * like `.hero`, `.eyebrow` and `.rule` into a consumer's global stylesheet is a\n * collision waiting to happen; a prefix plus one wrapper element makes the whole\n * sheet inert outside itself.\n *\n * There are no images anywhere in this template. The composition is the type\n * scale, the hairlines and the vertical rhythm, which is why it weighs nothing\n * and why it will not look dated when a stock photograph would.\n *\n * The two things most people will want to change are at the top: the palette,\n * and the display scale. Nothing below hard-codes a value that is a variable\n * up here.\n */\n.monoSite {\n  /* Palette. Monochrome by design — one accent is enough for a launch page. */\n  --ink: #f4f3f1;\n  --ink-dim: rgba(244, 243, 241, 0.62);\n  --ink-faint: rgba(244, 243, 241, 0.38);\n  --ink-ghost: rgba(244, 243, 241, 0.14);\n  --surface: #0b0b0c;\n  --surface-lift: #131315;\n  --line: rgba(244, 243, 241, 0.12);\n  --line-strong: rgba(244, 243, 241, 0.28);\n  --accent: #d8734a;\n\n  /*\n   * Display scale. The viewport unit is what makes the headline monumental on a\n   * desktop and still legible on a phone; the clamp stops it doing either badly.\n   */\n  --display: clamp(3.2rem, 13vw, 13.5rem);\n  --display-sm: clamp(2.4rem, 7.4vw, 6.5rem);\n  --heading: clamp(1.75rem, 3.4vw, 3.1rem);\n  --body: clamp(1rem, 1.12vw, 1.1875rem);\n\n  /* Rhythm. Sections are tall on purpose — the space is part of the design. */\n  --gutter: clamp(1.25rem, 4.5vw, 6rem);\n  --section: clamp(5.5rem, 12vh, 11rem);\n  --measure: 34rem;\n\n  --font-display: var(--font-mono-display, \"Inter Tight\", \"Helvetica Neue\", Arial, sans-serif);\n  --font-body: var(--font-mono-body, ui-sans-serif, system-ui, \"Segoe UI\", Arial, sans-serif);\n  --font-mono: ui-monospace, \"SFMono-Regular\", \"Cascadia Mono\", \"Roboto Mono\", monospace;\n\n  position: relative;\n  min-height: 100vh;\n  background: var(--surface);\n  color: var(--ink);\n  font-family: var(--font-body);\n  font-size: var(--body);\n  line-height: 1.6;\n  /* Contains the sticky header and any blend mode so neither escapes the wrapper. */\n  isolation: isolate;\n  overflow-x: hidden;\n}\n\n.monoSite *,\n.monoSite *::before,\n.monoSite *::after { box-sizing: border-box; }\n\n/*\n * `:where()` contributes no specificity, which is the whole point: `.monoSite a`\n * would be (0,1,1) and would beat every single-class rule below it, so a solid\n * button would inherit white text on a white background. This way the reset is\n * (0,1,0) and any later class rule wins.\n */\n.monoSite :where(a) { color: inherit; text-decoration: none; }\n.monoSite :where(button, input, textarea, select) { font: inherit; color: inherit; }\n.monoSite ::selection { background: var(--accent); color: #0b0b0c; }\n\n/* One visible focus style for the whole template, on the element that has focus. */\n.monoSite :focus-visible {\n  outline: 2px solid var(--accent);\n  outline-offset: 3px;\n  border-radius: 2px;\n}\n\n/* ---------------------------------------------------------------- primitives */\n\n.monoWrap {\n  width: 100%;\n  max-width: 96rem;\n  margin: 0 auto;\n  padding-inline: var(--gutter);\n}\n\n/* The hairline that separates every section. Its own element so a section can\n   opt out without a `:last-child` exception. */\n.monoRule {\n  height: 1px;\n  background: var(--line);\n  border: 0;\n  margin: 0;\n}\n\n.monoSection { padding-block: var(--section); }\n\n/* Wide-tracked micro-label. Used for eyebrows, group headings and table heads. */\n.monoLabel {\n  margin: 0;\n  font-family: var(--font-mono);\n  font-size: 0.6875rem;\n  font-weight: 500;\n  letter-spacing: 0.14em;\n  text-transform: uppercase;\n  color: var(--ink-faint);\n}\n\n.monoLede {\n  max-width: var(--measure);\n  margin: 0;\n  font-size: clamp(1.0625rem, 1.5vw, 1.375rem);\n  line-height: 1.55;\n  color: var(--ink-dim);\n  text-wrap: pretty;\n}\n\n.monoHeading {\n  margin: 0;\n  font-family: var(--font-display);\n  font-size: var(--heading);\n  font-weight: 500;\n  line-height: 1.04;\n  letter-spacing: -0.025em;\n  text-wrap: balance;\n}\n\n/* Label + heading, above a band of rows. */\n.monoSectionHead {\n  display: grid;\n  gap: 1.125rem;\n  max-width: 40rem;\n  margin-bottom: clamp(2rem, 5vw, 3.5rem);\n}\n\n/* The same pair with no space under it — for use inside a grid that already gaps. */\n.monoStack { display: grid; gap: 1.125rem; align-content: start; }\n\n/*\n * Consecutive sections would otherwise stack two full `--section` paddings\n * against each other. The first one in a run keeps its top padding; the rest\n * carry this.\n */\n.monoSectionTight { padding-top: 0; }\n\n/* The head that has a link on the far side of it at desktop width. */\n.monoSectionHeadRow {\n  display: grid;\n  gap: 1.5rem;\n  margin-bottom: clamp(2rem, 5vw, 3.5rem);\n}\n\n@media (min-width: 52rem) {\n  .monoSectionHeadRow {\n    grid-template-columns: minmax(0, 1fr) auto;\n    align-items: end;\n    gap: 2.5rem;\n  }\n  .monoSectionHeadRow .monoSectionHead { margin-bottom: 0; }\n}\n\n/* ------------------------------------------------------------------- buttons */\n\n.monoBtn {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.625rem;\n  padding: 0.9375rem 1.75rem;\n  border: 1px solid var(--line-strong);\n  border-radius: 999px;\n  background: transparent;\n  font-size: 0.9375rem;\n  font-weight: 500;\n  letter-spacing: -0.005em;\n  cursor: pointer;\n  transition:\n    background-color 0.35s cubic-bezier(0.22, 1, 0.36, 1),\n    color 0.35s cubic-bezier(0.22, 1, 0.36, 1),\n    border-color 0.35s cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.monoBtn:hover {\n  background: var(--ink);\n  border-color: var(--ink);\n  color: var(--surface);\n}\n\n.monoBtnSolid {\n  background: var(--ink);\n  border-color: var(--ink);\n  color: var(--surface);\n}\n\n.monoBtnSolid:hover {\n  background: var(--accent);\n  border-color: var(--accent);\n  color: #0b0b0c;\n}\n\n.monoBtn:disabled { opacity: 0.5; cursor: default; }\n\n/* The arrow that leans forward on hover. Inline so no icon library is needed. */\n.monoBtn > .monoArrow {\n  display: inline-block;\n  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.monoBtn:hover > .monoArrow { transform: translateX(3px); }\n\n/* -------------------------------------------------------------------- header */\n\n.monoHeader {\n  position: sticky;\n  top: 0;\n  z-index: 20;\n  background: color-mix(in oklab, var(--surface) 82%, transparent);\n  backdrop-filter: blur(14px) saturate(140%);\n  border-bottom: 1px solid var(--line);\n}\n\n.monoHeaderRow {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 1.5rem;\n  height: 4.25rem;\n}\n\n.monoBrand {\n  display: flex;\n  align-items: baseline;\n  gap: 0.625rem;\n  font-family: var(--font-display);\n  font-size: 1.0625rem;\n  font-weight: 600;\n  letter-spacing: -0.03em;\n}\n\n.monoBrandMark {\n  /* The logo is a square. A launch page does not need more than a square. */\n  width: 0.6875rem;\n  height: 0.6875rem;\n  background: var(--accent);\n  transform: translateY(-0.05em);\n}\n\n.monoBrandDescriptor {\n  font-family: var(--font-mono);\n  font-size: 0.625rem;\n  font-weight: 400;\n  letter-spacing: 0.12em;\n  text-transform: uppercase;\n  color: var(--ink-faint);\n}\n\n.monoNav {\n  display: none;\n  align-items: center;\n  gap: 2rem;\n}\n\n.monoNavLink {\n  position: relative;\n  font-size: 0.875rem;\n  color: var(--ink-dim);\n  transition: color 0.25s ease;\n}\n\n.monoNavLink:hover { color: var(--ink); }\n\n/* Underline grows from the left rather than fading in — cheaper to read. */\n.monoNavLink::after {\n  content: \"\";\n  position: absolute;\n  left: 0;\n  bottom: -0.3125rem;\n  width: 100%;\n  height: 1px;\n  background: currentColor;\n  transform: scaleX(0);\n  transform-origin: left;\n  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.monoNavLink:hover::after,\n.monoNavLink[aria-current=\"page\"]::after { transform: scaleX(1); }\n.monoNavLink[aria-current=\"page\"] { color: var(--ink); }\n\n/*\n * Mobile navigation is a native `<details>`. No state, no JS, no focus trap to\n * get wrong — and it still works if hydration never happens, which a marketing\n * page should survive.\n */\n.monoMenu { position: relative; }\n\n.monoMenuSummary {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.5rem;\n  padding: 0.5rem 0.875rem;\n  border: 1px solid var(--line-strong);\n  border-radius: 999px;\n  font-family: var(--font-mono);\n  font-size: 0.625rem;\n  letter-spacing: 0.12em;\n  text-transform: uppercase;\n  color: var(--ink-dim);\n  cursor: pointer;\n  list-style: none;\n}\n\n/* Safari still paints a disclosure triangle without this. */\n.monoMenuSummary::-webkit-details-marker { display: none; }\n.monoMenu[open] .monoMenuSummary { color: var(--ink); border-color: var(--ink); }\n\n.monoMenuPanel {\n  position: absolute;\n  right: 0;\n  top: calc(100% + 0.75rem);\n  z-index: 30;\n  display: grid;\n  gap: 0.25rem;\n  min-width: 12rem;\n  padding: 0.75rem;\n  border: 1px solid var(--line);\n  border-radius: 0.75rem;\n  background: var(--surface-lift);\n}\n\n.monoMenuLink {\n  padding: 0.5rem 0.625rem;\n  border-radius: 0.5rem;\n  font-size: 0.9375rem;\n  color: var(--ink-dim);\n}\n\n.monoMenuLink:hover { background: rgba(244, 243, 241, 0.06); color: var(--ink); }\n.monoMenuLink[aria-current=\"page\"] { color: var(--ink); }\n\n@media (min-width: 52rem) {\n  .monoNav { display: flex; }\n  .monoHeaderRow { height: 5rem; }\n  .monoMenu { display: none; }\n}\n\n/* ---------------------------------------------------------------------- hero */\n\n.monoHero {\n  padding-top: clamp(3.5rem, 9vh, 7rem);\n  padding-bottom: clamp(2rem, 5vh, 4rem);\n}\n\n.monoHeroEyebrow {\n  display: flex;\n  align-items: center;\n  gap: 0.75rem;\n  margin-bottom: clamp(2rem, 6vh, 4.5rem);\n}\n\n.monoHeroEyebrow::after {\n  content: \"\";\n  flex: 1;\n  height: 1px;\n  background: var(--line);\n}\n\n/*\n * The display headline. One word per line, set as tight as the face allows.\n * `text-transform` is deliberately absent — the scale is the emphasis, so the\n * words keep their natural case and stay readable.\n */\n.monoDisplay {\n  margin: 0;\n  font-family: var(--font-display);\n  font-size: var(--display);\n  font-weight: 500;\n  line-height: 0.86;\n  letter-spacing: -0.045em;\n}\n\n.monoDisplayWord { display: block; }\n\n/* Every other word steps back, which gives the block depth without colour. */\n.monoDisplayWord:nth-child(even) {\n  color: var(--ink-faint);\n  padding-left: 0.09em;\n}\n\n.monoHeroFoot {\n  display: grid;\n  gap: 2.5rem;\n  margin-top: clamp(2.5rem, 7vh, 5rem);\n}\n\n.monoHeroActions {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 0.75rem;\n  align-items: center;\n}\n\n@media (min-width: 52rem) {\n  .monoHeroFoot {\n    grid-template-columns: 1fr auto;\n    align-items: end;\n    gap: 4rem;\n  }\n}\n\n/* --------------------------------------------------------------------- ticker */\n\n.monoTicker {\n  border-block: 1px solid var(--line);\n  padding-block: 1.125rem;\n  overflow: hidden;\n  /* Fades the ends so the loop never shows a hard cut at the viewport edge. */\n  mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);\n}\n\n.monoTickerTrack {\n  display: flex;\n  width: max-content;\n  animation: monoTickerScroll 38s linear infinite;\n}\n\n.monoTickerItem {\n  display: flex;\n  align-items: center;\n  gap: 2.75rem;\n  padding-right: 2.75rem;\n  font-family: var(--font-mono);\n  font-size: 0.75rem;\n  letter-spacing: 0.1em;\n  text-transform: uppercase;\n  color: var(--ink-faint);\n  white-space: nowrap;\n}\n\n.monoTickerItem::after {\n  content: \"\";\n  width: 0.25rem;\n  height: 0.25rem;\n  background: var(--accent);\n  flex-shrink: 0;\n}\n\n@keyframes monoTickerScroll {\n  from { transform: translate3d(0, 0, 0); }\n  to { transform: translate3d(-50%, 0, 0); }\n}\n\n/* ----------------------------------------------------------------- manifesto */\n\n.monoManifesto {\n  display: grid;\n  gap: clamp(2rem, 5vw, 4.5rem);\n}\n\n.monoManifestoBody { display: grid; gap: 1.5rem; }\n\n.monoManifestoBody p {\n  margin: 0;\n  max-width: 40rem;\n  font-size: clamp(1.0625rem, 1.35vw, 1.3125rem);\n  line-height: 1.6;\n  color: var(--ink-dim);\n  text-wrap: pretty;\n}\n\n/* First paragraph reads as the statement, the rest as the argument. */\n.monoManifestoBody p:first-child {\n  color: var(--ink);\n  font-size: clamp(1.1875rem, 1.7vw, 1.625rem);\n  line-height: 1.45;\n}\n\n@media (min-width: 60rem) {\n  .monoManifesto { grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); }\n}\n\n/* ------------------------------------------------------------------- pillars */\n\n.monoPillars {\n  display: grid;\n  gap: 0;\n  border-top: 1px solid var(--line);\n}\n\n.monoPillar {\n  display: grid;\n  gap: 1rem;\n  padding-block: clamp(2rem, 4.5vw, 3.25rem);\n  border-bottom: 1px solid var(--line);\n}\n\n.monoPillarIndex {\n  font-family: var(--font-mono);\n  font-size: 0.75rem;\n  letter-spacing: 0.12em;\n  color: var(--accent);\n}\n\n.monoPillarTitle {\n  margin: 0;\n  font-family: var(--font-display);\n  font-size: clamp(1.5rem, 2.6vw, 2.375rem);\n  font-weight: 500;\n  line-height: 1.06;\n  letter-spacing: -0.03em;\n}\n\n.monoPillarBody {\n  margin: 0;\n  max-width: 32rem;\n  color: var(--ink-dim);\n  text-wrap: pretty;\n}\n\n.monoPillarMetric {\n  display: grid;\n  gap: 0.25rem;\n  align-content: start;\n}\n\n.monoPillarMetricValue {\n  font-family: var(--font-display);\n  font-size: clamp(2rem, 4.5vw, 3.5rem);\n  font-weight: 500;\n  line-height: 0.95;\n  letter-spacing: -0.04em;\n  font-variant-numeric: tabular-nums;\n}\n\n@media (min-width: 60rem) {\n  .monoPillar {\n    grid-template-columns: 4rem minmax(0, 1fr) minmax(0, 1fr) 10rem;\n    gap: 2.5rem;\n    align-items: start;\n  }\n  .monoPillarMetric { justify-items: end; text-align: right; }\n}\n\n/* --------------------------------------------------------------------- specs */\n\n.monoSpecGroup { border-bottom: 1px solid var(--line); }\n.monoSpecGroup:first-of-type { border-top: 1px solid var(--line); }\n\n.monoSpecGroupInner {\n  display: grid;\n  gap: 1.25rem;\n  padding-block: clamp(1.75rem, 3.5vw, 2.75rem);\n}\n\n.monoSpecRows {\n  display: grid;\n  gap: 0;\n  margin: 0;\n}\n\n/* A definition list, so a screen reader gets term/value pairing for free. */\n.monoSpecRow {\n  display: grid;\n  grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr);\n  gap: 1rem 1.5rem;\n  padding-block: 0.8125rem;\n  border-top: 1px solid var(--line);\n  font-size: 0.9375rem;\n}\n\n.monoSpecRow:first-child { border-top: 0; }\n.monoSpecRow dt { color: var(--ink-faint); }\n.monoSpecRow dd { margin: 0; color: var(--ink); }\n\n@media (min-width: 60rem) {\n  .monoSpecGroupInner { grid-template-columns: 12rem minmax(0, 1fr); gap: 2.5rem; }\n}\n\n/* --------------------------------------------------------------------- offer */\n\n.monoOffer {\n  display: grid;\n  gap: clamp(2rem, 5vw, 4rem);\n  padding: clamp(2rem, 5vw, 4rem);\n  border: 1px solid var(--line);\n  border-radius: 1.25rem;\n  background: var(--surface-lift);\n}\n\n.monoOfferPrice {\n  font-family: var(--font-display);\n  font-size: var(--display-sm);\n  font-weight: 500;\n  line-height: 0.9;\n  letter-spacing: -0.04em;\n  font-variant-numeric: tabular-nums;\n}\n\n.monoOfferNote {\n  margin: 0;\n  font-family: var(--font-mono);\n  font-size: 0.75rem;\n  letter-spacing: 0.05em;\n  color: var(--ink-faint);\n}\n\n.monoOfferBody { display: grid; gap: 1.5rem; align-content: start; }\n\n@media (min-width: 60rem) {\n  .monoOffer { grid-template-columns: minmax(0, 0.85fr) minmax(0, 1fr); align-items: start; }\n}\n\n/* --------------------------------------------------------------------- story */\n\n.monoBeat {\n  display: grid;\n  gap: 0.875rem;\n  padding-block: clamp(1.75rem, 4vw, 3rem);\n  border-top: 1px solid var(--line);\n}\n\n.monoBeatYear {\n  font-family: var(--font-mono);\n  font-size: 0.8125rem;\n  letter-spacing: 0.1em;\n  color: var(--accent);\n  font-variant-numeric: tabular-nums;\n}\n\n.monoBeatTitle {\n  margin: 0;\n  font-family: var(--font-display);\n  font-size: clamp(1.375rem, 2.2vw, 2rem);\n  font-weight: 500;\n  line-height: 1.1;\n  letter-spacing: -0.03em;\n}\n\n.monoBeatBody {\n  margin: 0;\n  max-width: 38rem;\n  color: var(--ink-dim);\n  text-wrap: pretty;\n}\n\n@media (min-width: 60rem) {\n  .monoBeat { grid-template-columns: 8rem minmax(0, 1fr); gap: 2.5rem; }\n  .monoBeatYear { padding-top: 0.3rem; }\n}\n\n/* ---------------------------------------------------------------------- form */\n\n.monoForm { display: grid; gap: 1.25rem; max-width: 32rem; }\n.monoField { display: grid; gap: 0.5rem; }\n\n.monoFieldLabel {\n  font-family: var(--font-mono);\n  font-size: 0.6875rem;\n  letter-spacing: 0.12em;\n  text-transform: uppercase;\n  color: var(--ink-faint);\n}\n\n.monoInput {\n  width: 100%;\n  padding: 0.8125rem 1rem;\n  border: 1px solid var(--line);\n  border-radius: 0.5rem;\n  background: var(--surface-lift);\n  font-size: 0.9375rem;\n  transition: border-color 0.25s ease;\n}\n\n.monoInput:hover { border-color: var(--line-strong); }\n.monoInput::placeholder { color: var(--ink-ghost); }\n.monoTextarea { min-height: 6.5rem; resize: vertical; }\n\n.monoFormNote {\n  margin: 0;\n  font-size: 0.8125rem;\n  line-height: 1.55;\n  color: var(--ink-faint);\n}\n\n/* Result line for the demo submit. `role=\"status\"` announces it politely. */\n.monoFormStatus {\n  margin: 0;\n  padding: 0.875rem 1rem;\n  border: 1px solid var(--line-strong);\n  border-radius: 0.5rem;\n  font-size: 0.875rem;\n  color: var(--ink);\n}\n\n/* -------------------------------------------------------------------- footer */\n\n.monoFooter { border-top: 1px solid var(--line); padding-block: clamp(3rem, 7vw, 5rem); }\n\n.monoFooterTop {\n  display: grid;\n  gap: 2.5rem;\n  padding-bottom: 3rem;\n}\n\n.monoFooterBlurb {\n  margin: 0;\n  max-width: 30rem;\n  font-size: 0.9375rem;\n  line-height: 1.6;\n  color: var(--ink-dim);\n  text-wrap: pretty;\n}\n\n.monoFooterCols {\n  display: grid;\n  gap: 2rem;\n  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));\n  align-content: start;\n}\n\n.monoFooterLinks { display: grid; gap: 0.625rem; margin-top: 1.125rem; }\n\n.monoFooterLink {\n  font-size: 0.9375rem;\n  color: var(--ink-dim);\n  transition: color 0.25s ease;\n}\n\n.monoFooterLink:hover { color: var(--ink); }\n\n.monoFooterBottom a { color: inherit; transition: color 0.25s ease; }\n.monoFooterBottom a:hover { color: var(--ink); }\n\n.monoFooterBottom {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 1rem 2rem;\n  justify-content: space-between;\n  padding-top: 1.75rem;\n  border-top: 1px solid var(--line);\n  font-family: var(--font-mono);\n  font-size: 0.6875rem;\n  letter-spacing: 0.1em;\n  text-transform: uppercase;\n  color: var(--ink-faint);\n}\n\n/*\n * The wordmark that closes the page. It is allowed to overflow its box and be\n * clipped by the wrapper — that is the effect, not a bug.\n */\n.monoFooterWord {\n  margin: 0;\n  padding-top: clamp(2rem, 6vw, 4rem);\n  font-family: var(--font-display);\n  font-size: clamp(3.5rem, 19vw, 18rem);\n  font-weight: 500;\n  line-height: 0.8;\n  letter-spacing: -0.05em;\n  color: var(--ink-ghost);\n  user-select: none;\n}\n\n@media (min-width: 52rem) {\n  .monoFooterTop { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 4rem; }\n}\n\n/* ----------------------------------------------------------------- page head */\n\n/*\n * The three inner pages open the same way: label, one large line, one lede.\n * Sharing it here is what keeps them feeling like one site.\n */\n.monoPageHead {\n  display: grid;\n  gap: 1.5rem;\n  padding-top: clamp(3rem, 8vh, 5.5rem);\n  padding-bottom: clamp(2rem, 5vh, 3.5rem);\n}\n\n.monoPageTitle {\n  margin: 0;\n  max-width: 22ch;\n  font-family: var(--font-display);\n  font-size: var(--display-sm);\n  font-weight: 500;\n  line-height: 0.94;\n  letter-spacing: -0.04em;\n  text-wrap: balance;\n}\n\n/* A link inside body copy still needs to look like one. */\n.monoInlineLink {\n  color: var(--ink);\n  border-bottom: 1px solid var(--line-strong);\n  transition: border-color 0.25s ease;\n}\n\n.monoInlineLink:hover { border-color: var(--ink); }\n\n/* ------------------------------------------------------------------- reserve */\n\n.monoReserveGrid { display: grid; gap: clamp(2.5rem, 6vw, 4.5rem); }\n\n/* The summary card beside the form. Same material as the offer block. */\n.monoAside {\n  display: grid;\n  gap: 1rem;\n  align-content: start;\n  padding: clamp(1.5rem, 3vw, 2rem);\n  border: 1px solid var(--line);\n  border-radius: 1.25rem;\n  background: var(--surface-lift);\n}\n\n.monoAsideList {\n  display: grid;\n  gap: 0.75rem;\n  margin: 0;\n  padding: 0;\n  list-style: none;\n  font-size: 0.9375rem;\n  color: var(--ink-dim);\n}\n\n/* A hairline tick rather than a bullet glyph — no font dependency. */\n.monoAsideList li {\n  display: grid;\n  grid-template-columns: 0.75rem minmax(0, 1fr);\n  gap: 0.75rem;\n}\n\n.monoAsideList li::before {\n  content: \"\";\n  margin-top: 0.6em;\n  width: 0.5rem;\n  height: 1px;\n  background: var(--accent);\n}\n\n@media (min-width: 60rem) {\n  .monoReserveGrid {\n    grid-template-columns: minmax(0, 1fr) minmax(0, 0.78fr);\n    align-items: start;\n  }\n}\n\n/* -------------------------------------------------------------- scroll reveal */\n\n/*\n * Reveal on scroll, driven by one IntersectionObserver in `components/reveal.tsx`\n * that flips `data-shown` on. Two rules matter for correctness:\n *\n *   - The hidden state is set here, not in JS, so nothing flashes in before the\n *     observer attaches.\n *   - Under `prefers-reduced-motion` the element is simply visible. Not a\n *     shorter animation — none, and no transform, because a translate is exactly\n *     what triggers vestibular discomfort.\n */\n.monoReveal {\n  opacity: 0;\n  transform: translate3d(0, 1.25rem, 0);\n  transition:\n    opacity 0.8s cubic-bezier(0.22, 1, 0.36, 1),\n    transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.monoReveal[data-shown=\"true\"] {\n  opacity: 1;\n  transform: translate3d(0, 0, 0);\n}\n\n/* Rows in a list stagger by index; the JS sets `--mono-delay`. */\n.monoReveal { transition-delay: var(--mono-delay, 0ms); }\n\n@media (prefers-reduced-motion: reduce) {\n  .monoReveal,\n  .monoReveal[data-shown=\"true\"] {\n    opacity: 1;\n    transform: none;\n    transition: none;\n  }\n\n  .monoTickerTrack { animation: none; }\n\n  .monoSite *,\n  .monoSite *::before,\n  .monoSite *::after {\n    transition-duration: 0.01ms !important;\n    animation-duration: 0.01ms !important;\n    animation-iteration-count: 1 !important;\n    scroll-behavior: auto !important;\n  }\n}\n","type":"registry:style"},{"path":"app/monolith-launch/page.tsx","target":"app/monolith-launch/page.tsx","content":"import Link from \"next/link\"\n\nimport { Reveal } from \"./components/reveal\"\nimport { SpecTable } from \"./components/spec-table\"\nimport { Ticker } from \"./components/ticker\"\nimport { hero, manifesto, offer, pillars, sections, specs } from \"./data/product\"\n\n/**\n * The launch page: one long scroll, no images.\n *\n * The order is the argument — a claim, the reason for it, four defences with\n * numbers, the specifications behind those numbers, then the price. Every string\n * comes from `data/product.ts`.\n */\nexport default function MonolithHome() {\n  return (\n    <>\n      <section className=\"monoWrap monoHero\">\n        <div className=\"monoHeroEyebrow\">\n          <p className=\"monoLabel\">{hero.eyebrow}</p>\n        </div>\n\n        <h1 className=\"monoDisplay\">\n          {hero.words.map((word, index) => (\n            <span key={`${word}-${index}`} className=\"monoDisplayWord\">\n              {word}\n            </span>\n          ))}\n        </h1>\n\n        <div className=\"monoHeroFoot\">\n          <p className=\"monoLede\">{hero.lede}</p>\n          <div className=\"monoHeroActions\">\n            <Link className=\"monoBtn monoBtnSolid\" href={hero.primary.href}>\n              {hero.primary.label}\n              <span className=\"monoArrow\" aria-hidden=\"true\">\n                →\n              </span>\n            </Link>\n            <Link className=\"monoBtn\" href={hero.secondary.href}>\n              {hero.secondary.label}\n            </Link>\n          </div>\n        </div>\n      </section>\n\n      <Ticker />\n\n      <section className=\"monoSection\">\n        <Reveal className=\"monoWrap monoManifesto\">\n          <div className=\"monoStack\">\n            <p className=\"monoLabel\">{sections.manifesto}</p>\n            <h2 className=\"monoHeading\">{manifesto.heading}</h2>\n          </div>\n          <div className=\"monoManifestoBody\">\n            {manifesto.body.map((paragraph) => (\n              <p key={paragraph}>{paragraph}</p>\n            ))}\n          </div>\n        </Reveal>\n      </section>\n\n      <section className=\"monoSection monoSectionTight\">\n        <div className=\"monoWrap\">\n          <Reveal className=\"monoSectionHead\">\n            <p className=\"monoLabel\">{sections.pillars.label}</p>\n            <h2 className=\"monoHeading\">{sections.pillars.heading}</h2>\n          </Reveal>\n\n          <div className=\"monoPillars\">\n            {pillars.map((pillar, index) => (\n              <Reveal key={pillar.index} className=\"monoPillar\" delay={index * 70}>\n                <p className=\"monoPillarIndex\">{pillar.index}</p>\n                <h3 className=\"monoPillarTitle\">{pillar.title}</h3>\n                <p className=\"monoPillarBody\">{pillar.body}</p>\n                <div className=\"monoPillarMetric\">\n                  <span className=\"monoPillarMetricValue\">{pillar.metric}</span>\n                  <span className=\"monoLabel\">{pillar.metricLabel}</span>\n                </div>\n              </Reveal>\n            ))}\n          </div>\n        </div>\n      </section>\n\n      <section className=\"monoSection monoSectionTight\">\n        <div className=\"monoWrap\">\n          <div className=\"monoSectionHeadRow\">\n            <Reveal className=\"monoSectionHead\">\n              <p className=\"monoLabel\">{sections.specs.label}</p>\n              <h2 className=\"monoHeading\">{sections.specs.heading}</h2>\n            </Reveal>\n            <Link className=\"monoBtn\" href={sections.specs.cta.href}>\n              {sections.specs.cta.label}\n              <span className=\"monoArrow\" aria-hidden=\"true\">\n                →\n              </span>\n            </Link>\n          </div>\n        </div>\n\n        {/* Two groups here; the rest live on the specifications page. */}\n        <SpecTable groups={specs.slice(0, 2)} />\n      </section>\n\n      <section className=\"monoSection monoSectionTight\">\n        <div className=\"monoWrap\">\n          <Reveal className=\"monoOffer\">\n            <div>\n              <p className=\"monoLabel\">{offer.eyebrow}</p>\n              <p className=\"monoOfferPrice\">{offer.price}</p>\n              <p className=\"monoOfferNote\">{offer.priceNote}</p>\n            </div>\n            <div className=\"monoOfferBody\">\n              <h2 className=\"monoHeading\">{offer.heading}</h2>\n              <p className=\"monoLede\">{offer.body}</p>\n              <div className=\"monoHeroActions\">\n                <Link className=\"monoBtn monoBtnSolid\" href={offer.cta.href}>\n                  {offer.cta.label}\n                  <span className=\"monoArrow\" aria-hidden=\"true\">\n                    →\n                  </span>\n                </Link>\n              </div>\n              <p className=\"monoOfferNote\">{offer.note}</p>\n            </div>\n          </Reveal>\n        </div>\n      </section>\n    </>\n  )\n}\n","type":"registry:page"},{"path":"app/monolith-launch/story/page.tsx","target":"app/monolith-launch/story/page.tsx","content":"import type { Metadata } from \"next\"\nimport Link from \"next/link\"\n\nimport { Reveal } from \"../components/reveal\"\nimport { offer, story } from \"../data/product\"\n\nexport const metadata: Metadata = {\n  title: \"Story\",\n  description: story.lede,\n}\n\n/** Four beats, each a year and a paragraph. No timeline component required. */\nexport default function StoryPage() {\n  return (\n    <>\n      <section className=\"monoWrap monoPageHead\">\n        <p className=\"monoLabel\">{story.eyebrow}</p>\n        <h1 className=\"monoPageTitle\">{story.heading}</h1>\n        <p className=\"monoLede\">{story.lede}</p>\n      </section>\n\n      <section className=\"monoSection monoSectionTight\">\n        <div className=\"monoWrap\">\n          {story.beats.map((beat, index) => (\n            <Reveal key={beat.year} className=\"monoBeat\" delay={index * 70}>\n              <p className=\"monoBeatYear\">{beat.year}</p>\n              <div className=\"monoStack\">\n                <h2 className=\"monoBeatTitle\">{beat.title}</h2>\n                <p className=\"monoBeatBody\">{beat.body}</p>\n              </div>\n            </Reveal>\n          ))}\n        </div>\n      </section>\n\n      <section className=\"monoSection monoSectionTight\">\n        <Reveal className=\"monoWrap monoStack\">\n          <p className=\"monoLabel\">{offer.eyebrow}</p>\n          <h2 className=\"monoHeading\">{offer.heading}</h2>\n          <div className=\"monoHeroActions\">\n            <Link className=\"monoBtn monoBtnSolid\" href={offer.cta.href}>\n              {offer.cta.label}\n              <span className=\"monoArrow\" aria-hidden=\"true\">\n                →\n              </span>\n            </Link>\n          </div>\n        </Reveal>\n      </section>\n    </>\n  )\n}\n","type":"registry:page"},{"path":"app/monolith-launch/specs/page.tsx","target":"app/monolith-launch/specs/page.tsx","content":"import type { Metadata } from \"next\"\nimport Link from \"next/link\"\n\nimport { Reveal } from \"../components/reveal\"\nimport { SpecTable } from \"../components/spec-table\"\nimport { offer, specs, specsIntro } from \"../data/product\"\n\nexport const metadata: Metadata = {\n  title: \"Specifications\",\n  description: specsIntro.lede,\n}\n\n/** Every group, full width, one hairline band each. */\nexport default function SpecsPage() {\n  return (\n    <>\n      <section className=\"monoWrap monoPageHead\">\n        <p className=\"monoLabel\">{specsIntro.eyebrow}</p>\n        <h1 className=\"monoPageTitle\">{specsIntro.heading}</h1>\n        <p className=\"monoLede\">{specsIntro.lede}</p>\n      </section>\n\n      <section className=\"monoSection monoSectionTight\">\n        <SpecTable groups={specs} />\n      </section>\n\n      <section className=\"monoSection monoSectionTight\">\n        <Reveal className=\"monoWrap monoStack\">\n          <p className=\"monoLabel\">{offer.eyebrow}</p>\n          <h2 className=\"monoHeading\">{offer.heading}</h2>\n          <p className=\"monoLede\">{offer.body}</p>\n          <div className=\"monoHeroActions\">\n            <Link className=\"monoBtn monoBtnSolid\" href={offer.cta.href}>\n              {offer.cta.label}\n              <span className=\"monoArrow\" aria-hidden=\"true\">\n                →\n              </span>\n            </Link>\n          </div>\n        </Reveal>\n      </section>\n    </>\n  )\n}\n","type":"registry:page"},{"path":"app/monolith-launch/reserve/page.tsx","target":"app/monolith-launch/reserve/page.tsx","content":"import type { Metadata } from \"next\"\n\nimport { ReserveForm } from \"../components/reserve-form\"\nimport { Reveal } from \"../components/reveal\"\nimport { offer, reserve } from \"../data/product\"\n\nexport const metadata: Metadata = {\n  title: \"Reserve\",\n  description: reserve.lede,\n}\n\n/** The form, and beside it the terms it commits you to — which is none. */\nexport default function ReservePage() {\n  return (\n    <>\n      <section className=\"monoWrap monoPageHead\">\n        <p className=\"monoLabel\">{reserve.eyebrow}</p>\n        <h1 className=\"monoPageTitle\">{reserve.heading}</h1>\n        <p className=\"monoLede\">{reserve.lede}</p>\n      </section>\n\n      <section className=\"monoSection monoSectionTight\">\n        <div className=\"monoWrap monoReserveGrid\">\n          <ReserveForm />\n\n          <Reveal className=\"monoAside\">\n            <p className=\"monoLabel\">{offer.eyebrow}</p>\n            <p className=\"monoOfferPrice\">{offer.price}</p>\n            <p className=\"monoOfferNote\">{offer.priceNote}</p>\n            <ul className=\"monoAsideList\">\n              {reserve.terms.map((term) => (\n                <li key={term}>{term}</li>\n              ))}\n            </ul>\n          </Reveal>\n        </div>\n      </section>\n    </>\n  )\n}\n","type":"registry:page"},{"path":"app/monolith-launch/data/product.ts","target":"app/monolith-launch/data/product.ts","content":"/**\n * Everything the template says, in one file.\n *\n * The pages read from here and hold no copy of their own, so re-voicing the whole\n * site for a different product is one file to edit and no JSX to touch. Swap the\n * palette in `monolith-launch.css` and the words here and nothing else needs to\n * change.\n *\n * The demo product is a single-dose espresso machine because a launch page needs\n * something with real specifications to talk about. Replace it with yours.\n */\n\n/** Where the template is mounted. Change this and every internal link follows. */\nexport const BASE = \"/monolith-launch\"\n\nexport const brand = {\n  name: \"Monolith\",\n  product: \"Monolith One\",\n  /** Sits in the corner of the header. Keep it to three or four words. */\n  descriptor: \"Single-dose espresso\",\n  email: \"hello@monolith.example\",\n  /** Shown in the footer and used for the reserve confirmation copy. */\n  city: \"Turin\",\n  year: 2026,\n} as const\n\nexport const nav = [\n  { label: \"Story\", href: `${BASE}/story` },\n  { label: \"Specifications\", href: `${BASE}/specs` },\n  { label: \"Reserve\", href: `${BASE}/reserve` },\n] as const\n\n/**\n * The opening. `words` is set one per line at display size, so three or four\n * short ones read best — the line breaks are the composition.\n */\nexport const hero = {\n  eyebrow: \"A machine that holds its temperature\",\n  words: [\"Pressure\", \"and\", \"patience\"] as const,\n  lede:\n    \"Ninety-two degrees, held to a tenth, for as long as the shot takes. Everything else on this machine exists to protect that one number.\",\n  primary: { label: \"Reserve one\", href: `${BASE}/reserve` },\n  secondary: { label: \"Read the story\", href: `${BASE}/story` },\n  /** The strip that scrolls under the hero. Short phrases, no punctuation. */\n  ticker: [\n    \"Brushed 316 steel\",\n    \"58mm basket\",\n    \"PID to 0.1°C\",\n    \"Nine bar, flat\",\n    \"Built in Turin\",\n    \"Ten year parts\",\n  ] as const,\n} as const\n\n/** The quiet full-width statement between the hero and the detail. */\nexport const manifesto = {\n  heading: \"We removed everything that was not the shot\",\n  body: [\n    \"A pressure gauge tells you what already happened. A second boiler solves a problem you do not have at home. A screen asks you to read while your coffee cools.\",\n    \"So none of them are here. What is here is a thermal core that recovers in four seconds, a pump curve you can shape, and a body heavy enough that nothing moves when you lock the basket in.\",\n  ] as const,\n} as const\n\n/** Three or four. Each one is a claim you can defend with a number. */\nexport const pillars = [\n  {\n    index: \"01\",\n    title: \"Holds its heat\",\n    body:\n      \"A 1.4kg brass core under PID control, sampled forty times a second. Group temperature stays inside a tenth of a degree from first shot to twentieth.\",\n    metric: \"±0.1°C\",\n    metricLabel: \"at the group\",\n  },\n  {\n    index: \"02\",\n    title: \"Shapes the pressure\",\n    body:\n      \"A geared pump instead of a vibration one, so the ramp is yours: pre-infuse low and long, hold at nine, then decline into the tail without touching a paddle.\",\n    metric: \"0–12 bar\",\n    metricLabel: \"programmable\",\n  },\n  {\n    index: \"03\",\n    title: \"Recovers immediately\",\n    body:\n      \"Four seconds back to setpoint after a double. Two people, two shots, one after the other, with no waiting and no compromise on the second.\",\n    metric: \"4s\",\n    metricLabel: \"to setpoint\",\n  },\n  {\n    index: \"04\",\n    title: \"Opens with two screws\",\n    body:\n      \"Every part inside is a part you can buy from us for ten years, with the diagrams published. Gaskets, the pump, the controller — all of it, all replaceable at the kitchen table.\",\n    metric: \"10 yr\",\n    metricLabel: \"parts guarantee\",\n  },\n] as const\n\n/**\n * Section labels and headings for the home page.\n *\n * These are here rather than in the JSX for the same reason as everything else:\n * so that re-voicing the site never means opening a component.\n */\nexport const sections = {\n  manifesto: \"Position\",\n  pillars: {\n    label: \"The machine\",\n    heading: \"Four claims, each with a number behind it\",\n  },\n  specs: {\n    label: \"Specifications\",\n    heading: \"The short version\",\n    cta: { label: \"Every specification\", href: `${BASE}/specs` },\n  },\n  offer: \"Availability\",\n} as const\n\n/** The opener on the specifications page. */\nexport const specsIntro = {\n  eyebrow: \"Specifications\",\n  heading: \"Every number we will stand behind\",\n  lede:\n    \"Measured on production units at 22°C ambient, not modelled. Where a figure carries a tolerance, the tolerance is the figure.\",\n} as const\n\n/** Grouped for the specifications page and the short list on the home page. */\nexport const specs = [\n  {\n    group: \"Thermal\",\n    rows: [\n      [\"Core\", \"1.4kg machined brass, insulated\"],\n      [\"Control\", \"PID, 40Hz sampling, 0.1°C band\"],\n      [\"Recovery\", \"4 seconds to setpoint after a double\"],\n      [\"Range\", \"86–96°C, set in tenths\"],\n    ],\n  },\n  {\n    group: \"Pressure\",\n    rows: [\n      [\"Pump\", \"Geared rotary, 48V brushless\"],\n      [\"Profile\", \"Six programmable points, 0–12 bar\"],\n      [\"Flow\", \"0.5–9 ml/s, measured not inferred\"],\n      [\"Basket\", \"58mm, 18g and 21g included\"],\n    ],\n  },\n  {\n    group: \"Body\",\n    rows: [\n      [\"Shell\", \"Brushed 316 stainless, 3mm\"],\n      [\"Mass\", \"14.2kg\"],\n      [\"Footprint\", \"245 × 310mm, 360mm tall\"],\n      [\"Water\", \"2.1L side-fill, or plumbed\"],\n    ],\n  },\n  {\n    group: \"Electrical\",\n    rows: [\n      [\"Supply\", \"220–240V, 50/60Hz, 1400W\"],\n      [\"Standby\", \"Under 0.4W\"],\n      [\"Warm-up\", \"11 minutes from cold\"],\n      [\"Certification\", \"CE, UKCA, cETLus\"],\n    ],\n  },\n] as const\n\n/** The commercial line. `note` carries the honest caveat. */\nexport const offer = {\n  eyebrow: \"First run\",\n  price: \"€2,400\",\n  priceNote: \"including VAT, shipped within the EU\",\n  heading: \"Four hundred machines, then we stop and listen\",\n  body:\n    \"The first run is built to order in Turin and ships in the order it was reserved. A reservation costs nothing and holds your place; we ask for payment when your machine is two weeks from the bench.\",\n  cta: { label: \"Reserve without paying\", href: `${BASE}/reserve` },\n  note: \"No card, no deposit, and cancel with one email.\",\n} as const\n\n/**\n * The brand story page. Each beat is a year and a paragraph, which is enough\n * structure to carry a page without inventing a timeline component.\n */\nexport const story = {\n  eyebrow: \"Story\",\n  heading: \"It started as a complaint\",\n  lede:\n    \"Two of us spent a decade building thermal controllers for laboratory equipment, then went home and made coffee on machines that could not hold a temperature.\",\n  beats: [\n    {\n      year: \"2021\",\n      title: \"The complaint\",\n      body:\n        \"A borrowed lab thermocouple in the group of a well-reviewed machine. Eleven degrees of swing across five shots. We stopped blaming the beans.\",\n    },\n    {\n      year: \"2023\",\n      title: \"The core\",\n      body:\n        \"Forty brass cores, each one heavier and more insulated than the last, until the curve went flat. The one that worked is the one in the machine — we stopped when it stopped mattering.\",\n    },\n    {\n      year: \"2025\",\n      title: \"The body\",\n      body:\n        \"Steel, because it can be refinished and it does not creak when you lock in a basket. Heavier than it needs to be for the same reason a good anvil is.\",\n    },\n    {\n      year: \"2026\",\n      title: \"The first four hundred\",\n      body:\n        \"Built to order, in the order reserved. When they are gone we will fix what we got wrong before building any more.\",\n    },\n  ] as const,\n} as const\n\n/** Reserve page copy. The form itself posts nowhere until you wire it. */\nexport const reserve = {\n  eyebrow: \"Reserve\",\n  heading: \"Hold a place in the first run\",\n  lede:\n    \"One machine per person. We will email you once when your slot is two weeks out, and never otherwise.\",\n  fields: {\n    name: \"Your name\",\n    email: \"Email\",\n    country: \"Shipping country\",\n    note: \"Anything you want us to know (optional)\",\n  },\n  submit: \"Reserve my place\",\n  /** The three lines in the card beside the form. */\n  terms: [\n    \"No payment now and no card stored — a reservation is a place in the queue.\",\n    `Built to order in ${brand.city}, and shipped in the order reserved.`,\n    \"Cancel with one reply to the email we send you.\",\n  ] as const,\n  finePrint:\n    \"No payment now, and nothing stored beyond what is in this form. Cancelling is one reply to that email.\",\n} as const\n\nexport const footer = {\n  blurb:\n    \"A single-dose espresso machine built around one number, in a shop in Turin. This is a demo of the Monolith launch template — every word on it lives in one file.\",\n  columns: [\n    {\n      heading: \"Product\",\n      links: [\n        { label: \"Story\", href: `${BASE}/story` },\n        { label: \"Specifications\", href: `${BASE}/specs` },\n        { label: \"Reserve\", href: `${BASE}/reserve` },\n      ],\n    },\n  ],\n} as const\n","type":"registry:lib"},{"path":"app/monolith-launch/components/site-header.tsx","target":"app/monolith-launch/components/site-header.tsx","content":"\"use client\"\n\nimport Link from \"next/link\"\nimport { usePathname } from \"next/navigation\"\nimport { useEffect, useRef } from \"react\"\n\nimport { BASE, brand, nav } from \"../data/product\"\n\n/**\n * The header, and the only client component in the chrome.\n *\n * The mobile menu is a native `<details>` rather than React state — nothing to\n * get wrong about focus or aria-expanded. The one hook it needs exists because\n * this lives in the layout, and a layout is *not* remounted on navigation: the\n * panel would otherwise stay open over the page you just navigated to.\n */\nexport function SiteHeader() {\n  const pathname = usePathname()\n  const menu = useRef<HTMLDetailsElement>(null)\n\n  useEffect(() => {\n    if (menu.current) menu.current.open = false\n  }, [pathname])\n\n  const links = nav.map((link) => ({ ...link, active: pathname === link.href }))\n\n  return (\n    <header className=\"monoHeader\">\n      <div className=\"monoWrap monoHeaderRow\">\n        <Link className=\"monoBrand\" href={BASE || \"/\"}>\n          <span className=\"monoBrandMark\" aria-hidden=\"true\" />\n          <span>{brand.name}</span>\n          <span className=\"monoBrandDescriptor\">{brand.descriptor}</span>\n        </Link>\n\n        <nav className=\"monoNav\" aria-label=\"Primary\">\n          {links.map((link) => (\n            <Link\n              key={link.href}\n              href={link.href}\n              className=\"monoNavLink\"\n              aria-current={link.active ? \"page\" : undefined}\n            >\n              {link.label}\n            </Link>\n          ))}\n        </nav>\n\n        <details className=\"monoMenu\" ref={menu}>\n          <summary className=\"monoMenuSummary\">Menu</summary>\n          <nav className=\"monoMenuPanel\" aria-label=\"Primary\">\n            {links.map((link) => (\n              <Link\n                key={link.href}\n                href={link.href}\n                className=\"monoMenuLink\"\n                aria-current={link.active ? \"page\" : undefined}\n              >\n                {link.label}\n              </Link>\n            ))}\n          </nav>\n        </details>\n      </div>\n    </header>\n  )\n}\n","type":"registry:component"},{"path":"app/monolith-launch/components/site-footer.tsx","target":"app/monolith-launch/components/site-footer.tsx","content":"import Link from \"next/link\"\n\nimport { brand, footer } from \"../data/product\"\n\n/**\n * The footer, closing on an oversized wordmark that is meant to be clipped by\n * the page edge. Server component — there is nothing here to hydrate.\n */\nexport function SiteFooter() {\n  return (\n    <footer className=\"monoFooter\">\n      <div className=\"monoWrap\">\n        <div className=\"monoFooterTop\">\n          <p className=\"monoFooterBlurb\">{footer.blurb}</p>\n\n          <div className=\"monoFooterCols\">\n            {footer.columns.map((column) => (\n              <div key={column.heading}>\n                <h2 className=\"monoLabel\">{column.heading}</h2>\n                <div className=\"monoFooterLinks\">\n                  {column.links.map((link) => (\n                    <Link key={link.href} href={link.href} className=\"monoFooterLink\">\n                      {link.label}\n                    </Link>\n                  ))}\n                </div>\n              </div>\n            ))}\n          </div>\n        </div>\n\n        <div className=\"monoFooterBottom\">\n          <span>\n            © {brand.year} {brand.name} — {brand.city}\n          </span>\n          <a href={`mailto:${brand.email}`}>{brand.email}</a>\n        </div>\n\n        <p className=\"monoFooterWord\" aria-hidden=\"true\">\n          {brand.name}\n        </p>\n      </div>\n    </footer>\n  )\n}\n","type":"registry:component"},{"path":"app/monolith-launch/components/reveal.tsx","target":"app/monolith-launch/components/reveal.tsx","content":"\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\n\ntype RevealProps = {\n  children: React.ReactNode\n  /** Merged after `monoReveal`, so this is where the layout class goes. */\n  className?: string\n  /** Stagger, in milliseconds. Feeds `--mono-delay` in the stylesheet. */\n  delay?: number\n}\n\n/**\n * Fades and lifts its children in when they first scroll into view.\n *\n * It renders a plain `<div>` and merges `className`, which means the revealed\n * element *is* the layout element — `<Reveal className=\"monoPillar\">` is a grid\n * child of `.monoPillars`, not a wrapper that breaks the grid. That is the whole\n * reason it does not try to be polymorphic.\n *\n * Three details are deliberate:\n *\n *   - The observer disconnects on the first intersection. Re-hiding content the\n *     reader has already passed is motion for its own sake, and it means a long\n *     page keeps zero observers alive by the time you reach the footer.\n *   - The hidden state lives in CSS, not here, so nothing flashes in before\n *     hydration and `prefers-reduced-motion` can switch the whole thing off\n *     without this component knowing.\n *   - If `IntersectionObserver` is missing, the content shows immediately.\n *     Failing open is the only acceptable direction for a fade-in.\n */\nexport function Reveal({ children, className, delay = 0 }: RevealProps) {\n  const ref = useRef<HTMLDivElement>(null)\n  const [shown, setShown] = useState(false)\n\n  useEffect(() => {\n    const node = ref.current\n    if (!node || shown) return\n\n    if (typeof IntersectionObserver === \"undefined\") {\n      setShown(true)\n      return\n    }\n\n    const observer = new IntersectionObserver(\n      (entries) => {\n        if (!entries.some((entry) => entry.isIntersecting)) return\n        setShown(true)\n        observer.disconnect()\n      },\n      /* Held back slightly from the bottom edge so it reads as a reveal, not a pop. */\n      { rootMargin: \"0px 0px -10% 0px\", threshold: 0.05 },\n    )\n\n    observer.observe(node)\n    return () => observer.disconnect()\n  }, [shown])\n\n  return (\n    <div\n      ref={ref}\n      className={className ? `monoReveal ${className}` : \"monoReveal\"}\n      data-shown={shown ? \"true\" : \"false\"}\n      style={delay ? ({ \"--mono-delay\": `${delay}ms` } as React.CSSProperties) : undefined}\n    >\n      {children}\n    </div>\n  )\n}\n","type":"registry:component"},{"path":"app/monolith-launch/components/ticker.tsx","target":"app/monolith-launch/components/ticker.tsx","content":"import { hero } from \"../data/product\"\n\n/**\n * The scrolling strip under the hero.\n *\n * The list is rendered twice because `@keyframes monoTickerScroll` translates the\n * track by exactly -50% — with one copy the loop would jump. The second copy is\n * `aria-hidden`, so a screen reader hears each phrase once rather than twice.\n */\nexport function Ticker() {\n  return (\n    <div className=\"monoTicker\">\n      <div className=\"monoTickerTrack\">\n        {hero.ticker.map((line) => (\n          <span key={line} className=\"monoTickerItem\">\n            {line}\n          </span>\n        ))}\n        {hero.ticker.map((line) => (\n          <span key={`echo-${line}`} className=\"monoTickerItem\" aria-hidden=\"true\">\n            {line}\n          </span>\n        ))}\n      </div>\n    </div>\n  )\n}\n","type":"registry:component"},{"path":"app/monolith-launch/components/spec-table.tsx","target":"app/monolith-launch/components/spec-table.tsx","content":"import { specs } from \"../data/product\"\n\nimport { Reveal } from \"./reveal\"\n\ntype Group = (typeof specs)[number]\n\n/**\n * The specification table: one full-bleed hairline band per group, with the group\n * name in the left column and the rows in the right.\n *\n * It is a `<dl>`, so a screen reader gets the term/value pairing for free — the\n * thing a grid of `<div>`s throws away. Takes the groups as a prop so the home\n * page can show two and the specifications page all of them.\n */\nexport function SpecTable({ groups }: { groups: readonly Group[] }) {\n  return (\n    <>\n      {groups.map((group, index) => (\n        <Reveal key={group.group} className=\"monoSpecGroup\" delay={index * 70}>\n          <div className=\"monoWrap monoSpecGroupInner\">\n            <h3 className=\"monoLabel\">{group.group}</h3>\n            <dl className=\"monoSpecRows\">\n              {group.rows.map(([term, value]) => (\n                <div key={term} className=\"monoSpecRow\">\n                  <dt>{term}</dt>\n                  <dd>{value}</dd>\n                </div>\n              ))}\n            </dl>\n          </div>\n        </Reveal>\n      ))}\n    </>\n  )\n}\n","type":"registry:component"},{"path":"app/monolith-launch/components/reserve-form.tsx","target":"app/monolith-launch/components/reserve-form.tsx","content":"\"use client\"\n\nimport { useState } from \"react\"\n\nimport { brand, reserve } from \"../data/product\"\n\n/**\n * The reservation form, with no backend — on purpose.\n *\n * A template cannot host a mail server or a database, and a form that silently\n * drops what someone typed is worse than one that admits it. So this hands the\n * message to something that can actually deliver it: the user's mail client, via\n * a `mailto:` the browser opens. Nothing is stored and nothing is sent anywhere\n * else.\n *\n * To make it real, replace `submit` with a server action or a `fetch` to your own\n * route. The markup and the status line can stay exactly as they are.\n */\nexport function ReserveForm() {\n  const [sent, setSent] = useState(false)\n\n  function submit(event: React.FormEvent<HTMLFormElement>) {\n    event.preventDefault()\n    const data = new FormData(event.currentTarget)\n    const get = (key: string) => String(data.get(key) ?? \"\").trim()\n\n    const body = [\n      `Name: ${get(\"name\")}`,\n      `Email: ${get(\"email\")}`,\n      `Country: ${get(\"country\")}`,\n      \"\",\n      get(\"note\") || \"(no note)\",\n    ].join(\"\\n\")\n\n    /* `encodeURIComponent` on both halves — a name with an `&` in it would\n       otherwise truncate the whole mailto. */\n    const subject = encodeURIComponent(`${brand.product} reservation`)\n    window.location.href = `mailto:${brand.email}?subject=${subject}&body=${encodeURIComponent(body)}`\n    setSent(true)\n  }\n\n  return (\n    <form className=\"monoForm\" onSubmit={submit}>\n      <div className=\"monoField\">\n        <label className=\"monoFieldLabel\" htmlFor=\"mono-name\">\n          {reserve.fields.name}\n        </label>\n        <input className=\"monoInput\" id=\"mono-name\" name=\"name\" required autoComplete=\"name\" />\n      </div>\n\n      <div className=\"monoField\">\n        <label className=\"monoFieldLabel\" htmlFor=\"mono-email\">\n          {reserve.fields.email}\n        </label>\n        <input\n          className=\"monoInput\"\n          id=\"mono-email\"\n          name=\"email\"\n          type=\"email\"\n          required\n          autoComplete=\"email\"\n        />\n      </div>\n\n      <div className=\"monoField\">\n        <label className=\"monoFieldLabel\" htmlFor=\"mono-country\">\n          {reserve.fields.country}\n        </label>\n        <input\n          className=\"monoInput\"\n          id=\"mono-country\"\n          name=\"country\"\n          required\n          autoComplete=\"country-name\"\n        />\n      </div>\n\n      <div className=\"monoField\">\n        <label className=\"monoFieldLabel\" htmlFor=\"mono-note\">\n          {reserve.fields.note}\n        </label>\n        <textarea className=\"monoInput monoTextarea\" id=\"mono-note\" name=\"note\" rows={4} />\n      </div>\n\n      <div className=\"monoHeroActions\">\n        <button type=\"submit\" className=\"monoBtn monoBtnSolid\">\n          {reserve.submit}\n          <span className=\"monoArrow\" aria-hidden=\"true\">\n            →\n          </span>\n        </button>\n      </div>\n\n      {sent && (\n        <p className=\"monoFormStatus\" role=\"status\">\n          Your mail app should be opening with the message ready to send to {brand.email}. Nothing\n          was stored by this page.\n        </p>\n      )}\n\n      <p className=\"monoFormNote\">{reserve.finePrint}</p>\n    </form>\n  )\n}\n","type":"registry:component"}],"meta":{"kind":"templates","categories":["landing","agency"],"docs":"https://ui.artbloom.tech/artbloom/templates/monolith-launch"}}