2026-09-23 15:41:21 -04:00
|
|
|
/*
|
|
|
|
|
* 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 <label> elements, and this stylesheet styles `label` as a small-caps
|
|
|
|
|
* FIELD CAPTION (uppercase, letter-spaced, muted) — correct for "API KEY", wrong
|
|
|
|
|
* for a sentence you click. It also styles bare `input` at width:100%, and
|
|
|
|
|
* `.row input` at min-width:10rem, so an unqualified checkbox rendered ten rems
|
|
|
|
|
* wide and shoved the language name off the side of the card. Both have to be
|
|
|
|
|
* undone explicitly here; verified in the browser, not assumed.
|
|
|
|
|
*/
|
|
|
|
|
.oprow, .langrow {
|
|
|
|
|
text-transform: none;
|
|
|
|
|
letter-spacing: normal;
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
color: var(--text);
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
.oprow {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: .5rem;
|
|
|
|
|
margin: .625rem 0 .125rem;
|
|
|
|
|
font-size: .8125rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
user-select: none;
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
}
|
|
|
|
|
/* Beat `input, select { width: 100% }` and `.row input { flex: 1; min-width: 10rem }`. */
|
|
|
|
|
.oprow input[type="checkbox"],
|
|
|
|
|
.langrow input[type="checkbox"],
|
|
|
|
|
.row .oprow input[type="checkbox"] {
|
|
|
|
|
width: auto;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
padding: 0;
|
|
|
|
|
margin: 0;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* The language on/off list. 104 rows, so it scrolls inside its own box rather
|
|
|
|
|
than pushing the rest of the operator page down a screen and a half. */
|
|
|
|
|
.langlist {
|
|
|
|
|
margin-top: .5rem;
|
|
|
|
|
max-height: 22rem;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
overflow-x: hidden;
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
border-radius: 9px;
|
|
|
|
|
background: var(--bg);
|
|
|
|
|
}
|
|
|
|
|
.langrow {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: .5rem;
|
|
|
|
|
padding: .375rem .625rem;
|
|
|
|
|
font-size: .8125rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
border-bottom: 1px solid rgba(42, 50, 68, .5);
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
.langrow:last-child { border-bottom: 0; }
|
|
|
|
|
.langrow:hover { background: var(--raised); }
|
|
|
|
|
|
|
|
|
|
/* The native name is what the listener sees, so it leads. It shrinks and
|
|
|
|
|
ellipsises rather than forcing the row wider than the card. */
|
|
|
|
|
.lname {
|
|
|
|
|
flex: 0 1 9rem;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
.lsub {
|
|
|
|
|
flex: 1 1 auto;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
font-size: .75rem;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
/* Whether a language can be SPOKEN is the thing being tuned — it earns a spot
|
|
|
|
|
on every row instead of a separate list to cross-reference. */
|
|
|
|
|
.ltag { flex: 0 0 auto; font-size: .6875rem; color: var(--go); white-space: nowrap; }
|
|
|
|
|
.langrow.novoice .ltag { color: var(--muted); }
|
|
|
|
|
.langrow.novoice .lname { color: #c3cad9; }
|
|
|
|
|
|
|
|
|
|
/* ── the language columns ─────────────────────────────────────────────────
|
|
|
|
|
On the left, off on the right. Both scroll independently, and both must
|
|
|
|
|
contain their scroll: with only a few languages on, the left column is not
|
|
|
|
|
scrollable at all, so a trackpad flick over it would chain to the page while
|
|
|
|
|
the identical box beside it trapped — two panes that look the same behaving
|
|
|
|
|
oppositely. */
|
|
|
|
|
.langcols { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; margin-top: .5rem; }
|
|
|
|
|
.langcol {
|
|
|
|
|
display: flex; flex-direction: column; min-width: 0;
|
|
|
|
|
border: 1px solid var(--line); border-radius: 9px; background: var(--bg);
|
|
|
|
|
}
|
|
|
|
|
.langcolhead {
|
|
|
|
|
display: flex; align-items: baseline; gap: .375rem;
|
|
|
|
|
padding: .4375rem .625rem; border-bottom: 1px solid var(--line);
|
|
|
|
|
font-size: .6875rem; text-transform: uppercase; letter-spacing: .07em;
|
|
|
|
|
color: var(--muted); font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
.langcolhead b { color: var(--text); font-weight: 600; }
|
|
|
|
|
.langcols .langlist {
|
|
|
|
|
margin-top: 0; border: 0; border-radius: 0; background: transparent;
|
|
|
|
|
max-height: 20rem; overscroll-behavior: contain;
|
|
|
|
|
}
|
|
|
|
|
/* Two columns halve the row width, and the first casualties were exactly the
|
|
|
|
|
languages this church distinguishes — "Spanish (Latin Am…", "Português
|
|
|
|
|
(Portu…". The native name is what the LISTENER sees; the operator is looking
|
|
|
|
|
for the English name, so the native name yields its space here and stays
|
|
|
|
|
available on hover. */
|
|
|
|
|
.langcols .lsub { display: none; }
|
|
|
|
|
.langcols .lname { flex: 1 1 auto; }
|
|
|
|
|
|
|
|
|
|
/* The flying row. position:fixed on <body>, so no ancestor overflow can clip it
|
|
|
|
|
— which is the whole reason it exists. Animating the real row instead meant
|
|
|
|
|
the row was clipped by its own column for the entire part of the flight that
|
|
|
|
|
crosses the gap, and the list is rebuilt every 3 seconds by the server tick,
|
|
|
|
|
which would destroy a real-node animation mid-air. A clone notices neither. */
|
|
|
|
|
.langfly {
|
|
|
|
|
position: fixed; z-index: 60; pointer-events: none; margin: 0;
|
|
|
|
|
border-radius: 7px; border: 1px solid var(--accent);
|
|
|
|
|
background: var(--raised); box-shadow: 0 8px 24px rgba(0,0,0,.45);
|
|
|
|
|
will-change: transform, opacity;
|
|
|
|
|
}
|
|
|
|
|
.langfly .ltag, .langfly input { display: none; }
|
|
|
|
|
.langrow.is-landing { visibility: hidden; }
|
|
|
|
|
/* A short accent sweep on the row under the cursor, so the click is confirmed
|
|
|
|
|
even if the eye never follows the clone. */
|
|
|
|
|
@keyframes rowTouched { from { background: rgba(110,168,254,.22); } to { background: transparent; } }
|
|
|
|
|
.langrow.touched { animation: rowTouched .45s ease-out both; }
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
.langfly { display: none; }
|
|
|
|
|
.langrow.touched { animation: none; }
|
|
|
|
|
}
|
2026-09-24 12:09:08 -04:00
|
|
|
|
|
|
|
|
/* Release notes are prose from the forge — wrap them, keep them short, and let
|
|
|
|
|
them scroll rather than push the rest of Settings off the pane. */
|
|
|
|
|
.updnotes {
|
|
|
|
|
max-height: 16rem; overflow: auto; white-space: pre-wrap; word-break: break-word;
|
|
|
|
|
font-size: .75rem; line-height: 1.45; color: var(--muted);
|
|
|
|
|
background: var(--panel, rgba(255,255,255,.03));
|
|
|
|
|
border-radius: 6px; padding: .625rem .75rem; margin: .375rem 0 0;
|
|
|
|
|
}
|
|
|
|
|
#updFound { margin-top: .5rem; }
|