@charset "UTF-8";
/* ==========================================================================
   KAANI100 — MOBILE-FIRST STYLESHEET
   --------------------------------------------------------------------------
   Written mobile-first: base styles target the smallest screen, and every
   media query is min-width. Nothing here relies on !important to win a
   cascade fight, and nothing relies on overflow-x:hidden to hide a layout bug.

   Layer order
     1. Reset
     2. Design tokens
     3. Base / typography
     4. Layout primitives
     5. Components
     6. Page-specific
     7. Utilities

   Breakpoints (min-width):  sm 640px · lg 1024px · xl 1280px
   ========================================================================== */


/* ==========================================================================
   1. RESET
   ========================================================================== */

*,
*::before,
*::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body,
h1, h2, h3, h4, h5, h6,
p, figure, blockquote, dl, dd, ol, ul {
  margin: 0;
  padding: 0;
}

/* Unconditional, not ul[class]/ol[class] — this design system never wants a
   native bullet or number, and a bare <ul>/<ol> (e.g. the footer nav lists,
   the breadcrumb) was slipping through the old class-gated version with
   default browser bullets and indent, throwing off alignment. */
ul, ol { list-style: none; padding-left: 0; }

img, picture, video, iframe, svg {
  display: block;
  max-width: 100%;
}

img { height: auto; }

input, button, textarea, select {
  font: inherit;
  color: inherit;
}

button { background: none; border: none; }

/* Flex/grid children refuse to shrink below content width by default, which
   is the single most common cause of horizontal overflow. Opt out globally. */
.container > *,
.p-grid > *,
.feature-grid > *,
.two-col > *,
.steps > *,
.field__row > * { min-width: 0; }


/* ==========================================================================
   2. DESIGN TOKENS
   ========================================================================== */

:root {
  /* ------------------------------------------------------------------
     COLOUR — green + gold, two hues only.
     Every value below is contrast-verified.

     RULE, one meaning per colour:
       green = structure (header type, footer, CTA bands, headings) AND
               the primary action. One hue doing both is only safe because
               the ramp below is deliberately WIDE — see its note.
       gold  = the single accent (price, verified, the one loud CTA)
       teal  = functional success only          red = functional error only

     WHERE THE LIGHT/DARK LINE FALLS, and it is the rule most easily broken by
     accident: a green carrying a WHITE label has to be green-700 or darker.
     700 is the lightest step that clears 4.5:1, and it does so at 4.65 — a
     real but thin margin, which is why the button, the pill and the category
     panel all sit exactly there and not one step lighter. 600 and up are
     fills, icons and decoration only.
     ------------------------------------------------------------------ */

  /* Green — a FULLY SATURATED emerald ramp, hue ~h140, S=100% down the whole
     scale. Saturation is what this ramp is for. Two earlier versions failed in
     opposite directions and both are worth remembering: a yellow-green lime at
     h90-h117 was vivid but read as a highlighter, and a desaturated h132 green
     (S~62%, blue channel opened up) fixed that and went flat instead. The fix
     for "dull" is not "lighter" — lightness is pinned by the white labels the
     buttons carry. It is chroma. So every step below is on the outer edge of
     the gamut for its lightness: nothing is greyed, and the glow tokens are
     tuned to match rather than to compensate.

     700 is THE brand green: the primary button, the active nav pill and the
     hero category panel. It sits at the lightest point that still carries
     white type, so the ramp cannot get brighter here without inverting labels.

     CONTRAST against white, because it constrains where each step is legal:
       400  1.79:1   500  2.27:1   600  3.16:1
       700  4.84:1   800  6.45:1   900  8.24:1   950 14.18:1

     In practice:
       * 400-500 are FILLS, GLOW and DECORATION. Never text, never white on
         them. This is where the ramp's brightness is allowed to live.
       * 600 clears 3:1, so it is legal for LARGE display type, icons and
         non-text UI (the carousel dot, the active thumb) — nothing smaller.
       * 700 is the lightest step that takes a white label (4.84:1). Do not
         lighten it; keep labels on it at 600-700 weight.
       * 800 is the workhorse for text ON white: links, --focus, band ends.
       * 900 and 950 are the dark bands, headings and the footer. */
  --green-950: #00330f;   /* deepest — footer, text on gold fills          */
  --green-900: #005c1e;   /* dark bands, CTA gradient end, headings        */
  --green-800: #006e24;   /* links, --focus, lightest safe text-on-white   */
  --green-700: #00842b;   /* THE brand green: button, pill, category panel */
  /* #00a836 measured 3.01:1 on the canvas — passing, with nothing to spare.
     Two steps darker buys real margin at a difference the eye cannot pick
     out, and this step carries the hero accent line and every UI indicator. */
  --green-600: #009e33;   /* large display type, icons, UI indicators      */
  --green-500: #00c740;   /* fill, and the colour the glow is made of      */
  --green-400: #22e05c;   /* the brightest step — glow and decoration only */
  --green-100: #b8f0c8;   /* tint                                          */
  --green-50:  #e6faec;   /* chip fills, the hero wash                     */

  /* Gold — the accent, hue 45 fully saturated. Its footprint shrank when
     green took over structure: gold is now the one loud CTA and small
     decoration, nothing more.
     gold-700 = text on light (5.5:1 on white)   gold-500 = fills
     Dark GREEN text on a gold fill, never white — white on gold is 2.3:1.
     Never gold-400 on a light surface: it is 1.4:1 on the header. */
  --gold-700: #8a6700;   /* the only gold legal as text on light, 5.2:1   */
  --gold-600: #d9a300;
  --gold-500: #f5b921;   /* the reference's Contact Us / icon-badge gold  */
  --gold-400: #ffd34f;
  --gold-50:  #fff8e4;

  /* --- Neutral surfaces ---
     These track the ramp's hue, and they have now followed it twice: mint
     when it was h122, yellow-green while it was lime, and back to a soft
     natural green at h132. The rule behind the churn is the only part worth
     keeping: a page ground at a different hue from the chrome standing on it
     reads as a mistake, however faint the tint. Same lightness steps each
     time; only the hue moves. */
  /* NOTE: this site used to ban pure white outright. That rule is gone —
     cards are now white, and the faint tinted canvas behind them is what
     supplies the separation the off-white used to. */
  --surface:        #ffffff;   /* cards / raised surfaces  */
  --surface-alt:    #f6fbf6;   /* the page canvas          */
  --surface-sunken: #ecf7ee;   /* image wells, pills       */
  --border:         #dcebde;
  --border-strong:  #bedcc3;

  /* The header is NOT --surface: it sits a half-step warmer so it reads as
     a distinct layer above a white page, then flattens to --surface on
     scroll (see "Header on scroll").
     KEEP THE theme-color META IN _head.html IN STEP WITH THIS VALUE. */
  --header-bg:      #f9fcf8;

  /* The footer is the ONE surface that does not track the green ramp, which
     is why it has its own token instead of using --green-950.
     --green-950 is #00330f: near-black, correct as a label on a gold fill,
     far too heavy as a full-width slab — the footer read as a hole in the
     page rather than as its base. This value is the softer, less saturated
     deep green the footer carried before the ramp was replaced. Keeping it
     off the ramp means a future palette change cannot silently re-darken it.
     White on it is 11.4:1; the muted footer text is 7.3:1. */
  --footer-bg:      #14431a;

  /* --- Text ---
     The green cast in these tracks the ramp too, for the same reason as the
     surfaces: ink at a different hue from the chrome reads as a smudge. */
  --text:          #1e2b20;   /* near-black, green-cast. 14.6:1 on header */
  /* Was #46564a, 7.4:1 on the canvas. Legal, but it lost next to the lit gold
     and green accent lines it sits between on the home page and read as washed
     out. 11.4:1 now, same green cast — and because ~20 rules take their colour
     from here, that lift lands on every muted paragraph on the site. */
  --text-muted:    #2c3a30;
  /* Was #58685c, 5.5:1 on the tinted canvas. Legal, but this token colours the
     SMALLEST type on the site — 13-14px labels like .hero__pin-label — and at
     that size 5.5:1 read as greyed out. ~9.0:1 now, same green cast. */
  --text-subtle:   #3a4a3f;
  /* The old --text-subtle, kept alive under a name that says what it is for:
     the two places where looking unfilled IS the job. Lifting these with the
     token above would make an empty input read as a filled one. */
  --text-placeholder: #58685c;
  --text-inverse:  #ffffff;
  --text-on-dark:       rgba(255, 255, 255, 0.92);
  --text-on-dark-muted: rgba(255, 255, 255, 0.76);

  /* --- Feedback: functional states ONLY, never decoration ---
     Success leans TEAL, not green. A green success flash under a green
     brand stops reading as a state and starts reading as chrome. */
  --success-bg:     #e8f6f1;
  --success-fg:     #0f6b52;
  --success-border: #a9d8c8;
  --error-bg:       #fdf1f0;
  --error-fg:       #a3271b;
  --error-border:   #eab5ae;
  --info-bg:        var(--gold-50);
  --info-fg:        var(--gold-700);

  /* Third-party brand mark, and the trickiest neighbour in the sheet: it is a
     bright green sitting next to a brand that is now also a bright green.
     WhatsApp mandates the exact value, so it cannot move. It survives on hue
     distance — h145 mint sits close to our h140 emerald, closer than any
     previous ramp, so these two now separate on LIGHTNESS rather than hue.
     Watch this pairing: it is the tightest colour relationship on the site. */
  --whatsapp:       #25d366;
  --whatsapp-hover: #1eb857;
  --whatsapp-fg:    #04310f;

  /* --- Type --- */
  /* Body text was Manrope and read as thin beside the Poppins headings. One
     face carries the whole site now, so these two tokens resolve to the SAME
     stack — deliberately, not by accident. They stay two names because that is
     what lets the display face diverge again later without touching the ~15
     rules that name one or the other.
     Poppins is discrete, not variable: every weight the sheet uses has to be
     listed in scripts/vendor_fonts.py or it falls back. See the note there. */
  --font-sans:    "Poppins", system-ui, -apple-system, "Segoe UI", sans-serif;
  /* Headings were Merriweather. A serif reads as established, but it fought
     the geometry of the new palette — the display face is now geometric to
     match. Poppins runs optically LARGER than Merriweather at the same
     size, which is why the hero weight dropped a step below. */
  --font-display: "Poppins", system-ui, -apple-system, "Segoe UI", sans-serif;

  /* An accent face, deliberately NOT --font-display. The note above says a
     serif fought the geometry of the palette, and that still holds for the
     site's headings — this one is scoped to the homepage welcome block, where
     three lines of serif read as a signature rather than as the page's voice.
     The fallbacks are serifs on purpose: if Fraunces fails to load, the block
     should degrade to a different serif, not snap to Poppins and lose the
     distinction that is the whole point of it. */
  --font-accent:  "Fraunces", Georgia, "Times New Roman", serif;

  /* Fluid scale. Minimums are never below 14px, and nothing shrinks as the
     viewport grows (the old sheet had rules that did exactly that). */
  --fs-xs:   0.8125rem;                              /* 13px — badges only */
  --fs-sm:   0.875rem;                               /* 14px floor for body */
  --fs-base: 1rem;
  --fs-md:   1.0625rem;
  --fs-lg:   clamp(1.125rem, 1.05rem + 0.35vw, 1.3125rem);
  --fs-xl:   clamp(1.375rem, 1.2rem + 0.8vw,  1.75rem);
  --fs-2xl:  clamp(1.625rem, 1.35rem + 1.3vw, 2.25rem);
  --fs-3xl:  clamp(1.875rem, 1.4rem + 2.2vw,  3rem);
  --fs-hero: clamp(2rem,     1.4rem + 3.2vw,  3.75rem);

  /* --- Spacing --- */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 2.5rem;
  --space-8: 3rem;
  --space-9: 4rem;
  --space-10: 5rem;

  /* Came down a step at every breakpoint when the bands went light. This
     token scales with the viewport but everything it butts against is fixed —
     .section__head's margin is flat at all widths — so at the old values a
     heading sat 80px below its section edge and 32px above its own content,
     reading as part of the grid beneath rather than as the section's head. */
  --section-y:    var(--space-7);
  --gutter:       var(--space-4);

  /* --- Shape --- */
  --radius-sm:   8px;
  --radius:      12px;
  --radius-lg:   18px;
  --radius-full: 999px;

  /* Tinted green-black, not neutral slate. A grey shadow on a mint canvas
     goes visibly cold and reads as a different page element's shadow. */
  --shadow-sm: 0 1px 2px rgba(0, 40, 15, 0.07);
  --shadow:    0 4px 16px rgba(0, 40, 15, 0.09);
  /* Bumped from .12: white cards on a near-white canvas have less ground
     darkness to push against than they did on the old cream. */
  --shadow-lg: 0 16px 40px rgba(0, 40, 15, 0.14);

  /* The only saturated glows in the sheet. Both sit DARKER than the surface
     they land on — a light glow on a light page is invisible, and spread is
     what does the work, not opacity. Green carries the primary button now;
     gold stayed behind for the one accent CTA. */
  /* Three layers, and they do different jobs — do not collapse them:
       1. a tight ring in green-800, which is what gives the button a crisp
          edge on a near-white canvas;
       2. a short drop in near-black for the lift;
       3. the halo, a wide low-opacity spread of green-500 — this is the glow.
     The halo is built from a step LIGHTER than the button fill on purpose: a
     halo darker than what it surrounds reads as a shadow, not as light. Spread
     does most of the work here, not opacity; pushing alpha instead just makes
     a hard green edge. */
  --glow-green:    0 0 0 1px rgba(0, 110, 36, 0.40),
                   0 3px 12px rgba(0, 40, 15, 0.28),
                   0 0 28px 8px rgba(0, 199, 64, 0.45);
  --glow-green-lg: 0 0 0 1px rgba(0, 110, 36, 0.55),
                   0 6px 20px rgba(0, 40, 15, 0.38),
                   0 0 46px 15px rgba(0, 199, 64, 0.62);

  --glow-gold:    0 0 0 1px rgba(181, 136, 0, 0.45),
                  0 3px 10px rgba(138, 103, 0, 0.45),
                  0 0 24px 6px rgba(217, 163, 0, 0.55);
  --glow-gold-lg: 0 0 0 1px rgba(181, 136, 0, 0.60),
                  0 6px 18px rgba(138, 103, 0, 0.55),
                  0 0 38px 12px rgba(217, 163, 0, 0.75);

  /* Glows for TYPE, and they are filters where everything above is a
     box-shadow. That is not a style choice: box-shadow would trace a rectangle
     around the element, and text-shadow paints behind the glyph — which ruins
     any line whose fill is a clipped gradient, because the blur then shows
     through the letters instead of around them. drop-shadow runs on the
     composited result, so it haloes the letterforms themselves.

     Same two-pass build as the button glows — a tight core plus a wide bloom —
     and the same rule applies: lit a step BRIGHTER than the type it surrounds,
     or it reads as a shadow rather than as light. Hence gold-500 behind
     gold-700 type and green-500 behind green-900 type.

     Contrast is unaffected. The halo lands outside the glyphs, so the text's
     own colour-on-background ratio is exactly what it was without it. */
  --glow-text-gold:  drop-shadow(0 0 10px rgba(245, 185, 33, 0.55))
                     drop-shadow(0 0 26px rgba(245, 185, 33, 0.30));
  --glow-text-green: drop-shadow(0 0 10px rgba(0, 199, 64, 0.45))
                     drop-shadow(0 0 26px rgba(0, 199, 64, 0.26));

  /* The lit eyebrow's glow. A BOX shadow, not a text shadow: the label sits on
     a dark chip, so the bloom belongs around the chip's edge, where it reads
     as light spilling off it.

     Two earlier attempts set light-yellow type directly on the pale mint band
     — first with a blurred bronze halo, then with a hard 0.6px outline. Both
     failed the same way. gold-400 is 1.6:1 there, so the only way to hold the
     letterforms was to darken them, and darkened yellow reads as dirty, not as
     lit. A glow only looks like light when the ground behind it is darker than
     the type. That is what the chip is for — do not try a third time. */
  --eyebrow-glow:    0 0 18px rgba(245, 185, 33, 0.40),
                     0 2px 10px rgba(0, 51, 15, 0.28);

  /* --- Layout --- */
  --container:        1200px;
  --container-narrow: 760px;

  /* --- Interaction --- */
  --tap-min: 44px;                 /* WCAG-friendly minimum touch target */
  --header-h: 64px;

  /* --- Motion ---
     One curve cannot do every job. This site used to have exactly one, and
     applying a snappy 180ms UI curve to a 700ms content reveal is what made
     the whole thing read as machine-made: it arrives at full speed and stops
     dead, with none of the long deceleration that reads as weight.

     Pick by intent:
       --ease          existing UI (drawer, filter sheet) — deliberately unchanged
       --ease-entrance expo-out. Things arriving from nothing: reveals, the hero
       --ease-settle   quint-out. Things responding to a pointer: hover-in
       --ease-exit     quad-in. Things relaxing back: hover-out

     Enter and exit are NOT the same length on purpose. A real object answers
     the hand quickly and relaxes slowly; matching the two is the single most
     reliable tell of motion nobody designed. */
  --ease:          cubic-bezier(0.2, 0.9, 0.3, 1);
  --ease-entrance: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-settle:   cubic-bezier(0.22, 1, 0.36, 1);
  --ease-exit:     cubic-bezier(0.4, 0, 1, 1);

  --dur-micro:  160ms;
  --dur-exit:   180ms;
  --dur-hover:  280ms;
  --dur-hero:   700ms;
  --dur-reveal: 720ms;

  /* --- Depth --- */
  --z-sticky-cta: 60;
  --z-header:     100;
  --z-scrim:      110;
  --z-drawer:     120;
  --z-skip:       200;
}

@media (min-width: 640px) {
  :root { --section-y: var(--space-8); --gutter: var(--space-5); }
}

@media (min-width: 1024px) {
  :root { --section-y: var(--space-9); --gutter: var(--space-6); --header-h: 76px; }
}


/* ==========================================================================
   3. BASE / TYPOGRAPHY
   ========================================================================== */

body {
  font-family: var(--font-sans);
  font-size: var(--fs-base);
  line-height: 1.65;
  color: var(--text);
  background: var(--surface-alt);
  -webkit-font-smoothing: antialiased;
  /* Room for the sticky mobile CTA bar so it never covers the footer. */
  padding-bottom: env(safe-area-inset-bottom);
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  line-height: 1.2;
  color: var(--green-900);
  text-wrap: balance;
}

h1 { font-size: var(--fs-3xl); }
h2 { font-size: var(--fs-2xl); }
h3 { font-size: var(--fs-lg); }
h4 { font-size: var(--fs-md); }

p { text-wrap: pretty; }

a { color: var(--green-800); }
a:hover { color: var(--green-950); }

/* The focus ring has to clear 3:1 against whatever it lands on, and this site
   has two grounds: light pages and dark green bands. One colour cannot do
   both — gold-500 is 2.2:1 on the new near-white header, which is why the
   old single-value ring was failing WCAG 2.4.11 even before the repaint.
   So the ring is a token, and the dark surfaces re-point it. */
:root { --focus: var(--green-800); }
.section--dark,
.cta-band,
.auth-page,
.site-footer,
.lightbox { --focus: var(--gold-400); }

:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.skip-link {
  position: absolute;
  left: var(--space-4);
  top: -100px;
  z-index: var(--z-skip);
  padding: var(--space-3) var(--space-4);
  background: var(--green-800);
  color: var(--text-inverse);
  border-radius: 0 0 var(--radius) var(--radius);
  text-decoration: none;
  transition: top 150ms var(--ease);
}
.skip-link:focus { top: 0; color: var(--text-inverse); }


/* ==========================================================================
   4. LAYOUT PRIMITIVES
   ========================================================================== */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.container--narrow { max-width: var(--container-narrow); }

/* Still no alternating bands, but the reason changed with the palette: the
   canvas is now a near-white mint and the cards are pure white, so a stripe
   of "slightly different white" would be even less visible than before.
   Separation comes from spacing, from card borders, and from the dark green
   chapter bands below. `.section--alt` stays a no-op so templates need no
   churn — do not be tempted to give it a fill. */
.section { padding-block: var(--section-y); }
.section--alt { background: transparent; }

/* Two light sections in a row used to stack a full --section-y from each with
   nothing drawn between them — about.html does this twice back to back. Since
   .section--alt paints nothing, that boundary was pure dead canvas. Halving
   the second one keeps the sections distinct without the void.
   Dark bands are excluded: there the colour change IS the divider, and
   trimming their top padding would crowd the band against its own heading. */
.section + .section:not(.section--dark) {
  padding-top: calc(var(--section-y) / 2);
}

/* Green "chapter" band. Breaks the long light stretch with a deliberate dark
   section rather than a near-invisible off-white stripe. Cards inside stay
   light so they lift off the green. Only the section HEADER text is recoloured
   here — card internals keep their own styling, so this never leaks. */
.section--dark {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: linear-gradient(160deg, var(--green-900) 0%, var(--green-800) 100%);
  color: var(--text-on-dark);
}

.section--dark::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image:
    radial-gradient(circle, rgba(255, 255, 255, 0.05) 1px, transparent 1px),
    radial-gradient(ellipse 60% 55% at 80% 15%, rgba(34, 224, 92, 0.20) 0%, transparent 60%);
  background-size: 26px 26px, 100% 100%;
}

.section--dark .section__head .section__title { color: var(--text-inverse); }
.section--dark .section__head .section__lead  { color: var(--text-on-dark-muted); }
/* No .eyebrow rule here on purpose. The eyebrow carries its own opaque chip,
   which is darker than this band, so it needs no dark-ground variant — it
   already reads. Only the title and lead above have to be recoloured. */

/* Dashed empty-state border needs to be visible against the green band. */
.section--dark .empty-state { border-color: rgba(255, 255, 255, 0.28); }

/* margin-bottom tracks --section-y deliberately. A heading has to sit nearer
   the content it introduces than the section boundary above it; at 32px
   against an 80px --section-y it did the opposite and read as a caption on
   the grid below. Keep this within roughly two-thirds of --section-y if the
   scale is ever retuned. */
.section__head {
  text-align: center;
  max-width: 46rem;
  margin-inline: auto;
  margin-bottom: var(--space-7);
}
.section__head--left { text-align: left; margin-inline: 0; }

.section__title { margin-bottom: var(--space-3); }

.section__lead {
  color: var(--text-muted);
  font-size: var(--fs-md);
}

.section__cta {
  margin-top: var(--space-6);
  text-align: center;
}

.eyebrow {
  display: inline-block;
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  /* gold-400 on green-950 is 9.9:1 — the light yellow finally gets to BE light,
     because the chip supplies the contrast the page background could not.
     The chip is opaque and darker than every ground on the site, including the
     green section bands and the hero photos, so this one rule covers all of
     them and the per-variant overrides that used to sit further down are gone. */
  color: var(--gold-400);
  background: var(--green-950);
  /* Extra left padding by exactly the tracking: letter-spacing hangs 0.12em of
     dead space off the final letter, inside the box, so equal padding makes the
     text look shunted right. */
  padding: 0.4em 0.9em 0.4em calc(0.9em + 0.12em);
  border-radius: var(--radius-full);
  box-shadow: var(--eyebrow-glow);
  margin-bottom: var(--space-3);
}
/* .eyebrow--on-dark is deliberately GONE. It set gold-400, which was correct
   when .page-hero was navy and is 1.4:1 now that it is a light mint band —
   both of its uses had become invisible text. Dark bands are already served
   by the .section--dark rule above, so the modifier was a trap, not a tool. */

.stack > * + * { margin-top: var(--space-4); }

.two-col {
  display: grid;
  gap: var(--space-5);
}

/* 500, not the inherited 400. Poppins is discrete, so this is only a real
   weight because vendor_fonts.py fetches 500 specifically for this rule —
   drop it from that list and every paragraph here becomes a synthesised
   smear. Scoped to .prose so long-form copy gains the presence without card
   and meta text — which share --text-muted — inflating with it. */
.prose {
  color: var(--text-muted);
  font-size: var(--fs-md);
  font-weight: 500;
}
.prose > * + * { margin-top: var(--space-4); }
.prose--center { text-align: center; }

/* Headings and lists inside a property description. The writer types them as
   free text and |rich_description turns them into real markup — see
   text_extras.py. A list following its own heading sits tighter than the
   --space-4 rhythm above, so the two read as one block rather than two. */
.prose__heading {
  font-family: var(--font-display);
  font-size: var(--fs-md);
  font-weight: 700;
  color: var(--green-900);
}
.prose__heading + .prose__list { margin-top: var(--space-2); }

.prose__list { display: grid; gap: var(--space-2); padding-left: 0; }
.prose__list li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  list-style: none;
}
/* The marker is drawn here rather than kept from the text: the emoji the
   writer typed is stripped on the way in, so every list on the site lines up
   on the same gold tick whatever they used to mark it. */
.prose__list li::before {
  content: "\f633";           /* bi-check-lg, from the vendored icon font */
  font-family: "bootstrap-icons";
  color: var(--gold-700);
  flex-shrink: 0;
  line-height: 1.4;
}
.prose__lead {
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: 700;
  color: var(--green-900);
}

@media (min-width: 768px) {
  .two-col { grid-template-columns: 1fr 1fr; gap: var(--space-6); }
}


/* ==========================================================================
   5. COMPONENTS
   ========================================================================== */

/* --- 5.1 Buttons ---------------------------------------------------------
   Every variant clears the 44px minimum target at every screen size. */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-full);
  font-weight: 700;
  font-size: var(--fs-sm);
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  /* The base state is the LEAVING state. The :hover rule below overrides the
     duration and curve on the way in, so a button answers the pointer quickly
     and relaxes slowly. */
  transition: background-color var(--dur-exit) var(--ease-exit),
              color           var(--dur-exit) var(--ease-exit),
              border-color    var(--dur-exit) var(--ease-exit),
              transform       var(--dur-exit) var(--ease-exit),
              box-shadow      var(--dur-exit) var(--ease-exit);
}

/* Every button lifts. `transform` was already listed in the transition above,
   but not one of the .btn--* hover rules ever set one — so the most-touched
   element on a property site changed colour and did nothing else. */
.btn:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow);
  transition-duration: var(--dur-hover);
  transition-timing-function: var(--ease-settle);
}

/* Both values in one transform on purpose: a press should push the already
   lifted button down, not cancel the lift and jump. */
.btn:active {
  transform: translateY(-1px) scale(0.97);
  transition-duration: var(--dur-micro);
}

/* Optional trailing arrow that leans toward where the button is taking you.
   Same idea as .link-arrow further down, which was the one piece of genuine
   micro-interaction the sheet already had. */
.btn__arrow { transition: translate var(--dur-exit) var(--ease-exit); }
.btn:hover .btn__arrow {
  translate: 3px 0;
  transition-duration: var(--dur-hover);
  transition-timing-function: var(--ease-settle);
}

.btn--lg { font-size: var(--fs-base); padding: var(--space-4) var(--space-6); }
.btn--sm { min-height: var(--tap-min); padding: var(--space-2) var(--space-4); }
.btn--block { display: flex; width: 100%; }

/* The main action: a gradient from the brand green into the dark one, white
   label. The gradient is not decoration — the LIGHT end is what constrains it.
   green-700 is 4.65:1 behind white, which passes and has nothing to spare, so
   this must never start any lighter than 700. The dark end is what gives the
   button its weight. */
.btn--primary {
  background: linear-gradient(135deg, var(--green-700) 0%, var(--green-800) 100%);
  color: var(--text-inverse);
  box-shadow: var(--glow-green);
}
/* Colour holds on hover; only the glow grows. */
.btn--primary:hover {
  color: var(--text-inverse);
  box-shadow: var(--glow-green-lg);
}
/* Touch has no hover, so :active carries the same glow on tap. */
.btn--primary:active { box-shadow: var(--glow-green-lg); }

/* The one loud CTA — the header's "Contact Us". Gold kept its glow because
   gold kept its job: this is the single highest-attention button on a page,
   and it is deliberately the only gold fill in the header. */
.btn--accent {
  background: var(--gold-500);
  color: var(--green-950);
  box-shadow: var(--glow-gold);
}
.btn--accent:hover {
  background: var(--gold-500);
  color: var(--green-950);
  box-shadow: var(--glow-gold-lg);
}
.btn--accent:active { box-shadow: var(--glow-gold-lg); }

.btn--ghost {
  background: var(--surface);
  color: var(--green-800);
  border: 1px solid var(--border-strong);
}
.btn--ghost:hover { background: var(--green-50); color: var(--green-900); border-color: var(--green-500); }

/* The hero's "Book a Site Visit" — the one secondary CTA that has to hold its
   own beside the glowing primary, where a plain outline read as an empty slot.
   Its own modifier and NOT a change to .btn--ghost, which is a dozen quiet
   buttons in the header, footer, auth pages and property detail that should
   stay exactly as they are.

   The ring is a gradient, which `border-color` cannot express. Two background
   layers do it: the white fill is painted to the PADDING box and the gradient
   to the BORDER box, so the gradient shows only in the 2px the fill does not
   cover. Green at the leading edge running into gold at the trailing one —
   the same pairing as the welcome block, so hero and section read as one
   palette rather than two ideas.

   Deliberately not a gold FILL: that is reserved for "Contact Us" as the one
   loudest button on the page (see .btn--accent). This borrows gold as an edge
   and an icon, which decorates without competing. */
.btn--outline-gradient {
  position: relative;
  overflow: hidden;
  /* Creates a stacking context, which is what lets the hover wash below sit at
     z-index -1: above this element's own background, still beneath the label. */
  isolation: isolate;
  color: var(--green-900);
  border: 2px solid transparent;
  background:
    linear-gradient(var(--surface), var(--surface)) padding-box,
    linear-gradient(100deg, var(--green-600) 0%, var(--gold-500) 100%) border-box;
  /* A resting lift, so it sits on the mint hero ground instead of dissolving
     into it the way a flat white pill did. */
  box-shadow: 0 2px 10px rgba(0, 40, 15, 0.08);
}
/* Gold, picking up the far end of the ring. gold-700 rather than the brighter
   golds because this sits on white at 5.2:1 — the icon is decorative, but a
   2.3:1 gold-600 would simply have looked washed out. */
.btn--outline-gradient .bi {
  color: var(--gold-700);
  transition: rotate var(--dur-exit) var(--ease-exit),
              scale  var(--dur-exit) var(--ease-exit);
}

/* The hover fill, as a fading LAYER rather than a background swap. The button's
   own background is a gradient and gradients do not interpolate, so setting a
   new one on :hover would snap. An overlay whose opacity animates gives the
   same result and actually eases.

   z-index -1 inside the isolate above puts it over the white fill but under
   the label, so the wash never touches the text. It runs mint into gold —
   the two ends of the ring, pulled inward across the face. */
.btn--outline-gradient::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(100deg, var(--green-50) 0%, var(--gold-50) 100%);
  opacity: 0;
  transition: opacity var(--dur-exit) var(--ease-exit);
}
.btn--outline-gradient:hover::before {
  opacity: 1;
  transition-duration: var(--dur-hover);
  transition-timing-function: var(--ease-settle);
}

/* The ring itself is left alone on purpose — same reason as the fill, except
   here there is no overlay trick to reach for, so it simply holds while the
   glow around it grows. box-shadow interpolates, so that part eases. */
.btn--outline-gradient:hover {
  color: var(--green-900);
  box-shadow: 0 6px 18px rgba(0, 40, 15, 0.16),
              0 0 24px rgba(245, 185, 33, 0.38);
}
.btn--outline-gradient:active { box-shadow: 0 0 20px rgba(245, 185, 33, 0.42); }

/* A gold shine crossing the button once per hover. Gated per rule 1 of the
   motion section: decoration lives inside no-preference so it cannot reach
   anyone who asked for less movement.
   overflow:hidden clips this to the padding box, so the shine sweeps the white
   interior and leaves the gradient ring itself untouched. The alpha stays low
   because the pseudo-element paints OVER the label — at 0.30 gold the dark
   green text stays perfectly readable as it passes. */
@media (prefers-reduced-motion: no-preference) {
  .btn--outline-gradient::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(100deg,
        transparent 30%,
        rgba(245, 185, 33, 0.30) 50%,
        transparent 70%);
    translate: -130% 0;
  }
  .btn--outline-gradient:hover::after {
    animation: kaani-btn-shine 700ms var(--ease-settle);
  }
  @keyframes kaani-btn-shine { to { translate: 130% 0; } }

  /* The calendar tips and grows, the way a real one would if you picked it up.
     Motion only — the icon's colour and size at rest are unchanged, so nothing
     here is load-bearing for anyone who never sees it. */
  .btn--outline-gradient:hover .bi {
    rotate: -8deg;
    scale: 1.12;
    transition-duration: var(--dur-hover);
    transition-timing-function: var(--ease-settle);
  }
}

/* Forced colours throws every background-image away, which would take the ring
   with it and leave `border: 2px solid transparent` — a button with no visible
   edge at all. Hand the border back to the system palette. */
@media (forced-colors: active) {
  .btn--outline-gradient {
    background: ButtonFace;
    border-color: ButtonBorder;
  }
  .btn--outline-gradient::before,
  .btn--outline-gradient::after { content: none; }
}

.btn--on-dark { background: var(--surface); color: var(--green-800); }
.btn--on-dark:hover { background: var(--green-50); color: var(--green-900); }

.btn--outline-light {
  background: transparent;
  color: var(--text-inverse);
  border: 2px solid rgba(255, 255, 255, 0.6);
}
.btn--outline-light:hover { background: rgba(255, 255, 255, 0.14); color: var(--text-inverse); border-color: var(--text-inverse); }

.btn--solid-dark { background: var(--green-800); color: var(--text-inverse); }
.btn--solid-dark:hover { background: var(--green-900); color: var(--text-inverse); }

.btn--whatsapp { background: var(--whatsapp); color: var(--whatsapp-fg); }
.btn--whatsapp:hover { background: var(--whatsapp-hover); color: var(--whatsapp-fg); }


/* --- 5.2 Header + navigation --------------------------------------------
   Mobile: off-canvas drawer. Desktop: inline row. One set of markup. */

.site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  background: var(--header-bg);
  /* This border is load-bearing now, not trim. The header used to be navy
     against a cream page, so its edge was free. Against a near-white page —
     and on the detail page, directly above a white breadcrumb and a white
     sticky section-nav — it is the ONLY thing separating the layers. */
  border-bottom: 1px solid var(--border);
}

/* Full width, not the 1200px container the rest of the page uses: the logo
   belongs on the left edge and the CTA on the right. It does mean the logo
   no longer lines up with the content below it — a deliberate trade. */
.site-header__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: var(--header-h);
  max-width: none;
}

.site-header__logo {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}
/* The asset is trimmed to the artwork, so this height IS the visible mark. */
.site-header__logo img {
  height: 34px;
  width: auto;
}

.nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  margin-right: calc(var(--space-2) * -1);
  /* Green, not ink: this is the drawer's only affordance on mobile and it
     should read as brand. It was white, which on a white header is nothing. */
  color: var(--green-800);
  font-size: 1.5rem;
  cursor: pointer;
  border-radius: var(--radius-sm);
}

.nav-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  color: var(--green-800);
  font-size: 1.25rem;
  cursor: pointer;
  border-radius: var(--radius-sm);
}

/* Drawer, closed by default on small screens */
.primary-nav {
  position: fixed;
  inset: 0 0 0 auto;
  width: min(20rem, 85vw);
  transform: translateX(100%);
  transition: transform 260ms var(--ease);
  z-index: var(--z-drawer);
  visibility: hidden;
}
.primary-nav.is-open { transform: translateX(0); visibility: visible; }

.primary-nav__panel {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--surface);
  overflow-y: auto;
  box-shadow: var(--shadow-lg);
}

/* Matches the header rather than opposing it: the drawer now slides out from
   a light bar into a light panel, so it reads as the header extending down.
   The border is what keeps the two from merging — head and panel are only
   #f9fcf8 against #ffffff, which is a 1.03:1 difference on its own. */
.primary-nav__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
  padding: var(--space-4);
  min-height: var(--header-h);
  background: var(--header-bg);
  border-bottom: 1px solid var(--border);
}

.primary-nav__title {
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--green-900);
}

.primary-nav__list,
.primary-nav__cta { padding-inline: var(--space-4); }
.primary-nav__cta { padding-bottom: var(--space-4); }

.primary-nav__list {
  display: flex;
  flex-direction: column;
  margin-top: var(--space-2);
}

.primary-nav__list a {
  display: flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: var(--space-3) var(--space-2);
  color: var(--text);
  font-size: var(--fs-md);
  font-weight: 600;
  text-decoration: none;
  border-bottom: 1px solid var(--border);
  /* There was no transition here at all: the hover state snapped on instantly,
     which is a large part of why the header felt abrupt. */
  transition: color            var(--dur-exit) var(--ease-exit),
              background-color var(--dur-exit) var(--ease-exit);
}
/* Green, not gold: "which page am I on" is a state, and state is green in
   this palette. Gold is reserved for accent and the one loud CTA. */
.primary-nav__list a:hover {
  color: var(--green-800);
  transition-duration: var(--dur-hover);
  transition-timing-function: var(--ease-settle);
}
.primary-nav__list a[aria-current="page"] { color: var(--green-800); }

.primary-nav__cta {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-5);
}

.nav-scrim {
  position: fixed;
  inset: 0;
  background: rgba(12, 38, 18, 0.5);
  z-index: var(--z-scrim);
}
.nav-scrim[hidden] { display: none; }

@media (min-width: 1024px) {
  .nav-toggle,
  .nav-close,
  .primary-nav__head,
  .nav-scrim { display: none; }

  .primary-nav {
    position: static;
    width: auto;
    transform: none;
    visibility: visible;
    z-index: auto;
    /* Take the rest of the bar so the account cluster's margin-left:auto has
       room to push against; min-width:0 lets the name below truncate instead
       of forcing the row wider than the header. */
    flex: 1 1 auto;
    min-width: 0;
  }
  .primary-nav__panel {
    flex-direction: row;
    align-items: center;
    gap: var(--space-4);
    height: auto;
    padding: 0;
    background: transparent;
    overflow: visible;
    box-shadow: none;
  }
  /* The drawer pads these; in the bar that padding is dead space that pushed
     the labels into wrapping. */
  .primary-nav__list,
  .primary-nav__cta { padding-inline: 0; padding-bottom: 0; }

  .primary-nav__list { flex-direction: row; gap: var(--space-1); margin-top: 0; }
  .primary-nav__list a {
    position: relative;              /* anchors the underline below */
    /* Label size and side padding move together: the tighter padding at this
       breakpoint is what pays for the larger type without the six links plus
       the account cluster overflowing a 1024px bar. */
    --nav-pad-x: var(--space-2);
    color: var(--text);
    border-bottom: none;
    padding-inline: var(--nav-pad-x);
    border-radius: var(--radius-sm);
    font-size: var(--fs-base);
    /* Two-word labels ("Buy Land", "Buyers Corner") must stay on one line —
       wrapping them is what made the bar look ragged. */
    white-space: nowrap;
  }
  .primary-nav__list a:hover { color: var(--green-900); background: var(--green-50); }
  /* The current page is a solid pill. It has to be a FILL rather than a tint:
     a tint on this header is ~1.05:1 and would not register as a shape at all.
     green-700, the brand green — white on it is 4.65:1. */
  .primary-nav__list a[aria-current="page"] { color: var(--text-inverse); background: var(--green-700); }

  /* Gold rule wipes in from the left under the label. Desktop only: in the
     mobile drawer these links already carry a border-bottom separator and the
     two would sit on top of each other.
     gold-700, not gold-400 — the light gold is 1.4:1 on this header, i.e.
     invisible. And this is HOVER ONLY now: on the current page the gold sat
     on the green pill at 1.04:1, which drew a phantom line inside it. The
     pill is the "current" signal; the rule is the "you're pointing at me" one. */
  .primary-nav__list a::after {
    content: "";
    position: absolute;
    left: var(--nav-pad-x);
    right: var(--nav-pad-x);
    bottom: 5px;
    height: 2px;
    border-radius: 2px;
    background: var(--gold-700);
    scale: 0 1;
    transform-origin: 0 50%;
    transition: scale var(--dur-exit) var(--ease-exit);
  }
  .primary-nav__list a:hover::after {
    scale: 1 1;
    transition-duration: var(--dur-hover);
    transition-timing-function: var(--ease-settle);
  }
  .primary-nav__list a[aria-current="page"]::after { scale: 0 1; }
  .primary-nav__cta { flex-direction: row; margin-top: 0; flex-shrink: 0; }
  .primary-nav__cta .btn--block { width: auto; }
  .primary-nav__cta .btn { white-space: nowrap; }
  .site-header__logo img { height: 42px; }
}

/* Wide screens have the room the 1024px bar does not, so the labels get their
   full size and their breathing room back. */
@media (min-width: 1280px) {
  .primary-nav__list a {
    --nav-pad-x: var(--space-3);
    font-size: var(--fs-md);
  }
}


/* --- 5.3 Footer ---------------------------------------------------------- */

/* Deepest surface on the site, one step below the CTA band that usually sits
   directly above it — otherwise the two dark greens meet as a single slab. */
.site-footer {
  background: var(--footer-bg);
  color: var(--text-on-dark-muted);
}

/* That separating margin is only wanted where the slab problem exists, i.e.
   where the page ends on the dark CTA band. On the four pages that end on a
   light .section (buyers, contact, create_sell, property_list) it was 48px of
   dead canvas on top of the section's own bottom padding. */
main:has(> .cta-band:last-child) + .site-footer { margin-top: var(--space-8); }

.site-footer__grid {
  display: grid;
  gap: var(--space-6);
  padding-block: var(--space-8);
}

.site-footer__logo {
  height: 46px;
  width: auto;
  margin-bottom: var(--space-3);
}

.site-footer__brand p { font-size: var(--fs-sm); max-width: 32ch; }

.site-footer__heading {
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-inverse);
  margin-bottom: var(--space-3);
}

.site-footer__col ul { display: flex; flex-direction: column; }

/* :not(.btn) so real buttons (e.g. the footer's "Send us a message" ghost
   button) keep their own navy-on-white styling instead of inheriting the
   near-white link color meant for plain text links on the dark footer. */
.site-footer__col a:not(.btn),
.site-footer__contact span {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  color: var(--text-on-dark-muted);
  text-decoration: none;
  font-size: var(--fs-sm);
}
.site-footer__col a:not(.btn):hover { color: var(--text-inverse); }
.site-footer__contact .btn { margin-top: var(--space-1); }

.site-footer__contact li { display: flex; }

.site-footer__social {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
.site-footer__social a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  border-radius: var(--radius-full);
  background: rgba(255, 255, 255, 0.1);
  font-size: 1.125rem;
}

/* The area links, which appear both in the footer and mid-page on the listing
   and location pages. One rule for both: the footer variant only has to invert
   its colours, which the block below it does. */
.area-links { padding-block: var(--space-6); }
.area-links__title {
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--space-4);
}
.area-links__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.area-links__list a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  background: var(--surface);
  color: var(--text-muted);
  font-size: var(--fs-sm);
  text-decoration: none;
}
.area-links__list a:hover {
  border-color: var(--green-600);
  color: var(--green-900);
}
/* The count is context, not the link's subject — quiet enough not to be read
   as part of the place name. */
.area-links__count {
  color: var(--text-subtle);
  font-variant-numeric: tabular-nums;
}

/* Footer variant: same shape, inverted for the dark band. */
.site-footer__areas { border-top: 1px solid rgba(255, 255, 255, 0.12); }
.site-footer__areas .area-links__title { color: var(--text-on-dark-muted); }
.site-footer__areas .area-links__list a {
  border-color: rgba(255, 255, 255, 0.16);
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-on-dark-muted);
}
.site-footer__areas .area-links__list a:hover {
  border-color: rgba(255, 255, 255, 0.4);
  color: var(--text-inverse);
}
.site-footer__areas .area-links__count { color: inherit; opacity: 0.7; }

.site-footer__bottom {
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  padding-block: var(--space-4);
  font-size: var(--fs-sm);
  text-align: center;
}

@media (min-width: 640px) {
  .site-footer__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
  .site-footer__grid { grid-template-columns: 2fr 1fr 1fr 1.4fr; }
}


/* --- 5.4 Hero + page hero ------------------------------------------------ */

/* Heroes and closers used to be one rule sharing one navy gradient. They are
   opposites now: a page OPENS light, on a mint wash, and CLOSES dark on green.
   That is what gives a page a top and a bottom again once the header — which
   used to supply all the contrast at the top — went white. */
/* Diagonal, not vertical: the wash has to be strongest at the top-left where
   the headline sits and clear away to white under the photo, or the mask that
   fades the photo in has nothing to fade into. */
.hero,
.page-hero {
  position: relative;
  overflow: hidden;
  /* Resolves to --surface-alt, the page canvas, NOT to white. Ending on white
     put a 1.02:1 step against the canvas, so the band had no perceptible
     bottom edge and its padding read as part of the gap below it. */
  background: linear-gradient(115deg,
      #dff7e4 0%, var(--green-50) 38%, var(--surface-alt) 82%);
  color: var(--text);
  isolation: isolate;
}

/* Photo variant, used by Sell Land.

   This used to be ONE flat 0.88 wash across the whole band, which meant the
   photo contributed 12% of every pixel and was barely there. The trap is that
   a flat wash has to be legible under the copy, so it gets set for the worst
   case and then that same opacity is applied everywhere — including the 80%
   of the band with no text on it at all.

   Split in two instead, and the two halves have different jobs:
     * the BAND wash below, at 0.42, exists only to keep the photo in the same
       colour family as the page. 58% of every pixel is now photo — 4.8x what
       it was — so the aerial actually reads.
     * a POOL, on .page-hero__inner, carries the contrast. It is near-opaque
       under the copy and fades out before the band's edges, so it costs the
       photo nothing where nothing is written.

   Worst case (a pure black pixel behind the copy): title and eyebrow 7.3:1,
   subtitle 6.9:1. At the pool's fade edge, 5.8:1 and 5.5:1.

   The stops are hardcoded because rgba() cannot take a hex token, so they MUST
   be updated by hand whenever the ramp moves — they match #dff7e4 /
   --green-50 / --surface-alt. */
.page-hero--photo {
  background-image:
    linear-gradient(115deg,
      rgba(223, 247, 228, 0.42) 0%,
      rgba(230, 250, 236, 0.40) 38%,
      rgba(246, 251, 246, 0.38) 82%),
    url("assets/aerial-land-2.cb37d47d05bd.jpg");
  background-size: cover;
  background-position: center;
}

/* The pool. Held at full strength out to 58% of the ellipse so every line of
   copy sits on the flat core, then stepped down rather than run straight to
   transparent — a single stop to 0 puts a visible hard ring where the falloff
   starts. It fades out inside the band, so the photo stays clean at the edges. */
.page-hero--photo .page-hero__inner {
  background: radial-gradient(ellipse 86% 82% at 50% 50%,
      rgba(246, 251, 246, 0.94) 0%,
      rgba(246, 251, 246, 0.94) 58%,
      rgba(246, 251, 246, 0.78) 80%,
      rgba(246, 251, 246, 0) 100%);
}

/* The eyebrow used to be recoloured green over a photo, because gold-700 type
   directly on the image was 3.5:1 even behind the wash. That is moot now: the
   label sits on its own opaque chip, so the photo behind it no longer touches
   its contrast, and it keeps the same gold-on-green it has everywhere else.

   The .hero-scoped drop-shadow glow that also lived here is gone for the same
   reason — the glow is the chip's box-shadow now, on every eyebrow rather than
   just the home hero's. */

@media (forced-colors: active) {
  /* Forced colours replace the chip's background and drop its shadow, which
     would leave light-yellow type on the system canvas. Hand the contrast back
     to a fill that stands up unaided. */
  .eyebrow {
    color: var(--gold-700);
    box-shadow: none;
  }
}

.cta-band {
  position: relative;
  overflow: hidden;
  background: linear-gradient(135deg, var(--green-900) 0%, var(--green-800) 100%);
  color: var(--text-inverse);
  isolation: isolate;
}

/* Layered CSS-only depth: a fine dot grid plus a soft bloom. Split from the
   dark version below because both stops have to invert — white dots and a
   pale bloom simply are not there on a mint ground. */
.hero__bg,
.page-hero__bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image:
    radial-gradient(circle, rgba(0, 92, 30, 0.07) 1px, transparent 1px),
    radial-gradient(ellipse 70% 60% at 75% 20%, rgba(0, 199, 64, 0.16) 0%, transparent 62%);
  background-size: 26px 26px, 100% 100%;
}

/* The dark closer keeps the original treatment, with one correction: the
   bloom used to be gold. Gold over navy sat near-complementary and mixed to
   a warm neutral; over green it mixes toward olive and reads as a stain.
   The bloom is now the light end of the green ramp instead. */
.cta-band__bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image:
    radial-gradient(circle, rgba(255, 255, 255, 0.06) 1px, transparent 1px),
    radial-gradient(ellipse 70% 60% at 75% 20%, rgba(34, 224, 92, 0.22) 0%, transparent 62%);
  background-size: 26px 26px, 100% 100%;
}

/* These paddings came down a step across the board when the band went light.
   The measurements never changed; their effect did. 80px of navy read as a
   deliberate dark band, so nobody saw it as space. The same 80px of near-white
   reads as a gap between the header and the page, on every page that has a
   hero — which is all of them. Light bands need less air than dark ones. */
/* Asymmetric on purpose. The top is what sits directly under the sticky
   header, so it is the value the reader perceives as "gap before the page
   starts" and it stays tight. The bottom can afford more — on the home page
   the category panel is positioned against it. */
.hero__inner,
.page-hero__inner {
  padding-top: var(--space-5);
  padding-bottom: var(--space-6);
  text-align: center;
}

.hero__title,
.page-hero__title {
  color: var(--green-900);
  font-size: var(--fs-hero);
  /* 800 not 900: Poppins runs optically larger and heavier than the
     Merriweather this replaced, and 900 at hero size closed up the counters. */
  font-weight: 800;
  margin-bottom: var(--space-4);
}

/* The headline in two greens — the bulk deep, one line bright. green-600 is
   3.16:1: legal for display type at this size and NOWHERE else, which is why
   this class is deliberately scoped to the title. It is the brightest step
   the ramp can put in front of a reader, so this is where the vividness of
   the palette actually shows up in type. */
.hero__title em,
.hero__title .hero__accent {
  font-style: normal;
  color: var(--green-600);
}

.page-hero__title { font-size: var(--fs-3xl); }

.page-hero__subtitle {
  color: var(--text-muted);
  font-size: var(--fs-md);
  max-width: 44ch;
  margin-inline: auto;
}

/* Split off .page-hero__subtitle, which it used to share: the interior pages
   still want a quiet muted line, and this one is now the hero's warm note. It
   takes the welcome block's lit-gold treatment — see .welcome__tagline, which
   carries the gradient and the forced-colors reset for both. The solid colour
   is declared here and unconditionally, so a browser that cannot clip a
   background to text shows solid gold rather than nothing. */
.hero__lead {
  font-family: var(--font-accent);
  font-size: var(--fs-xl);
  font-weight: 600;
  line-height: 1.35;
  max-width: 26ch;
  margin-inline: auto;
  text-wrap: balance;
  color: var(--gold-700);
  filter: var(--glow-text-gold);
}

.hero__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-6);
}

/* The hero's eyebrow, in everything but name — it took over that slot above
   the headline. The list itself is only a wrapping row now; the pill lives on
   each item, so the three promises read as three badges rather than as one
   long bar of text. */
.hero__proof {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
/* Each chip borrows .eyebrow wholesale: gold-400 on green-950 is 9.9:1, and
   the chip supplies the contrast the mint canvas could not. The left padding
   carries the same tracking compensation .eyebrow explains, at this rule's
   0.06em — 0.12em is too wide for three phrases that each carry an icon. */
.hero__proof li {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.4em 0.9em 0.4em calc(0.9em + 0.06em);
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--gold-400);
  background: var(--green-950);
  border-radius: var(--radius-full);
  box-shadow: var(--eyebrow-glow);
}
.hero__proof .bi { color: var(--gold-400); font-size: 1.1rem; }

.hero__media {
  position: relative;
  margin-top: var(--space-7);
}

/* The photo's own box, and the containing block for the marker and the pin.
   .hero__media cannot do that job below 900px: .hero__cats is a normal-flow
   sibling there, so .hero__media is photo-height PLUS slab-height and a
   bottom-anchored pin landed on the four category boxes rather than on the
   photo. .hero__media keeps its own position:relative — the slab still
   positions against it at 900px. */
.hero__frame { position: relative; }

.hero__photo {
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

/* The teardrop map marker over the photo. The shape is one element: three
   round corners and one square one, rotated 45deg so the square corner
   becomes the point. The glyph counter-rotates to sit upright inside it. */
/* Right-hand side below 900px, because the pin owns the bottom-left there and
   the two collided at 320px. The diagonal arrangement the reference draws is
   restored at 900px, where the photo is tall enough for both.

   44%, not the 16% this first tried: the aerial's top third is sky, so a
   marker high on the frame plants itself in the haze. It has to sit below the
   horizon, on the plotted land it is pointing at. */
.hero__marker {
  position: absolute;
  top: 44%;
  right: 8%;
  display: grid;
  place-items: center;
  /* A step smaller than it used to be, and it steps back up at 480px. At the
     narrow end the pin is ~180px of a 288px-wide photo, so the marker has
     little room to clear it and no need to be large. */
  width: 3rem;
  height: 3rem;
  /* Deepest step on the ramp, because the glyph is gold rather than white now:
     gold-500 on green-950 is 7.9:1, where the same gold on the green-700 this
     used to be is 2.7:1 and reads as a smudge. */
  background: var(--green-950);
  border: 3px solid var(--surface);
  border-radius: 50% 50% 50% 0;
  rotate: -45deg;
  box-shadow: var(--shadow-lg);
}
/* The same disc gold the category chips below carry, so the marker and the
   slab read as one accent rather than two. */
.hero__marker .bi {
  rotate: 45deg;
  color: var(--gold-500);
  font-size: 1.25rem;
  /* The glyph's optical centre sits low inside a teardrop, because the shape's
     mass is in the round half. Nudge it back up off the point. */
  margin-bottom: 2px;
}

@media (min-width: 480px) {
  .hero__marker { width: 3.75rem; height: 3.75rem; }
  .hero__marker .bi { font-size: 1.5rem; }
}

@media (min-width: 900px) {
  /* right:auto is not optional — leaving both offsets set makes the winner a
     function of writing direction rather than of this rule. */
  .hero__marker {
    top: 30%;
    right: auto;
    left: 58%;
    width: 4.5rem;
    height: 4.5rem;
  }
  .hero__marker .bi { font-size: 1.85rem; }
}

/* Floating locality card. Anchored bottom-left on mobile so it never covers
   the horizon, which is the part of the photo that carries the depth. */
.hero__pin {
  position: absolute;
  left: var(--space-4);
  bottom: var(--space-4);
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: 0 var(--space-2);
  padding: var(--space-3) var(--space-4);
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.hero__pin .bi {
  grid-row: span 2;
  color: var(--gold-600);
  font-size: 1.5rem;
}
/* The four land types, as the reference draws them: ONE solid green slab
   riding the bottom edge of the photo, with the four items separated only by
   hairlines. Gold discs on green is the palette's own accent pairing, and
   dark-green glyphs on gold rather than white ones because white on gold
   is 2.3:1.

   The fill is green-700 — the brand green, flat and opaque. The translucent
   glass fill that was here before is gone: even filtered, the aerial's greys
   and browns mixed into it and each item needed its own lighter tile to stay
   legible, which is what made the four read as separate washed-out boxes
   instead of one slab. Opaque green-700 carries WHITE labels at 4.84:1, so
   the items need no fill of their own at all. */
.hero__cats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* Back to a hairline gap now that the items are borderless: with dividers
     doing the separating, a wide gutter would only pull the slab apart. */
  gap: var(--space-1);
  margin-top: var(--space-4);
  padding: var(--space-3) var(--space-2);
  background: var(--green-700);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}
.hero__cats a {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-1);
  min-height: 0;
  /* White on the opaque green-700 slab: 4.84:1. */
  color: #fff;
  font-size: var(--fs-xs);
  font-weight: 700;
  line-height: 1.25;
  text-align: center;
  text-decoration: none;
  background: transparent;
  border-radius: var(--radius-sm);
  transition: background-color var(--dur-exit) var(--ease-exit);
}
/* Hover is the only fill any item gets — a whisper of white, well under the
   level at which it would start reading as a box again. */
.hero__cats a:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.12);
  transition-duration: var(--dur-hover);
  transition-timing-function: var(--ease-settle);
}

.hero__cat-icon {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-full);
  /* Gold discs on the green slab — the palette's accent pairing doing its
     job, and the one warm note in an otherwise all-green hero. */
  background: var(--gold-500);
  color: var(--green-950);
  font-size: 0.95rem;
  /* A soft drop shadow only. The 2px ink ring that used to be here was a
     contrast prop for the bright green-600 tile; on green-700 the disc reads
     cleanly, and the ring was the single most off-palette mark in the hero.
     Safe to drop because these glyphs are aria-hidden decoration — the text
     label underneath carries the meaning, so no graphic here has to clear
     3:1 on its own. */
  box-shadow: 0 2px 6px rgba(0, 40, 15, 0.35);
}
.hero__cat-icon--pale { background: var(--green-100); color: var(--green-900); }

/* Four across as soon as four fit, which is well below the 900px the desktop
   layout starts at — a 2x2 grid in a 530px-wide slab gives each item 257px to
   hold a 32px disc and a 13px label, and reads as four empty boxes.

   480px is a breakpoint this sheet does not otherwise use, and it is here
   because 640px — the next one up — would leave the whole 480-639px band on
   the 2x2. At 480px each cell gets ~105px, against the ~112px the 900px
   layout already ships inside the media column, so nothing here is tighter
   than a case that already works. */
@media (min-width: 480px) {
  .hero__cats { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .hero__cats a { font-size: var(--fs-sm); }
  /* The reference's hairline dividers. border-LEFT on all but the first, so
     the slab's own left and right edges stay clean — a border-right would
     have carved an extra line down the far edge of the panel. */
  .hero__cats a + a { border-left: 1px solid rgba(255, 255, 255, 0.22); }
}

/* 600, because this is --fs-xs (13px) over a photographic card — the smallest
   type on the page, and the colour lift alone did not carry it. */
.hero__pin-label { font-size: var(--fs-xs); font-weight: 600; color: var(--text-subtle); }
.hero__pin-place {
  font-family: var(--font-display);
  font-size: var(--fs-md);
  color: var(--green-900);
  /* The reference underlines the place name in gold. border-bottom rather
     than text-decoration so the rule sits clear of the descenders. */
  border-bottom: 2px solid var(--gold-500);
  justify-self: start;
}

@media (min-width: 640px) {
  .hero__inner { padding-top: var(--space-5); padding-bottom: var(--space-8); }
  /* The page-hero's bottom padding is the invisible half of the old gap: the
     band fades to white and the canvas is #f6fbf6, a 1.02:1 step, so this
     padding merged with the section's below it into one void. Kept short. */
  .page-hero__inner { padding-top: var(--space-6); padding-bottom: var(--space-5); }
  /* Wrap, never nowrap — this is what forced 358px of minimum width before. */
  .hero__actions { flex-direction: row; flex-wrap: wrap; justify-content: center; }
}

/* Two columns only once there is genuinely room for a 44ch measure beside a
   photo. Below this the stack order in the markup — copy, then image — is
   already the right reading order, so nothing needs reordering. */
@media (min-width: 900px) {
  .hero { position: relative; }

  /* Two tracks, but only the first is ever filled: the photo is positioned
     out of flow below, and the empty second track is what reserves room for
     it. Sizing the gap this way keeps the copy on a readable measure instead
     of letting it run under the image. */
  .hero__inner {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
    align-items: center;
    gap: var(--space-7);
    /* Just tall enough for the copy column plus the type panel riding the
       photo's bottom edge. Any taller and align-items:center splits the
       surplus above and below the copy, which puts the gap straight back. */
    min-height: 33rem;
    text-align: left;
  }
  .hero__copy {
    position: relative;
    z-index: 1;                    /* keeps the copy above the photo's fade */
    text-align: left;
  }
  .hero__lead { margin-inline: 0; }
  .hero__proof { justify-content: flex-start; }
  .hero__actions { justify-content: flex-start; }

  /* The photo leaves the 1200px container and runs to the viewport's right
     edge, exactly as the reference does. It is not a card here: no radius, no
     shadow, no edge — a mask dissolves its left side into the mint so the two
     meet as one surface rather than as a picture pasted onto a background. */
  .hero__media {
    position: absolute;
    inset-block: 0;
    left: 47%;
    right: 0;
    margin: 0;
  }
  /* Fills the media box, so the photo's height:100% below has something to
     resolve against and the pin's 24% measures the same box it always did. */
  .hero__frame { height: 100%; }
  .hero__photo {
    width: 100%;
    height: 100%;
    aspect-ratio: auto;
    object-fit: cover;
    border-radius: 0;
    box-shadow: none;
    /* 16%, not 28%: a wider ramp washed out most of the plotted land, which is
       the whole subject. Just enough to kill the hard edge. */
    -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 16%);
            mask-image: linear-gradient(to right, transparent 0%, #000 16%);
  }

  /* Sits down over the photo rather than up at the header, so the card, the
     marker and the type panel read as three stops down a diagonal instead of
     crowding the top edge. */
  .hero__pin {
    top: 24%;
    bottom: auto;
    left: 8%;
  }

  /* Rides the bottom edge of the photo and runs off to the right, as in the
     reference. Pulled left of the media box so it overlaps the fade and ties
     the panel to the copy column. */
  /* Sits wholly inside the media column and stops short of the viewport edge.
     It previously overhung to -6%, which put a heavy green slab right beside
     the hero buttons and made the two compete. */
  /* Positioning only. The four columns, the label size and the dividers all
     come from the 480px block above and are not restated here. */
  .hero__cats {
    position: absolute;
    left: 4%;
    right: var(--space-6);
    bottom: var(--space-6);
    margin-top: 0;
    padding: var(--space-3) var(--space-2);
  }
}


/* --- 5.5 Trust strip ----------------------------------------------------- */

.trust-strip {
  background: transparent;
  border-bottom: 1px solid var(--border);
}

.trust-strip__inner {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-3) var(--space-4);
  padding-block: var(--space-5);
}

.trust-strip__item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--green-950);
}
/* Green, not the gold-700 these were: this row sits on the open canvas with no
   chip behind it, and gold bright enough to read as bright is ~2:1 there. The
   gold that does look gold needs a dark ground — which is what .hero__cat-icon
   and .hero__proof li give it. Green-800 is 6.2:1 where gold-700 was 4.99:1. */
.trust-strip__item i { color: var(--green-800); font-size: 1.125rem; flex-shrink: 0; }

@media (min-width: 900px) {
  .trust-strip__inner { grid-template-columns: repeat(4, 1fr); }
}


/* --- 5.6 Property card + grid -------------------------------------------- */

/* Two columns from the smallest screen up. A single full-width column made one
   4:3 card eat an entire phone viewport, so nothing read as a browsable list. */
.p-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-3);
}

@media (min-width: 640px) { .p-grid { gap: var(--space-5); } }
@media (min-width: 1024px) { .p-grid { grid-template-columns: repeat(3, 1fr); } }

.p-card {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  /* Base = leaving; the hover rule speeds up the way in. See the motion tokens. */
  transition: transform  var(--dur-exit) var(--ease-exit),
              box-shadow var(--dur-exit) var(--ease-exit);
}
.p-card:hover {
  transform: translateY(-6px);
  box-shadow: var(--shadow-lg);
  transition-duration: var(--dur-hover);
  transition-timing-function: var(--ease-settle);
}

.p-card__media { position: relative; aspect-ratio: 4 / 3; background: var(--surface-sunken); }
.p-card__media img { width: 100%; height: 100%; object-fit: cover; }

.p-card__media-fallback,
.gallery__fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  color: var(--text-placeholder);
  font-size: 2rem;
  background: var(--surface-sunken);
}

.p-card__badge {
  position: absolute;
  top: var(--space-3);
  left: var(--space-3);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-full);
  background: var(--gold-500);
  color: var(--green-950);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
}
.property-badge--sell { background: var(--green-800); color: var(--text-inverse); }

.p-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  flex: 1;
}

.p-card__title { font-size: var(--fs-lg); }
.p-card__title a { color: var(--green-900); text-decoration: none; }
.p-card__title a:hover { color: var(--gold-700); }

.p-card__location {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

.p-card__facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.p-card__facts li {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-full);
  background: var(--surface-sunken);
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

.p-card__approvals { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.p-card__approvals li {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--success-fg);
  font-size: var(--fs-sm);
  font-weight: 600;
}

.p-card__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: auto;
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}

.p-card__price {
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: 900;
  color: var(--green-900);
}
/* Its own line: on a half-width phone card the digits already fill the row,
   and letting this wrap inline broke the price across two lines mid-number. */
.p-card__price-words {
  display: block;
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--text-muted);
}

/* Half-width cards on phones: the photo carries the card, so the body keeps
   only what someone scanning a list needs — title, place, size, price. The
   title clamps to two lines so a long one cannot stretch its row's height,
   and the foot stacks because price + button will not sit side by side in
   ~150px. Everything comes back at 640px, where the same two columns are
   wide enough to hold the full card. */
@media (max-width: 639.98px) {
  .p-card__body { padding: var(--space-3); gap: var(--space-1); }

  .p-card__title {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-size: var(--fs-base);
    line-height: 1.3;
  }

  .p-card__location {
    font-size: var(--fs-xs);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .p-card__location i { flex: none; }

  /* One fact chip is enough at this width; a second wraps to its own row. */
  .p-card__facts { margin-top: var(--space-1); }
  .p-card__facts li { padding: 2px var(--space-2); font-size: var(--fs-xs); }
  .p-card__facts li + li,
  .p-card__approvals { display: none; }

  .p-card__foot {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2);
    padding-top: var(--space-2);
  }
  .p-card__price { font-size: var(--fs-base); }
  .p-card__foot .btn {
    width: 100%;
    padding-inline: var(--space-3);
    font-size: var(--fs-sm);
  }

  .p-card__badge { top: var(--space-2); left: var(--space-2); padding: 2px var(--space-2); }
}


/* --- 5.7 Listing layout + filters ---------------------------------------- */

.listing-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}

.listing-toolbar__summary { display: flex; align-items: center; flex-wrap: wrap; }
.listing-toolbar__count { color: var(--text-muted); font-size: var(--fs-sm); }
.listing-toolbar__count strong { color: var(--green-900); font-size: var(--fs-md); }
.listing-toolbar__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }

/* Sits next to the count it undoes, not in the bar. Inline-flex with the tap
   minimum so it is a real target on a phone despite reading as a text link. */
.listing-toolbar__clear {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  margin-left: var(--space-3);
  font-weight: 700;
}

/* The sort control. Its own GET form so it can live here rather than in the
   filter bar; the hidden inputs in the markup are what stop it wiping the
   active filters. */
.sort-form { display: flex; align-items: center; gap: var(--space-2); }
.sort-form .input--select { min-width: 11rem; }

/* main.js submits on change and hides this. It is the no-JS path, so it must
   NOT be display:none here — only the script may take it away. */
.sort-form__go { flex: none; }

.filters-trigger__count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5rem;
  height: 1.5rem;
  padding-inline: var(--space-1);
  border-radius: var(--radius-full);
  background: var(--gold-500);
  color: var(--green-950);
  font-size: var(--fs-xs);
}

/* The bar, not a sidebar. It spans the results it filters and sits directly
   above them, which is where the reference puts it and where the eye looks
   after reading the page hero. */
.filters {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  margin-bottom: var(--space-5);
}

.filters__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-4);
}
.filters__title { font-size: var(--fs-md); margin: 0; }

/* The close button only makes sense while the sheet is open. */
.filters__close { display: none; }

.filters__body { display: grid; gap: var(--space-4); }

/* .filters__actions is a CHILD of .filters__body, so on wide screens the Apply
   button is simply the last cell of the same grid and lines up with the fields
   beside it instead of hanging below them. */
.filters__actions { display: grid; gap: var(--space-3); }

@media (min-width: 640px) {
  /* auto-fit, not a fixed count: the panel has five cells including the button,
     and the number that fit is a function of the viewport, not of a guess. */
  .filters__body {
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    /* Each field is label-over-input of differing height (the two range
       fieldsets are taller). end-alignment puts every control on one line. */
    align-items: end;
  }
}

/* Bottom-sheet presentation, applied by JS on small screens only.
   Without JS the form simply stays inline and fully usable. */
.filters.is-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  max-height: 85vh;
  overflow-y: auto;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  padding-bottom: calc(var(--space-5) + env(safe-area-inset-bottom));
  transform: translateY(100%);
  transition: transform 260ms var(--ease);
  z-index: var(--z-drawer);
  box-shadow: var(--shadow-lg);
}
.filters.is-sheet.is-open { transform: translateY(0); }
.filters.is-sheet .filters__close { display: inline-flex; align-items: center; justify-content: center; width: var(--tap-min); height: var(--tap-min); }

/* A sheet is a vertical surface. Undo the horizontal grid the 640px rule above
   would otherwise hand it on a wide phone or a small tablet. */
.filters.is-sheet .filters__body {
  grid-template-columns: 1fr;
  align-items: stretch;
}
.filters.is-sheet .filters__actions { margin-top: var(--space-2); }


/* --- 5.8 Forms ----------------------------------------------------------- */

.field { display: flex; flex-direction: column; gap: var(--space-2); }

.field__label {
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--green-900);
}
/* gold-700, not gold-500: this asterisk is meaningful text, and gold-500 is
   2.3:1 on a white field label. */
.field__required { color: var(--gold-700); }

.input {
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  /* 16px prevents iOS Safari from zooming the viewport on focus. */
  font-size: 1rem;
  color: var(--text);
  transition: border-color 160ms var(--ease), box-shadow 160ms var(--ease);
}
/* A focused field's border IS the state indicator, so it owes 3:1 — which
   rules out the bright steps this looks like it wants. green-800 is 6.45:1
   and is also --focus, so a focused field matches every other ring on the
   site. The halo below is green-500: a 3px spread is decoration and carries
   no contrast duty of its own, so that is where the brightness can go. */
.input:focus {
  outline: none;
  border-color: var(--green-800);
  box-shadow: 0 0 0 3px rgba(0, 199, 64, 0.30);
}
.input::placeholder { color: var(--text-placeholder); }

.input--textarea { min-height: 7rem; resize: vertical; }

.input--select {
  appearance: none;
  padding-right: var(--space-8);
  /* The fill is URL-encoded (%23 == #), so a hex search will never find it.
     Kept matched to --text-subtle by hand for exactly that reason — it is a
     control affordance, not a placeholder, so it tracks the text and NOT
     --text-placeholder. Moved with the token when that went to #3a4a3f. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%233a4a3f' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-4) center;
}

.input--file { padding: var(--space-2); }

/* Weight and colour follow the welcome block; the SIZE deliberately does not.
   .input is pinned at 16px to stop iOS zooming on focus, and help text larger
   than the value it explains inverts the hierarchy. */
.field__help { font-size: var(--fs-sm); font-weight: 500; color: var(--text-muted); }
/* Django hands some help text over as a <ul> — the password rules on signup
   are four separate items. The global reset strips list markers, so restore
   them here; four unmarked lines read as one run-on paragraph. */
.field__help ul {
  list-style: disc;
  padding-left: 1.1em;
  margin: 0;
}
.field__help li { margin-block: 0.15em; }

.field__errors { color: var(--error-fg); font-size: var(--fs-sm); }
.field--invalid .input { border-color: var(--error-border); }

.field--group { border: none; padding: 0; margin: 0; }
.field__row { display: flex; align-items: center; gap: var(--space-2); }
.field__sep { color: var(--text-subtle); flex-shrink: 0; }

.field-pair { display: grid; gap: var(--space-4); }

.fieldset { border: none; padding: 0; margin: 0; }
.fieldset + .fieldset { margin-top: var(--space-6); }
.fieldset__legend {
  font-family: var(--font-display);
  font-size: var(--fs-md);
  font-weight: 700;
  color: var(--green-900);
  margin-bottom: var(--space-4);
  padding: 0;
}
.fieldset > .field + .field,
.fieldset > .field + .field-pair,
.fieldset > .field-pair + .field { margin-top: var(--space-4); }

.form-errors {
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius);
  background: var(--error-bg);
  color: var(--error-fg);
  font-size: var(--fs-sm);
}

@media (min-width: 640px) {
  .field-pair { grid-template-columns: 1fr 1fr; }
}


/* --- 5.9 Flash messages -------------------------------------------------- */

.flash-list { display: grid; gap: var(--space-3); margin-top: var(--space-5); }

.flash {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4);
  border-radius: var(--radius);
  font-size: var(--fs-sm);
}
.flash i { flex-shrink: 0; font-size: 1.125rem; }
.flash--success { background: var(--success-bg); color: var(--success-fg); }
.flash--error   { background: var(--error-bg);   color: var(--error-fg); }
.flash--info,
.flash--warning { background: var(--info-bg);    color: var(--info-fg); }


/* --- 5.10 Cards, features, steps ----------------------------------------- */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
}
.card--flush { padding: 0; overflow: hidden; }
.card__title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--fs-lg);
  margin-bottom: var(--space-4);
}
/* The Vision/Mission pair — the palette expressing itself as a pair of
   rules, the accent beside the brand. */
.card--accent-gold  { border-top: 4px solid var(--gold-500); }
.card--accent-green { border-top: 4px solid var(--green-800); }

.feature-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-4); }

/* A column, so the tick row below can be pushed to the bottom edge. The grid
   already stretches every card in a row to the tallest one; without this the
   cards matched in height but their CONTENTS did not, because a heading that
   wraps to two lines and a longer paragraph walk the tick row down the card.
   Four cards then showed four different tick baselines over four different
   amounts of trailing white space, which is what read as misalignment. */
.feature-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-top: 4px solid var(--gold-500);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  transition: transform  var(--dur-exit) var(--ease-exit),
              box-shadow var(--dur-exit) var(--ease-exit);
}
/* auto margin eats the surplus, so the ticks sit on one line across the row
   however much copy is above them. space-4 is the floor when a card is the
   tall one and there is no surplus left to absorb. */
.feature-card .tick-list {
  margin-top: auto;
  padding-top: var(--space-4);
}
.feature-card:hover {
  transform: translateY(-6px);
  box-shadow: var(--shadow);
  transition-duration: var(--dur-hover);
  transition-timing-function: var(--ease-settle);
}
.feature-card h3 { margin-bottom: var(--space-2); }
/* The welcome block's weight and colour, one size down from its --fs-md: four
   of these sit side by side at 1024px, where 17px would set about 30
   characters to the line and read as a column of fragments. */
.feature-card p { color: var(--text-muted); font-size: var(--fs-base); font-weight: 500; }

.feature-card__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  border-radius: var(--radius);
  background: var(--gold-50);
  color: var(--gold-700);
  font-size: 1.375rem;
  margin-bottom: var(--space-4);
}

.feature-card__num {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  font-weight: 900;
  /* gold-600, not gold-500: 3.78:1 on white clears the large-text bar,
     where gold-500 sits at 2.77:1. (The original was 1.48:1 - invisible.) */
  color: var(--gold-600);
  line-height: 1;
  margin-bottom: var(--space-3);
}

@media (min-width: 640px) {
  .feature-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
  .feature-grid { grid-template-columns: repeat(3, 1fr); }
  .feature-grid--four { grid-template-columns: repeat(4, 1fr); }
}

/* Steps — stacked on mobile, never a squeezed 5-column row. */
.steps { display: grid; grid-template-columns: 1fr; gap: var(--space-4); counter-reset: step; }

.step {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
}
.step__num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius-full);
  background: var(--green-800);
  color: var(--text-inverse);
  font-weight: 800;
  margin-bottom: var(--space-3);
}
.step h3 { margin-bottom: var(--space-2); }
.step p { color: var(--text-muted); font-size: var(--fs-base); font-weight: 500; }
.step__link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  font-weight: 700;
  font-size: var(--fs-sm);
  text-decoration: none;
}

@media (min-width: 640px) { .steps { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) {
  .steps { grid-template-columns: repeat(4, 1fr); }
  .steps--five { grid-template-columns: repeat(5, 1fr); }
}


/* --- 5.11 Pillars, chips, lists ------------------------------------------ */

.pillars {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-3);
  margin-top: var(--space-6);
}
.pillar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4);
  border-radius: var(--radius);
  background: var(--surface);
  border: 1px solid var(--border);
}
.pillar i { font-size: 1.5rem; color: var(--gold-700); flex-shrink: 0; }
.pillar__num {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 900;
  color: var(--green-900);
  flex-shrink: 0;
}
.pillar__label { font-size: var(--fs-sm); font-weight: 600; color: var(--text-muted); }

@media (min-width: 768px) { .pillars { grid-template-columns: repeat(3, 1fr); } }

.chip-list { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-full);
  background: var(--gold-50);
  color: var(--green-900);
  font-size: var(--fs-sm);
  font-weight: 600;
}

.tick-list { display: grid; gap: var(--space-3); }
.tick-list li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  color: var(--text-muted);
  font-size: var(--fs-base);
  font-weight: 500;
}
.tick-list i { color: var(--gold-700); flex-shrink: 0; margin-top: 0.2em; }

.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  font-weight: 700;
  text-decoration: none;
  transition: gap 180ms var(--ease);
}
.link-arrow:hover { gap: var(--space-3); }

/* Settings rows on the account page: label, current value, chevron. Separators
   between rows rather than around them, so the list sits flush inside .card
   without doubling its border. */
.account-links li + li { border-top: 1px solid var(--border); }
.account-links a {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap-min);
  padding: var(--space-3) var(--space-2);
  border-radius: var(--radius-sm);
  color: var(--text);
  text-decoration: none;
}
.account-links a:hover { background: var(--green-50); color: var(--green-900); }
.account-links__label { font-weight: 600; }
/* margin-left:auto pushes the value and chevron right; the value truncates so a
   long address cannot shove the chevron off the row on a narrow screen. */
.account-links__value {
  margin-left: auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-subtle);
  font-size: var(--fs-sm);
}
.account-links .bi-chevron-right { flex-shrink: 0; color: var(--text-subtle); }

.pullquote {
  margin-block: var(--space-4);
  padding-left: var(--space-4);
  border-left: 4px solid var(--gold-500);
  font-family: var(--font-display);
  font-style: italic;
  color: var(--green-900);
}

.notice {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4);
  margin-bottom: var(--space-5);
  border-radius: var(--radius);
  background: var(--info-bg);
  color: var(--info-fg);
  font-size: var(--fs-sm);
}
.notice i { flex-shrink: 0; font-size: 1.125rem; }


/* --- 5.12 Empty state ---------------------------------------------------- */

.empty-state {
  text-align: center;
  padding: var(--space-8) var(--space-5);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--surface);
}
.empty-state__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 4rem;
  height: 4rem;
  border-radius: var(--radius-full);
  background: var(--gold-50);
  color: var(--gold-700);
  font-size: 1.75rem;
  margin-bottom: var(--space-4);
}
.empty-state h2, .empty-state h3 { margin-bottom: var(--space-2); }
.empty-state p {
  color: var(--text-muted);
  font-size: var(--fs-sm);
  max-width: 44ch;
  margin: 0 auto var(--space-5);
}


/* --- 5.13 Carousel ------------------------------------------------------- */

.carousel { position: relative; }

.carousel__viewport { overflow: hidden; border-radius: var(--radius-lg); }

.carousel__track {
  display: flex;
  transition: transform 400ms var(--ease);
}

.carousel__slide { flex: 0 0 100%; min-width: 0; }

.hotspot { position: relative; }
.hotspot img {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}
.hotspot__caption {
  position: absolute;
  inset: auto 0 0 0;
  padding: var(--space-6) var(--space-4) var(--space-4);
  background: linear-gradient(to top, rgba(12, 38, 18, 0.9), transparent);
  color: var(--text-inverse);
}
.hotspot__caption h3 { color: var(--text-inverse); margin-bottom: var(--space-1); }
.hotspot__caption p { font-size: var(--fs-sm); color: var(--text-on-dark-muted); }

/* Arrows sit below the image on mobile so they never cover the caption,
   and never shrink below the 44px target. */
.carousel__arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  border-radius: var(--radius-full);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  color: var(--green-900);
  font-size: 1.125rem;
  cursor: pointer;
  transition: background-color 180ms var(--ease), color 180ms var(--ease);
}
.carousel__arrow:hover { background: var(--gold-500); color: var(--green-950); border-color: var(--gold-500); }

.carousel__arrow--prev,
.carousel__arrow--next { position: absolute; top: calc(50% - var(--tap-min) / 2); }
.carousel__arrow--prev { left: var(--space-2); }
.carousel__arrow--next { right: var(--space-2); }

.carousel__dots {
  display: flex;
  justify-content: center;
  gap: var(--space-1);
  margin-top: var(--space-4);
}
.carousel__dot {
  /* 44px hit area with a small visible dot drawn inside it. */
  width: var(--tap-min);
  height: var(--tap-min);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  border-radius: var(--radius-full);
}
.carousel__dot::before {
  content: "";
  width: 10px;
  height: 10px;
  border-radius: var(--radius-full);
  background: var(--border-strong);
  transition: background-color 180ms var(--ease), transform 180ms var(--ease);
}
/* Green, not gold: "active" means one thing site-wide now, and it is green. */
/* This dot is the only thing saying which slide you are on, so it is a UI
   element owing 3:1 — green-600 clears it at 3.16:1. */
.carousel__dot.is-active::before { background: var(--green-600); transform: scale(1.3); }


/* --- 5.14 Property detail ------------------------------------------------ */

.breadcrumb { padding-block: var(--space-4); }
.breadcrumb ol { display: flex; flex-wrap: wrap; gap: var(--space-2); font-size: var(--fs-sm); }
.breadcrumb li { display: flex; align-items: center; gap: var(--space-2); color: var(--text-subtle); }
.breadcrumb li + li::before { content: "/"; color: var(--border-strong); }
.breadcrumb a { color: var(--text-muted); text-decoration: none; }
.breadcrumb a:hover { color: var(--gold-700); }

.detail__layout { display: grid; gap: var(--space-6); padding-bottom: var(--space-8); }
.detail__main { display: grid; gap: var(--space-6); }

.gallery__stage { border-radius: var(--radius-lg); overflow: hidden; background: var(--surface-sunken); }
.gallery__stage img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; }
.gallery__fallback { aspect-ratio: 3 / 2; }

.gallery__thumbs {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
  overflow-x: auto;
  padding-bottom: var(--space-2);
  scroll-snap-type: x mandatory;
}
.gallery__thumb {
  flex: 0 0 auto;
  width: 5rem;
  border-radius: var(--radius-sm);
  overflow: hidden;
  border: 2px solid transparent;
  cursor: pointer;
  scroll-snap-align: start;
}
.gallery__thumb img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; }
/* Same reasoning as the carousel dot: the border IS the selected state. */
.gallery__thumb.is-active { border-color: var(--green-600); }

.detail__title { margin-bottom: var(--space-2); }
.detail__location {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--text-muted);
  margin-bottom: var(--space-3);
}
.detail__price {
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  font-weight: 900;
  color: var(--green-900);
}
/* "1.08 Crore" under the digits. Quieter than the figure it restates —
   it is a reading aid, not a second price. */
.detail__price-words {
  margin-top: var(--space-1);
  font-family: var(--font-sans);
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--text-muted);
}
.detail__price-note {
  display: inline-block;
  margin-left: var(--space-2);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-full);
  background: var(--success-bg);
  color: var(--success-fg);
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  font-weight: 700;
  vertical-align: middle;
}

.approvals {
  padding: var(--space-4);
  border-radius: var(--radius-lg);
  background: var(--success-bg);
  border: 1px solid var(--success-border);
}
.approvals__title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--fs-md);
  color: var(--success-fg);
  margin-bottom: var(--space-3);
}
.approvals__list { display: grid; gap: var(--space-2); }
.approvals__list li {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--success-fg);
  font-size: var(--fs-sm);
  font-weight: 600;
}

.detail__section h2 { font-size: var(--fs-xl); margin-bottom: var(--space-4); }

.spec-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-3); }
.spec {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--border);
}
.spec dt { color: var(--text-subtle); font-size: var(--fs-sm); }
.spec dd { font-weight: 700; color: var(--green-900); text-align: right; font-size: var(--fs-sm); }

.map-embed {
  position: relative;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface-sunken);
  aspect-ratio: 4 / 3;
}
.map-embed iframe { width: 100%; height: 100%; border: 0; }
/* Sits under the map, and stands alone as the whole location block when there
   is no embed URL and no coordinates to build one from. */
.map-embed__link { margin-top: var(--space-3); }

/* Caveats under the gallery and the map: photos are indicative, the map is an
   area not a boundary. Laid out like .signin-note (5.18) but deliberately
   quieter — that note is gold and already on this page inside the enquiry
   card, and two panels competing for the same alarm would flatten both. */
.detail-note {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  margin-top: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius);
  background: var(--surface-sunken);
  color: var(--text-muted);
  font-size: var(--fs-sm);
}
.detail-note .bi { flex-shrink: 0; font-size: 1.125rem; }

/* The generated opening paragraph on a location landing page. Wider measure
   than .prose gets elsewhere would hurt it — this is one paragraph of facts,
   and it should read as an introduction, not as body copy. */
.area-intro {
  max-width: 46rem;
  margin-bottom: var(--space-6);
}

.enquiry-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-top: 4px solid var(--gold-700);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  scroll-margin-top: calc(var(--header-h) + var(--space-4));
}
.enquiry-card__title { font-size: var(--fs-lg); margin-bottom: var(--space-2); }
.enquiry-card__lead { color: var(--text-muted); font-size: var(--fs-sm); margin-bottom: var(--space-4); }
.enquiry-form { display: grid; gap: var(--space-4); }
.enquiry-card__or {
  text-align: center;
  color: var(--text-subtle);
  font-size: var(--fs-sm);
  margin-block: var(--space-4) var(--space-3);
}
.enquiry-card__direct { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }

@media (min-width: 640px) {
  .spec-grid { grid-template-columns: repeat(2, 1fr); gap: var(--space-3) var(--space-6); }
  .map-embed { aspect-ratio: 16 / 9; }
  .gallery__thumb { width: 6.5rem; }
}

@media (min-width: 1024px) {
  .detail__layout { grid-template-columns: 1fr 22rem; align-items: start; }
  /* Clears the section nav, which now sticks above it. Without the extra
     offset the enquiry card slides underneath the nav bar as you scroll. */
  .detail__aside { position: sticky; top: calc(var(--header-h) + 4rem); }
}


/* --- 5.15 Sticky mobile CTA ---------------------------------------------- */

.sticky-cta {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--z-sticky-cta);
  display: flex;
  gap: var(--space-2);
  padding: var(--space-3) var(--gutter);
  padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom));
  background: var(--surface);
  border-top: 1px solid var(--border);
  box-shadow: 0 -4px 16px rgba(0, 40, 15, 0.10);
}
.sticky-cta .btn { flex-shrink: 0; }
.sticky-cta .btn--block { flex: 1; }
.sticky-cta .btn:not(.btn--block) { width: var(--tap-min); padding-inline: 0; }

/* Only the detail page gets the bar, and only below the desktop breakpoint. */
.page-detail { padding-bottom: 5.5rem; }

@media (min-width: 1024px) {
  .sticky-cta { display: none; }
  .page-detail { padding-bottom: 0; }
}


/* --- 5.16 Contact + founder layouts -------------------------------------- */

.contact-layout { display: grid; gap: var(--space-6); }
.contact-layout__aside { display: grid; gap: var(--space-5); align-content: start; }

.contact-list { display: grid; gap: var(--space-4); }
.contact-list li { display: flex; align-items: flex-start; gap: var(--space-3); }
.contact-list i { color: var(--gold-700); font-size: 1.125rem; flex-shrink: 0; margin-top: 0.15em; }
.contact-list__label {
  display: block;
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--green-900);
}
.contact-list a { text-decoration: none; }
.contact-list a:hover { text-decoration: underline; }

.founder { display: grid; gap: var(--space-5); align-items: center; }
.founder__portrait { border-radius: var(--radius-lg); overflow: hidden; background: var(--surface-sunken); }
/* Founder_img is a tall 900x1355 portrait: centre-cropping cuts the head off,
   so anchor the crop to the top of the frame. */
.founder__portrait img {
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  object-position: 50% top;
}
.founder__portrait--round {
  width: min(12rem, 60%);
  margin-inline: auto;
  border-radius: var(--radius-full);
  border: 4px solid var(--surface);
  box-shadow: var(--shadow);
}
/* The round variant is fed founder-avatar.jpg, which is already a square
   head-and-shoulders crop — so it centres, rather than inheriting the top
   anchoring the tall portrait above needs. */
.founder__portrait--round img {
  aspect-ratio: 1 / 1;
  object-position: center;
}
.founder__name { margin-bottom: var(--space-1); }
.founder__credential {
  color: var(--gold-700);
  font-weight: 700;
  font-size: var(--fs-sm);
  margin-bottom: var(--space-4);
}
.founder__body > p { color: var(--text-muted); margin-bottom: var(--space-4); }

@media (min-width: 768px) {
  .contact-layout { grid-template-columns: 1.4fr 1fr; align-items: start; }
  .founder { grid-template-columns: 20rem 1fr; gap: var(--space-8); }
  .founder--teaser { grid-template-columns: 14rem 1fr; }
  .founder__portrait--round { width: 100%; }
}


/* --- 5.17 CTA band ------------------------------------------------------- */

.cta-band__inner {
  text-align: center;
  padding-block: var(--space-8);
}
.cta-band__title { color: var(--text-inverse); margin-bottom: var(--space-3); }
.cta-band__lead { color: var(--text-on-dark-muted); margin-bottom: var(--space-6); }
.cta-band__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  align-items: stretch;
}

@media (min-width: 640px) {
  .cta-band__actions { flex-direction: row; flex-wrap: wrap; justify-content: center; }
}


/* --- 5.18 Sign-in note ---------------------------------------------------- */
/* Sits above the submit button of an enquiry / contact / buyer-interest form
   when nobody is signed in. The form is filled in first and the account asked
   for on submit, so this is a quiet heads-up inside the form rather than a
   panel standing in front of it. Margin is 0: both .stack and .enquiry-form
   own the spacing between their children. */

.signin-note {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius);
  background: var(--info-bg);
  color: var(--info-fg);
  font-size: var(--fs-sm);
}
.signin-note .bi { flex-shrink: 0; font-size: 1.125rem; }

/* Inherit rather than take the page's link green: gold-700 on gold-50 is the
   pair this panel is already checked against, and the underline is what marks
   these as links. */
.signin-note a {
  color: inherit;
  font-weight: 600;
  text-decoration: underline;
}
.signin-note a:hover { color: var(--green-900); }


/* --- 5.19 Account pages --------------------------------------------------- */
/* Login, signup, password reset and email confirmation. Those pages are
   rendered by django-allauth through main/templates/allauth/, which reuses
   .card, .field, .input and .btn — only the few classes below are new. */

/* Green backdrop, matching .cta-band exactly so the account pages sit in the
   same visual world as the rest of the site. This used to note that the navy
   header met it seamlessly — no longer true, and not a loss: the white header
   now gives the band a clean top edge instead of dissolving into it. */
.auth-page {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  background: linear-gradient(135deg, var(--green-900) 0%, var(--green-800) 100%);
  /* Fill the viewport below the header even when the form is short, so no
     strip of page background shows beneath a compact login box. */
  min-height: calc(100vh - var(--header-h));
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: var(--space-7);
}

.auth-page__bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image:
    radial-gradient(circle, rgba(255, 255, 255, 0.06) 1px, transparent 1px),
    radial-gradient(ellipse 70% 60% at 75% 20%, rgba(34, 224, 92, 0.22) 0%, transparent 62%);
  background-size: 26px 26px, 100% 100%;
}

/* The logo above the card. On a page whose whole job is asking for a
   password, showing whose site it is carries real weight. */
.auth-page__brand {
  display: flex;
  justify-content: center;
  margin-bottom: var(--space-5);
}
.auth-page__brand img {
  height: 46px;
  width: auto;
}

.auth-card {
  max-width: 26rem;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  box-shadow: var(--shadow-lg);
}

.auth-card__title {
  font-size: var(--fs-xl);
  margin: 0;
}

.auth-card__text {
  color: var(--text-muted);
  font-size: var(--fs-sm);
  margin: 0;
}

/* The "or" between the password form and the social buttons. The rule is
   drawn with a flex row rather than an <hr> so the label sits in the gap. */
.auth-divider {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--text-subtle);
  font-size: var(--fs-sm);
}
.auth-divider::before,
.auth-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

.auth-providers {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.auth-provider {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
}

/* Account links in the mobile drawer, separated from the main nav above.
   Both values below used to be white-alpha, which was a plain bug and not
   one this repaint introduced: the drawer panel has always been light, so
   the divider never rendered and the signed-in email was white on white. */
.primary-nav__list--account {
  border-top: 1px solid var(--border);
  padding-top: var(--space-2);
}
.primary-nav__account {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  color: var(--text-subtle);
  font-size: var(--fs-sm);
}
/* "Log out" is a submit button so it can POST, but it has to read as a nav
   link — this strips the button chrome without losing the button semantics. */
.primary-nav__linkbutton {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: var(--fs-md);
  font-weight: 600;
  color: inherit;
  cursor: pointer;
  text-align: left;
}

/* Matches the breakpoint at which the drawer becomes a horizontal bar (5.2). */
@media (min-width: 1024px) {
  /* margin-left:auto is the single thing separating the left-hand nav links
     from the right-hand account + Contact Us cluster. */
  .primary-nav__list--account {
    flex-direction: row;
    align-items: center;
    gap: var(--space-1);
    border-top: none;
    padding-top: 0;
    margin-left: auto;
    min-width: 0;
  }
  .primary-nav__account {
    /* A signed-in address is context, not navigation — dimmer and capped so
       admin@kaani100.com cannot squeeze the links beside it. */
    max-width: 11rem;
    min-width: 0;
    padding: 0 var(--space-2);
    border-right: 1px solid var(--border);
    color: var(--text-subtle);
    font-size: var(--fs-xs);
  }
  .primary-nav__account span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .primary-nav__linkbutton {
    display: flex;
    align-items: center;
    min-height: 0;
    white-space: nowrap;
    /* Matches .primary-nav__list a exactly — "Log out" sits in that same row
       and must not hover differently from the links beside it. */
    color: var(--text);
    padding: var(--space-2);
    border-radius: var(--radius-sm);
    font-size: var(--fs-base);
    font-weight: 600;
  }
  .primary-nav__linkbutton:hover {
    color: var(--green-900);
    background: var(--green-50);
  }
}


/* --- 5.20 Motion ----------------------------------------------------------
   Four rules govern everything in this section.

   1. Decoration lives inside `prefers-reduced-motion: no-preference`, so it is
      structurally impossible for it to reach someone who asked for less
      movement. Only genuine function — the lightbox, the back-to-top button —
      sits outside, where section 7 strips its transitions instead.
   2. Anything that STARTS an element invisible is additionally gated on
      `html.js`, set inline in partials/_head.html. With JavaScript off or
      broken that hidden state never exists, so no content can be stranded.
   3. Reveal offsets use the standalone `translate` property rather than
      `transform`, so they compose with the hover lifts already declared on
      .p-card and .feature-card instead of fighting them for one declaration.
   4. Scroll-linked effects are wrapped in @supports. Where the browser can
      drive them off the scroller they cost no JavaScript and never jitter;
      where it cannot, they are simply absent.
   -------------------------------------------------------------------------- */


/* Same-origin navigations cross-fade instead of flashing the page background.
   Unsupported browsers just navigate the old way — there is no fallback to
   write. The header is named so it holds still while the content beneath it
   changes, rather than fading out and back in along with everything else. */

@view-transition { navigation: auto; }

.site-header { view-transition-name: site-header; }

/* Anchor targets stop below the sticky header instead of behind it.
   scroll-padding has to sit on the scrolling element itself — putting it on
   body does nothing — hence :has() to vary it per page rather than a body
   class. The detail page needs extra room because its section nav sticks
   below the header, and the nav's own links are anchors into the page. */
html { scroll-padding-top: calc(var(--header-h) + var(--space-4)); }
html:has(.page-detail) { scroll-padding-top: calc(var(--header-h) + 4.5rem); }


/* --- Header on scroll ---------------------------------------------------- */
/* Function, not decoration: it tells you the header is a layer above the page
   rather than part of it, which is worth keeping under reduced motion. */

.site-header {
  transition: background-color 260ms var(--ease), box-shadow 260ms var(--ease);
}
/* The header used to DARKEN here, back when it was navy and its links were
   white. It must not any more: the links are dark text now, so darkening the
   bar would erase every label the moment the page scrolls. Elevation is
   carried by the shadow and the logo shrink below instead, and the only
   colour move is the half-step from the tinted --header-bg to flat white. */
.site-header.is-scrolled {
  background: var(--surface);
  box-shadow: 0 4px 20px rgba(11, 42, 22, 0.10);
}
.site-header__logo img { transition: height 260ms var(--ease); }
.site-header.is-scrolled .site-header__logo img { height: 28px; }


/* --- Back to top --------------------------------------------------------- */

.to-top {
  position: fixed;
  right: var(--gutter);
  bottom: calc(var(--space-5) + env(safe-area-inset-bottom));
  z-index: var(--z-sticky-cta);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  border-radius: var(--radius-full);
  background: var(--green-800);
  color: var(--text-inverse);
  font-size: 1.125rem;
  cursor: pointer;
  box-shadow: var(--shadow-lg);
  opacity: 0;
  visibility: hidden;          /* keeps it out of the tab order while hidden */
  translate: 0 14px;
  transition: opacity 260ms var(--ease),
              translate 260ms var(--ease),
              visibility 260ms,
              background-color 180ms var(--ease);
}
.to-top.is-visible { opacity: 1; visibility: visible; translate: none; }
/* Hover goes DARKER, not lighter. The icon is white, and anything above 700
   sits under the 4.5:1 an 18px non-bold glyph owes. */
.to-top:hover { background: var(--green-900); }

/* Clear of the sticky enquiry bar that only the property page carries. */
.page-detail .to-top { bottom: calc(5.5rem + env(safe-area-inset-bottom)); }
@media (min-width: 1024px) {
  .page-detail .to-top { bottom: calc(var(--space-5) + env(safe-area-inset-bottom)); }
}


/* --- Photo lightbox ------------------------------------------------------ */

.gallery__stage img { cursor: zoom-in; }

.lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--z-drawer);
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  gap: var(--space-3);
  padding: var(--space-4);
  padding-block-end: calc(var(--space-4) + env(safe-area-inset-bottom));
  background: rgba(9, 30, 15, 0.94);
  opacity: 0;
  visibility: hidden;
  transition: opacity 240ms var(--ease), visibility 240ms;
}
.lightbox.is-open { opacity: 1; visibility: visible; }

.lightbox__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  color: var(--text-on-dark);
}
.lightbox__count { font-size: var(--fs-sm); font-weight: 600; }

.lightbox__figure { display: grid; place-items: center; min-height: 0; }

.lightbox__img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  object-fit: contain;
  border-radius: var(--radius);
  opacity: 0;
  scale: 0.94;
  transition: opacity 320ms var(--ease), scale 320ms var(--ease);
}
.lightbox.is-open .lightbox__img { opacity: 1; scale: 1; }

.lightbox__nav { display: flex; justify-content: center; gap: var(--space-4); }

.lightbox__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  border-radius: var(--radius-full);
  background: rgba(255, 255, 255, 0.12);
  color: var(--text-inverse);
  font-size: 1.125rem;
  cursor: pointer;
  transition: background-color 180ms var(--ease);
}
.lightbox__btn:hover { background: rgba(255, 255, 255, 0.24); }

/* Digits stop jostling their neighbours while a number counts up. */
.pillar__num { font-variant-numeric: tabular-nums; }


/* --- Everything below is decoration, and opts out for reduced motion ------ */

@media (prefers-reduced-motion: no-preference) {

  /* Page transitions: content lifts away, the next page settles in under it. */
  ::view-transition-old(root) { animation: kaani-page-out 160ms var(--ease-exit) both; }
  ::view-transition-new(root) { animation: kaani-page-in 320ms var(--ease-entrance) 60ms both; }

  @keyframes kaani-page-out { to   { opacity: 0; translate: 0 -8px; } }
  @keyframes kaani-page-in  { from { opacity: 0; translate: 0 14px; } }

  /* In-page anchors glide rather than jump. */
  html { scroll-behavior: smooth; }

  /* --- Entrance engine -------------------------------------------------
     Choreography lives in the TEMPLATES as data-anim attributes; this file only
     defines what each named effect looks like. That split is the point: it is
     what lets Buyers Corner move differently from the property list, instead of
     every page sharing one global reveal.

     WHY KEYFRAMES AND NOT TRANSITIONS: the previous version set `transition` on
     .p-card, which replaced the shorthand the card already carried and silently
     killed its hover lift until that was restated by hand. Keyframes touch only
     the properties they name, so a card can arrive and lift under the pointer
     with neither effect aware of the other.

     PROPERTY OWNERSHIP — do not break this split:
         entrance owns  opacity, translate, scale, filter
         hover    owns  transform
     Both are independent properties and compose freely.

     Note the base state sets opacity:0 and `.is-in` sets opacity:1. That is
     load-bearing, not redundant: these keyframes declare only a `from`, so the
     browser reads the `to` off the element's own computed style. Leave the base
     0 in place and every animation would run 0 → 0. */

  .js [data-anim],
  .js [data-anim-stagger] > * { opacity: 0; }

  .js [data-anim].is-in,
  .js [data-anim-stagger] > .is-in {
    opacity: 1;
    animation-duration: var(--dur-reveal);
    animation-timing-function: var(--ease-entrance);
    animation-delay: var(--anim-delay, 0ms);
    animation-fill-mode: both;
  }

  /* Default for anything inside a staggered container. Declared BEFORE the
     explicit effects, so a child carrying its own data-anim still wins. */
  .js [data-anim-stagger] > .is-in { animation-name: anim-settle; }

  .js [data-anim="rise"].is-in   { animation-name: anim-rise; }
  .js [data-anim="fade"].is-in   { animation-name: anim-fade; }
  .js [data-anim="settle"].is-in { animation-name: anim-settle; }
  .js [data-anim="left"].is-in   { animation-name: anim-left; }
  .js [data-anim="right"].is-in  { animation-name: anim-right; }
  .js [data-anim="zoom"].is-in   { animation-name: anim-zoom; }
  .js [data-anim="count"].is-in  { animation-name: anim-rise; }

  /* `draw` is the odd one out: never hidden, just collapsed to nothing and then
     wiped open. Used for rules, underlines and the timeline. */
  .js [data-anim="draw"] {
    opacity: 1;
    scale: 0 1;
    transform-origin: 0 50%;
  }
  .js [data-anim="draw"].is-in {
    scale: 1 1;
    animation-name: anim-draw;
  }

  /* Headings that assemble a word at a time. The heading itself must stay
     opaque or the words would fade in twice over. */
  .js [data-anim="words"] { opacity: 1; }
  .js [data-anim="words"] .word {
    display: inline-block;
    opacity: 0;
  }
  .js [data-anim="words"].is-in .word {
    opacity: 1;
    animation: anim-word 640ms var(--ease-entrance) var(--anim-delay, 0ms) both;
  }

  @keyframes anim-rise   { from { opacity: 0; translate: 0 22px; } }
  @keyframes anim-fade   { from { opacity: 0; } }
  @keyframes anim-settle { from { opacity: 0; translate: 0 26px; scale: 0.96; } }
  @keyframes anim-left   { from { opacity: 0; translate: -32px 0; } }
  @keyframes anim-right  { from { opacity: 0; translate:  32px 0; } }
  @keyframes anim-zoom   { from { opacity: 0; scale: 0.92; } }
  @keyframes anim-draw   { from { scale: 0 1; } }
  @keyframes anim-word   { from { opacity: 0; translate: 0 18px; filter: blur(4px); } }

  /* --- Hero entrance ---
     Everything down by ~950ms. The :not([data-anim]) is required, not tidiness:
     a hero child that opts into the engine (the home headline, which arrives a
     word at a time) must not ALSO get the block fade, and these rules match the
     engine's specificity exactly — so without the exclusion, source order would
     hand the element this animation-delay and wreck the word timing. */
  .js .hero__inner > *:not([data-anim]),
  .js .page-hero__inner > *:not([data-anim]) {
    animation: kaani-hero-in var(--dur-hero) var(--ease-entrance) both;
  }
  .js .hero__inner > :nth-child(1):not([data-anim]),
  .js .page-hero__inner > :nth-child(1):not([data-anim]) { animation-delay:  40ms; }
  .js .hero__inner > :nth-child(2):not([data-anim]),
  .js .page-hero__inner > :nth-child(2):not([data-anim]) { animation-delay: 110ms; }
  .js .hero__inner > :nth-child(3):not([data-anim]),
  .js .page-hero__inner > :nth-child(3):not([data-anim]) { animation-delay: 180ms; }
  .js .hero__inner > :nth-child(4):not([data-anim]),
  .js .page-hero__inner > :nth-child(4):not([data-anim]) { animation-delay: 250ms; }

  @keyframes kaani-hero-in {
    from { opacity: 0; translate: 0 26px; filter: blur(5px); }
    to   { opacity: 1; translate: none;   filter: none; }
  }

  /* --- Ambient glow ---
     A second, wandering pool of gold over the static one already in the
     gradient. Only translate and scale move, so it composites on the GPU.

     Was a 26s cycle, which is below the threshold at which movement registers
     as movement — it spent frames and delivered nothing anyone could see. 14s
     with wider travel and a little more alpha actually reads. */
  .hero__bg::after,
  .cta-band__bg::after {
    content: "";
    position: absolute;
    inset: -25%;
    background: radial-gradient(circle at 50% 50%, rgba(34, 224, 92, 0.22), transparent 62%);
    animation: kaani-float 14s ease-in-out infinite alternate;
    pointer-events: none;
  }

  @keyframes kaani-float {
    from { translate: -12% -8%; scale: 1; }
    to   { translate:  14%  9%; scale: 1.22; }
  }

  /* --- Property photo zoom ---
     Slow in, quick out. A photo that takes its time settling and then snaps
     back is the same fast-in/slow-out principle inverted: here the LEAVING is
     the cheap move, so it is the one that should be over quickly. */
  .p-card__media img { transition: scale var(--dur-exit) var(--ease-exit); }
  .p-card:hover .p-card__media img,
  .p-card:focus-within .p-card__media img {
    scale: 1.07;
    transition-duration: 620ms;
    transition-timing-function: var(--ease-settle);
  }

  /* --- Scroll-linked, where the browser can drive it itself --- */
  @supports (animation-timeline: scroll()) {

    /* Reading progress along the bottom edge of the header. */
    .site-header::after {
      content: "";
      position: absolute;
      left: 0;
      bottom: -1px;
      width: 100%;
      height: 2px;
      /* The bar grows from the left, so its RIGHT end is the leading edge —
         the part a reader actually tracks. That end must be the dark one.
         Gold-400 was 1.4:1 on this header and gold-600 only 3.1, and the
         bright end of any green ramp is worse still — so the dark step has to
         be the one on the leading edge. Leading edge green-900 at 7.9:1, tail
         green-600 at 3.1:1. */
      background: linear-gradient(90deg, var(--green-700), var(--green-900));
      transform-origin: 0 50%;
      scale: 0 1;
      animation: kaani-progress linear both;
      animation-timeline: scroll(root block);
    }

    @keyframes kaani-progress { to { scale: 1 1; } }

    /* The hero backdrop drifts slower than the page scrolling over it. The
       scale covers the gap the translate would otherwise open at the top. */
    .hero__bg {
      animation: kaani-parallax linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 100vh;
    }

    @keyframes kaani-parallax { to { translate: 0 12%; scale: 1.15; } }
  }
}


/* --- 5.21 Scroll-story components ----------------------------------------
   Page-specific furniture that only exists to tell a story as you scroll.
   Each of these degrades to something readable with JavaScript off — the rail
   shows as a solid line, steps sit at full opacity, nav links are plain
   anchors — so nothing here is load-bearing for comprehension.
   -------------------------------------------------------------------------- */

/* A parallaxed image is scaled up so the drift never exposes the frame behind
   it — its container clips to a fixed aspect ratio, so an unscaled image would
   show a sliver of background at one edge or the other for the whole scroll. */
.js [data-scroll="parallax"] { scale: 1.12; }


/* Buyers Corner: the four steps as a timeline that draws itself. */

.timeline { position: relative; }

.timeline__rail {
  position: absolute;
  left: 1.25rem;                 /* centre line of the 2.5rem numeral */
  top: 1.25rem;
  bottom: 1.25rem;
  width: 2px;
  border-radius: 2px;
  background: var(--border-strong);
}

/* The steps sit on a dark band, where the light tan rail reads as a drawn line
   rather than an undrawn one and steals attention from the gold fill. */
.section--dark .timeline__rail { background: rgba(255, 255, 255, 0.2); }

.timeline__fill {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  /* Stays gold — this one is on the dark green band, where gold is the thing
     that shows. But it runs light-to-mid, not mid-to-dark: against the rail
     (white 0.2 over green) the old gold-700 end was 1.03:1, so the bottom of
     the "drawn" line was indistinguishable from the undrawn one. */
  background: linear-gradient(180deg, var(--gold-400), var(--gold-500));
  transform-origin: 50% 0;
  /* Default full: with no JavaScript the line is simply drawn already. The
     .js rule below is what collapses it so scrolling can fill it in. */
  scale: 1 1;
}
.js .timeline__fill { scale: 1 0; }

/* Vertical at every width — a timeline that reflows into four columns on
   desktop is no longer a timeline. This deliberately overrides the .steps
   grid from section 5.10. */
.steps--timeline {
  grid-template-columns: 1fr;
  gap: var(--space-5);
  padding-left: 3.75rem;
}

.steps--timeline .step__num {
  position: absolute;
  left: -3.75rem;
  top: var(--space-5);
  margin-bottom: 0;
  background: var(--surface);
  color: var(--text-muted);
  border: 2px solid var(--border-strong);
  transition: background-color var(--dur-hover) var(--ease-settle),
              border-color    var(--dur-hover) var(--ease-settle),
              color           var(--dur-hover) var(--ease-settle),
              scale           var(--dur-hover) var(--ease-settle);
}

/* Dimming is gated on .js: without it, nothing would ever brighten these
   back up and the whole section would read as permanently disabled. */
.js .steps--timeline .step {
  opacity: 0.5;
  transition: opacity    var(--dur-hover) var(--ease-settle),
              transform  var(--dur-hover) var(--ease-settle),
              box-shadow var(--dur-hover) var(--ease-settle);
}
.js .steps--timeline .step.is-active {
  opacity: 1;
  transform: translateX(4px);
  box-shadow: var(--shadow);
}
.steps--timeline .step.is-active .step__num {
  background: var(--gold-500);
  border-color: var(--gold-500);
  color: var(--green-950);
  scale: 1.08;
}

@media (min-width: 640px) {
  .steps--timeline { grid-template-columns: 1fr; padding-left: 4.5rem; }
  .steps--timeline .step__num { left: -4.5rem; }
  .timeline__rail { left: 1.5rem; }
}
@media (min-width: 1024px) {
  .steps--timeline { grid-template-columns: 1fr; }
}


/* Property detail: nav that tracks the section you are reading. */

/* --surface, not --surface-alt. The detail page is the one page with no hero,
   so a white header now stacks straight onto a white breadcrumb onto this —
   and --surface-alt against the header is 1.03:1, i.e. one undifferentiated
   slab of near-white about 130px tall. Pure white makes it a second plane. */
.section-nav {
  position: sticky;
  top: var(--header-h);
  z-index: var(--z-sticky-cta);
  margin-bottom: var(--space-5);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
}

/* The scroll container is the inner row, not the sticky element itself —
   overflow on a sticky element is legal but makes it its own scroll container,
   which is a needless complication when a child can carry it instead. */
.section-nav__inner {
  display: flex;
  gap: var(--space-1);
  padding-block: var(--space-2);
  overflow-x: auto;
  scrollbar-width: none;
}
.section-nav__inner::-webkit-scrollbar { display: none; }

.section-nav a {
  flex-shrink: 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-full);
  color: var(--text-muted);
  font-size: var(--fs-sm);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color var(--dur-exit) var(--ease-exit),
              color            var(--dur-exit) var(--ease-exit);
}
.section-nav a:hover { color: var(--green-900); background: var(--surface-sunken); }
.section-nav a.is-current {
  background: var(--green-800);
  color: var(--text-inverse);
  transition-duration: var(--dur-hover);
  transition-timing-function: var(--ease-settle);
}


/* Listing: the result count and filter controls stay reachable while you
   scroll a long grid. */

.listing-toolbar {
  position: sticky;
  top: var(--header-h);
  z-index: var(--z-sticky-cta);
  padding-block: var(--space-3);
  background: var(--surface-alt);
}


/* List-your-land: how much of the form is left. */

.form-progress {
  position: sticky;
  top: calc(var(--header-h) + var(--space-2));
  z-index: 1;
  height: 4px;
  margin-bottom: var(--space-5);
  border-radius: var(--radius-full);
  overflow: hidden;
  background: var(--surface-sunken);
}

.form-progress__fill {
  display: block;
  height: 100%;
  /* Reversed for the same reason as the header's reading bar: this also grows
     from the left, so the dark step belongs on the leading edge. It sits in a
     --surface-sunken track, where the bright steps alone are barely a
     shape. */
  background: linear-gradient(90deg, var(--green-600), var(--green-900));
  transform-origin: 0 50%;
  scale: 0 1;
  transition: scale var(--dur-hover) var(--ease-settle);
}


/* ==========================================================================
   5.22 PAGE-SPECIFIC LAYOUTS
   --------------------------------------------------------------------------
   Furniture that exists on exactly one page. Everything here is a grid or a
   position rule over components defined above — no new colours, no new
   spacing values, and nothing that has to be undone anywhere else.
   ========================================================================== */

/* --- Home: the welcome block ---------------------------------------------- */

/* Centring is inherited: these two lines sit inside .section__head, which is
   already centred with an auto inline margin. Nothing here re-states it. */

.welcome__title {
  font-family: var(--font-accent);
  font-size: var(--fs-3xl);
  font-weight: 700;
}

/* Deliberately the OPPOSITE of the green title above it: gold, and lit. The
   two lines used to be near enough the same dark green that the pair read as
   one block of type.

   The glow is a filter, not a text-shadow. A text-shadow paints behind the
   glyph, and this glyph is a transparent fill clipped to a gradient — the
   shadow would show straight THROUGH the letters and silt them up. filter
   runs on the composited result, so the halo lands outside the letterforms
   where a glow belongs. It sits on the base rule rather than in the @supports
   so it lights the solid-gold fallback too. */
.welcome__tagline {
  font-family: var(--font-accent);
  font-size: var(--fs-xl);
  font-weight: 600;
  line-height: 1.35;
  margin-top: var(--space-3);
  text-wrap: balance;
  /* The solid colour is declared FIRST and unconditionally. Everything that
     makes this a gradient lives in the @supports below, so a browser that
     cannot clip a background to text never reaches the transparent fill and
     simply shows solid gold type. The failure mode is "less pretty", never
     "invisible" — which is what it would be if the order were reversed. */
  color: var(--gold-700);
  filter: var(--glow-text-gold);
}

/* .hero__lead rides along from here down: it wears this same treatment, and
   the recipe lives in one place so the fallback below can never cover one of
   them and miss the other. Its own sizing stays up in the hero block. */
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .welcome__tagline,
  .hero__lead {
    /* All gold now, where this used to run green-800 -> gold-700 and so read
       as more green type. The old comment's rule still binds and is the whole
       reason this sweep ripples DARKER rather than brighter: every pixel of a
       gradient has to be legible on its own, gold-700 is the lightest gold
       that clears 4.5:1 on --surface-alt (4.99:1), and gold-600 is 2.3:1 —
       it fails even the 3:1 large-text allowance. --fs-xl starts at 22px,
       under the 24px that would earn that allowance anyway, so 4.5:1 is the
       bar at every viewport.
       Hence the metallic banding goes down into bronze (#6f5200, 6.95:1) and
       back up to gold-700. Dark-light-dark is what sells "metal"; brighter
       bands are not available on a near-white canvas at any legible value.
       The glow above is what supplies the actual radiance. */
    background-image: linear-gradient(100deg,
        var(--gold-700)   0%,
        #6f5200          28%,
        var(--gold-700)  52%,
        #6f5200          78%,
        var(--gold-700) 100%);
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
    /* Safari needs the fill explicitly; `color` alone leaves it opaque. */
    -webkit-text-fill-color: transparent;
  }
}

/* Windows high-contrast throws the background image away but would keep the
   transparent fill, which erases the line completely. Put both back, and drop
   the glow with them — forced-colors exists to strip exactly this kind of
   decoration. */
@media (forced-colors: active) {
  .welcome__tagline,
  .hero__lead {
    background-image: none;
    color: CanvasText;
    -webkit-text-fill-color: currentColor;
    filter: none;
  }
}

/* Lit like the tagline that opens this block, but in GREEN, because that is
   the colour it already is. The glow was the ask here; the colour was not, and
   turning this gold too would have put the section's only two accent lines in
   the same colour and lost the green/gold pairing the tagline exists to make. */
.welcome__signature {
  font-family: var(--font-accent);
  font-size: var(--fs-lg);
  font-style: italic;
  font-weight: 600;
  color: var(--green-900);
  margin-top: var(--space-6);
  text-wrap: balance;
  filter: var(--glow-text-green);
}

@media (forced-colors: active) {
  .welcome__signature { filter: none; }
}


/* --- About: the split intro ---------------------------------------------- */

.about-split { display: grid; gap: var(--space-6); }

.about-points { display: grid; gap: var(--space-4); margin-top: var(--space-6); }

.about-point { display: flex; align-items: flex-start; gap: var(--space-4); }
.about-point h3 { font-size: var(--fs-md); margin-bottom: var(--space-1); }
.about-point p  { color: var(--text-muted); font-size: var(--fs-sm); }

/* Same 3rem badge as .feature-card__icon, in green rather than gold: these
   rows are structure, and gold is reserved for the accent. */
.about-point__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 3rem;
  height: 3rem;
  border-radius: var(--radius);
  background: var(--green-50);
  color: var(--green-800);
  font-size: 1.25rem;
}

.about-split__media { position: relative; }
.about-split__media img {
  width: 100%;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}

/* Below 1024px this is an ordinary card that follows the photo. The overlap
   is added by the breakpoint at the foot of this block, never removed by it —
   so a small screen cannot end up with a card sitting on top of the copy. */
.about-split__card {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-top: var(--space-4);
  padding: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
.about-split__card .founder__portrait { flex: none; width: 4.5rem; margin: 0; }
.about-split__name { font-weight: 800; color: var(--green-900); }
.about-split__role { color: var(--text-subtle); font-size: var(--fs-sm); margin-bottom: var(--space-2); }

@media (min-width: 1024px) {
  .about-split { grid-template-columns: 1fr 1fr; gap: var(--space-8); align-items: center; }

  /* The photo carries the card on its bottom edge, as in the reference. It
     needs a floor to sit on, so the image gets a fixed aspect and the card is
     pulled up over it rather than the image being padded down. */
  .about-split__media img { aspect-ratio: 4 / 5; object-fit: cover; }
  .about-split__card {
    position: absolute;
    left: var(--space-5);
    right: var(--space-5);
    bottom: var(--space-5);
    margin-top: 0;
    box-shadow: var(--shadow-lg);
  }
}


/* --- Sell Land: form beside the supporting rail --------------------------- */

.sell-layout { display: grid; gap: var(--space-6); }

.sell-aside { display: grid; gap: var(--space-5); align-content: start; }

.sell-aside__media img {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}

/* The rail is one column wide, so its feature cards must not try to be two or
   three across the way .feature-grid does everywhere else. */
.feature-grid--stack { grid-template-columns: 1fr; }

.sell-why__head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}
.sell-why__head .founder__portrait { flex: none; width: 4.5rem; margin: 0; }
.sell-why__name { font-weight: 800; color: var(--green-900); }
.sell-why__role { color: var(--text-subtle); font-size: var(--fs-sm); }
.sell-why .link-arrow { margin-top: var(--space-4); }

@media (min-width: 640px) {
  /* Two across while the rail is still full width, so the three benefits do
     not become a tall column between the photo and the founder card. */
  .feature-grid--stack { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 1024px) {
  .sell-layout { grid-template-columns: 1.1fr 0.9fr; gap: var(--space-7); align-items: start; }
  .feature-grid--stack { grid-template-columns: 1fr; }
}


/* --- Buyers Corner: the resources disclosure ------------------------------ */

/* A native <details>. No JavaScript, so it opens on a page where the script
   failed to load exactly as it does on one where it did not. */
.resources {
  max-width: 42rem;
  margin: var(--space-6) auto 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}

.resources summary {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  min-height: var(--tap-min);
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-lg);
  color: var(--green-900);
  font-weight: 700;
  cursor: pointer;
  /* Both properties: WebKit still uses the -webkit- pseudo-element, and
     without it the default triangle sits beside our own chevron. */
  list-style: none;
}
.resources summary::-webkit-details-marker { display: none; }

.resources__chevron { transition: rotate var(--dur-hover) var(--ease-settle); }
.resources[open] .resources__chevron { rotate: 180deg; }

.resources__list {
  display: grid;
  gap: var(--space-1);
  padding: 0 var(--space-4) var(--space-4);
}
.resources__list a {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap-min);
  padding-inline: var(--space-3);
  border-radius: var(--radius);
  font-size: var(--fs-sm);
  font-weight: 700;
  text-decoration: none;
}
.resources__list a:hover { background: var(--surface-sunken); }
.resources__list i { color: var(--gold-700); }


/* --- Buyer FAQ ------------------------------------------------------------ */
/* Same <details> mechanics as .resources above, but a stacked list rather than
   one panel, and left-aligned: these are questions to scan, not a menu. */

.faq { display: grid; gap: var(--space-3); }

.faq__item {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.faq__item summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: var(--tap-min);
  padding: var(--space-4) var(--space-5);
  cursor: pointer;
  list-style: none;
}
.faq__item summary::-webkit-details-marker { display: none; }
.faq__item summary:hover { background: var(--surface-sunken); }

/* The h3 is inside the summary so the questions are real headings a screen
   reader can jump between. Stripping its block display keeps it on the
   summary's flex row instead of pushing the chevron onto a second line. */
.faq__question {
  display: inline;
  font-size: var(--fs-md);
  font-family: var(--font-sans);
  font-weight: 600;
  margin: 0;
}

.faq__chevron {
  flex-shrink: 0;
  color: var(--gold-700);
  transition: rotate var(--dur-hover) var(--ease-settle);
}
.faq__item[open] .faq__chevron { rotate: 180deg; }

/* Reads as the welcome block's prose does — same weight, same colour, same
   size. An answer is the longest continuous copy on the page after that
   block, so it is the one place that earns the full treatment. */
.faq__answer {
  padding: 0 var(--space-5) var(--space-5);
  color: var(--text-muted);
  font-size: var(--fs-md);
  font-weight: 500;
  max-width: 42rem;
}


/* ==========================================================================
   6. UTILITIES
   ========================================================================== */

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Locks background scrolling while the drawer or filter sheet is open. */
.is-scroll-locked { overflow: hidden; }


/* ==========================================================================
   7. MOTION PREFERENCES
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
