@import url('./tokens.css?v=926');

/* Tell the phone which way each screen leans, so its own chrome — the gesture bar most
   of all — picks the matching side instead of always landing light. Declared per page in
   <meta name="color-scheme"> too; this is the CSS half, self-selecting off the screens
   whose bottom edge is pale. */
:root { color-scheme: dark; }
:root:has(.ob-bottom), :root:has(.rts-screen), :root:has(.recap-screen) { color-scheme: light; }
/* In a browser the page is a white sheet with a frame on it, and the CANVAS — everything
   outside the body's box — has to be that white too. Two things were wrong: nothing set a
   background on html, and color-scheme: dark makes the browser paint its canvas dark until
   a background is known. That is the black the gap flashes on reload, and the cut-off
   colour underneath it afterwards. Stated outright, both stop. */
html:not(.installed) { color-scheme: light; background: #fff; }

/* Smooth cross-document crossfade between screens (Chrome; ignored elsewhere) */
@view-transition { navigation: auto; }

/* 1회진입(pyeongso) ⇆ 올릴때(ollilttae) behaves like a MODAL: the whole 올릴때 page
   — image + blur + profile + black info, captured as one root snapshot — slides up
   from the bottom over the static 1회진입 page, and slides back down to dismiss.
   Everything moves together (no floating profile); types set in pageswap/pagereveal. */
@keyframes modal-up      { from { transform: translateY(100%); } }
@keyframes modal-topdown { from { transform: translateY(-100%); } }
/* Enter: 올릴때 (new) rides up over the static 1회진입 (old). */
html:active-view-transition-type(detail-in)::view-transition-old(root)  { animation: none; }
html:active-view-transition-type(detail-in)::view-transition-new(root)  { animation: modal-up .5s cubic-bezier(.32,.72,0,1) both; }
/* Dismiss (grab the profile frame, drag down): the 평소 (new) frame descends from
   the TOP over the static 올릴때 (old) — reads as pulling 평소 back down into place. */
html:active-view-transition-type(detail-out)::view-transition-old(root) { animation: none; }
html:active-view-transition-type(detail-out)::view-transition-new(root) { animation: modal-topdown .5s cubic-bezier(.32,.72,0,1) both; z-index: 2; }

/* Ordinary navigation gets NO root crossfade. Every page already has its own arrival —
   leaf rows sliding up one after another (home-slide, pack-slide, my-slide, lib-slide…)
   — and fading a snapshot taken at load over the top of it meant the arrival was already
   finished by the time the page was visible. The typed detail-in/out rules above are
   more specific, so the 평소 ⇆ 올릴때 modal keeps its slide. */
::view-transition-old(root), ::view-transition-new(root) { animation: none; }
/* The GROUP is separate from the two snapshots, and its default animation morphs the box
   between the old and new sizes — if two pages ever snapshot at different sizes that
   morph is a visible scale of the whole frame. Nothing here should ever resize the root,
   so the group is pinned; the typed rules above animate the snapshots, not the box. */
::view-transition-group(root) { animation: none; }

/* Feed card -> pack screen. The thumbnail used to be named and morphed into the detail
   photo; that is gone. A card and a full-bleed pack screen are not the same picture in
   the same crop, so growing one into the other never squared up — it always read as a
   stretched copy of the card sitting over the new page. The card says it was chosen (see
   .pack-card.picked in packs.css) and the two pages simply dissolve.

   The old half does NOT fade — the same reason report-in stopped fading its own (see
   below). Matched easings do not save a crossfade: the two snapshots are COMPOSITED, so
   at the midpoint the canvas behind them shows through both. Measured on the clay export,
   the white margin round the frame sank 255 -> 210 -> 255 across 40 frames, which is the
   surround going grey and back for half a second. The old page simply stays put at full
   strength and the new one arrives on top of it, so nothing behind is ever visible.
   The root is the ONLY thing in the transition, so the status bar is not in it and
   cannot move. */
html:active-view-transition-type(card-open)::view-transition-old(root) { animation: none; }
html:active-view-transition-type(card-open)::view-transition-new(root) {
  animation: vt-fade-in .58s cubic-bezier(.4,0,.25,1) both;
  z-index: 2;
}
@keyframes vt-fade-out { to { opacity: 0; } }
@keyframes vt-fade-in { from { opacity: 0; } }

/* Session Recap -> the full report. The recap is a full-bleed red screen and the report
   opens on the same wash, so the two dissolve rather than one sliding over the other —
   a slide put a hard edge between two frames that are nearly the same picture. Same
   easing on both halves, so their opacities sum to 1 and nothing shows through. */
/* .45, not .58: the arriving snapshot is frozen for the whole transition, so every extra
   frame is one the report spends standing still before its own wash rises. */
/* The OLD half does not fade. Two snapshots crossfading are composited, not added: at the
   midpoint both sit at ~50% over the page behind them and the frame washes out — on this
   pair, two red screens, that came out as a white flash that read as a fault. The recap
   stays put at full strength and the report simply arrives on top of it, so the colour
   never leaves and what moves is the report's own wash rising into place. */
html:active-view-transition-type(report-in)::view-transition-old(root) { animation: none; }
html:active-view-transition-type(report-in)::view-transition-new(root) {
  animation: vt-fade-in .45s cubic-bezier(.4,0,.25,1) both;
  z-index: 2;
}

/* Clay: what a link opens in. The page is pure white, the frame drops its shadow, and
   a dark grey stroke takes its place so the viewport reads as a device mockup rather
   than as a page with a card on it. Set before the first paint by the inline script in
   every page head — in a browser it is simply on, and there is nothing to toggle.
   The app itself (html.installed) never gets it: there is no margin to sit in.

   10, not 8. .phone is scaled by `zoom: var(--fit)` to fit whatever room the window
   has, and zoom scales the outline with it — at the 0.88 a 390-wide phone browser
   gives, an 8 was drawing 7.07 on screen. 10 keeps it at or above 8 where this is
   actually looked at. */
html.clay .phone {
  box-shadow: none;
  outline: 10px solid #3B3B3B;
  outline-offset: 0;
}
/* The grey it sits on is set with the centring box above (html:not(.installed)),
   which is the honest condition for "this is a page in a browser". */


/* A browser — any browser, a laptop or a phone — shows the frame as a frame: 360x780 on
   a white page, centred, scaled by --fit to whatever room there is. Installed, the app
   owns the screen and the rules under html.installed take over. */
/* html only. A definite height on the body would pin it to the layout box, and the frame
   has to be centred in what can be SEEN — the two differ by the address bar. min-height
   below does that, and leaves the body free to grow if anything ever overflows, which is
   what makes the page scrollable instead of cutting the rest off. */
html { height: 100%; }

/* ── In a browser, the page IS the centring box ───────────────────────────────
   The frame used to be centred by a body that had to be tall enough first —
   min-height off 100dvh, or off a --vph measured in JS. That works until a host
   disagrees about its own height: an in-app browser with its own header, a phone
   whose address bar is mid-collapse, a window that reports one height and paints
   another. Then the frame is centred in a box that is not the one being looked at.

   So the body is simply pinned to the visual viewport and centres its one child in
   it. Nothing to measure, nothing to agree about, and no dependence on the document
   having a height at all. It cannot scroll, and it never needs to: --fit is capped
   by h/844, so the frame is always shorter than the window that sized it.

   The ground goes on html AS WELL, because that is what paints past the edges while
   a phone's address bar is moving. */
html:not(.installed) { background: #DEDEDE; }
html:not(.installed) body {
  position: fixed;
  inset: 0;
  margin: 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #DEDEDE;
}
body {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 0;
  background: #fff;
  /* The SMALLER of innerHeight and visualViewport.height, measured in JS (openext.js) and
     handed over as --vph. Whichever is smaller is the box the frame has to fit inside —
     taking the larger sized it against room that was not there, and the frame overflowed a
     screen that cannot scroll, which is a navbar nobody can reach.
     And the page CAN scroll now. overflow: hidden was belt for a frame that always fits;
     if one ever does not — a short window, a text size we did not test — the rest has to
     be reachable rather than simply gone. */
  min-height: 100dvh;
  min-height: var(--vph, 100dvh);
}
.phone { zoom: var(--fit, 1); }

/* Kill the mobile tap flash / focus box so tapping a card or button never draws a
   blue rectangle (breaks immersion on Galaxy/Android). Text stays unselectable on
   interactive chrome so a double-tap doesn't highlight it either. */
* { -webkit-tap-highlight-color: transparent; }
a, button, [role="button"], .cell, .card-sel, .toggle-btn, .diff-seg,
.chip-sel, .mode-card, .lib-card, .pack-card, .seg, .stat-tab, .chip {
  outline: none;
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-user-select: none;
}
:focus, :focus-visible { outline: none; }

/* The 360x780 mobile viewport every screen renders inside (fixed size) */
.phone {
  position: relative;
  width: 360px;
  height: 780px;
  border-radius: 40px;
  overflow: hidden;
  /* Small enough to fade out inside the air the frame is given. 20/60 was drawn for a
     desktop page with room all round it; on a phone the frame nearly fills the screen, so
     that shadow ran off the bottom edge and stopped dead — a grey band under the frame
     that reads as the background being cut. 32px of air each side now (the boot script's
     424/844) and a shadow spent within about 28. */
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
}
/* NO background here. Black under the whole frame drew a dark hairline along the rounded
   top edge on every screen: the frame's corner is antialiased and the screen clipped
   inside it is antialiased on the SAME curve, and two partial coverages never sum back to
   one — the gap that leaves is the black underneath showing through. Measured on the
   export, a 1px stroke following the top-left and top-right arcs.
   The black is only ever structural at the foot, where the content frame's rounded bottom
   meets the flat-topped navbar and the concave junction reveals it. So it lives there and
   nowhere else. No radius on the band: .phone's own overflow clips it to the frame's
   corners, which keeps it right on a phone too, where that radius is 0.
   Anchored top AND bottom rather than given a height. A fixed 160 was measured against
   one state and Home has another: with the notification strip up the content frame is
   634 tall, so its rounded bottom corners sit at 594..634 while the band only started at
   620 — and the 26px the band missed showed the page's own grey through the concave
   junction, which is the white ground that read as a fault. 60 is below the 40px top arc,
   so the seam this rule exists to avoid stays avoided, and everything between there and
   the frame's foot is behind an opaque screen whatever height it takes. */
.phone:has(> .tabbar)::before {
  content: "";
  position: absolute;
  left: 0; right: 0;
  top: 60px; bottom: 0;
  background: var(--color-black);
  pointer-events: none;
}

/* Content scrolls inside the fixed viewport; taller (expanded) screens scroll */
.screen {
  position: relative;
  width: 360px;
  height: 100%;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* A screen that reaches its end must not hand the scroll on to the page behind it —
     that chaining is what let the desktop document slide up and clip the frame. */
  overscroll-behavior: contain;
  /* No paint containment. It was put here to force the rounded clip to hold, and it did
     that by clipping EVERYTHING to this box — including the hero carousel's own glow,
     which spills past the scroller by design and came back cut off. The junction is drawn
     over the frame now (see .tabbar's fillets), so nothing needs the clip to hold. */
}
.screen { border-radius: 40px 40px 0 0; }
.screen::-webkit-scrollbar { display: none; }
.screen { scrollbar-width: none; }

/* ── Frame system (common) ────────────────────────────────────────────────
   Any page that has a bottom navbar renders as a 360×700, r40 content frame
   sitting on the black phone background, with the 80px navbar filling the rest.
   The content's rounded bottom corners reveal the black bg beneath, so the
   flat-topped navbar reads with an inverted (concave) radius where the two meet
   — one shared rule, auto-applied to every page with a .tabbar. */
.phone:has(> .tabbar) > .screen {
  height: calc(100% - 80px);
  overflow: hidden auto;
}

/* Clips are decoration, never a player: no controls, and no iOS start-playback button
   on the frames where autoplay is refused (Low Power Mode). assets/clips.js asks for
   play() when the card is actually met; if that is refused too, the poster stays. */
video::-webkit-media-controls,
video::-webkit-media-controls-enclosure,
video::-webkit-media-controls-panel,
video::-webkit-media-controls-overlay-play-button,
video::-webkit-media-controls-start-playback-button,
video::-internal-media-controls-overlay-cast-button {
  display: none !important;
  -webkit-appearance: none;
  opacity: 0 !important;
  pointer-events: none !important;
}

/* Status bar — rendered from the reference Figma "Status Bar" SVG (light + dark) */
.status-bar {
  position: sticky;
  top: 0;
  z-index: 30;
  width: 360px;
  height: 44px;
  border-radius: 40px 40px 0 0;
}
.status-bar .statusbar-svg { display: block; width: 360px; height: 44px; }

/* App bar (title + actions) */
.app-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px;
}
.app-bar .logo { width: 140px; height: auto; display: block; }
.app-bar .actions { display: flex; align-items: center; gap: 16px; }
.app-bar .actions img, .app-bar .actions a img { width: 24px; height: 24px; display: block; }
.app-bar a { text-decoration: none; }

/* Transparent app bar over a hero image (back + kebab) */
.app-bar-transparent {
  position: absolute;
  top: 44px;
  left: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 16px;
  z-index: 20;
  box-sizing: border-box;
}
.icon-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border: none;
  background: none;
  padding: 0;
  cursor: pointer;
}
.icon-btn img { width: 24px; height: 24px; display: block; }
.handle-btn {
  width: 41.5px;
  height: 27.5px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  padding: 0;
  cursor: pointer;
  background: none;
}
.handle-btn img { display: block; width: 4px; height: 20px; }

/* Bottom tab bar — pinned to the very bottom of the 360x780 viewport,
   overlaying the scrollable content, with rounded top corners */
.tabbar {
  position: absolute;
  left: 0;
  bottom: 0;
  z-index: 30;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  height: 80px;                 /* Figma nav bar height */
  box-sizing: border-box;
  width: 360px;
  background: var(--color-black);
  border-radius: 0;              /* flat top; the phone frame rounds the bottom to 40 */
}
/* ── The concave junction ──────────────────────────────────────────────────────
   Drawn ON TOP of the frame, not cut out of it — a rounded clip on a scrolling,
   video-bearing layer is the one thing Chrome keeps dropping here, and what leaked
   through when it did was the glitching. Two masked fillets instead, the pair
   detail.css already uses for the pack sheet: a 40px square at each corner of the
   frame's foot with a quarter circle taken out of it.
   ONE element carrying both corners, positioned by --foot, so the junction is
   wherever the frame's foot currently is. That matters while the report strip opens:
   the foot travels 66px over a second, and a pair pinned to the navbar (or to the
   strip) is drawing a curve against nothing for the whole of that — which is the
   junction breaking as the strip arrives. home.html's tween writes --foot on the
   same frames it writes the height, so the curve travels with the edge.
   The fill follows the bar it belongs to: the onboarding bar is the same grey as its
   screen, so its pair simply does not read. */
.phone { --bar-fill: var(--color-black); --foot: 80px; }
.phone:has(> .tabbar.ob-bottom) { --bar-fill: var(--color-neutral-100); }
.phone.notif-on { --foot: 146px; }
.phone:has(> .tabbar)::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  bottom: var(--foot);
  height: 40px;
  z-index: 20;                 /* over the frame, under the navbar's own 30 */
  pointer-events: none;
  background:
    radial-gradient(40px at 40px 0, #0000 99%, var(--bar-fill) 100%) top left / 40px 40px no-repeat,
    radial-gradient(40px at 0 0,  #0000 99%, var(--bar-fill) 100%) top right / 40px 40px no-repeat;
}

.tabbar .tabs {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 20px 0;
}
.tabbar .tabs a {
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  padding: 0;
  border-radius: 80px;
  text-decoration: none;
  box-sizing: border-box;
}
/* Inactive tab icons render gray (#A3A3A3); active renders white.
   brightness(0)+invert normalizes every icon's baked fill to one color. */
.tabbar .tabs a img { width: 22px; height: 22px; display: block; filter: brightness(0) invert(0.64); }
.tabbar .tabs a.active img { filter: brightness(0) invert(1); }
.tabbar .tabs a .dot {
  width: 7px;
  height: 2px;
  border-radius: 41px;
  background: var(--color-white);
}
.tabbar .home-indicator {
  display: flex;
  align-items: center;
  justify-content: center;
  /* No side padding. 134px each side left a 92px content box, and the bar — a flex item
     with the default flex-shrink — gave up 52 of its 144px to fit it. */
  padding: 10px 0;
}
.tabbar .home-indicator span {
  flex: none;
  width: 144px;
  height: 4px;
  border-radius: 2px;
  background: rgba(255,255,255,0.6);
  mix-blend-mode: plus-lighter;
}
/* On a light bar that handle is white on near-white and simply is not there. The design
   gives it Neutral/400 on those screens (node 2749:599), and plus-lighter has nothing to
   add to a pale ground, so it comes off too. */
.tabbar.ob-bottom .home-indicator span {
  background: var(--color-neutral-400);
  mix-blend-mode: normal;
}

/* Buttons */
.btn-pill {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 100%;
  padding: 16px 20px;
  border-radius: 999px;
  border: none;
  cursor: pointer;
  font-family: var(--font-supreme-bold);
  font-size: 18px;
  letter-spacing: -0.54px;
  color: var(--color-white);
  background: var(--color-red);
  text-decoration: none;
  box-sizing: border-box;
}
.btn-pill img { width: 20px; height: 20px; }

a { color: inherit; }
button { font-family: inherit; }

/* Mobile: the viewport meta (width=360) makes phones scale the 360-wide canvas
   to fill the screen, so here we just drop the desktop framing (padding, rounded
   bezel, shadow) and let the phone go edge-to-edge. */


/* Desktop: the frame is a device sitting still, not a page. The document does not scroll
   at all — an 860px-tall layout in a shorter window used to slide up and down and cut the
   bottom off whatever screen was being looked at (or recorded) — and the phone shrinks to
   fit instead of being clipped. --fit is worked out ONCE, in the boot script in every
   page head, and openext.js only re-runs that same function on resize — there is not a
   second formula anywhere. Above 1 on a tablet, where there is room to spare. */


/* Installed (standalone): the OS DOES draw its own status bar, and viewport-fit=cover puts
   the page under it — so the back/kebab row and the top of every scrolling screen were
   sitting behind the clock. Ours stays hidden (the OS one is real); the frame just takes
   the inset as padding and every fixed height loses it too, so nothing runs under the
   tab bar instead. env() is 0 in a browser tab, and this is scoped to standalone anyway,
   so a tab is untouched.
   NOT VERIFIED on a device from here — display-mode cannot be emulated in a desktop
   Chrome window, and env() reports 0. The arithmetic is the same as the block below. */



/* ── The way back, on every other screen ───────────────────────────────────────
   A dot where a phone puts its camera. Top centre is the one strip free across the
   whole prototype — every app bar puts its controls in the corners — with a single
   exception: the report's centred pack pill starts at y16, so the dot sits at y8..16
   and the target is 26 tall rather than 44 to stay off it.
   Red on the grey screens, but half the prototype opens on a red wash or a photograph
   where a red dot would vanish, so it carries a soft white ring: whichever of the two
   the background swallows, the other one still reads. Measured — see below.
   Inside .phone, not fixed to the window: on desktop the frame is a 360x780 box
   sitting in the middle of the page, and `fixed` would pin this to the browser. */
.flow-exit {
  position: absolute;
  z-index: 900;
  top: var(--sat, env(safe-area-inset-top, 0px));
  /* Centred, and BELOW the system bar — see the installed rule further down. Sitting in
     the strip itself was the reason it stopped responding: the app draws behind Android's
     status bar, but that bar's own window still takes the touches in its height, so a tap
     up there reaches SystemUI and never the page. Below it the app bar's middle is empty
     on every screen (back and title left, actions right), which is where this goes. */
  left: 50%;
  /* A pill, not a dot. 8px of target is a miss on a phone even when it is in the clear —
     this is 56x22 of it, in the shape the status bar uses for a live activity, and the
     whole pill is the hit area. */
  width: 56px; height: 22px;
  margin-left: -28px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px;
  background: var(--color-red);
  /* A red pill on a red screen is not a pill. The hairline separates it from the report
     and the recap; on the pale screens it disappears into them and the fill does the work.
     The soft shadow is for the ones that are a photograph. */
  box-shadow: 0 0 0 1.5px rgba(255, 255, 255, 0.55), 0 2px 8px rgba(0, 0, 0, 0.18);
  /* Hidden until pulled down from the top of the frame (assets/flows.js). It comes down
     out of the strip rather than fading in place, so the gesture and the thing it reveals
     are one movement. */
  opacity: 0;
  transform: translateY(-14px);
  pointer-events: none;
  transition: opacity .22s ease, transform .28s cubic-bezier(.2,.8,.25,1), filter .18s ease;
  -webkit-tap-highlight-color: transparent;
}
.phone.exit-on .flow-exit { opacity: 1; transform: none; pointer-events: auto; }
.flow-exit i {
  display: block;
  width: 9px; height: 9px;
  border-radius: 2px;
  background: rgba(5, 10, 10, 0.86);
}
.phone.exit-on .flow-exit:active { transform: scale(0.94); filter: brightness(0.88); }
/* In a browser the 44px status bar is drawn, so the dot lives inside it — the same place
   a phone's camera sits, and clear of the clock and the battery. */
/* In a browser the 44px status bar is drawn by the mockup and nothing else wants those
   touches, so the pill sits inside it, centred. */
html:not(.installed) .flow-exit { top: 11px; }

/* ── "Back to the start" notice ────────────────────────────────────────────────
   Shown by assets/idle.js after ten still seconds, three seconds before it goes.
   Inside .phone for the same reason the exit dot is: on desktop the frame is a
   360x780 box in the middle of the page and `fixed` would pin this to the browser.
   Nothing new in it — the card is the app's: white, 20px radius, 16/13px Supreme,
   over the scrim the pack sheet already uses. */
/* display:flex beats the UA's [hidden] { display: none }, so a card that is meant to be
   waiting was simply on screen. Said outright, where every user of this shell can rely on
   it. */
.idle-note[hidden] { display: none; }
.idle-note {
  position: absolute;
  inset: 0;
  z-index: 950;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 32px;
  background: rgba(5, 10, 10, 0.42);
  animation: idle-in .22s ease both;
}
/* "Tap anywhere to stay" — and the tap should also do whatever it was aimed at. The
   warning covered the frame and took the hit, so somebody who paused long enough to read
   a screen (which is the whole reason it appears) had their next tap swallowed and had to
   press twice. idle.js listens on the window in the capture phase, so the countdown is
   still cancelled with the sheet out of the hit path — the tap lands on the button
   underneath and the warning goes at the same time.
   Only the countdown: setup-complete and schedule borrow this same shell for cards that
   have buttons of their own, and those have to stay pressable. */
.idle-note.idle-away, .idle-note.idle-away .idle-card { pointer-events: none; }
@keyframes idle-in { from { opacity: 0; } }
.idle-card {
  box-sizing: border-box;
  width: 100%;
  padding: 20px;
  border-radius: 20px;
  background: #fff;
  text-align: center;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
  animation: idle-card-in .28s cubic-bezier(.2,.85,.3,1) both;
}
@keyframes idle-card-in { from { opacity: 0; transform: translateY(8px) scale(.97); } }
.idle-t { margin: 0; font-family: var(--font-supreme-bold); font-size: 16px; line-height: 1.5; letter-spacing: -0.5px; color: var(--color-black); }
.idle-s { margin: 4px 0 0; font-family: var(--font-supreme); font-size: 13px; line-height: 1.4; letter-spacing: -0.5px; color: var(--color-neutral-500); }
.idle-s b { font-family: var(--font-supreme-bold); color: var(--color-red); }
.idle-h { margin: 12px 0 0; font-family: var(--font-supreme); font-size: 12px; line-height: 1.2; letter-spacing: -0.5px; color: var(--color-neutral-400); }

/* The same full-bleed layout, asked for a second time and keyed on what you are holding
   rather than on a number. A phone whose browser reports a layout wider than 400 — a
   larger display size, a browser that does not honour width=360 — was falling through to
   the desktop mockup: 40px of page above and below a rounded card. That is the line
   between the status bar and the app, and the pale strip under the navbar. Anything you
   touch, and that is small enough to be a phone, gets the frame edge to edge.
   Repeated rather than folded into one query because the two conditions have to be OR'd
   and CSS has no way to name a media condition; last in the file, so it settles any tie. */


/* Installed and asked for the whole screen. Measured on a Galaxy: in standalone the app
   is handed the band BETWEEN the system bars — 32.8pt of status bar above it and 25.4pt of
   gesture bar below, of a 780pt screen — and Android paints both of those itself. That is
   the hairline under the clock and the white strip under the navbar, and no CSS reaches
   either of them: viewport-fit=cover only tells the page where the insets are, it does not
   make Chrome hand them over.
   So the manifest asks for fullscreen and the app draws its own status bar — the 9:41 the
   design has always had, which is what these frames were drawn with. Nothing is left for
   the system to paint. iOS ignores the display mode and stays standalone; there the block
   below does the same job with the safe-area insets, which iOS does hand over. */

/* Belt and braces on the sizing, for both installed modes: a percentage of a window that
   has no browser UI in it is exact, where 100dvh on this Galaxy came out 58pt short of the
   screen. Last in the file so it settles the tie with the viewport-unit rules above. */

/* ── Installed ─────────────────────────────────────────────────────────────────
   The app owns the screen: the frame fills it, corner to corner, and draws the
   design's own status bar in the space the system gives back. Keyed on a class the
   page sets before its first paint, not on a media query — a phone reporting a
   width it was never asked for is exactly how this went wrong before.
   Percentages, not viewport units: a percentage of a window with no browser UI in
   it is exact, where 100dvh measured 58pt short of a Galaxy's screen. */
/* 100%, NOT the measured viewport. Read off an S23: installed, visualViewport /
   innerHeight / 100dvh all agree on 722 — but the page's own box is taller than that, and
   the frame sized to 722 sat 58pt short of it with the navbar riding up and black under
   it. Those three describe what is VISIBLE; html's 100% is the box the app was actually
   given, and when the app owns the screen that box is the answer. --vph stays for the
   browser, where the frame is centred in what can be seen and the two differ by the
   address bar. */
/* lvh — the viewport with every retractable bar retracted — with 100% as the fallback.
   On a phone whose gesture hint is switched off, the strip Android would otherwise keep is
   simply screen, and lvh is the unit that says so; 100% only ever resolves against the box
   the page was handed, which on that phone is short by exactly that strip. That shortfall
   is the white under the navbar. */
html.installed, html.installed body { height: 100%; height: 100lvh; background: var(--color-black); overscroll-behavior: none; }
html.installed body { padding: 0; align-items: stretch; justify-content: flex-start; }
/* Installed, the frame IS the window and has no corners of its own — so neither do
   the two layers that carry them in a browser. */
html.installed .screen, html.installed .status-bar { border-radius: 0; }
html.installed .phone {
  width: 100%;
  height: 100%;
  height: 100lvh;
  border-radius: 0;
  box-shadow: none;
  zoom: 1;
}
html.installed .recap-screen,
html.installed .records-screen,
html.installed .swipe-detail,
html.installed .creator-screen,
html.installed body .creator-screen,
html.installed body .recap-screen { height: 100%; min-height: 0; border-radius: 0; }
html.installed body .phone:has(> .tabbar) > .screen {
  height: calc(100% - 56px - max(24px, var(--sab, env(safe-area-inset-bottom, 0px))));
}
html.installed body .phone.notif-on:has(> .tabbar) > .screen {
  height: calc(100% - 56px - 66px - max(24px, var(--sab, env(safe-area-inset-bottom, 0px))));
}
/* The navbar sits at the foot of the FRAME, not of the window — the frame is the window
   here, and absolute keeps it in the same coordinate space as everything it meets. */
html.installed .tabbar { position: absolute; left: 0; right: 0; bottom: 0; width: auto; }
/* 56 of icons and 24 of gesture strip — the design's own, and the only one on screen now
   that Android has taken its hint away. env() is 0 in fullscreen, so max() simply keeps
   the 24; where a system inset does survive (iOS) it takes over and the pill sits above
   it rather than under it. */
html.installed body .tabbar {
  box-sizing: border-box;
  height: calc(56px + max(24px, var(--sab, env(safe-area-inset-bottom, 0px))));
  padding-bottom: 0;
}
/* The junction rides on the navbar's height, which grows into the bottom inset here. */
html.installed .phone { --foot: calc(56px + max(24px, var(--sab, env(safe-area-inset-bottom, 0px)))); }
html.installed .phone.notif-on { --foot: calc(56px + 66px + max(24px, var(--sab, env(safe-area-inset-bottom, 0px)))); }
/* Installed, the phone draws its own gesture handle — so ours is a second one drawn under
   it, and it goes. The 24pt it sat in stays: that is the space the real handle needs, and
   with the pill gone the navbar's own black simply fills it down to the foot of the app.
   Where the system reports that strip (the wrapper, or an Android that hands it over) the
   inset takes over from the 24 and the icons clear the real handle by exactly its height.
   In a browser the drawn pill stays — there is no real one there and it is part of the
   frame the mockup is drawing. */
html.installed .tabbar .home-indicator { display: none; }

html.installed .side-trigger { display: none; }
/* Installed, the strip belongs to the phone — and to SystemUI's touch region with it. The
   pill clears it by 5px, so it is the first thing in the page that a thumb can actually
   reach, and it lands in the app bar's empty middle. */
html.installed .flow-exit { top: calc(var(--sat, env(safe-area-inset-top, 0px)) + 5px); }

/* Standalone, and the phone's own status bar is the one at the top.
   Fullscreen was tried and measured: on a Galaxy with a camera cutout Android LETTERBOXES
   a fullscreen web app below it — 30pt of pure black across the top, and pure black is not
   a colour this app paints, so that band is outside the page and no CSS reaches it. In
   standalone there is no letterbox, because the system's own bar occupies that strip. So
   the strip is the system's and ours is not drawn: two status bars is what an iPhone and a
   Galaxy each showed when both were.
   The bottom is the other way round. The gesture hint is switched off by hand, so the app
   gets that space and the navbar's own drawn pill is the only one there — which is the
   arrangement that worked on the S25.
   The top inset as padding on every installed device: it is 0 wherever the system keeps
   that strip to itself, so it costs nothing there, and it is exactly right where the page
   does draw under the clock. */
html.installed .status-bar { display: none; }
html.installed .app-bar-transparent { top: 0; }
/* --sat/--sab are the shell's own measurement of the system bars. A WebView's env() only
   reports the display cutout, so inside the app that padding came out 0 and the app bar sat
   under the clock. Everywhere else --sat is unset and env() answers as it always did. */
html.installed .phone { box-sizing: border-box; padding-top: var(--sat, env(safe-area-inset-top, 0px)); }

/* ── The tick ─────────────────────────────────────────────────────────────────
   The designer's own mark (체크박스.svg), inserted by the page's own script at the moment
   of selection so the keyframes simply run on arrival and there is no resting state to
   clear between taps. Each family says only where it sits. */
.ob-check { position: absolute; z-index: 3; width: 52px; height: 52px; pointer-events: none;
  overflow: visible; filter: drop-shadow(0 0 12px rgba(0,0,0,0.2)); }
.ob-check-disc { fill: #fff; transform-origin: 159.69px 159.69px; animation: ob-disc .22s cubic-bezier(.2,.8,.3,1) both; }
.ob-check-tick {
  fill: none; stroke: #FA3030; stroke-width: 64; stroke-linecap: butt; stroke-linejoin: round;
  /* The centreline runs 137 units; 140 clears it, so the brush is fully hidden at 0%.
     A butt cap keeps the growing edge square — a round one would reveal ahead of the pen. */
  stroke-dasharray: 140; stroke-dashoffset: 140;
  animation: ob-tick .34s cubic-bezier(.6,0,.35,1) .12s forwards;
}
@keyframes ob-disc { from { transform: scale(.4); opacity: 0; } }
@keyframes ob-tick { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
  .ob-check-disc, .ob-check-tick { animation-duration: 1ms; animation-delay: 0s; }
}

/* ── The language switch must not resize the page ────────────────────────────
   Korean says the same thing in fewer line boxes: "Check Your Report" takes two
   lines on a hub card, 리포트 확인하기 takes one. The card relaid itself under the
   finger on the toggle, and the version dots at the foot walked up the screen.

   Only the hub. The toggle is the ONLY place in the app where both languages are
   seen one after the other — every other screen is read in whichever language was
   chosen, so a Korean card being a line shorter than the English one is a thing
   nobody is in a position to notice. Reserving the line box everywhere DID hold
   those screens still, and it also gave the English — which is the design as
   drawn — a blank line it never had, on fifteen cards and rows. Steadying a
   comparison no one makes is not worth redrawing the original to do it.

   `lh` is the element's own line box, so this follows whatever type the hub sets. */
.fl-t, .fl-sub { min-height: 2lh; }
