  /* ============================================================
     PERSONAL ARTIFACTS -- new layer on top of the existing graph/panel
     app. Ported from the standalone personal-artifacts.html prototype
     (pixel-matched against Figma). Loaded after icons.css in
     GraphApp.tsx, same pattern as styles.css/icons.css.

     Read var(--n900)/--curve-xs/--shadow-badge/etc. throughout instead
     of hardcoded values, so it drops in after styles.css's own :root
     block with no token translation needed -- same tokens, confirmed
     identical between this prototype and styles.css.

     One integration adaptation from the prototype, flagged and applied
     below: the prototype wrapped the whole existing site in a new
     .stage/.canvas-stage pair to push it back as a unit. The real site
     already has #app with the position/height/overflow base it needs
     (styles.css), so instead of adding a wrapper div, the push-back
     toggle lives on <body class="pa-open">, and #app/#pa-layer/
     .pa-trigger are plain siblings (see the comment at the top of the
     rules below for the full reasoning).
     ============================================================ */

  /* ---------- PA-specific design tokens ----------
     BUG FIX: these four lived in the prototype's own :root block and were
     dropped during migration on the assumption that every token it used
     already existed in styles.css. Four did not, and an undefined custom
     property makes its whole declaration invalid at computed-value time --
     so `background:var(--stage-grey)` silently painted NOTHING (the grey
     backdrop behind the pushed-back page never appeared, which is what
     made the sheet fail to read as a bottom sheet pulling up over the
     page), the 30% ghost/dimmed/filtered-out card tiers never dimmed, the
     small cards' hover elevation never showed, and the focal card lost its
     16px corner.

     Deliberately declared HERE rather than appended to styles.css's own
     :root: these are values Figma defines only for this layer, so keeping
     them file-local means a future change to the site's global tokens can
     never silently restyle Personal Artifacts, and vice versa. Only the
     tokens actually consumed are declared -- the prototype also defined
     --outline-ghost/--opacity-dim-thumb/--shadow-focal, which nothing in
     the migrated CSS references any more. */
  :root{
    /* The real stage/canvas backdrop, confirmed against Figma as #cdcdcd.
       Same hex as --n400 but kept its own token on purpose: --n400 is used
       throughout as a BORDER color and this is a background fill, so
       collapsing them would make a future border tweak repaint the whole
       backdrop. */
    --stage-grey:#cdcdcd;
    /* Was opacity/s (10%) straight from Figma -- raised to 30% on request:
       at 10% the surrounding artifacts were so faint the clicked one had
       nothing to sit among. Ghost cards and filtered-out cards both use it. */
    --opacity-ghost:.3;
    /* Figma authored the focal card's drop shadow against its 320px
       clicked variant; this is that shadow scaled by the card-width ratio
       (138.58/320 = 0.4331) for the DEFAULT 138.58px card's hover state --
       2.377 -> 1.03, 6.848 -> 2.966, alpha unchanged. See .pa-card:hover. */
    --shadow-focal-sm:0px 1.03px 2.966px rgba(170,170,170,.15);
    /* Figma's focal/ghost card radius -- its own literal value, not a step
       on the site's existing curve scale. */
    --curve-l:16px;
  }

  /* ---------- integration adaptation (flagged before applying) ----------
     The prototype wrapped "the existing site" in a new .stage/.canvas-stage
     pair so it could push the whole thing back as one unit while the
     Personal Artifacts sheet rose in front of it. The real site's #app
     already carries the position:relative / height:100vh (100dvh on
     mobile) / overflow:hidden base styles.css needs for exactly this (see
     styles.css's own `.app{...}` + the 100dvh media-query override) -- so
     wrapping it in a new div here would duplicate that sizing and change
     the existing top-level DOM nesting for no reason (constraint: don't
     introduce a parallel architecture, don't touch existing layers'
     structure). Instead:
       - #app takes only the ADDITIONAL properties .canvas-stage needed on
         top of what it already has (transform-origin/transition/z-index).
       - The open/closed toggle lives on <body class="pa-open"> rather than
         a new .stage element, since #pa-layer/.pa-trigger are mounted as
         plain siblings of #app (confirmed: "sibling of #app, like
         #trail-layer") -- there's no wrapping element for a .stage class
         to go on to begin with.
       - #pa-layer's position:absolute (below) resolves against the
         viewport itself (the initial containing block), same as it did
         against .stage before, since nothing between <body> and #pa-layer
         sets position -- confirmed by reading layout.tsx/GraphApp.tsx.
     ---------- */
  /* Unlike #pa-layer/#app, `body` had no transition of its own, so its
     background snapped from --bg to --stage-grey instantly while
     everything else eased over .9s -- same curve/duration as #app's own
     transition above so the two read as one movement. Transitioning on
     the base `body` selector (not just `.pa-open`) so the reverse (close)
     eases too. */
  body{transition:background .9s cubic-bezier(.4,0,.2,1);}
  body.pa-open{background:var(--stage-grey);}
  #app{
    transform-origin:50% 0%;
    /* .9s / same curve as .pa-layer above, so the canvas retreating and
       the sheet resolving into place are one movement rather than two
       overlapping ones at different speeds. */
    transition:transform .9s cubic-bezier(.4,0,.2,1), border-radius .9s cubic-bezier(.4,0,.2,1), box-shadow .9s cubic-bezier(.4,0,.2,1), opacity .9s cubic-bezier(.4,0,.2,1);
    z-index:1;
  }
  /* ---------- stacked-modal / modal-on-modal ----------
     Closed: the canvas is a plain, flush, edge-to-edge site -- no card
     chrome at all, so normal browsing looks completely ordinary.
     Open: the canvas is pushed BACKWARD -- scaled down slightly and
     shifted DOWN, its corners rounding off into a card, dimmed and
     inert -- while the Personal Artifacts sheet rises in from the
     BOTTOM to become the large foreground layer, covering nearly the
     whole stage. transform-origin sits at the TOP so the scale-down
     alone would retreat the canvas's bottom edge upward; shifting it
     back down by a smaller amount nets a shallow gap on BOTH edges
     (blank stage-grey peeking above the card, and beneath it toward
     the sheet's leading edge) rather than canceling out to zero at
     either one, so the retreat still reads as motion instead of a
     flat resize.
     Scale/translate re-measured directly against Figma's own numbers
     (node 292:2916): the "Home" frame sits at x=66,y=38,w=1467.06 inside
     a 1600-wide/1151-tall canvas, i.e. a ~4.1% gap on each side and a
     ~3.3% peek at the top -- was an unmeasured .94/3% guess, now tuned
     to match that ratio (scale .918 leaves ~4.1% side-gap; translateY
     3.4% leaves a ~3.3-3.5% top peek once the scale-down's own upward
     shift is netted out). */
  /* opacity:.6 replaces a dark rgba(20,20,18,.18) tint that used to fade
     in via a ::after overlay -- re-checked against Figma's own canvas
     sections, which don't tint the pushed-back Home frame darker at all;
     they just render it at flat 60% layer-opacity over the page's own
     grey backdrop, so this is now literal opacity on the element itself
     instead of a simulated darkening overlay. */
  body.pa-open #app{
    transform:scale(.918) translateY(3.4%);
    /* curve/m (10px), the same outer-frame token the Personal Artifacts
       sheet itself uses -- these two are peer frames on the Figma
       canvas, so they share a corner. Was an unmeasured 28px. */
    border-radius:var(--curve-m);
    /* The pushed-back page carries no shadow either, for the same reason
       the sheet above it doesn't -- see body.pa-open .pa-layer. */
    box-shadow:none;
    opacity:.6;
    pointer-events:none;
  }
  /* Safari's cube-roll stand-in (.cube-overlay, styles.css) is a body-level
     sibling of #app at z-index:500, so on its own it floats over this
     sheet (z-index:20) while the graph it belongs to retreats behind it.
     Same fix as the widget-open rule next to .cube-overlay in styles.css:
     drop it to 1 (above #app, below the sheet) and match #app's own .6
     push-back opacity on the same .9s curve. position already follows #app's
     scale, since positionCubeOverlay() reads the graph's live rect each
     frame. .pa-closing (set by closeLayer() for the .9s the sheet takes to
     leave) holds z-index:1 through the close, so the cube doesn't jump back
     to 500 over a sheet that's still sliding away. */
  body.pa-open .cube-overlay,
  body.pa-closing .cube-overlay{z-index:1;transition:filter .3s ease, opacity .9s cubic-bezier(.4,0,.2,1);}
  body.pa-open .cube-overlay{opacity:.6;}

  /* top/left/right/radius re-measured directly against Figma (node
     257:3227/289:2263, the "zoomed in" reference frame -- same relative
     geometry as 292:2916/292:2970's "zoomed out" one): the "Personal
     Artifacts Layer" frame sits at x=27,y=78,w=1538,h=1073 inside the
     1600x1151 canvas -- a 1.7% gap on the left, 2.2% on the right, 6.8%
     from the top, and flush (0%) at the bottom. Was full-bleed
     (left:0;right:0) with an unmeasured 28px radius; now carries the
     same small side-inset Figma shows (so the canvas-stage's edge is
     still visible as a thin sliver down each side, not just at the top).
     Background/padding are new: get_design_context on 289:2263 shows
     this isn't a flush white sheet with a header docked flat against its
     own edge -- the root is a n300 (#f2f2f2) frame with a uniform 4px
     padding, and the header/body sit INSET inside that padding as their
     own smaller white/n200 card with an 8px (curve/s) radius, not the
     outer 10px (curve/m) -- see .pa-header/.pa-body below for the other
     half of this. */
  .pa-layer{
    position:absolute;top:6.8%;bottom:0;left:1.7%;right:2.2%;
    background:var(--n300);
    padding:var(--pad-v-xxs, 4px) var(--pad-h-xxs, 4px);
    border-radius:var(--curve-m) var(--curve-m) 0 0;
    box-shadow:none;
    /* Open/close motion follows the live site's own vocabulary rather
       than a bespoke slide: styles.css opens its panels with
       `panel-appear .9s cubic-bezier(.4,0,.2,1)` -- opacity 0 to 1,
       blur(10px) to 0, and a short travel toward the resting position --
       and every staggered row on the site uses the same blur-plus-fade
       recipe (`appear-up`, 1.05s, same curve). So this sheet now rises
       with the same three properties on the same curve at .9s, instead
       of a pure .55s translate. The blur is what makes it read as the
       same site: things there don't slide in, they resolve into place. */
    opacity:0;
    filter:blur(10px);
    transform:translateY(12%);
    /* Load-bearing now that the closed sheet only travels 12% instead of
       a full 100% off-screen: at opacity 0 it still occupies the lower
       part of the stage and would silently swallow clicks meant for the
       canvas underneath. Re-enabled in the .pa-open rule below. */
    pointer-events:none;
    transition:transform .9s cubic-bezier(.4,0,.2,1),
               opacity .9s cubic-bezier(.4,0,.2,1),
               filter .9s cubic-bezier(.4,0,.2,1),
               box-shadow .9s cubic-bezier(.4,0,.2,1);
    display:flex;flex-direction:column;
    overflow:hidden;
    z-index:20;
  }
  /* box-shadow was previously unconditional on .pa-layer -- box-shadow moves
     with the element under `transform`, but at translateY(100%) (fully
     off-screen below) the shadow's own -20px upward offset still landed
     right at the stage's bottom edge, so a shadow was visible there even
     while the layer was closed. Gating it to the open state only
     (matching .canvas-stage's own box-shadow:none -> real-shadow pattern
     above) means it's only ever painted once the sheet is actually on
     screen. */
  /* No shadow cast onto the page behind it: Figma stacks the Personal
     Artifacts Layer over Home as two flat frames, with the layer beneath
     simply dimmed to 60%. The old -20px/50px shadow was invented and read
     as a heavy dark band along the sheet's top edge. */
  body.pa-open .pa-layer{transform:translateY(0);opacity:1;filter:blur(0);pointer-events:auto;box-shadow:none;}
  /* pointer-events:none on .pa-layer alone is NOT enough to make the
     closed sheet inert: pointer-events re-enables on any descendant that
     sets it back to auto, and .pa-view.active does exactly that. With the
     sheet now resting at translateY(12%) rather than fully off-screen,
     that left an invisible .pa-view covering the lower stage and eating
     every click meant for the canvas -- including the button that opens
     the sheet in the first place. This kills hit-testing on the whole
     subtree while closed; the .pa-open rule above restores it. */
  body:not(.pa-open) .pa-layer, body:not(.pa-open) .pa-layer *{pointer-events:none;}

  /* This is now exactly .badge-icon's own recipe (same padding formula,
     n100 fill, curve-xs radius, shadow-badge+hair-s, hover/active shadow
     states) instead of a bespoke fixed 44x44 box -- was its own special
     button style with a hand-built two-<i> icon that had no
     justify-content on its column flex, which is what left the icon
     riding high instead of centered. No PA-specific button styling left
     here at all; the icon is a plain 18px SVG glyph like every other
     badge-icon button already in this file (.pa-close, .pa-overlay-close). */
  /* Figma's Toggle (290:2464), which is the same nested chrome the site's
     own layout switcher uses: an n300 tray with a 4px frame and a curve/m
     corner, holding a white curve/s box, holding the option itself. Top
     right of the canvas, at the same 14px the layout switcher sits at so
     the two align across the top edge. */
  .pa-trigger{
    position:fixed;top:14px;right:14px;
    z-index:15;
    display:flex;align-items:center;justify-content:center;
    border:none;cursor:pointer;line-height:0;
    background:var(--n300);
    border-radius:var(--curve-m);
    padding:var(--pad-v-xxs, 4px);
    transition:opacity .3s ease, transform .3s ease, filter .3s ease;
  }
  .pa-trigger-inner{
    display:flex;align-items:center;justify-content:center;
    background:var(--n100);box-shadow:var(--hair-s);
    border-radius:var(--curve-s);
    padding:var(--pad-v-xxs, 4px);
  }
  /* The ToggleOption. Its hover is the switcher's own: the n200 fill
     arrives underneath the icon rather than the button changing shape,
     on .ls-hover's easing (opacity .18s ease), and the icon darkens to
     n600 on press exactly like .ls-btn:active. */
  .pa-trigger-opt{
    position:relative;display:flex;align-items:center;justify-content:center;
    width:31.2px;height:31.2px;border-radius:4.8px;
    color:var(--n500);transition:color .18s ease;
  }
  .pa-trigger-opt::before{
    content:'';position:absolute;inset:0;border-radius:inherit;
    background:var(--n200);opacity:0;transition:opacity .18s ease;
  }
  .pa-trigger:hover .pa-trigger-opt::before{opacity:1;}
  /* Same tooltip chip the layout switcher uses (.ls-btn::after): n200 fill,
     mono uppercase at font-size/xs, curve/xs corners, shadow-tag + hairline,
     rising 4px into place as it fades. Sits below the button, matching the
     switcher, since both live along the top edge.

     Anchored to the OUTER .pa-trigger tray, not the inner .pa-trigger-opt
     icon box -- the tray (the grey chrome) is the button as the user
     reads it, the same way .ls-track's grey chrome is the toggle's real
     edge, not each individual .ls-btn icon inside it. The gap and
     right-alignment are both measured from that outer edge, matching
     .ls-track/.contact-btn exactly. (A prior version anchored to
     .pa-trigger-opt instead, reasoning that the outer tray's own padding
     made the gap read as too far from the ICON -- reverted: the icon
     was the wrong reference point, not the gap value.) */
  /* Anchored to the button's RIGHT edge, not centred on it. The switcher
     this chip is borrowed from sits mid-screen, where centring is right;
     this button sits in the corner, where a centred chip runs off the
     side of the viewport. */
  .pa-trigger::after{content:attr(data-tip);position:absolute;top:calc(100% + 8px);right:0;
    transform:translateY(-4px);
    background:var(--n200);color:var(--n600);border-radius:var(--curve-xs);
    box-shadow:var(--shadow-tag), var(--hair-s);
    font-family:var(--font-mono);font-weight:400;font-size:var(--fs-xs);letter-spacing:0;text-transform:uppercase;
    /* Load-bearing: .pa-trigger sets line-height:0 (it holds an inline SVG
       and that kills the descender gap), and the tooltip inherits it --
       which collapsed the chip to a sliver with the text spilling out of
       it. Figma's Hover Tip (66:3881) is leading-[normal]. */
    line-height:normal;
    padding:var(--pad-v-xs) var(--pad-h-xs);white-space:nowrap;opacity:0;pointer-events:none;
    transition:opacity .18s ease, transform .22s cubic-bezier(.4,0,.2,1);z-index:40;}
  .pa-trigger:hover::after{opacity:1;transform:translateY(0);}
  .pa-trigger:active .pa-trigger-opt{color:var(--n600);}
  .pa-trigger .icon{position:relative;width:18px;height:18px;}
  /* the sheet itself carries the close button in its header once open, so
     the floating trigger just recedes with the canvas behind it */
  body.pa-open .pa-trigger{opacity:0;pointer-events:none;transform:scale(.7);}
  /* The trigger is a sibling of #app, not inside it, and #app is its own
     stacking context (z-index:1, above) -- so the "above everything"
     z-index:100 an expanded project widget or (on mobile) the expanded
     catalogue widget gets only counts inside #app, and this z-index:15
     button painted on top of them. While either is up, the trigger
     belongs to the home layer underneath: same blur/dim/inert treatment
     as .graph-wrap, and dropped below #app, exactly like the
     body:has(.app.case-open) .cube-overlay rule in styles.css. */
  body:not(.pa-open):has(.app.case-open) .pa-trigger,
  body:not(.pa-open):has(.app.panel-expanded) .pa-trigger{filter:blur(7px);opacity:.45;pointer-events:none;z-index:0;}
  /* 767px, not a one-off 600px -- matches the real site's single mobile
     breakpoint (see styles.css's own @media (max-width:767px), used for
     the switcher position and the catalogue panel's mobile layout) so PA
     switches to its mobile treatment at the same width everything else
     does, instead of staying in its desktop layout through part of the
     range the rest of the site already treats as mobile. */
  @media (max-width:767px){
    /* Side-inset drops to a hairline on mobile (no room to spare at
       narrow widths) but the corner radius now stays the same
       curve/m token as desktop instead of a separate invented 22px --
       Figma doesn't define a distinct mobile PA Layer radius, so the
       one confirmed value applies at both sizes rather than guessing
       a second one. */
    /* 5%, not 14%: on a phone the canvas inside this sheet is the whole
       point, and a 14% inset spent a seventh of an already-short screen
       on a sliver of the page behind it. The peek is now just enough to
       read as a layer over something. */
    .pa-layer{top:5%;left:0;right:0;border-radius:var(--curve-m) var(--curve-m) 0 0;}
  }

  /* ============================================================
     PA LAYER CHROME -- header, body
     Was a slim vertical rail down the sheet's left edge (close/title
     stacked top-to-bottom beside the body) -- an invented layout with
     no real equivalent. .pa-header below is now a literal copy of the
     real widget-header row (.panel-handle/.case-header: same padding
     formula, n200 fill, hairline shadow, mono/fs-xs/n900 label that
     takes the remaining width, badge-icon actions on the right)
     instead of a bespoke rail. One deliberate, reasoned departure from
     a literal copy: no leading drag-icon (PA doesn't drag, so a grab
     cursor/handle would be dishonest).
     Radius is curve/s (8px), NOT .pa-layer's own curve/m (10px) -- get_
     design_context on 289:2263 shows the header/body sit inset inside
     the sheet's 4px padding as their own smaller white/n200 card, one
     radius step down from the outer frame, not flush against the
     sheet's own edge at the sheet's own radius (see .pa-layer's comment
     above and .pa-body below for the other half of this nesting). */
  .pa-header{
    cursor:default;
    display:flex;align-items:center;gap:var(--gap-xs);
    padding:calc(var(--pad-v-s) + var(--outline-s)) calc(var(--pad-h-s) + var(--outline-s));
    font-family:var(--font-mono);font-weight:400;font-size:var(--fs-xs);letter-spacing:0;color:var(--n900);
    background:var(--n200);box-shadow:var(--hair-s);border-radius:var(--curve-s) var(--curve-s) 0 0;
    user-select:none;flex-shrink:0;white-space:nowrap;
  }
  .pa-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;text-transform:uppercase;}
  .pa-header-actions{display:flex;gap:var(--gap-xs);flex-shrink:0;}

  /* Inconsistency flagged and resolved: the prototype had its own .pa-close
     rule here, byte-for-byte identical to the real site's .badge-icon
     (styles.css) -- same padding formula, fill, radius, shadow, and
     hover/active states. Reused .badge-icon directly instead (markup:
     class="badge-icon pa-close", the .pa-close class kept only as an
     inert hook in case something needs to target it later; JS selects it
     by #pa-close id, not this class). No PA-specific button styling left
     here at all. */

  /* Same curve/s (8px) step-down as the header, bottom corners only, plus
     the literal b/l/r-only 0.4px n400 border get_design_context shows on
     289:2265 "Frame" (no top border -- seamless against the header
     above). Background is n100 (white), matching that same Figma node;
     it's mostly invisible in practice since #pa-group's own n300 fill
     covers the full body area, but it's the correct base color for
     anywhere that grey canvas doesn't reach (e.g. a view with no group
     surface at all). */
  .pa-body{position:relative;flex:1;min-height:0;width:100%;overflow:hidden;
    background:var(--n100);border:var(--outline-s) solid var(--n400);border-top:none;
    border-radius:0 0 var(--curve-s) var(--curve-s);}
  /* opacity/filter, not display:none -- so swapping views (Cluster Canvas
     <-> Connected) can crossfade instead of hard-cutting; see the same
     recipe repeated below with the reasoning (.app.case-open .panel's
     blur+opacity, reused for a view swap instead of a panel underneath
     one). */
  .pa-view{position:absolute;inset:0;opacity:0;filter:blur(7px);pointer-events:none;transition:opacity .3s ease, filter .3s ease;}
  .pa-view.active{opacity:1;filter:blur(0);pointer-events:auto;}

  /* ---------- shared artifact card -- Figma "Artifacts"/"Photo" component ----------
     Redesigned from the real .media-ph card (title+meta+likes chrome) to
     match the Figma redesign's polaroid-style card exactly: a plain white
     (n100) frame, hairline border, image only -- no title, no category
     label, no like count/heart anywhere on the card face, in ANY state
     (every Figma reference -- zoomed-out canvas, zoomed-in composition,
     bottom strip -- shows image-only cards). The only text Figma ever
     shows on a card is the focal card's own one-line caption below the
     image (.pa-card-caption below), revealed only on .pa-focal-highlight.
     These are the "Default" variant of Figma's Artifacts component set
     (293:3107 -> 293:3106): 8px (curve/s) radius, a 4px frame, 6px
     (gap/xs) between photo and caption, a 0.4px (outline/s) n400 border.
     The clicked and ghost states are a genuinely DIFFERENT variant of the
     same component, not this one magnified -- see the .pa-group.pa-focused
     rules further down, which swap in the 400px-wide values. */
  .pa-card{
    background:var(--n100);
    border:var(--outline-s) solid var(--n400);
    border-radius:var(--curve-s);
    overflow:hidden;cursor:pointer;user-select:none;
    padding:var(--pad-h-xxs, 4px);
    display:flex;flex-direction:column;gap:var(--gap-xs);
    /* resting elevation: the site's image/video card shadow (styles.css) */
    box-shadow:var(--shadow-card);
    transition:box-shadow .15s ease;
  }
  /* Hover feedback is a SHADOW, not a ring -- per explicit request, the
     0.8px n500 outline that used to thicken the card's border here is
     gone from the layer entirely, replaced everywhere by the same
     elevation the clicked-on card carries at rest (--shadow-focal),
     scaled down for this card's size. box-shadow rather than
     filter:drop-shadow deliberately: .pa-card is an opaque rounded rect
     with overflow:hidden, so the two render identically on this
     silhouette, and box-shadow is already the transitioned property
     while `filter` is spoken for by the dimmed tier's blur. */
  .pa-card:hover{box-shadow:var(--shadow-card), var(--shadow-focal-sm);}
  /* A filtered-out card can no longer be hovered at all (pointer-events:
     none, see .pa-group .pa-card.pa-filtered-out), so the explicit
     :hover carve-outs that used to sit here are gone as unreachable.
     Note .pa-dimmed is a DIFFERENT tier that looks similar and must not
     be confused with it: that's the ghost treatment around a focused
     artifact, and those cards stay interactive and do take the hover
     shadow. */
  /* The image itself -- Figma's own "Photo" leaf: aspect ratio taken
     directly from the Figma frame (449.586:473.328), curve-xs radius,
     object-cover. No real per-artifact photography exists in this
     prototype, so each card gets a distinct, deterministic placeholder
     gradient (computed from its id in JS -- see cardMediaStyle) instead
     of Figma's one repeated stock photo, purely so cards stay visually
     distinguishable from one another; this is a placeholder-content
     substitution, not a design change -- position/radius/aspect/fit all
     match Figma exactly. */
  /* Fill width, hug height: the width comes from the card, the height
     from the artifact's own ratio via --pa-aspect (set by
     cardMediaStyle). The Figma frame's ratio remains the fallback for
     any artifact with no aspect of its own. */
  .pa-card-media{aspect-ratio:var(--pa-aspect, 449.586/473.328);border-radius:var(--curve-xs);
    position:relative;overflow:hidden;flex-shrink:0;
    /* The artifact's own photograph, set as a background-image by
       cardMediaStyle alongside --pa-aspect. cover+center rather than
       contain, so a half-pixel difference between the stored ratio and
       the laid-out box crops imperceptibly instead of letterboxing. */
    background-color:var(--n300);background-size:cover;background-position:center;background-repeat:no-repeat;}
  /* One crop everywhere. The clicked-in card briefly took each artifact's
     own aspect ratio; the Figma component set (293:3107) settles it the
     other way -- all three variants declare the same 449.586/473.328
     photo, so a card never changes shape, it only changes size. */
  /* The polaroid "peeled corner" fold that used to sit on every overview
     card's photo is removed, on request -- photos render clean in every
     state now. */
  /* Focal-only one-line caption -- replaces the old title/meta/like block
     entirely (dropped per spec: "image + caption only, no title/category/
     likes/heart anywhere"). Hidden on every other tier; revealed just
     below the image, matching Figma's "A film still from **X** by **Y**."
     treatment (plain n600 prose with the caption's own bolded phrases in
     n900 -- see the <b> tags makeCard writes into .pa-card-caption). */
  /* 14px (font-size/xs) with an 18px (padding/vertical/s) tail, straight
     off the Biography block inside the clicked variant (293:3113). Only
     the clicked card ever shows it, and that card is now drawn at its own
     real size, so these are plain literal values -- nothing to counteract. */
  /* Read off Figma's clicked variant (293:3108) and the Biography block
     inside it (293:3113), value for value: Geist Regular at font-size/xs
     (14px), neutral/600, leading-[normal] -- NOT a 1.5 ratio, which is
     what it had and which set the two lines further apart than Figma's.
     The emphasised phrases are plain black (neutral/900) at the same
     weight, not bold. The 18px tail is padding/vertical/s on the block. */
  .pa-card-caption{display:none;font-family:var(--font-body);font-weight:400;
    font-size:var(--fs-s);
    line-height:normal;letter-spacing:0;color:var(--n600);padding:0 0 var(--pad-h-s);}
  .pa-card-caption b{color:var(--n900);font-weight:400;}
  .pa-card.pa-focal-highlight .pa-card-caption{display:block;}

  /* ============================================================
     MODE 1 -- OPEN CANVAS
     A big pannable board (apossible.com-style: drag empty space to look
     around, a dotted "corkboard" ground) holding every artifact
     individually scattered and tilted across ONE continuous space --
     never gridded, and never visually grouped by category (category is
     metadata/filter data only; see buildGroupSurface/scatterAssignment
     for how positions are assigned). The PA layer's own header/toggle/
     close stay outside this element entirely, so they never move when
     the board is panned.
     ============================================================ */
  /* The dotted "corkboard" ground lives here, on the fixed viewport,
     rather than on #pa-group-surface -- the surface is only ever exactly
     BOARD_W x BOARD_H (see .pa-group-surface below), so painting the
     dots on IT left plain flat --n300 showing everywhere the viewport is
     bigger than that board (any wide window) or once panned past its
     edge, instead of the ground reading as one continuous canvas. The
     tile still needs to track the pan/camera, or the dots would sit
     still while every card slides across them -- updateGroundPosition()
     (called from every spot that sets the surface's own transform) keeps
     this background-position in lockstep, mod 24px so a plain shift by
     a whole tile is invisible. */
  .pa-group{position:absolute;inset:0;overflow:hidden;cursor:grab;
    background-color:var(--n300);
    background-image:radial-gradient(var(--n400) 1px, transparent 1px);
    background-size:24px 24px;}
  .pa-group.panning{cursor:grabbing;}
  /* Only while the camera is gliding (focus/defocus) -- a live drag pan
     must track the pointer with zero lag (see applyGroupPan), so this
     transition stays off the base rule and is toggled on with the same
     .pa-cam-anim class/duration/easing #pa-group-surface's own transform
     uses, so the dots and the cards glide together. */
  .pa-group.pa-cam-anim{transition:background-position .6s cubic-bezier(.4,0,.2,1);}
  /* ---------- the spread factor ----------
     Clicking an artifact does NOT magnify the board. Figma's component
     set defines the clicked and ghost cards as their own 400px-wide
     variant with hand-picked values (padding 4->10, radius 8->16, border
     0.4->1, photo radius 4->8) -- not as a scaled-up copy of the 138.58px
     one. So the cards are re-drawn at their real size, which is why they
     come out sharp; magnifying a 138px card to 400px is what made the
     previous version look soft.
     Growing the cards alone would make them collide, though, since the
     gaps between them don't grow on their own. --pa-spread is that
     missing half: every card's position is multiplied by it, by the same
     factor the cards themselves grow, so the arrangement is preserved
     exactly and the result reads as a zoom into the same layout -- while
     every card is still drawn natively at its own size. Registered via
     @property so it interpolates as a number and the cards glide apart
     instead of jumping. */
  @property --pa-spread{ syntax:'<number>'; inherits:true; initial-value:1; }
  .pa-group-surface{
    --pa-spread:1;
    position:absolute;top:0;left:0;
    /* Explicit, not the 0/0-looking-but-actually-50%/50% CSS default --
       every camera calculation (computeCameraFor/applyGroupPan) treats a
       card's own style.left/top as ITS position after translate+scale,
       which is only true when scale happens around the surface's own
       top-left corner. Left at the default center origin, introducing
       scale() (for the focus camera zoom) would scale everything around
       the board's MIDDLE instead, throwing every card's screen position
       off by a scale-dependent amount -- harmless back when this
       transform was translate-only (panning), but load-bearing now. */
    transform-origin:0 0;
    width:calc(var(--pa-board-w, 1100) * 1px * var(--pa-spread, 1));
    height:calc(var(--pa-board-h, 900) * 1px * var(--pa-spread, 1));
    /* The dotted ground itself now lives on .pa-group (see its own
       comment) so it covers the whole viewport, not just this board-sized
       rect -- this element stays transparent and only positions cards. */
    will-change:transform;
  }
  /* Overview-only hover preview (see showHoverConnections/hideHoverConnections):
     a dedicated svg layer, faded as one unit via a class toggle rather
     than added/removed from the DOM each time, so the fade transition
     actually has something to animate. z-index above the plain cards --
     otherwise a line between two nearby artifacts would run mostly UNDER
     their own opaque bodies. Same stroke/dash language as the focused
     state's own persistent connection lines (.pa-focus-edge: n900
     dots) so a relationship reads the same way whether you're hovering in
     the overview or looking at it focused. */
  /* z-index 1, below the cards (z-index 2): a connecting line passes
     BEHIND the artifacts it joins rather than being drawn across their
     faces. */
  .pa-hover-lines{position:absolute;top:0;left:0;z-index:1;overflow:visible;pointer-events:none;opacity:0;transition:opacity .2s ease;}
  .pa-hover-lines.visible{opacity:1;}
  /* Now a dot-line.js <g> of dots (marching lives in that file). Same
     ink as the focused view's .pa-focus-edge, at a lighter .4 opacity. */
  .pa-hover-line{color:var(--n900);opacity:.4;}
  /* 138.58px is Figma's own Default-variant width (293:3106), used
     verbatim rather than derived.
     Position: --pa-x/--pa-y hold the card's CENTRE in unscaled board
     units, and the left/top calc multiplies them by --pa-spread. The
     `translate:-50% -50%` is what makes those coordinates a centre
     rather than a corner, so a card grows symmetrically about its own
     point instead of drifting down-right as it widens. `translate` is
     its own property here, not folded into `transform`, precisely so it
     can coexist with the scatter rotation below. */
  .pa-group .pa-card{position:absolute;touch-action:none;z-index:2;
    /* --pa-scale: per-artifact width multiplier (data `scale`, set by
       makeCard) for the very wide text strips. The clicked-in rule below
       multiplies its own width by the same factor. */
    width:calc(138.58px * var(--pa-scale, 1));
    left:calc(var(--pa-x, 0) * 1px * var(--pa-spread, 1));
    top:calc(var(--pa-y, 0) * 1px * var(--pa-spread, 1));
    translate:-50% -50%;
    /* Same idle drift the clicked-on view has (see @keyframes
       pa-card-drift below), at the canvas's smaller amplitude. */
    --pa-drift-amp:.7;
    animation:pa-card-drift 5s ease-in-out infinite;
    animation-delay:var(--pa-drift-delay, 0s);
    /* The card's scatter rotation lives in --pa-rot (a custom property set
       once at creation -- see buildGroupSurface) rather than a plain inline
       style.transform, specifically so the focal tier's own transform:none
       override below can still win when it applies: an inline
       style.transform would out-specificity ANY class rule on the same
       property regardless of how many classes that rule has, and the
       rotation would keep rendering on the focal card even though the
       focal-highlight rule explicitly overrides transform to cancel it. */
    transform:rotate(var(--pa-rot, 0deg));
    transition:box-shadow .15s ease, opacity .5s ease, filter .5s ease, transform .3s ease,
               width .6s cubic-bezier(.4,0,.2,1), padding .6s cubic-bezier(.4,0,.2,1),
               gap .6s cubic-bezier(.4,0,.2,1), border-radius .6s cubic-bezier(.4,0,.2,1),
               border-width .6s cubic-bezier(.4,0,.2,1);}
  /* Same swap for the canvas-scoped rule, which is what actually wins on
     the board -- this is the one the ghost cards around a focal artifact
     hit while the clicked-in view is open. */
  .pa-group .pa-card:hover{box-shadow:var(--shadow-card), var(--shadow-focal-sm);}


  /* ---------- the clicked-in state: Figma's other two variants ----------
     While an artifact is open, EVERY card on the board switches to the
     400px-wide variant -- the clicked one (293:3108) and the ghosts
     behind it (293:3116) share the same frame, and differ only in
     opacity and the caption. Figma's component set has no fourth
     "connected but not clicked" variant, so connected neighbours get the
     ghost treatment like everything else; the bottom strip is where
     connections stay legible.
     --pa-focus-w is set in JS from the frame's own width so a 400px card
     can shrink to fit a phone; --pa-spread is derived from whatever it
     ends up being, which keeps the arrangement consistent at any size. */
  /* Very slight idle drift, apossible.com-style -- every card (focal
     included) breathes a couple px around its own real position rather
     than sitting dead still, while never actually leaving that spot
     (the keyframes always return to the same -50%/-50% centering
     translate they rest at, each loop). Animates the `translate`
     individual property, not `transform` -- .pa-group .pa-card's own
     translate:-50% -50% centering AND its transform:rotate(var(--pa-rot))
     both need to survive this untouched (translate/rotate/scale combine
     with transform rather than replace it), including on the focal card,
     whose .pa-focal-highlight rule deliberately sets transform:none to
     cancel rotation -- translate is a separate property, so that cancel
     is unaffected. --pa-drift-delay (buildGroupSurface) is a negative,
     per-card offset into this same loop, so cards never visibly drift in
     unison. */
  /* Amplitude is --pa-drift-amp times these base offsets (7px / 4.5px):
     1 in the clicked-on view (.pa-focused), 0.7 on the full canvas, where
     cards are smaller and there are many more of them. One keyframe set
     for both, rather than a second copy per view. */
  @keyframes pa-card-drift{
    0%,100%{translate:-50% -50%;}
    25%{translate:calc(-50% + 7px * var(--pa-drift-amp, 1)) calc(-50% - 4.5px * var(--pa-drift-amp, 1));}
    50%{translate:calc(-50% - 4.5px * var(--pa-drift-amp, 1)) calc(-50% + 7px * var(--pa-drift-amp, 1));}
    75%{translate:calc(-50% + 4.5px * var(--pa-drift-amp, 1)) calc(-50% + 4.5px * var(--pa-drift-amp, 1));}
  }
  .pa-group.pa-focused .pa-card{
    /* × --pa-scale, same as the overview: positions spread by exactly
       focus-w/138.58, so a scaled strip keeps its overview proportions. */
    width:calc(var(--pa-focus-w, 320px) * var(--pa-scale, 1));
    border-width:var(--outline-m);
    border-radius:var(--curve-l);
    /* Literal 8px, on request -- was 10px (Figma's own padding/
       horizontal/xs token for this clicked variant), deliberately not
       var(--pad-h-xs), which is a different 4px token in this file's
       own scale despite the matching name. */
    padding:8px;
    gap:var(--gap-m);
    opacity:var(--opacity-ghost);
    --pa-drift-amp:1;
  }
  /* A card holds still while it's under the pointer, in both views, so
     it isn't a moving target to read or click. */
  .pa-group .pa-card:hover{animation-play-state:paused;}
  @media (prefers-reduced-motion: reduce){
    .pa-group .pa-card{animation:none;}
  }
  .pa-group.pa-focused .pa-card .pa-card-media{border-radius:var(--curve-s);}
  /* The clicked card itself: same frame as the ghosts, but opaque, with
     the drop shadow and the caption Figma gives only this variant. */
  .pa-group.pa-focused .pa-card.pa-focal-highlight{
    /* The clicked artifact holds perfectly still; only the ghosts and the
       cards around it drift (see @keyframes pa-card-drift). */
    animation:none;
    opacity:1;
    /* A scaled strip, when it's the one clicked, still fits a phone. */
    width:min(calc(var(--pa-focus-w, 320px) * var(--pa-scale, 1)), var(--pa-focus-max, 100vw));
    filter:drop-shadow(0 2.377px 6.848px rgba(170,170,170,.15));
  }
  /* Cards outside the active filter (see renderFilterPills/applyFilter) --
     corrected from an opacity:0/pointer-events:none "hide it entirely"
     treatment (there was no Figma evidence for that -- it was an
     assumption) to what filtering actually does: everything stays laid
     out exactly where it was, just faded to the same ghost opacity the
     focus-dimmed tier already uses (--opacity-ghost, 10%) -- still a
     real, clickable card, same reasoning as .pa-dimmed below. Opacity
     only, no blur -- on request, and consistent with .pa-dimmed's own
     "sharp (not blurred)" treatment right below. */
  /* Filtered-out artifacts are fully inert, per request: not clickable,
     no hover lines, no hover shadow, no pointer cursor. pointer-events:
     none is the single lever that does all four -- the card stops
     receiving any pointer event, so :hover never matches and the
     delegated pointerover that drives the hover-connection preview never
     fires for it either. The only route to another category is to change
     the filter. */
  .pa-group .pa-card.pa-filtered-out{opacity:var(--opacity-ghost);
    pointer-events:none;cursor:default;}

  /* ============================================================
     FOCUSED ARTIFACT (State B) -- NOT a separate view any more. Per the
     "one physical canvas, camera movement" redesign, clicking an artifact
     never swaps to a different composition or a different set of DOM
     cards: the SAME #pa-group-surface, with every artifact still sitting
     at its own real scattered position, just gets a camera move (a CSS
     transform scale+translate on the surface itself, see applyCamera/
     focusArtifact in the JS) that zooms in centered on the clicked
     artifact -- like a map zoom, not a scene change. Every other artifact
     keeps its real relative position; nothing is rearranged, re-centered,
     hidden, or resized independently of that one shared transform.
     Three visual tiers, applied as classes on the real persistent cards
     (see .pa-focal-highlight/.pa-conn-highlight/.pa-dimmed below): the
     clicked artifact itself (focal), the artifacts it's explicitly
     connected to (connMembers -- still full, real cards, never a tiny dot
     or a compact "satellite" stand-in, per spec section 1/5), and every
     other artifact on the canvas, which dims/blurs instead of
     disappearing (spec section 2: "artifacts belonging to other groups
     should remain visible... but blurred/de-emphasized"). A persistent
     SVG layer (#pa-focus-edges, a sibling of the hover-preview svg,
     living inside the same transformed surface so its lines scale/pan
     exactly in step with the cards they connect) draws a line from the
     focal artifact to each connected one; clicking a line/dot -- or any
     other card -- re-focuses there with the same camera move, per spec
     section 4's "Smart Animate-style" travel across the same canvas. */
  /* Overlay chrome (category pills above, same-category bottom navigator
     below) floats OVER the persistent canvas while focused, rather than
     living inside a separate swapped-in view -- the canvas underneath is
     exactly the same element the plain overview uses. The empty space
     between the two strips deliberately has no covering element, so
     clicks there still reach the canvas/cards beneath (see
     pointer-events:none on the wrapper, auto on the two strips -- but only
     once .active: these strips exist in the DOM even on the plain,
     unfocused canvas -- see focusArtifact's chrome measurement in
     computeCameraFor -- and even empty/invisible, their own padding gives
     them real height, so an unconditional pointer-events:auto here would
     silently swallow clicks/drags along the top and bottom edges of the
     plain overview too). */
  /* Figma shows the pills row in BOTH the zoomed-out default canvas and
     the zoomed-in composition -- not focus-only chrome -- so
     .pa-focus-chrome now only wraps the bottom nav (focus-only, per
     Figma's zoomed-in-only bottom strip) and the NETWORK widget's own
     focused-vs-default body swap is handled inside #pa-network itself.
     The pills live in their own always-on overlay, .pa-filter-pills. */
  /* Figma 292:2982: the four pills sit in one recessed tray -- n200 fill,
     0.4px n400 hairline, 8px radius, 4px padding, 6px between pills --
     rather than floating loose on the canvas. 14px in from the frame's
     corner, the site's edge margin; max-width keeps it inside the frame
     (with the same inset) on a narrow phone, where the pills wrap. The 0.4px hairline is --hair-s (an inset box-shadow), not
     a border: Chrome snaps a sub-1px border up to a whole device pixel,
     so a border reads 0.5px+ here. --outline-s is added to the padding
     instead, which is what the border's own width would have contributed. */
  .pa-filter-pills{position:absolute;left:14px;top:14px;z-index:7;display:flex;flex-wrap:wrap;
    align-items:center;gap:var(--gap-xs);max-width:calc(100% - 28px);box-sizing:border-box;
    padding:calc(var(--pad-h-xxs, 4px) + var(--outline-s));background:var(--n200);
    box-shadow:var(--hair-s);border-radius:var(--curve-s);}
  .pa-focus-chrome{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;justify-content:flex-end;
    pointer-events:none;}
  /* The strip is the one piece of chrome that appears and disappears
     with focus, so it was the jarring one -- it used to pop in and out.
     Slide-and-fade from below on the branded curve, reversing on exit. */
  .pa-focus-chrome .pa-zoom-bottom-nav{pointer-events:none;
    opacity:0;transform:translateY(14px);
    transition:opacity .35s cubic-bezier(.4,0,.2,1), transform .35s cubic-bezier(.4,0,.2,1);}
  .pa-focus-chrome.active .pa-zoom-bottom-nav{pointer-events:auto;opacity:1;transform:translateY(0);}
  /* Figma "Badge Buttons" pill (292:2983-2986), now a segmented control:
     the SELECTED pill is the raised one -- white n100 fill, 0.4px n400
     border, drop shadow, black n900 text/icon -- and every UNSELECTED
     pill is recessed: n200 fill, inset shadow, gray n500 text/icon.
     This reverses the earlier reading (selected recessed, unselected
     raised) that an older round of feedback had asked for; the new Figma
     frame is the newer and explicit source, so it wins. Font-size: this
     file's --fs-xs (12px) is what matches Figma's font-size/xxs -- its
     own --fs-xxs is a separate 10px token, so don't "fix" it. Hover: only
     the clickable (unselected) pills react, by easing the inset shadow
     from 15% to 8% black (on request; Figma has no hover variant); the
     selected one is not clickable (applyFilter ignores it), so it gets
     no hover change. */
  .pa-pill{border:0;border-radius:var(--curve-xs);
    display:flex;align-items:center;gap:var(--gap-xs);
    padding:calc(var(--pad-v-xxs, 4px) + var(--outline-s)) calc(var(--pad-h-xxs, 4px) + var(--outline-s));
    font-family:var(--font-mono);font-weight:500;font-size:var(--fs-xs);letter-spacing:0;
    cursor:pointer;position:relative;background:var(--n200);color:var(--n500);
    box-shadow:inset 0px 1px 2.9px rgba(0,0,0,.15), var(--hair-s);
    transition:background-color .15s ease, color .15s ease, box-shadow .15s ease;}
  .pa-pill .pa-pill-label{padding-left:var(--pad-h-xxs, 4px);white-space:nowrap;}
  .pa-pill .icon{width:18px;height:18px;}
  .pa-pill.active{background:var(--n100);color:var(--n900);
    box-shadow:var(--shadow-badge), var(--hair-s);}
  .pa-pill:not(.active):hover{box-shadow:inset 0px 1px 2.9px rgba(0,0,0,.08), var(--hair-s);}
  /* Figma 458:2274: the filters bar opens with a back button -- the
     raised (selected-pill) look, Return icon only -- then a 20px x 0.4px
     n400 divider, then the pills. It replaces the NETWORK widget's RETURN
     button and the phone-only one that stood in for it, at every
     viewport. Only there while an artifact is open: it eases in at the
     front and the pills slide over (width + opacity, .25s -- the old
     RETURN's fade timing), and the negative margin cancels the bar's own
     6px gap while it's closed so the pills sit flush. Padded vertically
     (and pulled back by the same amount) so overflow:hidden doesn't clip
     the button's drop shadow. Hover: the same lift as the site's other
     raised buttons. */
  .pa-filter-back{display:flex;align-items:center;gap:var(--gap-xs);flex-shrink:0;
    overflow:hidden;padding:3px 0;margin:-3px calc(-1 * var(--gap-xs)) -3px 0;
    max-width:0;opacity:0;visibility:hidden;
    transition:max-width .25s ease, margin-right .25s ease, opacity .25s ease, visibility .25s;}
  .pa-filter-back.shown{max-width:48px;margin-right:0;opacity:1;visibility:visible;}
  /* The 0.4px line is painted the same way as every other hairline here
     (an inset box-shadow, like --hair-s) on a 1px-wide box: a box that
     is itself only 0.4px wide gets antialiased almost to nothing, so the
     line barely showed next to the pills' own 0.4px outlines. */
  .pa-filter-divider{flex-shrink:0;width:1px;height:20px;box-shadow:inset var(--outline-s) 0 0 var(--n400);}
  .pa-pill.pa-back:hover{box-shadow:var(--shadow-badge), var(--shadow-badge-hover), var(--hair-s);}
  /* Phones (<=640px, this layer's phone breakpoint): the bar plus the back
     button is a few px too wide for the frame, so there the bar keeps its
     plain four pills and the same icon-only back button sits bottom-right
     instead -- the corner the NETWORK widget leaves once an artifact is
     open. Out, then in, as before: opening, the widget fades out at once
     and the button fades in after it (.25s delay); returning, the button
     fades out at once and the widget fades back in after it (the delay on
     .pa-network's own phone rule). */
  .pa-pill.pa-phone-back{display:none;}
  @media (max-width:640px){
    .pa-filter-back{display:none;}
    .pa-pill.pa-phone-back{display:flex;position:absolute;right:14px;bottom:14px;z-index:7;
      opacity:0;visibility:hidden;pointer-events:none;
      transition:opacity .25s ease, visibility .25s, background-color .15s ease, color .15s ease, box-shadow .15s ease;}
    .pa-pill.pa-phone-back.shown{opacity:1;visibility:visible;pointer-events:auto;
      transition-delay:.25s, .25s, 0s, 0s, 0s;}
  }
  /* Section 14-16: the focal's own connections (connMembers), the same
     set the connection lines/tags already draw -- Figma's example strip
     content was confirmed to be placeholder-only, not a real spec for
     "which 3," so this reuses the one real relationship the data model
     already has rather than inventing a separate selection rule. Image-
     only per Figma (no title underneath any more), positioned bottom-
     LEFT rather than stretched full-width. */
  /* Figma's switcher row (294:3173): items bottom-aligned with an 8px
     (gap/s) gap. overflow-y is no longer hidden -- that was what clipped
     the tops of the items off. Only the horizontal axis scrolls, and it
     gets a little headroom so nothing is shaved. */
  .pa-zoom-bottom-nav{pointer-events:none;align-self:flex-start;display:flex;gap:var(--gap-s);
    align-items:flex-end;padding:6px 0 20px 20px;
    overflow-x:auto;overflow-y:visible;-webkit-overflow-scrolling:touch;}
  /* Each item is the Default artifact card verbatim (293:3106): 138.58px
     wide, white, 0.4px n400 border, 8px radius, a 4px frame around a
     near-square photo with a 4px radius. */
  /* 112px rather than the artifact card's own 138.58px: the strip is a
     switcher, not a row of artifacts, and at full card width it took more
     of the frame than it earns. (96px on a phone -- see the mobile block.) */
  /* 53.76px: 67.2px scaled by 0.8x, for desktop/tablet only -- the
     mobile override below (48px) is untouched. */
  /* padding:2px/border-radius:curve-xs -- on request, tightened to match
     the mobile breakpoint's own pairing below rather than keeping a
     separate, looser desktop/tablet value (was padding:--pad-h-xxs/4px,
     radius:curve-s/8px). */
  .pa-zoom-bottom-item{flex:0 0 auto;width:53.76px;cursor:pointer;border-radius:var(--curve-xs);
    background:var(--n100);border:var(--outline-s) solid var(--n400);padding:2px;
    transition:box-shadow .15s ease, transform .15s ease;}
  /* Same fill-width/hug-height rule as the canvas card, so one artifact
     keeps its proportions everywhere it appears. The row bottom-aligns
     (align-items:flex-end), so thumbnails of differing height now sit on
     a shared baseline rather than being stretched to match. radius:2px to
     match .pa-zoom-bottom-item's own tightened corner above (was
     curve-xs/4px). */
  .pa-zoom-bottom-item .pa-zoom-bn-media{aspect-ratio:var(--pa-aspect, 449.586/473.328);border-radius:2px;
    position:relative;overflow:hidden;transition:opacity .25s ease;
    background-color:var(--n300);background-size:cover;background-position:center;background-repeat:no-repeat;}
  /* The dimming is on the PHOTO, not the card: the white frames all stay
     solid, and only the images of the artifacts you aren't looking at
     drop to 30% (opacity/m). The current artifact is the first item and
     keeps its photo at full strength. */
  .pa-zoom-bottom-item:not(.active) .pa-zoom-bn-media{opacity:var(--opacity-muted, .3);}
  .pa-zoom-bottom-item:hover .pa-zoom-bn-media{opacity:1;}
  /* The switcher thumbnails lose the ring too -- the request was to drop
     it from the layer entirely. They're 112px desktop / 48px mobile,
     close enough to the 138.58px card that a third scaled value would be
     false precision, so they share --shadow-focal-sm. */
  .pa-zoom-bottom-item:hover{box-shadow:var(--shadow-focal-sm);}
  .pa-zoom-bottom-item.active{cursor:default;}
  /* The camera move itself (translate+scale on #pa-group-surface) is
     eased via the .pa-cam-anim class toggled in JS around a focus/defocus/
     re-focus call -- see applyCamera. Never applied during a live pan
     drag, which must track the pointer with zero lag. */
  .pa-group-surface.pa-cam-anim{transition:transform .6s cubic-bezier(.4,0,.2,1), --pa-spread .6s cubic-bezier(.4,0,.2,1);}
  #pa-group.pa-focused{cursor:default;}
  /* Persistent connection lines while an artifact is focused -- distinct
     from the plain overview's own subtle hover-preview lines
     (.pa-hover-line), and deliberately bolder/heavier per spec section 3
     ("large and visually legible... not tiny, not hidden underneath the
     card"). Lives inside #pa-group-surface, sized to the same BOARD_W x
     BOARD_H board as every card and as .pa-hover-lines, so it pans/scales
     in lockstep with the cards it connects with no extra math. Positioned
     above the plain/dimmed cards (z-index:3, same precedent as
     .pa-hover-lines) but below the focal/connected cards themselves
     (z-index 4/5) -- so a line is never hidden behind unrelated
     background clutter, but naturally (and correctly) disappears under
     the opaque body of the very card its own end points at. */
  .pa-focus-edges{position:absolute;top:0;left:0;z-index:1;overflow:visible;pointer-events:auto;}
  /* stroke-width is in the SVG's own user-space units, which live INSIDE
     the scaled #pa-group-surface -- without vector-effect:non-scaling-stroke
     a 2px line would render at 2px*scale on screen, i.e. under 1px once
     computeCameraFor has to zoom out to keep a far-flung connection in
     frame. non-scaling-stroke pins the on-screen width to the CSS value
     regardless of camera scale, which is what spec section 3/5 mean by
     "large and visually legible at the zoomed-in scale... do NOT make the
     lines tiny." */
  /* Dotted (dot-line.js) like .pa-hover-line. The dots themselves take no
     pointer events -- the wide .pa-focus-edge-hit path beside them in the
     same <g> is the hover/click target, so hover is keyed off the <g>. */
  .pa-focus-edge{color:var(--n900);--dot-emph:var(--n900);opacity:.55;pointer-events:none;
    transition:opacity .15s ease, color .15s ease;}
  .pa-focus-edges g:hover > .pa-focus-edge{opacity:.9;}
  /* A wider, invisible hit-target under each visible line -- same reason
     as the old .pa-connected-arc-hit: the visible stroke is deliberately
     thin, too thin to comfortably click/tap on its own. Also pinned to a
     constant on-screen hit width via non-scaling-stroke, so the tap target
     doesn't shrink into uselessness at low camera scale either. */
  .pa-focus-edge-hit{fill:none;stroke:transparent;stroke-width:24;vector-effect:non-scaling-stroke;pointer-events:stroke;cursor:pointer;}
  /* No browser focus ring on the clicked-on view's own controls. These are
     tabindex=0 so they stay reachable, but once the keyboard has been
     used (the arrow keys step through artifacts) Chrome matches
     :focus-visible on whichever one holds focus and draws its blue
     default ring on a line, thumbnail or card the user only clicked. */
  .pa-focus-edge-hit:focus, .pa-focus-edge-hit:focus-visible,
  .pa-group .pa-card:focus, .pa-group .pa-card:focus-visible,
  .pa-zoom-bottom-item:focus, .pa-zoom-bottom-item:focus-visible{outline:none;}
  /* Figma "Tags" component ("STRATEGY" in the reference) -- per spec
     answer, the label is simply the connected artifact's own name/title,
     not a separate relationship-type field. Lives as a normal absolutely-
     positioned element inside #pa-group-surface (board-space, like the
     cards), NOT inside the SVG -- HTML text in SVG needs a foreignObject,
     and placing it as a sibling div lets it scale with the camera exactly
     like the cards/lines it's labeling, with no extra transform math. */
  /* Plain literal values -- the surface is never scaled any more, so a
     12px label is 12px on screen in every state. */
  /* Hidden until its own connection is hovered -- see showEdgeTagFor. */
  .pa-edge-tag{opacity:0;transition:opacity .16s ease;
    position:absolute;transform:translate(-50%,-50%);z-index:6;pointer-events:none;
    background:var(--n300);border-radius:var(--curve-xs);
    padding:var(--pad-v-xxs, 4px) var(--pad-h-xs, 10px);
    font-family:var(--font-mono);font-weight:400;font-size:var(--fs-xs);letter-spacing:0;color:var(--n600);
    text-transform:uppercase;white-space:nowrap;
    /* Capped and ellipsised so a long artifact title can't grow a label
       wider than the frame it has to sit inside -- on a phone two full
       titles simply cannot share a row, and an uncapped one would push
       its neighbour off-screen. */
    max-width:min(200px, 46vw);overflow:hidden;text-overflow:ellipsis;}
  .pa-edge-tag.visible{opacity:1;}
  /* Toast variant, for a label with nowhere to sit on its own line (the
     artifact fills the frame and every point on the visible stretch of
     the curve is behind it). Deliberately the SAME chip -- background,
     radius, padding, type, the max-width cap -- per request; only the
     anchor and the entrance differ. It is positioned beneath the focal
     card by layoutEdgeTags, and rises the last few pixels into place so
     it reads as arriving rather than blinking on. */
  .pa-edge-tag.pa-edge-tag-toast{
    transform:translate(-50%,-50%) translateY(6px);
    transition:opacity .16s ease, transform .22s cubic-bezier(.4,0,.2,1);}
  .pa-edge-tag.pa-edge-tag-toast.visible{transform:translate(-50%,-50%) translateY(0);}
  /* ============================================================
     NETWORK WIDGET -- Figma "Container" (node 292:2611). Persistent in
     both states (unlike the pills/bottom-nav, which are focus-only in
     the zoomed-in reference but absent from the zoomed-out one): Figma's
     own zoomed-out frame shows this same widget in its DEFAULT body
     (the site's philosophy blurb), and swaps to the per-artifact
     click-count body only once an artifact is focused. Pinned top-right
     as an overlay rather than reproducing Figma's literal fixed-canvas
     pixel offsets, which were computed against its own fixed 1600px-wide
     artboard and don't translate to this fluid-width layout.
     ============================================================ */
  /* Entrance matching the real site's own widget. styles.css gives
     `.panel` a `panel-appear` of opacity 0->1, blur(10px)->0 and a rise,
     over .9s on the branded curve; this widget previously had no
     entrance at all and simply rode in on the layer's transform. The
     keyframe is a PA-specific copy because the site's animates `top`
     60%->50%, which assumes that panel's centred positioning -- this one
     is pinned to a corner, so it travels with translateY instead. */
  @keyframes pa-network-appear{
    from{opacity:0;filter:blur(10px);transform:translateY(12px);}
    to{opacity:1;filter:blur(0);transform:translateY(0);}
  }
  .pa-network.pa-network-enter{animation:pa-network-appear .9s cubic-bezier(.4,0,.2,1) both;}
  .pa-network{position:absolute;right:14px;top:14px;z-index:7;width:330px;max-width:calc(100% - 28px);
    /* No outline on the outer frame (Figma 292:2611) -- just the n300
       frame, like the main widget's (.panel); only the header and body
       inside carry hairlines. */
    background:var(--n300);border-radius:var(--curve-m);
    padding:var(--pad-h-xxs, 4px);display:flex;flex-direction:column;
    /* Same two properties, same duration and easing the real site uses on
       its own collapsible widget (styles.css `.panel`) -- collapsing is a
       width-and-corner move there, so it is here. */
    transition:width .25s ease, border-radius .25s ease;}
  /* 200px matches the main widget's own collapsed width exactly
     (styles.css `.panel.collapsed`), on request, rather than Figma's
     own 264px (42:522) -- the two collapsed pills sit on the same
     canvas and should read as the same size. The header going to a
     full curve/s on every corner is Figma's radius for that node --
     collapsed, the header is no longer the lid of a box, so it stops
     being rounded on only its top two corners. Matches how
     `.panel.collapsed .panel-handle` behaves on the live site. */
  .pa-network.collapsed{width:200px;}
  .pa-network.collapsed .pa-network-header{border-radius:var(--curve-s);}
  /* Draggable via panel.js's shared makeDraggable() -- same cursor/shadow
     language as .panel-handle/.panel.dragging and .case-header/
     .case-panel.dragging: grab at rest, grabbing while held, and a shadow
     that only exists while .dragging is applied (no shadow at rest). */
  .pa-network-header{display:flex;align-items:center;gap:var(--gap-xs);
    padding:calc(var(--pad-v-s) + var(--outline-s)) calc(var(--pad-h-s) + var(--outline-s));
    background:var(--n200);box-shadow:var(--hair-s);border-radius:var(--curve-s) var(--curve-s) 0 0;
    font-family:var(--font-mono);font-weight:400;font-size:var(--fs-xs);letter-spacing:0;color:var(--n900);user-select:none;cursor:grab;}
  .pa-network-header:active{cursor:grabbing;}
  .pa-network.dragging{box-shadow:0 4px 9.35px rgba(0,0,0,.05);}
  .pa-network.snapping{transition:left .25s ease, top .25s ease;}
  /* Collapse/expand while parked at its home corner: the width change
     and the slide back to the exact corner run together (see the
     shrink button's handler in personal-artifacts.js). */
  .pa-network.pa-network-homing{transition:width .25s ease, border-radius .25s ease, left .25s ease, top .25s ease;}
  .pa-network-header .pa-network-title{flex:1;min-width:0;}
  .pa-network-body{background:var(--n100);border:var(--outline-s) solid var(--n400);border-top:none;
    border-radius:0 0 var(--curve-s) var(--curve-s);padding:var(--pad-h-s);
    display:flex;flex-direction:column;gap:var(--gap-xs);}
  .pa-network-body[hidden]{display:none;}
  /* font-size was var(--fs-xs) (12px in this file's own scale) -- Figma's
     "Biography" component (which this copy block and the focal card's
     caption both use) is font-size/xs, which is Figma's OWN 14px token;
     the two "xs" names don't line up across the two systems (see the
     .pa-pill comment above for the same mismatch elsewhere), so this was
     rendering 2px small. --fs-s is this file's 14px token. */
  /* Crossfaded on focus/defocus rather than swapped outright -- the copy
     used to change instantly (and RETURN popped in) while everything
     else on screen was still gliding for .6s. See swapNetworkBody. */
  /* line-height:normal, matching .bio-p (the home widget's About body
     text) and .pa-card-caption -- was 1.5, a noticeably looser line
     height than every other body-copy text on the site. */
  .pa-network-copy{font-family:var(--font-body);font-weight:400;font-size:var(--fs-s);line-height:normal;letter-spacing:0;color:var(--n600);padding-bottom:var(--pad-v-s);
    transition:opacity .25s ease;}
  .pa-network-copy.pa-swapping{opacity:0;}
  .pa-network-copy p{margin:0 0 .9em;}
  .pa-network-copy p:last-child{margin-bottom:0;}
  .pa-network-copy b{color:var(--n900);font-weight:400;}
  /* Same underline spec as .bio-link (Google/Cornell/UNDP in the About
     bio) -- was missing the offset/skip-ink longhands that rule carries,
     so the click-tracking numbers (clickTrackingCopy's <u>12</u>/<u>02</u>)
     sat visibly closer to the text than every other dotted underline on
     the site. No cursor/hover treatment added, unlike .bio-link itself --
     these numbers aren't links. */
  .pa-network-copy u{text-decoration-line:underline;text-decoration-style:dotted;
    text-decoration-thickness:8%;text-underline-offset:24%;text-decoration-skip-ink:auto;
    text-decoration-color:var(--n400);color:var(--n900);}
  @media (max-width:640px){
    /* The NETWORK widget stays in the default (unfocused) view, collapsed
       and parked bottom-right -- the same resting shape the site's main
       widget takes on a phone. It's only the CLICKED-ON view that drops
       it (the bottom-right back button gets you out). */
    /* An explicit width, not `auto`: CSS can't interpolate between auto
       and the collapsed 200px, so expand/collapse used to snap here
       while desktop/tablet (330px <-> 200px) eased. calc(100% - 28px)
       is what auto resolved to anyway (the body copy always fills to the
       max-width), so the expanded widget looks the same -- it just moves
       now. */
    .pa-network{top:auto;bottom:14px;right:14px;width:calc(100% - 28px);max-width:calc(100% - 28px);
      transition:width .25s ease, border-radius .25s ease, bottom .35s cubic-bezier(.4,0,.2,1),
                 opacity .25s ease .25s, visibility .25s .25s;}
    /* Same 200px as desktop -- the main widget's own .panel.collapsed
       carries no mobile override either (styles.css), so the two
       collapsed pills stay the same size at every breakpoint. */
    .pa-network.collapsed{width:200px;}
    /* Focused view: the widget's philosophy blurb / click-counter body
       doesn't earn its keep at this size once an artifact is open, and
       getting back out is the bottom-right back button. Selector relies
       on #pa-network being the LATER sibling of #pa-group in the DOM. */
    /* Faded rather than display:none, which dropped it in a single frame
       while every other change on focus eases -- desktop's own version of
       this moment is the widget's .25s content crossfade
       (updateNetworkWidget), so this uses the same .25s. */
    /* Out, then in -- never both in the corner at once. Opening an
       artifact: the widget fades out now (delay 0 here), the back button
       (.pa-phone-back) fades in after it. Returning: the button fades out
       now, the widget fades back in after it (the .25s delay on the base
       rule above). */
    .pa-group.pa-focused ~ .pa-network{opacity:0;visibility:hidden;pointer-events:none;transition-delay:0s;}
    /* Half the previous 96px, per request: the strip was eating the
       bottom-right corner the widget used to live in. At 48px it reads as
       a thumbnail row rather than a row of cards, which is what a
       switcher wants anyway. Padding/radius now match the desktop/
       tablet default above, so only width needs overriding here. */
    .pa-zoom-bottom-item{width:48px;}
    /* Stops short of .pa-phone-back instead of scrolling underneath it
       -- the reserve is that button's own footprint (~27px wide + its
       14px inset + a 14px gap). */
    .pa-zoom-bottom-nav{padding:6px 0 14px 14px;max-width:calc(100% - 56px);}
  }
  @media (max-width:767px){
    /* Bottom-right, beside the main widget rather than floating over the
       top of the canvas -- and deliberately NOT in the document flow, so
       it can't push the graph down into the fold. */
    /* + --safe-bottom, same as the main widget's pill (panel.js), so the
       two stay level above iOS's home indicator. */
    .pa-trigger{top:auto;bottom:calc(14px + var(--safe-bottom, 0px));right:14px;}
    /* Tooltip flips above the button down here, since there's nothing
       below it but the edge of the screen. */
    .pa-trigger::after{top:auto;bottom:calc(100% + 8px);transform:translateY(4px);}
    .pa-trigger:hover::after{transform:translateY(0);}
  }
  /* The three focus tiers, applied to the SAME real .pa-card elements the
     plain overview already uses -- never a separate dot/satellite/mini
     stand-in. Transition lives on the shared .pa-group .pa-card rule
     below so blur/opacity always eases in and out together with the
     camera move itself. */
  /* No extra local scale boost any more -- there used to be one here
     (transform:scale(2.6), on top of the shared camera zoom), removed
     after checking Figma's own focused-composition frame (289:2263/
     292:2855) directly: the focal card and the scattered "ghost" cards
     around it all sit at essentially the SAME size, not a boosted one.
     Worse, the boost was actively counter-productive -- computeCameraFor
     had to fit that boosted box within its target frame fraction, which
     meant a BIGGER local boost forced a SMALLER shared camera scale to
     compensate, and every other card (which only ever gets the shared
     scale, never the local one) rendered smaller as a result. Dropping
     it lets the shared camera zoom alone size everything, focal and
     ghosts together, which is both simpler and the actual Figma-accurate
     result (see computeCameraFor's own comment for the other half).
     Deliberately no rotate() either -- corrected per that same reference:
     the scattered ghost cards each carry their own random rotation, but
     the focal card sits perfectly upright regardless of whatever --pa-rot
     it was scattered with in the overview. Two classes
     (.pa-card.pa-focal-highlight) already out-specificities the single-
     class base rule's `transform:rotate(var(--pa-rot))`, so `transform:
     none` replaces the whole transform outright rather than needing to
     cancel the rotation explicitly. */
  /* No box-shadow here any more. This used to stack an extra 1.12px n400
     ring AND a second copy of the focal drop-shadow on top of what the
     clicked variant already carries -- the card was drawing its 1px
     border, then a ring just outside it, then two shadows. Figma's
     clicked variant (293:3108) has exactly two things: the 1px n400
     border (from .pa-group.pa-focused .pa-card) and one drop-shadow
     (from .pa-group.pa-focused .pa-card.pa-focal-highlight). That's all
     that's left. */
  .pa-card.pa-focal-highlight{z-index:5;transform:none;}
  /* ...and no hover elevation on it either -- it isn't a click target
     while it's the one being looked at, and it already carries
     --shadow-focal at rest, so adding the hover shadow would just draw a
     second copy of the same thing on top of itself. */
  .pa-group.pa-focused .pa-card.pa-focal-highlight:hover{box-shadow:var(--shadow-card);}
  .pa-card.pa-conn-highlight{z-index:4;}
  /* Hovering a connection line brings that
     one artifact to full opacity instead of leaving it at the member
     tier's usual 30% -- driven by showEdgeTagFor/hideEdgeTags, the same
     functions that already reveal that connection's label, so both react
     to the same line hover together. Scoped to .pa-conn-highlight so this
     never overrides the DIMMED tier: an artifact outside the focal's
     connections has no line to hover in the first place, so this should
     never apply to it, but the selector makes that structurally true
     rather than relying on the JS never calling it there. */
  .pa-group.pa-focused .pa-card.pa-conn-highlight.pa-edge-hover{opacity:1;}
  /* Figma's own background-texture "ghost" cards behind the focal are
     just this same persistent card at 10% opacity, sharp (not blurred) --
     see --opacity-ghost. Deliberately still a real, clickable .pa-card
     (per the explicit answer to "should these stay interactive": yes) --
     nothing here disables pointer-events, so a faint peripheral card is
     still a legitimate click target, exactly like the plain overview. */
  .pa-card.pa-dimmed{opacity:var(--opacity-ghost);}
  /* .pa-back (the old header back-chevron) removed -- see the HTML-side
     comment above <div class="pa-header"> for why: Figma's header never
     shows one, and the NETWORK widget's RETURN button now covers that
     role. */
  /* ---------- end of personal-artifacts.css migration target ---------- */
