/* =========================================================================
   VIP CONCIERGE — the board. Page-scoped only, authored from app.css tokens.

   THE ARGUMENT. We sell stone, and a board about people looking at stone was
   showing the stone at 56×34px inside a 300px card. Here the slab IS the tile,
   and the client's face and the running clock sit on it at a size you can read
   from a desk away. Nothing else on this page is bigger than the thing the
   client is actually looking at.

   THE BOARD IS LIGHT. A pass on navy came first — a photograph does light up on
   ink — but it made one page of the portal look like a different product, and
   the darkness was doing a job the tiles already do for themselves: every tile
   carries its own scrim, so white type over stone is legible whatever the board
   behind it is. The contrast that matters is photo-against-page, and a
   near-white ground gives the photographs more of it than navy did.

   MARKS ARE FOR THE EXCEPTION, NEVER THE MAJORITY. This is the rule the whole
   board is built on, and it is what makes it survive ten clients instead of
   three. Ownership already sorts at the GROUP level, so inside "On Your Floor"
   a gold ring on every tile and your own face in every corner say nothing the
   heading did not — and at ten they stop being marks at all and become the
   wallpaper. Both come off in that group. What is left is one gold frame on the
   whole board, on whichever tile the rank put first.

   Namespaces: .ga-* is the wall, .co-* the primitives that survive from the
   card board (the live flag, the unanswered bubble, the empty and stale states,
   the Keep Departed strip).
   ========================================================================= */

:root { --ga-gold: #D4AF37; }

/* Sizes that used to ride in style attributes. */
.ic-primary { color: var(--color-primary); }
.ic-15 { width: 15px; height: 15px; }
.dd-224 { min-width: 224px; }

/* ==========================================================================
   THE BAND — the floor in five figures
   memory.md §6: value first, label tight beside it, never spread to opposite
   edges. Five figures on one line divided by hairlines; five cards here would
   be five times the furniture for the same five numbers, and the page's
   subject is below this strip, not in it.

   Every figure is DERIVED from the tiles, so the strip cannot drift from the
   board it summarises. Two of the five are never still: the longest visit ticks
   with the tiles' own clock, and the row climbs to its values as the board
   paints. That is the difference between a page that reports the floor and a
   page that feels like it.
   ========================================================================== */
.ga-band { display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
           margin-bottom: 14px; padding: 11px 16px; border-radius: 7px;
           background: var(--color-surface); border: 1px solid var(--color-border);
           box-shadow: var(--shadow-sm); }
.ga-fig { display: inline-flex; align-items: baseline; gap: 7px; }
.ga-fig__v { font-size: 21px; font-weight: 600; line-height: 1.1; color: var(--color-text);
             font-variant-numeric: tabular-nums; }
.ga-fig__k { font-size: 11px; font-weight: 500; color: var(--color-text-subtle);
             text-transform: uppercase; letter-spacing: .06em; }
.ga-fig--live .ga-fig__v { color: var(--color-success); }
.ga-fig--money .ga-fig__v { color: #2F7F68; }
.ga-fig--alert .ga-fig__v { color: var(--color-danger); }
.ga-fig--helped .ga-fig__v { color: var(--color-text-muted); }
/* The only dot in the strip, on the one figure that counts people physically
   here. It breathes on the same 1.8s as the header badge, so the page has one
   heartbeat rather than three. */
.ga-fig__dot { width: 8px; height: 8px; border-radius: 50%; flex: none;
               align-self: center; background: var(--color-success);
               box-shadow: 0 0 0 3px rgba(63,158,130,.20);
               animation: ga-bandpulse 1.8s ease-in-out infinite; }
@keyframes ga-bandpulse {
  0%, 100% { box-shadow: 0 0 0 3px rgba(63,158,130,.20); }
  50%      { box-shadow: 0 0 0 6px rgba(63,158,130,0); }
}
.ga-figsep { width: 1px; height: 22px; background: var(--color-border); flex: none; }
.ga-bandspacer { flex: 1; }
.ga-bandhint { font-size: 11px; color: var(--color-text-subtle); }
/* The band reports the floor; this one line says what the floor is worth right
   now. It is written ONLY on a loaded board — "they are browsing right now" is
   the one sentence a stale poll cannot be allowed to keep saying. */
.ga-bandnote { display: flex; align-items: center; gap: 9px; margin-bottom: 14px;
               padding: 9px 14px; background: var(--color-warn-50);
               border: 1px solid var(--color-warn); border-radius: 5px;
               font-size: 12px; color: var(--color-text); }
.ga-bandnote svg { width: 15px; height: 15px; color: var(--color-warn); flex: none; }
@media (prefers-reduced-motion: reduce) { .ga-fig__dot { animation: none; } }

/* ==========================================================================
   THE WALL
   ========================================================================== */
/* Near-white rather than white so the tiles read as hung ON something — and so
   a tile's own shadow has a ground to fall on. */
.ga-panel { padding: 18px 20px 22px; background: var(--color-surface-alt); }

/* ---- Group headings -------------------------------------------------------
   The heading is a rule across the page with the label sitting on it, so a
   group reads as a band rather than as small text floating above loose tiles.
   The gap ABOVE a heading separates it from the group before; the gap below
   binds it to its own tiles. They must not be equal. */
.ga-seclabel { display: flex; align-items: center; gap: 10px; margin: 0 0 12px; flex-wrap: wrap;
               --ga-accent: #8FA6C0; }
.ga-grid + .ga-seclabel, .ga-chips + .ga-seclabel { margin-top: 30px; }
.ga-seclabel--mine { --ga-accent: var(--ga-gold); }
.ga-seclabel--helped { --ga-accent: var(--color-success); }
.ga-sect { display: inline-flex; align-items: center; gap: 9px; flex: none;
           font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .09em;
           color: var(--color-text); }
.ga-sect::before { content: ""; width: 8px; height: 8px; border-radius: 50%; flex: none;
                   background: var(--ga-accent); }
.ga-count { flex: none; padding: 0 7px; border-radius: 5px; font-size: 11px; font-weight: 600;
            color: #1B2836; background: var(--ga-accent); font-variant-numeric: tabular-nums; }
.ga-hint { font-size: 11px; color: var(--color-text-subtle); }
/* A hairline that runs to the end of the row so the heading does not float. It
   takes the leftover width, so a long hint simply shortens it. */
.ga-seclabel::after { content: ""; flex: 1; min-width: 24px; height: 1px;
                      background: var(--color-border); }

/* ---- The tile -------------------------------------------------------------
   A photograph with everything else living on top of it. A fixed frame so a
   wall of tiles is a wall and not a ragged edge.

   THREE ACROSS on a desktop wall, and that number is the point rather than a
   consequence: a tile is A THIRD of the row, which is the widest anything on
   this board is allowed to be. Four across made the stone too small to be worth
   photographing; a tile that took half a row, or a whole one, stopped being a
   tile and became a banner. Below a desktop the grid falls to two and then one
   because it must — a third of 380px is not a photograph of anything. */
.ga-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 16px; }
.ga-tile { position: relative; min-height: 300px; display: flex; flex-direction: column;
           justify-content: flex-end; overflow: hidden; border-radius: 7px;
           background: #1B2836;
           box-shadow: 0 0 0 1px var(--color-border-strong), var(--shadow-md);
           transition: transform 200ms ease, box-shadow 200ms ease; }
.ga-tile:hover { transform: translateY(-3px);
                 box-shadow: 0 0 0 1px var(--color-text-subtle), var(--shadow-lg); }

/* ---- THE HERO — one gold frame on the board -------------------------------
   Not "the tile that is yours" — the tile the rank put first inside your own
   group. That distinction is what lets the mark survive scale: "yours" is true
   of ten tiles and stops meaning anything, "go here next" is true of exactly
   one whatever the count.

   IT GETS NO EXTRA WIDTH. It keeps the same third-of-a-row footprint as every
   other tile and earns its place with the ring, a bigger clock and name, the
   basket figure and one extra act. A hero that spanned two columns — or the
   whole row when it happened to be alone — was solving for a board with one
   client on it and wrecking the grid on a board with ten: rows stopped lining
   up, and the tile you were meant to look at first was the one whose
   photograph had been stretched thinnest. Rank is what puts it first now,
   not size. */
.ga-tile--hero { box-shadow: 0 0 0 2px var(--ga-gold), 0 0 20px -5px rgba(212,175,55,.55),
                             var(--shadow-md); }
.ga-tile--hero:hover { box-shadow: 0 0 0 2px #E5C356, 0 0 26px -3px rgba(212,175,55,.75),
                                   var(--shadow-lg); }
/* Nobody's client. Not decoration — the one fact on this board that a group
   heading cannot state, because "not yours" and "nobody's" are different
   problems and only one of them is going to fix itself. */
.ga-tile--orphan { box-shadow: 0 0 0 2px rgba(76,116,166,.55), var(--shadow-md); }

/* Inside your own group the rep's face is your own face, printed once per tile.
   The heading already said whose these are. */
.ga-grid--own .ga-rep { display: none; }

/* ---- Another rep's client — SMALLER ---------------------------------------
   Real work, and not your first call. The size difference is the judgement,
   made before a single name is read: your own floor is where you look, this is
   where you look next. It keeps everything that decides whether to step in —
   the stone, the face, the clock, whether anyone is waiting on a reply — and
   loses only the scale. */
.ga-grid--other { grid-template-columns: repeat(auto-fill, minmax(272px, 1fr)); gap: 13px; }
.ga-grid--other .ga-tile { min-height: 262px; }
.ga-grid--other .ga-body { gap: 9px; padding: 34px 13px 12px; }
.ga-grid--other .ga-media__cap { top: 12px; }
.ga-grid--other .ga-media__cap .main { font-size: 13px; }
.ga-grid--other .ga-who > .avatar { width: 30px; height: 30px; font-size: 11px; }
.ga-grid--other .ga-who a.main { font-size: 13.5px; }
.ga-grid--other .ga-clock { font-size: 24px; }
.ga-grid--other .ga-rep .avatar,
.ga-grid--other .ga-rep--none { width: 25px; height: 25px; }
.ga-grid--other .ga-rep--none svg { width: 15px; height: 15px; }
.ga-grid--other .ga-ghost { padding: 5px 9px; font-size: 11.5px; }

.ga-media { position: absolute; inset: 0; }
/* NOT every stone in the catalogue has a photograph, and here that is not a
   small gap — the photo IS the tile. A stone with no image falls back to a
   quarried-slate gradient with its initials cut into it, so it reads as a stone
   we have no picture of rather than as a tile that failed to load. */
.ga-media__img { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 0;
                 display: flex; align-items: center; justify-content: center;
                 background: linear-gradient(145deg, #2C3B4C 0%, #1B2836 55%, #24313F 100%);
                 font-size: 40px; font-weight: 600; letter-spacing: .06em;
                 color: rgba(255,255,255,.13); }
.ga-media__img.is-photo { width: 100%; height: 100%;
                          background-size: cover; background-position: center; }
/* The scrim, in two parts, and NEITHER of them is a percentage of the tile.
   A percentage scrim scales with the tile, so shrinking a tile shrank the clear
   window with it and the stone turned into a strip with dimmed bands above and
   below — full-bleed, and not looking it. On a black marble nobody could tell,
   which is exactly why it survived a review.

   The head is a fixed 60px, which is one line of stone name.
   The FOOT is not here at all: it lives on .ga-body, so the dark is exactly as
   tall as the words that need it, at any tile size, and never a pixel more.
   Everything between is the slab, untouched — a taller tile just shows more. */
.ga-media::after { content: ""; position: absolute; inset: 0;
                   background: linear-gradient(to bottom, rgba(8,15,24,.46) 0px,
                                                          rgba(8,15,24,0) 60px); }
/* The stone's own name, on the stone. */
.ga-media__cap { position: absolute; top: 14px; left: 15px; right: 15px; z-index: 2; }
.ga-media__cap .main { color: #fff; font-size: 15px; font-weight: 600;
                       text-shadow: 0 1px 8px rgba(0,0,0,.5); }
.ga-media__cap .sub { color: rgba(255,255,255,.72); font-size: 11px; }
.ga-tile--hero .ga-media__cap .main { font-size: 18px; }
/* A departed client's tile carries a flag; a live one does not. On the Live tab
   every tile is live, so "Viewing Now" printed ten times was a label repeating
   what the tab already said — the exception is having LEFT, and that is what
   gets the words. */
.ga-tile--left .ga-media__cap { top: 44px; }

.ga-flag { position: absolute; top: 12px; left: 14px; z-index: 3;
           display: inline-flex; align-items: center; gap: 6px;
           padding: 3px 9px; border-radius: 5px; background: rgba(255,255,255,.14);
           backdrop-filter: blur(4px); color: #fff;
           font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; }
.ga-flag--off { background: rgba(255,255,255,.10); color: rgba(255,255,255,.62); }

.ga-rep { position: absolute; top: 11px; right: 12px; z-index: 3; display: inline-flex; }
.ga-rep .avatar { width: 28px; height: 28px; font-size: 10px; flex: none;
                  box-shadow: 0 0 0 2px rgba(255,255,255,.28); }
/* The gap is the point, so it cannot be the one mark on the tile that
   disappears — over a pale marble a bare white glyph did exactly that. */
.ga-rep--none { width: 28px; height: 28px; align-items: center; justify-content: center;
                border-radius: 50%; background: rgba(20,30,42,.55); backdrop-filter: blur(4px);
                box-shadow: 0 0 0 2px rgba(255,255,255,.28); }
.ga-rep--none svg { width: 17px; height: 17px; color: #fff; }

/* ---- The body, over the photograph --------------------------------------- */
/* The body carries its own darkness. The top padding is not spacing — it is the
   run-up the gradient needs to reach zero before it clears the text, which is
   why it is three times the bottom. */
.ga-body { position: relative; z-index: 2; display: flex; flex-direction: column; gap: 11px;
           padding: 40px 15px 14px;
           background: linear-gradient(to top, rgba(8,15,24,.94) 0%, rgba(8,15,24,.90) 42%,
                                               rgba(8,15,24,0) 100%); }
.ga-who { display: flex; align-items: center; gap: 10px; }
.ga-who .cell-stack { min-width: 0; flex: 1; }
.ga-who > .avatar { width: 34px; height: 34px; font-size: 12px; flex: none;
                    box-shadow: 0 0 0 2px rgba(255,255,255,.2); }
.ga-who a.main { color: #fff; font-size: 15px; font-weight: 600; }
.ga-who a.main:hover { color: #BBD6F2; }
.ga-who .sub { color: rgba(255,255,255,.62); font-size: 11px; }
.ga-tile--hero .ga-who a.main { font-size: 18px; }
.ga-tile--hero .ga-who > .avatar { width: 40px; height: 40px; font-size: 14px; }
/* The whole tile is the link; the name carries it so the buttons stay above. */
.ga-who a.main::after { content: ""; position: absolute; inset: 0; z-index: 1; }
/* An empty slot is still a flex item, and gap does not care that it is 0px wide. */
.ga-who [data-msgslot]:empty { display: none; }

.ga-nums { display: flex; align-items: center; gap: 11px; }
.ga-clock { flex: none; font-size: 30px; font-weight: 600; line-height: 1; color: #fff;
            letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
.ga-tile--hero .ga-clock { font-size: 38px; }
/* Stale board, frozen clock. The figures stay on screen — they were true when
   they landed — but they stop pretending to be current. */
.ga-clock--stale { color: rgba(255,255,255,.55); }
.ga-numstack { display: inline-flex; flex-direction: column; gap: 2px; min-width: 0; }
.ga-numk { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .07em;
           color: rgba(255,255,255,.55); }
.ga-numv { font-size: 11px; color: rgba(255,255,255,.82); }
.ga-numstack--money { align-items: flex-end; text-align: right; }
.ga-money { font-size: 17px; font-weight: 600; line-height: 1.1; color: #6EE7B7; }
.ga-spacer { flex: 1; }

.ga-act { display: flex; align-items: center; gap: 10px; position: relative; z-index: 3; }
/* A button on a photograph. It has to hold its shape without a fill loud enough
   to fight the stone — a hairline on frosted glass does both. */
.ga-ghost { display: inline-flex; align-items: center; gap: 6px; flex: none;
            padding: 6px 11px; border-radius: 5px; cursor: pointer;
            background: rgba(255,255,255,.10); border: 1px solid rgba(255,255,255,.22);
            backdrop-filter: blur(4px); text-decoration: none;
            font-family: inherit; font-size: 12px; font-weight: 500; color: #EAF1F8;
            transition: background 150ms ease, border-color 150ms ease; }
.ga-ghost svg { width: 14px; height: 14px; }
.ga-ghost:hover { background: rgba(255,255,255,.19); border-color: rgba(255,255,255,.42);
                  color: #fff; }
.ga-claimed { display: inline-flex; align-items: center; gap: 7px; flex: none;
              font-size: 12px; color: rgba(255,255,255,.78); }
.ga-claimed .avatar { width: 20px; height: 20px; font-size: 8px; flex: none; }

/* ---- Live Assist — the one act that starts a conversation ------------------
   `.btn-primary` is a cobalt built for white pages. Dropped onto a photograph
   under a scrim it lands at almost the same value as the stone behind it, and
   the loudest control on the board ended up the quietest thing on the tile.

   IT IS GOLD, and it is gold the way gold looks: not one colour but a colour
   with a HIGHLIGHT running through it — dark at the edges, near-white where the
   light catches. That is what the five-stop gradient buys, and it is the
   difference between mustard and metal. An orange pass came first and was too
   loud: on a wall of ten tiles, ten orange buttons stopped being a call and
   became the wallpaper.

   Gold is also the hero ring, and that overlap is deliberate rather than
   tolerated: the tile to act on is ringed in gold and the act it wants is gold,
   so "this one" and "go" are one idea instead of two competing ones. What keeps
   the warm marks apart is SHAPE. The hero ring and the unanswered bubble are
   OUTLINES; the call to action is the only warm thing on this board that is
   FILLED, and at 13px bold on a photograph that reads instantly.
   ------------------------------------------------------------------------- */
.ga-act .btn-primary {
  position: relative;
  background: linear-gradient(135deg, #A9791C 0%, #D9B440 20%, #FBF0BE 46%,
                              #E4C258 60%, #B4831F 100%);
  border-color: #F3E0A4;
  color: #33240A;
  font-weight: 700;
  letter-spacing: .01em;
  box-shadow: 0 4px 16px -4px rgba(180,131,31,.6);
}
.ga-act .btn-primary:hover {
  background: linear-gradient(135deg, #C08D24 0%, #EBC85A 20%, #FFFAE0 46%,
                              #F2D573 60%, #C89329 100%);
  border-color: #FFF8DE;
  transform: translateY(-1px);
}
.ga-act .btn-primary svg { width: 15px; height: 15px; }

/* ---- The call ------------------------------------------------------------
   TWO rings, staggered half a cycle apart and thrown 16px clear. One ring at
   2.4s reads as a slow blink; two overlapping read as something signalling.
   They are drawn on pseudo-elements so the button's own shadow stays put and
   the ring can leave the button's box without any layout moving — nothing on
   this tile reflows while it calls. pointer-events:none because a ring outside
   the button must not widen what the cursor can press.

   It calls only where the client is still in the building. A ring throbbing on
   a callback list is a board shouting about something nobody can act on. */
.ga-act .btn-primary::before,
.ga-act .btn-primary::after {
  content: ""; position: absolute; inset: 0; border-radius: 5px;
  pointer-events: none; z-index: -1;
}
@keyframes ga-call {
  0%        { box-shadow: 0 0 0 0 rgba(212,175,55,.9); }
  70%, 100% { box-shadow: 0 0 0 16px rgba(212,175,55,0); }
}
.ga-tile:not(.ga-tile--left) .ga-act .btn-primary::before,
.ga-tile:not(.ga-tile--left) .ga-act .btn-primary::after {
  animation: ga-call 2.4s cubic-bezier(.22,.61,.36,1) infinite;
}
.ga-tile:not(.ga-tile--left) .ga-act .btn-primary::after { animation-delay: 1.2s; }
/* The hero leads. Its pair runs on the off-beat so two tiles side by side read
   as two clients waiting rather than as one blinking widget. */
.ga-tile--hero .ga-act .btn-primary::before { animation-delay: .6s; }
.ga-tile--hero .ga-act .btn-primary::after  { animation-delay: 1.8s; }
@media (prefers-reduced-motion: reduce) {
  .ga-act .btn-primary::before, .ga-act .btn-primary::after { animation: none; }
}

/* The unanswered bubble keeps its red — it is the one fact on the board about
   us failing rather than about a client browsing — but on a tile it needs its
   own dark ground rather than paper white. */
.ga-who .co-msg { background: #2A1614; border-color: #E1786A; color: #FFB4A6; }
.ga-who .co-msg::after { border-color: #2A1614 transparent transparent transparent; }

/* ---- Departed — half light ------------------------------------------------
   Nothing here is happening, and a gallery says that by turning the lamp down,
   not by moving the picture. */
.ga-tile--left .ga-media__img { filter: grayscale(.85) brightness(.95); }
.ga-tile--left .ga-clock { color: rgba(255,255,255,.86); }

/* ---- Being helped — two facts, no more ------------------------------------
   Somebody is already with her. A tile would be a tile-shaped way of saying
   nothing, so this group is chips: who she is, and who is in the chat. */
.ga-chips { display: flex; flex-wrap: wrap; gap: 10px; }
.ga-chip { position: relative; display: inline-flex; align-items: center; gap: 9px;
           min-width: 244px; padding: 9px 12px 9px 14px; border-radius: 7px;
           background: var(--color-success-50); border: 1px solid #C9E7DD;
           text-decoration: none; transition: border-color 150ms ease; }
.ga-chip::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 4px;
                   border-radius: 5px 0 0 5px; background: var(--color-success); }
.ga-chip:hover { border-color: var(--color-success); }
.ga-chip > .avatar { width: 30px; height: 30px; font-size: 11px; flex: none; }
.ga-chip .cell-stack { min-width: 0; flex: 1; }
.ga-chip .main { font-size: 13px; font-weight: 500; color: var(--color-text); }
.ga-chip .sub { font-size: 11px; color: var(--color-text-subtle); }
.ga-chip__rep .avatar { width: 22px; height: 22px; font-size: 9px; flex: none; }

/* ---- The wall lights up one tile at a time -------------------------------- */
@keyframes ga-rise { from { opacity: 0; transform: translateY(12px) scale(.985); }
                     to   { opacity: 1; transform: none; } }
.ga-grid > .ga-tile, .ga-chips > .ga-chip {
  animation: ga-rise 460ms cubic-bezier(.16,.84,.44,1) backwards;
}
.ga-grid > *:nth-child(2), .ga-chips > *:nth-child(2) { animation-delay: 60ms; }
.ga-grid > *:nth-child(3) { animation-delay: 120ms; }
.ga-grid > *:nth-child(4) { animation-delay: 180ms; }
.ga-grid > *:nth-child(5) { animation-delay: 240ms; }
/* Past the fifth the stagger stops growing — a tenth tile waiting half a second
   to appear is a board that looks slow, not a board that looks alive. */
.ga-grid > *:nth-child(n+6) { animation-delay: 280ms; }

/* Loading. A still skeleton is not read as "this is loading" — it is read as a
   layout that failed to render. */
.ga-skel { background: linear-gradient(90deg,
             var(--color-control) 0%, var(--color-surface) 45%,
             var(--color-surface) 55%, var(--color-control) 100%);
           background-size: 200% 100%; box-shadow: none;
           animation: ga-shimmer 1.3s linear infinite; }
@keyframes ga-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }

/* ==========================================================================
   PRIMITIVES that survive from the card board
   ========================================================================== */

/* ---- The LIVE flag. It OBEYS THE POLL. ------------------------------------
   The header badge is the board's freshness claim, and it only animates under
   --online, which only a landed poll sets. A "Live" label pulsing over a board
   that stopped refreshing four minutes ago is the exact lie the fail-to-offline
   rule exists to prevent, and a label is a louder lie than a dot. It is GREEN,
   not broadcast-red — red already means "unanswered message" on this page, and
   a board cannot spend the same colour on "act now" and "we failed her". */
.co-livebadge { display: inline-flex; align-items: center; gap: 8px; flex: none;
                height: 36px; padding: 0 14px 0 12px; border-radius: 5px;
                background: var(--color-surface); border: 1px solid var(--color-border);
                font-family: 'Hanken Grotesk', sans-serif; font-size: 13px; font-weight: 600;
                letter-spacing: .04em; text-transform: uppercase; color: var(--color-text-subtle); }
.co-livebadge.co-presence--online { background: var(--color-success-50); border-color: #A7F3D0; color: #047857; }
.co-livebadge .co-liveflag,
.co-livebadge .co-liveflag__dot { width: 10px; height: 10px; }

.co-liveflag { position: relative; display: inline-flex; flex: none; width: 8px; height: 8px; }
.co-liveflag__dot { position: relative; width: 8px; height: 8px; border-radius: 50%;
                    background: var(--color-border-strong); flex: none; }
.co-presence { display: inline-flex; align-items: center; gap: 6px; font-size: 11px;
               color: var(--color-text-subtle); white-space: nowrap; }
.co-presence--online { color: var(--color-text-muted); }
.co-presence--online .co-liveflag__dot { background: var(--color-success); }
.co-presence--online .co-liveflag__dot::before,
.co-presence--online .co-liveflag__dot::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  background: var(--color-success); animation: colivering 1.8s ease-out infinite;
}
.co-presence--online .co-liveflag__dot::after { animation-delay: .9s; }
@keyframes colivering { 0% { transform: scale(1); opacity: .55; } 100% { transform: scale(3.2); opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .co-liveflag__dot::before, .co-liveflag__dot::after { animation: none; }
}

/* The tab pair carries mine / others. Two bare numbers are cryptic on their own,
   so the chip is title-ed and the group headings below spell both out in words.
   The pair is NOT two equals: yours is the number you are answerable for, so it
   keeps the chip's 11px while "/ others" drops to 9px and recedes behind it.
   Two type sizes on one baseline hang the small one low — the baseline is the
   glyphs' floor, not their middle — so the chip centres the two boxes instead. */
.tab .count { font-variant-numeric: tabular-nums; display: inline-flex; align-items: center; gap: 3px; }
.tab .count .co-cnt-o { font-size: 9px; line-height: 1; }
/* app.css flips the count chip to white on an active SUBtab, for the reason it
   states — on a tinted active pill the chip blends into it. The active .tab
   carries the identical primary-50 tint and has no such rule, so the chip and
   the tab resolve to the same colour and the number disappears into its own
   background. Same fix, same reason. */
.tab.active .count { background: var(--color-surface); }

/* ---- The unanswered message ------------------------------------------------
   A client talking to us with nobody answering — the one fact here that is
   about US failing rather than about her browsing. It sits in the identity row
   beside the face of the rep who owes the reply, because seeing it and knowing
   who owes it are one motion. The age rides INSIDE the bubble: a filled glyph
   is louder than a dot at a fraction of a badge's footprint, and the number
   needs no label when it is printed on a speech bubble. Long ages round down to
   two characters (17h 45m -> 18h) — the title carries the exact figure, and the
   RANK uses the exact figure too.

   Drawn in CSS, not lucide: the glyph is a 24×24 viewBox and preserveAspectRatio
   letterboxes rather than stretches, so there was no way to widen the bubble
   around the number — and the number was the point. */
.co-msg { position: relative; flex: none; display: inline-flex; align-items: center; justify-content: center;
          min-width: 34px; height: 20px; padding: 0 6px; margin-bottom: 5px;
          border: 1.5px solid var(--color-danger); border-radius: 5px; background: #fff;
          color: var(--color-danger); font-size: 10.5px; font-weight: 500; line-height: 1;
          font-variant-numeric: tabular-nums; letter-spacing: .02em; }
.co-msg::before, .co-msg::after { content: ""; position: absolute; width: 0; height: 0; border-style: solid; }
.co-msg::before { top: 100%; left: 6px; border-width: 6px 5px 0 0;
                  border-color: var(--color-danger) transparent transparent transparent; }
.co-msg::after  { top: calc(100% - 1.5px); left: 7.5px; border-width: 4px 3.5px 0 0;
                  border-color: #fff transparent transparent transparent; }
/* One nudge every five seconds. A movement that happens rarely is noticed where
   one that happens constantly is tuned out. */
@keyframes co-nudge {
  0%, 86%, 100% { transform: translateY(0); }
  91%           { transform: translateY(-3px); }
  95%           { transform: translateY(0); }
}
.co-msg { animation: co-nudge 5s ease-in-out infinite; }

/* ---- Empty · stale --------------------------------------------------------
   The empty state is the state a rep sees most; it is the design, not an edge
   case. Stale is a third condition a polled surface has and it must not look
   like empty — "nobody is here" and "we have not heard from the server" mean
   opposite things to a waiting rep. */
.co-empty { padding: 34px 16px; text-align: center; color: var(--color-text-subtle); font-size: 13px; line-height: 1.6; }
.co-empty svg { width: 30px; height: 30px; color: var(--color-text-subtle); margin-bottom: 8px; }
.co-empty__t { font-size: 14px; font-weight: 600; color: var(--color-text); display: block; margin-bottom: 3px; }
.co-empty__act { margin-top: 14px; }
/* A bell drifting on the spot is the difference between a page that is waiting
   and a page that has stopped. */
@keyframes co-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
.co-empty svg { animation: co-float 4.5s ease-in-out infinite; }

.co-stale { display: flex; align-items: center; gap: 10px; padding: 10px 14px; margin-bottom: 16px;
            background: var(--color-warn-50); border: 1px solid var(--color-warn); border-radius: 5px;
            font-size: 13px; }
.co-stale svg { width: 16px; height: 16px; color: var(--color-warn); flex: none; }

/* ---- The Keep Departed strip, inside the Visited tab ----------------------- */
.co-boardbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 14px;
               padding: 9px 12px; background: var(--color-surface); border: 1px solid var(--color-border);
               border-radius: 5px; }
/* White on white needs its own ground or it stops reading as a control and
   starts reading as a caption (memory.md §13). */
.co-boardbar--inset { background: var(--color-control); border-color: var(--color-control-border); }
.co-setting { display: inline-flex; align-items: center; gap: 8px; }
.co-setting__k { font-size: 10px; text-transform: uppercase; letter-spacing: .06em; font-weight: 600;
                 color: var(--color-text-subtle); }
.co-setting__link, .co-setting__note { font-size: 11px; }
/* .subtabs ships margin-bottom:8px for its designed use (a track hugging the
   panel below). Inside this centred flex row that margin lands in the line box
   and lifts the track ~4px above every sibling it should align with. */
.co-boardbar .subtabs { margin-bottom: 0; }

/* Clients Visibility, in the Page Settings modal — label, choice, consequence,
   read top to bottom so the answer and what it does to the board sit together. */
.co-vis { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.co-vis .subtabs { margin-bottom: 0; }
.co-vis .co-setting__note { line-height: 1.5; }

/* ---- Notification tier in the bell (D4) — escalated vs quiet by SIZE, not hue. */
.co-notif { display: flex; align-items: flex-start; gap: 9px; padding: 9px 12px; }
.co-notif .avatar { width: 26px; height: 26px; font-size: 10px; flex: none; }
.co-notif__b { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.co-notif__t { font-size: 13px; color: var(--color-text); }
.co-notif__s { font-size: 11px; color: var(--color-text-subtle); }
.co-notif--esc { padding: 11px 12px; background: var(--color-primary-50); }
.co-notif--esc .avatar { width: 34px; height: 34px; font-size: 12px; }
.co-notif--esc .co-notif__t { font-size: 14px; font-weight: 600; }

/* ==========================================================================
   RESPONSIVE
   ========================================================================== */
@media (max-width: 900px) {
  .ga-grid { grid-template-columns: 1fr; }
}
@media (max-width: 700px) {
  .co-boardbar { align-items: flex-start; flex-direction: column; }
}
@media (max-width: 480px) {
  .ga-panel { padding: 12px 12px 16px; }
  .ga-clock, .ga-tile--hero .ga-clock { font-size: 26px; }
  .ga-band { gap: 12px; }
  .ga-figsep { display: none; }
}
