/* ===========================================================================
   One rule about where things go in this file, and it is load-bearing.

   NEW RULES GO AT THE END. Existing rules are edited in place.

   Several sessions work on this product at once and most of them touch this
   stylesheet. A file where new work only ever lands at the bottom almost
   never conflicts, because two branches appending are two branches editing
   different lines. On 6 Nov 2026 this branch sat under two merges in a row
   and auto-merged through both of them without a single conflict in here,
   while the same two merges conflicted in CONTEXT.md every time. That is the
   convention working, not luck.

   Why it is worth a rule rather than a preference: resolving a conflict in a
   stylesheet by keeping both sides drops a closing brace whenever the
   ======= marker falls inside a rule, because each side is then a fragment
   rather than a block. That happened in October, three braces went missing,
   and the broken file reached production. NOTHING CATCHES IT. Typecheck does
   not read CSS, the build does not parse it, the page still loads, and the
   only symptom is that every rule after the break is swallowed by the
   unclosed one above it.

   So after any merge or conflict resolution in this file, two checks:

     grep -o '{' public/app.css | wc -l
     grep -o '}' public/app.css | wc -l

   They must MATCH EACH OTHER. That is the whole invariant. Do not check them
   against a remembered total: grep counts braces inside comments too, so this
   very block adds one of each by printing those two commands, and every rule
   anyone appends moves the number again. A total is stale the day it is
   written down; equality is not.

   And balanced is not the same as correct, because a brace that is balanced
   and in the wrong place passes that count and still eats the rules below it.
   The check that actually tests what you care about is to load a page and ask
   the browser what it built:

     const s = [...document.styleSheets].find(s => s.href.endsWith('app.css'));
     s.cssRules.length;
     s.cssRules[s.cssRules.length - 1].cssText;

   If the last rule in the file is there, and applies, nothing above it was
   swallowed, because an unclosed rule takes everything after it. One command,
   and it proves the whole stylesheet rather than its brace arithmetic.

   The two @import rules below must stay first. A comment may precede them,
   which is why this block is allowed to be here, but a style rule may not.
   =========================================================================== */
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600&family=Mukta:wght@300;400;500;600;700&display=swap');
@import url('/a/f4dcd467f6c8/tokens.css');

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-weight: 300;
  line-height: 1.6;
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 500;
  letter-spacing: -0.025em;
  line-height: 1.3;
  margin: 0;
}
a { color: inherit; }
img { max-width: 100%; display: block; }
input, select, textarea, button { font: inherit; color: inherit; }

.wrap { max-width: 1180px; margin: 0 auto; padding: 0 24px; }
.band { padding: 92px 0; }
.band--light { background: var(--toran-white); }
.band--ground { background: var(--toran-surface); }
.band--sand { background: var(--toran-sand); }
.band--dark { background: var(--toran-teal-deep); color: var(--toran-white); }
.band--plum { background: var(--toran-plum); color: var(--toran-white); }
/* The garland is the wordmark's other half, so it carries the same ink as the
   type beside it rather than a colour of its own. On the deep-teal chrome
   that is white; every light surface leaves `--mark` unset and gets the brand
   teal. Gold stayed on the mark here for a long time and the result was a
   two-colour lockup nobody had chosen: a gold object next to a white word.
   Gold is still the accent on these surfaces — the ticks, the state label,
   the Add time button — it is just not the logo. */
.band--dark, .band--plum, .side, .appbar, .foot, .cmp__us { --mark: var(--toran-white); }
.band--dark p, .band--plum p { color: var(--toran-sand); }
/* A light card inside a dark band inherits the band's white text, which puts
   white on white. Every light surface resets to ink. */
.band--dark .card, .band--plum .card,
.band--dark table.data, .band--plum table.data,
.band--dark .review, .band--plum .review,
.band--dark .preview__page, .band--plum .preview__page { color: var(--text); --mark: var(--brand); }
.band--dark .card p, .band--plum .card p { color: var(--text-muted); }

.eyebrow {
  font-size: 12px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--toran-muted); margin: 0 0 14px;
  display: flex; align-items: center; gap: 10px;
}
.eyebrow::before { content: ''; width: 26px; height: 1px; background: currentColor; }
.band--dark .eyebrow, .band--plum .eyebrow { color: var(--toran-gold); }

.pill {
  display: inline-block; padding: 6px 14px; border-radius: 999px;
  background: var(--toran-sand); color: var(--toran-amber-deep);
  font-size: 12px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
}

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 16px 30px; border-radius: 999px; border: 1px solid transparent;
  font-size: 16px; font-weight: 600; text-decoration: none; cursor: pointer;
  background: var(--brand); color: #fff;
}
.btn:hover { background: var(--brand-deep); }
/* Any class that sets `display` beats the user agent's `[hidden]`, which is
   how a "Use" button stayed on screen in a picker that had hidden it and how
   the image picker's drop zone sat above the photo it had just accepted.
   Both were a one-rule fix; this is the rule, stated once for the two
   classes in this file that script toggles with `hidden`. */
.btn[hidden], .ip__empty[hidden] { display: none; }
.btn--ghost { background: transparent; color: var(--text); border-color: var(--text); }
.btn--ghost:hover { background: var(--text); color: #fff; }
.btn--gold { background: var(--toran-gold); color: var(--toran-teal-deep); }
.btn--gold:hover { background: var(--toran-gold-light); }
.btn--sm { padding: 10px 18px; font-size: 14px; }
.btn[disabled] { opacity: .5; cursor: not-allowed; }

.card {
  background: var(--bg-raised); border: 1px solid var(--border);
  border-radius: 14px; padding: 24px;
}
.grid { display: grid; gap: 20px; }
.grid--3 { grid-template-columns: repeat(3, 1fr); }
.grid--2 { grid-template-columns: repeat(2, 1fr); }
.muted { color: var(--text-subtle); }
.small { font-size: 14px; }
.tiny { font-size: 12px; }
.stack > * + * { margin-top: 14px; }
.row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.spread { display: flex; align-items: center; justify-content: space-between; gap: 16px; }

/* ---- site header ---- */
.nav {
  position: sticky; top: 0; z-index: 50;
  background: rgba(255,255,255,.92); backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border);
}
.nav__in { display: flex; align-items: center; justify-content: space-between; padding: 16px 24px; max-width: 1180px; margin: 0 auto; }
.nav__links { display: flex; align-items: center; gap: 26px; font-size: 14px; }
.nav__links a { text-decoration: none; color: var(--text-muted); }
.nav__links a:hover { color: var(--text); }
/* .nav__links a outranks .btn on specificity, so the CTA has to win it back
   or it renders ink-soft on teal, about 1.2:1 and invisible. */
.nav__links a.btn { color: #fff; }
.nav__links a.btn:hover { color: #fff; }
/* ---- the lockup ----

   One lockup, built once. There used to be eight, each assembled by hand at
   its call site out of a `mark(N)` and a `<span>`, with N picked per site:
   24, 26, 28, 30, 32. They drifted, and two of them were plainly wrong — the
   sidebar's resolved to `display:block` because the rail's link rule (now
   `.side__nav a`, then `.side a`) outranks a bare
   `.brandmark`, so its `gap` never applied and the garland sat on the text
   baseline 4.5px high; the rest ran a visible gap of ~14px where the brand's
   own lockup uses 3.4px.

   So the wordmark's size is the only number, set on `.brandmark`, and the
   garland and the gap are derived from it in the proportions of
   brand/logo/toran-lockup.svg: garland 0.71 of the word's size, gap 0.18.
   Setting font-size on the span instead would break that, so don't. */
.brandmark {
  display: flex; align-items: center; gap: 0.18em; text-decoration: none;
  font-family: var(--font-display); font-weight: 600; font-size: 21px;
  letter-spacing: -0.025em;
}
.brandmark span { font-size: 1em; }
/* Optical, not geometric. "toran" is lowercase with no descenders, so its
   line box carries empty space under the baseline that the word never uses,
   and centring the two boxes leaves the garland reading high. A ninth of the
   word's size settles it — in em, so it holds at every size rather than only
   at the one it was eyeballed on. */
.brandmark svg { height: 0.71em; width: auto; transform: translateY(0.09em); }

/* ---- the Templates dropdown ----

   Opens on hover and on :focus-within, with no JavaScript, because the
   marketing pages load none and one menu is not worth a script on every page.
   The trigger is a real link, so a touch screen (where hover never fires) still
   gets to /templates on a tap. */
.navdrop { position: relative; }
.navdrop__btn { display: inline-flex; align-items: center; gap: 7px; }
.navdrop__caret {
  width: 6px; height: 6px; margin-top: -3px;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg);
}
.navdrop__menu {
  position: absolute; z-index: 60; top: 100%; left: 50%; margin-top: 15px;
  transform: translateX(-50%); width: 580px; padding: 18px;
  background: var(--toran-white); border: 1px solid var(--border); border-radius: 16px;
  box-shadow: 0 18px 50px rgba(12, 59, 54, .16);
  opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .13s ease;
}
/* The 15px gap to the header is dead space the pointer has to cross, and
   crossing it would close the menu. This bridges it without moving anything. */
.navdrop__menu::before { content: ''; position: absolute; left: 0; right: 0; top: -15px; height: 15px; }
.navdrop:hover .navdrop__menu,
.navdrop:focus-within .navdrop__menu { opacity: 1; visibility: visible; pointer-events: auto; }
.navdrop__cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0 16px; }
.navdrop__label {
  margin: 0 0 8px; padding: 0 10px;
  font-size: 11px; font-weight: 600; letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-subtle);
}
.navdrop__col a { display: block; padding: 7px 10px; border-radius: 10px; text-decoration: none; }
.navdrop__col a:hover { background: var(--toran-surface); }
.navdrop__col strong { display: block; font-size: 14px; font-weight: 600; color: var(--text); }
.navdrop__col span { display: block; font-size: 12.5px; line-height: 1.45; color: var(--text-subtle); }
.navdrop__all {
  display: block; margin-top: 14px; padding-top: 13px; border-top: 1px solid var(--border);
  font-size: 13px; font-weight: 600; color: var(--brand); text-align: center;
}

/* ---- templates page, one section per occasion ---- */
/* The header is sticky, so an anchor landing flush with the top of the viewport
   lands underneath it. This is the header's height plus a little air. */
.tsec { padding: 62px 0; scroll-margin-top: 78px; }
.chip--jump { display: inline-block; text-decoration: none; color: var(--text-muted); }
.chip--jump:hover { border-color: var(--brand); color: var(--brand); background: var(--toran-white); }
.tcard { padding: 0; overflow: hidden; }
.tcard__art { padding: 44px 20px; text-align: center; }
.tcard__who { font-family: var(--font-display); font-weight: 600; font-size: 27px; line-height: 1.25; }
.tcard__when { font-size: 11px; letter-spacing: .13em; text-transform: uppercase; margin-top: 9px; }
.tcard__lines { display: flex; flex-wrap: wrap; justify-content: center; gap: 7px; margin-top: 17px; }
.tcard__lines span {
  font-size: 10.5px; letter-spacing: .09em; text-transform: uppercase;
  padding: 5px 11px; border: 1px solid; border-radius: 999px;
}
.tcard__foot { padding: 20px 22px 22px; }

/* ---- hero ----
   Scoped to `.hero--split`, which is the marketing page's hero and the only
   other `.hero` in this codebase, because the one that is NOT the marketing
   page is every couple's invitation.

   `doc()` links this stylesheet on every page in the product, tenant sites
   included, and `src/routes/tenant.ts` also calls its hero section `.hero`.
   `.hero p` is (0,1,1) and beats `.hero-eyebrow` (0,1,0) in tenant.css, so
   these four lines were rewriting four paragraphs on every live invitation:
   the eyebrow, the "Next Ceremony" label, the date and the venue all came
   out at 18px with a 620px cap and a 30px bottom margin, instead of
   12/11/28/13px with the margins the card was designed around. The date,
   which should be the second-largest line on the card, rendered the same
   size as the venue label, and the whole hero below the names collapsed
   into one flat band. `.hero h1` did the same to the names' margins.

   Checked rather than assumed: on a rendered invitation these two are the
   only app.css selectors that match anything and are not also declared in
   tenant.css, apart from the bare element resets tenant.css already
   overrides. So scoping them closes the leak completely. The marketing hero
   carries both classes (`src/views/marketing.ts:68`) and is unaffected. */
.hero--split { text-align: center; padding: 96px 0 100px; background: var(--toran-white); }
.hero--split h1 { font-size: clamp(40px, 6vw, 76px); margin: 18px 0 20px; }
.hero--split p { max-width: 620px; margin: 0 auto 30px; font-size: 18px; color: var(--text-muted); }
.hero--split .row { justify-content: center; }

/* ---- the live demo ---- */
.demo { display: grid; grid-template-columns: 470px 1fr; gap: 56px; align-items: center; }
.field { display: flex; flex-direction: column; gap: 7px; }
.field label {
  font-size: 12px; font-weight: 600; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--text-subtle);
}
.field input, .field select, .field textarea {
  padding: 14px 16px; border-radius: 8px; border: 1px solid var(--border);
  background: var(--toran-surface); font-size: 16px; font-weight: 400; width: 100%;
}
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border: 2px solid var(--brand); padding: 13px 15px;
}
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
  padding: 9px 16px; border-radius: 999px; border: 1px solid var(--border);
  background: var(--toran-surface); font-size: 14px; cursor: pointer; user-select: none;
}
.chip[aria-pressed="true"], .chip.is-on { background: var(--brand); color: #fff; border-color: var(--brand); }

.preview { border-radius: 22px; overflow: hidden; background: var(--toran-teal-deep); }
.preview__bar { display: flex; align-items: center; gap: 8px; padding: 13px 18px; }
.preview__dot { width: 9px; height: 9px; border-radius: 999px; background: var(--toran-gold); }
.preview__url { margin-left: 10px; font-size: 12px; color: var(--toran-sand); }
.preview__page {
  background: #fff; padding: 52px 40px; text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
}
.preview__names { font-family: var(--font-display); font-weight: 600; font-size: 56px; letter-spacing: -0.025em; color: var(--text); }
.preview__meta { font-size: 14px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-muted); }
.preview__foot { padding: 12px; text-align: center; font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--toran-gold-pale); }

.counts { display: flex; gap: 30px; }
.counts div { text-align: center; }
.counts b { display: block; font-family: var(--font-display); font-size: 34px; color: var(--brand); font-weight: 600; }
.counts span { font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-subtle); }

/* ---- dashboard showcase ---- */
.showcase { display: grid; grid-template-columns: 430px 1fr; gap: 60px; align-items: start; }
.step { display: flex; gap: 18px; padding: 24px; border-radius: 14px; border-left: 3px solid transparent; opacity: .55; }
.step.is-on { background: var(--brand); opacity: 1; border-left-color: var(--toran-gold); }
.step b { font-size: 12px; letter-spacing: 0.14em; color: var(--toran-sand); }
.step.is-on b { color: var(--toran-gold); }
.step h3 { font-size: 25px; font-weight: 600; color: #fff; }
.step p { margin: 6px 0 0; font-size: 16px; }

/* ---- reviews ---- */
/* The home page band, built to hold ten to fifteen and still look right
   holding one. See src/views/reviews.ts.

   Three or fewer: centred and wrapping. It used to be `column-count: 3`,
   which fills left to right, so a single quote sat in the left third of a
   full-width plum band with two thirds of empty colour beside it, reading as
   a section that had failed to load. The first real testimonial will arrive
   on its own, so that was the case this had to handle best and handled worst.

   Four or more: `.reviews--rail` turns the same cards into a native
   scroll-snap row. Fifteen cards wrapped would be three screens of plum with
   the price pushed below all of it; a rail shows three at a time, keeps the
   band one screen tall, and needs no JavaScript to scroll. */
.reviews { display: flex; flex-wrap: wrap; gap: 20px; justify-content: center; }
.reviews--rail {
  flex-wrap: nowrap; justify-content: flex-start;
  overflow-x: auto; scroll-snap-type: x mandatory; scroll-behavior: smooth;
  /* The half-visible next card is the affordance; a scrollbar track across a
     plum band is furniture. Nothing is reachable only by dragging it: the
     container is focusable, so the arrow keys scroll it. */
  scrollbar-width: none; overscroll-behavior-x: contain;
  padding-bottom: 4px;
}
.reviews--rail::-webkit-scrollbar { display: none; }
.reviews--rail > .review { flex: 0 0 340px; max-width: 340px; scroll-snap-align: start; }
.reviews--rail:focus-visible { outline: 2px solid var(--toran-gold); outline-offset: 6px; border-radius: 22px; }
.reviews__hint { margin: 14px 0 0; font-size: 13px; color: var(--toran-sand); opacity: .6; }
.review {
  flex: 1 1 300px; max-width: 380px;
  background: #fff; border-radius: 22px; padding: 28px;
  color: var(--text); margin: 0;
  display: flex; flex-direction: column;
}
.review blockquote { margin: 0 0 18px; font-family: var(--font-display); font-size: 21px; line-height: 1.3; letter-spacing: -0.025em; }
/* The attribution sits at the foot whatever the quote's length, so a row of
   cards has its names on one line instead of stepping up and down. */
.review__by { display: flex; align-items: center; gap: 12px; margin-top: auto; }
.review__by strong { display: block; font-size: 14px; font-weight: 600; }
.review__by span span { display: block; font-size: 12.5px; color: var(--text-subtle); }
.avatar {
  width: 44px; height: 44px; border-radius: 999px; background: var(--toran-sand);
  display: grid; place-items: center; font-family: var(--font-display);
  font-weight: 600; font-size: 17px; color: var(--toran-teal-deep); flex-shrink: 0;
}
.stars { display: flex; gap: 3px; }

/* ---- pricing ---- */
.plans { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; align-items: stretch; }
.plans--two { grid-template-columns: 1.3fr 1fr; }
.plan { display: flex; flex-direction: column; gap: 22px; padding: 38px; border-radius: 22px; background: var(--toran-surface); border: 1px solid var(--border); }
.plan--featured { background: var(--toran-teal-deep); border-color: var(--toran-gold); color: #fff; }
.plan--featured li, .plan--featured .muted { color: var(--toran-sand); }
.plan--note { background: var(--toran-sand); border-color: transparent; }
.plan .price { font-family: var(--font-display); font-weight: 600; font-size: 52px; letter-spacing: -0.025em; }
.plan ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; font-size: 16px; }
.plan li { display: flex; gap: 11px; align-items: flex-start; }
.plan .btn { margin-top: auto; }
.tick { flex-shrink: 0; margin-top: 3px; }

/* ---- footer ---- */
.foot { background: var(--toran-teal-deep); color: #fff; padding: 68px 0 46px; }
.foot a { color: var(--toran-sand); text-decoration: none; font-size: 14px; }
.foot a:hover { color: #fff; }
.foot h4 { font-size: 12px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--toran-gold); font-family: var(--font-body); }
.foot__cols { display: flex; gap: 76px; }
.foot__col { display: flex; flex-direction: column; gap: 13px; }
.rule { height: 1px; background: var(--brand); margin: 40px 0 24px; }

/* ---- app shell ---- */
/* ---- the dashboard fills the viewport, and only ever scrolls inside ----
 *
 * An application's navigation does not scroll away. The sidebar used to go
 * up with the page, so halfway down the Design editor the only thing left of
 * the nav was the billing box, and getting to Guests meant scrolling back to
 * the top first.
 *
 * The fix is one locked column on <body>, not a sticky sidebar. Sticky fixes
 * the sidebar and leaves the page scrolling underneath it, which means the
 * two panes of the editor still scroll as one long document and the phone
 * still scrolls the app bar off. A flex column that is exactly the viewport
 * tall, with the panes as the only scroll containers, fixes both at once and
 * is the same rule on a phone and a laptop.
 *
 * `min-height: 0` on the scrollers is the part that is easy to leave out: a
 * flex item's automatic minimum size is its content, so without it the shell
 * grows past the viewport and the body scrollbar comes straight back.
 *
 * `dvh` after `vh` so a phone whose address bar is showing gets the real
 * height rather than 100vh of which the bottom is behind the browser chrome;
 * a browser that does not know `dvh` keeps the `vh` line above it.
 *
 * Only `.is-app`, set by `shell()`. The marketing site and a couple's own
 * wedding site are documents and must keep scrolling like documents.
 */
.is-app { height: 100vh; height: 100dvh; overflow: hidden; display: flex; flex-direction: column; }
.is-app .appbar { flex: none; }
.is-app .shell { flex: 1 1 auto; min-height: 0; }
.is-app .renew { flex: none; }
/* The rail itself does not scroll; its links do, and its foot stays put.
   That is what lets the account menu open upward from the foot without
   either being clipped or pushing itself off the bottom of the screen -- see
   the note on `.acct` further down. */
.is-app .side { min-height: 0; overflow: hidden; }
.is-app .side__nav,
.is-app .main { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }

.shell { display: grid; grid-template-columns: 248px 1fr; min-height: 100vh; }
.side { background: var(--toran-teal-deep); color: #fff; padding: 28px 18px; display: flex; flex-direction: column; }
/* `.side__nav a` sets display:block and font-size:16px and outranks a bare
   `.brandmark`, which is why the sidebar lockup was laid out as inline text
   rather than as a flex row. Both are taken back here. The link rule is
   scoped to the nav rather than to `.side`, because the account menu in the
   foot is full of anchors that want nothing to do with it. */
.side .brandmark { display: flex; font-size: 21px; padding: 0 10px 22px; color: #fff; }
.side__nav { display: flex; flex-direction: column; gap: 6px; flex: 0 1 auto; }
.side__nav a { display: block; padding: 11px 14px; border-radius: 8px; text-decoration: none; color: var(--toran-sand); font-size: 16px; }
.side__nav a:hover { background: rgba(255,255,255,.07); color: #fff; }
.side__nav a.is-on { background: var(--brand); color: #fff; }
/* The foot is pinned to the bottom of the rail and never scrolls, so the
   menu it opens is always on screen. */
.side__foot { margin-top: auto; flex: none; padding-top: 16px; }
.main { padding: 44px 48px; background: var(--toran-surface); }

/* ---- the phone's chrome, from A2m ----
   Both are display:none until 860px, so the desktop shell is untouched: the
   sidebar is still the only navigation above that width and neither of these
   contributes a box. Everything that makes them appear lives in the one
   media block below. */
.appbar, .tabbar { display: none; }

.appbar {
  align-items: center; justify-content: space-between; gap: 12px;
  padding: 8px 20px; background: var(--toran-teal-deep); color: #fff;
  position: sticky; top: 0; z-index: 30;
}
/* The phone bar runs the lockup one step down. One number, and the garland
   and the gap follow it. */
.appbar .brandmark { color: #fff; font-size: 19px; }
/* Which wedding, as plain text. The switcher said this until 6 Nov 2026 and
   has moved into the More sheet; on a phone, with no rail, something still
   has to name the thing being edited. Deliberately not a control: every
   place a tap could go is already a tab. */
.appbar__where {
  flex: 0 1 auto; min-width: 0; font-size: 13px; color: var(--toran-sand);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The bar itself. `env(safe-area-inset-bottom)` is why `shell()` asks doc()
   for viewport-fit=cover: without cover the inset resolves to 0 and the bar
   would sit under the home indicator on an iPhone. The side insets matter in
   landscape on a notched phone, where 0 would put the end tabs under the
   rounded corner. */
.tabbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  align-items: stretch; justify-content: center; gap: 2px;
  background: var(--bg-raised); border-top: 1px solid var(--border);
  padding: 6px max(10px, env(safe-area-inset-right)) calc(6px + env(safe-area-inset-bottom)) max(10px, env(safe-area-inset-left));
}
/* `flex: 1 1 0` up to a cap: on a phone the five tabs divide the width
   (69px each at 375px), and on a tablet they stop at 140px and cluster in
   the middle instead of stretching to a fifth of 860px each. */
.tab, .tabmore { max-width: 140px; }
.tab {
  flex: 1 1 0; min-width: 0; min-height: 48px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  padding: 5px 2px; border-radius: 10px; text-decoration: none;
  color: var(--text-subtle); font-size: 11px; font-weight: 500; line-height: 13px;
}
.tab svg { width: 22px; height: 22px; flex-shrink: 0; }
.tab span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tab.is-on, .tabmore.is-on > .tab, .tabmore[open] > .tab { color: var(--brand); font-weight: 600; }
.tab:active { background: var(--toran-surface); }

/* More is a <details>, not a script. It gives the open/close toggle, the
   keyboard handling and the aria wiring for free, and these pages load three
   scripts for real work — a menu is not worth a fourth. Tapping More again
   closes it, which on a phone is the thumb staying where it already is. */
.tabmore { flex: 1 1 0; min-width: 0; position: relative; }
.tabmore > .tab { list-style: none; cursor: pointer; }
.tabmore > .tab::-webkit-details-marker { display: none; }
.tabmore > .tab::marker { content: ""; }
/* The FAQ accordion hangs a gold "+" off every `details summary` on the
   marketing pages. This one is a tab, not an accordion row. */
.tabmore > .tab::after, .tabmore[open] > .tab::after { content: none; }
.moresheet {
  position: absolute; bottom: calc(100% + 8px); right: 0;
  width: min(264px, calc(100vw - 28px));
  background: var(--bg-raised); border: 1px solid var(--border);
  border-radius: 14px; padding: 8px;
  box-shadow: 0 18px 44px rgba(12, 59, 54, .22);
}
/* Rows, the account block and the old billing box all used to be described
   here. The rows are `.acct__menu` now, shared with the rail's menu so the
   two surfaces cannot offer a role different things, and the billing box is
   gone -- see `.renew`. */
.page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; margin-bottom: 26px; }
.page-head h1 { font-size: 38px; font-weight: 600; }

table.data { width: 100%; border-collapse: collapse; background: #fff; border-radius: 14px; overflow: hidden; border: 1px solid var(--border); }
table.data th {
  text-align: left; padding: 13px 24px; background: var(--toran-surface);
  font-size: 12px; font-weight: 600; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--text-subtle);
}
table.data td { padding: 15px 24px; border-top: 1px solid var(--border); font-size: 16px; font-weight: 400; vertical-align: middle; }
table.data tr:first-child td { border-top: 0; }

.tag { display: inline-block; padding: 5px 12px; border-radius: 999px; font-size: 10px; font-weight: 600; letter-spacing: 0.14em; }
.tag--yes { background: var(--toran-sand); color: var(--toran-teal-deep); }
.tag--wait { background: var(--toran-amber-soft); color: var(--toran-amber-deep); }
.tag--no { background: var(--toran-surface); color: var(--text-subtle); border: 1px solid var(--border); }

.kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 26px; }
.kpi { background: #fff; border: 1px solid var(--border); border-radius: 14px; padding: 22px; }
.kpi b { display: block; font-family: var(--font-display); font-size: 38px; font-weight: 600; letter-spacing: -0.025em; }
.kpi span { font-size: 12px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-subtle); }

/* The header's one-line status: address, live state, days to the first
   ceremony. Separated by dots rather than stacked, because it is a caption,
   not three facts worth three lines. */
.sub-line { display: flex; flex-wrap: wrap; gap: 0 8px; margin: 6px 0 0;
  font-size: 15px; color: var(--text-subtle); }
.sub-line .dotsep::before { content: "\00b7"; margin-right: 8px; }
.sub-line .is-live { color: var(--brand); font-weight: 500; }
.sub-line .is-draft { color: var(--toran-amber-deep); font-weight: 500; }

.kpi em { display: block; margin-top: 6px; font-size: 14px; font-style: normal;
  color: var(--text-subtle); }
.kpi b.accent { color: var(--brand); }

/* The only red on the screen, and only when money is genuinely due inside
   thirty days. A card that is always red is furniture, and gets read as
   furniture. */
.kpi--due { background: var(--toran-ruby); border-color: var(--toran-ruby); }
.kpi--due span, .kpi--due em { color: rgba(255,255,255,.82); }
.kpi--due b { color: #fff; }

.grid--wide { grid-template-columns: 1.55fr 1fr; }

.card-title { font-size: 22px; font-weight: 600; }
.card-link { font-size: 15px; color: var(--brand); text-decoration: none; }
.card-link:hover { text-decoration: underline; }

/* Ceremony bars. Name, track and count each get a fixed lane so the bars
   start and end on the same two verticals however long the names are. */
.cbars { margin-top: 18px; display: flex; flex-direction: column; gap: 14px; }
.cbar { display: grid; grid-template-columns: 112px 1fr 44px; align-items: center; gap: 14px; }
.cbar__name { font-size: 16px; }
.cbar__track { height: 11px; border-radius: 999px; background: var(--toran-sand); overflow: hidden; }
.cbar__track i { display: block; height: 100%; border-radius: 999px; background: var(--brand); }
.cbar__n { text-align: right; font-size: 16px; font-weight: 600; font-variant-numeric: tabular-nums; }

.feed-row { padding: 12px 0 12px 14px; border-left: 3px solid var(--brand); }
.feed-row + .feed-row { margin-top: 4px; }
.feed-row--warn { border-left-color: var(--toran-amber); }
.feed-text { margin: 0; font-size: 16px; }
.feed-when { margin: 2px 0 0; font-size: 14px; color: var(--text-subtle); }

/* `!important` because /app/rsvps sets grid-template-columns inline, one
   column per ceremony, and inline beats a media query. Four ceremonies gave
   four 72px columns on a 375px phone. One authoritative rule per breakpoint,
   never two in the same block fighting each other — that is the shape of the
   bug this file has already had once. */
@media (max-width: 900px) {
  .kpis { grid-template-columns: repeat(2, 1fr) !important; }
  .grid--wide { grid-template-columns: 1fr; }
}

/* The phone layout, from the A2m artboard. Two small counts sit side by side
   and anything carrying a rupee figure takes the full width — ₹11,83,000 in
   38px Fraunces is about 200px, so in a half-width card it blew the grid out
   past the viewport. The order is the artboard's: what you act on first. */
@media (max-width: 560px) {
  .kpis { grid-template-columns: 1fr 1fr !important; gap: 10px; margin-bottom: 18px; }
  .kpi { min-width: 0; padding: 16px; }
  .kpi--due, .kpi--wide { grid-column: 1 / -1; }
  .kpi--due { order: 1; }
  .kpi--wide { order: 2; }

  /* A2m's card metrics. The desktop card is 22px of padding around a 38px
     number with a caption under it, which on a phone is 175px of card for one
     digit and turns the overview into a column of near-empty boxes. These are
     the artboard's numbers: 16px padding, a 28px figure, a 10px label. The
     whole KPI block drops from about 500px to about 240px, so the ceremony
     card is on screen without scrolling. */
  .kpi span { font-size: 10px; }
  .kpi b { font-size: 28px; line-height: 34px; }
  .kpi em { font-size: 13px; margin-top: 3px; }
  .grid { gap: 12px; }
  .card { padding: 18px; }
  /* A2m sets the card heading in the body face at 16px, not 22px display.
     At this width a 22px Fraunces heading reads as a page title and competes
     with the real one in the header band. */
  .card-title { font-family: var(--font-body); font-size: 16px; font-weight: 600; line-height: 20px; letter-spacing: 0; }
  .card-link { font-size: 14px; }
  /* .spread stacks on a phone so a heading and its paragraph do not fight
     over 375px. A card heading and its "See all" are not that: they are a
     title and a 14px link, they fit on one line, and stacking them spends a
     row on nothing. */
  .card > .spread { flex-direction: row; align-items: center; }
  .feed-row { padding: 10px 0 10px 12px; }
  .feed-text { font-size: 15px; }
  .feed-when { font-size: 13px; }
  .sub-line { font-size: 14px; }

  .cbars { margin-top: 14px; gap: 11px; }
  .cbar { grid-template-columns: 62px 1fr 34px; gap: 10px; }
  .cbar__name, .cbar__n { font-size: 14px; }
  .cbar__track { height: 9px; }
  /* The address is the least useful thing on a phone and the longest. */
  .sub-line .addr { display: none; }
  /* .addr is still the first child while hidden, so the separator has to be
     killed on whatever now leads the line. */
  .sub-line .addr + .dotsep::before { content: none; margin: 0; }
}

.bar { display: flex; height: 11px; border-radius: 999px; overflow: hidden; background: var(--border); }
.bar i { display: block; }
.bar .paid { background: var(--brand); }
.bar .due { background: var(--toran-amber); }

.notice { padding: 15px 18px; border-radius: 8px; background: var(--toran-sand); color: var(--toran-amber-deep); font-size: 14px; }
.notice--bad { background: #FBE9E9; color: var(--toran-ruby); }

/* ---- onboarding ---- */
.ob { max-width: 600px; margin: 56px auto; background: #fff; border: 1px solid var(--border); border-radius: 22px; padding: 36px; }
.ob__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; }
.ticks { display: flex; gap: 6px; align-items: center; }
.ticks i { width: 22px; height: 3px; border-radius: 999px; background: var(--border); }
.ticks i.on { background: var(--brand); }
.ask { padding: 16px 20px; border-radius: 14px; background: var(--toran-surface); font-size: 18px; color: var(--text); margin-bottom: 22px; }

/* ---- tenant site ---- */
.site-hero { min-height: 72vh; display: grid; place-items: center; text-align: center; padding: 80px 24px; background: #fff; }
.site-hero h1 { font-size: clamp(44px, 9vw, 96px); font-weight: 600; }
.site-hero .meta { letter-spacing: 0.14em; text-transform: uppercase; font-size: 14px; color: var(--text-muted); margin-top: 12px; }
.ev { display: grid; grid-template-columns: 1fr 1fr; gap: 36px; align-items: center; padding: 40px 0; border-top: 1px solid var(--border); }
.ev iframe { width: 100%; height: 320px; border: 0; border-radius: 14px; }

@media (max-width: 860px) {
  .band { padding: 52px 0; }
  .wrap { padding: 0 20px; }
  .demo, .showcase, .ev { grid-template-columns: 1fr; gap: 26px; }
  .grid--3, .grid--2, .plans, .plans--two { grid-template-columns: 1fr !important; }
  .hero--split { padding: 40px 0 48px; }
  /* Side-by-side CTAs overflow a 390px screen. Stack and fill. */
  .hero--split .row, .band .row { width: 100%; }
  .hero--split .btn, .band--ground .btn { flex: 1 1 100%; }
  .row.keep-row .btn { flex: 0 0 auto; }
  .review { flex-basis: 300px; }
  .foot__cols { gap: 32px; flex-wrap: wrap; }
  /* ---- the dashboard on a phone, rebuilt against A2m ----
     The sidebar is gone, not reflowed. It used to become a strip that
     scrolled sideways across the top with the first item already cut off
     mid-word, which is not a navigation anyone can use. A2m replaces it with
     a top app bar and a bottom tab bar. */
  .appbar { display: flex; }
  .tabbar { display: flex; }
  .side { display: none; }
  .shell { grid-template-columns: 1fr; min-height: 0; }
  /* A grid item's min-width is auto, so a wide child sizes the whole column
     to its own content and drags the page sideways. The nav strip that first
     did this is gone, but a table or a long title would do it again; the
     column has to be told it may be narrower than what is inside it. */
  .shell > * { min-width: 0; }
  /* The gutter is A2m's 20px, plus whatever the device needs in landscape.
     The bottom pad clears the fixed bar: 48px tab + 12px padding + the home
     indicator, and the bar's own inset is already inside that env() call.
     Measured at 375px: the bar is 61px with no inset, so 84 leaves 22px of
     air under the last card rather than tucking it behind the bar. */
  .main {
    padding: 0 max(20px, env(safe-area-inset-right)) calc(84px + env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left));
  }

  /* A2m's header: a white band under the app bar carrying the name, the
     one-line status and the two actions. It is full-bleed, so it pulls back
     out through .main's gutter rather than asking the routes for different
     markup. */
  .page-head {
    flex-direction: column; align-items: stretch; gap: 14px;
    margin: 0 0 18px; padding: 22px 0 18px;
    margin-left: calc(-1 * max(20px, env(safe-area-inset-left)));
    margin-right: calc(-1 * max(20px, env(safe-area-inset-right)));
    padding-left: max(20px, env(safe-area-inset-left));
    padding-right: max(20px, env(safe-area-inset-right));
    background: var(--bg-raised); border-bottom: 1px solid var(--border);
  }
  .page-head h1 { font-size: 28px; line-height: 34px; }
  /* Two actions, each half the width, each a 44px target. Nothing in the app
     puts more than two in this row. */
  .page-head > .row { width: 100%; gap: 9px; flex-wrap: nowrap; }
  .page-head > .row > .btn { flex: 1 1 0; min-width: 0; }
  .page-head > .btn { width: 100%; }
  .page-head .btn--sm { padding: 13px 14px; font-size: 15px; }

  table.data { display: block; overflow-x: auto; }

  /* Keep the one CTA; drop only the secondary links. A nav with nothing in it
     leaves the phone with no way into the product. */
  .nav__links a:not(.btn) { display: none; }
  .nav__links .navdrop { display: none; }
  .nav__in { padding: 12px 20px; }

  /* .spread is a two-up row by default. On a phone it has to stack, or a
     heading and its paragraph fight over the same 375px. */
  .spread { flex-direction: column; align-items: flex-start; }
  .spread.keep-row { flex-direction: row; align-items: center; }

  /* `.hero--split`, not `.hero`: see the note at the hero block above.
     These two reached every couple's invitation as well, and the
     `!important` meant nothing in tenant.css could have argued with it. */
  .hero--split h1 { font-size: 38px !important; }
  .hero--split p { font-size: 16px; }
  .band h1 { font-size: 34px !important; }
  .band h2 { font-size: 30px !important; }
  .band h3 { font-size: 20px !important; }
  /* Nothing may push the page sideways on a phone. */
  body { overflow-x: hidden; }
  .preview__names { font-size: 32px; }
  .showcase { grid-template-columns: 1fr !important; }
  .demo { grid-template-columns: 1fr !important; }
  .reviews { column-count: 1; }
  .ob { margin: 20px 12px; padding: 22px; border-radius: 14px; }
  .plan .price { font-size: 42px; }
  .reviews { margin-right: -20px; padding-right: 20px; }
}

/* ---- conversion furniture ---- */
.trust {
  display: flex; justify-content: center; gap: 48px; flex-wrap: wrap;
  margin-top: 40px; padding-top: 30px; border-top: 1px solid var(--border);
}
.trust div { text-align: center; }
.trust b {
  display: block; font-family: var(--font-display); font-size: 30px;
  font-weight: 600; letter-spacing: -0.025em; color: var(--brand);
}
.trust span {
  font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-subtle);
}

details summary::-webkit-details-marker { display: none; }
details summary::after {
  content: '+'; float: right; color: var(--toran-amber);
  font-family: var(--font-body); font-weight: 400;
}
details[open] summary::after { content: '−'; }

/* A phone shows one screen at a time, so the CTA has to travel with them.
   Hidden on desktop, where the sticky header already carries one. */
.stickycta { display: none; }
@media (max-width: 860px) {
  .stickycta {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
    display: flex; flex-direction: column; align-items: center; gap: 2px;
    padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
    background: rgba(255,255,255,.96); backdrop-filter: blur(8px);
    border-top: 1px solid var(--border);
  }
  .stickycta .btn { width: 100%; }
  /* Leave room so the bar never covers the footer's last line. */
  .foot { padding-bottom: 120px; }
  .trust { gap: 26px 32px; margin-top: 30px; }
  .trust b { font-size: 24px; }
}

/* An 820px block inside an 1180px wrap needs the auto margin or it reads as
   misaligned against every centred section above it. */
.faq { max-width: 820px; margin: 0 auto; }

/* ---- comparison table ---- */
.vh {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.cmp-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.cmp {
  width: 100%; border-collapse: separate; border-spacing: 0;
  background: var(--toran-white); border: 1px solid var(--border);
  border-radius: 22px; overflow: hidden; min-width: 720px;
}
.cmp th, .cmp td { padding: 13px 20px; text-align: center; vertical-align: middle; }
.cmp thead th {
  font-family: var(--font-display); font-size: 19px; font-weight: 600;
  letter-spacing: -0.025em; color: var(--text);
  background: var(--toran-surface); border-bottom: 1px solid var(--border);
  padding-top: 18px; padding-bottom: 18px;
}
.cmp tbody th {
  text-align: left; font-family: var(--font-body); font-size: 16px;
  font-weight: 400; color: var(--text-muted);
  border-bottom: 1px solid var(--border);
}
.cmp tbody td { border-bottom: 1px solid var(--border); }
.cmp tbody tr:last-child th, .cmp tbody tr:last-child td { border-bottom: 0; }

/* The column we are selling. Gold rails, so it reads as the answer without
   shouting over the rows themselves. */
.cmp__us { background: var(--toran-teal-deep); color: #fff; }
.cmp thead .cmp__us {
  background: var(--toran-teal-deep); color: #fff;
  border-bottom: 1px solid var(--toran-gold);
}
.cmp__logo { display: block; margin: 0 auto 6px; }
.cmp tbody td.cmp__us { border-bottom-color: rgba(255,255,255,.12); }

.ci { display: inline-block; }
.ci--yes { color: var(--brand); }
.cmp__us .ci--yes { color: var(--toran-gold); }
.ci--no { color: var(--toran-ruby); opacity: .72; }
.ci--part { color: var(--toran-amber); }

.cmp__cost th, .cmp__cost td {
  background: var(--toran-surface); padding-top: 18px; padding-bottom: 18px;
}
.cmp__cost td.cmp__us { background: var(--toran-teal); }
.cmp__cost td.cmp__us .muted { color: var(--toran-sand); }
.cmp__cost strong { font-family: var(--font-display); font-size: 21px; font-weight: 600; }

/* A 4-column table cannot be read on a 375px screen, and making it scroll
   sideways just hides half the argument. Below 860px the same markup becomes
   one card per row, with each cell labelled by the column it came from. */
@media (max-width: 860px) {
  .cmp-scroll { overflow: visible; }
  .cmp {
    min-width: 0; width: 100%; border: 0; background: none;
    display: block; border-radius: 0;
  }
  .cmp thead { display: none; }
  .cmp tbody { display: block; }
  .cmp tbody tr {
    display: block; background: var(--toran-white);
    border: 1px solid var(--border); border-radius: 14px;
    padding: 16px; margin-bottom: 12px;
  }
  .cmp tbody th {
    display: block; border: 0; padding: 0 0 12px;
    font-family: var(--font-display); font-size: 18px; font-weight: 600;
    color: var(--text); letter-spacing: -0.025em;
  }
  .cmp tbody td {
    display: flex; align-items: center; justify-content: space-between;
    padding: 9px 0; border: 0; border-top: 1px solid var(--border);
    text-align: left; background: none !important; color: var(--text) !important;
  }
  .cmp tbody td::before {
    content: attr(data-label);
    font-size: 13px; color: var(--text-subtle);
  }
  /* Our column still has to read as ours without a dark fill to carry it. */
  .cmp tbody td.cmp__us::before { color: var(--brand); font-weight: 600; }
  .cmp tbody td.cmp__us .ci--yes { color: var(--brand); }
  .cmp tbody tr:has(td.cmp__us) { border-color: var(--toran-gold); }
  .cmp__cost th, .cmp__cost td { background: none; padding-top: 9px; padding-bottom: 9px; }
  .cmp__cost td { flex-wrap: wrap; }
  .cmp__cost strong { font-size: 16px; }
  .cmp__cost td .tiny { flex: 1 1 100%; text-align: right; }
}

/* ---- auth pages ---- */
.authwrap {
  display: grid; grid-template-columns: 1fr 1fr; min-height: 100vh;
  background: var(--toran-surface);
}
/* Sign-in and sign-up carry the dark panel of selling points beside the card;
   the invitation page and the expired-link page do not, because somebody
   accepting an invitation has already been sold the product by whoever sent
   it. Without a second child the two-column grid left the card pinned to the
   left half of a 1440px screen. One column when it is on its own. */
.authwrap:not(:has(.authside)) { grid-template-columns: 1fr; }
.authcard {
  width: 100%; max-width: 420px; justify-self: center; align-self: center;
  background: var(--toran-white); border: 1px solid var(--border);
  border-radius: 22px; padding: 36px; margin: 40px 24px;
}
.authside {
  background: var(--toran-teal-deep); color: #fff;
  padding: 64px 56px; display: flex; flex-direction: column; justify-content: center;
}

.authsep {
  display: flex; align-items: center; gap: 14px;
  margin: 20px 0; color: var(--text-subtle); font-size: 12px;
  letter-spacing: 0.14em; text-transform: uppercase;
}
.authsep::before, .authsep::after {
  content: ''; flex: 1; height: 1px; background: var(--border);
}

.btn--google { width: 100%; background: #fff; color: var(--text); border-color: var(--border); }
.btn--google:hover { background: var(--toran-surface); color: var(--text); }
.btn--google.is-off { opacity: .55; cursor: not-allowed; }

.pw { position: relative; }
.pw input { padding-right: 68px; }
.pw__toggle {
  position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
  background: none; border: 0; cursor: pointer; padding: 6px 10px;
  font-size: 13px; font-weight: 600; color: var(--brand); border-radius: 6px;
}
.pw__toggle:hover { background: var(--toran-surface); }
.pw__meter { height: 3px; border-radius: 999px; background: var(--border); margin-top: 8px; overflow: hidden; }
.pw__meter i { display: block; height: 100%; width: 0; transition: width .15s ease, background .15s ease; }

.check { display: flex; gap: 10px; align-items: flex-start; cursor: pointer; }
.check input { width: 17px; height: 17px; margin-top: 2px; flex-shrink: 0; accent-color: var(--brand); }

@media (max-width: 860px) {
  .authwrap { grid-template-columns: 1fr; min-height: auto; }
  .authcard { margin: 20px 16px; padding: 26px; border-radius: 14px; }
  /* The value prop goes below the form on a phone; the form is why they came. */
  .authside { padding: 36px 24px; order: 2; }
  .authside h2 { font-size: 26px !important; }
}

/* ---- reorderable ceremony chips ---- */
.chip--drag {
  display: inline-flex; align-items: center; gap: 8px;
  cursor: grab; user-select: none; background: var(--toran-white);
}
.chip--drag:active { cursor: grabbing; }
.chip--drag:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.chip--drag.is-dragging { opacity: .45; }
.chip--drag.is-over { box-shadow: -3px 0 0 var(--toran-amber); }

/* Six dots. The universal "this moves" affordance. */
.grip {
  width: 8px; height: 13px; flex-shrink: 0; opacity: .45;
  background-image: radial-gradient(currentColor 1px, transparent 1px);
  background-size: 4px 4px; background-position: 0 1px;
  color: var(--text-subtle);
}
.chip--drag:hover .grip { opacity: .8; }
.chip__x { cursor: pointer; padding: 0 2px; color: var(--text-subtle); }
.chip__x:hover { color: var(--toran-ruby); }

/* ---- onboarding step 3: one block per ceremony ---- */
.evrow {
  border: 1px solid var(--border); border-radius: 14px;
  padding: 16px 18px 18px; margin: 0 0 12px; min-width: 0;
}
.evrow legend {
  padding: 0 8px; font-family: var(--font-display);
  font-size: 18px; font-weight: 600; letter-spacing: -0.025em;
}
.evrow__grid {
  display: grid; grid-template-columns: 1fr 140px; gap: 12px;
}
/* The venue is the long one, so it gets its own full-width line rather than
   wrapping out of a flex row and reading as an orphan. */
.evrow__venue { grid-column: 1 / -1; }
.evrow input { width: 100%; }

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

/* ---- custom date picker ---- */
.dp { position: relative; }
.dp__native {
  position: absolute; opacity: 0; pointer-events: none;
  width: 0; height: 0; padding: 0; border: 0;
}
.dp__field {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; padding: 14px 16px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--toran-surface);
  font-size: 16px; font-weight: 400; color: var(--text);
  cursor: pointer; text-align: left;
}
.dp__field:hover { border-color: var(--brand); }
.dp__field:focus-visible { outline: none; border: 2px solid var(--brand); padding: 13px 15px; }
.dp__field.is-empty { color: var(--text-subtle); }
.dp__icon {
  width: 16px; height: 16px; flex-shrink: 0; opacity: .6;
  background: currentColor;
  -webkit-mask: var(--cal) center / contain no-repeat;
  mask: var(--cal) center / contain no-repeat;
}
:root {
  --cal: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 3h10a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z' fill='none' stroke='black' stroke-width='1.4'/%3E%3Cpath d='M2 6.5h12M5.5 1.5v3M10.5 1.5v3' stroke='black' stroke-width='1.4' stroke-linecap='round'/%3E%3C/svg%3E");
}

.dp__pop {
  position: absolute; z-index: 70; top: calc(100% + 8px); left: 0;
  width: 280px; padding: 14px; border-radius: 14px;
  background: var(--toran-white); border: 1px solid var(--border);
  box-shadow: 0 14px 40px rgba(12, 59, 54, .16);
}
.dp__pop--up { top: auto; bottom: calc(100% + 8px); }
.dp__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.dp__head strong { font-family: var(--font-display); font-size: 16px; font-weight: 600; }
.dp__nav {
  width: 30px; height: 30px; border-radius: 999px; border: 1px solid var(--border);
  background: none; cursor: pointer; font-size: 17px; line-height: 1; color: var(--text);
}
.dp__nav:hover { background: var(--toran-surface); }
.dp__dow, .dp__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.dp__dow span {
  text-align: center; font-size: 11px; font-weight: 600; letter-spacing: .06em;
  color: var(--text-subtle); padding-bottom: 6px;
}
.dp__day {
  aspect-ratio: 1; border: 0; background: none; border-radius: 8px; cursor: pointer;
  font-size: 14px; color: var(--text);
}
.dp__day:hover { background: var(--toran-sand); }
.dp__day.is-today { box-shadow: inset 0 0 0 1px var(--toran-amber); }
.dp__day.is-on { background: var(--brand); color: #fff; font-weight: 600; }
.dp__pad { aspect-ratio: 1; }
.dp__foot { display: flex; justify-content: space-between; margin-top: 10px;
  padding-top: 10px; border-top: 1px solid var(--border); }
.dp__link { background: none; border: 0; cursor: pointer; font-size: 13px;
  font-weight: 600; color: var(--brand); padding: 4px; }
.dp__link[data-clear] { color: var(--text-subtle); }

@media (max-width: 420px) { .dp__pop { width: calc(100vw - 72px); } }

/* ---- image picker ---- */
.ip {
  border: 1px dashed var(--border); border-radius: 14px;
  background: var(--toran-surface); overflow: hidden; position: relative;
}
.ip.is-over { border-color: var(--brand); background: var(--toran-sand); }
.ip__empty {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 26px 20px; cursor: pointer; color: var(--text-subtle); text-align: center;
}
.ip__empty strong { font-size: 16px; font-weight: 600; color: var(--text); }
.ip__empty:hover { background: var(--toran-sand); }
/* The library is the primary action and gets the only button in the box.
   It used to be a small link under a dashed target that opened the operating
   system's file dialog, which is how someone wanting the photo they already
   uploaded ends up in Finder uploading it again. */
.ip__primary {
  margin-top: 6px; padding: 8px 18px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--brand); background: var(--brand); color: #fff;
  font-size: 13.5px; font-weight: 600;
}
.ip__primary:hover { background: var(--brand-deep); border-color: var(--brand-deep); }
.ip__primary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* Uploading stays one tap away, as the sentence under the button. */
.ip__alt { margin-top: 2px; line-height: 1.5; }
.ip__up-open {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; color: var(--brand); font-weight: 600;
  text-decoration: underline; text-underline-offset: 2px;
}
.ip__preview { margin: 0; }
.ip__preview img {
  width: 100%; max-height: 260px; object-fit: cover; display: block;
  background: var(--toran-surface);
}
.ip__preview figcaption {
  display: flex; gap: 8px; padding: 10px 12px; background: var(--toran-white);
  border-top: 1px solid var(--border);
}
.ip__preview button {
  background: none; border: 0; cursor: pointer; font-size: 13px;
  font-weight: 600; color: var(--brand); padding: 4px 8px; border-radius: 6px;
}
.ip__preview button:hover { background: var(--toran-surface); }
.ip__remove { color: var(--toran-ruby) !important; }
.ip__bar { position: absolute; left: 0; right: 0; bottom: 0; height: 3px; background: var(--border); }
.ip__bar i { display: block; height: 100%; width: 0; background: var(--brand); transition: width .2s ease; }
.ip__err { margin: 0; padding: 10px 12px; color: var(--toran-ruby); background: #FBE9E9; }

.ip__lib-open {
  background: none; border: 0; cursor: pointer; padding: 6px 10px; margin-top: 2px;
  border-radius: 8px; font-size: 13px; font-weight: 600; color: var(--brand);
  text-decoration: underline; text-underline-offset: 3px;
}
.ip__lib-open:hover { background: var(--toran-white); text-decoration: none; }

/* The multi-select field: chosen photos in pick order, numbered, with a "+"
   at the end. The number is the point — a gallery is an ordered thing, and
   the person has to be able to see the order they built. */
.ip__strip {
  display: flex; flex-wrap: wrap; gap: 8px; padding: 12px;
  background: var(--toran-white);
}
.ip__cell {
  position: relative; width: 84px; height: 84px; border-radius: 10px;
  overflow: hidden; border: 1px solid var(--border); background: var(--toran-surface);
}
.ip__cell img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ip__cell > i {
  position: absolute; left: 5px; bottom: 5px; min-width: 19px; height: 19px;
  padding: 0 5px; border-radius: 999px; background: var(--brand); color: #fff;
  font-style: normal; font-size: 11px; font-weight: 600; line-height: 19px;
  text-align: center;
}
.ip__cell-x {
  position: absolute; top: 3px; right: 3px; width: 22px; height: 22px;
  border: 0; border-radius: 999px; cursor: pointer; padding: 0;
  background: rgba(12, 59, 54, .72); color: #fff; font-size: 15px; line-height: 22px;
}
.ip__cell-x:hover { background: var(--toran-ruby); }
.ip__cell--add {
  display: grid; place-items: center; cursor: pointer; padding: 0;
  border-style: dashed; color: var(--brand); font-size: 24px;
}
.ip__cell--add:hover { background: var(--toran-sand); }

/* ---- the media picker overlay ---- */

/* One overlay serves the inline fields and, per ADR 0008, the editor's card
   and gallery blocks. It is a sheet on a phone and a dialog on a laptop,
   which is the same markup and one media block: the inline panel this
   replaced could not be made usable at 375px inside a dashed box. */
.pk { position: fixed; inset: 0; z-index: 90; display: flex; }
.pk[hidden] { display: none; }
.pk__scrim { position: absolute; inset: 0; background: rgba(12, 59, 54, .42); }
/* Stop the page behind from scrolling under the sheet. */
html.pk-open, html.pk-open body { overflow: hidden; }

.pk__panel {
  position: relative; margin: auto; display: flex; flex-direction: column;
  width: min(720px, calc(100vw - 40px)); max-height: min(720px, calc(100vh - 56px));
  background: var(--bg-raised); border-radius: 18px;
  box-shadow: 0 30px 80px rgba(12, 59, 54, .32);
}
.pk__panel:focus { outline: none; }
.pk__head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  padding: 20px 22px 14px; border-bottom: 1px solid var(--border);
}
.pk__title { font-size: 24px; font-weight: 600; }
.pk__usage { margin: 3px 0 0; }
.pk__x {
  flex: 0 0 auto; width: 36px; height: 36px; margin: -4px -6px 0 0;
  display: grid; place-items: center; cursor: pointer;
  background: none; border: 0; border-radius: 999px; color: var(--text-subtle);
}
.pk__x:hover { background: var(--toran-surface); color: var(--text); }
.pk__body { overflow-y: auto; padding: 16px 22px; -webkit-overflow-scrolling: touch; }
.pk__say { margin: 8px 0; }
.pk__grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 12px;
}
.pk__item {
  position: relative; display: flex; flex-direction: column; gap: 4px;
  padding: 0; cursor: pointer; text-align: left; overflow: hidden;
  background: none; border: 2px solid var(--border); border-radius: 12px;
}
.pk__item:hover { border-color: var(--brand); }
.pk__item:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.pk__item.is-on { border-color: var(--brand); background: var(--toran-sand); }
.pk__item img {
  width: 100%; aspect-ratio: 1; object-fit: cover; display: block;
  background: var(--toran-surface);
}
.pk__item span {
  padding: 0 7px 7px; font-size: 11px; line-height: 14px; color: var(--text-subtle);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Empty until something is picked, so an unselected tile carries no chrome. */
.pk__tick {
  position: absolute; top: 7px; right: 7px; min-width: 24px; height: 24px;
  padding: 0 6px; border-radius: 999px; font-style: normal;
  font-size: 12px; font-weight: 600; line-height: 24px; text-align: center;
}
.pk__item.is-on .pk__tick { background: var(--brand); color: #fff; }
.pk__foot {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 14px 22px; border-top: 1px solid var(--border);
}
.pk__use[disabled] { opacity: .45; pointer-events: none; }
.pk__bar { position: relative; }
.pk__err { border-radius: 0; }

@media (max-width: 860px) {
  /* A sheet, not a dialog: full width, pinned to the bottom, and tall enough
     that the grid is the thing on screen rather than the furniture. The top
     inset keeps it clear of the notch, the bottom one of the home indicator. */
  .pk__panel {
    width: 100%; margin: auto 0 0;
    max-height: calc(100vh - 44px - env(safe-area-inset-top));
    border-radius: 18px 18px 0 0;
  }
  .pk__head { padding: 16px 18px 12px; }
  .pk__title { font-size: 20px; }
  .pk__body { padding: 14px 18px; }
  .pk__grid { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 10px; }
  .pk__foot {
    padding: 12px 18px calc(12px + env(safe-area-inset-bottom));
    flex-direction: row;
  }
  .pk__foot .btn { flex: 1 1 0; }
}

/* ---- media library (/app/media) ---- */

/* `position: relative` so the shared `.ip__bar` that imagepicker.js appends
   on submit pins to the bottom edge of this form rather than to the page. */
.ml-upload {
  position: relative; overflow: hidden;
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  background: #fff; border: 1px solid var(--border); border-radius: 14px;
  padding: 18px 20px; margin-bottom: 22px;
}
.ml-upload__say { display: flex; flex-direction: column; gap: 2px; margin-right: auto; }
.ml-upload__say strong { font-size: 16px; font-weight: 600; }
.ml-upload__file { flex: 0 1 280px; min-width: 0; font-size: 14px; }
.ml-upload__file::file-selector-button {
  margin-right: 10px; padding: 9px 14px; border-radius: 8px; cursor: pointer;
  border: 1px solid var(--border); background: var(--toran-surface);
  font-family: var(--font-body); font-size: 14px; font-weight: 600; color: var(--brand);
}
.ml-upload__file::file-selector-button:hover { background: var(--toran-sand); }
.ml-upload .ip__err { margin-top: 12px; flex: 1 1 100%; border-radius: 8px; }

/* `auto-fill` with a floor, not a fixed count: the grid is the one page whose
   right answer is "as many as fit", and it has to survive the sidebar
   disappearing at 860px without a second rule. */
.ml-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(208px, 1fr));
  gap: 18px; align-items: start;
}
.ml-item {
  margin: 0; background: #fff; border: 1px solid var(--border);
  border-radius: 14px; overflow: hidden;
}
.ml-item__shot { display: block; }
.ml-item__shot img {
  width: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block;
  background: var(--toran-surface);
}
.ml-item__body { display: flex; flex-direction: column; gap: 5px; padding: 13px 14px 14px; }
.ml-item__name {
  font-size: 15px; font-weight: 600; line-height: 19px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ml-item__edit > summary {
  list-style: none; cursor: pointer; margin-top: 4px;
  font-size: 13px; font-weight: 600; color: var(--brand);
}
.ml-item__edit > summary::-webkit-details-marker { display: none; }
.ml-item__edit .field label { font-size: 12px; }
.ml-item__edit .field input { padding: 10px 12px; font-size: 15px; }
.ml-item__del {
  background: none; border: 0; padding: 4px 0; cursor: pointer;
  font-family: var(--font-body); font-size: 13px; font-weight: 600; color: var(--toran-ruby);
}
.ml-item__del:hover { text-decoration: underline; }

@media (max-width: 860px) {
  /* Two across at 375px. A one-column library of 4:3 tiles is 280px of
     scrolling per photograph, which turns twelve photos into a page nobody
     reaches the bottom of. */
  .ml-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
  .ml-item__body { padding: 10px 11px 12px; gap: 4px; }
  .ml-item__name { font-size: 14px; }
  /* The uploader stacks: a file input and a button side by side at 375px put
     the native "Choose file" control half off the screen. */
  .ml-upload { flex-direction: column; align-items: stretch; gap: 12px; padding: 16px; }
  .ml-upload__say { margin-right: 0; }
  .ml-upload__file { flex: 1 1 auto; }
  .ml-upload .btn { width: 100%; }
}

.site-hero__photo {
  width: 190px; height: 190px; border-radius: 999px; object-fit: cover;
  margin: 0 auto 8px; box-shadow: 0 10px 40px rgba(12, 59, 54, .18);
}
@media (max-width: 560px) { .site-hero__photo { width: 140px; height: 140px; } }

/* ---- the waiting card ---- */
.loadwrap {
  position: fixed; inset: 0; z-index: 90;
  display: grid; place-items: center;
  background: var(--toran-surface);
}
.loadcard {
  background: var(--toran-white); border-radius: 22px;
  padding: 40px 48px; text-align: center;
  box-shadow: 0 18px 50px rgba(12, 59, 54, .10);
}
.loadring {
  width: 96px; height: 96px; margin: 0 auto 22px;
  display: grid; place-items: center; position: relative;
}
/* A gold arc sweeping over a faint track.
   This used to be the background of `.loadring` itself, masked to a ring —
   and a mask applies to an element's children too, so the hole punched in
   the middle took the mark with it. The spinner has been an empty ring
   since it was written; nobody saw it, because there was nothing to see.
   The arc gets its own layer so the mask stops at its own edges. */
.loadring::before {
  content: ''; position: absolute; inset: 0; border-radius: 999px;
  background:
    conic-gradient(var(--toran-gold) 0deg 90deg, var(--toran-sand) 90deg 360deg);
  -webkit-mask: radial-gradient(closest-side, transparent 85%, #000 86%);
  mask: radial-gradient(closest-side, transparent 85%, #000 86%);
  animation: loadspin 1.1s linear infinite;
}
@keyframes loadspin { to { transform: rotate(360deg); } }

.loadtext {
  font-family: var(--font-display); font-weight: 500; font-size: 22px;
  letter-spacing: var(--tracking-tight); color: var(--text);
}
.loaddots i {
  font-style: normal; opacity: .25;
  animation: loaddot 1.2s infinite;
}
.loaddots i:nth-child(2) { animation-delay: .2s; }
.loaddots i:nth-child(3) { animation-delay: .4s; }
@keyframes loaddot { 0%, 60%, 100% { opacity: .25; } 30% { opacity: 1; } }

/* Respect the system setting. A spinning ring is exactly what this is for. */
@media (prefers-reduced-motion: reduce) {
  .loadring::before, .loaddots i { animation: none; }
}

/* ---- sample site banner ---- */
.samplebar {
  position: sticky; top: 0; z-index: 80;
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 10px 20px; background: var(--toran-teal-deep); color: #fff;
  font-family: var(--font-body); font-size: 14px;
}
.samplebar__home {
  display: inline-flex; align-items: center; gap: 6px;
  color: #fff; text-decoration: none; font-family: var(--font-display);
  font-size: 17px; letter-spacing: .02em; white-space: nowrap;
}
.samplebar__home:hover { color: var(--toran-gold); }
.samplebar__arrow { font-size: 14px; opacity: .65; }
.samplebar__home:hover .samplebar__arrow { opacity: 1; }
.samplebar__tag {
  padding: 3px 10px; border-radius: 999px; background: var(--toran-gold);
  color: var(--toran-teal-deep); font-size: 11px; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase;
}
.samplebar__text { color: var(--toran-sand); flex: 1 1 240px; }
.samplebar__links { display: flex; align-items: center; gap: 14px; }
.samplebar__links a { color: var(--toran-sand); text-decoration: none; font-size: 14px; }
.samplebar__links a:hover { color: #fff; }
.samplebar__cta {
  padding: 8px 16px; border-radius: 999px;
  background: var(--toran-gold); color: var(--toran-teal-deep) !important;
  font-weight: 600;
}
@media (max-width: 560px) {
  .samplebar { font-size: 13px; padding: 9px 16px; gap: 10px; }
  .samplebar__text { flex-basis: 100%; order: 3; font-size: 12px; }
  .samplebar__home { font-size: 15px; }
}

/* ---- hero bullets, Zola's pattern: three objections, not a subheadline ---- */
.herolist {
  list-style: none; padding: 0; margin: 22px auto 30px; max-width: 560px;
  display: flex; flex-direction: column; gap: 12px; text-align: left;
}
.herolist li { display: flex; gap: 11px; align-items: flex-start; font-size: 17px; color: var(--text-muted); }
.herolist .tick { margin-top: 4px; }
.hero--split h1 em { font-style: normal; font-weight: 300; color: var(--brand); }

.chip--occ {
  display: inline-flex; align-items: center; gap: 7px;
  color: var(--text-muted); cursor: default; background: var(--toran-white);
}
.chip--occ svg { color: var(--toran-amber); }

@media (max-width: 860px) {
  .herolist { margin: 18px 0 24px; }
  .herolist li { font-size: 15px; }
}

/* ---- sample site cards: browser chrome so they read as sites, not cards ---- */
.samplecard {
  display: flex; flex-direction: column; text-decoration: none; color: inherit;
  background: var(--toran-white); border: 1px solid var(--border);
  border-radius: 14px; overflow: hidden; transition: transform .12s ease;
}
.samplecard:hover { transform: translateY(-3px); border-color: var(--brand); }
.samplecard__chrome {
  display: flex; align-items: center; gap: 5px; padding: 9px 12px;
  background: var(--toran-surface); border-bottom: 1px solid var(--border);
}
.samplecard__chrome i {
  width: 7px; height: 7px; border-radius: 999px; background: var(--border);
}
.samplecard__chrome em {
  font-style: normal; margin-left: 8px; font-size: 11px; color: var(--text-subtle);
}
.samplecard__page {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 44px 18px; text-align: center; min-height: 150px;
}
.samplecard__foot {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 16px 18px;
}
.samplecard__foot strong { display: block; font-size: 16px; }
.samplecard__foot em {
  font-style: normal; display: block; font-size: 13px;
  color: var(--text-subtle); margin-top: 3px;
}
.samplecard__go { font-size: 14px; font-weight: 600; color: var(--brand); flex-shrink: 0; }
.plan__two { columns: 2; column-gap: 26px; }
.plan__two li { break-inside: avoid; margin-bottom: 12px; }
@media (max-width: 860px) { .plan__two { columns: 1; } }

/* ---- split hero: the copy, and the moment of sharing ---- */
.hero--split { text-align: left; }
.hero__grid {
  display: grid; grid-template-columns: 1fr 360px;
  gap: 64px; align-items: center;
}
.hero--split h1 { font-size: clamp(38px, 4.6vw, 62px); margin: 14px 0 0; }
.hero--split .herolist { margin-left: 0; }
.hero--split .row { justify-content: flex-start; }
.hero--split p { margin-left: 0; }

/* The Hinglish line. Only one competitor in fourteen does this, and it is
   the strongest cultural signal available for the price of one line. */
.hinglish {
  font-family: var(--font-display); font-style: italic;
  font-size: 19px; color: var(--toran-amber-deep); margin: 0;
}

/* ---- the WhatsApp thread ----
   The survey's clearest finding: every competitor shows you the card. The
   one that shows the card *arriving* reframes the purchase from "a website"
   to "the thing that ends the nagging". It animates, because the point is
   the moment, and a still image is not a moment. */
.wa {
  border-radius: 26px; overflow: hidden; background: #ECE5DD;
  box-shadow: 0 24px 60px rgba(12, 59, 54, .18);
  border: 9px solid #10211C;
}
.wa__bar {
  display: flex; align-items: center; gap: 9px;
  padding: 10px 12px; background: #075E54; color: #fff;
}
.wa__avatar {
  width: 30px; height: 30px; border-radius: 999px; flex-shrink: 0;
  background: #25D366; color: #0B3D2E; display: grid; place-items: center;
}
.wa__who { flex: 1; min-width: 0; }
.wa__who strong { display: block; font-size: 13.5px; font-weight: 600; }
.wa__who em {
  font-style: normal; display: block; font-size: 10.5px; opacity: .78;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.wa__icons { display: flex; gap: 12px; opacity: .85; flex-shrink: 0; }

.wa__body {
  padding: 10px 10px 14px; display: flex; flex-direction: column; gap: 6px;
  background-color: #ECE5DD;
  background-image: radial-gradient(rgba(0,0,0,.04) 1px, transparent 1px);
  background-size: 15px 15px;
  min-height: 390px;
}
.wa__day {
  align-self: center; background: #fff; border-radius: 6px;
  padding: 3px 10px; font-size: 10px; color: #667; margin-bottom: 2px;
  box-shadow: 0 1px 1px rgba(0,0,0,.06);
}

/* The bubble is sized by its text, and the timestamp floats so it sits at
   the end of the last line the way WhatsApp's does. Absolutely positioning
   it meant every line reserved 56px of right padding, which forced short
   messages to wrap for no reason. */
.wa__msg {
  align-self: flex-start; width: fit-content; max-width: 84%;
  background: #fff; border-radius: 9px 9px 9px 2px;
  padding: 6px 10px 6px 10px; font-size: 13px; line-height: 1.5; color: #111;
  box-shadow: 0 1px 1px rgba(0,0,0,.09);
}
.wa__msg::after { content: ''; display: block; clear: both; }
.wa__msg--me {
  align-self: flex-end; background: #DCF8C6;
  border-radius: 9px 9px 2px 9px;
}
.wa__time {
  float: right; margin: 7px 0 -2px 10px;
  font-size: 9.5px; color: #8a9499; white-space: nowrap;
}
.wa__time--me { color: #6a9c7a; }

/* the link preview card */
.wa__link { padding: 5px 5px 4px; max-width: 78%; width: auto; }
.wa__preview {
  display: block; background: #fff; border-radius: 6px;
  padding: 16px 12px 14px; text-align: center;
}
.wa__preview-top { display: block; margin-bottom: 7px; }
.wa__preview strong {
  display: block; font-family: var(--font-display);
  font-size: 21px; font-weight: 600; color: var(--text); letter-spacing: -0.02em;
}
.wa__preview em {
  font-style: normal; display: block; font-size: 9px;
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-subtle);
  margin-top: 4px;
}
.wa__count {
  display: flex; justify-content: center; gap: 4px; align-items: baseline;
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border);
}
.wa__count b {
  font-family: var(--font-display); font-size: 17px;
  font-weight: 600; color: var(--brand);
}
.wa__count s {
  text-decoration: none; font-size: 8px; color: var(--text-subtle);
  text-transform: uppercase; letter-spacing: .08em; margin-right: 6px;
}
.wa__url { display: block; padding: 5px 5px 0; font-size: 10.5px; color: #3f8f6d; }
.wa__link .wa__time { margin: 2px 5px 2px 10px; }

/* typing indicator */
.wa__typing {
  align-self: flex-start; background: #fff; border-radius: 9px 9px 9px 2px;
  padding: 10px 12px; display: flex; gap: 4px;
  box-shadow: 0 1px 1px rgba(0,0,0,.09);
}
.wa__typing i {
  width: 6px; height: 6px; border-radius: 999px; background: #b6bec2;
  animation: waType 1.3s infinite;
}
.wa__typing i:nth-child(2) { animation-delay: .18s; }
.wa__typing i:nth-child(3) { animation-delay: .36s; }
@keyframes waType {
  0%, 60%, 100% { opacity: .35; transform: translateY(0); }
  30% { opacity: 1; transform: translateY(-3px); }
}

.wa__rsvp {
  align-self: center; display: flex; align-items: center; gap: 7px;
  margin-top: 6px; padding: 5px 12px; border-radius: 999px;
  background: rgba(7, 94, 84, .1); font-size: 10.5px; color: #075E54;
}
.wa__rsvp-dot {
  width: 6px; height: 6px; border-radius: 999px; background: #25D366;
  animation: waPulse 1.8s infinite;
}
@keyframes waPulse { 0%,100% { opacity: 1 } 50% { opacity: .3 } }

/* Everything after the first message starts hidden and is revealed in
   sequence by landing.js. With JS off, or reduced motion, it is all just
   visible, which is the correct fallback rather than an empty box. */
.wa.is-animating [data-step] { opacity: 0; transform: translateY(6px); }
.wa.is-animating [data-step].is-in {
  opacity: 1; transform: none;
  transition: opacity .28s ease, transform .28s ease;
}
.wa.is-animating .wa__typing { display: none; }
.wa.is-animating .wa__typing.is-in { display: flex; }

@media (prefers-reduced-motion: reduce) {
  .wa.is-animating [data-step] { opacity: 1; transform: none; }
  .wa__typing i, .wa__rsvp-dot { animation: none; }
}

@media (max-width: 980px) {
  .hero__grid { grid-template-columns: 1fr; gap: 36px; }
  .hero--split { text-align: center; }
  .hero--split .herolist { margin-left: auto; margin-right: auto; }
  .hero--split .row { justify-content: center; }
  .hero__phone { max-width: 330px; margin: 0 auto; }
}

/* ---- cost per ceremony ---- */
.cerem { background: #fff; border: 1px solid var(--border); border-radius: 14px; overflow: hidden; }
.cerem__row {
  display: grid; grid-template-columns: 1.2fr 0.8fr 1.4fr;
  gap: 18px; align-items: center;
  padding: 15px 22px; border-bottom: 1px solid var(--border);
}
.cerem__row:last-child { border-bottom: 0; }
.cerem__name strong { display: block; font-size: 17px; font-family: var(--font-display); font-weight: 600; }
.cerem__name span { display: block; margin-top: 2px; }
.cerem__amt { text-align: right; }
.cerem__amt strong { font-size: 17px; }
.cerem__amt span { display: block; margin-top: 2px; }
.cerem__bar .bar { margin-bottom: 6px; }

@media (max-width: 720px) {
  .cerem__row { grid-template-columns: 1fr auto; gap: 8px 14px; padding: 14px 16px; }
  .cerem__bar { grid-column: 1 / -1; }
}

/* ---- savings slider ---- */
.slider {
  width: 100%; -webkit-appearance: none; appearance: none;
  height: 4px; border-radius: 999px; background: var(--toran-teal);
  outline: none; margin: 14px 0 10px;
}
.slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 24px; height: 24px; border-radius: 999px;
  background: var(--toran-gold); cursor: grab; border: 0;
}
.slider::-moz-range-thumb {
  width: 24px; height: 24px; border-radius: 999px;
  background: var(--toran-gold); cursor: grab; border: 0;
}
.slider:active::-webkit-slider-thumb { cursor: grabbing; }

/* ---- trust strip ---- */
.trustrow {
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 20px;
}
.trustrow__item { display: flex; gap: 10px; align-items: flex-start; }
.trustrow__item strong { display: block; font-size: 15px; }
.trustrow__item em {
  font-style: normal; display: block; font-size: 13px;
  color: var(--text-subtle); margin-top: 2px;
}
@media (max-width: 980px) {
  .trustrow { grid-template-columns: 1fr 1fr; gap: 16px; }
}
@media (max-width: 560px) { .trustrow { grid-template-columns: 1fr; } }

/* The month-and-year jump. The title is a button now, so it needs to stop
   looking like a button and start looking like the heading it replaced. */
.dp__title {
  border: 0; background: none; padding: 4px 8px; border-radius: 6px;
  font-family: var(--font-display); font-size: 17px; font-weight: 600;
  color: var(--text); cursor: pointer;
}
.dp__title:hover { background: var(--toran-sand); }
.dp__title:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.dp__months {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; padding: 6px 0 2px;
}
.dp__month {
  padding: 12px 0; border: 0; border-radius: 8px; background: none;
  font-size: 14px; color: var(--text); cursor: pointer;
}
.dp__month:hover { background: var(--toran-sand); }
.dp__month.is-today { box-shadow: inset 0 0 0 1px var(--border); }
.dp__month.is-on { background: var(--brand); color: #fff; }
.dp__month:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }


/* ================================================================
   Desktop lives at the bottom of this file on purpose.

   The first attempt put it beside the other layout rules, around line 300,
   and only half of it worked: `max-width` applied because nothing else sets
   it, while every font-size was silently overridden by the base rules a
   hundred lines further down. Same specificity, later wins, and a media
   query buys no precedence at all.
   ================================================================ */

/* ---- the desktop pass ----

   The phone layout was rebuilt against A2m and came out well; the desktop one
   had not been touched since A2 and read, correctly, as the phone's type
   scale stretched across 1192 pixels. The h1 and the KPI numbers were 38px on
   both, which fills a card at 375 and floats in a 262px box at 1440, and the
   whole page fitted one screen with dead air under it.

   Three things, none of them structural:

   1. A ceiling on the measure. Past 1472 the page gains margin, not width;
      without it a 27 inch monitor gets a 1700px line of body copy.
   2. Display sizes that belong to the width they are shown at.
   3. Weight where the mobile layout already puts it. On a phone the red Due
      card is full width and dominates; on desktop it was one of four equal
      boxes, so the most urgent number on the page read as the least.
*/
@media (min-width: 1100px) {
  .main {
    width: 100%; max-width: 1472px; margin-inline: auto;
    padding: 56px clamp(48px, 4vw, 72px);
  }

  .page-head { margin-bottom: 34px; }
  .page-head h1 { font-size: 46px; }
  .sub-line { font-size: 16px; }

  /* Not four equal columns. The two counts are small numbers that need little
     room; money needs more, and the money that is actually due needs most. */
  .kpis { grid-template-columns: 1fr 1fr 1.2fr 1.4fr; gap: 20px; margin-bottom: 34px; }
  .kpi { padding: 26px 28px; }
  .kpi b { font-size: 44px; }
  .kpi em { font-size: 15px; margin-top: 8px; }
  .kpi--due b { font-size: 52px; }

  .grid--wide { gap: 24px; }
  .grid--wide > .card { padding: 30px 32px; }
  .card-title { font-size: 26px; }

  /* The panels were ending well short of the fold with nothing under them.
     Letting the rows breathe fills the page honestly, rather than padding it. */
  .cbars { margin-top: 24px; gap: 18px; }
  .cbar { grid-template-columns: 136px 1fr 52px; gap: 18px; }
  .cbar__name { font-size: 17px; }
  .cbar__track { height: 13px; }
  .cbar__n { font-size: 18px; }

  .feed-row { padding: 15px 0 15px 16px; }
  .feed-row + .feed-row { margin-top: 6px; }
}

/* A sidebar sized for a laptop looks mean beside 1472px of content. */
@media (min-width: 1440px) {
  .shell { grid-template-columns: 276px 1fr; }
  .side { padding: 32px 20px; }
}

/* ---- the design editor ----

   Blocks on the left, the real site on the right. The preview is an iframe of
   /s/:slug?preview=1, not a drawing of it, so it cannot drift from what a
   guest sees. Sticky because the block list is short and the page it previews
   is long; scrolling the list should not scroll the thing you are judging. */
.editor { display: grid; grid-template-columns: minmax(320px, 420px) 1fr; gap: 24px; align-items: start; }
.editor__panel { background: #fff; border: 1px solid var(--border); border-radius: 14px; padding: 20px; }
.editor__preview {
  position: sticky; top: 24px; border: 1px solid var(--border); border-radius: 14px;
  overflow: hidden; background: #fff; height: calc(100vh - 140px); min-height: 520px;
}
.editor__preview iframe { width: 100%; height: 100%; border: 0; display: block; }

.blks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.blk {
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--toran-surface);
}
/* The row is a header plus a panel now, so the three-column grid that used
   to sit on .blk moved down to the header. */
.blk__head {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px;
  padding: 12px 14px;
}
.blk__open {
  display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 10px;
  background: none; border: 0; padding: 0; margin: 0; font: inherit; color: inherit;
  text-align: left; cursor: pointer; min-width: 0;
}
.blk__open:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; border-radius: 6px; }
.blk__chev {
  width: 8px; height: 8px; flex: none;
  border-right: 1.6px solid var(--text-subtle); border-bottom: 1.6px solid var(--text-subtle);
  transform: rotate(45deg) translate(-2px, -2px); transition: transform .14s ease;
}
.blk__open[aria-expanded="true"] .blk__chev { transform: rotate(-135deg) translate(-2px, -2px); }
.blk.is-dragging { opacity: .4; }
.blk[data-on="0"] { background: transparent; }
.blk[data-on="0"] .blk__body { opacity: .5; }
.blk__grip { cursor: grab; color: var(--text-subtle); font-size: 15px; line-height: 1; user-select: none; }
.blk:active .blk__grip { cursor: grabbing; }
.blk__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.blk__body strong { font-size: 15px; font-weight: 600; }
.blk__what { font-size: 12.5px; color: var(--text-subtle); line-height: 1.45; }
/* Said out loud, because the renderer has always dropped an empty section and
   an editor that does the same silently just looks broken. */
.blk__empty { font-size: 12px; color: var(--toran-amber-deep); margin-top: 2px; }

.blk__acts { display: flex; align-items: center; gap: 4px; }
.blk__mv {
  width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--border);
  background: #fff; cursor: pointer; font-size: 13px; line-height: 1; color: var(--text);
}
.blk__mv:hover:not(:disabled) { border-color: var(--brand); color: var(--brand); }
.blk__mv:disabled { opacity: .3; cursor: default; }

/* A switch, built from a button so it is a real control: focusable, operable
   from a keyboard, and announced by its role and aria-checked. */
.blk__on {
  width: 44px; height: 26px; margin-left: 4px; padding: 3px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--toran-hairline); cursor: pointer;
  display: flex; align-items: center; transition: background .14s ease;
}
.blk__on i { width: 18px; height: 18px; border-radius: 999px; background: #fff;
  display: block; transition: transform .14s ease; box-shadow: 0 1px 2px rgba(0,0,0,.2); }
.blk__on[aria-checked="true"] { background: var(--brand); border-color: var(--brand); }
.blk__on[aria-checked="true"] i { transform: translateX(18px); }
.blk__on:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* One column on anything narrow. A side-by-side editor needs a side. */
@media (max-width: 1000px) {
  .editor { grid-template-columns: 1fr; }
  .editor__preview { position: static; height: 70vh; }
}
@media (prefers-reduced-motion: reduce) {
  .blk__on, .blk__on i { transition: none; }
}

/* ---- the card's settings, above the block list ---- */
.cardset { border-bottom: 1px solid var(--border); padding-bottom: 22px; }
.cardset__h { font-size: 18px; font-weight: 600; margin: 0 0 2px; }
.cardset__label {
  display: block; margin: 16px 0 7px;
  font-size: 11px; font-weight: 600; letter-spacing: .13em;
  text-transform: uppercase; color: var(--text-subtle);
}
.cardset__in {
  width: 100%; padding: 10px 12px; border: 1px solid var(--border);
  border-radius: 10px; background: #fff; font-size: 15px;
}
.cardset__in:focus { outline: none; border-color: var(--brand); }
.cardset__pick { margin-bottom: 2px; }
/* A pick-one row. aria-pressed carries the state, so the styling follows the
   thing a screen reader is already reading rather than a parallel class. */
.chips--pick .chip { cursor: pointer; font-size: 13.5px; padding: 7px 14px; }
.chips--pick .chip[aria-pressed="true"] { background: var(--brand); color: #fff; border-color: var(--brand); }

/* ---- a palette of their own ----
   Two swatches, each with the hex beside it. The swatch is what a phone can
   actually use and the hex is what a stationer hands you on a piece of paper;
   they are one field with two faces, kept in step by editor.js. */
.cardset__pal {
  display: grid; gap: 12px; margin: 2px 0 16px; padding: 14px;
  border: 1px solid var(--border); border-radius: 12px; background: var(--toran-surface);
}
.cardset__pal[hidden] { display: none; }
.pal { display: grid; grid-template-columns: auto 1fr; gap: 12px; align-items: start; }
/* A colour input draws its own chrome in every browser and none of it matches
   anything here, so the swatch is the control with the chrome clipped off. */
.pal__dab {
  width: 46px; height: 46px; padding: 0; border: 1px solid var(--border);
  border-radius: 10px; background: none; cursor: pointer; flex: none;
  -webkit-appearance: none; appearance: none;
}
.pal__dab::-webkit-color-swatch-wrapper { padding: 0; }
.pal__dab::-webkit-color-swatch { border: 0; border-radius: 9px; }
.pal__dab::-moz-color-swatch { border: 0; border-radius: 9px; }
.pal__dab:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.pal__body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.pal__l {
  font-size: 11px; font-weight: 600; letter-spacing: .13em;
  text-transform: uppercase; color: var(--text-subtle);
}
.pal__hex {
  width: 9.5em; padding: 6px 9px; border: 1px solid var(--border); border-radius: 8px;
  background: #fff; font-size: 13.5px; font-variant-numeric: tabular-nums;
  text-transform: uppercase;
}
.pal__hex:focus { outline: none; border-color: var(--brand); }

/* A second row of chips under the one that reveals it: the gradient styles
   under Background. Indented and quieter, so it reads as "inside Gradient"
   rather than as a fourth background. */
.chips--sub {
  margin-top: 8px; padding-left: 12px;
  border-left: 2px solid var(--toran-hairline);
}
.chips--sub .chip { font-size: 12.5px; padding: 6px 12px; }
#cardGradient[hidden] { display: none; }

/* The header's link labels. The same .pf rows a block panel uses, so there is
   one field style in this editor rather than two. */
.navlabels { display: flex; flex-direction: column; gap: 8px; }

/* ---- preview: desktop or phone ----
   The frame is narrowed, never scaled. A scaled-down desktop render would
   show the desktop layout at phone size, which is a convincing lie about
   what a guest sees; at 390px the site's own media queries fire. */
.editor__device {
  display: flex; gap: 6px; align-items: center; padding: 10px 12px;
  border-bottom: 1px solid var(--border); background: var(--toran-surface);
}
.editor__device .chip { cursor: pointer; font-size: 13px; padding: 6px 14px; }
.editor__device .chip[aria-pressed="true"] { background: var(--brand); color: #fff; border-color: var(--brand); }

/* Draft or published, said above the frame instead of inside it. The document
   used to carry this as a sticky bar, which in here covered the top of the
   hero — the thing the preview exists to show. */
.editor__state {
  margin: 0 0 0 auto; display: flex; align-items: center; gap: 7px;
  font-size: 12.5px; color: var(--text-subtle); min-width: 0;
}
.editor__state a { color: var(--brand); font-weight: 600; white-space: nowrap; }
.editor__dot {
  width: 7px; height: 7px; border-radius: 999px; flex: none;
  background: var(--warning); box-shadow: 0 0 0 3px var(--warning-tint);
}
.editor__dot.is-live { background: var(--brand); box-shadow: 0 0 0 3px var(--brand-tint); }
/* At phone width the bar would wrap the buttons; the note drops under them. */
@media (max-width: 560px) {
  .editor__device { flex-wrap: wrap; }
  .editor__state { margin-left: 0; width: 100%; }
}
.editor__stage { height: calc(100% - 45px); display: flex; justify-content: center; background: var(--toran-surface); }
.editor__stage iframe { width: 100%; height: 100%; border: 0; display: block; background: #fff; }
.editor__preview.is-phone .editor__stage iframe {
  width: 390px; max-width: 100%;
  border-left: 1px solid var(--border); border-right: 1px solid var(--border);
}


/* ---- a block's own panel -------------------------------------------- *
   Server-rendered and hidden rather than fetched on open: every one of
   these is small, and a panel that arrives over the network is a panel
   that can fail to arrive. */
.blk__panel {
  padding: 2px 14px 14px; border-top: 1px solid var(--border); margin: 0 0 0 0;
}
.blk__panel[hidden] { display: none; }
.pf-intro { font-size: 12.5px; color: var(--text-subtle); line-height: 1.5; margin: 12px 0 14px; }
.pf-note { font-size: 12px; color: var(--text-subtle); line-height: 1.5; margin: 8px 0 0; }
.pf-group {
  border: 1px solid var(--border); border-radius: 10px; padding: 12px;
  margin-top: 10px; background: var(--surface, #fff);
}
.pf-row { display: flex; gap: 10px; flex-wrap: wrap; }
.pf-row > .pf { flex: 1 1 140px; min-width: 0; }
.pf { display: block; margin-top: 10px; }
.pf-row > .pf:first-child, .pf-group > .pf:first-child { margin-top: 0; }
.pf__l {
  display: block; font-size: 11px; letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-subtle); margin-bottom: 5px;
}
.pf__i {
  width: 100%; padding: 9px 10px; font: inherit; font-size: 13.5px;
  border: 1px solid var(--border); border-radius: 8px; background: #fff; color: inherit;
}
.pf__i:focus { outline: 2px solid var(--brand); outline-offset: -1px; border-color: transparent; }
.pf-list { list-style: none; margin: 0; padding: 0; }
.pf-list li {
  display: flex; justify-content: space-between; gap: 12px; padding: 9px 0;
  border-bottom: 1px solid var(--border); font-size: 13px;
}
.pf-list li:last-child { border-bottom: 0; }
.pf-list span { color: var(--text-subtle); }

@media (prefers-reduced-motion: reduce) { .blk__chev { transition: none; } }
.pf__t { resize: vertical; min-height: 120px; line-height: 1.55; font-family: inherit; }
.pf-pick { margin-top: 12px; }
.pf-pick .pf__l { margin-bottom: 7px; }
.cardset__music { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.cardset__up { margin: 0; }
.cardset__drop {
  background: none; border: 0; padding: 0; font: inherit; font-size: 12.5px;
  color: var(--text-subtle); text-decoration: underline; cursor: pointer;
}
.cardset__drop:hover { color: var(--toran-rust, #a4432b); }
.cardset__switch {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 14px;
  margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--border);
}
.cardset__switch span { display: flex; flex-direction: column; gap: 3px; }
.cardset__switch .blk__on { flex: none; margin-top: 2px; }

/* ---------------------------------------------------------------------------
   The wedding's own structure, in the design tab's block panels.

   Settings was a flat stack of cards, and once Functions and the reply form
   joined it the page became longer than everything else on it put together.
   Three group headings give it a spine. Deliberately not tabs: a tab strip
   inside a settings page hides half the page behind a click and turns "where
   is the thing I came for" into a guess.

   .set__h is the group heading. Rows rather than a table on purpose. A table of functions wants six columns
   (name, date, time, venue, replies, actions) and the front-end rule here is
   that nothing may exceed the viewport at 375px. A row that stacks is the one
   shape that keeps every control reachable on a phone without a scroller.
   --------------------------------------------------------------------------- */

/* Sits above a group of cards, in the sidebar's own label voice so it reads
   as structure rather than as another card's title. */
.set__h {
  font-size: 11px; letter-spacing: .16em; text-transform: uppercase;
  font-weight: 600; color: var(--toran-gold); margin: 30px 0 0;
}
.page-head + .set__h { margin-top: 4px; }

.setup-list { list-style: none; margin: 0; padding: 0; counter-reset: fn; }
.setup-row {
  border-bottom: 1px solid var(--border); padding: 13px 0;
  display: flex; flex-direction: column; gap: 10px;
}
.setup-row:last-child { border-bottom: 0; }
.setup-row__main {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 14px;
}
.setup-row__main strong { font-size: 14.5px; font-weight: 600; }
.setup-row__meta { flex: none; text-align: right; }
.setup-row__acts { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }

/* The reorder pair. 32px square, which is the smallest a thumb reliably hits,
   and a real <button> inside its own form so it works with JS off. */
.setup-move {
  width: 32px; height: 32px; border-radius: 8px; cursor: pointer;
  border: 1px solid var(--border); background: var(--toran-white);
  color: var(--text-subtle); font-size: 13px; line-height: 1;
}
.setup-move:hover:not(:disabled) { border-color: var(--brand); color: var(--brand); }
.setup-move:disabled { opacity: .35; cursor: default; }

.setup-link { color: var(--brand); font-weight: 600; text-decoration: none; }
.setup-link:hover { text-decoration: underline; }
/* Remove is a link, not a button, and it is the quietest thing in the row.
   Deleting a ceremony destroys replies guests have already given, so it must
   never be the control the eye lands on first. */
.setup-link--quiet { color: var(--text-subtle); font-weight: 500; margin-left: auto; }
.setup-link--quiet:hover { color: var(--danger); }

/* The second press. Red, and it says how many replies it is about to take. */
.setup-danger {
  border: 1px solid var(--danger); border-radius: 12px;
  padding: 13px 14px; background: color-mix(in srgb, var(--danger) 5%, transparent);
}
.setup-danger p { margin: 0 0 4px; }
.setup-danger p:last-of-type { margin-bottom: 0; }
.setup-danger__go { background: var(--danger); border-color: var(--danger); color: #fff; }
.setup-danger__go:hover { background: var(--danger); filter: brightness(.92); }

.setup-add { margin-top: 20px; padding-top: 18px; border-top: 1px solid var(--border); }
.setup-add__h {
  font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
  font-weight: 600; color: var(--text-subtle); margin: 0 0 10px;
}

.setup-checks { display: flex; flex-direction: column; gap: 2px; }
.setup-check {
  display: flex; align-items: flex-start; gap: 11px; cursor: pointer;
  padding: 10px 0; border-bottom: 1px solid var(--border);
}
.setup-check:last-child { border-bottom: 0; }
.setup-check input { margin-top: 3px; flex: none; width: 16px; height: 16px; accent-color: var(--brand); }
.setup-check span { display: flex; flex-direction: column; gap: 2px; }
.setup-check strong { font-size: 14px; font-weight: 600; }

@media (max-width: 560px) {
  .setup-row__main { flex-direction: column; gap: 4px; }
  .setup-row__meta { text-align: left; }
}

/* "What they told you" — the guest list's foot. Messages are prose, so they
   get a quotation's measure and not a table cell. */
.told-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.told {
  background: var(--toran-white); border: 1px solid var(--border);
  border-radius: 12px; padding: 13px 15px;
}
.told__msg {
  margin: 7px 0 0; padding-left: 12px; border-left: 2px solid var(--toran-gold);
  font-size: 14px; line-height: 1.55; color: var(--text);
}
.told__qs { display: grid; gap: 7px; margin-top: 10px; }
.told__q { display: flex; flex-direction: column; gap: 1px; }



/* ---- budget: a section per ceremony ----
   The guest list's shape applied to money. One block per ceremony, then one
   for spend that covers more than one day, then the sum of the two.

   .bsec is deliberately not .card: a card has a border and a shadow, and six
   of them stacked reads as six unrelated things. A section is a heading with
   its own figures and its own table, separated by a rule, which reads as one
   document with parts. */
.bsec { padding: 22px 0 26px; border-top: 1px solid var(--border); }
.bsec:first-of-type { border-top: 0; padding-top: 4px; }
.bsec__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.bsec__head h3 {
  margin: 0; font-size: 21px; font-weight: 600; font-family: var(--font-display);
}
.bsec__head p { margin: 3px 0 0; }

/* Four figures across, each one a label over a number. auto-fit rather than a
   fixed count because Overdue is only there when there is some, and three
   figures should fill the row rather than leave a gap where it would be. */
.bsec__figs {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
  gap: 10px 18px; margin: 16px 0 12px;
}
.bfig { display: flex; flex-direction: column; gap: 1px; }
.bfig b { font-size: 19px; font-weight: 600; font-variant-numeric: tabular-nums; }

/* The arithmetic, shown rather than asserted. Wraps on a phone, where the
   plus signs end up at the start of a line, which still reads as a sum. */
.badds { margin-top: 22px; background: var(--toran-surface); border: 0; }
.badds__sum {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px 12px; margin-top: 12px;
}
.badds__sum span { display: flex; flex-direction: column; font-variant-numeric: tabular-nums; }
.badds__sum em {
  font-style: normal; font-size: 11px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--text-subtle);
}
.badds__sum i { font-style: normal; color: var(--text-subtle); padding-bottom: 1px; }
.badds__total { font-weight: 600; }
.badds__total em { color: var(--brand); }

@media (max-width: 560px) {
  .bsec__head { flex-direction: column; gap: 10px; }
  .bsec__figs { grid-template-columns: 1fr 1fr; }
  .bfig b { font-size: 17px; }
}

/* The whole-wedding card's header. `.card > .spread` forces a row even on a
   phone, which is right for a heading and a short caption and wrong for a
   heading beside a form: at 375px the title wrapped to two lines and the Set
   button landed under the input. Same specificity as that rule and later in
   the file, so it wins without !important. */
@media (max-width: 560px) {
  .card > .btot__head { flex-direction: column; align-items: flex-start; gap: 12px; }
  .btot__head form { width: 100%; }
  .btot__head form input { flex: 1; min-width: 0; }
}

/* ---- The guest list, a section per ceremony ----------------------------
   Appended at the end of the file on purpose: two other sessions were in
   app.css the same afternoon, and a block at the bottom that touches nothing
   above it is the one shape that cannot collide with theirs. Everything here
   is new; no existing selector is changed. */
.cx { margin: 0 0 40px; scroll-margin-top: 20px; }
.cx__head {
  display: flex; justify-content: space-between; align-items: flex-end;
  gap: 16px; margin: 0 0 14px; padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}
.cx__name {
  font-family: var(--font-display); font-size: 27px; font-weight: 600;
  letter-spacing: -0.02em; margin: 0; line-height: 1.2;
}
.cx__when { margin: 3px 0 0; font-size: 14px; color: var(--text-subtle); }

/* The picker. A <details>, so the open/close costs no script, and closed by
   default because five ceremonies each listing everyone not yet invited is a
   very long page nobody asked for. */
.cx__add { margin-top: 12px; }
.cx__add > summary {
  cursor: pointer; color: var(--brand); list-style: none;
  padding: 7px 0; font-weight: 500;
}
.cx__add > summary::-webkit-details-marker { display: none; }
.cx__add > summary::before { content: "+ "; font-weight: 600; }
.cx__add[open] > summary::before { content: "– "; }

/* Columns rather than one long strip: 60 names in a single column is 60
   lines of scrolling inside a page that already scrolls. */
.cx__picks {
  display: grid; gap: 2px 18px; max-height: 300px; overflow: auto;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
}

@media (max-width: 560px) {
  .cx__head { flex-direction: column; align-items: flex-start; gap: 10px; }
  .cx__name { font-size: 22px; }
  .cx__picks { grid-template-columns: 1fr; max-height: 240px; }
}
/* ---------------------------------------------------------------------------
   The three tabs of /app/design.

   Pulkit asked for tabs and said he may reorder them, so the strip is
   generated from one array in src/views/design-tabs.ts and nothing here knows
   a position. Anchors, not buttons: each tab is a real URL, so Back works and
   a tab can be linked to from helper text elsewhere in the product.

   One line at 320px is the constraint that sets every number below. Three
   labels, 13px, flex: 1 1 0 under a shared row: "Event settings", "The
   invitation" and "Go live" fit with the padding they have and no more, so
   the row never wraps into a second line pretending to be a second strip.
   "Event settings" replaced "Your wedding" on 6 Nov 2026 and is now the
   longest of the three. Measured in the browser at 320px: 76.6px of text in
   80px of available width, against 75.3px for "The invitation" and 36.5px
   for "Go live". It fits, with 3.4px to spare rather than the 4.7px the old
   label left. That is the whole budget. A fourth tab does not fit, and
   neither does a label longer than this one.
   --------------------------------------------------------------------------- */
.dtabs {
  display: flex; gap: 4px; margin: 0 0 22px;
  padding: 4px; border-radius: 12px;
  background: color-mix(in srgb, var(--toran-ink) 5%, transparent);
}
.dtab {
  flex: 1 1 0; min-width: 0; text-align: center;
  padding: 10px 6px; border-radius: 9px;
  font-size: 13.5px; font-weight: 600; line-height: 1.2;
  color: var(--text-subtle); text-decoration: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: background .12s ease, color .12s ease;
}
.dtab:hover { color: var(--text); }
/* The active tab is a raised card out of the trough, which is the one
   treatment that reads as "you are here" without a second colour. */
.dtab.is-on {
  background: var(--toran-white); color: var(--brand);
  box-shadow: 0 1px 2px rgba(16, 32, 28, .07), 0 0 0 1px rgba(16, 32, 28, .04);
}
.dtab:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* The sentence under the strip. Each tab says what it holds, in its own
   words, rather than the strip carrying three hints and growing tall. */
.tabintro {
  margin: -8px 0 18px; font-size: 14px; line-height: 1.6;
  color: var(--text-subtle); max-width: 68ch;
}
.tabintro a { color: var(--brand); font-weight: 600; }

@media (max-width: 420px) {
  .dtab { font-size: 12.5px; padding: 10px 4px; }
}

/* ---------------------------------------------------------------------------
   Tab 3: the WhatsApp message and the sending list.

   A drafter: one message, one link, copy it or open WhatsApp with it. The
   panel under the box shows the finished text with {link} resolved, which is
   also what the clipboard fallback selects when the browser refuses to copy
   for us.
   --------------------------------------------------------------------------- */
.sharebox {
  width: 100%; font: inherit; font-size: 14.5px; line-height: 1.6;
  padding: 13px 14px; border: 1px solid var(--border); border-radius: 10px;
  background: var(--toran-white); color: var(--text);
  resize: vertical; min-height: 180px;
}
.sharebox:focus { outline: 2px solid var(--brand); outline-offset: -1px; border-color: var(--brand); }

/* {name} and {link}. Monospace so they read as something to be replaced
   rather than as words the couple is meant to leave alone. */
.tok {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .9em; padding: 1px 5px; border-radius: 5px;
  background: var(--brand-tint); color: var(--text);
}

/* The worked example. Filled in with a real guest and their real link, and
   rewritten as the message is typed — the page's one rule is that nothing
   waits for a save to show its effect. */
.sharepv {
  border: 1px solid var(--border); border-radius: 10px;
  padding: 12px 14px; background: var(--toran-surface);
}
.sharepv__b {
  margin: 6px 0 0; white-space: pre-wrap; word-break: break-word;
  font-size: 14px; line-height: 1.6; color: var(--text);
}

/* A guest who added themselves from the invitation link.
   A quiet word, not a warning: somebody replying to a wedding invitation is
   the product working. It is here so the couple can scan the column and know
   which rows they vouched for, because a caterer is paid against that total. */
.self-mark {
  display: inline-block; margin-left: 7px; vertical-align: 1px;
  font-size: 10.5px; letter-spacing: .04em; white-space: nowrap;
  padding: 2px 7px; border-radius: 999px;
  background: var(--brand-tint); color: var(--text-subtle);
}

/* ---- the account switcher ----------------------------------------------
   Drawn only for somebody who is on more than one wedding, which is a
   planner, a sibling helping with two, and almost nobody else. A couple with
   one wedding never sees it and the shell is byte-for-byte what it was.

   Reference: FERMAT's brand switcher, which is a searchable grid of cards.
   Borrowed: the grid, the per-account card, the search. Not borrowed: the
   scale. That one holds two hundred brands and needs to; this holds up to
   six weddings, so the cards are wide enough to carry two names in the
   display face rather than shrinking to a logo and a label.

   In the sidebar it sits under the brandmark on the deep teal, so its own
   colours are the dark-band ones. On a phone it moves into the app bar and
   the menu drops as a sheet. */
.swx { position: relative; margin: 0 0 14px; }
.swx > summary {
  display: flex; align-items: center; gap: 10px; cursor: pointer;
  padding: 9px 12px; border-radius: 12px; list-style: none;
  background: rgba(255,255,255,.07); color: #fff;
  border: 1px solid rgba(255,255,255,.1);
}
.swx > summary::-webkit-details-marker { display: none; }
/* The FAQ accordion on the marketing page puts a +/- on every `details
   summary` in the stylesheet. This one has a chevron of its own, and the two
   together read as a control with two states and two affordances. Found by
   looking at the rendered page, not the rule. */
.swx > summary::after, .swx[open] > summary::after { content: none; }
.swx > summary:hover { background: rgba(255,255,255,.12); }
.swx > summary:focus-visible { outline: 2px solid var(--toran-gold); outline-offset: 2px; }
.swx > summary i {
  flex: 0 0 auto; width: 28px; height: 28px; border-radius: 9px;
  background: var(--toran-gold); color: var(--toran-teal-deep);
  display: grid; place-items: center; font-style: normal;
  font-size: 13px; font-weight: 600;
}
.swx > summary span {
  flex: 1 1 auto; min-width: 0; font-size: 14px; color: #fff;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.swx > summary svg { flex: 0 0 auto; color: var(--toran-sand); transition: transform .14s ease; }
.swx[open] > summary svg { transform: rotate(180deg); }

/* In the rail the panel is **in flow**, not floating over it: it is as wide
   as the rail's content box and it pushes the navigation down while it is
   open.

   That is not a compromise, it is the only thing that works here. `.side` is
   `overflow: auto` so the navigation can scroll on a short screen, and an
   overflow box clips its absolutely-positioned descendants -- a 330px panel
   in a 248px rail came out sliced down the middle, which is how this was
   found. The alternatives were a `position: fixed` panel with a hard-coded
   top (brittle the first time the brandmark changes height) or a script to
   measure it (a script, for a menu). A rail-width panel needs neither, and a
   switcher inside a navigation rail being rail-width is what every app with
   one does anyway. */
.swx__menu {
  margin-top: 8px;
  background: var(--toran-white); color: var(--text);
  border: 1px solid var(--border); border-radius: 16px;
  box-shadow: 0 10px 26px rgba(0,0,0,.18); padding: 12px;
}
.swx__head {
  display: flex; align-items: center; justify-content: space-between;
  margin: 0 2px 10px; font-size: 11px; letter-spacing: .13em;
  text-transform: uppercase; color: var(--text-subtle);
}
.swx__head span {
  background: var(--toran-sand); color: var(--brand-deep);
  border-radius: 999px; padding: 2px 8px; letter-spacing: 0; font-size: 11px;
}
.swx__find {
  width: 100%; margin-bottom: 10px; padding: 9px 12px; font: inherit;
  font-size: 14px; border: 1px solid var(--border); border-radius: 10px;
  background: var(--toran-surface); color: var(--text);
}
.swx__find:focus { outline: none; border-color: var(--brand); background: #fff; }

/* One column up to two weddings, two columns beyond, so three or six
   weddings read as a grid and two do not read as a broken one. */
/* Capped and scrollable, so six weddings cannot push the navigation off the
   bottom of a laptop screen. */
.swx__grid {
  display: grid; gap: 7px; max-height: 320px; overflow-y: auto;
  /* `minmax(0, 1fr)`, not the implicit `auto`. An auto track is sized to the
     widest item's max-content, so the longest couple's name made its own card
     wider than the panel and pushed the tick out of sight, while every
     shorter card sat correctly inside. The ellipsis on the name only works
     once the track can actually be narrower than the text. */
  grid-template-columns: minmax(0, 1fr);
}
.swx__card { margin: 0; }
.swx__card[hidden] { display: none; }
.swx__card button {
  width: 100%; display: flex; align-items: center; gap: 9px; text-align: left;
  /* `start`, so a two-line name does not shove the avatar off centre on one
     card and leave it centred on the next. */
  align-items: flex-start;
  padding: 9px; border: 1px solid var(--border); border-radius: 12px;
  background: var(--toran-white); cursor: pointer; font: inherit;
}
.swx__card button:hover { border-color: var(--brand); background: var(--toran-surface); }
.swx__card button:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.swx__card i {
  flex: 0 0 auto; width: 32px; height: 32px; border-radius: 10px;
  background: var(--toran-sand); color: var(--brand-deep);
  display: grid; place-items: center; font-style: normal;
  font-family: var(--font-display); font-size: 16px; font-weight: 600;
}
.swx__card span { flex: 1 1 auto; min-width: 0; }
/* Two lines, then clip. The couple's names are the content of this card and
   the rail is 212px wide, so "Vikram & Anjali" truncating to "Vikram & ..."
   loses the half of it that tells two weddings apart. Wrapping costs a row
   of height; truncating costs the answer. */
.swx__card strong {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  font-family: var(--font-display); font-size: 15.5px;
  font-weight: 600; line-height: 1.22; color: var(--text);
  overflow: hidden; overflow-wrap: anywhere;
}
.swx__card em { display: block; font-style: normal; font-size: 12px; color: var(--text-subtle); }
.swx__card.is-on button { border-color: var(--brand); background: var(--toran-sand); }
.swx__card.is-on i { background: var(--brand); color: #fff; }
/* The tick on the wedding you are already on. A border, not a glyph, so it
   needs no font and no second colour. */
.swx__card u {
  flex: 0 0 auto; width: 13px; height: 8px; margin: 7px 3px 0 0;
  border-left: 2px solid var(--brand); border-bottom: 2px solid var(--brand);
  transform: rotate(-45deg) translateY(-3px); text-decoration: none;
}

/* ---- people ------------------------------------------------------------
   `/app/people` was a stack of cards -- avatar, name, address, a role tag,
   a select and up to three buttons each -- plus a five-dot meter above
   them. Pulkit, 6 Nov 2026: *"this is too complicated. keep it simple,
   table like name email role delete option simple"*. It is `table.data`
   now, which already scrolls sideways below 860px, so `.ppl`, `.ppl__row`,
   `.ppl__who` and `.ppl__meter` are gone with the cards and only these two
   are left. */
.ppl__act { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ppl__act select, td .ppl__act + * select { font-size: 13px; }
td select {
  font: inherit; font-size: 13px; padding: 7px 10px; max-width: 100%;
  border: 1px solid var(--border); border-radius: 9px; background: #fff;
}
/* An invitation nobody has accepted holds one of the five places, so it is
   in the table and in the count. Amber, because the palette reserves amber
   for pending and this is the only pending thing on the page. */
.ppl__pending > td { background: var(--toran-amber-soft); }

/* The app bar does not clip, so there the panel is an overlay again: it
   hangs off the right edge, over the page, rather than pushing the whole
   dashboard down the screen on a phone. */
@media (max-width: 860px) {
  .appbar .swx { position: static; margin: 0; flex: 0 1 auto; min-width: 0; }
  .appbar .swx > summary { background: rgba(255,255,255,.1); }
  .appbar .swx > summary span { max-width: 34vw; }
  /* Anchored to the **bar**, not to the control. `.appbar` is sticky, so it
     is already a containing block; hanging the panel off the control's own
     right edge put a 347px panel at x = -27 on a 375px phone, because the
     control sits in the middle of the bar. Pinned to both gutters instead,
     it cannot be wrong at any width. */
  .appbar .swx__menu {
    position: absolute; z-index: 60; left: 14px; right: 14px; width: auto;
    top: calc(100% + 6px); margin-top: 0;
    box-shadow: 0 18px 44px rgba(12,59,54,.28);
  }
}

/* ---- the history, on /app/people ---------------------------------------
   A list, not a table. Three columns of very different weight — when, what,
   who — and a table gives them equal headings and equal borders, which makes
   scanning for "the thing that went missing" harder rather than easier.

   A row Toran support wrote is marked, in amber rather than the brand green,
   because it is the one row on this page the couple did not cause. */
.hist { border: 1px solid var(--border); border-radius: 14px; overflow: hidden; }
.hist__row {
  display: grid; grid-template-columns: 112px 1fr auto; gap: 14px;
  align-items: baseline; padding: 11px 16px; background: var(--toran-white);
  font-size: 14px;
}
.hist__row + .hist__row { border-top: 1px solid var(--border); }
.hist__when { color: var(--text-subtle); font-size: 12.5px; }
.hist__what { min-width: 0; overflow-wrap: anywhere; }
.hist__what strong { font-weight: 600; }
.hist__who { color: var(--text-subtle); font-size: 12.5px; text-align: right; }
.hist__row--toran { background: var(--toran-amber-soft); }
.hist__row--toran .hist__who { color: var(--warning-text); font-weight: 600; }

@media (max-width: 560px) {
  /* The timestamp leads, the actor tucks under what they did. Three columns
     at 375px gives the middle one about nine characters. */
  .hist__row { grid-template-columns: 1fr; gap: 3px; padding: 12px 14px; }
  .hist__who { text-align: left; }
}

/* `.sudo`, the amber support band, was deleted on 6 Nov 2026 at Pulkit's
   request. The reasoning, and where its exit went, is in the note that
   replaced `supportBanner()` in src/views/layout.ts. `.swx__card--sudo`
   below is a different thing and stays: it marks a support row inside the
   switcher. */

/* A wedding reached as support, in the switcher: marked before it is
   clicked, not only after. */
.swx__sub {
  margin: 12px 2px 7px; font-size: 11px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text-subtle);
}
.swx__grid + .swx__sub { margin-top: 14px; }
.swx__card--sudo button { border-color: #EBD9B4; background: var(--toran-amber-soft); }
.swx__card--sudo i { background: var(--toran-amber); color: #fff; }
.swx__card--sudo em { color: var(--warning-text); font-weight: 600; }

/* The price card's feature list has always carried .plan__two and has never
   been in two columns. `columns: 2` does nothing on a flex container, and
   `.plan ul { display: flex }` is (0,1,1) against the class's (0,1,0), so it
   wins wherever it sits in the file — reordering would not have helped.
   Noticed when the list grew from eleven items to fourteen and ran off the
   bottom of the card. */
.plan ul.plan__two { display: block; }


/* ---- which row of the editor is which part of the page (6 Nov 2026) ----
   Appended rather than placed beside the other `.blk` rules, per the rule at
   the top of this file. They belong next to them by subject and at the end
   by convention, and the convention is what keeps two sessions out of each
   other's lines.

   Two states, because they answer two different questions. `is-marked` is
   the cursor: this row and that section are the same thing, said while you
   are pointing at either of them. `is-open` is the choice you have made, and
   it is held — the matching section in the preview stays ringed for as long
   as the panel is open, which is what stops a tall panel from being a set of
   controls for something you cannot find. */
.blk.is-marked { border-color: var(--brand); }
.blk.is-open {
  border-color: var(--brand); background: var(--brand-tint);
  box-shadow: inset 3px 0 0 var(--brand);
}

/* The row's own trip to the preview, on a screen too narrow to hold both.
   Hidden above 1000px, where opening a row has already moved the preview
   beside it; below that the preview is a screen and a half further down. */
.blk__see {
  display: none; width: 100%; margin: 12px 0 2px; padding: 9px 12px;
  border: 1px solid var(--border); border-radius: 10px; background: #fff;
  font: inherit; font-size: 13px; color: var(--brand); font-weight: 600;
  cursor: pointer; text-align: center;
}
@media (max-width: 1000px) { .blk__see { display: block; } }

/* ---- you, at the foot of the rail --------------------------------------
   Pulkit, 6 Nov 2026, with a crop of the billing box that used to be here:
   *"mak eht ename of hte logged inperson show here clicking it shos things
   Settings settings as in the dashboard settings, invite new users, right
   billing, these all things"*.

   **It opens upward, in flow, at the rail's own width**, and that is not a
   preference. `.side` used to be the scroll container, and an overflow box
   clips absolutely-positioned descendants -- the trap that came out as a
   330px switcher panel sliced down the middle of a 248px rail. So the rail
   no longer scrolls: `.side__nav` does and `.side__foot` is pinned, which
   also means the control is on screen whatever the links are doing. The
   panel is laid out above the summary with `column-reverse` rather than
   floated over anything, so nothing can clip it and nothing needs a z-index
   fought out against `.main`.

   The same panel is the body of the More sheet on a phone, from the same
   function, so the two surfaces cannot start offering a role different
   things. Only the box around it differs: a white card hanging off the rail
   here, `.moresheet`'s own sheet there. */
.acct { display: flex; flex-direction: column-reverse; }
.acct > summary {
  display: flex; align-items: center; gap: 10px; cursor: pointer;
  padding: 9px 11px; border-radius: 12px; list-style: none;
  background: rgba(255,255,255,.07); color: #fff;
  border: 1px solid rgba(255,255,255,.1);
}
.acct > summary::-webkit-details-marker { display: none; }
.acct > summary::marker { content: ""; }
/* The marketing FAQ hangs a gold "+" off every `details summary` in this
   stylesheet. This one has a chevron of its own. */
.acct > summary::after, .acct[open] > summary::after { content: none; }
.acct > summary:hover { background: rgba(255,255,255,.12); }
.acct > summary:focus-visible { outline: 2px solid var(--toran-gold); outline-offset: 2px; }
/* Pointing at where the panel will appear, which is up. */
.acct > summary svg { flex: 0 0 auto; color: var(--toran-sand); transform: rotate(180deg); transition: transform .14s ease; }
.acct[open] > summary svg { transform: none; }

/* The who-block: the control in the dark rail, and the same thing standing
   still at the head of the phone's light sheet. One shape, two palettes. */
.acct > summary, .acct__who { display: flex; align-items: center; gap: 10px; }
.acct > summary > i, .acct__who > i {
  flex: 0 0 auto; width: 30px; height: 30px; border-radius: 10px;
  background: var(--toran-gold); color: var(--toran-teal-deep);
  display: grid; place-items: center; font-style: normal;
  font-size: 13px; font-weight: 600;
}
.acct > summary > span, .acct__who > span { flex: 1 1 auto; min-width: 0; }
.acct > summary strong, .acct__who strong {
  display: block; font-size: 14px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.acct > summary em, .acct__who em {
  display: block; font-style: normal; font-size: 11.5px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.acct > summary strong { color: #fff; }
.acct > summary em { color: var(--toran-sand); }
.acct__who {
  padding: 12px 10px; margin-top: 6px; border-top: 1px solid var(--border);
}
.acct__who strong { color: var(--text); }
.acct__who em { color: var(--text-subtle); }

/* The panel body. Shared; the box around it is not. */
.acct__menu { max-height: min(62vh, 460px); overflow-y: auto; overscroll-behavior: contain; }
.acct__menu--rail {
  background: var(--toran-white); color: var(--text);
  border: 1px solid var(--border); border-radius: 16px;
  box-shadow: 0 10px 26px rgba(0,0,0,.18);
  padding: 8px; margin-bottom: 8px;
}
.acct__menu a,
.acct__out button {
  display: flex; align-items: center; width: 100%; min-height: 44px;
  padding: 0 12px; border-radius: 10px; text-decoration: none;
  font: inherit; font-size: 15px; font-weight: 400; text-align: left;
  color: var(--text); background: none; border: 0; cursor: pointer;
}
.acct__menu a:hover, .acct__out button:hover { background: var(--toran-surface); }
.acct__menu a[aria-current] { background: var(--toran-surface); color: var(--brand); font-weight: 600; }
.acct__mail {
  margin: 2px 2px 6px; padding: 0 12px; font-size: 11.5px;
  color: var(--text-subtle); overflow-wrap: anywhere;
}
.acct__swx, .acct__out {
  margin: 6px 0 0; padding-top: 6px; border-top: 1px solid var(--border);
}

/* The marketing FAQ's `details[open] summary::after` is a **descendant**
   selector, not a child one, so opening this menu hangs a gold "−" off
   every summary inside it -- which, with the switcher nested in here, is a
   second affordance on a control that already has a chevron. The existing
   `.swx` guard does not catch it: `.swx > summary::after` is (0,1,2) and
   `details[open] summary::after` is (0,1,3), so the FAQ wins until an open
   ancestor is named. Both menus that can hold a nested `<details>` name one
   here. Found by looking at the rendered pixels; it does not show in the
   rule.
   See also: the same trap cost the switcher a stray "+" on 5 Nov 2026. */
.acct[open] summary::after,
.tabmore[open] summary::after { content: none; }

/* The switcher was built for the dark rail and now opens inside a white
   card. One override rather than a second component: the rule about who
   gets a switcher at all stays in `switcherFor()`, and this is only paint. */
.acct__menu .swx { margin: 0; }
.acct__menu .swx > summary {
  background: var(--toran-surface); border-color: var(--border);
}
.acct__menu .swx > summary:hover { background: var(--toran-sand); }
.acct__menu .swx > summary span { color: var(--text); }
.acct__menu .swx > summary svg { color: var(--text-subtle); }
.acct__menu .swx > summary i { background: var(--brand); color: #fff; }
.acct__menu .swx__menu { box-shadow: none; background: var(--toran-surface); }

/* ---- the site is about to stop being served -----------------------------
   The shape the support band `.sudo` had before it was removed, and for the
   reason it had it: a product should have one place that means "pay
   attention". This is now the only one.

   It replaces the permanent LIVE / Add time box that used to sit in the foot
   of the rail on every page for two years to say something that is true on
   about thirty days of them. `src/lib/renewal.ts` holds the thresholds and
   the argument; this only paints what that decides. Calm sand from thirty
   days out, amber inside seven and once it has passed. */
.renew {
  position: sticky; top: 0; z-index: 70;
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 10px 20px; font-size: 13.5px; line-height: 1.45;
  background: var(--toran-sand); color: var(--toran-teal-deep);
  border-bottom: 1px solid var(--border);
}
.renew span { flex: 1 1 320px; min-width: 0; }
/* What an editor or a viewer gets instead of a button they cannot use. */
.renew b { flex: 0 0 auto; font-weight: 600; color: var(--text-subtle); }
.renew .btn { flex: 0 0 auto; }
.renew--warn {
  background: var(--toran-amber); color: #fff; border-bottom-color: transparent;
}
.renew--warn b { color: #fff; }
.renew--warn .btn {
  background: #fff; color: var(--toran-amber-deep); border-color: #fff;
}
.renew--warn .btn:hover { background: var(--toran-amber-soft); }

@media (max-width: 860px) {
  /* The app bar is sticky at the top; with a band above it, both need to
     stack rather than overlap. */
  .renew { position: static; gap: 10px; padding: 10px 20px 12px; }
  /* The sentence takes the row and the button wraps under it at its own
     width. Stretched to 100% it made a twenty-days-out notice 99px tall on
     a 812px screen, which is a twelfth of the phone given over to something
     that is not urgent yet. The second line is prose, so it does fill. */
  .renew b { flex: 1 1 100%; }
  .renew .btn { flex: 0 0 auto; }
  /* The sheet grows upward from the bar and now carries the account block
     as well as the rail's overflow, so it needs a ceiling of its own. */
  .moresheet { max-height: min(74vh, 560px); }
}


/* ---- the copy control in the overview header (6 Nov 2026) --------------
   Pulkit wanted the link copyable from where it is written, with the QR and
   the WhatsApp message behind the same control.

   **The same `<details>` as `.acct`**, so this product has one menu
   behaviour and not two: open, close, Escape and focus are the browser's.
   It borrows `.acct__menu` for the rows, exactly as `.moresheet` does, and
   supplies only its own position.

   **Absolute, not in flow, and that is the opposite of `.swx__menu` on
   purpose.** The switcher goes in flow because it lives in a 248px rail
   where a floating panel has nowhere to be. The page head is full width, so
   this has room, and pushing the whole page down every time somebody opens
   a copy menu would be worse than either. The precedent followed here is
   `.dp__pop`, which is absolute at z-index 70 inside the same
   `overflow-y: auto` `.main` and has worked since the date picker shipped. */
.cpy { position: relative; display: inline-flex; vertical-align: baseline; }
.cpy > summary {
  display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
  list-style: none; padding: 1px 8px 2px; margin: -1px 0;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--toran-white); color: var(--text-subtle);
  font-size: 13px; line-height: 1.5;
}
.cpy > summary::-webkit-details-marker { display: none; }
.cpy > summary::marker { content: ""; }
/* The marketing FAQ hangs a gold "+" off every `details summary` in this
   stylesheet, and `details[open] summary::after` is a DESCENDANT selector,
   so an open ancestor reaches in here too. Both are named. */
.cpy > summary::after, .cpy[open] > summary::after,
.cpy[open] summary::after { content: none; }
.cpy > summary:hover { border-color: var(--brand); color: var(--brand); }
.cpy > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.cpy[open] > summary { border-color: var(--brand); color: var(--brand); }

.cpy__menu {
  position: absolute; z-index: 70; top: calc(100% + 8px); left: 0;
  width: 300px; padding: 8px; border-radius: 14px;
  background: var(--toran-white); border: 1px solid var(--border);
  box-shadow: 0 14px 40px rgba(12, 59, 54, .16);
  text-align: left;
}
/* `.acct__menu` gives the rows their height and hover. These are buttons
   carrying two lines rather than one-line links, so the height is their
   content and the padding is their own.

   `.cpy__dl` is the download row: the one row here that is an `<a>`, and the
   reason this is a selector list rather than one more class on the anchor.
   It cannot wear `.cpy__item`, because that class is what the click handler
   in copy.js keys on and it would answer "Could not copy." over a download
   that worked. So it needs the row style under its own name -- and under the
   panel's, because this panel also carries `.acct__menu`, whose plain `a`
   rule is (0,1,1) and beats a bare `.cpy__dl` at (0,1,0). Unqualified, that
   rule wins `padding: 0 12px` and draws one 44px row between two 54px ones.
   Naming the panel makes it (0,2,0). Same arithmetic that cost `.ppl-table`
   a day on 6 Nov 2026; it does not show up in either rule on its own.

   `text-decoration: none` is set here rather than inherited from
   `.acct__menu a`, which happens to supply it today: the day somebody stops
   sharing that class the row should not sprout an underline. */
.cpy__item,
.cpy__menu .cpy__dl {
  display: flex; align-items: center; width: 100%;
  min-height: 44px; padding: 9px 12px; border-radius: 10px;
  font: inherit; text-align: left; color: var(--text);
  text-decoration: none;
  background: none; border: 0; cursor: pointer;
}
.cpy__item:hover,
.cpy__menu .cpy__dl:hover { background: var(--toran-surface); }
.cpy__item:focus-visible,
.cpy__menu .cpy__dl:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.cpy__item strong, .cpy__dl strong { display: block; font-size: 15px; font-weight: 500; }
.cpy__item em, .cpy__dl em {
  display: block; font-style: normal; font-size: 12px; color: var(--text-subtle);
  margin-top: 1px; overflow-wrap: anywhere;
}
/* The message, visible where it is copied.

   Clamped to five lines: enough that the opening line and the first
   ceremonies answer "is this the one I wrote", few enough that the panel is
   still a menu. The clamp is visual only -- the whole message is in the DOM,
   which is what lets the copy fallback reveal and select it. */
.cpy__pv {
  margin: 2px 2px 4px; padding: 9px 12px 10px;
  border-radius: 10px; background: var(--toran-surface);
}
.cpy__pvb {
  margin: 0; font-size: 12px; line-height: 1.55; color: var(--text-subtle);
  /* The couple's own line breaks, including the blank ones between their
     paragraphs. Nothing is collapsed here: this element is what gets
     selected when the clipboard refuses, so it has to hold the message
     exactly. */
  white-space: pre-line; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 5;
  overflow: hidden;
}
/* Opened out when nothing automatic was allowed and this becomes the thing
   to select, because what they copy has to be what they can see. Capped and
   scrollable so a seven-function message cannot push the panel off screen. */
.cpy__pv.is-full .cpy__pvb {
  display: block; -webkit-line-clamp: none;
  max-height: 40vh; overflow-y: auto; color: var(--text);
}

/* Empty until something has happened, so it takes no room until it speaks. */
.cpy__say { margin: 0; padding: 0 12px; font-size: 12.5px; color: var(--text-subtle); }
.cpy__say:not(:empty) { padding: 8px 12px 4px; }
.cpy__raw {
  width: 100%; margin-top: 8px; min-height: 92px; padding: 8px 10px;
  border: 1px solid var(--border); border-radius: 10px;
  font: inherit; font-size: 12.5px; resize: vertical;
}

@media (max-width: 860px) {
  /* `.addr` is hidden at this width and this control is what now leads the
     line, so it keeps its place rather than following the address out. The
     panel is pinned to the left gutter instead of to the control, for the
     reason the app bar's switcher is: at 375px a 300px panel hung off a
     control that sits mid-line starts off the screen. */
  .cpy__menu { left: 0; width: min(300px, calc(100vw - 48px)); }
}
.cpy__alt {
  display: block; margin-top: 8px; padding: 10px 12px; border-radius: 10px;
  background: var(--toran-surface); color: var(--brand);
  font-size: 13px; font-weight: 500; text-decoration: none;
}
.cpy__alt:hover { background: var(--toran-sand); }
.cpy__alt[hidden] { display: none; }

/* A radio or checkbox beside a sentence long enough to wrap.

   `.row` wraps by default, which is right everywhere else and wrong here: the
   sentence was pushed onto its own line under the control, so the control and
   the thing it controls stopped reading as a pair. This is the consent choice
   on /thanks, where that pairing is the whole point. */
.optline { display: flex; align-items: flex-start; gap: 10px; flex-wrap: nowrap; }
.optline > input { margin-top: 3px; flex: 0 0 auto; }
.optline > span { flex: 1 1 auto; min-width: 0; }

/* ---- the one featured quote beside checkout ----

   One quote, centred, as large as it deserves. The panel is narrow and dark
   and its job is to carry somebody over the line, so this is built for
   exactly one — which is also the first real state it will ever be in.
   Zero quotes render no markup at all and none of this applies. */
.qfeature {
  margin: 26px 0 0; padding: 26px 24px 24px; border-radius: 18px; text-align: center;
  background: rgba(255, 255, 255, .07); border: 1px solid rgba(255, 255, 255, .13);
}
.qfeature blockquote {
  margin: 16px 0 0; font-family: var(--font-display); font-size: 22px;
  line-height: 1.34; letter-spacing: -0.02em; color: #fff;
  /* Their words, with the line breaks they typed. */
  white-space: pre-line; overflow-wrap: anywhere;
}
.qfeature figcaption { margin-top: 16px; font-size: 13px; color: var(--toran-sand); }
.qfeature figcaption strong { font-weight: 600; }
.qfeature figcaption span {
  margin-left: 8px; padding-left: 8px; border-left: 1px solid currentColor; opacity: .75;
}

/* The face, or the initial in its place.

   Identical geometry either way, so a row with no photograph reads as a
   choice rather than as an image that failed to load. A photograph is only
   ever one a real couple sent us under a permission of its own; there are no
   stock faces in this product. */
.qface {
  display: inline-grid; place-items: center; border-radius: 999px;
  background: var(--toran-sand); color: var(--toran-teal-deep);
  font-family: var(--font-display); font-weight: 600; flex-shrink: 0;
  overflow: hidden;
}
.qface--photo { object-fit: cover; display: inline-block; }
.review__by .qface { font-size: 17px; }

/* The refund, given the weight it has earned. For an unknown brand asking
   ₹1,999 it is the strongest honest thing on the page, and it was a sentence
   at the bottom of a paragraph. */
.refundblock {
  margin-top: 22px; padding: 18px 20px; border-radius: 14px;
  background: rgba(255, 255, 255, .06); border: 1px solid var(--toran-gold);
}
.refundblock strong {
  display: block; font-family: var(--font-display); font-size: 19px;
  font-weight: 600; color: #fff;
}
.refundblock p { margin: 6px 0 0; font-size: 13.5px; line-height: 1.55; color: var(--toran-sand); }

/* ---- a row of panel fields sits on one line (6 Nov 2026) ----
   Pulkit, on the Name and Role boxes in a person's row: "align these 2
   boxes". Only the FIRST field in a row had its top margin cleared, so
   every field after it in the same row hung 10px lower than the one before.

   `.pf-row` is a flex row with `gap: 10px`, and `gap` already spaces the
   fields when they wrap onto their own lines at a narrow panel width — so
   the margin was doing nothing the gap was not already doing, except
   knocking the row out of alignment. */
.pf-row > .pf { margin-top: 0; }
/* ---- the hidden native date input stays hidden ---- */
/* `.dp__native` is added to an input that already carries a page's own field
   class, and in the design panel that class is `.pf__i`, which is further
   down this file. Same specificity, so `.pf__i` was winning `width`, `padding`
   and `border` on a node that is meant to have none of them. Only `opacity: 0`
   was keeping the browser's own control out of sight, one declaration away
   from the native picker reappearing on top of the branded button.

   Appended here, after every field class, so it wins on order the way this
   file's convention intends. `!important` is not needed and is not used: the
   rule is last, which is the whole point of the append-only rule above. */
input.dp__native {
  position: absolute; opacity: 0; pointer-events: none;
  width: 0; height: 0; min-width: 0; padding: 0; margin: 0;
  border: 0; background: none; clip-path: inset(50%);
}

/* ---- the auth card on a narrow phone (6 Nov 2026) ----------------------
   Pulkit, from a Pixel: the payment card on `/checkout` is cut off at the
   right edge, "₹1,999" is clipped mid-number and the heading stops at
   "One payment, then you're i".

   It is not the grid failing to collapse. `.authwrap` does go to one column
   at 860px and that rule is applying — measured, not assumed. There are two
   separate defects under it, and only the second one clips.

   **One, on every auth page: the gutter is not being honoured.** `.authcard`
   is `width: 100%` with `margin: 20px 16px` on a `justify-self: center` grid
   item. A percentage width resolves against the whole grid area and the
   margins are then added outside it, so the margin box is 32px wider than
   the screen; the browser clamps the overhang and the card renders flush to
   both edges. At 412px it measured 412 wide at left 0 — the 16px gutters the
   rule asks for were simply gone, on `/signup`, `/signin`, `/checkout` and
   the invitation page alike. Size the card to the space that is actually
   free and centre it with auto margins instead. It is now 380 at left 16.

   **Two, on `/checkout` only: a hard 352px floor.** The price row is
   `.spread.keep-row`, `nowrap` by deliberate exception to the phone rule
   that stacks `.spread`. "₹1,999" at 34px display is 107px that cannot break
   at any width, which gave the whole card a min-content width of 352px.
   Below about 348px the card is physically wider than the screen and the
   excess is clipped — silently, because `body { overflow-x: hidden }` a few
   hundred lines above turns it into clipping rather than a scrollbar. That
   is the reported symptom, and it is why `scrollWidth === clientWidth` reads
   clean on a page that is visibly cut off: on a phone that test cannot fail.
   Measured before the fix: 32px past the viewport at 320, 12px at 340, clean
   at 360 and above. The other three auth pages never hit this because their
   cards have no unbreakable 107px word in them.

   Why widths below 360 are worth fixing when the phone is 412: Android's
   display-size setting is widely used and shrinks the reported CSS viewport,
   so a 412px device routinely reports 360 or less.

   Let the price drop under its own label below 400px. It is the one number
   on the page; it does not need to share a line with its caption. 400 is
   chosen so nothing changes at 412 while 390, 375, 360 and 320 gain room.

   Measured after: no element past the viewport on any of the four auth pages
   at any width from 320 to 900 in 20px steps, and 1440 is byte-identical to
   before — the card is still 460 in a 720px column with the price beside the
   label. */
@media (max-width: 860px) {
  .authcard { width: calc(100% - 32px); margin-left: auto; margin-right: auto; }
}
@media (max-width: 400px) {
  .authcard .card .spread.keep-row {
    flex-direction: column; align-items: flex-start; gap: 4px;
  }
}

/* `.row` is `flex-wrap: wrap`, so at 320px the tick and its line of copy in
   the feature list became two rows and each bullet rendered as a tick
   floating above its own text. They are a glyph and its label, not two
   items: keep them together and let the label take the slack. */
@media (max-width: 860px) {
  .authcard .card li.row { flex-wrap: nowrap; }
  .authcard .card li.row > span { min-width: 0; }
}
/* The people table, on a phone (6 Nov 2026).
   `table.data` is `display: block; overflow-x: auto` below 860px, which is
   right for the guest list and wrong here: at 375px the four columns put
   Remove off the right edge behind a sideways scroll nobody finds, and the
   name wrapped to two lines to make room for it. The cards this replaced
   stacked instead, for that same reason, and the reason did not go away
   with them.

   So below 560px the rows stack: who they are on one line, what you can do
   about them on the next. Same markup, same table on a laptop. */
@media (max-width: 560px) {
  /* Every selector here is qualified with the element, because the rules it
     has to beat are `table.data td` and `table.data { overflow-x: auto }`,
     which carry an element each: `.ppl-table td` is (0,1,1) against their
     (0,1,2) and loses however late it sits in the file. The symptom was a
     rule drawn between the name, the address and the role of the same
     person -- `table.data td`'s own `border-top`, still winning. */
  table.ppl-table,
  table.ppl-table tbody,
  table.ppl-table tr,
  table.ppl-table td { display: block; }
  table.ppl-table { overflow-x: visible; }
  table.ppl-table thead { display: none; }
  table.ppl-table tr { padding: 13px 16px; border-top: 1px solid var(--border); }
  table.ppl-table tbody tr:first-child { border-top: 0; }
  table.ppl-table td { border: 0; padding: 0; }
  table.ppl-table td:nth-child(2) { color: var(--text-subtle); overflow-wrap: anywhere; }
  table.ppl-table td:nth-child(3) { margin-top: 6px; }
  table.ppl-table td.ppl__act:not(:empty) { margin-top: 10px; }
  table.ppl-table td select { width: 100%; }
}

/* ---- the time half of the same control ---- */
/* Shares `.dp`, `.dp__field`, `.dp__pop`, `.dp__foot` and `.dp__link` with the
   date picker, so the two sit in one row looking like one component. Only the
   inside of the popup differs: a scrolling list of common times grouped by
   `partOfDay`'s own boundaries, and an exact-minute panel behind one link,
   because a muhurat is computed rather than rounded and `clockText` has a
   branch for it. */
.dp__icon--clock {
  -webkit-mask-image: var(--clock);
  mask-image: var(--clock);
}
:root {
  --clock: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6' fill='none' stroke='black' stroke-width='1.4'/%3E%3Cpath d='M8 4.6V8l2.4 1.6' fill='none' stroke='black' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/* The list scrolls inside the popup rather than making the popup tall: at
   half-hour steps a full day is 48 rows, and a popup that long would run off
   any screen and flip above on every open. */
.dp__pop--time { max-height: 320px; overflow-y: auto; overscroll-behavior: contain; }
.dp__part {
  position: sticky; top: -14px; z-index: 1;
  margin: 8px -14px 2px; padding: 6px 14px 4px;
  background: var(--toran-white);
  font-size: 11px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-subtle);
}
.dp__part:first-child { margin-top: -4px; }
.dp__time {
  display: block; width: 100%; padding: 8px 10px; border: 0; border-radius: 8px;
  background: none; cursor: pointer; text-align: left;
  font: inherit; font-size: 14px; color: var(--text);
}
.dp__time:hover { background: var(--toran-sand); }
.dp__time.is-on { background: var(--brand); color: #fff; font-weight: 600; }

/* The exact-minute panel. Two native selects on purpose: the keyboard and a
   screen reader get them for free, and a custom listbox here would be a
   second thing to keep accessible for no gain. */
.dp__exact {
  display: grid; grid-template-columns: 1fr 1fr auto; gap: 4px 8px; align-items: center;
}
.dp__exactl { font-size: 11px; letter-spacing: .04em; text-transform: uppercase; color: var(--text-subtle); }
.dp__sel {
  width: 100%; padding: 8px 6px; font: inherit; font-size: 14px;
  border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--text);
}
.dp__ampm { display: flex; gap: 4px; }
.dp__half {
  padding: 8px 9px; border: 1px solid var(--border); border-radius: 8px;
  background: #fff; cursor: pointer; font: inherit; font-size: 13px; color: var(--text-subtle);
}
.dp__half.is-on { background: var(--brand); border-color: var(--brand); color: #fff; font-weight: 600; }
.dp__set {
  width: 100%; margin-top: 10px; padding: 9px 12px; border: 0; border-radius: 8px;
  background: var(--brand); color: #fff; cursor: pointer;
  font: inherit; font-size: 14px; font-weight: 600;
}


/* ---- the card's look: one box, the tools nested inside it (6 Nov 2026) ----
   Pulkit, with the two chip rows stacked in a screenshot: "where are we on
   merging these?? i want like a these and inside it all the tools".

   THEME, a bordered "Your colours" card, BACKGROUND and PHOTOGRAPH were four
   controls in three groupings running flat down the panel, each reading as
   its own subject. They are one box now with two questions in it, and each
   question's dependent tools nested under it rather than stacked after it.

   The rule down the left of a nested group is the whole explanation: it says
   "this belongs to the thing above" in a way a flat stack of labels cannot.
   Indent alone was not enough to read as nesting at this width — measured
   against the panel's other groups, which are also indented by their own
   padding. */
.look {
  border: 1px solid var(--border); border-radius: 12px;
  padding: 14px; margin-top: 8px; background: var(--toran-surface);
}
.look__q + .look__q {
  margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border);
}
.look__sub {
  margin-top: 12px; padding-left: 14px;
  /* `--border`, not `--brand-tint`. The tint is #F2E9D9 on the box's own
     #F4F7F6 ground, which measured as very nearly the same colour and read
     as no rule at all — the nesting is the entire explanation here, so it
     has to be visible. This is the same hairline the panel uses everywhere
     else, which is what makes it read as structure rather than decoration. */
  border-left: 2px solid var(--border);
}
.look__sub[hidden] { display: none; }
/* The palette pair is a nested group now, so it drops the card it used to
   draw for itself — a box inside a box inside a box. */
.look__sub.cardset__pal { border-radius: 0; background: none; padding: 0 0 0 14px; }

/* A control that is moot right now. The three ground chips mean nothing
   while a photograph covers them; that was a sentence under them while the
   chips stayed bright, which is the same bug as a chip that repaints
   nothing — it is drawn, so it looks like it works. Dimmed rather than
   `disabled`, because a couple has to be able to reach the row to find out
   why it went quiet, and a disabled button takes no focus. */
.chips.is-moot { opacity: .45; }
.chips.is-moot .chip { cursor: default; }
#cardGradient.is-moot { opacity: .45; }


/* ---- tap targets on a phone (6 Nov 2026) -------------------------------
   The 6 Nov design audit measured roughly a dozen controls under 44px, the
   worst a 17px tick box and a 19px-tall Remove. Re-measured on this branch
   against the current build, every interactive element on every dashboard
   page at 375px, with the enclosing `<label>` counted as the target where a
   tick has one. 44px is the floor.

   **Two techniques, and which one is used where is a judgement, not a
   preference.** A control that is already a box — a chip, a switch, a tick
   row, a disclosure — is simply given `min-height: 44px`, because growing it
   is what it wanted anyway. A small control sitting in running layout — a
   `See all` beside a card title, a `Remove` at the end of a row — is given a
   transparent `::after` that extends the *hit area* to 44px while the
   painted box stays where the design put it. Inflating those visually would
   push the layouts they sit in around for no gain; a thumb does not care
   where the border is drawn, only where the tap lands.

   **One thing deliberately left alone: links inside a sentence.** `email us`
   on `/app/billing` is 41x20 and stays that way. WCAG 2.5.8 exempts inline
   links for the obvious reason — a 44px tap target inside a paragraph either
   overlaps the line above or tears the paragraph open. The rule is "a
   control that stands on its own", not "every anchor".

   All of it is inside the 860px block, so nothing on a desktop moves. */
@media (max-width: 860px) {
  /* Boxes that simply wanted to be bigger. */
  .chip { min-height: 44px; display: inline-flex; align-items: center; }
  .check { min-height: 44px; align-items: center; }
  details > summary.small { min-height: 44px; display: flex; align-items: center; }

  /* The block list's move buttons. 30x30 in a row with `gap: 4px`, so making
     them square at 44 would add 28px to a row that is already tight. Full
     height, a little more width, and the rest of the target taken from the
     gap around them. */
  .blk__mv { width: 36px; height: 44px; }

  /* The on/off switch is 44x26 and has to *stay* 26 tall — a pill twice that
     height stops reading as a switch. The hit area grows instead. */
  .blk__on { position: relative; }
  .blk__on::after { content: ''; position: absolute; inset: -9px -2px; }

  /* Small standalone controls: hit area only, painted box unchanged. */
  .card-link, .ip__up-open, .cpy > summary, button.tiny, a.tiny { position: relative; }
  .card-link::after, .ip__up-open::after, .cpy > summary::before,
  button.tiny::after, a.tiny::after {
    content: ''; position: absolute; left: 50%; top: 50%;
    width: max(100%, 44px); height: 44px; transform: translate(-50%, -50%);
  }
  /* `.cpy > summary` already has an `::after` pinned to `content: none` to
     keep the marketing FAQ's gold "+" out of it, so this one borrows
     `::before` rather than quietly reviving that selector. */

  /* The lockup in the app bar is a link home and the only way back from a
     deep page. */
  .appbar .brandmark { min-height: 44px; display: inline-flex; align-items: center; }
}

/* The rest of the under-44 controls, after the first pass. `.blk__on`'s
   overlay is given explicit dimensions rather than `inset`, because `inset`
   computes to `auto` and cannot then be read back when checking the result —
   a rule that cannot be measured is a rule nobody will notice breaking. */
@media (max-width: 860px) {
  .blk__mv { position: relative; }
  .blk__mv::after {
    content: ''; position: absolute; left: 50%; top: 50%;
    width: 44px; height: 44px; transform: translate(-50%, -50%);
  }
  .blk__on::after {
    content: ''; position: absolute; left: 50%; top: 50%;
    width: 48px; height: 44px; transform: translate(-50%, -50%);
  }
  .dtab { min-height: 44px; display: inline-flex; align-items: center; }
  .ip__primary { min-height: 44px; }
  .pf__i { min-height: 44px; }
}


/* ---- the guest list on a phone (6 Nov 2026) ----------------------------
   `table.data` is `display: block; overflow-x: auto` below 860px. That is
   the right answer for a table you *read* and the wrong one for a table you
   *act on*, because the action lives in the last column and the last column
   is exactly what goes off the edge. Measured at 375: the rows are wider
   than the 335px content column, and what is off-screen by default is the
   RSVP tag, `Invite link` and `Remove` — which is the entire reason a couple
   opens this page on a phone. The only affordance is a thin scrollbar under
   the card.

   So below 560px the row becomes a card, following the same pattern and the
   same reasoning `.ppl-table` already uses on `/app/people`:

     Meena Agarwal · Jain
     Bride's side · 2 heads
     [ Coming ]              Invite link  Remove

   The name leads because that is what you are scanning for. Side and heads
   are detail and share a line. The answer and the two things you can do
   about it are the bottom line, where a thumb is.

   The row is a wrapping flex rather than a stack of blocks, because the last
   line has to hold two items at opposite ends and a block cannot do that
   without another wrapper in the markup.

   **Every selector is qualified with its element**, for the reason the
   `.ppl-table` block below records: the rules being beaten are
   `table.data td` and `table.data { overflow-x: auto }`, which carry an
   element each, so an unqualified class is (0,1,1) against (0,1,2) and loses
   however late it sits in the file. */
@media (max-width: 560px) {
  table.gx-table, table.gx-table tbody { display: block; }
  table.gx-table { overflow-x: visible; }
  table.gx-table thead { display: none; }
  table.gx-table tr {
    display: flex; flex-wrap: wrap; align-items: center; gap: 2px 8px;
    padding: 12px 16px; border-top: 1px solid var(--border);
  }
  table.gx-table tbody tr:first-child { border-top: 0; }
  table.gx-table td { display: block; border: 0; padding: 0; font-size: 15px; }

  /* The name takes its own line. */
  table.gx-table td:nth-child(1) { flex: 1 1 100%; }
  /* Side and heads share the next one, with a separator drawn rather than
     written, so the markup stays a table on a laptop. */
  table.gx-table td:nth-child(2),
  table.gx-table td:nth-child(3) { flex: 0 0 auto; color: var(--text-subtle); }
  table.gx-table td:nth-child(3)::before { content: '·'; margin-right: 8px; }
  /* The heads cell is a bare number, and a bare number on its own line reads
     as "2 what?". It is always a number — `head_count ?? party_size` — so a
     unit can be appended unconditionally. The side cell above it can be an
     em dash when nobody recorded one; that is left as it is, because it says
     the same thing the desktop table says and inventing a way to hide it
     costs more than the noise it saves. */
  table.gx-table td:nth-child(3)::after { content: ' heads'; }
  /* The answer and the actions, on one line, at opposite ends. */
  table.gx-table td:nth-child(4) { flex: 0 0 auto; margin-top: 8px; }
  table.gx-table td:nth-child(5) {
    flex: 0 0 auto; margin-left: auto; margin-top: 8px;
    display: flex; align-items: center; gap: 14px; white-space: nowrap;
  }
  table.gx-table td:nth-child(5) form { margin: 0; }
  /* Both of these were bare text a thumb could not land on: 65x27 and 40x19.
     Hit area only — the painted link stays the size the row was designed
     around. */
  table.gx-table td:nth-child(5) a,
  table.gx-table td:nth-child(5) button { position: relative; }
  table.gx-table td:nth-child(5) a::after,
  table.gx-table td:nth-child(5) button::after {
    content: ''; position: absolute; left: 50%; top: 50%;
    width: max(100%, 44px); height: 44px; transform: translate(-50%, -50%);
  }
}


/* ---- the budget on a phone (6 Nov 2026) --------------------------------
   Same sideways-scroll problem as the guest list, but it earns a different
   card, because the two pages are for different things. The guest list is a
   list you *act on* — one tap per row — so its card leads with the name and
   puts the action under a thumb. The budget is a set of numbers you *read*:
   what is committed, what is paid, what is still owed, and whether anything
   is late. Measured at 375, the rows are 754px inside a 335px column and
   what is off-screen is PAID, PENDING, STATUS and Remove — so everything
   except the vendor, its category and the figure you already agreed.

   **Which is why this one gets labels and the guest list does not.** Hiding
   a table's header is free when the cells say what they are — a name, a
   side, a Yes. Three money figures in a row with the header gone are three
   indistinguishable amounts, and reading "paid" as "pending" on a wedding
   budget is a real mistake to hand somebody. The labels are drawn in CSS
   from the same column positions the hidden `<thead>` names, so they cannot
   drift from the data under them the way a hand-written label would.

   Shape:

     Rathore Tent House and Decorators
     Overdue · 16 Nov
     Decor
     COMMITTED ₹7,12,000  PAID ₹7,12,000  PENDING —
     [ Settled ]                                  Remove */
@media (max-width: 560px) {
  table.bx-table, table.bx-table tbody { display: block; }
  table.bx-table { overflow-x: visible; }
  table.bx-table thead { display: none; }
  table.bx-table tr {
    display: flex; flex-wrap: wrap; align-items: center; gap: 2px 12px;
    padding: 12px 16px; border-top: 1px solid var(--border);
  }
  table.bx-table tbody tr:first-child { border-top: 0; }
  table.bx-table td { display: block; border: 0; padding: 0; }

  /* Vendor, and the due date it already carries, on their own line. */
  table.bx-table td:nth-child(1) { flex: 1 1 100%; }
  /* Category under it, quietly. */
  table.bx-table td:nth-child(2) { flex: 1 1 100%; color: var(--text-subtle); }

  /* The three figures, each with the column name it lost when the header
     went. Tabular figures so the amounts line up as they wrap. */
  table.bx-table td:nth-child(3),
  table.bx-table td:nth-child(4),
  table.bx-table td:nth-child(5) {
    flex: 0 0 auto; margin-top: 6px; font-variant-numeric: tabular-nums;
  }
  table.bx-table td:nth-child(3)::before { content: 'Committed '; }
  table.bx-table td:nth-child(4)::before { content: 'Paid '; }
  table.bx-table td:nth-child(5)::before { content: 'Pending '; }
  table.bx-table td:nth-child(3)::before,
  table.bx-table td:nth-child(4)::before,
  table.bx-table td:nth-child(5)::before {
    display: block; font-size: 11px; letter-spacing: 0.12em;
    text-transform: uppercase; color: var(--text-subtle); font-weight: 600;
  }

  /* The verdict, and the one thing you can do, on the last line. */
  table.bx-table td:nth-child(6) { flex: 0 0 auto; margin-top: 10px; }
  table.bx-table td:nth-child(7) { flex: 0 0 auto; margin-left: auto; margin-top: 10px; }
  table.bx-table td:nth-child(7) form { margin: 0; }
  /* Remove measured 52x21. Hit area only. */
  table.bx-table td:nth-child(7) button { position: relative; }
  table.bx-table td:nth-child(7) button::after {
    content: ''; position: absolute; left: 50%; top: 50%;
    width: max(100%, 44px); height: 44px; transform: translate(-50%, -50%);
  }
}

/* The side and category cells used to hold a literal em dash when there was
   nothing to say. On a desktop table that is right — a blank cell in a grid
   of cells reads as a rendering fault. Once the row is a card the same dash
   costs a whole line per guest and per vendor to say nothing.

   So the markup now emits an empty cell and the dash is drawn here, which
   means it can be drawn only where it helps. `:empty` is exact: it matches a
   cell with no text and no children, which is precisely the case the dash
   was standing in for. */
table.gx-table td:nth-child(2):empty::before,
table.bx-table td:nth-child(2):empty::before {
  content: '—'; color: var(--text-subtle);
}
@media (max-width: 560px) {
  table.gx-table td:nth-child(2):empty,
  table.bx-table td:nth-child(2):empty { display: none; }
  /* With the side gone the separator would lead the line. */
  table.gx-table td:nth-child(2):empty + td::before { content: none; }
}


/* ---- the app bar in landscape on a notched phone (6 Nov 2026) ----------
   The dashboard asks `doc()` for `viewport-fit=cover`, so it paints into the
   notch and `env(safe-area-inset-*)` resolves to real numbers. `.tabbar`,
   `.main` and `.page-head` all take the side insets through `max()`. The app
   bar was the one that did not: a flat `padding: 8px 20px`.

   Measured at 844x390 with the side insets forced to 44px — a notched phone
   held sideways, notch on the left — the brandmark painted from x=20 and the
   wedding name ran to x=824, both inside the 44px band the hardware eats.
   The tab bar underneath was correct in the same screenshot, which is what
   made it obvious: the bottom of the screen respected the notch and the top
   did not.

   So the logo — which is the only way back to Overview from a deep page —
   and the name of the wedding you are editing were the two things under the
   camera housing. `max()` keeps the 20px gutter everywhere the inset is 0,
   which is every phone in portrait and every phone without a notch. */
.appbar {
  padding-left: max(20px, env(safe-area-inset-left));
  padding-right: max(20px, env(safe-area-inset-right));
}

/* The photo picker is a full-width sheet with an 18px gutter and insets for
   top and bottom only, so its title, its grid and its buttons had the same
   problem in the same orientation. */
@media (max-width: 860px) {
  .pk__head, .pk__body, .pk__foot {
    padding-left: max(18px, env(safe-area-inset-left));
    padding-right: max(18px, env(safe-area-inset-right));
  }
}

/* ---- a ceremony's own controls, under its fields (6 Nov 2026) ----
   The ceremonies moved out of a tab of their own and into the Functions
   block's panel, so each row now carries its words and its controls
   together. This is the line under the fields: where it sits, how many
   have answered, and how to remove it.

   The reply count sits beside Remove rather than on the guest list because
   it is the reason Remove is dangerous — guest_events is ON DELETE CASCADE,
   so deleting a ceremony deletes every reply to it. */
.cact {
  display: flex; align-items: center; gap: 6px;
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border);
}
.cact__n { margin-left: auto; }
.cact .setup-link { margin-left: 10px; }
/* ---- a control that only shows after saving says so (6 Nov 2026) ----
   Nine controls in this panel cannot repaint the preview, each for a real
   reason: a date is formatted on the server, a map link has to be chased
   through Google, the language swaps strings the server rendered. Every
   reason was written down — in editor.js, next to the code, where a couple
   is never going to read it. So Pulkit changed one, saw nothing happen, and
   reported the editor as broken. Five times in a day, three of them on
   controls working exactly as designed.

   Quiet on purpose. It is a fact about when the change appears, not a
   warning, and it sits on nine controls at once — at `--text-subtle` and
   10.5px it is findable when you are looking at that control and invisible
   when you are not. The louder half is the Save button, which changes to
   "Save to see it" the moment one of them is touched. */
.waits {
  display: inline-block; margin-left: 8px;
  font-size: 10.5px; font-weight: 500; letter-spacing: .04em;
  text-transform: none; color: var(--text-subtle); white-space: nowrap;
}
.waits::before { content: '↻ '; }
/* The row version is a sentence, so it gets its own line under the fields
   rather than sitting beside a label. */
.waits--row { display: block; margin-top: 8px; margin-left: 0; }
/* On a label that is already uppercase and letter-spaced, the marker should
   not inherit either or it reads as part of the label. */
.cardset__label .waits, .pf__l .waits { text-transform: none; letter-spacing: .02em; }

/* ===========================================================================
   ADD A MODULE (ADR 0009)

   The library under the block list, and the two verbs a module row has that
   a section row does not. A section is the wedding's own structure and can
   only be moved and hidden; a module is something the couple added, so it
   can also be duplicated and deleted. The kind decides which buttons exist,
   which is why Delete can never appear on a row holding somebody else's
   reply.
   =========================================================================== */

.addmod { margin: 18px 0 4px; }
.addmod__h {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-subtle);
  margin: 0 0 4px;
}
.addmod__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  margin-top: 10px;
}
.addmod__one {
  display: flex;
  flex-direction: column;
  gap: 3px;
  text-align: left;
  padding: 11px 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: #fff;
  cursor: pointer;
  font: inherit;
  color: inherit;
}
.addmod__one:hover { border-color: var(--brand); background: var(--brand-tint); }
.addmod__one:disabled { opacity: 0.5; cursor: default; }
.addmod__one strong { font-size: 13px; font-weight: 600; }
.addmod__one span { font-size: 12px; color: var(--text-subtle); line-height: 1.4; }
.addmod__full { margin: 6px 0 0; }

/* The two verbs a module has and a section does not, at the foot of its
   panel rather than in the row's action strip: they are rarer than move and
   hide, and Delete beside the on/off switch is a misclick with no undo. */
.blk__own {
  display: flex;
  gap: 8px;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
.blk__own button {
  font: inherit;
  font-size: 12px;
  padding: 6px 11px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: #fff;
  color: var(--text-subtle);
  cursor: pointer;
}
.blk__own button:hover { border-color: var(--brand); color: var(--text); }
.blk__own button:disabled { opacity: 0.5; cursor: default; }
.blk__del:hover { border-color: var(--danger); color: var(--danger); }

/* Between the click and the reload. The server replaces this row a moment
   later; it exists so the list shows what is about to happen. */
.blk--pending { opacity: 0.55; pointer-events: none; }
