/* GeoNotes landing page - "Nightfall".
   Dark, near-black surface with the app's mint accent. Committed to a single
   dark look, so there is no prefers-color-scheme branch here on purpose. */

:root {
  --bg: #0a0e0b;
  --surface: #10160f;
  --surface-2: #161f15;
  --line: #23301f;
  --fg: #e8f0e6;
  --muted: #93a48f;
  --accent: #7ee2a8;
  --accent-dim: #4f9d70;
  /* Every failure state was reaching for this by hand. Named so the error panel
     can build tints from it rather than adding a fourth copy of the hex. */
  --danger: #f2a2a2;
  --radius: 16px;
  --measure: 68ch;
  /* The sticky header's own height: a 68px row plus its 1px bottom border.
     Named because the desktop hero subtracts it to size itself against the
     first screen, and the two must not drift apart. */
  --header-h: 69px;
  /* How far past the small viewport a screen-sized section has to run before
     nothing of the next one shows underneath the browser's own furniture. Three
     terms, because no single one of them is the whole obstruction:

     100lvh - 100svh is the retractable chrome, and on a desktop browser it is
     zero. On an iPhone it turned out to be most of the answer but not all of
     it: measured against lvh alone, the section still stopped a line of the
     next heading short of the bottom, in Edge and in Safari both.

     env(safe-area-inset-bottom) is the home indicator's strip, which a phone
     keeps clear of the viewport rather than counting inside it.

     4vh is honest slack. What is left over after the first two is a number this
     stylesheet cannot read, it differs between browsers that all call their bar
     an address bar, and it is not worth another round of measuring on a device
     to shave. Overshooting is free: every section on the page is painted the
     same near-black, so a section that runs long past the fold looks like
     nothing at all, and only the next flick is a few pixels longer. */
  --screen-bleed: calc(100lvh - 100svh + env(safe-area-inset-bottom, 0px) + 4vh);
  --sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

* { box-sizing: border-box; }

/* scroll-padding keeps the sticky header out of the way of anything that
   scrolls itself into view: an #anchor from the nav, and the snap positions
   set up further down. Without it both land a section's first line behind
   the bar. */
html {
  scroll-behavior: smooth;
  scroll-padding-top: var(--header-h);
  /* The contact modal locks the page with overflow: hidden, which would take
     the scrollbar away with it and slide the whole layout sideways behind the
     blur. Reserving the gutter keeps the page the same width either way. */
  scrollbar-gutter: stable;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); }

img { max-width: 100%; height: auto; display: block; }

.wrap {
  width: min(1120px, 100% - 3rem);
  margin-inline: auto;
}

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

.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
  background: rgba(10, 14, 11, 0.85);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}

.site-header .wrap {
  display: flex;
  align-items: center;
  gap: 1rem;
  min-height: calc(var(--header-h) - 1px); /* the border makes up the rest */
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  font-weight: 700;
  font-size: 1.05rem;
  letter-spacing: -0.01em;
  color: var(--fg);
  text-decoration: none;
}

.brand img { width: 30px; height: 30px; border-radius: 8px; }

.site-header nav {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 1.5rem;
  font-size: 0.93rem;
}

.site-header nav a {
  color: var(--muted);
  text-decoration: none;
}

.site-header nav a:hover { color: var(--fg); }

.site-header nav .btn-sm {
  padding: 0.4rem 0.95rem;
  border-radius: 999px;
  background: var(--accent);
  color: #06120b;
  font-weight: 600;
}

.site-header nav .btn-sm:hover { color: #06120b; background: #96eeb9; }

/* ---- device-language hint -------------------------------------------- */

/* Ships hidden on every page and is revealed by assets/language.js for a
   visitor whose device speaks another live language. Above the sticky header
   rather than over it, so it pushes the page down once instead of covering the
   nav for as long as it is up. */
.lang-hint {
  margin: 0;
  padding: 0.7rem 1.5rem;
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.9rem;
  text-align: center;
}

.lang-hint a { color: var(--accent); }

.lang-hint-close {
  margin-left: 0.6rem;
  border: 0;
  background: none;
  color: inherit;
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}

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

.hero {
  position: relative;
  padding: clamp(3.5rem, 9vw, 7rem) 0 clamp(3rem, 7vw, 5rem);
  overflow: hidden;
}

/* Soft radial glow behind the hero, echoing the accent colour of the FAB
   in the app itself. Purely decorative. */
.hero::before {
  content: "";
  position: absolute;
  top: -30%;
  left: 50%;
  width: 900px;
  height: 900px;
  transform: translateX(-50%);
  background: radial-gradient(circle, rgba(126, 226, 168, 0.13), transparent 62%);
  pointer-events: none;
}

.hero .wrap {
  position: relative;
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
}

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--accent);
  border: 1px solid var(--line);
  background: var(--surface);
  padding: 0.32rem 0.8rem;
  border-radius: 999px;
  margin-bottom: 1.6rem;
}

h1 {
  font-size: clamp(2.4rem, 6vw, 3.9rem);
  line-height: 1.05;
  letter-spacing: -0.035em;
  margin: 0 0 1.2rem;
  font-weight: 700;
}

h1 em {
  font-style: normal;
  color: var(--accent);
}

.hero p.sub {
  font-size: clamp(1.05rem, 2vw, 1.2rem);
  color: var(--muted);
  max-width: 46ch;
  margin: 0 0 2.2rem;
}

/* ---- calls to action ------------------------------------------------- */

.cta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
}

/* The closing section is centred throughout, so its button row is too. The
   hero shares .cta-row and stays left, under its own column of text. */
.closing .cta-row { justify-content: center; }

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.85rem 1.6rem;
  border-radius: 999px;
  background: var(--accent);
  color: #06120b;
  font-weight: 650;
  font-size: 1rem;
  text-decoration: none;
  transition: transform 120ms ease, background 120ms ease;
}

.btn:hover { background: #96eeb9; transform: translateY(-1px); }

.btn svg { width: 18px; height: 18px; }

.cta-note {
  width: 100%;
  font-size: 0.85rem;
  color: var(--muted);
  margin: 0.2rem 0 0;
}

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

.device {
  justify-self: center;
  width: min(320px, 100%);
  border: 1px solid var(--line);
  border-radius: 34px;
  padding: 8px;
  background: var(--surface);
  box-shadow: 0 40px 90px -30px rgba(0, 0, 0, 0.9), 0 0 0 1px rgba(126, 226, 168, 0.06);
}

.device img { border-radius: 26px; }

/* ---- sections -------------------------------------------------------- */

/* Applied to both edges, so adjacent sections stack it: the visible gap is
   twice this value plus whatever bottom padding the last card carries. */
section { padding: clamp(2.25rem, 4.5vw, 3.75rem) 0; }

.section-head {
  margin-bottom: 2rem;
  text-align: center;
}

h2 {
  font-size: clamp(1.7rem, 3.6vw, 2.4rem);
  line-height: 1.15;
  letter-spacing: -0.025em;
  margin: 0 0 0.8rem;
}

/* The measure is 68ch resolved at this paragraph's own 17px, so it is a
   paragraph's measure and it caps the paragraph only. Held on .section-head it
   also bound the h2, which sets more than twice as large: the same box ran out
   after roughly half as many characters and wrapped headlines that had the
   full width of the wrap to sit on. The h2 is left to the wrap. */
.section-head p {
  max-width: var(--measure);
  /* The measure box is narrower than the wrap, so it has to be centred itself:
     centring the text inside a box still pinned left only moves the ragged
     edges. */
  margin: 0 auto;
  color: var(--muted);
}

.features {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.2rem;
}

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.6rem;
}

.card .icon {
  width: 38px;
  height: 38px;
  border-radius: 10px;
  display: grid;
  place-items: center;
  background: var(--surface-2);
  color: var(--accent);
  margin-bottom: 1rem;
}

.card .icon svg { width: 20px; height: 20px; }

.card h3 {
  margin: 0 0 0.45rem;
  font-size: 1.06rem;
  letter-spacing: -0.01em;
}

.card p { margin: 0; color: var(--muted); font-size: 0.95rem; }

/* Privacy pledge block, given more weight than an ordinary feature card. */
.pledge {
  border: 1px solid var(--accent-dim);
  border-radius: var(--radius);
  background: linear-gradient(180deg, rgba(126, 226, 168, 0.07), transparent);
  padding: clamp(1.8rem, 4vw, 3rem);
}

.pledge ul {
  list-style: none;
  margin: 1.5rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 0.8rem 2rem;
}

.pledge li {
  position: relative;
  padding-left: 1.9rem;
  color: var(--muted);
}

/* The closing paragraph, set off from the list above it. Held here rather than
   as an inline style on the element, which the compact rules further down could
   not have overridden. */
.pledge ul + p { margin-top: 1.8rem; }

.pledge li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 11px;
  height: 6px;
  border-left: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
  transform: rotate(-45deg);
}

/* ---- screenshot carousel --------------------------------------------- */

/* Card width drives every other measurement, including the stage height, so
   the layout reserves its space before the script positions anything. The
   30vh term keeps a tall phone screenshot from filling a short window.

   --cf-spread is how far apart neighbouring cards sit, as a percentage of a
   card's own width. It lives here rather than in carousel.js because it is the
   one number that has to change with the viewport: a phone has no room to
   spare, a wide desktop looks cramped without it. The script reads it back to
   place the cards. It is deliberately not the swipe distance, which stays a
   full card width at every breakpoint so the gesture feels the same. */
.coverflow {
  --cf-w: min(270px, 62vw, 30vh);
  --cf-ratio: 2.031; /* 1523 / 750, the shipped screenshot proportions */
  --cf-spread: 56;
  position: relative;
}

/* Base state, and the whole experience if the script never runs: an ordinary
   horizontal scroller. Everything 3D below is gated behind [data-ready],
   which carousel.js sets only once it has taken over. */
.cf-stage {
  display: flex;
  gap: 1.2rem;
  overflow-x: auto;
  padding-bottom: 1.5rem;
  scroll-snap-type: x mandatory;
  scrollbar-width: thin;
}

.cf-slide {
  margin: 0;
  flex: 0 0 var(--cf-w);
  scroll-snap-align: center;
}

.cf-slide img {
  border-radius: 18px;
  border: 1px solid var(--line);
}

.cf-slide figcaption {
  margin-top: 0.7rem;
  font-size: 0.85rem;
  color: var(--muted);
  text-align: center;
}

.cf-controls { display: none; }

/* ---- carousel, enhanced ---- */

.coverflow[data-ready] .cf-stage {
  display: block;
  position: relative;
  height: calc(var(--cf-w) * var(--cf-ratio) + 2.6rem);
  /* Side cards are pushed beyond the container; without this they would widen
     the page on narrow screens. */
  overflow: hidden;
  padding-bottom: 0;
  perspective: 1400px;
  scroll-snap-type: none;
  touch-action: pan-y; /* horizontal is ours, vertical still scrolls the page */
  cursor: grab;
}

.coverflow[data-ready] .cf-stage.is-dragging { cursor: grabbing; }

.coverflow[data-ready] .cf-slide {
  position: absolute;
  top: 0;
  left: 50%;
  width: var(--cf-w);
  margin-left: calc(var(--cf-w) / -2);
  transition: transform 0.55s cubic-bezier(0.22, 0.75, 0.24, 1),
              opacity 0.4s ease;
  will-change: transform;
}

/* While a finger is down the cards must track it exactly, not ease towards it. */
.coverflow[data-ready] .cf-stage.is-dragging .cf-slide { transition: none; }

.coverflow[data-ready] .cf-shot { position: relative; }

/* Dimming the off-centre cards reads as depth and keeps the image sharp,
   which a blur filter would not. The script sets --cf-dim per card. */
.coverflow[data-ready] .cf-shot::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 18px;
  background: var(--bg);
  opacity: var(--cf-dim, 0);
  transition: opacity 0.4s ease;
  pointer-events: none;
}

.coverflow[data-ready] .cf-slide img {
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
  /* The whole card is a drag surface, so the image must not swallow events. */
  pointer-events: none;
  user-select: none;
}

.coverflow[data-ready] .cf-slide.is-active img { border-color: var(--accent-dim); }

.coverflow[data-ready] .cf-slide figcaption {
  opacity: 0;
  transition: opacity 0.35s ease;
}

.coverflow[data-ready] .cf-slide.is-active figcaption {
  opacity: 1;
  color: var(--fg);
}

.coverflow[data-ready] .cf-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  margin-top: 1.3rem;
}

.cf-btn {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--fg);
  cursor: pointer;
  transition: border-color 0.2s ease, color 0.2s ease;
}

.cf-btn:hover {
  border-color: var(--accent-dim);
  color: var(--accent);
}

.cf-btn svg {
  width: 18px;
  height: 18px;
}

.cf-dots {
  display: flex;
  align-items: center;
  gap: 0.45rem;
}

.cf-dot {
  width: 7px;
  height: 7px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--line);
  cursor: pointer;
  transition: background 0.25s ease, width 0.25s ease;
}

.cf-dot[aria-current="true"] {
  width: 22px;
  background: var(--accent);
}

.cf-btn:focus-visible,
.cf-dot:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* On a phone the cards are swiped directly and there is no cursor to reveal an
   arrow to, so the buttons are dead weight competing with the dots for a narrow
   row. The dots stay: they still report position and give a way to jump. */
@media (max-width: 860px) {
  .cf-btn { display: none; }
}

/* Desktop: spend the room. The cards fan out further, and the arrows leave the
   row under the carousel for its outer edges, where they sit clear of the
   widest card instead of stacking more furniture below it. */
@media (min-width: 861px) {
  .coverflow { --cf-spread: 78; }

  .coverflow[data-ready] .cf-btn {
    position: absolute;
    /* Centred on the image, not on the stage, whose height also covers the
       caption line below it. */
    top: calc(var(--cf-w) * var(--cf-ratio) / 2);
    transform: translateY(-50%);
    z-index: 2; /* above the stage, which perspective makes a stacking context */
  }

  .coverflow[data-ready] [data-cf-prev] { left: 0; }
  .coverflow[data-ready] [data-cf-next] { right: 0; }
}

@media (min-width: 1200px) {
  .coverflow { --cf-spread: 92; }
}

/* Honour a reduced-motion preference: the carousel still works, it just cuts
   straight to each state instead of sliding and rotating into it. */
@media (prefers-reduced-motion: reduce) {
  .coverflow[data-ready] .cf-slide,
  .coverflow[data-ready] .cf-shot::after,
  .coverflow[data-ready] .cf-slide figcaption,
  .cf-dot {
    transition: none;
  }
}
/* ---- contact ---------------------------------------------------------- */

/* The form ships inside a <dialog open>, which lays out as an ordinary page
   section, so it is in the served HTML and #contact reaches something usable
   with assets/contact.js blocked. That script removes the open attribute and
   sets data-ready; this file keys off nothing else to switch to the modal.

   display is never set on .contact without [open] alongside it. The UA rule
   that hides a closed dialog is what keeps the modal shut once the script has
   taken the open attribute away, and an unconditional display here would
   override it and leave the panel stuck on the page. */

.contact {
  position: static;
  width: auto;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: clamp(2.25rem, 4.5vw, 3.75rem) 0;
  border: 0;
  background: transparent;
  color: inherit;
  overflow: visible;
}

/* The dialog is not the panel: it is a scroller filling the viewport, and the
   panel is the .wrap inside it. That is what makes a form taller than the
   screen scroll as one piece, panel and all, rather than scrolling its own
   contents inside a box pinned to a maximum height. Nothing is ever cut off
   at a fixed edge, and the close button travels with the panel it closes.

   display is set only alongside [open], never on [data-ready] alone: the UA
   rule that hides a closed dialog is what keeps the modal shut once the script
   has taken the open attribute away, and an unconditional display here would
   override it and leave the panel stuck on the page. Flex with margin: auto on
   the child is what centres a short panel; align-items would centre a tall one
   too, which pushes its top out of reach above the scroller. */
.contact[data-ready] {
  position: fixed;
  /* Anchored to the top and given a height, rather than pinned top and bottom.
     A dialog in the top layer sizes against the large viewport, the one with
     the browser's toolbars retracted, so inset: 0 puts the last inch of the
     scroller underneath the toolbar on a phone: the panel's bottom edge is
     unreachable no matter how far the thing is scrolled.

     The two numbers are the visible area, measured by assets/contact.js and
     set on this element. Neither viewport unit can stand in for them once a
     keyboard is up: it covers the screen without shortening the viewport any
     unit here measures, so the bottom of the panel goes behind the keys the
     same way it went behind the toolbar. dvh is the fallback for the instant
     before the first measurement, and the vh line before it for an engine that
     has no dvh. */
  inset: 0 0 auto;
  top: var(--modal-top, 0px);
  width: auto;
  height: 100vh;
  height: var(--modal-h, 100dvh);
  max-width: none;
  max-height: none;
  margin: 0;
  /* The gutter clears the home indicator at the bottom of a phone without a
     toolbar over it, and stays 1rem everywhere else. */
  padding: 1rem;
  padding-bottom: max(1rem, env(safe-area-inset-bottom));
  border: 0;
  border-radius: 0;
  background: transparent;
  color: inherit;
  overflow-y: auto;
  /* The panel is the whole screen now, so a scroll reaching either end has
     nowhere left to go inside the dialog and would otherwise chain out to the
     page behind the blur. The root lock below is the other half of this. */
  overscroll-behavior: contain;
  /* The scrollbar runs down the edge of the screen, over the backdrop, and the
     default for it is a light track with a light thumb: a bright stripe beside
     a near-black panel. color-scheme is what actually fixes that, telling the
     UA the box is dark so it draws its own dark scrollbar; the two properties
     after it thin the bar and tint the thumb from the palette so it reads as
     part of the modal rather than the browser. */
  color-scheme: dark;
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--muted) 35%, transparent) transparent;
}

.contact[data-ready][open] {
  display: flex;
}

/* The same result for WebKit, which has no scrollbar-color. The transparent
   border plus background-clip is what insets the thumb from the edge of the
   screen, so it floats clear of the panel instead of running alongside it. */
.contact[data-ready]::-webkit-scrollbar {
  width: 10px;
}

.contact[data-ready]::-webkit-scrollbar-track {
  background: transparent;
}

.contact[data-ready]::-webkit-scrollbar-thumb {
  border: 3px solid transparent;
  border-radius: 999px;
  background: color-mix(in srgb, var(--muted) 35%, transparent);
  background-clip: content-box;
}

.contact[data-ready]::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--muted) 60%, transparent);
  background-clip: content-box;
}

/* Blurring here rather than on the page itself: the backdrop already covers
   everything behind the panel, so backdrop-filter reaches the whole page
   without the stylesheet having to know how the page is built or the script
   having to touch anything outside the dialog. The tint is lighter than it was
   because the blur now does most of the separating on its own. */
.contact[data-ready]::backdrop {
  background: rgb(4 8 5 / 0.55);
  -webkit-backdrop-filter: blur(7px);
  backdrop-filter: blur(7px);
}

/* Set by assets/contact.js for as long as the panel is up. A modal dialog
   already makes the page inert, but a scroll that starts inside the panel still
   chains out to the document once the panel hits its end, which slides the page
   around behind the blur. */
:root[data-modal-open] {
  overflow: hidden;
}

/* Inside the modal the wrap is the panel itself, not a page column: it carries
   the border, the fill and the shadow the dialog around it used to, and it is
   as tall as its own contents. margin: auto centres it in the scroller both
   ways, and leaves it against the top gutter once it is taller than the screen.

   Lifted rather than only tinted: the backdrop sits on a page that is already
   near-black, so a shadow is what separates the panel from it. */
.contact[data-ready] .wrap {
  position: relative;
  width: min(32rem, 100%);
  margin: auto;
  padding: 1.5rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 24px 60px rgb(0 0 0 / 0.55);
}

.contact .section-head {
  margin-bottom: 1.5rem;
}

.contact-close {
  display: none;
  width: 2rem;
  height: 2rem;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--muted);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
}

/* Out of the flow rather than floated. A float shortens every box after it that
   establishes its own formatting context, and the result panel's mark is a grid
   container, so once the heading above it was hidden the mark inherited the
   float's width as a 26px shove to the left. Positioned, nothing has to dodge
   it, and the headings it used to shorten now centre on the full panel. */
.contact[data-ready] .contact-close {
  display: block;
  position: absolute;
  top: 1.15rem;
  right: 1.15rem;
}

.contact-close:hover { color: var(--fg); background: var(--surface-2); }

.contact-form {
  max-width: 32rem;
  margin: 0 auto;
}

.contact-field {
  margin: 0 0 1rem;
}

.contact-field label {
  display: block;
  margin-bottom: 0.4rem;
  color: var(--muted);
  font-size: 0.85rem;
}

.contact-field input,
.contact-field textarea {
  display: block;
  width: 100%;
  padding: 0.7rem 0.85rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--fg);
  font: inherit;
}

.contact-field textarea {
  min-height: 8rem;
  resize: vertical;
}

.contact-field input:focus-visible,
.contact-field textarea:focus-visible {
  outline: 2px solid var(--accent-dim);
  outline-offset: 1px;
  border-color: var(--accent-dim);
}

/* Mono tabular figures so the count does not jiggle while typing, matching the
   app's CharCounter. */
.contact-counter {
  display: block;
  margin-top: 0.35rem;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.contact-counter[data-full] {
  color: var(--danger);
  font-weight: 650;
}

.contact-status {
  margin: 1rem 0 0;
  font-size: 0.9rem;
}

/* Nothing to say yet: the line is in the HTML from the start so a later message
   does not shift the widget below it, but an empty one must not leave a gap.
   With it hidden the widget's own top margin still holds it off the button. */
.contact-status:empty { display: none; }

.contact-status[data-state='ok'] { color: var(--accent); }

.contact-status[data-state='error'] { color: var(--danger); }

/* The button sits with the fields it submits, directly under the message box
   and right-aligned to the end of them, so the widget below is the last thing
   in the form rather than something wedged between the two. */
.contact-actions {
  margin: 0;
  text-align: right;
}

.contact-actions .btn {
  border: 0;
  font-family: inherit;
  cursor: pointer;
}

.contact-actions .btn:disabled {
  opacity: 0.6;
  cursor: default;
  transform: none;
}

/* Last in the form, under the button. Cloudflare sizes the widget itself and
   renders it into a child element, so centring is done on the wrapper here
   rather than by touching anything the script owns. */
.contact .cf-turnstile {
  display: flex;
  justify-content: center;
  margin-top: 2.25rem;
}

/* ---- contact: the server's answer ------------------------------------ */

/* The form is long enough to scroll, so a line under it can sit below the fold
   where an answer is easy to miss entirely. The answer takes the panel over
   instead: the heading and the form go, this replaces them, and there is
   nothing left to scroll to. Hidden here rather than absent from the markup,
   because nothing on this page is built by a script. */
.contact-result {
  display: none;
  padding: 1rem 0 0.5rem;
  text-align: center;
  /* Focused when it appears, so the answer is announced and the keyboard lands
     on it. The ring would frame the whole panel for no benefit. */
  outline: 0;
}

.contact[data-result] .section-head,
.contact[data-result] .contact-form {
  display: none;
}

.contact[data-result] .contact-result {
  display: block;
}

/* The glyph is drawn from CSS rather than written into the markup: it repeats
   what the title already says, so it is decoration, and keeping it here means
   the script sets one attribute instead of an attribute and a character. */
.contact-result-mark {
  display: grid;
  place-items: center;
  width: 3.25rem;
  height: 3.25rem;
  margin: 0 auto 1.25rem;
  border-radius: 999px;
  font-size: 1.6rem;
  line-height: 1;
}

.contact[data-result='ok'] .contact-result-mark {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
}

.contact[data-result='ok'] .contact-result-mark::before { content: '\2713'; }

.contact[data-result='error'] .contact-result-mark {
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  color: var(--danger);
}

.contact[data-result='error'] .contact-result-mark::before { content: '\0021'; }

.contact-result-title {
  margin: 0 0 0.6rem;
  font-size: 1.35rem;
}

.contact-result-text {
  max-width: 26rem;
  margin: 0 auto 1.75rem;
  color: var(--muted);
}

/* Centred, unlike the send button, which aligns with the right edge of the
   fields it submits. Here there are no fields to line up with. */
.contact-result .contact-actions {
  text-align: center;
}

/* A failure has to look like one. The title carries the colour, because it is
   the line that gets read, and the button gives up the accent fill: a green
   button under "Not sent" is the whole panel saying the opposite of its words.
   Left as an outline rather than recoloured solid, so the eye goes to the
   sentence explaining what happened and not to the way out. */
.contact[data-result='error'] .contact-result-title {
  color: var(--danger);
}

.contact[data-result='error'] .contact-result .btn {
  border: 1px solid color-mix(in srgb, var(--danger) 45%, transparent);
  background: transparent;
  color: var(--danger);
}

.contact[data-result='error'] .contact-result .btn:hover {
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
}

/* ---- footer ---------------------------------------------------------- */

.site-footer {
  border-top: 1px solid var(--line);
  padding: 3rem 0 4rem;
  color: var(--muted);
  font-size: 0.9rem;
}

.site-footer .wrap {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  align-items: center;
}

.site-footer nav {
  margin-left: auto;
  display: flex;
  flex-wrap: wrap;
  gap: 1.4rem;
}

.site-footer a { color: var(--muted); text-decoration: none; }

.site-footer a:hover { color: var(--accent); }

.attribution { width: 100%; margin: 0; font-size: 0.82rem; opacity: 0.75; }

/* Phones. The row above only works while the copyright and the links share a
   line, which they cannot do at this width: the links wrap into rows that each
   start somewhere different, and the language nav, pushed right by the margin
   the same selector gives it, ends up alone against the opposite edge from
   everything else. So the footer is set as one column instead: the links in
   two columns against the same left edge as the rest of the page, then a rule,
   then the language row and the fine print centred under it. */
@media (max-width: 860px) {
  .site-footer { padding: 2.5rem 0 3rem; }

  .site-footer .wrap {
    display: grid;
    justify-items: start;
    gap: 1.25rem;
  }

  /* Two even columns rather than a wrapped row: five links of unequal length
     wrap into rows that begin in a different place each time, and the columns
     give them one edge to line up on. The rows are set far enough apart to be
     tapped at, which the 1.4rem of a single row does not have to be. */
  .site-footer nav:not(.languages) {
    width: 100%;
    margin-left: 0;
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 0.9rem 1.25rem;
  }

  /* Below the rule the footer closes centred: the language row and the two
     lines of fine print are short and self-contained, and centring them stops
     them reading as two more entries in the left column. The links above stay
     left, because columns need an edge to line up on.

     The copyright and the attribution are the same kind of line and belong
     together at the end. Ordered in the stylesheet rather than moved in the
     markup: the served HTML keeps the order a reader without the stylesheet
     wants. */
  .site-footer .languages {
    width: 100%;
    margin-left: 0;
    justify-content: center;
    padding-top: 1.75rem;
    border-top: 1px solid var(--line);
  }

  .site-footer .wrap > span {
    order: 1;
    width: 100%;
    text-align: center;
  }

  .attribution {
    order: 2;
    text-align: center;
  }
}

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

.legal {
  padding: clamp(3rem, 7vw, 5rem) 0 clamp(3rem, 7vw, 5rem);
}

.legal .wrap { max-width: var(--measure); }

.legal h1 { font-size: clamp(2rem, 5vw, 2.9rem); margin-bottom: 0.5rem; }

.legal h2 {
  font-size: 1.25rem;
  margin: 2.6rem 0 0.7rem;
  padding-top: 1.4rem;
  border-top: 1px solid var(--line);
}

.legal p, .legal li { color: #cddac9; }

.legal .updated {
  font-family: var(--mono);
  font-size: 0.85rem;
  color: var(--muted);
  margin: 0 0 2rem;
}

.legal .lede { font-size: 1.1rem; color: var(--fg); }

.legal ul { padding-left: 1.2rem; }

.legal li { margin-bottom: 0.5rem; }

.legal strong { color: var(--fg); }

/* Unresolved detail that must not ship silently. */
.placeholder {
  background: rgba(245, 158, 11, 0.16);
  border-bottom: 1px dashed #f59e0b;
  color: #fbbf24;
  padding: 0 0.25rem;
}

.back-link {
  display: inline-block;
  margin-bottom: 2rem;
  font-size: 0.9rem;
  color: var(--muted);
  text-decoration: none;
}

.back-link:hover { color: var(--accent); }

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

/* Desktop: the hero is sized to land inside the first screen, so the headline,
   both calls to action and the phone are all visible without scrolling. The
   header is sticky but still takes its own space in the flow, so its height
   comes out of the budget. min-height rather than height: on a very short
   window the hero grows and the page simply scrolls, which is better than
   clipping the buttons. */
@media (min-width: 861px) {
  .hero {
    --hero-pad: clamp(1.75rem, 4vh, 3rem);
    display: grid;
    align-content: center;
    min-height: calc(100svh - var(--header-h));
    padding: var(--hero-pad) 0;
  }

  /* A wider text column, which is enough to set the headline in two lines
     instead of three. That one wrap is worth about a fifth of the hero's
     height, and the room comes from the phone, which no longer needs it. */
  .hero .wrap { grid-template-columns: 1.15fr 0.85fr; }

  /* Height-led rather than width-led here. A 750x1523 screenshot scaled to fill
     its column stands taller than a laptop screen, and it is the one element
     that would push the hero past the fold, so it takes the room that is left
     over instead of asking for its own. The 18px subtracted is the frame around
     it: 8px of padding and a 1px border on each side. It never grows past the
     614px it used to render at, and stops shrinking at 340px, below which the
     screenshot stops being readable and the page may as well scroll. */
  .device { width: auto; }

  .device img {
    height: clamp(340px, calc(100svh - var(--header-h) - 2 * var(--hero-pad) - 18px), 614px);
    width: auto;
  }
}

/* Desktop: the screenshots get the same treatment as the hero, one section per
   screen, sized against the room under the sticky header. */
@media (min-width: 861px) {
  #screens {
    --screens-pad: clamp(1.75rem, 4vh, 3rem);
    display: grid;
    align-content: center;
    min-height: calc(100svh - var(--header-h));
    padding: var(--screens-pad) 0;
  }

  /* The gaps stacked around the carousel are the cheapest height to buy back on
     a laptop, and each pixel saved here is worth two on the card, which is
     twice as tall as it is wide. They keep their original size on a window tall
     enough to afford them, and --screens-chrome below reserves what is left
     after the trim, so the two have to move together. */
  #screens .section-head { margin-bottom: clamp(1.25rem, 2.6vh, 2rem); }

  #screens .coverflow[data-ready] .cf-controls { margin-top: clamp(0.8rem, 1.7vh, 1.3rem); }

  /* Height-led, like the hero's phone. Everything in the section that is not
     the screenshot itself is reserved up front as --screens-chrome (the
     heading block, the caption line inside the stage and the dots row, with a
     little slack for a heading that wraps), and the card takes what is left
     over. It stops growing at 300px, a shade under the hero phone's 614px of
     image, and stops shrinking at 180px, below which the screenshot stops
     being readable and the page may as well scroll. That floor
     used to be 220px, which a 1080p laptop cannot pay for: the card it forced
     was some 80px taller than the room under the header, so the one section
     that is sized to a screen was the one section that scrolled. */
  #screens .coverflow {
    --screens-chrome: 13.25rem;
    --cf-w: clamp(180px, calc(
      (100svh - var(--header-h) - 2 * var(--screens-pad) - var(--screens-chrome))
      / var(--cf-ratio)
    ), 300px);
  }
}

/* Desktop: the six feature cards get their screen too. */
@media (min-width: 861px) {
  #features {
    display: grid;
    align-content: center;
    min-height: calc(100svh - var(--header-h));
  }

  /* Three columns, stated rather than fitted. auto-fit drops to two just under
     1000px wide, and two columns is three rows, which is a screen and a half
     however tightly the cards are set. */
  #features .features { grid-template-columns: repeat(3, 1fr); }
}

/* Two rows of cards need around 850px of window to sit inside one screen, so
   below that the grid is set closer, on the same principle as the pledge: the
   section's own padding goes first and hardest, then small trims to the boxes
   inside a card. Type sizes are left alone. A card is as tall as its own text
   and there is nothing here to solve for the way the hero's phone and the
   screenshot card are solved for; what makes the difference is that the cards
   are stacked two deep, so every pixel taken out of one is charged twice. */
@media (min-width: 861px) and (max-height: 900px) {
  #features { padding: 1.5rem 0; }

  #features .section-head { margin-bottom: 1.25rem; }

  #features .section-head h2 { font-size: 2.2rem; }

  #features .features { gap: 0.9rem; }

  #features .card { padding: 1.25rem; }

  #features .card .icon {
    width: 34px;
    height: 34px;
    margin-bottom: 0.7rem;
  }

  /* A card's title is one line, so it has no use for the page's paragraph
     leading. The body copy keeps nearly all of its own. */
  #features .card h3 {
    line-height: 1.35;
    margin-bottom: 0.35rem;
  }

  #features .card p { line-height: 1.5; }
}

/* Desktop: the privacy pledge gets a screen of its own, like the hero and the
   screenshots above it. */
@media (min-width: 861px) {
  #privacy {
    display: grid;
    align-content: center;
    min-height: calc(100svh - var(--header-h));
  }
}

/* The pledge is the tallest block on the page: a two-line heading, eight list
   items over three rows and a closing paragraph, all inside a padded card that
   is itself inside a padded section. At the sizes above it needs around 825px
   of window to sit on one screen, so below that it is set compactly instead.
   None of this can be height-led the way the hero's phone and the screenshot
   card are: the card is as tall as its own text and there is no single
   measurement to solve for. What gives is the space around that text, taken in
   small amounts from every one of the boxes it nests inside.
   A step rather than a set of vh clamps: the two layouts are each composed for
   their own screen, and the sizes below read as a set. Nothing here applies
   above the breakpoint, so a desktop keeps the roomier version. */
@media (min-width: 861px) and (max-height: 900px) {
  /* The section's own padding goes first and hardest: it is the one measurement
     here that is read as the gap between two sections rather than as the pledge
     being cramped, and on a snapped page that gap is never seen whole anyway. */
  #privacy { padding: 1.75rem 0; }

  .pledge { padding: 2rem; }

  .pledge h2 {
    font-size: 2.2rem;
    margin-bottom: 0.7rem;
  }

  .pledge p { margin: 0.95rem 0; }

  .pledge ul {
    margin-top: 1.25rem;
    gap: 0.7rem 2rem;
  }

  /* The list is three rows deep and most items run to two lines, so a tenth off
     the leading is worth six lines of the shortfall and is barely visible. Type
     sizes are left alone throughout: shrinking the text is what made an earlier
     version of this block read as cramped rather than merely close-set. */
  .pledge li { line-height: 1.55; }

  .pledge ul + p { margin-top: 1.5rem; }
}

/* ---- scroll snapping -------------------------------------------------- */

/* The hero and the screenshots are each sized to a screen, so snapping makes
   the first two scrolls land squarely on one or the other. Both breakpoints do
   it, with different strengths: see the phone block below.

   Mandatory rather than proximity here: proximity let the page rest halfway
   between two screens, which is the one position neither section is composed
   for. A section taller than the window is not trapped by this, because a snap
   area larger than the scrollport is only held to keeping the scrollport inside
   it, so the long prose sections below still scroll through freely.

   The rule is on html because that is the element that scrolls. The legal
   pages share this stylesheet but have no sections, so they get no snap areas
   and the declaration does nothing there. */
@media (min-width: 861px) {
  html { scroll-snap-type: y mandatory; }

  /* Aligned to the start, offset by the scroll-padding above, so a snapped
     section sits directly under the header. The footer is deliberately not a
     snap point: it is short, and it should be reachable by scrolling the last
     section off the top rather than as a screen of its own. */
  main > section { scroll-snap-align: start; }
}

/* Phones, gently. The hero and the screenshots are sized to a screen here too,
   so the same two scrolls are worth landing squarely, but mandatory is the
   wrong strength for a touch scroll: it takes the flick away from the finger
   and re-aims it, and on a section the reader is only passing through that
   feels like the page grabbing back. It was tried, on the phone, and that is
   how it read. Proximity leaves an ordinary scroll alone and only tidies up the
   last stretch of one that already ends near an edge.

   Three snap points, not every section: the hero, the screenshots and the top
   of the features list. Those are the three the reader crosses in the first few
   flicks, and the features heading is where the page stops being screens and
   starts being a list to read through, so it is the last edge worth catching.
   Everything below it scrolls untouched. Sparse snap points are only safe
   because this is proximity: under mandatory the page must rest on one, and the
   sections with none would have been unreachable. */
@media (max-width: 860px) {
  html { scroll-snap-type: y proximity; }

  .hero,
  #screens,
  #features { scroll-snap-align: start; }
}

/* Phones. The hero's two columns collapse to one, and that column is the whole
   first impression, so it is words only: the headline, the sentence saying what
   GeoNotes is, and the button that opens it. */
@media (max-width: 860px) {
  /* One screen, the way the desktop hero is sized against its own, plus the
     bleed: the box runs past the fold so the next section's heading cannot
     appear in the strip under a floating address bar. The same bleed is added
     to the bottom padding, which is what keeps this from moving the words: the
     content box works out to 100svh - header - 2 * pad either way, and it sits
     against the top of the section, in the part of the screen the bar is not
     over. So the two measurements do different jobs, and only one of them is
     about reading. min-height, not height, so a short phone or large text
     scrolls the hero instead of clipping the button off the bottom of it. */
  .hero {
    --hero-pad: clamp(1.5rem, 5vw, 2.25rem);
    min-height: calc(100svh - var(--header-h) + var(--screen-bleed));
    display: grid;
    align-content: center;
    padding: var(--hero-pad) 0 calc(var(--hero-pad) + var(--screen-bleed));
  }

  /* The glow's 900px circle was drawn against a hero about 540px tall, and it
     runs out of tint roughly 490px in. Now that the hero is a whole screen
     that lands around the fine print, and the black below it reads as the
     page having ended there. Sized off the hero instead of off a fixed pixel
     count, so the wash reaches the bottom edge whatever height the screen
     gives it, and fades the whole way rather than stopping mid-section. */
  .hero::before {
    top: 0;
    width: 150%;
    height: 100%;
    background: radial-gradient(circle 820px at 50% 28%,
                                rgba(126, 226, 168, 0.14), transparent 78%);
  }

  .hero .wrap { grid-template-columns: 1fr; }

  .eyebrow { margin-bottom: 1.1rem; }
  .hero p.sub { margin-bottom: 1.75rem; }

  /* The hero phone is the same nearby-dark.png the carousel opens on, and in
     one column the two land within a screen of each other. Stacked they cost
     the pitch its place on the first screen to say the same thing twice, so on
     a phone the carousel is the only place the screenshots appear. Mobile
     only: the desktop hero keeps its phone, where it sits beside the copy
     rather than on top of it. Hidden rather than dropped from the markup, so
     the section that does the work still ships the image and its alt text. */
  .device { display: none; }

  /* The screenshots get a screen of their own too, sized the same way the hero
     above is: a screen tall plus the bleed, which the bottom padding hands
     straight back, and min-height so a short phone scrolls rather than clipping
     the dots off the bottom. The section padding is a little tighter than the
     page default, because on a phone every pixel spent around the card is a
     pixel the card cannot have, and it is twice as tall as it is wide. */
  #screens {
    --screens-pad: clamp(1.25rem, 3.5vh, 2rem);
    min-height: calc(100svh - var(--header-h) + var(--screen-bleed));
    display: grid;
    align-content: center;
    padding: var(--screens-pad) 0 calc(var(--screens-pad) + var(--screen-bleed));
  }

  #screens .section-head { margin-bottom: 1.5rem; }

  /* Height-led, like the desktop rule further up and for the same reason: the
     card is the one element that would push the section past the fold, so it
     takes the height that is left instead of asking for its own.
     --screens-chrome reserves everything in the section that is not the
     screenshot: the heading block and its margin, the caption line inside the
     stage, the dots row, and enough slack for the heading or the paragraph
     under it to take an extra line on a narrow phone. The card stops growing at
     the 270px it renders at today, and stops shrinking at 170px, below which
     the screenshot stops being readable and the page may as well scroll.

     Untouched by the bleed above, and it has to stay that way: the bleed is
     added to the section's height and to its bottom padding in equal measure,
     so it cancels out of the content box and this budget is still the right
     one. Anything that adds height here without adding the same padding would
     spend it on the card and push the dots under the address bar. */
  #screens .coverflow {
    --screens-chrome: 15.5rem;
    --cf-w: clamp(170px, calc(
      (100svh - var(--header-h) - 2 * var(--screens-pad) - var(--screens-chrome))
      / var(--cf-ratio)
    ), 270px);
  }

  .site-header nav a:not(.btn-sm) { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .btn { transition: none; }
}
