/* ==========================================================================
   Coloroid AI — /app, the web funnel.
   Loaded after styles.css and reusing its tokens; only the flow screens live
   here. Metrics are the app's own: 52px controls with a 26px radius from
   ActionButton.swift, a 560px column from ContentWidth.swift, and the paywall
   proportions from PaywallView.swift.
   ========================================================================== */

/* The page is exactly one viewport and never scrolls: there is no header and
   no footer to scroll past, and a screen that does not fit is compressed (see
   the short-viewport blocks at the foot of this file) rather than pushed below
   the fold. The lock belongs on <html>, which is the element that actually
   scrolls; `overflow` on the body would only propagate to the viewport and
   leave the body itself unclipped. `data-page` is stamped by the inline script
   in the head of each page that wants the lock — /app and /demo, which share
   this whole shell. */
html[data-page="app"],
html[data-page="demo"] {
  overflow: hidden;
  /* No rubber-band: with nothing underneath to reveal, the bounce reads as a
     glitch, and on iOS it is also what lets a stray drag retract the toolbars
     and change the viewport height mid-flow. */
  overscroll-behavior: none;
}

.app-page {
  /* The four insets are read once here rather than at every use: env() cannot
     be overridden, so routing them through variables is what makes the layout
     testable at a notch's dimensions without a notched device.

     They are 0 in mobile Safari, which reserves its own bars outside the
     viewport, and non-zero where the page really does run to the glass:
     an installed PWA, and Android 15+, which draws every app edge-to-edge
     under the gesture bar. */
  --inset-top: env(safe-area-inset-top, 0px);
  --inset-right: env(safe-area-inset-right, 0px);
  --inset-bottom: env(safe-area-inset-bottom, 0px);
  --inset-left: env(safe-area-inset-left, 0px);

  display: flex;
  overflow: hidden;
  overscroll-behavior: none;

  /* svh, and emphatically not vh. `vh` is the LARGE viewport — the height the
     page would have if the browser's bars were retracted — so on an iPhone
     with the bars showing it overshoots the glass and Safari hands back the
     difference as scroll. Measured on iOS 26, iPhone 17 Pro: vh and lvh resolve
     to 754px against an innerHeight of 714. `position: fixed; inset: 0` is the
     same trap, since it resolves against that same initial containing block.
     svh is the small viewport: the visible area with every bar on screen, and
     the only height guaranteed to fit. dvh would be equal here — nothing can
     scroll, so the bars never retract — but svh is the one that stays right
     even if they do. */
  height: 100vh;
  height: 100svh;
}

/* ── the column ──────────────────────────────────────────────────────────── */

.flow {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 560px;
  min-height: 0;
  margin-inline: auto;
  /* Nothing above it now, so the top inset is ours to clear as well. */
  padding:
    calc(16px + var(--inset-top))
    calc(16px + var(--inset-right))
    calc(16px + var(--inset-bottom))
    calc(16px + var(--inset-left));
}

/* Wide *and* tall enough: the gutters and the card's own padding cost 116px of
   height, which a window has to be able to spare. A phone on its side is 720px
   wide and 390px tall — there the full-bleed column below is the right answer,
   and the short-viewport blocks at the foot of this file take it from there. */
@media (min-width: 720px) and (min-height: 620px) {
  .app-page {
    align-items: center;
    justify-content: center;
    padding:
      calc(28px + var(--inset-top))
      calc(28px + var(--inset-right))
      calc(28px + var(--inset-bottom))
      calc(28px + var(--inset-left));
  }
  .flow {
    /* A card floating in the middle of the window rather than a full-bleed
       column. 660px is its comfortable height; below that it takes whatever
       the window has left over after the 28px of padding on each side. */
    height: min(660px, 100%);
    padding: 30px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
  }
}

/* min-height:0 is not optional: without it a flex item refuses to shrink below
   its content, and one tall photo further down would push the whole column
   past the fold. */
.screen { display: none; flex: 1; min-height: 0; flex-direction: column; gap: 18px; }
.screen.is-active { display: flex; }

.screen__body { flex: 1; display: flex; flex-direction: column; gap: 14px; min-height: 0; }
.screen__body--center { align-items: center; justify-content: center; text-align: center; gap: 10px; }
.screen__body--photo { justify-content: center; }

.screen__foot { display: grid; gap: 12px; }
.screen__foot--center { justify-items: center; }

.screen__ico { width: 46px; height: 46px; color: var(--accent-brand); margin-bottom: 6px; }

.screen__title { font-size: 26px; font-weight: 600; letter-spacing: -.02em; line-height: 1.2; }
.screen__title--sm { font-size: 22px; text-align: center; }
.screen__sub { max-width: 34ch; font-size: 17px; color: var(--muted); }

.screen__error { font-size: 15px; color: var(--magenta); }

.fineprint { font-size: 13px; line-height: 1.5; color: var(--muted); text-align: center; }
.fineprint a { color: var(--violet); text-decoration: underline; text-underline-offset: 2px; }

/* Kept out of the layout without `display:none`, which some browsers treat as
   a reason not to open the picker at all. */
.hidden-input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }

/* ── controls (ActionButton.swift) ───────────────────────────────────────── */

.act {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 52px; padding: 0 20px;
  border: 0; border-radius: 26px;
  background: var(--surface-2); color: var(--text);
  font-family: var(--font); font-size: 17px; font-weight: 700; line-height: 1;
  text-decoration: none; cursor: pointer;
  transition: background-color .2s var(--ease), opacity .15s var(--ease);
}
.act--primary { background: var(--accent-brand); }
.act--primary:hover { background: var(--accent-brand-hi); }
.act--secondary { background: rgba(255,255,255,.09); }
.act--secondary:hover { background: rgba(255,255,255,.14); }
.act:active { opacity: .72; }

/* Which upload controls make sense depends on the device, not the width: a
   phone gets the camera, a desktop gets a drop target. */
html[data-os="desktop"] .only-mobile { display: none; }
html:not([data-os="desktop"]) .only-desktop { display: none; }

.drop {
  display: grid; place-items: center;
  width: 100%; max-width: 380px; margin-top: 12px; padding: 30px 20px;
  border: 1.5px dashed var(--line); border-radius: var(--r-md);
  font-size: 15px; color: var(--muted);
  transition: border-color .2s var(--ease), background-color .2s var(--ease);
}
.drop.is-over { border-color: var(--accent-brand); background: rgba(156,81,249,.07); }

/* ── photos ──────────────────────────────────────────────────────────────── */

.photo { width: auto; max-width: 100%; object-fit: contain; margin-inline: auto; }
/* Shrinkable for the same reason the paywall's photo is, and `min-height: 0`
   for the same reason too: a replaced element's automatic minimum is its own
   intrinsic height, which would hold the six rows below it off the screen. */
.photo--analyze { flex: 0 1 auto; min-height: 0; max-height: 320px; border-radius: 20px; }

/* The preview screen shows the photo at whatever size is left over, and a photo
   can be any shape at all — a 4000px-tall scan included.
   An <img> left in normal flow reports its intrinsic height as a minimum that
   the column above it cannot refuse, so `max-height: 100%` never bit (the
   parent's height is indefinite, so the percentage resolves to nothing) and the
   card grew to the height of the photo. Taking the image out of flow fixes that
   at the root: it contributes no height at all, the box keeps exactly what flex
   hands it, and the photo letterboxes inside. */
.photo-fit { position: relative; flex: 1 1 auto; min-height: 120px; }
/* No corner radius, and not by omission: PhotoPreviewView shows the picked
   photo with no clipShape at all — only the analyze and paywall screens round
   it. It would not have worked here anyway, since `contain` insets the painted
   image inside the box the radius clips. */
.photo-fit img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: contain;
}
/* PaywallView gives the photo a layoutPriority of -1: it is the one element
   that gives way when the screen runs short, so the plans and the button never
   get pushed off. flex-shrink plus min-height:0 is the same bargain here. */
.photo--paywall { flex: 1 1 auto; min-height: 90px; max-height: 240px; border-radius: 14px; }

/* ── analyze rows (AnalyzeViewModel.Step) ────────────────────────────────── */

.analyze-steps { flex: none; list-style: none; margin: 6px 0 0; padding: 0 0 0 12px; display: grid; gap: 16px; }
.analyze-step {
  display: flex; align-items: center; gap: 12px;
  font-size: 15px; font-weight: 500; color: var(--muted);
  transition: color .2s var(--ease);
}
.analyze-step.is-running { color: var(--text); }

.analyze-step__mark { position: relative; flex: none; display: grid; place-items: center; width: 24px; height: 24px; }
.analyze-step__mark::before {
  content: ''; width: 18px; height: 18px; border-radius: 50%;
  border: 2px solid var(--accent-brand); opacity: .4;
}
.analyze-step.is-running .analyze-step__mark::before {
  opacity: 1; border-top-color: transparent;
  animation: step-spin .8s linear infinite;
}
.analyze-step.is-done .analyze-step__mark::before { opacity: 1; background: var(--accent-brand); }

.analyze-step__check { position: absolute; width: 11px; height: 11px; color: #fff; opacity: 0; }
.analyze-step.is-done .analyze-step__check { opacity: 1; }

@keyframes step-spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  /* The timings stay; only the spinning goes, so a running row is still told
     apart from a pending one by its solid ring. */
  .analyze-step.is-running .analyze-step__mark::before { animation: none; border-top-color: var(--accent-brand); }
}

/* ── paywall (PaywallView.swift) ─────────────────────────────────────────── */

.pw { gap: 14px; align-items: stretch; }

.pw__title {
  font-size: 27px; font-weight: 800; line-height: 1.12; letter-spacing: -.01em;
  text-align: center; text-transform: uppercase;
}
.pw__accent { color: var(--accent-brand); }

.pw__benefits { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; justify-content: center; }
.pw__benefits li { display: flex; align-items: center; gap: 12px; font-size: 17px; }
.pw__benefits svg { flex: none; width: 22px; height: 22px; color: var(--accent-brand); }

.plans { display: grid; gap: 12px; margin-top: 2px; }

.plan {
  position: relative;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  width: 100%; min-height: 62px; padding: 0 16px;
  background: transparent; border: 1.5px solid var(--line); border-radius: 14px;
  color: var(--text); font-family: var(--font); text-align: left; cursor: pointer;
  transition: border-color .2s var(--ease), background-color .2s var(--ease);
}
.plan.is-selected { border-color: var(--accent-brand); background: rgba(156,81,249,.06); }

.plan__name { font-size: 16px; font-weight: 600; }
.plan__price { display: grid; gap: 2px; justify-items: end; text-align: right; }
.plan__price b { font-size: 15px; font-weight: 700; }
.plan__price i { font-size: 15px; font-style: normal; color: var(--muted); }

.plan__badge {
  /* Half in, half out of the card, exactly like the app's layout. */
  position: absolute; top: -8px; left: 16px;
  display: inline-flex; align-items: center; height: 16px; padding: 0 8px;
  background: var(--accent-brand); border-radius: 8px;
  font-size: 11px; font-weight: 700; letter-spacing: .02em; color: #fff;
}

.pw__foot { display: grid; gap: 8px; padding-top: 2px; }
.pw__note { font-size: 12px; font-weight: 500; color: var(--muted); text-align: center; }
.pw__links { font-size: 14px; color: var(--muted); text-align: center; }
.pw__links a { text-decoration: none; }
.pw__links a:hover { color: var(--text); }

/* ── confirmation dialog ─────────────────────────────────────────────────── */

/* Fixed to the viewport, so it needs the insets exactly as the screens do —
   and it is the one place where losing the bottom of the box loses the Cancel
   button, which is the control someone reaches for when they did not mean to
   be here at all. */
.modal {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center;
  padding:
    calc(22px + var(--inset-top))
    calc(22px + var(--inset-right))
    calc(22px + var(--inset-bottom))
    calc(22px + var(--inset-left));
}
.modal[hidden] { display: none; }

.modal__backdrop {
  position: absolute; inset: 0;
  background: rgba(6,5,12,.74);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
}
.modal__card {
  position: relative;
  display: grid; gap: 12px;
  width: 100%; max-width: 380px; padding: 26px 22px 22px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-md);
  text-align: center;
}
.modal__title { font-size: 20px; font-weight: 700; letter-spacing: -.01em; }
.modal__plan { display: grid; gap: 4px; }
.modal__plan b { font-size: 16px; font-weight: 600; }
.modal__plan span { font-size: 22px; font-weight: 800; color: var(--accent-brand); }
.modal__fine { margin-bottom: 6px; font-size: 13px; color: var(--muted); }

/* ── narrow phones ───────────────────────────────────────────────────────── */

@media (max-width: 380px) {
  .screen__title { font-size: 23px; }
  .pw__title { font-size: 24px; }
  .pw__benefits li, .plan__name { font-size: 15px; }
  .photo--paywall { max-height: 170px; }
}

/* ── short viewports ─────────────────────────────────────────────────────── */
/* Nothing scrolls, so on a short window a screen has to give something up
   instead. The order is the app's own: the photo goes first (PaywallView hands
   it a layoutPriority of -1 for exactly this), then the spacing, then the type.
   The controls are last and the paywall's Privacy and Terms links never go —
   the whole point of the screen is that the visitor can act on it.

   The tiers are the heights the funnel actually meets: ~700px is a small phone
   held upright, ~600px a browser window sharing a laptop screen, ~480px a
   phone turned on its side. */

@media (max-height: 700px) {
  .flow {
    padding-block:
      calc(12px + var(--inset-top))
      calc(12px + var(--inset-bottom));
  }
  .screen { gap: 14px; }
  .screen__body { gap: 10px; }
  .pw { gap: 10px; }
  .pw__benefits { gap: 8px; }
  .plans { gap: 8px; }
  .analyze-steps { gap: 12px; }
  .photo--analyze { max-height: 240px; }
  .photo--paywall { max-height: 170px; }
}

@media (max-height: 600px) {
  .screen__ico { width: 38px; height: 38px; margin-bottom: 0; }
  .screen__title { font-size: 22px; }
  .screen__sub { font-size: 15px; }
  .pw__title { font-size: 21px; }
  .pw__benefits li { font-size: 15px; }
  .pw__benefits svg { width: 19px; height: 19px; }
  .plan { min-height: 54px; }
  .analyze-steps { gap: 9px; }
  .photo--analyze { max-height: 180px; }
  .photo--paywall { min-height: 0; max-height: 110px; }
}

@media (max-height: 480px) {
  /* Below this the paywall photo is too small to be worth the room it costs,
     and it is the element the app already treats as expendable. */
  .photo--paywall { display: none; }
  .photo--analyze { max-height: 120px; }
  .act { height: 46px; border-radius: 23px; }
  .plan { min-height: 44px; }   /* the 44px floor for a tap target, not below */
  .screen__ico { width: 30px; height: 30px; }
  .screen__body--center { gap: 6px; }
  .pw { gap: 8px; }
  .pw__benefits { gap: 6px; }
  .pw__foot { gap: 6px; }
  .pw__note { font-size: 11px; }
  .analyze-steps { gap: 7px; }
  .fineprint { font-size: 12px; }

  .modal { padding-block: calc(10px + var(--inset-top)) calc(10px + var(--inset-bottom)); }
  .modal__card { gap: 8px; padding: 16px 18px; }
  .modal__title { font-size: 18px; }
  .modal__plan span { font-size: 19px; }
  .modal__fine { margin-bottom: 0; }
}

/* Short *and* wide is a phone on its side, and there the width is the thing
   there is plenty of: the benefits go in a row and hand ~60px back to the
   plans and the button. Shaving the type instead would have bought a quarter
   of that and left nothing for a translation longer than the English. */
@media (max-height: 480px) and (min-width: 560px) {
  .pw__benefits { grid-auto-flow: column; justify-content: center; gap: 22px; }
}

/* Landscape spends width to buy height. A wider column puts the upload screen's
   subtitle and fine print back on one line each — about 40px the stack no
   longer has to find — and gives the two columns below room to be columns.
   The 560px cap is ContentWidth.swift's, and it is a portrait measurement;
   there is no phone this shape for it to match. */
@media (max-height: 480px) and (min-width: 700px) {
  .flow { max-width: 680px; }
}

/* The drop target is a desktop nicety; at this height it cannot have 30px of
   padding, and the button beside it does the same job. */
@media (max-height: 480px) {
  .drop { margin-top: 6px; padding: 12px 20px; }
}

/* Below ~430px the paywall's five stacked blocks stop fitting at any type size
   — a 360x800 Android on its side leaves about 245px of usable height and the
   stack needs about 280 — so the layout changes instead of the type: the pitch
   on the left, the thing you act on the right. Every other screen is only a
   photo or a couple of lines and stays in one column. */
@media (max-height: 430px) and (min-width: 620px) {
  .pw {
    display: grid;
    /* Not an even split: the right column carries "$49.99 per year" and the
       button label, both of which wrap at half of 680. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr);
    align-content: center;
    column-gap: 24px;
    row-gap: 10px;
  }
  .pw__title { grid-area: 1 / 1; align-self: center; text-align: left; }
  /* Back to a stacked list: the left column is half the width the row needed. */
  .pw__benefits { grid-area: 2 / 1; grid-auto-flow: row; justify-content: start; gap: 8px; }
  .plans { grid-area: 1 / 2; margin-top: 0; }
  .pw__foot { grid-area: 2 / 2; }
}
