Files
eranos/src/index.css
T
Chad Curtis f811245f90 Make settings utilitarian and disable whimsical content types by default
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.
2026-05-21 13:22:57 -05:00

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;
}
}