/*
 * Point360 landing page.
 *
 * The palette, the type ramp and the two elevations are the operator console's, lifted from
 * `packages/ui/src/styles.css` so the marketing page and the product are visibly one system. They are
 * copied rather than imported on purpose: this page is static files on S3 with no build step, and the
 * console's stylesheet is a Tailwind v4 source that needs one. If a token below changes there, change
 * it here too.
 *
 * Shape rule, applied everywhere: buttons and small controls 8px, panels 10px, pills fully round.
 * Motion: hover and active states, plus one scroll reveal via `animation-timeline: view()`. No JS,
 * no scroll listeners, and everything collapses under `prefers-reduced-motion: reduce`.
 */

@font-face {
  font-family: "Satoshi";
  src: url("fonts/satoshi-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("fonts/satoshi-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Satoshi";
  src: url("fonts/satoshi-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

:root {
  --font-sans: "Satoshi", system-ui, -apple-system, sans-serif;

  --background: #f7f8fb;
  --foreground: #0f172a;
  --card: #ffffff;
  --muted-fg: #475569;
  --faint: #5b6b7f;
  --border: #e4eaf3;
  --subtle: #f6f8fc;
  --hairline: #eef2f9;

  --accent-solid: #2563eb;
  --accent-hover: #1d4ed8;
  --accent-fg: #2157cf;
  --accent-bg: #e9effd;
  --accent-bd: #bed0f9;

  --pass-fg: #15803d;
  --pass-bg: #f0fdf4;
  --pass-bd: #bbf7d0;
  --path-b-fg: #b45309;
  --path-b-bg: #fffbeb;
  --path-b-bd: #fde68a;

  --elev-near: rgb(16 24 40 / 0.04);
  --elev-far: rgb(16 24 40 / 0.1);
  --elev-near-lg: rgb(16 24 40 / 0.05);
  --elev-far-lg: rgb(16 24 40 / 0.2);

  --lift: 0 1px 2px var(--elev-near), 0 6px 16px -8px var(--elev-far);
  --lift-lg: 0 1px 2px var(--elev-near-lg), 0 18px 40px -18px var(--elev-far-lg);

  --shell: 1200px;
  --gutter: 24px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #0b0f19;
    --foreground: #e8ecf4;
    --card: #141a26;
    --muted-fg: #9aa7bd;
    --faint: #8d98ae;
    --border: #262f40;
    --subtle: #1a2130;
    --hairline: #232b3b;

    /* The accent does not lighten: white on #2563eb measures 5.17:1 and stays AA in both themes.
       The brighter blue is used only for what is not text. */
    --accent-fg: #9dbcff;
    --accent-bg: #16233f;
    --accent-bd: #2b4a86;

    --pass-fg: #86efac;
    --pass-bg: #0f2318;
    --pass-bd: #1d4a2f;
    --path-b-fg: #fcd34d;
    --path-b-bg: #241c07;
    --path-b-bd: #4d3c10;

    --elev-near: rgb(0 0 0 / 0.4);
    --elev-far: rgb(0 0 0 / 0.55);
    --elev-near-lg: rgb(0 0 0 / 0.45);
    --elev-far-lg: rgb(0 0 0 / 0.6);
  }
}

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

/* Every in-page jump travels: the hero CTA, both nav links, the wordmark's return to top and the
   skip link. The travel is the point - it shows the visitor where they were taken instead of
   teleporting them. Gated, because an unasked-for scroll animation is exactly what
   prefers-reduced-motion is for; without the guard the jump still happens, instantly. */
@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

/* Every anchor target clears the sticky 68px nav. Without this the heading a link points at lands
   underneath it, which reads as the link having overshot. */
:target,
[id] {
  scroll-margin-top: 88px;
}

body {
  margin: 0;
  background: var(--background);
  color: var(--foreground);
  font-family: var(--font-sans);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1,
h2,
h3 {
  margin: 0;
  letter-spacing: -0.025em;
  line-height: 1.12;
  font-weight: 700;
  /* Balanced over ragged: the measures here are tight enough that the last line otherwise
     orphans a single word. */
  text-wrap: balance;
}

p {
  margin: 0;
}

a {
  color: inherit;
}

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.skip {
  position: absolute;
  left: -9999px;
}
.skip:focus {
  left: 12px;
  top: 12px;
  z-index: 10;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 14px;
}

:where(a, button):focus-visible {
  outline: 2px solid var(--accent-solid);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ---------- Navigation. One line, 68px tall. ---------- */

.nav {
  position: sticky;
  top: 0;
  z-index: 5;
  background: color-mix(in srgb, var(--background) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
}

.nav-inner {
  height: 68px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}

.wordmark {
  font-weight: 700;
  font-size: 19px;
  letter-spacing: -0.03em;
  text-decoration: none;
  white-space: nowrap;
}
/* Inline rather than flex: under `display: inline-flex` the bare text "Point" and the `<span>360</span>`
   are two flex items, so the `gap` meant for the mark opened a hole inside the word.

   The mark's own fill is the accent hex, which is the console's rule: the accent does not lighten
   between themes. It is not text, so it needs no contrast ratio. */
.wordmark img {
  height: 1.3em;
  width: auto;
  margin-right: 9px;
  vertical-align: -0.3em;
}
.wordmark span {
  color: var(--accent-solid);
}
.wordmark-sm {
  font-size: 16px;
}

.nav nav {
  display: flex;
  align-items: center;
  gap: 28px;
  white-space: nowrap;
}

.nav nav a {
  font-size: 14.5px;
  font-weight: 500;
  color: var(--muted-fg);
  text-decoration: none;
  transition: color 0.18s ease;
}
.nav nav a:hover {
  color: var(--foreground);
}

/* Specificity rather than `!important`: it has to win over `.nav nav a`, which is two selectors. */
.nav nav a.nav-cta {
  color: var(--accent-fg);
  background: var(--accent-bg);
  border: 1px solid var(--accent-bd);
  border-radius: 8px;
  padding: 7px 14px;
}
.nav nav a.nav-cta:hover {
  background: color-mix(in srgb, var(--accent-bg) 70%, var(--accent-solid) 12%);
}

/* ---------- Buttons ---------- */

.btn {
  display: inline-block;
  border-radius: 8px;
  font-weight: 700;
  font-size: 15.5px;
  line-height: 1;
  padding: 15px 26px;
  text-decoration: none;
  white-space: nowrap;
  transition:
    background-color 0.18s ease,
    transform 0.12s ease,
    box-shadow 0.18s ease;
}

.btn-primary {
  background: var(--accent-solid);
  color: #ffffff;
  box-shadow: var(--lift);
}
.btn-primary:hover {
  background: var(--accent-hover);
  box-shadow: var(--lift-lg);
}
.btn-primary:active {
  transform: translateY(1px);
}

.btn-lg {
  font-size: 17px;
  padding: 18px 34px;
}

/* ---------- Hero ---------- */

.hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding-block: clamp(44px, 6vw, 76px) clamp(44px, 6vw, 76px);
}

/* The ambient wash. One soft accent field behind the hero, drifting slowly. It is the brand blue at
   6% rather than a second hue, because a gradient that introduces a colour the system does not own is
   the thing that reads as generated. `z-index: -1` under `isolation: isolate` keeps it behind the
   copy without entering any other stacking context. */
.hero::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: -30%;
  right: -10%;
  width: 70%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--accent-solid) 12%, transparent) 0%, transparent 68%);
  pointer-events: none;
}

/* Centred, not left-aligned. The hero used to be a split - copy left, panel right - and the copy
   kept its left edge when the explainer moved below it and took the full shell width. That left a
   ragged column against a 1200px-wide stage, so the right half of the first screen read as space
   somebody forgot to fill. Centred copy over a centred stage is one axis instead of two.

   Each measure is centred individually: `text-align` centres the lines, `margin-inline: auto`
   centres the BOX. Without the second, a 26ch headline inside a 62ch column is centred text sitting
   in a left-hung block, which is off-axis by exactly half the difference. */
.hero-inner {
  max-width: 62ch;
  text-align: center;
}

/* Two lines at desktop, and the measure is what holds it there: at 60px on a 22ch measure this
   headline broke to three and the CTA started sliding out of the first viewport. */
.hero-copy h1 {
  font-size: clamp(36px, 4.5vw, 52px);
  max-width: 26ch;
  margin-inline: auto;
}

.lede {
  margin-top: 22px;
  font-size: clamp(17px, 1.4vw, 19.5px);
  color: var(--muted-fg);
  max-width: 46ch;
  margin-inline: auto;
}

.hero-copy .btn {
  margin-top: 34px;
}

/* ============================================================
   The explainer stage.

   Hosts the 1920x1080 composition imported from Claude Design (see
   explainer/SOURCE.md). The engine owns everything inside: it measures this
   box, scales the composition to fit and centres it, so the only jobs here are
   giving it a sized, positioned box and framing it like the rest of the page.

   No frame, no ground, no border: the piece renders straight onto the page. That
   is why its palette follows `prefers-color-scheme` rather than being fixed (see
   the DEVIATION note in point360-motion.jsx) - on a transparent ground the page
   IS the contrast partner, so a palette that does not follow the page is
   unreadable in one of the two themes. The composition's cards carry their own
   fills, so it still reads as a panel without one being drawn around it.

   16:9 exactly. The engine fits by the smaller of the two ratios, so any other
   aspect just pads with the frame colour on one axis - correct, but it reads as
   a mistake. `min-height` catches the narrow end, where 16:9 of a 340px column
   is 191px and the composition's own 26px body type lands at 4px.
   ============================================================ */

.stage-frame {
  margin-top: clamp(28px, 3.5vw, 44px);
}

.stage {
  position: relative;
  aspect-ratio: 16 / 9;
  min-height: 260px;
}

/* Holds the stage until the engine mounts, and it is NOT a loading state: it is
   the same four steps, and it is what a visitor with scripts blocked, or on a
   phone, keeps for good.

   Shaped as a centred row of chips rather than a left-aligned list because it
   stands under centred hero copy - and because it is the shape the animation's
   own opening scene resolves to, so the swap reads as the same thing starting
   to move rather than one element replaced by another. React clears this node
   on mount. */
.stage-fallback {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: clamp(24px, 4vw, 56px);
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  /* Top of the box, not its middle. The box is sized for the composition (16:9, ~648px at desktop)
     and the chips are a fraction of that, so centring them opened a ~300px gap between the CTA and
     the first thing under it - the hole this placeholder exists to fill. Sitting at the top they
     read as the end of the hero, and the remaining height reads as the air before the fold. */
  align-content: flex-start;
  justify-content: center;
  gap: clamp(10px, 1.2vw, 16px);
  color: var(--foreground);
}

.stage-fallback li {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 22px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--card);
  box-shadow: var(--lift);
  font-size: clamp(14px, 1.2vw, 16.5px);
  font-weight: 500;
  white-space: nowrap;
}

.stage-fallback span {
  flex: none;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 999px;
  background: var(--accent-bg);
  border: 1px solid var(--accent-bd);
  color: var(--accent-fg);
  font-size: 13px;
  font-weight: 700;
}

/* ---------- The problem: one statement, full width ---------- */

.problem {
  border-block: 1px solid var(--border);
  background: var(--card);
  padding-block: clamp(64px, 8vw, 100px);
}

.statement h2 {
  font-size: clamp(28px, 3.4vw, 40px);
  max-width: 18ch;
}

.statement p {
  margin-top: 24px;
  font-size: clamp(17px, 1.3vw, 19px);
  color: var(--muted-fg);
  max-width: 66ch;
}

/* ---------- The four checks: bento, exactly four cells ---------- */

.checks {
  padding-block: clamp(64px, 8vw, 104px);
}

.section-head {
  font-size: clamp(26px, 3.2vw, 38px);
  max-width: 20ch;
  margin-bottom: 40px;
}

.grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 20px;
}

.cell {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--lift);
  padding: 30px;
  transition:
    transform 0.25s cubic-bezier(0.16, 1, 0.3, 1),
    box-shadow 0.25s ease,
    border-color 0.25s ease;
}
.cell:hover {
  transform: translateY(-3px);
  box-shadow: var(--lift-lg);
  border-color: var(--accent-bd);
}
.cell-wide {
  grid-column: span 7;
}
.cell-narrow {
  grid-column: span 5;
}
.cell-accent {
  background: var(--accent-bg);
  border-color: var(--accent-bd);
}
.cell-subtle {
  background: var(--subtle);
}

.cell h3 {
  font-size: 21px;
}

.ask {
  margin-top: 10px;
  font-size: 16.5px;
  font-weight: 500;
  color: var(--accent-fg);
}

.cell p:last-child {
  margin-top: 14px;
  color: var(--muted-fg);
  font-size: 16px;
  max-width: 52ch;
}

/* ---------- Why verification: pull statement + body ---------- */

.why {
  background: var(--card);
  border-block: 1px solid var(--border);
  padding-block: clamp(64px, 8vw, 100px);
}

.why-inner {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: clamp(32px, 5vw, 72px);
  align-items: start;
}

/* No accent bar down the side: a coloured rule thicker than a hairline is a costume for emphasis that
   the scale already carries. A 1px rule above it groups the two columns instead. */
.pull {
  margin: 0;
  font-size: clamp(26px, 3vw, 36px);
  font-weight: 700;
  letter-spacing: -0.025em;
  line-height: 1.15;
  padding-top: 26px;
  border-top: 1px solid var(--border);
}

.why-body p {
  color: var(--muted-fg);
  font-size: 17px;
  max-width: 66ch;
}
.why-body p + p {
  margin-top: 18px;
}
.why-body {
  padding-top: 26px;
  border-top: 1px solid var(--border);
}

/* ---------- Process: vertical stack beside one made mark ---------- */

.process {
  padding-block: clamp(64px, 8vw, 104px);
}

.process-inner {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
  gap: clamp(32px, 5vw, 72px);
  align-items: start;
}

.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: step;
}

.steps li {
  counter-increment: step;
  position: relative;
  padding: 26px 0 26px 62px;
  border-top: 1px solid var(--border);
}
.steps li:last-child {
  border-bottom: 1px solid var(--border);
}

.steps li::before {
  content: counter(step);
  position: absolute;
  left: 0;
  top: 26px;
  width: 38px;
  height: 38px;
  border-radius: 999px;
  display: grid;
  place-items: center;
  background: var(--hairline);
  color: var(--faint);
  font-size: 14px;
  font-weight: 700;
}

.steps h3 {
  font-size: 19px;
}
.steps p {
  margin-top: 8px;
  color: var(--muted-fg);
  font-size: 16px;
  max-width: 48ch;
}

/* Stands where photography would, and does not pretend to be photography. The dot grid is the
   explainer's own workflow-canvas texture at the same 34px pitch, so the two surfaces read as one
   system; the mark sits on it, oversized and centred, over a single accent bloom. Nothing here makes
   a claim, which is the point - the alternative was a stock photo of somebody else's building. */
.mark-panel {
  margin: 0;
  aspect-ratio: 9 / 11;
  border: 1px solid var(--border);
  border-radius: 10px;
  background:
    radial-gradient(60% 45% at 50% 42%, color-mix(in srgb, var(--accent-solid) 10%, transparent) 0%, transparent 70%),
    radial-gradient(circle, var(--border) 1.6px, transparent 1.6px) 0 0 / 34px 34px,
    var(--subtle);
  display: grid;
  place-items: center;
  overflow: hidden;
}

.mark-panel img {
  width: clamp(96px, 40%, 180px);
  height: auto;
  /* The mark is the accent hex and does not lighten between themes, so on the dark ground it gets
     lifted off the field rather than recoloured. */
  filter: drop-shadow(0 10px 30px color-mix(in srgb, var(--accent-solid) 35%, transparent));
}

/* ---------- Contact band ---------- */

.contact {
  background: var(--card);
  border-top: 1px solid var(--border);
  padding-block: clamp(72px, 9vw, 116px);
}

/* Centred, to close the way the hero opens. The original note here said to constrain the children
   and never the shell, because a `max-width` on the shell element is centred by its own
   `margin-inline: auto` and the band would then sit in the middle of a left-aligned page. That
   reasoning stood while the page was left-aligned; the hero is centred now, and a left-hung column
   against an empty right half was the same missing-space problem in the closing screen.

   Still constrain the CHILDREN, not the shell - but centre each measure: `text-align` centres the
   lines, `margin-inline: auto` centres the box. Without the second, a 16ch heading in a 1200px shell
   is centred text in a left-hung block. */
.contact-inner {
  text-align: center;
}

.contact h2 {
  font-size: clamp(28px, 3.4vw, 42px);
  max-width: 16ch;
  margin-inline: auto;
}

.contact p {
  margin-top: 20px;
  font-size: 18px;
  color: var(--muted-fg);
  max-width: 50ch;
  margin-inline: auto;
}

.contact .btn {
  margin-top: 32px;
}

.contact-mail {
  margin-top: 22px;
  font-size: 16px;
}
.contact-mail a {
  color: var(--accent-fg);
  font-weight: 500;
}

/* ---------- Footer ---------- */

.foot {
  border-top: 1px solid var(--border);
  padding-block: 30px;
}

.foot-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
  font-size: 14px;
  color: var(--faint);
}

/* ============================================================
   Motion.

   Four animations, and each one is here because it says something:

   - `rise`      content entering the viewport, so the eye is led down the page in reading order
   - `settle`    the hero arriving, staggered so the headline lands before the panel
   - `trace`     a run passing through the four checks, which is the product's actual behaviour
   - `halo`      which check the run is on, timed to where the trace has reached
   - `drift`     the ambient field, slow enough to register as depth rather than as movement

   Every one is transform and opacity only, so none of them costs layout, and every one lives inside
   the `no-preference` block. The base rules above leave all of it in its finished state, so a visitor
   with reduced motion gets the same page with nothing missing rather than a page that never fades in.
   ============================================================ */

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    /* Two lists that appear as lists, and nothing else. Putting this on `.statement`, `.why-inner`
       and `.contact-inner` too gave every section the same entrance, which reads as a template
       rather than as authorship. */
    .cell,
    .steps li {
      animation: rise linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 55%;
    }
  }

  /* The hero arrives in reading order. `both` fill is what holds the pre-animation opacity, so these
     selectors and only these selectors start invisible. */
  .hero-copy h1,
  .lede,
  .hero-copy .btn,
  .stage-frame {
    animation: settle 0.75s cubic-bezier(0.16, 1, 0.3, 1) both;
  }
  .hero-copy h1 {
    animation-delay: 0.06s;
  }
  .lede {
    animation-delay: 0.16s;
  }
  .stage-frame {
    animation-delay: 0.22s;
  }
  .hero-copy .btn {
    animation-delay: 0.28s;
  }

  .hero::before {
    animation: drift 26s ease-in-out infinite alternate;
  }
}

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(18px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes settle {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* `.hero::before` has referenced this since the first commit and it was never defined, so the
   ambient wash has always been static - an undefined animation-name is not an error in CSS, it is
   simply ignored, which is why nothing ever said so. Slow and small on purpose: at 26s and 3% of the
   field's own size it registers as depth rather than as something moving. */
@keyframes drift {
  from {
    transform: translate3d(0, 0, 0) scale(1);
  }
  to {
    transform: translate3d(-3%, 2%, 0) scale(1.06);
  }
}

/* ---------- Mobile. Every multi-column layout collapses here. ---------- */

@media (max-width: 900px) {
  .why-inner,
  .process-inner {
    grid-template-columns: minmax(0, 1fr);
  }

  /* 16:9 of a phone-width column is a 190px-tall band, and the composition's own
     body type is authored at 19-26px against 1080, so it lands under 5px there.
     Give it a squarer box below this width: the engine fits by the smaller
     ratio, so a taller frame just means a larger scale factor. */
  .stage {
    aspect-ratio: 4 / 3;
  }

  .cell-wide,
  .cell-narrow {
    grid-column: span 12;
  }

  .hero-copy h1 {
    max-width: none;
  }

  .pull {
    font-size: 26px;
  }

  .mark-panel {
    aspect-ratio: 4 / 3;
  }
}

@media (max-width: 620px) {
  :root {
    --gutter: 18px;
  }

  .nav nav {
    gap: 16px;
  }
  .nav nav a:not(.nav-cta) {
    display: none;
  }

  .steps li {
    padding-left: 52px;
  }
  .steps li::before {
    width: 34px;
    height: 34px;
  }
}
