/* OtsoVision.ai: Field-grade AI marketing site */

/* ---------- Fonts (self-hosted: no Google Fonts hotlinking; GDPR + CSP) ----------
   Files are passthrough-copied from @fontsource in .eleventy.js. */
@font-face { font-family: "Space Grotesk"; font-style: normal; font-weight: 400; font-display: swap; src: url("/fonts/space-grotesk-400.woff2") format("woff2"); }
@font-face { font-family: "Space Grotesk"; font-style: normal; font-weight: 500; font-display: swap; src: url("/fonts/space-grotesk-500.woff2") format("woff2"); }
@font-face { font-family: "Space Grotesk"; font-style: normal; font-weight: 600; font-display: swap; src: url("/fonts/space-grotesk-600.woff2") format("woff2"); }
@font-face { font-family: "Space Grotesk"; font-style: normal; font-weight: 700; font-display: swap; src: url("/fonts/space-grotesk-700.woff2") format("woff2"); }
@font-face { font-family: "Hanken Grotesk"; font-style: normal; font-weight: 400; font-display: swap; src: url("/fonts/hanken-grotesk-400.woff2") format("woff2"); }
@font-face { font-family: "Hanken Grotesk"; font-style: normal; font-weight: 500; font-display: swap; src: url("/fonts/hanken-grotesk-500.woff2") format("woff2"); }
@font-face { font-family: "Hanken Grotesk"; font-style: normal; font-weight: 600; font-display: swap; src: url("/fonts/hanken-grotesk-600.woff2") format("woff2"); }
@font-face { font-family: "Hanken Grotesk"; font-style: normal; font-weight: 700; font-display: swap; src: url("/fonts/hanken-grotesk-700.woff2") format("woff2"); }
@font-face { font-family: "Space Mono"; font-style: normal; font-weight: 400; font-display: swap; src: url("/fonts/space-mono-400.woff2") format("woff2"); }
@font-face { font-family: "Space Mono"; font-style: normal; font-weight: 700; font-display: swap; src: url("/fonts/space-mono-700.woff2") format("woff2"); }

/* ---------- Tokens ---------- */
:root {
  --font-display: 'Space Grotesk', system-ui, sans-serif;
  --font-body: 'Hanken Grotesk', system-ui, sans-serif;
  --font-mono: 'Space Mono', ui-monospace, monospace;

  /* dark (default) */
  --bg:      oklch(0.165 0.012 135);
  --bg-2:    oklch(0.205 0.013 135);
  --surface: oklch(0.235 0.014 135);
  --surface-2: oklch(0.275 0.015 135);
  --line:    oklch(0.34 0.012 135);
  --line-soft: oklch(0.28 0.012 135);
  --fg:      oklch(0.96 0.008 95);
  --muted:   oklch(0.74 0.012 115);
  --faint:   oklch(0.58 0.012 120);

  /* accents: equal chroma, varied hue */
  --accent:    oklch(0.76 0.13 142);   /* moss green */
  --accent-ink: oklch(0.22 0.05 142);
  --accent-2:  oklch(0.80 0.13 76);    /* amber */

  /* The bar is the launch announcement now, not the pre-launch warning.
     body padding-top, the fixed nav's inset and html scroll-padding-top
     all read this, so it is the one number to change to retire it. */
  --nav-h: 78px;
  /* The bar itself, and the space it occupies. env() resolves to 0 today:
     the viewport meta has no viewport-fit=cover, so iOS already insets the
     layout viewport above the home indicator. It is here so that adding
     viewport-fit=cover later is one edit rather than three. */
  --botnav-h: 64px;
  --botnav-space: calc(var(--botnav-h) + env(safe-area-inset-bottom, 0px));

  --radius: 14px;
  --radius-lg: 22px;
  --maxw: 1240px;
  --ease: cubic-bezier(.22,.61,.36,1);

  --shadow: 0 1px 0 oklch(1 0 0 / .04) inset, 0 18px 50px -24px oklch(0 0 0 / .7);

  /* The frosted surface shared by the header and every card that sits
     directly on the page. Defined once so "same as the top bar" stays true
     when it is retuned, instead of being five copies that drift. Cards
     inside a device mockup keep a solid fill — see .stage / .phone.

     Two numbers, both deliberate. The base is --surface, NOT --bg: tinted
     the same as the page a card becomes invisible wherever the backdrop
     has faded to flat colour, which is most of the viewport, leaving only
     its 1px border to say it is a card. A lighter base reads as a panel
     everywhere and as frosted glass over the photo.

     64% is as thin as the panels go while still carrying text: they are
     the only protection it has now. The forest reads mainly through the
     gaps between panels rather than through the glass. Measured on a
     panel: body 11.1:1, --muted 5.4:1, --faint 5.4:1, accent 5.5:1. */
  --glass: color-mix(in oklch, var(--surface) 64%, transparent);
  --glass-blur: blur(14px) saturate(1.2);
  --glass-pad: clamp(22px, 3vw, 34px);
}

/* theme: ember (amber-led) */
:root[data-theme="ember"] {
  --accent:   oklch(0.74 0.14 52);
  --accent-ink: oklch(0.22 0.06 52);
  --accent-2: oklch(0.80 0.12 95);
}
/* theme: frost (teal-led) */
:root[data-theme="frost"] {
  --accent:   oklch(0.78 0.11 188);
  --accent-ink: oklch(0.22 0.05 200);
  --accent-2: oklch(0.82 0.10 145);
}

/* light mode */
:root[data-mode="light"] {
  --bg:      oklch(0.965 0.008 100);
  --bg-2:    oklch(0.945 0.009 100);
  --surface: oklch(0.99 0.005 100);
  --surface-2: oklch(0.925 0.01 100);
  --line:    oklch(0.86 0.01 110);
  --line-soft: oklch(0.90 0.008 110);
  --fg:      oklch(0.22 0.02 135);
  --muted:   oklch(0.42 0.015 130);
  --faint:   oklch(0.55 0.012 125);
  /* Deepen the moss accent for light mode: the default (0.76) is tuned
     for the dark bg and fails contrast as button/eyebrow/link colour on
     light. This sits in the brand forest-green family. */
  --accent:  oklch(0.50 0.12 152);
  --accent-ink: oklch(0.99 0.02 142);
  --shadow: 0 1px 0 oklch(1 0 0 / .6) inset, 0 18px 40px -26px oklch(0 0 0 / .25);
}
:root[data-mode="light"][data-theme="ember"] { --accent: oklch(0.66 0.16 48); }
:root[data-mode="light"][data-theme="frost"] { --accent: oklch(0.68 0.12 200); }

/* font themes */
:root[data-type="editorial"] {
  --font-display: 'Hanken Grotesk', system-ui, sans-serif;
}
:root[data-type="mono"] {
  --font-display: 'Space Mono', ui-monospace, monospace;
}

* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; overflow-x: hidden; scroll-padding-top: calc(var(--nav-h) + 14px); }
body {
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--fg);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  transition: background .4s var(--ease), color .4s var(--ease);
}

/* Landing backdrop: the forest sits behind the whole page, not just the
   hero. Two layers on one box — the scrim first (topmost), the photo under
   it — both `fixed`, so they are positioned against the viewport and hold
   still while every section scrolls over them. The photo carries its own
   alpha fade (opaque down to a third of its height, then out to
   transparent), so it dissolves into --bg near the fold instead of ending
   on a hard edge, and nothing has to know the page's height.
   The scrim is not decoration: --fg is near-black here and the trunks are
   dark, so without it body copy passing through the top of the viewport
   would sit on noise. It is what keeps the photo a watermark rather than a
   picture competing with the text. The cards on top of it are frosted
   (--glass), not opaque, so they lighten the forest without hiding it;
   their worst-case text contrast is measured in the same pass. */
body.page-landing {
  --scrim: linear-gradient(
    180deg,
    color-mix(in oklch, var(--bg) 52%, transparent) 0%,
    color-mix(in oklch, var(--bg) 62%, transparent) 62%,
    color-mix(in oklch, var(--bg) 90%, transparent) 100%
  );
  --backdrop-photo: url("/img/wildcamlanding.webp");
}
/* A fixed pseudo-element, not `background-attachment: fixed`. iOS Safari
   honours neither iPhone nor iPad, so the photo was absent on every Apple
   device; the ≤940px block below used to work around that with
   `scroll` + `100% auto`, which sizes the photo to the page width and its
   own height and therefore covered a band at the top of the document and
   nothing below it. The scrim was then cranked to 86/92/100% to hide where
   it stopped, and between them the backdrop was gone by mid-page — taking
   every --glass panel with it, since 0.99 surface at 64% over a flat 0.965
   background is invisible. A fixed element is the workaround iOS does
   honour, and its box is the viewport, so `cover` resolves exactly as
   `attachment: fixed` did and the measured contrast below still holds.
   z-index -1 puts it above the canvas and below everything in flow. */
body.page-landing::before {
  content: "";
  position: fixed;
  top: 0; left: 0; right: 0;
  /* Height in viewport units, deliberately NOT `bottom: 0`. A phone's layout
     viewport grows when the URL bar collapses and shrinks when it returns,
     so a bottom-anchored box changes height mid-scroll, `cover` re-resolves
     against the new height, and the photo visibly jumps the instant
     scrolling stops — and jumps back on the way up. vh is pinned to the
     largest viewport and does not follow the toolbar, so the layer holds one
     size for the life of the page; lvh states that explicitly where it is
     understood, and is ignored where it is not. Do not "simplify" this back
     to inset: 0. */
  height: 100vh;
  height: 100lvh;
  z-index: -1;
  background-image: var(--scrim), var(--backdrop-photo);
  background-repeat: no-repeat, no-repeat;
  background-position: center top, center top;
  background-size: cover, cover;
}
/* The scrim above is set by contrast, not taste. Measured against the
   composited backdrop, its darkest point leaves body text at 10.8:1 and
   --muted at 5.3:1, both past WCAG AA — but the 12px --accent eyebrow
   would land at 3.6:1, under the 4.5 that small text needs, because the
   trunks are genuinely dark. Washing the photo out far enough to fix that
   erases it (AA arrives near 94%, leaving 6% of the picture), so the
   accent is deepened here instead: 4.97:1 over the photo and 7.2:1 on
   plain --bg, against 5.1:1 before. Same forest green, two steps darker.
   Landing-page scope only, so every other page keeps the palette it was
   designed with. --faint moves for the same reason: it is the footer
   headings and the marquee, the smallest text on the page, and it was the
   one that failed (2.75:1) when the scrim came down.

   Re-measured for this scrim with every text block on glass: body 11.1:1,
   --muted 5.4:1, --faint 5.4:1, the accent eyebrow 5.5:1. Change the
   scrim, the glass alpha or either of these two and re-run the numbers —
   they were chosen together, and the binding one is whichever text is
   smallest. Re-run again on 2026-09-09 against the darkest 5% of each row
   of BOTH photos, since the phone breakpoint now uses this same scrim
   rather than a heavier one: 11.4 / 5.6 / 5.6 / 5.7 for the full image and
   the same to two decimals for wildcamlanding-sm. The small file is the
   same picture at half scale, so the two cannot drift apart on their own —
   but they can if only one is ever replaced. */
body.page-landing {
  --accent: oklch(0.40 0.12 152);
  --faint: oklch(0.42 0.012 125);
}
img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; }

/* Text that sits on the landing backdrop sits on glass instead of bare.
   That is what lets the scrim be light enough for the forest to read: with
   every paragraph on its own panel, the only thing the scrim has to
   protect is the gaps between them, where there is nothing to protect.
   Bare copy was the ceiling before — the lede paragraphs bottomed out at
   4.1:1 as soon as the scrim dropped far enough to see the trees.
   Full-width bands (marquee, footer) take the fill without a radius so
   they read as strips rather than floating cards. */
.page-landing .hero-copy,
.page-landing .band-head,
.page-landing .faq {
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--glass-pad);
}
/* band-head's 64ch cap is a measure for the text; with a panel around it
   the padding has to live outside that, or the line length shrinks. */
.page-landing .band-head { max-width: calc(64ch + 2 * var(--glass-pad)); }
.page-landing .faq { padding-block: 6px var(--glass-pad); }
/* A full-width band that reads as its own surface — the app gallery uses
   it to separate the phone mockups from the sections either side. It was a
   solid --bg-2 fill inline on the section, which stopped the backdrop dead
   across the full width of the page. */
.band-fill {
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  border-block: 1px solid var(--line-soft);
}
.page-landing .trust,
.page-landing footer.ft {
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
}

/* ---------- Layout ---------- */
.wrap { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 0 28px; }
section { position: relative; }
.eyebrow {
  font-family: var(--font-mono);
  font-size: 12px; letter-spacing: .22em; text-transform: uppercase;
  color: var(--accent); display: inline-flex; align-items: flex-start; gap: 10px;
  max-width: 62ch; line-height: 1.5; text-wrap: balance;
}
.eyebrow::before {
  content: ""; width: 22px; height: 1px; background: var(--accent);
  display: inline-block; flex: 0 0 22px; margin-top: .75em;
}
@media (max-width: 700px) {
  /* Uppercase mono at .22em is what makes the long locales run to four lines
     on a phone; tightening the tracking buys back roughly a line. */
  .eyebrow { font-size: 11px; letter-spacing: .14em; gap: 8px; }
  .eyebrow::before { width: 16px; flex-basis: 16px; }
}
h1, h2, h3 { font-family: var(--font-display); font-weight: 600; line-height: 1.04; letter-spacing: -0.02em; }
.h-display { font-size: clamp(40px, 6.6vw, 84px); font-weight: 700; letter-spacing: -0.035em; }
.h-section { font-size: clamp(30px, 4.4vw, 52px); }
p.lede { font-size: clamp(17px, 1.5vw, 20px); color: var(--muted); max-width: 56ch; }
/* The pricing page's one selling sentence. Full-contrast ink and a
   tinted panel so it does not read as another grey subtitle. */
p.offer {
  font-size: clamp(18px, 1.7vw, 22px); font-weight: 600; color: var(--ink);
  max-width: 46ch; margin: 18px auto 0; padding: 14px 20px;
  border: 1px solid var(--accent); border-radius: var(--radius-lg);
  background: color-mix(in oklch, var(--accent) 10%, transparent);
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 14px 22px; border-radius: 999px; font-weight: 600; font-size: 15px;
  border: 1px solid transparent; transition: transform .15s var(--ease), background .2s, border-color .2s, color .2s;
  white-space: nowrap;
}
.btn:active { transform: translateY(1px) scale(.99); }
.btn-primary { background: var(--accent); color: var(--accent-ink); }
.btn-primary:hover { filter: brightness(1.06); }
.btn-ghost { border-color: var(--line); color: var(--fg); background: transparent; }
.btn-ghost:hover { border-color: var(--fg); background: oklch(1 0 0 / .04); }
:root[data-mode="light"] .btn-ghost:hover { background: oklch(0 0 0 / .03); }
.btn .arrow { transition: transform .2s var(--ease); }
.btn:hover .arrow { transform: translateX(3px); }

/* ---------- Placeholder imagery ---------- */
.ph {
  position: relative; overflow: hidden;
  background-color: var(--surface);
  background-image: repeating-linear-gradient(
    -45deg, oklch(1 0 0 / .035) 0 1px, transparent 1px 11px);
}
:root[data-mode="light"] .ph { background-image: repeating-linear-gradient(-45deg, oklch(0 0 0 / .05) 0 1px, transparent 1px 11px); }
.ph::after {
  content: attr(data-label);
  position: absolute; left: 14px; bottom: 12px;
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--faint);
}

/* ---------- Header ---------- */
/* The bar carries its translucent fill from the first paint, not from the
   first scroll. It used to be fully transparent at the top, which read as
   a missing header over the landing photo and made the links sit on bare
   forest. `#nav` still gains `.scrolled` past 24px — kept because the
   border is what separates the bar from content scrolled up under it, and
   at the very top there is nothing to separate from. */
header.nav {
  position: fixed; inset: 0 0 auto 0; z-index: 60;
  transition: background .3s var(--ease), border-color .3s, backdrop-filter .3s;
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid transparent;
}
header.nav.scrolled { border-bottom-color: var(--line-soft); }
.nav-inner { display: flex; align-items: center; justify-content: space-between; height: var(--nav-h); }
.brand { display: flex; align-items: center; gap: 11px; font-family: var(--font-display); font-weight: 700; font-size: 19px; letter-spacing: -0.02em; }
.brand .mark {
  width: 30px; height: 30px; position: relative; display: grid; place-items: center;
  border: 1.5px solid var(--accent); border-radius: 9px; transform: rotate(45deg);
}
.brand .mark::after { content: ""; width: 9px; height: 9px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 4px color-mix(in oklch, var(--accent) 22%, transparent); }
.brand .dot-ai { color: var(--accent); }
.nav-links { display: flex; align-items: center; gap: 30px; }
.nav-links a.nl { font-size: 14.5px; color: var(--muted); transition: color .18s; font-weight: 500; }
.nav-links a.nl:hover { color: var(--fg); }
/* stretch, not center: the language selector and the CTA are different
   elements with different padding and font sizes, so matching their
   heights by hand means they drift apart again the next time either one
   is restyled. Letting the flex line set the height keeps them equal by
   construction. */
.nav-cta { display: flex; align-items: stretch; gap: 14px; }

/* Bottom bar: the four primary links on phones. Hidden here and switched on
   in the 940px block, at exactly the width where .nav-links switches off, so
   the links live in the header or in this bar and never in neither. It is a
   sibling of header.nav, not a child: the header's backdrop-filter makes it
   the containing block for fixed descendants, so a fixed child would be
   trapped in the 78px header box.

   Denser than --glass, which is 64% surface tuned for the top bar over the
   landing scrim. This bar is wrong at that alpha in both directions: it
   passes over .trust and footer.ft, which are themselves --glass, so it
   would thicken and double-blur as it crossed them; and on the pricing and
   legal pages, where there is no photo, 64% of --surface (0.99) over --bg
   (0.965) is invisible. 90% reads the same everywhere and still lets the
   forest through. --line rather than --line-soft for the same reason: at
   0.90 on a 0.965 page the hairline carrying the whole bar disappears. */
.botnav { display: none; }

/* ---------- Hero ---------- */
.hero { padding: 150px 0 90px; position: relative; overflow: hidden; }
.hero-glow {
  position: absolute; z-index: 0; top: -8%; right: -6%; width: 760px; height: 760px; pointer-events: none;
  background: radial-gradient(circle at center, color-mix(in oklch, var(--accent) 30%, transparent), transparent 62%);
  filter: blur(20px); opacity: .5;
}
/* The photo already supplies colour and depth here, so the accent glow
   only muddies it. */
.hero .hero-glow { opacity: .22; }
.hero-grid { display: grid; grid-template-columns: 1.04fr 1fr; gap: 56px; align-items: center; position: relative; z-index: 1; }
.hero h1 {
  margin: 22px 0 0;
  /* Heading is a phrase rather than two stacked words now, so it wraps
     on content instead of a hard <br>. 84px only fits ~15 characters in
     this column, which would strand a word alone on the last line. */
  font-size: clamp(36px, 5.4vw, 64px);
  text-wrap: balance;
}
.hero h1 .grad { color: var(--accent); }
.hero p.lede { margin-top: 24px; }
.hero-actions { display: flex; gap: 14px; margin-top: 36px; flex-wrap: wrap; }
.hero-meta { display: flex; gap: 28px; margin-top: 40px; flex-wrap: wrap; }
.hero-meta .m { display: flex; flex-direction: column; gap: 2px; }
.hero-meta .m b { font-family: var(--font-display); font-size: 26px; font-weight: 700; letter-spacing: -0.02em; }
.hero-meta .m span { font-family: var(--font-mono); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--faint); }

/* hero feed visual */
.feed {
  position: relative; border-radius: var(--radius-lg); overflow: hidden;
  border: 1px solid var(--line); box-shadow: var(--shadow);
  /* Matches sample-deer.jpg (800x578) exactly. When the container
     and the photo disagree, object-fit:cover crops by a different
     amount at each viewport and the detection box slides off the
     animal: which is what it was doing. */
  aspect-ratio: 800 / 578;
}
.feed .ph { position: absolute; inset: 0; }
.feed-hud {
  position: absolute; inset: 0; padding: 16px; display: flex; flex-direction: column; justify-content: space-between;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .08em;
}
.feed-top { display: flex; justify-content: space-between; align-items: flex-start; color: oklch(0.96 0.008 95); }
.rec { display: inline-flex; align-items: center; gap: 7px; padding: 5px 10px; border-radius: 999px; background: oklch(0 0 0 / .5); backdrop-filter: blur(4px); }
.rec .blink { width: 7px; height: 7px; border-radius: 50%; background: #ff5a4d; animation: blink 1.4s infinite; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:.25} }
.feed-tag { padding: 5px 10px; border-radius: 8px; background: oklch(0 0 0 / .5); backdrop-filter: blur(4px); color: oklch(0.96 0.008 95); }

/* detection box */
.detbox {
  position: absolute; border: 1.5px solid var(--accent);
  border-radius: 4px; transition: all .8s var(--ease);
  box-shadow: 0 0 0 9999px oklch(0 0 0 / .18);
}
.detbox .corner { position: absolute; width: 13px; height: 13px; border: 2px solid var(--accent); }
.detbox .c-tl { top: -2px; left: -2px; border-right: 0; border-bottom: 0; }
.detbox .c-tr { top: -2px; right: -2px; border-left: 0; border-bottom: 0; }
.detbox .c-bl { bottom: -2px; left: -2px; border-right: 0; border-top: 0; }
.detbox .c-br { bottom: -2px; right: -2px; border-left: 0; border-top: 0; }
.detbox .det-label {
  position: absolute; top: -28px; left: -1.5px; white-space: nowrap;
  font-family: var(--font-mono); font-size: 11px; font-weight: 700;
  background: var(--accent); color: var(--accent-ink); padding: 3px 9px; border-radius: 5px;
}

/* floating alert chip */
.alert-chip {
  position: absolute; right: -22px; bottom: 54px; z-index: 3;
  display: flex; gap: 11px; align-items: center;
  background: var(--glass); backdrop-filter: var(--glass-blur);
  border: 1px solid var(--line);
  padding: 12px 15px; border-radius: 14px; box-shadow: var(--shadow);
  animation: floaty 5s ease-in-out infinite;
}
@keyframes floaty { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-9px)} }
.alert-chip .ic { width: 36px; height: 36px; border-radius: 10px; background: color-mix(in oklch, var(--accent) 20%, transparent); display: grid; place-items: center; color: var(--accent); flex: none; }
.alert-chip .t { font-size: 13px; font-weight: 600; }
.alert-chip .s { font-family: var(--font-mono); font-size: 10px; letter-spacing: .08em; color: var(--faint); text-transform: uppercase; }

/* ---------- Marquee trust strip ---------- */
.trust { border-block: 1px solid var(--line-soft); padding: 26px 0; overflow: hidden; }
.trust-inner { display: flex; align-items: center; gap: 56px; white-space: nowrap; width: max-content; animation: marquee 32s linear infinite; }
.trust:hover .trust-inner { animation-play-state: paused; }
@keyframes marquee { to { transform: translateX(-50%); } }
.trust .tt { font-family: var(--font-mono); font-size: 13px; letter-spacing: .1em; text-transform: uppercase; color: var(--faint); display: inline-flex; align-items: center; gap: 12px; }
.trust .tt b { color: var(--fg); font-weight: 700; }

/* ---------- Generic section ---------- */
.band { padding: 110px 0; }
.band-head { max-width: 64ch; }
.band-head h2 { margin: 18px 0 0; }
.band-head p { margin-top: 18px; }

/* ---------- How it works ---------- */
.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; margin-top: 56px; }
.step {
  position: relative; padding: 30px 26px 30px; border: 1px solid var(--line);
  border-radius: var(--radius); background: var(--glass);
  backdrop-filter: var(--glass-blur);
}
.step .num { font-family: var(--font-mono); font-size: 12px; color: var(--accent); letter-spacing: .15em; }
.step .step-ic { width: 46px; height: 46px; border-radius: 12px; display: grid; place-items: center; color: var(--accent); background: color-mix(in oklch, var(--accent) 14%, transparent); margin: 18px 0 18px; }
.step h3 { font-size: 21px; }
.step p { color: var(--muted); margin-top: 10px; font-size: 15px; }
.step .line-link { position: absolute; top: 44px; right: -11px; color: var(--line); z-index: 2; }
.steps .step:last-child .line-link { display: none; }

/* ---------- Feature showcase (tabs) ---------- */
.showcase { margin-top: 54px; display: grid; grid-template-columns: 340px 1fr; gap: 34px; align-items: stretch; }
.tabs { display: flex; flex-direction: column; gap: 8px; }
.tab {
  text-align: left; color: var(--muted);
  padding: 20px 20px; border-radius: var(--radius); transition: all .2s var(--ease);
  /* The tabs sit on the backdrop like every other card, so they take the
     same frosted fill. They were fully transparent with a transparent
     border, gaining a surface only on hover/active — which read as four
     unstyled labels once the page behind them stopped being flat colour.
     The 2px left edge stays: it is what the accent colours on selection. */
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--line-soft);
  border-left: 2px solid var(--line-soft);
}
.tab:hover { color: var(--fg); border-color: var(--line); }
/* Selection has to survive every tab being glass now, so it is a denser
   fill plus the accent edge rather than fill-versus-nothing. */
.tab.active {
  background: color-mix(in oklch, var(--surface) 84%, transparent);
  border-color: var(--line); border-left-color: var(--accent);
  color: var(--fg);
}
.tab .tk { display: flex; align-items: center; gap: 12px; font-family: var(--font-display); font-weight: 600; font-size: 17px; }
.tab .tk .tn { font-family: var(--font-mono); font-size: 11px; color: var(--faint); }
.tab .td { font-size: 14px; color: var(--muted); margin-top: 8px; opacity: 0; max-height: 0; overflow: hidden; transition: all .3s var(--ease); }
.tab.active .td { opacity: 1; max-height: 90px; margin-top: 10px; }

.stage {
  position: relative; border: 1px solid var(--line); border-radius: var(--radius-lg);
  background: var(--glass); backdrop-filter: var(--glass-blur);
  overflow: hidden; min-height: 460px; box-shadow: var(--shadow);
}
.panel { position: absolute; inset: 0; padding: 30px; opacity: 0; transform: translateY(14px); pointer-events: none; transition: opacity .45s var(--ease), transform .45s var(--ease); }
.panel.active { opacity: 1; transform: none; pointer-events: auto; }
.panel-grid { display: grid; gap: 14px; height: 100%; }

/* species ID panel */
.spec-grid { grid-template-columns: 1.3fr 1fr; }
.spec-feed {
  position: relative; border-radius: 12px; overflow: hidden;
  border: 1px solid var(--line);
  /* Was unset, so this stretched to fill its grid cell and reshaped
     between the desktop 2-column and mobile 1-column layouts. Pinning it
     to the photo's own ratio is what makes it scale on mobile and keeps
     the detection box on the animal. align-self stops the grid from
     stretching it back. */
  aspect-ratio: 800 / 578; align-self: start; width: 100%;
}
.spec-feed .ph { position: absolute; inset: 0; }
.spec-side { display: flex; flex-direction: column; gap: 12px; }
.id-card { border: 1px solid var(--line); border-radius: 12px; padding: 16px; background: var(--surface); }
.id-card .id-top { display: flex; justify-content: space-between; align-items: baseline; }
.id-card .sp { font-family: var(--font-display); font-weight: 600; font-size: 18px; }
.id-card .cf { font-family: var(--font-mono); font-size: 13px; color: var(--accent); }
.id-card .lt { font-family: var(--font-mono); font-size: 11px; color: var(--faint); font-style: italic; margin-top: 2px; }
.bar { height: 6px; border-radius: 999px; background: var(--surface-2); margin-top: 14px; overflow: hidden; }
.bar > i { display: block; height: 100%; background: var(--accent); border-radius: 999px; width: 0; transition: width 1s var(--ease); }
.taglist { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 14px; }
.taglist span { font-family: var(--font-mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); border: 1px solid var(--line); padding: 4px 9px; border-radius: 999px; }

/* alerts panel */
.alerts-list { display: flex; flex-direction: column; gap: 12px; }
.al {
  display: grid; grid-template-columns: 46px 1fr auto; gap: 14px; align-items: center;
  border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; background: var(--surface);
}
.al .thumb { width: 46px; height: 46px; border-radius: 9px; }
.al .at { font-weight: 600; font-size: 15px; }
.al .as { font-family: var(--font-mono); font-size: 11px; color: var(--faint); letter-spacing: .06em; margin-top: 3px; }
.al .badge { font-family: var(--font-mono); font-size: 10.5px; padding: 4px 9px; border-radius: 999px; letter-spacing: .06em; text-transform: uppercase; }
.al .badge.real { background: color-mix(in oklch, var(--accent) 18%, transparent); color: var(--accent); }
.al .badge.filtered { background: var(--surface-2); color: var(--faint); text-decoration: line-through; }
.al.dim { opacity: .5; }

/* patterns panel */
.chart { display: flex; align-items: flex-end; gap: 7px; height: 200px; padding: 18px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); }
.chart .col { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; gap: 6px; height: 100%; }
.chart .col i { width: 100%; background: var(--surface-2); border-radius: 4px 4px 2px 2px; transition: height .9s var(--ease); }
.chart .col i.hot { background: var(--accent); }
.chart .col span { font-family: var(--font-mono); font-size: 9px; text-align: center; color: var(--faint); }
.pat-note { display: flex; gap: 12px; align-items: center; margin-top: 14px; font-size: 14px; color: var(--muted); }
.pat-note b { color: var(--fg); }

/* search panel */
.search-box { display: flex; align-items: center; gap: 12px; border: 1px solid var(--line); border-radius: 12px; padding: 14px 18px; background: var(--surface); font-family: var(--font-mono); font-size: 14px; }
.search-box .cursor { width: 2px; height: 18px; background: var(--accent); animation: blink 1.1s steps(1) infinite; }
.search-res { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 14px; }
.search-res .r { aspect-ratio: 1; border-radius: 10px; position: relative; overflow: hidden; }
.search-res .r img { width: 100%; height: 100%; object-fit: cover; display: block; }
.search-res .r .pin { position: absolute; left: 8px; top: 8px; font-family: var(--font-mono); font-size: 9px; background: oklch(0 0 0 / .55); color: #fff; padding: 3px 6px; border-radius: 5px; }

/* ---------- App gallery ---------- */
.phones { display: flex; gap: 26px; justify-content: center; margin-top: 56px; flex-wrap: wrap; }
.phone {
  width: 240px; aspect-ratio: 9 / 19.3; border-radius: 34px; padding: 9px;
  background: linear-gradient(160deg, var(--surface-2), var(--bg-2)); border: 1px solid var(--line);
  box-shadow: var(--shadow); flex: none; transition: transform .35s var(--ease);
}
.phone:hover { transform: translateY(-10px); }
.phone:nth-child(2) { margin-top: -28px; }
.phone .screen { width: 100%; height: 100%; border-radius: 26px; overflow: hidden; background: var(--bg); position: relative; border: 1px solid var(--line-soft); }
.phone .notch { position: absolute; top: 8px; left: 50%; transform: translateX(-50%); width: 78px; height: 18px; background: oklch(0 0 0 / .6); border-radius: 999px; z-index: 5; }
.scr-head { padding: 28px 16px 14px; }
.scr-title { font-family: var(--font-display); font-weight: 700; font-size: 17px; }
.scr-sub { font-family: var(--font-mono); font-size: 10px; color: var(--faint); letter-spacing: .08em; text-transform: uppercase; margin-top: 2px; }
.scr-body { padding: 0 14px 16px; display: flex; flex-direction: column; gap: 10px; }
.scr-card { border: 1px solid var(--line); border-radius: 12px; overflow: hidden; background: var(--surface); }
.scr-card .img { aspect-ratio: 16/10; }
.scr-card .meta { padding: 9px 11px; }
.scr-card .meta .a { font-size: 12.5px; font-weight: 600; }
.scr-card .meta .b { font-family: var(--font-mono); font-size: 9px; color: var(--faint); margin-top: 2px; letter-spacing: .06em; }
/* The phone mockups mirror the app's own screens: the dashboard's overview
   card, the cameras screen's grid, the activity screen's chips and plan
   row. Kept to the same type scale as .scr-card so the three read as one
   device rather than three designs. */
.scr-card.ov { padding: 11px; }
.ov-nums { display: flex; gap: 18px; }
.ov-chips { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 9px; }
.ov-chips span {
  font-size: 9px; padding: 3px 7px; border-radius: 999px;
  background: color-mix(in oklch, var(--accent) 14%, transparent);
  color: var(--ink); white-space: nowrap;
}
.ov-weather {
  margin-top: 9px; padding-top: 8px; border-top: 1px solid var(--line);
  font-family: var(--font-mono); font-size: 9px; color: var(--faint);
  letter-spacing: .05em;
}
.cam-head { padding: 9px 11px 7px; }
.cam-head .a { font-size: 12.5px; font-weight: 600; }
.cam-head .b { font-family: var(--font-mono); font-size: 9px; color: var(--faint); margin-top: 2px; letter-spacing: .06em; }
.strip { display: grid; grid-template-columns: repeat(5, 1fr); gap: 3px; padding: 0 3px 3px; }
.strip img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 4px; display: block; }
.cam-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 3px; padding: 0 3px; }
.cam-grid img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 4px; display: block; }
.cam-all {
  padding: 9px 11px; font-size: 10.5px; font-weight: 600; color: var(--accent);
  border-top: 1px solid var(--line); margin-top: 3px;
}
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
.chips span {
  font-size: 9.5px; padding: 4px 9px; border-radius: 999px;
  border: 1px solid var(--line); color: var(--muted); white-space: nowrap;
}
.scr-card.plan { padding: 10px 11px; }
.scr-card.plan .a { font-size: 11.5px; font-weight: 600; }
.scr-card.plan .b { font-family: var(--font-mono); font-size: 9px; color: var(--faint); margin-top: 3px; letter-spacing: .06em; }
.mini-row { display: flex; gap: 8px; }
.mini-stat { flex: 1; border: 1px solid var(--line); border-radius: 10px; padding: 10px; background: var(--surface); }
.mini-stat b { font-family: var(--font-display); font-size: 17px; font-weight: 700; }
.mini-stat span { font-family: var(--font-mono); font-size: 8.5px; color: var(--faint); text-transform: uppercase; letter-spacing: .06em; display: block; margin-top: 1px; }
.scr-pill { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono); font-size: 9px; color: var(--accent); border: 1px solid color-mix(in oklch, var(--accent) 40%, var(--line)); padding: 4px 8px; border-radius: 999px; letter-spacing: .06em; text-transform: uppercase; width: max-content; }

/* (Hardware / R1-camera section removed: OtsoVision does not sell hardware.) */

/* ---------- FAQ ---------- */
.faq { max-width: 820px; margin: 50px auto 0; }
.qa { border-bottom: 1px solid var(--line); }
.qa-q { width: 100%; text-align: left; background: none; border: 0; color: var(--fg); padding: 24px 0; display: flex; justify-content: space-between; align-items: center; gap: 20px; font-family: var(--font-display); font-weight: 600; font-size: clamp(17px, 2vw, 20px); }
.qa-q .pm { flex: none; width: 26px; height: 26px; border: 1px solid var(--line); border-radius: 50%; display: grid; place-items: center; transition: transform .3s var(--ease), border-color .2s; }
.qa.open .qa-q .pm { transform: rotate(45deg); border-color: var(--accent); color: var(--accent); }
.qa-a { max-height: 0; overflow: hidden; transition: max-height .4s var(--ease); }
.qa-a p { color: var(--muted); padding-bottom: 24px; max-width: 70ch; font-size: 16px; }

/* ---------- CTA ---------- */
.cta-band { padding: 88px 0 110px; }
.cta-box {
  position: relative; overflow: hidden; border-radius: 28px; border: 1px solid var(--line);
  background: var(--glass); backdrop-filter: var(--glass-blur);
  padding: clamp(40px, 6vw, 76px); text-align: center;
}
.cta-box .hero-glow { opacity: .4; top: -30%; right: 10%; left: 10%; margin: auto; }
.cta-box h2 { font-size: clamp(32px, 5vw, 60px); position: relative; z-index: 1; }
.cta-box p { position: relative; z-index: 1; margin: 18px auto 0; }
.cta-actions { position: relative; z-index: 1; display: flex; gap: 14px; justify-content: center; margin-top: 34px; flex-wrap: wrap; }
/* (App-store badges removed: OtsoVision is a web app / installable PWA.) */

/* ---------- Footer ---------- */
footer.ft { border-top: 1px solid var(--line); padding: 64px 0 50px; }
.ft-grid { display: grid; grid-template-columns: 1.8fr 1fr 1fr; gap: 40px; }
.ft-grid .col h4 { font-family: var(--font-mono); font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--faint); margin-bottom: 16px; font-weight: 700; }
.ft-grid .col a { display: block; color: var(--muted); font-size: 14.5px; padding: 6px 0; transition: color .18s; }
.ft-grid .col a:hover { color: var(--fg); }
.ft-grid .col.brandcol p { color: var(--muted); font-size: 14.5px; margin-top: 14px; max-width: 34ch; }
.ft-bottom { display: flex; justify-content: space-between; align-items: center; margin-top: 50px; padding-top: 26px; border-top: 1px solid var(--line-soft); flex-wrap: wrap; gap: 14px; }
.ft-bottom .cp { font-family: var(--font-mono); font-size: 12px; color: var(--faint); letter-spacing: .04em; }

/* ---------- Reveal ---------- */
.reveal { opacity: 0; transform: translateY(26px); transition: opacity .7s var(--ease), transform .7s var(--ease); }
.reveal.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  * { animation: none !important; }
}

/* ---------- Responsive ---------- */
@media (max-width: 940px) {
  .hero-grid { grid-template-columns: 1fr; gap: 40px; }
  .showcase { grid-template-columns: 1fr; }
  .steps { grid-template-columns: 1fr; }
  .step .line-link { display: none; }
  .ft-grid { grid-template-columns: 1fr 1fr; }
  .nav-links { display: none; }
  .botnav {
    display: flex;
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
    height: var(--botnav-h);
    background: color-mix(in oklch, var(--surface) 90%, transparent);
    /* Safari only unprefixed backdrop-filter in 18. Without this the bar is
       a flat fill on older iOS — as the other ten uses in this file still
       are. */
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    border-top: 1px solid var(--line);
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  /* min-width:0 overrides the flex default of auto, without which a long
     label widens its item instead of shrinking — and without which the
     ellipsis below never engages. */
  .botnav a {
    flex: 1 1 0; min-width: 0;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center; gap: 4px;
    padding: 0 4px;
    font-size: clamp(11px, 3.2vw, 12.5px); font-weight: 500; line-height: 1.15;
    color: var(--muted); text-align: center;
    -webkit-tap-highlight-color: transparent;
  }
  /* Last resort: an over-long or untranslated label shortens rather than
     breaking the row. fi "Ominaisuudet" is the binding case, and the clamp
     above is what actually keeps it inside a 320px screen. */
  .botnav a > span {
    display: block; max-width: 100%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .botnav a[aria-current="page"] { color: var(--accent); font-weight: 600; }
  /* Scoped, not global: the site has no focus indicator anywhere, and adding
     one everywhere is a visible change to every button on every page. Inset,
     because at a positive offset the viewport edge clips the ring. */
  .botnav a:focus-visible { outline: 2px solid var(--accent); outline-offset: -4px; border-radius: 8px; }
  body { padding-bottom: var(--botnav-space); }
  /* Fragment links land via scroll-padding-top and are unaffected. This is
     for sequential focus navigation, which shares the scroll-into-view
     algorithm: tabbing the FAQ buttons (faq-dark.njk, on the landing page)
     otherwise parks each one under the bar. */
  html { scroll-padding-bottom: calc(var(--botnav-space) + 14px); }
  /* margin-bottom shifts a fixed box with bottom:0 up by exactly that much,
     and margin is not in the banner's inline style attribute, so this needs
     no !important and no change to base.njk. */
  #ts-cookie-banner { margin-bottom: var(--botnav-space); }
  .hero { padding-top: 120px; }
  .phone:nth-child(2) { margin-top: 0; }
  /* Half-size photo; the pseudo-element above handles the rest, so this is
     the whole mobile difference now. The scrim stays as measured. */
  body.page-landing { --backdrop-photo: url("/img/wildcamlanding-sm.webp"); }
}
@media (max-width: 560px) {
  .wrap { padding: 0 18px; }
  .ft-grid { grid-template-columns: 1fr; }
  .nav-cta .btn-ghost { display: none; }
  .spec-grid { grid-template-columns: 1fr; }
  .search-res { grid-template-columns: repeat(2, 1fr); }
  .alert-chip { right: 0; }
}

/* =========================================================================
   Carried over from the old Tailwind layer (legal/auth pages were styled
   purely via these wrapper classes) + new components for the ported
   pricing and signup pages. Rewritten as plain CSS on the design tokens.
   ========================================================================= */

/* Brand logo (replaces the CSS .mark glyph) */
/* 70px in a 78px bar leaves 4px of air above and below — deliberate, and the
   reason the narrow-viewport rule below is not raised with it. */
.brand img { height: 70px; width: auto; }
footer.ft .brand img { height: 70px; }

/* Language switcher (replaces the old Tailwind .lang-switcher component) */
.lang-switcher {
  font-family: var(--font-body); font-size: 13.5px; color: var(--muted);
  background: transparent; border: 1px solid var(--line);
  border-radius: 999px; cursor: pointer;
  transition: border-color .2s, color .2s;
  /* Vertical padding removed on purpose: height now comes from the
     flex line so it tracks the button. A select centres its own text. */
  padding: 0 12px; text-align: center;
}
.lang-switcher:hover { border-color: var(--accent); color: var(--fg); }
/* Was display:none below 560px, which removed the only way to change
   language on a phone. Flag-only options keep it inside the row. */
@media (max-width: 560px) {
  .lang-switcher { padding: 0 8px; font-size: 16px; }
  /* The logo is 3.36:1, so 45px tall is 151px wide. On a 360px viewport that
     plus the (now visible) language selector and the CTA already fills the
     bar; the 25% the wide rule gained would be 189px of a 360px screen and
     overflow it, so this one holds. */
  .brand img { height: 45px; }
}

/* Container alias (the design uses .wrap; legacy markup uses .container) */
.container { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 0 28px; }

/* Button extras used by auth / signup / pricing */
.btn-outline { border-color: var(--line); color: var(--fg); background: transparent; }
.btn-outline:hover { border-color: var(--accent); color: var(--accent); }
.btn-block { width: 100%; justify-content: center; }
.btn-lg { padding: 16px 26px; font-size: 16px; }

/* PWA note (replaces the app-store badges in the CTA) */
.hero-note { font-family: var(--font-mono); font-size: 12px; letter-spacing: .04em; color: var(--faint); margin-top: 22px; }

/* ---------- Pricing cards ---------- */
.price-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(255px, 1fr)); gap: 22px; max-width: 1120px; margin: 54px auto 0; }
.price-card { position: relative; display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--bg-2); padding: 32px; }
.price-card.popular { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
/* A tier that cannot be bought yet: a neutral pill rather than the accent
   one, so it does not compete with "most popular" next to it. The card
   carries no CTA, and grid stretches it to its neighbours' height anyway. */
.price-card.soon .pc-badge.soon { background: var(--surface-2); color: var(--muted); }
.price-card .pc-badge { position: absolute; top: -12px; right: 24px; font-family: var(--font-mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: .08em; background: var(--accent); color: var(--accent-ink); padding: 4px 10px; border-radius: 999px; }
.price-card .pc-name { font-family: var(--font-display); font-weight: 700; font-size: 22px; }
.price-card .pc-tagline { color: var(--muted); font-size: 14px; margin-top: 4px; }
.price-card .pc-price { display: flex; align-items: baseline; gap: 8px; margin-top: 22px; }
.price-card .pc-price b { font-family: var(--font-display); font-size: 44px; font-weight: 700; letter-spacing: -0.02em; }
.price-card .pc-price span { font-family: var(--font-mono); font-size: 12px; color: var(--faint); text-transform: uppercase; letter-spacing: .06em; }
.price-card .pc-annual { font-size: 13px; color: var(--muted); margin-top: 6px; }
.price-card .pc-annual b { color: var(--accent); font-weight: 600; }
.price-card ul { list-style: none; margin-top: 26px; display: flex; flex-direction: column; gap: 12px; }
.price-card li { display: flex; align-items: flex-start; gap: 10px; font-size: 14.5px; color: var(--muted); }
.price-card li svg { color: var(--accent); flex: none; margin-top: 3px; }
.price-notes { max-width: 660px; margin: 32px auto 0; text-align: center; display: flex; flex-direction: column; gap: 8px; color: var(--muted); font-size: 14px; }
.price-notes .vat { font-size: 12px; color: var(--faint); }

/* ---------- Signup split ---------- */
.signup-split { display: grid; grid-template-columns: 1fr 1fr; max-width: 980px; margin: 40px auto; border: 1px solid var(--line); border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow); }
.signup-split .ss-pitch { background: linear-gradient(160deg, color-mix(in oklch, var(--accent) 20%, var(--bg-2)), var(--bg-2)); padding: 48px 40px; display: flex; flex-direction: column; justify-content: space-between; gap: 28px; }
.signup-split .ss-pitch h1 { font-size: clamp(28px, 3vw, 36px); }
.signup-split .ss-pitch p { color: var(--muted); margin-top: 14px; }
.signup-split .ss-proof { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 12px; color: var(--faint); text-transform: uppercase; letter-spacing: .06em; }
.signup-split .ss-form { background: var(--surface); padding: 48px 40px; display: flex; flex-direction: column; justify-content: center; }
.signup-split .ss-form h2 { font-size: 24px; font-weight: 700; }
.signup-split .ss-form p { color: var(--muted); margin-top: 8px; }

/* ---------- Legal / policy pages ---------- */
.legal-page { width: 100%; max-width: 768px; margin: 0 auto; padding: 96px 20px 64px; }
.legal-page h1 { font-size: 30px; font-weight: 700; margin-bottom: 16px; }
.legal-page h2 { font-size: 22px; font-weight: 700; margin: 40px 0 12px; }
.legal-page h3 { font-size: 18px; font-weight: 600; margin: 24px 0 8px; }
.legal-page p, .legal-page li { color: var(--muted); line-height: 1.7; }
.legal-page ul, .legal-page ol { margin: 16px 0; padding-left: 24px; }
.legal-page ul { list-style: disc; }
.legal-page ol { list-style: decimal; }
.legal-page li { margin-top: 6px; }
.legal-page a { color: var(--accent); text-decoration: underline; }
.legal-page a:hover { opacity: .8; }
.legal-page strong { font-weight: 600; color: var(--fg); }
.legal-page table { width: 100%; border-collapse: collapse; font-size: 14px; margin: 20px 0; }
.legal-page th, .legal-page td { border: 1px solid var(--line); padding: 8px 12px; text-align: left; vertical-align: top; }
.legal-page th { background: var(--surface-2); font-weight: 600; color: var(--fg); }
.legal-meta { margin-bottom: 32px; font-size: 14px; color: var(--muted); }

/* ---------- Auth hand-off pages (login / logout / signup fallback) ---------- */
.auth-section { width: 100%; max-width: 28rem; margin: 0 auto; min-height: 70vh; display: flex; flex-direction: column; justify-content: center; padding: 96px 20px 48px; }
.auth-card { border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface); padding: 32px; text-align: center; box-shadow: var(--shadow); }
.auth-card h1 { font-size: 24px; font-weight: 700; margin-bottom: 8px; }
.auth-subtitle { color: var(--muted); margin-bottom: 24px; }
.auth-footer { margin-top: 24px; font-size: 14px; color: var(--muted); }
.auth-footer a { color: var(--accent); text-decoration: underline; }

/* Landscape phone: 38 banner + 78 header + 64 bar is 180px of fixed chrome
   on a ~360px-tall viewport. Shrink rather than hide — hiding puts the links
   back out of reach, which is the whole reason the bar exists. */
@media (max-width: 940px) and (max-height: 500px) {
  :root { --botnav-h: 46px; }
  .botnav a svg { display: none; }
}

/* The site had no print styles, so the fixed bars printed over the content —
   worst on the legal pages, which are the ones anyone actually prints. The
   consent banner needs !important because site.js sets display:flex inline,
   and an author !important declaration outranks a normal inline one. */
@media print {
  header.nav, .botnav, #ts-cookie-banner { display: none !important; }
  body { padding-top: 0; padding-bottom: 0; }
}

@media (max-width: 760px) {
  .price-grid { grid-template-columns: 1fr; }
  .signup-split { grid-template-columns: 1fr; }
}

/* Activity heat map in the app gallery's insights screen.
   Replaces an empty grey placeholder. 7 rows (Mon-Sun) x 24 hour columns,
   plus an hour-of-day bar row underneath, both driven by real data in
   src/_data/activity.json. Sized in fr units so it scales with the phone
   mockup instead of needing its own breakpoints. */
.heat { padding: 7px 8px 6px; display: grid; gap: 5px; background: var(--surface); }
.heat-grid {
  display: grid; grid-template-columns: repeat(24, 1fr);
  grid-auto-rows: 1fr; gap: 1px; aspect-ratio: 24 / 8;
}
.heat-grid i { border-radius: 1px; background: var(--accent); display: block; }
/* Five buckets rather than 168 inline opacities. Level 0 stays visible so the
   grid reads as a grid, not as holes. */
.heat-grid i.h0 { opacity: .07; }
.heat-grid i.h1 { opacity: .22; }
.heat-grid i.h2 { opacity: .45; }
.heat-grid i.h3 { opacity: .70; }
.heat-grid i.h4 { opacity: 1; }
.heat-bars {
  display: grid; grid-template-columns: repeat(24, 1fr); gap: 1px;
  align-items: end; height: 22px;
}
.heat-bars i {
  display: block; background: var(--accent); opacity: .55;
  border-radius: 1px 1px 0 0; min-height: 1px;
}
.heat-axis {
  display: flex; justify-content: space-between;
  font-family: var(--font-mono); font-size: 7px; color: var(--faint);
  letter-spacing: .04em;
}

