Files
accounted/app/globals.css
T
Jakob Wennberg be9d630347 feat(home): concept Hem with Att göra + Fortsätt (UI migration PR 11) (#1132)
* feat(home): concept scene 14: greeting + Att gora/Fortsatt panes

Hem becomes the founder-approved two-panel layout: serif time-of-day
greeting with date and company, the Att gora worklist restyled to the
concept pane (eyebrow header, h-rows with count chips, hover chevrons)
and a new Fortsatt pane listing in-progress work derived purely from
draft state (lib/worklist/resume: journal drafts, invoice drafts/unsent,
mid-lifecycle salary runs; deadline boost, cap 3, tested). A completed
flow can never render as a resume row by construction: only draft-state
rows are fetched. KPI tiles, revenue/expense cards and the deadline/tax
widgets leave the page per dev_docs/last_session_resume.md section 8,
which also prunes their fetches (journal-line YTD aggregation, unpaid
totals, deadlines): the page got faster. Banners, checklist,
build-assistant hero and the Skatteverket nudge survive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(home): serif pane titles for Att gora and Fortsatt

Founder feedback: the uppercase eyebrow headers read as a stray font.
Both pane titles are now the Hedvig display serif (text-lg) over the
hairline, matching the page's heading language; the band headers inside
Att gora keep their small uppercase form as grouping devices.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(home): Geist pane titles for Att gora and Fortsatt

Founder call: the pane titles use the body sans (14px medium), not the
display serif.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): Geist section headers + drop stale-transactions chip

Founder feedback: pane/section headers (Att gora, Fortsatt, the reports
groups Lopande/Bokslut/Skatt & moms etc) render in Geist sentence case
instead of uppercase eyebrows or serif. The global h1-h3 display-font
rule moves into @layer base so utility classes like font-sans can
actually override it (unlayered element rules beat Tailwind's layered
utilities: this was silently eating the override). Also removes the
'N aldre an 14 dagar' chip from the Bokfora transaktioner row and its
stale-count plumbing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(ui): continuous nav crossfade + floating slide-over entrance

The rail/full nav states now stay mounted and crossfade past each other
(the inactive layer absolute, faded, nudged sideways, inert) while the
aside width animates: the switch reads as one continuous motion instead
of a DOM swap. The detail slide-over floats in from the right edge
(slide-in-from-right-full, 300ms decelerating curve) per the concept,
with a quicker ease-in exit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): make transitions and enter/exit animations actually run

Two silent app-wide animation killers found while chasing 'the nav still
is not smooth':

1. The codebase uses the shadcn animate-in/out vocabulary everywhere but
   no animate plugin was ever installed: Tailwind v4 silently dropped
   every such class, so popovers, dialogs, menus and the slide-over all
   appeared instantly. globals.css now defines the exact subset in use
   (accEnter/accExit keyframes + var-driven utilities), plugin-free,
   composing with duration/ease via --tw-duration/--tw-ease and
   collapsing under prefers-reduced-motion. Dialog drops its
   bracket-variant slide classes (zoom+fade carries the entrance).

2. The scrollbar auto-hide block's universal '* { transition:
   scrollbar-color ... }' was unlayered, and unlayered rules beat
   Tailwind's layered transition-* utilities regardless of specificity:
   every width/margin/color transition in the app was dead. The rule now
   lives in @layer base. Verified: the aside animates 248->64 over 300ms
   and the slide-over runs accEnter at 0.3s with the decelerating curve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): finish the rr-mask session-replay masking sweep

Main's #1105 switched one amount cell from the no-op sensitive-field
class to rr-mask (rrweb's built-in text-masking class). The reskinned
tables introduced more sensitive-field cells; all 12 occurrences now use
rr-mask so financial amounts are masked in session replays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:36:18 +02:00

511 lines
15 KiB
CSS

@import "tailwindcss";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:where(.dark, .dark *));
/* ─────────────────────────────────────────────────────────────────────────
* Spacing scale: locked.
* Card padding: p-6. Section gap: space-y-8. Form/control gap: gap-4.
* Allowed Tailwind values: 1, 2, 3, 4, 6, 8, 10, 12. No 2.5 / 5.
* Sidebar width is md:w-64 (256px); main offset matches md:pl-64.
* ───────────────────────────────────────────────────────────────────────── */
:root {
color-scheme: light;
/* Sidebar column width (frame layout). Default for shells that don't
override it; the dashboard shell (#dash-shell) sets it inline from
user_preferences.ui_state (248px expanded / 64px collapsed rail) and
the nav toggle updates it client-side. aside and <main> both read the
variable so they stay in lockstep. */
--nav-w: 248px;
/* Grayscale Chrome Palette: paper white + warm beige */
--background: 0 0% 100%;
--foreground: 0 0% 9%;
/* Warm-toned frame behind the floating page panel (frame layout) */
--frame: 40 18% 96%;
--card: 0 0% 100%;
--card-foreground: 0 0% 9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 100%;
--secondary: 40 11% 89%; /* Warm beige paper chip */
--secondary-foreground: 0 0% 9%;
--muted: 40 8% 93%;
--muted-foreground: 0 0% 40%;
--accent: 40 8% 93%;
--accent-foreground: 0 0% 9%;
--destructive: 6 36% 48%; /* Muted terracotta */
--destructive-foreground: 0 0% 100%;
--border: 45 5% 85%;
--input: 45 5% 85%;
--ring: 0 0% 9%;
/* Radius */
--radius: 0.5rem;
/* Success: Sage green */
--success: 155 25% 40%;
--success-foreground: 0 0% 100%;
/* Warning: Muted ochre */
--warning: 38 55% 50%;
--warning-foreground: 0 0% 9%;
/* Attn: darker ochre for the one-sentence attention line (.attn / AttnLine),
derived from --warning but AA-safe as small text on the page background. */
--attn: 38 50% 34%;
--warm-accent: 38 45% 52%;
/* Chart colors */
--chart-1: 155 25% 40%;
--chart-2: 6 36% 48%;
--chart-3: 38 55% 50%;
--chart-4: 0 0% 50%;
/* Custom tokens */
--font-display: var(--font-hedvig-serif), Georgia, serif;
--font-body: var(--font-geist-sans), system-ui, sans-serif;
/* Shadows: barely-there; chrome relies on hairline borders */
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.03), 0 1px 1px rgba(15, 23, 42, 0.02);
--shadow-md: 0 4px 12px rgba(15, 23, 42, 0.03);
--shadow-lg: 0 12px 40px rgba(15, 23, 42, 0.05);
/* Animation */
--ease-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--duration-fast: 150ms;
--duration-base: 300ms;
--duration-slow: 500ms;
}
.dark {
color-scheme: dark;
--background: 0 0% 7%;
--foreground: 0 0% 87%;
--frame: 0 0% 5%;
--card: 0 0% 11%;
--card-foreground: 0 0% 87%;
--popover: 0 0% 11%;
--popover-foreground: 0 0% 87%;
--primary: 0 0% 87%;
--primary-foreground: 0 0% 7%;
--secondary: 0 0% 14%;
--secondary-foreground: 0 0% 87%;
--muted: 0 0% 16%;
--muted-foreground: 0 0% 50%;
--accent: 0 0% 16%;
--accent-foreground: 0 0% 87%;
--destructive: 6 32% 56%;
--destructive-foreground: 0 0% 100%;
--border: 45 4% 18%;
--input: 45 4% 18%;
--ring: 0 0% 87%;
--success: 155 22% 48%;
--success-foreground: 0 0% 100%;
--warning: 38 50% 55%;
--warning-foreground: 38 50% 90%;
--attn: 38 45% 62%;
--warm-accent: 38 42% 58%;
--chart-1: 155 22% 48%;
--chart-2: 6 32% 56%;
--chart-3: 38 50% 55%;
--chart-4: 0 0% 55%;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.25);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35);
--shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.45);
}
@theme inline {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-frame: hsl(var(--frame));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-success: hsl(var(--success));
--color-success-foreground: hsl(var(--success-foreground));
--color-warning: hsl(var(--warning));
--color-warning-foreground: hsl(var(--warning-foreground));
--color-attn: hsl(var(--attn));
--color-warm-accent: hsl(var(--warm-accent));
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--font-sans: var(--font-body);
--font-mono: var(--font-geist-mono), monospace;
--font-serif: var(--font-display);
}
* {
border-color: hsl(var(--border));
}
body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
font-family: var(--font-body);
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Display typography - Fraunces */
.font-display {
font-family: var(--font-display);
font-optical-sizing: auto;
}
/* Layered so utility classes (e.g. font-sans on a pane title) can override:
unlayered element rules would beat Tailwind's layered utilities. */
@layer base {
h1, h2, h3 {
font-family: var(--font-display);
font-weight: 500;
letter-spacing: -0.015em;
font-optical-sizing: auto;
}
}
/* Number styling - tabular for alignment */
.tabular-nums {
font-variant-numeric: tabular-nums;
}
/* Text balance for better line wrapping on descriptions */
.text-balance {
text-wrap: balance;
}
/* Safe area padding for mobile devices with notch/home indicator */
.safe-area-bottom {
padding-bottom: max(0.5rem, env(safe-area-inset-bottom));
}
/* Main content padding: clears bottom nav + safe area on mobile */
.safe-area-main-padding {
padding-bottom: calc(5rem + env(safe-area-inset-bottom, 0px));
}
/* Animation Keyframes */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-8px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes gentlePulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* Three-dot "typing" indicator used by the agent chat pre-token bubble.
Each dot rides the same wave but with staggered animation-delay so the
bounce reads left-to-right. */
@keyframes typingDot {
0%, 60%, 100% { transform: translateY(0); opacity: 0.35; }
30% { transform: translateY(-3px); opacity: 1; }
}
.animate-typing-dot {
animation: typingDot 1.1s ease-in-out infinite;
}
/* Marching-ants dashed border used by the import entity selector. */
@keyframes marchingAnts {
to { stroke-dashoffset: -14; }
}
.animate-marching-ants {
animation: marchingAnts 1.2s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.animate-typing-dot {
animation: gentlePulse 1.6s ease-in-out infinite;
transform: none;
}
.animate-marching-ants {
animation: none;
}
}
/* ─────────────────────────────────────────────────────────────────────────
* Enter/exit animation utilities (animate-in / animate-out vocabulary).
* The codebase uses the shadcn/tailwindcss-animate classes but no animate
* plugin was ever installed, so Tailwind v4 silently dropped them all:
* every popover, dialog and slide-over appeared instantly. These implement
* the exact subset in use, plugin-free. The duration and ease utilities
* compose via Tailwind v4's --tw-duration and --tw-ease variables; the
* global prefers-reduced-motion block collapses them like everything else.
* ───────────────────────────────────────────────────────────────────────── */
@keyframes accEnter {
from {
opacity: var(--tw-enter-opacity, 1);
transform: translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0)
scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), 1);
}
}
@keyframes accExit {
to {
opacity: var(--tw-exit-opacity, 1);
transform: translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0)
scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), 1);
}
}
@utility animate-in {
animation-name: accEnter;
animation-duration: var(--tw-duration, 150ms);
animation-timing-function: var(--tw-ease, cubic-bezier(0.32, 0.72, 0, 1));
animation-fill-mode: both;
}
@utility animate-out {
animation-name: accExit;
animation-duration: var(--tw-duration, 150ms);
animation-timing-function: var(--tw-ease, cubic-bezier(0.32, 0.72, 0, 1));
animation-fill-mode: both;
}
@utility fade-in { --tw-enter-opacity: 0; }
@utility fade-in-0 { --tw-enter-opacity: 0; }
@utility fade-out { --tw-exit-opacity: 0; }
@utility fade-out-0 { --tw-exit-opacity: 0; }
@utility zoom-in-95 { --tw-enter-scale: 0.95; }
@utility zoom-out-95 { --tw-exit-scale: 0.95; }
@utility slide-in-from-top { --tw-enter-translate-y: -100%; }
@utility slide-in-from-top-1 { --tw-enter-translate-y: -0.25rem; }
@utility slide-in-from-top-2 { --tw-enter-translate-y: -0.5rem; }
@utility slide-in-from-top-full { --tw-enter-translate-y: -100%; }
@utility slide-in-from-bottom { --tw-enter-translate-y: 100%; }
@utility slide-in-from-bottom-1 { --tw-enter-translate-y: 0.25rem; }
@utility slide-in-from-bottom-2 { --tw-enter-translate-y: 0.5rem; }
@utility slide-in-from-left { --tw-enter-translate-x: -100%; }
@utility slide-in-from-left-1 { --tw-enter-translate-x: -0.25rem; }
@utility slide-in-from-left-2 { --tw-enter-translate-x: -0.5rem; }
@utility slide-in-from-right { --tw-enter-translate-x: 100%; }
@utility slide-in-from-right-2 { --tw-enter-translate-x: 0.5rem; }
@utility slide-in-from-right-full { --tw-enter-translate-x: 100%; }
@utility slide-out-to-top { --tw-exit-translate-y: -100%; }
@utility slide-out-to-bottom { --tw-exit-translate-y: 100%; }
@utility slide-out-to-left { --tw-exit-translate-x: -100%; }
@utility slide-out-to-right { --tw-exit-translate-x: 100%; }
@utility slide-out-to-right-full { --tw-exit-translate-x: 100%; }
/* Staggered entrance animation */
.stagger-enter > * {
animation: slideUp var(--duration-slow) var(--ease-out) both;
}
.stagger-enter > *:nth-child(1) { animation-delay: 0ms; }
.stagger-enter > *:nth-child(2) { animation-delay: 80ms; }
.stagger-enter > *:nth-child(3) { animation-delay: 160ms; }
.stagger-enter > *:nth-child(4) { animation-delay: 240ms; }
.stagger-enter > *:nth-child(5) { animation-delay: 320ms; }
.stagger-enter > *:nth-child(6) { animation-delay: 400ms; }
.stagger-enter > *:nth-child(7) { animation-delay: 480ms; }
.stagger-enter > *:nth-child(8) { animation-delay: 560ms; }
.stagger-enter > *:nth-child(9) { animation-delay: 640ms; }
.stagger-enter > *:nth-child(10) { animation-delay: 720ms; }
/* Utility animations */
.animate-fade-in {
animation: fadeIn var(--duration-base) var(--ease-out);
}
.animate-slide-up {
animation: slideUp var(--duration-slow) var(--ease-out);
}
.animate-scale-in {
animation: scaleIn var(--duration-base) var(--ease-out);
}
/* Soft focus states */
.focus-ring:focus-visible {
outline: none;
box-shadow: 0 0 0 2px hsl(var(--background)),
0 0 0 4px hsl(var(--primary) / 0.35);
}
/* Overlay-style scrollbars: invisible at rest, revealed only while the
container is actually scrolling. ScrollbarReveal (mounted in the root
layout) toggles .is-scrolling on the scrolled element; the thumb fades
back out when scrolling stops. The gutter is reserved either way, so
revealing never shifts layout. */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 3px;
}
.is-scrolling::-webkit-scrollbar-thumb {
background: hsl(var(--muted-foreground) / 0.25);
}
@supports (scrollbar-color: auto) {
/* Layered: the universal transition must not beat Tailwind's layered
transition-* utilities (unlayered rules win over layers regardless of
specificity: this silently killed every width/margin/color transition
in the app). scrollbar-color still animates via the base default. */
@layer base {
* {
scrollbar-width: thin;
scrollbar-color: transparent transparent;
transition: scrollbar-color 300ms ease-out;
}
}
.is-scrolling {
scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
}
}
/* Hover-reveal fallback for touch: coarse pointers have no hover, so the
controls that desktop reveals on hover (row checkboxes, chevrons, quiet
row actions) stay visible. Targets exactly the opacity-0 +
group-hover:opacity-100 pairing and nothing else. */
@media (pointer: coarse) {
.group .opacity-0.group-hover\:opacity-100 {
opacity: 1;
}
}
/* Always-visible thumb for modal scroll containers (DialogContent,
SlideOverBody): inside an overlay the scrollbar is the only cue that
content continues below the fold, so the at-rest auto-hide would read
as "the dialog is cut off". */
.scrollbar-visible::-webkit-scrollbar-thumb {
background: hsl(var(--muted-foreground) / 0.25);
}
@supports (scrollbar-color: auto) {
.scrollbar-visible {
scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
}
}
/* Hide scrollbar utility */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Hide number input spinners */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}
/* Selection color */
::selection {
background: hsl(var(--primary) / 0.15);
color: hsl(var(--foreground));
}
.dark ::selection {
background: hsl(var(--primary) / 0.3);
color: hsl(var(--foreground));
}
/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
.stagger-enter > * {
animation: none;
opacity: 1;
}
}