Files
eranos/src/index.css
T
Alex Gleason 740fc1c63c Merge ditto/main into agora
Pulls in 387 commits from ditto/main while preserving Agora-specific
features. Where the two codebases diverged on the same concept, kept
the Agora side per project direction.

Kept Agora-specific:
- SparkWallet stack (over Ditto's nostr-derived Bitcoin wallet)
- Communities (NIP-72 + chat + members), Messages, Organizers,
  Actions, Verified, Appearance settings
- DMProviderWrapper, country/organizer moderation in NoteMoreMenu
- 'Agora' branding, pub.agora.app bundle ID, version 2.8.0
- Built-in theme system (src/themes.ts) only

Rejected from Ditto:
- All Blobbi virtual pet code (80+ files, route, provider, sidebar,
  kind labels, feed setting, NIP.md entries, CSS animations)
- Custom theme events (kinds 36767/16767) — ThemesPage, ThemeContent,
  active profile themes, theme snapshot recovery
- On-chain zaps (kind 8333) and the entire Bitcoin wallet implementation
  (useBitcoinWallet, bitcoin-signers, BitcoinContentHeader,
  bitcoinjs-lib / @bitcoinerlab/secp256k1 / ecpair / tiny-secp256k1)
- ZapSuccessScreen (depended on dropped bitcoin lib)

Pulled in from Ditto:
- .agents/skills/* (12 new specialized skills, slim AGENTS.md)
- @nostrify bumps to 0.52 / 0.6 / 0.37
- New routes/pages: Music, Podcasts, Videos, Vines, Wikipedia, Books,
  Bluesky, Archive, AIChat, Trends, Webxdc, Highlights, Decks, Emojis,
  Development, Treasures, Colors, Packs
- Birdstar feed integration (kinds 2473, 12473, 30621)
- Wikipedia/Wikidata/Scryfall lookup in ExternalContentPage
- release-notes CI job + extract-release-notes.mjs script
- nsite:// URI handling in feed/sidebar
- iOS fastlane setup
- src/lib/avatarShape.ts + Avatar shape prop (kept for new Music/People
  components that depend on it)

Preserved Agora's ABSOLUTE 'NEVER COMMIT' rule at the top of AGENTS.md
and dropped Ditto's contradicting 'Commit at the end of every task'
section.

Validation: npm run test passes (tsc, eslint, 40/40 vitest, vite build).
2026-05-13 18:35:03 -05:00

672 lines
18 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));
}
/* FAB bottom offset: clears bottom nav + safe area inset on mobile */
.bottom-fab {
bottom: calc(1.5rem + var(--bottom-nav-height) + 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 the world discovery drawer above the mobile bottom nav so the
bottom-nav stays usable while the drawer sits in its peek state. On
desktop there's no bottom nav, so the drawer anchors to the viewport
bottom. Vaul drives translation via [data-vaul-drawer] CSS, so we only
adjust the anchor — not the transform. */
.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;
}
/* Slow continuous pulse for nagging buttons */
@keyframes pulse-slow {
0%, 100% { transform: scale(1); opacity: 0.5; }
50% { transform: scale(1.8); opacity: 0; }
}
.animate-pulse-slow {
animation: pulse-slow 2.5s ease-in-out infinite;
}
/* Eerie sigil glow pulse */
@keyframes sigil-glow {
0%, 100% { opacity: 0.12; filter: drop-shadow(0 0 4px hsl(var(--primary) / 0.3)); }
50% { opacity: 0.28; filter: drop-shadow(0 0 12px hsl(var(--primary) / 0.6)) drop-shadow(0 0 24px hsl(var(--primary) / 0.25)); }
}
.animate-sigil-glow {
animation: sigil-glow 4s ease-in-out infinite;
}
/* 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;
}