/* * engine.css — the backstage app. * * Nobody wants to look at this during a service. It exists to answer three * questions fast: is it hearing audio, is it connected to the church server, * and are the words right. */ /* * Make the `hidden` attribute actually hide things. * * The browser's own [hidden] rule is `display: none`, but it lives in the user- * agent stylesheet, so any author rule that sets `display` — every .warn, * .toast and flex row below — silently outranks it. The element stays on * screen with hidden="true" and the JavaScript looks like it isn't running. * That is how the operator console ended up permanently insisting the Mac * engine was disconnected while it was sitting there working. */ [hidden] { display: none !important; } :root { --bg: #0b0d12; --surface: #151922; --raised: #1d2331; --line: #2a3244; --text: #eef1f6; --muted: #8b93a7; --accent: #6ea8fe; --go: #2fbf71; --stop: #e5484d; --warn: #f5a524; } * { box-sizing: border-box; } body { margin: 0; background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; font-size: 14px; } h1 { font-size: 1.5rem; margin: 0 0 .375rem; font-weight: 700; } h2 { font-size: .9375rem; margin: 0 0 .25rem; font-weight: 650; } p { margin: 0 0 .75rem; line-height: 1.5; } code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .75rem; background: var(--raised); padding: .0625rem .3125rem; border-radius: 4px; } hr { border: 0; border-top: 1px solid var(--line); margin: 1.25rem 0; } .muted { color: var(--muted); font-size: .8125rem; } .hint { color: var(--muted); font-size: .75rem; line-height: 1.5; margin: .375rem 0 0; } /* ---- header ---- */ .head { position: sticky; top: 0; z-index: 5; display: flex; align-items: center; justify-content: space-between; padding: .75rem 1.25rem; background: rgba(11,13,18,.92); backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); -webkit-app-region: drag; } .brand { display: flex; align-items: center; gap: .5rem; font-size: .875rem; padding-left: 4.5rem; } .brand b { font-weight: 700; } .mark { width: .625rem; height: .625rem; border-radius: 3px; background: var(--accent); } .head-right { display: flex; align-items: center; gap: .4375rem; -webkit-app-region: no-drag; } .pip { width: .5rem; height: .5rem; border-radius: 50%; background: var(--muted); } .pip.ok { background: var(--go); } .pip.bad { background: var(--stop); } .pip-label { font-size: .75rem; color: var(--muted); } .build { font-size: .6875rem; color: var(--muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; border: 1px solid var(--line); border-radius: 999px; padding: .0625rem .4375rem; margin-left: .125rem; } /* ---- page ---- Hero across the top, settings in a column of the same width they have always had, and the congregation card parked in a rail on the right where it stays put while the settings scroll under it. Narrow windows fold back to one column and the rail returns to the top of the flow, where it is still the first thing you need. */ .page { max-width: 72rem; margin: 0 auto; padding: 1.25rem 1.25rem 3rem; display: grid; gap: 0 1rem; justify-content: center; grid-template-columns: minmax(0, 44rem) minmax(17rem, 21rem); align-items: start; } .page > .hero { grid-column: 1 / -1; } .col-main { grid-column: 1; min-width: 0; } .col-rail { grid-column: 2; position: sticky; top: 4rem; min-width: 0; } /* * One column. The order is deliberate and it is NOT source order: the hero * stays first — it is the button you came to press — then the congregation * card, then the settings. `order: -1` on the rail alone put the QR ABOVE the * hero, which is the one arrangement that is wrong. */ @media (max-width: 68rem) { .page { grid-template-columns: minmax(0, 44rem); } .col-main, .col-rail { grid-column: 1; } .page > .hero { order: 1; } .col-rail { position: static; order: 2; } .col-main { order: 3; } } .card { background: var(--surface); border: 1px solid var(--line); border-radius: 14px; padding: 1.125rem; margin-bottom: .875rem; } /* ---- collapsible groups ---- */ .group { background: var(--surface); border: 1px solid var(--line); border-radius: 14px; margin-bottom: .875rem; overflow: hidden; } .group-head { display: flex; align-items: center; gap: .625rem; width: 100%; appearance: none; background: transparent; border: 0; color: var(--text); padding: 1rem 1.125rem; cursor: pointer; text-align: left; font: inherit; transition: background .16s ease; } .group-head:hover { background: var(--raised); } .group-head h2 { margin: 0; } /* Clipped by CSS, not by slicing the string — slicing cut mid-word and left things like "Dante Virtual Soundcard (Vir" with the bracket hanging open. */ .group-sub { margin-left: auto; font-size: .75rem; color: var(--muted); max-width: 55%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .chev { width: .5rem; height: .5rem; flex: none; border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted); transform: rotate(-45deg); transition: transform .32s cubic-bezier(.2,.7,.3,1); } .group[data-open="true"] .chev { transform: rotate(45deg); } /* * 0fr -> 1fr, not a pixel height: the content of these sections changes size * after they open (a 105-row language list, a live meter, a QR that loads), * and any measured max-height would be wrong the moment it did. */ .group-body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .34s cubic-bezier(.2,.7,.3,1), opacity .22s ease; opacity: 0; } .group[data-open="true"] .group-body { grid-template-rows: 1fr; opacity: 1; } .group-body > .group-inner { overflow: hidden; min-height: 0; padding: 0 1.125rem; } .group[data-open="true"] > .group-body > .group-inner { padding-bottom: 1.125rem; } @media (prefers-reduced-motion: reduce) { .group-body { transition: none; } .chev { transition: none; } } .subhead2 { margin: 1rem 0 .25rem; font-size: .75rem; color: var(--muted); font-weight: 650; } /* ---- right rail ---- */ .rail-card .qr { display: block; margin: 0 auto .625rem; } /* The rail is a third of the width the settings column has, and an operator URL does not fit in it. Let it wrap rather than clip — a truncated address is worse than a two-line one. */ .rail-card .codeblock { flex-wrap: wrap; } .rail-card .codeblock code { flex: 1 1 100%; min-width: 0; overflow-wrap: anywhere; white-space: normal; } .rail-card .codeblock .btn { margin-left: auto; } .railbtns { display: grid; gap: .375rem; margin-top: .625rem; } .railwide { width: 100%; margin-top: .375rem; } /* ---- output routing matrix ---- One row per language, one button per physical channel. The grid is the whole point: you should be able to see at a glance that nothing is doubled up and nothing is unrouted. */ /* Sized for 64 channels as readily as for 2: the rows scroll rather than the page, and the buttons shrink a step once there are more than sixteen so a 64-wide row still fits without becoming a horizontal scroll hunt. */ .outmap { display: grid; gap: .375rem; margin-top: .5rem; max-height: 22rem; overflow-y: auto; } .outrow { display: flex; align-items: flex-start; gap: .5rem; } .outrow > b { flex: none; width: 9.5rem; padding-top: .25rem; font-size: .8125rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .outchans { display: flex; flex-wrap: wrap; gap: .1875rem; flex: 1; min-width: 0; } .outch { appearance: none; cursor: pointer; font: inherit; font-size: .75rem; min-width: 1.875rem; padding: .1875rem .375rem; background: var(--raised); border: 1px solid var(--line); color: var(--muted); border-radius: 6px; font-variant-numeric: tabular-nums; transition: border-color .14s ease, background .14s ease, color .14s ease; } .outmap.many .outch { min-width: 1.5rem; padding: .125rem .1875rem; font-size: .6875rem; } /* Adjacent selected channels fuse into one pill, so a stereo pair reads as a pair rather than as two numbers that happen to be lit. This is the "link" made visible — the shape says they belong together. */ .outch.on:has(+ .outch.on) { border-top-right-radius: 0; border-bottom-right-radius: 0; } .outch.on + .outch.on { border-top-left-radius: 0; border-bottom-left-radius: 0; margin-left: -.1875rem; border-left-color: rgba(110,168,254,.35); } .outmap.many .outch.on + .outch.on { margin-left: -.1875rem; } .outtag { flex: none; width: 8rem; padding-top: .25rem; text-align: right; font-size: .6875rem; color: var(--muted); font-variant-numeric: tabular-nums; } .outrow.none .outtag { opacity: .6; } .outtrim { flex: none; width: 5.5rem; margin: 0; accent-color: var(--accent); } .outtrim:disabled { opacity: .3; } .outdb { flex: none; width: 3.25rem; padding-top: .25rem; text-align: right; font-size: .6875rem; color: var(--muted); font-variant-numeric: tabular-nums; } .outch:hover { border-color: var(--muted); } .outch.on { border-color: var(--accent); background: rgba(110,168,254,.16); color: var(--text); } .outrow.none > b { color: var(--muted); font-weight: 500; } /* ---- channel picker ---- A grid of numbered buttons, each with its own live level. Finding the speaker on a 64-channel Dante device is a matter of watching for the bar that moves, so the meter is the point and the number is the label. */ .chanbox { margin: .75rem 0; } .chanhead { display: flex; align-items: center; gap: .5rem; margin-bottom: .375rem; } .chanhead .btn { margin-left: auto; } .chanlist { display: grid; gap: .3125rem; grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr)); max-height: 13rem; overflow-y: auto; padding: .125rem; } .chanrow { display: flex; align-items: center; gap: .375rem; appearance: none; cursor: pointer; font: inherit; background: var(--raised); border: 1px solid var(--line); color: var(--muted); border-radius: 7px; padding: .3125rem .4375rem; transition: border-color .16s ease, background .16s ease, color .16s ease; } .chanrow:hover { border-color: var(--muted); } .chanrow b { font-size: .75rem; min-width: 1.25rem; text-align: right; font-variant-numeric: tabular-nums; } .chanrow.on { border-color: var(--accent); color: var(--text); background: rgba(110,168,254,.12); } .chanbar { flex: 1; height: .375rem; border-radius: 999px; background: var(--bg); overflow: hidden; } .chanbar-fill { display: block; height: 100%; width: 0; border-radius: 999px; background: var(--muted); transition: width .08s linear; } .chanbar-fill.hot { background: var(--go); } .chanrow.on .chanbar-fill { background: var(--accent); } .chanrow.on .chanbar-fill.hot { background: var(--go); } /* ---- hero ---- */ .hero { background: var(--surface); border: 1px solid var(--line); border-radius: 14px; padding: 1.75rem; margin-bottom: .875rem; text-align: center; } .hero[data-state="live"] { border-color: var(--go); background: linear-gradient(180deg, rgba(47,191,113,.10), var(--surface)); } .hero[data-state="ready"] { border-color: #2c4a7a; } .hero[data-state="loading"] { border-color: var(--accent); } .hero[data-state="error"] { border-color: var(--stop); background: linear-gradient(180deg, rgba(229,72,77,.10), var(--surface)); } .hero-icon { width: 2.75rem; height: 2.75rem; margin: 0 auto .75rem; border-radius: 50%; background: var(--raised); position: relative; } .hero[data-state="live"] .hero-icon { background: var(--go); animation: pulse 2s ease-in-out infinite; } .hero[data-state="loading"] .hero-icon { background: var(--accent); animation: pulse 1.4s ease-in-out infinite; } .hero[data-state="error"] .hero-icon { background: var(--stop); } /* A voice falling back to built-in is amber, on the hero, in a sentence — the one place whoever is standing at the Mac is actually looking. */ .voice-warn { margin: .5rem 0 0; padding: .5rem .75rem; border-radius: 8px; font-size: .8125rem; color: #fbbf24; background: rgba(251,191,36,.10); border: 1px solid rgba(251,191,36,.35); } @keyframes pulse { 0%,100% { transform: scale(1); opacity: 1; } 50% { transform: scale(.86); opacity: .55; } } .hero p { color: var(--muted); font-size: .875rem; max-width: 30rem; margin-left: auto; margin-right: auto; } .bar { height: .4375rem; background: var(--raised); border-radius: 999px; overflow: hidden; margin: .875rem auto .5rem; max-width: 24rem; } .bar-fill { height: 100%; width: 0; background: var(--accent); border-radius: 999px; transition: width .25s ease; } .prog-note { font-size: .75rem; color: var(--muted); font-family: ui-monospace, Menlo, monospace; } /* ---- controls ---- */ label { display: block; font-size: .75rem; color: var(--muted); margin: .875rem 0 .375rem; text-transform: uppercase; letter-spacing: .07em; font-weight: 600; } label:first-of-type { margin-top: 0; } input, select { width: 100%; padding: .625rem .75rem; background: var(--raised); color: var(--text); border: 1px solid var(--line); border-radius: 9px; font: inherit; font-size: .875rem; outline: none; } input:focus, select:focus { border-color: var(--accent); } input[type=range] { padding: 0; background: transparent; border: 0; accent-color: var(--accent); } .row { display: flex; gap: .5rem; align-items: center; margin-top: .5rem; flex-wrap: wrap; } .row select, .row input { flex: 1; min-width: 10rem; } .row .btn { flex: none; } .btn { appearance: none; border: 1px solid var(--line); background: var(--raised); color: var(--text); padding: .5625rem 1rem; border-radius: 9px; font: inherit; font-size: .875rem; font-weight: 550; cursor: pointer; } .btn:hover { background: #262d3d; } .btn-primary { background: var(--accent); border-color: var(--accent); color: #08101f; font-weight: 650; } .btn:disabled { opacity: .5; cursor: not-allowed; } /* ---- level meter ---- */ .meter-row { display: flex; align-items: center; gap: .75rem; margin-top: .875rem; } .meter { position: relative; flex: 1; height: .875rem; background: var(--raised); border-radius: 5px; overflow: hidden; } .meter-fill { height: 100%; width: 0; background: var(--muted); border-radius: 5px; transition: width .06s linear, background .15s ease; } .meter-fill.speech { background: var(--go); } .meter-fill.hot { background: var(--warn); } .meter-fill.clip { background: var(--stop); } .meter-thresh { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--text); opacity: .65; left: 0; transition: left .2s ease; } .meter-label { font-family: ui-monospace, Menlo, monospace; font-size: .75rem; color: var(--muted); width: 5.5rem; text-align: right; } /* The gate control sits under the meter it moves, and lines its readout up with the meter's own so the two numbers can be compared at a glance. */ .threshrow { display: flex; align-items: center; gap: .75rem; margin-top: .5rem; } .threshrow .oprow { flex: 1 1 auto; } .threshrow .outtrim { flex: none; } /* ---- monitor ---- */ .monitor { background: var(--raised); border-radius: 10px; padding: .875rem; height: 9rem; overflow-y: auto; font-size: .875rem; line-height: 1.55; display: flex; flex-direction: column; gap: .5rem; } .mon-empty { color: var(--muted); font-size: .8125rem; margin: auto; } .mon-line { color: var(--text); } .mon-line.partial { color: var(--muted); font-style: italic; } .mon-line small { display: block; color: var(--muted); font-size: .6875rem; margin-top: .1875rem; } /* English, then what each language was actually told. The operator reads this to verify the service, so the two must be visually distinct but adjacent. */ .mon-line + .mon-line { margin-top: .625rem; } .mon-en { color: var(--text); } .mon-tr { margin: .1875rem 0 0 .875rem; font-size: .8125rem; color: var(--muted); display: flex; gap: .5rem; align-items: baseline; } .mon-tr b { flex: none; font-size: .625rem; font-weight: 700; letter-spacing: .03em; text-transform: uppercase; color: var(--accent); opacity: .85; } .mon-tr.dropped .mon-tr-text { color: var(--warn); font-style: italic; } .mon-wait { margin: .1875rem 0 0 .875rem; font-size: .75rem; color: var(--muted); opacity: .5; font-style: italic; } /* ---- details ---- */ .details summary { cursor: pointer; list-style: none; display: flex; align-items: center; gap: .5rem; } .details summary::-webkit-details-marker { display: none; } .details summary::before { content: "▸"; color: var(--muted); font-size: .75rem; } .details[open] summary::before { content: "▾"; } .details summary h2 { margin: 0; } /* ---- log ---- */ .log { background: var(--raised); border-radius: 10px; padding: .75rem; height: 8rem; overflow-y: auto; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .6875rem; line-height: 1.6; color: var(--muted); white-space: pre-wrap; word-break: break-word; } .log .err { color: var(--stop); } .log .ok { color: var(--go); } /* The guard trimming an invented clause is not an error and not a success — it is the model being caught. Amber so a run of them is visible at a glance, because a spike is the only signal that the guard has started misfiring. */ .log .warn { color: #d8a23a; } /* ---- the address the congregation uses ---- */ .sharerow { display: flex; gap: 1.25rem; flex-wrap: wrap; align-items: flex-start; margin-top: .875rem; } .qr { width: 9.5rem; height: 9.5rem; background: #fff; border-radius: 10px; padding: .4375rem; flex: none; } .shareside { flex: 1; min-width: 15rem; } .addrlist { display: flex; flex-direction: column; gap: .375rem; margin-bottom: .875rem; } .addr { display: flex; align-items: baseline; gap: .625rem; background: var(--raised); border: 1px solid var(--line); border-radius: 9px; padding: .5625rem .75rem; } .addr b { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9375rem; font-weight: 500; color: var(--text); word-break: break-all; } .addr span { font-size: .6875rem; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; flex: none; } .addrlist:empty::before { content: "No network connection"; color: var(--muted); font-size: .8125rem; } .addr { cursor: pointer; width: 100%; text-align: left; font: inherit; transition: border-color .15s ease; } .addr:hover { border-color: var(--accent); } .addr.is-chosen { border-color: var(--accent); } .addr.is-chosen span { color: var(--accent); } /* ---- who is listening right now ---- */ .listenbox { display: flex; align-items: baseline; flex-wrap: wrap; gap: .5rem .75rem; background: var(--raised); border: 1px solid var(--line); border-radius: 10px; padding: .75rem .9375rem; margin-bottom: .875rem; } .listenbox[hidden] { display: none; } .listenbox > b { font-size: 1.5rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1; } .listenbox > span { font-size: .8125rem; color: var(--muted); } .listenbreak { display: flex; flex-wrap: wrap; gap: .375rem; width: 100%; } .listenbreak span { font-size: .75rem; color: var(--text); background: var(--line); border-radius: 999px; padding: .1875rem .5625rem; } .listenbreak b { font-variant-numeric: tabular-nums; } /* Which devices are connected — so a forgotten tab can be found. */ .listenwho { display: flex; flex-direction: column; gap: .25rem; width: 100%; margin-top: .5rem; } .listenwho:empty { display: none; } .wholine { display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap; font-size: .75rem; padding: .3125rem 0; border-top: 1px solid var(--line); } .wholine b { font-weight: 600; color: var(--text); } .wholine span { color: var(--muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .6875rem; } /* ---- Setup / Operator, in one window ---- */ /* Panes cross-fade rather than jumping, so switching mode reads as the same window changing rather than a new screen appearing. */ /* Both panes are now one continuous settings page. The entrance animation is kept on the first only — running it on both produced a visible two-stage fade with a seam between them. */ .mode-pane { animation: none; } #setupPane { animation: paneIn .26s cubic-bezier(.2,.7,.3,1) both; } .mode-pane[hidden] { display: none; } @keyframes paneIn { from { opacity: 0; transform: translateY(.5rem); } to { opacity: 1; transform: none; } } @media (prefers-reduced-motion: reduce) { .mode-pane { animation: none; } } /* The hero becomes the control in operator mode. */ .hero-go { appearance: none; border: 0; cursor: pointer; width: 100%; margin-top: 1.125rem; padding: 1.125rem; border-radius: 12px; font: inherit; font-size: 1.375rem; font-weight: 750; letter-spacing: .02em; background: var(--go); color: #052e18; transition: background .18s ease, transform .08s ease, opacity .18s ease; animation: paneIn .26s cubic-bezier(.2,.7,.3,1) both; } .hero-go[hidden] { display: none; } .hero-go:active { transform: scale(.99); } .hero-go[data-live="true"] { background: var(--stop); color: #fff; } .hero-go:disabled { background: var(--raised); color: var(--muted); cursor: not-allowed; } .stats { display: grid; grid-template-columns: repeat(3,1fr); gap: .5rem; margin-top: 1rem; animation: paneIn .3s cubic-bezier(.2,.7,.3,1) both; } .stats[hidden] { display: none; } .stat { background: var(--raised); border-radius: 10px; padding: .625rem; } .stat b { display: block; font-size: 1.25rem; font-weight: 700; font-variant-numeric: tabular-nums; } .stat span { font-size: .6875rem; color: var(--muted); } /* The cough button. Amber, not red — red is "stopped", and this is the opposite of stopped: everything is still running, it just is not going out. */ .hold-btn { display: block; width: 100%; max-width: 22rem; margin: .5rem auto 0; padding: .5rem; font-weight: 600; letter-spacing: .02em; } .hold-btn[data-held="true"] { border-color: var(--warn); background: rgba(245,165,36,.18); color: var(--warn); } .holdbar { margin: .625rem auto 0; max-width: 34rem; padding: .5rem .75rem; border-radius: 7px; border: 1px solid var(--warn); background: rgba(245,165,36,.14); font-size: .8125rem; line-height: 1.45; animation: holdPulse 1.6s ease-in-out infinite; } .holdbar b { color: var(--warn); letter-spacing: .04em; } @keyframes holdPulse { 0%,100% { opacity: 1; } 50% { opacity: .68; } } @media (prefers-reduced-motion: reduce) { .holdbar { animation: none; } } .subhead { margin: 1rem 0 .375rem; font-size: .8125rem; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; } /* The QR is printed and handed out. When the address behind it moves, or when it is pointing at a network no phone is on, that has to be impossible to miss — it looks fine either way, and the failure only shows up in the room. */ .qrwarn { margin: 0 0 .625rem; padding: .5rem .6875rem; border-radius: 7px; border: 1px solid var(--warn); background: rgba(245,165,36,.12); color: var(--text); font-size: .8125rem; line-height: 1.45; } .qrwarn b { color: var(--warn); } .chips { display: flex; flex-wrap: wrap; gap: .375rem; margin: .625rem 0; min-height: 1.75rem; } .chips:empty::before { content: "None"; color: var(--muted); font-size: .8125rem; } .chip { display: inline-flex; align-items: center; gap: .375rem; background: var(--raised); border: 1px solid var(--line); border-radius: 999px; padding: .25rem .4375rem .25rem .6875rem; font-size: .8125rem; } .chip button { appearance: none; border: 0; background: transparent; color: var(--muted); cursor: pointer; font-size: 1rem; line-height: 1; padding: 0 .1875rem; } .vtag.warn { color: var(--warn); } .chip button:hover { color: var(--stop); } /* Pinning and unpinning used to be instantaneous, which on a page this long reads as a chip appearing from nowhere. Same treatment as the language columns: grow in, shrink out, and fly from the search result you picked. */ @keyframes chipIn { from { transform: scale(.86); opacity: 0; } to { transform: scale(1); opacity: 1; } } @keyframes chipOut { from { transform: scale(1); opacity: 1; max-width: 18rem; } to { transform: scale(.82); opacity: 0; max-width: 0; padding-left: 0; padding-right: 0; margin-left: -.375rem; } } .chip.chip-in { animation: chipIn .2s cubic-bezier(.2,.7,.3,1) both; } .chip.chip-out { animation: chipOut .19s ease-in both; overflow: hidden; white-space: nowrap; pointer-events: none; } .chip.is-landing { visibility: hidden; } .chipfly { position: fixed; z-index: 60; pointer-events: none; margin: 0; box-shadow: 0 8px 24px rgba(0,0,0,.45); will-change: transform, opacity; } .chipfly button { display: none; } @media (prefers-reduced-motion: reduce) { .chipfly { display: none; } .chip.chip-in, .chip.chip-out { animation: none; } } .opresults { display: grid; gap: .1875rem; margin-top: .375rem; max-height: 11rem; overflow-y: auto; } .opresults:empty { display: none; } .opresults button { appearance: none; text-align: left; width: 100%; font: inherit; font-size: .8125rem; background: var(--raised); border: 1px solid transparent; color: var(--text); border-radius: 8px; padding: .4375rem .625rem; cursor: pointer; } .opresults button:hover { border-color: var(--accent); } .opresults button span { color: var(--muted); margin-left: .4375rem; font-size: .75rem; } .codeblock { display: flex; align-items: center; gap: .5rem; background: var(--raised); border: 1px solid var(--line); border-radius: 9px; padding: .5rem .625rem; } .codeblock code { flex: 1; overflow-x: auto; white-space: nowrap; font-size: .75rem; color: #cbd5e6; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } .codeblock + .codeblock { margin-top: .375rem; } .urlname { flex: 0 0 3.5rem; font-size: .75rem; font-weight: 600; color: var(--dim); } h2.mt { margin-top: 1.5rem; } textarea { width: 100%; padding: .625rem .75rem; background: var(--raised); color: var(--text); border: 1px solid var(--line); border-radius: 9px; font-family: ui-monospace, Menlo, monospace; font-size: .8125rem; line-height: 1.6; resize: vertical; outline: none; } textarea:focus { border-color: var(--accent); } /* The volunteer's traffic light: one sentence, colored by severity. */ .op-health { margin: .375rem 0 0; font-size: .8125rem; font-weight: 600; } .op-health[data-level="green"] { color: #4ade80; } .op-health[data-level="amber"] { color: #fbbf24; } .op-health[data-level="red"] { color: #f87171; } /* The voice auditioning list: one row per candidate, play and choose. */ .voicelist { display: flex; flex-direction: column; gap: .25rem; margin-top: .5rem; max-height: 19rem; overflow-y: auto; } .vrow { display: flex; align-items: center; gap: .5rem; padding: .375rem .5rem; background: var(--raised); border: 1px solid var(--line); border-radius: 8px; } .vrow.is-current { border-color: var(--accent); } .vrow .vname { flex: 1; font-size: .8125rem; } .vrow .vtag { font-size: .6875rem; color: var(--dim); } .vrow .vmsg { font-size: .6875rem; color: #fbbf24; max-width: 14rem; } .vrow .vmsg.bad { color: #f87171; } .vrow button { flex: 0 0 auto; } /* * "Offer this voice to listeners". * * Subject to exactly the two traps documented for .oprow below — `label` is * styled as a small-caps field caption and bare `input` is width:100% — so it * has to undo both, or the word "Offer" arrives shouting in letter-spaced caps * next to a checkbox ten rems wide. */ .vrow .voffer { display: flex; align-items: center; gap: .3rem; flex: 0 0 auto; margin: 0; text-transform: none; letter-spacing: normal; font-weight: 400; font-size: .6875rem; color: var(--dim); cursor: pointer; user-select: none; } .vrow .voffer input[type="checkbox"] { width: auto; min-width: 0; flex: 0 0 auto; margin: 0; } .vrow .voffer:has(input:checked) { color: var(--accent); } /* * Checkbox rows on the operator page. * * These are