/* ==========================================================================
   picks-honeycomb.css - "who's engaging with this pick" band.

   Scope: ONLY the honeycomb owned by picks-honeycomb.js. Deliberately its
   own stylesheet (not picks.css) so it can be built without touching a file
   another concurrent change is editing.

   Geometry decision, written down because it is not obvious from the CSS
   alone: the hex is POINTY-TOP (point at top/bottom, flat-ish edges at
   left/right, per the classic honeycomb.polygon below), because the task's
   "offset by half a cell per row" tessellation is exactly what pointy-top
   hexes do naturally in ROWS. A pointy-top hex's bounding box is TALLER
   than it is wide - the flat left/right edges (the ones that touch a
   same-row neighbour) are also its widest cross-section. The flip therefore
   rotates around the HORIZONTAL axis (rotateX): that axis foreshortens
   HEIGHT, not width, so the left/right edges that matter for same-row
   paging seams stay at full width for the whole rotation. rotateY would
   have foreshortened exactly those edges first. See picks-honeycomb.js's
   header comment for the full reasoning and what could not be verified.

   Row tessellation itself needs no explicit offset rule: each tier's two
   row counts differ by exactly one (7/6, 5/4, 4/3, 3/2), and both rows are
   independently centered - centering a 6-wide row and a 7-wide row of
   identical-width cells produces exactly the half-cell stagger a honeycomb
   needs, for free, with no separate "is-offset" class to keep in sync with
   the row-count table.
   ========================================================================== */

.mh-pick-viewers {
    /* Containing block for the "nobody yet" note overlaid on the lattice. */
    position: relative;
    display: none;
    align-items: center;
    gap: 8px;
    width: 100%;
    box-sizing: border-box;

    --hex-w: clamp(36px, 5.5vw, 56px);
    --mh-hex-gap: 4px;
    --hex-h: calc(var(--hex-w) * 1.1547);
    --mh-hex-flip-dur: 420ms;
}

.mh-pick-viewers.has-content {
    display: flex;
    /* Column so the paging row sits BELOW the grid, not beside it. */
    flex-direction: column;
    align-items: center;
    gap: 6px;
}

/* ---- paging controls (below the grid, owner) -----------------------------
   Gestures stay the primary affordance; these are the reliable fallback and
   the band's only keyboard-reachable control.

   The display rule is scoped to :not([hidden]) deliberately. An
   unconditional `display` on an element the JS toggles via the `hidden`
   attribute outranks the user agent's [hidden] { display: none }, so the row
   would simply never hide and there would be no error saying why. */
.mh-honeycomb-nav-row[hidden] {
    display: none;
}

.mh-honeycomb-nav-row:not([hidden]) {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
}

.mh-honeycomb-nav {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    border: none;
    border-radius: var(--radius-circle);
    background: transparent;
    color: var(--text-secondary);
    cursor: pointer;
    font-size: 0.78rem;
    transition: background var(--dur-base) var(--ease-standard), color var(--dur-base) var(--ease-standard);
}

.mh-honeycomb-nav:hover {
    background: var(--surface-hover);
    color: var(--text);
}

.mh-honeycomb-nav[aria-disabled="true"] {
    opacity: 0.35;
    cursor: default;
    pointer-events: none;
}

/* ---- the grid -------------------------------------------------------- */

.mh-honeycomb-grid {
    align-items: center;
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    min-width: 0;
    padding: calc(var(--hex-h) * 0.14) 0;
    /* Yield the vertical axis to the browser so a swipe here never
       traps the page scroll; the horizontal axis pages the band. */
    touch-action: pan-y;
}

/* Holds all rows at an explicit width JS computes from the tier's widest
   row. This is the element the grid centres, so each row inside keeps its
   own margin-left offset and the cluster can be deliberately asymmetric
   (a row overhanging the ones above it) without drifting off centre. */
.mh-honeycomb-cluster {
    flex-shrink: 0;
}

.mh-honeycomb-row {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    /* MUST stay 0: the lattice pitch IS the hex width, so any gap here
       pushes every column off the lattice and the offset row stops nesting
       (that is what made the spacing read as uneven). Visual separation
       comes from insetting the flipper inside its cell - see --mh-hex-gap. */
    gap: 0;
}

/* Pulls every row after the first up into the row above's points - the
   vertical half of the tessellation. `gap` cannot go negative (the CSS gap
   property rejects negative values outright), so this has to be a margin. */
.mh-honeycomb-row + .mh-honeycomb-row {
    margin-top: calc(var(--hex-h) * -0.25);
}

/* ---- one hex cell ------------------------------------------------------
   Two-faced flip card: .mh-hex-flipper carries the 3D rotation, its two
   .mh-hex-face children are opposite sides of the same card, each clipped
   to the same hex polygon and each hidden on its own back (backface-
   visibility). The BACK face is pre-rotated 180deg in the stylesheet (not
   by JS), and picks-honeycomb.js always paints it with the INCOMING avatar
   before starting the rotation - so what plays on screen is a pure
   rotation with no mid-flight image swap. */

.mh-hex-cell {
    position: relative;
    flex-shrink: 0;
    width: var(--hex-w);
    height: var(--hex-h);
    perspective: 640px;
    transition: transform var(--dur-base) var(--ease-standard);
}


/* Status glow, and the hover mechanic.

   The glow lives on the CELL as a pseudo-element, never on a face. A
   `filter` applied to anything inside .mh-hex-flipper's preserve-3d context
   forces that context flat and the flip silently stops compositing - while
   still hit-testing perfectly, which is what makes that failure so hard to
   spot. This pseudo-element is a SIBLING of the flipper, so the 3D chain is
   untouched.

   It also cannot be a box-shadow: clip-path clips the shadow away with the
   box, so a hexagon's box-shadow is simply invisible. A blurred copy of the
   same clipped shape is the shape-following equivalent. */
.mh-hex-cell::before {
    content: '';
    position: absolute;
    inset: calc(var(--mh-hex-gap) / 2);
    background: var(--mh-hex-ring-color, transparent);
    clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
    filter: blur(5px);
    opacity: 0.4;
    transform: scale(1.03);
    transition: opacity var(--dur-base) var(--ease-standard), transform var(--dur-base) var(--ease-standard);
    pointer-events: none;
}

/* Hover/focus lifts the whole cell rather than the flipper: the flipper's
   transform belongs to the flip keyframes, and a hover transform on the same
   element would be recomposed away on the next tween frame. */
.mh-hex-cell:not(.is-empty):hover,
.mh-hex-cell:not(.is-empty):focus-visible {
    transform: scale(1.15);
    z-index: 6;
}

.mh-hex-cell:not(.is-empty):hover::before,
.mh-hex-cell:not(.is-empty):focus-visible::before {
    opacity: 0.95;
    transform: scale(1.18);
}

.mh-hex-cell:focus-visible {
    outline: none;
}

@media (prefers-reduced-motion: reduce) {
    .mh-hex-cell,
    .mh-hex-cell::before {
        transition: none;
    }
}
/* An unfilled slot keeps its SHAPE (owner: "show a skeleton of the hex, so
   that position is reserved and doesn't seemingly vanish").

   visibility: hidden reserved the layout box but punched a hole in the
   cluster's silhouette, so a short viewer list read as a BROKEN lattice
   rather than a partly filled one - the irregular four-row shape depends on
   its outline being continuous to read as deliberate at all.

   No status glow here: an empty slot has no status, and colouring one would
   invent a viewer that does not exist. */
/* GHOST outline. An unfilled slot is a barely-there hairline hexagon, not a
   filled shape: at one viewer the band renders 42 of these, and anything
   with presence turns the lattice into a wall of placeholders that reads as
   "still loading" rather than "nobody here yet".

   The outline is the face's existing 3px padding - the same thickness every
   filled hex uses - so the ghosts sit on the same geometry rather than
   introducing a second border weight. Only the COLOUR changes.

   The centre is painted --bg rather than left transparent: the face itself
   is a filled hexagon, so a transparent centre would show the hairline
   colour across the whole shape and give a faint FILL instead of an
   outline. Matching the page background is what turns it into a hole. */
/* The face paints the ring band, the mask paints the centre - the same two
   layers a filled hex uses, so an empty slot gets a subtle white outline
   from exactly the geometry that carries status colour elsewhere. No second
   border mechanism, and no thickness of its own.

   This only became possible once the ring stopped being zero pixels wide
   and the src-less <img> stopped painting a broken-image frame over it;
   every earlier attempt at an outline here was fighting that artefact
   rather than the CSS. */
.mh-hex-cell.is-empty .mh-hex-face {
    background: color-mix(in srgb, var(--text) 17%, transparent);
}

.mh-hex-cell.is-empty .mh-hex-avatar-mask {
    background: color-mix(in srgb, var(--text) 4%, var(--bg));
}

.mh-hex-cell.is-empty::before {
    opacity: 0;
}

.mh-hex-flipper {
    position: absolute;
    inset: calc(var(--mh-hex-gap) / 2);
    transform-style: preserve-3d;
    transform: rotateX(0deg);
}

.mh-hex-flipper.is-flipping {
    animation-duration: var(--mh-hex-flip-dur);
    animation-timing-function: var(--ease-standard);
    animation-fill-mode: forwards;
}

/* The scale peak at the midpoint (owner's second seam mitigation, used
   alongside the rotateX axis choice above) briefly enlarges the whole card
   right when foreshortening is worst, to keep it overlapping its
   neighbours instead of visibly pulling away from them. */
@keyframes mh-hex-flip-next {
    0%   { transform: rotateX(0deg) scale(1); }
    45%  { transform: rotateX(90deg) scale(1.08); }
    100% { transform: rotateX(180deg) scale(1); }
}

@keyframes mh-hex-flip-prev {
    0%   { transform: rotateX(0deg) scale(1); }
    45%  { transform: rotateX(-90deg) scale(1.08); }
    100% { transform: rotateX(-180deg) scale(1); }
}

.mh-hex-face {
    position: absolute;
    inset: 0;
    box-sizing: border-box;
    /* The status ring: this background shows as an even band around the
       SCALED-DOWN avatar mask nested inside (see .mh-hex-avatar-mask for why
       scale and not inset). --mh-hex-ring-color is set per-face by JS from
       the viewer's watch status.

       It rendered at zero width for most of this feature's life: the mask is
       absolutely positioned, so its inset: 0 resolved against the padding box
       and the face's padding never moved it. Empty cells override this to
       match their mask, so a slot with no viewer has no ring to explain. */
    background: var(--mh-hex-ring-color, var(--border-strong));
    clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
    backface-visibility: hidden;
}

.mh-hex-face--back {
    transform: rotateX(180deg);
}

/* Friends read as distinct without a hover: a thicker ring (more padding
   showing the status-coloured background) plus a soft halo. drop-shadow,
   not box-shadow - box-shadow is a rectangle and would spill outside the
   hex clip; drop-shadow follows the clipped alpha shape. rgba(var(--fg-rgb))
   is the bare-triple token this codebase's theme flip actually carries. */
.mh-hex-face.is-friend {
    padding: 4.5px;
    filter: drop-shadow(0 0 4px rgba(var(--fg-rgb), 0.55));
}

/* The ring is made by SCALING this mask down, not by the face's padding.

   padding was the original mechanism and it never worked: this element is
   absolutely positioned, and inset: 0 resolves against the containing
   block's PADDING box, so the mask covered the face exactly and the ring was
   zero pixels wide. What showed instead was sub-pixel antialiasing bleed
   where two identical clip paths met - visible only along the vertical
   edges, which is why it read as scattered un-uniform ticks rather than as
   a border.

   Scale rather than a larger inset, because inset is not a parallel offset
   on a hexagon: shrinking the BOX by 3px moves the vertical edges in by 3px
   but barely moves the diagonals, and the gap collapses to nothing at the
   top and bottom points. A centred scale of a regular hexagon is similar to
   itself, so every edge moves in by the same perpendicular distance and the
   ring reads as one even width. */
.mh-hex-avatar-mask {
    position: absolute;
    inset: 0;
    overflow: hidden;
    transform: scale(0.9);
    clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
    background: var(--surface-2);
}


/* An <img> whose src attribute has been REMOVED still generates a box, and
   Chrome paints its empty-image frame. That frame is a RECTANGLE, clipped
   here by the hexagon - so what reaches the screen is the rectangle's left
   and right edges as bright vertical strokes, plus fragments near the top
   and bottom points. It is the artefact that survived three rounds of
   recolouring the hex, because it was never the hex: no background,
   border or ring value can affect a frame the browser draws for a
   src-less image.

   Keyed on :not([src]) rather than on .is-empty. The empty class is a
   proxy for "this slot has no viewer"; the actual precondition is "this
   image has no source", which is also true for a filled cell whose src is
   cleared mid-flip. Keying on the real condition covers both. */
.mh-hex-avatar:not([src]) {
    display: none;
}
.mh-hex-avatar {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: 1;
    transition: opacity 130ms var(--ease-standard);
}

/* Capability-fallback path only (see the JS header): a plain crossfade for
   browsers where CSS.supports() reports no 3D-transform/backface-visibility/
   clip-path support together. Never engaged alongside the flip - the two
   paths are mutually exclusive per page-turn. */
.mh-hex-avatar.is-fading {
    opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
    /* Belt-and-suspenders: picks-honeycomb.js already checks reduced motion
       before ever starting a flip or a crossfade and takes the instant-swap
       path instead, but a live OS toggle mid-transition is still covered
       here rather than left to race the JS check. */
    .mh-hex-flipper.is-flipping {
        animation: none !important;
        transform: none !important;
    }

    .mh-hex-avatar {
        transition: none !important;
    }
}

@media (max-width: 480px) {
    .mh-pick-viewers {
        --hex-w: clamp(30px, 10.5vw, 42px);
        gap: 4px;
    }
}

/* "Nobody yet" note, overlaid ON the lattice.

   The band deliberately does NOT collapse when a pick has no viewers
   (owner): collapsing traded an expand-shift for a collapse-shift instead
   of removing either. Holding the height keeps the layout still, and this
   note is what stops a full grid of empty hexes implying that people are
   there.

   Display is scoped to :not([hidden]) because JS toggles the hidden
   attribute. An unconditional display here would outrank the user agent's
   [hidden] rule and the note would never hide - with no error to say why. */
