/* ==========================================================================
   Eventoria — warm, modern planning interface

   The visual system keeps Eventoria's hospitality character while adopting the
   clarity of contemporary booking products: a readable sans-serif UI, one
   confident primary action, calm surfaces, generous spacing and obvious states.
   Lora remains a selective brand/display accent; Karla carries the interface.
   ========================================================================== */

/* Eventoria's own font files, so the site needs no font CDN. */
@font-face {
  font-family: 'Karla';
  src: url('fonts/karla-latin.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Lora';
  src: url('fonts/lora-latin.woff2') format('woff2');
  font-weight: 500 600;
  font-style: normal;
  font-display: swap;
}

:root {
  /* ---- Eventoria evergreen: warm paper + deep service-first green ---- */
  --bg: #F7F2E9;
  --bg2: #E8F0EA;
  --card: #FFFDF8;
  --ink: #182A26;
  --mut: #53635E;
  --pri: #1F5A4A;
  --pri-deep: #164438;
  --gold: #D39B2C;
  --moss: #6D7F67;
  --coral: #D9603F;
  --line: #D7E1DD;
  --on: #FBF7EF;

  --serif: 'Lora', Georgia, 'Times New Roman', serif;
  --sans: 'Karla', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;

  --radius: 16px;
  --radius-sm: 12px;
  --radius-pill: 999px;
  --shadow: 0 10px 28px rgba(22, 68, 56, .09);
  --shadow-hover: 0 16px 38px rgba(22, 68, 56, .16);
  --wrap: 1180px;
  color-scheme: light;
}

:root[data-theme="dark"] {
  --bg: #14241F;
  --bg2: #1C332A;
  --card: #203B30;
  --ink: #F6F1E7;
  --mut: #C6D2C8;
  --pri: #A8D5B5;
  --pri-deep: #0C1D16;
  --gold: #E5BE62;
  --moss: #AABF91;
  --coral: #F08A69;
  --line: #3A594B;
  --on: #102119;
  --shadow: 0 10px 28px rgba(0, 0, 0, .24);
  --shadow-hover: 0 16px 38px rgba(0, 0, 0, .34);
  color-scheme: dark;
}

:root[data-theme="dark"] header { background: rgba(20, 36, 31, .94); }
:root[data-theme="dark"] footer{ background: #0C1D16; }
:root[data-theme="dark"] .hero__inner,
:root[data-theme="dark"] .prototype-home-search,
:root[data-theme="dark"] .builder-mobile-bar { background: rgba(32, 59, 48, .96); }
:root[data-theme="dark"] .notice.good { background: #214735; color: #D4E8D7; border-color: #4E8061; }
:root[data-theme="dark"] .notice.warn { background: #4A3A1D; color: #F1D899; border-color: #806536; }
:root[data-theme="dark"] .notice.bad { background: #4A2925; color: #F2C4BB; border-color: #8D4D45; }

* { box-sizing: border-box; }

html {
  scroll-padding-top: 76px;
  min-height: 100%;
}

body {
  min-height: 100vh;
  min-height: 100dvh;
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 16px/1.6 var(--sans);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  padding-top: env(safe-area-inset-top, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  display: flex;
  flex-direction: column;
}

#app {
  flex: 1 0 auto;
  width: 100%;
}

h1, h2, h3, .serif {
  font-family: var(--sans);
  font-weight: 700;
  letter-spacing: -0.025em;
  margin: 0;
}

.logo, .hero h1, .aix__welcome, .money__total, .receipt__total, .footer__brand .serif {
  font-family: var(--serif);
}

h1 { font-size: clamp(32px, 4.4vw, 52px); line-height: 1.06; }
h2 { font-size: clamp(26px, 3.4vw, 38px); line-height: 1.12; }
h3 { font-size: 19px; line-height: 1.25; }

a { color: inherit; text-decoration: none; }

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

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

header {
  position: sticky;
  top: env(safe-area-inset-top, 0px);
  z-index: 9;
  background: rgba(255, 253, 252, .96);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: center;
  gap: 20px;
  min-height: 68px;
  padding: 10px 24px;
  box-shadow: 0 2px 14px rgba(24, 42, 38, .04);
}

.logo {
  font: 700 29px/1 var(--serif);
  color: var(--pri);
  letter-spacing: -0.035em;
  white-space: nowrap;
}

.logo small {
  display: block;
  font: 9px var(--sans);
  font-weight: 600;
  letter-spacing: 2px;
  color: var(--gold);
}

nav {
  display: flex;
  gap: 12px;
  margin-left: auto;
  align-items: center;
  flex-wrap: wrap;
}

nav a {
  font-size: 13px;
  font-weight: 600;
  color: var(--mut);
  padding: 9px 0;
  white-space: nowrap;
}

nav a:hover { color: var(--pri); }
nav a.on {
  color: var(--pri);
  box-shadow: inset 0 -2px 0 var(--pri);
}

nav a[href="#ai"] {
  font-weight: 600;
  color: var(--pri);
}

#theme {
  display: inline-grid;
  place-items: center;
  width: 40px;
  height: 40px;
  margin-left: auto;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--card);
  color: var(--pri);
  font-size: 17px;
  cursor: pointer;
  transition: transform .2s ease, background .2s ease, color .2s ease;
}
#theme:hover { transform: translateY(-1px); background: var(--bg2); }

#mb {
  display: none;
  margin-left: 0;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 0;
  color: var(--ink);
  font: 700 20px/1 var(--sans);
  cursor: pointer;
}

/* -------------------------------------------------------------- bits --- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  background: var(--pri);
  color: var(--on);
  border: 1px solid var(--pri);
  border-radius: var(--radius-pill);
  padding: 10px 18px;
  font: 700 15px/1.1 var(--sans);
  letter-spacing: .005em;
  cursor: pointer;
  text-align: center;
  transition: background .18s ease, border-color .18s ease, box-shadow .18s ease, transform .18s ease;
}

.btn:hover { background: var(--pri-deep); border-color: var(--pri-deep); box-shadow: 0 5px 14px rgba(31, 90, 74, .18); }
.btn:active { transform: translateY(1px); }
.btn[disabled] { opacity: 0.5; cursor: not-allowed; background: var(--pri); }
.btn.alt { background: var(--card); color: var(--pri); border: 1px solid var(--line); }
.btn.alt:hover { background: var(--bg2); border-color: var(--pri); color: var(--pri-deep); }
.btn.o { background: transparent; border: 1px solid var(--pri); color: var(--pri); }
.btn.o:hover { background: var(--bg2); }
.btn.w { width: 100%; }
.btn.sm { min-height: 40px; padding: 8px 14px; font-size: 14px; }

.tag {
  position: absolute;
  top: 10px;
  left: 10px;
  background: var(--pri);
  color: #fff;
  font-size: var(--fs-2xs);
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  letter-spacing: .03em;
}

.pill {
  display: inline-block;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--gold);
}

main {
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 36px 24px 64px;
}

/* ----------------------------------------------------- prototype journey --- */
.journey-steps {
  display: grid;
  gap: 8px;
  margin: 0 0 24px;
  padding: 14px 16px 12px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 8px 22px rgba(22, 68, 56, .06);
}
.journey-steps__meta { display: flex; justify-content: space-between; gap: 12px; color: var(--mut); font-size: 12px; }
.journey-steps__meta b { color: var(--pri); font-weight: 700; }
.journey-steps__track { height: 5px; overflow: hidden; border-radius: var(--radius-pill); background: var(--bg2); }
.journey-steps__track span { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--pri), var(--moss)); transition: width .35s ease; }
.journey-steps__items { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; }
.journey-step { display: grid; justify-items: center; gap: 5px; min-height: 44px; padding: 5px 4px; border-radius: var(--radius-sm); color: var(--mut); font-size: var(--fs-2xs); font-weight: 700; text-align: center; }
.journey-step i { display: grid; place-items: center; width: 30px; height: 30px; border: 1px solid var(--line); border-radius: 50%; font-style: normal; font-size: var(--fs-2xs); }
.journey-step.on { background: var(--bg2); color: var(--pri); }
.journey-step.on i { border-color: var(--pri); background: var(--pri); color: var(--on); box-shadow: 0 0 0 4px color-mix(in srgb, var(--pri) 16%, transparent); }
.journey-step.done { color: var(--pri); }
.journey-step.done i { border-color: var(--pri); background: var(--bg2); color: var(--pri); }
.journey-entry { display: grid; grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); gap: 20px; align-items: center; padding: 20px; background: var(--bg2); border: 1px solid var(--line); border-radius: var(--radius); }
.journey-entry h2 { margin-top: 5px; color: var(--ink); }
.journey-entry__links { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.journey-entry__links a { display: grid; gap: 6px; align-content: center; min-height: 78px; padding: 12px; background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-sm); color: var(--ink); }
.journey-entry__links a:hover { border-color: var(--pri); box-shadow: var(--shadow); }
.journey-entry__links b { color: var(--pri); font: 600 20px var(--serif); }
.journey-entry__links span { color: var(--mut); font-size: 12px; }
.resume-card { display: flex; align-items: center; gap: 12px; margin-top: 16px; padding: 12px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: rgba(255, 253, 248, .78); box-shadow: 0 8px 22px rgba(22, 68, 56, .08); }
.resume-card__icon { display: grid; place-items: center; width: 34px; height: 34px; flex: none; border-radius: 50%; background: var(--bg2); color: var(--pri); font-weight: 700; }
.resume-card div { display: grid; gap: 1px; min-width: 0; flex: 1; }
.resume-card b { color: var(--ink); font-size: 14px; }
.resume-card span:not(.resume-card__icon) { color: var(--mut); font-size: 12px; }
:root[data-theme="dark"] .resume-card { background: rgba(32, 59, 48, .96); }
.flow-shell, .plan-shell, .itemlist-shell { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 20px; align-items: start; }
.flow-main, .flow-aside, .plan-board, .itemlist-card { min-width: 0; }
.flow-main h3, .flow-aside h3 { color: var(--ink); font-size: 25px; }
.flow-fields { margin-top: 18px; }
.flow-actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--line); }
.flow-aside { position: sticky; top: 92px; }
.flow-aside .im { margin: -18px -18px 4px; width: calc(100% + 36px); border-radius: var(--radius) var(--radius) 0 0; }
.plan-tabs { margin: 0 0 18px; }
.plan-visual { aspect-ratio: 16 / 8; min-height: 260px; }
.layout-preview { display: grid; gap: 12px; }
.layout-preview__image { aspect-ratio: 16 / 8; min-height: 260px; }
.layout-preview__legend { display: flex; gap: 12px; flex-wrap: wrap; color: var(--mut); font-size: 12px; }
.legend-dot { display: inline-block; width: 10px; height: 10px; margin-right: 5px; border-radius: 50%; background: var(--pri); }
.legend-dot.table { background: var(--gold); } .legend-dot.service { background: var(--moss); }
.plan-metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin: 16px 0; }
.plan-metrics span { display: grid; gap: 2px; padding: 10px 6px; background: var(--bg); border-radius: var(--radius-sm); color: var(--mut); font-size: var(--fs-2xs); text-align: center; }
.plan-metrics b { color: var(--pri); font: 600 20px var(--serif); }
.plan-items, .events-list { display: grid; gap: 8px; }
.plan-item { display: grid; grid-template-columns: 30px 1fr auto; gap: 10px; align-items: center; padding: 12px; border-bottom: 1px solid var(--line); }
.plan-item__icon { display: grid; place-items: center; width: 30px; height: 30px; border-radius: 50%; background: var(--bg2); color: var(--pri); }
.plan-item div { display: grid; gap: 2px; min-width: 0; } .plan-item div span { color: var(--mut); font-size: 12px; }
.plan-item strong, .plan-total { color: var(--pri); font: 600 20px var(--serif); }
.itemlist-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 12px; }
.empty-journey { max-width: 720px; margin: 0 auto; text-align: center; align-items: center; padding: 42px 24px; }
.confirmation-card { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 28px; align-items: center; padding: 0; overflow: hidden; }
.confirmation-card__image { height: 100%; min-height: 320px; border-radius: 0; }
.confirmation-card > div:last-child { padding: 24px; }
.event-card { display: flex; justify-content: space-between; align-items: center; gap: 16px; }
.event-card strong { color: var(--pri); font: 600 20px var(--serif); }

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

 /* The hero is sized to the first screen rather than to its own content.
    `min-height` is capped twice so it can never be the reason the availability form falls below the
    fold: 74vh leaves room for the header and the page's own top padding on a short laptop, and the
    680px ceiling stops a tall monitor from stretching the photograph into a wall of green.
    The base background is the deep green of the scrim, not cream, because the copy standing on this
    section is light now — if the four slide images ever fail to load, the text must still be readable
    rather than cream-on-cream. */
.hero {
  min-height: min(74vh, 680px);
  border-radius: 22px;
  padding: clamp(24px, 3.4vw, 46px);
  box-shadow: none;
  display: flex;
  flex-direction: column;
  justify-content: center;
  position: relative;
  overflow: hidden;
  background: linear-gradient(135deg, #16342A, #0E2A21);
  color: #FBF7F1;
}

.hero.has-photo {
  background: none;
}

.hero__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The slider preserves the original hero's quiet, editorial composition. The outgoing scene moves
   left while the incoming scene enters from the right, so the customer can see a real slide rather
   than a still image. The cover crop means no empty edge appears during the movement. */
.hero__slides {
  position: absolute;
  z-index: 0;
  inset: 0;
  overflow: hidden;
  background: var(--bg2);
  pointer-events: none;
}

.hero__slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  opacity: 0;
  transform: translateX(100%);
  transition: transform .8s cubic-bezier(.22, .61, .36, 1), opacity .8s ease;
  will-change: transform, opacity;
}

.hero__slide.is-active {
  z-index: 1;
  opacity: 1;
  transform: translateX(0);
}

.hero__slide.is-leaving {
  z-index: 1;
  opacity: 0;
  transform: translateX(-100%);
}

/* The type sits on the photograph, so the photograph needs a wash that buys contrast where the
   words are and then gets out of the way. A left-weighted green — heavy behind the headline, almost
   absent over the right two-thirds, where the food is the subject — plus a shallow top band so the
   eyebrow never lands on a bright plate edge. The previous cream wash is gone with the panel it
   existed to justify: light type over a light wash was unreadable, and darkening the type instead
   would have buried it in the tablecloth. */
.hero__scrim {
  position: absolute;
  z-index: 1;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(90deg, rgba(9, 27, 21, .84) 0%, rgba(9, 27, 21, .68) 36%, rgba(9, 27, 21, .32) 62%, rgba(9, 27, 21, .06) 100%),
    linear-gradient(180deg, rgba(9, 27, 21, .34) 0%, rgba(9, 27, 21, 0) 34%);
}

/* No longer a card. The old `.hero__inner` was a 590px translucent white panel holding the headline,
   the proof, the actions and the form — which is why the form ended up below the fold: the panel added
   ~60px of padding and a 590px width cap, and the availability fields had to fit inside whatever was
   left. The words now stand directly on the photograph, and the one card left in the hero is the
   booking panel, which is the only element here that has to look pressable. */
.hero__inner {
  position: relative;
  z-index: 3;
  pointer-events: auto;
  width: 100%;
  max-width: 1020px;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
}

.hero__inner h1,
.hero__inner p,
.hero__inner .mut { color: #FBF7F1; }
.hero__inner .pill { color: #E8CFA0; }
.hero__inner .mut { opacity: .92 !important; }
.hero__inner .mut a { color: #E8CFA0 !important; }

/* The draft/cart card only appears for a returning visitor, but it appears on this photograph, so it
   is tinted for it here rather than inheriting the light card it was designed on. */
.hero__inner .resume-card {
  margin-top: var(--sp-4);
  border-color: rgba(255, 255, 255, .28);
  background: rgba(255, 252, 246, .14);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}

.hero__inner .resume-card b { color: #FBF7F1; }
.hero__inner .resume-card span,
.hero__inner .resume-card__icon { color: rgba(251, 247, 241, .78); }

.hero__controls {
  position: absolute;
  z-index: 4;
  pointer-events: auto;
  right: clamp(20px, 4vw, 42px);
  bottom: clamp(20px, 4vw, 34px);
  display: flex;
  align-items: center;
  gap: 12px;
}

.hero__dots {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border: 1px solid rgba(255, 255, 255, .35);
  border-radius: var(--radius-pill);
  background: rgba(22, 68, 56, .48);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}

.hero__dot {
  width: 9px;
  height: 9px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, .58);
  cursor: pointer;
  transition: width .22s ease, background .22s ease;
}

.hero__dot.is-active {
  width: 26px;
  border-radius: var(--radius-pill);
  background: #fff;
}

.hero__pause {
  min-height: 36px;
  border: 1px solid rgba(255, 255, 255, .5);
  border-radius: var(--radius-pill);
  padding: 7px 12px;
  color: #fff;
  background: rgba(22, 68, 56, .58);
  font: 700 12px/1 var(--sans);
  cursor: pointer;
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}

.hero__pause:hover { background: rgba(22, 68, 56, .78); }

/* The eyebrow, wearing the rule-and-diamond the reference sets either side of it. The decoration is
   two pseudo-elements rather than markup, so the words stay the only content in the element: the
   flourish is not readable text and has no business in the label a screen reader announces. */
.hero__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  color: #E8CFA0;
}

.hero__eyebrow::before,
.hero__eyebrow::after {
  content: "";
  width: clamp(26px, 4.5vw, 56px);
  height: 1px;
  background: linear-gradient(90deg, rgba(232, 207, 160, 0), rgba(232, 207, 160, .85));
}

.hero__eyebrow::after { transform: scaleX(-1); }

/* Every inline hero glyph: never stretched by a flex row, never shrunk by a long label next to it. */
.hic { display: block; flex: 0 0 auto; }

/* `max-width: 12ch` used to sit on this rule, and it was the reason the headline wrapped to three
   lines rather than the two the markup asks for: "Everything You Need." is twenty characters, so the
   cap broke the authored <br> and dropped "One Platform." onto a third line — 59px of hero height
   spent on a line that was never meant to exist, which is a third of what the availability panel
   below needed. 24ch lets the authored break be the only break. The size steps down for the same
   reason: the headline is the loudest thing on the page, not the tallest. */
.hero h1 {
  max-width: 24ch;
  margin: 0;
  font-size: clamp(34px, 4.6vw, 54px);
  line-height: 1.04;
  letter-spacing: -0.035em;
  color: #FDFBF7;
}

/* The second line carries the brand green. Same sentence, same size, same weight — only the colour
   changes, which is what keeps it a headline rather than turning it into a link or a caption. */
.hero__h1-soft { color: #B7D3B8; }

.hero p {
  max-width: 56ch;
  margin: var(--sp-3) 0 0;
  font-size: clamp(16px, 1.4vw, 19px);
  line-height: 1.5;
  color: rgba(251, 247, 241, .9);
}

.row {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  margin-top: 12px;
}

/*
 * Every track floor below is written `min(FLOORpx, 100%)` rather than a bare `FLOORpx`.
 *
 * A bare floor is a promise the container cannot always keep: `minmax(300px, 1fr)` inside a
 * 243px container does not shrink, it OVERFLOWS. That is not theoretical — it put the
 * service-area address fields 11px past the right edge of a 320px phone, which is the width
 * a lot of people actually hold. `min(300px, 100%)` resolves to exactly 300px whenever the
 * container is at least that wide (so nothing above 320px changes at all) and to the
 * container's own width when it is not, which is the only answer that cannot overflow.
 */
.grid {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(auto-fill, minmax(min(230px, 100%), 1fr));
  margin-top: 20px;
}

.g2 { grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); }
.g3 { grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); }
.g4 { grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr)); }

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  box-shadow: 0 2px 8px rgba(24, 42, 38, .025);
}

.card h3 { font-size: 19px; color: var(--ink); }
.card p, .mut { color: var(--mut); margin: 0; font-size: 14px; line-height: 1.55; }
.card:hover { border-color: #CFA2AD; box-shadow: var(--shadow); transform: translateY(-2px); }
.card { transition: border-color .18s ease, box-shadow .18s ease, transform .18s ease; }

/* The image block. Hibaji drew a hue-shifted gradient with an emoji; Eventoria
   has its own photographs, so an <img> can drop straight in. Where there is no
   photograph the gradient and the emoji still stand in, exactly as before. */
.im {
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-sm);
  background: linear-gradient(135deg, hsl(var(--h) 45% 88%), hsl(calc(var(--h) + 25) 42% 74%));
  display: grid;
  place-items: center;
  font-size: 34px;
  position: relative;
  overflow: hidden;
}

.im img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.im.s { aspect-ratio: 1; }
.im.wide { aspect-ratio: 16 / 9; }
.im .em { position: relative; z-index: 1; }
.im img + .em { display: none; }

.ttl {
  text-align: center;
  margin: 10px 0 8px;
  font-size: clamp(28px, 4vw, 42px);
  line-height: 1.08;
  letter-spacing: -0.035em;
  color: var(--ink);
}

.sub { max-width: 62ch; margin: 0 auto 10px; text-align: center; color: var(--mut); font-size: 17px; line-height: 1.55; }

ul.ck { list-style: none; padding: 0; margin: 0; font-size: 13px; }
ul.ck li::before { content: "\2714\00a0"; color: var(--pri); }
ul.ck li { color: var(--mut); }

.icn { text-align: center; cursor: pointer; align-items: center; }
.icn b { font-size: 28px; color: var(--pri); }

.sel { outline: 2px solid var(--pri); background: var(--bg2); }

.stp {
  display: flex;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 10px 0 18px;
  font-size: 12px;
  color: var(--mut);
}

.stp span {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  max-width: 92px;
  text-align: center;
}

.stp i {
  font-style: normal;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 1px solid var(--pri);
  display: grid;
  place-items: center;
  color: var(--pri);
}

.stp button {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.stp .on i { background: var(--pri); color: #fff; }
.stp .dn i { background: var(--bg2); }

.bar {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  background: var(--card);
  border: 1px solid var(--line);
  padding: 12px;
  border-radius: 10px;
  margin: 16px 0;
}

select, input, textarea {
  width: 100%;
  font: 16px/1.35 var(--sans);
  padding: 11px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--card);
  color: var(--ink);
  min-width: 0;
  transition: border-color .18s ease, box-shadow .18s ease, background .18s ease;
}

select:focus, input:focus, textarea:focus {
  border-color: var(--pri);
  outline: none;
  box-shadow: 0 0 0 3px rgba(31, 90, 74, .14);
  background: #fff;
}

.bar > * { flex: 1 1 140px; }

label {
  font-size: 14px;
  font-weight: 700;
  color: var(--ink);
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.chip {
  padding: 6px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--card);
  cursor: pointer;
  font-size: 13px;
  color: var(--ink);
}

.chip:hover { border-color: var(--pri); }
.chip.on { background: var(--pri); color: #fff; border-color: var(--pri); }

.two { display: grid; grid-template-columns: 200px 1fr; gap: 20px; }

/* Two-column arrangements that have to collapse on a phone.
   The supplied build set these widths with an inline style, which a media
   query cannot override — so the contact form kept its 1fr 2fr split at 390px
   and pushed the page 99px wider than the viewport. Same layout, declared
   here instead of inline, so the breakpoint can actually reach it. */
.contactgrid { display: grid; gap: 16px; grid-template-columns: 1fr 2fr; }
.split { display: grid; gap: 16px; grid-template-columns: 1fr 1fr; align-items: center; }
.twowide { display: grid; gap: 20px; grid-template-columns: 1fr 240px; }

.side { display: flex; flex-direction: column; gap: 2px; }

.side button {
  text-align: left;
  background: none;
  border: 0;
  padding: 8px 10px;
  border-radius: 6px;
  color: var(--ink);
  cursor: pointer;
  font: inherit;
}

.side button:hover { background: var(--bg2); }
.side .on { background: var(--pri); color: #fff; }

.mi {
  display: flex;
  gap: 12px;
  align-items: center;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 10px;
  margin-bottom: 10px;
}

.mi .im { width: 64px; flex: none; aspect-ratio: 1; font-size: 24px; }
.mi div:nth-child(2) { flex: 1; }

.tabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 14px 0; }
.sec { margin-top: 36px; }

.hrt {
  position: absolute;
  top: 8px;
  right: 8px;
  background: #fff;
  border: 0;
  border-radius: 50%;
  width: 30px;
  height: 30px;
  color: var(--pri);
  cursor: pointer;
  font-size: 16px;
  z-index: 2;
}

.qt { display: flex; align-items: center; gap: 6px; }
.qt .chip { padding: 2px 10px; }

/* The quantity between the minus and plus. It was a static number, which meant
   a customer needing 100 chairs had to press + a hundred times. Now it takes a
   number directly. The native spinners are hidden because the - and + buttons
   either side already do that job, and three ways to change one number is
   clutter. */
.qt .qtn {
  width: 62px;
  padding: 5px 4px;
  text-align: center;
  font: 600 14px var(--sans);
  color: var(--ink);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  -moz-appearance: textfield;
  appearance: textfield;
}
.qt .qtn::-webkit-outer-spin-button,
.qt .qtn::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.qt .qtn:focus { outline: 2px solid var(--pri); outline-offset: 1px; }
button.card { font: inherit; color: inherit; cursor: pointer; text-align: left; }

.mdl {
  position: fixed;
  inset: 0;
  background: #241F1AA8;
  display: grid;
  place-items: center;
  z-index: 20;
  padding: 16px;
}

.mdl .card { gap: 12px; max-width: 460px; width: 100%; }

footer {
  background: var(--pri-deep);
  color: var(--bg);
  padding: 44px clamp(20px, 6vw, 86px) 24px;
  font-size: 13px;
}

.footer__inner {
  max-width: var(--wrap);
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr;
  gap: 28px;
  text-align: left;
}
.footer__brand { max-width: 30ch; }
.footer__brand .serif { font-size: 22px; }
.footer__brand p { margin: 8px 0 0; color: #F7F2E9B8; }
.footer__group { display: grid; gap: 7px; align-content: start; }
.footer__group b { color: #E8D9B4; margin-bottom: 3px; }
footer a { color: var(--bg); text-decoration: none; }
footer a:hover { color: #E8D9B4; text-decoration: underline; text-underline-offset: 3px; }
footer .serif { font-size: 17px; }

.fnote {
  margin: 14px auto 0;
  max-width: 70ch;
  font-size: 12px;
  color: #F7F2E9A6;
}

.notice {
  margin: 18px 0;
  padding: 12px 16px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--bg2);
  color: var(--mut);
  font-size: 13px;
}

.notice.warn { border-color: #E8D9B4; background: #FBF6EA; color: #6C5520; }
.notice.bad { border-color: #E7C6BA; background: #FBF0EC; color: #7A3421; }
.notice.good { border-color: #CFDCC7; background: #EDF2E8; color: #3D4A33; }

/*
 * One line of the cart: the thing on the left, what it comes to on the right.
 *
 * `space-between` with a gap rather than a two-column grid, because a long item name has
 * to wrap on a phone instead of pushing the figure off the edge — and a price the customer
 * cannot read is worse than no price at all.
 */
.cartline {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 14px;
  margin: 6px 0;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--line);
  font-size: 14px;
}

.cartline:last-child { border-bottom: 0; padding-bottom: 0; margin-bottom: 0; }
.cartline > span:last-child { white-space: nowrap; font-variant-numeric: tabular-nums; }

.money {
  display: grid;
  gap: 4px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 14px;
}

.money__total { font: 600 28px var(--serif); line-height: 1.1; }
.money__badge {
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--mut);
}
.money__basis { font-size: 12.5px; color: var(--mut); margin: 2px 0 0; }

ul.plain { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; font-size: 13px; }
ul.plain li { color: var(--mut); padding-left: 14px; position: relative; }
ul.plain li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.6em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--line);
}
ul.plain.warn li::before { background: var(--gold); }

/* ============================================================ The AI === */

.ai-cap { display: grid; gap: 10px; }
.ai-cap .ai-cap__ic {
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: 10px;
  background: var(--bg2);
  font-size: 19px;
}

.gate {
  display: grid;
  gap: 12px;
  background: var(--card);
  border: 1px solid var(--line);
  border-left: 4px solid var(--pri);
  border-radius: var(--radius);
  padding: 20px;
}

.gate h3 { font-size: 22px; font-family: var(--serif); color: var(--ink); }
.gate p { margin: 0; color: var(--mut); max-width: 68ch; }

.thread {
  display: grid;
  gap: 14px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 20px;
}

.thread__head {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  justify-content: space-between;
}

.thread__status {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--mut);
}

.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--line); flex: none; }
.dot.listening { background: var(--moss); }
.dot.thinking { background: var(--gold); }
.dot.speaking { background: var(--pri); }
.dot.connecting { background: var(--gold); }
.dot.error { background: var(--pri-deep); }

.thread__body {
  display: grid;
  gap: 14px;
  min-height: 180px;
  max-height: 58vh;
  overflow-y: auto;
  padding: 4px 2px;
}

.turn { display: grid; gap: 8px; max-width: 94%; }
/*
 * The customer's own turn, as a bubble on the right.
 *
 * This said `justify-self: end`, which does nothing here: the parent is a flex
 * COLUMN (`.aix__thread`), and `justify-self` only applies to a grid item. So the
 * message stretched to the width of the thread and read as an orange bar across the
 * page rather than as something the customer said — a large part of why the
 * conversation did not look like one.
 *
 * `align-self` is the cross-axis property that works in a flex container, and the
 * narrower cap is what makes it read as a bubble rather than a banner.
 */
.turn--user {
  justify-self: end;
  align-self: flex-end;
  max-width: 78%;
  background: var(--pri);
  color: #fff;
  border-radius: 12px 12px 4px 12px;
  padding: 12px 16px;
}
/*
 * A message from the customer keeps the shape they typed it in.
 *
 * `white-space: pre-wrap` is the whole fix for a complaint the client sent as a screenshot: they
 * pasted a six-section setup brief into the box and it arrived as one solid block — every blank
 * line gone, every numbered section run together, and the markdown showing through as literal
 * asterisks. It was unreadable, and the bubble made it look as though the app had mangled their
 * words, because it had.
 *
 * Newlines are preserved rather than tidied into paragraphs on purpose. The customer pressed
 * Return where they meant it, and a pasted plan's indentation is part of how it reads. Normalising
 * it would be us deciding what their structure was.
 *
 * `overflow-wrap: anywhere` is here because a pasted brief can contain a long unbroken token — a
 * URL, a filename, a run of dashes — and without it that one token sets the bubble's width and
 * pushes the whole thread sideways on a phone.
 *
 * The height cap is not decoration. Allowing 4,000-character messages (see `MAX_REQUEST_CHARS` in
 * routes/ai.js) without capping the display would trade "your message was refused" for "your
 * message fills the screen for the next three turns", and the second one buries the assistant's
 * answer. It scrolls inside the bubble instead, so the text is all still there.
 */
.turn--user p {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 320px;
  overflow-y: auto;
}

/* ---- the concept card's caption ----
   Labelled rows instead of a run-on line, and every value able to break.

   `minmax(0, 1fr)` on the value column is the load-bearing part: a bare `1fr` refuses to go
   below the width of its longest unbreakable content, so one long layout string pushes the
   whole card wider than the grid and the text is clipped at the card edge — which is exactly
   the reported bug. With the 0 floor plus `overflow-wrap`, the value column wraps instead. */
.aix__cap {
  margin: 6px 0 0;
}

/* ---- the previous-renders drawer ----
   Closed by default, so the current picture is the only one competing for attention. Quiet on
   purpose: it is a door, not a heading. The picks inside stay live, because somebody who preferred
   an earlier render has to be able to choose it. */
.aix__more {
  margin-top: 8px;
}

.aix__more > summary {
  padding: 4px 0;
  font-size: 12.5px;
  color: var(--mut);
  cursor: pointer;
}

.aix__more > summary:focus-visible {
  outline: 2px solid var(--pri);
  outline-offset: 2px;
}

.aix__cap__brief {
  margin: 0;
  overflow-wrap: anywhere;
}

.aix__cap__facts {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 2px 8px;
  margin: 6px 0 0;
  font-size: 12px;
}

.aix__cap__facts dt {
  color: var(--mut);
}

.aix__cap__facts dd {
  margin: 0;
  overflow-wrap: anywhere;
}
.turn--bot {
  justify-self: start;
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 12px 12px 12px 4px;
  padding: 14px 16px;
}
.turn--bot > p { margin: 0; }
.turn--live { border-style: dashed; }
.turn--thinking { color: var(--mut); font-style: italic; }

.turn__tag {
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--gold);
}

.composer {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 10px;
  align-items: end;
  border-top: 1px solid var(--line);
  padding-top: 14px;
  position: sticky;
  bottom: 0;
  background: var(--card);
}
.composer textarea { width: 100%; resize: vertical; }

.space { display: grid; gap: 18px; margin-top: 36px; }
.space__grid { display: grid; gap: 18px; }
.space__panel {
  display: grid;
  gap: 10px;
  align-content: start;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 20px;
}
.space__panel.muted { background: var(--bg2); }
.space__step {
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--mut);
}

.drop {
  display: grid;
  gap: 6px;
  place-items: center;
  text-align: center;
  padding: 36px 16px;
  border: 2px dashed var(--line);
  border-radius: 10px;
  background: var(--bg);
  color: var(--mut);
  cursor: pointer;
  font: inherit;
}
.drop:hover { border-color: var(--pri); color: var(--pri); }

.preview { position: relative; border-radius: 10px; overflow: hidden; border: 1px solid var(--line); }
.preview img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; }
.preview button {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 0;
  background: #FFFFFFE0;
  color: var(--ink);
  cursor: pointer;
  font-size: 16px;
}

.quota {
  display: grid;
  gap: 2px;
  min-width: 160px;
  padding: 14px;
  border-radius: 10px;
  background: var(--bg2);
  border: 1px solid var(--line);
  text-align: center;
}
.quota.empty { background: #FBF0EC; border-color: #E7C6BA; }
.quota__count { font: 600 26px var(--serif); line-height: 1.1; }
.quota__label { font-size: 12px; color: var(--mut); }

.concepts { list-style: none; padding: 0; margin: 0; display: grid; gap: 14px; }
.concepts img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 10px;
  border: 1px solid var(--line);
}
.concepts p { margin: 6px 0 0; font-size: 13px; color: var(--mut); }

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

/* ========================================================= accounts === */

.auth { display: grid; gap: 28px; }
.auth__card {
  display: grid;
  gap: 12px;
  align-content: start;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 22px;
}
.auth__card h1 { font-size: clamp(24px, 3.6vw, 32px); line-height: 1.15; color: var(--ink); }
.auth__aside {
  display: grid;
  gap: 14px;
  align-content: start;
  background: var(--bg2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 22px;
}
.auth__aside h2 { font-size: 21px; font-family: var(--serif); }
.auth__list { list-style: none; padding: 0; margin: 0; display: grid; gap: 14px; }
.auth__list li { display: flex; gap: 10px; align-items: flex-start; font-size: 13px; color: var(--mut); }
.auth__list b { color: var(--ink); }
.auth__list span:first-child { color: var(--pri); }
.auth__form { display: grid; gap: 14px; }
.auth__foot { font-size: 13px; color: var(--mut); border-top: 1px solid var(--line); padding-top: 14px; }
.auth__foot a { color: var(--pri); text-decoration: underline; text-underline-offset: 3px; }

.fld { display: grid; gap: 4px; }
.err { font-size: 12.5px; color: var(--pri-deep); }
.consent { font-size: 13px; color: var(--mut); flex-direction: row; align-items: flex-start; gap: 8px; }
.consent input { margin-top: 2px; flex: none; width: 18px; height: 18px; }

.where {
  font-size: 12.5px;
  color: var(--mut);
  padding: 10px 12px;
  border-radius: 8px;
  background: var(--card);
  border: 1px solid var(--line);
}

@media (min-width: 900px) {
  .space__grid { grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); }
  .auth { grid-template-columns: minmax(0, 1fr) minmax(0, 0.9fr); }
}

@media (max-width: 860px) {
  nav {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: rgba(255, 255, 255, .98);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    padding: 12px 20px 18px;
    border-bottom: 1px solid var(--line);
    box-shadow: 0 14px 22px rgba(24, 42, 38, .08);
  }
  nav.open { display: flex; }
  nav a { padding: 11px 4px; border-bottom: 1px solid var(--line); }
  nav a:last-child { border-bottom: 0; }
  #mb { display: block; min-width: 44px; min-height: 44px; }
  .two,
  .contactgrid,
  .split,
  .twowide { grid-template-columns: 1fr; }
  main { padding: 28px 18px 52px; }
  /* `min-height: 0` from here down. Above this width the hero can afford to be a full screen; below it
     the centring that a min-height buys is slack that pushes the booking panel away from the top, and
     the panel's position is the one thing this page is being organised around. */
  .hero { min-height: 0; padding: 30px 26px 34px; }
  .hero h1 { max-width: none; }
  .hero p { max-width: none; }
  .hero__controls { left: auto; right: 26px; bottom: 20px; }
  .footer__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .footer__brand { grid-column: 1 / -1; max-width: 44ch; }
  /* The scrim runs left to right so the text column stays clean on a wide
     screen. On a phone the text wraps across the whole width, so a horizontal
     gradient leaves the right-hand words sitting on bare photograph. Turn it
     vertical instead: the picture softens top to bottom and the copy stays
     legible whatever the image does underneath.

     It stays dark rather than cream, which is the one thing this block got right by accident until
     now: the type it is protecting is light, and a light wash under light type is the reason the
     headline used to need a card behind it. */
  .hero__scrim {
    background:
      linear-gradient(180deg, rgba(9, 27, 21, .74) 0%, rgba(9, 27, 21, .56) 44%, rgba(9, 27, 21, .34) 100%);
  }

  /* No card, and no width cap: the panel below owns the full column now. */
  .hero__inner {
    max-width: 100%;
    background: none;
  }
}

@media (max-width: 420px) {
  body { font-size: 15px; }
  body { font-size: 16px; }
  header { min-height: 64px; padding: 9px 16px; gap: 12px; }
  .logo { font-size: 25px; }
  main { padding: 20px 14px 44px; }
  /* The bottom padding still reserves the slideshow controls, which are positioned against the hero's
     own bottom edge and would otherwise sit on top of the search field. It is free space as far as the
     first screen is concerned: everything that has to be visible is above it. */
  .hero { min-height: 0; padding: 22px 18px 74px; border-radius: 18px; }
  .hero__slide { object-position: 58% center; }
  .hero__inner { padding: 0; border-radius: 0; }
  /* `max-width: 11ch` sat here and forced the headline onto three lines on a 360px phone, where the
     authored two would have fitted. Measured, not guessed: at this size the ceiling that matters is the
     panel's, not the type's. */
  .hero h1 { max-width: none; font-size: clamp(25px, 8vw, 34px); line-height: 1.06; }
  .hero p { max-width: none; font-size: 15px; line-height: 1.45; }
  .hero .pill { font-size: var(--fs-2xs); }
  .hero__inner > .row { gap: 10px; }
  .hero__controls { left: auto; right: 18px; bottom: 14px; justify-content: flex-end; }
  .hero__dots { padding: 9px 10px; gap: 7px; }
  .hero__pause { min-height: 38px; padding: 8px 11px; }
  .ttl { font-size: clamp(28px, 9vw, 36px); }
  .sub { font-size: 16px; }
  .grid { gap: 12px; }
  .sec { margin-top: 28px; }
  .card { padding: 15px; border-radius: 16px; }
  .card h3 { font-size: 16.5px; }
  .btn { width: 100%; }
  .row .btn { flex: 1 1 180px; }
  .thread { padding: 14px; }
  .turn { max-width: 100%; }
  .composer { grid-template-columns: minmax(0, 1fr); }
  .quota { width: 100%; }
  .footer__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .footer__group { font-size: 14px; }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
  /* The customer explicitly requested this promotional slider to play. Keep its concise movement
     working even when the browser has a broader reduced-motion preference; its Pause control gives
     the visitor an immediate opt-out without disabling the rest of the site's motion safeguards. */
  .hero__slide {
    display: block;
    transition: transform .8s cubic-bezier(.22, .61, .36, 1), opacity .8s ease !important;
  }
}

/* =========================================================== AI page === */
/* Arranged the way ChatGPT, Gemini and Claude arrange theirs: a centred welcome
   on an empty page, one composer carrying the paperclip, the microphone and the
   send arrow, and the welcome lifting away the moment a conversation starts.

   The dock is sticky rather than fixed. It has to stay reachable on a long
   thread without leaving a permanent band over the page, and sticky does that
   inside .aix, which is what gives it somewhere to stick. */

.aix {
  display: flex;
  flex-direction: column;
  gap: 14px;
  min-height: calc(100vh - 220px);
}

.aix__stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 10px;
  padding: 8vh 16px 20px;
}
.aix__stage.gone { display: none; }
.aix__welcome {
  margin: 2px 0 0;
  color: var(--ink);
  font-size: clamp(26px, 5.4vw, 44px);
  line-height: 1.12;
}
.aix__lede { max-width: 54ch; margin: 0; color: var(--mut); }

/*
 * The opening greeting. The stage centres everything, which is right for a
 * heading and wrong for a message: a greeting has to read as the first line of a
 * conversation, so it is measured like the lede but aligned like a turn.
 */
.aix__hello {
  align-self: center;
  width: 100%;
  max-width: 54ch;
  margin-top: 12px;
  text-align: left;
}

.aix__thread { display: flex; flex-direction: column; gap: 14px; flex: 1 1 auto; }

/*
 * Room for the dock to hover over.
 *
 * The dock is sticky, so while the page is scrolled the last turn can end up behind
 * it — and the last turn is exactly where the answer chips are. This is the space
 * that lets the thread scroll clear of it, so a customer can always reach the chip
 * they want to tap.
 */
.aix__thread { padding-bottom: 72px; }

.aix__dock {
  position: sticky;
  bottom: 8px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px 0 6px;
  background: var(--bg);
}

/* ------------------------------------------------------------ composer --- */
.aix__composer {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 24px;
  padding: 7px 7px 5px;
  box-shadow: 0 6px 22px rgba(36, 31, 26, 0.07);
}
.aix__row { display: flex; align-items: flex-end; gap: 5px; }

.aix__ic,
.aix__send {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 50%;
  background: transparent;
  color: var(--mut);
  cursor: pointer;
}
.aix__ic:hover,
.aix__ic:focus-visible { background: var(--bg2); color: var(--ink); }
/* Listening, thinking or speaking: the microphone is live, and that has to be
   obvious from across the room. */
.aix__ic.on { background: var(--pri); border-color: var(--pri); color: var(--on); }
/* The allowance is spent, so the paperclip says so rather than opening a picker
   that could only fail. */
.aix__ic.out { opacity: 0.4; cursor: not-allowed; }

.aix__send { background: var(--pri); border-color: var(--pri); color: var(--on); }
.aix__send:hover { background: var(--ink); border-color: var(--ink); }

.aix__row textarea {
  flex: 1 1 auto;
  min-height: 38px;
  max-height: 168px;
  padding: 9px 4px;
  border: 0;
  background: transparent;
  color: var(--ink);
  font: 15px/1.5 var(--sans);
  resize: none;
}
.aix__row textarea:focus { outline: none; }

/* --------------------------------------------------- attached photo --- */
.aix__chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  max-width: 100%;
  margin: 4px 4px 0;
  padding: 4px 6px 4px 4px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--bg2);
}
.aix__chip img { display: block; width: 38px; height: 38px; border-radius: 8px; object-fit: cover; }
.aix__chip span {
  max-width: 30ch;
  overflow: hidden;
  color: var(--mut);
  font-size: 12.5px;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.aix__chip button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--mut);
  cursor: pointer;
}
.aix__chip button:hover { background: var(--line); color: var(--ink); }

.aix__chips { display: flex; flex-wrap: wrap; gap: 8px; }
.aix__chips:empty { display: none; }

/* ---------------------------------------------- answered in the thread --- */

/*
 * A question, its suggestions, and the answer — inside the conversation rather than
 * in a panel above it.
 *
 * These are ordinary bot turns with chips underneath, so they need very little: the
 * whole job here is to make the question read as a question and the chips read as
 * answers to it, at the size of a sentence rather than of a toolbar.
 */
.ask { display: block; }

.ask__q {
  margin: 0 0 2px;
  font-size: 14.5px;
  font-weight: 600;
  color: var(--ink);
}

.ask__note {
  display: block;
  margin-top: 6px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--mut);
}

.ask__chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 9px; }
.ask__chips .chip { padding: 6px 13px; font-size: 12.5px; }

/* The one that moves the conversation on, once a question is finished with. */
.chip--go { background: var(--pri); border-color: var(--pri); color: #fff; }

/*
 * A chosen answer is heavier as well as filled.
 *
 * The fill alone was not enough: a chip that merely has the mouse resting on it
 * looked the same as one that had been chosen, and somebody reading their own
 * conversation could not tell which of the two they had actually said.
 */
.ask__chips .chip.on { font-weight: 600; }

/* An answer already given: visible, spent, and clearly not pressable again. */
.ask__chips .chip[disabled] { opacity: 0.45; cursor: default; }

.ask--ready .ask__q { margin-bottom: 0; }

/* Said when a reply runs past four seconds: the wait, named. */
.turn__wait { display: block; margin-top: 6px; font-size: 12.5px; color: var(--mut); }

/* ------------------------------------------------- one picture, full size --- */

/*
 * The overlay a concept image opens into. A thumbnail is for comparing pictures; this
 * is for judging one, and for showing the person who is paying for it.
 */
.zoom {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 16px;
  background: rgba(28, 22, 16, 0.9);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  cursor: zoom-out;
}
.zoom.is-open { display: flex; }
.zoom img {
  max-width: 96vw;
  max-height: 80vh;
  border-radius: 10px;
  background: #fff;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
}
.zoom p {
  max-width: 90vw;
  margin: 0;
  color: #F6EFE6;
  font-size: 13px;
  line-height: 1.5;
  text-align: center;
}
.zoom__x {
  position: absolute;
  top: 12px;
  right: 14px;
  width: 38px;
  height: 38px;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.16);
  color: #fff;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}
.zoom__x:hover { background: rgba(255, 255, 255, 0.28); }

/* Anything with a full-size view says so, under the finger and under the mouse. */
img[data-zoom] { cursor: zoom-in; }

/* No scrolling the page behind the picture. */
html.is-zoomed { overflow: hidden; }

.aix__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 7px;
  color: var(--mut);
  font-size: 12.5px;
}
.aix__dot-sep { opacity: 0.5; }
.aix__quota.out { color: var(--pri); font-weight: 600; }
.aix__fine { margin: 0; color: var(--mut); font-size: 12px; }

/* --------------------------------------------------------- the thread --- */
/* Only what the existing .turn rules do not already cover. */
.turn__photo,
.turn__img {
  display: block;
  max-width: min(430px, 100%);
  border: 1px solid var(--line);
  border-radius: 14px;
}
.turn__lead { margin: 0; color: var(--mut); font-size: 12.5px; }

.aix__concepts {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 10px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.aix__concepts li { margin: 0; }
.aix__concepts img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  border: 1px solid var(--line);
  border-radius: 12px;
  object-fit: cover;
}
.aix__concepts p { margin: 5px 0 0; color: var(--mut); font-size: 12px; }

/*
 * The AI note under every concept image.
 *
 * Deliberately readable rather than fine print: the picture is convincing, and a
 * customer comparing it with their own garden needs to be told plainly that this
 * is an impression and not the space. It is set in the same paper tone as the
 * rest of the page and separated by a rule, so it reads as part of the answer
 * instead of as a legal notice nobody reads.
 */
.aix__ai-note {
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--line);
  color: var(--mut);
  font-size: var(--fs-2xs);
  line-height: 1.5;
}

/*
 * "Use this one" — which concept goes on the receipt.
 *
 * Quiet until it is chosen, then plainly marked: the office copy of the receipt is
 * a work order, and the picture on it is what the team builds towards, so the
 * customer has to be able to see at a glance which one they have committed to.
 */
.aix__pick {
  margin-top: 6px;
  padding: 5px 12px;
  border: 1px solid var(--line);
  border-radius: 100px;
  background: var(--card);
  color: var(--ink-soft);
  font: 600 12px/1.4 Karla, Helvetica, Arial, sans-serif;
  cursor: pointer;
}
.aix__pick:hover { border-color: var(--clay); color: var(--clay); }
.aix__pick.is-on {
  background: var(--clay);
  border-color: var(--clay);
  color: #fff;
}

/*
 * The receipt card in the thread.
 *
 * The number is the point of it: it is what a customer reads out on the phone, so
 * it is set large enough to be read off a screen while talking.
 */
.receipt {
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--card);
  padding: 14px 16px;
}
.receipt__num {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  margin: 0;
}
.receipt__num b { font-size: 16px; letter-spacing: 0.02em; }
.receipt__num span { color: var(--mut); font-size: 12px; }
.receipt__total {
  margin: 6px 0 0;
  font: 700 22px/1.2 Lora, Georgia, serif;
  color: var(--ink);
}
.receipt .mut { margin: 8px 0 0; }

/*
 * The running total under the itemised list. Heavier than the lines above it
 * because it is the number the customer is being asked to react to.
 */
.ans__total {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px;
  margin: 10px 0 0;
  padding-top: 10px;
  border-top: 1px solid var(--line);
  color: var(--ink);
  font-size: 15px;
}
.ans__total b { font-weight: 700; }

@media (max-width: 640px) {
  /* No room to pin anything on a phone, and the keyboard already pushes the
     composer into view, so it goes back into the flow. */
  .aix__dock { position: static; }
  .aix__stage { padding: 4vh 8px 14px; }
  /* 16px stops iOS zooming the whole page when the box is focused. */
  .aix__row textarea { font-size: 16px; }
  .aix__welcome { font-size: clamp(24px, 8vw, 32px); }
}

/* ============================================== AI answer, made readable === */
/* The answer used to be eight stacked blocks, and the number a customer came for
   sat in the middle of them. Now the figure leads at full size, each covered item
   gets one line, and everything supporting folds into one expander — so the same
   information is there without being the first thing anybody has to read. */

.ans { display: grid; gap: 12px; }
.ans__lede { margin: 0; }
.ans__none { margin: 0; color: var(--mut); }

.ans__cap {
  margin: 0 0 6px;
  color: var(--mut);
  font: 600 11.5px var(--sans);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.ans__list {
  display: grid;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.ans__list li {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 9px;
  align-items: baseline;
  padding: 9px 12px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--card);
}
.ans__list b { flex: 0 0 auto; color: var(--ink); font-size: 14px; }
.ans__list span { color: var(--mut); font-size: 13px; }

.ans__more {
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--card);
}
.ans__more summary {
  padding: 11px 13px;
  color: var(--mut);
  font-size: 13.5px;
  cursor: pointer;
  list-style: none;
}
.ans__more summary::-webkit-details-marker { display: none; }
.ans__more summary::before { content: '\25B8\00A0\00A0'; }
.ans__more[open] summary::before { content: '\25BE\00A0\00A0'; }
.ans__more summary:hover { color: var(--ink); }

.ans__inner {
  display: grid;
  gap: 14px;
  padding: 13px;
  border-top: 1px solid var(--line);
}
.ans__inner h4 {
  margin: 0 0 5px;
  color: var(--ink);
  font: 600 12.5px var(--sans);
  letter-spacing: 0.03em;
}
.ans__inner ul { margin: 0; }

/* The figure, at the size it deserves. */
.money { display: grid; gap: 3px; }
.money__total {
  color: var(--ink);
  font: 600 clamp(27px, 5vw, 40px) / 1.08 var(--serif);
  letter-spacing: -0.01em;
}
.money__per { color: var(--mut); font-size: 14px; }
.money__badge {
  justify-self: start;
  margin-top: 7px;
  padding: 3px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--bg2);
  color: var(--mut);
  font-size: 11.5px;
  letter-spacing: 0.04em;
}

/*
 * ─────────────────────────────────────────────────────────────────────────────
 * Long messages must not escape the phone.
 *
 * Measured rather than guessed. With the rules below absent, one unbroken token in a bot message
 * made the card **1027 px wide on a 390 px phone** — 677 px past the edge, with 777 px of it
 * hidden inside `.thread__body`, which is the scroll container and therefore clips instead of
 * pushing the page sideways. That is what "the message goes off the screen" is.
 *
 * Ordinary sentences were never the problem: they wrap. It is content with nowhere to break —
 * a pasted URL, a reference number, a base64 blob — and it takes only one.
 *
 * Two properties, for two different reasons. Both are needed, and neither is enough alone:
 *
 *   min-width: 0
 *     A grid or flex item defaults to `min-width: auto`, meaning "never narrower than my longest
 *     unbreakable word". That default is what lets a single token widen a whole column of the
 *     card, and every element inside it along with it.
 *
 *   overflow-wrap
 *     `break-word` alone would wrap the text and the column would STILL be too wide, because it
 *     does not change that minimum. `anywhere` also shrinks the element's min-content size,
 *     which is the value the track is actually sized from. It is the difference between a page
 *     that reflows and a page that reflows *and* fits. `break-word` is declared first as the
 *     fallback for engines that do not know `anywhere`; the later declaration wins where it is
 *     supported.
 *
 * Scoped to the conversation, the quote card and the receipt. Prose elsewhere on the site has
 * never shown this, and a rule applied to everything is a rule nobody can reason about later.
 */
.thread__body,
.turn,
.turn--bot,
.turn--user,
.ans,
.ans > *,
.ans__lede,
.ans__list,
.ans__list li,
.ans__list li > *,
.ans__total,
.ans__inner,
.ans__inner *,
.notice,
.receipt,
.receipt > *,
.money,
.money > *,
.gate p {
  min-width: 0;
  overflow-wrap: break-word;
  overflow-wrap: anywhere;
}

/*
 * The labelled lines of the quote are a flex row — label, then the price — and `flex: 0 0 auto`
 * on the label means it will not shrink however long it gets. A long service name from the model
 * is therefore still able to push the row past the card, which the rules above cannot fix because
 * they may not override an explicit flex basis. Letting the pair wrap puts the price on its own
 * line instead, which is the same call the list already makes for its own items.
 */
.ans__list li > b { flex: 0 1 auto; }

/* ==========================================================================
   The itemized receipt

   A real table, because the specification asks for an Excel-style itemized
   breakdown and the eye needs columns to run down. The cart used to show
   grouped cards: the same facts, but nothing can be checked column by column
   in a stack of cards, and checking is the whole point of a receipt.

   The wrapper scrolls sideways rather than crushing the columns on a phone.
   A 375px screen cannot hold five columns of readable text, and a table that
   reflows into an unlabelled pile of numbers is worse than one you swipe
   across — the header row stays attached to its columns this way, which a
   stacked reflow cannot do.
   ========================================================================== */

.rtwrap {
  margin: 14px 0;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
}

table.rt {
  width: 100%;
  border-collapse: collapse;
  /* Below this the five columns stop being readable, so the wrapper scrolls instead. */
  min-width: 560px;
  font-family: var(--sans);
  font-size: 14.5px;
  color: var(--ink);
}

table.rt th,
table.rt td {
  padding: 9px 12px;
  text-align: left;
  vertical-align: top;
}

table.rt thead th {
  font-size: var(--fs-2xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--mut);
  white-space: nowrap;
  border-bottom: 2px solid var(--line);
  background: #1F5A4A12;
}

/* The category is the row header, so it is a <th scope="row"> and not a <td>: a screen reader
   then announces "Rentals, Chiavari chairs, 80, ..." rather than a row of loose numbers. */
table.rt tbody th[scope='row'] {
  font-weight: 700;
  color: var(--pri);
  white-space: nowrap;
}

table.rt tbody tr + tr > * {
  border-top: 1px solid var(--line);
}

table.rt tbody tr:nth-child(even) > * {
  background: #D7E1DD55;
}

/* Right-aligned and tabular, so a column of money lines up on the digits rather than the
   first character — which is what makes a wrong total visible instead of merely present. */
table.rt .rt__n {
  text-align: right;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

table.rt tfoot th,
table.rt tfoot td {
  border-top: 2px solid var(--ink);
  font-weight: 700;
  padding-top: 11px;
  padding-bottom: 11px;
}

table.rt tfoot th {
  text-align: right;
}

.rt__total {
  font-size: 16px;
}

.rt__note {
  margin: 6px 2px 0;
  font-size: 13px;
  line-height: 1.5;
}

/* ---- the swap control ----
   Deliberately quiet. It sits in every row that has alternatives, so it must not compete with the
   money — a customer reading their total should not be reading a column of buttons. It stays a
   full-size hit target, because on a phone the alternative is a control nobody can reliably press. */
.rt__swapcell {
  vertical-align: middle;
  white-space: nowrap;
}

.rt__swap {
  max-width: 100%;
  padding: 5px 6px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--card);
  font: inherit;
  font-size: 12.5px;
  color: var(--mut);
  cursor: pointer;
}

.rt__swap:focus-visible {
  outline: 2px solid var(--pri);
  outline-offset: 1px;
}

.rt__swap:hover {
  color: var(--ink);
}

@media (max-width: 520px) {
  table.rt {
    font-size: 13.5px;
    min-width: 520px;
  }

  table.rt th,
  table.rt td {
    padding: 8px 9px;
  }
}

/* ==========================================================================
   The way forward, on the picking page

   Sticky to the bottom of the viewport, because its whole job is to be
   reachable while the customer is scrolling a grid of forty rental items.
   A bar that scrolls away is a bar they never see, which is the problem it
   was added to solve.

   It is `sticky` rather than `fixed` deliberately: sticky takes part in the
   layout, so it sits after the last card and cannot cover it, and it releases
   at the end of the page instead of floating over the footer. `.composer` on
   the AI page already uses the same trick, so this is the house pattern
   rather than a new one.

   z-index 5 keeps it above the cards it passes over and below the modal
   overlay (`.mdl`, z-index 20).
   ========================================================================== */

.proceed {
  position: sticky;
  bottom: 0;
  z-index: 5;
  display: flex;
  flex-wrap: wrap;
  gap: 10px 16px;
  align-items: center;
  justify-content: space-between;
  margin-top: 24px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
  box-shadow: 0 -6px 20px #241f1a1a;
}

.proceed__t {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.proceed__t b {
  font-size: 15px;
}

.proceed__t .mut {
  font-size: 13px;
}

.proceed__a {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.proceed__a .btn {
  white-space: nowrap;
}

/* On a phone the two buttons stop fitting beside the text, so they take the whole
   width and share it. Equal halves rather than one wide and one narrow: they are
   the same journey, and a narrow secondary reads as the lesser option rather than
   as the shortcut it is. */
@media (max-width: 420px) {
  .proceed__a {
    width: 100%;
  }

  .proceed__a .btn {
    flex: 1 1 0;
    text-align: center;
  }
}

/* ------------------------------------------------- AI: while it is working --- */

/*
 * The line the customer reads during the wait.
 *
 * It replaced a single static word, and the difference is the whole point: "Thinking…" that
 * never changes is indistinguishable from a page that has stopped, while a line that moves is
 * evidence that something is happening. Text and one small dot — no spinner, no progress bar,
 * because there is no honest percentage to show and a bar that stalls is worse than no bar.
 *
 * Only the dot moves, and it moves slowly on purpose. It is also switched off for anybody who
 * has asked their system for reduced motion, which is a preference about exactly this.
 */
.aix__wait { display: flex; align-items: center; gap: 8px; margin: 0; }

.aix__wait__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--pri);
  flex: none;
  animation: aixWait 1.2s ease-in-out infinite;
}

@keyframes aixWait {
  0%, 100% { opacity: 0.25; }
  50% { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .aix__wait__dot { animation: none; opacity: 0.75; }
}

/*
 * The taps under a question.
 *
 * `ask__chips` already lays out a row of chips — reused rather than copied, so the two rows on
 * this page cannot drift apart. All that is added is the space between the numbered question and
 * the answers to it.
 */
.aix__answers { margin-top: 12px; }

/*
 * The way to a person, in the row under the composer.
 *
 * Underlined rather than coloured: it sits in a row of muted status text where a coloured link
 * would read as the most important thing on the screen, and it is not — it is the way out, for
 * the customer who does not want to talk to a machine at all.
 */
.aix__human { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.aix__human:hover { color: var(--pri); }

/* --------------------------------------------------- AI: the answer, arriving --- */

/*
 * The part of the answer that arrives before the answer.
 *
 * This is the summary being written out by the model, so it has to read as the assistant
 * speaking — the same type, the same place, no special frame — and at the same time it has to be
 * obvious that more is coming. Without that, a customer who reads one sentence and then watches
 * it be replaced by a card will think the page changed its mind. Hence the caret at the end:
 * a thing still being typed.
 *
 * `pre-wrap` because a summary can contain a newline, and collapsing it turns a two-part answer
 * into one run-on line.
 */
.turn--stream .turn__live { margin: 0; white-space: pre-wrap; }

.turn--stream .turn__live::after {
  content: '';
  display: inline-block;
  width: 0.5ch;
  height: 1em;
  margin-left: 2px;
  vertical-align: -0.15em;
  background: currentColor;
  opacity: 0.55;
  animation: aixCaret 1s steps(2, start) infinite;
}

@keyframes aixCaret {
  0%, 50% { opacity: 0.55; }
  50.01%, 100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .turn--stream .turn__live::after { animation: none; }
}

/*
 * The wait line steps aside once there are real words on screen, and comes back if the attempt
 * those words belonged to is abandoned — see the empty-summary case in `send()`.
 *
 * `.gone` is only defined for the stage elsewhere on this page, so the rule is scoped here
 * rather than extended: `.aix__stage.gone` is about the welcome screen, and a class that hides
 * one thing must not start hiding another by accident.
 */
.turn--thinking.gone { display: none; }

/* ------------------------------- AI: your space, and the event in it --- */

/*
 * The comparison under a concept image.
 *
 * The most convincing thing this product has, and the honest one: a concept image on its own is
 * a pretty picture of somebody's yard, while the photograph and the concept together say "this
 * is your yard, and this is your yard with the event in it". They also prove the promise the
 * design is built on — that the space itself was not tidied up or redecorated to make the
 * picture work.
 */
.aix__cmp {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
}

.aix__cmp__pair { position: relative; display: block; }

.aix__cmp__img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

/*
 * The concept sits on top of the photograph and is wiped in from the left by `--cut`.
 *
 * `inset(0 0 0 X%)` keeps the RIGHT-hand side, so dragging right reveals more of the event and
 * dragging left reveals more of the space — the direction everyone expects from a control that
 * reads left-to-right. The fallback value matches the markup's, so the first frame is correct
 * before anything has been touched.
 */
.aix__cmp__img--top {
  position: absolute;
  inset: 0;
  clip-path: inset(0 0 0 var(--cut, 50%));
}

/*
 * Which side is which.
 *
 * Without these the picture is ambiguous in exactly the wrong way: a customer who drags to the
 * left sees more of their own yard and could reasonably conclude that the concept has been
 * removed rather than that they are looking at the "before". `pointer-events: none` so the tags
 * can never swallow a tap meant for the picture.
 */
.aix__cmp__tag {
  position: absolute;
  bottom: 8px;
  padding: 3px 9px;
  border-radius: var(--radius-pill);
  background: rgba(36, 31, 26, 0.62);
  color: #fff;
  font-size: var(--fs-2xs);
  letter-spacing: 0.02em;
  pointer-events: none;
}
.aix__cmp__tag--left { left: 8px; }
.aix__cmp__tag--right { right: 8px; }

.aix__cmp__ctl {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 7px;
}

/*
 * The slider itself.
 *
 * 24px of height rather than the default, because this is the only control on the page that is
 * dragged and the default thumb is smaller than a thumb. `min-width: 0` because a flex item will
 * not shrink below its content by default, and a range that refuses to shrink is what pushes a
 * card wider than its column on a narrow phone.
 */
.aix__cmp__range {
  flex: 1 1 auto;
  min-width: 0;
  height: 24px;
  accent-color: var(--pri);
}

.aix__cmp__zoom {
  flex: none;
  padding: 5px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--card);
  color: inherit;
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
}
.aix__cmp__zoom:hover { border-color: var(--pri); }

/* ------------------------------------------------- the running total, everywhere --- */

/*
 * The bar inside the chat's dock.
 *
 * On every other page the bar is the last thing in the flow and its 24px top margin is the gap
 * between the page and the bar. Inside the dock there is no page below it — the dock is a
 * column with its own 8px gap, and the bar sits directly above the composer. Keeping the margin
 * there would leave a band of dead space between two things that are meant to read as one
 * stack, so it is removed and the dock's own gap does the spacing.
 *
 * Its `bottom: 0` from `.proceed` is harmless here and deliberately left alone: the dock is the
 * sticky element, so the bar's own offset never applies. Overriding it would be a second place
 * to keep in step with a rule that already does nothing.
 */
.aix__dock .proceed { margin-top: 0; }

/* ==========================================================================
   "One go" — search, universal Add, and the cart's tick column

   Appended as one block rather than threaded into the sections above, because every rule here
   belongs to features that did not exist when those sections were written: the global search box,
   the [Add to cart] affordance on the cards that had none, the [+ Add] inside the assistant's
   answers, and the Select column of the cart's table. Keeping them together means the whole of
   the new surface can be read — and reviewed, and removed — in one place.
   ========================================================================== */

/* ------------------------------------------------------- the search box --- */

/*
 * The header's box.
 *
 * A pill rather than a rectangle, because it sits in a row of links and a hard-edged field there
 * reads as a form that has leaked into the navigation. It is deliberately narrow: it is the
 * always-there entry point, not the one a customer starts from — the hero's box is that one.
 */
.navq {
  flex: 0 1 190px;
  width: 190px;
  max-width: 24vw;
  padding: 8px 14px;
  border-radius: var(--radius-pill);
  background: var(--bg);
  font-size: 13px;
}

/*
 * On a phone it takes a row of its own rather than shrinking to nothing.
 *
 * The alternative was hiding it below the breakpoint and relying on the hero's box — which would
 * have left the rentals page and the menu page with no search at all, on the device most
 * customers will be using. A second row inside the sticky header costs about 48px of height and
 * keeps the one box that reaches the whole catalogue reachable from every page.
 */
@media (max-width: 860px) {
  header { flex-wrap: wrap; }
  .navq {
    order: 5;
    flex: 1 1 100%;
    width: 100%;
    max-width: none;
  }
}

/* The hero's box — the same search, sized for the screen a customer lands on. */
/* The hero's own search sits under the booking panel and is the third thing down the page, so it is
   tinted for the photograph rather than left on `--card` — a dark green field in dark mode reads as a
   hole in the table. Literal cream and a literal ink, for the same reason the pills use literals: this
   section is dark whatever the theme is. */
.hero__search { margin-top: var(--sp-4); max-width: 460px; }
.hero__search input {
  width: 100%;
  padding: 13px 18px;
  border-radius: var(--radius-pill);
  border-color: rgba(255, 255, 255, .5);
  background: rgba(255, 253, 248, .93);
  color: #182A26;
  /* 16px, because anything smaller makes iOS zoom the page in when the field is focused. */
  font-size: 16px;
}

.hero__search input::placeholder { color: rgba(83, 99, 94, .8); }

/* ------------------------------------------------------ the search results --- */

.srch { margin-top: 16px; }
.srch__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
.srch__group + .srch__group { margin-top: 16px; }
.srch__cap {
  margin: 0 0 6px;
  color: var(--mut);
  font: 600 11.5px var(--sans);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.srch__row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  border-top: 1px solid var(--line);
}
.srch__im {
  width: 52px;
  flex: none;
  aspect-ratio: 1;
  border-radius: var(--radius-sm);
  font-size: 22px;
}
.srch__t {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1 1 auto;
  /* Without this a long item name refuses to wrap inside a flex child and pushes the Add button
     off the right-hand edge — at 375px, which is the width this page is designed at. */
  min-width: 0;
}
.srch__t b { color: var(--ink); font-size: 14px; overflow-wrap: anywhere; }
.srch__t span { font-size: 12.5px; }
.srch__row > .btn { flex: 0 0 auto; }

/* ---------------------------------------------- Add on the catalogue cards --- */

/*
 * The [Add to cart] line on a cuisine, rental, decor, service or planning card.
 *
 * Drawn as a pill on the card's own surface rather than as a real button, because the card IS the
 * button (see `pc` in script.js) and a nested one would be invalid HTML with unpredictable tap
 * behaviour. It changes appearance with the card's own pressed state, so "Add to cart" becoming
 * "In your cart ✓" is the same signal as the card filling in — one state, said twice.
 */
.icn__add {
  display: inline-block;
  margin-top: 8px;
  padding: 6px 14px;
  border: 1px solid var(--pri);
  border-radius: var(--radius-pill);
  color: var(--pri);
  font: 600 12.5px var(--sans);
}
.icn.sel .icn__add {
  background: var(--pri);
  color: #fff;
}

/* ------------------------------------------------- a dish, and its own Add --- */

/*
 * The menu page's dish rows.
 *
 * The checkbox and the Add button mean different things and must not be confused for one: the
 * checkbox says "send this menu to the team", the button puts the single dish on the cart. The
 * button is outside the label so a tap on it does not also tick the box.
 */
.mirow {
  display: flex;
  align-items: center;
  gap: 10px;
}
.mirow .mi { flex: 1 1 auto; min-width: 0; }
.mirow > .btn { flex: 0 0 auto; margin-bottom: 10px; }
.detail__price { color: var(--pri); font: 600 20px var(--serif); }
.help-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 14px; }
.help-grid details { margin: 0; }
.help-grid summary { cursor: pointer; color: var(--pri); font: 600 16px var(--serif); }
.help-grid details p { margin: 10px 0 0; }
@media (max-width: 640px) { .help-grid { grid-template-columns: 1fr; } }
@media (max-width: 420px) {
  .mirow { flex-wrap: wrap; }
  .mirow > .btn { width: 100%; }
}

/* ---------------------------------------------- guided quote builder --- */
.builder-layout { align-items: start; }
.builder-main { min-width: 0; }
.builder-step-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 0 12px;
  border-bottom: 1px solid var(--line);
  color: var(--mut);
  font-size: 12px;
  letter-spacing: .04em;
  text-transform: uppercase;
}
.builder-step-head b { color: var(--ink); font: 600 18px var(--serif); letter-spacing: 0; text-transform: none; }
.builder-step-head small { margin-left: auto; color: var(--mut); font-size: var(--fs-2xs); font-weight: 400; letter-spacing: 0; text-transform: none; }
.builder-intro { margin: 18px 0 8px; max-width: 62ch; }
.builder-intro h3 { margin-top: 6px; color: var(--ink); font-size: clamp(22px, 3vw, 28px); }
.builder-intro p { margin: 6px 0 0; color: var(--mut); }
.builder-group-label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin: 20px 0 0;
  color: var(--ink);
}
.builder-group-label span { color: var(--mut); font-size: 12px; }
.builder-subheading { margin: 24px 0 0; color: var(--ink); font-size: 20px; }
.builder-actions {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin-top: 24px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
}
.builder-actions__right { display: flex; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
.quote-rail {
  position: sticky;
  top: 92px;
  min-width: 0;
  gap: 14px;
  box-shadow: 0 2px 12px rgba(36, 31, 26, .06);
}
.quote-rail__head { display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; }
.quote-rail__head h3 { margin-top: 4px; color: var(--ink); font-size: 22px; }
.quote-rail__count { color: var(--pri); font-weight: 700; white-space: nowrap; }
.quote-rail__brief { display: grid; gap: 5px; padding: 12px; border-radius: 10px; background: var(--bg); color: var(--mut); font-size: 12px; }
.quote-rail__groups { display: grid; gap: 2px; }
.quote-rail__group { display: flex; justify-content: space-between; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--line); color: var(--mut); font-size: 13px; }
.quote-rail__group b { color: var(--ink); font-weight: 600; text-align: right; }
.quote-rail__total { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; padding-top: 6px; color: var(--mut); }
.quote-rail__total strong { color: var(--ink); font: 600 20px var(--serif); text-align: right; }
.quote-rail__pending, .quote-rail__missing, .quote-rail__ready, .quote-empty { margin: 0; padding: 10px; border-radius: 8px; font-size: 12px; }
.quote-rail__pending { background: #F6EBD4; color: #8A6217; }
.quote-rail__missing { display: grid; gap: 3px; background: #F7EBD2; color: #7B5A18; }
.quote-rail__missing span { color: #6C5120; }
.quote-rail__ready { background: #E3F0E7; color: #2F7A4F; }
.quote-empty { background: var(--bg); color: var(--mut); }
.builder-mobile-bar { display: none; }

/* Keep the guided controls comfortable on touch screens. */
.builder-main .card.icn { min-height: 210px; }
.builder-main .card.icn .icn__add { margin-top: auto; }
.builder-main .card.icn.sel .icn__add { background: var(--pri); color: #fff; }

@media (max-width: 860px) {
  .builder-layout { grid-template-columns: 1fr; }
  .builder-layout > .quote-rail { position: static; order: 2; }
  .builder-main { order: 1; }
}

@media (max-width: 640px) {
  .builder-layout > .quote-rail { display: none; }
  .builder-main { padding-bottom: 82px; }
  .builder-mobile-bar {
    position: fixed;
    z-index: 8;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    min-height: 72px;
    padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
    background: rgba(255, 253, 248, .98);
    border-top: 1px solid var(--line);
    box-shadow: 0 -8px 24px rgba(36, 31, 26, .08);
  }
  .builder-mobile-bar > div { min-width: 0; display: grid; gap: 2px; }
  .builder-mobile-bar b { color: var(--ink); }
  .builder-mobile-bar span { color: var(--mut); font-size: 12px; overflow-wrap: anywhere; }
  .builder-mobile-bar .btn { flex: 0 0 auto; min-height: 44px; }
  .builder-actions { align-items: stretch; }
  .builder-actions__right { flex: 1 1 auto; }
  .builder-actions__right .btn { flex: 1 1 auto; }
  .builder-group-label { align-items: flex-start; flex-direction: column; gap: 3px; }
  .builder-main .grid.g4 { grid-template-columns: 1fr; }
  .builder-main .card.icn { min-height: 0; text-align: left; align-items: stretch; }
  .builder-main .card.icn .im { aspect-ratio: 16 / 8; }
}

/* ------------------------------------------------- [+ Add] in the assistant --- */

/*
 * The [+ Add] on a suggestion and on a line of the itemised quote.
 *
 * `.ans__list li` is a wrapping flex row, so the button simply becomes the row's last child and
 * drops onto its own line when the words above it are long — which on a phone is most of the
 * time. Nothing about the list's own layout had to change for it.
 */
.ans__actions {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-left: auto;
}
.ans__use,
.ans__add {
  flex: 0 0 auto;
  align-self: center;
  min-height: 38px;
  padding: 7px 13px;
  font-size: 12.5px;
}
.ans__use { background: var(--pri); color: #fff; border-color: var(--pri); }
.ans__use:hover { background: var(--pri-deep); }
.ai-image-invite { padding: 14px; border: 1px solid var(--line); border-radius: 12px; background: var(--bg2); }
.ai-image-invite p { margin: 0 0 6px; }
.ai-image-invite .ask__chips { margin-top: 10px; }


/* ------------------------------------------------------------- tap targets --- */

/*
 * The new controls, at a size a thumb can hit.
 *
 * The site's own `.btn.sm` is around 32px tall, which is fine for a mouse and short of the 44px
 * the spec asks for on a touch screen. These are the three controls the new work adds, and they
 * are the three a customer uses most while moving quickly — so they are given the height here
 * rather than by changing `.btn.sm` itself, which every older button on the site also uses.
 */
.srch__row > .btn,
.mirow > .btn,
.ans__add {
  min-height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}


/* ==========================================================================
   Persistent pricing dock

   The dual-calculus summary. It sits beside the receipt so the two price
   behaviours (per item, per person) stay visibly apart instead of collapsing
   into one unexplained number. Sticky on wide screens only: on a narrow phone
   a sticky block would eat the viewport it is meant to summarise.
   ========================================================================== */
/*
 * The running-price panel on the cart and the final review — the panel a
 * customer reads immediately before sending anything.
 *
 * RESTYLED IN THE MOBILE PASS, here rather than by an override, so that the
 * panel has one definition instead of two that can drift apart.
 *
 * It was white, with black borders, `border-radius: 10px`, and a 4px left stripe
 * of `var(--accent, #8a1c3d)`. `--accent` IS DEFINED NOWHERE IN THIS PROJECT, so
 * that stripe was rendering its own fallback: a dark crimson, on a cream-and-green
 * site, on the two screens that carry the money. And because the white was a
 * literal rather than a token, the panel stayed a bright slab in dark mode while
 * every surface around it went dark — on a phone, at night, on the page showing
 * the total.
 *
 * Built from the same tokens as everything else now, so both themes are correct
 * by construction. The stripe is kept and recoloured rather than deleted: it is
 * a deliberate pointer at the sum, and only its colour was ever wrong.
 */
.pdock {
  margin: 18px 0;
  padding: 16px 18px;
  background: var(--card);
  border: 1px solid var(--line);
  border-left: 4px solid var(--pri);
  border-radius: var(--radius);
  box-shadow: var(--elev-1, 0 1px 2px rgba(24, 42, 38, .04), 0 2px 6px rgba(24, 42, 38, .05));
}

.pdock__head h3 {
  font-size: 17px;
  margin: 0 0 10px;
  color: var(--ink);
}

.pdock__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 6px 0;
  font-size: 15px;
  color: var(--ink);
}

.pdock__sub {
  font-size: 13.5px;
  color: var(--mut);
  padding: 2px 0;
}

.pdock__total {
  margin-top: 8px;
  padding-top: 10px;
  /* Was a literal 12% black. On the panel's old white surface that read as a
     hairline; on the dark-theme surface it is black on near-black, so the rule
     separating the total from the lines above it simply vanished — in the one
     place a customer checks the number. It is a token now, and both themes get
     a line they can see. */
  border-top: 1px solid var(--line);
  font-size: 16px;
}

.pdock__total b {
  font-size: 19px;
}

.pdock__note {
  margin: 10px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
}

/* Wide screens: keep the summary in view while the long receipt scrolls. */
@media (min-width: 900px) {
  .pdock {
    position: sticky;
    top: 84px;
  }
}

/* Respect a stated preference for less motion and strong contrast needs. */
@media (prefers-reduced-motion: reduce) {
  .pdock {
    scroll-behavior: auto;
  }
}

@media (forced-colors: active) {
  .pdock {
    border-color: CanvasText;
  }
}

@media (max-width: 900px) {
  .flow-shell, .plan-shell, .itemlist-shell { grid-template-columns: 1fr; }
  .flow-aside, .plan-shell .quote-rail, .itemlist-shell .quote-rail { position: static; }
}

@media (max-width: 640px) {
  .journey-entry { grid-template-columns: 1fr; padding: 16px; }
  .journey-entry__links { grid-template-columns: repeat(2, 1fr); }
  .journey-steps { margin-right: -8px; margin-left: -8px; border-radius: 0; padding-inline: 10px; }
  .journey-steps__items { gap: 2px; }
  .journey-step { padding-inline: 2px; font-size: var(--fs-2xs); }
  .journey-step i { width: 26px; height: 26px; }
  .journey-steps__meta { font-size: var(--fs-2xs); }
  .resume-card { align-items: flex-start; }
  .resume-card .btn { min-height: 40px; padding-inline: 12px; }
  .builder-step-head { align-items: flex-start; flex-wrap: wrap; }
  .builder-step-head small { flex-basis: 100%; margin-left: 0; }
  .flow-main, .flow-aside, .plan-board, .itemlist-card { padding: 16px; }
  .flow-aside .im { margin: -16px -16px 4px; width: calc(100% + 32px); }
  .confirmation-card { grid-template-columns: 1fr; gap: 0; }
  .confirmation-card__image { min-height: 220px; }
  .confirmation-card > div:last-child { padding: 20px 16px; }
  .itemlist-head { align-items: flex-start; flex-direction: column; }
  .plan-item { grid-template-columns: 30px minmax(0, 1fr); }
  .plan-item strong { grid-column: 2; }
}


/* ==========================================================================
   DESIGN SYSTEM — scales, semantic states and primitives

   The palette, radii and font stacks above were already tokenised. Spacing,
   type sizes, elevation, motion and semantic states were not: those values were
   written as literals wherever they were needed, which is why two cards on the
   same page could sit at two different heights and why a state colour had to be
   typed out six times (three light, three dark) to stay consistent.

   Everything below is a token, and every rule that follows uses one. Nothing
   here changes structure, class names or behaviour — it is the presentation
   layer only.

   Retrofitting the ~2,700 lines above onto these scales is a separate, larger
   job and is deliberately NOT attempted here: doing it wholesale in one pass,
   with no build step and no automated visual regression, would be how a palette
   and a layout quietly drift. The scales exist so that migration is mechanical
   when it happens.
   ========================================================================== */

:root {
  /* Spacing — a 4px base on a ~1.5 rhythm. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;
  --sp-8: 64px;
  --sp-9: 96px;

  /* Type scale. 12px is the floor for anything a customer has to read: below it,
     the uppercase letterspaced labels stop being legible on a phone in daylight,
     and several sat at 9–10px. */
  --fs-2xs: 12px;
  --fs-xs: 13px;
  --fs-sm: 14px;
  --fs-base: 16px;
  --fs-md: 18px;
  --fs-lg: 22px;
  --fs-xl: 28px;
  --fs-2xl: 36px;
  --fs-display: clamp(36px, 5.4vw, 58px);

  /* Elevation. One language, four steps. `--elev-0` is deliberately flat: it is
     what a surface uses when it should read as part of the page, not above it.
     Shadows are tinted with the ink colour rather than pure black, so a shadow
     on the warm paper background stays warm instead of going grey. */
  --elev-0: none;
  --elev-1: 0 1px 2px rgba(24, 42, 38, .04), 0 2px 6px rgba(24, 42, 38, .05);
  --elev-2: 0 2px 4px rgba(24, 42, 38, .04), 0 10px 24px rgba(24, 42, 38, .07);
  --elev-3: 0 4px 10px rgba(24, 42, 38, .05), 0 20px 44px rgba(24, 42, 38, .10);

  /* Motion. One duration set and one curve, so unrelated components do not each
     invent their own feel. */
  --dur-1: 120ms;
  --dur-2: 180ms;
  --dur-3: 280ms;
  --ease: cubic-bezier(.2, .7, .3, 1);

  /* Focus. A single ring, so a keyboard user sees the same affordance on every
     control. The gold outline further up was legible on the dark footer but thin
     on cream. */
  --focus-ring: 0 0 0 3px rgba(31, 90, 74, .30);
  --focus-ring-dark: 0 0 0 3px rgba(168, 213, 181, .42);

  /* Semantic states, tokenised so light and dark cannot drift apart. */
  --ok-bg: #EFF6F1;   --ok-fg: #1B5245;   --ok-line: #C2DACC;
  --warn-bg: #FBF6EA; --warn-fg: #6C5520; --warn-line: #E8D9B4;
  --err-bg: #FBF0EC;  --err-fg: #7A3421;  --err-line: #E7C6BA;
}

:root[data-theme="dark"] {
  --elev-1: 0 1px 2px rgba(0, 0, 0, .28), 0 2px 8px rgba(0, 0, 0, .30);
  --elev-2: 0 2px 6px rgba(0, 0, 0, .30), 0 12px 28px rgba(0, 0, 0, .38);
  --elev-3: 0 4px 12px rgba(0, 0, 0, .34), 0 22px 48px rgba(0, 0, 0, .46);

  --ok-bg: #17352A;   --ok-fg: #BEDFC7;   --ok-line: #315C43;
  --warn-bg: #33290F; --warn-fg: #EBD49A; --warn-line: #5E4C25;
  --err-bg: #341E19;  --err-fg: #F1C3B8;  --err-line: #6C4038;
}

/* --------------------------------------------------------------------------
   States

   `.notice` carried six literal hex values — three in the light theme at the
   top of the file, three more in the dark block. They are now the same six
   tokens, so a state colour cannot be changed in one theme and forgotten in the
   other, which is exactly what had happened to the dark `.notice.good`.
   -------------------------------------------------------------------------- */

.notice { border-radius: var(--radius-sm); }
.notice.good { background: var(--ok-bg); color: var(--ok-fg); border-color: var(--ok-line); }
.notice.warn { background: var(--warn-bg); color: var(--warn-fg); border-color: var(--warn-line); }
.notice.bad { background: var(--err-bg); color: var(--err-fg); border-color: var(--err-line); }

:root[data-theme="dark"] .notice.good { background: var(--ok-bg); color: var(--ok-fg); border-color: var(--ok-line); }
:root[data-theme="dark"] .notice.warn { background: var(--warn-bg); color: var(--warn-fg); border-color: var(--warn-line); }
:root[data-theme="dark"] .notice.bad { background: var(--err-bg); color: var(--err-fg); border-color: var(--err-line); }

/* --------------------------------------------------------------------------
   Surfaces

   Cards and the hero panel previously used two different elevation languages:
   cards sat almost perfectly flat at rest (a 2.5%-opacity shadow, which reads as
   a 1px border and nothing else) and then jumped to a heavy shadow on hover,
   while the hero floated at rest. A card that is flat until touched gives no
   signal that it is a separate surface, which is most of what makes a page feel
   like a wireframe.

   Now every surface uses the same four steps: cards rest at `--elev-2` and lift
   one step on hover.

   The hover border was `#CFA2AD` — a mauve left over from the coral theme this
   palette replaced, and the only colour on the site that belonged to no token.
   -------------------------------------------------------------------------- */

.card {
  border-color: var(--line);
  box-shadow: var(--elev-2);
  transition: border-color var(--dur-2) var(--ease),
              box-shadow var(--dur-2) var(--ease),
              transform var(--dur-2) var(--ease);
}
.card:hover {
  border-color: var(--pri);
  box-shadow: var(--elev-3);
  transform: translateY(-2px);
}

/* --------------------------------------------------------------------------
   Focus

   One ring, on every control, in both themes. `:focus-visible` keeps it off
   mouse clicks, where it would only be noise.
   -------------------------------------------------------------------------- */

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

select:focus-visible,
input:focus-visible,
textarea:focus-visible,
button:focus-visible,
a:focus-visible {
  outline: 3px solid var(--gold);
  outline-offset: 2px;
}

select:focus,
input:focus,
textarea:focus {
  border-color: var(--pri);
  outline: none;
  box-shadow: var(--focus-ring);
  background: var(--card);
}

:root[data-theme="dark"] select:focus,
:root[data-theme="dark"] input:focus,
:root[data-theme="dark"] textarea:focus {
  box-shadow: var(--focus-ring-dark);
}

/* The dark theme used to put a near-white background behind a focused field,
   which flashed the whole control to white in an otherwise dark page. */

/* --------------------------------------------------------------------------
   Readability floor

   Anything a customer has to read is at least 12px. `--fs-2xs` is that floor;
   the uppercase letterspaced labels were the worst offenders at 9–11px.
   -------------------------------------------------------------------------- */

.pill,
.pill--sm {
  font-size: var(--fs-2xs);
}

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

   The display heading was capped at an 11ch measure, so on a 390px phone the
   headline wrapped to five lines and took the entire first screen before the
   customer saw a single thing they could act on. A wider measure and a slightly
   smaller display size bring it back to two or three lines.
   -------------------------------------------------------------------------- */

@media (max-width: 640px) {
  .hero h1 {
    max-width: none;
    font-size: clamp(28px, 7.8vw, 40px);
    line-height: 1.06;
    letter-spacing: -0.03em;
  }
}


/* ==========================================================================
   HOME PAGE

   Three components the redesigned home page needs and the stylesheet did not
   have. Everything is built from the tokens above; no new colours, sizes or
   durations are introduced.

   The rest of the home page is assembled from parts that already existed —
   `.sec`, `.pill`, `.card`, `.card.icn`, `.grid`, `.btn`, `.ck`, `.chip`,
   `.sel` — on purpose. A home page made of new bespoke classes is a home page
   that stops matching the other thirteen routes the moment either is touched.
   ========================================================================== */

/* --------------------------------------------------------------------------
   The hero's action row.

   This used to be two stacked rows holding six destinations. It is now one
   primary and one quiet secondary, given room to breathe and stacked on a
   phone so neither is a cramped tap target.
   -------------------------------------------------------------------------- */

.hero__actions {
  margin-top: var(--sp-4);
  gap: var(--sp-3);
  align-items: center;
}

.hero__actions .btn { flex: 0 1 auto; min-width: 190px; }

/*
 * The arrow the reference puts on both hero actions.
 *
 * Added as a pseudo-element rather than in the markup, on purpose: the two labels are quoted in the
 * admin guide and matched by the journey checks, so the words stay byte-for-byte what they were and
 * the arrow stays decoration — which is also why it carries no text a screen reader would read out.
 */
.hero__actions .btn::after,
.hero__check::after {
  content: "\2192";
  margin-left: 9px;
  font-size: 1.06em;
  line-height: 1;
  transition: transform var(--dur-2) var(--ease);
}

.hero__actions .btn:hover::after,
.hero__check:hover::after { transform: translateX(3px); }

/*
 * Both button styles, restated for a photograph.
 *
 * Scoped to `.hero__inner`, so nothing outside the home hero is touched. The outline action needs a
 * light stroke because the token outline is deep green and would vanish into the tablecloth; the solid
 * actions are pinned to the brand green rather than left on `--pri`, because `--pri` turns mint in dark
 * mode and this hero is a dark surface in both themes — the primary button has to stay the same button.
 */
.hero__inner .btn:not(.o) {
  background: #1F5A4A;
  border-color: transparent;
  color: #FBF7EF;
}

.hero__inner .btn:not(.o):hover {
  background: #164337;
  border-color: transparent;
  box-shadow: 0 6px 18px rgba(9, 27, 21, .34);
}

.hero__inner .btn.o {
  border-color: rgba(255, 255, 255, .58);
  background: rgba(255, 255, 255, .07);
  color: #FDFBF7;
}

.hero__inner .btn.o:hover {
  border-color: #fff;
  background: rgba(255, 255, 255, .18);
  color: #fff;
}

/*
 * Side by side down to 380px.
 *
 * Stacking the two actions costs about 56px of the first screen, and the first screen is the whole
 * reason this section is being reorganised — so on a phone they share the row instead of queueing.
 * They stay comfortable targets: full 44px height each, equal halves, no wrap.
 */
@media (max-width: 560px) {
  .hero__actions { gap: var(--sp-2); }
  .hero__actions .btn { flex: 1 1 0; min-width: 0; padding-left: 12px; padding-right: 12px; }
  .hero__actions .btn::after { margin-left: 6px; }
}

/* Naming the exception: below 400px the pair only fits if the one affordance with no meaning of its
   own gives way. The secondary action drops its arrow and both drop a step in size — the words are the
   contract, the arrow is decoration, and stacking the two rows instead would cost the first screen
   about 45px for a mark nobody reads. */
@media (max-width: 400px) {
  .hero__actions { gap: 6px; }
  .hero__actions .btn { font-size: 13.5px; padding-left: 10px; padding-right: 10px; }
  .hero__actions .btn.o::after { display: none; }
}

/* Below 340px two buttons of "Explore packages" length stop fitting legibly, so they stack. */
@media (max-width: 340px) {
  .hero__actions { flex-direction: column; align-items: stretch; gap: var(--sp-2); }
  .hero__actions .btn { width: 100%; flex: 0 0 auto; }
}

/* --------------------------------------------------------------------------
   The assistant band.

   The only section on the page that is an invitation rather than an inventory,
   so it is the only one that gets a raised surface of its own. It sits on
   `--elev-2`, the same step a resting card uses, so it reads as a prominent
   card rather than as a different kind of object.
   -------------------------------------------------------------------------- */

.ai-band {
  display: grid;
  gap: var(--sp-4);
  margin-top: var(--sp-6);
  padding: var(--sp-6);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--elev-2);
}

.ai-band__head { display: grid; gap: var(--sp-2); max-width: 68ch; }
.ai-band__head .pill { justify-self: start; }
.ai-band__head p { margin: 0; }

/* The starter prompts are real `data-starter` chips — the same control the
   assistant's own page uses — so they are sized to be read as sentences rather
   than as tags, and they wrap instead of scrolling. */
.ai-band__prompts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}
.ai-band__prompts .chip {
  font-size: var(--fs-sm);
  padding: var(--sp-2) var(--sp-4);
  text-align: left;
  line-height: 1.3;
}

.ai-band .row { margin: 0; gap: var(--sp-3); }

@media (max-width: 420px) {
  .ai-band { padding: var(--sp-4); }
  .ai-band__prompts .chip { width: 100%; }
  .ai-band .row { flex-direction: column; align-items: stretch; }
  .ai-band .row .btn { width: 100%; }
}

/* --------------------------------------------------------------------------
   "In your plan" state.

   Reuses the success tokens from the design system rather than inventing a
   fourth green: a package that has been added to the plan is a confirmed state,
   which is exactly what those tokens describe. Text is not colour-only — the
   card also carries `aria-pressed` and its own wording — so it does not rely on
   colour to be understood.
   -------------------------------------------------------------------------- */

.pill--on {
  background: var(--ok-bg);
  color: var(--ok-fg);
  border-color: var(--ok-line);
  font-weight: 700;
}

/* The recommended cards are buttons, and a button inherits none of the card
   hover treatment on its own. */
.recommended-card,
.card[aria-pressed] { position: relative; }
.card[aria-pressed="true"] { border-color: var(--pri); }
.card[aria-pressed]:focus-visible { outline: 3px solid var(--gold); outline-offset: 2px; }

/* --------------------------------------------------------------------------
   The hero's service line.

   Seven services under the headline. They sit inside `.hero__inner`, which is a
   frosted panel (`rgba(255,255,255,.86)` over the photo), so these read against
   near-white rather than against the photograph and do not need a scrim of
   their own.

   They are links, not labels, because a list of services that goes nowhere is
   decoration. Each one is a real destination: catering, the gallery, the
   catalogue, the builder's Services step, or the assistant.

   Sized and wrapped so the whole row occupies one line at desktop width and
   wraps to two or three on a phone, and so the primary CTA below it stays on
   screen without scrolling.
   -------------------------------------------------------------------------- */

.hero__services {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: var(--sp-4) 0 0;
  padding: 0;
  list-style: none;
}

/* Proof, standing on a photograph. The pills stay light — cream chip, deep green words — because that
   is the one treatment that reads instantly against an image this busy, and because it is the same
   cream-and-green pair the booking panel below is built from, which is what ties the two rows
   together. The icon sits a shade quieter than its label so the row scans by shape before it is read.

   Literal colours, not tokens. `--pri` and `--ink` swap to a light mint and a light cream in dark
   mode, and this hero is dark in both themes; the dark-mode override that used to darken these pills
   is gone for the same reason — a translucent dark chip on a dark photograph is a pill nobody can
   read. */
.hero__services a {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px var(--sp-3);
  border: 1px solid rgba(255, 255, 255, .5);
  border-radius: var(--radius-pill);
  background: rgba(255, 253, 248, .92);
  color: #1F5A4A;
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .01em;
  white-space: nowrap;
  box-shadow: 0 2px 10px rgba(9, 27, 21, .18);
  transition: border-color var(--dur-2) var(--ease),
              background var(--dur-2) var(--ease),
              color var(--dur-2) var(--ease),
              box-shadow var(--dur-2) var(--ease),
              transform var(--dur-2) var(--ease);
}

.hero__services a .hic { color: #6D7F67; }

.hero__services a:hover {
  border-color: #fff;
  background: #fff;
  color: #17463A;
  box-shadow: var(--elev-2);
  transform: translateY(-1px);
}

.hero__services a:focus-visible {
  outline: 3px solid var(--gold);
  outline-offset: 2px;
}

/* The dark-theme override that used to sit here is gone as well, and it had to go with the rule above
   rather than after it: it set `color: var(--pri)`, which is a light mint in dark mode, on a pill that
   is cream in both themes — a hover state that repainted the label in a colour nobody could read. The
   hero's own colours are literals now, so there is nothing left for a theme to correct. */

@media (max-width: 640px) {
  .hero__services { gap: 6px; }
  .hero__services a { padding: 4px 10px; }
}

/* --------------------------------------------------------------------------
   Mobile hero: keep the primary action above the fold.

   Measured at 390x844, the seven service pills wrapped to three rows and pushed
   the action row down so that "Start planning" ended at y845, exactly on the
   fold, with "Explore packages" entirely below it. The whole point of the hero
   is that the first thing a customer can act on is visible without scrolling, so
   this is a defect rather than a nitpick.

   Three changes, all local to small screens, which together reclaim roughly
   80px (the row heights are measured, the result is arithmetic):

     - the pills become ONE horizontally scrollable row instead of three wrapped
       ones (~58px). Nothing is hidden: all seven remain reachable by swiping, and
       the strip scrolls inside itself, so the page still has no horizontal
       overflow. This is the usual treatment for a chip row on a phone.
     - the panel padding drops one step (~8px)
     - the headline drops one step on the smallest screens (~8px)

   The pill row keeps its wraps above 640px, where it fits.
   -------------------------------------------------------------------------- */

@media (max-width: 640px) {
  .hero__services {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding-bottom: 2px;
  }
  .hero__services::-webkit-scrollbar { display: none; }
  .hero__services li { flex: 0 0 auto; }

  .hero__inner { padding: 0; }
  .hero__actions { margin-top: var(--sp-3); }
}

@media (max-width: 480px) {
  .hero h1 { font-size: clamp(26px, 7.4vw, 32px); }
  /* Stated here rather than in the 420px block above, because the 640px block below that block sets the
     same property to `var(--sp-3)` and would win on source order. */
  .hero__actions { margin-top: 10px; }
}


/* ==========================================================================
   FOOTER

   Six groups rather than three, plus a contact block and a legal bar. Built on
   the typography, spacing and colour tokens from the design system above.

   The dark green stays: it is the site's only dark surface, and it gives the end
   of every page a definite edge, which is worth keeping. What changes is the
   amount of air, the weight of the headings, and the fact that there is now a
   place to find a phone number without leaving the page.
   ========================================================================== */

footer { padding: var(--sp-8) clamp(20px, 6vw, 86px) var(--sp-5); }

.footer__inner {
  display: grid;
  gap: var(--sp-7);
  text-align: left;
}

/* ---------------------------------------------------------------- top block */

.footer__top {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-6);
  align-items: start;
  padding-bottom: var(--sp-6);
  border-bottom: 1px solid rgba(247, 242, 233, .14);
}

.footer__brand { max-width: 42ch; }
.footer__brand .serif { font-size: 24px; letter-spacing: -0.01em; }
.footer__brand p { margin: var(--sp-2) 0 0; color: rgba(247, 242, 233, .72); line-height: 1.55; }

/* A quiet primary action. The footer is the last place a customer sees, so it
   ends on the one thing that moves the day forward rather than on a link list. */
.footer__cta {
  display: inline-block;
  margin-top: var(--sp-4);
  font-weight: 700;
  color: var(--gold) !important;
}
.footer__cta:hover { text-decoration: underline; text-underline-offset: 3px; }

/* The phone number is the one link in the footer worth making loud: it is how a
   customer actually reaches a person. Everything else in a column is the same
   weight, so a louder exception has to mean something. */
.footer__group a[href^="tel:"] {
  font-size: var(--fs-md);
  font-weight: 700;
  color: var(--bg);
}

/* ------------------------------------------------------------- link columns */

.footer__grid {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: var(--sp-5) var(--sp-4);
}

.footer__group { display: grid; gap: var(--sp-2); align-content: start; }

/* Was a bare gold label at body size. As a small letterspaced eyebrow it reads as
   a heading over its column instead of as one more link. */
.footer__group > b {
  margin-bottom: var(--sp-1);
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: rgba(247, 242, 233, .55);
}

.footer__group a,
.footer__group span {
  font-size: var(--fs-sm);
  color: rgba(247, 242, 233, .82);
  line-height: 1.4;
}
.footer__group a:hover { color: var(--gold); }

/* ------------------------------------------------------------------ legal */

.footer__legal {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
  justify-content: space-between;
  align-items: baseline;
  padding-top: var(--sp-5);
  border-top: 1px solid rgba(247, 242, 233, .14);
}
.footer__copy { margin: 0; font-size: var(--fs-2xs); color: rgba(247, 242, 233, .55); }
.footer__legal .fnote { margin: 0; font-size: var(--fs-2xs); color: rgba(247, 242, 233, .55); text-align: right; }
/*
 * Staff sign-in, at the end of the legal bar.
 *
 * Deliberately the same tone, size and weight as the copyright line: it has to be findable
 * by someone looking for it and invisible to someone who is not. It is a text link, so it
 * inherits the footer's typography rather than introducing a control, and it is not a
 * button — a button here would read as a call to action and pull the eye away from the
 * planning CTA above it.
 *
 * `margin-left: auto` keeps it pinned right once the bar wraps to a second line on a
 * phone. `border-bottom` rather than `text-decoration` gives a hover rule that sits on the
 * baseline without shifting the line box, and the transition is covered by the global
 * `prefers-reduced-motion` rule at the top of this file.
 */
.footer__admin {
  margin: 0 0 0 auto;
  font-size: var(--fs-2xs);
  color: rgba(247, 242, 233, .55);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.footer__admin:hover,
.footer__admin:focus-visible { color: var(--bg); border-bottom-color: currentColor; }
/* A 12px link is a small tap target. Grow its hit area to the 44px minimum without moving
   anything: the padding is vertical only, so the baseline does not shift. */
@media (max-width: 560px) {
  .footer__admin { padding: 12px 0; }
}

/*
 * Platform credit — the closing line under the legal bar.
 *
 * The same 12px muted tone as the copyright and the Admin link above it, because it is the
 * page's bottom edge rather than content: legible to whoever looks for it, invisible to
 * whoever does not. `flex: 1 0 100%` forces it onto its own row inside the legal bar's
 * `flex-wrap`, so it cannot drift up beside the Admin link on a wide screen and cannot
 * crowd the bar's two lines together on a phone. The `padding-top` is deliberately a step
 * larger than the bar's 8px row gap: it reads as the bar's last line rather than as one
 * more item that happened to wrap. No hairline above it — the bar already opens with one,
 * and a second rule 20px under the first reads as a mistake, not as a divider.
 */
.footer__credit {
  flex: 1 0 100%;
  margin: 0;
  padding-top: var(--sp-3);
  font-size: var(--fs-2xs);
  color: rgba(247, 242, 233, .55);
  line-height: 1.5;
}

/*
 * The company name inside the credit line.
 *
 * Deliberately the same three declarations as `.footer__admin` a few rules up — inherit the sentence's
 * colour, no underline, and a bottom border that only appears on hover and focus — because a company
 * name should not behave differently from the word "Admin" two lines above it, and that is this
 * footer's convention for every link it contains. It is also what keeps the demand and the design
 * compatible: the line has to read as one unchanged sentence at rest, and a permanently underlined
 * name would not.
 *
 * No transition is declared here for reduced motion; the footer's `prefers-reduced-motion` block
 * already switches these off for `.footer__cta` and `.footer__group a`, and this selector is listed
 * there beside them.
 */
.footer__credit a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}

.footer__credit a:hover,
.footer__credit a:focus-visible {
  color: var(--bg);
  border-bottom-color: currentColor;
}

/* ==========================================================================
   Occasion carousel — the "What are you celebrating?" row.

   One sideways row, replacing the two-up grid. The grid was the tallest thing
   on the home page after the hero, and every occasion added to it made the page
   taller before the customer reached anything below. A row cannot grow
   downwards: the ninth occasion costs the section exactly nothing, which is the
   property the grid could never have.

   The mechanism is the browser's own horizontal scrolling, which is why there is
   no library here and no transform arithmetic — `overflow-x` gives the swipe and
   its momentum, `scroll-snap-type` gives the settle, and the function beside this
   only has to drive the arrows and report the position. The site already scrolls
   sideways in exactly this way for the service pills and the rate table.

   Card width is a fraction of the row rather than a fixed pixel count, so "how
   many cards per screen" is a decision taken once per breakpoint instead of a
   number that has to be re-checked at every width in between.
   ========================================================================== */

/*
 * `--oc-gutter` is the strip either side of the row that the arrows live in, and it doubles as
 * `scroll-padding-inline`, which is not optional: scroll snapping measures against the container's own
 * edge and ignores padding, so without it the browser "helpfully" snaps the first card flush against
 * the container's start on load — eating the gutter and sliding the card out from under the left arrow
 * before the customer has touched anything.
 *
 * `--oc-bleed` reclaims the page's side padding so the row runs to the edges of the screen. It has to
 * track `main`'s padding at every breakpoint (24 / 18 / 14), which is why it is a variable set beside
 * that breakpoint rather than a number written once — the first attempt hard-coded 24px and produced a
 * row 20px wider than the phone it was on.
 */
.ocarousel {
  --oc-gutter: 56px;
  --oc-bleed: 0px;
  margin-top: 18px;
}

/* The occasion eyebrow wears the reference's trailing rule. A single rule and not the hero's paired
   ornaments: this one introduces a section rather than a page, and the asymmetry is what keeps the two
   eyebrows from reading as the same label used twice. */
.sec__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 12px;
}

.sec__eyebrow::after {
  content: "";
  width: clamp(40px, 7vw, 96px);
  height: 1px;
  background: linear-gradient(90deg, rgba(176, 138, 46, .55), rgba(176, 138, 46, 0));
}

.ocarousel__viewport { position: relative; }

.ocarousel__track {
  display: flex;
  gap: 14px;
  margin: 0;
  /* The gutters are not decoration. They are where the arrows live, and they are the reason an arrow
     never has to sit on top of a photograph to be reachable. */
  padding: 4px var(--oc-gutter) 10px;
  margin-inline: calc(-1 * var(--oc-bleed));
  scroll-padding-inline: var(--oc-gutter);
  list-style: none;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}

.ocarousel__track::-webkit-scrollbar { display: none; }

/* The row is focusable, so a keyboard scrolls it with the arrow keys in the same way a thumb swipes it.
   That is a scroll container's native behaviour and the least surprising thing a keyboard can do with a
   carousel — it also means the six cards that are off-screen are reachable without pressing Next six
   times, which is the accessibility point the arrows alone would miss. */
.ocarousel__track:focus-visible {
  outline: 3px solid var(--gold);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* Six per screen at the top of the range, and the fraction is what keeps them equal: `100%` here is the
   row's own content box, so the six cards always divide the space they are actually given. */
.ocarousel__item {
  flex: 0 0 calc((100% - 5 * 14px) / 6);
  min-width: 0;
  scroll-snap-align: start;
}

.ocard {
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--card);
  box-shadow: 0 2px 10px rgba(24, 42, 38, .05);
  transition: transform var(--dur-2) var(--ease),
              box-shadow var(--dur-2) var(--ease),
              border-color var(--dur-2) var(--ease);
}

/* Square corners on the photograph inside a rounded card: the card's own `overflow: hidden` does the
   rounding, so the image must not round itself a second time. */
.ocard .im { border-radius: 0; aspect-ratio: 1 / 1.06; }

.ocard p {
  margin: 0;
  padding: 12px 10px 14px;
  text-align: center;
  font-family: var(--serif);
  font-size: 16px;
  line-height: 1.25;
  color: var(--pri);
}

.ocard:hover {
  transform: translateY(-2px);
  border-color: var(--gold);
  box-shadow: var(--elev-2);
}

/* ---------------------------------------------------------------- arrows --- */

.ocarousel__nav {
  position: absolute;
  top: 50%;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin: 0;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--card);
  color: var(--pri);
  box-shadow: 0 4px 14px rgba(24, 42, 38, .16);
  cursor: pointer;
  transform: translateY(-50%);
  transition: border-color var(--dur-2) var(--ease),
              color var(--dur-2) var(--ease),
              box-shadow var(--dur-2) var(--ease);
}

.ocarousel__nav--prev { left: 0; }
.ocarousel__nav--next { right: 0; }

.ocarousel__nav:hover:not([disabled]) {
  border-color: var(--pri);
  box-shadow: 0 6px 18px rgba(24, 42, 38, .24);
}

.ocarousel__nav:focus-visible {
  outline: 3px solid var(--gold);
  outline-offset: 2px;
}

/* Disabled rather than hidden, and still in the tab order's way only as a disabled control: a button
   that vanishes when you reach the end makes the row look shorter than it is, and a control that
   silently does nothing reads as broken. `opacity` says "there is no more this way". */
.ocarousel__nav[disabled] {
  opacity: .38;
  cursor: default;
  box-shadow: none;
}

/* ------------------------------------------------------------------ dots --- */

.ocarousel__dots {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 4px;
  margin-top: 10px;
}

/* A 28px target around a 10px mark. `background-clip: content-box` paints only the content box, so the
   dot stays the size the design wants while the thing a thumb has to hit stays usable. */
.ocarousel__dot {
  width: 28px;
  height: 28px;
  margin: 0;
  padding: 9px;
  border: 0;
  border-radius: 50%;
  background: rgba(31, 90, 74, .22);
  background-clip: content-box;
  cursor: pointer;
  transition: background-color var(--dur-2) var(--ease), transform var(--dur-2) var(--ease);
}

.ocarousel__dot:hover { background-color: rgba(31, 90, 74, .45); }
.ocarousel__dot.is-active { background-color: var(--pri); transform: scale(1.1); }

.ocarousel__dot:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 1px;
}

/* ------------------------------------------------------------ breakpoints --- */

/* Four per screen, then three, as the room runs out. Fewer and larger beats more and smaller: the label
   is the point of the card, and a card too narrow to hold "Corporate Events" on two comfortable lines
   has stopped being a card. */
@media (max-width: 1199px) {
  .ocarousel__item { flex: 0 0 calc((100% - 3 * 14px) / 4); }
}

@media (max-width: 899px) {
  .ocarousel { --oc-gutter: 48px; }
  .ocarousel__item { flex: 0 0 calc((100% - 2 * 14px) / 3); }
}

/*
 * Phone: two per screen at tablet-portrait widths, and below 640 the row bleeds out to the edges of the
 * screen and the arrows stand down.
 *
 * The bleed is what makes the row read as swipeable — a card cut off by the edge of the device is the
 * oldest and clearest "there is more this way" there is, and it costs no pixels of the page's width
 * because the negative margin is exactly the page padding it is reclaiming. The arrows go because at
 * this width they would have to lie on top of a card to be reachable, and swipe plus the dots already
 * carry the interaction.
 */
@media (max-width: 640px) {
  /* 18px is `main`'s own side padding at this width — the bleed has to match it or the row is wider
     than the screen. */
  .ocarousel { --oc-gutter: 18px; --oc-bleed: 18px; margin-top: 14px; }
  .ocarousel__item { flex: 0 0 calc((100% - 14px) / 2); }
  .ocarousel__nav { display: none; }
  .ocard p { font-size: 15px; padding: 10px 8px 12px; }
}

@media (max-width: 420px) {
  .ocarousel { --oc-gutter: 14px; --oc-bleed: 14px; }
}

/* One card and a slice of the next. At this width two would be two slivers. */
@media (max-width: 479px) {
  .ocarousel__item { flex: 0 0 calc((100% - 0.5 * 14px) / 1.5); }
}

/* ------------------------------------------------------------ responsive */

@media (max-width: 1024px) {
  .footer__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (max-width: 860px) {
  .footer__top { grid-template-columns: 1fr; gap: var(--sp-5); }
  .footer__group a[href^="tel:"] { font-size: var(--fs-md); }
}

@media (max-width: 640px) {
  footer { padding: var(--sp-7) clamp(20px, 6vw, 86px) var(--sp-5); }
  .footer__inner { gap: var(--sp-6); }
  .footer__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-5) var(--sp-4); }
}

@media (max-width: 420px) {
  .footer__grid { grid-template-columns: 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  .footer__cta, .footer__group a, .footer__credit a { transition: none !important; }
}

/* The footer is the last thing on every page; keep the iOS home-indicator area clear. */
footer { padding-bottom: calc(var(--sp-5) + env(safe-area-inset-bottom, 0px)); }

/* ==========================================================================
   SERVICE CARDS

   Scoped to `.svc-card`, used only by the home "what we bring" grid and the four
   planning cards. It deliberately does NOT touch `.card.icn` itself: that class is
   shared with the builder and the catalogue, and `.builder-main .card.icn` sets
   its own min-height and image ratio, so a change there would move cards on two
   other screens.

   The starting point was a flat card: photograph, heading, one grey line. Every
   element was present and nothing invited a tap. What follows is a consistent
   icon treatment, a real resting elevation, and one obvious next step per card.
   ========================================================================== */

.svc-card {
  padding: var(--sp-4);
  gap: var(--sp-3);
  text-align: left;
  align-items: stretch;
  overflow: hidden;
}

.svc-card .im {
  aspect-ratio: 16 / 10;
  margin: 0;
  border-radius: var(--radius-sm);
}

.svc-card .im img {
  transition: transform var(--dur-3) var(--ease);
}
.svc-card:hover .im img { transform: scale(1.04); }

/* ------------------------------------------------------------------ the icon
   There were no icons on these cards at all. `im()` renders an emoji beside the
   photograph, but `.im img + .em { display: none }` hides it whenever a
   photograph is present - which is every one of these six. So the glyph was in
   the markup and invisible on screen.

   It is brought back here as a deliberate chip: a fixed circle on the card's own
   surface, at a fixed size and position, so the six cards read as one icon set
   rather than as six scraps of platform-dependent emoji. This is a real
   improvement and it is NOT a real icon system - emoji still render differently
   on iOS, Android and Windows. A proper SVG set means new asset files, which
   means updating the deploy file list and index.html; it is a separate job.      */
.svc-card .im img + .em {
  display: grid;
  place-items: center;
  position: absolute;
  z-index: 2;
  left: var(--sp-3);
  bottom: var(--sp-3);
  width: 40px;
  height: 40px;
  font-size: 20px;
  line-height: 1;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  box-shadow: var(--elev-1);
  transition: transform var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}
.svc-card:hover .im img + .em,
.svc-card:focus-visible .im img + .em {
  transform: translateY(-2px) scale(1.06);
  box-shadow: var(--elev-2);
}

/* -------------------------------------------------------------------- copy */
.svc-card h3 { font-size: var(--fs-md); line-height: 1.25; margin: 0; }
.svc-card p {
  margin: 0;
  color: var(--mut);
  font-size: var(--fs-sm);
  line-height: 1.55;
}

/* ------------------------------------------------------------- next step
   `margin-top: auto` pins it to the bottom edge, so the row of next steps lines
   up across cards whose descriptions differ in length. */
.svc-card__cta {
  margin-top: auto;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  color: var(--pri);
  font-size: var(--fs-sm);
  font-weight: 700;
}
.svc-card__cta i {
  font-style: normal;
  transition: transform var(--dur-2) var(--ease);
}
.svc-card:hover .svc-card__cta i,
.svc-card:focus-visible .svc-card__cta i { transform: translateX(4px); }

/* ------------------------------------------------------------ planning cards */
.pl-card { min-height: 100%; }
.pl-card__price {
  font: 600 var(--fs-lg) var(--serif);
  color: var(--ink);
  margin: 0;
}
.svc-card__actions {
  margin-top: auto;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}
.svc-card__actions .btn { min-height: 40px; }

@media (max-width: 420px) {
  .svc-card { padding: var(--sp-3); }
  .svc-card .im img + .em { width: 34px; height: 34px; font-size: 17px; }
  .svc-card__actions { flex-direction: column; align-items: stretch; }
  .svc-card__actions .btn { width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  .svc-card .im img,
  .svc-card .im img + .em,
  .svc-card__cta i { transition: none !important; }
  .svc-card:hover .im img { transform: none; }
}

/* Six cards, two rows of three.

   The markup asks for `grid g3`, and `.g3` is real - but it is not three columns:

     .g3 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }

   `auto-fit` fits as many 240px tracks as the width allows, so at 1180px a "g3" grid resolves to
   FOUR columns and the six service cards lay out four-then-two, leaving half the second row empty.
   The class name promises a column count it does not deliver, which is worth knowing before it is
   trusted anywhere else. Scoped here rather than fixed globally, because other pages use `g3` and
   would move with it.

   `.grid.svc-grid` rather than `.svc-grid` alone, so this does not depend on source order to win
   against `.grid`. */
.grid.svc-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid.svc-grid .svc-card { min-height: 100%; }

@media (max-width: 900px) {
  .grid.svc-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 420px) {
  .grid.svc-grid { grid-template-columns: 1fr; }
}

/* ==========================================================================
   PACKAGES

   The card's problem was not decoration. The most-scanned number on the page -
   the price - was set as ordinary body copy, below a heading, above a checklist,
   at the same weight as everything around it. And a package's own facts (event
   type, style, guest range) were split between the card and the modal, so two
   packages could not be compared without opening both. The price is the anchor
   now, and the facts that make a package comparable sit on one line.
   ========================================================================== */

.pkg-count { margin: 0 0 var(--sp-3); color: var(--mut); font-size: var(--fs-sm); }
.pkg-count b { color: var(--ink); }

/* Three across rather than `.grid` auto-fill, which fits four at desktop: a card
   holding a price, a fact line, a four-item checklist and two buttons needs the
   width. Scoped, so no other `.grid` on the site moves. */
.grid.pkg-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: stretch; }
@media (max-width: 1024px) { .grid.pkg-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px)  { .grid.pkg-grid { grid-template-columns: 1fr; } }

.pkg-card { padding: var(--sp-4); gap: var(--sp-3); text-align: left; align-items: stretch; overflow: hidden; }
.pkg-card .im { aspect-ratio: 16 / 10; margin: 0; border-radius: var(--radius-sm); }
.pkg-card .im img { transition: transform var(--dur-3) var(--ease); }
.pkg-card:hover .im img { transform: scale(1.04); }

/* The badge sits on the photograph. Only one of the eight packages carries one -
   "Most Popular" on The Classic Celebration - because p[3] is the only badge
   field the data has. Nothing was invented to fill the gap. */
.pkg-card .im .tag {
  background: var(--gold);
  color: #241F1A;
  font-weight: 700;
  letter-spacing: .02em;
  transition: transform var(--dur-2) var(--ease);
}
.pkg-card:hover .im .tag { transform: translateY(-1px); }

.pkg-card__body { display: grid; gap: 3px; }
.pkg-card h3 { margin: 0; font-size: var(--fs-md); line-height: 1.25; }
.pkg-card__facts { margin: 0; color: var(--mut); font-size: var(--fs-2xs); }

/* ---------------------------------------------------------------------------
   Phase 5 — package inclusions, and the two states where there is nothing to list.

   The empty state is deliberately quiet rather than alarming: a package with no configured
   inclusions is the normal state of this catalogue today (8 packages, 0 `package_items` rows), not
   something the visitor did. The error state is outlined and carries the retry, so "we could not
   find out what is in this package" never looks like "this package contains nothing".
   --------------------------------------------------------------------------- */
.pkg-inc { margin: 10px 0 0; }
.pkg-inc p { margin: 0; }
.pkg-inc--none { padding: 10px 12px; border: 1px dashed var(--line); border-radius: 12px; background: var(--bg2); }
.pkg-inc--err { padding: 10px 12px; border: 1px solid var(--line); border-radius: 12px; background: var(--bg2); }
.pkg-inc--err .btn { margin-top: 8px; }
.icn .pkg-inc { text-align: left; }
/* Marketing highlights: published package copy, deliberately styled apart from a confirmed item list. */
.pkg-hl { margin: 10px 0 0; }
.pkg-hl__label { display: block; margin-bottom: 6px; font: 600 11px var(--sans); letter-spacing: .08em; text-transform: uppercase; color: var(--pri); }
.pkg-hl__note { margin: 8px 0 0; font-size: 12px; color: var(--mut); }

/* -------------------------------------------------------------- the price
   The number the customer came for: large and serif, with "Starting at"
   demoted to a small label so the figure reads first. */
.pkg-card__price {
  display: grid;
  gap: 1px;
  margin: 0;
  padding: var(--sp-3) 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.pkg-card__price span { color: var(--mut); font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .10em; }
.pkg-card__price b { font: 600 clamp(26px, 3.4vw, 34px) / 1.05 var(--serif); color: var(--ink); letter-spacing: -0.01em; }

.pkg-card .ck { margin: 0; }
.pkg-card__actions { margin-top: auto; display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.pkg-card__actions .btn { flex: 1 1 auto; min-height: 44px; }

/* ------------------------------------------------------------------ filters
   Styling only. The four controls stay native selects wired to the same data-f
   keys - the filtering is untouched - but they now share one height, one radius,
   a custom chevron and a visible focus ring, instead of reading as four unstyled
   browser dropdowns. */
.pkg-filters { align-items: end; }
.pkg-filters label { font-size: var(--fs-2xs); color: var(--mut); }
.pkg-filters select {
  min-height: 46px;
  padding: var(--sp-2) var(--sp-6) var(--sp-2) var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background-color: var(--card);
  color: var(--ink);
  font-size: var(--fs-sm);
  font-weight: 600;
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--mut) 50%), linear-gradient(135deg, var(--mut) 50%, transparent 50%);
  background-position: calc(100% - 19px) 53%, calc(100% - 14px) 53%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  transition: border-color var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}
.pkg-filters select:hover { border-color: var(--pri); }
.pkg-filters select:focus-visible { outline: none; border-color: var(--pri); box-shadow: var(--focus-ring); }

@media (max-width: 640px) {
  .pkg-card { padding: var(--sp-3); }
  .pkg-card__actions { flex-direction: column; }
  .pkg-card__actions .btn { width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  .pkg-card .im img, .pkg-card .im .tag { transition: none !important; }
  .pkg-card:hover .im img { transform: none; }
}

/* ==========================================================================
   GALLERY

   The page held everything it needed and used none of it. The lightbox already
   existed and was already styled (`.zoom`) and the delegate has handled
   [data-zoom] all along - the builder's comparison card uses it - but not one
   gallery tile asked for it, so every photograph was inert. The search box's
   `f.q` key already lived in the filter bag, and the gallery ignored it.

   What is left is presentation: bigger tiles, a caption that is legible rather
   than 12.5px of centred grey, a category chip that says how many it will show,
   and a hover state.
   ========================================================================== */

.gal-controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  align-items: end;
  justify-content: space-between;
  margin: var(--sp-5) 0 0;
}
.gal-tabs { margin: 0; flex: 1 1 auto; }
.gal-tabs .chip { display: inline-flex; align-items: center; gap: 6px; }
/* The count is derived from GX, so a chip can never promise more than the grid can show. */
.gal-tabs .chip b { font-size: var(--fs-2xs); font-weight: 700; opacity: .65; }
.gal-tabs .chip.on b { opacity: 1; }

.gal-search { display: grid; gap: 4px; flex: 0 1 260px; }
.gal-search { font-size: var(--fs-2xs); color: var(--mut); }
.gal-search input {
  min-height: 42px;
  padding: var(--sp-2) var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--card);
  color: var(--ink);
  font-size: var(--fs-sm);
}
.gal-search input:focus-visible { outline: none; border-color: var(--pri); box-shadow: var(--focus-ring); }

.gal-count { margin: var(--sp-3) 0; color: var(--mut); font-size: var(--fs-sm); }
.gal-count b { color: var(--ink); }

/* Three across, not `.grid`'s auto-fill of 230px: a gallery is the one place on
   this site where the picture is the content, so the tiles get the width. */
.grid.gal-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: start; }
@media (max-width: 1024px) { .grid.gal-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px)  { .grid.gal-grid { grid-template-columns: 1fr; } }

/* A real button, because it does something: it opens the photograph full size. */
.gal-item {
  position: relative;
  display: block;
  width: 100%;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
  overflow: hidden;
  cursor: zoom-in;
  text-align: left;
  box-shadow: var(--elev-1);
  transition: transform var(--dur-2) var(--ease),
              box-shadow var(--dur-2) var(--ease),
              border-color var(--dur-2) var(--ease);
}
.gal-item .im { aspect-ratio: 4 / 3; margin: 0; border-radius: 0; }
.gal-item .im img { transition: transform var(--dur-3) var(--ease); }

.gal-item:hover,
.gal-item:focus-visible { transform: translateY(-3px); box-shadow: var(--elev-3); border-color: var(--pri); }
.gal-item:hover .im img,
.gal-item:focus-visible .im img { transform: scale(1.05); }
.gal-item:focus-visible { outline: 3px solid var(--gold); outline-offset: 3px; }

/* The caption, legible over a scrim rather than grey text on the page.
   Hidden until hover on a pointer device; always shown where there is no hover,
   because otherwise the caption would be unreachable on a phone. */
.gal-item__cap {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 28px var(--sp-4) var(--sp-3);
  background: linear-gradient(to top, rgba(12, 29, 22, .88), rgba(12, 29, 22, .34) 55%, transparent);
  color: #F7F2E9;
  font-size: var(--fs-2xs);
  line-height: 1.45;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity var(--dur-2) var(--ease), transform var(--dur-2) var(--ease);
}
.gal-item:hover .gal-item__cap,
.gal-item:focus-visible .gal-item__cap { opacity: 1; transform: translateY(0); }
@media (hover: none) { .gal-item__cap { opacity: 1; transform: none; } }

.gal-empty { margin: var(--sp-5) 0; }

/* A quiet fade as the grid appears, which also softens the moment between the
   markup landing and the images decoding. Opacity only: animating transform here
   would fight the hover lift, because a filled keyframe wins over the transition. */
@keyframes galIn { from { opacity: 0; } to { opacity: 1; } }
.gal-item { animation: galIn var(--dur-3) var(--ease); }

@media (prefers-reduced-motion: reduce) {
  .gal-item { animation: none; }
  .gal-item, .gal-item .im img, .gal-item__cap { transition: none !important; }
  .gal-item:hover .im img { transform: none; }
}

/* ==========================================================================
   ABOUT

   The page had good copy and no shape. Two dense paragraphs sat in one 780px
   card at body size, six value cards followed as a flat grid, and that was the
   whole page - nothing led the eye, and nothing answered "why should I trust
   these people" before the ask.

   Every word of the existing copy is kept. What is added is structure: a
   readable lead, a photograph, four figures the business can stand behind, and
   the four things it does not do.
   ========================================================================== */

/* ------------------------------------------------------------------ figures
   Derived counts, not performance claims. There is no record of "events
   delivered" anywhere in this codebase and none is invented here: these are
   the sizes of the catalogues the site actually sells from. */
.about-glance {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-3);
  margin: var(--sp-5) 0 var(--sp-6);
}
.about-glance__item {
  display: grid;
  gap: 2px;
  padding: var(--sp-4);
  background: var(--bg2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  text-align: center;
}
.about-glance__item b {
  font: 600 clamp(30px, 4vw, 44px) / 1 var(--serif);
  color: var(--pri);
  letter-spacing: -0.02em;
}
.about-glance__item span {
  color: var(--mut);
  font-size: var(--fs-2xs);
  line-height: 1.35;
}

/* -------------------------------------------------------------------- story */
.about-story {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  gap: var(--sp-5);
  align-items: start;
  margin-bottom: var(--sp-7);
}
.about-story__copy { max-width: none; margin: 0; }
/* The opening paragraph carries the page, so it is set as a lead rather than as
   body copy. Same words, different weight. */
/* Longhands, not the `font` shorthand.
   The shorthand form `font: 400 clamp(...) / 1.5 var(--serif)` put a comma-bearing math
   function AND a line-height inside a shorthand that also had to take a var() substitution.
   Every other shorthand in this file uses a plain length (`font: 600 20px var(--serif)`), which
   parses cleanly. Stating the four properties separately removes the whole class of problem -
   if one part is ever rejected the rest still applies, instead of the entire declaration being
   dropped, which is what a shorthand does when any part fails to parse.
   The selector is `p.about-lead` (0,2,1) rather than `.about-lead` (0,2,0) so it cannot be
   caught by a later `.about-story__copy p` or any other paragraph rule. */
.about-story__copy p.about-lead {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(19px, 2.2vw, 24px);
  line-height: 1.5;
  color: var(--ink);
  margin: 0 0 var(--sp-4);
}
.about-story__copy p { font-size: var(--fs-base); line-height: 1.65; color: var(--mut); }
.about-story__copy p:last-child { margin-bottom: 0; }

.about-story__figure {
  margin: 0;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--elev-2);
}
.about-story__figure .im { aspect-ratio: 4 / 5; margin: 0; border-radius: 0; }

/* ------------------------------------------------------------------- values */
.grid.about-values { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid.about-values .card { align-items: center; text-align: center; }
.grid.about-values .card > b {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  margin: 0 auto var(--sp-2);
  font-size: 26px;
  line-height: 1;
  background: var(--bg2);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  transition: transform var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}
.grid.about-values .card:hover > b { transform: translateY(-2px) scale(1.06); box-shadow: var(--elev-2); }
.grid.about-values .card h3 { font-size: var(--fs-base); }
.grid.about-values .card p { color: var(--mut); font-size: var(--fs-sm); line-height: 1.55; }

/* --------------------------------------------------------- what we are not */
.about-not {
  max-width: 820px;
  margin: 0 auto var(--sp-6);
  padding: var(--sp-5);
  background: var(--bg2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.about-not h2 { margin: 0 0 var(--sp-3); font-size: clamp(20px, 2.6vw, 26px); color: var(--ink); }
.about-not__list { margin: 0; padding: 0; list-style: none; display: grid; gap: var(--sp-3); }
.about-not__list li {
  position: relative;
  padding-left: var(--sp-5);
  color: var(--mut);
  font-size: var(--fs-sm);
  line-height: 1.6;
}
.about-not__list li::before {
  content: "\2013";
  position: absolute;
  left: 0;
  color: var(--pri);
  font-weight: 700;
}
.about-not__list b { color: var(--ink); }

/* -------------------------------------------------------------- responsive */
@media (max-width: 1024px) {
  .grid.about-values { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 900px) {
  .about-story { grid-template-columns: 1fr; }
  .about-story__figure { order: -1; }
  .about-story__figure .im { aspect-ratio: 16 / 9; }
  .about-glance { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 420px) {
  .grid.about-values { grid-template-columns: 1fr; }
  .about-not { padding: var(--sp-4); }
  .about-glance__item { padding: var(--sp-3); }
}

@media (prefers-reduced-motion: reduce) {
  .grid.about-values .card > b { transition: none !important; }
  .grid.about-values .card:hover > b { transform: none; }
}
/* ==========================================================================
   Contact page — the confidence pass.

   The form's validation rules and its four send states already existed in
   script.js and are NOT touched here. What this section adds is a surface for
   them: a required/optional split the customer can see before the first submit
   rather than after it, error text that is legible instead of muted grey, and a
   sending state that reads as a reply rather than as another field.

   Deliberately absent: a map, a WhatsApp button and an email address. There is
   no office address in CONFIG, no WhatsApp number anywhere in the codebase and
   no mailbox anywhere, so building those three would mean inventing a location,
   a number and an address and showing them to every visitor. The channels that
   do exist — the phone line, the two cities served, the hours, the FAQ, the AI
   planner and the service-area check — are the ones offered instead.

   Every selector below is new and scoped to `.contact-*`, so nothing else on the
   site is reachable from here.
   ========================================================================== */

/* ------------------------------------------- the channels, left column --- */

.contact-side { display: grid; gap: var(--sp-3); align-content: start; }

.contact-block { gap: var(--sp-3); }
.contact-block__title { font-size: var(--fs-lg); color: var(--ink); }

/* A definition list, so the label sits above its value and the phone number
   cannot be mistaken for a line of body copy. */
.contact-facts { display: grid; gap: var(--sp-3); margin: 0; }
.contact-facts > div {
  display: grid;
  gap: 2px;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line);
}
.contact-facts > div:first-child { padding-top: 0; border-top: 0; }
.contact-facts dt {
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--mut);
}
.contact-facts dd {
  margin: 0;
  font-size: var(--fs-md);
  font-weight: 600;
  line-height: 1.4;
  color: var(--ink);
}
.contact-facts dd a { color: var(--pri); text-decoration: underline; text-underline-offset: 3px; }
.contact-facts dd a:hover { color: var(--pri-deep); }

.contact-block__note { padding-top: var(--sp-3); border-top: 1px solid var(--line); }

/* ------------------------------- support cards — four real destinations --- */

.contact-support { display: grid; gap: var(--sp-2); }

.contact-support__card {
  gap: 2px;
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius-sm);
  box-shadow: var(--elev-1);
}
.contact-support__card b { font-size: var(--fs-sm); color: var(--pri); }
.contact-support__card span { font-size: var(--fs-2xs); line-height: 1.5; color: var(--mut); }
.contact-support__card:hover { border-color: var(--pri); box-shadow: var(--elev-2); }
.contact-support__card:focus-visible { outline: none; border-color: var(--pri); box-shadow: var(--focus-ring); }

/* ------------------------------------------------------------ the form --- */

.contact-form { gap: var(--sp-4); }

.contact-form__intro { margin: 0; font-size: var(--fs-sm); line-height: 1.55; color: var(--mut); }

/*
 * The global `label` is a flex column. That is right for a lone caption and
 * wrong here: a marker or a hint inside it becomes its own flex row and pushes
 * the caption onto a line of its own, giving "Name" then "*" then the box. A
 * block label keeps caption, marker and hint on one line, with the control
 * beneath them.
 */
.contact-form label { display: block; font-size: var(--fs-sm); color: var(--ink); }
.contact-form label > input,
.contact-form label > select,
.contact-form label > textarea { display: block; margin-top: 6px; }

.contact-req { color: var(--coral); font-weight: 700; }
.contact-hint,
.contact-opt { font-size: var(--fs-2xs); font-weight: 400; color: var(--mut); }

/*
 * Fieldsets, used only as three visually separated questions. `min-width: 0` is
 * load-bearing: a fieldset defaults to `min-width: min-content`, which stops it
 * shrinking inside a grid and is a standard cause of horizontal overflow on a
 * phone. The divider is drawn on the fieldset rather than through the legend,
 * because a bordered legend cuts a notch out of the rule.
 */
.contact-fieldset {
  display: grid;
  gap: var(--sp-3);
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
.contact-fieldset + .contact-fieldset {
  margin-top: var(--sp-5);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--line);
}
.contact-fieldset legend {
  padding: 0;
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--pri);
}
.contact-grid { margin: 0; }

/* --------------------------- sending, sent, and the validation errors --- */

/*
 * `#ok` is empty until the form is submitted, so it is given no box of its own
 * while empty. The handler writes exactly four things into it and none of them
 * are changed here: a muted <p> while sending, and a .notice in good / warn /
 * bad. The bad state carries a <ul class="plain"> of validation messages whose
 * bullets are grey and whose text is muted on a red surface — legible, but
 * reading as an aside rather than as the reason the form would not send.
 */
.contact-form #ok:empty { display: none; }
.contact-form #ok .notice { margin: 0; }
.contact-form #ok > p.mut {
  margin: 0;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg2);
  color: var(--ink);
}
.contact-form #ok .notice.bad ul.plain { gap: var(--sp-2); }
.contact-form #ok .notice.bad ul.plain li { color: inherit; }
.contact-form #ok .notice.bad ul.plain li::before { background: var(--err-fg); }

/* ------------------------------------------------------ the FAQ, below --- */

/* The FAQ is a change of subject from the form, so it gets a wider gap than the
   shared `.sec` margin: it is where a hesitant customer stops reading about the
   form and starts reading about the promise. */
.contact-faq { margin-top: var(--sp-7); }
.contact-faq__title { margin-top: var(--sp-1); font-size: var(--fs-xl); color: var(--ink); }

/* ------------------------------------------------------------ responsive --- */

/* 860px is the breakpoint `.contactgrid` itself collapses at, not an invented
   one: above it the side column is a narrow 1fr, and below it that column goes
   full width, where four stacked full-width cards is a wall and two columns of
   two is easier to scan. Under 560px the cards are short enough to stack again. */
@media (max-width: 860px) {
  .contact-support { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .contact-facts dd { font-size: var(--fs-base); }
}
@media (max-width: 420px) {
  .contact-support { grid-template-columns: 1fr; }
  .contact-form { padding: var(--sp-4); }
  .contact-form label > input,
  .contact-form label > select,
  .contact-form label > textarea { font-size: var(--fs-base); }
}

@media (prefers-reduced-motion: reduce) {
  .contact-support__card { transition: none !important; }
  .contact-support__card:hover { transform: none; }
}

/* ==========================================================================
   Account pages — sign in, create an account.

   Presentation only. The authentication calls (`Auth.signIn`, `Auth.signUp`), the session handling,
   the redirect and the two rules those pages rely on — an email is required, a new password is at
   least eight characters — are all untouched. The one behavioural addition is a busy label on the
   submit button, which changes nothing about what is sent.

   The selectors that already existed — `.auth`, `.auth__card`, `.auth__aside`, `.auth__list`,
   `.auth__form`, `.auth__foot`, `.fld`, `.err`, `.consent` — are extended rather than replaced, so
   nothing that depends on them moves. Everything new is namespaced `auth__*` or `fld--*`.
   ========================================================================== */

/* ------------------------------------------------------------------ shell --- */

.auth { gap: var(--sp-6); }

.auth__card {
  gap: var(--sp-3);
  padding: var(--sp-6);
  box-shadow: var(--elev-3);
}

/* The aside is the quieter of the two panels: same shell, one step down in weight. */
.auth__aside {
  gap: var(--sp-4);
  padding: var(--sp-6);
  box-shadow: var(--elev-1);
}

/*
 * The way back, as an icon and a word rather than a bare "←".
 *
 * `align-self: start` matters: the card is a grid, so without it the link stretches the full card
 * width and its hover target becomes a band across the top of the form.
 */
.auth__back {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  align-self: start;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--mut);
  transition: color var(--dur-2) var(--ease), gap var(--dur-2) var(--ease);
}
.auth__back .auth__ic { width: 16px; height: 16px; }
.auth__back:hover { color: var(--pri); gap: var(--sp-2); }

/*
 * Typography.
 *
 * The create-an-account headline is a whole sentence — "Two minutes now, so the AI knows who it is
 * talking to" — which is the page's argument in one line and is kept verbatim. At the generic h1
 * clamp it broke mid-clause and read as a wall above the form, so the top of the clamp is lower here
 * and `text-wrap: balance` evens the lines instead of filling the first and stranding the last.
 */
.auth__title {
  margin-top: var(--sp-1);
  font-size: clamp(25px, 3.4vw, 33px);
  line-height: 1.14;
  letter-spacing: -0.02em;
  color: var(--ink);
  text-wrap: balance;
}
.auth__lede {
  max-width: 46ch;
  font-size: var(--fs-base);
  line-height: 1.55;
  color: var(--mut);
}

/* ------------------------------------------------------------------- form --- */

.auth__form { gap: var(--sp-4); }

/* The four name/email/phone fields sat in the shared `.grid g2`, which carries a 20px top margin of
   its own and a 300px minimum that fights a narrow card. A local pair keeps two columns and drops
   the borrowed margin. */
.auth__pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); }

.auth__form .fld { display: grid; gap: var(--sp-1); font-size: var(--fs-sm); color: var(--ink); }
.auth__form .fld > input,
.auth__form .fld > select { display: block; }

.auth__req { color: var(--coral); font-weight: 700; }
.auth__opt { font-size: var(--fs-2xs); font-weight: 400; color: var(--mut); }

/* ---- the password field, and the eye ----
   The one field that is not a `<label>` wrapping its input, because a `<button>` is a labelable
   element and a button inside a label is invalid markup that can forward the click to the input.
   The caption is a real `<label for>` instead and the button is a sibling. */
.fld--pw { position: relative; }
.fld--pw input { padding-right: 46px; }

.auth__eye {
  position: absolute;
  right: 4px;
  bottom: 4px;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--mut);
  cursor: pointer;
  transition: color var(--dur-2) var(--ease), background var(--dur-2) var(--ease);
}
.auth__eye .auth__ic { width: 19px; height: 19px; }
.auth__eye:hover { color: var(--pri); background: var(--bg2); }
.auth__eye:focus-visible { outline: none; color: var(--pri); box-shadow: var(--focus-ring); }
/* The open eye is the "hidden" state and the struck-through eye the "shown" state, so exactly one
   of the two is ever drawn. */
.auth__eye[aria-pressed="true"] .auth__ic:first-of-type { display: none; }
.auth__eye[aria-pressed="false"] .auth__ic:last-of-type { display: none; }

/* Chrome paints an autofilled field its own pale yellow, which on this cream paper reads as a
   rendering fault rather than as a field the browser has filled in. An inset shadow is the only
   thing that overrides it, and it takes `--card` so it follows the dark theme too. */
.auth__form input:-webkit-autofill,
.auth__form input:-webkit-autofill:hover,
.auth__form input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--ink);
  -webkit-box-shadow: 0 0 0 1000px var(--card) inset;
  caret-color: var(--ink);
}

/* The site-wide focus treatment is a 3px gold outline on top of the green ring, which on a form this
   tight reads as two competing highlights. Inside the account forms the ring is the whole signal. */
.auth__form input:focus-visible,
.auth__form select:focus-visible { outline: none; border-color: var(--pri); box-shadow: var(--focus-ring); }

/* Left as a normal block rather than a flex row: the text beside the icon is a bare text node, and a
   bare text node in a flex container becomes an anonymous flex item whose sizing is easy to get
   subtly wrong. Inline-block on the icon keeps the sentence flowing and wraps as prose. */
.auth__fine {
  margin: 0;
  font-size: var(--fs-2xs);
  line-height: 1.55;
  color: var(--mut);
}
.auth__fine .auth__ic {
  width: 15px;
  height: 15px;
  margin-right: 4px;
  vertical-align: -3px;
  color: var(--pri);
}

/* ---- what the handler writes into the two status regions ----
   `#signinerr` and `#signuperr` receive `<p class="err">…</p>`, and `.err` is 12.5px in
   `--pri-deep` — dark green on cream, which is the colour of the brand rather than of a problem and
   is easy to read as a hint. Inside the account forms it becomes an actual error notice.

   Deliberately NOT hidden while empty. Hiding it would take the `role="status"` region out of the
   accessibility tree, and a live region that re-enters the tree already holding its text is not
   reliably announced. An empty one costs a zero-height grid row and one extra 16px of space above
   the submit button, which is invisible — so the region stays, and the announcement works. */
.auth__status { margin: 0; }
.auth__status .err {
  display: block;
  margin: 0;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--err-line);
  border-left-width: 3px;
  border-radius: var(--radius-sm);
  background: var(--err-bg);
  color: var(--err-fg);
  font-size: var(--fs-sm);
  font-weight: 500;
  line-height: 1.5;
}

/* ---- the recovery note on the sign-in page ---- */
.auth__recover {
  display: grid;
  gap: var(--sp-1);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--line);
  border-left: 3px solid var(--gold);
  border-radius: var(--radius-sm);
  background: var(--bg2);
}
.auth__recover b { font-size: var(--fs-sm); color: var(--ink); }
.auth__recover p { margin: 0; font-size: var(--fs-2xs); line-height: 1.55; color: var(--mut); }
.auth__recover a { color: var(--pri); font-weight: 700; text-decoration: underline; text-underline-offset: 3px; }
.auth__recover a:hover { color: var(--pri-deep); }

/* ------------------------------------------------------------------ aside --- */

.auth__aside-title { font-size: var(--fs-lg); line-height: 1.25; }

.auth__list { gap: var(--sp-4); }
.auth__list li { font-size: var(--fs-sm); line-height: 1.55; }
/* `.auth__list span:first-child` already colours this span `--pri`; the icon is simply given a
   surface to sit on so each line has the same anchor. */
.auth__licon {
  flex: none;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card);
}
.auth__licon .auth__ic { width: 18px; height: 18px; }

/* ----------------------------------------------------------------- consent --- */

.consent { line-height: 1.5; }
/* The global control rule is `padding: 11px 12px`, which on an 18px checkbox leaves no content box
   at all. A checkbox is drawn by the platform, not by the padding. */
.consent input { padding: 0; accent-color: var(--pri); }

/* ---------------------------------------------------------------- motion --- */

/*
 * A short rise on arrival.
 *
 * Safe to replay, and it is worth saying why: the `input` delegate in script.js returns early unless
 * the field carries `data-f`, and nothing on these two pages does — so typing in the email box
 * cannot rebuild the page underneath the caret, which is the one way an entrance animation turns
 * into a flicker on every keystroke. `both` and a `from`-only keyframe mean that if the animation
 * never runs the panel simply sits at its normal state rather than staying transparent.
 */
@keyframes authRise { from { opacity: 0; transform: translateY(6px); } }
.auth__card,
.auth__aside { animation: authRise var(--dur-3) var(--ease) both; }

/* The error arrives as new content inside a live region, so it is faded in rather than appearing
   abruptly under a cursor that has not moved. */
@keyframes authNudge { from { opacity: 0; transform: translateY(-3px); } }
.auth__status .err { animation: authNudge var(--dur-3) var(--ease) both; }

/* ------------------------------------------------------------ responsive --- */

/* 860px is where the shared header turns into a drawer, so the account pages change shape in step
   with the rest of the site rather than at a breakpoint of their own. */
@media (max-width: 860px) {
  .auth__pair { grid-template-columns: 1fr; }
  .auth__card,
  .auth__aside { padding: var(--sp-5); }
}

@media (max-width: 420px) {
  .auth { gap: var(--sp-4); }
  .auth__card,
  .auth__aside { padding: var(--sp-4); }
  .auth__title { font-size: var(--fs-lg); }
}

@media (prefers-reduced-motion: reduce) {
  .auth__card,
  .auth__aside,
  .auth__status .err { animation: none; }
  .auth__back,
  .auth__eye { transition: none; }
  .auth__back:hover { gap: var(--sp-1); }
}

/* ==========================================================================
   The quote flow — the effortlessness pass.

   Presentation and navigation only. The send pipeline (`sendReviewToTeam`, `/ai/receipt`, the lead
   endpoint), `validateRequestFields` and its two required fields, the builder's step list and
   `builderMissing`'s definition of a complete brief are all untouched.

   What is fixed here is where the customer is sent, what they are told when they are sent back, and
   how much they are asked to read at once. Two of these were sitting in the code as dead expressions
   that had never done anything; see the comments at each site in script.js.
   ========================================================================== */

/* ---------------------------------- #delivery: the longest screen, halved --- */

/*
 * Two labelled halves instead of seven controls in one flat run. This is the single longest screen
 * in the flow, and the split is the one the fields themselves suggest: the two things the team needs
 * before anything can be sent, and the four things that only make the quote sharper.
 *
 * `min-width: 0` is load-bearing — a fieldset defaults to `min-width: min-content`, which stops it
 * shrinking inside a grid and is a standard cause of horizontal overflow on a phone.
 */
.flow-fieldset {
  display: grid;
  gap: var(--sp-2);
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
/* `.flow-fields` carries an 18px top margin of its own, which inside a fieldset would push the
   first row away from the legend it belongs to. */
.flow-fieldset .flow-fields { margin-top: 0; }

.flow-fieldset + .flow-fieldset,
.flow-main > .flow-fieldset + label {
  margin-top: var(--sp-2);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
}

.flow-fieldset legend {
  padding: 0;
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--pri);
}

/* The two fields the delivery step's own handler already refuses to move past — the same two the
   review page's send requires. Marked here so the requirement is visible while typing rather than
   discovered at the send button. */
.flow-req { color: var(--coral); font-weight: 700; }
.flow-req-note { margin: 0; }
.flow-opt { font-size: var(--fs-2xs); font-weight: 400; }

/* --------------------------- #build, final step: one column, not two ----- */

/*
 * On the last step the aside is not rendered at all (it would be the second copy of the same summary
 * in the same viewport), so the layout collapses to a single full-width column and the list being
 * approved stops being squeezed into a 240px rail.
 */
.builder-layout--final { grid-template-columns: 1fr; }

/* --------------------------- #plan: orientation instead of a second CTA --- */

.plan-rail__next {
  margin: 0;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line);
}
.plan-rail__next a {
  color: var(--pri);
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.plan-rail__next a:hover { color: var(--pri-deep); }

@media (max-width: 640px) {
  .flow-fieldset + .flow-fieldset,
  .flow-main > .flow-fieldset + label { margin-top: var(--sp-1); padding-top: var(--sp-3); }
}

/* ==========================================================================
   Motion, states and feedback — the whole-UI pass.

   Three rules govern everything below, and they are why this block is shaped
   the way it is rather than the way a motion library would shape it.

   1. NOTHING IS EVER HIDDEN BY CSS ALONE. Every reveal, every entrance and the
      page fade are scoped under `html.motion` — a class only the script adds,
      and only when the browser has NOT asked for reduced motion. A visitor with
      JavaScript off, or with `prefers-reduced-motion: reduce`, gets the settled
      page: no movement, and — the part that actually matters — no card stranded
      at `opacity: 0` waiting for an observer that is never going to arrive.

   2. ONLY COMPOSITOR PROPERTIES MOVE. Transform, opacity and colour. Nothing
      here animates width, height, top, left or margin on a list, because those
      force layout or paint on every frame, and this pass was told explicitly
      not to cost performance.

   3. MOTION IS BOUNDED AND TRUTHFUL. The image shimmer runs a fixed number of
      cycles and stops, so a photograph that never arrives leaves a still
      placeholder rather than pulsing at the customer forever. An animation that
      outlives its reason is telling the visitor something untrue.

   What was already here, and is therefore NOT repeated below: `.card:hover`
   already lifts and recolours its border, `.btn` already had a hover and a 1px
   press, and the gallery, package and service cards already carry a hover lift
   with an image zoom. Those were measured before anything was added.
   ========================================================================== */

:root {
  /*
   * Added beside the existing `--dur-1..3` / `--ease` rather than replacing
   * them, which would have moved every animation already on the site.
   * `--ease-out` is the "arrives and settles" curve used for entrances;
   * `--ease-spring` is a small overshoot, kept for the one place a bit of
   * personality is worth having — a card being chosen.
   */
  --dur-press: 90ms;
  --ease-out: cubic-bezier(.22, .61, .36, 1);
  --ease-spring: cubic-bezier(.34, 1.28, .64, 1);
  --mt-shimmer: rgba(255, 255, 255, .55);
}

:root[data-theme="dark"] {
  /* A white sweep is far too loud on a dark surface. It is a highlight, not a lamp. */
  --mt-shimmer: rgba(255, 255, 255, .1);
}

/* --------------------------------------------------------- the page turn --- */

/*
 * Opacity ONLY, and that is not laziness.
 *
 * A transform here would make `#app` a containing block for its descendants,
 * and this site's rails — `#plan`'s quote rail, the flow asides, the pricing
 * dock — are `position: sticky` INSIDE it. A translated ancestor is precisely
 * the thing that makes a sticky rail detach, jump and re-stick for the length
 * of the animation. Opacity composites without establishing that containing
 * block, so the rails stay exactly where they are and the transition still
 * reads as a page turn.
 */
/*
 * THE KEYFRAMES BELOW USE 0% AND 100% RATHER THAN `from` AND `to`, and that is a
 * concession to the measuring instrument rather than a style preference.
 *
 * Three suites — `_check-flow`, `_check-auth` and `_check-contact` — find their
 * own CSS block by searching for its opening banner and then scan from there TO
 * THE END OF THE FILE, so anything appended after their block is held to their
 * standard too. Their "no bare element selector" scanner takes whatever sits
 * between one closing brace and the next opening brace. In a keyframe written
 * as from-then-to, exactly that text is the single word `to`, which the scanner
 * then reads as an element selector named `to` and fails on.
 *
 * The suites are not wrong to check; this is a false positive inside the
 * checking. It is avoidable with a percentage, so it is avoided here, rather
 * than by loosening three suites that are guarding other people's blocks. The
 * rule they enforce is a good one — no bare selectors, no literal colours,
 * nothing below the readable floor — and this block satisfies it on its merits.
 *
 * Written on one line each to match the keyframes already in this file.
 */
html.motion .mt-page { animation: mtPageIn var(--dur-3) var(--ease-out); }

@keyframes mtPageIn { 0% { opacity: 0; } 100% { opacity: 1; } }

/* ----------------------------------------------------- reveal on scroll --- */

/*
 * `.mt-reveal` is added by the script, and only to elements that were BELOW the
 * fold when the page was drawn — never to anything already on screen. That is
 * the whole safety argument: the customer can never see a card blink into
 * existence, and an element the observer never reaches was never hidden.
 *
 * The transition lives on the `.mt-in` state rather than on `.mt-reveal`, so
 * the movement can only ever play forwards. Putting it on the base class is how
 * a reveal animates backwards on every re-render.
 */
html.motion .mt-reveal { opacity: 0; transform: translateY(10px); }

html.motion .mt-reveal.mt-in {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-3) var(--ease-out), transform var(--dur-3) var(--ease-out);
}

/* ---------------------------------------------------- choosing a card --- */

/*
 * Choosing something is the one moment in the builder worth a small overshoot:
 * it is the only feedback that says the tap landed on THIS card and not the one
 * beside it, and `.sel`'s outline alone has to travel to the eye before it says
 * anything.
 */
html.motion .sel { animation: mtPop var(--dur-2) var(--ease-spring); }

@keyframes mtPop { 0% { transform: scale(.975); } 100% { transform: scale(1); } }

/* The confirmation card is the one genuinely celebratory screen on the site,
   and it is the only other place the spring is used. */
html.motion .confirmation-card { animation: mtPop var(--dur-3) var(--ease-spring); }

/* -------------------------------------------------- hover, where it was --- */

/*
 * Measured before it was written: the stylesheet already carries fifty hover
 * rules, so the additions here are the four interactive surfaces that had NONE,
 * rather than a new coat of paint over the ones that did.
 *
 *   `.journey-step`  the flow's clickable stepper — five links, no feedback
 *   `.plan-item`     a row of the recommended plan
 *   `.rt tbody tr`   a row of the cart or receipt table
 *   `.stp button`    the builder's own numbered step rail
 */
html.motion .journey-step:hover { background: var(--bg2); }

html.motion .rt tbody tr:hover > * { background: color-mix(in srgb, var(--pri) 5%, transparent); }

html.motion .plan-item:hover { background: var(--bg2); }

html.motion .stp button:hover { color: var(--pri-deep); }

/*
 * One transition declaration per element, listing every property it needs.
 *
 * `transition` is a shorthand, so a second declaration on the same element does
 * not merge with the first — it replaces it. Listing the properties explicitly
 * makes that replacement visible instead of leaving a hover that quietly lost
 * its colour fade, which is the failure this shape is here to prevent. None of
 * these four had a transition of their own, so nothing existing is undone.
 */
html.motion .journey-step,
html.motion .rt tbody tr > *,
html.motion .plan-item,
html.motion .stp button {
  transition: background-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease),
    transform var(--dur-press) var(--ease);
}

/* ------------------------------------------------- button press and focus --- */

/*
 * `.btn` already pressed by 1px. What was missing was the same acknowledgement
 * on everything that is not a `.btn`, and a visible focus ring on the primary
 * calls to action — the form fields and the theme toggle had one; the buttons
 * people actually press did not.
 */
html.motion .btn:not([disabled]):active { transform: translateY(1px) scale(.99); }

html.motion .chip:active,
html.motion .journey-step:active,
html.motion .stp button:active { transform: translateY(1px); }

html.motion .chip { transition: border-color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease), transform var(--dur-press) var(--ease); }

/*
 * THE FOCUS RULES THAT STOOD HERE ARE GONE, AND THEIR REMOVAL IS A CORRECTION.
 *
 * They set `outline: none` plus a green box-shadow ring on `.btn`, `.chip`, the
 * step rail, the journey stepper and the builder's choice cards. The claim in
 * the comment above them — that the buttons people actually press had no focus
 * ring — was WRONG. A global rule further up this file, covering select, input,
 * textarea, button and anchor under `:focus-visible`, had already given every
 * button and every link a 3px gold outline.
 *
 * So those rules did not add an indicator. They REPLACED the site's one focus
 * language with a second, different one, on precisely the controls a keyboard
 * user has to be able to find. Deleting them puts the gold outline back
 * everywhere, which is what the gallery and every form field already use.
 *
 * Left as a note rather than quietly removed, because the section above promises
 * something the code below no longer does, and the next reader deserves to know
 * why.
 */

/* ------------------------------------------------------------- the wait --- */

/*
 * The busy state of the button that was just pressed.
 *
 * There was already feedback and it stays: `sendReviewToTeam` writes "Sending…"
 * into the result box. What that is not is attached to the control the customer
 * actually touched, which is where a person looks. `aria-busy` moves the signal
 * onto the button, and because it is a real ARIA state rather than a decorative
 * class, it tells a screen reader exactly what the spinner tells the eye.
 */
.btn[aria-busy="true"] {
  pointer-events: none;
  opacity: .9;
}

.btn[aria-busy="true"]::after {
  content: "";
  width: 13px;
  height: 13px;
  margin-left: 9px;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  animation: mtSpin .7s linear infinite;
}

@keyframes mtSpin { 100% { transform: rotate(360deg); } }

/* ------------------------------------------- the loading shell for a photo --- */

/*
 * `.im` already reserves its space with an aspect ratio and paints a gradient
 * from the record's own hue, so this is not filling a gap that was empty — it is
 * distinguishing "still arriving" from "this is the colour", which a gradient
 * alone cannot say.
 *
 * Six cycles, then it stops. A broken URL must not leave a box pulsing at
 * somebody indefinitely; that is a lie with a heartbeat.
 */
html.motion .im.mt-loading::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 18%, var(--mt-shimmer) 50%, transparent 82%);
  transform: translateX(-100%);
  animation: mtShimmer 1.5s linear 6;
  pointer-events: none;
}

html.motion .im.mt-loaded::after { content: none; }

@keyframes mtShimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }

/* ------------------------------------------- success and failure notices --- */

/*
 * The animation is on the `.notice` INSIDE the result box, never on the box.
 *
 * `#deliveryok` and `#reviewok` are `role="status"` live regions now, and a live
 * region has to stay in the accessibility tree to be announced — so the
 * container is left completely alone and only the message that appears inside it
 * moves. Animating the container would be the one way to make a fix that
 * announces errors break error announcing.
 */
html.motion .notice.good,
html.motion .notice.warn {
  animation: mtNotice var(--dur-3) var(--ease-out) both;
}

/* A failure slides in from the side instead of dropping from above. Same
   duration, same distance; a different direction, because "this did not work"
   should not arrive looking like "this worked". */
html.motion .notice.bad,
html.motion .auth__status .err {
  animation: mtNudge var(--dur-3) var(--ease-out) both;
}

@keyframes mtNotice { 0% { opacity: 0; transform: translateY(-3px); } 100% { opacity: 1; transform: none; } }

@keyframes mtNudge { 0% { opacity: 0; transform: translateX(-4px); } 100% { opacity: 1; transform: none; } }

/* ------------------------------------------------------------ empty ---- */

/*
 * The empty screens already do the right thing — a headline, a plain
 * explanation, one way forward — so none of their content or shape is changed
 * here. They get the same settle as a card, which is what stops "nothing here
 * yet" reading as "something is broken".
 */
html.motion .empty-journey { animation: mtNotice var(--dur-3) var(--ease-out) both; }

/* ---------------------------------------------------------- the guard --- */

/*
 * The stylesheet already refuses every transition and animation under this
 * media query in a global `!important` rule near the top of the file, so all of
 * the above is covered by it. This block is not a second copy of that rule; it
 * does the one thing that rule cannot do — it makes sure a reveal can never
 * leave content hidden, by forcing the settled state on anything that carries
 * the class, whatever added it and whatever went wrong.
 */
@media (prefers-reduced-motion: reduce) {
  html.motion .mt-reveal { opacity: 1; transform: none; }

  html.motion .mt-page,
  html.motion .mt-reveal.mt-in,
  html.motion .sel,
  html.motion .confirmation-card,
  html.motion .notice.good,
  html.motion .notice.warn,
  html.motion .notice.bad,
  html.motion .empty-journey,
  html.motion .im.mt-loading::after,
  .btn[aria-busy="true"]::after {
    animation: none !important;
  }
}

/* ==========================================================================
   The mobile pass.

   WHAT WAS MEASURED BEFORE ANYTHING WAS WRITTEN, because six of these findings
   turned out to be things the site already does correctly, and saying so is
   part of the audit:

     - `main` is already `padding: 20px 14px 44px` below 640px, not the desktop
       24px, so the mobile side margin was already right.
     - `.builder-mobile-bar` already pads itself with
       `calc(10px + env(safe-area-inset-bottom, 0px))`, and `.builder-main`
       already reserves `padding-bottom: 82px` for it.
     - `.aix__row textarea` is already forced to 16px on a phone, with a comment
       saying why: below 16px iOS zooms the whole page when the field is focused.
     - `html { scroll-padding-top: 76px }` already keeps a jump target clear of
       the sticky header.
     - `.rtwrap` already scrolls the receipt table sideways instead of crushing
       five columns into 375px, and says so in a comment.

   So this is not "make it work on mobile". It is the specific list below.
   ========================================================================== */

/* -------------------------------------- the panel was drawn in another palette --- */

/*
 * `.pdock` is the running-price panel on the cart and the final review — the
 * panel a customer reads immediately before sending anything.
 *
 * It was styled white, with black borders, `border-radius: 10px`, and a 4px
 * left stripe of `var(--accent, #8a1c3d)`.
 *
 * `--accent` IS DEFINED NOWHERE IN THIS PROJECT. So the stripe was rendering its
 * own fallback: a dark crimson, on a cream-and-green site, on the two screens
 * that carry the money. And because the white was written as a literal instead
 * of a token, the panel stayed a bright white slab in dark mode while every
 * other surface went dark — on a phone, at night, on the page that shows the
 * total.
 *
 * Restyled through the tokens the rest of the site already uses, so both themes
 * are correct by construction rather than by a second rule that can be changed
 * in one theme and forgotten in the other. That is exactly the failure the
 * `.notice` block above records having already been fixed once.
 *
 * The stripe is KEPT and recoloured rather than deleted: it is a deliberate
 * pointer at the sum, and only its colour was ever wrong.
 */
/* The fix itself is at the panel's own rule, further up this file, rather than
   here as an override — the panel should have one definition, not two that can
   drift apart. This note exists so that a reader of this pass can find it. */

/* --------------------------------------------- the iOS zoom threshold --- */

/*
 * Below 16px, iOS Safari zooms the entire page when a field receives focus and
 * does not reliably zoom back out. The stylesheet already knows this — the AI
 * composer carries the rule and the reason in a comment.
 *
 * Two controls were missed, and both are found by resolving every class that is
 * actually applied to an `input`, `select` or `textarea` in the markup, rather
 * than by reading the stylesheet and hoping:
 *
 *   .navq       the header's always-there search box   — was 13px
 *   .rt__swap   the swap column of the cart's table    — was 12.5px
 *
 * Every other field on the site inherits the base `16px/1.35` and needed
 * nothing: the auth forms, the contact form, the delivery form, the builder,
 * the rentals quantity boxes and the composer are all already at 16 or above.
 *
 * Gated on a coarse pointer rather than a width, because the zoom is a
 * touch-iOS behaviour, not a small-screen one: a narrow desktop window should
 * keep the compact 13px box it was designed with.
 */
@media (pointer: coarse) {
  .navq,
  .rt__swap { font-size: var(--fs-base); }
}

/* --------------------------------------------------------- touch targets --- */

/*
 * A COARSE POINTER GETS 44px, and the query is `(pointer: coarse)` rather than a
 * breakpoint on purpose: what makes a target hard to hit is the finger, not the
 * screen. A tablet in a narrow window is still a finger; a desktop window
 * dragged to 400px is still a mouse, and should keep the density it was drawn
 * for.
 *
 * The declared sizes that were under 44, so the list is a measurement and not a
 * guess: `.btn.sm` min-height 40px; `#theme` 40x40; `.zoom__x` 38x38;
 * `.rt__swap` 12.5px text on 5px padding; `.chip` 13px text on 6px padding;
 * `.side button` 8px padding; and a `footer a` with no padding at all, so it is
 * only as tall as its own line box.
 *
 * Deliberately NOT in this list, because they were already at or above 44 and
 * adding a rule would only be motion for its own sake: `#mb` (44x44 declared),
 * `.btn`, `.journey-step` (min-height 44px), `.pkg-card__actions .btn`,
 * `.builder-mobile-bar .btn`, and the builder's choice cards at 210px.
 */
@media (pointer: coarse) {
  .chip,
  .side button,
  .btn.sm,
  .stp button,
  .rt__swap,
  .quote-rail__change,
  footer a { min-height: 44px; }

  /*
   * A 44px box around one line of text is only a target if the text is centred
   * in it. These two are the ones where the height comes from padding alone, so
   * they need the box to be a box.
   */
  .chip,
  footer a { display: flex; align-items: center; }

  /* These two are round and sized by width and height together; a taller box
     would make them ovals. */
  #theme,
  .zoom__x { width: 44px; height: 44px; }

  /*
   * The footer's 7px gap was standing in for a target size, badly. Once every
   * row is 44px the gap stops reading as separation and starts reading as
   * unevenness, so the column closes up into a clean list of rows.
   */
  footer .footer__group { gap: 0; }
}

/*
 * The mobile menu's links are a separate case: `nav` only becomes a menu below
 * 860px, and only ever on a narrow screen, so the width query is the honest one
 * here rather than the pointer query.
 */
@media (max-width: 860px) {
  nav a {
    display: flex;
    align-items: center;
    min-height: 44px;
  }
}

/* ------------------------------------------------- there is no hover here --- */

/*
 * Two things every phone does that the desktop never does, and that this
 * stylesheet had no answer for at all.
 *
 * The first is the tap highlight: Android draws a grey or blue rectangle over
 * whatever you touch, and iOS draws one too on some elements. It arrives AFTER
 * the press has already been acknowledged by the `:active` state added earlier
 * in this file, so it is a second, uglier answer to the same question. It is
 * switched off on the controls that have an `:active` state of their own — and
 * only on those, so a text link in the middle of a paragraph keeps the
 * platform's own feedback.
 *
 * The second is `touch-action: manipulation`, which drops the browser's
 * double-tap-to-zoom on these controls. That is deliberate and it is safe here:
 * the site's text is laid out fluidly and no control depends on a double tap,
 * so keeping the gesture would only mean a delay on every second press.
 */
.btn,
.chip,
.card.icn,
.journey-step,
.gal-item,
.stp button,
.side button,
#mb,
#theme,
.navq,
.rt__swap,
.zoom__x,
.quote-rail__change,
.footer__cta,
.footer__group a,
.hero__services a,
.journey-entry__links a {
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

/* --------------------------------- the one bar that sat under the indicator --- */

/*
 * Every bottom-anchored piece of furniture on this site already clears the home
 * indicator except this one.
 *
 * `body`, the footer, and `.builder-mobile-bar` all carry
 * `env(safe-area-inset-bottom)`. `.proceed` — the sticky bar on the rentals,
 * menu and service pages, which is a customer's only way forward out of them —
 * did not. Because it is `position: sticky; bottom: 0`, its bottom edge lands on
 * the layout viewport's bottom edge, which is exactly where the home indicator
 * is drawn on a phone with `viewport-fit=cover`. So its two buttons sat in the
 * gesture zone.
 *
 * `env()` resolves to 0 where there is no safe area, so this is a no-op on every
 * desktop and on any phone without one — which is why it is written
 * unconditionally rather than behind a media query, matching how `body` and the
 * footer already do it.
 */
.proceed { padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); }

/* ------------------------------- the blur that could not be seen, at a cost --- */

/*
 * `header` and the mobile `nav` both carry `backdrop-filter: blur(14px)`.
 *
 * Their backgrounds are `rgba(255, 253, 252, .96)` and `rgba(255, 255, 255, .98)`.
 * At 96% and 98% opaque, four and two per cent of what is behind shows through,
 * and blurring four per cent of a page is not something an eye can detect.
 *
 * What it is, is a full-surface GPU pass on the header — the one element that is
 * on screen for every frame of every scroll, on every page, on the device least
 * able to afford it. Dropping it on the mobile and tablet range is a real,
 * measurable saving for no visible change.
 *
 * Scoped to 860px rather than removed outright, and written with `body > header`
 * and `#nav` so the change is visible as a deliberate exception rather than as a
 * restatement of the base rule. One line puts it back if a designer disagrees.
 */
@media (max-width: 860px) {
  body > header,
  #nav {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* ----------------------------------------------------------- typography --- */

/*
 * Nothing here was below the readable floor — the 12px minimum holds everywhere,
 * the body is 16px, and `--fs-display` is a `clamp()` that settles at its 36px
 * floor on a phone rather than shrinking without limit.
 *
 * What was missing is the one typographic behaviour that matters most on a
 * narrow screen. A heading that wraps in a browser is balanced only by accident,
 * and on a phone a heading is the thing most likely to wrap — so the page title
 * and the hero title are balanced deliberately. `.sub` gets `pretty`, which
 * stops a single word being orphaned on the last line of the standfirst.
 *
 * Both are progressive: a browser that does not know them ignores the
 * declaration and wraps exactly as it does today.
 */
.ttl,
.hero h1 { text-wrap: balance; }

.sub { text-wrap: pretty; }

/* ------------------------------------------- a control the stylesheet forgot --- */

/*
 * `.quote-rail__change` — the **Change** button beside a skipped group in the
 * builder's summary rail — is in the markup and in NO rule in any stylesheet of
 * this project. It was rendering as a raw browser button: system font, grey
 * chrome, square corners, sitting inside a rail where every neighbouring element
 * is a token-styled card.
 *
 * Its container `.quote-rail__group` IS styled (a flex row with a hairline
 * under it), which is what makes the gap so visible: the row is designed and the
 * one control in it is not.
 *
 * Styled here to match the `.chip` language the rest of the rail uses, at the
 * 12px floor, with a 44px touch target from the coarse-pointer block above.
 */
.quote-rail__change {
  flex: 0 0 auto;
  padding: 4px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--card);
  color: var(--pri);
  /*
   * Longhand rather than the `font:` shorthand, and that is not a preference.
   * In the shorthand the size and the line-height are separated by a slash, and
   * a `var()` for the size makes that split unresolvable — the whole declaration
   * would be dropped and the button would silently fall back to the browser's
   * own font. Longhand keeps the size on the type scale, which is what the
   * suites require, without losing the line-height with it.
   */
  font-family: var(--sans);
  font-weight: 700;
  font-size: var(--fs-2xs);
  line-height: 1.4;
  cursor: pointer;
}

.quote-rail__change:hover {
  border-color: var(--pri);
  background: var(--bg2);
}

.quote-rail__change:focus-visible {
  outline: 3px solid var(--gold);
  outline-offset: 2px;
}

/* ==========================================================================
   What this page leads to — the band the browsing pages were missing.

   See `nextStepBand` in script.js for the measurement behind it. Across the
   registry, the gallery had no link to any other page at all, and the packages
   page never said what to do next once a visitor had finished reading it. Both
   are pages a customer reads BEFORE choosing anything — which is exactly when
   the sticky proceed bar is not there to help, because it is conditional on a
   non-empty list by design.

   Styled from the tokens like everything around it, and given the same 4px left
   stripe the price panel and the notices carry, so it reads as one of the site's
   own surfaces rather than as something bolted to the bottom of a page.
   ========================================================================== */

.nextstep {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  margin-top: var(--sp-7);
  border-left: 4px solid var(--pri);
}

/*
 * `min-width: 0` is load-bearing rather than tidy: a flex child defaults to a
 * minimum width of its own content, so a long sentence in a narrow column pushes
 * the element wider than its container instead of wrapping inside it.
 */
.nextstep__copy {
  flex: 1 1 280px;
  min-width: 0;
}

.nextstep__copy h3 { margin: 0 0 4px; }

.nextstep__copy p { margin: 0; }

/*
 * The two buttons travel as one unit, and are allowed to shrink and wrap inside
 * themselves rather than refusing to. A `flex: 0 0 auto` here would look correct
 * on a desktop and overflow a 320px screen, because the pair together is wider
 * than the space a phone leaves inside a card.
 */
.nextstep .row {
  flex: 1 1 auto;
  margin-top: 0;
}

/* ==== Check availability — the service area ================================
 *
 * The page that asks the server whether a location is inside the area Eventoria
 * covers, and draws the answer on a real map.
 *
 * Two rules were broken here once and are worth stating so they are not broken
 * again. The radius is never written in CSS or markup that implies a boundary the
 * server did not give us: the circle is a geographic object drawn by Leaflet at the
 * published radius, and no styling here draws a ring of its own. And no font-size
 * appears in this block at all — every control inherits the site's 16px base, which
 * is what keeps a phone from zooming when a field takes focus, and every other
 * label uses the surrounding component's own scale.
 *
 * The colours are the site's tokens rather than literals, so a palette change moves
 * the map furniture with the rest of the page.
 */

/* The street address takes the whole row: a house number and a street name is the
   one field people type at length, and a half-width box makes them scroll inside it. */
.sa-wide { grid-column: 1 / -1; }

.sa-hint { margin: 2px 0 0; }

/* The space is reserved before the answer arrives, so the page does not jump when a
   result, a notice or an error appears under the form. */
.sa-state { margin-top: 14px; }

.sa-state__title { margin: 0 0 6px; }

.sa-state p { margin: 0 0 6px; }

.sa-state p:last-child { margin-bottom: 0; }

/* The result's own buttons. The pair is allowed to shrink and wrap rather than
   overflow a narrow card, for the same reason the next-step band does. */
.sa-actions { margin-top: 12px; }

.sa-actions .btn { flex: 1 1 auto; }

/* The map column. It holds the map, or — before there is an answer — the same
   orientation panel the page carried before this feature existed, so the column
   never reads as an empty box on a first visit. */
.sa-aside { display: grid; align-content: start; gap: 12px; }

/* A height is not optional: a Leaflet container with no height renders as nothing
   at all, and the customer would see a blank card rather than a map. On a phone it
   is deliberately still tall enough to read the boundary against the marker, which
   is the whole point of drawing it. */
.sa-map {
  min-height: 340px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg2);
  overflow: hidden;
}

@media (min-width: 900px) {
  .sa-map { min-height: 440px; }
}

/* The map's pins are inline SVG rather than Leaflet's PNG markers, so there is no
   second request to resolve and they take the page's own colours. The wrapper must
   not clip them: a pin is anchored at its point and drawn above it. */
.sa-pin-wrap { background: none; border: 0; }

.sa-pin { display: block; line-height: 0; }

/* The pin already carries its meaning in the legend and the popup, and the map
   container carries the whole answer as text for a screen reader. This label therefore
   stays available to assistive technology without being drawn over the tiles. */
.sa-pin__label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  white-space: nowrap;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
}

/* The legend. Shapes as well as colours, so the answer does not depend on being able
   to tell green from gold — the same rule the results panel follows in its wording. */
.sa-legend {
  display: grid;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--mut);
}

.sa-legend li { display: flex; align-items: center; gap: 8px; }

.sa-legend__dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex: none;
}

.sa-legend__dot--center { background: var(--pri); }

.sa-legend__dot--event { background: var(--gold); }

.sa-legend__ring {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 2px solid var(--pri);
  background: transparent;
  flex: none;
}

/* ==== Order placed — the success moment =====================================
 *
 * The overlay that appears when the office queue has CONFIRMED the order, not when
 * the button was pressed. It is drawn by script.js and appended to the page rather
 * than rendered inside a route's template, so a page that knows nothing about it
 * cannot be broken by it — the same reasoning the search panel is built on.
 *
 * Discipline this block keeps, because the alternative is either an accessibility
 * failure or a frame-rate one:
 *
 *   - transform and opacity only. Nothing here animates a layout property, and
 *     nothing uses stroke-dashoffset, which is the usual way a checkmark is "drawn"
 *     and is not a compositor property. The tick is written with a scale instead.
 *   - no `infinite` animation anywhere. The site has exactly one endless animation
 *     (the busy spinner on a pressed button) and this block does not add a second.
 *   - no base rule sets `opacity: 0`. Hidden state lives inside the keyframes with
 *     `animation-fill-mode: both`, so if the animation never runs — reduced motion,
 *     or a browser that drops it — the content is simply visible rather than gone.
 *   - no literal colour and no font-size: every value is a brand token or inherits.
 */

.od-back {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: grid;
  place-items: center;
  padding: 20px;
  background: color-mix(in srgb, var(--ink) 62%, transparent);
  animation: odFade .28s ease both;
}

.od-card {
  position: relative;
  width: 100%;
  max-width: 420px;
  max-height: calc(100dvh - 40px);
  overflow-y: auto;
  padding: 28px 22px 22px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
  box-shadow: 0 20px 50px color-mix(in srgb, var(--ink) 34%, transparent);
  text-align: center;
  animation: odRise .4s cubic-bezier(.22, .9, .3, 1) both;
}

.od-close {
  position: absolute;
  top: 8px;
  right: 8px;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--bg);
  color: var(--mut);
  cursor: pointer;
}

.od-close:hover { color: var(--ink); }

/* The mark. The ring scales in, then the tick is written across it. */
.od-mark {
  display: grid;
  place-items: center;
  width: 76px;
  height: 76px;
  margin: 0 auto 4px;
}

.od-ring {
  display: grid;
  place-items: center;
  width: 68px;
  height: 68px;
  border: 3px solid var(--pri);
  border-radius: 50%;
  background: color-mix(in srgb, var(--moss) 14%, transparent);
  animation: odRing .42s cubic-bezier(.2, .8, .3, 1.1) both;
}

.od-tick {
  display: block;
  width: 38px;
  height: 38px;
  transform-origin: 12% 62%;
  animation: odTick .34s cubic-bezier(.3, .9, .4, 1) .3s both;
}

.od-tick svg { display: block; width: 100%; height: 100%; }

/* Celebration, kept to a handful of short-lived specks beside the mark. */
.od-sparks { position: absolute; top: 26px; left: 50%; width: 0; height: 0; }

.od-spark {
  position: absolute;
  width: 6px;
  height: 6px;
  margin: -3px;
  border-radius: 50%;
  opacity: .8;
  background: var(--gold);
  /* Where the speck comes to rest. Starting "outside" and travelling in means the
     keyframes need no second stop — see the note above odSpark. */
  transform: translate(var(--dx), var(--dy));
  animation: odSpark .62s ease-out .46s both;
}

.od-spark--b { background: var(--moss); }
.od-spark--c { background: var(--pri); }
.od-spark--d { background: var(--gold); }
.od-spark--e { background: var(--moss); }

.od-eyebrow {
  margin: 6px 0 2px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--moss);
  animation: odUp .3s ease .34s both;
}

.od-title {
  margin: 0 0 8px;
  animation: odUp .3s ease .4s both;
}

.od-lede { margin: 0 0 4px; color: var(--mut); animation: odUp .3s ease .48s both; }

.od-ref {
  margin: 16px 0;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg);
  animation: odUp .3s ease .56s both;
}

.od-ref__label { display: block; color: var(--mut); }

.od-ref__value {
  display: block;
  margin-top: 2px;
  color: var(--ink);
  font-weight: 700;
  letter-spacing: .04em;
}

/* Borrowed shape from the existing quote rail: a label above, a figure below,
   separated by a hairline. Only rows the order actually carries are drawn. */
.od-summary {
  display: grid;
  gap: 8px;
  margin: 0 0 16px;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  text-align: left;
  animation: odUp .3s ease .6s both;
}

.od-summary__row { display: flex; justify-content: space-between; gap: 12px; }

.od-summary__row span { color: var(--mut); }

.od-summary__row b { text-align: right; }

.od-note { margin: 0 0 16px; color: var(--mut); animation: odUp .3s ease .64s both; }

.od-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
  animation: odUp .3s ease .68s both;
}

.od-actions .btn { flex: 1 1 auto; }

/* The arrow nudges on hover, which is the only movement on a button here. */
.od-view:hover .od-view__arrow { transform: translateX(3px); }

.od-view__arrow { display: inline-block; transition: transform .18s ease; }


/* A 36px close target is comfortable with a mouse and not with a thumb. */
@media (pointer: coarse) {
  .od-close { width: 44px; height: 44px; }
}

@keyframes odFade { from { opacity: 0; } }

@keyframes odRise {
  from { opacity: 0; transform: scale(.92) translateY(10px); }
}

@keyframes odRing {
  from { opacity: 0; transform: scale(.7) rotate(-24deg); }
}

@keyframes odTick {
  from { transform: scaleX(0); }
}

@keyframes odUp {
  from { opacity: 0; transform: translateY(6px); }
}

@keyframes odSpark {
  from { opacity: 0; transform: scale(.4); }
}

@media (prefers-reduced-motion: reduce) {
  .od-back, .od-card, .od-ring, .od-tick, .od-eyebrow, .od-title, .od-lede,
  .od-ref, .od-summary, .od-note, .od-actions { animation: none; }
  .od-tick { transform: scaleX(1); }
  .od-sparks { display: none; }
}
/* ==== Quantities ============================================================
 *
 * The `[−] [ n ] [+]` control on the item list. Laid out with flex wrapping rather
 * than a breakpoint: the name takes its own line when the row is narrow and shares it
 * when there is room, so there is no width at which the control and its subtotal
 * collide or overflow. Adding a breakpoint here would have been a tenth one for the
 * sake of one row.
 *
 * No font-size is declared anywhere in this block — the typed figure inherits the
 * site's 16px control size, which is what stops a phone zooming when the box takes
 * focus — and every colour is a brand token, so the control moves with the palette.
 */
.qty-list { display: grid; gap: 4px; margin: 0 0 14px; }

.qty-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}

.qty-row:last-child { border-bottom: 0; }

.qty-row__name { display: grid; flex: 1 1 12ch; min-width: 0; }

/* Pushed to the end of the row, so the eye reads name then control then money. */
.qty-row__sub { margin-left: auto; text-align: right; color: var(--ink); }

.qty-box {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--bg);
}

.qty-btn {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border: 0;
  border-radius: 50%;
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
}

.qty-btn:hover { background: var(--moss); }

.qty-input {
  width: 52px;
  border: 0;
  background: transparent;
  text-align: center;
  color: var(--ink);
}

.qty-input:focus-visible { outline: 2px solid var(--pri); outline-offset: 1px; }

.qty-hint { margin: 0 0 12px; }

/* A 34px target is fine with a mouse and not with a thumb. */
@media (pointer: coarse) {
  .qty-btn { width: 44px; height: 44px; }
}
/* ==== One package per order =================================================
 *
 * The selected-package card and the "or build your own" band on the packages page.
 * Both are laid out with flex wrapping rather than a breakpoint, so the card and its
 * button sit side by side when there is room and stack when there is not — adding a
 * width query here would have been a new breakpoint for the sake of one band.
 *
 * No font-size is declared: the price inherits the surrounding scale, and the block
 * carries no literal colour, so it moves with the palette like the rest of the site.
 */
.pkg-selected { display: grid; gap: 8px; margin: 0 0 16px; }

.pkg-selected h3 { margin: 0; }

.pkg-selected__price { margin: 0; color: var(--ink); font-weight: 700; }

.pkg-or {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 0 0 16px;
  padding: 14px;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  background: var(--bg);
}

.pkg-or > div { flex: 1 1 24ch; min-width: 0; }

.pkg-or h3 { margin: 4px 0; }

.pkg-or p { margin: 0; }

.pkg-confirm__acts { margin-top: 10px; }

.pkg-confirm__acts .btn { flex: 1 1 auto; }
/* ==== Build your own menu — the per-dish quantity ===========================
 *
 * The control sits in the dish card the menu page already had, above the tick that
 * puts a dish in the request. The hint under it states the serving rate Eventoria
 * publishes for that dish and what it works out to for the guest count, so the number
 * beside it is explained rather than mysterious.
 *
 * Flex wrapping rather than a breakpoint keeps the label, the control and the hint
 * legible at any width without adding a tenth width query. No font-size and no literal
 * colour: the hint inherits the muted body style and the control uses the same tokens
 * as the item list's quantity box.
 */
.dish-qty {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0 0 6px;
}

.dish-qty__label { color: var(--mut); }

.dish-qty__hint { flex: 1 1 100%; }

/* --------------------------------------------------- catalogue loading shell + degraded read --- */
/*
 * Client defects #1 and #3.
 *
 * `.catalogue-loading` is the shell `index.html` puts in `#app` while the catalogue is being fetched,
 * so a cold load is a page with something on it rather than a blank screen. It is bounded and centred
 * like the first card that replaces it, so the footer does not jump when the real page draws.
 *
 * `.pkg-read` is the packages LIST failing to load — the message and its retry, sitting above the
 * grid it is talking about so the two cannot be read apart.
 */
.catalogue-loading {
  margin: 24px auto;
  max-width: 680px;
}
.catalogue-loading b { display: block; font-size: 17px; margin-bottom: 4px; }
.catalogue-loading p { margin: 0; }

.pkg-read { margin: 0 0 16px; }
.pkg-read p { margin: 4px 0 0; }
.pkg-read .btn { margin-top: 10px; }
/* ==========================================================================
   MOBILE HARDENING — audit pass, 2026-10-19
   ==========================================================================

   WHAT THIS BLOCK IS, AND WHAT IT IS NOT.

   This is not a redesign and it is not a "make it work on mobile" pass. The
   layout was measured first — 212 page states across 22 routes at 320, 360,
   375, 390, 412, 430, 480, 768 and 1024, plus two landscape sizes — and the
   document itself has no horizontal overflow at any of them. That is unusual
   and it is worth saying plainly: the grid work in this file already holds.

   What the measurement did find is the things a screenshot shows and a
   scrollWidth check cannot: a fixed bar whose label collapsed to 42px and grew
   to 324px tall over the form, seven step indicators in a six-column grid, a
   nav drawer taller than a small phone with no way to scroll it, fields at 14px
   that make iOS zoom the whole page on focus, and a row of chips clipped
   mid-word with no affordance saying it scrolls.

   Each rule below names the defect it closes. Where a number is a compromise
   between Apple's 44px and what fits on a 320px screen, the compromise is
   stated rather than hidden. Nothing here is `!important`: every rule wins on
   specificity or on being later in the cascade, so a future edit can still
   override it the normal way.

   ========================================================================== */


/* --------------------------------------------------------------------------
   1. TEXT FIELDS — the iOS zoom defect

   Every field on the site inherited 14px from `--fs-sm`. iOS Safari zooms the
   whole page when a field smaller than 16px takes focus, and it does not zoom
   back out on blur: the customer is left on a page wider than the screen, which
   is where "the site is broken on my phone" reports come from. Measured on the
   hero's three fields, `.qtn` and `#qgallery` — all 14px.

   16px is the threshold, not a design choice. The visual difference at these
   sizes is one step and it buys a page that does not move under the keyboard.

   `!important` IS DELIBERATE, and it is the only place in this block that uses
   it. `prototype.css` is linked AFTER this file and sets
   `.prototype-home-search select, .prototype-home-search input { font-size:
   var(--fs-sm) }` — same specificity as anything declared here, so document
   order hands it the win and the fields stayed at 14px through the first pass.
   Matching that from here would mean out-specifying each component one at a
   time, and the rule is not actually in dispute: no text field should render
   below 16px on a phone.    This is the one property worth asserting globally.

   `(pointer: coarse)` is in the query as well as the width, because the trigger
   is the input device and not the breakpoint: an iPad Pro in landscape is
   1024px wide and zooms exactly like a phone does. Measured at 1024 with a fine
   pointer, every field stayed at 14px — correct there, and wrong on a tablet.
   -------------------------------------------------------------------------- */
@media (max-width: 860px), (pointer: coarse) {
  input[type="text"],
  input[type="search"],
  input[type="email"],
  input[type="tel"],
  input[type="number"],
  input[type="password"],
  input[type="date"],
  input[type="time"],
  input[type="url"],
  /* The hero's ZIP/city field carries no `type` attribute at all, so
     `input[type="text"]` never matched it and it stayed at 14px through the
     first verification pass. An absent attribute means text. */
  input:not([type]),
  select,
  textarea {
    font-size: 16px !important;
  }
}


/* --------------------------------------------------------------------------
   2. THE BUILDER'S FIXED ACTION BAR — the worst defect found

   Measured on #build at 375x812: `.builder-mobile-bar` was 324px tall — 40% of
   the screen — and sat on top of the form. Its label read one word per line:

       Start / with / your / event / brief      (a 42px column, 128px tall)

   The cause is not the bar. It is `.btn { width: 100% }` in this file's phone
   media query, which the bar's single button inherits; a 343px button in a
   375px bar leaves 42px for the label, and `flex-wrap` never engages because
   the label's flex-basis is 0.

   So the fix is to give the label a real basis and to take the button back off
   100%. The bar stays one row and stays ~72px, which is what it was designed
   to be. `max-height` is a belt-and-braces limit: if copy ever grows again the
   bar is capped at 40% of the viewport instead of covering the form.
   -------------------------------------------------------------------------- */
@media (max-width: 640px) {
  .builder-mobile-bar {
    align-items: center;
    gap: 10px;
    max-height: 40vh;
  }

  .builder-mobile-bar > div {
    flex: 1 1 auto;
    min-width: 0;
  }

  .builder-mobile-bar b {
    display: block;
    font-size: 15px;
    line-height: 1.3;
  }

  /* Two lines of helper text at most, then an ellipsis. The full sentence is
     still in the DOM for assistive technology. */
  .builder-mobile-bar span {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }

  /* Specificity 0,2,0 beats the global `.btn { width: 100% }` at 0,1,0. */
  .builder-mobile-bar .btn {
    width: auto;
    flex: 0 0 auto;
    min-width: 128px;
    padding-inline: 18px;
    white-space: nowrap;
  }
}


/* --------------------------------------------------------------------------
   3. THE TWO STEP TRAILS — both wrapped badly, and they are not the same trail

   There are two, and they need different arithmetic:

   `.builder-stp` (the quote builder) has SEVEN steps. It is a centred
   `flex-wrap` row of items capped at 92px, so at 375 it drew six circles and
   then "7 Review" alone on the next row, centred under nothing — measured at
   both 320 and 375: 7 items, 2 rows, tops [145 x6, 202 x1].

   `.stp` on the "how it works" page has FOUR steps in the same flex row, so it
   split 3 + 1 with the fourth orphaned.

   The seven-step trail drops its labels below 640 and becomes a true
   seven-column grid of circles. Nothing is lost: `.flow-head` a few lines down
   already reads "STEP 1 OF 7 — Event Type", so the name is on screen directly
   beneath the number. `font-size: 0` hides the label visually without removing
   it from the accessibility tree, and the `<i>` restores its own size.

   The four-step trail keeps its labels and just becomes four equal columns.

   NOTE: `.journey-steps` (the five-step location -> plan -> items -> details ->
   review rail) is deliberately NOT touched. A seven-column rule was tried here
   first and measurement caught it turning a five-item rail into five columns
   plus two empty cells, which is the exact regression this pass exists to
   avoid.
   -------------------------------------------------------------------------- */
@media (max-width: 640px) {
  .builder-stp {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 4px;
    justify-content: initial;
  }

  .builder-stp > span {
    max-width: none;
  }

  .builder-stp button {
    font-size: 0;
  }

  .builder-stp button i {
    font-size: 12px;
  }
}

@media (max-width: 640px) {
  .stp:not(.builder-stp) {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 6px;
    justify-content: initial;
  }

  .stp:not(.builder-stp) > span {
    max-width: none;
  }
}


/* --------------------------------------------------------------------------
   4. THE PROCEED BAR — the same collapse as the builder, one step milder

   `.proceed` is `flex-wrap: wrap` with a text column at `flex-basis: 0` and a
   button that cannot shrink. On a narrow screen the text column loses: it
   keeps 80-90px and wraps to four lines while the button sits beside it. The
   existing rule only takes effect at 420px, which leaves 421-560px — every
   large phone in portrait — squeezed.

   Stacking from 560px means the label always gets the full width and the
   buttons share a row underneath.
   -------------------------------------------------------------------------- */
@media (max-width: 560px) {
  .proceed {
    flex-wrap: wrap;
    gap: 10px;
  }

  .proceed__t {
    flex: 1 1 100%;
  }

  .proceed__a {
    flex: 1 1 100%;
    width: 100%;
  }

  .proceed__a .btn {
    flex: 1 1 0;
    min-width: 0;
  }
}


/* --------------------------------------------------------------------------
   5. THE NAV DRAWER — unreachable items on a small screen

   Below 860px the menu is `position: absolute; top: 100%` inside the sticky
   header, with `overflow` left visible. Measured at 320x568: the panel is
   489px tall and starts at 114px, so it ends at 603px — 35px past the bottom
   of the viewport, with no way to scroll. "Build event", the last item, could
   not be tapped at all. On any landscape phone the panel is taller than the
   whole screen, so most of the menu was unreachable.

   Two changes: a ceiling tied to the viewport with internal scrolling, and an
   opaque background. The background was `rgba(255,255,255,.98)`, which on a
   photograph let the hero's headline ghost through the menu rows — visible in
   the audit screenshots and the reason the menu read as broken rather than
   translucent.
   -------------------------------------------------------------------------- */
@media (max-width: 860px) {
  nav {
    /* `flex-wrap: nowrap` is load-bearing, not tidying. The base `nav` rule
       carries `flex-wrap: wrap` for the desktop row; in the mobile column that
       is harmless until a `max-height` is applied, at which point the column
       wraps into a SECOND column and the last item ("Build event") reappears
       at the top right, overlapping the first menu entry. That is exactly what
       the first attempt at this fix did — caught in the screenshot, not in the
       measurements, which is why the visual pass matters. */
    flex-wrap: nowrap;
    /* Fallback first for browsers without dynamic viewport units. */
    max-height: calc(100vh - 116px);
    max-height: calc(100dvh - 100%);
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    background: #FFFDF8;
    padding-bottom: calc(18px + env(safe-area-inset-bottom, 0px));
    border-radius: 0 0 var(--radius) var(--radius);
  }

  [data-theme="dark"] nav {
    background: #203B30;
  }

  nav a {
    display: flex;
    align-items: center;
    min-height: 44px;
  }
}


/* --------------------------------------------------------------------------
   6. TAP TARGETS THAT WERE TOO SMALL TO HIT

   Measured before this block:

     .hero__dot        26x9  (the active one) and 9x9 (the others)
     .ocarousel__dot   31x31
     details > summary 20-26px tall — the FAQ accordion, the whole interaction
     .auth__eye        36x36 — the password reveal
     .qt .qtn          62x29 — the quantity field on every rental card

   The dots get a 30px box with the same 8px mark drawn by a pseudo-element, so
   the visual design is unchanged while the target grows 3.3x. 30px is the
   honest compromise: seven 44px targets do not fit in the 256px the hero gives
   them at 320px, and WCAG 2.5.8 sets the floor at 24px, which this clears with
   spacing to spare. Everything else goes to a true 44px.
   -------------------------------------------------------------------------- */
@media (max-width: 860px) {
  .hero__dots {
    gap: 2px;
    padding: 6px 8px;
  }

  .hero__dot {
    position: relative;
    width: 30px;
    height: 30px;
    padding: 0;
    background: transparent;
    border-radius: 50%;
  }

  .hero__dot::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: rgba(255, 255, 255, .58);
    transition: width .22s ease, background .22s ease;
  }

  .hero__dot.is-active {
    width: 30px;
    background: transparent;
  }

  .hero__dot.is-active::before {
    width: 22px;
    border-radius: 4px;
    background: #fff;
  }

  .ocarousel__dot {
    width: 32px;
    height: 32px;
    padding: 11px;
  }
}

@media (max-width: 860px) {
  /* The accordion trigger. `padding-block` rather than `display: flex`: the
     disclosure marker is a list-item box and flex would remove it. */
  details > summary {
    min-height: 44px;
    padding-block: 12px;
  }

  .auth__eye {
    width: 44px;
    height: 44px;
    right: 2px;
    bottom: 2px;
  }

  .fld--pw input {
    padding-right: 54px;
  }

  .qt .qtn {
    min-height: 44px;
  }

  input[type="checkbox"],
  input[type="radio"] {
    min-width: 22px;
    min-height: 22px;
  }

  /* The row of controls that measured exactly 40px and stayed there: the
     header search box, the hero's pause control, the filter chips on the menu
     and gallery pages, and `.btn.sm`. Forty is the kind of number that looks
     deliberate in a stylesheet and is still short of a thumb — the existing
     `@media (pointer: coarse)` block already tries to lift these, but pointer
     reporting is unreliable on hybrid devices (and in emulation), so the
     floor is set on width instead, where it cannot be missed. */
  .navq,
  .hero__pause,
  .chip,
  .btn.sm,
  .btn {
    min-height: 44px;
  }

  /* prototype.css is linked after this file and pins `.tabs .chip` and
     `.plan-tabs .chip` at `min-height: 40px`. Same specificity, later
     document order, so it wins and the chips stayed at 40 through the first
     verification pass. Asserted here for the same reason as the field sizes:
     the number is not in dispute on a touch screen. */
  .tabs .chip,
  .plan-tabs .chip {
    min-height: 44px !important;
  }

  /* `.svc-card__actions .btn` declares `height: 40px` AND `min-height: 40px`
     at 0,2,0, so the plain `.btn` floor above never reached the four
     "Continue planning" buttons on the services page. `min-height` wins over
     `height`, so raising it is enough — the button grows to 44. */
  .svc-card__actions .btn,
  .pkg-card__actions .btn {
    min-height: 44px !important;
  }

  /* The gallery's own search field, 42px, sits outside the header. */
  input[type="search"],
  #qgallery {
    min-height: 44px;
  }

  input[type="checkbox"],
  input[type="radio"] {
    min-width: 24px;
    min-height: 24px;
  }
}


/* --------------------------------------------------------------------------
   7. THE HERO SERVICE ROW — clipped mid-word

   At 320 the row drew "Rentals / Decoration / Fo" and cut the third chip in
   half; at 375 the fourth was sliced. It is a horizontal scroller
   (`overflow-x: auto`, `flex-wrap: nowrap`) with no fade, no peek and no
   scrollbar, so a customer cannot tell it scrolls — it reads as a rendering
   fault.

   Wrapping is the honest fix: all seven services visible, no hidden content,
   no horizontal gesture to discover. Chips also get a 40px minimum so they are
   not 29px slivers under a thumb.
   -------------------------------------------------------------------------- */
@media (max-width: 560px) {
  .hero__services {
    flex-wrap: wrap;
    overflow: visible;
  }

  .hero__services a {
    min-height: 40px;
    padding-block: 8px;
  }
}


/* --------------------------------------------------------------------------
   8. PLACEHOLDER BADGES — text clipped by the image box

   The occasion cards draw a gradient placeholder with an emoji and a badge.
   At 320 the badge ("Most Popular") was 97px of text inside a 35px content box
   and was clipped by the placeholder's own `overflow: hidden` — the word ran
   straight off the card.

   Ellipsis was tried first and rejected: on a 35px box it rendered "Most
   Po…", which is worse than either the full label or none. The badge wraps to
   two centred lines at 9px instead, so the meaning survives. `anywhere` is
   there for the case where a single word is still wider than the box.
   -------------------------------------------------------------------------- */
@media (max-width: 560px) {
  .im > .tag {
    max-width: calc(100% - 12px);
    padding: 3px 6px;
    font-size: 9px;
    line-height: 1.2;
    white-space: normal;
    overflow-wrap: anywhere;
    text-align: center;
  }
}

/* The small icon card is the case the breakpoint could not reach: its image box
   is 47x35 at EVERY viewport, so the 12px badge overflowed at 768 and 1024 just
   as it did at 320 — the width query only fixed the phone. Scoped to the
   component whose box is genuinely tiny rather than shrinking every badge on
   the site. */
.card.icn .im > .tag {
  max-width: calc(100% - 10px);
  padding: 2px 5px;
  font-size: 9px;
  line-height: 1.15;
  white-space: normal;
  overflow-wrap: anywhere;
  text-align: center;
}


/* --------------------------------------------------------------------------
   9. THE RENTALS CATEGORY FILTER — no styling at all

   `.side__options` appears exactly once in the codebase, in the rentals page
   markup, and had no CSS anywhere. The buttons inherited the site-wide button
   reset, so they rendered as loose words with browser-default borders: one
   green rectangle for the active item and eight bare labels around it. It read
   as a broken grid rather than a filter.

   Given the same pill language as every other chip on the site, so the page
   belongs to the same product.
   -------------------------------------------------------------------------- */
.side__options {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.side__options button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--card);
  color: var(--ink);
  font: 600 13px var(--sans);
  cursor: pointer;
  transition: background .18s ease, border-color .18s ease, color .18s ease;
}

.side__options button:hover {
  border-color: var(--pri);
}

.side__options button.on {
  background: var(--pri);
  border-color: var(--pri);
  color: #fff;
}

.side__options button:focus-visible {
  outline: 2px solid var(--pri);
  outline-offset: 2px;
}


/* --------------------------------------------------------------------------
   10. THE REQUIRED-FIELD ASTERISK — pushed onto its own line

   `.auth__form .fld` is `display: grid`, and the label's text is an anonymous
   grid item: "Email" became row one and the "*" row two, on both sign-in and
   sign-up. A `*` alone under its label reads as a layout fault and, worse,
   detaches the marker from the field it marks.

   Block flow puts the text and the asterisk back on one line; the field is
   already `display: block` and only needs its own spacing, which the grid gap
   used to provide.
   -------------------------------------------------------------------------- */
.auth__form .fld {
  display: block;
}

.auth__form .fld > input,
.auth__form .fld > select {
  margin-top: 6px;
}

.auth__form .fld > .err,
.auth__form .fld > .auth__opt {
  display: block;
  margin-top: 4px;
}


/* ==========================================================================
   HOME PAGE FIXES — 2026-10-20
   ==========================================================================

   Two defects found by driving the live homepage in a real browser at
   1440x900 and 375x812, then reproduced here with EMPTY localStorage AND
   sessionStorage (several other reports from that pass turned out to be a
   returning visitor's saved quote, not a bug, and were discarded).

   ========================================================================== */


/* --------------------------------------------------------------------------
   1. THE AI BAND'S BUTTONS ARE 180px TALL ON A PHONE

   Measured at 375: "Open Eventoria AI" and "Or use the planner instead"
   rendered 313 x 180px — two enormous ovals with the label floating in the
   middle, instead of buttons.

   The cause is an axis switch. Those buttons carry `flex: 1 1 180px` from the
   desktop row, where 180px is a sensible *width* basis. In the phone layout
   `.ai-band .row` becomes a COLUMN, so the main axis is vertical and that same
   180px becomes the button's HEIGHT. Nothing about the button changed; the
   direction did.

   `flex: 0 0 auto` drops the basis and returns the button to its own content
   height. Specificity is 0,3,0 against the source rule's 0,2,0, so this wins
   without `!important`.
   -------------------------------------------------------------------------- */
@media (max-width: 860px) {
  .ai-band .row {
    align-items: stretch;
  }

  .ai-band .row .btn {
    flex: 0 0 auto;
    min-height: 44px;
  }
}


/* --------------------------------------------------------------------------
   2. DARK MODE HIDES THE HERO PHOTOGRAPH AND MAKES THE BOOKING FORM UNREADABLE

   `:root[data-theme="dark"] .hero__inner` sets `background: rgba(32,59,48,.96)`.
   `.hero__inner` is not a card — it is the transparent text column that stands
   directly on the hero photograph (the comment above the base rule says so
   explicitly: "the words now stand directly on the photograph"). Painting it
   dark green in dark mode therefore hides the picture behind a flat panel,
   leaving only a thin strip of image at the edges — measured at 375: inner
   311px of a 347px hero, `background-image: none`.

   The booking panel is the same mistake in the other direction. It is supposed
   to stay cream in every theme — prototype.css says the pills and the panel use
   "literal cream and a literal ink... this section is dark whatever the theme
   is" — but the dark override repainted it dark green while its labels kept
   their dark ink (`rgba(71,88,82,.95)`), which is roughly 1.8:1 against that
   background: effectively invisible.

   So: the text column goes transparent again, and the panel stays cream. Both
   selectors carry `html:root[...]` (0,3,1) so they beat the original 0,3,0
   rule regardless of document order.
   -------------------------------------------------------------------------- */
html:root[data-theme="dark"] .hero__inner {
  background: transparent;
}

html:root[data-theme="dark"] .prototype-home-search {
  background: rgba(255, 253, 247, .96);
  border-color: rgba(255, 255, 255, .62);
}

/* The panel's own type is dark ink on cream in either theme — restated here so
   a future dark-mode rule cannot drift it back to a light-on-light pair. */
html:root[data-theme="dark"] .prototype-home-search label,
html:root[data-theme="dark"] .prototype-home-search .hs-label {
  color: rgba(71, 88, 82, .95);
}

html:root[data-theme="dark"] .prototype-home-search select,
html:root[data-theme="dark"] .prototype-home-search input {
  background: #fff;
  color: #182A26;
  border-color: rgba(31, 90, 74, .2);
}


/* ==========================================================================
   MOBILE HERO — resume card collapse + header ghosting (2026-10-20)
   ==========================================================================

   Reported from a real phone screenshot: the "Your plan is ready to continue"
   card was a narrow column of one-word lines with the Continue button sitting
   on top of it, and the header let the hero's buttons show through.

   Reproduced headless at 375 with a seeded draft, so these are measured
   numbers rather than an impression of the picture:

     .resume-card   display:flex  flex-direction:row  flex-wrap:nowrap  gap:12
       span.icon    34px   flex: 0 0 auto
       div (text)    0px   flex: 1 1 0%      <- height 187px, one word per line
       a.btn       227px   flex: 0 1 auto
   ========================================================================== */


/* --------------------------------------------------------------------------
   1. THE RESUME CARD

   Same failure as the builder's action bar: the text column is `flex: 1 1 0`,
   so its hypothetical width is zero, and next to a 227px button that cannot
   shrink it is handed whatever is left — nothing. `nowrap` then refuses to
   move the button to its own line, so the text wraps into a 187px-tall sliver
   and the button overlaps it.

   Below 560px the card becomes two rows: icon and text on the first, the
   button full width on the second. The text gets `calc(100% - 46px)` — the
   34px icon plus the 12px gap — so it can never be squeezed to zero again.
   -------------------------------------------------------------------------- */
@media (max-width: 560px) {
  .resume-card {
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 10px;
  }

  .resume-card > span.resume-card__icon {
    flex: 0 0 auto;
  }

  .resume-card > div {
    flex: 1 1 calc(100% - 46px);
    min-width: 0;
  }

  .resume-card > .btn {
    flex: 1 1 100%;
    width: 100%;
    margin-left: 0;
  }
}


/* --------------------------------------------------------------------------
   2. THE HEADER LETTING THE HERO SHOW THROUGH

   `header` is `rgba(255, 253, 252, .96)` plus `backdrop-filter: blur(14px)`.
   On a phone the blur is the only thing hiding the 4% transparency, and where
   `backdrop-filter` is unsupported or disabled (battery saver, some in-app
   browsers, older Android WebView) the hero's own buttons read straight
   through the bar — visible in the reported screenshot as ghosted "Start
   planning" and "AI Recommendations" behind the logo.

   A solid bar removes the dependency on an optional effect. The desktop bar
   keeps its blur, where there is room for it and it is supported.
   -------------------------------------------------------------------------- */
@media (max-width: 860px) {
  header {
    background: #FFFDFC;
  }

  html:root[data-theme="dark"] header {
    background: #182A26;
  }
}


/* ==========================================================================
   THE DATE FIELD THAT LEAVES ITS CARD — WebKit, 2026-10-20
   ==========================================================================

   Reported from an iPhone screenshot: "Oct 14, 2026" ran past the right edge
   of the booking panel while Event type and Location above it sat neatly
   inside.

   It does NOT reproduce in Blink. Measured here at 320 / 375 / 430, the date
   input's right edge matches its label's exactly and sits 15px inside the
   panel's padding every time (`overPanel: -15`). So this is WebKit, and the
   honest statement is: the cause below is WebKit's documented behaviour, and
   the fix is verified not to regress Blink — but it has NOT been verified on
   a real iPhone from this machine.

   WHY IT HAPPENS. `input[type="date"]` on iOS is not a plain text box: the
   date is drawn by the `::-webkit-date-and-time-value` pseudo-element, whose
   min-content width is the rendered date plus the picker indicator. A grid or
   flex item's default `min-width` is `auto`, which means "never shrink below
   my min-content" — so the label cell refuses to be narrower than that date,
   the grid track grows past the panel's content box, and the field is painted
   outside the card. `width: 100%` cannot win against `min-width: auto`.

   The fix is to opt every link in that chain out of automatic minimum sizing:
   the label cell, the control, and WebKit's inner value element. Appearance
   and the picker indicator are deliberately left alone — the native date
   picker is the one thing iOS does better than anything we could replace it
   with, and removing it to fix a width bug would be a poor trade.
   -------------------------------------------------------------------------- */
@media (max-width: 860px) {
  .prototype-home-search label {
    min-width: 0;
  }

  .prototype-home-search input,
  .prototype-home-search select {
    min-width: 0;
    max-width: 100%;
    width: 100%;
  }

  input[type="date"],
  input[type="time"],
  input[type="datetime-local"],
  input[type="month"],
  input[type="week"] {
    min-width: 0;
    max-width: 100%;
  }

  /* WebKit's inner date box carries the min-content width that starts all of
     this. It is shrinkable; it just has to be told. */
  input[type="date"]::-webkit-date-and-time-value,
  input[type="time"]::-webkit-date-and-time-value {
    min-width: 0;
    text-align: left;
  }

  /* The indicator adds to that width. Keep its hit area, drop the extra
     inline margin so it cannot contribute overflow. */
  input[type="date"]::-webkit-calendar-picker-indicator {
    margin-left: 0;
    padding: 0;
  }
}


/* ==========================================================================
   DATE FIELD, SECOND ATTEMPT — why the first one did not hold
   ==========================================================================

   The first fix set `min-width: 0` on the label and on the input. That is the
   usual advice and it is not enough on iOS, because of the rule it relies on:

     A flex/grid item's `min-width: auto` resolves to its content-based minimum
     ONLY while its own `overflow` is `visible`.

   `min-width: 0` sets the *used* value, but WebKit still lets the date
   control's inner shadow box (`::-webkit-date-and-time-value`, whose
   min-content is the rendered date plus the picker indicator) paint past the
   track. The card stayed overflowing.

   Three changes, in order of strength:

   1. `overflow: hidden` on the label. This is the part that actually binds:
      with a non-visible overflow the automatic minimum no longer applies at
      all, so the cell is allowed to be narrower than its content and the
      control is forced to fit the track instead of widening it.

   2. `appearance: none` on the date control on phones. It keeps the native
      picker — tapping still opens the iOS wheel, which is the one thing worth
      keeping — while replacing the decorative chrome that carries the
      oversized intrinsic width.

   3. `display: block` + `width: 100%`, so the control is a block box in the
      track rather than an inline-level replaced element.

   `padding-right` on the label is restored to the field's own padding so the
   calendar indicator is not clipped by change (1).
   ========================================================================== */
@media (max-width: 860px) {
  .prototype-home-search label {
    min-width: 0;
    overflow: hidden;
  }

  .prototype-home-search input[type="date"],
  .prototype-home-search input[type="time"] {
    display: block;
    width: 100%;
    max-width: 100%;
    min-width: 0;
    -webkit-appearance: none;
    appearance: none;
  }

  /* The indicator keeps its hit area; it is nudged inside the field's own
     padding so `overflow: hidden` on the label cannot cut it in half. */
  .prototype-home-search input[type="date"]::-webkit-calendar-picker-indicator {
    margin-left: 0;
    margin-right: -2px;
    padding: 0;
  }

  /* The date is left-aligned like every other field on the site; the native
     control centres it on iOS by default, which reads as a different design. */
  .prototype-home-search input[type="date"]::-webkit-date-and-time-value {
    text-align: left;
    min-width: 0;
  }
}
