/* ══════════════════════════════════════════════════════════════════════════
   site.css: everything the WEBSITE needs that the app does not.

   Loads after assets/kyra-theme.css and assets/overlay.css, which are the
   shipping app's stylesheets synced verbatim. This file must never restate a
   colour, radius, blur or easing curve that those two already define: if the
   overlay's glass changes, the page's glass has to change with it, and the only
   way that stays true is by not having a second copy here. Use the tokens.

   The page is THREE ACTS, and the body carries which one is running:

     body.chaos     a Mac you recognise, coming apart, for seven seconds
     body.sweeping  the glass arrives and takes every banner with it
     body.calm      the interface, working, and nothing under it

   So this file has six jobs:
     1. Undo the few assumptions the app's CSS makes that a browser breaks.
        Scrolling is no longer one of them: the page is one screen now.
     2. Draw the Mac behind the glass, which is the only thing here that is set
        dressing rather than interface.
     3. Draw the noise: the storm, the count, and the button that presses itself.
     4. Draw the sweep, which is the only thing on the page that is neither.
     5. Draw act III: the interface, the questions under the bar, and the gate.
     6. Make all of it work on a phone, which the app never has to.

   NOTHING BELOW DRAWS A RECEIPT. The four marks the sweep lands on (done,
   waiting, couldn't, working) are .mark in the app's own overlay.css, along
   with .mtime and .mtip. The site puts them on the rail; the app says what they
   look like.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 1 · UNDOING THE APP'S ASSUMPTIONS ─────────────────────────────────── */

/* The overlay is exactly one screen and never scrolls, and now neither does the
   page: act I, the sweep and act III are all exactly the height of the window.
   There used to be an `html:has(body.calm)` rule here that unlocked the document
   once the glass had landed, because three thousand words of argument lived
   underneath it. The argument is now asked of the island instead (see #asks in
   index.html), so there is nothing under the fold to reach and the lock is
   unconditional. `overscroll-behavior` stops the rubber band, which on a fixed
   full-bleed interface reads as the interface coming loose. */
html, body.landing {
  height: 100%;
  overflow: hidden;
  overscroll-behavior-y: none;
  background: #0A140C;
  color: var(--label);
}

/* THE CURSOR IS THE SYSTEM'S UNTIL THE LAYER ARRIVES. kyra-theme.css sets a
   global `*{cursor:none !important}` because the overlay draws its own pointer,
   and that is right from `calm` onward. It is wrong in act I: the drawn dot
   arriving WITH the glass is half of what the sweep is saying, and it cannot
   arrive if it was there all along. */
body.chaos *, body.sweeping * { cursor: auto !important; }
body.chaos .cta { cursor: pointer !important; }
body.chaos .cur-dot, body.sweeping .cur-dot { opacity: 0; }
/* The drawn dot hides itself when the pointer leaves the surface, which now
   means leaving the window. demo.js toggles this. */
body.cursor-off .cur-dot { opacity: 0; }

/* The overlay's surfaces are all `position:fixed`, which in the app means "on
   the screen", and here has to keep meaning that while the argument scrolls
   past underneath. So the surface is itself fixed: its fixed descendants are
   then positioned against a box that never moves, and the story slides under an
   interface that stays exactly where it is. Which is the claim the page is
   making, made in the scroll behaviour rather than in a sentence about it. */
#surface {
  position: fixed;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  /* Doing double duty. A transform makes #surface the containing block for its
     fixed descendants (the whole point) AND a backdrop root, which is what
     keeps .veil's backdrop-filter sampling the fake desktop rather than the
     page's own ground. */
  transform: translateZ(0);
}

/* Everything the interface owns is absent until the glass brings it, and the
   RAIL is the only part of it that arrives during the sweep, because the sweep
   is banners flying into the rail and they need somewhere to land. Kyra herself
   waits until the last one has: "And breathe" over a screen still mid-throw is
   the line arriving before the thing it is about. */
body.chaos .margin,
body.chaos .ov-day, body.sweeping .ov-day,
body.chaos .edge-pull, body.sweeping .edge-pull,
body.chaos .greet, body.sweeping .greet,
body.chaos .island, body.sweeping .island,
body.chaos .honest, body.sweeping .honest,
body.chaos .asks, body.sweeping .asks { opacity: 0; pointer-events: none; }
/* THE ONE EXCEPTION, and it is the greeting during the sweep. "And breathe" is
   a caption on the count draining from 23 to 4, so it has to be on screen while
   that is happening, which is squarely inside `sweeping` and therefore inside
   the rule directly above. demo.js sets `breathing` from fire() and drops it at
   calm. Everything else in the interface still waits for the glass. */
body.sweeping.breathing .greet { opacity: 1; pointer-events: none; }
/* Faster in, and no delay: the ladder that staggers act III's entrance would
   have this arriving after the number it is about has stopped moving. */
body.breathing .greet { transition-duration: .45s; transition-delay: 0s; }
/* The brief is written during the sweep but belongs to the calm screen. It is
   hidden rather than absent so the greeting is already its full height when
   placeGreet measures it: added at calm instead, the block would grow by a line
   and a half and jump the whole greeting upward on the frame the glass lands.
   `visibility`, not `opacity`, because .g-brief's own entrance animation is
   `both` and would win an opacity fight with this. */
body.breathing .g-brief { visibility: hidden; }

body.chaos .veil, body.chaos .dots { opacity: 0; }
/* The veil is the light dismiss once Kyra is up; before that it is a
   pane of nothing over the only button on the page. */
body.chaos .veil, body.sweeping .veil { pointer-events: none; }

/* ── 2 · BEHIND THE GLASS ──────────────────────────────────────────────────
   A Mac at 9%, with twenty tabs open.
   BRIGHT, deliberately, and much brighter than it looks like it should be. In
   the app this layer is the user's real screen at full backlight, and the veil
   is tuned against that; a "tasteful" dark wallpaper here gets darkened a
   second time by the same veil and the result is a black rectangle with a
   search bar on it. What the page has to reproduce is not a dim desktop, it is
   a normal one seen through glass. */
.behind {
  position: absolute;
  inset: 0;
  z-index: 1;
  /* The gradient stack under the picture is not decoration, it is the fallback:
     the same three bands the SVG paints, so a frame where the file has not
     arrived yet is still a bright blue desktop rather than a white flash. */
  background:
    url("/kyra/tahoe-wallpaper.svg") center / cover no-repeat,
    linear-gradient(178deg, #0B4F9E 0%, #8FD6E6 40%, #33B9B4 66%, #0C5C6E 100%);
  user-select: none;
}

/* TAHOE'S MENU BAR HAS NO BAR. macOS 26 removed the tinted, blurred strip
   across the top of the screen: the desktop picture runs all the way up and
   only the glyphs are drawn on it. So there is no background and no
   backdrop-filter here any more, and every item carries its own shadow instead,
   which is what the real one does to stay legible over a bright wallpaper.
   Taller, too — Tahoe's bar is a touch deeper than Sequoia's. */
.menubar {
  position: absolute; top: 0; left: 0; right: 0; height: 30px; z-index: 4;
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 15px;
  font-size: 13px; font-weight: 500; color: #fff;
  text-shadow: 0 0 10px rgba(0, 0, 0, .3), 0 1px 2px rgba(0, 0, 0, .28);
}
.mb-l { display: flex; align-items: center; gap: 18px; }
.mb-l b { font-weight: 640; }

/* There is deliberately nothing clickable in the menu bar, and nothing
   clickable anywhere in the fiction. Every real control on this page is inside
   the interface, or is the one button that ends the fiction. */
.mb-apple {
  width: 13px; height: 15px;
  background: #fff;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .3));
  -webkit-mask: radial-gradient(circle at 50% 62%, #000 46%, transparent 47%);
  mask: radial-gradient(circle at 50% 62%, #000 46%, transparent 47%);
}
.mb-r { display: flex; align-items: center; gap: 11px; }
/* The status glyphs. Tahoe draws them as thin monochrome strokes with no
   background of their own, at roughly the cap height of the clock beside them. */
.mb-ico {
  width: 16px; height: 16px; color: #fff; flex-shrink: 0;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .3));
}
.mb-batt {
  width: 25px; height: 12.5px; border-radius: 4px;
  box-shadow: inset 0 0 0 1.2px rgba(255, 255, 255, .62), 0 1px 2px rgba(0, 0, 0, .22);
  background: linear-gradient(90deg, rgba(255, 255, 255, .85) 62%, transparent 62%);
  background-clip: content-box; padding: 2px;
}
/* Nine per cent. Nobody's fault, everybody's problem, and the one number in the
   menu bar that is itself an open loop. */
.mb-batt-low { background: linear-gradient(90deg, var(--amber) 9%, transparent 9%); }
.mb-low { color: var(--amber); font-weight: 700; font-variant-numeric: tabular-nums; }
.mb-clock { font-variant-numeric: tabular-nums; margin-left: 3px; }

/* Off-centre and well short of the edges, so there is real wallpaper on three
   sides and room for the things that landed on top of it. A window that fills
   the frame reads as a page background once it is blurred, and then there is no
   desktop left for Kyra to be sitting on top of. */
/* Rounder than it was, because every window corner in Tahoe is: macOS 26 took
   the window radius up to roughly the corner of the display itself, and a 12px
   window under a Tahoe dock is the one thing in act I that would still read as
   the old OS. */
.mac-window {
  position: absolute; left: 8%; right: 23%; top: 62px; bottom: 148px; z-index: 2;
  border-radius: 17px; overflow: hidden;
  background: #FBFBF8;
  box-shadow: 0 34px 90px rgba(0, 0, 0, .5), 0 0 0 0.5px rgba(0, 0, 0, .3);
  display: flex; flex-direction: column;
}

/* THE TAB STRIP. Twenty of them, flexed into a width that fits five, because a
   tab is the purest open loop there is: a thing left open so it would not be
   forgotten, which is the same as not having been dealt with. They get narrower
   as the count goes up and that is the entire point: nothing here is legible,
   and the illegibility is the message. */
.tabstrip {
  display: flex; align-items: flex-end; gap: 1px; flex-shrink: 0;
  padding: 7px 8px 0; background: #DCDCD8; overflow: hidden;
}
.tl-row { display: flex; gap: 7px; align-items: center; padding: 0 10px 8px 4px; flex-shrink: 0; }
.tabstrip .tl { width: 11px; height: 11px; border-radius: 50%; display: block; }
.tab {
  display: flex; align-items: center; gap: 5px; min-width: 0;
  flex: 1 1 0; max-width: 160px; height: 29px;
  background: #CDCDC8; border-radius: 8px 8px 0 0; padding: 0 8px;
  font-size: 11.5px; color: #4C4C46;
  border-right: 1px solid rgba(0, 0, 0, .05);
}
.tab.on { background: #FBFBF8; color: #23231F; }
.tab .fav { width: 8px; height: 8px; border-radius: 3px; flex-shrink: 0; }
.tab .tt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.tab .tn { font-weight: 700; color: var(--amber); flex-shrink: 0; filter: brightness(.8); }

.chrome {
  height: 38px; display: flex; align-items: center; gap: 8px; padding: 0 14px;
  background: #EDEDE8; border-bottom: 1px solid rgba(0, 0, 0, .09);
  flex-shrink: 0;
}
.chrome .tl { width: 12px; height: 12px; border-radius: 50%; }
.chrome .t-title { margin-left: 14px; font-size: 12.5px; font-weight: 600; color: #6C6C66; }
.chrome-url { height: 36px; gap: 11px; }
.url-nav { color: #9A9A93; font-size: 15px; letter-spacing: 2px; }
.urlbar {
  flex: 1; min-width: 0; height: 25px; border-radius: 7px; background: #E2E2DD;
  display: flex; align-items: center; padding: 0 11px;
  font-size: 12px; color: #71716B;
}
.urlbar span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.urlbar b { color: #4C4C46; }

/* SET LARGER THAN A REAL DOCUMENT. Everything in here is read through
   blur(18px) once the glass lands, and 14px body copy under an 18px blur is not
   small text, it is grey soup: the window stops reading as a document at all.
   At this size the lines survive as lines, still unreadable, which is correct,
   but legibly text-shaped, which is the whole job this layer has. */
.page {
  padding: 34px 44px; color: #23231F; flex: 1; overflow: hidden;
  background: #FBFBF8;
  background-image: radial-gradient(circle, rgba(60, 60, 54, .10) 1.1px, transparent 1.4px);
  background-size: 26px 26px;
}
.page h1 {
  font-family: 'Instrument Serif', 'Iowan Old Style', Georgia, serif;
  font-size: 42px; font-weight: 400; letter-spacing: -.4px; line-height: 1.1;
}
.page .doc-meta { font-size: 18px; color: #9A9A93; margin-top: 10px; margin-bottom: 22px; }
.page p { font-size: 19px; line-height: 1.62; margin-top: 9px; max-width: 34ch; color: #3A3A34; }
/* Every line of the plan, crossed out by the day rather than by you. */
.page s { color: #A6A6A0; text-decoration-color: rgba(216, 146, 107, .75); }
.page .caret::after {
  content: ""; display: inline-block; width: 2px; height: 1.05em;
  background: #4A7DC9; vertical-align: -.18em; margin-left: 2px;
  animation: caret 1.05s steps(1) infinite;
}
@keyframes caret { 50% { opacity: 0 } }

/* ── the windows you did not choose to have open ── */
.win {
  position: absolute; z-index: 3; border-radius: 15px; overflow: hidden;
  background: #FBFBF8;
  box-shadow: 0 18px 55px rgba(0, 0, 0, .38), 0 0 0 0.5px rgba(0, 0, 0, .22);
}
/* Tahoe's title bar is the window, not a strip on top of it: the hard rule
   under the toolbar is gone and the tint is barely there, so the bar reads as
   the top of one continuous surface. */
.win .w-bar {
  height: 32px; display: flex; align-items: center; gap: 7px; padding: 0 12px;
  background: linear-gradient(180deg, #F7F7F3, #F1F1EC);
  border-bottom: 1px solid rgba(0, 0, 0, .045);
  font-size: 11.5px; font-weight: 650; color: #6C6C66;
}
.win .w-bar .tl { width: 10px; height: 10px; border-radius: 50%; background: #D6D6D1; }
.win .w-title { margin-left: 4px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.win .w-count {
  margin-left: auto; border-radius: 999px; padding: 1px 8px;
  font-size: 10.5px; font-weight: 700; color: #FBFBF8; background: #C6564A;
  font-variant-numeric: tabular-nums;
}

.win-mail { right: 2.5vw; bottom: 92px; width: min(345px, 34vw); }
.mailrow {
  position: relative; padding: 8px 13px 8px 18px; font-size: 12px; color: #55554E;
  border-bottom: 1px solid rgba(0, 0, 0, .05);
}
.mailrow::before {
  content: ""; position: absolute; left: 6px; top: 13px;
  width: 6px; height: 6px; border-radius: 50%; background: #4A7DC9;
}
.mailrow b { display: block; color: #23231F; font-size: 12px; padding-right: 34px; }
.mailrow i {
  display: block; font-style: normal; font-weight: 650; color: #3A3A34; margin-top: 1px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mailrow span {
  display: block; color: #8E8E88; margin-top: 1px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mailrow em { position: absolute; right: 11px; top: 8px; font-style: normal; font-size: 10.5px; color: #9A9A93; }

.win-chat { left: 3vw; bottom: 74px; width: min(315px, 31vw); }
.chatline { display: flex; gap: 9px; padding: 8px 13px; font-size: 12.5px; color: #2F2F2A; align-items: flex-start; }
.chatline .av {
  width: 24px; height: 24px; border-radius: 7px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 700; color: #FBFBF8;
}
.chatline b { font-size: 12px; margin-right: 5px; }
.chatline .at { background: rgba(198, 86, 74, .14); color: #A8443A; border-radius: 3px; padding: 0 3px; font-weight: 650; }
.typing { padding: 2px 13px 11px 46px; font-size: 11.5px; color: #9A9A93; font-style: italic; }
.typing i {
  display: inline-block; width: 4px; height: 4px; border-radius: 50%;
  background: #B3B3AD; margin-right: 2px; animation: typ 1.2s infinite;
}
.typing i:nth-child(2) { animation-delay: .15s }
.typing i:nth-child(3) { animation-delay: .3s }
@keyframes typ { 0%, 60%, 100% { transform: none; opacity: .55 } 30% { transform: translateY(-3px); opacity: 1 } }

/* ── the interruptions that will not take silence for an answer ── */
/* An alert in Tahoe is a glass panel: much rounder, lighter, and lit from the
   wallpaper behind it rather than painted a flat near-white. */
.dlg {
  position: absolute; z-index: 8; width: 292px; border-radius: 21px;
  padding: 18px 18px 15px; text-align: center;
  background: rgba(252, 252, 250, .82);
  backdrop-filter: blur(28px) saturate(180%);
  -webkit-backdrop-filter: blur(28px) saturate(180%);
  box-shadow:
    0 24px 80px rgba(0, 0, 0, .45),
    inset 0 0.5px 0 rgba(255, 255, 255, .9),
    0 0 0 0.5px rgba(0, 0, 0, .13);
  opacity: 0; transform: scale(.92);
  transition: opacity .3s, transform .35s var(--spring);
}
.dlg.show { opacity: 1; transform: none; }
.dlg-ico { font-size: 25px; margin-bottom: 6px; }
.dlg b { display: block; font-size: 13.5px; color: #23231F; letter-spacing: -.2px; }
.dlg p { font-size: 12px; color: #6D6D66; margin-top: 4px; line-height: 1.45; }
.dlg-row { display: flex; gap: 8px; margin-top: 13px; }
/* Fully round, which is what every button in Tahoe became. */
.dlg-btn {
  flex: 1; height: 29px; border-radius: 999px;
  background: rgba(255, 255, 255, .72); color: #3A3A34;
  box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, .1), 0 1px 2px rgba(0, 0, 0, .07);
  font-size: 12px; font-weight: 650; display: flex; align-items: center; justify-content: center;
}
.dlg-btn.pri {
  background: linear-gradient(180deg, #4E8DE6, #2F6ED3); color: #FBFBF8;
  box-shadow: inset 0 0.5px 0 rgba(255, 255, 255, .45), 0 2px 6px rgba(47, 110, 211, .35);
}
.dlg-meet { top: 30%; left: 34%; }
.dlg-update { top: 56%; right: 26vw; }

/* ── the dock, in liquid glass ──
   Tahoe's dock is the clearest single tell that this is macOS 26 and not 15:
   the shell went from a frosted slab to a lens. It is nearly fully rounded, it
   is far more transparent than it was, it SATURATES what is behind it rather
   than only blurring it, and it is lit twice — a bright rim on the top inside
   edge and a dark one along the bottom, which is what sells it as a piece of
   glass with thickness instead of a translucent rectangle. */
.dock {
  position: absolute; left: 50%; bottom: 11px; transform: translateX(-50%); z-index: 5;
  display: flex; align-items: flex-end; gap: 9px;
  padding: 7px 11px; border-radius: 27px;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, .26), rgba(255, 255, 255, .09) 45%, rgba(255, 255, 255, .16));
  backdrop-filter: blur(28px) saturate(190%);
  -webkit-backdrop-filter: blur(28px) saturate(190%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .55),
    inset 0 -1px 0 rgba(255, 255, 255, .16),
    inset 0 0 0 0.5px rgba(255, 255, 255, .32),
    0 12px 34px rgba(0, 0, 0, .3);
}

/* THE ICONS ARE ROUNDER THAN BIG SUR'S. Tahoe took the app-icon squircle up to
   about a quarter of its own width — the "concentric" radius that lines the
   icon corner up with the dock's — and gave every one of them the same glassy
   top-light. Both are done here rather than per icon, so a new app in the dock
   only has to bring its own tint and glyph. */
.dock .dk {
  position: relative; width: 46px; height: 46px; border-radius: 11.5px;
  display: flex; align-items: center; justify-content: center;
  box-shadow:
    0 4px 10px rgba(0, 0, 0, .26),
    0 1px 2px rgba(0, 0, 0, .18),
    inset 0 0 0 0.5px rgba(255, 255, 255, .2);
}
/* The specular pass: a bright wash off the top edge and the faintest shading
   into the bottom one. Over the glyph would flatten it, so it sits under. */
.dock .dk::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  background: linear-gradient(
    180deg, rgba(255, 255, 255, .24), rgba(255, 255, 255, .03) 46%, rgba(0, 0, 0, .06));
  pointer-events: none;
}
.dock .dk svg { position: relative; width: 30px; height: 30px; }

/* Finder splits down the MIDDLE, not across: the dark half is the left one and
   the face reads over both, which is the whole recognisability of it. */
.dock .dk-finder { background: linear-gradient(90deg, #3FA4F2 0 50%, #EEF4FA 50% 100%); }
.dock .dk-finder svg { width: 28px; height: 28px; }
.dock .dk-safari { background: linear-gradient(180deg, #4FB4F7, #1C6FD6); }
.dock .dk-safari svg { width: 41px; height: 41px; }
.dock .dk-mail   { background: linear-gradient(180deg, #63C2FF, #1E7AE6); }
.dock .dk-msg    { background: linear-gradient(180deg, #6FE97C, #24BE45); }
.dock .dk-wa     { background: linear-gradient(180deg, #57DE79, #12A63A); }
.dock .dk-notes  { background: linear-gradient(180deg, #FFD968 0 30%, #FEFDF7 30% 100%); }
/* The bin is glass in Tahoe rather than a picture of a bin: it takes the
   wallpaper straight through it and is drawn almost entirely in its rim. */
.dock .dk-trash {
  background: rgba(255, 255, 255, .17);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  box-shadow: inset 0 0 0 0.7px rgba(255, 255, 255, .42), 0 4px 10px rgba(0, 0, 0, .2);
}

/* Calendar says what day it is the way the real one does, which is the only
   icon in the row that carries information rather than a mark. */
.dock .dk-cal {
  background: #FCFCFA; flex-direction: column; gap: 0; overflow: hidden;
  font-family: var(--font);
}
.dock .dk-cal .cal-d {
  position: relative; z-index: 1;
  width: 100%; text-align: center; padding-top: 3px;
  font-style: normal; font-size: 8.5px; font-weight: 700; letter-spacing: .4px;
  color: #E2453A;
}
.dock .dk-cal .cal-n {
  position: relative; z-index: 1; margin-top: -2px;
  font-style: normal; font-size: 22px; font-weight: 350; letter-spacing: -.6px;
  color: #2A2A26;
}

.dock .dk-sep {
  width: 1px; height: 40px; border-radius: 0; align-self: center;
  background: linear-gradient(180deg, transparent, rgba(255, 255, 255, .45), transparent);
  box-shadow: none;
}
.dock .dk-sep::before { content: none; }
/* Badges only ever go up. That is the whole grammar of the thing: a number that
   cannot count down is not information, it is a debt. */
.dock .badge {
  position: absolute; z-index: 2; top: -5px; right: -6px; min-width: 19px; height: 19px;
  border-radius: 999px; padding: 0 5px;
  display: flex; align-items: center; justify-content: center;
  background: linear-gradient(180deg, #FF5F52, #D93B2E);
  color: #FBFBF8; font-size: 10.5px; font-weight: 700;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 2px 6px rgba(0, 0, 0, .35), inset 0 0.5px 0 rgba(255, 255, 255, .5);
}
.dock .badge.bump { animation: bump .3s var(--spring); }
@keyframes bump { 50% { transform: scale(1.3) } }

/* ── 3 · THE NOISE ─────────────────────────────────────────────────────────
   Twenty banners on a script, each one a real category of thing that is
   genuinely happening to somebody's Monday. The stack compresses as it fills:
   newest legible on top, the history crushing under it, which is what a morning
   of these actually feels like by ten. */
/* UNDER THE GLASS, NOT OVER IT. kyra-theme.css puts .veil at z-index 5, and the
   storm has to be below it: the whole of act II is the noise going under a
   layer, and a banner still crisp on top of the glass says the layer did not
   land. It also stops the pile from ever covering the mail window whole:
   buried is the point, erased is a rendering bug. */
.nstack {
  position: absolute; top: 38px; right: 14px; z-index: 3;
  display: flex; flex-direction: column; gap: 9px;
  width: min(340px, calc(100vw - 28px));
  max-height: min(56vh, calc(100% - 330px)); overflow: hidden; pointer-events: none;
  -webkit-mask-image: linear-gradient(180deg, #000 84%, transparent);
  mask-image: linear-gradient(180deg, #000 84%, transparent);
}
/* Tahoe's banners are the same glass as its dock: rounder, thinner, and lit
   along the top edge rather than outlined all the way round. */
.nb {
  display: flex; gap: 11px; align-items: flex-start; padding: 11px 13px;
  border-radius: 19px; background: rgba(252, 252, 250, .82);
  backdrop-filter: blur(30px) saturate(180%); -webkit-backdrop-filter: blur(30px) saturate(180%);
  box-shadow:
    0 10px 34px rgba(0, 0, 0, .3),
    inset 0 0.5px 0 rgba(255, 255, 255, .85),
    0 0 0 0.5px rgba(0, 0, 0, .07);
  transform-origin: top right;
  animation: nbin .5s var(--spring) backwards;
}
@keyframes nbin { from { opacity: 0; transform: translateX(60%) scale(.9) } to { opacity: 1; transform: none } }
.nb .nb-ico {
  width: 30px; height: 30px; border-radius: 7.5px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; font-size: 16px;
  box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, .06);
}
.nb .nb-body { min-width: 0; flex: 1; }
.nb .nb-app { font-size: 9.5px; font-weight: 700; letter-spacing: .6px; color: #8F8F88; }
.nb .nb-t { font-size: 12.5px; font-weight: 700; color: #23231F; letter-spacing: -.15px; margin-top: 1px; }
.nb .nb-x { font-size: 12px; color: #4C4C46; line-height: 1.4; letter-spacing: -.1px; margin-top: 1px; }
.nb.squish { transform: scale(.965) rotate(var(--tilt, 0deg)); opacity: .85; margin-top: -4px; }
.nb.squish2 { transform: scale(.93) rotate(var(--tilt, 0deg)); opacity: .6; margin-top: -30px; }

/* ── the count ──
   The one honest readout on the desktop, and the only thing in act I that
   changes state when the glass lands: it drains instead of disappearing,
   because the loops did not go away, they changed hands. */
.loopchip {
  position: absolute; left: 50%; bottom: 130px; transform: translateX(-50%); z-index: 3;
  text-align: center; opacity: 0; transition: opacity .5s; pointer-events: none;
}
.loopchip.show { opacity: 1; }
.lc-label {
  font-size: 10px; font-weight: 800; letter-spacing: 2.6px; color: #2E3A31;
  text-shadow: 0 1px 0 rgba(255, 255, 255, .35);
}
.lc-n {
  font-size: 44px; font-weight: 800; letter-spacing: -2px; line-height: 1.12; color: #16211A;
  font-variant-numeric: tabular-nums; text-shadow: 0 1px 0 rgba(255, 255, 255, .3);
}
.lc-n.tick { animation: lctick .28s var(--spring); }
@keyframes lctick { 40% { transform: scale(1.12); color: #8C4034 } }
.lc-sub { font-size: 11.5px; color: #34413A; margin-top: 2px; letter-spacing: -.1px; }
/* Once the glass is over it, it is Kyra's readout, so it takes Kyra's ink. */
body.sweeping .loopchip { bottom: auto; top: calc(50% + 122px); z-index: 60; transition: opacity .5s, top .7s var(--spring); }
body.sweeping .lc-label, body.sweeping .lc-sub { color: var(--label-2); text-shadow: none; }
body.sweeping .lc-n { color: var(--label); text-shadow: none; }
body.calm .loopchip, body.calm .nstack { opacity: 0; }

/* ── the button that presses itself ──
   The first piece of Kyra's glass to arrive, so it is built out of the app's
   own tokens and nothing else: --glass-heavy, --hairline, --blue, --spring.
   It is on screen for ten seconds and it is the only thing in act I you can
   touch. */
.cta {
  /* Clear of the dock, which got taller when it became Tahoe's: at 62px the
     button printed across the badges it is offering to take away. */
  position: absolute; left: 50%; bottom: 78px; z-index: 6;
  transform: translateX(-50%) translateY(16px);
  display: flex; align-items: center; gap: 14px;
  padding: 12px 20px 12px 13px; border: none; border-radius: 999px;
  background: var(--glass-heavy);
  backdrop-filter: blur(40px) saturate(180%);
  -webkit-backdrop-filter: blur(40px) saturate(180%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .09),
              inset 0 0 0 0.5px var(--hairline),
              0 22px 70px rgba(0, 0, 0, .55);
  font-family: var(--font); text-align: left;
  opacity: 0; transition: opacity .6s, transform .6s var(--spring);
}
.cta.show { opacity: 1; transform: translateX(-50%); }
.cta:hover {
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12),
              inset 0 0 0 0.5px rgba(255, 255, 255, .16),
              0 26px 80px rgba(0, 0, 0, .6);
}
.cta:active { transform: translateX(-50%) scale(.98); }
.cta:focus-visible { outline: 1.5px solid rgba(255, 255, 255, .5); outline-offset: 3px; }
.cta .cd { position: relative; width: 44px; height: 44px; flex-shrink: 0; }
.cta .cd svg { width: 44px; height: 44px; transform: rotate(-90deg); }
.cta .cd circle { fill: none; stroke-width: 3; }
.cta .cd .cdbg { stroke: var(--fill-2); }
.cta .cd .cdfg { stroke: var(--blue); stroke-linecap: round; transition: stroke-dashoffset 1s linear; }
.cta .cd b {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  color: var(--label); font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums;
}
.cta-txt { display: block; }
.cta-t { display: block; font-size: 16.5px; font-weight: 650; color: var(--label); letter-spacing: -.3px; }
.cta-s { display: block; font-size: 12px; color: var(--label-2); margin-top: 3px; letter-spacing: -.1px; }
.cta-s b { color: var(--label); font-variant-numeric: tabular-nums; }
.cta-s kbd {
  font-family: var(--font); font-size: 10px; font-weight: 700; color: var(--label-2);
  background: rgba(255, 255, 255, .09); border-radius: 4px; padding: 1.5px 5px; letter-spacing: .4px;
}
body.sweeping .cta, body.calm .cta { opacity: 0; pointer-events: none; transform: translateX(-50%) scale(.9); }
@media (pointer: coarse) { .cta-k { display: none } }

/* ── 4 · THE SWEEP ─────────────────────────────────────────────────────────
   Two and a half seconds, and the only part of the page that is neither the
   noise nor the interface. The world holds still, the glass lands, and every
   banner flies to the mark it was always going to become. */
body.frozen .behind *, body.frozen .nb { animation-play-state: paused !important; }

/* Softening the noise once the layer is over it. The BROWSER stays lit: the
   work you actually meant to do is still open, exactly where you left it, and
   dimming that too would say Kyra replaced your day rather than took the noise
   off it. Everything that was shouting goes to a whisper. */
.win, .dock, .dlg, .menubar, .mac-window { transition: opacity 1s ease; }
body.calm .win, body.calm .dock, body.calm .dlg.show { opacity: .14; }
body.calm .menubar { opacity: .4; }
body.calm .mac-window { opacity: .85; }

/* A receipt that has not been thrown at yet. overlay.css pops every .mark on
   arrival; during the sweep the pop has to wait for the banner that causes it,
   so the mark is held until its dot lands and `pending` comes off, which
   restores overlay.css's own animation and runs it, at the moment of impact. */
.mark.pending { opacity: 0; animation: none !important; }

/* The dots that fly. Coloured by the state the banner turns out to have been,
   which is the first time on the page that the four colours mean anything,
   and they are the app's four, from the app's own tokens. */
.flydot {
  position: fixed; z-index: 62; width: 12px; height: 12px; border-radius: 50%;
  pointer-events: none; box-shadow: 0 0 14px 0 currentColor;
}
.flydot.fail { border-radius: 3px; }

/* ── 5 · THE INTERFACE, AND THE QUESTIONS UNDER IT ─────────────────────────
   Act III opens on exactly the overlay: greeting, rail, island, right edge. The
   staggered entrance is the one the old tour's ending used, on the same delays,
   because it was the right entrance and only its trigger changed. */
.margin, .ov-day, .edge-pull, .honest, .greet, .island, .asks {
  transition: opacity .8s ease, transform .8s var(--spring);
}
body.sweeping .margin { opacity: 1; }
body.calm .greet { transition-delay: .1s; }
body.calm .island { transition-delay: .25s; }
body.calm .ov-day { transition-delay: .35s; }
body.calm .edge-pull { transition-delay: .5s; }
body.calm .honest { transition-delay: .8s; }
body.calm .asks { transition-delay: 1s; }

.honest {
  position: absolute; left: 20px; bottom: 18px; z-index: 8;
  display: flex; align-items: center; gap: 10px;
  font-family: var(--font); font-size: 11px; font-weight: 500;
  letter-spacing: .2px; color: var(--label-3);
}
.honest span { pointer-events: none; }
.honest a { color: var(--label-3); text-decoration: none; transition: color .16s; }
.honest a:hover { color: var(--label); }

/* ── the three beats ──
   Each word arrives on its own, out of a blur and slightly from below, which is
   the only place on the page anything is animated for feel rather than to show
   a state change. The blur is doing the work: opacity alone reads as a fade and
   a fade reads as loading. Coming out of focus reads as something settling.

   demo.js owns WHEN (greetSequence); this owns what arriving looks like. */
.gw {
  display: inline-block;
  animation: gwin .95s var(--spring) both;
}
@keyframes gwin {
  from { opacity: 0; transform: translateY(9px); filter: blur(7px); }
  to { opacity: 1; transform: none; filter: blur(0); }
}
/* ── the sizer ──
   .g-hero is a centred flex row, so the mark sits wherever the text beside it
   leaves room. "And breathe" is narrower than "Welcome, traveler", and swapping
   one for the other slid the logo left at the exact moment the page is claiming
   to have settled: the one element that should read as fixed, moving, on the
   frame the glass lands.

   .gw-size holds the longer line's width permanently and is invisible; the live
   text is laid over it. The mark lands where "Welcome, traveler" puts it and
   never moves again. Scoped to .g-word-box so /download, whose greeting is one
   static line and never swaps, keeps its plain centred layout. */
.g-word-box { position: relative; display: inline-block; }
.gw-size { visibility: hidden; }
.g-word-box #greetWord {
  position: absolute; left: 0; top: 0; white-space: nowrap;
  transition: opacity .42s ease, transform .42s ease;
}
.g-word-box #greetWord.out { opacity: 0; transform: translateY(-8px); }

@media (prefers-reduced-motion: reduce) {
  .gw { animation-duration: .3s; }
  @keyframes gwin { from { opacity: 0 } to { opacity: 1 } }
}

/* ── the questions ──
   Where "THE IDEA ↓" used to point at three thousand words of scroll.

   50% + 53px is the bottom of the bar plus a 22px gap: the island is centred
   (`top:50%`, translateY(-50%)) and 62px tall in bar mode, so its lower edge is
   31px below the middle. That is a constant and not a guess — these are only
   ever on screen while the island IS a bar, because setMode hides them the
   moment it is anything else, and every other mode is a fixed height too. The
   greeting above the bar is measured in JS instead, because it has to clear all
   four of those heights rather than just this one. */
.asks {
  position: absolute; left: 50%; top: calc(50% + 53px);
  transform: translateX(-50%); z-index: 8;
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
  width: min(560px, 92vw);
}
body:not(.calm) .asks { opacity: 0; pointer-events: none; }
.asks.gone { opacity: 0; pointer-events: none; transform: translateX(-50%) translateY(6px); }

.ask {
  font-family: var(--font); font-size: 12.5px; font-weight: 500;
  letter-spacing: -.1px; color: var(--label-2);
  padding: 7px 14px; border-radius: 999px;
  background: var(--glass); border: none;
  box-shadow: inset 0 0 0 0.5px var(--hairline);
  -webkit-backdrop-filter: blur(20px); backdrop-filter: blur(20px);
  transition: color .16s, background .16s, transform .16s var(--spring);
}
.ask:hover { color: var(--label); background: var(--glass-heavy); transform: translateY(-1px); }
.ask:active { transform: translateY(0); }

/* ── the gate ──
   Seven seconds in, and it stops everything until it is answered — but it takes
   no for an answer, and once. It is here because the beta is small and an
   address is the only way we can reach anyone who liked what they saw.

   #surface gets `inert` when this opens, which is what actually does the
   blocking. All this does is cover it, hold the caret, and give the pointer
   back (see below). */
.gate {
  position: fixed; inset: 0; z-index: 90;
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  background: rgba(6, 12, 8, .58);
  -webkit-backdrop-filter: blur(18px) saturate(.9);
  backdrop-filter: blur(18px) saturate(.9);
  animation: gatein .5s ease both;
}
.gate[hidden] { display: none; }
@keyframes gatein { from { opacity: 0 } }

/* THE POINTER COMES BACK FOR THE GATE, and it has to be said here in
   `!important` or there is no cursor on this card at all.

   Two things conspire. kyra-theme.css hides the system pointer everywhere with
   `*{cursor:none !important}` because the overlay draws its own dot, and that
   dot is a child of #surface — which the gate covers at z-index 90 and demo.js
   marks `inert`. So the drawn cursor is behind the modal and the real one is
   turned off: the visitor is mousing at a card with nothing to aim. Act I has
   the same rule for the same reason (`body.chaos *`), and this is its twin at
   the other end of the page. */
.gate, .gate * { cursor: auto !important; }
.gate-form input { cursor: text !important; }
.gate-form button, .gate-skip { cursor: pointer !important; }
/* And the drawn dot goes, rather than sitting frozen under the glass where it
   was when the gate opened. */
body.gated .cur-dot { opacity: 0; }

.gate-card {
  width: min(420px, 100%);
  padding: 24px 24px 22px;
  border-radius: 26px;
  background: var(--glass-heavy);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .10),
              inset 0 0 0 0.5px var(--hairline),
              0 40px 110px rgba(0, 0, 0, .55);
  text-align: center;
  animation: gatecard .6s var(--spring) both .06s;
}
@keyframes gatecard { from { opacity: 0; transform: translateY(14px) scale(.97) } }

/* The mark sits ON the line rather than above it. Stacked, a 40px ring over
   three words is a lockup and wants a lockup's whitespace; inline, it is the
   card's one piece of furniture and the card can be the size of its form. */
.gate-t {
  display: flex; align-items: center; justify-content: center; gap: 11px;
  font-family: 'Instrument Serif', 'Iowan Old Style', Georgia, serif;
  font-weight: 400; font-size: 25px; letter-spacing: .2px; color: #fff;
}
.gate-logo { width: 27px; height: 27px; opacity: .9; flex-shrink: 0; }

.gate-form { display: flex; gap: 8px; margin-top: 18px; }
.gate-form input {
  flex: 1; min-width: 0;
  font-family: var(--font); font-size: 14px; color: var(--label);
  padding: 11px 14px; border-radius: 12px; border: none;
  background: rgba(255, 255, 255, .07);
  box-shadow: inset 0 0 0 0.5px var(--hairline);
  user-select: text; -webkit-user-select: text; cursor: text !important;
}
.gate-form input::placeholder { color: var(--label-3); }
.gate-form input:focus { outline: none; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .28); }
.gate-form button {
  font-family: var(--font); font-size: 13.5px; font-weight: 600; color: #0C1B0F;
  padding: 11px 17px; border-radius: 12px; border: none; white-space: nowrap;
  background: #E8F3E9;
  transition: transform .16s var(--spring), opacity .16s;
}
.gate-form button:hover { transform: translateY(-1px); }
.gate-form button:disabled { opacity: .6; transform: none; }

.gate-skip {
  display: block; margin: 14px auto 0; padding: 4px 8px;
  background: none; border: none;
  font-family: var(--font); font-size: 11.5px; font-weight: 500;
  letter-spacing: .1px; color: var(--label-3);
  transition: color .16s;
}
.gate-skip:hover { color: var(--label-2); }


/* ── 6 · PHONES ────────────────────────────────────────────────────────────
   No dock, no chat window, no second dialog: on a 390px screen the storm alone
   is more than enough noise, and three overlapping windows under it is mush
   rather than pressure. */
@media (max-width: 820px) {
  .mb-hide { display: none; }
  /* The status glyphs go with the menus. Tahoe's own bar collapses the same way
     on a narrow screen, and keeping them here pushed the clock off the edge.
     demo.js drops the date from the clock at the same width, for the same
     reason: on a 390px bar the full string is wider than everything else in it
     put together. */
  .menubar { height: 28px; font-size: 12px; padding: 0 11px; }
  .mb-ico { display: none; }
  .mb-l { gap: 12px; }
  .mb-r { gap: 8px; }
  /* Shorter, so there is bare wallpaper under it for the count and the button
     to sit on rather than printing across the document. */
  .mac-window { left: 4%; right: 4%; top: 56px; bottom: 300px; }
  /* THE SATELLITE WINDOWS GO. On a 390px screen three overlapping windows under
     a storm is not pressure, it is mush: you cannot tell there are three. The
     twenty tabs, the crossed-out plan and the storm carry act I on their own,
     and the storm gets a floor so it stops covering both of them. */
  .win, .dock, .dlg-update { display: none; }
  .dlg-meet { top: 30%; left: 50%; transform: translateX(-50%) scale(.92); width: min(292px, 84vw); }
  .dlg-meet.show { transform: translateX(-50%); }
  /* Below the tab strip, not over it: twenty tabs collapsed to slivers is the
     single clearest thing in act I on a phone and the storm was covering it. */
  .nstack { top: 118px; right: 10px; width: min(330px, calc(100vw - 20px)); max-height: min(44vh, 372px); }
  .page { padding: 22px 24px; }
  .page h1 { font-size: 30px; }
  .page .doc-meta { font-size: 14px; margin-bottom: 14px; }
  .page p { font-size: 15px; }
}

@media (max-width: 640px) {
  /* Bottom left, not top left: the storm owns the top of a phone completely,
     and a count printed under it is a count nobody reads. */
  .loopchip { left: 16px; bottom: 196px; transform: none; text-align: left; }
  .lc-label { letter-spacing: 2px; }
  .lc-n { font-size: 32px; letter-spacing: -1px; }
  .lc-sub { max-width: 58vw; }
  body.sweeping .loopchip { left: 50%; transform: translateX(-50%); text-align: center; top: calc(50% + 110px); }
  .cta { gap: 11px; padding: 10px 15px 10px 10px; bottom: 46px; max-width: 94vw; }
  .cta .cd, .cta .cd svg { width: 36px; height: 36px; }
  .cta .cd b { font-size: 13px; }
  .cta-t { font-size: 14px; }
  .cta-s { font-size: 10.5px; margin-top: 2px; }
  .m-evlab, .mtime, .m-nowlab { transition: opacity .4s ease; }
  /* Four chips two-up on a phone rather than four lines of them: the island is
     92vw here and the questions have to fit under it, not beside it. */
  .ask { font-size: 12px; padding: 6px 12px; }
  .gate-card { padding: 22px 20px 20px; }
  .gate-t { font-size: 22px; }
  .gate-logo { width: 24px; height: 24px; }
  /* An address and "Join" will share 350px, so the row survives the phone that
     the download bar's row does not. */
  .gate-form { gap: 7px; }
}

/* Very short windows: the greeting and the island stop competing for the middle
   of a screen that does not have one, and the questions are the first thing to
   go, because the bar they are suggestions for is still there. */
@media (max-height: 620px) {
  .asks { display: none; }
  .loopchip { bottom: 112px; }
}

/* ── 7 · REDUCED MOTION ────────────────────────────────────────────────────
   The argument survives without the theatre. Half the storm, no flying dots,
   and the marks land on the rail directly instead of being thrown at it. */
@media (prefers-reduced-motion: reduce) {
  .flydot { display: none; }
  .nb, .cta, .loopchip { animation-duration: .001s !important; }
  .gate, .gate-card { animation: none; }
  .page .caret::after, .typing i, .dock .badge.bump { animation: none; }
}
