f811245f90
Remove the spellbook-themed settings index: drop the "Codex of Configuration" heading, the gradient ornaments with ✦/◆ dividers, the sigil that appeared after two minutes of inactivity, and the IntroImage illustration tiles on every section row and sub-page intro block. The index is now a flat divider-separated list of labels and one-line descriptions, with breathing room on both sides. Delete the Magic settings page, its CursorFireEffect overlay, the animate-sigil-glow / animate-pulse-slow keyframes, the magicMouse AppConfig flag (schema, default, test fixture), and the /settings/magic route. Delete the now-unreferenced IntroImage component and the ten *-intro.png assets it masked. Disable content types that don't fit an activist tool by default: vines, treasures (geocaches + found logs), colors, decks, webxdc, birdstar (detections / birdex / constellations), emoji packs, custom emojis, user statuses, music, podcasts, and development. They remain available in settings — just off out of the box. Highlights is bumped on by default to pair with Articles. Posts, comments, reposts, articles, highlights, events, polls, communities, people lists, badges, photos, videos, and voice messages stay on.
761 lines
21 KiB
CSS
761 lines
21 KiB
CSS
@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
|
|
@layer base {
|
|
:root {
|
|
--radius: 0.75rem;
|
|
--success: 142 72% 29%;
|
|
--success-foreground: 138 60% 94%;
|
|
/* 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 */
|
|
}
|
|
|
|
.dark {
|
|
--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;
|
|
}
|
|
}
|
|
|
|
@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)));
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Leaflet control theming — override the default white/gray controls to
|
|
match the app's themed palette. The default `leaflet/dist/leaflet.css`
|
|
ships with hardcoded `background: white` / dark border colors that
|
|
clash with our themed surfaces (especially in dark mode and on the
|
|
`/world` map where controls sit on top of the dark Positron tiles).
|
|
|
|
We re-style only the controls; tile rendering and marker popovers are
|
|
untouched. Hover state uses the primary color so the controls match
|
|
the same orange accent as buttons elsewhere. */
|
|
.leaflet-bar {
|
|
border: 1px solid hsl(var(--border)) !important;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18) !important;
|
|
border-radius: calc(var(--radius) - 0.25rem) !important;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.leaflet-bar a,
|
|
.leaflet-bar a:hover,
|
|
.leaflet-bar a:focus {
|
|
background-color: hsl(var(--background) / 0.95) !important;
|
|
color: hsl(var(--foreground)) !important;
|
|
border-bottom: 1px solid hsl(var(--border)) !important;
|
|
backdrop-filter: blur(4px);
|
|
transition: background-color 120ms, color 120ms;
|
|
}
|
|
|
|
.leaflet-bar a:last-child {
|
|
border-bottom: none !important;
|
|
}
|
|
|
|
.leaflet-bar a:hover,
|
|
.leaflet-bar a:focus {
|
|
background-color: hsl(var(--primary)) !important;
|
|
color: hsl(var(--primary-foreground)) !important;
|
|
}
|
|
|
|
.leaflet-bar a.leaflet-disabled {
|
|
background-color: hsl(var(--muted)) !important;
|
|
color: hsl(var(--muted-foreground) / 0.5) !important;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* Attribution control (hidden on /world via `attributionControl={false}`,
|
|
but kept consistent for any other map usage). */
|
|
.leaflet-control-attribution {
|
|
background-color: hsl(var(--background) / 0.85) !important;
|
|
color: hsl(var(--muted-foreground)) !important;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
.leaflet-control-attribution a {
|
|
color: hsl(var(--primary)) !important;
|
|
}
|
|
|
|
@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. */
|
|
@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; }
|
|
|
|
/* Slow breathing glow used on the hero globe's outer halo so the beacon
|
|
feels alive rather than statically pasted. The radius itself doesn't
|
|
change — only opacity — so the layout stays rock-stable. Range is
|
|
intentionally narrow + low so the halo is felt, not seen. */
|
|
@keyframes heroGlobeHaloBreath {
|
|
0% { opacity: 0.32; }
|
|
50% { opacity: 0.55; }
|
|
100% { opacity: 0.32; }
|
|
}
|
|
.hero-globe-halo-breath { animation: heroGlobeHaloBreath 6.5s ease-in-out infinite; }
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.hero-pan-right, .hero-pan-left { animation: none; }
|
|
.hero-globe-halo-breath { animation: none; opacity: 0.42; }
|
|
}
|
|
|
|
/* Dark drop-shadow for hero copy — photo backgrounds are always colorful,
|
|
so we want a dark shadow regardless of light/dark theme. */
|
|
.hero-text-shadow {
|
|
text-shadow:
|
|
0 2px 8px rgba(0, 0, 0, 0.8),
|
|
0 0 20px rgba(0, 0, 0, 0.45);
|
|
}
|
|
|
|
.hero-text-shadow-soft {
|
|
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.65);
|
|
}
|
|
|
|
/* On mobile the globe sits under the headline — soften its left edge. */
|
|
.hero-globe-mask {
|
|
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 48%, black 100%);
|
|
mask-image: linear-gradient(to right, transparent 0%, black 48%, black 100%);
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.hero-globe-mask {
|
|
-webkit-mask-image: none;
|
|
mask-image: none;
|
|
}
|
|
}
|