tsx
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";

// Notification center: a bell button that opens a glassmorphism popover with a
// segmented switch toggling between a notifications list and a "What's new" feed.
//
// Drop <NotificationCenter /> anywhere with room below-right (e.g. an app top bar).
// The popover anchors to the bell's top-right corner and scales from that origin.
// Motion uses CSS transforms and opacity, with a reduced-motion override.
//
// Swap NOTIFICATIONS / ANNOUNCEMENTS for your data. Images here live under
// /playground/notification-center/ — point them at your own assets.

export default function NotificationCenter() {
  const uid = useId();
  const [open, setOpen] = useState(false);
  const [hasOpened, setHasOpened] = useState(false);
  const [tab, setTab] = useState<TabId>("notifications");
  const [dir, setDir] = useState(1);
  const [peek, setPeek] = useState(false); // inactive tab hovered → active pill leans toward it

  const wrapRef = useRef<HTMLDivElement>(null);
  const bellRef = useRef<HTMLButtonElement>(null);
  const bodyRef = useRef<HTMLDivElement>(null); // shared scroll container — reset to top on tab switch
  const tabRefs = useRef<Record<TabId, HTMLButtonElement | null>>({
    notifications: null,
    whatsnew: null,
  });

  const tabIndex = (id: TabId) => TABS.findIndex((t) => t.id === id);

  function selectTab(id: TabId) {
    setDir(tabIndex(id) > tabIndex(tab) ? 1 : -1);
    setTab(id);
    setPeek(false); // clear the lean before the pill slides
  }

  // While open: close on outside click (mousedown beats the bell's onClick) and
  // on Escape. The Escape listener is capture-phase + stopPropagation so it
  // closes the popover before any ancestor/global Escape handler can react,
  // regardless of where focus currently sits.
  useEffect(() => {
    if (!open) return;
    function onDown(e: MouseEvent) {
      if (!wrapRef.current?.contains(e.target as Node)) setOpen(false);
    }
    function onKey(e: KeyboardEvent) {
      if (e.key === "Escape") {
        e.stopPropagation();
        e.preventDefault();
        setOpen(false);
        bellRef.current?.focus();
      }
    }
    document.addEventListener("mousedown", onDown);
    document.addEventListener("keydown", onKey, true);
    return () => {
      document.removeEventListener("mousedown", onDown);
      document.removeEventListener("keydown", onKey, true);
    };
  }, [open]);

  // Move focus into the popover when it opens.
  useEffect(() => {
    if (open) tabRefs.current[tab]?.focus();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [open]);

  function onTabKeyDown(e: React.KeyboardEvent) {
    const i = tabIndex(tab);
    let next: TabId | null = null;
    if (e.key === "ArrowRight") next = TABS[(i + 1) % TABS.length].id;
    else if (e.key === "ArrowLeft") next = TABS[(i - 1 + TABS.length) % TABS.length].id;
    else if (e.key === "Home") next = TABS[0].id;
    else if (e.key === "End") next = TABS[TABS.length - 1].id;
    if (next) {
      e.preventDefault();
      selectTab(next);
      tabRefs.current[next]?.focus();
    }
  }

  const panelDirectionClass = dir > 0 ? "is-forward" : "is-backward";

  return (
    <div className="nc-bell-wrap" ref={wrapRef}>
      <style>{styles}</style>
      <button
        ref={bellRef}
        type="button"
        className="nc-bell"
        aria-label="Notifications"
        aria-haspopup="dialog"
        aria-expanded={open}
        onClick={() => {
          if (!open) preloadAnnouncementImages();
          if (!open) setHasOpened(true);
          setOpen((value) => !value);
        }}
      >
        <BellIcon />
        <span className="nc-bell-dot" aria-hidden="true" />
      </button>

      <div
        className={`nc-popover${open ? " is-open" : ""}`}
        role="dialog"
        aria-label="Notification center"
        aria-modal="false"
        aria-hidden={!open}
        inert={!open}
      >
        {hasOpened && (
          <>
            <div className="nc-header">
              <div className="nc-switch" role="tablist" aria-label="Notification view">
                <span
                  className={`nc-pill nc-pill--${tab}${peek ? " is-peeking" : ""}`}
                  aria-hidden="true"
                />
                {TABS.map((t) => {
                  const active = tab === t.id;
                  return (
                    <button
                      key={t.id}
                      ref={(el) => {
                        tabRefs.current[t.id] = el;
                      }}
                      type="button"
                      role="tab"
                      id={`${uid}-tab-${t.id}`}
                      aria-selected={active}
                      aria-controls={`${uid}-panel-${t.id}`}
                      tabIndex={active ? 0 : -1}
                      className={`nc-tab${active ? " is-active" : ""}`}
                      onClick={() => selectTab(t.id)}
                      onMouseEnter={() => {
                        setPeek(!active);
                        if (t.id === "whatsnew") preloadAnnouncementImages();
                      }}
                      onMouseLeave={() => setPeek(false)}
                      onFocus={() => {
                        if (t.id === "whatsnew") preloadAnnouncementImages();
                      }}
                      onKeyDown={onTabKeyDown}
                    >
                      <span className="nc-tab-label">{t.label}</span>
                    </button>
                  );
                })}
              </div>
            </div>

            <div className="nc-body" ref={bodyRef}>
              {tab === "notifications" ? (
                  <div
                    className={`nc-panel ${panelDirectionClass}`}
                    role="tabpanel"
                    id={`${uid}-panel-notifications`}
                    aria-labelledby={`${uid}-tab-notifications`}
                    tabIndex={0}
                  >
                    <ScrollReset containerRef={bodyRef} />
                    <ul className="nc-list">
                      {NOTIFICATIONS.map((n) => (
                        <li key={n.id} className="nc-row">
                          <div className="nc-avatar">
                            {n.logo ? (
                              <img
                                src={n.logo.src}
                                alt={n.logo.ticker}
                                width={32}
                                height={32}
                                className="nc-avatar-logo"
                                translate="no"
                              />
                            ) : (
                              <span className="nc-avatar-glyph" aria-hidden="true">
                                <Glyph name={n.glyph!} />
                              </span>
                            )}
                          </div>
                          <div className="nc-row-body">
                            <p className="nc-row-msg">{n.message}</p>
                            <p className="nc-row-time">{n.time}</p>
                          </div>
                        </li>
                      ))}
                    </ul>
                  </div>
                ) : (
                  <div
                    className={`nc-panel ${panelDirectionClass}`}
                    role="tabpanel"
                    id={`${uid}-panel-whatsnew`}
                    aria-labelledby={`${uid}-tab-whatsnew`}
                    tabIndex={0}
                  >
                    <ScrollReset containerRef={bodyRef} />
                    <div className="nc-feed">
                      {ANNOUNCEMENTS.map((a, i) => (
                        <div key={a.id}>
                          {i > 0 && <div className="nc-divider" />}
                          <article className={`nc-item nc-item--${a.layout}`}>
                            {a.layout === "hero" ? (
                              <>
                                <div className="nc-item-text">
                                  <div className="nc-item-top">
                                    {a.unread && <span className="nc-blip" aria-hidden="true" />}
                                    <h3 className="nc-item-title">{a.title}</h3>
                                    <ArrowIcon />
                                  </div>
                                  <p className="nc-item-body">{a.body}</p>
                                </div>
                                <img
                                  className="nc-hero-img"
                                  src={a.image}
                                  alt={a.alt}
                                  width={360}
                                  height={182}
                                  loading="eager"
                                  fetchPriority="high"
                                  decoding="async"
                                />
                                <p className="nc-item-date">{a.date}</p>
                              </>
                            ) : (
                              <>
                                <div className="nc-item-row">
                                  <div className="nc-item-content">
                                    <div className="nc-item-text">
                                      <div className="nc-item-top">
                                        {a.unread && <span className="nc-blip" aria-hidden="true" />}
                                        <h3 className="nc-item-title">{a.title}</h3>
                                        <ArrowIcon />
                                      </div>
                                      <p className="nc-item-body nc-clamp">{a.body}</p>
                                    </div>
                                    <p className="nc-item-date">{a.date}</p>
                                  </div>
                                  <img
                                    className="nc-thumb"
                                    src={a.image}
                                    alt={a.alt}
                                    width={136}
                                    height={102}
                                    loading="lazy"
                                    decoding="async"
                                  />
                                </div>
                              </>
                            )}
                          </article>
                        </div>
                      ))}
                    </div>
                  </div>
                )}
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// ----- Icons -----

function Glyph({ name }: { name: "premium" | "gift" | "coin" }) {
  if (name === "premium") {
    return (
      <svg viewBox="0 0 15.869 13.0751" width="16" height="16" fill="currentColor" aria-hidden="true">
        <path fillRule="evenodd" clipRule="evenodd" d="M10.7016 0C11.5166 0 12.2947 0.343337 12.8441 0.945312L15.4379 3.78809C16.0127 4.41809 16.0127 5.38269 15.4379 6.0127L9.70742 12.293C8.75571 13.3359 7.11332 13.3358 6.16152 12.293L0.43105 6.0127C-0.143604 5.38267 -0.143763 4.41802 0.43105 3.78809L3.0248 0.945312C3.57413 0.343494 4.35157 0.000152031 5.1664 0H10.7016ZM2.85683 6.00098L7.49062 11.0791C7.72856 11.3399 8.1394 11.3399 8.37734 11.0791L13.0102 6.00098H2.85683ZM5.1664 1.80078C4.8574 1.80093 4.5622 1.93091 4.3539 2.15918L2.49062 4.2002H13.3773L11.5141 2.15918C11.3057 1.93094 11.0106 1.80078 10.7016 1.80078H5.1664Z" />
      </svg>
    );
  }
  if (name === "gift") {
    return (
      <svg viewBox="0 0 14.8008 14.9005" width="16" height="16" fill="currentColor" aria-hidden="true">
        <path d="M9.05859 0.0674641C9.51838 -0.121025 10.0447 0.0990024 10.2334 0.558676C10.4219 1.01849 10.2019 1.54483 9.74219 1.73348C9.32212 1.90571 9.00312 2.11838 8.74707 2.40048H12.4004C13.7259 2.40048 14.8008 3.47539 14.8008 4.80087V6.00107C14.8007 6.55197 14.6131 7.05804 14.3008 7.46299V12.0001C14.3008 13.6016 13.0021 14.9005 11.4004 14.9005H3.40039C1.79876 14.9005 0.5 13.6017 0.5 12.0001V7.46299C0.187717 7.05804 0.000107375 6.55197 0 6.00107V4.80087C0 3.47539 1.07491 2.40048 2.40039 2.40048H6.05371C5.79766 2.11838 5.47866 1.90571 5.05859 1.73348C4.59887 1.54483 4.37886 1.01849 4.56738 0.558676C4.75605 0.0990024 5.2824 -0.121025 5.74219 0.0674641C6.41828 0.344696 6.96437 0.714891 7.40039 1.1993C7.83641 0.714891 8.3825 0.344696 9.05859 0.0674641ZM2.30078 12.0001C2.30078 12.6076 2.79288 13.1007 3.40039 13.1007H6.55078V8.40049H2.40039C2.36698 8.40049 2.33386 8.39598 2.30078 8.39463V12.0001ZM12.4004 8.40049H8.25V13.1007H11.4004C12.0077 13.1006 12.5 12.6077 12.5 12.0001V8.39463C12.4669 8.39598 12.4338 8.40049 12.4004 8.40049ZM2.40039 4.20126C2.06902 4.20126 1.80078 4.4695 1.80078 4.80087V6.00107C1.80104 6.33222 2.06918 6.60068 2.40039 6.60068H6.55078V4.20126H2.40039ZM8.25 6.60068H12.4004C12.7316 6.60068 12.9997 6.33222 13 6.00107V4.80087C13 4.4695 12.7318 4.20126 12.4004 4.20126H8.25V6.60068Z" />
      </svg>
    );
  }
  return (
    <svg viewBox="0 0 15 15" width="16" height="16" fill="currentColor" aria-hidden="true">
      <path d="M7.5 2.7627C7.94183 2.7627 8.2998 3.12067 8.2998 3.5625V3.80469C8.6895 3.90197 9.07847 4.06936 9.44629 4.31641C9.81287 4.56281 9.91035 5.06006 9.66406 5.42676C9.41765 5.79331 8.9204 5.89082 8.55371 5.64453C8.00666 5.27716 7.41317 5.23751 6.9834 5.36816C6.52109 5.50886 6.4248 5.75745 6.4248 5.86523C6.42487 6.06215 6.48862 6.17401 6.6748 6.30664C6.91572 6.47818 7.26627 6.60817 7.76465 6.7832C8.20344 6.93731 8.79083 7.13683 9.25293 7.46582C9.76958 7.83377 10.1748 8.39422 10.1748 9.21191C10.1748 10.2969 9.31354 11.0002 8.38477 11.2168C8.35675 11.2233 8.32818 11.2276 8.2998 11.2334V11.4375C8.2998 11.8793 7.94183 12.2373 7.5 12.2373C7.05817 12.2373 6.7002 11.8793 6.7002 11.4375V11.1768C6.19385 11.0479 5.67467 10.8161 5.1709 10.4688C4.80728 10.2179 4.71603 9.72014 4.9668 9.35645C5.21763 8.99282 5.71541 8.90157 6.0791 9.15234C6.85971 9.69052 7.56966 9.7645 8.02148 9.65918C8.49894 9.54786 8.5752 9.30529 8.5752 9.21191C8.57518 9.01458 8.51153 8.90227 8.3252 8.76953C8.08428 8.59796 7.73375 8.46801 7.23535 8.29297C6.7965 8.13885 6.20919 7.93936 5.74707 7.61035C5.23049 7.24255 4.82529 6.68277 4.8252 5.86523C4.8252 4.79436 5.6666 4.09677 6.5166 3.83789C6.57686 3.81955 6.63808 3.80287 6.7002 3.78809V3.5625C6.7002 3.12067 7.05817 2.7627 7.5 2.7627Z" />
      <path fillRule="evenodd" clipRule="evenodd" d="M7.5 0C11.642 0 14.9998 3.35804 15 7.5C14.9998 11.642 11.642 15 7.5 15C3.35817 14.9998 0.000197928 11.6418 0 7.5C0.000211036 3.35817 3.35817 0.000211043 7.5 0ZM7.5 1.80078C4.35229 1.80099 1.80099 4.35229 1.80078 7.5C1.80098 10.6477 4.35228 13.2 7.5 13.2002C10.6479 13.2002 13.2 10.6479 13.2002 7.5C13.2 4.35216 10.6479 1.80078 7.5 1.80078Z" />
    </svg>
  );
}

function BellIcon() {
  return (
    <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinejoin="round" aria-hidden="true">
      <path d="M16 17C16 19.2091 14.2091 21 12 21C9.79086 21 8 19.2091 8 17" />
      <path d="M20 16.191C20 16.6378 19.6378 17 19.191 17H4.80902C4.36221 17 4 16.6378 4 16.191C4 16.0654 4.02924 15.9415 4.08541 15.8292L5.21846 13.5631C5.40413 13.1917 5.51071 12.7859 5.53144 12.3712L5.70037 8.99251C5.86822 5.63561 8.6389 3 12 3C15.3611 3 18.1318 5.63561 18.2996 8.99251L18.4686 12.3712C18.4893 12.7859 18.5959 13.1917 18.7815 13.5631L19.9146 15.8292C19.9708 15.9415 20 16.0654 20 16.191Z" strokeLinecap="square" />
    </svg>
  );
}

// Resets the shared scroll container to the top the instant a panel mounts —
// pre-paint and while the panel is still at opacity 0, so flipping tabs always
// starts at the top without the outgoing list visibly jumping.
function ScrollReset({ containerRef }: { containerRef: React.RefObject<HTMLDivElement | null> }) {
  useLayoutEffect(() => {
    if (containerRef.current) containerRef.current.scrollTop = 0;
  }, []);
  return null;
}

function ArrowIcon() {
  return (
    <svg className="nc-item-arrow" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
      <path d="M8.36328 3.36334C8.71475 3.01186 9.28525 3.01186 9.63672 3.36334L13.6367 7.36334C13.9882 7.71481 13.9882 8.28531 13.6367 8.63677L9.63672 12.6368C9.28526 12.9882 8.71474 12.9882 8.36328 12.6368C8.01182 12.2853 8.01184 11.7148 8.36328 11.3633L10.8262 8.90044H3C2.50297 8.90044 2.09965 8.49708 2.09961 8.00005C2.09961 7.503 2.50294 7.09966 3 7.09966H10.8262L8.36328 4.63677C8.01182 4.28531 8.01184 3.71481 8.36328 3.36334Z" fill="currentColor" />
    </svg>
  );
}

// ----- Data -----

const TABS = [
  { id: "notifications", label: "Notifications" },
  { id: "whatsnew", label: "What’s new" },
] as const;

type TabId = (typeof TABS)[number]["id"];

const ASSETS = "/playground/notification-center";

type Notification = {
  id: number;
  glyph?: "premium" | "gift" | "coin";
  logo?: { src: string; ticker: string };
  message: string;
  time: string;
};

const NOTIFICATIONS: Notification[] = [
  { id: 2, logo: { src: `${ASSETS}/logo-aapl.webp`, ticker: "Apple" }, message: "Your limit order to buy 15 shares of AAPL was completed.", time: "7 hours ago" },
  { id: 3, logo: { src: `${ASSETS}/logo-aapl.webp`, ticker: "Apple" }, message: "Your price target of $310.00 USD for AAPL has been reached.", time: "10 hours ago" },
  { id: 4, logo: { src: `${ASSETS}/logo-nvda.webp`, ticker: "Nvidia" }, message: "Your limit order to buy 50 shares of NVDA was completed.", time: "10 hours ago" },
  { id: 5, logo: { src: `${ASSETS}/logo-amzn.webp`, ticker: "Amazon" }, message: "Your target price of $255.00 USD for AMZN has been reached.", time: "a day ago" },
  { id: 6, glyph: "gift", message: "The time has come! Check if you won $1,000,000 by June 2 at 11:59 PM. T&Cs apply.", time: "a day ago" },
  { id: 7, glyph: "coin", message: "Your INTERAC e-Transfer was deposited into Jeff Bridges account.", time: "a day ago" },
  { id: 8, glyph: "coin", message: "Your INTERAC e-Transfer was deposited into Jeff Bridges account.", time: "a day ago" },
  { id: 9, glyph: "coin", message: "See your interest payout for this month.", time: "2 days ago" },
  { id: 1, glyph: "premium", message: "Now that you have $500,000 with Wealthsimple, say hello to new benefits.", time: "23 days ago" },
];

type Announcement = {
  id: number;
  layout: "hero" | "row";
  unread: boolean;
  title: string;
  body: string;
  image: string;
  alt: string;
  date: string;
};

const IPO_BODY =
  "Get shares at IPO prices — before the market does. Browse our list of initial public offerings (IPO) for access to companies about to go public.";

const ANNOUNCEMENTS: Announcement[] = [
  { id: 1, layout: "hero", unread: true, title: "IPO access unlocked", body: IPO_BODY, image: `${ASSETS}/img-ipo.webp`, alt: "A glowing keyhole emitting light in a dark room.", date: "May 28, 2026" },
  { id: 2, layout: "row", unread: false, title: "We’re giving away a million dollars, every month", body: "Buy in bulk. Become a millionaire. T&Cs apply.", image: `${ASSETS}/img-million.webp`, alt: "Coins falling into a glass jar of cash.", date: "May 28, 2026" },
  { id: 3, layout: "row", unread: false, title: "Hate your business account? We made a better one.", body: "Running a business comes with a whole lot of headaches, and your bank is probably one of th…", image: `${ASSETS}/img-business.webp`, alt: "A copper coil and a studded metal disc on a pale concrete floor.", date: "Apr 23, 2026" },
  { id: 4, layout: "row", unread: false, title: "Introducing The Trade Show", body: "A new live event all about trading and new Wealthsimple trading products.", image: `${ASSETS}/img-tradeshow.webp`, alt: "A promotional graphic for The Trade Show, a trading broadcast.", date: "Apr 11, 2026" },
  { id: 5, layout: "row", unread: false, title: "We’re giving away a 1-kilogram bar of gold", body: "Want to know what a 1-kilogram bar of gold feels like in your hand? Then this is the contest…", image: `${ASSETS}/img-gold.webp`, alt: "An upright gold bar engraved with a W.", date: "Nov 5, 2025" },
];

const preloadedAnnouncementImages = new Set<string>();

function preloadAnnouncementImages() {
  if (typeof Image === "undefined") return;

  for (const { image: src } of ANNOUNCEMENTS.slice(0, 2)) {
    if (preloadedAnnouncementImages.has(src)) continue;
    preloadedAnnouncementImages.add(src);
    const image = new Image();
    image.decoding = "async";
    image.src = src;
  }
}

// ----- Styles -----

const styles = `
.nc-bell-wrap {
  /* ---- Theme tokens — light-dark(<light>, <dark>); the dark arm is the
     original design, tune the light arm here. Glass doesn't invert 1:1: light
     glass is a near-white translucent fill + hairline border + soft shadow.
     Multi-shadow lists use helper vars (light-dark() takes two args). Inherits
     the page color-scheme, so these resolve to the visitor's theme. ---- */
  --nc-title:      light-dark(#1a1a1a, #fafafa);
  --nc-muted:      light-dark(#5f5f5c, #a3a3a3);
  --nc-soft:       light-dark(rgba(0, 0, 0, 0.45), rgba(255, 255, 255, 0.5));
  --nc-ink-strong: light-dark(#111, #fff);
  --nc-accent:     #ed4f3c;
  --nc-outline:  light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.12));
  --nc-hairline: light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.08));
  --nc-divider:  light-dark(rgba(0, 0, 0, 0.07), rgba(255, 255, 255, 0.06));
  --nc-hover: light-dark(rgba(0, 0, 0, 0.045), rgba(255, 255, 255, 0.04));
  --nc-focus: light-dark(rgba(0, 0, 0, 0.4), rgba(255, 255, 255, 0.55));
  --nc-scroll-thumb:       light-dark(rgba(0, 0, 0, 0.22), rgba(255, 255, 255, 0.24));
  --nc-scroll-thumb-hover: light-dark(rgba(0, 0, 0, 0.34), rgba(255, 255, 255, 0.40));
  --nc-glass-bg:     light-dark(rgba(252, 252, 250, 0.78), rgba(38, 38, 38, 0.9));
  --nc-glass-border: light-dark(rgba(0, 0, 0, 0.1), #3e3f40);
  --nc-glass-shadow-light:
    0 0 0 1px rgba(0, 0, 0, 0.04),
    0 8px 24px rgba(0, 0, 0, 0.1),
    0 24px 60px -12px rgba(0, 0, 0, 0.16);
  --nc-glass-shadow-dark:
    0 0 0 1px rgba(0, 0, 0, 0.3),
    0 8px 24px rgba(0, 0, 0, 0.36),
    0 24px 60px -12px rgba(0, 0, 0, 0.5);
  --nc-glass-shadow: light-dark(var(--nc-glass-shadow-light), var(--nc-glass-shadow-dark));
  --nc-header-bg:    light-dark(rgba(255, 255, 255, 0.5), rgba(37, 37, 37, 0.6));
  --nc-track-bg:     light-dark(rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.5));
  --nc-track-border: light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.08));
  --nc-pill-bg:      light-dark(rgba(255, 255, 255, 0.92), rgba(38, 38, 38, 0.5));
  --nc-pill-border:  light-dark(rgba(0, 0, 0, 0.06), #3e3f40);
  --nc-pill-shadow:  light-dark(0 1px 2px rgba(0, 0, 0, 0.12), 0 7px 26px rgba(0, 0, 0, 0.12));
  --nc-bell-ink:       light-dark(rgba(0, 0, 0, 0.6), rgba(255, 255, 255, 0.7));
  --nc-bell-bg-hover:  light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.06));
  --nc-bell-bg-active: light-dark(rgba(0, 0, 0, 0.07), rgba(255, 255, 255, 0.08));
  --nc-stage:         light-dark(#F7F7F4, #0D0D0D);   /* page/top-bar bg behind the bell (matches the site canvas) */
  /* Ring tracks the stage so it can never drift. Keep in sync with the live
     file's --nc-bell-dot-ring (which points at the site's --background). */
  --nc-bell-dot-ring: var(--nc-stage);
  position: relative;
  display: inline-flex;
  font-family: var(--font-sans, system-ui, sans-serif);
}
.nc-bell-wrap *, .nc-bell-wrap *::before, .nc-bell-wrap *::after { box-sizing: border-box; }

/* ---- Bell ---- */
.nc-bell {
  position: relative;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: 10px;
  border: none;
  background: transparent;
  color: var(--nc-bell-ink);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: background-color 160ms ease, color 160ms ease, transform 125ms ease;
}
.nc-bell:hover { background: var(--nc-bell-bg-hover); color: var(--nc-ink-strong); }
.nc-bell:active { transform: scale(0.94); }
.nc-bell[aria-expanded="true"] { background: var(--nc-bell-bg-active); color: var(--nc-ink-strong); }
.nc-bell:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--nc-stage), 0 0 0 4px var(--nc-focus);
}
.nc-bell-dot {
  position: absolute;
  top: 8px;
  right: 9px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--nc-accent);
  border: 1.5px solid var(--nc-bell-dot-ring); /* matches the stage canvas */
}

/* ---- Popover shell ---- */
.nc-popover {
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  width: min(396px, calc(100vw - 32px));
  display: flex;
  flex-direction: column;
  background: var(--nc-glass-bg);
  backdrop-filter: blur(32px) saturate(1.4);
  -webkit-backdrop-filter: blur(32px) saturate(1.4);
  border: 1px solid var(--nc-glass-border);
  border-radius: 18px;
  box-shadow: var(--nc-glass-shadow);
  overflow: clip;
  z-index: 20;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(-4px) scale(0.96);
  transform-origin: top right;
  transition:
    opacity 180ms ease,
    transform 240ms cubic-bezier(0.22, 1, 0.36, 1),
    visibility 0s linear 240ms;
}
.nc-popover.is-open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateY(0) scale(1);
  transition:
    opacity 180ms ease,
    transform 240ms cubic-bezier(0.22, 1, 0.36, 1),
    visibility 0s linear 0s;
}

/* ---- Header + switch ---- */
.nc-header {
  flex-shrink: 0;
  height: 68px;
  display: flex;
  align-items: center;
  padding: 0 16px;
  background: var(--nc-header-bg);
  border-bottom: 1px solid var(--nc-outline);
}
.nc-switch {
  position: relative;
  display: flex;
  width: 100%;
  padding: 2px;
  gap: 2px;
  border-radius: 10px;
  background: var(--nc-track-bg);
  border: 1px solid var(--nc-track-border);
}
.nc-tab {
  position: relative;
  z-index: 1;
  flex: 1 0 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8px 12px;
  border: none;
  background: transparent;
  border-radius: 8px;
  cursor: pointer;
  color: var(--nc-soft);
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: color 200ms ease;
}
.nc-tab.is-active { color: var(--nc-ink-strong); }
/* Brighten the inactive tab's label on hover to signal clickability (animated
   by the existing color transition). Scoped so the active tab stays strong. */
.nc-tab:not(.is-active):hover { color: light-dark(rgba(0, 0, 0, 0.7), rgba(255, 255, 255, 0.8)); }
.nc-tab:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--nc-focus);
}
.nc-tab-label {
  position: relative;
  z-index: 1;
  font-size: 12px;
  line-height: 16px;
  font-weight: 500;
  letter-spacing: 0.18px;
}
.nc-pill {
  position: absolute;
  top: 2px;
  bottom: 2px;
  left: 2px;
  width: calc((100% - 6px) / 2);
  z-index: 0;
  border-radius: 8px;
  background: var(--nc-pill-bg);
  border: 1px solid var(--nc-pill-border);
  box-shadow: var(--nc-pill-shadow);
  transition: transform 240ms cubic-bezier(0.22, 1, 0.36, 1);
}
.nc-pill--notifications { transform: translateX(0) scaleX(1); transform-origin: left center; }
.nc-pill--whatsnew { transform: translateX(calc(100% + 2px)) scaleX(1); transform-origin: right center; }
.nc-pill--notifications.is-peeking { transform: translateX(0) scaleX(1.0113); }
.nc-pill--whatsnew.is-peeking { transform: translateX(calc(100% + 2px)) scaleX(1.0113); }

/* ---- Scroll body ---- */
.nc-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* Clip the transient horizontal overflow from the tab-switch slide (panels
     translate x: ±8px). Without this, overflow-y:auto forces overflow-x to
     compute to auto, flashing a horizontal scrollbar mid-transition. */
  overflow-x: clip;
  overscroll-behavior: contain;
  max-height: min(60vh, 468px);
  /* Firefox: thin recolored bar, transparent track. */
  scrollbar-width: thin;
  scrollbar-color: var(--nc-scroll-thumb) transparent;
}
.nc-body > *:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--nc-focus); border-radius: 12px; }
/* Chromium/Safari: hide the track, keep a slim recolored thumb. Styling
   ::-webkit-scrollbar opts out of the macOS overlay scrollbar and reserves a
   ~6px gutter (no pure-CSS overlay exists) — an accepted tradeoff for a
   persistent, recolored pill. Matches the site-wide scrollbar (see global.css):
   6px pill, faint-always-visible, darkening on container hover. */
.nc-body::-webkit-scrollbar { width: 6px; }
.nc-body::-webkit-scrollbar-track { background: transparent; }
.nc-body::-webkit-scrollbar-thumb { background: var(--nc-scroll-thumb); border-radius: 100px; }
.nc-body:hover::-webkit-scrollbar-thumb { background: var(--nc-scroll-thumb-hover); }

.nc-panel {
  --nc-panel-offset: 8px;
  animation: nc-panel-enter 220ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
}
.nc-panel.is-backward { --nc-panel-offset: -8px; }
@keyframes nc-panel-enter {
  from { opacity: 0; transform: translateX(var(--nc-panel-offset)); }
  to { opacity: 1; transform: translateX(0); }
}

/* ---- Notifications list ---- */
/* Uniform 6px padding so each row's hover highlight sits an even 6px from the
   list edges on every side (first row's content lands 18px below the header). */
.nc-list { list-style: none; margin: 0; display: flex; flex-direction: column; gap: 2px; padding: 6px; }
.nc-row {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 12px;
  border-radius: 12px;
  transition: background-color 160ms ease;
}
.nc-row:hover { background: var(--nc-hover); }
.nc-avatar { flex-shrink: 0; width: 32px; height: 32px; }
.nc-avatar-logo { width: 32px; height: 32px; border-radius: 50%; display: block; }
.nc-avatar-glyph {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 999px;
  background: var(--nc-hover);
  color: var(--nc-title);
}
.nc-row-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; padding-top: 1px; }
.nc-row-msg { margin: 0; font-size: 14px; line-height: 20px; letter-spacing: 0.14px; color: var(--nc-muted); }
.nc-row-time { margin: 0; font-size: 12px; line-height: 16px; letter-spacing: 0.18px; color: var(--nc-soft); }

/* ---- What's new feed (spacing per the Paper artboard) ---- */
/* Feed has no gap/padding and dividers no margin — the breathing room around
   each divider (and around the hover cards) comes from the item padding (hero
   18px, rows 16px); the first/last item's hover card lands an even 6px in. */
.nc-feed { display: flex; flex-direction: column; gap: 0; padding: 0; }
.nc-divider { height: 1px; width: 100%; background: var(--nc-divider); }
.nc-item { display: flex; flex-direction: column; position: relative; isolation: isolate; }
/* Hover card — mirrors the notifications highlight. The ::before sits inside the
   item's padding (~12px around the content) and floats ~6px off the dividers;
   only the background fades in, so the resting layout is untouched. */
.nc-item::before {
  content: "";
  position: absolute;
  inset: 6px;
  border-radius: 12px;
  background-color: transparent;
  transition: background-color 160ms ease;
  z-index: -1;
}
.nc-item:hover::before { background-color: var(--nc-hover); }
.nc-item--hero { gap: 12px; padding: 18px; }
.nc-item--row { padding: 16px 18px; }
.nc-item-row { display: flex; gap: 16px; align-items: flex-start; }
.nc-item-content { display: flex; flex-direction: column; gap: 12px; flex: 1; min-width: 0; }
.nc-item-text { display: flex; flex-direction: column; gap: 4px; }
.nc-item-top { display: flex; align-items: center; gap: 8px; min-width: 0; }
.nc-blip { flex-shrink: 0; width: 8px; height: 8px; border-radius: 50%; background: var(--nc-accent); }
/* Title truncates to a single line; the trailing arrow keeps its in-flow slot
   (flex-shrink: 0 + the 8px gap) so the ellipsis lands before it and the
   hover-reveal causes no reflow. */
.nc-item-title { margin: 0; font-size: 14px; line-height: 20px; font-weight: 700; color: var(--nc-title); flex: 0 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Hover affordance: a right-arrow that appears after the title, signalling
   the item links somewhere. Decorative (aria-hidden). */
.nc-item-arrow { flex-shrink: 0; color: var(--nc-title); opacity: 0; }
.nc-item:hover .nc-item-arrow { opacity: 1; }
.nc-item-body { margin: 0; font-size: 14px; line-height: 20px; letter-spacing: 0.14px; color: var(--nc-muted); }
.nc-clamp { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.nc-item-date { margin: 0; font-size: 12px; line-height: 16px; letter-spacing: 0.18px; color: var(--nc-soft); }
.nc-hero-img {
  width: 100%;
  aspect-ratio: 360 / 182;
  height: auto;
  object-fit: cover;
  border-radius: 8px;
  border: 1px solid var(--nc-hairline);
}
.nc-thumb {
  flex-shrink: 0;
  width: 128px;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: 12px;
  border: 1px solid var(--nc-hairline);
}
@media (max-width: 420px) { .nc-thumb { width: 104px; } }

@media (prefers-reduced-motion: reduce) {
  .nc-bell-wrap *, .nc-bell-wrap *::before, .nc-bell-wrap *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
  .nc-popover { transition-delay: 0ms !important; }
}
`;
Notification center