TalkToDoc / static /css /style.css
goctests0's picture
Upload style.css
86469e7 verified
Raw
History Blame Contribute Delete
38.6 kB
/*
TalkToDoc design system.
Palette: "Olive Garden Feast" (user-supplied Coolors palette):
#606C38 (olive), #283618 (dark forest green), #FEFAE0 (cream),
#DDA15E (tan/gold), #BC6C25 (rust).
Patient view uses the olive as its primary color, provider view uses a
darkened variant of the rust, one color family per role, same approach
as the previous palette. Raw #BC6C25 and raw #DDA15E both fail WCAG
contrast as text on light backgrounds, so darkened variants are used
anywhere either color needs to be read as text rather than a fill.
System fonts only, no external font download, this matters for the
low-bandwidth rural settings this system is built for.
*/
:root {
/* Base */
--color-bg: #FEFAE0;
--color-surface: #FFFFFF;
--color-border: #E8DFC0;
/* Text */
--color-text: #283618;
--color-text-secondary: #5C6650;
--color-text-on-primary: #FFFFFF;
--color-text-on-provider-accent: #FFFFFF;
/* Primary (shared, patient role) */
--color-primary: #606C38;
--color-primary-dark: #283618;
--color-primary-light: #E8ECDC;
/* Accent */
--color-accent: #DDA15E;
/* Raw #DDA15E fails WCAG contrast as text (2.14:1), this darkened
variant clears AA at 5.33:1, used anywhere it needs to be text. */
--color-accent-text: #8A5F26;
/* Provider-only accent */
/* Raw #BC6C25 gives only 3.95:1 with white text, not enough for
normal-size text, this darkened variant clears AA at 5.25:1. */
--color-provider-accent: #A15A1F;
--color-provider-accent-light: #FBEADB;
--color-provider-accent-text: #7A431A;
/* Semantic */
--color-success: #2E7D32;
--color-error: #B3261E;
--color-error-light: #FBEAE9;
/* Spacing scale */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 24px;
--space-6: 32px;
--space-7: 48px;
--space-8: 64px;
/* Type scale */
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.5rem;
--text-2xl: 2rem;
--radius: 20px;
--radius-sm: 14px;
--radius-lg: 28px;
/* Soft elevation, replaces hard 1px borders on cards and controls */
--shadow-sm: 0 1px 2px rgba(40, 54, 24, 0.05), 0 2px 6px rgba(40, 54, 24, 0.04);
--shadow-md: 0 6px 20px rgba(40, 54, 24, 0.08), 0 2px 6px rgba(40, 54, 24, 0.05);
--shadow-lg: 0 16px 40px rgba(40, 54, 24, 0.14);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: var(--color-bg);
color: var(--color-text);
font-size: var(--text-base);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
h1, h2, h3 {
margin: 0 0 var(--space-2) 0;
font-weight: 700;
line-height: 1.25;
letter-spacing: -0.02em;
}
h1 { font-size: var(--text-2xl); }
h2 { font-size: var(--text-xl); }
h3 { font-size: var(--text-lg); letter-spacing: -0.01em; }
p {
margin: 0 0 var(--space-4) 0;
color: var(--color-text-secondary);
}
.app-shell {
max-width: 480px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
background: var(--color-bg);
}
.app-header {
padding: var(--space-5) var(--space-4) var(--space-6);
background: linear-gradient(155deg, var(--color-primary) 0%, var(--color-primary-dark) 130%);
color: var(--color-text-on-primary);
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-3);
}
.header-icon-btn {
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, 0.18);
color: inherit;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
flex-shrink: 0;
transition: background 150ms ease, transform 100ms ease;
}
.header-icon-btn:hover {
background: rgba(255, 255, 255, 0.28);
}
.header-icon-btn:active {
transform: scale(0.94);
}
.header-icon-btn svg {
width: 20px;
height: 20px;
}
.app-header.provider {
background: linear-gradient(155deg, var(--color-provider-accent) 0%, var(--color-provider-accent-text) 160%);
color: var(--color-text-on-provider-accent);
}
.brand {
display: flex;
align-items: center;
gap: var(--space-2);
}
.brand-mark {
width: 28px;
height: 28px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.18);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.brand-mark svg {
width: 16px;
height: 16px;
}
.app-header h1 {
font-size: var(--text-lg);
margin: 0;
}
.app-header .role-tag {
display: inline-block;
margin-top: var(--space-1);
font-size: var(--text-sm);
opacity: 0.85;
}
.app-main {
flex: 1;
padding: var(--space-5) var(--space-4) var(--space-7);
}
.card {
background: var(--color-surface);
border-radius: var(--radius);
padding: var(--space-5);
margin-bottom: var(--space-4);
box-shadow: var(--shadow-sm);
}
.step {
display: none;
}
.step.active {
display: block;
animation: step-in 220ms ease;
}
@keyframes step-in {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
.step.active {
animation: none;
}
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
width: 100%;
padding: var(--space-3) var(--space-4);
font-size: var(--text-base);
font-weight: 600;
border-radius: var(--radius-sm);
border: none;
cursor: pointer;
transition: background-color 120ms ease, box-shadow 120ms ease, transform 120ms ease;
}
.btn:active {
transform: scale(0.98);
}
.btn:focus-visible {
outline: 2px solid var(--color-primary-dark);
outline-offset: 2px;
}
.btn-primary {
background: var(--color-primary);
color: var(--color-text-on-primary);
box-shadow: 0 4px 14px rgba(96, 108, 56, 0.32);
}
.btn-primary:hover { background: var(--color-primary-dark); }
.btn-primary:disabled {
background: var(--color-border);
color: var(--color-text-secondary);
box-shadow: none;
cursor: not-allowed;
}
.btn-secondary {
background: var(--color-primary-light);
color: var(--color-primary-dark);
}
.btn-secondary:hover { background: #DAE1CD; }
.btn-provider {
background: var(--color-provider-accent);
color: var(--color-text-on-provider-accent);
box-shadow: 0 4px 14px rgba(161, 90, 31, 0.32);
}
.btn-provider:hover { background: var(--color-provider-accent-text); }
.btn-row {
display: flex;
gap: var(--space-3);
margin-top: var(--space-4);
}
/* Language grid */
.language-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-3);
margin-top: var(--space-4);
}
.language-option {
padding: var(--space-4);
text-align: center;
font-weight: 600;
border: none;
border-radius: var(--radius-sm);
background: var(--color-surface);
box-shadow: var(--shadow-sm);
cursor: pointer;
transition: box-shadow 120ms ease, transform 120ms ease, background-color 120ms ease;
}
.language-option:hover {
box-shadow: var(--shadow-md);
transform: translateY(-1px);
}
.language-option:active {
transform: scale(0.96);
}
.language-option[aria-pressed="true"] {
background: var(--color-primary);
color: var(--color-text-on-primary);
box-shadow: 0 4px 14px rgba(96, 108, 56, 0.32);
animation: select-pop 260ms ease;
}
@keyframes select-pop {
0% { transform: scale(1); }
40% { transform: scale(1.06); }
100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
.language-option[aria-pressed="true"] { animation: none; }
}
/* Inputs */
label {
display: block;
font-weight: 600;
font-size: var(--text-sm);
margin-bottom: var(--space-2);
}
textarea, input[type="text"] {
width: 100%;
padding: var(--space-3) var(--space-4);
font-size: var(--text-base);
font-family: inherit;
border: 1.5px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-surface);
color: var(--color-text);
resize: vertical;
transition: border-color 120ms ease, box-shadow 120ms ease;
}
textarea:focus, input[type="text"]:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 4px var(--color-primary-light);
}
.helper-text {
font-size: var(--text-sm);
color: var(--color-text-secondary);
margin-top: var(--space-2);
}
.divider {
display: flex;
align-items: center;
gap: var(--space-3);
margin: var(--space-4) 0;
color: var(--color-text-secondary);
font-size: var(--text-sm);
}
.divider::before, .divider::after {
content: "";
flex: 1;
height: 1px;
background: var(--color-border);
}
/* Recording indicator */
.record-btn {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
}
.record-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--color-error);
display: none;
}
.record-btn.recording .record-dot {
display: inline-block;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
/* Status / results */
.status-banner {
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-sm);
font-size: var(--text-sm);
margin-bottom: var(--space-4);
}
.status-banner.error {
background: var(--color-error-light);
color: var(--color-error);
}
.status-banner.info {
background: var(--color-primary-light);
color: var(--color-primary-dark);
}
.result-block {
margin-bottom: var(--space-4);
}
.search-input {
width: 100%;
padding: var(--space-3) var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--radius);
font-size: var(--text-base);
font-family: inherit;
color: var(--color-text);
background: var(--color-surface);
}
.search-input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px var(--color-primary-light);
}
.history-row {
cursor: pointer;
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: var(--space-3) var(--space-4);
margin-bottom: var(--space-3);
transition: box-shadow 150ms ease, border-color 150ms ease, transform 100ms ease;
}
.history-row:hover {
border-color: var(--color-primary);
box-shadow: var(--shadow-sm);
}
.history-row:active {
transform: scale(0.99);
}
.history-row:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.history-row .timestamp {
font-size: var(--text-sm);
color: var(--color-text-secondary);
font-weight: 400;
float: right;
}
/* Provider queue tabs (Waiting / Active / Completed) */
.queue-tabs {
display: flex;
gap: var(--space-2);
margin-bottom: var(--space-4);
}
.queue-tab {
flex: 1;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
color: var(--color-text-secondary);
font-size: var(--text-sm);
font-weight: 600;
cursor: pointer;
transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
}
.queue-tab:hover {
border-color: var(--color-provider-accent);
}
.queue-tab.active {
background: var(--color-provider-accent);
border-color: var(--color-provider-accent);
color: var(--color-text-on-provider-accent);
}
.queue-tab .tab-count {
opacity: 0.8;
margin-left: var(--space-1);
}
/* Workspace: reference/status line above the patient's message */
.workspace-meta {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-2);
margin-bottom: var(--space-2);
}
/* Original-language transcript, collapsed by default under the English
translation so the provider isn't required to read a language they
may not speak, but can always verify against the source. */
.original-transcript {
margin-top: var(--space-3);
font-size: var(--text-sm);
}
.original-transcript summary {
cursor: pointer;
color: var(--color-text-secondary);
font-weight: 600;
}
.original-transcript summary:hover {
color: var(--color-provider-accent-text);
}
.original-transcript p {
margin: var(--space-2) 0 0;
color: var(--color-text);
background: var(--color-bg);
border-radius: var(--radius-sm);
padding: var(--space-3);
}
/* Read-only rows for a patient's other consultations, shown for context
in the workspace. Deliberately not clickable - see the comment next to
#patient-history-list in provider.html for why. */
.history-row-static {
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: var(--space-3) var(--space-4);
margin-bottom: var(--space-3);
}
.history-row-static .timestamp {
float: none;
margin-left: auto;
font-weight: 400;
}
/* Response composer preview: a real translation + real generated speech,
so it's visually set apart as "this is what actually gets sent",
distinct from the (still-editable, not-yet-committed) reply above it. */
.preview-block {
margin-top: var(--space-4);
padding: var(--space-4);
border: 1px dashed var(--color-provider-accent);
border-radius: var(--radius);
background: var(--color-provider-accent-light);
}
.preview-label {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-provider-accent-text);
margin-bottom: var(--space-3);
}
.preview-block .chat-bubble.from-provider {
margin-bottom: var(--space-3);
}
/* Keep the shared audio player on-brand in the provider context (rust
accent) instead of the patient page's green. */
.provider .audio-btn {
background: var(--color-provider-accent);
color: var(--color-text-on-provider-accent);
}
.provider .audio-btn:hover {
background: var(--color-provider-accent-text);
}
.provider .audio-btn-sm {
background: var(--color-provider-accent-light);
color: var(--color-provider-accent-text);
}
.provider .audio-seek {
accent-color: var(--color-provider-accent);
}
.result-label {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-text-secondary);
margin-bottom: var(--space-1);
}
.result-text {
font-size: var(--text-base);
color: var(--color-text);
}
audio {
width: 100%;
margin-top: var(--space-3);
}
.interaction-id {
font-family: ui-monospace, monospace;
background: var(--color-primary-light);
color: var(--color-primary-dark);
padding: 2px 8px;
border-radius: 4px;
font-size: var(--text-sm);
}
.provider .interaction-id {
background: var(--color-provider-accent-light);
color: var(--color-provider-accent-text);
}
.nlu-summary {
background: var(--color-provider-accent-light);
border-left: 3px solid var(--color-provider-accent-text);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-sm);
font-size: var(--text-base);
margin-bottom: 0;
}
.nav-link {
display: block;
text-align: center;
margin-top: var(--space-5);
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
.nav-link a {
color: var(--color-primary);
font-weight: 600;
text-decoration: none;
}
.nav-link.provider a {
color: var(--color-provider-accent-text);
}
/* ============================================================
Hero section: greeting + ambient presence orb
============================================================ */
.hero {
text-align: center;
padding: var(--space-4) 0 var(--space-6);
}
.hero-orb-wrap {
position: relative;
width: 132px;
height: 132px;
margin: 0 auto var(--space-5);
display: flex;
align-items: center;
justify-content: center;
}
.hero-orb-glow {
position: absolute;
inset: -20px;
border-radius: 50%;
background: radial-gradient(circle, var(--color-primary) 0%, rgba(96, 108, 56, 0) 70%);
opacity: 0.35;
animation: breathe 3400ms ease-in-out infinite;
}
.provider .hero-orb-glow {
background: radial-gradient(circle, var(--color-provider-accent) 0%, rgba(161, 90, 31, 0) 70%);
}
.hero-orb {
position: relative;
width: 96px;
height: 96px;
border-radius: 50%;
background: linear-gradient(155deg, #7C8A47 0%, var(--color-primary) 45%, var(--color-primary-dark) 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12px 32px rgba(40, 54, 24, 0.32), inset 0 1px 1px rgba(255, 255, 255, 0.25);
}
.provider .hero-orb {
background: linear-gradient(155deg, #C97D3A 0%, var(--color-provider-accent) 45%, var(--color-provider-accent-text) 100%);
box-shadow: 0 12px 32px rgba(122, 67, 26, 0.32), inset 0 1px 1px rgba(255, 255, 255, 0.25);
}
.hero-orb svg {
width: 36px;
height: 36px;
color: var(--color-bg);
}
@keyframes breathe {
0%, 100% { transform: scale(1); opacity: 0.35; }
50% { transform: scale(1.12); opacity: 0.55; }
}
@media (prefers-reduced-motion: reduce) {
.hero-orb-glow { animation: none; }
}
.hero h2 {
margin-bottom: var(--space-1);
}
.hero p {
max-width: 320px;
margin-left: auto;
margin-right: auto;
}
/* ============================================================
Unified input bar (textarea + inline mic button)
============================================================ */
.input-bar {
position: relative;
}
.input-bar textarea {
padding-right: 52px;
}
.mic-inline-btn {
position: absolute;
right: 8px;
bottom: 8px;
width: 38px;
height: 38px;
border-radius: 50%;
border: none;
background: var(--color-primary);
color: var(--color-text-on-primary);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
overflow: visible;
box-shadow: 0 4px 12px rgba(96, 108, 56, 0.35);
transition: background-color 120ms ease, transform 120ms ease;
}
.mic-inline-btn:hover {
background: var(--color-primary-dark);
}
.mic-inline-btn.recording {
background: var(--color-error);
animation: pulse-ring 1400ms ease-in-out infinite;
}
@keyframes pulse-ring {
0%, 100% { box-shadow: 0 0 0 0 rgba(179, 38, 30, 0.35); }
50% { box-shadow: 0 0 0 8px rgba(179, 38, 30, 0); }
}
.mic-inline-btn svg {
width: 18px;
height: 18px;
}
/* ============================================================
Listening state: replaces the input bar while recording
============================================================ */
.listening-panel {
text-align: center;
padding: var(--space-6) 0 var(--space-4);
}
.listening-orb-wrap {
position: relative;
width: 128px;
height: 128px;
margin: 0 auto var(--space-4);
display: flex;
align-items: center;
justify-content: center;
}
.listening-ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px solid var(--color-primary);
opacity: 0;
animation: ripple 2200ms ease-out infinite;
}
.listening-ring:nth-child(2) { animation-delay: 700ms; }
.listening-ring:nth-child(3) { animation-delay: 1400ms; }
@keyframes ripple {
0% { transform: scale(0.55); opacity: 0.5; }
100% { transform: scale(1); opacity: 0; }
}
.listening-core {
position: relative;
width: 72px;
height: 72px;
border-radius: 50%;
background: var(--color-primary);
color: var(--color-text-on-primary);
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.listening-core svg {
width: 28px;
height: 28px;
}
@media (prefers-reduced-motion: reduce) {
.listening-ring { animation: none; opacity: 0.25; transform: scale(0.8); }
}
.listening-caption {
font-weight: 600;
margin-bottom: var(--space-1);
}
/* ============================================================
Quick symptom chips
============================================================ */
.chip-row {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
margin: var(--space-3) 0 var(--space-4);
}
.chip {
padding: 8px 16px;
border-radius: 999px;
border: none;
background: var(--color-surface);
color: var(--color-text);
font-size: var(--text-sm);
font-weight: 600;
box-shadow: var(--shadow-sm);
cursor: pointer;
transition: box-shadow 120ms ease, transform 120ms ease, background-color 120ms ease;
}
.chip:hover {
box-shadow: var(--shadow-md);
background: var(--color-primary-light);
transform: translateY(-1px);
}
.chip:active {
transform: scale(0.95);
}
/* ============================================================
Chat bubbles, used for the conversation view and history
============================================================ */
.chat-thread {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.chat-bubble {
max-width: 85%;
padding: var(--space-3) var(--space-4);
border-radius: var(--radius);
font-size: var(--text-base);
line-height: 1.45;
}
.chat-bubble .chat-label {
display: block;
font-size: var(--text-sm);
font-weight: 600;
margin-bottom: 2px;
opacity: 0.75;
}
.chat-bubble.from-patient {
align-self: flex-end;
background: var(--color-primary);
color: var(--color-text-on-primary);
border-bottom-right-radius: 4px;
}
.chat-bubble.from-provider {
align-self: flex-start;
background: var(--color-surface);
box-shadow: var(--shadow-sm);
color: var(--color-text);
border-bottom-left-radius: 4px;
}
.provider .chat-bubble.from-provider {
background: var(--color-provider-accent-light);
box-shadow: none;
}
/* ============================================================
Provider queue cards
============================================================ */
.queue-card {
display: flex;
align-items: flex-start;
gap: var(--space-3);
width: 100%;
text-align: left;
padding: var(--space-3) var(--space-4);
margin-bottom: var(--space-3);
background: var(--color-surface);
border: none;
border-radius: var(--radius-sm);
box-shadow: var(--shadow-sm);
cursor: pointer;
transition: box-shadow 120ms ease, transform 120ms ease;
}
.queue-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-1px);
}
.queue-card:active {
transform: scale(0.98);
}
.queue-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--color-provider-accent-light);
color: var(--color-provider-accent-text);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
flex-shrink: 0;
}
.queue-body {
flex: 1;
min-width: 0;
}
.queue-top-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-2);
margin-bottom: 2px;
}
.queue-top-right {
display: flex;
align-items: center;
gap: var(--space-2);
flex-shrink: 0;
}
.queue-lang {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-text-secondary);
text-transform: capitalize;
}
.queue-preview {
color: var(--color-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ============================================================
Micro-animations: list entrance, mic ripple, skeleton loading
============================================================ */
.list-item-in {
animation: list-item-in 260ms ease both;
}
@keyframes list-item-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
.list-item-in { animation: none; }
}
/* One-shot ripple on the inline mic button when tapped */
.mic-inline-btn::after {
content: "";
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px solid var(--color-primary);
opacity: 0;
}
.mic-inline-btn.tapped::after {
animation: mic-ripple 500ms ease-out;
}
@keyframes mic-ripple {
0% { opacity: 0.6; transform: scale(1); }
100% { opacity: 0; transform: scale(1.9); }
}
@media (prefers-reduced-motion: reduce) {
.mic-inline-btn.tapped::after { animation: none; }
}
/* Skeleton loading shimmer, shown briefly while the queue or history
is being fetched, instead of a blank gap */
.skeleton-row {
height: 64px;
border-radius: var(--radius-sm);
margin-bottom: var(--space-3);
background: linear-gradient(100deg, var(--color-border) 30%, #F3EDD3 50%, var(--color-border) 70%);
background-size: 200% 100%;
animation: skeleton-shimmer 1300ms ease-in-out infinite;
}
@keyframes skeleton-shimmer {
from { background-position: 140% 0; }
to { background-position: -40% 0; }
}
@media (prefers-reduced-motion: reduce) {
.skeleton-row { animation: none; opacity: 0.6; }
}
/* Queue heading count, smooth transition when the number changes */
.queue-heading-text {
display: inline-block;
animation: count-change 220ms ease;
}
@keyframes count-change {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
/* Relative timestamps on queue cards and history entries */
.timestamp {
font-size: var(--text-sm);
color: var(--color-text-secondary);
white-space: nowrap;
flex-shrink: 0;
}
/* Offline banner */
.offline-banner {
display: none;
align-items: center;
gap: var(--space-2);
background: var(--color-error-light);
color: var(--color-error);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-sm);
font-size: var(--text-sm);
font-weight: 600;
margin-bottom: var(--space-4);
animation: step-in 220ms ease;
}
.offline-banner.visible {
display: flex;
}
.offline-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-error);
flex-shrink: 0;
animation: pulse 1.4s ease-in-out infinite;
}
/* ============================================================
Web / desktop environment (min-width: 900px)
Mobile layout above this point is untouched.
============================================================ */
@media (min-width: 900px) {
body {
background:
radial-gradient(640px circle at 12% 8%, rgba(96, 108, 56, 0.10), transparent 60%),
radial-gradient(520px circle at 92% 85%, rgba(221, 161, 94, 0.16), transparent 60%),
var(--color-bg);
min-height: 100vh;
padding: var(--space-7) var(--space-5);
}
.app-shell {
max-width: 640px;
margin: 0 auto;
min-height: auto;
border-radius: 20px;
box-shadow: 0 30px 70px rgba(40, 54, 24, 0.16);
overflow: hidden;
}
/* The provider shell is wider, to make room for the two-column layout */
.app-shell.provider-shell {
max-width: 1080px;
}
.provider-page .app-main {
padding-bottom: var(--space-6);
}
.provider-layout {
display: grid;
grid-template-columns: 380px 1fr;
gap: var(--space-5);
align-items: start;
}
/* On desktop both panes stay visible, mobile's one-at-a-time step
system is only used below this breakpoint. */
.provider-layout .step {
display: block !important;
animation: none !important;
}
.provider-layout [data-step="list"] .hero {
text-align: left;
padding: 0 0 var(--space-4);
}
.provider-layout [data-step="list"] .hero-orb-wrap {
margin: 0 0 var(--space-3);
width: 64px;
height: 64px;
}
.provider-layout [data-step="list"] .hero-orb {
width: 56px;
height: 56px;
}
.provider-layout [data-step="list"] .hero-orb svg {
width: 24px;
height: 24px;
}
.provider-layout [data-step="list"] .hero-orb-glow {
inset: -10px;
}
.provider-layout [data-step="respond"] #back-to-list {
display: none;
}
.provider-layout [data-step="respond"] .btn-row {
justify-content: flex-end;
}
.provider-layout [data-step="respond"] .btn-provider {
width: auto;
padding-left: var(--space-6);
padding-right: var(--space-6);
}
.detail-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: var(--space-3);
min-height: 320px;
border: 1px dashed var(--color-border);
border-radius: var(--radius);
padding: var(--space-6);
color: var(--color-text-secondary);
}
.detail-placeholder-mark {
position: static;
width: 44px;
height: 44px;
background: var(--color-provider-accent-light);
}
.detail-placeholder-mark svg {
width: 20px;
height: 20px;
color: var(--color-provider-accent-text);
}
/* Patient shell: a composed, contained card on a filled background,
rather than a phone screen stretched across a wide window. */
.patient-shell .hero {
padding-top: var(--space-3);
}
}
/* Icons inside buttons and labels */
.icon {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.icon-sm {
width: 14px;
height: 14px;
}
/* Loading spinner, shown inside buttons while an AI step is running */
.spinner {
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.4);
border-top-color: currentColor;
animation: spin 700ms linear infinite;
flex-shrink: 0;
}
.btn-secondary .spinner,
.btn[disabled] .spinner {
border: 2px solid rgba(0, 0, 0, 0.15);
border-top-color: currentColor;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
.spinner { animation-duration: 1400ms; }
}
/* Recording timer */
.record-timer {
font-variant-numeric: tabular-nums;
font-weight: 600;
}
/* Status badges, used in the provider queue and patient history */
.status-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: var(--text-sm);
font-weight: 600;
padding: 2px 10px;
border-radius: 999px;
}
.status-badge.waiting {
background: var(--color-provider-accent-light);
color: var(--color-provider-accent-text);
}
.status-badge.answered {
background: #E3EDE3;
color: var(--color-success);
}
/* Waiting-state status timeline: 3 honest steps (submitted / waiting /
ready). Only "submitted" and "waiting" are ever shown as reached,
because that's genuinely all two states the backend tracks; the app
never fabricates an in-between "provider is reviewing" stage. */
.status-timeline {
display: flex;
align-items: flex-start;
margin: var(--space-4) 0 var(--space-5);
}
.timeline-step {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
min-width: 0;
text-align: center;
}
.timeline-dot {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--color-surface);
border: 2px solid var(--color-border);
margin-bottom: var(--space-2);
flex-shrink: 0;
}
.timeline-step.done .timeline-dot {
background: var(--color-primary);
border-color: var(--color-primary);
}
.timeline-step.current .timeline-dot {
border-color: var(--color-primary);
box-shadow: 0 0 0 4px var(--color-primary-light);
animation: timeline-pulse 1.8s ease-in-out infinite;
}
@keyframes timeline-pulse {
0%, 100% { box-shadow: 0 0 0 4px var(--color-primary-light); }
50% { box-shadow: 0 0 0 7px var(--color-primary-light); }
}
@media (prefers-reduced-motion: reduce) {
.timeline-step.current .timeline-dot {
animation: none;
}
}
.timeline-label {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-text-secondary);
line-height: 1.3;
}
.timeline-step.done .timeline-label,
.timeline-step.current .timeline-label {
color: var(--color-text);
}
.timeline-connector {
flex: 0 0 auto;
width: 32px;
height: 2px;
background: var(--color-border);
margin: 7px -4px 0;
}
/* General, clearly-non-personalized safety guidance shown while waiting.
Uses the primary-light tint rather than the error/warning colors, since
it's reassurance and a standing safety note, not an alert. */
.waiting-note {
background: var(--color-primary-light);
border-left: 3px solid var(--color-primary);
border-radius: var(--radius-sm);
padding: var(--space-3) var(--space-4);
font-size: var(--text-sm);
color: var(--color-text);
margin-bottom: var(--space-4);
}
.waiting-note strong {
color: var(--color-primary-dark);
}
/* Custom audio player, replaces the bare native <audio controls> element
so playback matches the rest of the design system instead of looking
like a raw, inconsistently-styled browser widget. */
.audio-player audio {
display: none;
}
.audio-player-controls {
display: flex;
align-items: center;
gap: var(--space-2);
}
.audio-btn {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 44px;
height: 44px;
border-radius: 50%;
border: none;
background: var(--color-primary);
color: var(--color-text-on-primary);
cursor: pointer;
transition: background 150ms ease, transform 100ms ease;
}
.audio-btn svg {
width: 20px;
height: 20px;
}
.audio-btn:hover {
background: var(--color-primary-dark);
}
.audio-btn:active {
transform: scale(0.94);
}
.audio-btn-sm {
width: 36px;
height: 36px;
background: var(--color-primary-light);
color: var(--color-primary-dark);
}
.audio-btn-sm:hover {
background: #DAE1CD;
}
.audio-time {
font-size: var(--text-sm);
font-variant-numeric: tabular-nums;
color: var(--color-text-secondary);
flex-shrink: 0;
min-width: 34px;
}
.audio-seek {
flex: 1;
min-width: 0;
height: 4px;
accent-color: var(--color-primary);
cursor: pointer;
}
/* Toast confirmation, appears briefly after an action completes */
.toast {
position: fixed;
left: 50%;
bottom: var(--space-6);
transform: translateX(-50%) translateY(12px);
background: var(--color-text);
color: var(--color-bg);
padding: var(--space-3) var(--space-5);
border-radius: var(--radius);
font-size: var(--text-sm);
font-weight: 600;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
opacity: 0;
pointer-events: none;
transition: opacity 200ms ease, transform 200ms ease;
z-index: 100;
}
.toast.visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ============================================================
Filter chips (12E), focus rings, disabled states,
skip link, sr-only utility, desktop Back-hide (12F)
============================================================ */
/* Filter chips — status/language filters above list and history views.
Distinct from .chip (symptom/reply quick-select) which uses pill shape
and sits inline in a card. These are tighter, sit in their own row,
and have an explicit active/selected state. */
.filter-chip-row {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
margin-bottom: var(--space-3);
}
.filter-chip {
padding: var(--space-1) var(--space-3);
border: 1.5px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
color: var(--color-text-secondary);
font-size: var(--text-sm);
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: background 130ms ease, border-color 130ms ease, color 130ms ease;
}
.filter-chip:hover {
border-color: var(--color-primary);
color: var(--color-text);
}
.filter-chip.active {
background: var(--color-primary-light);
border-color: var(--color-primary);
color: var(--color-primary-dark);
}
.filter-chip:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.provider .filter-chip:hover {
border-color: var(--color-provider-accent);
}
.provider .filter-chip.active {
background: var(--color-provider-accent-light);
border-color: var(--color-provider-accent);
color: var(--color-provider-accent-text);
}
.provider .filter-chip:focus-visible {
outline-color: var(--color-provider-accent);
}
/* Sort toggle button, shown on provider Completed tab only */
.sort-toggle-btn {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-1) var(--space-3);
border: 1.5px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
color: var(--color-text-secondary);
font-size: var(--text-sm);
font-weight: 600;
font-family: inherit;
cursor: pointer;
margin-top: var(--space-2);
transition: border-color 130ms ease, color 130ms ease;
}
.sort-toggle-btn:hover {
border-color: var(--color-provider-accent);
color: var(--color-provider-accent-text);
}
.sort-toggle-btn:focus-visible {
outline: 2px solid var(--color-provider-accent);
outline-offset: 2px;
}
.sort-toggle-btn svg {
width: 14px;
height: 14px;
flex-shrink: 0;
}
/* Queue tab and card focus rings */
.queue-tab:focus-visible {
outline: 2px solid var(--color-provider-accent);
outline-offset: 2px;
}
.queue-card:focus-visible {
outline: 2px solid var(--color-provider-accent);
outline-offset: 2px;
}
/* Audio button focus rings */
.audio-btn:focus-visible {
outline: 2px solid var(--color-primary-dark);
outline-offset: 2px;
}
.provider .audio-btn:focus-visible {
outline-color: var(--color-provider-accent-text);
}
/* Provider Send button disabled state, mirrors .btn-primary:disabled */
.btn-provider:disabled {
background: var(--color-border);
color: var(--color-text-secondary);
box-shadow: none;
cursor: not-allowed;
}
/* Spinner inside the provider Send button */
.btn-provider .spinner {
border: 2px solid rgba(255, 255, 255, 0.35);
border-top-color: #fff;
}
/* Empty state: shown when a list is genuinely empty or no results match
filters/search. Intentionally minimal — icon, short title, one line. */
.empty-state {
text-align: center;
padding: var(--space-7) var(--space-5);
}
.empty-state-icon {
width: 36px;
height: 36px;
stroke: var(--color-border);
margin: 0 auto var(--space-3);
display: block;
}
.empty-state-title {
font-size: var(--text-base);
font-weight: 600;
color: var(--color-text);
margin-bottom: var(--space-2);
}
.empty-state-body {
font-size: var(--text-sm);
color: var(--color-text-secondary);
margin: 0;
}
/* Desktop provider: hide Back buttons inside the respond pane.
On desktop both panes are always visible (two-column grid), so
Back belongs to mobile-only one-at-a-time navigation. */
@media (min-width: 900px) {
.provider-layout [data-step="respond"] #back-to-list,
.provider-layout [data-step="respond"] #back-to-list-readonly {
display: none;
}
}
/* Visually-hidden utility: screen-reader-only text, visible to AT
but not rendered on screen. */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Skip link: keyboard users can bypass the header and jump to main
content. Visually hidden until focused, rises to the top on Tab. */
.skip-link {
position: absolute;
top: -40px;
left: var(--space-4);
background: var(--color-text);
color: var(--color-bg);
font-size: var(--text-sm);
font-weight: 600;
padding: var(--space-2) var(--space-4);
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
text-decoration: none;
z-index: 200;
transition: top 150ms ease;
}
.skip-link:focus {
top: 0;
}