diff --git a/public/locales/en.json b/public/locales/en.json
index b5919e14..8c681b38 100644
--- a/public/locales/en.json
+++ b/public/locales/en.json
@@ -533,6 +533,7 @@
"incoming": "Incoming call…",
"incoming_label": "Incoming call",
"accept": "Accept",
+ "answer": "Answer",
"decline": "Decline",
"unknown_caller": "Unknown caller",
"ctl_mic": "Mic",
diff --git a/public/locales/ru.json b/public/locales/ru.json
index e13a925d..399b7960 100644
--- a/public/locales/ru.json
+++ b/public/locales/ru.json
@@ -537,6 +537,7 @@
"incoming": "Входящий звонок…",
"incoming_label": "Входящий звонок",
"accept": "Принять",
+ "answer": "Ответить",
"decline": "Отклонить",
"unknown_caller": "Неизвестный абонент",
"ctl_mic": "Микрофон",
diff --git a/src/app/features/call-status/CallActionButton.tsx b/src/app/features/call-status/CallActionButton.tsx
index 00aa659c..73561a31 100644
--- a/src/app/features/call-status/CallActionButton.tsx
+++ b/src/app/features/call-status/CallActionButton.tsx
@@ -57,7 +57,10 @@ export function CallActionButton({
aria-hidden
>
{busy ? (
-
+ // Match the disc tone: a destructive (red) disc shows a Critical spinner,
+ // everything else a neutral Secondary — a grey-purple spinner on the red
+ // hangup disc read as a foreign element, esp. in light theme.
+
) : (
)}
diff --git a/src/app/features/call-status/CallStatus.tsx b/src/app/features/call-status/CallStatus.tsx
index 50051d80..b0a7101e 100644
--- a/src/app/features/call-status/CallStatus.tsx
+++ b/src/app/features/call-status/CallStatus.tsx
@@ -96,6 +96,15 @@ export function CallStatus({ callEmbed }: CallStatusProps) {
: t('Call.in_call_count', { count: memberCount });
}
+ // Screen-reader phase word — the same lifecycle WITHOUT the per-second timer, so
+ // AT announces «Connecting → Calling → In call» transitions instead of reading
+ // out every tick of the visible counter.
+ let phaseLabel: string;
+ if (!callJoined) phaseLabel = t('Call.connecting');
+ else if (!everConnected) phaseLabel = t('Call.calling');
+ else if (isOneOnOne) phaseLabel = t('Call.in_call');
+ else phaseLabel = t('Call.in_call_count', { count: memberCount });
+
const native = isNativePlatform();
const avatar = (
@@ -131,9 +140,12 @@ export function CallStatus({ callEmbed }: CallStatusProps) {
-
+
{subText}
+
+ {phaseLabel}
+
);
diff --git a/src/app/features/call-status/IncomingCallStrip.tsx b/src/app/features/call-status/IncomingCallStrip.tsx
index 17a59bde..ddd2f3f8 100644
--- a/src/app/features/call-status/IncomingCallStrip.tsx
+++ b/src/app/features/call-status/IncomingCallStrip.tsx
@@ -109,7 +109,7 @@ export function IncomingCallStrip({ call, room }: IncomingCallStripProps) {
const buttons = (
+
{callEmbed ? (
) : (
diff --git a/src/app/features/call-status/styles.css.ts b/src/app/features/call-status/styles.css.ts
index 97eda777..1c495efe 100644
--- a/src/app/features/call-status/styles.css.ts
+++ b/src/app/features/call-status/styles.css.ts
@@ -102,12 +102,16 @@ export const CallAvatarCompact = style({
flexShrink: 0,
});
-// Pulsing ring on the incoming-call avatar — a soft teal halo that expands and
-// fades, the «ringing right now» cue. Matches the rail's orbit accent.
+// Pulsing ring on the incoming-call avatar — a soft halo that expands and fades,
+// the «ringing right now» cue. Tuned to Success.Main #7dd3a8 (rgb 125,211,168) so
+// the «live / answer» green is the SAME hue across every call surface (LiveDot,
+// native ring glow + chevrons, answer disc). NB: this is deliberately NOT the
+// brand orbit-accent teal #5be3c5 (SyncIndicator / HorseshoeContainer) — that one
+// signals sync, this one signals an answerable call.
const callerPulse = keyframes({
- '0%': { boxShadow: '0 0 0 0 rgba(91, 227, 197, 0.45)' },
- '70%': { boxShadow: `0 0 0 ${toRem(14)} rgba(91, 227, 197, 0)` },
- '100%': { boxShadow: '0 0 0 0 rgba(91, 227, 197, 0)' },
+ '0%': { boxShadow: '0 0 0 0 rgba(125, 211, 168, 0.45)' },
+ '70%': { boxShadow: `0 0 0 ${toRem(14)} rgba(125, 211, 168, 0)` },
+ '100%': { boxShadow: '0 0 0 0 rgba(125, 211, 168, 0)' },
});
export const CallerPulse = style({
@@ -281,3 +285,63 @@ export const RingButtonGrow = style({
flexGrow: 1,
flexBasis: 0,
});
+
+// Press feedback for the big Answer/Decline buttons (folds Button has none of its
+// own) so they feel as tactile as the control discs (which scale on :active).
+export const RingButtonPress = style({
+ transition: 'transform 90ms ease',
+ selectors: {
+ '&:active:not(:disabled)': { transform: 'scale(0.97)' },
+ },
+ '@media': {
+ '(prefers-reduced-motion: reduce)': { transition: 'none' },
+ },
+});
+
+// === Over-lock screen entrance ===
+// The over-lock root is the privacy cover and must stay opaque/static from frame
+// one; only its CONTENTS animate in so the screen doesn't hard-cut over the
+// lockscreen. CSS-only (plays once on portal mount, never replays on the 1s timer
+// re-render), reduced-motion aware.
+const overlockEnter = keyframes({
+ from: { opacity: 0, transform: 'translateY(10px) scale(0.985)' },
+ to: { opacity: 1, transform: 'none' },
+});
+
+export const OverLockReveal = style({
+ animation: `${overlockEnter} 260ms cubic-bezier(0.22, 1, 0.36, 1) both`,
+ '@media': {
+ '(prefers-reduced-motion: reduce)': { animation: 'none' },
+ },
+});
+
+// Controls trail the identity in by a beat (staggered arrival reads as "settling").
+export const OverLockRevealDelayed = style([OverLockReveal, { animationDelay: '70ms' }]);
+
+// Minimize chevron: a faint resting disc so the affordance is discoverable (was a
+// fully-transparent icon), plus press feedback.
+export const OverLockIconButton = style({
+ backgroundColor: 'rgba(255, 255, 255, 0.08)',
+ transition: 'background-color 140ms ease, transform 90ms ease',
+ selectors: {
+ '&:hover': { backgroundColor: 'rgba(255, 255, 255, 0.14)' },
+ '&:active': { transform: 'scale(0.94)' },
+ },
+ '@media': {
+ '(prefers-reduced-motion: reduce)': { transition: 'background-color 140ms ease' },
+ },
+});
+
+// Screen-reader-only: announce call-phase transitions (Connecting → Calling → In
+// call) without speaking the per-second ticking timer. Standard visually-hidden.
+export const SrOnly = style({
+ position: 'absolute',
+ width: 1,
+ height: 1,
+ padding: 0,
+ margin: -1,
+ overflow: 'hidden',
+ clip: 'rect(0, 0, 0, 0)',
+ whiteSpace: 'nowrap',
+ border: 0,
+});