/* Home — the hero: headline column, the TikTok phone mockup, the app card.

   The phone is the biggest single component on the site and none of it is used
   anywhere else. Its markup in index.html is frozen by
   backend/scripts/build_hero_slides.py; the CSS is not. */

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

.hero { position: relative; padding-top: clamp(110px, 13vw, 168px); padding-bottom: var(--gap); overflow: hidden; }
.hero > * { position: relative; }

.hero__grid { display: grid; grid-template-columns: minmax(0, 1.08fr) minmax(0, .92fr); gap: clamp(32px, 4.6vw, 72px); align-items: center; }

/* Sized off the longest line, not .h1's generic cap: the payoff runs 19 characters
   against a 1.08fr column, and at 78px it wrapped mid-phrase and broke the swash. */
.hero__title { margin-top: 22px; font-size: clamp(34px, 4.3vw, 58px); }
/* the signature: a hand-drawn marker stroke under the payoff line */
.swash { position: relative; display: inline-block; white-space: nowrap; }
.swash svg { position: absolute; left: -1.5%; bottom: -.16em; width: 103%; height: .34em; overflow: visible; }
.swash path { fill: none; stroke: var(--blue-deep); stroke-width: 15; stroke-linecap: round; opacity: .95;
  stroke-dasharray: 1000; stroke-dashoffset: 1000; animation: draw 1.1s ease-out .55s forwards; }
@keyframes draw { to { stroke-dashoffset: 0; } }

.hero__sub { margin: clamp(20px, 2.4vw, 28px) 0 0; max-width: 40ch; }

.console {
  margin-top: clamp(24px, 3vw, 34px);
  background: #fff; border: 1px solid var(--line-ctl); border-radius: 999px;
  padding: 7px 7px 7px 22px; display: flex; align-items: center; gap: 14px; max-width: 540px;
  box-shadow: 0 10px 30px -18px rgba(25,27,30,.28);
}
/* The field is the input; the typed URL is a ghost lying on top of it. Both are laid
   out the same way — one flex row, centred — so the ghost's first character sits where
   the real caret will be and the swap on focus doesn't move the text. */
.console__field { position: relative; flex: 1; min-width: 0; display: flex; align-items: center; overflow: hidden; }
.console__input {
  flex: 1; min-width: 0; font: inherit; font-size: 14.5px; font-weight: 600; line-height: 1.5;
  color: var(--ink); background: none; border: 0; border-radius: 0; padding: 0; margin: 0; appearance: none;
}
.console__input:focus { outline: none; } /* the whole console takes the ring instead, below */
.console__ghost { position: absolute; inset: 0; display: flex; align-items: center; pointer-events: none; white-space: nowrap; }
/* Two conditions, and the focused one is CSS rather than the class hero.js also sets:
   focus is a state the stylesheet can read for itself, and a browser that hands out
   focus without firing the event (a background tab restoring one) would otherwise leave
   the animation typing across the visitor's own caret. `is-live` covers the other half
   — a field holding text that nobody is currently in. */
.console:focus-within .console__ghost,
.console.is-live .console__ghost { display: none; }
.console__scheme { font-size: 14.5px; line-height: 1.5; color: var(--ink-50); flex: none; }
.console__typed { font-size: 14.5px; line-height: 1.5; font-weight: 600; white-space: nowrap; }
.console__caret { display: inline-block; width: 2px; height: 1.05em; background: var(--blue-deep); margin-left: 2px; animation: blink 1.1s steps(1) infinite; }
@keyframes blink { 50% { opacity: 0; } }

/* The input has no border of its own, so the focus ring belongs to the pill around it —
   without this a keyboard user tabs into an invisible control. */
.console:focus-within { border-color: var(--blue-deep); box-shadow: 0 0 0 3px rgba(21,99,242,.16), 0 10px 30px -18px rgba(25,27,30,.28); }

/* Reserves no space when empty: an error that pushes the note and the phone down the
   page on first paste is worse than the mistake it reports. */
.console__error { margin: 10px 0 0; font-size: 13.5px; font-weight: 600; color: var(--cross); max-width: 540px; }
.console__error:empty { display: none; }

/* The tick and the line of text are flex items, so before `flex: 1` below, a note too
   long for one line wrapped as a whole block and left the tick stranded on a line of
   its own. It shows up as soon as the copy grows. Top alignment (plus a 2px optical
   nudge) keeps the tick against the FIRST line of a two-line note, where a checkmark
   belongs. */
.hero__note { margin: 16px 0 0; font-size: 14.5px; color: var(--ink-70); display: flex; align-items: flex-start; gap: 9px; }
.hero__note > span:not(.hero__tick) { flex: 1; min-width: 0; }
.hero__watch { margin: 18px 0 0; font-size: 15px; font-weight: 700; }
.hero__watch a { color: var(--blue-deep); text-decoration: underline; text-underline-offset: 4px; text-decoration-thickness: 1.5px; }
.hero__tick { width: 18px; height: 18px; border-radius: 50%; background: var(--green); color: var(--ink); display: grid; place-items: center; font-size: 10px; font-weight: 700; flex: none; margin-top: 2px; }

/* One width for the frame and the row beneath it. They were two independent
   min(100%, 300px) declarations, which is a drift waiting to happen — the app card
   is meant to sit flush under the phone and nothing enforced it.
   The phone is the tallest thing in the hero, so on a short window it takes what
   the viewport can spare rather than pushing the app card and its arrow below the
   fold: 362px is everything else in the column (the section's padding, the dots,
   the card) and 375/812 turns the height that leaves into a width. 268px is the
   ceiling; below 240 the chrome's smallest labels stop reading as words. The flat
   declaration comes first as the fallback for engines without svh. */
.phone-wrap { --phone-w: 268px; }
.phone-wrap { --phone-w: clamp(240px, calc((100svh - 362px) * 375 / 812 + 18px), 268px); }

/* phone with real TikTok chrome, showing a real generated slide */
.phone {
  width: min(100%, var(--phone-w, 268px)); margin-inline: auto;
  background: var(--ink); border-radius: 38px; padding: 9px;
  box-shadow: 0 30px 70px -30px rgba(25,27,30,.5);
  position: relative;
}
/* 375x812 — the reference capture's own screen size. The chrome below is measured
   off that PNG in its pixels, so the frame has to carry its aspect or every offset
   drifts; --u converts one reference pixel into a share of the rendered width. */
.phone__screen {
  position: relative; width: 100%; aspect-ratio: 375/812;
  border-radius: 30px; overflow: hidden; background: #000;
  container-type: inline-size;
  --u: calc(100cqw / 375);
  font-family: var(--sans);
}
.phone__screen img { width: 100%; height: 100%; object-fit: cover; }
/* Eight real slides crossfading in place, one per app on the account. Six static
   dots asked the visitor to infer "slideshow"; actually advancing proves it before
   they read a word. Stacked layers rather than one swapped src, so the change is a
   fade and never a flash of empty — only the first carries its bytes in the markup,
   the rest are hydrated from data-src (see the hero block in main.js). */
.phone__slide { position: absolute; inset: 0; opacity: 0; transition: opacity .5s ease; }
.phone__slide.is-on { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .phone__slide { transition: none; } }
/* Chrome geometry follows the in-app reference capture
   (frontend/src/components/molecules/TikTokOverlay/tiktok-home.png): status bar and
   feed tabs at the top, action rail bottom-right, caption block bottom-left above the
   slideshow dots, tab bar across the foot. Drawn in CSS rather than dropped in as that
   PNG so the caption and handle can change with the slide. */
.phone__scrim {
  position: absolute; inset: 0;
  background:
    linear-gradient(to bottom, rgba(0,0,0,.42) 0%, rgba(0,0,0,.10) 11%, rgba(0,0,0,0) 22%),
    linear-gradient(to top, rgba(0,0,0,.62) 10.4%, rgba(0,0,0,.34) 20%, rgba(0,0,0,.08) 38%, rgba(0,0,0,0) 55%);
}
.phone__screen svg { fill: currentColor; display: block; }

/* Status bar — clock baseline at ref y17-28, 27/20 side margins. */
.phone__status {
  position: absolute; top: calc(16 * var(--u)); left: calc(32 * var(--u)); right: calc(20 * var(--u));
  display: flex; align-items: center; justify-content: space-between;
  color: #fff; font-size: calc(14 * var(--u)); font-weight: 600; letter-spacing: .01em; line-height: 1;
}
.phone__statusR { display: flex; align-items: center; gap: calc(5 * var(--u)); }
/* These three were the last part of the chrome still sized in absolute px — the
   width="17"/"16"/"24" attributes in the markup — so they alone did not follow the
   frame. At a 240px phone that drew a 24px battery next to an 8px clock. Same
   N-reference-px x --u rule as every other offset in this block; the attributes
   stay as the no-CSS fallback, and their viewBox numbers are the reference sizes. */
.phone__statusR svg { height: auto; }
.phone__statusR svg:nth-of-type(1) { width: calc(17 * var(--u)); }
.phone__statusR svg:nth-of-type(2) { width: calc(16 * var(--u)); }
.phone__statusR svg:nth-of-type(3) { width: calc(24 * var(--u)); }

/* Top bar — LIVE shortcut and search flank the tabs, all on one row at ref y55. */
.phone__top {
  position: absolute; top: calc(52 * var(--u)); left: calc(14 * var(--u)); right: calc(14 * var(--u));
  display: flex; align-items: center; justify-content: space-between;
  color: #fff; text-shadow: 0 calc(1 * var(--u)) calc(6 * var(--u)) rgba(0,0,0,.4);
}
.phone__live { display: grid; justify-items: center; gap: calc(1 * var(--u)); }
.phone__live svg { width: calc(22 * var(--u)); height: calc(22 * var(--u)); }
.phone__live b { font-size: calc(8 * var(--u)); font-weight: 800; letter-spacing: .04em; line-height: 1; }
.phone__search svg { width: calc(23 * var(--u)); height: calc(23 * var(--u)); }

.phone__tabs {
  display: flex; align-items: center; gap: calc(16 * var(--u));
  font-size: calc(15 * var(--u)); line-height: 1.15;
  color: rgba(255,255,255,.7); font-weight: 500;
}
/* The active tab carries a short underline; Following badges a red dot. */
.phone__tabs .on { position: relative; color: #fff; font-weight: 700; }
.phone__tabs .on::after {
  content: ''; position: absolute; left: 50%; transform: translateX(-50%);
  bottom: calc(-7 * var(--u)); width: calc(26 * var(--u)); height: calc(2.5 * var(--u));
  border-radius: calc(2 * var(--u)); background: #fff;
}
.phone__tabs .phone__badge { position: relative; }
.phone__tabs .phone__badge::after {
  content: ''; position: absolute; right: calc(-4 * var(--u)); top: calc(-1 * var(--u));
  width: calc(6 * var(--u)); height: calc(6 * var(--u)); border-radius: 50%; background: #FE2C55;
}

/* Action rail — items centred on ref x344. Five stops now: the capture's order is
   avatar, heart, comment, bookmark, share, sound disc; the bookmark was missing
   before, which is the kind of gap a reader spots without being able to name it. */
.phone__rail {
  position: absolute; right: calc(1 * var(--u)); bottom: calc(100 * var(--u));
  display: grid; justify-items: center; color: #fff; width: calc(60 * var(--u));
  filter: drop-shadow(0 calc(1 * var(--u)) calc(4 * var(--u)) rgba(0,0,0,.32));
}
.phone__avatar { position: relative; display: block; width: calc(46 * var(--u)); height: calc(46 * var(--u)); margin-bottom: calc(28 * var(--u)); }
.phone__avatar img { width: 100%; height: 100%; border-radius: 50%; border: calc(1.5 * var(--u)) solid #fff; object-fit: cover; background: #191b1e; }
.phone__avatar i {
  position: absolute; left: 50%; bottom: calc(-9 * var(--u)); transform: translateX(-50%);
  width: calc(19 * var(--u)); height: calc(19 * var(--u)); border-radius: 50%;
  background: #FE2C55; color: #fff; font-style: normal; font-weight: 700;
  font-size: calc(14 * var(--u)); line-height: calc(18 * var(--u)); text-align: center;
}
.phone__act { display: grid; justify-items: center; font-size: calc(12 * var(--u)); font-weight: 600; line-height: 1; }
.phone__act svg { width: calc(33 * var(--u)); height: calc(33 * var(--u)); }
.phone__act b { font-weight: 600; margin-top: calc(6 * var(--u)); }
.phone__act + .phone__act { margin-top: calc(16 * var(--u)); }
/* The sound disc rides lower than the rail rhythm — 30 ref px below the share count. */
.phone__disc {
  width: calc(44 * var(--u)); height: calc(44 * var(--u)); margin-top: calc(30 * var(--u));
  border-radius: 50%; display: grid; place-items: center;
  background: linear-gradient(150deg, #2e2e2e, #0b0b0b 62%);
  animation: discSpin 5s linear infinite;
}
.phone__disc img { width: calc(24 * var(--u)); height: calc(24 * var(--u)); border-radius: 50%; object-fit: cover; }
@keyframes discSpin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .phone__disc { animation: none; } }

/* Caption block — ref left margin 12, handle at y644, caption 15/18, sound row to y713. */
.phone__info { position: absolute; left: calc(12 * var(--u)); right: calc(74 * var(--u)); bottom: calc(99 * var(--u)); }
.phone__meta { margin: 0; font-size: calc(15 * var(--u)); font-weight: 700; line-height: calc(17 * var(--u)); color: #fff; }
.phone__cap {
  margin: calc(8 * var(--u)) 0 0; font-family: var(--post); font-weight: 400;
  font-size: calc(15 * var(--u)); line-height: calc(18 * var(--u)); color: #fff;
}
.phone__music { margin: calc(5 * var(--u)) 0 0; display: flex; align-items: center; gap: calc(6 * var(--u)); font-size: calc(14 * var(--u)); line-height: 1; color: #fff; }
.phone__music svg { width: calc(14 * var(--u)); height: calc(14 * var(--u)); flex: none; }

/* Slideshow dots — outside the frame, above it. They belong to the page (they say
   "this is advancing"), not to the screenshot, so they take page ink rather than
   the white-on-video TikTok draws inside the screen. */
.phone__dots { display: flex; justify-content: center; gap: 7px; margin-bottom: 14px; }
.phone__dots i { width: 6px; height: 6px; border-radius: 50%; background: rgba(25,27,30,.22); transition: background .3s ease, transform .3s ease; }
.phone__dots i.on { background: var(--ink); transform: scale(1.25); }
@media (prefers-reduced-motion: reduce) { .phone__dots i { transition: none; } }

/* Tab bar — opaque black from ref y728 to the foot, five equal columns, icons at
   y734-755 with their labels under them, home indicator at y799. */
.phone__nav {
  position: absolute; left: 0; right: 0; bottom: 0; height: calc(84 * var(--u));
  display: grid; grid-template-columns: repeat(5, 1fr); align-content: start;
  padding-top: calc(2 * var(--u)); background: #000; color: rgba(255,255,255,.6);
}
.phone__nav span { display: grid; justify-items: center; font-size: calc(10 * var(--u)); font-weight: 600; line-height: 1; }
.phone__nav svg { width: calc(24 * var(--u)); height: calc(24 * var(--u)); margin-bottom: calc(4 * var(--u)); }
.phone__nav .on { color: #fff; }
/* TikTok's create button: a white pill flanked by its cyan and red offsets. */
.phone__plus { position: relative; align-self: center; }
.phone__plus i {
  display: block; width: calc(43 * var(--u)); height: calc(28 * var(--u));
  border-radius: calc(8 * var(--u)); background: #fff;
  box-shadow: calc(-3.5 * var(--u)) 0 0 calc(-1 * var(--u)) #25F4EE, calc(3.5 * var(--u)) 0 0 calc(-1 * var(--u)) #FE2C55;
}
.phone__plus b {
  position: absolute; inset: 0; display: grid; place-items: center;
  color: #191b1e; font-size: calc(18 * var(--u)); font-weight: 700; line-height: 1;
}
.phone__home {
  position: absolute; left: 50%; transform: translateX(-50%); bottom: calc(9 * var(--u));
  width: calc(134 * var(--u)); height: calc(5 * var(--u)); border-radius: calc(3 * var(--u)); background: #fff;
}

/* The app behind the post — a quiet row under the frame, not a second card
   competing with the phone. Fixed height and a reserved icon box so it never
   reflows as the slideshow swaps apps. */
.appcard {
  width: min(100%, var(--phone-w, 268px)); margin: 16px auto 0;
  display: flex; align-items: center; gap: 12px;
}
/* The hairline is load-bearing, not decoration: App Store artwork is delivered on
   whatever background the maker chose, and two of these eight are white to the
   edge (Feel is a rose on pure white). On a white page those have no silhouette at
   all — the icon reads as a fragment floating next to the name. The border is what
   gives every icon the same squircle, whatever its artwork does. Inset rather than
   a real border so it never eats 2px of a 48px icon or shifts the row. */
.appcard__icon {
  width: 48px; height: 48px; flex: none; border-radius: 12px;
  object-fit: cover; background: var(--line);
  box-shadow: inset 0 0 0 1px var(--line-ctl);
}
.appcard__text { display: grid; gap: 2px; min-width: 0; flex: 1 1 auto; }
.appcard__name { font-size: 15px; font-weight: 800; color: var(--ink); line-height: 1.2; }
/* Two reserved lines rather than one clipped one: the arrow takes 40px of a 268px
   row, which leaves the subtitle too little to hold a line like "Daily affirmations,
   gently delivered" — every one of the eight would ellipsis. The height is fixed
   for the reason the row always had one: the slideshow swaps apps every 2.6s and
   must never reflow the column under the phone. */
.appcard__sub {
  font-style: normal; font-size: 13px; line-height: 1.3; color: var(--ink-50);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
  overflow: hidden; height: calc(2 * 1.3 * 13px);
}
/* The visitor's own handle on the slideshow. Round, like the frame it sits under;
   --line-ctl rather than --line because this is an interactive boundary and 1.4.11
   wants 3:1. Hover, focus ring and the reduced-motion stop all come from the
   global rules at the head of this file. */
.appcard__next {
  flex: none; width: 40px; height: 40px; padding: 0;
  border-radius: 50%; border: 1px solid var(--line-ctl);
  background: #fff; color: var(--ink);
  display: grid; place-items: center; box-shadow: var(--shadow);
}
.appcard__next svg { width: 16px; height: 16px; fill: currentColor; display: block; }

/* ── Breakpoints ─────────────────────────────────────────────── */

@media (max-width: 980px) {
  .hero__grid { grid-template-columns: 1fr; }
  /* Stacked, the phone no longer has to share a screenful with the copy beside
     it, so it goes back to full size and scrolls to like everything else. */
  .phone-wrap { --phone-w: 268px; }
}
@media (max-width: 720px) {
  .hero__title .swash { white-space: normal; }
  /* The console used to vanish here, leaving only the button: it was a decorative
     typing animation, and a phone has no room to run one beside a CTA — the field
     clipped to "https://apps". Now that it takes a real paste it has to survive, so it
     stacks instead: field on its own row at full width, button beneath it, still sized
     like the primary CTA it also is. Nothing types into it on a phone (hero.js leaves a
     short static hint), so the caret has nothing to mark. */
  .console { flex-direction: column; align-items: stretch; gap: 12px; background: none; border: 0; box-shadow: none; padding: 0; }
  /* Stacked, the outer pill would be a box drawn around two things that already look
     like themselves, so the card moves INTO the field — which is the half that has to
     read as tappable. The button below it is already unmistakable. */
  .console__field { background: #fff; border: 1px solid var(--line-ctl); border-radius: 999px; padding: 15px 22px; box-shadow: 0 10px 30px -18px rgba(25,27,30,.28); }
  .console:focus-within { border-color: transparent; box-shadow: none; }
  .console__field:focus-within { border-color: var(--blue-deep); box-shadow: 0 0 0 3px rgba(21,99,242,.16); }
  .console__caret { display: none; }
  .console .btn { width: 100%; justify-content: center; padding: 15px 24px; font-size: 15px; }
}
