/* SD-2163: the shell's own chrome — the connection states only. Everything
 * inside the iframe is the Remote UI, styled by localServerPage.js through
 * the tunnel; nothing here may leak into it. Tokens mirror
 * docs/design/styleguide.md / src/app/index.html (dark default, light via
 * media query) — the mirror is enforced byte-for-byte by
 * src/app/test/Vitest/specs/remoteShellTokens.test.js, so a token change in
 * the app goes red here instead of drifting silently. */

:root {
  /* SD-2774: the app moved onto the website's ground, palette and type; these
   * four follow it. The shell paints a flat --bg rather than the app's radial
   * wash — it is a connection screen, not the board. */
  --bg: #060a1e;
  --surface: #131731;
  --text: #f4f5fa;
  --muted: #99a1b8;
  --accent: #5a7dff;
  --danger: #e0524d;
  --danger-text: #f07f7a;
  --danger-soft: rgba(224, 82, 77, 0.16);
  /* SD-2224: the landing state borrows the sightdeck.app palette, not the
   * app's. It is the page a stranger meets the product on, so it should look
   * like the product's website — these values are the website's own
   * (src/website/public/styles.css). Not part of the mirrored token set above:
   * those mirror the desktop app, and drifting them would go red in
   * remoteShellTokens.test.js, which is exactly right. Same in both themes —
   * the website has one look. */
  --site-bg: #060a1e;
  --site-bg-2: #0c1432;
  --site-text: #f3f4f9;
  --site-accent: #22d3ee;
  --site-violet: #7c3aed;
  --site-pink: #ff2db1;
  --site-online: #6fde85;
  --site-online-soft: rgba(40, 201, 64, 0.10);
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #fbfcff;
    --surface: #ffffff;
    --text: #141a2e;
    --muted: #5d6684;
    --accent: #4361ee;
    --danger: #c0312d;
    --danger-text: #b52e2a;
    --danger-soft: rgba(192, 49, 45, 0.10);
  }
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif;
}

#app {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  background: var(--bg);
}

#overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: var(--bg);
  z-index: 10;
}

/* While reconnecting over a live page, the overlay dims instead of hiding it.
 * The rgba line is the fallback for engines without color-mix() (the same
 * older phones the classic service worker exists for) — without it the dim
 * would drop entirely and the copy would sit unreadable over live content. */
#overlay[data-state="reconnecting"].over-page {
  background: rgba(15, 15, 16, 0.82);
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(2px);
}

@media (prefers-color-scheme: light) {
  #overlay[data-state="reconnecting"].over-page {
    background: rgba(250, 250, 250, 0.82);
    background: color-mix(in srgb, var(--bg) 82%, transparent);
  }
}

#overlay[hidden] { display: none; }

.card {
  max-width: 360px;
  text-align: center;
}

.brand {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 28px;
}

.spinner {
  width: 28px;
  height: 28px;
  margin: 0 auto 20px;
  border-radius: 50%;
  border: 3px solid var(--surface);
  border-top-color: var(--accent);
  animation: shell-spin 0.9s linear infinite;
}

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

/* The stopped red ring is the SECURITY wall's treatment and only its —
 * "not paired" and a config gap are normal setup states and must not borrow
 * the alarm (Lena-review, SD-2163). */
#overlay[data-state="unverified"] .spinner {
  animation: none;
  border-color: var(--danger-soft);
  border-top-color: var(--danger);
}

#overlay[data-state="unpaired"] .spinner,
#overlay[data-state="config"] .spinner,
#overlay[data-state="unsupported"] .spinner,
#overlay[data-state="busy"] .spinner {
  animation: none;
  border-color: var(--surface);
  border-top-color: var(--muted);
}

h1 {
  /* Styleguide type scale: 17px heading, 13px body — same steps as the app. */
  font-size: 17px;
  font-weight: 600;
  margin: 0 0 8px;
}

#overlay[data-state="unverified"] h1 { color: var(--danger-text); }

p {
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted);
  margin: 0;
}

/* SD-2224: `unpaired` is the only state a stranger reaches by typing the
 * address, so it is not a connection state at all — it is the service's
 * landing page, and it is dressed as sightdeck.app rather than as the neutral
 * chrome every other state shares. Scoped to the state so none of the mirrored
 * tokens move, and identical in both themes: the website has one look.
 *
 * The website's own ground: two radial washes over a vertical navy gradient,
 * values lifted from src/website/public/styles.css so the two pages read as
 * one product. */
#overlay[data-state="unpaired"] {
  background:
    radial-gradient(ellipse at 30% -10%, rgba(124, 58, 237, 0.25), transparent 50%),
    radial-gradient(ellipse at 80% 110%, rgba(255, 45, 177, 0.18), transparent 50%),
    linear-gradient(180deg, var(--site-bg), var(--site-bg-2));
  color: var(--site-text);
}

#overlay[data-state="unpaired"] .brand,
#overlay[data-state="unpaired"] .spinner,
#overlay[data-state="unpaired"] .away {
  display: none;
}

/* SD-2225: the mark and its grid belong to the landing state only. Every other
 * state is a connection state and keeps the brand line and the spinner. */
.beacon,
.beacon-grid { display: none; }

#overlay[data-state="unpaired"] .beacon { display: block; }
#overlay[data-state="unpaired"] .beacon-grid { display: block; }

/* The mark emits. Three rings leave the core on a long stagger, so at any
 * moment one is near the mark and one is fading out at the edge — a signal
 * going out, not a loading indicator going round. */
.beacon {
  position: relative;
  width: 96px;
  height: 96px;
  margin: 0 auto 26px;
}

.beacon .mark {
  position: relative;
  z-index: 2;
  display: block;
}

.beacon .ring {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 96px;
  height: 96px;
  border-radius: 50%;
  border: 1px solid rgba(34, 211, 238, 0.55);
  opacity: 0;
  transform: scale(0.55);
  animation: shell-emit 4.2s cubic-bezier(0.22, 0.61, 0.36, 1) infinite;
}

/* The three ring colours walk the logo's own gradient — cyan at the core,
 * pink furthest out — so the emission is the mark's palette unrolled in time. */
.beacon .ring:nth-child(2) {
  animation-delay: 1.4s;
  border-color: rgba(124, 58, 237, 0.55);
}

.beacon .ring:nth-child(3) {
  animation-delay: 2.8s;
  border-color: rgba(255, 45, 177, 0.45);
}

@keyframes shell-emit {
  0%   { opacity: 0.85; transform: scale(0.55); }
  70%  { opacity: 0.12; }
  100% { opacity: 0; transform: scale(3.1); }
}

/* SD-2240: the same emission, stopped before it reaches the headline. Used on
 * narrow screens, where 3.1× of a 72px mark lands squarely in the text. */
@keyframes shell-emit-tight {
  0%   { opacity: 0.85; transform: scale(0.55); }
  70%  { opacity: 0.12; }
  100% { opacity: 0; transform: scale(2.15); }
}

/* A faint radar grid in the same circular geometry as the rings, masked so it
 * fades before it reaches the edges. Concentric rather than square on purpose:
 * a rectangular grid would be texture borrowed from somewhere else. */
.beacon-grid {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: repeating-radial-gradient(
    circle at 50% 44%,
    rgba(255, 255, 255, 0.045) 0 1px,
    transparent 1px 62px
  );
  -webkit-mask-image: radial-gradient(circle at 50% 44%, #000 0%, transparent 72%);
  mask-image: radial-gradient(circle at 50% 44%, #000 0%, transparent 72%);
}

/* The card sits above the grid. */
#overlay[data-state="unpaired"] .card {
  position: relative;
  z-index: 1;
}

#overlay[data-state="unpaired"] h1 {
  /* The website's hero weight and negative tracking, at a size that suits a
   * page holding one line. */
  font-size: clamp(30px, 7vw, 40px);
  font-weight: 600;
  letter-spacing: -0.022em;
  line-height: 1.15;
  color: var(--site-text);
  margin: 0 0 20px;
}

/* SD-2448: "Beta" as a smaller subtitle under the product name, not at hero size. */
#overlay[data-state="unpaired"] h1 .title-beta {
  display: block;
  font-size: 0.5em;
  font-weight: 600;
  letter-spacing: 0;
  opacity: 0.68;
  margin-top: 4px;
}

/* The website's `.kicker` pill carrying its `.ver-nav-dot` — the same idiom it
 * already uses for a released/live marker, so this is its status vocabulary
 * rather than a new one.
 *
 * It reports that this host answered, which is all a page can honestly report
 * about itself. It is not a health check of the service behind it. */
.badge {
  display: none;
  align-items: center;
  gap: 9px;
  padding: 5px 13px;
  border-radius: 999px;
  background: rgba(34, 211, 238, 0.12);
  color: var(--site-accent);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

#overlay[data-state="unpaired"] .badge { display: inline-flex; }

.badge .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--site-online);
  box-shadow: 0 0 0 3px var(--site-online-soft);
  flex: none;
}

/* SD-2765: the landing page's one line of explanation, and the three beats
 * under the actions. Both are landing-state only — every other state is a step
 * in a handshake, where a description of the product would be noise.
 *
 * They say what the product is and where it runs. They do not say how a
 * pairing is established: SD-2224 set that boundary for this state, and a
 * property ("paired once") stays on the right side of it where a procedure
 * would not. */
.what,
.beats { display: none; }

#overlay[data-state="unpaired"] .what {
  display: block;
  max-width: 25ch;
  margin: -8px auto 20px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted);
}

#overlay[data-state="unpaired"] .what b {
  color: var(--site-text);
  font-weight: 600;
}

#overlay[data-state="unpaired"] .beats {
  display: flex;
  margin: 26px 0 0;
  padding: 16px 0 0;
  list-style: none;
  /* The website's own --border value. Not a token: :root here is mirrored
   * byte-for-byte against the app by remoteShellTokens.test.js, and the
   * landing state looks the same in both themes anyway. */
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  text-align: left;
}

.beats li {
  flex: 1;
  min-width: 0;
  padding: 0 10px;
  border-left: 1px solid rgba(255, 255, 255, 0.08);
}

.beats li:first-child {
  border-left: 0;
  padding-left: 0;
}

.beats li:last-child { padding-right: 0; }

.beats b {
  display: block;
  margin-bottom: 3px;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--site-text);
}

.beats span {
  display: block;
  font-size: 11px;
  line-height: 1.4;
  color: var(--muted);
}

/* Under ~360px three columns of 11px text stop being readable, so they stack. */
@media (max-width: 380px) {
  #overlay[data-state="unpaired"] .beats {
    flex-direction: column;
    gap: 11px;
  }
  .beats li {
    padding: 0;
    border-left: 0;
  }
  .beats b { margin-bottom: 1px; }
}

/* SD-2224: the way out, on the resting states only. Quiet on purpose — it is
 * an exit for somebody who is in the wrong place, not a call to action for
 * somebody who is in the right one. `p.away` keeps its own gap even while the
 * link inside it is hidden, so a state change does not shift the card. */
.away {
  margin-top: 18px;
  min-height: 1.5em;
}

.away a {
  font-size: 13px;
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  padding-bottom: 1px;
}

.away a:hover,
.away a:focus-visible {
  border-bottom-color: currentColor;
}

.away a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

.away a[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; }
  /* SD-2225: the emission stops but does not vanish — the three rings hold the
   * positions they would sweep through, so the mark still reads as a
   * transmitter instead of losing half its composition. */
  .beacon .ring {
    animation: none;
    transform: scale(1.8);
    opacity: 0.22;
  }
  .beacon .ring:nth-child(2) { transform: scale(2.4); opacity: 0.14; }
  .beacon .ring:nth-child(3) { transform: scale(3.0); opacity: 0.08; }
}

/* ---------------------------------------------------------------------------
 * SD-2237 — "Check this connection"
 *
 * The button on the overlay, and the page behind it. Deliberately quiet: it
 * appears only on the two waiting states (boot.js, CHECKABLE), and on those it
 * is the second thing to read, never the first. The sentence above it is still
 * the answer for most people; this is for the ones it did not help.
 * ------------------------------------------------------------------------ */

.check-link {
  margin-top: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 40px;
  padding: 9px 17px;
  border: 1px solid var(--surface);
  border-radius: 999px;
  color: var(--muted);
  font-size: 13px;
  font-weight: 500;
  text-decoration: none;
  transition: color 0.15s ease, border-color 0.15s ease;
}

/* An author `display` beats the UA [hidden] rule regardless of specificity —
 * the same trap .sd-status-row hit in SD-1302. */
.check-link[hidden] { display: none; }

.check-link svg {
  width: 14px;
  height: 14px;
  flex: none;
  display: block;
}

.check-link:hover,
.check-link:focus-visible {
  color: var(--text);
  border-color: var(--muted);
}

.check-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* ---- the page itself ---- */

.check-body {
  margin: 0;
  min-height: 100dvh;
  background: var(--bg);
  color: var(--text);
}

.check {
  max-width: 34rem;
  margin: 0 auto;
  padding: 34px 20px calc(48px + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.check-head { display: flex; flex-direction: column; gap: 8px; }

.check-head h1 {
  margin: 0;
  font-size: 23px;
  font-weight: 600;
  letter-spacing: -0.022em;
  line-height: 1.2;
}

.check-head p {
  margin: 0;
  font-size: 14px;
  line-height: 1.55;
  color: var(--muted);
}

.check-cta {
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  color: var(--bg);
  background: var(--accent);
  border: none;
  border-radius: 11px;
  padding: 13px 20px;
  min-height: 48px;
  cursor: pointer;
}

.check-cta:disabled { opacity: 0.55; cursor: default; }

.check-cta:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.check-steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.check-steps li {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  color: var(--muted);
}

.check-steps .dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex: none;
  background: var(--surface);
}

.check-steps [data-state="done"] .dot { background: var(--site-online); }
.check-steps [data-state="running"] .dot {
  background: var(--accent);
  animation: check-pulse 1.6s ease-in-out infinite;
}

@keyframes check-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.35; transform: scale(0.7); }
}

@media (prefers-reduced-motion: reduce) {
  .check-steps [data-state="running"] .dot { animation: none; }
}

.check-result {
  border: 1px solid var(--surface);
  border-radius: 13px;
  padding: 17px 18px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.check-result h2 {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.012em;
}

.check-result p {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--muted);
}

/* The tone is on the border and the heading only. A filled card would make the
 * bad outcome shout, and the bad outcome is a fact about the network, not a
 * failure the reader caused. */
.check-result[data-tone="ok"] { border-color: var(--site-online); }
.check-result[data-tone="ok"] h2 { color: var(--site-online); }
.check-result[data-tone="bad"] { border-color: var(--danger); }
.check-result[data-tone="bad"] h2 { color: var(--danger-text); }
.check-result[data-tone="warn"] { border-color: var(--site-accent); }
.check-result[data-tone="warn"] h2 { color: var(--site-accent); }

.check-detail {
  border-top: 1px solid var(--surface);
  padding-top: 13px;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.check-detail .hd {
  font-size: 11px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}

.check-detail-row {
  display: flex;
  justify-content: space-between;
  gap: 14px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11.5px;
  line-height: 1.7;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.check-detail-row span:last-child {
  color: var(--text);
  text-align: right;
  word-break: break-all;
}

.check-back { margin: 6px 0 0; }

.check-back a {
  font-size: 13px;
  color: var(--accent);
  text-decoration: none;
}

.check-back a:hover,
.check-back a:focus-visible { text-decoration: underline; }

/* ---------------------------------------------------------------------------
 * SD-2239 — the landing page is the front door again
 *
 * The bare address stopped showing it once a browser had paired: a stored
 * pairing booted straight into the tunnel, so the one screen that says what
 * this host is was unreachable for exactly the people who use it. Now the
 * address shows the landing page and offers the way through as a button.
 * ------------------------------------------------------------------------ */

.connect-btn {
  /* Block, so the check link below starts on its own line rather than sitting
     next to it — the card centres its children with text-align. */
  display: block;
  margin: 26px auto 0;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--site-bg);
  background: var(--site-accent);
  border: none;
  border-radius: 11px;
  padding: 13px 24px;
  min-height: 48px;
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.2s ease;
  box-shadow: 0 6px 22px rgba(34, 211, 238, 0.22);
}

.connect-btn[hidden] { display: none; }

/* SD-2240: the landing's way back for a browser with no stored pairing —
   shown exactly where the Connect button is not. Website palette, since the
   landing ground is the site's, not the app's. */
.repair-hint {
  margin: 22px auto 0;
  max-width: 320px;
  color: rgba(243, 244, 249, 0.62);
  font-size: 13.5px;
  line-height: 1.55;
  text-align: center;
}
.repair-hint[hidden] { display: none; }
.repair-hint b { color: var(--site-accent); font-weight: 600; white-space: nowrap; }

.connect-btn:hover { transform: translateY(-1px); box-shadow: 0 9px 28px rgba(34, 211, 238, 0.3); }
.connect-btn:active { transform: translateY(0); }

.connect-btn:focus-visible {
  outline: 2px solid var(--site-accent);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .connect-btn { transition: none; }
  .connect-btn:hover { transform: none; }
}

/* On the landing state the ground is the website's, not the app's, so the
 * check link has to borrow that palette too — otherwise it is app-grey on a
 * deep blue and reads as a rendering fault. */
#overlay[data-state="unpaired"] .check-link {
  margin-top: 14px;
  color: rgba(243, 244, 249, 0.58);
  border-color: rgba(243, 244, 249, 0.18);
}

#overlay[data-state="unpaired"] .check-link:hover,
#overlay[data-state="unpaired"] .check-link:focus-visible {
  color: var(--site-text);
  border-color: rgba(243, 244, 249, 0.42);
}

/* ---------------------------------------------------------------------------
 * SD-2239 — the check page wears the same clothes
 *
 * It is reached from the landing page, so it cannot look like a different
 * product. Rather than a second set of rules, the page rebinds the tokens the
 * rules above already use: one place decides the palette, every component
 * follows. Single-theme on purpose, like the landing state it belongs to.
 * ------------------------------------------------------------------------ */

.check-body {
  --bg: var(--site-bg);
  --surface: rgba(243, 244, 249, 0.14);
  --text: var(--site-text);
  --muted: rgba(243, 244, 249, 0.62);
  --accent: var(--site-accent);
  --danger: var(--site-pink);
  --danger-text: #ff8fc9;
  color: var(--site-text);
  background:
    radial-gradient(ellipse at 28% -8%, rgba(124, 58, 237, 0.26), transparent 52%),
    radial-gradient(ellipse at 84% 108%, rgba(255, 45, 177, 0.18), transparent 52%),
    linear-gradient(180deg, var(--site-bg), var(--site-bg-2));
  background-attachment: fixed;
  font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Inter', 'Segoe UI', sans-serif;
}

.check-body .check-cta {
  color: var(--site-bg);
  box-shadow: 0 6px 22px rgba(34, 211, 238, 0.22);
}

.check-body .check-result[data-tone="ok"] { border-color: rgba(111, 222, 133, 0.5); background: rgba(111, 222, 133, 0.08); }
.check-body .check-result[data-tone="bad"] { border-color: rgba(255, 45, 177, 0.5); background: rgba(255, 45, 177, 0.08); }
.check-body .check-result[data-tone="warn"] { border-color: rgba(34, 211, 238, 0.5); background: rgba(34, 211, 238, 0.08); }

/* ---------------------------------------------------------------------------
 * SD-2240 — the landing page on a phone
 *
 * It was laid out at desktop width and never had a narrow breakpoint. On a
 * phone that showed: a 96px mark and a 40px headline eating the upper half,
 * then a button and a link crowding the lower edge with nothing between them,
 * and no notch inset — the card sat under the home indicator on the models
 * that have one.
 *
 * Most of this is spacing rather than styling. The elements were right; the
 * rhythm between them was a desktop rhythm.
 * ------------------------------------------------------------------------ */

@media (max-width: 520px) {
  #overlay {
    /* The notch and the home indicator. Without the insets the card is
       centred in a box that is partly not visible. */
    padding: calc(20px + env(safe-area-inset-top)) calc(20px + env(safe-area-inset-right))
             calc(20px + env(safe-area-inset-bottom)) calc(20px + env(safe-area-inset-left));
  }

  #overlay[data-state="unpaired"] .card {
    /* Was capped at 360px, which on a 390px screen left an 15px gutter and
       made the headline look wedged. */
    max-width: none;
    width: 100%;
    padding: 0 4px;
  }

  .beacon {
    width: 72px;
    height: 72px;
    /* The rings travel outward from the mark, and at desktop spacing the
       outermost one crossed the headline — a signal going out, drawn straight
       through the word it is going out from. The gap is what fixes that, not
       a smaller mark. */
    margin-bottom: 38px;
  }

  .beacon .ring {
    width: 72px;
    height: 72px;
  }

  .beacon .mark {
    width: 72px;
    height: 72px;
  }

  /* Same reason: the rings reach roughly three times the mark on desktop,
     which at 72px still lands in the text. Stopping them earlier keeps the
     emission legible and the headline clean. */
  #overlay[data-state="unpaired"] .beacon .ring { animation-name: shell-emit-tight; }

  #overlay[data-state="unpaired"] h1 {
    /* 7vw hits 27px on a small phone and 40 on a large one — a two-word title
       should not swing that much, and at 33px "Sightdeck Anywhere" fills the
       line edge to edge with nothing left over. */
    font-size: clamp(24px, 6.8vw, 29px);
    margin-bottom: 22px;
    text-wrap: balance;
  }

  .connect-btn {
    /* A full-width primary action is the phone idiom, and it gives the thumb
       the whole line instead of a 220px target in the middle. */
    display: block;
    width: 100%;
    margin-top: 34px;
    padding: 15px 20px;
    font-size: 16px;
    /* The desktop glow reads as a lit panel at this width. Enough to lift it
       off the ground, not enough to become the brightest thing on screen. */
    box-shadow: 0 4px 16px rgba(34, 211, 238, 0.14);
  }

  .connect-btn:hover { box-shadow: 0 4px 16px rgba(34, 211, 238, 0.14); }

  .check-link {
    margin-top: 14px;
    padding: 11px 18px;
  }

  /* The grid behind the mark is a backdrop, and at this size it competed with
     the mark instead of sitting behind it. */
  .beacon-grid { opacity: 0.4; }
}

/* Short and wide: a phone on its side has no room for the mark at all, and
   dropping it is better than scrolling a page that holds four elements. */
@media (max-height: 460px) and (orientation: landscape) {
  #overlay[data-state="unpaired"] .beacon,
  #overlay[data-state="unpaired"] .beacon-grid { display: none; }

  #overlay[data-state="unpaired"] h1 { font-size: 26px; margin-bottom: 12px; }
  .connect-btn { margin-top: 18px; }
}

/* ---------------------------------------------------------------------------
 * SD-2447 — "Save connection link"
 *
 * The durable re-pair net: a small, quiet corner control, never the first
 * thing on the page. It sits above both the overlay (z-index 10) and the
 * connected iframe, so it is reachable through the states boot.js's
 * SAVEABLE lists without needing its own copy in each one. `.away`'s
 * treatment (SD-2224) is the closest sibling — an exit that does not compete
 * with the primary content — this borrows the same quiet register.
 *
 * Top-right, not bottom: the connected page is the Remote UI rendered by
 * localServerPage.js through the tunnel, and this shell has no say over what
 * that page puts where. Its primary actions (the composer's send button, task
 * row taps) sit in-flow near the bottom edge on a phone; a top corner is the
 * quieter place for chrome the shell owns but the app underneath does not
 * know about (Jonas-review, SD-2447).
 * ------------------------------------------------------------------------ */

/* SD-2783: its own row at the top, not a corner. It used to be fixed
 * top-right above everything (z-index 20), which on a phone is exactly where
 * the Remote page keeps its logo and its theme button — so it covered them.
 * Now it is a full-width strip and #overlay starts below it, which means it
 * cannot overlap anything by construction rather than by luck.
 *
 * Only the overlay states reach this now (boot.js, SD-2783): the connected
 * page carries its own offer in the connection card the app renders. So the
 * strip only ever sits above the shell's own centred card, which has neither a
 * logo nor a theme button. */
:root { --shell-notice-h: 0px; }

/* A floor, not the truth: boot.js measures the strip and overwrites this with
 * the real height (it wraps to two lines while the fallback URL is shown).
 * This value only has to be close enough that the first paint does not jump. */
.has-save-link { --shell-notice-h: calc(44px + env(safe-area-inset-top)); }

.save-link {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 20;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 10px;
  padding: 6px max(12px, env(safe-area-inset-right)) 6px max(12px, env(safe-area-inset-left));
  padding-top: calc(6px + env(safe-area-inset-top));
  background: var(--surface);
  border-bottom: 1px solid var(--border, rgba(255, 255, 255, 0.09));
}

/* SD-2783: the strip pushes the card down instead of sitting on it. */
#overlay { top: var(--shell-notice-h); }

.save-link[hidden] { display: none; }

/* SD-2783: in a strip nobody can close, a filled floating button reads as an
 * unfinished task. A text action, at tap size. */
.save-link-btn {
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 4px 0;
  min-height: 32px;
  cursor: pointer;
  transition: color 0.15s ease;
}

.save-link-btn:hover,
.save-link-btn:focus-visible {
  color: var(--text);
  border-color: var(--muted);
}

.save-link-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.save-link-hint,
.save-link-fallback {
  font-size: 13px;
  line-height: 1.5;
}

.save-link-hint {
  margin: 0;
  flex: 1 1 14ch;
  min-width: 0;
  color: var(--muted);
  text-align: left;
  background: var(--surface);
  border-radius: 10px;
  padding: 8px 11px;
}

.save-link-hint[hidden],
.save-link-fallback[hidden] { display: none; }

.save-link-fallback {
  /* Its own line: a URL this long next to a button wraps the strip anyway. */
  flex: 1 1 100%;
  width: 100%;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--muted);
  border-radius: 8px;
  padding: 7px 9px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* The check page, same treatment: it is reached from the landing page and is
   read on the same screen. */
@media (max-width: 520px) {
  .check {
    padding: calc(24px + env(safe-area-inset-top)) 18px
             calc(36px + env(safe-area-inset-bottom));
    gap: 18px;
  }

  .check-head h1 { font-size: 21px; }
}
