/* ==========================================================================
   AEROVISTA - COMPONENTS
   Every component here has: desktop, tablet, mobile, touch, hover, focus,
   and reduced-motion behaviour. DESIGN.md 53.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. BUTTONS  - DESIGN.md 42, restyled to HOME-PAGE-DEMO.html
   The demo's button is a square orange slab with a wide-tracked uppercase
   label that lifts 3px and throws an orange shadow on hover.

   The label is white and the fill is the deepened brand orange, which is the
   pairing that reads as a button. The two alternatives were both worse: white
   on the mark's own #FC4E00 is 3.37:1 and fails, and navy on it passes at
   5.84:1 but reads as a warning stripe rather than a call to action. See the
   accent block in tokens.css.
   -------------------------------------------------------------------------- */

/* One geometry, three fills. Every value below comes from a --btn-* token so
   that primary, secondary, the navbar CTA and a form submit cannot drift apart
   -- change the token, every call to action on the site follows. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--btn-gap);
  min-height: var(--btn-h);
  padding: 0 var(--btn-pad-x);
  border: var(--btn-border) solid transparent;
  border-radius: var(--btn-radius);
  font-size: var(--btn-font-size);
  font-weight: var(--btn-weight);
  letter-spacing: var(--btn-tracking);
  text-transform: uppercase;
  white-space: nowrap;
  transition: background-color var(--btn-transition) var(--btn-ease),
              color            var(--btn-transition) var(--btn-ease),
              border-color     var(--btn-transition) var(--btn-ease),
              box-shadow       var(--btn-transition) var(--btn-ease),
              transform        var(--m-micro)        var(--btn-ease);
}
.btn:active { transform: translateY(0) scale(.99); box-shadow: none; }
.btn > svg { flex: none; }

/* The arrow. One shape (av_arrow() in project.php), one size, one motion,
   whichever variant it sits in. Sized in em so it tracks the label. */
.btn__arrow,
.cta__arrow {
  flex: none;
  width: 1.15em;
  height: auto;
  transition: transform var(--btn-transition) var(--btn-ease);
}
.btn:hover .btn__arrow,
.btn:focus-visible .btn__arrow { transform: translateX(var(--btn-arrow-travel)); }

/* --- Primary: the conversion. Orange fill, white label. ------------------- */
.btn-primary {
  background: var(--c-accent);
  color: var(--c-white);
}
.btn-primary:hover {
  background: var(--c-accent-hover);
  transform: translateY(calc(var(--btn-lift) * -1));
  box-shadow: var(--shadow-accent);
}
/* Same problem as the sticky bar: orange on orange is not a focus ring. */
.btn-primary:focus-visible { outline-color: var(--c-ink); }

/* --- Dark: the navy fill the header CTA wears, so the one orange button on
   screen is always the conversion and never the navigation. ---------------- */
.btn-dark {
  background: var(--c-ink);
  color: var(--c-white);
}
.btn-dark:hover {
  background: var(--c-ink-soft);
  transform: translateY(calc(var(--btn-lift) * -1));
  box-shadow: var(--shadow-lift);
}
.btn-dark:focus-visible { outline-color: var(--c-accent); }
.mode-dark .btn-dark {
  background: var(--c-white);
  color: var(--c-ink);
}

@media (prefers-reduced-motion: reduce) {
  .btn-primary:hover,
  .btn-dark:hover { transform: none; box-shadow: none; }
}

/* --- Secondary: hairline outline that fills in on hover. ------------------
   It used to only darken its border, which read as a disabled button sitting
   next to the orange one rather than as its counterpart. Filling with navy
   gives the pair one interaction language, and the border moves with the
   background so nothing changes size. */
.btn-secondary {
  background: transparent;
  color: var(--c-ink);
  border-color: var(--hairline-strong);
}
.btn-secondary:hover {
  background: var(--c-ink);
  border-color: var(--c-ink);
  color: var(--c-white);
  transform: translateY(calc(var(--btn-lift) * -1));
}
.mode-dark .btn-secondary {
  color: var(--c-warm-white);
  border-color: var(--hairline-dark);
}
.mode-dark .btn-secondary:hover {
  background: var(--c-warm-white);
  border-color: var(--c-warm-white);
  color: var(--c-ink);
}
@media (prefers-reduced-motion: reduce) {
  .btn-secondary:hover { transform: none; }
}

/* Full-width buttons carry panel-editable labels (the lead-gate CTA among
   them), so they must be allowed to wrap. nowrap on a fixed 420px modal turns
   a long label into horizontal overflow. */
.btn-block {
  display: flex;
  width: 100%;
  white-space: normal;
  text-align: center;
  padding-block: 10px;
}

.btn[disabled], .btn[aria-disabled="true"] {
  opacity: .45;
  pointer-events: none;
}

/* nowrap is right at desktop widths and wrong at 320px, where "Get directions"
   inside a two-button row has nowhere to go but outside the viewport. Below the
   tablet breakpoint a long label wraps to two lines and the button grows;
   clipped or overflowing CTA text is never acceptable. Report, Buttons. */
/* Phones keep the same component: same uppercase, same radius, same arrow.
   Only the height and the horizontal padding step down, and the tracking eases
   slightly because 0.18em on a multiword label at 360px starts to break the
   word into letters. Labels are still allowed to wrap rather than overflow. */
@media (max-width: 767px) {
  .btn {
    min-height: var(--btn-h-sm);
    padding-inline: var(--btn-pad-x-sm);
    white-space: normal;
    text-align: center;
    line-height: var(--lh-tight);
    letter-spacing: .14em;
  }
}

/* Text CTA. The arrow travels on hover - motion inventory item 7. */
/* Tertiary: the same typography as .btn with the container taken away, so a
   text link reads as the quiet member of the family rather than as unrelated
   text. Underline draws in from the left as the arrow travels. */
.cta {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--btn-gap);
  min-height: var(--tap-min);
  font-size: var(--btn-font-size);
  font-weight: var(--btn-weight);
  letter-spacing: var(--btn-tracking);
  text-transform: uppercase;
}
@media (max-width: 767px) {
  .cta { letter-spacing: .14em; }
}
.cta::before {
  content: "";
  position: absolute;
  left: 0;
  bottom: 12px;
  height: var(--border-width);
  width: 100%;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform var(--btn-transition) var(--btn-ease);
}
.cta:hover::before,
.cta:focus-visible::before { transform: scaleX(1); }
.cta:hover .cta__arrow,
.cta:focus-visible .cta__arrow { transform: translateX(var(--btn-arrow-travel)); }
@media (prefers-reduced-motion: reduce) {
  .cta::before { transition: none; }
}

/* Underline that draws rather than appears. */
.link-u {
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 0% var(--border-width);
  background-position: 0 100%;
  background-repeat: no-repeat;
  transition: background-size var(--m-fast) var(--ease-out);
  padding-bottom: 2px;
}
.link-u:hover, .link-u:focus-visible { background-size: 100% var(--border-width); }

/* --------------------------------------------------------------------------
   2. FORM CONTROLS  - DESIGN.md 32
   Borders use --hairline-strong (3.09:1) so the field boundary itself meets
   WCAG non-text contrast. Decorative hairlines are too light for this job.
   -------------------------------------------------------------------------- */

.field { display: block; }

.field__label {
  display: block;
  margin-bottom: var(--s-xs);
  font-size: var(--t-small);
  font-weight: var(--w-medium);
  letter-spacing: var(--ls-meta);
  text-transform: uppercase;
  color: var(--c-stone-text);
}

.input {
  display: block;
  width: 100%;
  min-height: var(--tap-min);
  padding: 0 var(--s-sm);
  background: var(--c-white);
  color: var(--c-ink);
  border: var(--border-width) solid var(--hairline-strong);
  border-radius: var(--r-sm);
  font-size: 16px;   /* below 16px iOS zooms the viewport on focus */
  transition: border-color var(--m-fast) var(--ease-out);
}
.input::placeholder { color: var(--c-stone); }
.input:hover  { border-color: var(--c-ink-soft); }
.input:focus  { border-color: var(--c-ink); }

textarea.input {
  min-height: 120px;
  padding-block: var(--s-sm);
  line-height: var(--lh-body);
  resize: vertical;
}

/* Phone field with a fixed +91 affix. */
.field-phone {
  display: flex;
  align-items: stretch;
  border: var(--border-width) solid var(--hairline-strong);
  border-radius: var(--r-sm);
  background: var(--c-white);
  transition: border-color var(--m-fast) var(--ease-out);
  overflow: hidden;
}
.field-phone:focus-within { border-color: var(--c-ink); }
.field-phone__cc {
  display: flex;
  align-items: center;
  padding-inline: var(--s-sm);
  border-right: var(--border-width) solid var(--hairline);
  font-size: 16px;
  color: var(--c-stone-text);
  background: var(--c-warm-white);
}
.field-phone .input {
  border: 0;
  border-radius: 0;
  background: transparent;
}

/* Inline errors. DESIGN.md 45 - direct, never cute. */
.field__error {
  display: none;
  margin-top: 6px;
  font-size: var(--t-small);
  color: var(--c-error);   /* 5.90:1 on warm white */
}
.field.is-invalid .input,
.field.is-invalid .field-phone { border-color: var(--c-error); }
.field.is-invalid .field__error { display: block; }

.hp { position: absolute; left: -9999px; }   /* honeypot */

/* --------------------------------------------------------------------------
   3. FACT STRIP  - hairline-divided cells, the signature component
   -------------------------------------------------------------------------- */

.facts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  border-top: var(--border-width) solid var(--hairline);
  border-left: var(--border-width) solid var(--hairline);
}
.mode-dark .facts { border-color: var(--hairline-dark); }

/* Five facts in two columns leaves one dangling in a half-width cell with an
   empty box beside it. The last item spans the row instead — balanced, and it
   reads as a closing line rather than an oversight. Only when the count is
   actually odd, so editing the strip cannot break the layout.
   Report, Responsive content: "the final odd item spans full width". */
@media (max-width: 767px) {
  .facts__item:last-child:nth-child(odd) { grid-column: 1 / -1; }
}

.facts__item {
  padding: var(--s-md) var(--s-sm);
  border-right: var(--border-width) solid var(--hairline);
  border-bottom: var(--border-width) solid var(--hairline);
  transition: background-color var(--m-fast) var(--ease-out);
}
.mode-dark .facts__item { border-color: var(--hairline-dark); }
/* The demo washes a cell in 8% orange on hover. Pointer-only: on touch there
   is no hover to reveal it and a permanently tinted cell would read as
   selected. */
@media (hover: hover) and (pointer: fine) {
  .facts__item:hover { background: rgba(204, 63, 0, .07); }
}

/* Report: this is t-property-stat. Tabular figures so ₹70.99 L and ₹96.78 L
   line up as a column instead of jittering against each other. */
/* Serif, like every other figure in the demo — the fact strip is the clearest
   place the pairing does its job, because a serif ₹70.99 L above a tracked
   sans "Onwards" is the whole theme in one cell. */
.facts__value {
  font-family: var(--font-display);
  font-size: var(--t-h3);
  font-weight: var(--w-display);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-heading);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
/* t-property-stat-label. Sentence case at 12px minimum: "Onwards" and
   "Possession" are the words that make the number above them mean something,
   so they are not decorative micro type. */
.facts__label {
  margin-top: 6px;
  font-size: var(--t-ui);
  font-weight: var(--w-normal);
  line-height: var(--lh-tight);
  color: var(--c-stone-text);
}
.mode-dark .facts__label { color: var(--c-stone-on-dark); }

@media (min-width: 768px)  { .facts { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1200px) { .facts { grid-template-columns: repeat(5, minmax(0, 1fr)); } }

/* --------------------------------------------------------------------------
   4. EDITORIAL CARD  - DESIGN.md 10
   No fill, no shadow. A 1px border and an image.
   -------------------------------------------------------------------------- */

/* The demo does not box a card. It rules one line across the top of it, hangs
   the label above the image and the title below, and on hover turns that rule
   orange while the whole card rises. Nothing is enclosed, so a row of cards
   reads as a column of entries in a document. */
.card {
  display: flex;
  flex-direction: column;
  border: 0;
  border-top: var(--border-width) solid var(--hairline);
  border-radius: var(--r-md);
  background: transparent;
  transition: border-color var(--m-normal) var(--ease-out),
              transform    var(--m-normal) var(--ease-out);
}
.mode-dark .card { border-top-color: var(--hairline-dark); }

.card__media {
  position: relative;
  overflow: hidden;
  aspect-ratio: 4 / 3;
  margin-top: var(--s-sm);
  background: var(--c-ink-soft);
  box-shadow: 0 0 0 1px transparent;
  transition: box-shadow var(--m-normal) var(--ease-out);
}
/* av_picture() wraps every image in a <picture>, and base.css gives that
   `height: auto`. So `height: 100%` on the img was resolving against a wrapper
   that had no height of its own — the image kept its intrinsic size, stopped
   short of the ratio box, and the card's own background showed through
   underneath it. Most cards hid it because their artwork happens to match the
   ratio; the proof section, which is 3/4 with some landscape photographs in
   it, showed a solid band under every one of them.

   The wrapper has to fill the box before object-fit on the img means anything. */
.card__media picture {
  display: block;
  width: 100%;
  height: 100%;
}
.card__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform var(--m-image) var(--ease);
}
.card__body { padding: var(--s-md) var(--s-sm) var(--s-sm); }
/* .card__index colour lives in sections.css, which splits it by ground. */
.card__title { margin-top: var(--s-xs); }
.card__meta  { margin-top: var(--s-xs); }
.card__cta   { margin-top: var(--s-sm); }

/* Hover is a desktop enhancement only. Everything it reveals is already
   visible on touch - DESIGN.md 30. */
@media (hover: hover) and (pointer: fine) {
  .card:hover { border-top-color: var(--c-accent); transform: translateY(-6px); }
  .card:hover .card__media { box-shadow: var(--shadow-lift); }
  /* The demo runs the image scale slower than the lift — 900ms against 260 —
     so the photograph is still settling after the card has arrived. */
  .card:hover .card__media img { transform: scale(1.045); }
  .card:hover .cta__arrow { transform: translateX(4px); }
}
@media (prefers-reduced-motion: reduce) {
  .card:hover { transform: none; }
  .card:hover .card__media { box-shadow: none; }
}

/* Flat scrim for text over imagery. Never blur the photograph.
   The z-index is load-bearing: a positioned image inside the same element
   paints above an unlayered pseudo-element, which silently defeats the
   scrim and leaves text sitting on bare photography. */
.media-scrim::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: var(--z-scrim);
  background: var(--scrim-image);
  pointer-events: none;
}
.media-scrim--hero::after { background: var(--scrim-hero); }

/* Honest labelling for real site photography - the trust device.
   Sentence case at caption size, not 10px all-caps: this is the claim the
   whole trust argument rests on, so it has to be read as a sentence rather
   than decoded as a label. Report, "Site-photo captions". */
.photo-stamp {
  position: absolute;
  left: var(--s-sm);
  bottom: var(--s-sm);
  z-index: var(--z-overlay);
  max-width: calc(100% - var(--s-md));
  padding: 6px 10px;
  border: var(--border-width) solid var(--hairline-dark);
  border-radius: var(--r-xs);
  background: rgba(12, 8, 48, .48);
  font-size: var(--t-caption);
  font-weight: var(--w-normal);
  line-height: var(--lh-tight);
  letter-spacing: 0;
  color: var(--c-white);
}

/* --------------------------------------------------------------------------
   5. RAIL  - mobile horizontal snap scrolling, no JS carousel
   -------------------------------------------------------------------------- */

.rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 78%;          /* next card stays partly visible */
  gap: var(--col-gap);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--gutter);
  padding-inline: var(--gutter);
  margin-inline: calc(var(--gutter) * -1);
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.rail::-webkit-scrollbar { display: none; }
.rail > * { scroll-snap-align: start; }

@media (min-width: 768px) {
  .rail { grid-auto-columns: 42%; }
}
/* Above 1200px the rail stops being a rail and becomes a grid. */
@media (min-width: 1200px) {
  .rail {
    grid-auto-flow: row;
    grid-template-columns: repeat(var(--rail-cols, 4), minmax(0, 1fr));
    overflow-x: visible;
    padding-inline: 0;
    margin-inline: 0;
  }
}

/* --------------------------------------------------------------------------
   6. ACCORDION
   The one exception to the transform/opacity rule. Animating to an unknown
   content height cannot be done on the compositor: max-height needs a magic
   number and eases wrong, and measuring height in JS forces layout too.
   grid-template-rows 0fr -> 1fr is the least-bad technique - it does trigger
   layout, but only inside this subtree, and only while a panel is opening.
   It is the only layout-animating property in the codebase. Keep it that way.
   -------------------------------------------------------------------------- */

.acc__item { border-bottom: var(--border-width) solid var(--hairline); }
.mode-dark .acc__item { border-bottom-color: var(--hairline-dark); }

.acc__trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-sm);
  width: 100%;
  min-height: var(--tap-min);
  padding-block: var(--s-sm);
  text-align: left;
  font-size: var(--t-body);
}
.acc__sign {
  flex: none;
  width: 12px; height: 12px;
  position: relative;
  color: var(--c-accent);
}
.acc__sign::before, .acc__sign::after {
  content: "";
  position: absolute;
  background: currentColor;
  transition: transform var(--m-normal) var(--ease);
}
.acc__sign::before { inset: 5px 0; height: 1px; }
.acc__sign::after  { inset: 0 5px; width: 1px; }
.acc__trigger[aria-expanded="true"] .acc__sign::after { transform: scaleY(0); }

.acc__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--m-normal) var(--ease);
}
.acc__trigger[aria-expanded="true"] + .acc__panel { grid-template-rows: 1fr; }
.acc__panel > div { overflow: hidden; }
.acc__panel p { padding-bottom: var(--s-sm); }

/* --------------------------------------------------------------------------
   7. MODAL / LEAD GATE  - BUILD-PLAN.md 7
   The scrim is flat. There is no blur behind it. This carries the only
   shadow on the site.
   -------------------------------------------------------------------------- */

.modal {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: grid;
  place-items: end center;
  background: var(--scrim-modal);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--m-modal) var(--ease-out),
              visibility var(--m-modal);
}
.modal.is-open { opacity: 1; visibility: visible; }

/* Below 768px this is a bottom sheet: rounded top corners, capped height, and
   its own scroll. Without the cap a phone in landscape — or any phone with the
   keyboard up — pushes the submit button off the bottom of a panel that cannot
   scroll, which is a form nobody can complete. Report, "Mobile popup". */
.modal__panel {
  position: relative;
  width: 100%;
  max-width: 420px;
  max-height: min(90dvh, 90vh);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  /* No top padding. The close bar is the first child and provides the top
     spacing itself — see .modal__bar, where the old negative margin was
     fighting sticky positioning and covering the heading. */
  padding: 0 var(--s-sm) var(--s-sm);
  padding-bottom: max(var(--s-sm), env(safe-area-inset-bottom));
  background: var(--c-warm-white);
  border: var(--border-width) solid var(--hairline);
  border-radius: var(--r-md) var(--r-md) 0 0;
  box-shadow: var(--shadow-modal);
  transform: translateY(12px);
  transition: transform var(--m-modal) var(--ease);
}
.modal.is-open .modal__panel { transform: translateY(0); }

@media (min-width: 768px) {
  .modal { place-items: center; }
  .modal__panel {
    padding: 0 var(--s-md) var(--s-md);
    border-radius: var(--r-sm);
    max-height: min(88dvh, 88vh);
  }
}

/* With no close control there is no bar to space the panel, so the panel
   provides its own top padding. */
.modal[data-dismissible="false"] .modal__panel { padding-top: var(--s-md); }

/* Sticky, not absolute: the close control has to stay reachable while the panel
   scrolls, and an absolutely-positioned one scrolls off the top with the
   heading. The row is its own sticky band so the button never overlaps copy,
   and it sits far enough from the submit button that a thumb aiming for one
   cannot land on the other. */
/* Sticky, not absolute: the close control has to stay reachable while the
   panel scrolls, and an absolutely-positioned one scrolls off the top with the
   heading.

   It used to carry `margin-top: -32px` to sit flush against a panel that had
   32px of top padding — and a sticky element cannot honour a negative margin
   that pulls it above its scrollport. Sticky clamped it back down to the
   padding-box top while the flow still deducted the 32px, so the bar rendered
   32px lower than the space it reserved and its opaque band sat on top of the
   copy underneath: the eyebrow was completely hidden and the first line of the
   heading was cut off across the caps.

   Now the panel has no top padding, the bar occupies the top of it honestly,
   and its own padding is the panel's top spacing. Nothing is pulled anywhere. */
.modal__bar {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  justify-content: flex-end;
  margin-inline: calc(var(--s-sm) * -1);
  padding: var(--s-xs) var(--s-xs) 0;
  background: var(--c-warm-white);
}
@media (min-width: 768px) {
  .modal__bar { margin-inline: calc(var(--s-md) * -1); }
}

.modal__close {
  width: var(--tap-min); height: var(--tap-min);
  display: grid; place-items: center;
  color: var(--c-stone-text);
  transition: color var(--m-fast) var(--ease-out);
}
.modal__close:hover { color: var(--c-ink); }
/* When the panel setting popup_dismissible is off, the close control is not
   rendered at all - focus still cycles inside, so keyboard and screen-reader
   users can complete the form. */
.modal[data-dismissible="false"] .modal__close { display: none; }

.modal__foot {
  margin-top: var(--s-sm);
  padding-top: var(--s-xs);
  border-top: var(--border-width) solid var(--hairline);
}

/* --- Panel polish --------------------------------------------------------
   The sheet is the one place on the site where a visitor is asked to type, so
   it gets a little more room than a section does and the heading is allowed to
   be the largest thing in it.                                                */

/* Leading only — the size belongs to the template, which asks for .t-h3, and
   a 420px sheet is no place for the 64px section-heading size. --lh-heading is
   0.98, which is right for a 116px hero line and too tight for a 32px serif:
   at that size the descenders of one line meet the caps of the next. */
.modal__panel h2 { line-height: 1.08; }
/* The scrim reads as a surface the sheet is lying on, so the sheet gets a
   little more separation from it than a hairline gives. */
.modal__panel { box-shadow: 0 24px 60px rgba(11, 6, 36, .34); }

/* An accent rule under the eyebrow, so the sheet opens with the same device
   the section heads use rather than as an undifferentiated stack of text. */
.modal__panel > .t-eyebrow {
  display: inline-block;
  padding-bottom: var(--s-xs);
  border-bottom: 2px solid var(--c-accent);
}
.modal__panel form { margin-top: var(--s-sm); }

/* Enquiry sheet: the interaction is deliberately quieter than the page
   around it. One heading block, two clear fields, then one decisive action. */
.modal__head {
  padding-bottom: var(--s-sm);
  border-bottom: var(--border-width) solid var(--hairline);
}
.modal__head .t-eyebrow {
  display: block;
  width: max-content;
  padding-bottom: var(--s-xs);
  border-bottom: 2px solid var(--c-accent);
}
.modal__head h2 { margin-top: var(--s-xs); max-width: 15ch; }
.modal__subtitle { margin-top: 8px; color: var(--c-stone-text); }
/* The sheet is short by design: heading block, two fields, one action. The
   old --s-md rhythm here was section spacing applied to a 420px panel and it
   was most of the sheet's height. */
.modal__form { gap: var(--s-sm); margin-top: var(--s-sm) !important; }
.modal__form .field__label { margin-bottom: 7px; }
.modal__form .input,
.modal__form .field-phone { background: var(--c-white); }
/* The sheet's submit is the same button as every other primary. It used to
   set its own height and its own .08em tracking, which is why it read as a
   different component from the CTA that opened it. */
.modal__form .btn-primary { margin-top: var(--s-xs); }
.modal__close {
  width: 38px;
  height: 38px;
  border: var(--border-width) solid var(--hairline);
  background: var(--c-white);
}
.modal__close:hover,
.modal__close:focus-visible { border-color: var(--c-accent); color: var(--c-accent-text); }
.modal__foot { padding-bottom: 0; }
@media (min-width: 768px) {
  .modal__panel { max-width: 464px; padding-inline: var(--s-lg); padding-bottom: var(--s-lg); }
  .modal__bar { margin-inline: calc(var(--s-lg) * -1); padding: var(--s-sm) var(--s-sm) 0; }
}
@media (max-width: 479px) {
  .modal__panel { max-height: min(92dvh, 92vh); padding-inline: var(--s-md); }
  .modal__bar { margin-inline: calc(var(--s-md) * -1); padding: var(--s-xs) var(--s-xs) 0; }
  .modal__head h2 { max-width: 13ch; }
}

/* --------------------------------------------------------------------------
   8. STICKY MOBILE CTA BAR  - DESIGN.md 20
   -------------------------------------------------------------------------- */

.bar {
  position: fixed;
  left: max(8px, env(safe-area-inset-left));
  right: max(8px, env(safe-area-inset-right));
  width: auto;
  max-width: calc(100vw - 16px - env(safe-area-inset-left) - env(safe-area-inset-right));
  bottom: max(10px, env(safe-area-inset-bottom));
  z-index: var(--z-sticky);
  display: grid;
  /* Four actions remain equal-width, but now sit inside one quiet floating
     dock rather than a full-width utility strip. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 3px;
  min-height: var(--bar-h);
  padding: 5px;
  background: rgba(11, 6, 36, .94);
  border: 1px solid rgba(255, 255, 255, .17);
  border-radius: 22px;
  box-shadow: 0 12px 32px rgba(11, 6, 36, .24);
  overflow: hidden;
  transform: translateY(0);
  transition: transform var(--m-normal) var(--ease);
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .bar {
    background: rgba(11, 6, 36, .78);
    -webkit-backdrop-filter: blur(18px) saturate(135%);
    backdrop-filter: blur(18px) saturate(135%);
  }
}
.bar.is-hidden { transform: translateY(110%); }

.bar__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  min-height: calc(var(--bar-h) - 10px);
  min-width: 0;
  padding: 6px 2px;
  color: var(--c-warm-white);
  /* Not --t-micro: at 10px these are the site's most-tapped controls and the
     report's minimum for an actionable label is 12px. */
  font-size: var(--t-bar-label);
  font-weight: var(--w-medium);
  letter-spacing: .06em;
  text-transform: none;    /* sentence case reads faster at this size */
  line-height: 1.1;
  text-align: center;
  border: 0;
  border-radius: 16px;
  transition: background-color var(--m-fast) var(--ease-out),
              color var(--m-fast) var(--ease-out),
              transform var(--m-micro) var(--ease-out);
}
.bar__item:active { background: rgba(255, 255, 255, .12); transform: scale(.97); }
.bar__item--primary {
  background: var(--c-warm-white);
  color: var(--c-ink);
  box-shadow: 0 3px 12px rgba(0, 0, 0, .14);
}
.bar__item--primary:active { background: var(--c-white); }

/* The global focus ring is terracotta, which vanishes on a terracotta fill, and
   a 3px outset ring is clipped by the bar's own edges. Inset it, and switch to
   black where the ground is the accent colour (4.96:1 either way). */
.bar__item:focus-visible { outline: 2px solid var(--c-accent-bright); outline-offset: -2px; }
.bar__item--primary:focus-visible { outline-color: var(--c-ink); }

.bar__icon {
  width: 19px; height: 19px;
  flex: none;
  display: block;
}
.bar__label { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Below 360px four full labels stop fitting. Swap in the short wording rather
   than letting a label clip or ellipsise — the accessible name comes from the
   link's aria-label, so it is unchanged either way. */
@media (max-width: 599px) {
  .bar__label[data-short] { font-size: 0; }
  .bar__label[data-short]::after {
    content: attr(data-short);
    font-size: var(--t-bar-label);
  }
}

@media (min-width: 1200px) { .bar { display: none; } }

/* The dock helps discovery, but it must never compete with the form that
   completes the visit request. ui.js applies this only while the enquiry
   section is in view, leaving the conversion actions available elsewhere. */
.bar.is-near-enquiry { opacity: 0; pointer-events: none; transform: translateY(calc(100% + var(--s-sm))); }

/* --------------------------------------------------------------------------
   8b. LIGHTBOX  - injected by ui.js, so none of this exists without JS

   The photographs are the reason this site exists, and every one of them used
   to stop at the edge of its card. The viewer is deliberately plain: a full
   scrim, the image at whatever size fits, the caption the page already wrote,
   and a counter. No chrome that is not doing a job.
   -------------------------------------------------------------------------- */

/* The affordance. A small cut-corner mark in the corner of every openable
   image - faint at rest so it does not compete with the photograph, solid on
   hover and focus. It is always present rather than hover-only, because on a
   touch screen hover never happens and the image would look inert. */
.zoom {
  position: absolute;
  inset: 0;
  z-index: var(--z-overlay);
  display: block;
  width: 100%;
  cursor: zoom-in;
  background: none;
  border: 0;
}
.zoom__mark {
  position: absolute;
  right: 10px; bottom: 10px;
  display: grid;
  place-items: center;
  width: 30px; height: 30px;
  color: var(--c-white);
  background: rgba(11, 6, 36, .55);
  opacity: .78;
  transition: opacity var(--m-fast) var(--ease-out),
              background-color var(--m-fast) var(--ease-out),
              transform var(--m-fast) var(--ease-out);
}
.zoom:hover .zoom__mark,
.zoom:focus-visible .zoom__mark {
  opacity: 1;
  background: var(--c-accent);
  color: var(--c-white);
}
/* The press. A tap has to land somewhere visible or it reads as a dead
   control on a slow connection - this is the whole point of the exercise. */
.zoom:active .zoom__mark { transform: scale(.88); }
.zoom:focus-visible { outline: 2px solid var(--c-accent); outline-offset: -4px; }

/* The photo stamp sits in the same corner on site photographs, so the mark
   moves to the opposite one rather than covering it. */
.media-scrim .zoom__mark { right: auto; left: 10px; }

@media (prefers-reduced-motion: reduce) {
  .zoom__mark { transition: none; }
  .zoom:active .zoom__mark { transform: none; }
}

.lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-rows: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--s-sm);
  padding: var(--s-sm);
  background: var(--scrim-modal);
  opacity: 0;
  transition: opacity var(--m-modal) var(--ease-out);
}
.lightbox.is-open { opacity: 1; }

.lightbox__bar {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-sm);
  color: var(--c-warm-white);
}
.lightbox__count {
  color: var(--c-warm-white);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

.lightbox__btn,
.lightbox__nav {
  display: grid;
  place-items: center;
  width: var(--tap-min);
  height: var(--tap-min);
  color: var(--c-warm-white);
  border: var(--border-width) solid var(--hairline-dark);
  background: rgba(11, 6, 36, .4);
  transition: background-color var(--m-fast) var(--ease-out),
              border-color var(--m-fast) var(--ease-out),
              color var(--m-fast) var(--ease-out),
              transform var(--m-micro) var(--ease-out);
}
.lightbox__btn:hover,
.lightbox__nav:hover {
  background: var(--c-accent);
  border-color: var(--c-accent);
  color: var(--c-white);
}
.lightbox__btn:active,
.lightbox__nav:active { transform: scale(.9); }
/* The arrows are hidden for a single-image group. No local rule needed: the
   global [hidden] in base.css covers it. */

.lightbox__stage {
  grid-column: 2;
  grid-row: 2;
  display: grid;
  gap: var(--s-sm);
  justify-items: center;
  align-content: center;
  min-height: 0;
  max-height: 100%;
  margin: 0;
}
.lightbox__stage picture { display: block; min-height: 0; }
.lightbox__stage img {
  max-width: 100%;
  /* The caption and the bar are the other two rows; the image takes what is
     left rather than pushing them off the screen on a landscape phone. */
  max-height: calc(100svh - var(--tap-min) - 6rem);
  width: auto;
  height: auto;
  object-fit: contain;
}
.lightbox__cap {
  max-width: 72ch;
  color: var(--c-warm-white);
  text-align: center;
}

/* Opening is the moment the action is answered: the frame scales up the last
   4% as the scrim fades in, so the image reads as having come from the page
   rather than having been swapped in behind it. */
.lightbox__stage { transform: scale(.96); transition: transform var(--m-modal) var(--ease); }
.lightbox.is-open .lightbox__stage { transform: scale(1); }

/* Stepping through: the new frame arrives from the side it was asked for. */
@keyframes lb-from-right { from { opacity: 0; transform: translateX(28px); } }
@keyframes lb-from-left  { from { opacity: 0; transform: translateX(-28px); } }
/* No fill mode. With `both` the picture holds the keyframe's opacity:0 until
   the animation actually starts, so in any tab the browser is not giving
   frames to — a background tab, a throttled one — the viewer opens with the
   image invisible. Without it the resting state is the finished state and the
   animation is purely additive. */
.lightbox__stage.is-from-right picture { animation: lb-from-right var(--m-normal) var(--ease); }
.lightbox__stage.is-from-left  picture { animation: lb-from-left  var(--m-normal) var(--ease); }

@media (max-width: 767px) {
  /* The side arrows would each eat 48px of a 390px screen. They move under the
     image, where a thumb reaches them anyway. */
  .lightbox { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr) auto; }
  .lightbox__stage { grid-column: 1; grid-row: 2; }
  .lightbox__nav--prev { grid-column: 1; grid-row: 3; justify-self: start; }
  .lightbox__nav--next { grid-column: 1; grid-row: 3; justify-self: end; }
  .lightbox__stage img { max-height: calc(100svh - (var(--tap-min) * 2) - 6rem); }
}

@media (prefers-reduced-motion: reduce) {
  .lightbox,
  .lightbox__stage { transition: none; }
  .lightbox__stage { transform: none; }
  .lightbox.is-open .lightbox__stage { transform: none; }
  .lightbox__stage.is-from-right picture,
  .lightbox__stage.is-from-left  picture { animation: none; }
}

/* --------------------------------------------------------------------------
   8c. DESKTOP QUICK ACTIONS  - call, WhatsApp, directions

   The mobile bar covers these three below 1200px and this covers them above,
   so exactly one of the two is ever on screen.

   No tile and no panel: three marks floating against the page, each in the
   colours people already know it by. The only chrome is a small glass chip
   that rises above the mark on hover, and that one does need a ground - it
   sits over whatever the page happens to be showing, which may be a
   photograph.
   -------------------------------------------------------------------------- */

.fab {
  display: none;
  position: fixed;
  z-index: var(--z-sticky);
  right: clamp(1rem, 1.6vw, 2rem);
  bottom: clamp(1.5rem, 6vh, 4rem);
  flex-direction: column;
  /* flex-end, not the default stretch. Stretch made every item as wide as the
     widest, so opening one label widened the column and every other mark slid
     across with it - they all appeared to open at once. Each item now sizes to
     its own content and only the hovered one moves. */
  align-items: flex-end;
  gap: 12px;
}
@media (min-width: 1200px) {
  .fab { display: flex; }
}

/* The item is exactly the size of its mark. The label is taken out of flow
   entirely and sits above it, so the column geometry is identical open or
   closed and hovering one mark cannot move any other. */
.fab__item {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: flex-end;
}

.fab__icon {
  position: relative;
  z-index: 1;
  display: block;
  width: 52px;
  height: 52px;
  flex: none;
  /* The marks sit on the page with no tile behind them, so the only thing
     separating them from a photograph is their own edge. A soft drop shadow
     does that without adding a box. */
  filter: drop-shadow(0 4px 10px rgba(11, 6, 36, .28));
  transition: transform var(--m-fast) var(--ease-out),
              filter var(--m-fast) var(--ease-out);
}
.fab__icon svg { display: block; width: 100%; height: 100%; }

.fab__item:hover .fab__icon,
.fab__item:focus-visible .fab__icon {
  transform: scale(1.08);
  filter: drop-shadow(0 8px 18px rgba(11, 6, 36, .34));
}
/* The press. Without it the only feedback is the browser following the link,
   which on a tel: or WhatsApp handoff can take a noticeable moment. */
.fab__item:active .fab__icon {
  transform: scale(.94);
  filter: drop-shadow(0 2px 6px rgba(11, 6, 36, .3));
}
.fab__item:focus-visible { outline: none; }
.fab__item:focus-visible .fab__icon { outline: 2px solid var(--c-accent); outline-offset: 4px; }

/* The label sits directly above its own mark, right-aligned to it, as a small
   smoked-glass chip.

   This is the second and last place on the site that blurs — the fixed header
   is the other — and it is the same justification: a chrome surface floating
   over the page, never a photograph being obscured. The @supports fallback is
   a solid dark chip at full opacity, so a browser without backdrop-filter gets
   readable white-on-near-black rather than white on a translucent wash. */
.fab__label {
  position: absolute;
  /* Above the icons. The marks are z-index:1 and the chip reaches up into the
     gap over the mark above its own, so without this it paints underneath a
     neighbouring icon — every stacking comparison here happens inside .fab, so
     DOM order does not save it. */
  z-index: 2;
  right: 0;
  bottom: calc(100% + 10px);
  padding: 9px 14px;
  border-radius: 9px;
  border: 1px solid rgba(255, 255, 255, .14);
  background: rgba(11, 6, 36, .92);
  color: var(--c-white);
  /* --t-micro bottoms out at 10px, which is decorative-index size. This chip
     carries a phone number someone is about to dial. */
  font-size: var(--t-caption);
  font-weight: var(--w-medium);
  line-height: 1.25;
  letter-spacing: .03em;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  box-shadow: 0 6px 18px rgba(11, 6, 36, .3);
  pointer-events: none;
  opacity: 0;
  transform: translateY(4px) scale(.96);
  transform-origin: bottom right;
  transition: opacity var(--m-fast) var(--ease-out),
              transform var(--m-normal) var(--ease);
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .fab__label {
    background: rgba(11, 6, 36, .55);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    backdrop-filter: blur(14px) saturate(140%);
  }
}
.fab__item:hover .fab__label,
.fab__item:focus-visible .fab__label {
  opacity: 1;
  transform: translateY(0) scale(1);
}

@media (prefers-reduced-motion: reduce) {
  .fab__icon,
  .fab__label { transition: none; }
  .fab__item:hover .fab__icon,
  .fab__item:focus-visible .fab__icon,
  .fab__item:active .fab__icon { transform: none; }
  .fab__label { transform: none; }
  .fab__item:hover .fab__label,
  .fab__item:focus-visible .fab__label { transform: none; }
}

/* --------------------------------------------------------------------------
   9. HEADER  - three floating elements, not a bar

   Left:   an ivory-glass logo island pinned to the left edge of the viewport.
   Centre: a navigation rail that is almost not there — a 15% wash over a blur,
           carrying the text and nothing else.
   Right:  a navy pill CTA, standing on its own with no container.

   The header element has no background and no border. The photograph shows
   through the gaps, which is the whole point: the navigation is supposed to
   complement the architecture rather than sit on a white band in front of it.
   -------------------------------------------------------------------------- */

.nav {
  position: fixed;
  top: var(--nav-top);
  left: 0;
  right: 0;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* No padding on the header itself. An absolutely positioned child resolves
     50% against the padding box, so padding here would push the centred rail
     off true viewport centre by half of it. The right inset lives on
     .nav__actions instead. */
  /* The header spans the viewport but is mostly empty. Without this the
     transparent gaps would still swallow every click aimed at the top of the
     hero — an invisible element that intercepts pointers is the same class of
     bug as a modal that fails to hide. Each element turns pointers back on. */
  pointer-events: none;
}
.nav > * { pointer-events: auto; }

/* Entrance. One orchestrated moment on load, not a per-element cascade: the
   whole header settles down 8px and fades in, once. */
@keyframes nav-in { from { opacity: 0; transform: translateY(-8px); } }
.js .nav > * {
  animation: nav-in 620ms cubic-bezier(.22, 1, .36, 1) both;
}
.js .nav__links  { animation-delay: 70ms; }
.js .nav__actions { animation-delay: 130ms; }

/* --- Left: the logo island ----------------------------------------------- */

.nav__logo {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 3px;
  height: var(--nav-logo-h);
  /* Flush to the viewport edge, so the island reads as anchored to the page
     rather than floating a gutter's width inside it. A left border would be a
     hairline against nothing, so it goes. */
  padding-left: clamp(14px, 1.5vw, 26px);
  padding-right: clamp(16px, 1.8vw, 30px);
  background: var(--glass-logo);
  border: 1px solid var(--glass-logo-edge);
  border-left: 0;
  border-radius: 0 var(--r-island) var(--r-island) 0;
  box-shadow: var(--shadow-logo);
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nav__logo {
    -webkit-backdrop-filter: var(--glass-logo-blur);
    backdrop-filter: var(--glass-logo-blur);
  }
}
.nav__brand-row {
  display: inline-flex;
  align-items: center;
  gap: clamp(10px, .9vw, 16px);
  min-width: 0;
}
.logo-brand {
  height: var(--logo-brand-h);
  width: auto;
  transition: transform var(--m-fast) var(--ease-out);
}
.nav__project {
  padding-left: clamp(10px, .9vw, 16px);
  border-left: 1px solid rgba(11, 6, 36, .14);
  /* 20% larger than the shared UI scale, limited to this project label. */
  font-size: clamp(.9rem, .876rem + .12vw, 1.05rem);
  font-weight: var(--w-medium);
  line-height: var(--lh-tight);
  letter-spacing: .16em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--c-ink);
  transition: color var(--m-fast) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .nav__logo:hover .logo-brand   { transform: translateY(-1px); }
  .nav__logo:hover .nav__project { color: var(--c-accent-text); }
}
.nav__logo:active .logo-brand { transform: scale(.98); }

/* --- Centre: the navigation rail ------------------------------------------

   Nearly transparent by design. Everything that makes the text readable is
   doing it quietly: the blur flattens whatever is behind, the 15% wash lifts
   it a little, the scrim below softens a dark patch, and the text itself
   carries a light halo. No white rectangle anywhere.                        */

.nav__links {
  /* Centred on the viewport. The logo island and the CTA are never the same
     width, so a flex child would sit wherever their difference left it. */
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  display: none;
  align-items: center;
  gap: clamp(2px, .5vw, 8px);
  height: var(--nav-rail-h);
  padding: 7px 10px;
  background: var(--glass-rail);
  border: 1px solid var(--glass-rail-edge);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow-nav);
  transition: background-color var(--m-normal) var(--ease-out),
              box-shadow var(--m-normal) var(--ease-out),
              backdrop-filter var(--m-normal) var(--ease-out);
}
@media (min-width: 1200px) { .nav__links { display: flex; } }

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nav__links {
    -webkit-backdrop-filter: var(--glass-rail-blur);
    backdrop-filter: var(--glass-rail-blur);
  }
}
/* Without backdrop-filter a 15% wash over a photograph is nothing at all, so
   the fallback is a genuine, if still light, ivory. Readability first. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nav__links { background: rgba(245, 245, 240, .82); }
}

/* The photographic readability layer. Not a rectangle: an ellipse of 8% navy
   that has faded to nothing well before the rail's own edge, so it reads as
   the type being easier to see rather than as a panel behind it. */
.nav__links::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -140% -18%;
  background: radial-gradient(ellipse at center,
              rgba(10, 15, 40, .10) 0%,
              rgba(10, 15, 40, .05) 45%,
              rgba(10, 15, 40, 0) 72%);
  pointer-events: none;
}

.nav__link {
  position: relative;
  display: inline-flex;
  align-items: center;
  height: 100%;
  padding-inline: clamp(12px, 1.05vw, 20px);
  border-radius: var(--r-pill);
  color: var(--c-ink);
  font-size: var(--t-ui);
  font-weight: var(--w-medium);
  letter-spacing: .16em;
  text-transform: uppercase;
  white-space: nowrap;
  /* Not a glow. A one-pixel light halo that stops navy type dissolving into a
     mid-tone patch of photograph. */
  text-shadow: 0 1px 8px rgba(255, 255, 255, .35);
  transition: background-color var(--m-normal) var(--ease-out),
              color var(--m-normal) var(--ease-out),
              box-shadow var(--m-normal) var(--ease-out),
              text-shadow var(--m-normal) var(--ease-out);
}
/* No permanent chip. It appears under the pointer and nowhere else. */
.nav__link:hover,
.nav__link:focus-visible {
  /* The hero image can absorb a 18% wash. This remains glass rather than a
     solid chip, but is intentionally bright enough to register on photography. */
  background: rgba(255, 255, 255, .42);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .34);
  color: var(--c-ink);
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nav__link:hover,
  .nav__link:focus-visible {
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
  }
}
.nav__link:active { background: rgba(255, 255, 255, .26); }

/* The current page. A 2px accent tick, and nothing else — no travelling
   underline, no chip, no weight change. */
.nav__link[aria-current="page"]::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 5px;
  width: 14px;
  height: 2px;
  margin-left: -7px;
  border-radius: 2px;
  background: var(--c-accent-bright);
}

/* --- Adaptive contrast & Dark Background Handling ------------------------
   When any dark background (e.g. .mode-dark, dark hero, night section, footer)
   comes under the nav bar, the navigation capsule, logo, and toggle turn
   pure/solid white glass with bold, crystal-clear dark ink text.            */

.nav.is-over-dark .nav__links,
.nav[data-nav-contrast="light"] .nav__links,
.nav[data-nav-contrast="dark-bg"] .nav__links {
  background: rgba(255, 255, 255, 0.95);
  border: 1px solid rgba(255, 255, 255, 0.90);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.32);
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nav.is-over-dark .nav__links,
  .nav[data-nav-contrast="light"] .nav__links,
  .nav[data-nav-contrast="dark-bg"] .nav__links {
    -webkit-backdrop-filter: blur(24px) saturate(140%);
    backdrop-filter: blur(24px) saturate(140%);
  }
}
.nav.is-over-dark .nav__links::before,
.nav[data-nav-contrast="light"] .nav__links::before,
.nav[data-nav-contrast="dark-bg"] .nav__links::before {
  display: none;
}
.nav.is-over-dark .nav__link,
.nav[data-nav-contrast="light"] .nav__link,
.nav[data-nav-contrast="dark-bg"] .nav__link {
  color: var(--c-ink);
  font-weight: var(--w-medium);
  text-shadow: none;
}
.nav.is-over-dark .nav__link:hover,
.nav.is-over-dark .nav__link:focus-visible,
.nav[data-nav-contrast="light"] .nav__link:hover,
.nav[data-nav-contrast="light"] .nav__link:focus-visible,
.nav[data-nav-contrast="dark-bg"] .nav__link:hover,
.nav[data-nav-contrast="dark-bg"] .nav__link:focus-visible {
  background: rgba(11, 6, 36, .08);
  color: var(--c-accent-text);
  text-shadow: none;
}

.nav.is-over-dark .nav__logo,
.nav[data-nav-contrast="dark-bg"] .nav__logo {
  background: rgba(255, 255, 255, 0.96);
  border-color: rgba(255, 255, 255, 0.90);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}

.nav.is-over-dark .nav__toggle,
.nav[data-nav-contrast="dark-bg"] .nav__toggle {
  background: rgba(255, 255, 255, 0.96);
  border-color: rgba(255, 255, 255, 0.90);
  color: var(--c-ink);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}

/* --- Scrolled -------------------------------------------------------------
   The rail firms up with a clean ivory-white surface and deep shadow.       */

.nav.is-scrolled .nav__links {
  background: rgba(255, 255, 255, 0.92);
  box-shadow: var(--shadow-nav-on);
  border-color: rgba(255, 255, 255, 0.85);
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nav.is-scrolled .nav__links {
    -webkit-backdrop-filter: blur(24px) saturate(135%);
    backdrop-filter: blur(24px) saturate(135%);
  }
}
.nav.is-scrolled.is-over-dark .nav__links,
.nav.is-scrolled[data-nav-contrast="light"] .nav__links,
.nav.is-scrolled[data-nav-contrast="dark-bg"] .nav__links {
  background: rgba(255, 255, 255, 0.96);
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.38);
}

/* --- Right: the CTA and the menu trigger --------------------------------- */

.nav__actions {
  display: flex;
  align-items: center;
  gap: clamp(8px, .9vw, 14px);
  margin-right: var(--gutter);
}

/* The header CTA is `.btn .btn-dark .nav__cta` — it IS the site button, and
   this class only says "the standing conversion action is allowed a little
   more presence than the rest". It was a --r-pill capsule with its own height,
   padding, tracking, timing and arrow, which is why it read as belonging to a
   different site than the hero pair directly below it. Everything structural
   now comes from .btn; what is left here is the extra height and the resting
   shadow that give it its standing. */
.nav__cta {
  height: var(--nav-cta-h);
  cursor: pointer;
  box-shadow: 0 4px 16px rgba(11, 6, 36, .20);
}
.nav__cta:hover { box-shadow: 0 10px 28px rgba(11, 6, 36, .28); }
.nav__cta:active { box-shadow: 0 3px 10px rgba(11, 6, 36, .22); }
.nav__cta:focus-visible { outline: 2px solid var(--c-accent-bright); outline-offset: 3px; }

/* The menu trigger. Glass like the logo island so the right-hand pair reads as
   one family, square-ish rather than a pill so it is not a second CTA. */
.nav__toggle {
  display: grid;
  place-items: center;
  width: var(--nav-cta-h);
  height: var(--nav-cta-h);
  border-radius: 16px;
  background: var(--glass-logo);
  border: 1px solid var(--glass-logo-edge);
  box-shadow: var(--shadow-logo);
  color: var(--c-ink);
  cursor: pointer;
  transition: background-color var(--m-normal) var(--ease-out),
              transform var(--m-fast) var(--ease-out);
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nav__toggle {
    -webkit-backdrop-filter: var(--glass-logo-blur);
    backdrop-filter: var(--glass-logo-blur);
  }
}
@media (min-width: 1200px) { .nav__toggle { display: none; } }

/* Three rules drawn from one element, so there is no icon font and no SVG to
   load, and the close state is the same three rules rotated. */
.nav__toggle-bars,
.nav__toggle-bars::before,
.nav__toggle-bars::after {
  display: block;
  width: 18px;
  height: 1.5px;
  background: currentColor;
  border-radius: 2px;
  transition: transform var(--m-normal) var(--ease), opacity var(--m-fast) linear;
}
.nav__toggle-bars { position: relative; }
.nav__toggle-bars::before,
.nav__toggle-bars::after { content: ""; position: absolute; left: 0; }
.nav__toggle-bars::before { top: -5px; }
.nav__toggle-bars::after  { top: 5px; }
.nav__toggle[aria-expanded="true"] .nav__toggle-bars { background: transparent; }
.nav__toggle[aria-expanded="true"] .nav__toggle-bars::before { transform: translateY(5px) rotate(45deg); }
.nav__toggle[aria-expanded="true"] .nav__toggle-bars::after  { transform: translateY(-5px) rotate(-45deg); }
.nav__toggle:active { transform: scale(.95); }

/* --- Narrow ---------------------------------------------------------------
   Not a compressed desktop header: the rail is gone entirely, the CTA loses
   its arrow and tightens, and the trigger takes the navigation.             */

@media (max-width: 999px) {
  /* The original Dukia artwork includes its own tagline; it is shown as one
     enlarged, unbroken mark and the project name is not repeated here. */
  .nav__logo {
    padding-right: clamp(16px, 1.8vw, 30px);
  }
  .nav__brand-row { gap: clamp(10px, .9vw, 16px); }
  .logo-brand { height: clamp(38px, 9vw, 46px); }
  .nav__project { display: none; }
  .nav__toggle { width: 46px; height: 46px; border-radius: 14px; }
}
/* From tablet desktop upward, the project name is not repeated in the header. */
@media (min-width: 1000px) {
  .nav__project { display: none; }
}
@media (max-width: 719px) {
  .nav__actions { gap: 0; margin-right: max(12px, env(safe-area-inset-right)); }
  /* The dock carries the visit action on phones. Keeping a second CTA in the
     fixed header is what pushed the menu trigger beyond the viewport. */
  .nav__cta { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .js .nav > * { animation: none; }
  .nav__link,
  .nav__links,
  .nav__cta,
  .logo-brand,
  .nav__toggle,
  .nav__toggle-bars,
  .nav__toggle-bars::before,
  .nav__toggle-bars::after { transition: none; }
  .nav__cta:hover { transform: none; }
  .nav__logo:hover .logo-brand { transform: none; }
}

/* --------------------------------------------------------------------------
   9b. MOBILE MENU  - DESIGN.md 08, motion inventory item 5
   Sequence: panel clips open -> items stagger -> CTA last.
   The panel is solid black, not a blurred scrim - blur is banned, and a
   near-full-screen panel needs no transparency to establish itself.
   -------------------------------------------------------------------------- */


.menu {
  position: fixed;
  inset: 0;
  z-index: var(--z-menu);
  display: grid;
  /* Content-height rows, pinned to the top. The body row used to be `1fr`,
     which stretched the panel to the full screen however few links it held and
     left a band of empty ink under the CTA. */
  grid-template-rows: auto auto auto;
  align-content: start;
  /* The ink is painted by the three panel rows, not by this box. This element
     is the full-screen scrim that catches the outside click; if it carried the
     ground itself the panel would read as full-height however short it is. */
  background: var(--scrim-modal);
  color: var(--c-warm-white);
  overflow-y: auto;
  overscroll-behavior: contain;
  visibility: hidden;
  /* clip-path is compositor-only, so the reveal costs no layout work. */
  clip-path: inset(0 0 100% 0);
  transition: clip-path var(--m-menu) var(--ease),
              visibility var(--m-menu);
}
.menu > * { position: relative; }
/* A quiet, solid architectural ground keeps the links and touch targets in
   focus. The menu is navigation, not another hero composition. */
.menu__top,
.menu__body,
.menu__foot { background: var(--c-ink); }
.menu.is-open {
  visibility: visible;
  clip-path: inset(0 0 0 0);
}

/* The drawer is the dark counterpart of the header islands: the same frosted
   treatment, inverted. Held at 94% — this panel carries the whole navigation
   and a form CTA, so it is glass for the language, not for the transparency. */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .menu__top,
  .menu__body,
  .menu__foot {
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
  }
}

/* Inside the drawer the close control is on a dark ground, so it drops the
   ivory glass it wears in the header and takes a hairline instead. */
.menu .nav__toggle {
  background: rgba(248, 248, 244, .10);
  border-color: rgba(255, 255, 255, .24);
  box-shadow: 0 8px 24px rgba(0, 0, 0, .12);
  color: var(--c-warm-white);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
}
.menu .nav__toggle:hover {
  background: rgba(248, 248, 244, .17);
  border-color: rgba(255, 255, 255, .42);
}
/* It lives inside the drawer, which is itself hidden above the breakpoint, so
   the header's own display rule must not reach it. */
@media (min-width: 1200px) {
  .menu .nav__toggle { display: grid; }
}

.menu__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 54px;
  border-bottom: 1px solid rgba(255, 255, 255, .14);
}
.menu__label {
  color: var(--c-accent-bright);
  font-size: var(--t-ui);
  font-weight: var(--w-medium);
  letter-spacing: var(--ls-meta);
  text-transform: uppercase;
}

.menu__body { padding-block: clamp(6px, 1.4vh, 14px); }
.menu__body nav { max-width: 540px; }

.menu__item {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity var(--m-normal) var(--ease),
              transform var(--m-normal) var(--ease);
  /* Items begin after the panel has opened, then stagger. */
  transition-delay: calc(160ms + (var(--i, 0) * 40ms));
}
.menu.is-open .menu__item { opacity: 1; transform: translateY(0); }

/* Serif, at heading size: the menu panel is the mobile equivalent of the
   section heads, and setting it in the sans would make it the one large piece
   of type on the site that is not in the display face. */
.menu__link {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* 44px is the tap floor. Ten links at the old 57px row could not fit a
     phone screen alongside the bar and the CTA, so the panel scrolled and the
     last link and the CTA sat below the fold. */
  min-height: 44px;
  padding: 2px 0;
  border: 0;
  border-bottom: 1px solid rgba(255, 255, 255, .15);
  border-radius: 0;
  font-family: var(--font-display);
  font-size: clamp(1.1rem, 3.6vw, 1.3rem);
  font-weight: var(--w-display);
  letter-spacing: var(--ls-heading);
  transition: background-color var(--m-normal) var(--ease-out),
              border-color var(--m-normal) var(--ease-out),
              color var(--m-normal) var(--ease-out),
              transform var(--m-normal) var(--ease-out);
}
.menu__link::after {
  content: "→";
  font-family: var(--font);
  font-size: var(--t-ui);
  font-weight: var(--w-medium);
  opacity: .58;
  transform: translateX(-4px);
  transition: transform var(--m-normal) var(--ease-out), opacity var(--m-normal) var(--ease-out);
}
.menu__link:hover,
.menu__link:focus-visible {
  background: transparent;
  border-bottom-color: var(--c-accent-bright);
  color: var(--c-accent-bright);
}
.menu__link:hover::after,
.menu__link:focus-visible::after { opacity: 1; transform: translateX(0); }
.menu__link[aria-current="page"] {
  color: var(--c-warm-white);
  border-bottom-color: var(--c-accent-bright);
  background: transparent;
}
.menu__link[aria-current="page"]::before {
  content: "";
  position: absolute;
  left: 0;
  width: 3px;
  height: 28px;
  border-radius: 3px;
  background: var(--c-accent-bright);
}

.menu__foot {
  padding-block: var(--s-xs);
  border-top: 1px solid rgba(255, 255, 255, .14);
  /* The lift sits on top of the ink, so it is an image layer over the colour
     rather than the background it used to be when .menu painted the ground. */
  background-image: linear-gradient(rgba(255,255,255,.025), rgba(255,255,255,.025));
  padding-bottom: calc(var(--s-xs) + env(safe-area-inset-bottom));
}
/* Nothing to restate — the drawer CTA is .btn .btn-primary and inherits the
   system. Only the resting shadow is dropped, because the panel is already a
   raised surface. */
.menu .btn-primary,
.menu .btn-primary:hover { box-shadow: none; }

/* A tablet has enough width for two compact columns; a phone keeps one clear
   reading path with every row still comfortably tappable. */
@media (min-width: 640px) and (max-width: 1199px) {
  .menu__body nav ul { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--s-lg); }
}

@media (min-width: 1200px) {
  .menu { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .menu       { clip-path: none; opacity: 0; transition: opacity 120ms, visibility 120ms; }
  .menu.is-open { opacity: 1; }
  .menu__item { transform: none; transition-delay: 0ms; }
}

/* --------------------------------------------------------------------------
   9c. PRESS FEEDBACK  - the answer to a tap

   The site had a full hover vocabulary and almost no press vocabulary, which
   is backwards for how it is actually used: most visitors are on a phone,
   where hover never fires, and the first feedback they got for tapping a
   card or a menu link was the next page painting. On a 3G connection that is
   long enough to tap again.

   Everything below is :active only. It costs nothing, it never fires on
   desktop hover, and it is the difference between a control that feels
   connected and one that feels broken. transform and colour only - nothing
   here animates layout.
   -------------------------------------------------------------------------- */

/* Cards that are links - the residence panels, the price rows. The whole card
   dips rather than the label, because the whole card is the target. */
a.card:active { transform: translateY(-2px) scale(.995); }

/* Text CTAs and underlined links: the arrow completes its travel and the
   label dims, so a tap reads as "taken" rather than "missed". */
.cta:active,
.link-u:active { opacity: .68; }
.cta:active .cta__arrow { transform: translateX(7px); }

/* Nav, menu and footer links. A menu link is a 22px line of text on a black
   panel; without this, tapping one is completely silent. */
.nav__link:active,
.footer__list a:active,
.footer__list button:active { opacity: .6; }
.menu__link:active { color: var(--c-accent); transform: translateX(3px); }

/* The accordion. The panel already animates open; the trigger itself did not
   acknowledge the tap that opened it. */
.acc__trigger:active { opacity: .7; }
.acc__trigger:active .acc__sign { transform: scale(.85); }
.acc__sign { transition: transform var(--m-micro) var(--ease-out); }

/* Landmark rows on the location map. These select a pin, which is a state
   change the visitor is looking for confirmation of. */
.dist__name:active { transform: translateX(3px); }

/* The brand mark. */
.nav__logo:active .logo-brand { transform: translateY(0) scale(.97); }

/* Fact cells are not controls and must not pretend to be - no press state. */

@media (prefers-reduced-motion: reduce) {
  a.card:active,
  .cta:active .cta__arrow,
  .menu__link:active,
  .dist__name:active,
  .nav__logo:active .logo-brand,
  .acc__trigger:active .acc__sign { transform: none; }
}

/* --------------------------------------------------------------------------
   10. MOTION  - the complete inventory, BUILD-PLAN.md 6
   transform and opacity only, plus clip-path for image reveals.
   Nothing here animates a layout property.
   -------------------------------------------------------------------------- */

/* Every starting state below is scoped to .js — the class the inline script in
   the layout adds. No JavaScript, no hidden content: the page renders in its
   finished state instead of waiting for a reveal that will never come.
   Motion confirms, it is never the only way to see something. */

/* 1. Section entry */
.js [data-anim="rise"] {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity var(--m-text) var(--ease),
              transform var(--m-text) var(--ease);
  transition-delay: calc(var(--i, 0) * var(--stagger));
}
.js [data-anim="rise"].is-in { opacity: 1; transform: translateY(0); }

/* 2. Image reveal */
.js [data-anim="reveal"] {
  clip-path: inset(6% 0 6% 0);
  transition: clip-path var(--m-image) var(--ease);
}
.js [data-anim="reveal"].is-in { clip-path: inset(0 0 0 0); }

/* 3. Hero image, once on load */
.js [data-anim="hero"] {
  transform: scale(1.03);
  transition: transform var(--m-hero) var(--ease);
}
.js [data-anim="hero"].is-in { transform: scale(1); }

/* Reduced motion removes the transforms entirely rather than shortening
   them - a fast slide is still a slide. DESIGN.md 27. */
@media (prefers-reduced-motion: reduce) {
  .js [data-anim="rise"]   { opacity: 1; transform: none; }
  .js [data-anim="reveal"] { clip-path: none; }
  .js [data-anim="hero"]   { transform: none; }
  .card:hover .card__media img { transform: none; }
}

/* --------------------------------------------------------------------------
   11. LOGO
   The Aerovista mark is a stacked lockup (tower + wordmark + tagline) at
   1.4:1. Height drives it and width is left to the intrinsic ratio, so it can
   never be stretched. It is never cropped to the tower alone.

   Two files rather than a CSS filter: the mark is supplied as flat black, and
   `filter: invert()` on a brand asset is the kind of arbitrary transform that
   damages marks. The reversed file is a genuine colour variant of the same
   artwork, alpha channel untouched.
   -------------------------------------------------------------------------- */

.logo {
  display: block;
  height: var(--logo-nav-h);
  width: auto;
}

/* The two-variant crossfade the nav used to run is gone with the transparent
   nav: the bar has one ground, so the mark has one colour. The reversed file
   is still the right one on the menu panel and the footer, which are dark, and
   it is used there directly. */

.logo-foot  { height: var(--logo-foot-h); width: auto; }
.logo-dukia { height: var(--logo-dukia-h); width: auto; }

/* --------------------------------------------------------------------------
   12. LIGHT-THEME REFINEMENTS
   The light theme is not the dark theme inverted. On a warm-white ground the
   same 1px hairline reads heavier than it does on black, so borders are used
   more sparingly and space does more of the work.
   -------------------------------------------------------------------------- */

/* This block used to drop the card border entirely on light grounds, because
   a full box in hairline reads heavy on warm white. The demo answers the same
   problem differently and better: keep one line, at the top only, and let it
   turn orange on hover. So the rule that erased it is gone — .card already
   carries exactly that top hairline — and only the padding rule remains. */
.mode-light .card__body,
.mode-alt   .card__body { padding-inline: 0; }

/* A raised white surface, used for the enquiry panel and the FAQ block. */
.surface {
  background: var(--c-surface);
  border: var(--border-width) solid var(--hairline);
  border-radius: var(--r-md);
  padding: clamp(1.25rem, 0.7rem + 2.4vw, 2.5rem);
}
/* A .surface inside a dark band is the enquiry panel on the dark home section:
   it needs the demo's paper, not a white slab on navy. */
.mode-dark .surface { border-color: var(--hairline-dark); }
