2026-01-22 12:21:19 +02:00
|
|
|
|
/* Mobile Adaptations - All Devices */
|
|
|
|
|
|
|
|
|
|
|
|
/* General mobile improvements for all mobile devices */
|
|
|
|
|
|
@media (max-width: 1024px) {
|
|
|
|
|
|
/* Override CSS custom properties for mobile */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) {
|
|
|
|
|
|
--text-markdown: 1rem;
|
|
|
|
|
|
--text-code: 0.875rem;
|
2026-06-28 12:33:10 +03:00
|
|
|
|
--text-ui-header: 0.9375rem;
|
|
|
|
|
|
--text-ui-label: 0.875rem;
|
|
|
|
|
|
--text-meta: 0.875rem;
|
|
|
|
|
|
--text-micro: 0.8125rem;
|
2026-07-18 00:11:05 +03:00
|
|
|
|
--text-settings-page-title: 1.125rem;
|
2026-01-22 12:21:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Force override with higher specificity */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) * {
|
|
|
|
|
|
--text-markdown: 1rem !important;
|
|
|
|
|
|
--text-code: 0.875rem !important;
|
2026-06-28 12:33:10 +03:00
|
|
|
|
--text-ui-header: 0.9375rem !important;
|
|
|
|
|
|
--text-ui-label: 0.875rem !important;
|
|
|
|
|
|
--text-meta: 0.875rem !important;
|
|
|
|
|
|
--text-micro: 0.8125rem !important;
|
2026-07-18 00:11:05 +03:00
|
|
|
|
--text-settings-page-title: 1.125rem !important;
|
2026-01-22 12:21:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Additional override for elements using typography classes */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .typography-markdown,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .typography-code,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .typography-ui-header,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .typography-ui-label,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .typography-meta,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .typography-micro {
|
|
|
|
|
|
font-size: unset !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Fix font size for tool displays */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) [class*="tool"],
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) [class*="code"] {
|
|
|
|
|
|
--text-code: 0.875rem !important;
|
|
|
|
|
|
font-size: var(--text-code) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Improve touch targets for mobile */
|
2026-04-19 15:52:54 +03:00
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) button:not([role="radio"]):not([role="checkbox"]):not([role="switch"]),
|
2026-01-22 12:21:19 +02:00
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .btn,
|
2026-08-01 21:16:36 +03:00
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) [role="button"],
|
|
|
|
|
|
/* Static chat tool rows: not interactive as a whole, but they must share
|
|
|
|
|
|
the 36px rhythm of the [role="button"] expandable/reasoning rows. */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .oc-static-tool-row {
|
2026-01-22 12:21:19 +02:00
|
|
|
|
min-height: 36px;
|
|
|
|
|
|
min-width: 36px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-28 13:33:55 +03:00
|
|
|
|
/* Composer footer action buttons (sessions / attach / auto-accept): hug the
|
|
|
|
|
|
icon so the group stays tight. The container only renders in the mobile
|
|
|
|
|
|
JSX, so no pointer/runtime gating is needed; !important overrides both the
|
|
|
|
|
|
w-8 utility and the global mobile touch-target min-width regardless of
|
|
|
|
|
|
cascade layer or specificity. */
|
|
|
|
|
|
.composer-mobile-actions button {
|
|
|
|
|
|
width: 1.5rem !important;
|
|
|
|
|
|
min-width: 0 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 12:21:19 +02:00
|
|
|
|
/* Improve input field touch targets */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) input,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) textarea,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) select {
|
|
|
|
|
|
min-height: 36px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Fix mobile text inputs */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) input[type="text"],
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) input[type="search"],
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) input[type="email"],
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) input[type="password"],
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) textarea {
|
|
|
|
|
|
font-size: 16px !important; /* Prevents iOS zoom */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Prevent keyboard on non-input elements */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime)
|
|
|
|
|
|
button:not([type="submit"]):not([type="button"]):not([type="reset"]),
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .btn,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) [role="button"] {
|
|
|
|
|
|
-webkit-touch-callout: none;
|
|
|
|
|
|
-webkit-user-select: none;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Specific fix for session buttons */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) button[inputmode="none"] {
|
|
|
|
|
|
-webkit-touch-callout: none;
|
|
|
|
|
|
-webkit-user-select: none;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
pointer-events: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 01:13:49 -08:00
|
|
|
|
/* Prevent system context menu on message content when using custom selection menu */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .message-content-text {
|
|
|
|
|
|
-webkit-touch-callout: none;
|
|
|
|
|
|
-webkit-user-select: text;
|
|
|
|
|
|
user-select: text;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 20:45:03 +02:00
|
|
|
|
|
2026-02-04 01:13:49 -08:00
|
|
|
|
/* Allow system context menu only when explicitly requested via the "More" button */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .message-content-text.show-system-menu {
|
|
|
|
|
|
-webkit-touch-callout: default;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 12:21:19 +02:00
|
|
|
|
/* Fix mobile scroll containers */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .overflow-hidden {
|
|
|
|
|
|
overflow-x: hidden !important;
|
|
|
|
|
|
overflow-y: auto !important;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 16:14:09 +07:00
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) [data-page-scroll-lock="true"] {
|
|
|
|
|
|
overflow: hidden !important;
|
|
|
|
|
|
overscroll-behavior: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-05 19:24:23 +03:00
|
|
|
|
/* Keep markdown code blocks horizontally scrollable on mobile. */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .markdown-content [data-markdown="code-block-body"] {
|
2026-03-01 10:54:02 -03:00
|
|
|
|
overflow-x: auto !important;
|
|
|
|
|
|
overflow-y: hidden !important;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 12:21:19 +02:00
|
|
|
|
/* Fix mobile viewport height */
|
2026-04-26 00:58:40 +07:00
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) {
|
2026-01-22 12:21:19 +02:00
|
|
|
|
height: 100%;
|
|
|
|
|
|
height: -webkit-fill-available;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Fix main layout container */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-screen {
|
|
|
|
|
|
height: 100vh;
|
|
|
|
|
|
height: -webkit-fill-available;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Fix main content area */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .flex-1.overflow-hidden {
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Fix chat container */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-full {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Ensure proper flex behavior */
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col {
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS PWA Adaptations */
|
|
|
|
|
|
|
2026-01-24 18:37:03 +02:00
|
|
|
|
/* iOS safe area defaults (applies to PWA + Safari) */
|
|
|
|
|
|
@supports (-webkit-touch-callout: none) {
|
2026-05-04 00:04:29 +03:00
|
|
|
|
:root.device-mobile:not(.desktop-runtime),
|
|
|
|
|
|
:root.device-tablet:not(.desktop-runtime),
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) {
|
2026-01-24 18:37:03 +02:00
|
|
|
|
--oc-safe-area-top: env(safe-area-inset-top, 0);
|
|
|
|
|
|
--oc-safe-area-right: env(safe-area-inset-right, 0);
|
|
|
|
|
|
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
|
2026-08-01 21:16:36 +03:00
|
|
|
|
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.35), 12px);
|
2026-01-24 18:37:03 +02:00
|
|
|
|
--oc-safe-area-left: env(safe-area-inset-left, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 00:04:29 +03:00
|
|
|
|
:root.device-mobile:not(.desktop-runtime) .header-safe-area,
|
|
|
|
|
|
:root.device-tablet:not(.desktop-runtime) .header-safe-area,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
2026-01-24 18:37:03 +02:00
|
|
|
|
padding-top: var(--oc-safe-area-top);
|
|
|
|
|
|
}
|
2026-07-01 20:13:53 +03:00
|
|
|
|
|
|
|
|
|
|
:root.oc-capacitor-app [data-sonner-toaster][data-y-position='top'] {
|
|
|
|
|
|
top: calc(env(safe-area-inset-top, 0px) + 16px) !important;
|
|
|
|
|
|
}
|
2026-01-24 18:37:03 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 12:21:19 +02:00
|
|
|
|
/* Phase 1: iOS PWA safe area handling - Enhanced positioning approach */
|
|
|
|
|
|
@media (display-mode: standalone) {
|
2026-08-01 21:16:36 +03:00
|
|
|
|
/* iOS-specific safe area handling. The --oc-safe-area-* tokens and the
|
|
|
|
|
|
.header-safe-area padding are already set by the unconditional iOS block
|
|
|
|
|
|
above; only the standalone-specific rules live here. */
|
2026-01-22 12:21:19 +02:00
|
|
|
|
@supports (-webkit-touch-callout: none) {
|
2026-05-04 00:04:29 +03:00
|
|
|
|
:root.device-mobile:not(.desktop-runtime) .main-content-safe-area,
|
|
|
|
|
|
:root.device-tablet:not(.desktop-runtime) .main-content-safe-area,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .main-content-safe-area {
|
2026-01-22 12:21:19 +02:00
|
|
|
|
padding-top: 0;
|
|
|
|
|
|
padding-bottom: 0;
|
|
|
|
|
|
padding-left: var(--oc-safe-area-left);
|
|
|
|
|
|
padding-right: var(--oc-safe-area-right);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Safe area for bottom fixed/bottom-0 elements */
|
2026-05-04 00:04:29 +03:00
|
|
|
|
:root.device-mobile:not(.desktop-runtime) .bottom-safe-area,
|
|
|
|
|
|
:root.device-tablet:not(.desktop-runtime) .bottom-safe-area,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .bottom-safe-area {
|
2026-01-22 12:21:19 +02:00
|
|
|
|
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Fix iOS viewport issues */
|
2026-05-04 00:04:29 +03:00
|
|
|
|
:root.device-mobile:not(.desktop-runtime) .flex.flex-col.h-screen,
|
|
|
|
|
|
:root.device-tablet:not(.desktop-runtime) .flex.flex-col.h-screen,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-screen {
|
2026-01-22 12:21:19 +02:00
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
min-height: -webkit-fill-available;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Prevent content overlap in iOS */
|
2026-05-04 00:04:29 +03:00
|
|
|
|
:root.device-mobile:not(.desktop-runtime) .flex-1.overflow-hidden,
|
|
|
|
|
|
:root.device-tablet:not(.desktop-runtime) .flex-1.overflow-hidden,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .flex-1.overflow-hidden {
|
2026-01-22 12:21:19 +02:00
|
|
|
|
position: relative;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Mobile typography improvements */
|
|
|
|
|
|
.typography-markdown {
|
|
|
|
|
|
font-size: 1rem !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.typography-code {
|
|
|
|
|
|
font-size: 0.875rem !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.typography-ui-header {
|
2026-06-28 12:33:10 +03:00
|
|
|
|
font-size: 0.9375rem !important;
|
2026-01-22 12:21:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.typography-ui-label {
|
2026-06-28 12:33:10 +03:00
|
|
|
|
font-size: 0.875rem !important;
|
2026-01-22 12:21:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.typography-meta {
|
2026-06-28 12:33:10 +03:00
|
|
|
|
font-size: 0.875rem !important;
|
2026-01-22 12:21:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.typography-micro {
|
2026-06-28 12:33:10 +03:00
|
|
|
|
font-size: 0.8125rem !important;
|
2026-01-22 12:21:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Fix font size for tool displays */
|
|
|
|
|
|
[class*="tool"], [class*="code"] {
|
|
|
|
|
|
font-size: 0.875rem !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Fallback for non-iOS standalone mode */
|
|
|
|
|
|
@supports not (-webkit-touch-callout: none) {
|
2026-05-04 00:04:29 +03:00
|
|
|
|
:root.device-mobile:not(.desktop-runtime),
|
|
|
|
|
|
:root.device-tablet:not(.desktop-runtime),
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) {
|
2026-01-22 12:21:19 +02:00
|
|
|
|
--oc-safe-area-top: env(safe-area-inset-top, 0);
|
|
|
|
|
|
--oc-safe-area-right: env(safe-area-inset-right, 0);
|
|
|
|
|
|
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
|
2026-08-01 21:16:36 +03:00
|
|
|
|
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.35), 12px);
|
2026-01-22 12:21:19 +02:00
|
|
|
|
--oc-safe-area-left: env(safe-area-inset-left, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 00:04:29 +03:00
|
|
|
|
:root.device-mobile:not(.desktop-runtime) .header-safe-area,
|
|
|
|
|
|
:root.device-tablet:not(.desktop-runtime) .header-safe-area,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
2026-01-22 12:21:19 +02:00
|
|
|
|
padding-top: var(--oc-safe-area-top);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 00:04:29 +03:00
|
|
|
|
:root.device-mobile:not(.desktop-runtime) .main-content-safe-area,
|
|
|
|
|
|
:root.device-tablet:not(.desktop-runtime) .main-content-safe-area,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .main-content-safe-area {
|
2026-01-22 12:21:19 +02:00
|
|
|
|
padding-top: 0;
|
|
|
|
|
|
padding-bottom: 0;
|
|
|
|
|
|
padding-left: var(--oc-safe-area-left);
|
|
|
|
|
|
padding-right: var(--oc-safe-area-right);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 00:04:29 +03:00
|
|
|
|
:root.device-mobile:not(.desktop-runtime) .bottom-safe-area,
|
|
|
|
|
|
:root.device-tablet:not(.desktop-runtime) .bottom-safe-area,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) .bottom-safe-area {
|
2026-01-22 12:21:19 +02:00
|
|
|
|
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Phase 6: Home indicator blur gradient fix */
|
|
|
|
|
|
@media (display-mode: standalone) and (max-width: 768px) {
|
|
|
|
|
|
.pwa-dialog-content {
|
|
|
|
|
|
--pwa-dialog-padding: clamp(0.75rem, 3vw, 1.25rem);
|
2026-06-08 19:55:56 +07:00
|
|
|
|
top: auto;
|
2026-05-24 19:19:19 +08:00
|
|
|
|
/* Browsers that don't understand dvh ignore the second declaration and
|
|
|
|
|
|
keep the vh-based fallback. Newer browsers use 100dvh so Android
|
|
|
|
|
|
Chrome's collapsible URL bar doesn't push the dialog footer off-screen. */
|
|
|
|
|
|
max-height: calc(
|
|
|
|
|
|
100vh
|
|
|
|
|
|
- var(--oc-safe-area-top, 0px)
|
|
|
|
|
|
- var(--oc-safe-area-bottom-visual, 0px)
|
|
|
|
|
|
- 20px
|
|
|
|
|
|
);
|
|
|
|
|
|
max-height: calc(
|
|
|
|
|
|
100dvh
|
|
|
|
|
|
- var(--oc-safe-area-top, 0px)
|
|
|
|
|
|
- var(--oc-safe-area-bottom-visual, 0px)
|
|
|
|
|
|
- 20px
|
|
|
|
|
|
);
|
2026-01-22 12:21:19 +02:00
|
|
|
|
padding: var(--pwa-dialog-padding);
|
|
|
|
|
|
padding-top: calc(var(--pwa-dialog-padding) + (var(--oc-safe-area-top, 0px) * 0.35));
|
|
|
|
|
|
row-gap: clamp(0.75rem, 2vw, 1.25rem);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pwa-dialog-content.pwa-compact {
|
|
|
|
|
|
--pwa-dialog-padding: clamp(0.5rem, 2vw, 0.85rem);
|
|
|
|
|
|
row-gap: clamp(0.45rem, 1.5vw, 0.8rem);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pwa-dialog-content [data-slot="dialog-close"] {
|
|
|
|
|
|
top: calc(0.25rem + (var(--oc-safe-area-top, 0px) * 0.35));
|
|
|
|
|
|
right: var(--pwa-dialog-padding);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pwa-dialog-content.pwa-compact [data-slot="dialog-close"] {
|
|
|
|
|
|
top: calc(0.2rem + (var(--oc-safe-area-top, 0px) * 0.3));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pwa-dialog-content .settings-page-body {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
padding: 0.75rem 1rem;
|
|
|
|
|
|
row-gap: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pwa-dialog-content .settings-page-body > :not([hidden]) ~ :not([hidden]) {
|
|
|
|
|
|
margin-top: 0.85rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pwa-dialog-content .directory-dialog-body {
|
|
|
|
|
|
padding: 0.5rem 0.75rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pwa-dialog-content .directory-dialog-body .directory-grid {
|
|
|
|
|
|
gap: 0.6rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pwa-overlay-panel {
|
2026-06-02 20:26:48 +03:00
|
|
|
|
margin-bottom: 0;
|
2026-01-22 12:21:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pwa-overlay-panel .pwa-overlay-scroll {
|
|
|
|
|
|
padding-bottom: calc(var(--oc-safe-area-bottom-visual, 0px) + 12px);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (display-mode: standalone) {
|
2026-04-07 21:46:38 +03:00
|
|
|
|
/* Home indicator overlay */
|
2026-05-04 00:04:29 +03:00
|
|
|
|
:root.device-mobile:not(.desktop-runtime) body::after,
|
|
|
|
|
|
:root.device-tablet:not(.desktop-runtime) body::after,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) body::after {
|
2026-01-22 12:21:19 +02:00
|
|
|
|
content: '';
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
height: var(--oc-safe-area-bottom-visual, var(--oc-safe-area-bottom, env(safe-area-inset-bottom, 0)));
|
|
|
|
|
|
background: linear-gradient(
|
|
|
|
|
|
to top,
|
|
|
|
|
|
var(--background) 0%,
|
|
|
|
|
|
var(--background) 30%,
|
|
|
|
|
|
rgba(from var(--background) r g b / 0.8) 60%,
|
|
|
|
|
|
rgba(from var(--background) r g b / 0.3) 85%,
|
|
|
|
|
|
transparent 100%
|
|
|
|
|
|
);
|
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Fallback for browsers without rgba(from ...) support */
|
|
|
|
|
|
@supports not (color: rgba(from white r g b / 0.5)) {
|
2026-05-04 00:04:29 +03:00
|
|
|
|
:root.device-mobile:not(.desktop-runtime) body::after,
|
|
|
|
|
|
:root.device-tablet:not(.desktop-runtime) body::after,
|
|
|
|
|
|
:root.mobile-pointer:not(.desktop-runtime) body::after {
|
2026-01-22 12:21:19 +02:00
|
|
|
|
background: linear-gradient(
|
|
|
|
|
|
to top,
|
|
|
|
|
|
var(--background) 0%,
|
|
|
|
|
|
var(--background) 30%,
|
|
|
|
|
|
rgba(21, 19, 19, 0.8) 60%, /* Dark theme fallback */
|
|
|
|
|
|
rgba(21, 19, 19, 0.3) 85%,
|
|
|
|
|
|
transparent 100%
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Light theme fallback */
|
|
|
|
|
|
:root body::after {
|
|
|
|
|
|
background: linear-gradient(
|
|
|
|
|
|
to top,
|
|
|
|
|
|
var(--background) 0%,
|
|
|
|
|
|
var(--background) 30%,
|
|
|
|
|
|
rgba(248, 247, 243, 0.8) 60%, /* Light theme fallback */
|
|
|
|
|
|
rgba(248, 247, 243, 0.3) 85%,
|
|
|
|
|
|
transparent 100%
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-01 09:55:41 +03:00
|
|
|
|
|
|
|
|
|
|
/* Small app-wide bottom safe area for the native shell. The phone's rounded hardware
|
|
|
|
|
|
corners clip controls flush against the bottom edge, and the PWA's own safe-area
|
|
|
|
|
|
padding is gated behind display-mode: standalone — which the Capacitor WebView does
|
|
|
|
|
|
not match — so nothing reserves bottom room in the native app. Expose it as a token
|
|
|
|
|
|
so any native surface can consume it; the chat shell does so below. */
|
|
|
|
|
|
:root.oc-capacitor-app {
|
2026-07-10 20:25:48 +03:00
|
|
|
|
--oc-app-bottom-safe: max(16px, calc(var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)) * 0.5));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Android native safe areas. Every --oc-safe-area-* definition above is gated
|
|
|
|
|
|
behind -webkit-touch-callout (iOS) or display-mode: standalone, neither of
|
|
|
|
|
|
which matches the Capacitor Android WebView — so the Android app used to rely
|
|
|
|
|
|
entirely on StatusBar overlay:false insetting the WebView natively. Android 15
|
|
|
|
|
|
enforces edge-to-edge and ignores that call, leaving content under the status
|
|
|
|
|
|
bar. Capacitor injects the real insets as --safe-area-inset-* inline vars on
|
|
|
|
|
|
<html> (env() still reads 0 on WebViews without the safe-area passthrough
|
|
|
|
|
|
fix), so read those first and fall back to env(). On devices where the native
|
|
|
|
|
|
inset still applies, both sources report 0 — no double padding. */
|
|
|
|
|
|
:root.oc-capacitor-app.oc-platform-android {
|
|
|
|
|
|
--oc-safe-area-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
|
|
|
|
|
|
--oc-safe-area-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
|
|
|
|
|
|
--oc-safe-area-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
|
2026-08-01 21:16:36 +03:00
|
|
|
|
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.35), 12px);
|
2026-07-10 20:25:48 +03:00
|
|
|
|
--oc-safe-area-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:root.oc-capacitor-app.oc-platform-android .header-safe-area {
|
|
|
|
|
|
padding-top: var(--oc-safe-area-top);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:root.oc-capacitor-app.oc-platform-android [data-sonner-toaster][data-y-position='top'] {
|
|
|
|
|
|
top: calc(var(--oc-safe-area-top) + 16px) !important;
|
2026-07-01 09:55:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Paint the document canvas with the theme background in the native app — the same
|
|
|
|
|
|
thing .desktop-runtime does for body/#root, which the Capacitor shell never got.
|
|
|
|
|
|
The status bar is overlaid (transparent), and in dark mode `color-scheme: dark`
|
|
|
|
|
|
makes the bare UA canvas dark, so any sliver not covered by content (notably the
|
|
|
|
|
|
area behind the status bar) bled through as a dark band at the top. It only showed
|
|
|
|
|
|
in dark mode, which is why it tracked the system theme. */
|
|
|
|
|
|
:root.oc-capacitor-app,
|
|
|
|
|
|
:root.oc-capacitor-app body,
|
|
|
|
|
|
:root.oc-capacitor-app #root {
|
|
|
|
|
|
background: var(--background) !important;
|
|
|
|
|
|
background-color: var(--background) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Native (Capacitor) keyboard handling.
|
|
|
|
|
|
The Keyboard plugin runs in `resize: 'none'` mode so the WebView keeps its full
|
2026-07-04 13:55:12 +03:00
|
|
|
|
height; instead we shrink the app shell by the keyboard frame height. Two vars,
|
|
|
|
|
|
both set by useNativeMobileChrome: --oc-keyboard-inset is the keyboard's target
|
|
|
|
|
|
height from the moment `keyboardWillShow` fires (overlay surfaces below animate
|
|
|
|
|
|
against it as before), while --oc-kb-layout is the same height applied to the
|
|
|
|
|
|
shell's LAYOUT only at the choreography point where a resize is invisible.
|
2026-07-01 09:55:41 +03:00
|
|
|
|
Scoped to .oc-capacitor-app so the browser PWA keeps its dvh / interactive-widget
|
|
|
|
|
|
behaviour untouched.
|
|
|
|
|
|
|
2026-07-04 13:55:12 +03:00
|
|
|
|
The shell height is NOT transitioned. Animating `height` reflows the whole shell
|
|
|
|
|
|
every frame and forces the chat's ResizeObserver to chase the bottom on each of
|
|
|
|
|
|
those frames — the source of the visible micro-jitter on the composer/chat while
|
|
|
|
|
|
pinned. Instead the visual rise is transform-only (compositor) — a FLIP
|
|
|
|
|
|
choreography driven by useNativeMobileChrome:
|
|
|
|
|
|
|
|
|
|
|
|
show: shell keeps its full height for the whole 0.25s; the composer (and, when
|
2026-07-05 01:41:44 +03:00
|
|
|
|
pinned, the chat scroll container — see useChatAutoFollow) slides up via
|
|
|
|
|
|
inline transforms in sync with the keyboard; at the end the shell snaps to
|
2026-07-04 13:55:12 +03:00
|
|
|
|
its final height (--oc-kb-layout, one reflow) and the shift is removed
|
|
|
|
|
|
in the same frame — visually identical, so the swap is invisible.
|
|
|
|
|
|
hide: the shell snaps back to full height immediately (still hidden behind the
|
|
|
|
|
|
keyboard), the composer/content are FLIP-offset to their raised position
|
|
|
|
|
|
and slide down with the keyboard.
|
|
|
|
|
|
|
|
|
|
|
|
`keyboardWillShow` fires at the start of the iOS keyboard animation with the final
|
|
|
|
|
|
height, and the duration/curve mimic the native keyboard (≈0.25s,
|
|
|
|
|
|
cubic-bezier(0.38, 0.7, 0.125, 1)). (visualViewport live-tracking would be exact
|
2026-07-01 09:55:41 +03:00
|
|
|
|
but doesn't report under WKWebView's `resize: 'none'`, so this is the best signal.) */
|
|
|
|
|
|
:root.oc-capacitor-app .oc-mobile-app-shell {
|
2026-07-04 13:55:12 +03:00
|
|
|
|
height: calc(100dvh - var(--oc-kb-layout, 0px));
|
2026-07-01 09:55:41 +03:00
|
|
|
|
/* Reserve the bottom safe area only while the keyboard is down — when it's up the
|
|
|
|
|
|
inset cancels it out (the home indicator is hidden and the composer should sit
|
|
|
|
|
|
flush above the keyboard). The shell keeps its own bg behind this padding. */
|
2026-07-04 13:55:12 +03:00
|
|
|
|
padding-bottom: max(0px, calc(var(--oc-app-bottom-safe, 0px) - var(--oc-kb-layout, 0px)));
|
2026-07-01 09:55:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Portal surfaces (bottom sheets, overlay panels) render at <body> level, outside
|
|
|
|
|
|
the app shell, so they don't inherit the shell's keyboard inset. They're full-
|
|
|
|
|
|
height `fixed inset-0` scrims with a bottom-anchored (`mt-auto`) sheet, so raising
|
|
|
|
|
|
their bottom edge by the keyboard height shrinks scrim + sheet together and lifts
|
|
|
|
|
|
any input above the keyboard instead of hiding it underneath. The opacity term
|
|
|
|
|
|
preserves the scrim's enter fade (Tailwind's `transition-opacity` would otherwise
|
|
|
|
|
|
be overridden by this rule's `transition` shorthand). */
|
|
|
|
|
|
:root.oc-capacitor-app .oc-keyboard-inset-surface {
|
|
|
|
|
|
bottom: var(--oc-keyboard-inset, 0px);
|
|
|
|
|
|
transition: bottom 0.25s cubic-bezier(0.38, 0.7, 0.125, 1), opacity 0.2s ease-out;
|
|
|
|
|
|
}
|
2026-07-05 00:17:19 +03:00
|
|
|
|
/* While a panel is playing its enter animation, its bottom anchor snaps
|
|
|
|
|
|
instead of transitioning — opening an overlay typically dismisses the
|
|
|
|
|
|
keyboard in the same moment, and animating both made the entrance jerky. */
|
|
|
|
|
|
:root.oc-capacitor-app .oc-keyboard-inset-surface.oc-keyboard-inset-snap {
|
|
|
|
|
|
transition: opacity 0.2s ease-out;
|
|
|
|
|
|
}
|
2026-07-01 09:55:41 +03:00
|
|
|
|
|
2026-07-12 17:01:40 +03:00
|
|
|
|
/* Bottom-sheet overlays live inside a keyboard-inset surface whose bottom is
|
|
|
|
|
|
lifted by the keyboard height, but their max-height is expressed in 100dvh —
|
|
|
|
|
|
which does NOT shrink in the Capacitor app (native keyboard resize is off).
|
|
|
|
|
|
Without this cap a sheet with the keyboard raised (typing inside it) can
|
|
|
|
|
|
grow past the top safe area and slide under the notch/status bar. Subtract
|
|
|
|
|
|
both the keyboard inset and the top safe area from the available height. */
|
|
|
|
|
|
:root.oc-capacitor-app .pwa-overlay-panel {
|
|
|
|
|
|
max-height: calc(
|
|
|
|
|
|
100dvh
|
|
|
|
|
|
- var(--oc-keyboard-inset, 0px)
|
|
|
|
|
|
- max(0.75rem, env(safe-area-inset-top))
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 09:55:41 +03:00
|
|
|
|
/* Full-screen scroll views (e.g. the connect/login screen) live outside the app
|
|
|
|
|
|
shell, so shrink them by the keyboard height the same way the shell does. Capping
|
|
|
|
|
|
the height (instead of min-height: 100dvh) is what makes overflow-y-auto actually
|
|
|
|
|
|
scroll, so a field near the bottom lifts above the keyboard rather than staying
|
|
|
|
|
|
hidden behind it. min-height: 0 neutralises the Tailwind min-h-dvh baseline. */
|
|
|
|
|
|
:root.oc-capacitor-app .oc-keyboard-fill-screen {
|
|
|
|
|
|
height: calc(100dvh - var(--oc-keyboard-inset, 0px));
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
transition: height 0.25s cubic-bezier(0.38, 0.7, 0.125, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 01:41:44 +03:00
|
|
|
|
/* Keyboard slide choreography: the composer (and the draft title, see
|
|
|
|
|
|
.oc-draft-center note below) are moved with INLINE transform/transition set
|
|
|
|
|
|
by useNativeMobileChrome — WebKit does not reliably start transitions when a
|
|
|
|
|
|
transform changes via a CSS custom property, which parked the composer until
|
|
|
|
|
|
the keyboard finished. Only the sliding backdrop lives here. */
|
2026-07-04 13:55:12 +03:00
|
|
|
|
:root.oc-capacitor-app.oc-kb-animating .oc-mobile-composer {
|
|
|
|
|
|
/* While sliding, the composer passes over chat content that hasn't moved (user
|
|
|
|
|
|
scrolled up) — give it an opaque backdrop so text doesn't show through the
|
|
|
|
|
|
form's transparent padding. */
|
|
|
|
|
|
background: var(--background);
|
|
|
|
|
|
}
|
2026-07-05 01:41:44 +03:00
|
|
|
|
|
|
|
|
|
|
/* Constant-viewport trick for the chat scroller: when the keyboard settles the
|
|
|
|
|
|
shell shrinks by --oc-kb-layout, but the scroller keeps its full height by
|
|
|
|
|
|
extending that far below its (shrunken) region — behind the composer and the
|
|
|
|
|
|
keyboard — and converts the covered strip into its own bottom padding. For
|
|
|
|
|
|
the virtualizer NOTHING resizes across keyboard transitions: clientHeight is
|
|
|
|
|
|
constant, every row that can become visible is already mounted, and the
|
|
|
|
|
|
open/close reposition is a single cheap scrollTop change (the settle re-pin /
|
|
|
|
|
|
the clamp) instead of a viewport resize that mounts rows with a visible
|
|
|
|
|
|
pause on long chats. */
|
|
|
|
|
|
:root.oc-capacitor-app .oc-mobile-app-shell .chat-scroll {
|
|
|
|
|
|
/* The shell shrinks by the keyboard height BUT also gives up its bottom
|
|
|
|
|
|
safe-area padding at the same moment (see the shell's padding-bottom
|
|
|
|
|
|
formula) — so the scroller's parent only loses (keyboard − safe). Extend
|
|
|
|
|
|
by exactly that, or the settle grows clientHeight by the safe inset and
|
|
|
|
|
|
the pinned chat clamps one row down a beat after the keyboard opens. */
|
|
|
|
|
|
bottom: calc(-1 * max(var(--oc-kb-layout, 0px) - var(--oc-app-bottom-safe, 0px), 0px));
|
|
|
|
|
|
/* The padding leg is driven by its own variable so it can be applied at the
|
|
|
|
|
|
very START of the keyboard rise (with an immediate re-pin — the bottom of
|
|
|
|
|
|
the chat moves up as the keyboard begins, not after it finishes), while
|
|
|
|
|
|
the bottom-extension leg still tracks the shell's settle-time shrink. Both
|
|
|
|
|
|
legs equal the keyboard height once settled, so the settle snap is
|
|
|
|
|
|
geometry-neutral for the scroller. */
|
|
|
|
|
|
padding-bottom: var(--oc-kb-scroll-inset, var(--oc-kb-layout, 0px));
|
2026-07-04 13:55:12 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 00:05:38 +03:00
|
|
|
|
/* WKWebView draws the text caret as a native layer that ignores CSS transforms:
|
|
|
|
|
|
after the keyboard rise it visibly flies from the pre-keyboard position to
|
|
|
|
|
|
the final one. Hide it during the transition (+ UIKit's reposition lag,
|
|
|
|
|
|
see oc-kb-caret-hold timing in useNativeMobileChrome) and pop it back in. */
|
|
|
|
|
|
:root.oc-capacitor-app.oc-kb-caret-hold textarea,
|
2026-07-27 22:21:38 +03:00
|
|
|
|
:root.oc-capacitor-app.oc-kb-caret-hold input,
|
|
|
|
|
|
:root.oc-capacitor-app.oc-kb-caret-hold [contenteditable] {
|
2026-07-05 00:05:38 +03:00
|
|
|
|
caret-color: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-27 22:21:38 +03:00
|
|
|
|
/* The composer is a CodeMirror editor, so it is neither a textarea nor an
|
|
|
|
|
|
input, and its caret is not the native one either: drawSelection() hides
|
|
|
|
|
|
that and paints a .cm-cursor element instead. Hiding caret-color alone
|
|
|
|
|
|
would leave that element riding across the screen. */
|
|
|
|
|
|
:root.oc-capacitor-app.oc-kb-caret-hold .cm-cursor,
|
|
|
|
|
|
:root.oc-capacitor-app.oc-kb-caret-hold .cm-dropCursor {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* On touch devices the composer re-enables the NATIVE caret with `!important`
|
|
|
|
|
|
(composerNativeSelectionTheme: iOS colours its selection handles from the
|
|
|
|
|
|
caret colour, so the caret cannot stay transparent). That declaration beats
|
|
|
|
|
|
the plain [contenteditable] rule above, so the caret-hold needs its own
|
|
|
|
|
|
heavier rule for the composer — !important plus more specificity than the
|
|
|
|
|
|
theme's `.cm-editor .cm-content` selector. */
|
|
|
|
|
|
:root.oc-capacitor-app.oc-kb-caret-hold .cm-editor .cm-content,
|
|
|
|
|
|
:root.oc-capacitor-app.oc-kb-caret-hold .cm-editor .cm-content .cm-line {
|
|
|
|
|
|
caret-color: transparent !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-04 13:55:12 +03:00
|
|
|
|
/* The composer keeps its 1rem bottom padding while the keyboard is down (breathing
|
|
|
|
|
|
room above the home indicator), but that gap looks artificial sitting right above
|
|
|
|
|
|
the keyboard — so tighten it while the keyboard is open. Snaps at the start of the
|
|
|
|
|
|
show animation (a ~10px pre-slide change; transitioning it would reflow the chat
|
|
|
|
|
|
every frame). */
|
2026-07-01 09:55:41 +03:00
|
|
|
|
:root.oc-capacitor-app.oc-keyboard-open .oc-mobile-composer {
|
2026-07-04 13:55:12 +03:00
|
|
|
|
padding-bottom: 12px;
|
2026-07-01 09:55:41 +03:00
|
|
|
|
}
|
2026-07-04 16:41:17 +03:00
|
|
|
|
|
2026-07-27 22:21:38 +03:00
|
|
|
|
/* Draft starter chips leave when the full composer takes over — with the
|
|
|
|
|
|
composer expanded (and hence the keyboard up) there is only room for the
|
|
|
|
|
|
draft title. oc-composer-expanded is set in the SAME frame as the pill swap
|
|
|
|
|
|
(see useMobileComposerShell), so the chips leave together with the morph;
|
|
|
|
|
|
the keyboard classes remain as fallbacks for keyboard-up states that do not
|
|
|
|
|
|
go through the pill (and for mobile browsers). Instant show/hide (no squish
|
|
|
|
|
|
animation); the title's own keyboard compensation (.oc-draft-center below)
|
2026-08-02 16:25:16 +03:00
|
|
|
|
carries the smooth motion.
|
|
|
|
|
|
|
|
|
|
|
|
A tablet has room to keep them: `oc-keep-draft-starters` is set by the shell
|
|
|
|
|
|
(portrait, or any orientation with a hardware keyboard — see MobileApp) and
|
|
|
|
|
|
opts the whole draft screen out of the hiding below. */
|
|
|
|
|
|
:root:not(.oc-keep-draft-starters).oc-composer-expanded .oc-draft-starters,
|
|
|
|
|
|
:root:not(.oc-keep-draft-starters).oc-capacitor-app.oc-keyboard-open .oc-draft-starters,
|
|
|
|
|
|
:root:not(.oc-keep-draft-starters).oc-browser-keyboard-open .oc-draft-starters {
|
2026-07-05 00:05:38 +03:00
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 01:41:44 +03:00
|
|
|
|
/* The draft title (.oc-draft-center) is vertically centered, so it has no
|
|
|
|
|
|
scroll-pinning to compensate the keyboard like the chat does. It rides the
|
|
|
|
|
|
keyboard choreography with an INLINE transform of half the keyboard shift
|
|
|
|
|
|
(exactly how far the center moves after the shell snap) — see the kb-movers
|
|
|
|
|
|
list in useNativeMobileChrome. No CSS rules needed here; the class only
|
|
|
|
|
|
marks the element for the mover query. */
|
2026-07-05 00:05:38 +03:00
|
|
|
|
|
2026-07-05 09:14:55 +03:00
|
|
|
|
/* Mobile-browser fullscreen composer (visual-viewport pinned form in
|
|
|
|
|
|
ChatInput): the header would otherwise paint above it — the form can't
|
|
|
|
|
|
out-stack it from inside the composer wrapper's z-10 context. */
|
|
|
|
|
|
:root.oc-browser-kb-fullscreen .oc-mobile-header {
|
|
|
|
|
|
visibility: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 01:25:28 +03:00
|
|
|
|
/* Installed PWA (standalone) safe areas. In the in-browser page Safari's own
|
|
|
|
|
|
chrome covers the home indicator and status bar, and the Capacitor shell has
|
|
|
|
|
|
its own choreography (--oc-app-bottom-safe + keyboard FLIP) — neither matches
|
|
|
|
|
|
display-mode: standalone, so this block is PWA-only and cannot regress them.
|
|
|
|
|
|
Standalone runs the page edge-to-edge (viewport-fit=cover), so the composer
|
|
|
|
|
|
needs real top/bottom safe-area room of its own. */
|
|
|
|
|
|
@media (display-mode: standalone) {
|
|
|
|
|
|
/* Bottom: reserve home-indicator room under the composer while the keyboard
|
|
|
|
|
|
is down. The generic .bottom-safe-area token is intentionally capped at
|
|
|
|
|
|
4px (visual tuck under the gradient overlay), which is not enough to keep
|
|
|
|
|
|
controls clear of the indicator / rounded corners. A fixed floor is
|
|
|
|
|
|
required: iOS 26 standalone reports env(safe-area-inset-bottom) as 0.
|
|
|
|
|
|
Cancel the room while the keyboard is up (the indicator area is behind the
|
|
|
|
|
|
keyboard, and the pinned form anchors by its offsetHeight). The class flip
|
|
|
|
|
|
is deliberately delayed (tap race, see ChatInput onBlur) — the transition
|
|
|
|
|
|
turns that late flip into a slide instead of a dip-then-pop. */
|
|
|
|
|
|
:root:not(.oc-capacitor-app) .oc-mobile-composer {
|
|
|
|
|
|
transition: padding-bottom 220ms cubic-bezier(0.33, 1, 0.68, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:root:not(.oc-capacitor-app):not(.oc-browser-keyboard-open) .oc-mobile-composer {
|
|
|
|
|
|
padding-bottom: calc(0.75rem + max(16px, var(--oc-safe-area-bottom, env(safe-area-inset-bottom, 0px)))) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Fullscreen composer: ChatInput pins the form to the visual viewport from
|
|
|
|
|
|
y = visualViewport.offsetTop, which is 0 in standalone (no URL bar), so the
|
|
|
|
|
|
form's first row lands under the status bar / Dynamic Island. Pad the
|
|
|
|
|
|
content down into the safe region instead. */
|
|
|
|
|
|
:root.oc-browser-kb-fullscreen:not(.oc-capacitor-app) .oc-mobile-composer {
|
|
|
|
|
|
padding-top: calc(var(--oc-safe-area-top, env(safe-area-inset-top, 0px)) + 0.5rem) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS standalone can leave 100dvh stuck at the keyboard-shrunk value after
|
|
|
|
|
|
the keyboard hides (WebKit doesn't reliably restore the dynamic viewport),
|
|
|
|
|
|
which shifted the whole shell up by that leftover. Standalone has no
|
|
|
|
|
|
collapsing browser chrome, so the dynamic and large viewports are the same
|
|
|
|
|
|
thing — pin the shell to 100lvh, which always reports full screen height.
|
|
|
|
|
|
Initial geometry is identical (lvh == dvh on load). */
|
2026-08-01 21:16:36 +03:00
|
|
|
|
:root:not(.oc-capacitor-app) .oc-mobile-app-shell {
|
2026-07-07 01:25:28 +03:00
|
|
|
|
height: 100lvh;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Top toasts: same offset the Capacitor shell already applies — without it
|
|
|
|
|
|
they render under the clock / Dynamic Island. */
|
|
|
|
|
|
:root:not(.oc-capacitor-app) [data-sonner-toaster][data-y-position='top'] {
|
|
|
|
|
|
top: calc(var(--oc-safe-area-top, env(safe-area-inset-top, 0px)) + 16px) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-04 16:41:17 +03:00
|
|
|
|
@keyframes oc-composer-morph-fade {
|
|
|
|
|
|
from { opacity: 0; }
|
|
|
|
|
|
to { opacity: 1; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Voice overlay variant: the overlay BACKGROUND appears immediately (it rides
|
|
|
|
|
|
the morphing shape), but its content stays hidden until the shape has mostly
|
|
|
|
|
|
finished growing, then fades in — no clipped controls mid-morph. Timings
|
|
|
|
|
|
follow the 340ms wrapper morph in ChatInput. */
|
|
|
|
|
|
.oc-composer-morph-content-fade > * {
|
|
|
|
|
|
animation: oc-composer-morph-fade 0.18s ease-out 0.26s both;
|
|
|
|
|
|
}
|