Rudder's corner is a pill that shows it is hearing you, and its draft scrolls like a message thread

Collapsed, the pill reads "Rudder · voice to text". While recording it drops the
label for a red dot, a mic that lights only while speech is detected, and the
last few words on one truncated line, so a closed panel still shows the reader
is being heard. Rudder reports this through an `activity` event. Open, the
panel no longer scrolls: the draft fills it and scrolls on its own, stays
editable while recording, and notices sit above it; with settings or info
open the panel scrolls so they stay reachable.
This commit is contained in:
Doxa Maps 2026-09-27 21:58:07 +00:00
parent 533ef04a9f
commit 4e94e92160
13 changed files with 72 additions and 28 deletions

View file

@ -22,7 +22,7 @@
"cap2": "إعداد مسبق", "cap2": "إعداد مسبق",
"cap1": "مفرد", "cap1": "مفرد",
"stageExperimental": "تجريبي", "stageExperimental": "تجريبي",
"widget:rudder-voice/rudder-voice": "Rudder — تحويل الصوت إلى نص", "widget:rudder-voice/rudder-voice": "Rudder · تحويل الصوت إلى نص",
"toggleList": "إظهار القائمة أو إخفاؤها", "toggleList": "إظهار القائمة أو إخفاؤها",
"openViews": "فتح قائمة العروض", "openViews": "فتح قائمة العروض",
"expandList": "توسيع القائمة", "expandList": "توسيع القائمة",

View file

@ -22,7 +22,7 @@
"cap2": "Voreinstellung", "cap2": "Voreinstellung",
"cap1": "Einzeln", "cap1": "Einzeln",
"stageExperimental": "Experimentell", "stageExperimental": "Experimentell",
"widget:rudder-voice/rudder-voice": "Rudder — Sprache zu Text", "widget:rudder-voice/rudder-voice": "Rudder · Sprache zu Text",
"toggleList": "Liste ein-/ausblenden", "toggleList": "Liste ein-/ausblenden",
"openViews": "Ansichtsliste öffnen", "openViews": "Ansichtsliste öffnen",
"expandList": "Liste ausklappen", "expandList": "Liste ausklappen",

View file

@ -22,7 +22,7 @@
"cap2": "Preset", "cap2": "Preset",
"cap1": "Single", "cap1": "Single",
"stageExperimental": "Experimental", "stageExperimental": "Experimental",
"widget:rudder-voice/rudder-voice": "Rudder — voice to text", "widget:rudder-voice/rudder-voice": "Rudder · voice to text",
"toggleList": "Toggle list", "toggleList": "Toggle list",
"openViews": "Open the list of views", "openViews": "Open the list of views",
"expandList": "Expand list", "expandList": "Expand list",

View file

@ -22,7 +22,7 @@
"cap2": "Preajuste", "cap2": "Preajuste",
"cap1": "Individual", "cap1": "Individual",
"stageExperimental": "Experimental", "stageExperimental": "Experimental",
"widget:rudder-voice/rudder-voice": "Rudder — voz a texto", "widget:rudder-voice/rudder-voice": "Rudder · voz a texto",
"toggleList": "Mostrar u ocultar la lista", "toggleList": "Mostrar u ocultar la lista",
"openViews": "Abrir la lista de vistas", "openViews": "Abrir la lista de vistas",
"expandList": "Expandir la lista", "expandList": "Expandir la lista",

View file

@ -22,7 +22,7 @@
"cap2": "Préréglage", "cap2": "Préréglage",
"cap1": "Simple", "cap1": "Simple",
"stageExperimental": "Expérimental", "stageExperimental": "Expérimental",
"widget:rudder-voice/rudder-voice": "Rudder — voix vers texte", "widget:rudder-voice/rudder-voice": "Rudder · voix vers texte",
"toggleList": "Afficher ou masquer la liste", "toggleList": "Afficher ou masquer la liste",
"openViews": "Ouvrir la liste des vues", "openViews": "Ouvrir la liste des vues",
"expandList": "Déplier la liste", "expandList": "Déplier la liste",

View file

@ -22,7 +22,7 @@
"cap2": "प्रीसेट", "cap2": "प्रीसेट",
"cap1": "एकल", "cap1": "एकल",
"stageExperimental": "प्रायोगिक", "stageExperimental": "प्रायोगिक",
"widget:rudder-voice/rudder-voice": "Rudder — आवाज़ से टेक्स्ट", "widget:rudder-voice/rudder-voice": "Rudder · आवाज़ से टेक्स्ट",
"toggleList": "सूची दिखाएँ या छिपाएँ", "toggleList": "सूची दिखाएँ या छिपाएँ",
"openViews": "दृश्यों की सूची खोलें", "openViews": "दृश्यों की सूची खोलें",
"expandList": "सूची फैलाएँ", "expandList": "सूची फैलाएँ",

View file

@ -22,7 +22,7 @@
"cap2": "Preimpostato", "cap2": "Preimpostato",
"cap1": "Singolo", "cap1": "Singolo",
"stageExperimental": "Sperimentale", "stageExperimental": "Sperimentale",
"widget:rudder-voice/rudder-voice": "Rudder — voce in testo", "widget:rudder-voice/rudder-voice": "Rudder · voce in testo",
"toggleList": "Mostra o nascondi l’elenco", "toggleList": "Mostra o nascondi l’elenco",
"openViews": "Apri l’elenco delle viste", "openViews": "Apri l’elenco delle viste",
"expandList": "Espandi l’elenco", "expandList": "Espandi l’elenco",

View file

@ -22,7 +22,7 @@
"cap2": "Predefinição", "cap2": "Predefinição",
"cap1": "Único", "cap1": "Único",
"stageExperimental": "Experimental", "stageExperimental": "Experimental",
"widget:rudder-voice/rudder-voice": "Rudder — voz para texto", "widget:rudder-voice/rudder-voice": "Rudder · voz para texto",
"toggleList": "Mostrar ou ocultar a lista", "toggleList": "Mostrar ou ocultar a lista",
"openViews": "Abrir a lista de visualizações", "openViews": "Abrir a lista de visualizações",
"expandList": "Expandir a lista", "expandList": "Expandir a lista",

View file

@ -22,7 +22,7 @@
"cap2": "Presetat", "cap2": "Presetat",
"cap1": "Simplu", "cap1": "Simplu",
"stageExperimental": "Experimental", "stageExperimental": "Experimental",
"widget:rudder-voice/rudder-voice": "Rudder — voce în text", "widget:rudder-voice/rudder-voice": "Rudder · voce în text",
"toggleList": "Afișează sau ascunde lista", "toggleList": "Afișează sau ascunde lista",
"openViews": "Deschide lista de vizualizări", "openViews": "Deschide lista de vizualizări",
"expandList": "Extinde lista", "expandList": "Extinde lista",

View file

@ -22,7 +22,7 @@
"cap2": "Предустановка", "cap2": "Предустановка",
"cap1": "Одиночный", "cap1": "Одиночный",
"stageExperimental": "Экспериментальные", "stageExperimental": "Экспериментальные",
"widget:rudder-voice/rudder-voice": "Rudder — голос в текст", "widget:rudder-voice/rudder-voice": "Rudder · голос в текст",
"toggleList": "Показать или скрыть список", "toggleList": "Показать или скрыть список",
"openViews": "Открыть список представлений", "openViews": "Открыть список представлений",
"expandList": "Развернуть список", "expandList": "Развернуть список",

View file

@ -22,7 +22,7 @@
"cap2": "预设", "cap2": "预设",
"cap1": "单一", "cap1": "单一",
"stageExperimental": "实验性", "stageExperimental": "实验性",
"widget:rudder-voice/rudder-voice": "Rudder — 语音转文字", "widget:rudder-voice/rudder-voice": "Rudder · 语音转文字",
"toggleList": "显示或隐藏列表", "toggleList": "显示或隐藏列表",
"openViews": "打开视图列表", "openViews": "打开视图列表",
"expandList": "展开列表", "expandList": "展开列表",

View file

@ -471,21 +471,32 @@ const WIDGET_CSS = `
[data-studio-widgets^="top"] [data-widget]{flex-direction:column-reverse} [data-studio-widgets^="top"] [data-widget]{flex-direction:column-reverse}
[data-widget-panel]{display:none;position:relative;width:360px;max-width:calc(100vw - 32px);height:min(480px,calc(100vh - 110px));height:min(480px,calc(100dvh - 110px - env(safe-area-inset-top) - env(safe-area-inset-bottom)));border-radius:16px;overflow:hidden;background:#0b0f14;box-shadow:0 12px 40px rgba(0,0,0,.35)} [data-widget-panel]{display:none;position:relative;width:360px;max-width:calc(100vw - 32px);height:min(480px,calc(100vh - 110px));height:min(480px,calc(100dvh - 110px - env(safe-area-inset-top) - env(safe-area-inset-bottom)));border-radius:16px;overflow:hidden;background:#0b0f14;box-shadow:0 12px 40px rgba(0,0,0,.35)}
[data-widget-panel][data-open]{display:block} [data-widget-panel][data-open]{display:block}
[data-widget-bubble]{position:relative;width:56px;height:56px;padding:0;border:0;border-radius:50%;background:#0284c7;color:#fff;cursor:pointer;display:grid;place-items:center;box-shadow:0 4px 16px rgba(0,0,0,.3)} [data-widget-bubble]{position:relative;display:inline-flex;align-items:center;gap:8px;height:48px;max-width:min(360px,calc(100vw - 32px));padding:0 18px 0 14px;border:1px solid #334155;border-radius:999px;background:#0f172a;color:#e5e7eb;font:600 14px/1 system-ui,-apple-system,"Segoe UI",sans-serif;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.3)}
[data-widget-bubble]:focus-visible{outline:3px solid #7dd3fc;outline-offset:3px} [data-widget-bubble]:focus-visible{outline:3px solid #7dd3fc;outline-offset:3px}
[data-widget-bubble] svg{width:26px;height:26px;display:block} [data-wb-icon]{display:grid;place-items:center;flex:none;color:#38bdf8}
[data-widget-bubble][data-listening="true"]{background:#dc2626;animation:studio-widget-pulse 1.4s ease-out infinite} [data-wb-icon] svg,[data-wb-chev] svg{width:20px;height:20px;display:block}
[data-widget-bubble][data-listening="true"]::after{content:"";position:absolute;top:3px;right:3px;width:12px;height:12px;border-radius:50%;background:#fff;border:2px solid #dc2626} [data-wb-text]{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
@keyframes studio-widget-pulse{0%{box-shadow:0 0 0 0 rgba(220,38,38,.6)}70%{box-shadow:0 0 0 14px rgba(220,38,38,0)}100%{box-shadow:0 0 0 0 rgba(220,38,38,0)}} [data-wb-dot],[data-wb-chev]{display:none;flex:none}
@media (prefers-reduced-motion:reduce){[data-widget-bubble][data-listening="true"]{animation:none}} [data-wb-dot]{width:10px;height:10px;border-radius:50%;background:#ef4444}
[data-widget-bubble][aria-expanded="true"] [data-wb-chev]{display:block}
[data-widget-bubble][data-listening="true"]{border-color:#ef4444;background:#1f0f12;animation:studio-widget-pulse 1.4s ease-out infinite}
[data-widget-bubble][data-listening="true"] [data-wb-dot]{display:block}
[data-widget-bubble][data-listening="true"] [data-wb-icon]{color:#6b7280}
[data-widget-bubble][data-speaking="true"] [data-wb-icon]{color:#4ade80;animation:studio-widget-speak .5s ease-in-out infinite alternate}
@keyframes studio-widget-pulse{0%{box-shadow:0 0 0 0 rgba(239,68,68,.55)}70%{box-shadow:0 0 0 12px rgba(239,68,68,0)}100%{box-shadow:0 0 0 0 rgba(239,68,68,0)}}
@keyframes studio-widget-speak{from{transform:scale(1)}to{transform:scale(1.18)}}
@media (prefers-reduced-motion:reduce){[data-widget-bubble][data-listening="true"],[data-widget-bubble][data-speaking="true"] [data-wb-icon]{animation:none}}
@media (max-width:480px){ @media (max-width:480px){
[data-studio-widgets^="bottom"]{bottom:max(8px,env(safe-area-inset-bottom))} [data-studio-widgets^="bottom"]{bottom:max(8px,env(safe-area-inset-bottom))}
[data-studio-widgets$="right"]{right:max(8px,env(safe-area-inset-right))} [data-studio-widgets$="right"]{right:max(8px,env(safe-area-inset-right))}
[data-studio-widgets$="left"]{left:max(8px,env(safe-area-inset-left))} [data-studio-widgets$="left"]{left:max(8px,env(safe-area-inset-left))}
[data-widget-panel]{width:calc(100vw - 16px);max-width:none;height:60vh;height:60dvh} [data-widget-panel]{width:calc(100vw - 16px);max-width:none;height:60vh;height:60dvh}
[data-widget-bubble]{max-width:calc(100vw - 16px)}
}` }`
const WIDGET_ICON_OPEN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10v1a7 7 0 0 0 14 0v-1"/><path d="M12 18v4"/></svg>' const WIDGET_ICON_OPEN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10v1a7 7 0 0 0 14 0v-1"/><path d="M12 18v4"/></svg>'
const WIDGET_ICON_CLOSE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>' const WIDGET_ICON_CLOSE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>'
// The pill: [recording dot] [mic] [label, or the last words while recording] [chevron when open]
const WIDGET_PILL = '<span data-wb-dot aria-hidden="true"></span><span data-wb-icon>' + WIDGET_ICON_OPEN + '</span><span data-wb-text></span><span data-wb-chev>' + WIDGET_ICON_CLOSE + '</span>'
export default defineComponent({ export default defineComponent({
name: 'PluginStudio', name: 'PluginStudio',
@ -1131,7 +1142,8 @@ export default defineComponent({
btn.setAttribute('aria-label', wl !== wk ? wl : row.label) btn.setAttribute('aria-label', wl !== wk ? wl : row.label)
btn.title = btn.getAttribute('aria-label') btn.title = btn.getAttribute('aria-label')
btn.setAttribute('aria-expanded', 'false') btn.setAttribute('aria-expanded', 'false')
btn.innerHTML = WIDGET_ICON_OPEN btn.innerHTML = WIDGET_PILL
btn.querySelector('[data-wb-text]').textContent = btn.getAttribute('aria-label')
btn.addEventListener('click', () => this.openWidget(row, !panel.hasAttribute('data-open'))) btn.addEventListener('click', () => this.openWidget(row, !panel.hasAttribute('data-open')))
box.appendChild(panel); box.appendChild(btn); host.appendChild(box) box.appendChild(panel); box.appendChild(btn); host.appendChild(box)
} }
@ -1151,7 +1163,6 @@ export default defineComponent({
// transcript stays where it was. // transcript stays where it was.
if (open) panel.setAttribute('data-open', ''); else panel.removeAttribute('data-open') if (open) panel.setAttribute('data-open', ''); else panel.removeAttribute('data-open')
btn.setAttribute('aria-expanded', open ? 'true' : 'false') btn.setAttribute('aria-expanded', open ? 'true' : 'false')
btn.innerHTML = open ? WIDGET_ICON_CLOSE : WIDGET_ICON_OPEN
try { localStorage.setItem('plugin-studio:widget-open:' + row.key, open ? '1' : '0') } catch (_) {} try { localStorage.setItem('plugin-studio:widget-open:' + row.key, open ? '1' : '0') } catch (_) {}
const W = this.widgetStore() const W = this.widgetStore()
if (!open || W.els[row.key]) return if (!open || W.els[row.key]) return
@ -1165,10 +1176,21 @@ export default defineComponent({
el.style.cssText = 'display:block;position:absolute;inset:0;width:100%;height:100%;overflow-y:auto;overscroll-behavior:contain;' el.style.cssText = 'display:block;position:absolute;inset:0;width:100%;height:100%;overflow-y:auto;overscroll-behavior:contain;'
// LIVE MIC, VISIBLE WHEN COLLAPSED: a widget that emits `recording` (Rudder does) turns its bubble // LIVE MIC, VISIBLE WHEN COLLAPSED: a widget that emits `recording` (Rudder does) turns its bubble
// red, pulsing, with a dot while it listens, so closing the panel never hides that the mic is on. // red, pulsing, with a dot while it listens, so closing the panel never hides that the mic is on.
// While recording the pill drops its label for the last words heard, and its mic lights only while
// speech is detected (`activity`), so a collapsed Rudder still shows it is hearing the reader.
const pill = box.querySelector('[data-widget-bubble]')
const text = pill.querySelector('[data-wb-text]')
el.addEventListener('recording', (e) => { el.addEventListener('recording', (e) => {
const on = !!(Array.isArray(e.detail) ? e.detail[0] : e.detail) const on = !!(Array.isArray(e.detail) ? e.detail[0] : e.detail)
const b = box.querySelector('[data-widget-bubble]') pill.setAttribute('data-listening', on ? 'true' : 'false')
if (b) b.setAttribute('data-listening', on ? 'true' : 'false') if (!on) { pill.setAttribute('data-speaking', 'false'); text.textContent = pill.getAttribute('aria-label') }
else text.textContent = ''
})
el.addEventListener('activity', (e) => {
const a = (Array.isArray(e.detail) ? e.detail[0] : e.detail) || {}
if (pill.getAttribute('data-listening') !== 'true') return
pill.setAttribute('data-speaking', a.speaking ? 'true' : 'false')
text.textContent = a.words || ''
}) })
W.els[row.key] = el W.els[row.key] = el
panel.appendChild(el) panel.appendChild(el)

View file

@ -22,7 +22,7 @@ const props = defineProps({ profileConfig: { type: String, default: '' }, varian
const compact = computed(() => props.variant === 'widget') const compact = computed(() => props.variant === 'widget')
// `recording` (true/false) tells a host that Rudder started or stopped listening — the Plugin Studio's // `recording` (true/false) tells a host that Rudder started or stopped listening — the Plugin Studio's
// corner button turns red with it, so a collapsed corner still shows the mic is live. // corner button turns red with it, so a collapsed corner still shows the mic is live.
const emit = defineEmits(['recording']) const emit = defineEmits(['recording', 'activity'])
const cfg = computed(() => { const cfg = computed(() => {
try { return props.profileConfig ? JSON.parse(props.profileConfig) : {} } catch { return {} } try { return props.profileConfig ? JSON.parse(props.profileConfig) : {} } catch { return {} }
@ -545,6 +545,23 @@ onBeforeUnmount(() => {
watch(recording, (v) => emit('recording', v)) watch(recording, (v) => emit('recording', v))
// `activity` feeds a host's collapsed pill while recording: whether speech is heard right now (the level,
// held briefly so the icon doesn't flicker between words) and the last few words, so a reader with the
// panel closed can see they are being heard.
let speakingUntil = 0, activityTimer = null, lastActivity = ''
function sendActivity() {
activityTimer = null
const speaking = recording.value && Date.now() < speakingUntil
const tail = recording.value ? draft.value.trim().split(/\s+/).slice(-5).join(' ') : ''
const key = speaking + '|' + tail
if (key === lastActivity) return
lastActivity = key
emit('activity', { speaking, words: tail })
}
function queueActivity() { if (!activityTimer) activityTimer = setTimeout(sendActivity, 150) }
watch(level, (v) => { if (v > 0.12) speakingUntil = Date.now() + 400; queueActivity(); setTimeout(queueActivity, 450) })
watch([draft, recording], queueActivity)
const statusText = computed(() => ({ const statusText = computed(() => ({
connecting: 'Connecting…', connecting: 'Connecting…',
starting: 'Loading speech model… (about 20 s the first time)', starting: 'Loading speech model… (about 20 s the first time)',
@ -556,7 +573,7 @@ const statusText = computed(() => ({
</script> </script>
<template> <template>
<div class="rudder" :class="{ rec: recording, compact }"> <div class="rudder" :class="{ rec: recording, compact, more: compact && (showSettings || showInfo) }">
<header v-if="compact" class="bar"> <header v-if="compact" class="bar">
<b class="name">Rudder</b> <b class="name">Rudder</b>
<div class="status" :data-state="engine" role="status"> <div class="status" :data-state="engine" role="status">
@ -779,7 +796,10 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 1
.debug dt { color: var(--muted); } .debug dt { color: var(--muted); }
.debug dd { margin: 0; overflow-wrap: anywhere; } .debug dd { margin: 0; overflow-wrap: anywhere; }
/* The corner panel (variant="widget"): no page header, the draft fills the panel. */ /* The corner panel (variant="widget"): no page header, the draft fills the panel. */
.rudder.compact { min-height: 100%; padding: 10px 12px 12px; gap: 8px; font-size: 14px; } .rudder.compact { height: 100%; min-height: 0; overflow: hidden; padding: 10px 12px 12px; gap: 8px; font-size: 14px; }
/* Like a messaging app: the panel itself never scrolls; the draft does. With settings or info open, the
panel scrolls instead so they stay reachable. */
.rudder.compact.more { overflow-y: auto; }
.bar { display: flex; align-items: center; gap: 8px; } .bar { display: flex; align-items: center; gap: 8px; }
.bar .name { font-size: 14px; } .bar .name { font-size: 14px; }
.bar .status { padding: 3px 9px; font-size: 12px; gap: 6px; } .bar .status { padding: 3px 9px; font-size: 12px; gap: 6px; }
@ -788,15 +808,17 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 1
.icon-btn svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; } .icon-btn svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
.icon-btn[aria-expanded='true'], .icon-btn:hover { color: var(--accent); border-color: var(--accent); } .icon-btn[aria-expanded='true'], .icon-btn:hover { color: var(--accent); border-color: var(--accent); }
.compact .controls .mic { display: none; } .compact .controls .mic { display: none; }
.compact .compose { min-height: 140px; } .compact .compose { flex: 1 1 0; min-height: 120px; }
.compact .draft { min-height: 120px; resize: none; padding: 10px 12px; font-size: 15px; line-height: 1.5; } .compact.more .compose { flex: 0 0 auto; height: 200px; }
.compact .draft { flex: 1 1 0; min-height: 0; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
resize: none; padding: 10px 12px; font-size: 16px; line-height: 1.5; }
.compact .hearing { display: none; } .compact .hearing { display: none; }
.compact .bottom { order: 2; gap: 8px; } .compact .bottom { order: 2; gap: 8px; }
.compact .bottom button { padding: 7px 12px; } .compact .bottom button { padding: 7px 12px; }
.compact .bottom .mic.sm { padding: 7px 14px; font-size: 14px; gap: 6px; border-radius: 999px; } .compact .bottom .mic.sm { padding: 9px 18px; font-size: 15px; gap: 6px; border-radius: 999px; }
.compact .bottom .mic.sm:not(.on) { background: var(--accent); color: #04121c; border-color: var(--accent); } .compact .bottom .mic.sm:not(.on) { background: var(--accent); color: #04121c; border-color: var(--accent); }
.compact .bottom .mic.sm.on { background: var(--rec); color: #fff; border-color: var(--rec); } .compact .bottom .mic.sm.on { background: var(--rec); color: #fff; border-color: var(--rec); }
.compact .notice, .compact .hint { order: 3; font-size: 12.5px; padding: 8px 10px; } .compact .notice, .compact .hint { order: 0; font-size: 12.5px; padding: 8px 10px; }
.compact .debug { order: 4; } .compact .debug { order: 4; }
/* A phone-sized panel: less padding, and a shorter draft so it stays in view below the controls. */ /* A phone-sized panel: less padding, and a shorter draft so it stays in view below the controls. */
@media (max-width: 480px) { @media (max-width: 480px) {