/* Homepage lab: shared pieces for the three variants (deck, index, chapters).
   Loads after /home-alt/spectrum.css, which owns the nav, hero, filmstrip, buttons, chips, figures, demos, and footer. */
:root { --gx: clamp(1rem, 4vw, 3rem); }
.lab h4 { margin: 0; }   /* the card titles are h4s; the shared sheet only resets h1 to h3 */
body.lab { background: var(--ink); }

/* ---------- Copy column: the same anatomy in every unit ---------- */
.sec-copy { display: grid; gap: 1rem; align-content: center; min-width: 0; }
.sec-copy > * { min-width: 0; }
.sec-fig { width: clamp(6rem, 10vw, 9rem); aspect-ratio: 1; margin-bottom: 0.5rem; }
.sec-kicker {
  display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;
  font-family: var(--font-display); font-weight: 600; font-size: 0.75rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: rgba(255,255,255,0.7);
}
.sec-kicker b { color: var(--accent); font-variant-numeric: tabular-nums; font-weight: 700; }
.sec-name {
  font-family: var(--font-display); font-weight: 800;
  font-size: clamp(2.6rem, 5.6vw, 5.25rem); line-height: 0.92; letter-spacing: -0.035em;
  color: var(--accent); text-wrap: balance; overflow-wrap: anywhere;
}
.sec-name a, .shelf-name a { color: inherit; }
.shelf-name a { display: inline-block; padding: calc((44px - 1em) / 2) 0; margin: calc((1em - 44px) / 2) 0; }   /* a 44px tall target without moving the name (line-height is 1) */
.sec-name a:hover, .shelf-name a:hover { text-decoration: underline; text-decoration-thickness: 0.06em; text-underline-offset: 0.08em; }
.sec-name .amp { font-weight: 300; color: rgba(255,255,255,0.55); }
@media (min-width: 40rem) { .sec-name .nw { white-space: nowrap; } }
.sec-desc { font-size: clamp(1.1rem, 1.45vw, 1.3rem); line-height: 1.5; color: rgba(255,255,255,0.88); max-width: 32rem; }
.sec-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 1rem;
  font-family: var(--font-display); font-size: 0.875rem; font-weight: 600; color: rgba(255,255,255,0.62);
}
.sec-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 0.25rem; }

/* Compact rungs for the B-Roller family: three lines, no boxes */
.rungs { list-style: none; margin: 0.25rem 0 0; padding: 0; border-top: 1px solid rgba(255,255,255,0.14); max-width: 32rem; }
.rungs li { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 0.9rem; padding: 0.6rem 0; border-bottom: 1px solid rgba(255,255,255,0.14); }
.rungs li > div { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.4rem; }
/* The name takes its own line at EVERY width, not only narrow ones. Sharing a line with the chips meant the rung
   with the longest name wrapped and the other two did not, so the flagship was the ragged one: 76px against 67px
   at 1440. Three rows that hold the same things should be the same height whatever they are called. */
.rungs li > div > b { flex-basis: 100%; }
.rungs img { width: 40px; height: 40px; border-radius: 10px; }
.rungs b { font-family: var(--font-display); font-weight: 700; font-size: 1.05rem; }
.rungs .chip { font-size: 0.8rem; padding: 0.15rem 0.5rem; margin-left: 0.25rem; vertical-align: middle; }
.rung-get { font-family: var(--font-display); font-weight: 700; font-size: 0.9rem; color: var(--accent); white-space: nowrap; padding: 0.6rem 0; min-height: 44px; display: inline-flex; align-items: center; border-bottom: 2px solid transparent; }
.rung-get:hover { border-bottom-color: var(--accent); }

/* ---------- Objects: the demos without their window chrome ---------- */
.obj { display: grid; place-items: center; min-width: 0; width: 100%; }
.obj .demo {
  width: min(100%, 40rem); min-width: 0; background: none; border: 0; box-shadow: none; padding: 0;
  backdrop-filter: none; -webkit-backdrop-filter: none;
  display: grid; gap: 0.9rem; align-content: start;
}
.obj .demo-tag, .obj .demo-bar { display: none; }
.obj-cap { font-family: var(--font-body); font-weight: 400; font-size: 0.95rem; color: rgba(255,255,255,0.66); padding-top: 0.15rem; }

/* soundbites */
.obj .demo-transcript { font-size: clamp(1.05rem, 1.35vw, 1.3rem); line-height: 1.6; }
.obj .demo-clips { margin-top: 0; min-width: 0; }
.obj .clip { min-width: 0; }
.obj .clip > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }   /* the row gives up label width before it runs past the folder */
.obj .clip { font-size: 0.95rem; padding: 0.7rem 0.9rem; background: rgba(255,255,255,0.09); }
.obj .clip i { width: 7rem; height: 22px; }

/* cull grid (the seventh cell only shows where the grid bleeds off the sheet) */
.cull-row .k13 { background: linear-gradient(135deg,#3b6a7a,#1a2f3a); }
.cull-row .k14 { background: linear-gradient(135deg,#5a4a7a,#241f36); }
.cull-row .is-more { display: none; }
.sheet-broller .obj .cull-row .is-more { display: block; }
.obj .cull-row { gap: 10px; }
.obj .cull-row + .cull-row { margin-top: 10px; }
.obj .cull-row i { border-radius: 10px; box-shadow: 0 10px 24px -14px rgba(0,0,0,0.8); }
.obj .cull-row i::after { width: 20px; height: 20px; font-size: 12px; line-height: 20px; right: 6px; bottom: 6px; }
.obj .cull-scrub { margin-top: 0.25rem; height: 8px; }
.obj .cull-foot { margin-top: 0; }
.obj .cull-foot .keycaps kbd { width: 40px; height: 40px; font-size: 1rem; }
.obj .cull-status { font-size: 1rem; }
.obj .cull-status .count { font-size: 1.3rem; }

/* browser tab (B-Roller Lite) */
.browser { display: flex; align-items: center; gap: 0.75rem; padding: 0.55rem 0.8rem; border-radius: 10px 10px 0 0; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.1); border-bottom: 0; }
.browser .dots { display: inline-flex; gap: 5px; }
.browser .dots i { width: 10px; height: 10px; border-radius: 50%; background: rgba(255,255,255,0.28); }
.browser .url { font-family: var(--font-display); font-size: 0.8rem; color: rgba(255,255,255,0.6); background: rgba(0,0,0,0.25); border-radius: 6px; padding: 0.25rem 0.6rem; flex: 1; }
.obj .demo-lite { gap: 0; }
.obj .demo-lite .cull-row { padding: 0 0.8rem; background: rgba(255,255,255,0.04); border-left: 1px solid rgba(255,255,255,0.1); border-right: 1px solid rgba(255,255,255,0.1); }
.obj .demo-lite .cull-row:first-of-type { padding-top: 0.8rem; }
.obj .demo-lite .cull-row + .cull-row { margin-top: 0; padding-top: 10px; }
.lite-foot { padding: 0.8rem; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.1); border-top: 0; border-radius: 0 0 10px 10px; font-size: 0.95rem; color: rgba(255,255,255,0.7); }
.lite-foot b { font-family: var(--font-display); color: var(--accent); }
.obj .demo-lite .obj-cap { margin-top: 0.9rem; }

/* slide + description */
.obj .demo-desc .desc-frame { border-radius: 14px; box-shadow: 0 30px 60px -30px rgba(0,0,0,0.8); }
.obj .slide { padding: clamp(0.8rem, 1.8vw, 1.4rem) clamp(1rem, 2vw, 1.6rem); }
.obj .slide b { font-size: clamp(1.1rem, 1.7vw, 1.6rem); margin-bottom: 0.5rem; }
.obj .slide ul { font-size: clamp(0.85rem, 1.1vw, 1.05rem); line-height: 1.4; }
.obj .cap { font-size: clamp(0.9rem, 1.1vw, 1.05rem); padding: 0.55rem 0.7rem; border-radius: 8px; }
.obj .desc-track { height: 26px; margin-top: 0; }
.obj .desc-legend { font-size: 0.85rem; margin-top: 0; }

/* photo + description (PhotoDesc) */
.photo-frame { position: relative; border-radius: 14px; overflow: hidden; aspect-ratio: 4 / 3; box-shadow: 0 30px 60px -30px rgba(0,0,0,0.8); }
.photo-frame .photo { position: absolute; inset: 0; background:
  radial-gradient(60% 40% at 70% 20%, rgba(255,214,160,0.55), transparent 70%),
  linear-gradient(180deg, #4b3d52 0%, #8a4a3c 45%, #2a1e22 100%); }
.photo-frame .photo::before { content: ""; position: absolute; left: 10%; right: 10%; bottom: 0; height: 58%; background: linear-gradient(180deg, #7a3f34, #5a2c26); border-radius: 6px 6px 0 0; }
.photo-frame .photo::after { content: ""; position: absolute; left: 42%; bottom: 22%; width: 26%; height: 6%; border-radius: 999px; background: #ffd08a; box-shadow: 0 0 30px 12px rgba(255,208,138,0.35); }
.photo-frame .desc-caption { position: absolute; left: 0.6rem; right: 0.6rem; bottom: 0.6rem; }

/* transcript lines (Ruff Cutter) */
.obj .lines { gap: 0.5rem; font-size: clamp(0.95rem, 1.2vw, 1.1rem); }
.obj .lines li { padding: 0.55rem 0.9rem; border-radius: 10px; }
.obj .lines li b { font-size: 0.95rem; }
.obj .timeline { height: 34px; margin-top: 0; padding: 5px; }

/* phone (Bubble Reel): the phone is its own object */
.obj .demo-chat { width: min(100%, 21rem); padding: 0.75rem; border-radius: 42px; background: #15171f; box-shadow: 0 40px 80px -30px rgba(0,0,0,0.85), 0 0 0 1px rgba(255,255,255,0.12); gap: 0; }
.obj .phone { height: 30rem; border-radius: 32px; padding: 1.1rem 1rem 1.2rem; }
.obj .phone-top { font-size: 1rem; }
.obj .msg { font-size: 1.05rem; padding: 0.6rem 0.9rem; }
.obj .demo-chat .obj-cap { display: none; }   /* the phone fills this folder: a caption under it leaves 3px above the floor at 1366x768 and 1440x800 */

/* waveforms (Song Reviewer) */
.demo-songs { gap: 0.6rem; }
.obj .song { grid-template-columns: 7rem 1fr 26px; font-size: 1rem; gap: 0.9rem; }
.obj .song i { height: 32px; }
.obj .song em { width: 26px; height: 26px; font-size: 14px; }
.obj .song.is-now b { color: #fff; }
.obj .song.is-now i { position: relative; color: #fff; opacity: 0.85; overflow: hidden; }
.obj .song.is-now i::after { content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 2px; background: var(--accent); box-shadow: 0 0 12px var(--accent); }
@media (prefers-reduced-motion: no-preference) {
  .demo-songs .song.is-now i::after { animation: playhead 6s linear infinite; }
  @keyframes playhead { from { left: 0; } to { left: 100%; } }
}

/* photos + beats (Kinora) */
.demo-kinora .photos { display: flex; gap: 12px; }
.demo-kinora .photos i { flex: 1; aspect-ratio: 4 / 3; border-radius: 6px; border: 5px solid #f6f3ec; box-shadow: 0 16px 30px -12px rgba(0,0,0,0.85); }
.demo-kinora .photos i:nth-child(1) { background: linear-gradient(160deg, #6b8cae, #2f4b6e); transform: rotate(-3deg); }
.demo-kinora .photos i:nth-child(2) { background: linear-gradient(160deg, #d9a066, #7a4a2b); transform: rotate(2deg); }
.demo-kinora .photos i:nth-child(3) { background: linear-gradient(160deg, #7fb59a, #2f5d4a); transform: rotate(-1deg); }
.demo-kinora .photos i:nth-child(4) { background: linear-gradient(160deg, #c97b8d, #5a2f45); transform: rotate(3deg); }
.demo-kinora .beats { display: flex; gap: 6px; align-items: flex-end; height: 64px; margin-top: 0.5rem; justify-content: space-between; }
.demo-kinora .beats i { flex: 1; border-radius: 4px 4px 2px 2px; background: var(--accent); height: 30%; transform-origin: bottom; }
.demo-kinora .beats i:nth-child(3n) { height: 100%; } .demo-kinora .beats i:nth-child(3n+1) { height: 55%; } .demo-kinora .beats i:nth-child(4n) { height: 75%; }
@media (prefers-reduced-motion: no-preference) {
  .demo-kinora .beats i { animation: beat 1.8s ease-in-out infinite; }
  .demo-kinora .beats i:nth-child(2n) { animation-delay: 0.3s; } .demo-kinora .beats i:nth-child(3n) { animation-delay: 0.6s; } .demo-kinora .beats i:nth-child(5n) { animation-delay: 0.9s; }
}

/* lower third (Lower Key) */
.demo-cg .cg-frame { aspect-ratio: 16 / 9; border-radius: 14px; }
.demo-cg .cg-bug { font-size: 0.8rem; padding: 0.35rem 0.55rem; }
.demo-cg .cg-third { padding: 0.7rem 1rem; border-left-width: 5px; }
.demo-cg .cg-third b { font-size: 1.3rem; }
.demo-cg .cg-third span { font-size: 0.95rem; }
@media (prefers-reduced-motion: no-preference) {
  .demo-cg .cg-third { animation: thirdin 8s var(--ease) infinite; }
}

/* ---------- Colophon: the quiet ending, on paper ---------- */
.colophon { position: relative; z-index: 20; background: var(--paper); color: var(--navy); padding: clamp(4rem, 10vh, 7rem) var(--gx); }
.colophon-inner { max-width: 96rem; margin: 0 auto; display: grid; gap: 2.25rem; }
.colophon-lead { font-family: var(--font-display); font-weight: 700; font-size: clamp(1.5rem, 2.8vw, 2.5rem); line-height: 1.15; letter-spacing: -0.02em; max-width: 30ch; text-wrap: balance; }
.colophon-links { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem 2rem; font-family: var(--font-display); font-weight: 600; font-size: 1.05rem; }
.colophon-links a { color: var(--link); text-decoration: underline; text-decoration-color: rgba(196,71,31,0.35); text-underline-offset: 5px; padding: 0.5rem 0; display: inline-block; }
.colophon-links a:hover { text-decoration-color: var(--link); }
.colophon a:focus-visible { outline-color: var(--navy); box-shadow: none; }
.colophon-next { display: flex; align-items: center; gap: 0.9rem; color: #4a5a6e; font-size: 1rem; max-width: 60ch; padding-top: 1.5rem; border-top: 1px solid rgba(30,45,64,0.14); }
.colophon-next img { width: 40px; height: 40px; border-radius: 10px; filter: saturate(0.7); flex: 0 0 auto; }
.colophon-next b { color: var(--navy); font-family: var(--font-display); }
.lab .footer { position: relative; z-index: 20; background: var(--ink); }
.lab .footer .brand { white-space: nowrap; }
body.lab-index { background: var(--page); }

/* ---------- Playable demos (demos.js): attract loop until touched, then live ---------- */
.demo.is-live *, .demo.is-live *::before, .demo.is-live *::after { animation: none !important; }
.demo.is-live .typing span { animation: dot 1.2s ease-in-out infinite !important; }
/* the keyboard path stays out of sight until it has focus, so the drag is what a mouse user discovers */
/* This button is only for the keyboard, so it hides until focus lands on it. It was hiding by accident: the
   rule below sets padding, a min-height and border-box sizing, which outrank a bare width and height, so the
   'hidden' button measured 34x44 with 133px of scroll width and only the deprecated `clip` was keeping it out
   of sight. The recipe now cancels everything that can give a hidden box a size. */
.demo-try:not(:focus-visible) { position: absolute; width: 1px; height: 1px; min-width: 0; min-height: 0; padding: 0; border: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
.demo-try {
  justify-self: start; margin-top: 0.25rem;
  font-family: var(--font-display); font-weight: 700; font-size: 0.875rem;
  padding: 0.55rem 1rem; min-height: 44px; border-radius: 999px;
  border: 1px solid rgba(255,255,255,0.35); background: rgba(255,255,255,0.06); color: #fff; cursor: pointer;
}
.demo-try:hover { background: rgba(255,255,255,0.14); }
/* soundbites */
.demo-transcript { cursor: text; }
.demo-bites.is-playable .demo-transcript, .demo-transcript.is-live { user-select: none; -webkit-user-select: none; touch-action: pan-y; }
.demo-transcript .w, .demo-transcript .g { padding: 0.05em 0; transition: background-color 0.15s, color 0.15s; box-decoration-break: clone; -webkit-box-decoration-break: clone; }
.demo-transcript .pre { background: color-mix(in srgb, var(--accent) 45%, transparent); color: #fff; }
.demo-transcript .sel { background: var(--accent); color: var(--bg); font-weight: 600; }
.demo-transcript .w.rs { padding-left: 0.14em; border-radius: 4px 0 0 4px; }
.demo-transcript .w.re { padding-right: 0.14em; border-radius: 0 4px 4px 0; }
.demo-transcript .w.rs.re { border-radius: 4px; }
.demo-bites .clip { transition: opacity 0.3s, transform 0.3s var(--ease); }
.demo-bites .clip.is-new { opacity: 0; transform: translateY(8px); }
.demo-bites .clip-x { margin-left: auto; flex: 0 0 44px; width: 44px; height: 44px; margin-top: -6px; margin-bottom: -6px; border-radius: 50%; border: 0; background: rgba(255,255,255,0.1); color: #fff; font-size: 1.1rem; line-height: 1; cursor: pointer; }
.demo-bites .clip-x:hover { background: rgba(255,255,255,0.22); }
.demo-bites .clip-q { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-style: normal; font-size: 0.92em; opacity: 0.72; }   /* the phrase, so rows can be told apart */
.lab .demo-chat .receipt { font-size: 0.85rem; }
.lab .cg-bug { font-size: 0.75rem; }   /* nothing on the page reads below 12px */
.demo-bites.is-live .demo-clips:empty::before { content: "No soundbites yet."; font-size: 0.9rem; color: rgba(255,255,255,0.5); }
/* slide */
.demo-desc .slide [contenteditable] { cursor: text; border-radius: 3px; outline: none; transition: background-color 0.15s; display: block; padding: 3px 0; margin: -3px 0; }   /* every editable line clears 24px, paid back by the margin so the slide does not grow */
.demo-desc .slide [contenteditable]:hover { background: rgba(30,45,64,0.06); }
@media (hover: none) {   /* touch: each slide line is its own 44px tap target, so the slide grows and the frame stacks slide over caption instead of overlaying them */
  .demo-desc .slide [contenteditable] { display: block; padding: 0.75rem 0; min-height: 44px; box-sizing: border-box; }
  .obj .demo-desc .desc-frame { aspect-ratio: auto; }   /* outranks the phone block's 4:3 */
  .obj .slide { position: relative; left: auto; right: auto; top: auto; height: auto; margin: 7% 8% 0; }
  .obj .desc-caption { position: relative; left: auto; right: auto; bottom: auto; margin: 0.8rem 0.6rem 0.55rem; }
}
.demo-desc .slide [contenteditable]:focus { background: rgba(30,45,64,0.08); outline: 2px solid var(--navy); outline-offset: 2px; box-shadow: none; }
.demo-desc.is-live .cap-sp { visibility: hidden; }
.demo-desc.is-live .cap-ad { opacity: 1; }
.demo-desc.is-live .ad1 { flex: 4; }
.demo-desc.is-live .dg2 { flex: 3; }
/* phone */
.compose { display: flex; gap: 0.5rem; align-items: center; margin-top: 0.75rem; flex: 0 0 auto; }
/* THE FIELD HAS TO SHOW WHAT YOU TYPED. On a 390px phone the input came out 84px wide: the placeholder rendered
   as "Type a r" and typing a sentence left only its tail visible, under a sticker inviting you to type a reply.
   The send button keeps its 44px and the field takes everything else, down to a floor it can actually read. */
.compose input { min-width: 7.5rem; }
@media (max-width: 30rem) { .compose { gap: 0.35rem; } .compose input { padding-inline: 0.6rem; } }
.compose input {
  flex: 1; min-width: 0; font: inherit; font-size: 0.95rem; color: #fff;
  background: #1d2028; border: 1px solid rgba(255,255,255,0.14); border-radius: 999px; padding: 0.55rem 0.9rem;
}
.compose input::placeholder { color: rgba(255,255,255,0.58); }
.compose input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.compose button { width: 44px; height: 44px; flex: 0 0 auto; border-radius: 50%; border: 0; background: var(--accent); color: var(--bg); font-family: var(--font-display); font-weight: 800; font-size: 1.1rem; cursor: pointer; }
/* The browser picks an anchor element and keeps it still when things resize around it. The demos animate their
   own insides constantly, so it kept choosing a bubble or a beat bar and nudging the page to hold it in place,
   which is what made scrolling on a phone twitch and land a few pixels off. Nothing inside a demo is an anchor. */
.demo, .mini, .obj .phone { overflow-anchor: none; }
.demo-chat .msgs .is-clipped { display: none; }   /* a bubble the box cannot hold: hidden, not thrown away */
.demo-chat .msgs { flex: 1 1 auto; min-height: 0; justify-content: flex-end; overflow: hidden; mask-image: linear-gradient(to bottom, transparent, #000 3rem); -webkit-mask-image: linear-gradient(to bottom, transparent, #000 3rem); }   /* the fade has to outlast a bubble (about 41px) or the one the phone cuts off shows as a hard grey sliver under the contact name */
.demo-chat .msg { transition: opacity 0.3s, transform 0.3s var(--ease); }
/* spectrum.css names the bubble keyframes with (0,2,0) selectors that lose to its own (0,4,0) :not() rule, so the loop never ran; these win */
@media (prefers-reduced-motion: no-preference) {
  .lab .demo-chat .msg.b3 { animation-name: b3; } .lab .demo-chat .msg.b4 { animation-name: b4; } .lab .demo-chat .msg.b5 { animation-name: b5; } .lab .demo-chat .msg.b6 { animation-name: b6; }
}
.demo-chat.is-live .msg, .demo-chat.is-live .receipt { opacity: 1; transform: none; height: auto; }
.demo-chat.is-live .msg.typing { padding: 0.7rem 0.8rem; margin-top: 0; }
.demo-chat .msg.is-new { opacity: 0; transform: translateY(8px); }
/* transcript lines (each one is a button) */
.demo-cut .lines li { cursor: pointer; transition: background-color 0.2s, color 0.2s, border-color 0.2s; }
.demo-cut .lines li { position: relative; }
.demo-cut .lines li button { all: unset; display: block; width: 100%; cursor: pointer; color: inherit; font: inherit; }
.demo-cut .lines li button::after { content: ""; position: absolute; inset: -2px 0; }   /* the whole padded row takes the click, not just the text line */
.demo-cut .lines li button:focus-visible { outline: 3px solid #fff; outline-offset: 3px; border-radius: 6px; }
.demo-cut .lines li:hover { background: rgba(255,255,255,0.06); }
.demo-cut .timeline i { transition: width 0.4s var(--ease); }
.demo-cut.is-live .timeline i { opacity: 1; }
/* takes */
.mini-songs .song, .demo-songs .song { cursor: pointer; border-radius: 6px; transition: background-color 0.2s; min-height: 44px; }
.mini-songs .song:hover, .demo-songs .song:hover { background: rgba(255,255,255,0.06); }
.mini-songs .song:focus-visible, .demo-songs .song:focus-visible { outline-offset: 2px; }

/* the "Try it" badge on every playable demo */
.demo.is-playable { position: relative; }
.demo-badge {
  --sticker: #ffd53d; --sticker-ink: #1a1400;
  position: absolute; top: -1.4rem; right: 0.25rem; z-index: 3;
  display: inline-flex; align-items: center; gap: 0.45rem;
  padding: 0.6rem 0.9rem 0.6rem 0.75rem; border-radius: 6px;
  background: var(--sticker); color: var(--sticker-ink);
  font-family: var(--font-display); font-weight: 800; font-size: 0.9rem; line-height: 1; letter-spacing: -0.01em;
  box-shadow: 0 14px 30px -12px rgba(0,0,0,0.85);
  transform: rotate(-3deg);
  pointer-events: none;
  transition: opacity 0.5s, transform 0.5s var(--ease);
}
.demo-badge::after {   /* the tail, pointing at the demo */
  content: ""; position: absolute; left: 1rem; bottom: -7px; width: 14px; height: 14px;
  background: var(--sticker); transform: rotate(45deg); border-radius: 2px;
}
.demo-badge svg { width: 18px; height: 18px; }
.demo-badge.is-done { opacity: 0; transform: rotate(-3deg) translateY(-8px); }
@media (prefers-reduced-motion: no-preference) {
  .demo-badge { animation: badge-bob 2.4s ease-in-out infinite; }
  @keyframes badge-bob { 0%, 100% { transform: rotate(-3deg) translateY(0); } 50% { transform: rotate(-3deg) translateY(-6px); } }
  /* in the shelf the room above a sticker is a card's padding, so the bob is shorter there rather than the sticker
     being pushed down into the demo it points at */
  .mini-kinora .demo-badge, .mini-songs .demo-badge { animation-name: badge-bob-small; }
  @keyframes badge-bob-small { 0%, 100% { transform: rotate(-3deg) translateY(0); } 50% { transform: rotate(-3deg) translateY(-3px); } }
  .demo-badge.is-done { animation: none; }
}
.demo-bites .demo-badge { top: -2.9rem; right: 0; }
.demo-cut .demo-badge { top: -2.4rem; right: 1rem; }   /* clear above the first line, never on its words */
.demo-cut:not(.is-live) .lines li.pick { background: color-mix(in srgb, var(--accent) 8%, transparent); color: rgba(255,255,255,0.86); border-left-color: color-mix(in srgb, var(--accent) 50%, transparent); }
   /* the loop reads as a preview through its row tint and rule; the timecode stays full accent, which is the contrast-safe part */
.demo-chat .demo-badge { top: -1.4rem; right: 1rem; }
.mini-songs .demo-badge { top: -1.6rem; right: 0.5rem; }
.demo-desc .demo-badge { top: -1.4rem; right: 0.25rem; }
/* hover says "touch me" too */
.demo.is-playable:not(.is-live):hover .demo-transcript, .demo-cut.is-playable:not(.is-live):hover .lines { filter: brightness(1.08); }

/* a third soundbite in the attract loop */
@media (prefers-reduced-motion: no-preference) {
  .demo-bites .m3 { animation: fill3 9s linear infinite; }
  .demo-bites .c3 { animation: pop3 9s var(--ease) infinite; }
  /* THE FILL SWEEPS, THE INK JUMPS. Tweening both together left the word mid-grey on pink for about a second of
     every nine, measured at 1.8:1, on the demo whose subject is highlighting words. The colour now turns over in
     a fiftieth of the loop, as the fill covers the word. */
  @keyframes fill3 { 0%, 68% { background-size: 0 100%; color: #fff; } 79% { color: #fff; } 81%, 100% { background-size: 100% 100%; color: var(--bg); } }
  @keyframes pop3 { 0%, 84% { opacity: 0; transform: translateY(8px); } 89%, 97% { opacity: 1; transform: none; } 100% { opacity: 0; } }
  /* in the deck the rows that have not popped yet take no room, so the caption sits under the visible ones instead of 150px below */
  .lab .demo-bites .clip.c1 { animation-name: popc1; } .lab .demo-bites .clip.c2 { animation-name: popc2; } .lab .demo-bites .clip.c3 { animation-name: popc3; }
  @keyframes popc1 { 0%, 24% { opacity: 0; transform: translateY(8px); max-height: 0; padding-top: 0; padding-bottom: 0; margin-top: -0.5rem; overflow: hidden; } 30%, 96% { opacity: 1; transform: none; max-height: 4rem; } 100% { opacity: 0; max-height: 4rem; } }
  @keyframes popc2 { 0%, 60% { opacity: 0; transform: translateY(8px); max-height: 0; padding-top: 0; padding-bottom: 0; margin-top: -0.5rem; overflow: hidden; } 66%, 96% { opacity: 1; transform: none; max-height: 4rem; } 100% { opacity: 0; max-height: 4rem; } }
  @keyframes popc3 { 0%, 84% { opacity: 0; transform: translateY(8px); max-height: 0; padding-top: 0; padding-bottom: 0; margin-top: -0.5rem; overflow: hidden; } 89%, 97% { opacity: 1; transform: none; max-height: 4rem; } 100% { opacity: 0; max-height: 4rem; } }
  /* On a phone the folders lie in normal flow, so a row that collapses to nothing and grows back moves the whole
     page under the reader: the B-Roller folder breathed 105px every cycle, which is what made scrolling twitch and
     land somewhere other than where you left it. There the rows keep their space and only fade. */
  @media (max-width: 55.99rem) {
    .lab .demo-bites .clip.c1 { animation-name: popc1p; } .lab .demo-bites .clip.c2 { animation-name: popc2p; } .lab .demo-bites .clip.c3 { animation-name: popc3p; }
    @keyframes popc1p { 0%, 24% { opacity: 0; transform: translateY(8px); } 30%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }
    @keyframes popc2p { 0%, 60% { opacity: 0; transform: translateY(8px); } 66%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }
    @keyframes popc3p { 0%, 84% { opacity: 0; transform: translateY(8px); } 89%, 97% { opacity: 1; transform: none; } 100% { opacity: 0; } }
  }
  .lab .demo-chat .msg.b5 { interpolate-size: allow-keywords; }
  /* the lower third stays on screen for most of its loop, so the monitor is never a long empty gradient */
  .lab .mini-cg .cg-third { animation-name: thirdin2; }
  @keyframes thirdin2 { 0%, 4% { opacity: 0; transform: translateX(-14px); } 10%, 92% { opacity: 1; transform: none; } 98%, 100% { opacity: 0; transform: translateX(-14px); } }
  /* the timecode turns amber with its row, not six seconds early */
  .lab .demo-cut .l2 { animation: none; }   /* the first line stays picked, so the loop never opens empty */
  .lab .demo-cut .timeline .t1 { animation-name: grow-stay; }
  @keyframes grow-stay { 0%, 100% { width: 34%; } }
  .lab .demo-cut .l2 b { animation: none; }
  .lab .demo-cut .l2.pick b { color: var(--accent); }   /* the amber timecode marks a line that is IN the cut; unscoped, it stayed amber on a line the visitor had just taken out */ .lab .demo-cut .l4 b { animation: pickb2 10s linear infinite; } .lab .demo-cut .l5 b { animation: pickb3 10s linear infinite; }
  @keyframes pickb  { 0%, 12% { color: rgba(255,255,255,0.5); } 16%, 100% { color: var(--accent); } }
  @keyframes pickb2 { 0%, 36% { color: rgba(255,255,255,0.5); } 40%, 100% { color: var(--accent); } }
  @keyframes pickb3 { 0%, 60% { color: rgba(255,255,255,0.5); } 64%, 100% { color: var(--accent); } }
}

/* ---------- Finishing a demo: a button appears once there is something to save, then a receipt ---------- */
.demo-go {
  justify-self: start; display: none; align-items: center; gap: 0.5rem;
  font-family: var(--font-display); font-weight: 700; font-size: 0.95rem;
  padding: 0.7rem 1.2rem; min-height: 44px; border-radius: 999px; border: 0; cursor: pointer;
  background: var(--accent); color: var(--bg);
  transition: transform 0.25s var(--ease), filter 0.2s;
}
.demo-go.is-ready { display: inline-flex; }
.demo-hint { display: none; font-size: 0.85rem; color: rgba(255,255,255,0.6); }   /* only shown beside a button that is standing and waiting */
.demo-go:hover { transform: translateY(-2px); filter: brightness(1.08); }
.demo-done {
  display: none; align-items: center; gap: 0.9rem;
  padding: 0.7rem 1rem; border-radius: 12px;
  background: rgba(255,255,255,0.08); border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
}
.demo-done.is-shown { display: flex; animation: done-in 0.5s var(--ease) both; }
.demo-done .tick { flex: 0 0 auto; width: 40px; height: 40px; border-radius: 12px; background: var(--accent); color: var(--bg); display: grid; place-items: center; transform: rotate(-6deg); box-shadow: 0 8px 18px -8px rgba(0,0,0,0.7); }
.demo-done .tick svg { width: 20px; height: 20px; }
.demo-done .tick path { stroke-dasharray: 30; stroke-dashoffset: 30; animation: tick-draw 0.5s 0.15s var(--ease) forwards; }
.demo-done .done-text { min-width: 0; }
.demo-done .done-title { display: block; font-family: var(--font-display); font-weight: 800; font-size: 1.1rem; letter-spacing: -0.01em; }
/* ONE LINE, ALWAYS. Two media rectangles used to hide this line outright, and both existed for the same reason:
   in a narrow column the sentence wrapped to three or four rows and pushed the receipt through the folder floor.
   Clamped, its height is a constant 21.8px at every width, so the wrapping those rectangles guarded against cannot
   happen and they are gone. What remains is one height threshold, because 19px is 19px and a short folder has not
   got it. Do not replace the clamp with a hide; the hide is what cost the visitor the sentence. */
.demo-done .done-detail { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; overflow: hidden; font-size: 0.85rem; color: rgba(255,255,255,0.75); }
/* The chat receipt lives INSIDE the phone, so its line box is the phone's width, 145 to 210px, not the folder's.
   The sentence needs 209.6px, so between 896 and 1009 wide it wrapped and the clamp cut it mid-word. Two lines is a
   ceiling, not a target: where the sentence fits it still takes one line, and where it does not the folder has about
   170px of floor going spare. A ceiling is a function of the text; a media query here would have been a rectangle. */
.demo-chat .demo-done .done-detail { -webkit-line-clamp: 2; }
.demo:has(> .demo-done.is-shown) > .obj-cap { display: none; }   /* the receipt says it, so the caption steps aside and the folder keeps its floor */
.demo-done .done-actions { margin-left: auto; flex: 0 0 auto; display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; justify-content: flex-end; }
.demo-done .done-cta { display: inline-flex; align-items: center; min-height: 44px; padding: 0.5rem 0.9rem; border-radius: 999px; background: var(--accent); color: var(--bg); font-family: var(--font-display); font-weight: 700; font-size: 0.85rem; white-space: nowrap; }
.demo-done .done-cta:hover { filter: brightness(1.08); }
.demo-done button { flex: 0 0 auto; background: none; border: 0; color: rgba(255,255,255,0.8); font-family: var(--font-display); font-weight: 700; font-size: 0.85rem; cursor: pointer; text-decoration: underline; text-underline-offset: 3px; min-height: 44px; padding: 0 0.25rem; white-space: nowrap; }
.demo-done button:hover { color: #fff; }
@keyframes done-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes tick-draw { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) { .demo-done.is-shown { animation: none; } .demo-done .tick path { animation: none; stroke-dashoffset: 0; } }
/* the live-state animation kill must not stop the receipt itself (but reduced motion still wins) */
@media (prefers-reduced-motion: no-preference) {
  .demo.is-live .demo-done.is-shown { animation: done-in 0.5s var(--ease) both !important; }
  .demo.is-live .demo-done .tick path { animation: tick-draw 0.5s 0.15s var(--ease) forwards !important; }
}
.demo-chat .demo-go { margin: 0.5rem auto; }
.demo-chat .demo-done { margin: 0.5rem 0; padding: 0.6rem 0.8rem; }
.shelf-card .demo-go { padding: 0.5rem 0.9rem; font-size: 0.85rem; align-self: flex-start; }
.shelf-card .demo-done { padding: 0 0.7rem; gap: 0.6rem; }   /* one compact row inside the card */
.shelf-card .demo-done .done-title { font-size: 0.9rem; white-space: nowrap; } .shelf-card .demo-done .done-detail { display: none; }
.shelf-card:has(.demo-done.is-shown) .panel-meta, .shelf-card:has(.demo-go.is-ready) .panel-meta { display: none; }   /* the play button and the receipt take the meta row's space, so the card never grows past its folder (keeping the row runs 44px over the floor at 1440x740) */
.shelf-card .demo-done .tick { width: 28px; height: 28px; } .shelf-card .demo-done .tick svg { width: 15px; height: 15px; }
.shelf-card .demo-done .done-actions { gap: 0.4rem; }
.shelf-card .demo-done .done-cta { min-height: 44px; padding: 0.35rem 0.8rem; font-size: 0.8rem; }
.shelf-card .demo-done button { font-size: 0.8rem; min-height: 44px; }
.demo-chat .demo-done { flex-wrap: wrap; }
.demo-chat .demo-done .done-actions { flex-basis: 100%; margin-left: 0; justify-content: flex-start; }

/* phones: editable text at 16px so iOS Safari does not zoom on focus */
@media (max-width: 55.99rem) {
  .compose input { font-size: 1rem; }
  .obj .slide ul { font-size: 1rem; }
  .obj .slide { height: auto; min-height: 50%; }   /* the card grows to fit its bullets at this size */
  .obj .desc-frame { aspect-ratio: 4 / 3; }   /* a taller frame so the slide and the caption bar both fit */
}
/* the page pause stops the stickers on the shelf minis too */
.motion-paused .demo-badge { animation-play-state: paused !important; }

/* the Kinora photos are tappable wherever the demo appears (the deck scopes these to its shelf card) */
.mini-kinora .photos i { min-height: 44px; max-height: 8rem; cursor: pointer; transition: transform 0.25s var(--ease), box-shadow 0.25s, outline-color 0.2s; outline: 3px solid transparent; outline-offset: 2px; }
.mini-kinora .photos i.is-cut { outline-color: var(--accent); transform: translateY(-4px) rotate(0deg) !important; box-shadow: 0 16px 30px -10px rgba(0,0,0,0.9); }
.mini-kinora .photos i:focus-visible { outline-color: #fff; }
.obj:has(> .demo-done.is-shown) .obj-cap { display: none; }
@media (max-width: 55.99rem) {
  .demo-done { flex-wrap: wrap; }
  .demo-done .done-actions { flex-basis: 100%; margin-left: 0; justify-content: flex-start; }
}

/* Bubble Reel: the export control sits in the phone's header like a share button; the receipt drops in above the reply box */
.demo-chat .phone-top { display: flex; align-items: center; gap: 0.5rem; }
.demo-chat .phone-top .demo-go { margin-left: auto; min-height: 36px; padding: 0.35rem 0.8rem; font-size: 0.8rem; border-radius: 999px; position: relative; }
.demo-chat .phone-top .demo-go::after { content: ""; position: absolute; left: 0; right: 0; top: -4px; bottom: -4px; }   /* a 44px hit area on a 36px pill */
.demo-chat .phone-top .demo-go { white-space: nowrap; }   /* the label is one line or the header grows and shoves the thread down */
@media (max-width: 60rem) {
  .demo-chat .phone-top .demo-go { font-size: 0.78rem; padding: 0.35rem 0.5rem; letter-spacing: -0.01em; }   /* the narrowest phone: the header holds the name, the pill and its 44px target on one line, and no type on this site goes under 12px */
}
.demo-chat .demo-badge-next { position: absolute; top: -1.5rem; right: -1rem; left: auto; bottom: auto; }
.demo-chat .demo-badge-next::after { top: auto; bottom: -7px; left: auto; right: 1.4rem; margin-top: 0; }

/* ---------- The export ratio in the Bubble Reel demo ----------
   THE RATIO BELONGS TO THE VIDEO, NOT TO THE DEVICE AROUND IT. These buttons choose the shape of the video Bubble
   Reel exports, and it used to be applied to the whole phone: contact header, conversation and composer together.
   At 16:9 in a 304px column that made the phone 171px tall, of which 124px is chrome, so fit() hid every bubble and
   a demo about turning a thread into a video showed an empty box after one ordinary click. The conversation carries
   the ratio; the phone is its chrome plus that.

   THIS LIVES HERE BECAUSE THE DEMO DOES. The markup comes from tools/demos-markup.mjs and the behaviour from
   js/demos.js, both shared by the homepage and the Bubble Reel page, but the rules that gave the buttons their
   effect were in deck.css, which only the homepage loads. app.css had two rules of its own that set the phone's
   height instead of the conversation's shape, and a more specific rule above them won anyway, so on the app page
   all three buttons did exactly nothing: the frame measured 250x368 whichever was pressed, 21% out at 9:16 and 62%
   out at 16:9. One variable, in the one stylesheet both pages load, and each context only says how tall the phone
   may be. --ar serves as both the multiplier and the aspect-ratio, so the two cannot drift apart. */
/* --phone-h has a fallback because it is the one thing each context supplies, and a context that forgets should
   get a sensible phone rather than a collapsed one. That is the whole fault this rule was moved here to end. */
.demo-chat { --ar: 9 / 16; --phone-chrome: 7.8rem;   /* header 38 + composer 44 + its margin 12 + the phone's own padding 30 */
              /* The phone's width, named once. The "Try it" nudge has to be placed against the phone's edge, and
                 before this it was placed against the phone's grid COLUMN: the phone is narrower than its column,
                 so on the app page the nudge landed 44px past the phone and over the settings panel instead. */
              --phone-w: min(var(--chat-phone-col, 19rem), calc((var(--phone-h, 32rem) - var(--phone-chrome)) * (var(--ar)))); }
.demo-chat.size-square { --ar: 1 / 1; }
.demo-chat.size-wide   { --ar: 16 / 9; }
.demo-chat .phone { aspect-ratio: auto; height: auto; max-height: var(--phone-h, 32rem); width: min(100%, var(--phone-w)); }
.demo-chat .msgs { aspect-ratio: var(--ar); flex: 0 1 auto; }
