34c9ab7492
- Wrap composer + comments / composer + feed in a rounded muted panel with continuous border-primary rails. - Composer and campaign sidebar use a near-white surface with a brand-orange border; dark mode swaps to a near-black warm surface. - Per-article borders and backgrounds retinted via scoped selectors so NoteCard stays untouched globally. - Add hideCommentContext, authorBadge to NoteCard and renderAuthorBadge, leafCardClassName to ThreadedReplyList; move the Campaigner badge into the author row. - Light-mode tokens: --card to 0 0% 98%, --muted to 30 12% 94% so cards and muted regions read as real surface steps. - Drop the unused campaignsDetail.commentCount locale key everywhere.
785 lines
23 KiB
CSS
785 lines
23 KiB
CSS
@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
|
|
@layer base {
|
|
/*
|
|
* Agora's built-in color tokens. These are hardcoded and ship with the
|
|
* bundle — there is no runtime injection path, no remote theme loader,
|
|
* no localStorage-driven CSS-variable mutation. The only thing that
|
|
* changes at runtime is the `.dark` class on <html>, set by
|
|
* AppProvider based on the resolved theme preference.
|
|
*
|
|
* If you want to recolor the app, edit these values directly.
|
|
*/
|
|
:root {
|
|
--radius: 0.75rem;
|
|
/* Mobile chrome dimensions — referenced by layout utility classes below */
|
|
--top-bar-height: 2.5rem; /* MobileTopBar h-10 = 40px */
|
|
--bottom-nav-height: 2.75rem; /* MobileBottomNav h-11 = 44px */
|
|
|
|
/* ── Light theme tokens ───────────────────────────────────────────── */
|
|
--background: 0 0% 100%;
|
|
--foreground: 222 47% 11%;
|
|
/* Card surfaces sit one subtle step above the page. The 2%
|
|
luminance drop is barely perceptible in isolation but gives
|
|
every card real elevation against the pure-white background
|
|
without needing borders or shadows. */
|
|
--card: 0 0% 98%;
|
|
--card-foreground: 222 47% 11%;
|
|
--popover: 0 0% 100%;
|
|
--popover-foreground: 222 47% 11%;
|
|
--primary: 24 100% 50%;
|
|
--primary-foreground: 0 0% 100%;
|
|
--secondary: 0 0% 96%;
|
|
--secondary-foreground: 222 47% 11%;
|
|
/* Muted surfaces (input fields, skeleton fills, comment-region
|
|
wraps, etc.) are a genuine luminance step away from background
|
|
so they read as a distinct surface in light mode. A very faint
|
|
warm hue ties them to the brand without coloring the page. */
|
|
--muted: 30 12% 94%;
|
|
--muted-foreground: 222 17% 46%;
|
|
--accent: 0 0% 92%;
|
|
--accent-foreground: 222 47% 11%;
|
|
--destructive: 0 84.2% 60.2%;
|
|
--destructive-foreground: 210 40% 98%;
|
|
--border: 24 50% 82%;
|
|
--input: 24 50% 82%;
|
|
--ring: 24 100% 50%;
|
|
--success: 142 72% 29%;
|
|
--success-foreground: 138 60% 94%;
|
|
}
|
|
|
|
.dark {
|
|
--background: 0 0% 10%;
|
|
--foreground: 0 0% 95%;
|
|
--card: 0 0% 12%;
|
|
--card-foreground: 0 0% 95%;
|
|
--popover: 0 0% 12%;
|
|
--popover-foreground: 0 0% 95%;
|
|
--primary: 24 100% 50%;
|
|
--primary-foreground: 0 0% 100%;
|
|
--secondary: 0 0% 18%;
|
|
--secondary-foreground: 0 0% 95%;
|
|
--muted: 0 0% 18%;
|
|
--muted-foreground: 0 0% 65%;
|
|
--accent: 0 0% 24%;
|
|
--accent-foreground: 0 0% 95%;
|
|
--destructive: 0 72% 51%;
|
|
--destructive-foreground: 0 0% 95%;
|
|
--border: 24 40% 30%;
|
|
--input: 24 40% 30%;
|
|
--ring: 24 100% 50%;
|
|
--success: 142 60% 22%;
|
|
--success-foreground: 138 60% 94%;
|
|
}
|
|
}
|
|
|
|
@layer base {
|
|
* {
|
|
@apply border-border;
|
|
scrollbar-width: none;
|
|
}
|
|
|
|
*::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
body {
|
|
@apply bg-background text-foreground;
|
|
}
|
|
}
|
|
|
|
/* ── Han display-font overrides for Chinese (zh, zh-Hant, zh-TW, zh-HK) ────
|
|
Bebas Neue (the `.font-display` family) ships only Latin glyphs, so
|
|
Chinese hero text falls back to system fonts and loses its industrial
|
|
display character. Swap in Noto Sans TC weight 900 for any element
|
|
using `.font-display` while the page language is a Chinese variant.
|
|
|
|
Tailwind's `italic`, `uppercase`, and `tracking-*` utilities are
|
|
applied directly to those hero elements; we reverse them here because
|
|
Han text doesn't have a meaningful italic form, never uppercases, and
|
|
wants tighter (not wider) letter-spacing. The `:lang()` selector has
|
|
higher specificity (0,1,1) than a single utility class (0,1,0) so
|
|
these rules win without `!important`.
|
|
|
|
`-webkit-text-stroke` is neutralized because the hero h1 fattens its
|
|
weight with a 0.022em stroke (Bebas Neue only ships weight 400); at
|
|
Noto Sans TC weight 900 the stroke trick muddies CJK strokes. */
|
|
:root[lang|="zh"] .font-display {
|
|
font-family: 'Noto Sans TC', 'PingFang TC', 'PingFang SC', 'Microsoft JhengHei', 'Microsoft YaHei', 'Heiti TC', sans-serif;
|
|
font-weight: 900;
|
|
font-style: normal;
|
|
letter-spacing: 0;
|
|
text-transform: none;
|
|
-webkit-text-stroke: 0;
|
|
}
|
|
|
|
@layer utilities {
|
|
/* ── Safe-area inset utilities ────────────────────────────────────────────
|
|
Use var(--safe-area-inset-*, …) as the outer wrapper so that
|
|
Capacitor's SystemBars plugin (which injects --safe-area-inset-* CSS
|
|
variables on Android) takes precedence when available. The inner
|
|
env(safe-area-inset-*, 0px) is the standard fallback for iOS / web. */
|
|
|
|
.safe-area-top {
|
|
padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
|
|
}
|
|
|
|
.safe-area-bottom {
|
|
padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
|
|
}
|
|
|
|
.safe-area-inset-top {
|
|
top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
|
|
}
|
|
|
|
.safe-area-inset-bottom {
|
|
bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
|
|
}
|
|
|
|
.overflow-wrap-anywhere {
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
/* FAB bottom offset: no bottom nav, only clear safe area on native shells. */
|
|
.bottom-fab {
|
|
bottom: calc(2rem + var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
|
|
}
|
|
|
|
/* Position above mobile bottom nav + safe area + arc overhang (28px) */
|
|
.bottom-mobile-nav {
|
|
bottom: calc(var(--bottom-nav-height) + 28px + var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
|
|
}
|
|
|
|
/* Bottom overscroll padding for the center column:
|
|
clears the mobile bottom nav + safe area + generous extra space
|
|
so content can be scrolled well past the bottom bar */
|
|
.pb-overscroll {
|
|
padding-bottom: calc(10vh + var(--bottom-nav-height) + var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
|
|
}
|
|
|
|
@media (min-width: 900px) {
|
|
.pb-overscroll {
|
|
padding-bottom: 10vh;
|
|
}
|
|
}
|
|
|
|
/* Mobile top bar height + safe area inset for sticky elements */
|
|
.top-mobile-bar {
|
|
top: calc(var(--top-bar-height) + var(--safe-area-inset-top, env(safe-area-inset-top, 0px)));
|
|
}
|
|
|
|
/* New-posts pill: just below the SubHeaderBar on both mobile and desktop */
|
|
.new-posts-pill {
|
|
top: calc(var(--top-bar-height) + var(--safe-area-inset-top, env(safe-area-inset-top, 0px)) + 3.5rem);
|
|
}
|
|
@media (min-width: 900px) {
|
|
.new-posts-pill {
|
|
top: 4rem;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* Slide the sub-header bar out of view together with the mobile top bar.
|
|
Must clear its own height (100%) + top bar + safe area + arc overhang (20px). */
|
|
@media (max-width: 899px) {
|
|
.nav-hidden-slide {
|
|
transform: translateY(calc(-100% - var(--top-bar-height) - 20px - var(--safe-area-inset-top, env(safe-area-inset-top, 0px))));
|
|
}
|
|
}
|
|
|
|
/* Negative margin to pull content area up behind the mobile top bar (only when it's visible) */
|
|
@media (max-width: 899px) {
|
|
.-mt-mobile-bar {
|
|
margin-top: calc(-1 * var(--top-bar-height) - var(--safe-area-inset-top, env(safe-area-inset-top, 0px)));
|
|
padding-top: calc(var(--top-bar-height) + var(--safe-area-inset-top, env(safe-area-inset-top, 0px)));
|
|
}
|
|
}
|
|
|
|
/* AI chat height on mobile: full viewport minus top bar, extends behind bottom nav.
|
|
Padding-bottom keeps input above the nav. */
|
|
.ai-chat-height {
|
|
height: calc(100dvh - var(--top-bar-height) - var(--safe-area-inset-top, env(safe-area-inset-top, 0px)));
|
|
padding-bottom: calc(var(--bottom-nav-height) + var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
|
|
}
|
|
@media (min-width: 900px) {
|
|
.ai-chat-height {
|
|
padding-bottom: 0;
|
|
}
|
|
}
|
|
|
|
/* Live stream page height on mobile: full viewport minus top bar, bottom nav, and safe-area insets */
|
|
.livestream-height {
|
|
height: calc(100dvh - var(--top-bar-height) - var(--bottom-nav-height) - var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
|
|
max-height: calc(100dvh - var(--top-bar-height) - var(--bottom-nav-height) - var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
|
|
}
|
|
|
|
/* Vine feed slide height: full viewport on mobile (top bar + bottom nav are
|
|
both hidden for an immersive experience). Tab bar floats over video. */
|
|
.vine-slide-height {
|
|
height: 100dvh;
|
|
}
|
|
|
|
/* World map height: full viewport minus mobile chrome (top bar, bottom nav,
|
|
safe-area insets). On desktop where neither chrome is present, fills the
|
|
full viewport. The bottom nav is preserved on mobile (it's primary nav)
|
|
and the discovery drawer floats above it. */
|
|
.world-map-height {
|
|
height: calc(100dvh - var(--top-bar-height) - var(--safe-area-inset-top, env(safe-area-inset-top, 0px)) - var(--bottom-nav-height) - var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
|
|
}
|
|
@media (min-width: 900px) {
|
|
.world-map-height {
|
|
height: 100dvh;
|
|
}
|
|
}
|
|
|
|
/* Anchor world-page floating overlays (the discovery launcher button and
|
|
the docked desktop drawer slot) above the mobile bottom nav so the
|
|
bottom-nav stays usable. On desktop there's no bottom nav, so they
|
|
anchor to the viewport bottom. */
|
|
.world-drawer-anchor {
|
|
bottom: 0;
|
|
}
|
|
@media (max-width: 899px) {
|
|
.world-drawer-anchor {
|
|
bottom: calc(var(--bottom-nav-height) + var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
|
|
}
|
|
}
|
|
}
|
|
|
|
@keyframes spin {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* 3-burst ping: pulses 3 times then stops */
|
|
@keyframes ping-3 {
|
|
0% { transform: scale(1); opacity: 0.6; }
|
|
11% { transform: scale(2); opacity: 0; }
|
|
22% { transform: scale(1); opacity: 0; }
|
|
33% { transform: scale(1); opacity: 0.6; }
|
|
44% { transform: scale(2); opacity: 0; }
|
|
55% { transform: scale(1); opacity: 0; }
|
|
66% { transform: scale(1); opacity: 0.6; }
|
|
77% { transform: scale(2); opacity: 0; }
|
|
100% { transform: scale(1); opacity: 0; }
|
|
}
|
|
|
|
.animate-ping-3 {
|
|
animation: ping-3 4s ease-out forwards;
|
|
}
|
|
|
|
/* Eyeball blink animation - eyelid sweeps down then back up */
|
|
@keyframes eyelid-blink {
|
|
0% { transform: translateY(-100%); }
|
|
50% { transform: translateY(0%); }
|
|
100% { transform: translateY(-100%); }
|
|
}
|
|
|
|
.animate-eyelid-blink {
|
|
animation: eyelid-blink 0.5s cubic-bezier(0.4, 0, 0.6, 1) forwards;
|
|
}
|
|
|
|
/* Landing hero staggered fade-in */
|
|
@keyframes landing-hero-fade-in {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(8px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.landing-hero-fade {
|
|
opacity: 0;
|
|
animation: landing-hero-fade-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
}
|
|
|
|
@keyframes nudge-cancel-fade-in {
|
|
from { opacity: 0; }
|
|
to { opacity: 0.6; }
|
|
}
|
|
|
|
/* ── NIP-62 Request to Vanish ───────────────────────────────────────────────── */
|
|
|
|
/* Animated caution stripes — red/dark diagonal bands that scroll */
|
|
.vanish-stripes {
|
|
background: repeating-linear-gradient(
|
|
-45deg,
|
|
hsl(0 72% 51% / 0.7),
|
|
hsl(0 72% 51% / 0.7) 10px,
|
|
hsl(0 72% 51% / 0.25) 10px,
|
|
hsl(0 72% 51% / 0.25) 20px
|
|
);
|
|
background-size: 28.28px 28.28px;
|
|
animation: vanish-stripe-scroll 1.5s linear infinite;
|
|
}
|
|
|
|
:is(.dark) .vanish-stripes {
|
|
background: repeating-linear-gradient(
|
|
-45deg,
|
|
hsl(0 72% 51% / 0.6),
|
|
hsl(0 72% 51% / 0.6) 10px,
|
|
hsl(0 72% 51% / 0.15) 10px,
|
|
hsl(0 72% 51% / 0.15) 20px
|
|
);
|
|
background-size: 28.28px 28.28px;
|
|
animation: vanish-stripe-scroll 1.5s linear infinite;
|
|
}
|
|
|
|
@keyframes vanish-stripe-scroll {
|
|
from { background-position: 0 0; }
|
|
to { background-position: 28.28px 0; }
|
|
}
|
|
|
|
/* Glitch text effect on the /// symbol */
|
|
.vanish-glitch-text {
|
|
position: relative;
|
|
animation: vanish-glitch 3s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes vanish-glitch {
|
|
0%, 90%, 100% { opacity: 1; transform: translate(0); }
|
|
92% { opacity: 0.8; transform: translate(-2px, 1px); }
|
|
94% { opacity: 0.6; transform: translate(2px, -1px); }
|
|
96% { opacity: 0.9; transform: translate(-1px, -1px); }
|
|
98% { opacity: 1; transform: translate(1px, 0); }
|
|
}
|
|
|
|
/* Ring pulse animation for the icon */
|
|
.vanish-ring-pulse {
|
|
animation: vanish-ring 3s ease-out infinite;
|
|
}
|
|
|
|
.vanish-ring-pulse-delayed {
|
|
animation: vanish-ring 3s ease-out 1.5s infinite;
|
|
}
|
|
|
|
@keyframes vanish-ring {
|
|
0% { transform: scale(1); opacity: 0.4; }
|
|
50% { transform: scale(1.15); opacity: 0; }
|
|
100% { transform: scale(1); opacity: 0; }
|
|
}
|
|
|
|
/* Slow blink for the status dot */
|
|
.vanish-blink {
|
|
animation: vanish-blink-kf 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes vanish-blink-kf {
|
|
0%, 40%, 100% { opacity: 1; }
|
|
50%, 90% { opacity: 0.2; }
|
|
}
|
|
|
|
/* ── Wii Mail envelope grid ─────────────────────────────────────────────────── */
|
|
|
|
/* Staggered entrance: envelopes pop in one by one.
|
|
Keep the animation short and avoid large rotations so the tap target
|
|
doesn't shift while the user is already trying to interact. */
|
|
@keyframes envelope-entrance {
|
|
0% { opacity: 0; transform: scale(0.85) translateY(12px); }
|
|
70% { opacity: 1; transform: scale(1.02) translateY(-1px); }
|
|
100% { opacity: 1; transform: scale(1) translateY(0); }
|
|
}
|
|
|
|
.envelope-card {
|
|
animation: envelope-entrance 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
|
animation-delay: var(--entrance-delay, 0ms);
|
|
transform-origin: center bottom;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Hover wobble — playful Wii-style oscillation */
|
|
@keyframes envelope-wobble {
|
|
0% { transform: rotate(0deg); }
|
|
15% { transform: rotate(-5deg); }
|
|
30% { transform: rotate(4deg); }
|
|
45% { transform: rotate(-3deg); }
|
|
60% { transform: rotate(2deg); }
|
|
75% { transform: rotate(-1deg); }
|
|
100% { transform: rotate(0deg); }
|
|
}
|
|
|
|
.envelope-body {
|
|
transition: transform 0.15s ease;
|
|
}
|
|
|
|
/* Wobble only on true pointer hover — skip on touch where :hover sticks
|
|
and the rotation shifts the tap target, making repeated taps miss. */
|
|
@media (hover: hover) and (pointer: fine) {
|
|
.envelope-card:hover .envelope-body,
|
|
.envelope-card:focus-visible .envelope-body {
|
|
animation: envelope-wobble 0.5s ease-in-out;
|
|
}
|
|
}
|
|
|
|
.envelope-card:active .envelope-body {
|
|
transform: scale(0.95);
|
|
animation: none;
|
|
}
|
|
|
|
/* Envelope skeleton shimmer */
|
|
@keyframes envelope-skeleton-shimmer {
|
|
0% { opacity: 0.4; }
|
|
50% { opacity: 0.7; }
|
|
100% { opacity: 0.4; }
|
|
}
|
|
|
|
.envelope-skeleton {
|
|
animation: envelope-skeleton-shimmer 1.5s ease-in-out infinite;
|
|
}
|
|
|
|
/* Reduced motion: disable envelope animations */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.envelope-card {
|
|
animation: none;
|
|
opacity: 1;
|
|
}
|
|
.envelope-card:hover .envelope-body,
|
|
.envelope-card:focus-visible .envelope-body {
|
|
animation: none;
|
|
}
|
|
}
|
|
|
|
/* Drawn SVG stickers — ensure inline SVGs fill their container */
|
|
[data-sticker] svg,
|
|
.sticker-svg-wrap svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
}
|
|
|
|
/* Letter send animation keyframes */
|
|
@keyframes letter-send-fade-up {
|
|
from { opacity: 0; transform: translateY(8px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
@keyframes letter-send-avatar-in {
|
|
0% { transform: scale(0); opacity: 0; }
|
|
50% { transform: scale(1.15); opacity: 1; }
|
|
70% { transform: scale(0.92); }
|
|
85% { transform: scale(1.04); }
|
|
100% { transform: scale(1); opacity: 1; }
|
|
}
|
|
|
|
@keyframes letter-send-burst {
|
|
0% { transform: translate(0, 0) rotate(0deg) scale(0); opacity: 0; }
|
|
15% { opacity: 1; transform: translate(0, 0) rotate(0deg) scale(1); }
|
|
60% { opacity: 0.8; transform: translate(var(--burst-tx), var(--burst-ty)) rotate(var(--burst-rot)) scale(1); }
|
|
100% { opacity: 0; transform: translate(var(--burst-tx), var(--burst-ty)) rotate(var(--burst-rot)) scale(0.4); }
|
|
}
|
|
|
|
/* Reduced motion: disable all vanish animations */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.vanish-stripes {
|
|
animation: none;
|
|
}
|
|
.vanish-glitch-text {
|
|
animation: none;
|
|
}
|
|
.vanish-ring-pulse,
|
|
.vanish-ring-pulse-delayed {
|
|
animation: none;
|
|
opacity: 0;
|
|
}
|
|
.vanish-blink {
|
|
animation: none;
|
|
}
|
|
}
|
|
|
|
/* ─── Floating Music Notes ───────────────────────────────────────────────────── */
|
|
|
|
@keyframes float-up {
|
|
0% {
|
|
opacity: 0;
|
|
transform: translateY(0) scale(0.8);
|
|
}
|
|
15% {
|
|
opacity: 1;
|
|
transform: translateY(-10px) scale(1);
|
|
}
|
|
85% {
|
|
opacity: 0.8;
|
|
transform: translateY(-60px) scale(1);
|
|
}
|
|
100% {
|
|
opacity: 0;
|
|
transform: translateY(-80px) scale(0.6);
|
|
}
|
|
}
|
|
|
|
.animate-float-up {
|
|
animation: float-up 3s ease-out infinite;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.animate-float-up {
|
|
animation: none !important;
|
|
opacity: 0 !important;
|
|
}
|
|
}
|
|
|
|
/* ── Milkdown Editor Styles ─────────────────────────────────────────────────── */
|
|
|
|
.milkdown-editor .milkdown {
|
|
@apply outline-none;
|
|
}
|
|
|
|
.milkdown-editor .editor {
|
|
@apply outline-none min-h-[400px] p-3;
|
|
font-size: 1.125rem;
|
|
line-height: 1.75;
|
|
}
|
|
|
|
.milkdown-editor .ProseMirror {
|
|
@apply outline-none min-h-[400px];
|
|
}
|
|
|
|
.milkdown-editor .ProseMirror:focus {
|
|
@apply outline-none;
|
|
}
|
|
|
|
/* Headings */
|
|
.milkdown-editor h1 {
|
|
@apply text-3xl font-bold mt-6 mb-4;
|
|
}
|
|
|
|
.milkdown-editor h2 {
|
|
@apply text-2xl font-semibold mt-5 mb-3;
|
|
}
|
|
|
|
.milkdown-editor h3 {
|
|
@apply text-xl font-semibold mt-4 mb-2;
|
|
}
|
|
|
|
.milkdown-editor h4 {
|
|
@apply text-lg font-medium mt-3 mb-2;
|
|
}
|
|
|
|
/* Inline styles */
|
|
.milkdown-editor strong {
|
|
@apply font-bold;
|
|
}
|
|
|
|
.milkdown-editor em {
|
|
@apply italic;
|
|
}
|
|
|
|
.milkdown-editor del {
|
|
@apply line-through text-muted-foreground;
|
|
}
|
|
|
|
.milkdown-editor code {
|
|
@apply bg-muted px-1.5 py-0.5 rounded text-sm font-mono;
|
|
}
|
|
|
|
/* Block elements */
|
|
.milkdown-editor p {
|
|
@apply my-1.5;
|
|
}
|
|
|
|
.milkdown-editor blockquote {
|
|
@apply border-l-4 border-primary/40 pl-4 my-4 italic text-muted-foreground;
|
|
}
|
|
|
|
.milkdown-editor pre {
|
|
@apply bg-muted rounded-lg p-4 my-4 overflow-x-auto;
|
|
}
|
|
|
|
.milkdown-editor pre code {
|
|
@apply bg-transparent p-0 font-mono;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
/* Lists */
|
|
.milkdown-editor ul {
|
|
@apply list-disc list-inside my-3 space-y-1;
|
|
}
|
|
|
|
.milkdown-editor ol {
|
|
@apply list-decimal list-inside my-3 space-y-1;
|
|
}
|
|
|
|
.milkdown-editor li {
|
|
@apply pl-1;
|
|
}
|
|
|
|
.milkdown-editor li p {
|
|
@apply inline my-0;
|
|
}
|
|
|
|
/* Task lists (GFM) */
|
|
.milkdown-editor ul.task-list {
|
|
@apply list-none pl-0;
|
|
}
|
|
|
|
.milkdown-editor li.task-list-item {
|
|
@apply flex items-start gap-2 pl-0;
|
|
}
|
|
|
|
.milkdown-editor li.task-list-item input[type="checkbox"] {
|
|
@apply mt-1.5 h-4 w-4 rounded border-border;
|
|
}
|
|
|
|
/* Links */
|
|
.milkdown-editor a {
|
|
@apply text-primary underline underline-offset-2 hover:text-primary/80 transition-colors;
|
|
}
|
|
|
|
/* Horizontal rule */
|
|
.milkdown-editor hr {
|
|
@apply my-6 border-border;
|
|
}
|
|
|
|
/* Images */
|
|
.milkdown-editor img {
|
|
@apply max-w-full h-auto rounded-lg my-4;
|
|
}
|
|
|
|
/* Tables (GFM) */
|
|
.milkdown-editor table {
|
|
@apply w-full border-collapse my-4;
|
|
}
|
|
|
|
.milkdown-editor th,
|
|
.milkdown-editor td {
|
|
@apply border border-border px-3 py-2 text-left;
|
|
}
|
|
|
|
.milkdown-editor th {
|
|
@apply bg-muted font-semibold;
|
|
}
|
|
|
|
.milkdown-editor tr:nth-child(even) {
|
|
@apply bg-muted/30;
|
|
}
|
|
|
|
|
|
|
|
/* Upload placeholder */
|
|
.milkdown-upload-placeholder {
|
|
@apply inline-flex items-center gap-2 px-3 py-1.5 bg-muted/50 rounded-md text-sm text-muted-foreground;
|
|
}
|
|
|
|
.milkdown-upload-placeholder::before {
|
|
content: '';
|
|
@apply w-4 h-4 border-2 border-muted-foreground/30 border-t-primary rounded-full animate-spin;
|
|
}
|
|
|
|
/* Placeholder — only when unfocused AND content is empty. */
|
|
.milkdown-editor .ProseMirror:not(:focus):not(.has-content) > p:first-child::before {
|
|
@apply text-muted-foreground pointer-events-none float-left h-0;
|
|
content: var(--ph, '');
|
|
}
|
|
|
|
/* Milkdown content area */
|
|
.milkdown-editor .milkdown-content .ProseMirror {
|
|
@apply min-h-[350px];
|
|
}
|
|
|
|
/* Room navigation arrow nudge — subtle horizontal pulse */
|
|
@keyframes room-arrow-nudge-left {
|
|
0%, 100% { transform: translateX(0); }
|
|
50% { transform: translateX(-3px); }
|
|
}
|
|
|
|
@keyframes room-arrow-nudge-right {
|
|
0%, 100% { transform: translateX(0); }
|
|
50% { transform: translateX(3px); }
|
|
}
|
|
|
|
/* ── Overstimulation block overlay ─────────────────────────────────────────── */
|
|
|
|
@keyframes overstim-vignette-in {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
@keyframes overstim-vignette-out {
|
|
from { opacity: 1; }
|
|
to { opacity: 0; }
|
|
}
|
|
|
|
@keyframes overstim-shockwave {
|
|
0% { transform: scale(0.1); opacity: 1; }
|
|
60% { opacity: 0.6; }
|
|
100% { transform: scale(2.2); opacity: 0; }
|
|
}
|
|
|
|
@keyframes vomit-fall {
|
|
0% { transform: translate(-50%, -100%) translate(0, 0) scale(1); opacity: 1; }
|
|
100% { transform: translate(-50%, -100%) translate(var(--vomit-dx), var(--vomit-dy)) scale(1); opacity: 1; }
|
|
}
|
|
|
|
/* Override global scrollbar-hiding for elements that need a visible thin scrollbar */
|
|
.scrollbar-thin {
|
|
scrollbar-width: thin;
|
|
}
|
|
.scrollbar-thin::-webkit-scrollbar {
|
|
display: block;
|
|
height: 4px;
|
|
}
|
|
.scrollbar-thin::-webkit-scrollbar-thumb {
|
|
background: hsl(var(--muted-foreground) / 0.3);
|
|
border-radius: 9999px;
|
|
}
|
|
.scrollbar-thin::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
/* Slow Ken-Burns-style pan for full-bleed hero images. The image is
|
|
oversized (scale 1.12) so the translation never reveals the edge.
|
|
Used by HeroBanner on Communities/Actions/Guide pages. */
|
|
@keyframes heroPanRight {
|
|
0% { transform: scale(1.12) translateX(-1.5%); }
|
|
50% { transform: scale(1.12) translateX(1.5%); }
|
|
100% { transform: scale(1.12) translateX(-1.5%); }
|
|
}
|
|
@keyframes heroPanLeft {
|
|
0% { transform: scale(1.12) translateX(1.5%); }
|
|
50% { transform: scale(1.12) translateX(-1.5%); }
|
|
100% { transform: scale(1.12) translateX(1.5%); }
|
|
}
|
|
.hero-pan-right { animation: heroPanRight 28s ease-in-out infinite; }
|
|
.hero-pan-left { animation: heroPanLeft 28s ease-in-out infinite; }
|
|
|
|
/* Lightning-arc flow on the home hero. Each path is dashed and the
|
|
dash offset animates by exactly one full dash period, so the loop
|
|
point is visually identical to the start point — no snap, no
|
|
visible restart. Dash sizes are in screen pixels because the arcs
|
|
use `vector-effect: non-scaling-stroke`. */
|
|
@keyframes heroArcFlow {
|
|
0% { stroke-dashoffset: 0; }
|
|
100% { stroke-dashoffset: -16; }
|
|
}
|
|
.hero-arc-flow {
|
|
stroke-dasharray: 6 10;
|
|
animation: heroArcFlow 2.4s linear infinite;
|
|
}
|
|
|
|
/* City-node pulse used on the home hero. The dot itself doesn't move;
|
|
we just breathe the halo's opacity so the location reads as a live
|
|
beacon. Offsets are staggered per-node from the JSX so the field
|
|
pulses organically rather than in lockstep. */
|
|
@keyframes heroNodePulse {
|
|
0% { opacity: 0.35; transform: scale(0.85); }
|
|
50% { opacity: 0.95; transform: scale(1.15); }
|
|
100% { opacity: 0.35; transform: scale(0.85); }
|
|
}
|
|
.hero-node-pulse {
|
|
transform-origin: center;
|
|
transform-box: fill-box;
|
|
animation: heroNodePulse 3.2s ease-in-out infinite;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.hero-pan-right, .hero-pan-left { animation: none; }
|
|
.hero-arc-flow { animation: none; stroke-dasharray: none; }
|
|
.hero-node-pulse { animation: none; opacity: 0.7; }
|
|
}
|