vojo/apps/widget-whatsapp/src/styles.css

1114 lines
30 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Dawn palette — must stay in sync with
* docs/design/new-direct-messages-design/project/stream-v2-dawn.jsx
* (DAWN const, lines 4-23). The widget renders inside the Vojo chat slot
* which is itself a Dawn surface; the iframe inherits the same visual
* canon to feel like a continuation of the host. */
:root {
--bg: #181a20;
--bg2: #0d0e11;
--surface: #21232b;
--surface2: #2a2d36;
--divider: rgba(255, 255, 255, 0.06);
--hairline: rgba(255, 255, 255, 0.08);
--text: #e6e6e9;
--muted: rgba(230, 230, 233, 0.55);
--faint: rgba(230, 230, 233, 0.32);
--fleet: #9580ff;
--fleet-soft: #a59cff;
--green: #7dd3a8;
--amber: #d4b88a;
--rose: #c08e7b;
--section-pad-x: 40px;
}
[data-theme='light'] {
/* Light theme is intentionally a thin remap. Vojo is dark-default; the
* theme param exists so we don't fight an explicit user/host setting,
* not because we expect daily light-mode use. */
--bg: #f5f5f7;
--bg2: #ffffff;
--surface: #f0f0f2;
--surface2: #e8e8ec;
--divider: rgba(0, 0, 0, 0.08);
--hairline: rgba(0, 0, 0, 0.1);
--text: #1a1a1d;
--muted: rgba(26, 26, 29, 0.62);
--faint: rgba(26, 26, 29, 0.4);
}
@media (max-width: 600px) {
:root {
--section-pad-x: 20px;
}
}
* {
box-sizing: border-box;
/* Kills the translucent grey overlay iOS/Android WebViews paint on top
* of any tapped element. On the wide refresh card this overlay was
* read as «button stuck on grey» — the underlying state was correct,
* the WebView's tap-highlight was not. Web browsers ignore this. */
-webkit-tap-highlight-color: transparent;
}
html,
body,
#app {
height: 100%;
}
body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--text);
font: 14px/1.45 -apple-system, 'Segoe UI', 'Inter', system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
.app {
display: flex;
flex-direction: column;
min-height: 100%;
max-width: 960px;
margin: 0 auto;
}
/* The hero (avatar + name + handle + description + three-dots menu) is
* OWNED BY THE HOST, not the widget — see src/app/features/bots/BotShell.tsx.
* Removing the widget-side hero collapses the duplicate header that used to
* sit between the host's BotShellHero (which the user actually sees) and
* the iframe content. The widget body now starts with the active-state
* section directly. */
/* ── Section ──────────────────────────────────────────────────────── */
.section {
padding: 24px var(--section-pad-x) 20px;
}
.section + .section {
padding-top: 4px;
}
/* Section label — same dark-bg pill vocabulary as `.section-status` so the
* two pieces in the section-header row read as a matched pair (label
* pill + status pill). The pill chrome wraps the existing uppercase
* letter-spaced typography; chip is non-interactive, no cursor. */
.section-label {
display: inline-flex;
align-items: center;
font-size: 13px;
line-height: 20px;
text-transform: uppercase;
letter-spacing: 1.4px;
font-weight: 600;
color: var(--muted);
background: var(--bg2);
border: 1px solid var(--divider);
border-radius: 8px;
padding: 8px 14px;
margin: 0 0 14px;
white-space: nowrap;
user-select: none;
}
/* Status pill — button-styled but intentionally non-interactive (no
* cursor:pointer, no hover). Replaces the section header for stateful
* sections (disconnected / connected / unknown / logging_out) — the
* pill itself carries the section's identity, so a separate
* `.section-label` would just duplicate the meaning. Same dark-bg
* vocabulary (--bg2 / divider border) as `.recovery-action` and the
* host hero's «О боте» chip. */
.section-status {
display: inline-flex;
align-items: center;
gap: 10px;
font-size: 13px;
line-height: 20px;
color: var(--muted);
background: var(--bg2);
border: 1px solid var(--divider);
border-radius: 8px;
padding: 8px 14px;
margin: 0 0 14px;
user-select: none;
white-space: nowrap;
}
.section-status .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--faint);
flex-shrink: 0;
}
.section-status.connected {
color: var(--green);
}
.section-status.connected .dot {
background: var(--green);
box-shadow: 0 0 0 3px rgba(125, 211, 168, 0.16);
}
.section-status.disconnected {
color: var(--rose);
}
.section-status.disconnected .dot {
background: var(--rose);
}
.section-status.checking {
color: var(--amber);
}
.section-status.checking .dot {
background: var(--amber);
}
/* Wraps the section-status pill + a labeled refresh action when the
* state has no other affordance (unknown / logging_out / connected
* without loginId). Without this row, the user can stare at a
* «Проверка статуса…» pill forever if the first list-logins reply
* dropped on the wire. */
.section-recovery-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 14px;
}
.section-recovery-row > .section-status {
margin-bottom: 0;
}
.recovery-action {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--bg2);
border: 1px solid var(--divider);
border-radius: 8px;
padding: 8px 14px;
font: inherit;
font-size: 13px;
line-height: 20px;
color: var(--muted);
cursor: pointer;
transition: background 0.12s, color 0.12s, border-color 0.12s;
}
:root[data-input='mouse'] .recovery-action:hover:not(:disabled) {
background: var(--surface);
color: var(--text);
border-color: var(--hairline);
}
.recovery-action:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.recovery-action svg {
width: 16px;
height: 16px;
}
/* ── Command card (action card with name + desc + chevron) ──────── */
.command-card {
/* The widget runs in an iframe, so it does NOT inherit the host's
* `button { -webkit-appearance: button }` rule (src/index.css:112). The
* browser default for <button> on WebKit is `auto`, which on iOS/Android
* Capacitor WebView resolves to native button rendering — the WebView
* draws its own focus/active overlay ON TOP of our explicit background.
* That overlay was the «button greys out and doesn't snap back» bug:
* after tap, the WebView holds the native focus paint until focus moves
* elsewhere. Setting appearance:none strips the native paint and makes
* our CSS the sole source of truth, matching what the host does for
* inputs (src/index.css:122-124). On the OLD 70px icon-only refresh
* chip the native overlay had nowhere to render visibly; on a wide
* command-card it was very visible. Web browsers ignore appearance for
* <button> already, so this only matters on native WebViews. */
-webkit-appearance: none;
appearance: none;
background: var(--bg2);
border: 1px solid var(--divider);
border-radius: 10px;
padding: 14px 16px;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
text-align: left;
font: inherit;
color: inherit;
transition: border-color 0.12s, background 0.12s;
}
/* Hover scoped to mouse-mode sessions only. Capacitor Android WebView
* reports `(hover: hover)` as TRUE on a pure-touch device (verified
* via on-device console.log), so a media-query gate doesn't work — the
* rule would apply, then the WebView would synthesise `:hover` on the
* focused element after tap and leave it stuck until the user tapped
* elsewhere (visible symptom: card greys after tap, only un-greys on
* tapping a different button). `[data-input]` is set in main.tsx from
* the actual `pointerdown.pointerType`, which the WebView reports
* truthfully even when its media queries lie. */
:root[data-input='mouse'] .command-card:hover:not(:disabled) {
background: var(--surface);
border-color: var(--hairline);
}
.command-card:focus {
outline: none;
}
/* Keyboard focus ring — same data-input gate. On touch sessions there's
* no keyboard navigation to support and the ring would also stick (focus
* stays on the tapped button until something else takes it). */
:root[data-input='mouse'] .command-card:focus-visible {
outline: 2px solid var(--fleet);
outline-offset: 2px;
}
.command-card:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.command-card-body {
flex: 1;
min-width: 0;
}
.command-card-name {
/* Sans-serif + on-surface text color — the previous monospace + fleet-soft
* styling read like a `/login` CLI label. With «Войти в Telegram» as the
* actual name (no slash, no command-line mimicry), the row should look
* like a primary action title, not a code token. */
font-size: 15px;
color: var(--text);
font-weight: 600;
margin-bottom: 3px;
}
.command-card-desc {
font-size: 14px;
color: var(--muted);
line-height: 19px;
}
.command-card-chevron {
color: var(--muted);
font-size: 18px;
flex-shrink: 0;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* When the chevron slot carries an icon (e.g. refresh card uses
* `<RefreshIcon />` instead of ``), size the SVG explicitly — the icon
* has no intrinsic width and would expand to 300×150 (SVG default) inside
* a flex-shrink:0 container. 18px keeps it visually equivalent to the
* `` glyph used by the other cards. */
.command-card-chevron svg {
width: 18px;
height: 18px;
display: block;
}
/* Spin the leading refresh icon while the card is in its `refreshing`
* in-flight state. Combined with `disabled` (which dims the card to
* opacity 0.5 and gates :hover via :not(:disabled)), the spinner is
* the unambiguous «I'm working» signal — no more guessing whether the
* click registered. The selector targets the leading icon slot since
* the refresh card moved its glyph from the chevron (right) to the
* lead slot (left) for parity with every other card. */
.command-card.refreshing .command-card-lead-icon svg {
animation: command-card-spin 0.8s linear infinite;
}
@keyframes command-card-spin {
to {
transform: rotate(360deg);
}
}
/* ── Transcript ──────────────────────────────────────────────────── */
.transcript {
background: var(--bg2);
border: 1px solid var(--divider);
border-radius: 10px;
padding: 12px 14px;
font-family: ui-monospace, 'JetBrains Mono', 'SF Mono', monospace;
font-size: 12.5px;
line-height: 1.55;
max-height: 360px;
overflow-y: auto;
/* Custom scrollbar styled into the dark palette. Native browser
* scrollbars (gray, system-themed) clash with the Dawn surface. */
scrollbar-width: thin;
scrollbar-color: var(--surface2) transparent;
}
.transcript::-webkit-scrollbar {
width: 8px;
}
.transcript::-webkit-scrollbar-track {
background: transparent;
}
.transcript::-webkit-scrollbar-thumb {
background: var(--surface2);
border-radius: 4px;
border: 2px solid var(--bg2);
background-clip: padding-box;
}
.transcript::-webkit-scrollbar-thumb:hover {
background: var(--surface);
border: 2px solid var(--bg2);
background-clip: padding-box;
}
.transcript-line {
padding: 4px 0;
display: flex;
gap: 10px;
align-items: flex-start;
white-space: pre-wrap;
word-break: break-word;
}
.transcript-line + .transcript-line {
border-top: 1px dashed var(--divider);
}
.transcript-line .ts {
color: var(--faint);
flex-shrink: 0;
font-variant-numeric: tabular-nums;
}
.transcript-line .body {
flex: 1;
min-width: 0;
}
.transcript-line.from-bot .body {
color: var(--text);
}
.transcript-line.from-user .body {
color: var(--fleet-soft);
}
.transcript-line.diag .body {
color: var(--muted);
}
.transcript-line.error .body {
color: var(--rose);
}
.transcript-empty {
color: var(--faint);
text-align: center;
padding: 16px 0;
font-style: italic;
}
/* Destructive card — keeps the red name to mark «Выйти из WhatsApp» as a
* destructive action, distinguishing it from the primary login card.
* Hover border stays on the generic input-gated rule (hairline) so the
* accent is consistent across touch and mouse modes. A previous
* `.command-card.danger:hover { border-color: var(--rose) }` override
* was dead in mouse mode (lower specificity than the input-gated rule)
* and only fired in the pre-first-pointerdown touch stub. */
.command-card.danger .command-card-name {
color: var(--rose);
}
/* Generic leading-icon slot — every command-card carries one as a
* left-side semantic glyph (mirror of the right-side chevron). The
* SVG picks up `currentColor` so it tints with the card's modifier
* — muted by default, amber inside `.command-card.warn`, rose-ish
* inheritance left intentionally OFF for `.danger` (only the title
* goes rose; the lead icon stays muted to keep one accent per card). */
.command-card-lead-icon {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--muted);
}
.command-card-lead-icon svg {
width: 20px;
height: 20px;
display: block;
}
/* Warn variant — WhatsApp-only, applied to the AboutCard so the amber
* border, amber-tinted name, and amber lead icon together signal that
* the modal behind this card carries a Meta-ToS risk disclosure (in
* addition to the «how it works» copy). The hybrid card description
* («о работе и рисках») depends on this visual cue to feel honest. */
.command-card.warn {
background: rgba(212, 184, 138, 0.06);
border-color: var(--amber);
}
.command-card.warn .command-card-name {
color: var(--amber);
}
.command-card.warn .command-card-lead-icon {
color: var(--amber);
}
:root[data-input='mouse'] .command-card.warn:hover:not(:disabled) {
background: rgba(212, 184, 138, 0.12);
border-color: var(--amber);
}
/* Inline confirm-in-place body for the destructive logout card. The button
* group lives inside the same card frame — no modal, no layout shift. */
.command-card-confirm {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
flex: 1;
min-width: 0;
}
.command-card-confirm-prompt {
font-size: 14px;
color: var(--text);
flex: 1;
min-width: 0;
}
.command-card-confirm-yes,
.command-card-confirm-no,
.btn-primary,
.btn-text {
font: inherit;
cursor: pointer;
}
.command-card-confirm-yes {
background: var(--rose);
color: #0c0c0e;
border: none;
border-radius: 7px;
padding: 7px 14px;
font-size: 13px;
font-weight: 600;
}
.command-card-confirm-no {
background: transparent;
color: var(--muted);
border: 1px solid var(--divider);
border-radius: 7px;
padding: 7px 14px;
font-size: 13px;
}
.command-card-confirm-yes:disabled,
.command-card-confirm-no:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.command-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-auto-rows: 1fr;
gap: 10px;
}
/* ── Auth card (login forms inside transcript section) ───────────── */
.auth-card {
background: var(--bg2);
border: 1px solid var(--divider);
border-radius: 10px;
padding: 16px 18px;
display: flex;
flex-direction: column;
gap: 12px;
}
.auth-card.error {
border-color: var(--rose);
}
.auth-card-title {
font-size: 13px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 1.4px;
font-weight: 600;
}
.auth-card-hint {
font-size: 14px;
color: var(--muted);
line-height: 19px;
}
.auth-card-row {
display: flex;
align-items: stretch;
gap: 10px;
flex-wrap: wrap;
}
.auth-input {
flex: 1;
min-width: 0;
background: var(--bg);
border: 1px solid var(--hairline);
border-radius: 8px;
padding: 11px 14px;
color: var(--text);
font: inherit;
font-size: 15px;
outline: none;
transition: border-color 0.12s, box-shadow 0.12s;
}
.auth-input:hover:not(:focus):not(:disabled) {
border-color: rgba(255, 255, 255, 0.16);
}
.auth-input:focus {
border-color: var(--fleet);
/* Stronger ring than border-color alone — matches Dawn's emphasis on
* accent halos (BotsDesktop avatar shadow / hero-status.ok glow). */
box-shadow: 0 0 0 3px rgba(149, 128, 255, 0.18);
}
.auth-card.error .auth-input {
border-color: var(--rose);
}
.auth-card.error .auth-input:focus {
box-shadow: 0 0 0 3px rgba(192, 142, 123, 0.22);
}
/* Note: TG-style `.auth-input.code` / `.auth-input.password` /
* `.password-row` / `.btn-icon` selectors were intentionally NOT
* carried over — WhatsApp has no SMS-code form (pairing-code is
* displayed by the bridge, not typed) and no 2FA password form
* (multidevice handshake is single-factor). If a future flow
* needs them, copy from widget-telegram styles.css. */
.btn-primary {
background: var(--fleet);
color: #0c0c0e;
border: none;
border-radius: 8px;
padding: 10px 18px;
font-size: 13px;
font-weight: 600;
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-text {
background: transparent;
border: none;
color: var(--muted);
padding: 10px 12px;
font-size: 13px;
}
.btn-text:hover:not(:disabled) {
color: var(--text);
}
.auth-card-error {
font-size: 13px;
line-height: 18px;
color: var(--rose);
}
.auth-card-warn {
font-size: 13px;
line-height: 18px;
color: var(--amber);
}
.auth-card-waiting {
font-size: 13px;
color: var(--faint);
line-height: 18px;
}
/* Countdown text on the code form: same baseline tone as waiting hint
* but a touch more prominent because it carries an actual number. The
* color tween softens the muted→amber transition at expiry — without it
* the line jumps between palettes mid-sentence, which reads broken
* against Dawn's measured aesthetic. */
.auth-card-countdown {
font-size: 13px;
color: var(--muted);
line-height: 18px;
font-variant-numeric: tabular-nums;
transition: color 0.2s ease-out;
}
.auth-card-countdown.expired {
color: var(--amber);
}
/* ── QR-login panel ─────────────────────────────────────────────── */
/* Override the auth-card row layout — QR panel stacks vertically with the
* matrix as the visual anchor. Keeps the same outer chrome (border, radius,
* padding) so it reads as a sibling to the phone/code/password forms. */
.auth-card-qr {
align-items: stretch;
}
/* The QR matrix sits on a hard #fff plate regardless of theme — phone
* camera scanners need maximum contrast, and the bridge's PNG fallback
* also bakes in a white background. The frame is centered, fixed-size,
* with a soft inner padding so the quiet zone (already 4 modules in the
* SVG itself) is reinforced visually for low-contrast displays. */
.auth-card-qr-frame {
align-self: center;
background: #fff;
border-radius: 12px;
padding: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
/* Lock the inner box to the SVG's rendered size so the placeholder
* variant doesn't collapse to zero height while the matrix is being
* computed (`buildQrModules` is synchronous but the first React commit
* after `start_qr_login` flips state with tgUrl='', and we want the
* placeholder to occupy the same footprint). */
min-width: 260px;
min-height: 260px;
/* Drop a subtle outer shadow so the white plate visually separates from
* the surrounding dark surface — without this the corners look
* paste-on-paper. */
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.06), 0 12px 24px rgba(0, 0, 0, 0.32);
}
/* Placeholder while we wait for the bridge's first qr_displayed event.
* Same visual vocabulary as `.section-status.checking`: amber dot + muted
* text — but inverted onto the white plate so the colors work. */
.auth-card-qr-placeholder {
display: inline-flex;
align-items: center;
gap: 8px;
color: rgba(26, 26, 29, 0.62);
font-size: 13px;
line-height: 20px;
padding: 96px 16px;
}
.auth-card-qr-placeholder .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--amber);
flex-shrink: 0;
}
/* Step list under the QR — explicit phone-side instructions matter more
* here than for SMS, because Telegram's «Link Device» menu isn't a place
* users hit often (vs the typing-an-SMS-code muscle memory). */
.auth-card-qr-steps {
margin: 0;
padding-left: 1.4em;
display: flex;
flex-direction: column;
gap: 6px;
font-size: 13px;
line-height: 19px;
color: var(--muted);
}
.auth-card-qr-steps li::marker {
color: var(--faint);
}
/* ── Pairing-code panel ─────────────────────────────────────────────
* WhatsApp's pairing-code login flow renders an 8-character code
* (`XXXX-XXXX`) that the user types into the WhatsApp mobile app under
* Settings → Linked devices → Link with phone number. The code box is
* the visual anchor — large, monospace, generous letter-spacing so the
* user can read it at arm's length. Mirrors the QR panel's vocabulary
* (centred frame, step list, countdown) but no white plate is needed
* because there's no machine-readable matrix to render at high contrast. */
.auth-card-pairing {
align-items: stretch;
}
.auth-card-pairing-frame {
align-self: center;
background: var(--bg);
border: 1px solid var(--hairline);
border-radius: 12px;
padding: 24px 32px;
display: inline-flex;
align-items: center;
justify-content: center;
/* Match the QR plate's footprint roughly so the panel doesn't reflow
* dramatically when the user switches between login flows. */
min-width: 260px;
min-height: 120px;
/* Subtle inner glow on the fleet accent — picks up the same emphasis
* vocabulary as the input :focus ring, telling the eye «this is the
* thing you act on». */
box-shadow: inset 0 0 0 1px rgba(149, 128, 255, 0.18);
}
.auth-card-pairing-code-text {
font-family: ui-monospace, 'JetBrains Mono', 'SF Mono', monospace;
font-size: 32px;
font-weight: 600;
letter-spacing: 6px;
color: var(--text);
/* Tabular numerics keep the hyphen and digits aligned — without it
* proportional widths shift the centre of the code on narrow screens. */
font-variant-numeric: tabular-nums;
user-select: all;
/* Soft text shadow tints the digits with the fleet accent without
* relying on a dedicated colour token — tone-on-tone. */
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.3);
}
/* Visually-hidden helper — used to attach a supplemental description
* (e.g. «Pairing code for WhatsApp sign-in. Enter it in the app on
* your phone.») to a `<output>` via aria-describedby without putting
* the description in the visible UI. Standard screen-reader-only
* pattern. */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.auth-card-pairing-placeholder {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--muted);
font-size: 13px;
line-height: 20px;
padding: 24px 16px;
}
.auth-card-pairing-placeholder .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--amber);
flex-shrink: 0;
}
/* Step list under the pairing code — same vocabulary as QR steps. */
.auth-card-pairing-steps {
margin: 0;
padding-left: 1.4em;
display: flex;
flex-direction: column;
gap: 6px;
font-size: 13px;
line-height: 19px;
color: var(--muted);
}
.auth-card-pairing-steps li::marker {
color: var(--faint);
}
/* External-logout warn block — same shape as auth-card-warn but lifted
* to a dedicated banner that appears above the disconnected command
* grid. `external_logout` is a hard event from WhatsApp itself
* (multidevice unlink from the phone or another device), so it needs
* to be visually louder than a stale form-side warning. */
.section-warn-banner {
display: flex;
align-items: flex-start;
gap: 10px;
background: rgba(212, 184, 138, 0.10);
border: 1px solid var(--amber);
border-radius: 10px;
padding: 12px 14px;
font-size: 13px;
line-height: 18px;
color: var(--amber);
margin-bottom: 14px;
}
.section-warn-banner .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--amber);
flex-shrink: 0;
margin-top: 6px;
}
@media (max-width: 600px) {
.auth-card-row {
flex-direction: column;
}
.btn-primary,
.btn-text {
width: 100%;
}
/* Compact .command-card on mobile — preserves the «two-row title +
* chevron» structure but trims padding so a single login/logout card
* doesn't dominate a phone-height viewport. */
.command-card {
padding: 12px 14px;
border-radius: 8px;
}
.command-card-name {
font-size: 14px;
margin-bottom: 2px;
}
.command-card-desc {
font-size: 13px;
line-height: 17px;
}
.command-grid {
grid-template-columns: minmax(0, 1fr);
}
/* Mobile QR plate — keep edge-to-edge readable. The 232px SVG matches
* desktop, but the surrounding plate gets a smaller min-size to fit
* narrower viewports without horizontal scroll. */
.auth-card-qr-frame {
min-width: 232px;
min-height: 232px;
padding: 10px;
}
.auth-card-qr-placeholder {
padding: 80px 12px;
}
/* Mobile pairing-code panel — keep the code legible at phone widths
* but trim the surrounding plate so it doesn't dominate. The font is
* already large; what we shrink is padding + min-size. */
.auth-card-pairing-frame {
min-width: 232px;
padding: 18px 20px;
}
.auth-card-pairing-code-text {
/* The 32px desktop size with 6px letter-spacing is ~290 px wide for
* `XXXX-XXXX`. Phone viewport at 360px screen 40px section padding
* = 320px, so reduce a notch to leave room for the frame border. */
font-size: 28px;
letter-spacing: 4px;
}
}
/* ── Linkified transcript bodies ─────────────────────────────────── */
.transcript-line a {
color: var(--fleet-soft);
text-decoration: underline;
}
.transcript-line a:hover {
color: var(--text);
}
/* ── Hint text ───────────────────────────────────────────────────── */
.hint {
font-size: 12px;
color: var(--faint);
margin-top: 8px;
line-height: 17px;
}
/* ── Diagnostic banner (pre-bootstrap failure) ───────────────────── */
.error-banner {
margin: var(--section-pad-x);
padding: 14px 16px;
background: rgba(192, 142, 123, 0.08);
border: 1px solid var(--rose);
border-radius: 10px;
color: var(--rose);
font-size: 13px;
line-height: 19px;
}
.error-banner strong {
display: block;
margin-bottom: 4px;
color: var(--rose);
font-weight: 600;
}
.error-banner code {
background: var(--bg2);
padding: 1px 6px;
border-radius: 4px;
font-family: ui-monospace, 'JetBrains Mono', monospace;
font-size: 12px;
color: var(--text);
}
/* ── About modal ─────────────────────────────────────────────────── */
/* Lightweight modal — fixed inside the widget iframe, not crossing into
* the host. Backdrop click + Escape close; no focus-trap library (the
* widget is a small surface — a heavier mechanism would be overkill). */
.about-overlay {
position: fixed;
inset: 0;
background: rgba(13, 14, 17, 0.72);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
/* Animate in so the panel doesn't feel like a hard pop — matches the
* reassuring tone of the body copy itself. */
animation: about-fade 0.15s ease-out;
}
@keyframes about-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.about-panel {
background: var(--bg);
border: 1px solid var(--hairline);
border-radius: 14px;
width: 100%;
max-width: 520px;
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.about-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 18px;
border-bottom: 1px solid var(--divider);
}
.about-title {
flex: 1;
font-size: 17px;
font-weight: 600;
color: var(--text);
margin: 0;
line-height: 1.3;
}
.about-close-x {
background: transparent;
border: none;
color: var(--muted);
width: 32px;
height: 32px;
border-radius: 8px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font: inherit;
font-size: 24px;
line-height: 1;
transition: background 0.12s, color 0.12s;
}
.about-close-x:hover {
background: var(--surface);
color: var(--text);
}
.about-body {
padding: 16px 18px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
}
.about-body p {
margin: 0;
font-size: 14px;
line-height: 1.55;
color: var(--text);
}
.about-body a {
color: var(--fleet-soft);
text-decoration: underline;
overflow-wrap: anywhere;
}
.about-body a:hover {
color: var(--text);
}
.about-footer {
padding: 12px 18px 16px;
display: flex;
justify-content: flex-end;
border-top: 1px solid var(--divider);
}
/* WhatsApp-only: Meta-ToS risk-disclosure callout at the top of the
* About modal body. Amber tint + border distinguish it from the plain
* paragraphs that follow — same visual language the dedicated warning
* card used to carry on the disconnected screen, now folded inline.
*
* Body text inside the callout still picks up the about-body's
* `.about-body p` rule (default text colour), which is intentional:
* only the title is amber-tinted, the paragraphs stay readable. */
.about-warn-callout {
background: rgba(212, 184, 138, 0.08);
border: 1px solid var(--amber);
border-radius: 8px;
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 10px;
/* Keep the about-body's `gap: 12px` honoured (no extra margin). */
}
.about-warn-callout-head {
display: flex;
align-items: center;
gap: 10px;
}
.about-warn-callout-icon {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--amber);
}
.about-warn-callout-icon svg {
width: 20px;
height: 20px;
display: block;
}
.about-warn-callout-title {
font-size: 15px;
font-weight: 600;
color: var(--amber);
margin: 0;
line-height: 1.3;
}