/* Cloud9 direct booking page.
   Built 1:1 against the Claude Design handoff bundle, project "Cloud9 Windy City Landing",
   file "Cloud9 Landing Page.dc.html", sha256 89ccb042..., plus the seven token files in
   _ds/c9wc-ds-1-0/tokens/. Numbers below are the design's own. The design carries NO media
   queries: the mobile layout IS the desktop layout at a narrower width, which is why almost
   every value here is a clamp or an auto-fit rather than a breakpoint.

   WHERE THIS PAGE IS DELIBERATELY NOT 1:1, AND WHY. Read this before "fixing" any of it.

   1. LONG-STAYS TRACK is minmax(min(100%,300px),1fr), not minmax(300px,1fr).
      The design's own track overflows its card by 37px at 375. Same intent, no overflow.
   2. FOOTER a:hover KEEPS colour --text-inverse. The design's hover colour is
      --text-link-hover, terracotta-900, which fails contrast on the forest ground. The
      underline and 3px offset the design pairs with it ARE built, on both header and footer.
      The header keeps the design's colour too: terracotta-900 over --glass-light passes.
   3. GUESTY WIDGET INTERIOR is unreachable. We own two parameters, siteUrl and color, and the
      card around it. Nothing inside is ours to match.
   4. AMENITY LIST CONTENT and the neighbourhood text are the verified factual record, not the
      design's array. The design still carries towels, unqualified fast Wi-Fi, a vary-by-building
      hedge and "parking options". A layout spec does not overrule a factual correction.
      Marcus's standing authorisation. See the provenance comment in index.html.
   5. --text-muted is remapped from sand-600 to sand-700. The system's own alias computes
      4.20:1 on its own page surface, below the 4.5:1 body floor. sand-700 is 7.29:1 and is
      still the system's own value, so the ramp stays coherent.
   6. --accent stays #a54d26 where the design says --text-accent, terracotta-700 #a24f27.
      #a54d26 is the Guesty widget's own colour parameter, is already contrast-asserted by
      qa.py, and the two differ by three channel values. One source of truth beats a
      near-duplicate token that differs invisibly.
   7. PHOTO HOVER is .apt:hover img, not img:hover. Identical hit area, and it cannot flicker
      when the scaling image moves under a stationary cursor.
   8. ONE LAYOUT BREAKPOINT AT 768, where the design has none. Below it the photo figures stack
      full width and the panorama is dropped. Ruled by Marcus. The design's 12-column grid
      degenerates on a phone: a track is 3.9px at 375 and the portrait figure renders 88px wide.
   9. FONT-DISPLAY IS FALLBACK, where the design uses swap. Ruled by Marcus, speed first. The
      reasoning is at the @font-face block and it is load-bearing: do not flip it back to swap
      in the name of 1:1.

   NOT a deviation, recorded because it was raised as one: the design's base.css does give every
   <a> a 35% text-link border-bottom, but the page's own <style> block overrides it with
   border-bottom:0 after base.css and wins on order. No anchor in the composed design carries a
   hairline. The prose-only scoping below matches the design; it does not depart from it.

   No build step, no framework, no JavaScript of ours. */

/* SELF-HOSTED BRAND FACES, TWO VARIABLE FILES. No fonts.googleapis.com, no fonts.gstatic.com,
   no third-party FONT connection of any kind. The PAGE now carries third-party connections, all
   from one host, googletagmanager.com: gtag/js and gtm.js in <head> and the ns.html noscript
   iframe after <body>. The GTM container can load further hosts at runtime that appear nowhere
   in this file or in index.html, so treat this sentence as a statement about FONTS and about the
   declared source only. The design's own tokens/fonts.css says the Google
   Fonts import is a stopgap and asks for licensed binaries with local @font-face; this IS that
   replacement, so it is not a deviation from the design, it is the instruction the design gave.

   WHY VARIABLE AND NOT THE FOUR STATIC FILES WE SHIPPED BEFORE. Newsreader carries an OPTICAL
   SIZE axis, opsz 6..72, and the design loads the full range. The four files we had reported no
   fvar table at all and the Newsreader pair named themselves "Newsreader 16pt Light" and
   "16pt Regular": static instances with opsz frozen at 16, the TEXT cut. h1 is 76px. Every
   visitor was reading the 16pt text cut blown up to display size instead of the 72pt display
   cut, which is a different drawing, not a different size. Re-requesting the RANGE from css2
   with a modern UA returns one file per family carrying real axes.

   BOTH FILES ARE THEN SLICED with fontTools instancer to the weights this page uses, because
   Google honours the opsz range but ships the whole wght range regardless of what you asked
   for. Newsreader: wght clamped 300..400, OPSZ 6..72 LEFT INTACT, 131,848B to 91,108B.
   Jost: wght clamped 300..500, 26,588B to 23,624B. Four static files were 65,412B; two
   variable files are 114,732B. The +49,320B buys the display cut at h1, the text cut at body
   sizes, and Jost 300.

   JOST 300 WAS MISSING ENTIRELY. --text-lead is var(--fw-light) over var(--font-sans), so .sub
   and the guarantee paragraph both ask for Jost 300. We shipped 400 and 500 only, and browsers
   do not synthesise a lighter weight, so both rendered 400. Silent, and it looked like taste.
   One variable file spanning 300..500 closes it.

   font-display:FALLBACK. NOT swap, and NOT 1:1 with the design, which uses swap. Marcus ruled
   speed first. DO NOT "RESTORE 1:1" BY FLIPPING THIS BACK TO SWAP; that would reintroduce the
   defect described here.
   WHY. Under `swap` the measured CLS was 0 only CONTINGENTLY: it depended on where the headline
   happened to wrap, and the worst case in a 40px band around 1120px viewport was 0.09552. A zero
   that holds because a line break lands favourably is luck, not a property. `fallback` gives a
   swap window of roughly 3 seconds, and Newsreader was measured landing at 8.5 seconds on a
   throttled connection, so on a slow link the swap CANNOT happen and the shift becomes
   structurally impossible rather than fortunate. On a fast link the face still arrives inside
   the window and the visitor sees the brand type immediately, which is what `optional` gave up.
   `fallback` is the only one of the three that gets both.
   NO metric overrides accompany this, deliberately: ascent-override and its siblings only
   neutralise a swap if they reconcile against the font the visitor ACTUALLY falls back to, and
   our fallbacks are Georgia, Century Gothic, Avenir Next and system-ui, so any single number
   would be right on one machine and wrong on the rest while claiming to be a guarantee. */
@font-face{font-family:"Newsreader";font-style:normal;font-weight:300 400;font-display:fallback;src:url(/fonts/newsreader-var.woff2) format("woff2")}
@font-face{font-family:"Jost";font-style:normal;font-weight:300 500;font-display:fallback;src:url(/fonts/jost-var.woff2) format("woff2")}


:root{
  /* Brand core, sampled from the supplied logo files */
  --forest-800:#022328;--forest-700:#022e33;--forest-panel:#03383e;--forest-600:#0a4148;--forest-500:#14565e;
  --forest-200:#a8cdd0;
  --terracotta-100:#f7e6db;

  /* Warm neutrals */
  --sand-100:#f7f4ef;--sand-200:#efeae2;--sand-300:#e2dbd0;--sand-500:#a49b8d;
  --sand-700:#57503f;--sand-800:#33302a;
  --white:#ffffff;

  --brass-400:#c9a86a;  /* forest grounds ONLY. 2.06:1 on parchment, forbidden on light. */

  /* Semantic surfaces */
  --surface-page:var(--sand-100);
  --surface-card:var(--white);
  --surface-sunken:var(--sand-200);
  --surface-inverse:var(--forest-700);
  --surface-inverse-raised:var(--forest-600);
  /* Header glass and the image scrim, both verbatim from tokens/colors.css. The header used to
     carry these as literals. */
  --glass-light:rgba(252,250,247,.72);
  --blur-glass:saturate(1.1) blur(14px);
  --scrim-image-top:linear-gradient(180deg,rgba(1,26,29,.55) 0%,rgba(1,26,29,0) 55%);

  /* Semantic text. Every pair below is asserted >= 4.5:1 by qa.py. */
  --text-heading:var(--forest-700);   /* 13.28:1 on parchment */
  --text-body:var(--sand-800);        /* 11.99:1 on parchment */
  --text-muted:var(--sand-700);       /*  7.29:1 on parchment. NOT sand-600, see note above. */
  --text-inverse:var(--sand-100);     /* 13.28:1 on forest, 11.68:1 on the guarantee panel */
  --text-inverse-muted:var(--forest-200); /* 8.54:1 on forest */
  /* --text-faint paints exactly two aria-hidden decorative icons, the neighbourhood map pin and
     the long-stays mail glyph, and NEVER text. That is why it carries no contrast row: 1.4.3 is
     about text and 1.4.11 exempts purely decorative graphics. If it is ever put on a word, it
     needs a row in qa.py's PAIRS first. */
  --text-faint:var(--sand-500);

  /* Accent is the Guesty widget's own color parameter and is NOT ours to choose.
     The design system's terracotta-700 is #a24f27, three channel values away and visually
     identical. Reconciling belongs in Guesty's Booking Engine design settings, not here.
     If that changes, the widget snippet must be re-copied. See README. */
  --accent:#a54d26;                   /* 5.69:1 on white, 5.19:1 on parchment */
  --accent-hover:#5c2c15;             /* 10.48:1 on parchment. The design's --text-link-hover,
                                         terracotta-900. TEXT LINK hover only. */
  /* DELIBERATELY UNUSED, like --forest-panel. tokens/colors.css keeps two hover roles apart:
     --text-link-hover is terracotta-900 and --action-accent-bg-hover is terracotta-800. We had
     collapsed both onto one token. This restores the second role. NOTHING REFERENCES IT TODAY,
     because the page has no accent button of ours: the only button is inside the Guesty widget
     and is unreachable. Declared so the next accent action takes the right value instead of
     reaching for the link colour. Dead ON PURPOSE and documented, not an oversight. */
  --action-accent-bg-hover:#7a3b1d;

  --border-hairline:var(--sand-300);
  --border-inverse:var(--forest-500);
  --border-focus:var(--forest-500);

  /* Type. Display is the system's declared serif fallback, sans its declared stack. */
  /* Verbatim from tokens/typography.css, project C9WC-DS-1.0. The fallback halves were drifted
     too, not just the missing faces: ours read system-ui,-apple-system,"Segoe UI",Roboto,... */
  --font-display:"Newsreader",Georgia,"Times New Roman",serif;
  --font-sans:"Jost","Century Gothic","Avenir Next",system-ui,sans-serif;
  --fw-light:300;--fw-regular:400;--fw-medium:500;

  --size-body:17px;--lh-body:1.62;
  --size-title-2:23px;--size-lead:20px;--lh-lead:1.6;
  --size-small:14px;--lh-small:1.55;--size-caption:12px;--lh-caption:1.45;
  --lh-display-3:1.12;--lh-title-1:1.2;--lh-title-2:1.28;--track-display:-.02em;--track-display-xl:-.032em;--track-title:-.01em;

  /* Spacing */
  --space-2:8px;--space-3:12px;--space-4:16px;--space-5:24px;--space-6:32px;--space-7:48px;
  --space-8:64px;--space-9:96px;
  --page-max:1240px;--page-pad:32px;
  --radius-lg:14px;
  /* Motion, from tokens/motion.css. --dur-scenic is zeroed under prefers-reduced-motion below,
     which is the design system's own mechanism and the only one the photo strip needs.
     --section-gap and --hairline-inverse are RETIRED. Every section now carries the design's own
     clamp, and the footer rule is --border-inverse, which is the design's value. */
  --ease-standard:cubic-bezier(.2,.6,.2,1);--dur-scenic:900ms;
  --transition-image:transform var(--dur-scenic) var(--ease-standard);
  --shadow-hairline:0 0 0 1px var(--border-hairline);
  --shadow-focus:0 0 0 2px var(--surface-page),0 0 0 4px var(--border-focus);
}
@media (prefers-reduced-motion:reduce){:root{--dur-scenic:0ms}}

*,*::before,*::after{box-sizing:border-box}
/* A1. Guesty's widget CSS injects `body{background:transparent}` into head at runtime. It lands
   after this file at equal specificity, so it wins and the canvas paints white. Painting the page
   surface on html instead puts it out of their reach. They target only body. No !important needed.
   qa.py fetches their stylesheet and fails if a second bare element selector ever appears. */
html{background:var(--surface-page);-webkit-text-size-adjust:100%}
body{
  margin:0;background:var(--surface-page);color:var(--text-body);
  font:var(--fw-regular) var(--size-body)/var(--lh-body) var(--font-sans);
  -webkit-font-smoothing:antialiased;
}
h1,h2,h3{margin:0;color:var(--text-heading);font-family:var(--font-display);font-weight:var(--fw-regular);letter-spacing:var(--track-display)}
p{margin:0;text-wrap:pretty}
a{color:var(--accent);text-decoration:none}
a:hover{color:var(--accent-hover)}
/* The design pairs its hover colour with an underline on these two surfaces only, base.css
   companion rule: `footer a:hover, header a:hover`. Built on both. The FOOTER's colour is the
   deviation, not the underline: terracotta-900 on forest fails contrast, on the header's glass
   over parchment it passes. Deviation 2 in the register at the top of this file. */
.site-header a:hover,.site-footer a:hover{text-decoration:underline;text-underline-offset:3px}
.site-footer a:hover{color:var(--text-inverse)}
/* The persistent rule belongs to PROSE links only, where a link sits inside a sentence and has
   to be findable without hovering. Nav, the header phone, the long-stays pair and the footer
   are not prose and the design gives them no rule. Never setting the border globally clears it
   everywhere by construction, which beats clearing it again on five selectors that each have
   to be remembered. It also retires .wordmark{border-bottom:0}, which existed only to fight it. */
.guarantee a,noscript a,.prose a{border-bottom:1px solid color-mix(in oklab,currentColor 35%,transparent)}
.guarantee a:hover,noscript a:hover,.prose a:hover{border-bottom-color:currentColor}
:focus-visible{outline:2px solid var(--border-focus);outline-offset:2px;box-shadow:var(--shadow-focus)}
input:focus-visible,select:focus-visible{box-shadow:inset 0 0 0 1.5px var(--border-focus)}
::selection{background:var(--terracotta-100);color:var(--forest-800)}
.wrap{max-width:var(--page-max);margin:0 auto;padding:0 var(--page-pad)}
.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* 1. HEADER */
.site-header{position:sticky;top:0;z-index:40;background:var(--glass-light);backdrop-filter:var(--blur-glass);-webkit-backdrop-filter:var(--blur-glass);border-bottom:1px solid var(--border-hairline)}
.header-row{display:flex;align-items:center;justify-content:space-between;gap:var(--space-5);height:72px}
.wordmark{display:flex;align-items:center;border:0}
.wordmark img{display:block;height:22px;width:auto}
.phone{display:inline-flex;align-items:center;gap:var(--space-2);font-size:var(--size-small);line-height:var(--lh-small);font-weight:var(--fw-medium);letter-spacing:.02em;color:var(--text-heading);font-variant-numeric:tabular-nums;text-decoration:none;white-space:nowrap}
.phone-icon{flex:0 0 auto}
/* Touch target floor. Not from the design, which sets no minimum: 44px is the 2.5.5 target size
   and these three are the only anchors OF OURS small enough to miss it. The header is 72px tall,
   so a 44px child changes no layout.
   KNOWN EXCEPTION THIS RULE CANNOT REACH: the Guesty submit button renders 40px tall at 375,
   four pixels under the floor. It is inside the widget, whose interior is ruled unreachable, and
   we own two parameters there and nothing else. Restyling it is out of bounds, so this is
   recorded rather than fixed. If 2.5.5 has to hold across the whole page, that is a conversation
   with Guesty about their Booking Engine, not a selector here. */
@media (max-width:759px){
  .phone,.long-stays a,.footer-col a,.footer-legal a{display:inline-flex;align-items:center;min-height:44px}
}

/* FOCUS REPAIR. Repair, not restyling.
   Guesty's stylesheet loads after ours and carries five `outline:none` rules that strip the focus
   indicator from controls we cannot otherwise reach, including the Search button. STEP 2 of the brief
   lists "usable by keyboard alone" as not optional.
   THE PRIMARY INDICATOR IS box-shadow, via the design's own --shadow-focus on :focus-visible above.
   `outline:none` does NOT remove a box-shadow, and all five of their rules target outline only, so the
   design's mechanism is immune to them and needs NO specificity fight. That is also why --shadow-focus
   existed in this file unused while a substitute outline did the work: the substitute was the one
   mechanism they disable.
   The rule below adds the outline back purely for FORCED-COLORS and high-contrast modes, where
   box-shadow is dropped and outline survives. Only this half needs to beat their (0,3,0), so it is
   anchored on .widget-card to reach (0,4,0). No !important. Nothing of theirs is restyled: no colour,
   radius or dimension. Neither property affects layout, so CLS and the fold budget are untouched.
   qa.py snapshots their outline:none selector set, so a rename fails the run instead of silently
   un-repairing these controls. */
.widget-card .guesty-root-element .guesty-search-submit-btn:focus-visible,
.widget-card .guesty-root-element .selectr-selected:focus-visible,
.widget-card .guesty-root-element .lightpick__previous-action:focus-visible,
.widget-card .guesty-root-element .lightpick__next-action:focus-visible,
.widget-card .guesty-root-element .lightpick__close-action:focus-visible{
  outline:2px solid var(--border-focus);outline-offset:2px}

/* 2. ABOVE THE FOLD */
.hero{padding:clamp(22px,6vw,72px) 0 clamp(40px,6vw,64px)}
/* The hero section is a two-child grid, copy then widget. The old margin-bottom is retired.
   THE OLD "DESIGNATED FIRST REVERT" NOTE IS RETIRED TOO: it named var(--space-4) as the fold
   escape hatch on a flat var(--space-5) gap. The design's gap is a clamp that already resolves
   to 14px at 375, two pixels UNDER the escape hatch, so there is nothing left to revert to. */
.hero .wrap{display:grid;gap:clamp(20px,4vw,44px)}
.hero-copy{display:grid;gap:clamp(14px,2.4vw,24px);max-width:780px}
h1{font-size:clamp(34px,6.4vw,76px);line-height:1.04;letter-spacing:var(--track-display-xl);font-weight:var(--fw-light);text-wrap:pretty}
.sub{font-size:clamp(17px,2.2vw,var(--size-lead));line-height:var(--lh-lead);font-weight:var(--fw-light);color:var(--text-body);max-width:620px}
.proof-line{display:flex;align-items:flex-start;gap:var(--space-2);font-size:var(--size-small);line-height:var(--lh-small);color:var(--text-muted);font-variant-numeric:tabular-nums}
/* The star inherits --text-muted from the paragraph, as the design has it. The 4px nudge is the
   design's own: it optically seats a 15px glyph against a 14px first line. */
.star-slot{line-height:0;flex:0 0 auto;margin-top:4px}
.widget-card{background:var(--surface-card);border-radius:var(--radius-lg);box-shadow:var(--shadow-hairline);padding:clamp(16px,2.5vw,24px);max-width:var(--page-max)}
/* SUBMIT BUTTON COPY, RECORDED AND NOT BUILT. Marcus ruled the word stays Guesty's: "search is
   fine unless i change it in guesty", so it is a Booking Engine console change if he ever wants
   it, not a CSS one. Recorded here because the safe route was measured and would otherwise be
   lost.
   THE SAFE ROUTE IS APPENDING, NOT REPLACING. Replacing breaks the accessible name and it was
   measured breaking: font-size:0 plus ::after yields the name "SearchSearch availability", and
   text-indent:-9999px yields "Search Search availability", because font-size:0 does not remove a
   text node from the accessibility tree and generated content IS counted toward name-from-content.
   Appending works for the same reason it breaks replacing: keep the original text and add
   ::after{content:" availability"} and the computed name is the single clean "Search
   availability". Their hard-coded width:117px overflows and clears with width:auto, (0,2,0), no
   important needed.
   OPEN IF EVER BUILT: the AX tree is verified, one VoiceOver pass is not. This is the primary
   conversion control on the page, so that pass is owed before it ships.
   The general rule this produced: generated content with NO TEXT is safe, generated content that
   REPLACES text is not. Drawing an icon with content:"" was never the risk. */
/* Widget height is RESERVED so the page does not shift when the Guesty script loads.
   MEASURED against the real widget inside this card. Re-measure if the layout changes. */
.widget-slot{min-height:280px}

/* 3. OFFER, on the inverse forest ground */
.offer{background:var(--surface-inverse);color:var(--text-inverse)}
/* The offer section is a three-child grid: head, list, guarantee. The old h2 margin-bottom and
   guarantee margin-top are retired in favour of the design's single gap. */
.offer .wrap{padding-top:clamp(56px,7vw,96px);padding-bottom:clamp(56px,7vw,96px);display:grid;gap:clamp(32px,4vw,48px)}
/* The design's .c9-eyebrow carries NO margin. Every block that holds one is a grid or a flex
   column with its own gap, so a margin here would double-space all five of them. */
.c9-eyebrow{display:block;font-family:var(--font-sans);font-size:11px;line-height:1.2;font-weight:var(--fw-medium);letter-spacing:.22em;text-transform:uppercase;color:var(--text-muted);margin:0}
.offer-head{display:grid;gap:var(--space-4);max-width:620px}
.offer .c9-eyebrow{color:var(--brass-400)}
.offer h2{color:var(--text-inverse);font-size:clamp(28px,4vw,40px);line-height:var(--lh-display-3)}
/* auto-fit, NOT a hardcoded count. This replaces BOTH a base `1fr` and a
   @media(min-width:900px){repeat(3,1fr)}. The media query was already giving three columns on
   desktop, so the visible defect was the 760px to 900px band, where the design wraps to two
   columns and we stayed at one. The hardcoded count had to go regardless. */
.offer-list{margin:0;padding:0;list-style:none;display:grid;gap:var(--space-6) var(--space-7);grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}
.offer-list li{display:grid;gap:var(--space-3);padding-top:var(--space-4);border-top:1px solid var(--border-inverse);color:var(--text-inverse-muted)}
/* Was a <strong>. It is a heading: three sibling titles under the section h2, and the design
   marks them up as h3. Heading order is now 1,2,3,3,3,2,2,2 and skips no level. */
.offer-list h3{color:var(--text-inverse);font-family:var(--font-display);font-weight:var(--fw-regular);font-size:var(--size-title-2);line-height:var(--lh-title-2);letter-spacing:var(--track-title)}

.guarantee{padding:clamp(20px,3vw,28px);border:1px solid var(--border-inverse);border-radius:var(--radius-lg);background:var(--surface-inverse-raised);max-width:860px}
.guarantee p{font-size:clamp(17px,2vw,var(--size-lead));line-height:var(--lh-lead);font-weight:var(--fw-light);color:var(--text-inverse)}

.offer-icon{color:var(--brass-400);display:block;margin-bottom:var(--space-2)}
.guarantee{display:flex;align-items:flex-start;gap:var(--space-4)}
.guarantee-icon{color:var(--brass-400);flex:0 0 auto;margin-top:4px}

/* 4. PROOF STRIP */
.proof{padding:clamp(48px,6vw,72px) 0 0}
/* The rules are on the CONTAINER, top and bottom. The per-cell border-left this file used to
   carry was a build invention: the design's strip has no cell borders at all, and under
   min(100%,120px) tracks the left borders land mid-wrap. Removed. */
.proof-strip{margin:0;padding:clamp(24px,3vw,32px) 0;list-style:none;display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,120px),1fr));gap:clamp(24px,3vw,32px) var(--space-5);border-top:1px solid var(--border-hairline);border-bottom:1px solid var(--border-hairline)}
.proof-strip li{display:flex;flex-direction:column;gap:var(--space-2)}
.proof-strip .fact{font-family:var(--font-display);font-weight:var(--fw-regular);font-size:40px;line-height:var(--lh-display-3);letter-spacing:var(--track-display);color:var(--text-heading);font-variant-numeric:tabular-nums}

/* 4b. THE APARTMENTS.
   A TWELVE-COLUMN GRID WITH NO BREAKPOINT, which is what the design composes and what its own
   Mobile Preview artboard renders at 375, 390 and 768. Built as composed, deliberately.
   Consequence, stated because it is real and it is a design decision rather than a build one:
   at 375 the content box is 311px, eleven gaps take 264px of it, so a track is 3.9px. The wide
   figure lands 199x133 CSS px and the tall figure lands 88x117. No overflow, because the
   figures are overflow:hidden so their automatic minimum size is zero and the tracks collapse
   cleanly, but the tall figure is a postage stamp on a phone. Surfaced to Marcus. If he rules
   for a mobile stack it is one media query here and nothing else moves. */
.apartments{padding:clamp(40px,5vw,64px) 0 0}
.apt-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--space-5)}
.apt{margin:0;position:relative;overflow:hidden;border-radius:var(--radius-lg);background:var(--surface-sunken)}
.apt img{width:100%;height:100%;object-fit:cover;display:block;transition:var(--transition-image)}
/* On the figure, not the image: identical hit area, and it cannot flicker when the scaling
   image moves out from under a stationary cursor. --transition-image resolves through
   --dur-scenic, which prefers-reduced-motion zeroes at the top of this file. */
.apt:hover img{transform:scale(1.04)}
.apt-wide{grid-column:span 8;aspect-ratio:3/2}
.apt-tall{grid-column:span 4;aspect-ratio:3/4}
.apt-tall img{object-position:62% center}
.apt-pano{grid-column:span 12;aspect-ratio:21/9}
.apt-pano img{object-position:center 42%}
.apt-scrim{position:absolute;inset:0;pointer-events:none;background:var(--scrim-image-top)}
/* MOBILE STACK, and the ONLY breakpoint on this page that changes layout. The design has none and
   this is a ruled deviation, number 8 in the register at the top of this file.
   Below 768 the 12-column grid degenerates: eleven 24px gaps eat 264px of a 311px content box, so
   a track is 3.9px and the portrait figure renders 88px wide. Full-width stacking is the fix.
   THE PANORAMA IS THE ONE DROPPED, and that is a judgment worth stating. At 311px a 21/9 frame is
   133px tall, a letterbox sliver that shows nothing. The other two are 311x207 and 311x415, and a
   tall portrait is the frame that actually works on a phone. ONE-LINE REVERT if the panorama is
   preferred over the portrait: swap .apt-pano for .apt-tall in the display:none rule below.
   display:none, NOT visibility or width:0, and it genuinely prevents the fetch: all three images
   are loading=lazy, a display:none element never gets a box, so it never intersects the viewport
   and the browser never requests it. The bytes are not merely hidden, they are not spent. */
@media (max-width:767px){
  .apt-wide,.apt-tall{grid-column:span 12}
  .apt-pano{display:none}
}

/* 5 and 6. CARD SECTIONS */
.panel{background:var(--surface-card);border-radius:var(--radius-lg);box-shadow:var(--shadow-hairline)}
.what-you-get{padding:clamp(48px,6vw,72px) 0 0}
.long-stays{padding:clamp(48px,6vw,96px) 0 clamp(56px,7vw,96px)}
.long-stays h2{font-size:clamp(24px,3vw,30px);line-height:var(--lh-title-1);letter-spacing:var(--track-title)}
/* T-13 items 1 and 2. The heading block is a SIBLING of the card, on parchment. The card holds
   ONLY the list. The section is a 3-child grid: heading block, card, neighbourhood line.
   Numbers from the composition's what-you-get section in project Cloud9 Windy City Landing:
   section gap clamp(28px,4vw,40px), head grid gap var(--space-3), head max-width 620px. */
.what-you-get .wrap{display:grid;gap:clamp(28px,4vw,40px)}
.wyg-head{display:grid;gap:var(--space-3);max-width:620px}
.what-you-get h2{font-size:clamp(26px,3.6vw,40px);line-height:var(--lh-display-3);letter-spacing:var(--track-display);margin:0}
/* Scoped to THIS card only. The shared .panel keeps clamp(20px,3.5vw,40px), so long stays is
   untouched. This is the composition's own card padding, and it is what lands the auto-fit
   below where the design lands it rather than 8px late. */
.what-you-get .panel{padding:clamp(20px,3vw,32px)}
/* Neighbourhood line, third grid child. Ruled final text: Streeterville is CUT, no evidence of
   any kind. Grant Park is NOT added: it is evidenced, but adding it is new copy and Marcus's
   call. The pin is now the design's --text-faint, 17px. It is aria-hidden and decorative, so it
   carries no contrast obligation and no qa.py row; the sentence beside it is --text-muted and
   is covered. */
.neighborhoods{display:flex;align-items:center;gap:var(--space-3);padding-top:var(--space-4);border-top:1px solid var(--border-hairline);color:var(--text-muted)}
.nb-icon{color:var(--text-faint);flex:0 0 auto}
.long-stays p{color:var(--text-muted)}
/* min(100%,300px), NOT a flat 300px, and this is deviation 1 in the register at the top: the
   design's own minmax(300px,1fr) overflows its card by 37px at 375. Same intent, no overflow.
   It also replaces a @media(min-width:780px){1.4fr 1fr}, which was a hardcoded count. */
.long-stays .panel{padding:clamp(24px,4vw,48px);display:grid;gap:clamp(24px,3vw,40px);grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr));align-items:center}
.ls-copy{display:grid;gap:var(--space-4)}
.ls-contact{display:grid;gap:var(--space-4);justify-items:start}
.ls-phone{font-family:var(--font-display);font-size:var(--size-title-2);color:var(--text-heading);font-variant-numeric:tabular-nums;display:inline-flex;align-items:center;gap:var(--space-3)}
.ls-mail{display:inline-flex;align-items:center;gap:var(--space-3);color:var(--text-body)}
/* Phone glyph takes the accent, mail glyph takes --text-faint. Both aria-hidden, so neither
   needs a contrast row. The design says --text-accent for the phone; ours stays --accent,
   deviation 6 in the register. */
.ls-icon{flex:0 0 auto}
.ls-phone .ls-icon{color:var(--accent)}
.ls-mail .ls-icon{color:var(--text-faint)}
/* min(100%,...) so a 320px viewport cannot overflow a 280px track. */
.amenities{margin:0;padding:0;list-style:none;display:grid;gap:var(--space-4) var(--space-6);grid-template-columns:repeat(auto-fit,minmax(min(100%,280px),1fr))}
/* AmenityList.jsx sets the li to `color: fg` (--text-body) and passes `muted` ONLY to the Icon.
   Ours had both muted, because the svg inherits currentColor from the li. Split them. */
.amenities li{color:var(--text-body);display:flex;align-items:center;gap:var(--space-3)}
/* Centred, so the optical nudge that flex-start needed is retired. Icon is 18px, the size
   AmenityList.jsx passes. */
.amenity-check{color:var(--text-muted);flex:0 0 auto}

/* 7. FOOTER
   STRUCTURE AND SPACING are the composition's, not SiteFooter.jsx's: the composed page is the
   authority and it lays the footer out inline. THREE auto-fit columns, brand / Book / Contact,
   each headed by a brass eyebrow. The earlier one-column ruling is superseded by 1:1.
   CONTENT IS STILL NOT THE DESIGN'S. Marcus's description stands, the LLC line stands, and the
   link set is still four real destinations: the design's own dead links are not restored, they
   are only rearranged into the two columns the design uses. No social icons, and the design has
   none now either. */
.site-footer{background:var(--surface-inverse);color:var(--text-inverse);padding:clamp(48px,6vw,72px) 0 var(--space-6)}
.site-footer .wrap{display:flex;flex-direction:column;gap:clamp(32px,4vw,48px)}
.footer-top{display:grid;gap:clamp(28px,4vw,48px);grid-template-columns:repeat(auto-fit,minmax(min(100%,260px),1fr))}
.footer-brand{display:flex;flex-direction:column;gap:var(--space-4);align-items:flex-start}
.site-footer .c9-eyebrow{color:var(--brass-400)}
/* STOPGAP. No reversed wordmark file exists, so the forest wordmark is knocked out to parchment.
   This is the design system's OWN fallback for variant "wordmark-light" in Logo.jsx, copied
   deliberately rather than invented. Replace it the moment a real reversed file is supplied. */
.footer-brand img{display:block;height:24px;width:auto;filter:brightness(0) invert(1) opacity(.92)}
.footer-desc{font-size:var(--size-small);line-height:var(--lh-small);color:var(--text-inverse-muted);max-width:300px}
.footer-col{display:flex;flex-direction:column;gap:var(--space-3);align-items:flex-start}
.footer-col a{width:fit-content;font-size:var(--size-small);line-height:var(--lh-small);color:var(--text-inverse-muted)}
.footer-tel{font-variant-numeric:tabular-nums}
/* --hairline-inverse IS RETIRED, and it goes dead a second time by doing so. It was revived last
   round to hold rgba(242,237,229,.18) for exactly this rule; the design's rule is
   --border-inverse, forest-500, which is a real token already carried for the offer list borders.
   Two dead tokens remain, both deliberate and both documented: --forest-panel and
   --action-accent-bg-hover. */
/* FLEX ROW, not a fourth footer column, and that is arithmetic rather than taste.
   .footer-top is repeat(auto-fit,minmax(min(100%,260px),1fr)) with gap clamp(28px,4vw,48px), and
   page-max 1240 minus 64 of page-pad caps the grid at 1176px FOREVER. Four 260px tracks plus three
   48px gaps need 1184px. It misses by EIGHT PIXELS at every viewport, so auto-fit resolves to three
   columns always and a fourth item wraps onto its own row at full width, permanently lopsided.
   Do not re-propose the column. The legal row is also where this belongs semantically: it already
   holds the entity and the jurisdiction, and a privacy policy is legal boilerplate, not a booking
   destination.
   align-items:center, NOT baseline, because the 44px touch floor above gives the link a taller box
   than the copyright text and baseline alignment would visibly knock the row. */
.footer-legal{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2) var(--space-5);border-top:1px solid var(--border-inverse);padding-top:var(--space-5)}
.footer-legal a{font-size:var(--size-caption);line-height:var(--lh-caption);color:var(--text-inverse-muted)}
/* --text-caption-default, 12px/1.45, and --text-inverse-muted. Both are the design's. This
   REMOVES the alpha-contrast hole the old rule carried: rgba(242,237,229,.60) had to be
   composited by hand because qa.py's table is hex-only, whereas forest-200 on forest-700 is a
   pair qa.py already asserts at 8.54:1. The old 15px was a pre-1:1 ruling. */
.footer-legal p{font-size:var(--size-caption);line-height:var(--lh-caption);color:var(--text-inverse-muted)}

/* THREE MEDIA QUERIES RETIRED THIS ROUND. --section-gap at 900px, .long-stays .panel at 780px
   and .offer-list at 900px are all gone: the design has no breakpoints, and the last two were
   hardcoded column counts. Every section's rhythm is now the design's own clamp and every
   multi-column block is auto-fit. The two that remain are ours and are not layout:
   a touch-target floor and the widget height reservation. */
@media (min-width:760px){
  .widget-slot{min-height:44px}
}

/* FORCED-COLORS FALLBACK for the two Guesty date inputs.
   Normally these show an inset box-shadow ring, per the design's treatment for inputs. Under
   forced-colors the shadow is dropped and Guesty's own `outline:none` leaves them with no indicator
   at all. This restores an outline ONLY in that mode, so normal rendering keeps the design's inset
   ring and gains no second indicator.
   The `#guesty-search-widget__datepicker` ID is REQUIRED, not decoration. Their rule is (1,2,1) and
   an ID outranks any number of classes, so an ID-less selector loses no matter how many classes it
   carries. Measured: the ID-less form left outline `none` under forced-colors; this form does not.
   `Highlight` is the system colour keyword, which is the correct expression of intent here because
   the user agent overrides author colours in this mode anyway. No !important. Outline does not
   affect layout, so CLS and the fold budget are untouched. */
@media (forced-colors: active){
  .widget-card .guesty-root-element #guesty-search-widget__datepicker [id^=date-picker-wrapper] input:focus-visible{
    outline:2px solid Highlight;outline-offset:2px}
}

/* ============================================================================
   PRIVACY PAGE ONLY. Consumed by privacy.html and by nothing else.
   EVERY SELECTOR BELOW IS A DESCENDANT OF .legal OR .prose. That is deliberate and it is the
   whole point of this block: a bare `p`, `ul`, `table` or `a` rule here would leak onto the
   landing page, which has none of this typography and does not want it. If you add a rule to this
   section, scope it the same way or it is not a privacy-page rule any more.
   The alternative was a second stylesheet, which is how two stylesheets start to drift. One file,
   one clearly-fenced section.
   ============================================================================ */
.legal{padding:clamp(32px,5vw,64px) 0 clamp(56px,7vw,96px)}
/* Reading measure. The landing page has no long-form copy, so nothing else on the site needs it. */
.prose{max-width:68ch;display:grid;gap:var(--space-5)}
.legal h1{font-size:clamp(28px,4vw,44px);line-height:1.1;letter-spacing:var(--track-display-xl);font-weight:var(--fw-light)}
.legal h2{font-size:clamp(19px,2.2vw,24px);line-height:var(--lh-title-1);letter-spacing:var(--track-title);margin-top:var(--space-4)}
.legal .updated{font-size:var(--size-small);line-height:var(--lh-small);color:var(--text-muted)}
.prose ul{margin:0;padding-left:var(--space-5);display:grid;gap:var(--space-2)}
.prose li{color:var(--text-body)}
/* <address> is the correct element for contact details and it italicises by default. */
.prose .contact{font-style:normal;line-height:var(--lh-body);color:var(--text-body)}
/* The processors table is three columns of text and would overflow a 320px viewport, so it
   scrolls inside its own container and the page body never scrolls sideways. */
.table-scroll{overflow-x:auto}
.prose table{border-collapse:collapse;width:100%;min-width:34rem;font-size:var(--size-small);line-height:var(--lh-small)}
.prose th,.prose td{text-align:left;padding:var(--space-3) var(--space-4) var(--space-3) 0;border-bottom:1px solid var(--border-hairline);vertical-align:top}
.prose thead th{font-family:var(--font-sans);font-size:11px;line-height:1.2;font-weight:var(--fw-medium);letter-spacing:.22em;text-transform:uppercase;color:var(--text-muted)}
.prose tbody tr:last-child td{border-bottom:0}
