frontend/src/app/globals.css
Rama main · réplica de lectura del repositorio original.
@tailwind base;
@tailwind components;
@tailwind utilities;
/* The app has no light theme, so the UA is told so once, at the root. Without
this every browser-drawn control defaults to its light palette — most visibly
the window and pane scrollbars, which rendered as light grey over the
near-black surfaces. It also darkens native selects, date pickers and
autofill, so it is the single declaration that keeps UA chrome on-theme.
`scrollbar-color` is inherited, so setting it here reaches every scroll
container that does not opt into the `::-webkit-scrollbar` styling below;
it is honoured by Firefox and Chromium 121+, while older WebKit and Safari
fall back to the dark UA scrollbar `color-scheme` already gives them.
Thumb is `border.bright`, track is `surface` — the same tokens the panes use. */
:root {
color-scheme: dark;
scrollbar-width: thin;
scrollbar-color: #3d3d5c #12121c;
}
html,
body {
height: 100%;
}
body {
@apply bg-background font-mono text-slate-200 antialiased;
}
::selection {
@apply bg-primary text-black;
}
/* Thin scrollbars for log/stream panes. Defining any `::-webkit-scrollbar`
pseudo-element makes Chromium ignore `scrollbar-color` for that element and
draw a classic (non-overlay) bar, so the track is painted explicitly here
rather than inherited. */
.scrollbar-thin::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.scrollbar-thin::-webkit-scrollbar-track {
@apply bg-transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
@apply rounded-full bg-border-bright;
}
.scrollbar-thin::-webkit-scrollbar-corner {
@apply bg-transparent;
}
/* Scrollable but chromeless — the landing nav's tab strip on narrow screens. */
.scrollbar-none {
scrollbar-width: none;
}
.scrollbar-none::-webkit-scrollbar {
display: none;
}
@layer utilities {
/* Faint grid backdrop, tintable per module via `--grid-rgb` (inline style).
Defaults to neutral slate; auth/landing set periwinkle, architect sets violet. */
.bg-grid {
background-image:
linear-gradient(to right, rgb(var(--grid-rgb, 148 163 184) / 0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgb(var(--grid-rgb, 148 163 184) / 0.05) 1px, transparent 1px);
background-size: 32px 32px;
}
/* Lit blueprint plane — the architect canvas backdrop. Three static layers on
one element, split this way on purpose: `mask-image` applies to an element
AND its pseudo-elements, so the grid's dissolve mask lives on ::before
alone. Putting the vignette under that mask would erase it exactly at the
edges where it has to be strongest. Hue via `--grid-rgb`, same contract as
`.bg-grid`. Host element must be positioned. Nothing animates, so there is
no reduced-motion entry. */
.canvas-depth {
pointer-events: none;
background-image: radial-gradient(
ellipse 90% 70% at 50% 34%,
rgb(var(--grid-rgb, 148 163 184) / 0.05),
transparent 70%
);
}
.canvas-depth::before,
.canvas-depth::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
}
/* Two-tier grid: 128px majors over 32px minors. The 32px cadence and its 0.05
alpha are shared with `.bg-grid` so the two backdrops stay one family; 128
is 4x32, so every major lands on a minor and composites brighter. */
.canvas-depth::before {
background-image:
linear-gradient(to right, rgb(var(--grid-rgb, 148 163 184) / 0.09) 1px, transparent 1px),
linear-gradient(to bottom, rgb(var(--grid-rgb, 148 163 184) / 0.09) 1px, transparent 1px),
linear-gradient(to right, rgb(var(--grid-rgb, 148 163 184) / 0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgb(var(--grid-rgb, 148 163 184) / 0.05) 1px, transparent 1px);
background-size:
128px 128px,
128px 128px,
32px 32px,
32px 32px;
-webkit-mask-image: radial-gradient(ellipse 88% 78% at 50% 40%, #000 45%, transparent 100%);
mask-image: radial-gradient(ellipse 88% 78% at 50% 40%, #000 45%, transparent 100%);
}
/* Edge vignette — deliberately outside the mask above. Darkens toward the
page `background` token, not a new hue. */
.canvas-depth::after {
background: radial-gradient(
ellipse 80% 80% at 50% 45%,
transparent 45%,
rgb(10 10 16 / 0.5) 100%
);
}
/* Colored spotlight beam for the landing stage. A beam is three stacked
parts — source hotspot, coned shaft, hot core — each hued via its inline
gradient. The shaft and core get a slow opacity breathe plus a gentle sway
around the top apex; the source only breathes (swaying it would drift the
lamp off the apex). Each part carries its own duration/delay so beams never
move in lockstep. Breathe touches only opacity and sway only transform, so
they never fight over the same property on one element. */
.spotlight-beam,
.spotlight-core {
animation:
spotlight-breathe var(--beam-dur, 8s) ease-in-out infinite,
spotlight-sway var(--beam-sway, 16s) ease-in-out infinite;
}
.spotlight-source {
animation: spotlight-breathe var(--beam-dur, 8s) ease-in-out infinite;
}
/* Uppercase micro-label used for stat labels and section eyebrows. */
.text-micro {
@apply text-[10px] font-semibold uppercase tracking-micro;
}
/* Vertical label for a collapsed side rail — reads bottom-to-top. The rotate
is what flips `vertical-rl` from top-down to bottom-up, which is the
conventional direction for a Latin tab label. Compose with `text-micro`. */
.text-vertical {
writing-mode: vertical-rl;
transform: rotate(180deg);
}
/* Architect step-2 column resize when a side panel collapses to its rail.
A tween, not a spring: FlowLayer re-measures the whole agent graph on every
frame of this transition, so overshoot would cost real work. */
.panel-grid {
transition: grid-template-columns 250ms cubic-bezier(0.16, 1, 0.3, 1);
}
/* Animated neon gradient border for featured cards — double-background
technique, hue via `--gb-rgb` (inline style). Degrades to a static
gradient border without animation support. */
.gradient-border {
border: 1px solid transparent;
background:
linear-gradient(#12121c, #12121c) padding-box,
linear-gradient(
90deg,
rgb(var(--gb-rgb, 211 203 192) / 0.7),
rgb(var(--gb-rgb, 211 203 192) / 0.1) 40%,
rgb(var(--gb-rgb, 211 203 192) / 0.7)
)
border-box;
background-size:
100% 100%,
200% 100%;
animation: border-pan 6s linear infinite;
}
/* Diagonal light sweep on hover — for solid CTAs and featured cards. */
.shimmer-hover {
position: relative;
overflow: hidden;
}
.shimmer-hover::after {
content: '';
position: absolute;
inset: 0;
width: 40%;
transform: translateX(-150%) skewX(-12deg);
background: linear-gradient(110deg, transparent, rgb(255 255 255 / 0.14), transparent);
pointer-events: none;
}
.shimmer-hover:hover::after {
animation: shimmer 0.9s ease-out;
}
/* 1px neon line sweeping down once on route entry — tint via `--pt-rgb`.
Fixed so it crosses the whole viewport regardless of scroll position. */
.page-scanline {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 1px;
z-index: 60;
pointer-events: none;
background: linear-gradient(
90deg,
transparent,
rgb(var(--pt-rgb, 211 203 192) / 0.9) 30%,
rgb(var(--pt-rgb, 211 203 192) / 0.9) 70%,
transparent
);
box-shadow: 0 0 12px 1px rgb(var(--pt-rgb, 211 203 192) / 0.55);
animation: scanline 0.35s ease-in both;
}
/* Sparkline entrance — bars grow from the baseline, staggered per bar via
inline animation-delay. */
.spark-grow rect {
transform-box: fill-box;
transform-origin: bottom;
animation: spark-grow 0.5s ease-out both;
}
/* Staggered blur-in for a container's direct children (forms, card innards).
Caps at 10 children; later ones share the last beat. */
.stagger-children > * {
animation: stagger-in 0.45s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.stagger-children > *:nth-child(2) {
animation-delay: 0.06s;
}
.stagger-children > *:nth-child(3) {
animation-delay: 0.12s;
}
.stagger-children > *:nth-child(4) {
animation-delay: 0.18s;
}
.stagger-children > *:nth-child(5) {
animation-delay: 0.24s;
}
.stagger-children > *:nth-child(6) {
animation-delay: 0.3s;
}
.stagger-children > *:nth-child(7) {
animation-delay: 0.36s;
}
.stagger-children > *:nth-child(n + 8) {
animation-delay: 0.42s;
}
/* One-shot light pan along the TopBar bottom edge — tint via `--tb-rgb`. */
.topbar-pan {
background: linear-gradient(
90deg,
transparent,
rgb(var(--tb-rgb, 211 203 192) / 0.8),
transparent
);
background-size: 40% 100%;
background-repeat: no-repeat;
animation: topbar-pan 0.7s ease-out both;
}
/* Horizontal bar entrance (BarList) — pair with inline animation-delay. */
.bar-grow {
transform-origin: left;
animation: bar-grow 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
}
/* Power-on line sweeping down a panel once per task start — `--ps-rgb`. */
.panel-scan {
position: absolute;
inset: 0;
pointer-events: none;
background-image: linear-gradient(
90deg,
transparent,
rgb(var(--ps-rgb, 167 139 250) / 0.8),
transparent
);
background-size: 100% 2px;
background-repeat: no-repeat;
animation: panel-scan 0.6s ease-in both;
}
/* Architect task rail — one status bar per task, hue via `--bar-rgb`. The
glow is what makes a 6px bar read as neon rather than as a plain divider. */
.rail-bar {
background-color: rgb(var(--bar-rgb, 139 143 163));
box-shadow: 0 0 8px 0 rgb(var(--bar-rgb, 139 143 163) / 0.45);
}
/* Selected task: `--glow-rgb` (the architect hue) blooms around the bar,
which keeps its own status color. */
.rail-bar-active {
box-shadow:
0 0 10px 0 rgb(var(--bar-rgb, 139 143 163) / 0.6),
0 0 20px 4px rgb(var(--glow-rgb, 157 77 255) / 0.5);
}
/* Fresh EventLog row: tinted highlight decaying to transparent — `--lg-rgb`. */
.log-flash {
animation: log-flash 1.2s ease-out both;
}
/* Loading placeholder sheen — module tint via `--sk-rgb`. */
.skeleton {
@apply rounded-md bg-surface-2;
background-image: linear-gradient(
100deg,
transparent 30%,
rgb(var(--sk-rgb, 148 163 184) / 0.08) 50%,
transparent 70%
);
background-size: 200% 100%;
background-repeat: no-repeat;
animation: skeleton-sheen 1.6s ease-in-out infinite;
}
/* Toast entrance — a short fade + slide from the right edge. */
.toast-in {
animation: toast-in 0.22s ease-out both;
}
/* Sponsor like-burst — a particle flies out along its own `--tx`/`--ty`. */
.like-particle {
animation: like-particle-fly 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
}
/* Sponsor like-burst — the filled heart pops, then fades to the outline. */
.like-heart-pop {
animation: like-heart-pop 0.45s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@media (prefers-reduced-motion: reduce) {
.gradient-border,
.shimmer-hover:hover::after,
.skeleton,
.spark-grow rect,
.stagger-children > *,
.topbar-pan,
.bar-grow,
.log-flash,
.toast-in,
.like-particle,
.like-heart-pop {
animation: none;
}
.spotlight-beam,
.spotlight-source,
.spotlight-core {
animation: none;
}
.page-scanline,
.panel-scan {
display: none;
}
.panel-grid {
transition: none;
}
}
}
@keyframes spotlight-breathe {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.55;
}
}
@keyframes spotlight-sway {
0%,
100% {
transform: rotate(-1.5deg);
}
50% {
transform: rotate(1.5deg);
}
}
@keyframes spark-grow {
from {
transform: scaleY(0);
}
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateX(12px);
}
}
@keyframes like-particle-fly {
from {
transform: translate(-50%, -50%) scale(0.2);
opacity: 1;
}
60% {
opacity: 1;
}
to {
transform: translate(calc(-50% + var(--tx)), calc(-50% + var(--ty))) scale(0.5);
opacity: 0;
}
}
@keyframes like-heart-pop {
0% {
transform: scale(0.6);
opacity: 0;
}
40% {
transform: scale(1.35);
opacity: 1;
}
70% {
transform: scale(0.95);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}
@keyframes stagger-in {
from {
opacity: 0;
transform: translateY(8px);
filter: blur(4px);
}
}
@keyframes topbar-pan {
from {
background-position: -40% 0;
}
to {
background-position: 140% 0;
}
}
@keyframes bar-grow {
from {
transform: scaleX(0);
}
}
@keyframes skeleton-sheen {
from {
background-position: -150% 0;
}
to {
background-position: 250% 0;
}
}
@keyframes scanline {
from {
transform: translateY(0);
opacity: 0.9;
}
to {
transform: translateY(100vh);
opacity: 0;
}
}
@keyframes panel-scan {
from {
background-position: 0 0;
opacity: 1;
}
to {
background-position: 0 100%;
opacity: 0;
}
}
@keyframes log-flash {
0% {
background-color: rgb(var(--lg-rgb, 167 139 250) / 0.14);
opacity: 0;
transform: translateY(6px);
}
25% {
opacity: 1;
transform: translateY(0);
}
100% {
background-color: transparent;
opacity: 1;
transform: translateY(0);
}
}
/* ---- Per-domain squad-card hover motifs ---------------------------------- */
/* Decorative loop timings (~2s) sit outside motion.ts's DUR scale (one-shots,
≤0.8s), so they are CSS literals here — consistent with the keyframes above.
The shared EASE.out curve is reused as the cubic-bezier literal. Every motif
applies to nodes tagged [data-play], which are paused until the card hovers.
These rules are intentionally unlayered: the `.motif-*` classes below use the
`animation` shorthand, which resets `animation-play-state` to `running`. An
unlayered rule beats Tailwind's layered utilities, and the higher-specificity
`.card-motif [data-play]` selector re-asserts `paused` over that shorthand, so
the grid stays idle until a card is hovered. */
/* Rendered always, invisible + paused at rest; fades to a faint watermark and
resumes only while the parent card (a `.group`) is hovered. */
.card-motif {
opacity: 0;
transition: opacity 0.3s ease-out;
}
.group:hover .card-motif {
opacity: 0.16;
}
.card-motif [data-play] {
transform-box: fill-box; /* transform-origin relative to each node's own box */
animation-play-state: paused;
}
.group:hover .card-motif [data-play] {
animation-play-state: running;
}
/* Rotations/tilts that must pivot on a point in the SVG, not each node's box. */
.card-motif .motif-spin {
transform-box: view-box;
transform-origin: 50% 50%;
}
.card-motif .motif-tip {
transform-box: view-box;
transform-origin: 50% 28%;
}
.card-motif .motif-swing {
transform-box: view-box;
transform-origin: 75% 41%; /* the tassel's attachment point on the cap */
}
@media (prefers-reduced-motion: reduce) {
/* Freeze at the static frame; the hover opacity fade (a transition) stays. */
.card-motif [data-play] {
animation: none;
}
}
/* Software: each code line wipes in from the left, holds, then retypes. */
.motif-codeline {
animation: motif-codeline 2.4s cubic-bezier(0.16, 1, 0.3, 1) infinite;
}
@keyframes motif-codeline {
0% {
transform: scaleX(0);
opacity: 0;
}
20% {
opacity: 1;
}
40%,
80% {
transform: scaleX(1);
opacity: 1;
}
100% {
transform: scaleX(0);
opacity: 0;
}
}
/* Blinking text caret. */
.motif-caret {
animation: motif-caret 1s steps(1) infinite;
}
@keyframes motif-caret {
0%,
49% {
opacity: 1;
}
50%,
100% {
opacity: 0;
}
}
/* Finance: a candle grows from the baseline, holds, then the oldest fades out. */
.motif-candle {
animation: motif-candle 2s cubic-bezier(0.16, 1, 0.3, 1) infinite;
}
@keyframes motif-candle {
0% {
transform: scaleY(0);
opacity: 0;
}
25%,
80% {
transform: scaleY(1);
opacity: 1;
}
100% {
transform: scaleY(1);
opacity: 0;
}
}
/* Expanding signal ring / ripple (marketing rings, searching + opensource + local). */
.motif-ping {
animation: motif-ping 1.8s ease-out infinite;
transform-origin: center;
}
@keyframes motif-ping {
0% {
transform: scale(0.3);
opacity: 0.9;
}
100% {
transform: scale(1.9);
opacity: 0;
}
}
/* SEO: the magnifier sweeps back and forth across the result rows. */
.motif-scan {
animation: motif-scan 2.4s ease-in-out infinite;
}
@keyframes motif-scan {
0%,
100% {
transform: translateX(-8px);
}
50% {
transform: translateX(6px);
}
}
/* SEO: the rank arrow ticks up and fades. */
.motif-rankup {
animation: motif-rankup 2.4s ease-in-out infinite;
}
@keyframes motif-rankup {
0%,
100% {
transform: translateY(4px);
opacity: 0;
}
50% {
transform: translateY(-2px);
opacity: 1;
}
}
/* Continuous rotation (radar sweep, electron orbit). */
.motif-spin {
animation: motif-spin 3s linear infinite;
}
@keyframes motif-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Data: a bar rises and falls, staggered into a travelling wave. */
.motif-bar {
animation: motif-bar 1.6s ease-in-out infinite;
}
@keyframes motif-bar {
0%,
100% {
transform: scaleY(0.25);
}
50% {
transform: scaleY(1);
}
}
/* Stroke that draws itself along a normalised path (content frame, git branch). */
.motif-draw {
stroke-dasharray: 1;
animation: motif-draw 2.6s ease-in-out infinite;
}
@keyframes motif-draw {
0% {
stroke-dashoffset: 1;
}
60%,
100% {
stroke-dashoffset: 0;
}
}
/* Legal: the balance beam tips gently back and forth. */
.motif-tip {
animation: motif-tip 2.6s ease-in-out infinite;
}
@keyframes motif-tip {
0%,
100% {
transform: rotate(-9deg);
}
50% {
transform: rotate(9deg);
}
}
/* Education: the tassel swings like a pendulum (pivot set in the gating block). */
.motif-swing {
animation: motif-swing 2.2s ease-in-out infinite;
}
@keyframes motif-swing {
0%,
100% {
transform: rotate(-16deg);
}
50% {
transform: rotate(16deg);
}
}
/* Chat/network dot pulse (social typing dots, community nodes). */
.motif-typedot {
animation: motif-typedot 1.4s ease-in-out infinite;
}
@keyframes motif-typedot {
0%,
70%,
100% {
transform: scale(0.7);
opacity: 0.4;
}
35% {
transform: scale(1.15);
opacity: 1;
}
}
/* Community: a connective link brightens and dims. */
.motif-link {
animation: motif-link 2s ease-in-out infinite;
}
@keyframes motif-link {
0%,
100% {
opacity: 0.25;
}
50% {
opacity: 1;
}
}
/* Local: the pin drops in and settles with a short bounce. */
.motif-drop {
animation: motif-drop 2.4s ease-in-out infinite;
}
@keyframes motif-drop {
0% {
transform: translateY(-16px);
opacity: 0;
}
25% {
opacity: 1;
}
45% {
transform: translateY(0);
}
58% {
transform: translateY(-4px);
}
70%,
100% {
transform: translateY(0);
opacity: 1;
}
}
/* General: sparkles twinkle in and out of phase. */
.motif-twinkle {
animation: motif-twinkle 1.8s ease-in-out infinite;
}
@keyframes motif-twinkle {
0%,
100% {
transform: scale(0.4);
opacity: 0.2;
}
50% {
transform: scale(1);
opacity: 1;
}
}