/* ============================================================
   Anello: "the fridge door"
   The household's real memory surface: a lived door with cream
   paper sheets pinned by melon and piscine magnets at mixed
   sizes and slight rotations, marine marker underlines and
   sweeps, soft paper-lift shadows, and office-stamp states.
   Nellie is the door that answers. The proportions are the
   palette: warm cream is the door (60), deep marine is ink and
   marker (30), melon the power magnets (5), piscine the clip
   layer (3), chili the stop (2).

   Every text pair clears WCAG AA (4.5:1) on the surface it
   sits on. Melon is too light for plain marine text (3.2:1),
   so melon tiles and buttons carry --melon-ink, a deep marine.
   Chili never carries text on the door (2.9:1) — only
   --alert-deep does.
   ============================================================ */

:root {
	/* Anello, 2026: the fridge door. Warm cream is the door itself; lighter
	   cream sheets lift off it on soft shadows, pinned by melon and piscine
	   magnets. Deep marine is ink and marker. The phone demo hangs on the
	   door like the household's real command center. */
	--paper: #f4e7ce; /* warm cream: the door, the 60% */
	--paper-alt: #ebd8b2; /* deeper cream for banded surfaces and the foot */
	--surface: #fcf6e9; /* lighter cream sheets, cards, and inputs */

	--ink: #284d5c; /* deep marine: ink, plate, rules — the 30%. 7.44:1 on paper */
	--ink-2: #35586a; /* secondary text; 6.23:1 on paper, 5.44:1 on the band */
	--ink-3: #3f6477; /* muted text; 5.20:1 on paper, 4.54:1 on the band */

	--melon: #f28b49; /* ripe melon, brand + "power" signal */
	--melon-hi: #f49d63; /* hover lift; carries --melon-ink at 5.6:1 */
	--melon-mid: #d97738; /* deeper melon for the panel's offset shadow */
	--melon-deep: #94420f; /* text-safe melon: small marks, links, focus ring */
	--melon-ink: #1b3948; /* deep marine; the only text that sits on melon (4.96:1) */
	--melon-tint: #f3cfa8;
	--melon-wash: #f9e5d2;

	--pisc: #9ec3d3; /* piscine, the "reference / code" layer */
	--pisc-deep: #27597b; /* text-safe piscine: inline code, links, chips */
	--pisc-tint: #e3eef4;

	/* Alert. Chili at full strength is 2.9:1 on the sand, so it is borders
	   and decoration only; --alert-deep is the text-safe version. */
	--alert: #d84b20;
	--alert-deep: #a93411; /* text-safe on the wash and on paper */
	--alert-wash: #f9e3d7;

	/* Piscine carries the accents on the dark plate (chat head, close). */
	--pisc-on-dark: #c2dce7;

	--plate: #284d5c;
	--plate-ink: #f8f0de;

	--rule: #e0cfa6; /* hairline on the alt band (darker than either ground) */
	--line-strong: #c3a878; /* a bit more presence for input/secondary-button edges */
	--ink-rule: #284d5c; /* device and input edges */
	--frame: rgba(40, 77, 92, 0.25); /* door-frame hairlines for section rules */

	/* Paper lifting off the door: soft, offset, neutral — never a colored glow. */
	--pin: 0 1px 2px rgba(0, 0, 0, 0.1), 0 10px 28px rgba(0, 0, 0, 0.13);
	--pin-hi: 0 2px 4px rgba(0, 0, 0, 0.12), 0 18px 40px rgba(0, 0, 0, 0.17);

	--mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
	--sans: "Manrope", system-ui, -apple-system, "Segoe UI", sans-serif;

	--wrap: 1200px;
	--pad: clamp(1.25rem, 4vw, 3rem);

	/* Height the sticky topbar occupies, so anchor jumps can clear it. Grows
     when the nav wraps to its own row below 900px. */
	--topbar-h: 64px;
	--tr: 0.2s ease;
	--radius: 12px; /* paper sheet */
	--radius-sm: 8px; /* tiles, inputs, small chrome */
}

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

/* The `hidden` attribute is only `display: none` in the UA stylesheet, so any
   author rule that sets `display` silently beats it — .signup-form's
   `display: grid` did exactly that, and the invite gate rendered its refusal
   with the form still sitting underneath it. !important rather than ordering,
   so a later `display` on any future component can't reintroduce the bug. */
[hidden] {
	display: none !important;
}

html {
	scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
	html {
		scroll-behavior: auto;
	}
}

body {
	margin: 0;
	background: var(--paper);
	color: var(--ink);
	font-family: var(--sans);
	font-size: clamp(1rem, 0.96rem + 0.2vw, 1.0625rem);
	line-height: 1.6;
	-webkit-font-smoothing: antialiased;
	text-rendering: optimizeLegibility;
}

.wrap {
	max-width: var(--wrap);
	margin-inline: auto;
	padding-inline: var(--pad);
}

a {
	color: inherit;
}

::selection {
	background: var(--melon);
	color: var(--melon-ink);
}

/* Target of both the skip link and the wordmark, so it gets the same clearance
   the bands do. */
#main {
	scroll-margin-top: var(--topbar-h);
}
#main:focus {
	outline: none;
}

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

.skip {
	position: absolute;
	left: -9999px;
	top: 0;
	background: var(--melon);
	color: var(--melon-ink);
	padding: 0.6rem 1rem;
	font: 700 0.8rem / 1 var(--sans);
	z-index: 50;
}
.skip:focus {
	left: 0;
}

/* ── shared type ─────────────────────────────────────────── */

/* Manrope carries the display voice as well as the body; the mono that
   remains (badges, code, small terminal furniture) is an accent, not the
   headline face. */
h1,
h2,
h3 {
	font-family: var(--sans);
	margin: 0;
	text-wrap: balance;
}

h1 {
	font-weight: 800;
	font-size: clamp(2.1rem, 1.3rem + 3.4vw, 4rem);
	line-height: 1.02;
	letter-spacing: -0.025em;
	margin-block: 0.6rem 0;
}
h1 em {
	font-style: normal;
	color: var(--ink); /* ink over the swipe, like a real marker: melon-deep drops to 4.0:1 on the wash */
	white-space: nowrap; /* keep "one text" on one line */
	/* melon marker swipe under the words */
	background: linear-gradient(to top, rgba(242, 139, 73, 0.45) 24%, transparent 24%);
	padding-inline: 0.08em;
}

h2 {
	font-weight: 700;
	font-size: clamp(1.55rem, 1.15rem + 1.5vw, 2.15rem);
	line-height: 1.1;
	letter-spacing: -0.02em;
}

h3 {
	font-weight: 600;
	font-size: 1.02rem;
	line-height: 1.35;
	letter-spacing: -0.01em;
}

p {
	margin: 0;
}

.eyebrow {
	font: 700 0.72rem / 1 var(--sans);
	letter-spacing: 0.2em;
	text-transform: uppercase;
	color: var(--melon-deep);
}

/* The hero eyebrow carries the same tick every band label does. */
.hero .eyebrow {
	display: flex;
	align-items: center;
	gap: 0.7rem;
}
.hero .eyebrow::before {
	content: "";
	width: 11px;
	height: 11px;
	border-radius: 50%;
	background: var(--melon);
	box-shadow: inset 0 -2px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.25);
	flex: none;
}

/* ── buttons ─────────────────────────────────────────────── */

/* Solid melon buttons with a soft paper shadow — the strongest magnet on the
   door; deep-marine text keeps them at 4.9:1. */
.btn {
	display: inline-flex;
	align-items: center;
	gap: 0.45rem;
	font: 600 0.9rem / 1 var(--sans);
	letter-spacing: 0.005em;
	text-decoration: none;
	padding: 0.78rem 1.35rem;
	border: 1.5px solid transparent;
	border-radius: 9px;
	cursor: pointer;
	transition:
		background-color var(--tr),
		color var(--tr),
		border-color var(--tr),
		box-shadow var(--tr),
		transform var(--tr);
}
.btn:active {
	transform: translateY(1px);
}

.btn-solid {
	background: var(--melon);
	color: var(--melon-ink);
	box-shadow: 0 1px 2px rgba(0, 0, 0, 0.09), 0 3px 10px rgba(0, 0, 0, 0.13);
}
.btn-solid:hover {
	background: var(--melon-hi);
}

.btn-ghost {
	background: var(--surface);
	color: var(--ink);
	border-color: var(--line-strong);
}
.btn-ghost:hover {
	background: var(--paper-alt);
	border-color: var(--ink-3);
}

.btn-lg {
	padding: 0.95rem 1.7rem;
	font-size: 0.98rem;
}

/* Touch pointers get the 44px target; the topbar CTA sits under it otherwise,
   and below 900px it's the only tappable control in the header. */
@media (pointer: coarse) {
	.btn {
		min-height: 44px;
	}
}
@media (prefers-reduced-motion: reduce) {
	.btn:active {
		transform: none;
	}
	.card,
	.card:hover {
		transform: none;
		transition: none;
	}
}

/* ── topbar ──────────────────────────────────────────────── */

.topbar {
	position: sticky;
	top: 0;
	z-index: 20;
	background: rgba(244, 231, 206, 0.96);
	backdrop-filter: blur(10px);
	border-bottom: 1px solid var(--frame);
}

/* Invite-code nudge banner (revealed by signup.js when ?code= is present). A piscine band directly
   under the topbar with a strong marine rule, matching the site's boxed 198X tell. */
.code-banner {
	background: var(--pisc-tint);
	border-bottom: 1px solid var(--frame);
}
.code-banner[hidden] {
	display: none;
}
.code-banner-inner {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 0.75rem 1.5rem;
	flex-wrap: wrap;
	padding-top: 0.85rem;
	padding-bottom: 0.85rem;
}
.code-banner-text {
	margin: 0;
	font-weight: 600;
	color: var(--ink);
	font-size: 1.02rem;
}
.code-banner-text::before {
	content: "";
	display: inline-block;
	width: 0.6rem;
	height: 0.6rem;
	margin-right: 0.55rem;
	border-radius: 50%;
	background: var(--alert);
	vertical-align: middle;
}
.code-banner .btn-solid {
	flex: none;
}

.topbar-inner {
	display: flex;
	align-items: center;
	gap: 1.5rem;
	min-height: 64px;
}

.mark {
	display: flex;
	align-items: center;
	gap: 0.55rem;
	text-decoration: none;
	margin-right: auto;
}
.mark-badge {
	display: grid;
	place-items: center;
	width: 26px;
	height: 26px;
	border-radius: var(--radius-sm);
	background: var(--melon);
	color: var(--melon-ink);
	font: 600 0.85rem / 1 var(--mono);
	flex: none;
}
.mark-name {
	font-family: var(--sans);
	font-weight: 700;
	font-size: 1.05rem;
	letter-spacing: -0.01em;
}

.topnav {
	display: flex;
	gap: 1.5rem;
}
.topnav a {
	font: 600 0.82rem / 1 var(--sans);
	color: var(--ink-2);
	text-decoration: none;
	padding-block: 0.4rem;
	border-bottom: 2px solid transparent;
	white-space: nowrap;
}
.topnav a:hover {
	color: var(--melon-deep);
	border-bottom-color: var(--melon-deep);
}

.topbar-cta {
	padding: 0.5rem 0.9rem;
}

/* The nav moves to its own row rather than disappearing, so phone visitors
   aren't left scrolling the whole page to reach Setup. */
@media (max-width: 900px) {
	:root {
		--topbar-h: 104px;
	}

	/* row-gap has to be zeroed explicitly: the base rule sets `gap: 1.5rem`,
     which would otherwise open 24px of dead space above the strip. */
	.topbar-inner {
		flex-wrap: wrap;
		column-gap: 1rem;
		row-gap: 0;
		padding-block: 0.5rem 0;
	}

	.topnav {
		order: 3;
		width: 100%;
		gap: 1.5rem;
		overflow-x: auto;
		overscroll-behavior-inline: contain;
		border-top: 1px solid var(--rule);
		/* Bleed the strip to the viewport edges while its items stay on the
       same left edge as the wordmark above them. */
		margin-inline: calc(var(--pad) * -1);
		padding-inline: var(--pad);
		scrollbar-width: none;
	}
	.topnav::-webkit-scrollbar {
		width: 0;
		height: 0;
	}

	.topnav a {
		flex: none;
		display: inline-flex;
		align-items: center;
		min-height: 44px;
		padding-block: 0;
	}
}

/* ── hero ────────────────────────────────────────────────── */

.hero {
	padding-block: clamp(3.5rem, 7vw, 6.5rem) clamp(3.5rem, 6vw, 6rem);
	border-bottom: 1px solid var(--frame);
	background: var(--paper);
	overflow-x: clip;
}

.hero-grid {
	display: grid;
	grid-template-columns: minmax(0, 1.02fr) minmax(0, 1fr);
	gap: clamp(2.5rem, 5vw, 4.5rem);
	align-items: center;
}

.lede {
	margin-top: 1.5rem;
	max-width: 34em;
	color: var(--ink-2);
	font-size: 1.08rem;
	line-height: 1.62;
}

/* 600 and a step darker rather than a full 700 bold. The lede is five sentences
   and the emphasis only has to mark where the questions stop and the answer
   starts; a hard bold at this size turns into a slab and pulls the eye off the
   headline above it. */
.lede strong {
	font-weight: 600;
	color: var(--ink);
}

.cta-row {
	display: flex;
	flex-wrap: wrap;
	gap: 0.85rem;
	margin-top: 2rem;
}

.hero-facts {
	list-style: none;
	display: flex;
	flex-wrap: wrap;
	gap: 0.5rem 1.5rem;
	margin: 2.25rem 0 0;
	padding: 1.5rem 0 0;
	border-top: 1px solid var(--rule);
}
.hero-facts li {
	font: 600 0.76rem / 1.4 var(--sans);
	color: var(--ink-3);
	display: flex;
	align-items: center;
	gap: 0.5rem;
}
.hero-facts li::before {
	content: "";
	width: 7px;
	height: 7px;
	border-radius: 50%;
	background: var(--pisc);
	box-shadow: inset 0 -1px 2px rgba(0, 0, 0, 0.25);
	flex: none;
}

/* ── the conversation (signature) ────────────────────────── */

/* The phone hangs on the door like the household's command center: a melon
   magnet grips its top edge, paper-lift shadow beneath, back-lit warmth
   behind. The screen inside stays genuinely iOS. */
/* ── the phone ───────────────────────────────────────────── */

/* Device drawn in this page's materials: ink body, hard rule, the same offset
   shadow the buttons carry. The screen inside is genuinely iOS. That split is
   the point — a phone resting on this page, not this page dressed as iOS. */
.demo {
	min-width: 0;
	display: grid;
	gap: 1rem;
	justify-items: center;
	position: relative;
}
/* Back-lit paper warmth: light diffusing through the sheet behind the phone,
   a soft melon wash, not a glow. */
.demo::before {
	content: "";
	position: absolute;
	inset: 12% -4% 6%;
	background: radial-gradient(closest-side, rgba(242, 139, 73, 0.2), rgba(242, 139, 73, 0.07) 62%, transparent 78%);
	z-index: 0;
	pointer-events: none;
}
.demo > * {
	position: relative;
	z-index: 1;
}

.demo-tabs {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 0.4rem;
}
.demo-tab {
	font: 600 0.76rem / 1 var(--sans);
	letter-spacing: 0.01em;
	color: var(--ink-2);
	background: transparent;
	border: 1.5px solid transparent;
	border-radius: var(--radius);
	padding: 0.5rem 0.7rem;
	cursor: pointer;
	transition:
		color var(--tr),
		border-color var(--tr),
		background-color var(--tr);
}
.demo-tab:hover {
	color: var(--ink);
	border-color: var(--rule);
}
.demo-tab[aria-selected="true"] {
	color: var(--ink);
	background: var(--surface);
	border-color: var(--ink-rule);
	font-weight: 600;
}
@media (pointer: coarse) {
	.demo-tab {
		min-height: 44px;
	}
}

/* Three tabs on one row is the whole point of a tablist — wrapped, the odd one
   out reads as a different control. At the labels' current lengths the row
   needs about 411px of viewport, which is wider than every common phone, so
   these two steps tighten the type and padding just enough to keep it in line:
   the first covers 375px and up, the second 360px, which is where most Android
   sits. The 44px tap target is set on min-height and is unaffected by either.

   Below 360px it wraps, and that is the right place to stop — the type would
   have to go under 10px to hold the line, and two rows of legible tabs beat
   one row nobody can read. flex-wrap is the backstop, here and for whatever
   longer label comes later. */
@media (max-width: 30rem) {
	.demo-tabs {
		gap: 0.3rem;
	}
	.demo-tab {
		font-size: 0.68rem;
		padding: 0.5rem 0.55rem;
	}
}
@media (max-width: 23rem) {
	.demo-tabs {
		gap: 0.25rem;
	}
	.demo-tab {
		font-size: 0.63rem;
		padding: 0.5rem 0.42rem;
	}
}

/* Default is a readable vertical stack: with no JS you get three complete
   conversations rather than three overlapping ones. app.js adds .is-tabbed,
   which collapses them into a single cell so switching never changes the
   hero's height. */
/* Widths are definite the whole way down on purpose. Letting the stage
   shrink-wrap the panel, the panel shrink-wrap the phone, and the phone size
   itself with min(20rem, 100%) makes the 100% resolve against an indefinite
   width — the chain is circular and the phone settles off-centre. */
.demo-stage {
	width: 100%;
	display: grid;
	gap: 1.75rem;
	justify-items: center;
	min-width: 0;
}
.demo-panel {
	width: min(20rem, 100%);
	min-width: 0;
}
/* With JS but before app.js takes over, only the first panel shows. Otherwise
   all three paint stacked and then collapse — the other half of the flash.
   Once .is-tabbed lands, the hidden attribute drives it instead. */
.has-js .demo-stage:not(.is-tabbed) .demo-panel:nth-child(n + 2) {
	display: none;
}
.demo-stage.is-tabbed {
	gap: 0;
}
.demo-stage.is-tabbed .demo-panel {
	grid-area: 1 / 1;
}

.phone {
	position: relative;
	width: 100%;
	background: var(--ink);
	border-radius: 2.4rem;
	box-shadow: var(--pin);
	padding: 0.55rem;
}
/* The melon magnet holding the phone to the door. */
.phone::after {
	content: "";
	position: absolute;
	top: -12px;
	left: calc(50% - 12px);
	width: 24px;
	height: 24px;
	border-radius: 50%;
	background: radial-gradient(circle at 32% 28%, #f8b985, var(--melon) 55%, var(--melon-mid));
	box-shadow: 0 3px 6px rgba(0, 0, 0, 0.35), inset 0 -3px 4px rgba(0, 0, 0, 0.28);
	z-index: 2;
}

.phone-screen {
	position: relative;
	background: #fff;
	border-radius: 1.95rem;
	overflow: hidden;
	display: flex;
	flex-direction: column;
	height: 30rem;
	/* The recipe thread runs to ~28rem once the photo lands. Any shorter and
	   justify-content: flex-end starts shoving earlier messages out the top,
	   which shifts the whole stack on every arrival and reads as jitter.
	   Containment stops the animating subtree from relaying out anything
	   outside the screen regardless. */
	contain: layout paint;
}

/* The thread header, as iOS draws it: translucent, hairline, name centred. */
.thread-head {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 0.25rem;
	padding: 1.5rem 1rem 0.5rem;
	background: rgba(249, 249, 249, 0.94);
	backdrop-filter: blur(12px);
	border-bottom: 1px solid #dcdcdf;
	flex: none;
}
.thread-ava {
	display: grid;
	place-items: center;
	width: 30px;
	height: 30px;
	border-radius: 50%;
	background: var(--melon);
	color: var(--melon-ink);
	font: 600 0.8rem / 1 var(--mono);
}
.thread-name {
	font-family: var(--sans);
	font-size: 0.7rem;
	font-weight: 500;
	color: #1c1c1e;
}

.chat-log {
	list-style: none;
	margin: 0;
	padding: 0.75rem 0.7rem 1rem;
	display: flex;
	flex-direction: column;
	gap: 0.3rem;
	overflow: hidden;
	flex: 1;
	justify-content: flex-end;
}

/* Grid rather than flex so the row itself can collapse to nothing: a staged
   message is 0fr tall, and expanding it to 1fr is what pushes the thread
   upward as each new bubble lands. Flex has no equivalent you can transition. */
.msg {
	display: grid;
	grid-template-rows: 1fr;
	justify-items: end;
	transition:
		grid-template-rows 0.55s cubic-bezier(0.25, 0.8, 0.25, 1),
		opacity 0.46s ease-out;
}
.msg.is-bax {
	justify-items: start;
}
.msg .msg-body {
	min-height: 0;
	overflow: hidden;
	display: flex;
	flex-direction: column;
	align-items: flex-end;
	gap: 0.15rem;
	max-width: 78%;
	transition: transform 0.55s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.msg.is-bax .msg-body {
	align-items: flex-start;
}

/* Bubbles. Sent is blue and right, received is grey and left, which is the
   arrangement everyone already knows how to read. The blue is two steps
   deeper than Apple's #007AFF so white text clears 4.5:1 instead of 4.0. */
.bubble {
	max-width: 100%;
	padding: 0.44rem 0.75rem;
	border-radius: 1.15rem;
	font-family: var(--sans);
	font-size: 0.83rem;
	line-height: 1.38;
	background: #0a6ce0;
	color: #fff;
	border-bottom-right-radius: 0.35rem;
}
.msg.is-bax .bubble {
	background: #e9e9eb;
	color: #1c1c1e;
	border-bottom-right-radius: 1.15rem;
	border-bottom-left-radius: 0.35rem;
}
/* Only the last bubble of a run gets the tail; the rest stay fully round. */
.msg .bubble:not(:last-child) {
	border-bottom-right-radius: 1.15rem;
}
.msg.is-bax .bubble:not(:last-child) {
	border-bottom-left-radius: 1.15rem;
}

.bubble-media {
	padding: 0;
	overflow: hidden;
	background: #d9d9dd;
	line-height: 0;
}
.bubble-photo {
	display: block;
	width: 7.5rem;
	height: auto;
	border-radius: inherit;
}

.msg-body {
	min-width: 0;
}

.msg-h {
	display: flex;
	align-items: baseline;
	flex-wrap: wrap;
	gap: 0.45rem;
	margin-bottom: 0.15rem;
}
.msg-h b {
	font-family: var(--mono);
	font-weight: 600;
	font-size: 0.9rem;
}
.msg-h span {
	font: 400 0.68rem / 1 var(--mono);
	color: var(--ink-3);
}

/* Staged reveal. Keyed on has-js, set by an inline script in <head>, so this
   applies on the first paint — app.js adds its class several frames later and
   the demo would flash fully-rendered before collapsing. The markup stays
   complete for anyone without JS, which is the point of doing it this way.

   (was: app.js adds .is-staged only when motion is welcome and the
   script is actually running, so all three conversations are complete without
   it. Enter from below, the way a message actually arrives. */
/* A staged row has no height at all, so the thread sits at the bottom of the
   screen and grows upward, one message at a time, the way a real one does.
   The bubble rides up the last few pixels on its own transform so it arrives
   rather than simply appearing. */
.has-js .chat-log .msg {
	grid-template-rows: 0fr;
	opacity: 0;
}
.has-js .chat-log .msg > .msg-body {
	overflow: hidden;
	transform: translateY(7px);
}
.has-js .chat-log .msg.is-in {
	grid-template-rows: 1fr;
	opacity: 1;
}
.has-js .chat-log .msg.is-in > .msg-body {
	transform: none;
}

/* Typing indicator, drawn the way iOS draws it: its own small grey bubble
   rather than dots floating over the reply. The real bubbles are removed from
   flow while it shows, so the thread grows when the reply lands. The screen is
   a fixed height with messages anchored to the bottom, so that growth reads as
   a conversation filling up instead of the panel resizing. */
.typing-dots {
	display: none;
	align-items: center;
	gap: 4px;
	padding: 0.62rem 0.8rem;
	background: #e9e9eb;
	border-radius: 1.15rem;
	border-bottom-left-radius: 0.35rem;
}
.msg.is-typing .msg-body > p {
	display: none;
}
.msg.is-typing .typing-dots {
	display: flex;
}
.typing-dots i {
	width: 6px;
	height: 6px;
	border-radius: 50%;
	background: #8e8e93;
	animation: tdot 1.15s ease-in-out infinite;
}
.typing-dots i:nth-child(2) {
	animation-delay: 0.15s;
}
.typing-dots i:nth-child(3) {
	animation-delay: 0.3s;
}
@keyframes tdot {
	0%,
	60%,
	100% {
		opacity: 0.25;
	}
	30% {
		opacity: 1;
	}
}

@media (prefers-reduced-motion: reduce) {
	/* Must reset grid-template-rows as well as opacity: staging collapses the
	   row to 0fr, so resetting only opacity would leave every message present,
	   fully opaque, and zero pixels tall. */
	.has-js .chat-log .msg {
		grid-template-rows: 1fr;
		opacity: 1;
	}
	.has-js .chat-log .msg > .msg-body {
		transform: none;
	}
	.msg,
	.msg .msg-body {
		transition: none;
	}
	.typing-dots i {
		animation: none;
	}
}

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

/* ── band scaffolding ────────────────────────────────────── */

.band {
	padding-block: clamp(3.5rem, 6vw, 6rem);
	border-bottom: 1px solid var(--frame);
	scroll-margin-top: var(
		--topbar-h
	); /* clears the sticky topbar on anchor jumps */
}
.band-alt {
	background: var(--paper-alt);
}

.band-grid {
	display: grid;
	grid-template-columns: 190px minmax(0, 1fr);
	gap: clamp(1.75rem, 4vw, 3.5rem);
}

.band-label {
	font: 700 0.72rem / 1 var(--sans);
	letter-spacing: 0.18em;
	text-transform: uppercase;
	color: var(--melon-deep);
	display: flex;
	align-items: center;
	gap: 0.7rem;
	height: fit-content;
	padding-top: 0.5rem;
}
.tick {
	width: 11px;
	height: 11px;
	border-radius: 50%;
	background: var(--melon);
	box-shadow: inset 0 -2px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.25);
	flex: none;
}

/* Grid items default to min-width:auto, which refuses to shrink below their
   content, so the panes inside these columns would push the page wide. */
.band-body,
.hero-copy {
	min-width: 0;
}

.band-lede {
	margin-top: 1.1rem;
	max-width: 58ch;
	color: var(--ink-2);
}
@media (max-width: 860px) {
	.band-grid {
		grid-template-columns: 1fr;
		gap: 1.5rem;
	}
	.band-label {
		padding-top: 0;
	}
}

/* ── ability cards ───────────────────────────────────────── */

.cards {
	margin-top: 2.75rem;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(258px, 1fr));
	gap: 2rem 2.5rem;
}
/* Pinned paper sheets: cream stock on the door, a magnet holding each one,
   slight rotations so the grid reads as a lived board. A sheet straightens
   and lifts when you reach for it. */
.card {
	position: relative;
	padding: 1.6rem 1.5rem 1.7rem;
	background: var(--surface);
	border-radius: var(--radius);
	box-shadow: var(--pin);
	transition: transform var(--tr), box-shadow var(--tr);
}
.card::before {
	content: "";
	display: block;
	width: 12px;
	height: 12px;
	border-radius: 50%;
	background: radial-gradient(circle at 32% 28%, #f8b985, var(--melon) 55%, var(--melon-mid));
	box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3), inset 0 -2px 3px rgba(0, 0, 0, 0.25);
	margin: 0 auto 1.1rem;
}
.card:nth-child(odd) {
	transform: rotate(-0.6deg);
}
.card:nth-child(even) {
	transform: rotate(0.5deg);
}
.card:nth-child(3n)::before {
	background: radial-gradient(circle at 32% 28%, #d5e7ef, var(--pisc) 55%, #7fb2c6);
}
.card:hover {
	transform: rotate(0deg) translateY(-3px);
	box-shadow: var(--pin-hi);
}
.card h3 {
	color: var(--ink);
}
.card p {
	margin-top: 0.6rem;
	font-size: 0.92rem;
	line-height: 1.58;
	color: var(--ink-2);
}

/* ── security plate ──────────────────────────────────────── */

/* A spec sheet, deliberately quiet: one column, so each rule gets a full
   line to state itself. */
.plate {
	margin: 2.5rem 0 0;
	border-top: 1px solid var(--frame);
}
.plate > div {
	display: grid;
	grid-template-columns: 16rem minmax(0, 1fr);
	gap: 1.5rem;
	padding-block: 1rem;
	border-bottom: 1px solid var(--rule);
	align-items: baseline;
}
/* The term is what you scan, so it has to outweigh the prose explaining it. */
.plate dt {
	font-family: var(--sans);
	font-weight: 700;
	font-size: 0.92rem;
	line-height: 1.4;
	color: var(--ink);
}
.plate dd {
	margin: 0;
	font-size: 0.92rem;
	line-height: 1.55;
	color: var(--ink-2);
}

@media (max-width: 620px) {
	.plate > div {
		grid-template-columns: 1fr;
		gap: 0.35rem;
	}
}

/* ── close ───────────────────────────────────────────────── */

/* The page's second dark moment: the close picks up the chat window's title
   bar (marine plate, piscine accents) so the scroll ends on a peak instead of
   fading out. */
.close {
	padding-block: clamp(4rem, 8vw, 7rem);
	background: var(--plate);
	color: var(--plate-ink);
}
.close .band-label {
	color: var(--pisc-on-dark);
}
.close .tick {
	background: var(--pisc-on-dark);
}
.close h2 {
	color: var(--plate-ink);
}
.close-inner p {
	margin-top: 1rem;
	color: rgba(248, 240, 222, 0.8);
}
.close :focus-visible {
	outline-color: var(--pisc-on-dark);
}
.close .btn {
	margin-top: 2.25rem;
}
.close .btn-solid {
	box-shadow: 0 2px 10px rgba(0, 0, 0, 0.28);
}
.close .btn-solid:active {
	box-shadow: 0 1px 4px rgba(0, 0, 0, 0.28);
}

/* ── footer ──────────────────────────────────────────────── */

.foot {
	border-top: 1px solid var(--frame);
	padding-block: 1.75rem;
	background: var(--paper-alt);
}
.foot-inner {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.6rem 1.5rem;
	font: 500 0.78rem / 1.5 var(--sans);
	color: var(--ink-3);
}
.foot-mark {
	font-family: var(--sans);
	font-weight: 700;
	letter-spacing: -0.01em;
	color: var(--ink);
	font-size: 1rem;
}
/* It links home, but it should still read as the wordmark rather than as a
   third footer link: no rule under it, and the dark ink kept. Specificity has
   to beat `.foot a`, which would otherwise underline it and lighten it. */
.foot a.foot-mark {
	color: var(--ink);
	text-decoration: none;
}
.foot a.foot-mark:hover {
	color: var(--melon-deep);
}
.foot-dim {
	margin-right: auto;
}
/* Underlined: an undecorated link beside grey footer text is easy to miss. */
.foot a {
	color: var(--ink-2);
	text-decoration: underline;
	text-decoration-color: var(--rule);
	text-underline-offset: 0.25em;
}
.foot a:hover {
	color: var(--melon-deep);
	text-decoration-color: var(--melon-deep);
}

/* ── legal / long-form prose ─────────────────────────────── */

/* The rest of the site is one scannable page; this is the opposite: thirteen
   sections someone reads top to bottom, or lands in the middle of from a table
   of contents. So: a narrow measure, real paragraph rhythm (the global
   `p { margin: 0 }` is a landing-page convenience that prose can't use), and
   section headings dialed well down from the display h2. */

.legal {
	padding-block: clamp(3rem, 6vw, 5rem) clamp(4rem, 8vw, 7rem);
	background: var(--paper);
}
.legal-inner {
	max-width: 44rem;
}

.legal h1 {
	font-size: clamp(1.9rem, 1.4rem + 2vw, 2.9rem);
}

.legal-dates {
	margin-top: 1rem;
	display: flex;
	flex-wrap: wrap;
	gap: 0.35rem 1.5rem;
	font: 500 0.78rem / 1.5 var(--sans);
	color: var(--ink-3);
}

.legal .lede {
	max-width: none;
}

.legal p {
	margin-top: 1rem;
}
.legal p,
.legal li {
	color: var(--ink-2);
}

/* Numbered headings, so "§2" in the prose points somewhere findable. The rule
   above each is the section break; without it thirteen headings read as a wall. */
.legal h2 {
	margin-top: 3rem;
	padding-top: 1.4rem;
	border-top: 1px solid var(--frame);
	font-size: 1.32rem;
	color: var(--ink);
}
.legal h3 {
	margin-top: 1.9rem;
	color: var(--ink);
}
.legal h3 + p {
	margin-top: 0.6rem;
}

/* Anchor targets sit under the sticky topbar otherwise. */
.legal :is(h2, h3)[id] {
	scroll-margin-top: calc(var(--topbar-h) + 1.5rem);
}

.legal a {
	color: var(--pisc-deep);
	text-decoration: underline;
	text-decoration-color: var(--pisc-tint);
	text-underline-offset: 0.2em;
	transition: text-decoration-color var(--tr);
}
.legal a:hover {
	text-decoration-color: var(--pisc-deep);
}

.legal-toc {
	margin-top: 2.25rem;
	padding: 1.25rem 1.5rem;
	background: var(--surface);
	border-radius: var(--radius);
	box-shadow: var(--pin);
}
.legal-toc ol {
	margin: 0;
	padding-left: 1.6rem;
	columns: 2;
	column-gap: 2rem;
	font: 500 0.85rem / 1.9 var(--sans);
}
.legal-toc li {
	break-inside: avoid;
}
.legal-toc a {
	text-decoration: none;
	color: var(--ink-2);
}
.legal-toc a:hover {
	color: var(--melon-deep);
}

@media (max-width: 560px) {
	.legal-toc ol {
		columns: 1;
	}
}

.legal-list {
	margin-top: 1rem;
	padding-left: 1.25rem;
}
.legal-list li {
	margin-top: 0.5rem;
}
.legal-list strong {
	color: var(--ink);
}

/* Tighter term column than the homepage's 16rem: this measure is half as wide. */
.legal .plate {
	margin-top: 1.75rem;
}
.legal .plate > div {
	grid-template-columns: 11rem minmax(0, 1fr);
	gap: 1.25rem;
}

@media (max-width: 620px) {
	.legal .plate > div {
		grid-template-columns: 1fr;
		gap: 0.35rem;
	}
}

/* The three disclosures carriers audit for (frequency, rates, and the
   non-sharing pledge) plus the restatement of the pledge. Boxed so a reviewer
   skimming for them finds them without reading the section, and so they survive
   being quoted out of context. */
.legal-callout {
	margin-top: 1.25rem;
	padding: 1rem 1.25rem;
	background: var(--melon-wash);
	border: 1.5px solid var(--melon-tint);
	border-radius: var(--radius-sm);
	color: var(--ink-2);
}
.legal-callout strong {
	color: var(--ink);
}

.legal-pledge {
	background: var(--surface);
	border-left-color: var(--ink-rule);
	font-size: 0.95rem;
	line-height: 1.62;
}

/* ── invite form + consent disclosure ────────────────────── */

/* The page captures an email; the SMS consent form lives behind it. Fields are
   boxed in the same hard border as everything else: squared corners, offset
   shadow on focus rather than a glow, so inputs read as part of the chassis. */
.signup-form {
	margin-top: 2rem;
	max-width: 30rem;
	display: grid;
	gap: 1.1rem;
}
.field {
	display: grid;
	gap: 0.4rem;
}
.field label {
	font: 700 0.74rem / 1 var(--sans);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--ink-2);
}
.field input {
	font: 500 1rem / 1.4 var(--sans);
	color: var(--ink);
	background: var(--surface);
	border: 1.5px solid var(--line-strong);
	border-radius: var(--radius-sm);
	padding: 0.7rem 0.85rem;
	width: 100%;
	transition:
		box-shadow var(--tr),
		border-color var(--tr);
}
.field input::placeholder {
	color: var(--ink-3);
}
.field input:focus-visible {
	outline: none;
	border-color: var(--melon-deep);
	box-shadow: 0 0 0 3px rgba(242, 139, 73, 0.35);
}
/* :user-invalid only fires after the field has been touched, so the empty
   form doesn't shout on load. (The old :not(:placeholder-shown) guard never
   matched, since these inputs have no placeholders, and did exactly that.) */
.field input:user-invalid {
	border-color: var(--melon-deep);
}

.signup-form .btn-solid {
	justify-self: start;
}

/* Short consent note under a form. The carrier-vetting wall this replaced had
   to be exhaustive; this only has to be honest and quick to read. */
.signup-note {
	margin-top: 1.4rem;
	max-width: 34rem;
	font-size: 0.9rem;
	line-height: 1.65;
	color: var(--ink-3);
}
.signup-note a {
	color: var(--pisc-deep);
	text-decoration: underline;
	text-underline-offset: 0.2em;
}

.field-hint {
	margin-top: 0.1rem;
	font-size: 0.82rem;
	line-height: 1.5;
	color: var(--ink-3);
}

/* The box is a control, not a bullet, so it sits with the first line of the
   label rather than floating above the block. */
.consent {
	display: grid;
	grid-template-columns: auto minmax(0, 1fr);
	gap: 0.7rem;
	align-items: start;
	max-width: 34rem;
}
.consent input[type="checkbox"] {
	width: 1.15rem;
	height: 1.15rem;
	margin: 0.15rem 0 0;
	accent-color: var(--melon-deep);
	cursor: pointer;
}
.consent label {
	font-size: 0.9rem;
	line-height: 1.6;
	color: var(--ink-2);
	cursor: pointer;
	text-transform: none;
	letter-spacing: normal;
	font-family: var(--sans);
	font-weight: 400;
}

/* Invite gate on join.html: shown when ?key= is missing, malformed, unsigned,
   or expired. Every one of those is a dead end, so it takes the alert red
   rather than a neutral note — this is the only thing on the page, and it
   should read as "stop" at a glance.

   Same hard offset shadow the buttons and the chat panel use, in red. It pops
   because the chassis is already built that way, not because it's louder. */
/* The invite code, sitting above the waiting list. Mono and uppercased on
   sight so a code read off a card looks like the thing on the card; the value
   is uppercased again on the way out, and /api/join does it a third time. */
.code-form {
	margin-top: 2rem;
	max-width: 30rem;
}
.code-row {
	display: flex;
	align-items: stretch;
	gap: 0.6rem;
}
/* The border lives on the wrapper so the prefix and the field read as one
   control; the input inside is stripped bare and the focus ring is raised to
   the wrapper with :focus-within. */
.code-input {
	display: flex;
	align-items: center;
	background: var(--surface);
	border: 1.5px solid var(--line-strong);
	border-radius: var(--radius-sm);
	padding: 0.7rem 0.85rem;
	transition:
		box-shadow var(--tr),
		border-color var(--tr);
}
.code-input:focus-within {
	border-color: var(--melon-deep);
	box-shadow: 0 0 0 3px rgba(242, 139, 73, 0.35);
}
.code-input input {
	font: 400 1rem / 1.4 var(--mono);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--ink);
	background: none;
	border: 0;
	border-radius: 0;
	padding: 0;
	width: 5.5ch;
	min-width: 0;
}
.code-input input:focus-visible {
	outline: none;
	box-shadow: none;
}
.code-input input::placeholder {
	color: var(--ink-3);
	text-transform: none;
	opacity: 0.65;
}
.code-row .btn {
	flex: none;
}
.code-row .btn[disabled] {
	opacity: 0.55;
	cursor: default;
}

/* The waiting list, demoted to the fallback under the code field. The rule is
   the divider: what's above it is for people who already have an invite, what's
   below is for everyone else. */
.signup-alt {
	margin-top: 2.75rem;
	padding-top: 1.75rem;
	border-top: 1px solid var(--rule);
	color: var(--ink);
}
.signup-alt-lede {
	margin-top: 0.7rem;
	max-width: 58ch;
	color: var(--ink-2);
}

.gate {
	margin-top: 2rem;
	max-width: 34rem;
	padding: 1.15rem 1.35rem;
	background: var(--alert-wash);
	border: 2px solid var(--alert);
	border-radius: var(--radius-sm);
	box-shadow: var(--pin);
	transform: rotate(-0.4deg);
}
.gate p {
	font-size: 0.95rem;
	line-height: 1.65;
	color: var(--alert-deep);
}

/* The household name becomes an address, so it is set in the mono face here to
   match how it will look when they type it. */
.field-hint code {
	font-family: var(--mono);
	font-size: 0.95em;
	color: var(--pisc-deep);
}

/* "optional" beside a field label. Marking the one optional field is clearer
   than starring the four required ones: the exception is what people need
   told, and asterisks make a form look more demanding than it is. */
.field-optional {
	margin-left: 0.5rem;
	font-weight: 400;
	letter-spacing: 0.04em;
	text-transform: none;
	color: var(--ink-3);
}

/* A link as it arrives in a message: mono, underlined, clipped rather than
   wrapped. Real messaging apps never wrap a URL across three lines, and a
   demo that does reads as a mockup instead of a screenshot. */
.msg-link {
	display: inline-block;
	max-width: 100%;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	vertical-align: bottom;
	font-family: var(--mono);
	font-size: 0.86em;
	color: var(--pisc-deep);
	text-decoration: underline;
	text-underline-offset: 0.2em;
}

/* Items ticked off a list, inside one of Nellie's replies. Brand colour on the
   same reasoning as .msg-link above; the size and shift settle the glyph, which
   otherwise floats above the baseline of the word it belongs to. */
.tick-mark {
	color: var(--melon-deep);
	font-weight: 600;
	font-size: 0.95em;
	vertical-align: -0.02em;
	padding-left: 0.06em;
}

/* The household address inside a card. Same mono treatment the join form uses
   for it, so the one string that appears on both pages looks like one thing. */
.card code {
	font-family: var(--mono);
	font-size: 0.88em;
	color: var(--pisc-deep);
	white-space: nowrap;
}

/* A locked field still reads as a field, just not one you can argue with. The
   invite decides this address, and the server overrides whatever is submitted,
   so an editable box would only mislead. */
.field input.is-locked {
	background: var(--paper-alt);
	color: var(--ink-2);
	cursor: default;
}

/* ── 404 ─────────────────────────────────────────────────── */

/* Vertically centred, because there is nothing below it to scroll to. The
   min-height accounts for the sticky topbar so the block sits in the middle of
   what's actually visible rather than the middle of the document. */
.lost {
	display: grid;
	align-content: center;
	min-height: calc(100svh - var(--topbar-h) - 6rem);
	padding-block: clamp(3rem, 8vw, 6rem);
	background: var(--paper);
}
.lost-inner {
	max-width: 34rem;
}

/* The hero's bubbles outside a phone. .chat-log can't be reused here: it is
   flex-end inside a fixed-height screen, which is meaningless on a page. */
.lost-thread {
	list-style: none;
	margin: 1.9rem 0 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 0.35rem;
	max-width: 30rem;
}
.lost-thread .msg-body {
	max-width: 88%;
}

.lost-cta {
	margin-top: 2.25rem;
}

/* Hero signature. Separated from the facts above it by a hairline rather than
   another bullet: those are product facts, this is a person putting their name
   to it, and they shouldn't read as the same kind of statement. Quiet on
   purpose — provenance stops working the moment it looks like a badge. */
.hero-sig {
	margin-top: 1.5rem;
	padding-top: 1.1rem;
	border-top: 1px solid var(--rule);
	max-width: 26rem;
	font: 500 0.8rem / 1.5 var(--sans);
	color: var(--ink-3);
}

/* The address has to clear 4.5:1 against paper, which --ink-3 around it does
   not; the deeper melon carries the link on its own without turning a quiet
   signature into a call to action. */
.hero-sig a {
	color: var(--melon-deep);
	text-decoration: underline;
	text-underline-offset: 0.2em;
	text-decoration-thickness: 1px;
}
.hero-sig a:hover {
	color: var(--ink);
}
