Merge origin/i18n-spanish: whole-interface translation meets KartaView

This branch moved twice at once: upstream merged KartaView (#1121, a
third keyless street-level provider sharing a new lineCoverage.js), and
the remote i18n-spanish gained the whole-interface translation work
(1993 catalog entries, translateKnown, data-i18n-runtime, the unmarked-
string detector in npm run i18n) built on the Street View merge.

Integration keeps the remote's mechanisms and re-adds KartaView on top:

- presentMeta: remote's translateHint for coverage hints, plus the
  KartaView count line as its own literal tn pair (Russian plurals need
  the noun inside the template; keyed off coverageLabel.noun with the
  sequences pair as the default).
- uiState: upstream's highlightsSequence legend gate (KartaView's lines
  do not highlight) with t('Selected').
- layer-panels.html: remote's data-i18n/data-i18n-runtime hooks plus the
  sl-since-note wiring KartaView's upload-date SINCE note needs.
- kartaview viewer: the photo-navigation aria-label and img alt go
  through t(), as the remote's unmarked-string detector requires.
- catalogs: remote's 1987-entry base (its refined keys supersede this
  branch's earlier Street View wording) plus the KartaView entries:
  coverage hints, the upload-date note, the streets count line (with
  Russian one/few/many/other forms) and the viewer strings.

npm run i18n: complete, no unmarked strings. npm test: 6520/6522 (the
one failure is the pre-existing environmental OPENAI_API_KEY leak in
devFreshDotenv). Build passes.
This commit is contained in:
Meir Michanie committed 2026-10-14 17:45:57 +02:00
commit 32b11e27de
176 files changed
+9713 -2210

No files matched your search

+96 -29
View File
@@ -12,7 +12,9 @@
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 6px;
box-shadow: 0 18px 52px rgba(0, 0, 0, 0.48), inset 0 0 28px rgba(14, 78, 92, 0.12);
box-shadow:
0 18px 52px rgba(0, 0, 0, 0.48),
inset 0 0 28px rgba(14, 78, 92, 0.12);
backdrop-filter: blur(24px) saturate(1.4);
font-family: var(--font-mono);
letter-spacing: 0;
@@ -153,33 +155,69 @@
}
@keyframes bhote-embedded-anchor-enter {
from { opacity: 0; transform: scale(0.2); }
to { opacity: 1; transform: scale(1); }
from {
opacity: 0;
transform: scale(0.2);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes bhote-embedded-leader-enter {
from { opacity: 0; stroke-dashoffset: 1; }
to { opacity: 1; stroke-dashoffset: 0; }
from {
opacity: 0;
stroke-dashoffset: 1;
}
to {
opacity: 1;
stroke-dashoffset: 0;
}
}
@keyframes bhote-embedded-body-enter {
from { opacity: 0; transform: translateY(8px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
from {
opacity: 0;
transform: translateY(8px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes bhote-embedded-body-exit {
from { opacity: 1; transform: translateY(0) scale(1); }
to { opacity: 0; transform: translateY(6px) scale(0.985); }
from {
opacity: 1;
transform: translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateY(6px) scale(0.985);
}
}
@keyframes bhote-embedded-leader-exit {
from { opacity: 1; stroke-dashoffset: 0; }
to { opacity: 0; stroke-dashoffset: 1; }
from {
opacity: 1;
stroke-dashoffset: 0;
}
to {
opacity: 0;
stroke-dashoffset: 1;
}
}
@keyframes bhote-embedded-anchor-exit {
from { opacity: 1; transform: scale(1); }
to { opacity: 0; transform: scale(0.2); }
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.2);
}
}
@media (prefers-reduced-motion: reduce) {
@@ -243,16 +281,22 @@
border-top: 3px solid #6be8ff;
}
/* The title takes the full card width and up to two lines, so a translated
title keeps its place name; the link sits beside the status below it. */
.bhote-embedded-callout footer strong {
grid-column: 1 / -1;
display: -webkit-box;
overflow: hidden;
font-size: 11px;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
overflow-wrap: anywhere;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.bhote-embedded-callout footer small {
grid-column: 1;
align-self: center;
color: rgba(151, 211, 224, 0.7);
font-size: 8px;
line-height: 1.35;
@@ -260,8 +304,9 @@
.bhote-embedded-callout footer a {
grid-column: 2;
grid-row: 1 / span 2;
grid-row: 2;
align-self: center;
white-space: nowrap;
color: #6be8ff;
font-size: 8px;
text-decoration: none;
@@ -354,6 +399,12 @@
color: rgba(222, 240, 245, 0.88);
}
/* A longer translated heading wraps; the readout beside it stays one piece. */
html:not([lang='en']) .bhote-event-section-head span:last-child {
flex: none;
white-space: nowrap;
}
.bhote-event-range {
width: 100%;
height: 4px;
@@ -372,6 +423,11 @@
color: rgba(174, 208, 216, 0.62);
}
/* Keep "16 OCT 2021" together so the year never wraps alone. */
.bhote-event-date {
white-space: nowrap;
}
.bhote-event-story-nav {
display: grid;
grid-template-columns: 34px minmax(0, 1fr) 34px;
@@ -388,7 +444,11 @@
border-radius: 4px;
color: var(--accent);
background: rgba(7, 25, 35, 0.9);
font: 22px/1 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace;
font:
22px/1 'JetBrains Mono',
'SFMono-Regular',
Consolas,
monospace;
cursor: pointer;
}
@@ -398,7 +458,7 @@
align-content: center;
column-gap: 9px;
min-width: 0;
padding: 7px 10px;
padding: 5px 10px;
border: 1px solid rgba(88, 196, 218, 0.18);
background: rgba(6, 19, 27, 0.72);
}
@@ -410,22 +470,28 @@
line-height: 1.25;
}
/* Up to two lines: a translated beat title keeps its subject. Two title lines
and one meta line still fit the 54px arrow buttons, so the controls stay put. */
.bhote-event-story-current strong {
display: -webkit-box;
overflow: hidden;
color: var(--text-primary);
font-size: 11px;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
overflow-wrap: break-word;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
/* Phase / time / sources run long in translation: allow a second line. */
.bhote-event-story-current small {
display: -webkit-box;
grid-column: 1 / -1;
margin-top: 4px;
margin-top: 3px;
overflow: hidden;
color: rgba(116, 216, 235, 0.7);
text-overflow: ellipsis;
white-space: nowrap;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.bhote-event-actions {
@@ -448,14 +514,15 @@
cursor: pointer;
}
/* One line in English; a longer translation wraps to a second line. */
.bhote-event-actions button {
min-width: 0;
min-height: 30px;
padding: 0 5px;
padding: 3px 5px;
overflow: hidden;
font-size: 8px;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.2;
overflow-wrap: break-word;
}
.bhote-event-actions button:disabled,
@@ -469,7 +536,7 @@
border-color: rgba(88, 196, 218, 0.36);
}
.bhote-event-actions button[data-action="cinematic"][aria-pressed="true"] {
.bhote-event-actions button[data-action='cinematic'][aria-pressed='true'] {
color: #071017;
border-color: var(--accent);
background: var(--accent);
@@ -593,7 +660,6 @@
max-height: 48vh;
overflow-y: auto;
}
}
* {
@@ -602,7 +668,8 @@
box-sizing: border-box;
}
html, body {
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
+41
View File
@@ -78,6 +78,14 @@ html[lang='es'] .cctv-controls {
flex-wrap: wrap;
}
/* A translated label longer than its button wraps inside the button instead
of running past the panel edge. English labels fit on one line, so the
English layout is unchanged. */
.cctv-controls .scene-btn {
white-space: normal;
overflow-wrap: break-word;
}
.cctv-calibration-block {
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 9px;
@@ -165,6 +173,9 @@ html[lang='es'] .cctv-controls {
padding: 6px 8px;
font-family: var(--font-sans);
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
outline: none;
}
@@ -373,3 +384,33 @@ html[lang='es'] .cctv-controls {
color: #a8f7ff;
text-shadow: 0 0 10px rgba(0, 212, 255, 0.18);
}
/* Longer translated labels and units: tighter calibration chips keep the
seven values to two rows so SAVE / RESET stay in view, and the camera
select uses the compact mono face. English keeps its original look. */
html:not([lang='en']) .cctv-cal-readout {
gap: 4px;
}
html:not([lang='en']) .cctv-cal-value {
flex: 1 1 auto;
white-space: nowrap;
letter-spacing: 0.3px;
padding: 4px;
}
html:not([lang='en']) #cctv-camera-select {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.04em;
}
/* The camera name gets its own row; PREV / NEXT share the row below it. */
html:not([lang='en']) .cctv-controls:has(#cctv-camera-select) {
flex-wrap: wrap;
}
html:not([lang='en']) #cctv-camera-select {
flex: 1 1 100%;
order: -1;
}
+24 -1
View File
@@ -569,6 +569,9 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed))
text-align: center;
vertical-align: -0.08em;
}
.cockpit-roll-token.is-separator {
min-width: 0.3em;
}
.cockpit-roll-token.is-rolling {
color: color-mix(in srgb, currentColor 92%, var(--cockpit-accent));
text-shadow: 0 0 10px
@@ -1499,6 +1502,12 @@ body.cockpit-mode #view-switcher button {
.cockpit-signal-header > div {
display: grid;
gap: 2px;
min-width: 0;
}
.cockpit-signal-kicker {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cockpit-signal-header strong {
color: #fff;
@@ -1526,6 +1535,11 @@ body.cockpit-mode #view-switcher button {
align-items: center;
gap: 3px;
}
/* A longer translated state label widens the cycle button; the other
controls keep their size instead of being squeezed. */
.cockpit-brief-actions > * {
flex-shrink: 0;
}
.cockpit-brief-actions button {
display: grid;
width: 23px;
@@ -1548,10 +1562,13 @@ body.cockpit-mode #view-switcher button {
font-size: 14px;
}
#cockpit-brief-auto {
width: 50px;
width: auto;
min-width: 50px;
padding: 0 4px;
color: rgba(244, 251, 255, 0.42);
font: 700 6px/1 var(--font-mono);
letter-spacing: 0.05em;
white-space: nowrap;
}
#cockpit-brief-auto[aria-pressed='true'] {
border-color: var(--cockpit-accent);
@@ -1857,14 +1874,20 @@ body.cockpit-mode #view-switcher button {
letter-spacing: 0.1em;
}
.cockpit-signal-footer > span {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cockpit-brief-tabs {
display: flex;
flex: 0 0 auto;
gap: 3px;
}
.cockpit-brief-tabs button {
white-space: nowrap;
}
.cockpit-brief-tabs button {
padding: 2px 4px;
border: 0;
+51
View File
@@ -696,3 +696,54 @@
rgba(232, 234, 237, 0.24)
);
}
/* Language switch under the Visual Presets title: small mono chips, the
active language in accent like the other dock toggles. */
#command-dock #control-panel .dock-locale-switch {
display: flex;
justify-content: flex-end;
gap: 0.2rem;
margin-top: 0.22rem;
}
#command-dock .dock-locale-btn {
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
min-width: 1.35rem;
height: 0.95rem;
margin: 0;
padding: 0 0.22rem;
color: var(--text-secondary);
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.18rem;
font-family: var(--font-mono);
font-size: 0.46rem;
font-weight: 500;
letter-spacing: 0.06rem;
line-height: 1;
cursor: pointer;
transition:
color var(--transition-fast),
border-color var(--transition-fast),
background var(--transition-fast);
}
#command-dock .dock-locale-btn:hover {
color: var(--text-primary);
border-color: var(--glass-border-hover);
background: rgba(255, 255, 255, 0.06);
}
#command-dock .dock-locale-btn:focus-visible {
outline: 1px solid var(--text-primary);
outline-offset: 1px;
}
#command-dock .dock-locale-btn.active {
color: var(--accent);
border-color: rgba(0, 212, 255, 0.55);
background: var(--accent-dim);
}
+6
View File
@@ -392,6 +392,12 @@
transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
/* The side wings sit at z-index 100 (controls.css, location.css), so their
open trays would cover the alert; lift the voice column while it shows. */
#command-dock > #gev-voice-control[data-status='error']:not(.error-dismissed) {
z-index: 101;
}
#command-dock
#gev-voice-control[data-status='error']:not(.error-dismissed)
.gev-voice-error-tray {
+42 -6
View File
@@ -221,6 +221,18 @@
white-space: nowrap;
}
/* A longer translated title in a collapsed rail launcher tightens its
tracking rather than pushing the disclosure button out of the card. */
html:not([lang='en'])
#right-context-rail
.panel-collapsible.collapsed
.panel-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: 1.5px;
}
.panel-divider {
flex: 1;
height: 1px;
@@ -228,6 +240,8 @@
}
.panel-collapse-btn {
/* A long translated title must not squeeze the toggle into a sliver. */
flex-shrink: 0;
width: 22px;
height: 22px;
border-radius: 6px;
@@ -770,6 +784,15 @@
white-space: nowrap;
}
/* One label column across the Display rows: --pp-label-chars is the longest
label of the active language (set by applicationShell), and the labels are
monospace, so every slider track starts at the same x whatever the
language. 0.125em is the 1px letter-spacing at 8px. */
.pp-slider-row > .pp-slider-mini-label {
flex: 0 0 auto;
min-width: calc(var(--pp-label-chars, 0) * (1ch + 0.125em));
}
.detection-slider-row {
min-width: 170px;
padding: 5px 8px 6px;
@@ -966,6 +989,11 @@
#draw-mode-row {
min-width: 184px;
}
/* Equal thirds with room around the label, so a long shape name never runs
edge to edge in its segment. */
#draw-mode-row .pp-mode-btn {
padding: 0 4px;
}
#draw-label-row {
flex-wrap: wrap;
row-gap: 6px;
@@ -987,8 +1015,11 @@
.pp-text-input:focus {
border-color: var(--accent);
}
/* Sized to its longest colour name so a translated one is never clipped;
74px keeps the English width. */
.draw-color-select {
flex: 0 0 74px;
flex: 0 0 auto;
min-width: 74px;
}
.draw-clear-btn {
flex: 0 0 auto;
@@ -1234,15 +1265,20 @@ body.ui-clean-view #scene-runtime.active {
);
}
.param-slider-row {
display: flex;
/* One label column for every parameter, as wide as the longest label in the
active language, so all slider tracks start and end at the same x. */
#param-sliders {
display: grid;
grid-template-columns: max-content minmax(0, 1fr) max-content;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.param-slider-row:last-child {
margin-bottom: 0;
.param-slider-row {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
align-items: center;
}
.param-label {
+3 -1
View File
@@ -1371,10 +1371,12 @@
padding: 2px 4px;
}
/* ALPR keeps its nearby count in the legend while a wider status chip is shown. */
/* ALPR keeps its nearby count in the legend while a wider status chip
(including ENABLING / DISABLING) is shown. */
.data-toggle-row[data-layer-id='alpr-cameras']
.data-toggle-right:has(
.data-toggle-btn:is(
.transitioning,
.feed-loading,
.feed-degraded,
.feed-stale,
+88 -5
View File
@@ -250,8 +250,12 @@
gap: 5px;
}
.radio-transport button {
min-width: 0;
min-height: 30px;
padding: 6px 3px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border: 1px solid rgba(65, 220, 231, 0.28);
border-radius: 5px;
color: #8feaf2;
@@ -610,10 +614,14 @@
gap: 8px;
}
/* The name takes the room the status column leaves; a long word wraps inside
it instead of pushing the toggle out of line. */
.data-toggle-left {
display: flex;
flex: 1 1 auto;
align-items: center;
gap: 8px;
min-width: 0;
}
.data-icon {
@@ -621,16 +629,37 @@
line-height: 1;
}
/* A fixed icon box, so every name (and the meta line under it, 26px in)
starts at the same x whatever the glyph's width. */
.data-toggle-left .data-icon {
flex: none;
width: 18px;
text-align: center;
}
.data-name {
min-width: 0;
font-family: var(--font-sans);
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
letter-spacing: 0.3px;
overflow-wrap: anywhere;
}
/* Translated layer names are longer: break between words, and hyphenate a
word that still does not fit instead of splitting it anywhere. */
html:not([lang='en']) .data-name {
overflow-wrap: break-word;
hyphens: auto;
/* No two-letter pieces such as "es-paciales": a word hyphenates only when
at least four letters stay on each side. */
hyphenate-limit-chars: 8 4 4;
}
.data-toggle-right {
display: flex;
flex: 0 1 auto;
align-items: center;
gap: 10px;
}
@@ -640,8 +669,56 @@
font-size: 11px;
letter-spacing: 0.5px;
color: var(--text-secondary);
min-width: 30px;
text-align: right;
text-wrap: balance;
}
/* Row text pieces that must not break inside: a hyphenated code (ADS-B) and
the space before a " · " separator (see setRowText in layerPanel.js). */
.data-keep {
white-space: nowrap;
}
/* A count phrase ("24 nearby · 2 on screen") takes its own line under the
name instead of squeezing it beside the toggle. */
.data-toggle-row--count-line .data-toggle-top {
flex-wrap: wrap;
row-gap: 2px;
}
.data-toggle-row--count-line .data-toggle-right {
display: contents;
}
/* A zero basis, so a long name wraps beside the toggle instead of pushing the
toggle onto the count's line. */
.data-toggle-row--count-line .data-toggle-left {
flex-basis: 0;
}
.data-toggle-row--count-line .data-count {
order: 1;
flex: 1 0 100%;
padding-left: 26px;
font-size: 10px;
text-align: left;
}
/* The "—" placeholder gives way to a wide status chip, so the name keeps the
room it needs. */
.data-toggle-right:has(
.data-toggle-btn:is(
.transitioning,
.feed-loading,
.feed-degraded,
.feed-stale,
.feed-partial,
.feed-fallback,
.feed-unavailable
)
)
.data-count-empty {
display: none;
}
.data-toggle-btn {
@@ -662,8 +739,10 @@
min-width var(--transition-fast),
letter-spacing var(--transition-fast);
outline: none;
flex: none;
min-width: 38px;
text-align: center;
white-space: nowrap;
}
/* An inset ring stays visible inside the scrolling layer list. */
@@ -672,13 +751,15 @@
outline-offset: -3px;
}
/* Status chips up to eight characters share one width; a longer word grows
its own chip rather than widening every chip and squeezing every name. */
.data-toggle-btn.feed-loading,
.data-toggle-btn.feed-degraded,
.data-toggle-btn.feed-stale,
.data-toggle-btn.feed-partial,
.data-toggle-btn.feed-fallback,
.data-toggle-btn.feed-unavailable {
min-width: 82px;
min-width: 72px;
letter-spacing: 0.8px;
}
@@ -883,14 +964,16 @@
.data-row-list-lead {
min-width: 34px;
flex: none;
white-space: nowrap;
opacity: 0.85;
}
/* The whole instruction wraps under itself rather than losing its tail:
"Turn right onto …" says nothing, and Russian or Spanish run longer. */
.data-row-list-text {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
overflow-wrap: anywhere;
}
/* A list line that states a limit rather than offering an action. */
+40 -6
View File
@@ -293,17 +293,34 @@
height: 18px;
font-weight: 700;
}
/* Outside A / B there is one chip (SHOW): it takes the whole row, so a long
translation is not cut to a stub. */
.recent-imagery-readout:not([data-mode='ab'])
.ri-card-chips
.ri-chip[data-slot='a'] {
grid-column: 1 / -1;
}
.recent-imagery-readout:not([data-mode='ab'])
.ri-card-chips
.ri-chip[data-slot='b'] {
display: none;
}
/* 4. The selection bar: mode, then two rows that are always there. */
/* 4. The selection bar: mode, then two rows that are always there. The slot
rows share the bar's columns (subgrid), so the tag column fits the longest
tag in either row and both rows stay aligned. */
.recent-imagery-readout .ri-selection {
display: grid;
grid-template-columns: minmax(44px, max-content) minmax(0, 1fr) 22px;
grid-template-rows: 20px 20px 20px;
column-gap: 6px;
row-gap: 4px;
padding: 5px;
border: 1px solid var(--weather-card-border);
border-radius: 6px;
}
.recent-imagery-readout .ri-mode {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 4px;
@@ -312,14 +329,16 @@
height: 20px;
}
.recent-imagery-readout .ri-slot {
grid-column: 1 / -1;
display: grid;
grid-template-columns: 44px minmax(0, 1fr) 22px;
grid-template-columns: subgrid;
align-items: center;
column-gap: 6px;
height: 20px;
}
.recent-imagery-readout .ri-slot-tag {
height: 16px;
padding: 0 3px;
line-height: 16px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 3px;
@@ -347,6 +366,15 @@
color: var(--text-secondary);
font-style: italic;
}
/* A translated slot line (date · source · resolution · state) is longer:
let it take a second line rather than cut off the state at the end. */
html:not([lang='en']) .recent-imagery-readout .ri-slot {
height: auto;
min-height: 20px;
}
html:not([lang='en']) .recent-imagery-readout .ri-slot-value {
white-space: normal;
}
.recent-imagery-readout .ri-unpin {
width: 22px;
padding: 0;
@@ -355,19 +383,25 @@
}
/* 5. Opacity (both images), SWAP and export, one row. SWAP keeps its slot
while no swipe is live. */
while no swipe is live. A label longer than its 76px column widens it to
the label's longest word, and the value wraps under the label. */
.recent-imagery-readout .ri-controls {
display: grid;
grid-template-columns: 76px minmax(0, 1fr) 40px 128px;
grid-template-columns:
minmax(76px, min-content) minmax(0, 1fr) minmax(40px, max-content)
128px;
align-items: center;
column-gap: 6px;
height: 22px;
}
.recent-imagery-readout .ri-opacity-label {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 4px;
align-content: center;
column-gap: 4px;
font-size: 8px;
line-height: 10px;
letter-spacing: 1px;
white-space: nowrap;
}
@@ -385,7 +419,7 @@
opacity: 0.35;
}
.recent-imagery-readout .ri-swap {
width: 40px;
min-width: 40px;
padding: 0 2px;
}
.recent-imagery-readout .ri-exports .rail-card-actions {
+23
View File
@@ -160,9 +160,20 @@
padding: 6px 8px;
font-family: var(--font-sans);
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
outline: none;
}
/* A translated action longer than its button wraps inside the button instead
of running past it. English labels fit on one line, so the English layout
is unchanged. */
#scene-panel .scene-controls .scene-btn {
white-space: normal;
overflow-wrap: break-word;
}
.scene-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.14);
@@ -454,3 +465,15 @@
.director-sharing-dialog::backdrop {
background: rgba(0, 8, 14, 0.65);
}
html:not([lang='en']) #scene-select {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.04em;
}
/* A translated scene name needs the width: the select takes its own row and
NEW / DELETE share the next one. */
html:not([lang='en']) #scene-select {
flex: 1 1 100%;
}
+14
View File
@@ -213,6 +213,20 @@ button.sl-status:focus-visible {
display: none;
}
/* Longer translations: rather than squeeze the hint into a narrow column
beside the button, put the button on its own line when both do not fit. */
html:not([lang='en']) .sl-meta-row {
flex-wrap: wrap;
}
html:not([lang='en']) .sl-meta-row .sl-meta {
flex: 1 1 14em;
}
html:not([lang='en']) .sl-meta-row .sl-nearest-btn {
flex: 1 0 auto;
}
/* ── Errors ──────────────────────────────────────────────────────────────── */
.sl-error {
font-family: var(--font-sans);
+60
View File
@@ -82,3 +82,63 @@
.gev-voice-cost-value[hidden] {
display: none;
}
/* The dock heading row is only as wide as the voice column, so the cost
cluster keeps its size and the state label gives way: a long translated
state (ПОДКЛЮЧЕНИЕ, CONECTANDO) ends in an ellipsis instead of pushing the
cost past the card edge. The kicker already collapses to zero width here;
taking it out of the row returns its gap to the state label. The row also
reaches left over the gutter beside the mic ring: at heading height the
ring has already curved away. English fits as it is, so this applies to
the other languages only. */
html:not([lang='en']) #command-dock .gev-voice-heading {
gap: 0.2rem;
margin-left: -0.6rem;
}
html:not([lang='en']) #command-dock .gev-voice-heading > .gev-voice-kicker {
display: none;
}
html:not([lang='en']) #command-dock .gev-voice-heading > #gev-voice-status {
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
letter-spacing: 0;
text-overflow: ellipsis;
white-space: nowrap;
}
html:not([lang='en']) #command-dock .gev-voice-cost {
gap: 0.16rem;
padding-left: 0.1rem;
}
html:not([lang='en']) #command-dock .gev-voice-tier-btn {
min-width: 0;
padding: 0 0.2rem;
}
html:not([lang='en']) #command-dock .gev-voice-cost-value {
letter-spacing: 0;
}
/* Live hints ("Ask or command", "Requesting microphone") run longer in other
languages; give them a second line from the spare space under the readout
rather than cutting them mid-word. */
html:not([lang='en'])
#command-dock
#gev-voice-control:is(
[data-status='connecting'],
[data-status='listening'],
[data-status='executing']
)
#gev-voice-detail {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
line-height: 1.15;
white-space: normal;
overflow-wrap: anywhere;
}
+14 -3
View File
@@ -147,9 +147,10 @@
.rail-card-line {
white-space: pre-line;
}
/* One line stays reserved so the card does not jump when a status appears;
a long (or translated) status wraps instead of being cut. */
.weather-card [data-line-id='status'] {
height: 1.45em;
overflow-y: auto;
min-height: 1.45em;
color: var(--accent);
}
.rail-card-ramp {
@@ -258,6 +259,7 @@ body.recording-mode .gev-wind-inspection-marker {
top: 8px;
left: 0;
transform: translateX(-50%);
white-space: nowrap;
font: 9px/1.45 var(--font-mono);
color: var(--text-dim);
}
@@ -311,9 +313,18 @@ body.recording-mode .gev-wind-inspection-marker {
.rail-card-compact.status {
color: var(--accent);
}
/* One label column per card, as wide as its longest label: chips line up
across rows, and a long translated label never runs under its chips. */
.rail-card-settings {
display: grid;
grid-template-columns: minmax(56px, max-content) minmax(0, 1fr);
column-gap: 6px;
}
.rail-card-setting {
display: grid;
grid-template-columns: 56px minmax(0, 1fr);
grid-column: 1 / -1;
grid-template-columns: minmax(56px, max-content) minmax(0, 1fr);
grid-template-columns: subgrid;
align-items: baseline;
gap: 6px;
margin-top: 6px;