/* =====================================================================
   Le Mas de Grâce — site stylesheet
   Aesthetic direction: editorial French farmhouse. Warm photography held
   inside a cool, quiet frame — hairline blue-grey rules, small-cap
   eyebrows, generous negative space, asymmetric photo/text bands. The
   mark is a drawing on paper; the page is the paper.

   THIS BLOCK IS THE AUTHORITY for every brand colour value. It used to cite
   design/brand-canon.md §2; that file was absorbed and retired 2026-08-17, and
   the values did not move anywhere else. Do not hand-pick a colour that is not
   in this block, and do not restate these hexes in a doc — the rules that
   surround them (the retired maroon, the held gold) are in ../../CLAUDE.md
   § Brand.
   ===================================================================== */

/* ---------------------------------------------------------------------
   FONTS — LANDED 2026-08-17. The page is finally in the brand's type;
   until this date it fell through to a system serif in production.

   EB Garamond (SIL OFL, headings) and Erode (Fontshare FFL, body) are
   self-hosted subset woff2 in site/fonts/ with both licences alongside,
   built by backend/build-fonts.py. 4 faces, 70.6 KB total. The subset
   preserves â (Grâce) — asserted by `build-fonts.py --verify`, which is
   the check to re-run after any charset change.

   Sources live on Jeremy's disk, NOT in the repo, so a fresh clone cannot
   rebuild these — that is why the woff2 files themselves are committed.

   ERODE HAS NO SMALL CAPS and no arrows. Every all-small-caps rule below
   sets --font-text, so all 14 are browser-synthesized — unchanged from the
   fallback, but see backend/build-fonts.py and TODO.md §0b. --------------- */

@font-face{
  font-family:"EB Garamond"; font-style:normal; font-weight:400;
  font-display:swap; src:url("../fonts/eb-garamond-400.woff2") format("woff2");
}
@font-face{
  font-family:"EB Garamond"; font-style:italic; font-weight:400;
  font-display:swap; src:url("../fonts/eb-garamond-400-italic.woff2") format("woff2");
}
@font-face{
  font-family:"Erode"; font-style:normal; font-weight:400;
  font-display:swap; src:url("../fonts/erode-400.woff2") format("woff2");
}
@font-face{
  font-family:"Erode"; font-style:normal; font-weight:500;
  font-display:swap; src:url("../fonts/erode-500.woff2") format("woff2");
}

:root{
  /* --- colour: the authority, see the header note --- */
  --chalk:#F7F4EF;
  --linen:#EDE6DC;
  --papier:#FFFDFA;
  --ink:#26262B;
  --ink-soft:#4A4A50;
  --bleu:#8A97A6;
  --bleu-profond:#5E6E7E;
  --bleu-pale:#C9D2D8;
  --rule:rgba(138,151,166,.45);

  /* Scrim — carried over verbatim from the Salon guide. It was measured
     against this exact salon photograph's blown-out windows. Do not
     re-tune by eye. */
  --scrim:linear-gradient(to top,
      rgba(24,19,14,.82) 0%, rgba(24,19,14,.62) 34%,
      rgba(24,19,14,.22) 66%, rgba(24,19,14,0) 100%);
  --scrim-left:linear-gradient(to right,
      rgba(24,19,14,.62) 0%, rgba(24,19,14,.22) 46%, rgba(24,19,14,0) 72%);

  /* --- type --- */
  --font-display:"EB Garamond","Hoefler Text",Garamond,"Times New Roman",serif;
  --font-text:"Erode",Charter,"Bitstream Charter","Palatino Linotype",Palatino,Georgia,serif;

  /* TYPE SCALE — raised across the board, Grace's review 2026-08-06: every
     size on the page read too small. This block is the single place it is
     tuned; no component sets a raw px size any more.

     Two floors are load-bearing:
     · nothing a guest READS may fall below 16px at any viewport, so --fs-body
       and --fs-caption both floor at 17px and 16px. --fs-caption carries real
       prose (captions, room notes, the policy line, detail values), so it is
       body copy in everything but name and gets a body-copy floor.
     · --fs-sc and --fs-ui are set in all-small-caps, whose glyphs render at
       roughly 72% of the nominal size. 13px small-caps read as ~9px — that is
       why the eyebrows and the facts strip were the worst offenders. Floored
       at 16px so they land near a 12px cap-height.

     The retired brand doc (now design/archive/brand-canon.md §3) dropped --fs-h1
     to clamp(34px,5.6vw,56px) because the headings came back too large; this
     raises it again but stays well under the clamp(40px,8.4vw,76px) she
     rejected. This block supersedes that figure. */
  --fs-h1:clamp(38px,6vw,62px);
  --fs-h2:clamp(30px,4vw,44px);
  --fs-h3:clamp(23px,2.4vw,28px);
  --fs-lead:clamp(19px,1.9vw,22px);
  --fs-body:clamp(17px,1.35vw,19px);
  --fs-quote:clamp(24px,3.4vw,34px);
  --fs-caption:clamp(16px,1.15vw,17px);
  --fs-sc:clamp(16px,1.15vw,17px);
  --fs-ui:clamp(16px,1.2vw,18px);

  --ls-sc:.18em; --ls-eyebrow:.26em; --lh-body:1.68;

  /* --- space --- */
  --pad-section:clamp(64px,8vw,120px);
  --pad-gutter:clamp(20px,5vw,88px);
  --maxw:1180px;
  --measure:34rem; --measure-lead:26rem;
  --rule-w:40px;
  --header-h:68px;

  /* Anchor landing offset. The whole sticky <header> is one block, and on
     mobile it is TWO rows — the bar plus the always-visible .subnav — so
     scroll-padding-top of (--header-h + 12px) dropped #write under it and the
     form heading landed behind the subnav. --header-total is written by JS from
     the header's measured height (site.js §0) and is exact at every breakpoint;
     the value here is the no-JS fallback and is deliberately generous. */
  --header-total:116px;

  /* Icon slot for the Highlights block. Real dimensions reserved now so
     commissioned icons drop in without a relayout. */
  --icon-box:44px;
}

@media (min-width:900px){ :root{ --header-h:82px; --header-total:82px; } }

/* ---------------------------------------------------------------------
   reset
   --------------------------------------------------------------------- */
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%; scroll-behavior:smooth; scroll-padding-top:calc(var(--header-total) + 20px)}
body{
  margin:0; background:var(--chalk); color:var(--ink);
  font:400 var(--fs-body)/var(--lh-body) var(--font-text);
  overflow-x:hidden;
  -webkit-font-smoothing:antialiased;
}
img,svg,video{max-width:100%; display:block}
h1,h2,h3,h4,p,ul,ol,dl,dd,figure,fieldset,legend{margin:0}

/* ORPHAN CONTROL — added 2026-08-17 (v1.5), Jeremy's ask: no line should end on
   a lone word. Only the LAST line of a block can ever be short — every interior
   line is full by definition — so the rule is really "no short last line".

   `pretty` makes the browser reconsider the final few lines and re-break an
   earlier one to pull a word down. It is Chrome 117+ / Safari 26+; Firefox has
   not shipped it and simply ignores it, which lands back on today's behaviour,
   so there is nothing to guard and no fallback to write.

   IT ONLY EVER RESCUES ONE WORD. It does not deliver the three-word floor on its
   own — that is what the hand-placed &nbsp; in the markup are for. Do not read
   this rule as having solved the problem.

   ELEMENT SELECTORS ON PURPOSE, so any class-level rule still wins: notably
   .videoband__tagline, which sets `balance` (correct for a short display line)
   and must keep it. `balance` was considered for body copy and rejected — it
   re-rags a whole block to even line lengths, which is a heading tool. Do not
   add it anywhere else. */
p,li,dd,dt,figcaption,blockquote,h1,h2,h3,h4,label,legend,summary,td,th{
  text-wrap:pretty;
}
ul{padding:0; list-style:none}
fieldset{border:0; padding:0; min-width:0}
a{color:var(--bleu-profond); text-underline-offset:.22em; text-decoration-thickness:1px}
a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,
summary:focus-visible,[tabindex]:focus-visible{
  outline:2px solid var(--bleu-profond); outline-offset:3px;
}
::selection{background:var(--bleu-pale); color:var(--ink)}

/* ---------------------------------------------------------------------
   atoms
   --------------------------------------------------------------------- */
.wrap{max-width:var(--maxw); margin-inline:auto; padding-inline:var(--pad-gutter)}
.wrap--narrow{max-width:860px}

.section{padding-block:var(--pad-section)}
.section--linen{background:var(--linen)}
.section--papier{background:var(--papier)}

/* --bleu-profond on --chalk is 4.78:1 but only 4.23:1 on --linen, and the
   eyebrows sit on both grounds. One value that passes AA everywhere beats two
   that drift, so they take --ink-soft: 8.02:1 on chalk, 7.10:1 on linen. The
   quiet-label reading is carried by size, small caps and tracking, not colour. */
.eyebrow{
  font:400 var(--fs-sc)/1.3 var(--font-text);
  font-variant-caps:all-small-caps; letter-spacing:var(--ls-eyebrow);
  text-transform:lowercase; color:var(--ink-soft);
}
.eyebrow--c{text-align:center}

/* The name, wherever it is live type, is small-capped (../../CLAUDE.md § Brand). */
.name{font-variant-caps:all-small-caps; letter-spacing:.04em}

.hairline{width:var(--rule-w); height:1px; background:var(--bleu); border:0; margin:18px 0 26px}
.hairline--c{margin-inline:auto}

h1,.h1{font:400 var(--fs-h1)/1.10 var(--font-display); letter-spacing:-.005em}
h2,.h2{font:400 var(--fs-h2)/1.16 var(--font-display); letter-spacing:0}
h3,.h3{font:400 var(--fs-h3)/1.24 var(--font-display)}

.lead{font-size:var(--fs-lead); line-height:1.62; max-width:var(--measure-lead)}
.body{max-width:var(--measure)}
.body p + p{margin-top:1.05em}
.caption{
  font:italic 400 var(--fs-caption)/1.5 var(--font-text);
  color:var(--ink-soft); margin-top:10px;
}

/* Placeholder copy. Every prose slot on this site is one of these until
   Grace's copywriter replaces it. The dotted rule is deliberate — it must
   be impossible to mistake filler for finished copy. */
.ph{
  color:var(--ink-soft);
  background:rgba(138,151,166,.09);
  border-bottom:1px dotted var(--bleu);
  padding:0 .16em;
  font-style:normal;
}
.ph--light{color:rgba(255,255,255,.86); background:rgba(255,255,255,.13); border-bottom-color:rgba(255,255,255,.5)}

/* ---------------------------------------------------------------------
   CTA — one label on the whole site: "Write to Grace"
   --------------------------------------------------------------------- */
/* --papier on --bleu was 2.93:1 — the site's only CTA failed AA outright. The
   fill moves to --bleu-profond (5.16:1), which canon §2 already lists for
   "small type on pale grounds"; hover darkens to --ink-soft (8.67:1) since
   there is no darker blue in the palette. --bleu keeps rules and ornament. */
.cta{
  display:inline-flex; align-items:center; justify-content:center;
  background:var(--bleu-profond); color:var(--papier);
  font:500 var(--fs-ui)/1 var(--font-text);
  font-variant-caps:all-small-caps; letter-spacing:var(--ls-sc);
  text-decoration:none; padding:15px 30px; border:0; border-radius:1px;
  cursor:pointer; transition:background-color .18s ease, transform .18s ease;
}
.cta:hover{background:var(--ink-soft)}
.cta:active{transform:translateY(1px)}
/* No size modifiers. "Write to Grace" is one button at one size in the
   header, over the video, in every room dialog and on /details/ — it is the
   site's only CTA and it should not shrink or swell to suit its container
   (2026-08-07). .cta--ghost is a *colour* variant for the form's Back
   buttons, which are not the CTA, and keeps the same metrics. */
.cta--ghost{
  background:transparent; color:var(--bleu-profond);
  box-shadow:inset 0 0 0 1px var(--rule);
}
.cta--ghost:hover{background:transparent; color:var(--ink); box-shadow:inset 0 0 0 1px var(--bleu)}

.textlink{
  font-variant-caps:all-small-caps; letter-spacing:var(--ls-sc);
  font-size:var(--fs-ui); color:var(--bleu-profond); text-decoration:none;
  border-bottom:1px solid var(--rule); padding-bottom:3px;
  transition:border-color .18s ease, color .18s ease;
}
.textlink:hover{color:var(--ink); border-bottom-color:var(--bleu)}

/* ---------------------------------------------------------------------
   image frames — fixed aspect, cover crop, linen ground while loading
   --------------------------------------------------------------------- */
.frame{
  position:relative; overflow:hidden; background:var(--linen);
  margin:0;
}
.frame img{width:100%; height:100%; object-fit:cover}
.frame--45{aspect-ratio:4/5}
.frame--32{aspect-ratio:3/2}
.frame--11{aspect-ratio:1/1}
.frame--43{aspect-ratio:4/3}

/* .frame--trimleft — v1.5, 2026-08-17. Shaves the left edge of ONE photograph,
   the §4 "Arrival" entrance frame, whose source carries 5 columns of pure black
   down its left side (see the comment in index.html).

   PERCENTAGES, NOT PIXELS: the frame is 530px on desktop and 92vw on mobile, so
   a -6px offset would under-crop at one width and over-crop at the other.
   6 of 1232 columns is 0.49%; 0.6% leaves a margin for the transitional column.

   SCOPED TO ITS OWN CLASS ON PURPOSE. The obvious selector, .houserow__media
   .frame img, would have caught all SEVEN house-row figures and nudged every
   one of them off-centre. Do not widen this. */
.frame--trimleft img{width:100.6%; max-width:none; margin-left:-0.6%}

/* ---------------------------------------------------------------------
   header
   --------------------------------------------------------------------- */
.header{
  position:sticky; top:0; z-index:60;
  background:rgba(247,244,239,.92);
  backdrop-filter:saturate(140%) blur(8px);
  border-bottom:1px solid var(--rule);
}
.header__bar{
  max-width:var(--maxw); margin-inline:auto;
  padding-inline:var(--pad-gutter);
  min-height:var(--header-h);
  display:flex; align-items:center; gap:16px;
}
.header__home{flex:0 0 auto; display:block; line-height:0}
.header__home img{height:34px; width:auto}
.header__home .lockup--wide{display:none}
@media (min-width:900px){
  .header__home img{height:46px}
  .header__home .lockup--tight{display:none}
  .header__home .lockup--wide{display:block}
}

.nav{margin-left:auto; display:flex; align-items:center; gap:clamp(14px,2vw,30px)}
/* The header CTA is one line at every width (2026-08-12, Jeremy). It was a flex
   item with flex:0 1 auto, so it took whatever the lock-up left and wrapped
   "Write to Grace" onto two lines below 900px. flex:0 0 auto + nowrap makes its
   width a property of the button rather than of its neighbour — the same
   reasoning as the .fieldrow fix in §5. The mobile lock-up drops 34px -> 28px
   and the padding tightens to buy the room: measured, the header then lays out
   at 157x42 with the page still fitting 390 exactly. Above 560px the 34px mark
   and full padding return and nothing moves. */
.nav .cta{flex:0 0 auto; white-space:nowrap}
@media (max-width:560px){
  .header__home img{height:28px}
  .nav .cta{padding:13px 18px}
}
/* 320px — the original SE and the narrowest viewport worth serving. The header
   overflowed here BEFORE this change too (scrollWidth 328 against a 320 view,
   measured on the v1.2 build), because the two-line button was still wider than
   what the 34px lock-up left. A one-line button needs this tier to fit, and it
   closes a defect that was already shipping. */
@media (max-width:359px){
  .header__home img{height:24px}
  .nav .cta{padding:11px 14px}
}
.nav__list{display:none}
@media (min-width:900px){ .nav__list{display:flex; align-items:center; gap:30px} }
.nav__item{position:relative}
.nav__link{
  font:400 var(--fs-ui)/1 var(--font-text);
  /* top-level size is set on .nav__list > .nav__item > .nav__link below */
  font-variant-caps:all-small-caps; letter-spacing:var(--ls-sc);
  color:var(--ink-soft); text-decoration:none; background:none; border:0;
  padding:8px 0; cursor:pointer;
}
.nav__link:hover{color:var(--ink)}
/* Story and Explore are post-launch (sitemap.md). They are shown as inert
   labels rather than links so nothing on the page 404s.
   --bleu-pale on the header ground was 1.40:1 — invisible. --bleu-profond is
   4.78:1 and still reads as a different, quieter register than the live links
   because it is a hue shift rather than a tint. */
.nav__link[aria-disabled="true"]{color:var(--bleu-profond); cursor:default}
/* The SOON marker was 9px --bleu on chalk: 2.71:1, and the client could not
   see it at all. Now --bleu-profond (4.78:1) and never smaller than 12px. */
.nav__soon{
  /* font-variant-caps is reset: the parent's all-small-caps would shrink these
     already-uppercase letters to ~72% again, undoing the size bump. */
  font-variant-caps:normal; font-size:max(12px,.72em);
  letter-spacing:.12em; vertical-align:.45em;
  margin-left:6px; color:var(--bleu-profond);
}

/* THE DROPDOWN DISCLOSURE CARET — added 2026-08-17 (v1.5).
   CLAUDE.md § Nav has always specified "STAY ▾" and no file carried the marker,
   so the dropdown opened on hover with nothing to announce it existed. That,
   not a missing link, is why /explore/ read as a dead end.

   DRAWN IN CSS, NOT SET AS A GLYPH, and this is deliberate — do not "simplify"
   it back to a ▾ character. Neither shipped face has one: Erode has no
   U+25BE (nor any arrow), and EB Garamond has only the heavier U+25BC, so a
   text caret would fall through to a system font and render inconsistently or
   as tofu. Borders have no such dependency and give crisp edges at this size.
   currentColor so it inherits the link's hover and small-caps colour changes. */
.nav__caret{
  display:inline-block; vertical-align:.16em; margin-left:.42em;
  width:0; height:0;
  border-left:.26em solid transparent;
  border-right:.26em solid transparent;
  border-top:.28em solid currentColor;
  transition:transform .18s ease;
}
.nav__item:hover .nav__caret,
.nav__item:focus-within .nav__caret{transform:rotate(180deg)}
@media (prefers-reduced-motion:reduce){
  .nav__caret{transition:none}
}

/* Stay's children. Desktop: a dropdown panel on hover/focus-within, using
   @starting-style + allow-discrete so there is no JS state machine
   (motion-spec.md §5). Mobile: a second, always-visible row. */
.nav__sub{
  display:flex; gap:22px; align-items:center;
  padding:0; margin:0;
}
@media (min-width:900px){
  .nav__sub{
    display:none; position:absolute; top:100%; left:50%;
    translate:-50% 0; flex-direction:column; gap:0; min-width:190px;
    background:var(--papier); border:1px solid var(--rule);
    box-shadow:0 12px 34px rgba(38,38,43,.10);
    /* 6px of the top padding is the bridge that used to be a dead gap.
       background-clip:padding-box keeps the panel reading as if it still
       floats clear of the header rule. */
    padding:14px 0 8px;
    opacity:0; translate:-50% -6px;
    transition:opacity .18s ease, translate .18s ease, display .18s allow-discrete;
  }
  .nav__item:hover .nav__sub,
  .nav__item:focus-within .nav__sub{display:flex; opacity:1; translate:-50% 0}
  @starting-style{
    .nav__item:hover .nav__sub,
    .nav__item:focus-within .nav__sub{opacity:0; translate:-50% -6px}
  }
  /* Stay and Explore only. Scoped by child combinator so the dropdown's
     own .nav__links are untouched. */
  .nav__list > .nav__item > .nav__link{font-size:clamp(19px,1.6vw,23px)}
  .nav__sub .nav__link{padding:9px 20px; width:100%; font-size:var(--fs-ui)}
  .nav__sub .nav__link:hover{background:var(--linen)}
}

.subnav{
  border-top:1px solid var(--rule);
  overflow-x:auto; -webkit-overflow-scrolling:touch;
  scrollbar-width:none;
}
.subnav::-webkit-scrollbar{display:none}
.subnav .nav__sub{
  max-width:var(--maxw); margin-inline:auto;
  padding:11px var(--pad-gutter); white-space:nowrap;
}
@media (min-width:900px){ .subnav{display:none} }

/* ---------------------------------------------------------------------
   1 — hero
   --------------------------------------------------------------------- */
.hero{position:relative; background:var(--ink)}
.hero__media{position:relative; height:clamp(520px,92svh,880px); overflow:hidden}
.hero__media img{width:100%; height:100%; object-fit:cover}
/* THE HERO SCRIM IS GONE — 2026-08-22, Jeremy's call. The photograph now
   runs clean under the header. .hero__scrim and its 900px override were
   deleted with the div; --scrim / --scrim-left stay, they still dress
   .videoband__scrim and .band__scrim. Do not reinstate an overlay here. */
/* CENTRE ON THE INK, NOT THE CANVAS (../../CLAUDE.md § Brand rule 4, TODO.md §2a).
   Both lock-up files carry baked-in padding, and it is asymmetric, so the
   canvas box lies about where the artwork is. Measured off the source PNGs
   rather than taken from the doc's rounded figures:

     stacked    1500x762   ink 100,78 -> 1400,656   pad L6.67 R6.67 T10.24 B13.91
     horizontal 3039x470   ink 151,84 -> 2929,395   pad L4.97 R3.62 T17.87 B15.96

   The padding is cancelled with negative margins derived from the file's own
   aspect ratio, so the compensation stays exact at every width. Percentage
   margins would resolve against the container's INLINE size, not the image's
   height, so the height is recomputed in calc() instead. */
/* Widths are re-set because the lock-ups are wider-per-unit-of-type than the
   plain wordmark they replace (6.47:1 and 1.97:1 against 5.45:1). Held at the
   old width the lettering would have shrunk — the opposite of what the review
   asked for. These keep the NAME at the size Grace approved: 340px matches
   canon §4's own arithmetic for a 390px viewport (~168px tall, wordmark ~286px,
   above the ~240px legibility floor); 510px is 430 x 6.466/5.449. */
.hero__lockup{
  --lockup-w:min(340px,88vw);
  --lockup-h:calc(var(--lockup-w) / (1500 / 762));
  /* aspect-ratio, not the width/height attributes: <picture> swaps in a
     3039x470 file above 900px while the attributes still say 1500x762, and
     without this the box would resize when the desktop file lands. */
  width:var(--lockup-w); height:auto; aspect-ratio:1500 / 762;
  margin-top:calc(var(--lockup-h) * -0.1024);
  margin-bottom:calc(26px - var(--lockup-h) * 0.1391);
}
@media (min-width:900px){
  /* One <img>, swapped by <picture>, so the aspect ratio and therefore the
     padding compensation both change at this breakpoint. */
  .hero__lockup{
    --lockup-w:min(510px,48vw);
    --lockup-h:calc(var(--lockup-w) / (3039 / 470));
    aspect-ratio:3039 / 470;
    margin-top:calc(var(--lockup-h) * -0.1787);
    margin-bottom:calc(22px - var(--lockup-h) * 0.1596);
    margin-left:calc(var(--lockup-w) * -0.0497);
  }
}
.hero__scrollcue{
  position:absolute; bottom:14px; left:50%; translate:-50% 0;
  width:1px; height:38px; background:linear-gradient(to bottom,transparent,var(--bleu-pale));
}

/* ---------------------------------------------------------------------
   2 — introduction + facts strip
   --------------------------------------------------------------------- */
.intro{text-align:center}
.intro .body{margin-inline:auto; text-align:center}
.facts{
  margin-top:clamp(36px,5vw,58px);
  border-top:1px solid var(--rule); border-bottom:1px solid var(--rule);
  padding:18px 0;
  display:flex; flex-wrap:wrap; justify-content:center;
  gap:6px clamp(10px,2vw,22px);
  font:500 var(--fs-sc)/1.5 var(--font-text);
  font-variant-caps:all-small-caps; letter-spacing:var(--ls-eyebrow);
  color:var(--ink-soft);   /* was --bleu-profond, 4.78:1 → 8.02:1 */
}
.facts li{display:flex; align-items:center; gap:clamp(10px,2vw,22px)}
/* the separator was --bleu-pale at 1.40:1 */
.facts li:not(:last-child)::after{content:"·"; color:var(--bleu-profond); letter-spacing:0}
.facts li:first-child{color:var(--ink)}

/* ---------------------------------------------------------------------
   2b — the video tour
   Full-bleed band. Grace's tagline sits over it; nothing sits over the
   still hero above. Same scrim treatment as .band so the two full-bleed
   sections read as one family.
   --------------------------------------------------------------------- */
.videoband{position:relative; background:var(--ink); overflow:hidden}
.videoband__media{
  display:block; width:100%; height:auto;
  aspect-ratio:3/2; max-height:82svh; object-fit:cover;
}
/* The band's height comes from the media, but its content is absolutely
   positioned against the bottom edge — so a tagline taller than the media
   overflows UPWARD and .videoband's overflow:hidden clips it. At 390px the
   full tagline is four lines (~250px) against a 3/2 band only 260px tall, and
   the first line was cut off (2026-08-10). Crop the poster to a portrait
   ratio on phones instead: object-fit:cover handles the frame, and the text
   gets the room it needs. Do not put the short tagline back to fix this. */
@media (max-width:899px){
  .videoband__media{aspect-ratio:4/5}
}
.videoband__scrim{position:absolute; inset:0; background:var(--scrim); pointer-events:none}
.videoband__inner{
  position:absolute; inset:auto 0 0 0;
  max-width:var(--maxw); margin-inline:auto;
  padding:0 var(--pad-gutter) clamp(34px,5vw,64px);
  display:flex; flex-direction:column; align-items:center; text-align:center;
  gap:clamp(20px,2.5vw,30px);
}
@media (min-width:900px){
  .videoband__inner{align-items:flex-start; text-align:left; left:0; right:0}
}
.videoband__tagline{
  font:400 var(--fs-h1)/1.12 var(--font-display);
  letter-spacing:-.005em; color:var(--chalk);
  max-width:18ch; text-wrap:balance; margin:0;
}

/* ---------------------------------------------------------------------
   3 — the rooms
   --------------------------------------------------------------------- */
.rooms{
  display:grid; gap:clamp(22px,3vw,28px);
  grid-template-columns:1fr;
  margin-top:clamp(34px,4vw,52px);
}
@media (min-width:900px){ .rooms{grid-template-columns:repeat(3,1fr)} }

.room{
  position:relative; text-align:left;
  background:var(--papier); border:1px solid var(--rule);
  padding:0 0 22px; cursor:pointer; width:100%;
  font:inherit; color:inherit;
  transition:box-shadow .22s ease, border-color .22s ease;
}
.room:hover{box-shadow:0 16px 40px rgba(38,38,43,.09); border-color:var(--bleu)}
.room .frame{border-bottom:1px solid var(--rule)}
.room img{transition:scale .5s cubic-bezier(.2,.7,.3,1)}
.room:hover img{scale:1.03}
.room__body{padding:20px clamp(18px,2vw,24px) 0; position:relative}
.room__name{font:400 var(--fs-h3)/1.2 var(--font-display)}
.room__note{font-size:var(--fs-caption); color:var(--ink-soft); margin-top:9px; line-height:1.6}
.room__more{
  display:inline-block; margin-top:14px;
  font:400 var(--fs-sc)/1 var(--font-text);
  font-variant-caps:all-small-caps; letter-spacing:var(--ls-sc);
  color:var(--bleu-profond);
}
/* LE mark, corner detail (spec §3) */
.room__mark{
  position:absolute; right:clamp(18px,2vw,24px); top:20px;
  height:34px; width:auto; opacity:.5;
}

/* --- room dialog --- */
.roomdlg{
  border:0; padding:0; max-width:min(980px,94vw); width:100%;
  max-height:92svh; background:var(--papier); color:var(--ink);
  box-shadow:0 30px 90px rgba(24,19,14,.32);
  opacity:0; translate:0 16px;
  transition:opacity .24s ease, translate .24s ease, display .24s allow-discrete, overlay .24s allow-discrete;
}
.roomdlg[open]{opacity:1; translate:0 0}
@starting-style{ .roomdlg[open]{opacity:0; translate:0 16px} }
.roomdlg::backdrop{background:rgba(24,19,14,0); transition:background .24s ease, display .24s allow-discrete, overlay .24s allow-discrete}
.roomdlg[open]::backdrop{background:rgba(24,19,14,.62)}
@starting-style{ .roomdlg[open]::backdrop{background:rgba(24,19,14,0)} }

.roomdlg__scroll{max-height:92svh; overflow-y:auto; overscroll-behavior:contain}
.roomdlg__hero{position:relative}
.roomdlg__hero .frame{aspect-ratio:3/2}
@media (max-width:640px){ .roomdlg__hero .frame{aspect-ratio:4/5} }
.roomdlg__close{
  position:absolute; top:14px; right:14px; z-index:2;
  width:40px; height:40px; border:0; border-radius:50%;
  background:rgba(255,253,250,.92); color:var(--ink);
  font:400 24px/1 var(--font-text); cursor:pointer;
}
.roomdlg__close:hover{background:var(--papier)}
.roomdlg__body{padding:clamp(24px,3.4vw,40px)}
.roomdlg__name{font:400 var(--fs-h2)/1.14 var(--font-display)}
.roomdlg__facts{
  margin-top:22px; padding-top:18px; border-top:1px solid var(--rule);
  display:grid; gap:12px 28px; grid-template-columns:1fr;
}
@media (min-width:640px){ .roomdlg__facts{grid-template-columns:repeat(2,1fr)} }
.roomdlg__facts dt{
  font:500 var(--fs-sc)/1.4 var(--font-text);
  font-variant-caps:all-small-caps; letter-spacing:var(--ls-eyebrow);
  color:var(--bleu-profond);
}
.roomdlg__facts dd{font-size:var(--fs-caption); color:var(--ink-soft); margin-top:3px}

/* gallery — VERTICAL STACK, one scroll axis. 2026-08-20, Jeremy's call
   (TODO.md §0f #4). It used to be a bounded horizontal snap track, which put a
   second scroll axis inside .roomdlg__scroll — already a vertical scroller
   above at :592. Do not put overflow-x or scroll-snap back.
   The matching tabindex="0" came off the three .gallery divs in index.html at
   the same time: it existed only to make a scrollable region keyboard
   reachable, and with nothing to scroll it is an a11y false positive. */
.gallery{
  margin-top:28px; display:grid; grid-template-columns:1fr; gap:12px;
}
.gallery .frame{
  width:100%; aspect-ratio:2/3;
}
.gallery .frame--land{aspect-ratio:3/2}

/* ---------------------------------------------------------------------
   6b — the gallery (§6b in index.html). ADDED 2026-08-20, TODO.md §0f #13.
   Distinct from .gallery, which is the stack inside a room dialog. Named
   .lgal so a later edit to one cannot silently move the other.

   HOW THE ROWS STAY FLUSH. Ten portraits and four landscapes is eighteen
   column-units — six clean rows of three at desktop (rewritten 2026-08-22; it
   read "nine and three" from an earlier, smaller set) — but only if every row
   agrees on a height. Two approaches were tried and thrown away before this
   one: a hand-computed 8/5 ratio on the landscape (wrong at every width but
   one, because the true value is (2col + gap) / 1.25col and both col and gap
   are clamp()ed), and taking the landscape out of flow so the portrait would
   size the row (a grid item's automatic minimum size still floors at the
   photograph's intrinsic height, so it kept sizing its own row anyway).

   What works is the other direction: let the LANDSCAPE size the row at its
   natural 3/2, and give every frame height:100% so the portraits stretch to
   whatever the row turns out to be. In a row of three portraits nothing is
   taller than the 4/5, so the row is 4/5 and height:100% is a no-op. No
   arithmetic, no breakpoint assumptions.

   INVARIANT: each landscape must be paired with exactly one portrait in its
   row, or the 15 units stop tiling. Reordering the twelve means re-checking
   that pairing — backend/verify-stay.py asserts flush rows so a bad reorder
   fails the suite rather than shipping.
   --------------------------------------------------------------------- */
.lgal{
  display:grid; gap:clamp(10px,1.4vw,16px);
  grid-template-columns:repeat(2,1fr);
  margin-top:clamp(8px,1.6vw,20px);
}
.lgal .frame{aspect-ratio:4/5; height:100%}
/* Below 900px there are two columns and a landscape takes both, so it sits on
   its own row with no portrait to agree with — it keeps its natural 3/2. */
.lgal .frame--land{grid-column:1 / -1; aspect-ratio:3/2}

@media (min-width:900px){
  .lgal{grid-template-columns:repeat(3,1fr)}
  .lgal .frame--land{grid-column:span 2}
}

.roomdlg__foot{
  margin-top:26px; padding-top:20px; border-top:1px solid var(--rule);
  display:flex; flex-wrap:wrap; gap:14px; align-items:center;
  justify-content:space-between;
}
.roomdlg__nav{display:flex; gap:8px}
.roomdlg__nav button{
  background:none; border:1px solid var(--rule); color:var(--bleu-profond);
  font:400 var(--fs-sc)/1 var(--font-text);
  font-variant-caps:all-small-caps; letter-spacing:var(--ls-sc);
  padding:11px 16px; cursor:pointer;
}
.roomdlg__nav button:hover{border-color:var(--bleu); color:var(--ink)}

/* ---------------------------------------------------------------------
   4 — the house
   --------------------------------------------------------------------- */
.houserow{
  display:grid; gap:clamp(20px,3vw,44px); align-items:center;
  grid-template-columns:1fr;
}
.houserow + .houserow{margin-top:clamp(56px,7vw,96px)}
@media (min-width:900px){
  .houserow{grid-template-columns:1.15fr .85fr}
  .houserow:nth-child(even) .houserow__media{order:2}
}
.houserow__text h3{margin-bottom:4px}

/* .vignettes deleted 2026-08-10 (Jeremy's call) along with the three stray
   setting photos it laid out in §4. Nothing else on the site used the class. */

/* full-bleed band */
.band{position:relative; background:var(--ink)}
.band .frame{aspect-ratio:4/5; max-height:78svh}
@media (min-width:760px){ .band .frame{aspect-ratio:21/9} }
.band__scrim{position:absolute; inset:0; background:var(--scrim); pointer-events:none}
.band__quote{
  position:absolute; inset:auto 0 0 0; padding:0 var(--pad-gutter) clamp(34px,5vw,64px);
  max-width:var(--maxw); margin-inline:auto;
}
.band__quote p{
  font:italic 400 var(--fs-quote)/1.34 var(--font-display);
  color:var(--chalk); max-width:22ch;
}
/* Desktop only: the quote centres in the photograph instead of sitting low
   (Grace's review). It is already left-aligned; this only moves it vertically.
   The mobile block below 760px is approved as-is and is left untouched — which
   is why this is scoped to the same breakpoint the band's aspect ratio uses.
   Centred text needs cover at mid-height, where the bottom-up --scrim has
   thinned out, so --scrim-left is layered in. That is not a re-tune: it is the
   same token, used for the same job, as the desktop hero. */
@media (min-width:760px){
  .band__scrim{background:var(--scrim-left),var(--scrim)}
  .band__quote{
    inset:0; display:flex; flex-direction:column; justify-content:center;
    padding-block:clamp(34px,5vw,64px);
  }
}

/* The reviews carousel, 2026-08-10 (Jeremy's call). It replaces the single
   pull quote inside the SAME .band__quote box, so the quote's position,
   measure and type are unchanged and inherited — only the advancing is new.
   The photograph is not part of the carousel and never moves.

   All four slides stay stacked in ONE grid cell, so the box is always as tall
   as the LONGEST quote and the arrows do not jump when a one-line slide comes
   round. That is why an off slide is `visibility:hidden` rather than
   `display:none`: display:none would drop it out of the grid's sizing. It
   still leaves the accessibility tree — visibility:hidden removes a subtree
   from AT the same as display:none does — so only the on-slide is announced.
   The `hidden` attribute in the markup is what gives the no-JS fallback its
   single quote; this rule re-homes what `hidden` does, it does not defeat it. */
.reviews__viewport{display:grid}
.reviews__slide{grid-area:1/1}
.reviews__slide[hidden]{display:block; visibility:hidden}
.reviews__by{
  margin-top:clamp(14px,1.6vw,20px);
  font:400 var(--fs-sc)/1.3 var(--font-text);
  font-variant-caps:all-small-caps; letter-spacing:var(--ls-eyebrow);
  color:var(--chalk);
}
.reviews__nav{display:flex; gap:8px; margin-top:clamp(18px,2.4vw,28px)}
/* display:flex above outranks the UA sheet's [hidden]{display:none}, so
   without this the arrows rendered with JS off — inert buttons, the one thing
   the no-JS fallback exists to avoid. Caught in verification 2026-08-10. */
.reviews__nav[hidden]{display:none}
/* Same white-on-photography values as .ph--light, which is the only other
   place in this stylesheet that has to sit type over a photograph. Not a new
   colour: the palette has no light neutral other than --chalk. */
.reviews__arrow{
  background:rgba(255,255,255,.13); border:1px solid rgba(255,255,255,.5);
  color:var(--chalk); font:400 var(--fs-ui)/1 var(--font-text);
  padding:10px 16px; cursor:pointer;
}
.reviews__arrow:hover{background:rgba(255,255,255,.24); border-color:var(--chalk)}

/* ---------------------------------------------------------------------
   5 — write to grace (the inquiry form)
   --------------------------------------------------------------------- */
.formwrap{
  max-width:660px; margin-inline:auto; margin-top:clamp(30px,4vw,46px);
  background:var(--papier); border:1px solid var(--rule);
  padding:clamp(24px,4vw,48px);
}
/* The three rules alone did not read as "there are multiple steps" — Grace's
   review. They stay (she liked the restraint); a plain worded count is set
   beside them and carries the meaning. No stepper component. */
.steps{
  display:flex; gap:12px; align-items:center;
  font:500 var(--fs-sc)/1 var(--font-text);
  font-variant-caps:all-small-caps; letter-spacing:var(--ls-eyebrow);
  color:var(--bleu-profond); margin-bottom:26px;   /* --bleu was 2.93:1 */
}
.steps__count{flex:0 0 auto; white-space:nowrap}
.steps__rules{flex:1 1 auto; display:flex; gap:8px; align-items:center}
.steps__rules span{flex:1; height:1px; background:var(--bleu-pale)}
.steps__rules span.is-on{background:var(--bleu-profond)}

.screen[hidden]{display:none}
/* .policy and .section--linen .policy deleted 2026-08-10 (Jeremy's call): the
   policy paragraph came off the page entirely and nothing else — including
   /details/ — uses the class. The copy survives in
   design/booking-platform.md §Screen 1; see TODO.md §0d. */
.field + .field{margin-top:18px}
.fieldrow{display:grid; gap:18px; grid-template-columns:1fr}
@media (min-width:560px){ .fieldrow{grid-template-columns:1fr 1fr} }
.field label{
  display:block; margin-bottom:7px;
  font:500 var(--fs-sc)/1.4 var(--font-text);
  font-variant-caps:all-small-caps; letter-spacing:var(--ls-eyebrow);
  color:var(--bleu-profond);
}
.field input,.field textarea{
  width:100%; font:400 var(--fs-body)/1.5 var(--font-text); color:var(--ink);
  background:var(--chalk); border:1px solid var(--rule); border-radius:1px;
  padding:12px 13px;
}
/* Safari only, and invisible to every check we run (2026-08-12, Jeremy's
   iPhone screenshot). WebKit gives input[type=date] an intrinsic width from its
   own shadow content and lets that beat width:100%, so check-in and check-out —
   the only two date inputs on the site — rendered wider than .formwrap and
   broke its right border, while #guests in the identical .field sat flush.
   Chromium at 390px measures all three at 300px inside a 350px card and sees
   nothing wrong. appearance:none drops the intrinsic sizing; min-width:0 stops
   min-width:auto from re-floring it; the box then sizes to its container
   instead of to its content, which is the actual requirement. */
.field input[type="date"]{
  -webkit-appearance:none; appearance:none;
  min-width:0; max-width:100%;
}
.field input[type="date"]::-webkit-date-and-time-value{
  width:100%; text-align:left;
}
/* The other half of the same problem, and the half that is engine-agnostic:
   .fieldrow is a grid and its items take min-width:auto, so a control wider
   than its column widens the COLUMN — the box escapes the card even though the
   card is the constraint. Jeremy's framing was exact: the issue is the input's
   width in relation to the form's bounding box, not the input's width. This
   pins the column to the card no matter what a control asks for. */
.fieldrow > .field{min-width:0}
.field input:focus,.field textarea:focus{border-color:var(--bleu); outline-offset:1px}
.field textarea{min-height:130px; resize:vertical}
.field small{display:block; margin-top:6px; font-size:var(--fs-caption); color:var(--ink-soft)}

.err{
  margin-top:16px; font-size:var(--fs-caption); color:var(--ink);
  background:rgba(138,151,166,.16); border-left:2px solid var(--bleu-profond);
  padding:11px 14px;
}
.err[hidden]{display:none}

/* Continue/Send sits right, Back stays left (Grace's review). With one button
   the row has no Back, so justify-content:flex-end puts it right on its own;
   with two, margin-inline-end:auto pushes Back back to the left. */
.formnav{display:flex; gap:12px; align-items:center; margin-top:26px; justify-content:flex-end}
.formnav .cta{flex:0 0 auto}
.formnav .cta--ghost{margin-inline-end:auto}
/* the right-pointing arrow on Continue */
.cta__arrow{margin-left:.6em; font-variant-caps:normal; line-height:1}

/* honeypot — off-screen but NOT display:none, and untabbable */
.hp{
  position:absolute; width:1px; height:1px; overflow:hidden;
  clip-path:inset(50%); white-space:nowrap;
}

.thanks{text-align:center; padding:clamp(14px,3vw,30px) 0}
.thanks img{width:74px; margin:0 auto 20px; opacity:.75}

.formnote{
  text-align:center; margin-top:22px;
  font-size:var(--fs-caption); color:var(--ink-soft);
}

/* ---------------------------------------------------------------------
   6 — details preview
   --------------------------------------------------------------------- */
.detailgrid{
  display:grid; gap:0; grid-template-columns:1fr;
  margin-top:clamp(28px,3.5vw,42px);
  border-top:1px solid var(--rule);
}
@media (min-width:760px){ .detailgrid{grid-template-columns:1fr 1fr; column-gap:clamp(30px,4vw,60px)} }
.detailrow{
  display:flex; gap:18px; justify-content:space-between; align-items:baseline;
  padding:15px 0; border-bottom:1px solid var(--rule);
}
.detailrow dt{
  font:500 var(--fs-sc)/1.4 var(--font-text);
  font-variant-caps:all-small-caps; letter-spacing:var(--ls-eyebrow);
  color:var(--bleu-profond); flex:0 0 auto;
}
.detailrow dd{font-size:var(--fs-caption); color:var(--ink-soft); text-align:right}

/* ---------------------------------------------------------------------
   7 — footer
   --------------------------------------------------------------------- */
.footer{position:relative; background:var(--linen); overflow:hidden; padding-block:clamp(56px,7vw,92px)}
.footer__rose{
  position:absolute; right:-9%; bottom:-24%;
  width:min(560px,86vw); opacity:.34; pointer-events:none;
  mix-blend-mode:multiply;
}
.footer__inner{position:relative; text-align:center}
.footer__mark{width:min(300px,68vw); margin:0 auto 26px}
.footer address{font-style:normal; font-size:var(--fs-caption); line-height:1.9; color:var(--ink-soft)}
.footer address a{color:var(--bleu-profond)}
.footer__legal{
  margin-top:28px; padding-top:20px; border-top:1px solid var(--rule);
  font-size:var(--fs-caption); color:var(--ink-soft);
}

/* ---------------------------------------------------------------------
   the floating card (spec §6)
   --------------------------------------------------------------------- */
.stickycard{
  position:fixed; z-index:55;
  background:var(--papier); border:1px solid var(--bleu);
  display:none;
  opacity:0; transition:opacity .24s ease, translate .24s ease,
                        display .24s allow-discrete;
}
.stickycard.is-in{display:block; opacity:1; translate:0 0}

/* MOBILE — a floating card, not a bottom bar. Grace's review 2026-08-06: the
   old full-bleed bar merged into the bottom edge of the viewport and she did
   not notice it was there. It is now inset from all three edges so the page
   shows through underneath it, and it carries a real drop shadow so it reads
   as sitting above the page rather than welded to it. Safe-area inset is kept
   and the JS that hides it over the footer is untouched. */
.stickycard{
  left:var(--pad-gutter); right:var(--pad-gutter);
  bottom:calc(16px + env(safe-area-inset-bottom));
  padding:11px 14px;
  box-shadow:0 10px 20px rgba(38,38,43,.13), 0 22px 54px rgba(38,38,43,.22);
  translate:0 calc(100% + 24px);
}
.stickycard.is-in{translate:0 0}
@starting-style{ .stickycard.is-in{opacity:0; translate:0 calc(100% + 24px)} }
.stickycard__inner{
  display:flex; align-items:center; gap:14px;
  max-width:var(--maxw); margin-inline:auto;
}

/* THE CTA ENVELOPE — v1.5, 2026-08-17; the ruled panel REMOVED same day.
   The first build wrapped the mark in a .stickycard__frame span carrying a 1px
   --rule border and a --papier ground, built from Jeremy's mockup. He rejected
   it: "I just want the cream envelope with its rounded corner and letters."
   The wrapper is gone from the markup and this rule is deliberately not
   replaced — the artwork already has its own outline stroke and rounded
   corners. DO NOT put a border, background or padding back around this mark. */
/* The whole mark-plus-label is one <a>, so the envelope itself is clickable.
   It is the flex row on mobile; the dismiss button is its only sibling. */
.stickycard__link{
  flex:1 1 auto; display:flex; align-items:center; gap:14px;
  text-decoration:none; color:inherit; min-width:0;
}
/* Width only — the height follows from the intrinsic 1500x816 attributes.
   Setting both would fight the ratio and squash the envelope. */
/* opacity was .92, inherited from the facade stand-in this replaced. Now 1:
   with the ruled panel gone the mark sits on the page directly, and .92 read as
   a washed-out cream rather than the colour Jeremy drew. */
.stickycard__art{display:block; width:76px; height:auto; opacity:1}
/* Mobile is the wordless mark; the wide one is desktop-only. Mirrors the
   .lockup--tight / .lockup--wide switch in the header. */
.stickycard__art--wide{display:none}
/* --bleu-profond, NOT --bleu, and this is a contrast decision — do not "match
   the mock" by reverting it. Jeremy's mock shows the label in the soft blue, but
   --bleu #8A97A6 on --papier measures 2.93:1 at this 16px size, well under the
   4.5:1 AA floor for normal text; the label was --ink before this pass, so
   moving it to --bleu would have been a regression I introduced. --bleu-profond
   is 5.16:1 and still reads as the same blue register rather than reverting to
   ink. Same substitution the nav already makes, for the same reason. */
.stickycard__title{
  font:400 var(--fs-ui)/1.2 var(--font-display); flex:1 1 auto;
  font-variant-caps:all-small-caps; letter-spacing:var(--ls-sc);
  color:var(--bleu-profond);
}
.stickycard__dismiss{
  flex:0 0 auto; width:28px; height:28px; border:1px solid var(--rule);
  border-radius:50%; background:var(--papier);
  color:var(--ink); font:400 17px/1 var(--font-text); cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  transition:background-color .18s ease, border-color .18s ease;
}
.stickycard__dismiss:hover{background:var(--linen); border-color:var(--bleu)}

/* DESKTOP — bottom-right corner, not right-edge-centred (Grace's review). */
@media (min-width:900px){
  /* THE CARD IS THE ARTWORK, 2026-08-17 · Jeremy: "remove the white border and
     the black thin line border. I just want the cream envelope."
     So at this breakpoint the .stickycard chrome is stripped to nothing — no
     --papier ground, no --bleu rule, no padding. What floats bottom-right is
     the drawn envelope and nothing else. The shadow moves onto the image as a
     drop-shadow filter so it follows the envelope's rounded corners instead of
     hugging a rectangle that is no longer painted.

     THE BREAKPOINTS DIFFER ON PURPOSE — Jeremy approved it 2026-08-17: "i like
     the mobile exactly as it is." Below 900px the card KEEPS its --papier
     ground, --bleu rule, padding and shadow, because the label beside the mark
     is live text that needs a ground over photography; only the box around the
     mark came off there. Do not harmonize the two. Stripping the mobile card is
     a regression; putting chrome back on desktop is what he rejected. */
  .stickycard{
    left:auto; right:26px; bottom:26px; top:auto;
    width:184px; padding:0;
    background:none; border:0; box-shadow:none;
    translate:0 22px;
  }
  .stickycard.is-in{translate:0 0}
  @starting-style{ .stickycard.is-in{opacity:0; translate:0 22px} }
  .stickycard__inner{flex-direction:column; text-align:center; gap:10px}
  .stickycard__link{flex-direction:column; width:100%; gap:0}
  .stickycard__art{
    width:100%; margin-inline:auto;
    filter:drop-shadow(0 10px 20px rgba(38,38,43,.16))
           drop-shadow(0 22px 48px rgba(38,38,43,.20));
  }
  .stickycard__art--wide{display:block}
  .stickycard__art--tight{display:none}
  /* THE DUPLICATE-WORDS FIX. The desktop art already says "Write to Grace", so
     the text link is taken out of the visual flow but kept in the accessibility
     tree — it stays a real, focusable link, which is what the dismiss button and
     keyboard users need. `gap:10px` would otherwise still reserve a row for it,
     hence the flex reset. The art's alt="Write to Grace" supplies the visible
     name; the two never both render. Reverting this without also blanking the
     art's alt would announce the phrase twice. */
  .stickycard__title{
    position:absolute; width:1px; height:1px; overflow:hidden;
    clip-path:inset(50%); white-space:nowrap; flex:0 0 auto;
  }
  /* Outside the padding box on desktop so it never sits on the title. */
  .stickycard__dismiss{position:absolute; top:-11px; right:-11px}
}

/* ---------------------------------------------------------------------
   motion (motion-spec.md)
   Everything here is additive: content is visible at rest. If the
   @supports fails or JS never runs, the page is a plain, complete page.
   --------------------------------------------------------------------- */
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    .rise{
      animation:rise-in linear both;
      animation-timeline:view();
      animation-range:entry 4% cover 26%;
    }
    .wipe{
      animation:clip-wipe linear both;
      animation-timeline:view();
      animation-range:entry 2% cover 30%;
    }
    .rooms .room:nth-child(2){animation-delay:0s; animation-range:entry 0% cover 32%}
    .rooms .room:nth-child(3){animation-range:entry -4% cover 34%}
    .draw{
      animation:rule-draw linear both;
      animation-timeline:view();
      animation-range:entry 10% cover 22%;
    }
  }
}
@keyframes rise-in{
  from{opacity:0; transform:translateY(26px)}
  to{opacity:1; transform:none}
}
@keyframes clip-wipe{
  from{clip-path:inset(14% 0 0 0); opacity:.55}
  to{clip-path:inset(0 0 0 0); opacity:1}
}
@keyframes rule-draw{ from{width:0} to{width:var(--rule-w)} }

/* hero: scale-and-settle, no parallax (it competes with the drone video
   that replaces this still) */
@media (prefers-reduced-motion:no-preference){
  .hero__media img{animation:settle 1400ms cubic-bezier(.2,.7,.3,1) both}
}
@keyframes settle{ from{scale:1.06} to{scale:1} }

/* Same-document view transition: the card photo morphs into the dialog
   hero. Names are assigned in JS so only one pair is live at a time. */
::view-transition-old(room-hero),
::view-transition-new(room-hero){
  animation-duration:.36s; mix-blend-mode:normal;
}

@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{
    animation-duration:.001ms !important; animation-iteration-count:1 !important;
    transition-duration:.001ms !important; scroll-behavior:auto !important;
  }
  .hero__media img{animation:none; scale:1}
  .room:hover img{scale:1}
  .rise,.wipe{opacity:1; transform:none; clip-path:none}
  .draw{width:var(--rule-w)}
  .stickycard{translate:none}
  .stickycard.is-in{translate:none}
}

/* off-screen sections are not laid out until needed */
/* .defer previously carried content-visibility:auto with a 900px intrinsic-size
   estimate. Removed 2026-08-06: the estimate is wrong for every section, so the
   document grew 6972 -> 9208px while an in-page scroll was in flight and every
   "Write to Grace" click landed ~2340px short of #write. The anchor is the whole
   conversion path; the perf win on an 8-section page with correctly-sized lazy
   images did not come close to paying for it. Do not reintroduce without
   per-section contain-intrinsic-size AND an anchor-landing test. */
.defer{}

.visually-hidden{
  position:absolute; width:1px; height:1px; overflow:hidden;
  clip-path:inset(50%); white-space:nowrap;
}

/* ---------------------------------------------------------------------
   /details/ shell
   --------------------------------------------------------------------- */
.page-head{padding-block:clamp(48px,7vw,96px) clamp(30px,4vw,48px); text-align:center}
/* The eyebrow and the h1 were touching — a measured 0px between the eyebrow's
   bottom edge and the h1's top edge at both 390 and 1280, which on /explore/
   read as a collision because that headline runs four lines on a phone. Set
   here rather than on .eyebrow so the in-page section eyebrows, which sit in
   left-aligned blocks with their own rhythm, are untouched. Applies to both
   /explore/ and /details/ — same block, and they should agree. */
.page-head .eyebrow{margin-bottom:clamp(14px,1.6vw,20px)}
/* /explore/'s banner CTA. The .lead above it is a centred --measure-lead block,
   so the button needs its own centring context rather than inheriting the
   block's text-align. */
.page-head__cta{margin-top:clamp(24px,3vw,34px)}
.dtable{width:100%; border-collapse:collapse; margin-top:20px}
.dtable th,.dtable td{
  text-align:left; padding:14px 0; border-bottom:1px solid var(--rule);
  vertical-align:top; font-size:var(--fs-caption); line-height:1.65;
}
.dtable th{
  width:38%; font:500 var(--fs-sc)/1.5 var(--font-text);
  font-variant-caps:all-small-caps; letter-spacing:var(--ls-eyebrow);
  color:var(--bleu-profond);
}
.dtable td{color:var(--ink-soft)}
/* /explore/ drive-time tables, added 2026-08-12. Same table, one column split.
   The 38% label column above is sized for "Check-in" and "Address"; a venue
   name is three times that ("Dienner's Country Restaurant") while the value it
   carries is four characters ("7 min"). At 390px the default split wraps every
   label to three lines and leaves the time column two-thirds empty. Giving the
   name the room and letting the time size to its content fixes both ends. */
.dtable--venues th{width:auto}
.dtable--venues td{width:1%; white-space:nowrap; padding-left:18px}
.amenities{
  margin-top:20px; display:grid; gap:10px 30px;
  grid-template-columns:1fr;
}
@media (min-width:640px){ .amenities{grid-template-columns:repeat(2,1fr)} }
@media (min-width:960px){ .amenities{grid-template-columns:repeat(3,1fr)} }
.amenities li{
  font-size:var(--fs-caption); color:var(--ink-soft);
  padding-bottom:9px; border-bottom:1px solid var(--rule);
}
.dblock + .dblock{margin-top:clamp(46px,6vw,78px)}

/* ---------------------------------------------------------------------
   APPENDED BY THE MARKUP PASS — four gaps the stylesheet did not cover.
   Nothing above this rule was altered.
   --------------------------------------------------------------------- */

/* <picture> is an inline wrapper the layout above does not expect: it breaks
   `.frame img{height:100%}` and `.hero__media img{height:100%}` by sitting
   between the sized box and the image. display:contents removes it from
   layout while keeping art direction. */
picture{display:contents}

/* The reset sets `img{max-width:100%}` but not `height:auto`, so every image
   that carries real width/height attributes (required for CLS) and is then
   sized by a CSS `width` kept its attribute height — .stickycard__art was
   1500px tall, .footer__mark and .footer__rose likewise. Every deliberate
   height in this stylesheet is set through a class selector, which still
   wins on specificity (.frame img, .hero__media img, .header__home img,
   .room__mark). */
img{height:auto}

/* A room card is a <button>, so its boxes have to be <span>s (button content
   model is phrasing content). Give them their block layout back. */
.room .frame,.room__body,.room__name,.room__note{display:block}

/* The sticky card's CTA is "a plain header, set beneath the facade"
   — it is an <a>, so it needs the link chrome removed.
   COLOUR MOVED 2026-08-17 (v1.5): the label is now small-capped --bleu, set
   alongside the envelope, and that declaration lives with the rest of the
   .stickycard block above. This rule sits ~180 lines later in the cascade, so
   leaving `color:var(--ink)` here would have silently won and the label would
   have stayed sentence-case ink. Only the link chrome belongs here now.

   THE <a> ALSO MOVED, same pass: it is .stickycard__link now, wrapping both the
   envelope and the label, because the art had to be inside the link to be
   clickable. .stickycard__title is therefore a plain <span> and the hover
   belongs on the link around it. */
.stickycard__link{text-decoration:none}
.stickycard__link:hover .stickycard__title{color:var(--bleu-profond)}

/* two spacing utilities: the "All the details →" link under .detailgrid,
   and centring a --measure-width block inside a centred section. */
.moreline{margin-top:26px}
.centred{margin-inline:auto}

/* `.field + .field{margin-top:18px}` also matches the two fields inside a
   .fieldrow grid, which staggered check-out below check-in. */
.fieldrow > .field + .field{margin-top:0}
