/* Noraneko — teaser page.
   Glass over a warm dark ground, echoing the app: frosted layers that keep
   what is behind them. */

:root {
  --ink: #f6f2ee;
  --ink-dim: rgba(246, 242, 238, 0.66);
  --ink-faint: rgba(246, 242, 238, 0.42);

  /* The app's own accent. */
  --accent: #f9844c;
  --accent-soft: rgba(249, 132, 76, 0.16);

  --ground: #0d0b0a;

  --glass: rgba(255, 255, 255, 0.055);
  --glass-edge: rgba(255, 255, 255, 0.12);
  --glass-blur: 18px;

  --radius: 22px;
  --gap: 20px;
  --page: 1080px;

  --font: ui-rounded, -apple-system, BlinkMacSystemFont, "SF Pro Rounded",
          "Hiragino Maru Gothic ProN", "Segoe UI", system-ui, sans-serif;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  position: relative;
  margin: 0;
  padding: 0 20px 80px;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* The warm light behind everything. Fixed, so the glass has something to
   hold as the page moves. */
.glow {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(60vw 60vw at 50% -10%, rgba(249, 132, 76, 0.30), transparent 65%),
    radial-gradient(50vw 50vw at 90% 20%, rgba(212, 92, 140, 0.16), transparent 60%),
    radial-gradient(70vw 50vw at 5% 60%, rgba(70, 120, 190, 0.14), transparent 65%);
}

/* **Paw prints, faint, all over.** Somebody walked here — which is the whole
   subject of the app, and the one thing a page of glass panels cannot say on
   its own. Behind everything and untouchable, so they are scenery rather than
   something to notice twice.

   Absolute rather than fixed: they belong to the page and should scroll with
   it, the way tracks belong to the ground and not to the window. */
.paws {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}

.paws svg {
  position: absolute;
  color: var(--ink);
}

/* **Said here, because SVG's default fill is black.** The mark carries
   `fill="currentColor"` on its own path; the paw's shapes carried nothing, so
   they were drawn in black on a near-black page and were perfectly present and
   perfectly invisible. */
.paws svg * {
  fill: currentColor;
}

h1, h2, h3 { line-height: 1.15; letter-spacing: -0.02em; margin: 0 0 12px; }
p { margin: 0 0 14px; }
p:last-child { margin-bottom: 0; }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

/* ── Glass ─────────────────────────────────────────────────────────── */

.glass {
  background: var(--glass);
  border: 1px solid var(--glass-edge);
  border-radius: var(--radius);
  backdrop-filter: blur(var(--glass-blur)) saturate(160%);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.10) inset,
    0 24px 60px rgba(0, 0, 0, 0.45);
}

.panel { padding: 30px; }

/* A block with no box. Used where something beside it is the thing being
   looked at, and a frame would be a second object competing with the first. */
.open { padding: 4px 2px; }
.open p { color: var(--ink-dim); }

.panel.accent {
  border-color: rgba(249, 132, 76, 0.35);
  background: linear-gradient(180deg, var(--accent-soft), var(--glass));
}

/* ── Hero ──────────────────────────────────────────────────────────── */

.hero {
  max-width: 760px;
  margin: 0 auto;
  padding: 96px 0 72px;
  text-align: center;
}

.hero-icon {
  width: 120px; height: 120px;
  border-radius: 28px;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.55);
  margin-bottom: 26px;
}

.hero h1 {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  margin-bottom: 18px;
}

.name { font-size: clamp(48px, 11vw, 78px); font-weight: 800; }

.kanji {
  font-size: clamp(22px, 5vw, 30px);
  font-weight: 500;
  letter-spacing: 0.32em;
  /* The character spacing leaves a gap after the last glyph; pull it back so
     the word still reads as centred. */
  text-indent: 0.32em;
  color: var(--accent);
}

.tagline {
  font-size: clamp(19px, 3.4vw, 23px);
  font-weight: 600;
  margin-bottom: 16px;
}

.lede {
  color: var(--ink-dim);
  max-width: 56ch;
  margin: 0 auto 34px;
}

/* ── The way in ──────────────────────────────────────────── */

/* The newsletter form stood here. It has been replaced by the TestFlight
   link itself, which is the thing the form existed to announce — and which
   collects nothing, so the promise made on the privacy page stays whole. */

.invite { margin: 0 auto 20px; }

.invite-note {
  display: block;
  margin-top: 14px;
  font-size: 13.5px;
  color: var(--ink-faint);
}

.button {
  appearance: none;
  border: 0;
  border-radius: 999px;
  padding: 14px 26px;
  font: inherit;
  font-size: 16px;
  font-weight: 700;
  color: #2a1206;
  background: linear-gradient(180deg, #ffa571, var(--accent));
  cursor: pointer;
  white-space: nowrap;
  transition: transform 0.15s ease, filter 0.15s ease;
  text-decoration: none;
  display: inline-block;
}

.button:hover { transform: translateY(-1px); filter: brightness(1.06); }
.button:active { transform: translateY(0); }

/* Inert, and visibly so. The lift on hover is the whole signal that pressing
   it will do something, so that goes too — a button that rises to meet the
   cursor and then refuses is worse than one that never moved. */
.button.is-waiting {
  background: var(--glass);
  border: 1px solid var(--glass-edge);
  color: var(--ink-dim);
  cursor: default;
}

.button.is-waiting:hover { transform: none; filter: none; }

.badge {
  font-size: 14px;
  color: var(--ink-faint);
  letter-spacing: 0.04em;
}

/* ── Sections ──────────────────────────────────────────────────────── */

main { max-width: var(--page); margin: 0 auto; }

.section {
  display: grid;
  gap: var(--gap);
  margin-bottom: 72px;
}

@media (min-width: 780px) {
  .section#offline,
  .section#cards,
  .section#adoption { grid-template-columns: 1fr 1fr; }

  /* The card sits beside what it describes; everything below it runs the
     full width. */
  .section#cards > .panel { grid-column: 1 / -1; }
}

.section-title {
  font-size: clamp(26px, 4.5vw, 34px);
  text-align: center;
  margin-bottom: 10px;
}

.eyebrow {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 10px;
}

.feature-wide h2 { font-size: clamp(24px, 3.6vw, 31px); }

/* The mark, set in a line of type. Sized against the font rather than in
   pixels so it keeps its place as the heading grows with the viewport. */
.title-mark {
  width: 0.92em;
  height: 0.89em;
  color: var(--ink);
  vertical-align: -0.06em;
  margin: 0 0.06em;
}

/* One glyph per card, in the accent, drawn rather than filled — a page of
   solid marks would compete with the words they belong to. */
.feature-icon {
  width: 28px;
  height: 28px;
  margin-bottom: 14px;
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.panel p { color: var(--ink-dim); }
.panel h3 { font-size: 20px; }

.ticks { list-style: none; margin: 16px 0 0; padding: 0; }

.ticks li {
  position: relative;
  padding-left: 28px;
  margin-bottom: 8px;
  color: var(--ink-dim);
}

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

/* What it asks for, and what it does not. Two lists rather than three
   zeroes: nought accounts is a number counting nothing, and the point is the
   shape of the pair — a long list struck out, a short one standing. */

.needs-label {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 22px 0 10px;
}

.needs {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.needs li {
  border-radius: 999px;
  padding: 7px 15px;
  font-size: 15px;
  border: 1px solid var(--glass-edge);
}

.needs.gone li {
  color: var(--ink-faint);
  text-decoration: line-through;
  text-decoration-color: rgba(249, 132, 76, 0.75);
  text-decoration-thickness: 1.5px;
}

.needs.have li {
  color: var(--ink);
  font-weight: 600;
  border-color: rgba(249, 132, 76, 0.45);
  background: var(--accent-soft);
}

/* A pill holding the mark instead of a word. Narrower than a worded one, so a
   run of them reads as a count rather than as five empty labels. */
.mark-pill {
  display: grid;
  place-items: center;
  padding: 7px 11px;
}

.mark-pill svg {
  display: block;
  width: 17px;
  height: 16px;
  /* White, like the words beside it. The pill's own border and wash already
     carry the accent; a coloured mark inside a coloured pill was the same
     note struck twice. */
  color: var(--ink);
}

/* ── The foil card ─────────────────────────────────────────────────── */

.card-stage {
  /* Room above and below rather than a card filling the column: what sits
     under this is a whole section, and it needs air to read as one. */
  margin: 0;
  padding: 12px 0 28px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
  /* The depth the tip is read in. Without it the card rotates flat and reads
     as a picture being skewed rather than an object being turned. */
  perspective: 900px;
  --turn: 0deg;
}

.card-tilt {
  position: relative;
  width: min(228px, 62%);
  aspect-ratio: 63 / 88;
  /* The app's own corner and border, in proportion: 20pt and 7pt on a 375pt
     card, against the 228px this is drawn at. */
  --corner: 12px;
  --edge: 4px;
  border-radius: var(--corner);
  overflow: hidden;
  /* Set by foil.js from the same number the shader is lit by, so the tip and
     the highlight belong to one angle rather than two. */
  transform: rotateY(var(--turn)) rotateX(calc(var(--turn) * -0.28));
  box-shadow: 0 34px 70px rgba(0, 0, 0, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.16);
}

/* **Each layer rounds its own corners.** The parent clips with `overflow`,
   but it also carries a 3-D transform, and a canvas promoted to its own
   compositing layer escapes that clip — the card came out a rectangle. A
   radius on the layer itself cannot be escaped. */
.foil-canvas {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
}

/* The card's frame, drawn over the coating rather than around it — in the app
   the foil runs *under* the border to the card's own edge, so a frame outside
   the picture would be a different object. Proportional, like the corner: the
   border is 7pt on a 375pt card. */
.card-edge {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  /* A real border, not an inset shadow: shadow spread takes a length only, and
     the percentage this used to carry made the whole declaration invalid — so
     the card had no frame at all. */
  border: var(--edge) solid rgba(255, 255, 255, 0.45);
  pointer-events: none;
}

/* The cat, cut out of her own photograph by the app and carried on the card.
   Barely there on purpose: in the app it is a highlight the light catches, not
   a sticker — visible when the coating sweeps past it and nearly gone the rest
   of the time. */
.card-silhouette {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  pointer-events: none;
  mix-blend-mode: soft-light;
}

/* **The viewBox is a thousand units across, not one.** A unit-square viewBox
   is legal and it rasterises terribly: inside a layer the browser is already
   rendering to a texture — which this card is, being rotated in 3-D — the
   coordinate space is what the raster size is worked out from, and one unit
   buys about one pixel. The cat came out as a single soft blob stretched over
   the whole card, which is the translucent square that appeared whenever it
   moved.

   Fill only, too. A shape this soft never needed an outline round it. */
.card-silhouette path {
  fill: rgba(255, 255, 255, 0.55);
  stroke: none;
}

/* `?bare=1`: the coating off and the outline drawn plainly, to check it is
   there and that it lands on the cat. Not a state the page is ever shipped in
   — see `foil.js`. */
.card-stage.bare .card-silhouette { mix-blend-mode: normal; }

.card-stage.bare .card-silhouette path {
  fill: rgba(255, 90, 60, 0.30);
  stroke: #ff5a3c;
  /* User units, and the viewBox is a thousand across. */
  stroke-width: 6;
}

/* What is left when there is no WebGL: still a card, still iridescent, just
   not answering the angle. */
.foil-fallback {
  display: none;
  position: absolute;
  inset: 0;
  background:
    linear-gradient(125deg,
      #b9c3d6 0%, #d9b8c8 18%, #c8d6b8 36%,
      #b8c8d9 54%, #d6c8b8 72%, #bcc6d8 100%);
}

.no-webgl .foil-canvas { display: none; }
.no-webgl .foil-fallback { display: block; }

.card-stage figcaption {
  font-size: 14.5px;
  color: var(--ink-dim);
  text-align: center;
}

@media (prefers-reduced-motion: reduce) {
  .card-tilt { transform: none; }
}

/* The card itself, not a phone holding one — it is made to be sent to
   somebody, and it leaves the app as a picture this shape. */
.adoption-shot { margin: 0; align-self: center; }

.adoption-shot img {
  display: block;
  width: min(340px, 100%);
  margin: 0 auto;
  border-radius: 18px;
  border: 1px solid var(--glass-edge);
  box-shadow: 0 30px 66px rgba(0, 0, 0, 0.55);
}

/* ── The trade, drawn ──────────────────────────────────────────────── */

/* Inside the panel that describes it, not a box of its own. The words and the
   picture are one thought, and two frames made them two. */
.trade-scene {
  --device: clamp(64px, 13vw, 92px);
  margin-top: 30px;
}

/* Held to a width the eye can cross. Left to fill a panel this wide the two
   phones would sit at opposite ends of the page, too far apart to read as
   two people standing together. */
.devices {
  position: relative;
  max-width: 560px;
  margin: 0 auto;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

/* A phone, reduced to the two things that make it one: the shape, and the
   mark lit on the front.
   **Opaque on purpose.** It is what a mark disappears behind, so the glass
   here sits on a solid ground rather than letting the field show through. */
.device {
  position: relative;
  z-index: 1;
  width: var(--device);
  aspect-ratio: 9 / 17;
  border-radius: 16px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background:
    linear-gradient(160deg, rgba(255,255,255,0.12), rgba(255,255,255,0.03)),
    #16120f;
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.55);
  display: grid;
  place-items: center;
}

.device svg {
  width: 42%;
  aspect-ratio: 771 / 745;
  color: var(--accent);
  opacity: 0.85;
}

/* The field the cards cross. Marks are placed into it by app.js, each with
   its own speed and opacity — fainter is slower, exactly as in the app, so it
   reads as depth rather than as a pattern sliding.
   Spans the whole scene rather than the gap between the devices: a mark should
   travel until it is *under* a phone and go out there, not stop short of one
   and vanish in the open. */
/* Reaches from the centre of one device to the centre of the other, and no
   taller than they are. A mark begins where a phone already covers it and
   ends the same way, so it comes out from behind one and goes in behind the
   other rather than appearing in open air. Clipped top and bottom, because a
   card crossing above the phones is not crossing between them. */
.crossing {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--device) / 2);
  right: calc(var(--device) / 2);
  z-index: 0;
  overflow: hidden;
  container-type: inline-size;
}

.crossing svg {
  position: absolute;
  color: var(--ink);
  will-change: transform;
}

/* Nought to the full width: both ends sit under a device, since the field
   starts and stops at their centres. */
@keyframes cross-right {
  from { transform: translateX(0); }
  to   { transform: translateX(100cqw); }
}

@keyframes cross-left {
  from { transform: translateX(100cqw); }
  to   { transform: translateX(0); }
}

/* Four cards, so two by two — a square block rather than a row with a short
   line under it. Held to a width that keeps the pair side by side legible
   instead of running the full page. */
.grid {
  display: grid;
  gap: var(--gap);
  grid-template-columns: 1fr;
  max-width: 760px;
  margin: 0 auto;
  width: 100%;
}

@media (min-width: 640px) {
  .grid { grid-template-columns: 1fr 1fr; }
}

/* ── Screenshots ───────────────────────────────────────────────────── */

.shots { justify-items: center; }

/* **One row that never ends.** Six frames wrapped into two rows read as a
   contact sheet — a thing to audit rather than glance at. Moving, it is a
   window onto an app rather than an inventory of it.
   The edges fade instead of cutting, so nothing is ever seen arriving. */
/* **Faded to nothing at both ends, and wider than the page.** The strip is a
   window: what leaves it dissolves into the background rather than passing
   behind something, so it is a mask and not a gradient painted over the ends —
   the ground here is a warm glow, and a flat colour over it reads as two black
   bars.

   Full-bleed, because a row that moves wants more room than a column of prose:
   held to the text's width it looked like a component, and spilling past both
   edges it looks like something going by.

   Kept plain on purpose. This was written with a custom property inside
   `calc()` and an `isolation` alongside, and between them the mask did nothing
   at all. Percentages and two stops are enough. */
.shot-marquee {
  position: relative;
  /* Stretched to the column, then pushed out to the viewport on both sides.
     A plain `width: 100vw` did not centre: an item wider than its grid area
     falls back to start alignment rather than overflowing evenly, so the strip
     hung off to the right. Margins do not care how wide the thing is. */
  justify-self: stretch;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right,
    transparent 0%, #000 16%, #000 84%, transparent 100%);
  mask-image: linear-gradient(to right,
    transparent 0%, #000 16%, #000 84%, transparent 100%);
}

.shot-track {
  display: flex;
  width: max-content;
  /* The no-JS fallback pace: one run of about 1,608px at the 60 px/s that
     `app.js` works everything else out from. See PIXELS_PER_SECOND — the speed
     is chosen so a frame moves a whole number of pixels. */
  animation: shots 26.8s linear infinite;

  /* **A layer of its own, moved in three dimensions.** A 2-D translate is
     allowed to stay on the main thread, where it competes with everything else
     the page is doing; asking for depth puts the strip on the compositor and
     it is drawn once and slid thereafter. */
  will-change: transform;
  backface-visibility: hidden;
}

/* **Spacing on the items, not `gap` on the track.** `gap` goes *between*
   things, so twelve frames carry eleven gaps — and half of that is six frames
   and five and a half gaps, while one run is six frames and six. The loop
   jumped back by half a gap every lap. A margin on each frame makes the track
   twelve equal units, and half of it exactly one run. */
.shot-track > .phone {
  margin-right: clamp(20px, 3vw, 34px);
}

/* **A distance in pixels, set by `app.js`.** A percentage is resolved against
   the track's own width, which lands on fractions of a pixel and leaves the
   compositor resampling every frame — the wobble that reads as jank at this
   speed. The shift is a whole number of runs, so the frame it lands on is the
   frame it started with. */
@keyframes shots {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(calc(var(--shift, 50%) * -1), 0, 0); }
}

.shot-marquee:hover .shot-track { animation-play-state: paused; }

.phone { margin: 0; text-align: center; }

/* A real capture now, so the frame is a border and a shadow and nothing more.
   It used to carry a glass background and a `backdrop-filter` for the empty
   placeholder — and inside the masked strip that filter had no backdrop to
   sample, so it resolved to black and painted a dark band behind the whole
   row. */
.screen {
  display: block;
  width: 234px;
  aspect-ratio: 9 / 19.5;
  object-fit: cover;
  border-radius: 34px;
  border: 1px solid var(--glass-edge);
}

.phone figcaption {
  margin-top: 14px;
  font-size: 14.5px;
  color: var(--ink-dim);
}

/* ── Closing ───────────────────────────────────────────────────────── */

.centre { text-align: center; }
.centre .button { margin-top: 8px; }
.closing { margin-bottom: 56px; }

/* ── Footer ────────────────────────────────────────────────────────── */

footer {
  max-width: var(--page);
  margin: 0 auto;
  /* The last section above only leaves its own bottom margin, which reads as
     the gap between two sections rather than as the end of the page. */
  padding-top: 44px;
  text-align: center;
  color: var(--ink-faint);
  font-size: 14.5px;
}

.footer-mark { color: var(--ink-faint); margin-bottom: 10px; }
footer p { margin: 0 0 6px; }
.fine { font-size: 13.5px; opacity: 0.75; }

/* ── Preferences ───────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { transition: none !important; }

  /* The marks stay where they were placed — a still field still says the two
     devices are talking, which is the whole job of the picture. */
  .crossing svg { animation: none !important; }

  /* And the strip stops, showing whichever screens it had reached. */
  .shot-track { animation: none !important; }
}

/* A light-mode visitor still gets the dark page — it is the app's own
   surface, and a glass design needs something behind it to be glass over. */

/* ── Diagnostics ───────────────────────────────────────────────────── */
/* See the switches at the foot of `app.js`. None of these is a state the page
   is shipped in; they exist so a slow frame can be attributed to something
   rather than argued about. */

.noglass .glass,
.noglass .device {
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
}

.nopaws .paws { display: none; }
.nofoil .card-stage { display: none; }

/* ── Legal pages ───────────────────────────────────────────────────── */
/* privacy.html is one column of prose reusing the panels from the front
   page. The width that suits a row of phone screenshots is far too wide to
   read a sentence across, so the measure is cut rather than the styling
   rebuilt. */

.legal { max-width: 720px; }

.legal .section { gap: 16px; margin-bottom: 40px; }

.legal .panel { padding: 26px 30px; }

/* Inside a panel these sit a rung below a section title, not beside it. */
.legal h2 { font-size: clamp(21px, 3vw, 25px); }

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

/* A paragraph that closes a list is still part of it, and wants the air the
   list's own items get between them. */
.ticks + p,
.needs + p { margin-top: 16px; }

.legal-head {
  max-width: 720px;
  margin: 0 auto;
  padding: 76px 0 40px;
  text-align: center;
}

.legal-head h1 { font-size: clamp(32px, 6.5vw, 44px); font-weight: 800; }

.legal-head .tagline { margin-bottom: 12px; }

/* The name is a line of running text here rather than a title of its own, so
   the tracking that suits it at 78px is loosened back to something readable. */
.legal-head .kanji {
  font-size: 17px;
  letter-spacing: 0.22em;
  text-indent: 0.22em;
}

.legal-mark {
  width: 46px; height: 44px;
  color: var(--accent);
  margin-bottom: 18px;
}

/* Stands alone between the last panel and the footer, so it wants clear air
   on both sides rather than sitting against either. */
.legal-return { text-align: center; margin: 8px 0 72px; }

.legal-back {
  color: var(--accent);
  text-decoration: none;
  font-weight: 600;
}

.legal-back:hover { text-decoration: underline; }

/* ── Footer links ──────────────────────────────────────────────────── */
/* Kept the colour of the text around them: the footer is the quietest part of
   the page and a row of accent links would be the loudest thing in it. */

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