/* ═══════════════════════════════════════════════════════════════════════════
   Gravity — one visual language across three surfaces
   Imported by: assets/css/style.css (the public site)
                admin/theme.css      (the panel)
   Mirrored by: android-app/src/theme.js (React Native cannot read CSS)

   WHY THIS FILE EXISTS. The site, the panel and the phone app each had their
   own copy of the same intentions — the same gradient, roughly the same navy,
   three different sets of radii and no shared idea of elevation or motion. They
   drifted, because three copies of a decision are three chances to change one
   of them. The brand triad and the ground live here now, once.

   WHAT DID NOT CHANGE, deliberately: orange → pink → purple is the mark. It is
   more distinctive than the single accent most dashboards run on, and throwing
   it away to look like a reference would trade an identity for a resemblance.
   What changed is how it is USED — see "the gradient is light, not wallpaper".
   ═══════════════════════════════════════════════════════════════════════════ */

/* A custom property is an untyped string by default, and a string cannot be
   interpolated — which is why a naive glow snaps to the cursor. Registering
   these two as lengths lets the transition below do the easing in CSS, with no
   rAF loop, no lerp constant to tune, and no JavaScript running between frames.
   The pointer sets a target; the browser walks there. */
/* inherits:TRUE, and that is not a detail. A registered property declared
   non-inheriting is invisible to the element's own ::before/::after, which fall
   back to initial-value instead — so the light was pinned at 0px and never
   followed anything. The pseudo-element is where this value is USED, and the
   only way it reaches there is by inheriting. */
@property --gx { syntax: "<length>"; inherits: true; initial-value: 0px; }
@property --gy { syntax: "<length>"; inherits: true; initial-value: 0px; }

:root {
  /* ---- the mark. These three never move. ---- */
  --g-orange: #ef8f20;
  --g-pink:   #d31d53;
  --g-purple: #462d93;
  --g-grad:   linear-gradient(140deg, var(--g-orange), var(--g-pink) 52%, var(--g-purple));
  /* a shallower rake for thin things — rings, bars, rules — where a 140°
     sweep would show only one of the three stops */
  --g-grad-flat: linear-gradient(90deg, var(--g-orange), var(--g-pink) 55%, var(--g-purple));

  /* ---- motion ----
     One decisive ease-out, three durations, and nothing longer than a beat.
     A panel is opened thirty times a day; anything that makes you WAIT for the
     interface is a tax collected thirty times. 180ms is the point where a
     change reads as movement rather than as a jump, and 260 is reserved for
     things travelling a long way. */
  --g-t-fast: 120ms;
  --g-t:      180ms;
  --g-t-slow: 260ms;
  --g-ease:   cubic-bezier(.2, .8, .2, 1);
  /* Magnetic: overshoots slightly and settles back, which is how a thing being
     PULLED arrives — an ease-out only ever decelerates into place, and that
     reads as sliding rather than as attraction. Used for the light and the tab
     highlight; never for anything carrying text, where overshoot is nausea. */
  --g-ease-mag: cubic-bezier(.22, 1.28, .36, 1);
  --g-ease-io: cubic-bezier(.4, 0, .2, 1);
  --g-stagger: 40ms;

  /* ---- radii ----
     NESTING RULE: an inner corner is the outer radius minus the padding
     between them. Concentric corners are why a card looks machined; equal
     radii inside and out are why it looks assembled from parts. */
  --g-r-card: 26px;
  --g-r-panel: 18px;
  --g-r-el:   12px;
  --g-r-pill: 999px;

  /* ---- spacing: a 4px rhythm ---- */
  --g-s1: .25rem; --g-s2: .5rem;  --g-s3: .75rem;
  --g-s4: 1rem;   --g-s5: 1.5rem; --g-s6: 2rem;   --g-s7: 3rem;
}

/* ═══ ground and ink, both themes ══════════════════════════════════════════
   Light is the default because the panel is, and the dark set sits behind the
   same two switches theme.css already uses — the explicit [data-theme] and the
   system preference guarded against an explicit light choice. One switch would
   have been enough for one of them and wrong for the other.

   Elevation is border-first in both, and one trick does most of the work: a 1px
   inset highlight along the top edge of every raised surface. On dark it is how
   light falls on a milled edge; on light it is the same edge catching the sky.
   Without it a card reads as a hole cut in the page rather than an object on
   it. Shadows stay for things that genuinely float — dialogs, menus. */
:root {
  --g-ground:   #0b0e1c;
  --g-surface:  #121734;
  --g-raised:   #182052;
  --g-sunk:     rgba(255, 255, 255, .045);
  --g-line:     rgba(255, 255, 255, .085);
  --g-line-up:  rgba(255, 255, 255, .16);
  --g-edge:     inset 0 1px 0 rgba(255, 255, 255, .07);
  --g-float:    0 24px 60px -24px rgba(0, 0, 0, .65);

  --g-ink:   #ebedf9;
  --g-title: #ffffff;
  --g-muted: #9ba3cc;
  --g-faint: #6e77a6;
}

:root[data-theme="light"] {
  --g-ground:   #f5f6fc;
  --g-surface:  #ffffff;
  --g-raised:   #ffffff;
  --g-sunk:     rgba(20, 26, 58, .055);
  --g-line:     rgba(20, 26, 58, .11);
  --g-line-up:  rgba(20, 26, 58, .20);
  --g-edge:     inset 0 1px 0 rgba(255, 255, 255, .9);
  --g-float:    0 24px 60px -24px rgba(23, 28, 51, .28);

  --g-ink:   #2c3357;
  --g-title: #141a3a;
  --g-muted: #626a8a;
  --g-faint: #8a90ab;
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --g-ground:   #f5f6fc;
    --g-surface:  #ffffff;
    --g-raised:   #ffffff;
    --g-sunk:     rgba(20, 26, 58, .055);
    --g-line:     rgba(20, 26, 58, .11);
    --g-line-up:  rgba(20, 26, 58, .20);
    --g-edge:     inset 0 1px 0 rgba(255, 255, 255, .9);
    --g-float:    0 24px 60px -24px rgba(23, 28, 51, .28);

    --g-ink:   #2c3357;
    --g-title: #141a3a;
    --g-muted: #626a8a;
    --g-faint: #8a90ab;
  }
}
/* ═══ devices ═══════════════════════════════════════════════════════════════
   The four things worth stealing from a good dashboard, built once. Each is
   plain CSS over plain markup — no library, no build step, and each degrades
   to something readable if the CSS never arrives. */

/* ---- 1. the segmented bar ----
   One track split into labelled parts, each carrying its own share. It answers
   "what is this made of" in the space a single number would take, and it is the
   right shape for money leaving in categories or output arriving in formats.
   Only the outer ends are rounded: rounding every segment turns a bar into a
   row of lozenges and loses the sense of one whole being divided. */
.g-seg {
  display: flex; width: 100%; height: 34px; gap: 2px;
  border-radius: var(--g-r-pill); overflow: hidden;
  background: var(--g-sunk);
}
.g-seg > i {
  display: flex; align-items: center; justify-content: center;
  font-style: normal; font-size: .72rem; font-weight: 700; color: #fff;
  min-width: 0; white-space: nowrap; overflow: hidden;
  /* grows into place on first paint — see .g-in below */
  transition: flex-basis var(--g-t-slow) var(--g-ease);
}
.g-seg > i:first-child { border-start-start-radius: var(--g-r-pill); border-end-start-radius: var(--g-r-pill); }
.g-seg > i:last-child  { border-start-end-radius:   var(--g-r-pill); border-end-end-radius:   var(--g-r-pill); }
/* a share too thin to letter drops its label rather than clipping it */
.g-seg > i[data-thin] { font-size: 0; }

.g-seg-key { display: flex; flex-wrap: wrap; gap: var(--g-s2) var(--g-s4); margin-top: var(--g-s3); }
.g-seg-key span { display: inline-flex; align-items: center; gap: .35rem; font-size: .78rem; color: var(--g-muted); }
.g-seg-key b { width: 8px; height: 8px; border-radius: 3px; flex: none; }

/* ---- 2. the ring ----
   A radial gauge for one figure that has a ceiling — capacity used, collected
   against billed. Rounded caps and a faint full track, so an empty ring still
   reads as a ring rather than as nothing. The value is drawn by
   stroke-dashoffset, which animates on the compositor. */
.g-ring { position: relative; display: inline-grid; place-items: center; }
.g-ring svg { display: block; transform: rotate(-90deg); }
.g-ring .trk { fill: none; stroke: var(--g-sunk); }
.g-ring .val {
  fill: none; stroke-linecap: round;
  transition: stroke-dashoffset var(--g-t-slow) var(--g-ease);
}
.g-ring .mid {
  position: absolute; text-align: center; line-height: 1.15;
  /* CONTAINED, BECAUSE AN ABSOLUTE BOX HAS NO WIDTH TO OBEY.
     .mid was positioned with nothing bounding it, so a label longer than the
     figure above it simply ran out through both sides of the circle — the ring
     drawn around it and the words crossing the stroke. It looked like a caller
     passing too long a string, and it was the component: any label past about
     three short words did it. 78% is the chord at the label's own height, so
     text wraps inside the ring rather than over it. */
  max-width: 78%;
  /* A word longer than the chord has nowhere to go, and would otherwise push
     back out through the stroke rather than break. */
  overflow-wrap: anywhere;
}
.g-ring .mid b { display: block; font-size: 1.5rem; font-weight: 800; color: var(--g-title); }
.g-ring .mid span { font-size: .72rem; color: var(--g-muted); }

/* ---- 3. the accent card ----
   THE GRADIENT IS LIGHT, NOT WALLPAPER. Exactly one panel per screen is
   flooded with it; everything else stays dark and lets it be the loudest thing
   in the room. Spread across every card it stops being an accent and becomes a
   background, which is how a brand colour gets tuned out. */
.g-accent {
  position: relative; overflow: hidden;
  background: var(--g-grad); color: #fff;
  border-radius: var(--g-r-card); padding: var(--g-s5);
  box-shadow: 0 18px 44px -22px rgba(211, 29, 83, .55);
}
.g-accent h2, .g-accent h3, .g-accent b { color: #fff; }
.g-accent .sub { color: rgba(255, 255, 255, .78); }
/* a soft bloom in the top corner so the flood has a light source rather than
   being a flat fill */
.g-accent::after {
  content: ""; position: absolute; inset-inline-end: -20%; top: -60%;
  width: 70%; aspect-ratio: 1; border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 255, 255, .22), transparent 70%);
  pointer-events: none;
}

/* ---- 4. glass ----
   Translucent panel over a coloured ground. Restricted ON PURPOSE to entry
   surfaces — login, the dashboard hero — because backdrop blur behind sixty
   rows of Arabic costs legibility and GPU for decoration, and the people
   reading those rows are reading them all day. */
/* THE HERO BAND IS DARK IN BOTH THEMES, and that is a choice rather than an
   oversight. Brand washes over a light ground go pastel, the glass stops
   reading as glass, and white headline type on it fails outright — so the
   light version would be a weaker design wearing the same name. A masthead
   that stays dark while the working surfaces below it go light is a normal,
   deliberate thing for a panel to do, and it keeps one dramatic moment on a
   screen that is otherwise built for reading numbers.

   Its values are therefore literal, not tokenised: tokens would flip with the
   theme, which is exactly what must not happen here. */
.g-glass {
  background: rgba(18, 23, 52, .55);
  backdrop-filter: blur(22px) saturate(1.3);
  -webkit-backdrop-filter: blur(22px) saturate(1.3);
  border: 1px solid rgba(255, 255, 255, .12);
  border-radius: var(--g-r-card);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07),
              0 24px 60px -24px rgba(0, 0, 0, .5);
  color: #ebedf9;
}
/* ---- the light behind the glass ----
   A frosted panel with nothing behind it is a tinted box: the blur has nothing
   to refract, so the material never announces itself. This puts a moving light
   source BEHIND the glass — the pointer drags it across the band, and the panel
   blurs it on the way through, which is the whole point of a backdrop filter
   and the only thing that makes it read as glass rather than as opacity.

   PAINT ORDER IS THE WHOLE TRICK. A parent's ::after paints ABOVE its children
   by default, which would put the light on top of the panel and blur nothing.
   z-index:-1 drops it behind the children while staying above the parent's own
   background — and that only holds because .g-aurora isolates, giving the
   negative index a stacking context to be negative WITHIN rather than
   disappearing behind the page.

   Hover only, and only on a device with a real pointer: a glow chasing a finger
   is invisible under the finger and costs battery to compute. */
.g-aurora::after {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  /* ONE HUE. The orange stop made the light read as a second brand object
     sitting in the band rather than as illumination — a lamp is one colour,
     and the aurora underneath already carries the full sweep. */
  background: radial-gradient(300px circle at var(--gx) var(--gy),
              color-mix(in srgb, var(--g-pink) 60%, transparent),
              color-mix(in srgb, var(--g-pink) 18%, transparent) 45%,
              transparent 70%);
  opacity: 0;
  /* The position eases as well as the fade, so the light TRAILS the cursor
     instead of being welded to it. 260ms of follow is the difference between a
     lamp being carried and a sprite being dragged. */
  transition: opacity var(--g-t-slow) var(--g-ease),
              --gx 420ms var(--g-ease-mag),
              --gy 420ms var(--g-ease-mag);
}
.g-aurora.lit::after { opacity: 1; }

@media (hover: none), (prefers-reduced-motion: reduce) {
  .g-aurora::after { display: none; }
}

/* the ground it floats on: three slow brand-coloured washes, no image to load */
.g-aurora {
  position: relative; isolation: isolate;
  background:
    radial-gradient(60% 55% at 18% 12%, color-mix(in srgb, var(--g-orange) 26%, transparent), transparent 70%),
    radial-gradient(55% 50% at 85% 20%, color-mix(in srgb, var(--g-pink) 30%, transparent), transparent 70%),
    radial-gradient(70% 60% at 60% 95%, color-mix(in srgb, var(--g-purple) 34%, transparent), transparent 72%),
    #0b0e1c;   /* fixed — see the note on .g-glass */
}

/* ---- 4b. the hero ----
   A HERO IS A GRID ITEM, NOT A BANNER. It spans the same width as the cards
   beneath it and carries the same inner padding, so its headline starts on
   the exact vertical line every card title starts on. A narrower hero inside
   a wider grid puts one edge in open space, aligned with nothing — which
   reads as a mistake rather than as a decision, because every other edge on
   the screen agrees with some edge.

   The width it gains is spent on figures rather than air: a hero that only
   carries a headline is a strip of decoration on a screen people came to
   read numbers on. */
.g-hero {
  display: grid; grid-template-columns: 1fr auto;
  gap: var(--g-s5); align-items: center;
  /* Generous inset, and deliberately more than a card carries. The OUTER edges
     still align with the grid — that is the alignment that was broken and the
     one that matters. Inside, a hero can afford room a dense card cannot: it is
     read once on arrival, not scanned all day.

     Contents step down a fifth from their default sizes for the same reason. At
     full size they crowded a panel this wide, and scale is what tells you a
     headline is a headline — not sheer bulk. */
  padding: 3rem 2.5rem;
  font-size: .84rem;
}
.g-hero .g-display { font-size: clamp(1.75rem, 4.6vw, 3.35rem); }
/* The figures hug the FAR edge, so the panel has a clean line down both
   sides. Right-aligned inside a fractional column they came to rest in the
   middle of the panel, level with nothing — the same fault as the narrow
   hero, one level down. `auto` sizes the column to the figures and `end`
   puts their edge on the padding line. */
.g-hero-figs { display: grid; gap: var(--g-s4); text-align: end; }
.g-hero-figs > div { display: grid; gap: .1rem; }
.g-hero-figs b { font-size: 1.3rem; font-weight: 800; color: #fff; line-height: 1.1; }
.g-hero-figs span { font-size: .72rem; color: rgba(255, 255, 255, .68); }
/* one column on a phone: two columns of 40ch each is one column of 20ch */
@media (max-width: 760px) {
  .g-hero { grid-template-columns: 1fr; gap: var(--g-s5); }
  .g-hero-figs { grid-template-columns: repeat(3, 1fr); }
}

/* ---- 4c. tabs, as a segmented control ----
   THE ACTIVE TAB DOES NOT GET THE GRADIENT. It used to, and that put two
   gradient objects on a screen whose whole hierarchy rests on there being
   exactly one — the moment the accent is in two places it stops pointing at
   anything. The active tab instead RISES: a raised surface in a sunk track,
   which is how a physical segmented control reads, plus a hairline of gradient
   under it so the brand is still present without being loud.

   The track is a single sunk pill rather than separate outlined chips. Chips
   make every tab an object competing for attention; a track makes the group
   one object with a position in it, which is what a tab bar actually is.

   The moving underline is drawn on the button rather than as a sliding element,
   because a slider needs JS to know where to be, and a tab bar that needs
   JavaScript to look right is broken for the first paint of every page load. */
.g-tabs {
  position: relative;
  display: inline-flex; gap: .15rem; padding: .25rem;
  background: var(--g-sunk); border-radius: var(--g-r-pill);
  border: 1px solid var(--g-line);
  max-width: 100%; overflow-x: auto; scrollbar-width: none;
}

/* ---- the travelling highlight ----
   One pill that MOVES between tabs rather than a highlight that switches off
   here and on over there. The eye follows a moving object and reads the two
   tabs as one control with a position in it; a teleport reads as two separate
   things blinking, and you lose track of where you came from.

   PHYSICAL left, not inset-inline-start, and translateX rather than a logical
   offset — offsetLeft is measured from the physical left edge in both
   directions, so pairing it with a logical property would send the pill the
   wrong way in RTL. Mixing the two coordinate systems is the standard way this
   component breaks in Arabic.

   Width AND position are transitioned, so a narrow tab widening into a long
   one stretches rather than jumping.

   .slid is added by JS. Without it the ::before has no width and the active
   tab keeps its own background — so a tab bar still shows which tab is active
   before the script runs, or if it never does. */
.g-tabs::before {
  content: ""; position: absolute; left: 0; top: .25rem; bottom: .25rem;
  width: var(--tw, 0); transform: translateX(var(--tx, 0));
  border-radius: var(--g-r-pill); background: var(--g-surface);
  box-shadow: var(--g-edge), 0 1px 2px rgba(0, 0, 0, .18);
  pointer-events: none;
  transition: transform var(--g-t-slow) var(--g-ease-mag),
              width var(--g-t-slow) var(--g-ease-mag);
}
/* first paint, and every resize: be where you belong without travelling there */
.g-tabs.noslide::before { transition: none; }
/* once the slider is carrying the highlight the tab must stop drawing its own,
   or the two overlap and the moving one looks like a ghost */
.g-tabs.slid .g-tab.on, .g-tabs.slid .g-tab.active { background: transparent; box-shadow: none; }
@media (prefers-reduced-motion: reduce) { .g-tabs::before { transition: none; } }
.g-tabs::-webkit-scrollbar { display: none; }
.g-tab {
  position: relative; z-index: 1; flex: none;
  font: inherit; font-size: .78rem; font-weight: 600; white-space: nowrap;
  padding: .45rem 1rem; border: 0; border-radius: var(--g-r-pill);
  background: transparent; color: var(--g-muted); cursor: pointer;
  transition: color var(--g-t) var(--g-ease), background var(--g-t) var(--g-ease);
}
.g-tab:hover { color: var(--g-title); background: var(--g-sunk); }
/* .active as well as .on: one screen had already drifted to that spelling, and
   renaming a class its JavaScript sets is a bigger risk than accepting both. */
.g-tab.on, .g-tab.active {
  color: var(--g-title); background: var(--g-surface);
  box-shadow: var(--g-edge), 0 1px 2px rgba(0, 0, 0, .18);
}
/* the hairline: 60% of the tab, centred, so it reads as an indicator rather
   than as a border on a box */
.g-tab.on::after, .g-tab.active::after {
  content: ""; position: absolute; inset-inline: 20%; bottom: .28rem; height: 2px;
  border-radius: 2px; background: var(--g-grad-flat);
}
/* a count riding on a tab — how many drafts, how many unpaid */
.g-tab i {
  font-style: normal; font-size: .69rem; font-weight: 700;
  margin-inline-start: .35rem; padding: .05rem .35rem;
  border-radius: var(--g-r-pill); background: var(--g-sunk); color: var(--g-muted);
}
.g-tab.on i, .g-tab.active i { background: var(--g-grad-flat); color: #fff; }
/* ---- 4d. the rolling word ----
   A headline that names every service by becoming each one in turn. It says
   more than a list does and takes one line to do it, which matters on a phone
   where a six-item service grid is six screens of scrolling before anybody
   learns what the company sells.

   THE MASK IS THE COMPONENT. Words are stacked in a column that slides; the
   overflow crops it to exactly one line, so a word arrives from below and the
   old one leaves above — the movement carries the meaning that these are
   alternatives, which a cross-fade does not.

   Height is set in `em` and the column steps by exactly 100%, so the roll stays
   locked to the type size at any clamp value. A pixel height would drift out of
   register the moment the headline responded to a different viewport.

   ARABIC KEEPS ITS JOINS. The display class strips tracking for RTL already;
   this adds nothing that would re-break it, and the words are whole strings
   rather than split into characters — per-character animation is the usual way
   a beautiful Latin effect destroys an Arabic word. */
/* THREE LEVELS, and each has exactly one job. Collapsing any two of them is
   what broke this the first time: the height that crops to one line was put on
   the COLUMN, and the per-word wrappers were left inline — so all the words sat
   on a single line, the column blew the page width out sideways, and stepping
   it up by 300% scrolled the headline into blank space.

     .g-roll            the mask   — one line tall, crops
     .g-roll > span     the column — as tall as all the words, this is what moves
     .g-roll > span > * each word  — one line tall each, block so they stack */
.g-roll {
  display: inline-block; position: relative;
  height: 1.15em; overflow: hidden; vertical-align: bottom;
}
.g-roll > span {
  display: block;
  transition: transform var(--g-t-slow) var(--g-ease-mag);
}
.g-roll > span > * {
  display: block; height: 1.15em; line-height: 1.15em; white-space: nowrap;
}
/* the coloured word is the one thing on the line that moves, so it takes the
   accent — this is the headline's single loud element */
.g-roll b {
  font-weight: inherit;
  background: var(--g-grad-flat); -webkit-background-clip: text;
  background-clip: text; color: transparent;
}
@media (prefers-reduced-motion: reduce) {
  .g-roll > span { transition: none; }
}
/* ---- 5. card chrome ----
   The corner action and the stacked avatars — small, and most of why a card
   reads as designed rather than as a div with a border. */
.g-open {
  position: absolute; inset-inline-end: var(--g-s4); top: var(--g-s4);
  width: 30px; height: 30px; display: grid; place-items: center;
  border: 1px solid var(--g-line); border-radius: var(--g-r-el);
  background: transparent; color: inherit; cursor: pointer;
  transition: background var(--g-t) var(--g-ease), transform var(--g-t) var(--g-ease);
}
.g-open:hover { background: var(--g-sunk); transform: translateY(-1px); }

.g-faces { display: inline-flex; }
.g-faces img, .g-faces span {
  width: 28px; height: 28px; border-radius: 50%; object-fit: cover;
  border: 2px solid var(--g-surface); margin-inline-start: -9px;
  display: grid; place-items: center; font-weight: 700;
  background: var(--g-raised); color: var(--g-ink);
  /* initials are two glyphs and a space, which wrapped to two lines inside a
     28px circle and read as a rendering fault. One line, always. */
  font-size: .62rem; line-height: 1; white-space: nowrap; overflow: hidden;
}
.g-faces > :first-child { margin-inline-start: 0; }

/* ---- 6. the display headline ---- */
.g-display {
  font-weight: 800; line-height: .95; letter-spacing: -.02em;
  font-size: clamp(2.2rem, 6vw, 4.2rem); text-wrap: balance;
}
/* ARABIC NEVER TAKES THE TRACKING. Negative letter-spacing on an Arabic string
   pulls the joins apart and the word stops being a word — the same bug that
   broke the invoice labels. Latin display type wants it; Arabic must be
   exempted explicitly, not hoped about. */
:is([lang="ar"], [dir="rtl"]) .g-display {
  letter-spacing: normal;
  /* AND THE LINE HEIGHT. .95 is a Latin display setting — it works because
     Latin caps sit inside a shallow band. Arabic does not: a هـ descender and
     the dots of a ي reach past that band, so two lines of Arabic at .95
     physically overlap, which is what «كل شيء / في شاشة واحدة» was doing on
     the overview. Exempted here for the same reason the tracking is, and in
     the same place, so the next Arabic headline inherits the fix. */
  line-height: 1.22;
}

/* ---- 7. entrances ----
   Applied on load and on view change, staggered by index. Cheap, and the one
   piece of motion that makes a screen feel composed rather than dumped. */
@keyframes g-rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.g-in { animation: g-rise var(--g-t-slow) var(--g-ease) both; }
.g-in:nth-child(1) { animation-delay: 0ms; }
.g-in:nth-child(2) { animation-delay: calc(var(--g-stagger) * 1); }
.g-in:nth-child(3) { animation-delay: calc(var(--g-stagger) * 2); }
.g-in:nth-child(4) { animation-delay: calc(var(--g-stagger) * 3); }
.g-in:nth-child(5) { animation-delay: calc(var(--g-stagger) * 4); }
.g-in:nth-child(n+6) { animation-delay: calc(var(--g-stagger) * 5); }

/* figures that count up to their value rather than appearing at it */
.g-num { font-variant-numeric: tabular-nums; }

/* ═══ reduced motion ═════════════════════════════════════════════════════════
   Not a courtesy. Vestibular disorders are real and a finance panel is not the
   place to make somebody feel ill; everything here still WORKS with every
   animation off, because none of it carries meaning that the static state does
   not already show. */
@media (prefers-reduced-motion: reduce) {
  .g-in { animation: none; }
  .g-seg > i, .g-ring .val, .g-open { transition: none; }
  .g-aurora { background-attachment: initial; }
}

/* ---- 9. the line chart ----
   Twelve months as a shape rather than twelve numbers. The SVG is stretched to
   the card with preserveAspectRatio="none", so every stroke has to be declared
   non-scaling or the horizontal stretch would thin the lines to nothing. */
.g-line { position: relative; }
.g-line svg { display: block; width: 100%; height: clamp(180px, 24vw, 260px); overflow: visible; }
.g-line .grid line { stroke: var(--g-line); stroke-width: 1; vector-effect: non-scaling-stroke; }
/* :not(.area) — a CSS `fill` beats the fill ATTRIBUTE on the element, so a
   blanket `fill: none` here silently cancelled fill="url(#…)" on the area and
   the gradient under the line never painted. Presentation attributes are the
   lowest-priority source there is; this is the classic way an SVG gradient
   vanishes with no error anywhere. */
.g-line path:not(.area) { fill: none; }
.g-line path { vector-effect: non-scaling-stroke; stroke-linecap: round; stroke-linejoin: round; }
.g-line path.area { stroke: none; }
/* the read-against line: present, quiet, dashed so it is never mistaken for
   the series the card is about */
.g-line path.b { stroke: var(--g-muted); stroke-width: 2; stroke-dasharray: 5 6; opacity: .75; }
.g-line path.a { stroke: var(--g-pink); stroke-width: 3; }
.g-line .dot { fill: var(--g-pink); stroke: var(--g-surface); stroke-width: 3; vector-effect: non-scaling-stroke; }
/* One cell per month, laid out by the same grid the points were spaced on,
   so a label sits under its point without any measuring. Small on purpose:
   this is an axis, and an axis that competes with the line it labels has
   stopped being an axis. */
.g-line-x {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  margin-top: .35rem;
  /* NOT direction: ltr. x(0) is drawn at the RIGHT edge — month one belongs on
     the right in an RTL panel — so the first cell must be the rightmost one,
     which is exactly what the inherited RTL direction does. Forcing ltr here
     put January under December. */
}
.g-line-x span {
  font-size: .62rem; color: var(--g-muted); text-align: center;
  white-space: nowrap; overflow: hidden;
}
.g-line-key {
  display: flex; gap: 1.2rem; flex-wrap: wrap; margin-top: .7rem;
  font-size: .78rem; color: var(--g-muted);
}
.g-line-key span { display: inline-flex; align-items: center; gap: .4rem; }
.g-line-key b { color: var(--g-title); font-weight: 700; font-variant-numeric: tabular-nums; }
.g-line-key span::before {
  content: ""; width: 14px; height: 3px; border-radius: 2px; background: var(--g-pink);
}
.g-line-key .b::before { background: var(--g-muted); opacity: .75; }
