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

1372 lines
35 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;
}
/* Status pill — button-styled but intentionally non-interactive (no
* cursor:pointer, no hover). The pill carries the section's identity;
* 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, transform 0.08s ease-out;
}
/* Press feedback — :active fires on touch and mouse alike, and unlike
* :hover the WebView never leaves it stuck after the finger lifts. Scale
* is subtle on purpose: cards are wide, a deep squash looks broken. */
.command-card:active:not(:disabled) {
transform: scale(0.985);
background: var(--surface);
}
/* 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;
}
/* Generic leading-icon slot — every command-card carries a semantic
* left-side glyph (mirror of the right-side chevron). Picks up
* `currentColor` from the parent and stays muted by default; the
* `.danger` modifier on logout deliberately does NOT colour the lead
* icon so the rose accent stays reserved for the title (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;
}
/* 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 lead 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);
}
}
/* ── Press feedback for the smaller controls ──────────────────────── */
/* Same rationale as .command-card:active above. Filled (violet/rose)
* buttons darken instead of swapping background so their accent reads
* as «pressed», not «replaced». */
.recovery-action,
.icon-btn,
.btn-primary,
.btn-text,
.contact-action,
.probe-check,
.command-card-confirm-yes,
.command-card-confirm-no {
transition: transform 0.08s ease-out, background 0.12s, color 0.12s, border-color 0.12s,
filter 0.08s ease-out;
}
.recovery-action:active:not(:disabled),
.icon-btn:active:not(:disabled),
.btn-text:active:not(:disabled),
.probe-check:active:not(:disabled),
.command-card-confirm-no:active:not(:disabled) {
transform: scale(0.96);
}
.btn-primary:active:not(:disabled),
.contact-action:active:not(:disabled),
.command-card-confirm-yes:active:not(:disabled) {
transform: scale(0.96);
filter: brightness(0.85);
}
/* ── Icon button (compact action next to a status pill / search field) ── */
.icon-btn {
-webkit-appearance: none;
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
background: var(--bg2);
border: 1px solid var(--divider);
border-radius: 8px;
color: var(--muted);
cursor: pointer;
flex-shrink: 0;
transition: background 0.12s, color 0.12s, border-color 0.12s;
}
:root[data-input='mouse'] .icon-btn:hover:not(:disabled) {
background: var(--surface);
color: var(--text);
border-color: var(--hairline);
}
.icon-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.icon-btn svg {
width: 16px;
height: 16px;
}
/* Square back button — same chrome as .icon-btn, bigger glyph: it's the
* primary way out of the contacts sub-screen, the arrow should read at a
* glance. */
.icon-btn-back svg {
width: 22px;
height: 22px;
}
.icon-btn.spinning svg {
animation: command-card-spin 0.8s linear infinite;
}
/* ── Spinner (inline in-flight indicator) ─────────────────────────── */
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: command-card-spin 0.7s linear infinite;
flex-shrink: 0;
opacity: 0.8;
}
/* ── Notice strip (inline action feedback — replaces the transcript) ── */
.notice-slot {
padding: 16px var(--section-pad-x) 0;
}
.notice {
display: flex;
align-items: flex-start;
gap: 10px;
border-radius: 10px;
border: 1px solid var(--divider);
background: var(--bg2);
padding: 10px 14px;
font-size: 13px;
line-height: 19px;
color: var(--text);
margin-bottom: 12px;
}
.notice.error {
border-color: var(--rose);
color: var(--rose);
background: rgba(192, 142, 123, 0.08);
}
.notice.warn {
border-color: var(--amber);
color: var(--amber);
background: rgba(212, 184, 138, 0.08);
}
.notice.info {
border-color: var(--green);
color: var(--green);
background: rgba(125, 211, 168, 0.08);
}
.notice-body {
flex: 1;
min-width: 0;
}
.notice-dismiss {
background: transparent;
border: none;
color: inherit;
font: inherit;
font-size: 16px;
line-height: 1;
cursor: pointer;
padding: 0 2px;
flex-shrink: 0;
opacity: 0.7;
}
.notice-dismiss:hover {
opacity: 1;
}
/* ── Avatar (initials on a Dawn accent) ───────────────────────────── */
.avatar {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 10px;
color: #0c0c0e;
font-weight: 700;
flex-shrink: 0;
user-select: none;
overflow: hidden;
}
/* Photo layer (MSC4039 download). Sits on top of the initials — a missing
* or still-loading photo leaves the colored-letter fallback visible. */
.avatar-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
/* ── Contacts surface ─────────────────────────────────────────────── */
.contacts {
display: flex;
flex-direction: column;
gap: 12px;
}
/* Search shell mirrors the mock's «Быстрый запуск» row: one integrated
* input strip with a leading glyph and a trailing refresh action. */
.search-shell {
display: flex;
align-items: center;
gap: 10px;
background: var(--bg2);
border: 1px solid var(--divider);
border-radius: 10px;
padding: 6px 14px;
transition: border-color 0.12s, box-shadow 0.12s;
}
.search-shell:focus-within {
border-color: var(--fleet);
box-shadow: 0 0 0 3px rgba(149, 128, 255, 0.18);
}
.search-shell-icon {
display: inline-flex;
color: var(--muted);
flex-shrink: 0;
}
.search-shell-icon svg {
width: 16px;
height: 16px;
}
.search-input {
flex: 1;
min-width: 0;
background: transparent;
border: none;
outline: none;
color: var(--text);
font: inherit;
font-size: 15px;
padding: 8px 0;
}
.search-input::placeholder {
color: var(--faint);
}
/* Probe affordance — explicit «check on Telegram» action for identifier-
* shaped queries. Click-gated on purpose: resolve calls have no flood-wait
* retry on the bridge side. */
.probe-check {
-webkit-appearance: none;
appearance: none;
display: flex;
align-items: center;
gap: 10px;
background: rgba(149, 128, 255, 0.08);
border: 1px dashed var(--fleet);
border-radius: 10px;
padding: 12px 14px;
font: inherit;
font-size: 14px;
color: var(--fleet-soft);
cursor: pointer;
text-align: left;
transition: background 0.12s;
}
:root[data-input='mouse'] .probe-check:hover:not(:disabled) {
background: rgba(149, 128, 255, 0.14);
}
.probe-check:disabled {
cursor: progress;
}
.probe-check svg {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.probe-result {
display: flex;
flex-direction: column;
gap: 8px;
}
.probe-result-label {
font-size: 13px;
color: var(--green);
}
.probe-result-label.missing {
color: var(--amber);
}
.probe-result .contact-row {
border-color: var(--fleet);
}
.contact-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.contact-row {
display: flex;
align-items: center;
gap: 12px;
background: var(--bg2);
border: 1px solid var(--divider);
border-radius: 10px;
padding: 10px 12px;
}
.contact-main {
flex: 1;
min-width: 0;
}
.contact-name {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.contact-name-text {
font-size: 14.5px;
font-weight: 600;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.contact-handle {
display: flex;
flex-wrap: wrap;
column-gap: 10px;
row-gap: 1px;
font-size: 12px;
color: var(--muted);
font-family: ui-monospace, 'JetBrains Mono', monospace;
margin-top: 2px;
min-width: 0;
}
.contact-handle-part {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.contact-action {
-webkit-appearance: none;
appearance: none;
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--fleet);
color: #0c0c0e;
border: none;
border-radius: 7px;
padding: 8px 14px;
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
flex-shrink: 0;
white-space: nowrap;
}
.contact-action.linked {
background: transparent;
color: var(--muted);
border: 1px solid var(--divider);
font-weight: 500;
}
:root[data-input='mouse'] .contact-action.linked:hover:not(:disabled) {
color: var(--text);
border-color: var(--hairline);
background: var(--surface);
}
.contact-action:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.contacts-placeholder {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
color: var(--muted);
font-size: 13.5px;
padding: 28px 12px;
text-align: center;
}
.contacts-error-text {
color: var(--rose);
}
/* ── Account tab ──────────────────────────────────────────────────── */
.account-card {
display: flex;
align-items: center;
gap: 14px;
background: var(--bg2);
border: 1px solid var(--divider);
border-radius: 10px;
padding: 14px 16px;
}
.account-main {
flex: 1;
min-width: 0;
}
.account-name {
font-size: 16px;
font-weight: 600;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.account-handles {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 3px;
font-size: 12.5px;
color: var(--muted);
font-family: ui-monospace, 'JetBrains Mono', monospace;
}
/* Destructive card — keeps the red name to mark «Выйти из Telegram» 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);
}
/* 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);
}
[data-theme='light'] .auth-input:hover:not(:focus):not(:disabled) {
border-color: rgba(0, 0, 0, 0.22);
}
.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);
}
/* Soft-warn variant for client-side phone validation. The bridge still
* has the authoritative word (and the cooldown clears itself on
* `invalid_value`), so we use amber rather than the harder rose tone
* reserved for server-confirmed errors. */
.auth-input.warn {
border-color: var(--amber);
}
.auth-input.warn:focus {
box-shadow: 0 0 0 3px rgba(231, 178, 90, 0.22);
}
/* Phone-input shell: lets us position a country-flag emoji over the
* input's left padding without splitting the input's own background /
* border / focus ring. The shell IS the layout flex child; the input
* fills it. `with-flag` bumps text padding-left so the digits clear
* the flag glyph. */
.auth-phone-shell {
position: relative;
display: flex;
flex: 1;
min-width: 0;
}
.auth-phone-shell .auth-input {
flex: 1;
min-width: 0;
}
.auth-phone-shell.with-flag .auth-input {
padding-left: 44px;
}
.auth-phone-flag {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
line-height: 1;
pointer-events: none;
user-select: none;
}
.auth-input.code,
.auth-input.password {
font-family: ui-monospace, 'JetBrains Mono', 'SF Mono', monospace;
letter-spacing: 4px;
font-size: 20px;
}
/* Password-input shell — host for the reveal eye-button positioned over
* the input's right padding. Mirrors `.auth-phone-shell` (left flag) and
* follows the same UX as Vojo's main auth `PasswordInput` (eye toggle
* embedded in the input chrome, no sibling pill that can overflow off
* the right edge on narrow viewports). Replaces the previous
* `.password-row` flex-pair that stacked into a full-width button on
* mobile, see commit 8d8b39e8. */
.auth-password-shell {
position: relative;
display: flex;
flex: 1;
min-width: 0;
}
.auth-password-shell .auth-input {
flex: 1;
min-width: 0;
/* Reserve room for the eye button so the password bullets don't run
* under the glyph. Eye button ≈ 36 px wide + 6 px breathing room. */
padding-right: 44px;
}
/* Suppress the legacy Edge / IE11 native reveal glyph (`::-ms-reveal`)
* so it doesn't render on top of our own eye button. Chromium / WebKit
* / Firefox ignore this pseudo — no-op on the platforms we actually
* ship to, cheap defence for users who arrive on legacy Edge. */
.auth-password-shell .auth-input::-ms-reveal {
display: none;
}
.auth-password-eye {
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: transparent;
border: none;
border-radius: 6px;
color: var(--muted);
cursor: pointer;
padding: 0;
}
.auth-password-eye:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Hover / focus rings gated on `[data-input='mouse']` (set by main.tsx
* from `pointerdown.pointerType`) because Capacitor Android WebView lies
* about `(hover: hover)` on pure-touch devices — a media-query gate
* would still let the WebView paint a stuck `:hover` on the tapped
* button. Same reason `.command-card:hover` upstream is gated this way. */
:root[data-input='mouse'] .auth-password-eye:hover:not(:disabled) {
color: var(--text);
background: var(--hairline);
}
:root[data-input='mouse'] .auth-password-eye:focus-visible {
outline: 2px solid var(--fleet);
outline-offset: 1px;
color: var(--text);
}
.auth-password-eye svg {
/* 16 px matches folds `<Icon size="100">` used by the canonical
* password input in `src/app/components/password-input`. */
width: 16px;
height: 16px;
}
.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);
}
[data-theme='light'] .auth-card-qr-frame {
/* The dark-mode edge highlight is invisible on a light surface; a plain
* soft drop shadow separates the white plate instead. */
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.14);
}
/* 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);
}
@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;
}
/* Contact rows: tighter chrome, and the action button stays inline (it's
* short — «Открыть чат» worst case) rather than wrapping full-width. */
.contact-row {
padding: 9px 10px;
gap: 10px;
}
.contact-action {
padding: 8px 11px;
font-size: 12.5px;
}
/* Narrow viewports: @ник и телефон друг под другом вместо обрезанной
* одной строки — на телефоне рядом с кнопкой им не хватает ширины. */
.contact-handle {
flex-direction: column;
}
/* Header row on phones: the status pill grows to fill the row at the
* search-field height (~48px), and the square icon buttons (refresh /
* back) match it, so the trio lines up edge-to-edge with the search
* shell below. Desktop keeps the compact content-sized pill. */
.section-recovery-row {
flex-wrap: nowrap;
}
.section-recovery-row > .section-status {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
font-size: 15px;
padding: 13px 16px;
}
.section-recovery-row > .icon-btn {
width: 48px;
height: 48px;
border-radius: 10px;
}
.section-recovery-row > .icon-btn svg {
width: 18px;
height: 18px;
}
.section-recovery-row > .icon-btn.icon-btn-back svg {
width: 25px;
height: 25px;
}
/* Labeled row actions (Отмена / Повторить on the transient screens)
* match the taller pill so the row reads as one piece. */
.section-recovery-row > .recovery-action {
padding: 13px 16px;
border-radius: 10px;
white-space: nowrap;
flex-shrink: 0;
}
}
/* ── 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;
}
[data-theme='light'] .about-overlay {
background: rgba(26, 26, 29, 0.36);
}
@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);
}