/* Hawthorne Patio Homes — marketing homepage.
   Built from "HPH homepage handoff v1.0". The handoff's design references are
   React on a design-tool runtime; this is the same design as plain HTML + CSS,
   which is what the Worker's static assets want. Component blocks below
   reproduce the design system's Button, Badge, Wordmark, Icon and form controls
   exactly — every value is lifted from _ds_bundle.js, with React's hover/press
   state expressed as :hover/:active.

   The token files are lifted verbatim; do not edit them here. */

@import url("tokens/fonts.css");
@import url("tokens/colors.css");
@import url("tokens/typography.css");
@import url("tokens/spacing.css");
@import url("tokens/shape.css");
@import url("tokens/motion.css");
@import url("tokens/base.css");

/* ------------------------------------------------------------------ page --- */

html { scroll-behavior: smooth; }
body { background: var(--color-page); }

/* The one centred column. The gutter is a variable because it is the only
   layout value that changes on a phone. */
.shell {
  max-width: 1120px;
  margin: 0 auto;
  padding-left: var(--gutter);
  padding-right: var(--gutter);
  box-sizing: border-box;
}
:root {
  --gutter: var(--page-gutter);
  /* The hero has to know this: the header is in flow above it, so the pinned
     child's bottom edge is exactly this far below the fold at rest. */
  --header-h: 84px;
}

.sec { padding-top: 88px; }
.sec--last { padding-bottom: 96px; }

h1, h2, h3 { margin: 0; }
.h-title {
  font: var(--font-title);
  letter-spacing: var(--title-track);
  color: var(--color-text-primary);
}
.h-display-m {
  font: var(--font-display-m);
  letter-spacing: var(--display-m-track);
  color: var(--color-text-primary);
}
.eyebrow {
  font: var(--font-eyebrow);
  letter-spacing: var(--eyebrow-track);
  text-transform: uppercase;
  color: var(--color-text-muted);
  /* the trailing letter's track would otherwise push the centred text left */
  margin-right: -0.2em;
}
.label {
  font: var(--font-label);
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  color: var(--color-text-secondary);
}
.body { font: var(--font-body); color: var(--color-text-secondary); }
.body-sm { font: var(--font-body-sm); color: var(--color-text-muted); }
.measure { max-width: var(--measure); }
.on-brand { color: var(--paper-00); }
/* Paper 400 fails contrast on Pine (4.1:1). Secondary text on Pine is Paper 300. */
.on-brand-muted { color: var(--paper-300); }

/* A short page (the inquiry confirmation) still puts its footer on the floor. */
.page-fill { min-height: 100vh; display: flex; flex-direction: column; }
.page-fill > .fill { flex: 1; }

.card {
  background: var(--color-surface);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius-lg);
}
.band { background: var(--pine-800); }
.stack { display: flex; flex-direction: column; }
.row { display: flex; align-items: center; }

/* ------------------------------------------------------- components: ds --- */

/* Button — components/core/Button.jsx */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  box-sizing: border-box;
  border-radius: var(--radius-md);
  letter-spacing: 0.005em;
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--ease),
              color var(--duration-fast) var(--ease),
              border-color var(--duration-fast) var(--ease);
}
.btn--sm { height: var(--control-h-sm); padding: 0 14px; font: var(--weight-semibold) 13px/1 var(--font-sans); }
.btn--md { height: var(--control-h-md); padding: 0 20px; font: var(--weight-semibold) 15px/1 var(--font-sans); }
.btn--lg { height: var(--control-h-lg); padding: 0 26px; font: var(--weight-semibold) 16px/1 var(--font-sans); }

/* Primary is Vermilion and there is one per view. */
.btn--primary {
  background: var(--color-action);
  color: var(--color-action-label);
  border: 1px solid transparent;
}
.btn--primary:hover { background: var(--color-action-hover); color: var(--color-action-label); }
.btn--primary:active { background: var(--color-action-pressed); }

.btn--secondary {
  background: var(--color-surface);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border-strong);
}
.btn--secondary:hover,
.btn--secondary:active { background: var(--color-raised); color: var(--color-text-primary); }

.btn--tertiary {
  background: transparent;
  color: var(--pine-800);
  border: 1px solid transparent;
}
.btn--tertiary:hover { background: var(--color-raised); color: var(--pine-800); }

.btn--on-brand {
  background: transparent;
  color: var(--paper-00);
  border: 1px solid var(--pine-300);
}
.btn--on-brand:hover { background: var(--pine-600); color: var(--paper-00); }

/* Badge — components/core/Badge.jsx */
.badge {
  display: inline-block;
  height: 20px;
  padding: 0 7px;
  border-radius: var(--radius-sm);
  font: var(--font-label);
  line-height: 20px;
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  white-space: nowrap;
}
.badge--neutral {
  background: var(--color-neutral-tint);
  color: var(--color-text-secondary);
  border: 1px solid transparent;
}

/* Icon — components/core/Icon.jsx. 1.5px stroke at every size, outline only. */
.ico { display: block; flex-shrink: 0; }

/* Wordmark — components/core/Wordmark.jsx. --wm is the size prop. */
.wordmark {
  font: var(--weight-medium) var(--wm, 15px)/1 var(--font-display);
  letter-spacing: var(--wayfinding-track);
  text-transform: uppercase;
  white-space: nowrap;
}
.wordmark--pine { color: var(--pine-800); }
.wordmark--on-brand { color: var(--paper-00); }
.wordmark-stacked {
  display: inline-flex;
  flex-direction: column;
  gap: calc(var(--wm) * 0.28);
}
.wordmark-stacked > .wm-name {
  font: var(--weight-medium) var(--wm)/1 var(--font-display);
  letter-spacing: 0.155em;
  text-transform: uppercase;
  margin-right: -0.155em;
  white-space: nowrap;
}
.wordmark-stacked > .wm-rule {
  height: max(1px, calc(var(--wm) * 0.09));
  background: currentColor;
}
.wordmark-stacked > .wm-sub {
  font: var(--weight-medium) calc(var(--wm) * 0.58)/1 var(--font-display);
  letter-spacing: 0.63em;
  text-transform: uppercase;
  margin-right: -0.63em;
  white-space: nowrap;
}

/* FormField / Input / Select — components/forms/*.jsx */
.field { display: flex; flex-direction: column; gap: 6px; }
.field > label { font: var(--font-label); letter-spacing: var(--label-track); text-transform: uppercase; color: var(--color-text-secondary); }
.field .req { color: var(--color-action); }
.field .hint { font: var(--font-body-sm); color: var(--color-text-muted); }
.field .err { font: var(--font-body-sm); color: var(--vermilion-700); }

/* Focus is a 2px Pine ring; invalid is a 2px Vermilion border. Both are drawn
   with an inset box-shadow rather than a thicker border so the control never
   changes height by a pixel when it takes focus. */
.control {
  box-sizing: border-box;
  width: 100%;
  height: var(--control-h-md);
  padding: 0 12px;
  border: var(--border-width) solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  font: var(--font-body);
  color: var(--color-text-primary);
  transition: border-color var(--duration-fast) var(--ease),
              box-shadow var(--duration-fast) var(--ease);
}
.control:focus {
  outline: none;
  border-color: var(--color-focus-ring);
  box-shadow: inset 0 0 0 1px var(--color-focus-ring);
}
.control:user-invalid {
  border-color: var(--color-action);
  box-shadow: inset 0 0 0 1px var(--color-action);
}
.control::placeholder { color: var(--color-text-muted); }
textarea.control {
  height: auto;
  min-height: 108px;
  padding: 12px;
  line-height: var(--body-line);
  resize: vertical;
}
/* Native select in brand chrome with the Lucide chevron as the only decoration. */
.select-wrap { position: relative; display: flex; }
select.control { appearance: none; -webkit-appearance: none; padding-right: 38px; cursor: pointer; }
.select-wrap > .ico {
  position: absolute;
  right: 12px;
  top: 50%;
  margin-top: -9px;
  pointer-events: none;
  color: var(--color-text-muted);
}
/* iOS renders an empty date input as a blank box; the placeholder-ish shim keeps
   the field legible before a value is picked. */
input[type="date"].control { -webkit-appearance: none; appearance: none; }

/* -------------------------------------------------------------- header --- */

.site-header { background: var(--pine-800); }
.site-header .shell {
  height: var(--header-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 32px;
}
.site-header .brand { display: flex; align-items: center; gap: 14px; text-decoration: none; }
.site-header .brand img { height: 42px; width: auto; display: block; }
.site-header nav { display: flex; align-items: center; gap: 12px; }

/* ---------------------------------------------------------------- hero --- */

/* The tall scroll track only earns its height while the walk is running.
   svh keeps a phone's collapsing URL bar from changing the walk's length
   mid-scroll; vh is the fallback for anything that does not know svh. */
/* The hero's own metrics. Desktop keeps the handoff's fixed values exactly;
   below 900px they become fluid in the viewport (see the responsive section). */
#top {
  --hero-pad-top: 72px;
  --hero-text-gap: 22px;
  --hero-hint-gap: 40px;
  --wm: 40px;
  position: relative;
  background: var(--color-page);
  height: 210vh;
  height: 210svh;
}
#top[data-walk="off"] { height: 100vh !important; height: 100svh !important; }
.hero-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  padding-top: var(--hero-pad-top);
  /* The handoff specifies a 100vh pinned child. Its own reference file leaves
     box-sizing at content-box, so the padding makes the child 972px in a 900px
     viewport and 72px of grove is permanently below the fold. border-box gives
     the 100vh the spec asks for. */
  box-sizing: border-box;
}
/* The canvas sits over the pinned hero without participating in its column.
   This has to be a wrapper: hero-scene.js assigns position to <hero-grove>
   inline when it upgrades, which beats any rule aimed at the element itself. */
.hero-canvas { position: absolute; inset: 0; }
hero-grove { display: block; width: 100%; height: 100%; }
/* The no-WebGL fallback lays the three plates out as plain images in a row.
   Cap each one so the row cannot overrun a narrow viewport. */
hero-grove img { max-width: 30%; object-fit: contain; object-position: bottom; }
[data-hero-text] {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--hero-text-gap);
  pointer-events: none;
  will-change: opacity, transform;
}
/* The hint sits under the wordmark, inside the title block. The handoff pins it
   to the bottom of the pinned child, which starts below the header — so it lands
   past the fold and is never seen. Here it is always on Paper, and it fades and
   travels with the title it belongs to. */
.hero-hint {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  margin-top: var(--hero-hint-gap);
  color: var(--color-text-muted);
}
/* The design system forbids looping motion; the marketing site's owner
   exemption now covers this alongside the hero walk, the reveals and the photo
   parallax. Kept to a 5px travel on the system's own curve, alternating so the
   curve reads in both directions — enough to catch an eye, not enough to be a
   thing that moves on the page. */
.hero-hint__chev { animation: hero-nudge 2600ms var(--ease) infinite alternate; }
@keyframes hero-nudge {
  from { transform: translateY(0); }
  to { transform: translateY(5px); }
}

/* ---------------------------------------------------- photograph frames --- */

.ph { position: relative; overflow: hidden; border-radius: var(--radius-lg); }
/* 112%/-6% leaves 6% of the frame height of headroom at each edge for the
   scroll parallax to travel into. */
.ph > img {
  position: absolute;
  left: 0;
  top: -6%;
  width: 100%;
  height: 112%;
  object-fit: cover;
  display: block;
}
/* Colour on hover has to be a stylesheet rule — an inline opacity:0 would win. */
.ph-color { opacity: 0; transition: opacity 300ms var(--ease); }
@media (hover: hover) {
  .ph:hover .ph-color { opacity: 1; }
}
/* A phone has no cursor to bring the colour in with, so there main.js drives
   this layer from how near the frame is to the middle of the screen: colour
   arrives as a photograph reaches the centre and drains as it leaves. The
   transition has to come off first — the loop sets a new target every frame,
   and a 300ms ease on top of that only lags behind the scroll. */
@media (hover: none) {
  .ph-color { transition: none; }
}
figure { margin: 0; display: flex; flex-direction: column; gap: 10px; }
figcaption { font: var(--font-body-sm); color: var(--color-text-muted); }

.grid-photos { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
.span-2 { grid-column: span 2; }
.ph--tall { height: 360px; }
.ph--wide { height: 260px; }
.ph--interior { height: 230px; }
.ph--map { height: 380px; }
.ph--closing { height: 420px; border-radius: 0; }

/* -------------------------------------------------------- availability --- */

.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
.sec-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 32px;
  margin-bottom: 32px;
}
.bld { overflow: hidden; display: flex; flex-direction: column; }
.bld__plate {
  background: var(--color-page);
  border-bottom: var(--border-width) solid var(--color-border);
  height: 300px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 20px 20px 0;
  overflow: hidden;
}
.bld__plate img { height: 280px; width: auto; display: block; }
/* The lag on the transform is what reads as depth. */
[data-tilt] img { transition: transform 260ms var(--ease); }
.bld__name {
  font: var(--weight-medium) 20px/1 var(--font-display);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--pine-800);
}
.bld__head { padding: 22px 22px 6px; display: flex; flex-direction: column; gap: 6px; }
.bld__units { display: flex; flex-direction: column; margin-top: 16px; }
.unit {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 22px;
  border-top: var(--border-width) solid var(--color-border);
}
.unit__name { font: var(--weight-semibold) var(--body-size)/1.3 var(--font-sans); color: var(--color-text-primary); }
.unit__spec { font: var(--font-body-sm); color: var(--color-text-secondary); }

/* --------------------------------------------------------- waiting list --- */

.waitlist {
  border-radius: var(--radius-lg);
  padding: 44px 56px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 44px;
}
.waitlist__lede {
  font: var(--weight-medium) 25px/1.2 var(--font-display);
  letter-spacing: var(--title-track);
  color: var(--paper-00);
}

/* ----------------------------------------------------------- the homes --- */

/* The 1px grid gap over a Paper 200 background is what draws the dividers. */
.specs {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--color-border);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.spec {
  background: var(--color-surface);
  padding: 26px 22px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.spec > .ico { color: var(--pine-600); }
.spec__title { font: var(--weight-semibold) var(--body-size)/1.4 var(--font-sans); color: var(--color-text-primary); }
.spec__body { font: var(--font-body-sm); color: var(--color-text-secondary); }

/* ------------------------------------------------------------- inquire --- */

.inquire { display: grid; grid-template-columns: 1fr 1.15fr; gap: 56px; align-items: start; }
/* Its short top padding is intentional: "The homes" supplies the other 56px,
   which together make the page's standard 88px seam. */
.sec--inquire { padding-top: 32px; }
.inquire__form { padding: 32px; display: flex; flex-direction: column; gap: 20px; }
.pair { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.inquire__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding-top: 4px;
}
.contact-line { display: flex; align-items: center; gap: 10px; color: var(--color-text-secondary); }

/* -------------------------------------------------------- where we are --- */

.where { display: grid; grid-template-columns: 1fr 1.6fr; gap: 20px; align-items: stretch; }
.where__card { padding: 32px; display: flex; flex-direction: column; gap: 26px; }

/* -------------------------------------------------------------- footer --- */

.site-footer { background: var(--pine-800); padding: 56px 0; }
.site-footer .shell {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 56px;
}
.site-footer .brand { display: flex; align-items: center; gap: 16px; }
.site-footer .brand img { height: 52px; width: auto; display: block; }
.site-footer .meta { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }

/* ------------------------------------------------------------- reveals --- */

[data-reveal] {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 460ms var(--ease), transform 460ms var(--ease);
}
/* A heading resolves after the block it sits on. */
[data-reveal][data-slow] { transition-duration: 820ms; }
[data-reveal].is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  /* Without this the walk's two extra viewports are empty scrolling. */
  #top { height: 100vh !important; height: 100svh !important; }
  .hero-hint__chev { animation: none; }
  [data-reveal] { opacity: 1; transform: none; transition: none; }
  .ph-color { transition: none; }
  [data-tilt] img { transition: none; }
  html { scroll-behavior: auto; }
}

/* ----------------------------------------------------------- responsive --- */
/* Not in the handoff: the references are fixed at the 1120px column. These
   rules are the smallest set that keeps every section's intent at phone and
   tablet widths — the column collapses, nothing is hidden except the header
   wordmark, and no new component appears. */

@media (max-width: 1040px) {
  :root { --gutter: 32px; }
  .inquire { gap: 32px; }
}

@media (max-width: 900px) {
  .sec { padding-top: 56px; }
  .sec--last { padding-bottom: 56px; }
  .grid-photos { grid-template-columns: repeat(2, 1fr); }
  /* At three columns the tall figures and the wide ones each had a row to
     themselves. At two they pair up, so a 360 beside a 260 leaves a hole under
     the shorter one — one height per frame keeps the rows even. */
  .ph--tall, .ph--wide { height: 300px; }
  /* two columns wide is the whole row */
  .grid-3 { grid-template-columns: 1fr; }
  .specs { grid-template-columns: repeat(2, 1fr); }
  .inquire { grid-template-columns: 1fr; }
  .where { grid-template-columns: 1fr; }
  .ph--map { height: 300px; }
  /* Phone and tablet: the hero's rhythm follows the viewport rather than
     stepping at breakpoints, so it fits an iPhone SE and an iPad Pro from one
     rule. Every value is clamped at both ends — the floors are what keep a
     short landscape phone from crushing, the ceilings what stop a large tablet
     from sprawling, and between them the composition just scales. The wordmark
     tracks width (its constraint is fitting across); everything vertical tracks
     svh (small viewport height, so a collapsing URL bar cannot change it). */
  #top {
    --hero-pad-top: clamp(24px, 6svh, 56px);
    --hero-text-gap: clamp(10px, 2svh, 22px);
    --hero-hint-gap: clamp(16px, 3svh, 40px);
    /* The wordmark's constraint is fitting across — except on a landscape
       phone, where height is the scarce dimension and a width-derived size
       would crowd the canopy. min() takes whichever is the real limit. */
    --wm: clamp(21px, min(7vw, 5.5svh), 34px);
  }
}

/* A portrait phone is the one case where the grove cannot fill the frame: it is
   fitted to the width, so its height is set by how wide the screen is and not
   by how tall. That leaves surplus air, and the title drops down to meet the
   canopy rather than leaving the surplus as a hole in the middle of the
   composition — above the title it reads as margin under the header instead.
   The ceiling is what stops that becoming a void on a very tall phone. */
/* Portrait fallback only. main.js overrides this with a value computed against
   where the grove actually landed — see "hero fitting" there. This is what a
   phone with no WebGL, and therefore no published composition, gets. */
@media (max-width: 900px) and (orientation: portrait) {
  #top { --hero-pad-top: clamp(48px, calc(46svh - 200px), 240px); }
}

@media (max-width: 700px) {
  .waitlist { flex-direction: column; align-items: flex-start; gap: 26px; padding: 32px 26px; }
  .site-footer { padding: 44px 0; }
  .site-footer .shell { flex-direction: column; align-items: flex-start; gap: 32px; }
  .site-footer .meta { align-items: flex-start; }
  .ph--closing { height: 300px; }
  .sec-head { flex-direction: column; align-items: flex-start; gap: 6px; }
}

@media (max-width: 640px) {
  :root { --gutter: 22px; }
  /* One column: the span-2 rule would otherwise overflow the grid. */
  .grid-photos { grid-template-columns: 1fr; }
  .span-2 { grid-column: auto; }
  .ph--tall { height: 260px; }
  .ph--wide { height: 220px; }
  .ph--interior { height: 220px; }
  .specs { grid-template-columns: 1fr; }
  .pair { grid-template-columns: 1fr; }
  .inquire__form { padding: 22px; }
  .where__card { padding: 22px; }
  /* The full lockup does not fit beside the mark and both buttons. The mark
     carries the brand here; the hero states the name in full immediately below. */
  .site-header .brand .wordmark { display: none; }
  .site-header .shell { gap: 12px; }
  .waitlist__lede { font-size: 22px; }
  .h-display-m { font-size: 30px; }
  .inquire__foot { flex-direction: column; align-items: stretch; gap: 16px; }
  .inquire__foot .btn { width: 100%; }
}

@media (max-width: 400px) {
  .site-header .brand img { height: 36px; }
  .btn--sm { padding: 0 11px; }
}

/* ------------------------------------------------------------- legal --- */
/* /privacy and /terms. Not in the handoff: added 2026-09-17 so the A2P 10DLC
   campaign has the policy pages carriers require. One readable column of
   running text, the site's own chrome, and no script on either page — there is
   no hero and nothing to reveal, so neither loads main.js or three.js.
   Every rule lives here rather than in the page: these two files are served
   straight out of Workers Assets, so they never get the Worker's
   style-src 'unsafe-inline'. */

.legal { max-width: var(--measure); }
.legal > h1 { margin: 10px 0 0; }
.legal > h2 { margin: 40px 0 0; }
.legal > p { margin: 14px 0 0; }
.legal .body { color: var(--color-text-primary); }
.legal__lede { margin-top: 18px; }
.legal ul {
  margin: 12px 0 0;
  padding-left: 22px;
  color: var(--color-text-primary);
}
.legal li { margin-top: 8px; }
/* The effective date closes the document, so it is ruled off from the last
   section rather than reading as another paragraph of it. */
.legal__date {
  margin-top: 40px;
  padding-top: 24px;
  border-top: var(--border-width) solid var(--color-border);
}
.legal__also { margin-top: 10px; }

/* The footer's policy links. Paper 300 is the on-Pine secondary; the link
   colour tokens are all tuned for Paper behind them, not Pine. */
.footer-links a { color: var(--paper-300); }
.footer-links a:hover { color: var(--paper-00); }

@media (max-width: 640px) {
  .legal > h2 { margin-top: 32px; }
}

/* Consent evidence screenshots must fit the policy column on phones. */
.legal img { display: block; max-width: 100%; height: auto; }
