// PHYSIO SAMOS — admin console: custom pickers on window.PSA — Select, DatePicker, DateTimePicker,
// the popover layer they share, and PSA.athens, the console's only reader of wall-clock time.
//
// No native dropdown, suggestion list or date/time input anywhere in the console (owner, 2026-09-14).
// Measured facts this file rests on (Chrome 153 and Gecko, probes in the pickers spec):
//  - A body portal is inert while a showModal() <dialog> is open, so the layer is hosted inside the
//    nearest open <dialog>, else <body>, and shown as popover="manual" (top layer, clickable there).
//  - A press on a popover's ::backdrop falls through to <body>, so the bottom sheet has a real scrim.
//  - Greek time formats need hourCycle "h23" (the default is "09:00 π.μ."), and a nominative month
//    comes from formatToParts({ month: "long", year: "numeric" }), never from { month: "long" } alone.
// One function scope, like every admin file: nothing is declared at the top level.
(() => {
  const { useState, useEffect, useLayoutEffect, useRef, useId, useMemo, useCallback } = React;
  const PSA = (window.PSA = window.PSA || {});
  const Icon = (props) => <PSA.Icon {...props} />;

  // ---------- time: Europe/Athens, whatever zone the device is in ----------
  // Instants are epoch ms (UTC). A civil date is "YYYY-MM-DD". Date#getHours and friends are banned
  // in the console: they read the device's zone, and a writer abroad would schedule the wrong hour.
  const TZ = "Europe/Athens";
  const pad2 = (n) => String(n).padStart(2, "0");
  const partsFmt = new Intl.DateTimeFormat("en-US", { timeZone: TZ, hourCycle: "h23", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" });
  const parts = (ms) => {
    const o = {};
    for (const p of partsFmt.formatToParts(new Date(ms))) if (p.type !== "literal") o[p.type] = Number(p.value);
    return { y: o.year, m: o.month, d: o.day, h: o.hour, min: o.minute, s: o.second };
  };
  const offsetAt = (ms) => { const p = parts(ms); return Date.UTC(p.y, p.m - 1, p.d, p.h, p.min, p.s) - Math.floor(ms / 1000) * 1000; };
  // Wall time -> instant. `shifted` marks a time the spring-forward gap skips (moved forward by the
  // gap); `ambiguous` marks October's repeated hour, where the earlier instant wins.
  const toMs = ({ y, m, d, h = 0, min = 0 }) => {
    const wall = Date.UTC(y, m - 1, d, h, min);
    const o1 = offsetAt(wall - 3 * 3600e3), o2 = offsetAt(wall + 3 * 3600e3);
    const exact = [...new Set([o1, o2])].map((o) => wall - o).sort((a, b) => a - b)
      .filter((t) => { const p = parts(t); return p.y === y && p.m === m && p.d === d && p.h === h && p.min === min; });
    if (exact.length) return { ms: exact[0], shifted: false, ambiguous: exact.length > 1 };
    return { ms: wall - Math.min(o1, o2), shifted: true, ambiguous: false };
  };
  const key = (y, m, d) => `${String(y).padStart(4, "0")}-${pad2(m)}-${pad2(d)}`;
  const parseKey = (k) => {
    const r = /^(\d{4})-(\d{2})-(\d{2})$/.exec(k || "");
    if (!r) return null;
    const y = +r[1], m = +r[2], d = +r[3], t = new Date(Date.UTC(y, m - 1, d));
    return t.getUTCFullYear() === y && t.getUTCMonth() === m - 1 && t.getUTCDate() === d ? { y, m, d } : null;
  };
  const fromUTC = (t) => key(t.getUTCFullYear(), t.getUTCMonth() + 1, t.getUTCDate());
  const addDays = (k, n) => { const { y, m, d } = parseKey(k); return fromUTC(new Date(Date.UTC(y, m - 1, d + n))); };
  const daysInMonth = (y, m) => new Date(Date.UTC(y, m, 0)).getUTCDate();
  const addMonths = (k, n) => {
    const { y, m, d } = parseKey(k);
    const t = new Date(Date.UTC(y, m - 1 + n, 1));
    const yy = t.getUTCFullYear(), mm = t.getUTCMonth() + 1;
    return key(yy, mm, Math.min(d, daysInMonth(yy, mm)));
  };
  const weekday = (k) => { const { y, m, d } = parseKey(k); return (new Date(Date.UTC(y, m - 1, d)).getUTCDay() + 6) % 7; };
  const monthGrid = (y, m) => { const first = key(y, m, 1); const start = addDays(first, -weekday(first)); return Array.from({ length: 42 }, (_, i) => addDays(start, i)); };
  const dateKey = (ms) => { const p = parts(ms); return key(p.y, p.m, p.d); };
  // Civil dates are formatted at noon UTC in the UTC zone, so no zone can move them to another day.
  const civil = (o) => new Intl.DateTimeFormat("el-GR", { timeZone: "UTC", ...o });
  const noon = (k) => { const { y, m, d } = parseKey(k); return Date.UTC(y, m - 1, d, 12); };
  const F = {
    full: civil({ weekday: "long", day: "numeric", month: "long", year: "numeric" }),
    short: civil({ weekday: "short", day: "numeric", month: "short", year: "numeric" }),
    monthYear: civil({ month: "long", year: "numeric" }),
    wdShort: civil({ weekday: "short" }),
    wdLong: civil({ weekday: "long" }),
    time: new Intl.DateTimeFormat("el-GR", { timeZone: TZ, hourCycle: "h23", hour: "2-digit", minute: "2-digit" }),
  };
  const athens = (PSA.athens = {
    TZ, parts, toMs, key, parseKey, addDays, addMonths, daysInMonth, weekday, monthGrid, dateKey,
    todayKey: (now = Date.now()) => dateKey(now),
    clampKey: (k, min, max) => (min && k < min ? min : max && k > max ? max : k),
    startOfDayMs: (k) => toMs({ ...parseKey(k), h: 0, min: 0 }).ms,
    ceilToStep: (ms, stepMin) => Math.ceil(ms / (stepMin * 60000)) * stepMin * 60000,
    isPast: (ms, graceMs = 60000, now = Date.now()) => !ms || ms <= now + graceMs,
    defaultScheduleMs: (now = Date.now()) => toMs({ ...parseKey(addDays(dateKey(now), 1)), h: 9, min: 0 }).ms,
    deviceMatches: (now = Date.now()) => [0, 182].every((dd) => { const t = now + dd * 864e5; return offsetAt(t) === -new Date(t).getTimezoneOffset() * 60000; }),
    fmt: {
      fullDate: (k) => F.full.format(noon(k)),
      shortDate: (k) => F.short.format(noon(k)),
      monthYear: (y, m) => F.monthYear.format(Date.UTC(y, m - 1, 15, 12)),
      monthName: (y, m) => F.monthYear.formatToParts(Date.UTC(y, m - 1, 15, 12)).find((p) => p.type === "month").value,
      weekdays: Array.from({ length: 7 }, (_, i) => ({ short: F.wdShort.format(Date.UTC(2026, 8, 14 + i, 12)), long: F.wdLong.format(Date.UTC(2026, 8, 14 + i, 12)) })),
      time: (ms) => F.time.format(ms),
      dateTime: (ms) => `${F.short.format(noon(dateKey(ms)))}, ${F.time.format(ms)}`,
    },
  });

  // ---------- shared infrastructure ----------
  // React 18's useId has colons, which break querySelector.
  const useUid = (prefix = "pk") => `${prefix}-${useId().replace(/:/g, "")}`;

  // Typeahead folding: accents off, Greek lower case, final sigma as sigma ("ά" finds "Αγγλικά").
  PSA.foldText = (s) => String(s).normalize("NFD").replace(/\p{M}/gu, "").toLocaleLowerCase("el").replace(/ς/g, "σ");

  // A bottom sheet on a phone, and on a landscape phone too (844x390), on purpose.
  const SHEET_QUERY = "(max-width: 560px), (max-height: 560px)";
  PSA.usePickerMode = () => {
    const read = () => typeof window.matchMedia === "function" && window.matchMedia(SHEET_QUERY).matches;
    const [sheet, setSheet] = useState(read);
    useEffect(() => {
      const mq = window.matchMedia(SHEET_QUERY);
      const on = () => setSheet(mq.matches);
      on();
      mq.addEventListener("change", on);
      return () => mq.removeEventListener("change", on);
    }, []);
    return sheet ? "sheet" : "anchored";
  };

  let locks = 0;
  PSA.lockPageScroll = (on) => {
    locks = Math.max(0, locks + (on ? 1 : -1));
    document.documentElement.classList.toggle("pk-lock", locks > 0);
  };

  PSA.trapTab = (e, panel) => {
    if (e.key !== "Tab" || !panel) return;
    const list = [...panel.querySelectorAll('button:not([disabled]), input:not([disabled]), [tabindex="0"]')].filter((el) => el.getClientRects().length);
    if (!list.length) return;
    const i = list.indexOf(document.activeElement);
    if (e.shiftKey && i <= 0) { e.preventDefault(); list[list.length - 1].focus(); }
    else if (!e.shiftKey && i === list.length - 1) { e.preventDefault(); list[0].focus(); }
  };

  const SUPPORTS_POPOVER = typeof HTMLElement.prototype.showPopover === "function";
  // The layer's host: the open <dialog> around the trigger (a body portal is dead under a modal
  // dialog — measured), else <body>. Created with the DOM API; `display: contents`.
  // A <dialog> that is not open is display:none; a top-layer popover hosted in one hung Chrome 153's
  // renderer outright (measured), so only an open dialog hosts.
  const hostFor = (anchor) => {
    const dialog = anchor && anchor.closest("dialog");
    const scope = dialog && dialog.open ? dialog : document.body;
    let host = [...scope.children].find((c) => c.classList.contains("pk-host"));
    if (!host) { host = document.createElement("div"); host.className = "pk-host"; scope.appendChild(host); }
    return host;
  };

  const GAP = 6, EDGE = 8;
  const place = (root, panel, anchor, { placement, matchWidth }) => {
    if (!root || !panel || !anchor) return;
    const r = anchor.getBoundingClientRect();
    const vw = document.documentElement.clientWidth;
    const vh = window.visualViewport ? window.visualViewport.height : window.innerHeight;
    if (matchWidth) panel.style.minWidth = `${Math.round(r.width)}px`;
    root.style.removeProperty("--pk-max-h");
    const w = panel.offsetWidth, h = panel.offsetHeight;
    const below = vh - r.bottom - GAP - EDGE, above = r.top - GAP - EDGE;
    const side = h <= below || below >= above ? "bottom" : "top";
    const room = Math.max(160, side === "bottom" ? below : above);
    const shown = Math.min(h, room);
    let left = placement === "bottom-end" ? r.right - w : r.left;
    left = Math.max(EDGE, Math.min(left, vw - w - EDGE));
    const top = side === "bottom" ? r.bottom + GAP : r.top - GAP - shown;
    root.style.left = `${Math.round(left)}px`;
    root.style.top = `${Math.round(Math.max(EDGE, top))}px`;
    root.style.setProperty("--pk-max-h", `${Math.floor(room)}px`);
    root.dataset.side = side;
  };

  // Where focus goes after the layer is dismissed without a commit (§2.4 of the spec).
  const focusAfterDismiss = (reason, anchor, focusWasInside) => {
    if (!anchor) return;
    if (reason === "outside") {
      requestAnimationFrame(() => { const a = document.activeElement; if (!a || a === document.body) anchor.focus({ preventScroll: true }); });
    } else if (reason === "anchor-hidden") {
      if (focusWasInside) anchor.focus({ preventScroll: true });
    } else {
      anchor.focus({ preventScroll: true });
    }
  };

  // ---------- the layer ----------
  const Popover = ({
    anchorRef, mode = "anchored", placement = "bottom-start", matchWidth = false, onDismiss,
    panelId, panelRole, ariaModal, ariaLabel, ariaLabelledBy, sheetTitle, panelClassName = "", panelRef, onPanelKeyDown, children,
  }) => {
    const [host] = useState(() => hostFor(anchorRef.current));
    const ownId = useUid("pk-panel");
    const pid = panelId || ownId;
    const rootRef = useRef(null);
    const panelEl = useRef(null);
    const dismiss = useRef(onDismiss);
    dismiss.current = onDismiss;
    const opts = useRef({ placement, matchWidth });
    opts.current = { placement, matchWidth };
    const setPanel = (el) => {
      panelEl.current = el;
      if (typeof panelRef === "function") panelRef(el);
      else if (panelRef) panelRef.current = el;
    };
    const doPlace = () => place(rootRef.current, panelEl.current, anchorRef.current, opts.current);

    useLayoutEffect(() => {
      const root = rootRef.current;
      if (SUPPORTS_POPOVER) { try { root.showPopover(); } catch (e) {} }
      return () => { try { if (SUPPORTS_POPOVER && root.matches(":popover-open")) root.hidePopover(); } catch (e) {} };
    }, []);

    useLayoutEffect(() => {
      const root = rootRef.current, panel = panelEl.current;
      if (mode === "anchored") { doPlace(); return undefined; }
      // A sheet is placed by CSS; drop whatever the anchored mode wrote.
      for (const p of ["left", "top", "--pk-max-h"]) root.style.removeProperty(p);
      delete root.dataset.side;
      if (panel) panel.style.removeProperty("min-width");
      PSA.lockPageScroll(true);
      return () => PSA.lockPageScroll(false);
    }, [mode]);

    useEffect(() => {
      const anchor = anchorRef.current;
      const inPanel = (t) => panelEl.current && t instanceof Node && panelEl.current.contains(t);
      // A press on the scrim is left to the scrim's own click, so the click lands on the scrim and
      // never on whatever sits under it once the sheet is gone.
      const onDown = (e) => {
        const t = e.target;
        if (inPanel(t) || (anchor && t instanceof Node && anchor.contains(t))) return;
        if (t instanceof Element && t.closest(".pk-scrim")) return;
        if (dismiss.current) dismiss.current("outside");
      };
      document.addEventListener("pointerdown", onDown, true);
      if (mode !== "anchored") return () => document.removeEventListener("pointerdown", onDown, true);

      let raf = 0;
      const schedule = () => { if (!raf) raf = requestAnimationFrame(() => { raf = 0; doPlace(); }); };
      const onScroll = (e) => { if (!inPanel(e.target)) schedule(); };
      window.addEventListener("scroll", onScroll, { capture: true, passive: true });
      window.addEventListener("resize", schedule);
      const vv = window.visualViewport;
      if (vv) vv.addEventListener("resize", schedule);
      let ro = null;
      if (typeof ResizeObserver === "function") {
        ro = new ResizeObserver(schedule);
        if (panelEl.current) ro.observe(panelEl.current);
        if (anchor) ro.observe(anchor);
      }
      let io = null;
      if (typeof IntersectionObserver === "function" && anchor) {
        let first = true;
        io = new IntersectionObserver((entries) => {
          const entry = entries[entries.length - 1];
          if (first) { first = false; if (entry.isIntersecting) return; }
          if (!entry.isIntersecting && dismiss.current) dismiss.current("anchor-hidden");
        }, { threshold: 0 });
        io.observe(anchor);
      }
      return () => {
        document.removeEventListener("pointerdown", onDown, true);
        window.removeEventListener("scroll", onScroll, { capture: true });
        window.removeEventListener("resize", schedule);
        if (vv) vv.removeEventListener("resize", schedule);
        if (ro) ro.disconnect();
        if (io) io.disconnect();
        if (raf) cancelAnimationFrame(raf);
      };
    }, [mode]);

    const sheet = mode === "sheet";
    const rootClass = `pk-pop${sheet ? " pk-pop--sheet" : ""}${SUPPORTS_POPOVER ? "" : " pk-pop--fallback"}`;
    const panelProps = sheet
      ? { role: "dialog", "aria-modal": "true", "aria-labelledby": `${pid}-title` }
      : { role: panelRole, "aria-modal": ariaModal ? "true" : undefined, "aria-label": ariaLabelledBy ? undefined : ariaLabel, "aria-labelledby": ariaLabelledBy };
    return ReactDOM.createPortal(
      <div ref={rootRef} className={rootClass} popover={SUPPORTS_POPOVER ? "manual" : undefined}>
        {sheet && <div className="pk-scrim" aria-hidden="true" onClick={() => { if (dismiss.current) dismiss.current("scrim"); }} />}
        <div ref={setPanel} id={pid} className={`pk-panel${panelClassName ? ` ${panelClassName}` : ""}${sheet ? " pk-sheet" : ""}`}
             onKeyDown={onPanelKeyDown} {...panelProps}>
          {sheet && (
            <div className="pk-sheet__head">
              <h2 id={`${pid}-title`}>{sheetTitle || ariaLabel || ""}</h2>
              <button type="button" className="btn btn--quiet btn--icon" aria-label="Κλείσιμο"
                      onClick={() => { if (dismiss.current) dismiss.current("close-button"); }}>
                <Icon name="close" />
              </button>
            </div>
          )}
          {children}
        </div>
      </div>,
      host,
    );
  };
  PSA.Popover = Popover;

  // ---------- PSA.Select: WAI-ARIA select-only combobox ----------
  const Avatar = ({ option, size }) => {
    const [broken, setBroken] = useState(false);
    useEffect(() => setBroken(false), [option.avatarUrl]);
    if (broken) return <span className="pk-avatar avatar" aria-hidden="true">{PSA.initials(option.label)}</span>;
    return <img className="pk-avatar" src={option.avatarUrl} alt="" width={size} height={size} loading="lazy" draggable="false" onError={() => setBroken(true)} />;
  };

  const Select = ({
    value, onChange, options = [], placeholder = "Επιλέξτε…", disabled = false, id, labelId, ariaLabel, describedBy,
    invalid = false, sheetTitle, size = "md", className, renderValue, renderOption,
    emptyText = "Δεν υπάρχουν επιλογές.", action, restoreFocus = true,
  }) => {
    const uid = useUid("pk-sel");
    const tid = id || uid;
    const listId = `${tid}-list`;
    const optId = (i) => `${tid}-opt-${i}`;
    const mode = PSA.usePickerMode();
    const [open, setOpen] = useState(false);
    const [active, setActiveState] = useState(-1);
    const [nav, setNav] = useState("keyboard");
    const triggerRef = useRef(null);
    const listRef = useRef(null);
    const panelRef = useRef(null);
    const buffer = useRef("");
    const timer = useRef(null);
    const prevFocus = useRef(null);
    const warned = useRef(false);
    const scrolledFor = useRef(null);

    const selIndex = options.findIndex((o) => o.value === value);
    const selected = selIndex >= 0 ? options[selIndex] : null;
    const count = options.length + (action ? 1 : 0);
    const last = count - 1;
    const setActive = (i, how) => { setActiveState(i); if (how) setNav(how); };

    useEffect(() => {
      if (value != null && !selected && !warned.current) {
        warned.current = true;
        console.warn(`[admin] PSA.Select #${tid}: no option has the value ${JSON.stringify(value)}`);
      }
    });

    const openIndex = () => {
      if (!count) return -1;
      if (selIndex >= 0) return selIndex;
      const first = options.findIndex((o) => !o.disabled);
      return first >= 0 ? first : 0;
    };
    const clearTypeahead = () => { clearTimeout(timer.current); buffer.current = ""; };
    const openAt = (i, how) => { setActive(i, how); setOpen(true); };
    const close = () => { clearTypeahead(); setOpen(false); };
    useEffect(() => () => clearTimeout(timer.current), []);

    // With restoreFocus false the consumer moves focus; if it did not, focus goes back to where it
    // was before the trigger was pressed, never left sitting on the trigger.
    const settleFocus = () => {
      const t = triggerRef.current;
      if (!t) return;
      if (restoreFocus !== false) { t.focus({ preventScroll: true }); return; }
      if (document.activeElement !== t && !(listRef.current && listRef.current.contains(document.activeElement))) return;
      const p = prevFocus.current;
      if (p && p !== t && p.isConnected && p !== document.body) p.focus({ preventScroll: true });
      else t.blur();
    };

    const runAction = () => {
      close();
      settleFocus();
      requestAnimationFrame(() => { if (action && action.onAction) action.onAction(); });
    };
    const commit = (i) => {
      const o = options[i];
      if (!o || o.disabled) return false;
      if (o.value !== value && onChange) onChange(o.value, o);
      return true;
    };
    const choose = (i) => {
      if (action && i === options.length) { runAction(); return; }
      const o = options[i];
      if (!o || o.disabled) return;
      close();
      commit(i);
      settleFocus();
    };

    const onType = (ch, from) => {
      if (!options.length) return;
      clearTimeout(timer.current);
      buffer.current += ch;
      timer.current = setTimeout(() => { buffer.current = ""; }, 500);
      const q = PSA.foldText(buffer.current);
      if (!q) return;
      const same = [...q].every((c) => c === q[0]);
      const needle = same ? q[0] : q;
      const start = same || buffer.current.length === 1 ? from + 1 : Math.max(from, 0);
      for (let k = 0; k < options.length; k++) {
        const i = (((start + k) % options.length) + options.length) % options.length;
        if (PSA.foldText(options[i].label).startsWith(needle)) { setActive(i, "keyboard"); return; }
      }
    };
    const printable = (e) => e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey;

    const onOpenKey = (e) => {
      const k = e.key;
      const handled = () => { e.preventDefault(); setNav("keyboard"); };
      if (k === "Escape") {
        e.preventDefault(); e.stopPropagation();
        close();
        if (mode === "sheet") triggerRef.current && triggerRef.current.focus({ preventScroll: true });
        return;
      }
      if (k === "Tab") {
        if (mode === "sheet") return; // the panel traps it
        if (active >= 0 && active < options.length && !options[active].disabled) commit(active);
        close();
        return;
      }
      if (k === "ArrowUp" && e.altKey) { handled(); choose(active); return; }
      if (k === "ArrowDown") { handled(); setActive(Math.min(active + 1, last)); return; }
      if (k === "ArrowUp") { handled(); setActive(Math.max(active - 1, 0)); return; }
      if (k === "Home") { handled(); setActive(0); return; }
      if (k === "End") { handled(); setActive(last); return; }
      if (k === "PageDown") { handled(); setActive(Math.min(active + 10, last)); return; }
      if (k === "PageUp") { handled(); setActive(Math.max(active - 10, 0)); return; }
      if (k === "Enter") { handled(); choose(active); return; }
      if (k === " ") {
        handled();
        if (buffer.current) onType(" ", active); else choose(active);
        return;
      }
      if (printable(e)) { handled(); onType(k, active); }
    };

    const onTriggerKeyDown = (e) => {
      if (open) { if (mode === "anchored") onOpenKey(e); return; }
      const k = e.key;
      if (k === "ArrowDown" || k === "Enter" || k === " ") { e.preventDefault(); prevFocus.current = null; openAt(openIndex(), "keyboard"); return; }
      if (k === "ArrowUp" || k === "Home") { e.preventDefault(); prevFocus.current = null; openAt(count ? 0 : -1, "keyboard"); return; }
      if (k === "End") { e.preventDefault(); prevFocus.current = null; openAt(last, "keyboard"); return; }
      if (printable(e)) {
        e.preventDefault();
        prevFocus.current = null;
        const from = openIndex();
        openAt(from, "keyboard");
        onType(k, from);
      }
    };
    const onTriggerBlur = () => { if (open && mode === "anchored") close(); };
    const toggle = () => {
      if (open) { close(); return; }
      openAt(openIndex(), "pointer");
    };

    const onPanelKeyDown = (e) => {
      if (mode !== "sheet") return;
      if (e.key === "Escape") {
        e.preventDefault(); e.stopPropagation();
        close();
        triggerRef.current && triggerRef.current.focus({ preventScroll: true });
        return;
      }
      PSA.trapTab(e, panelRef.current);
    };
    const onDismiss = (reason) => {
      const inside = !!(panelRef.current && panelRef.current.contains(document.activeElement));
      close();
      if (mode === "sheet" || reason === "anchor-hidden") focusAfterDismiss(reason, triggerRef.current, inside);
    };

    // On open: centre the active option; a sheet takes DOM focus into its listbox.
    useLayoutEffect(() => {
      if (!open) { scrolledFor.current = null; return; }
      const list = listRef.current;
      if (!list) return;
      if (mode === "sheet" && document.activeElement !== list) list.focus({ preventScroll: true });
      const el = active >= 0 ? document.getElementById(optId(active)) : null;
      if (scrolledFor.current === null) {
        scrolledFor.current = active;
        if (el) list.scrollTop = el.offsetTop - (list.clientHeight - el.offsetHeight) / 2;
        return;
      }
      if (!el || scrolledFor.current === active) return;
      scrolledFor.current = active;
      const top = el.offsetTop, bottom = top + el.offsetHeight;
      if (top < list.scrollTop) list.scrollTop = top - 4;
      else if (bottom > list.scrollTop + list.clientHeight) list.scrollTop = bottom - list.clientHeight + 4;
    }, [open, active, mode]);
    // `data-nav` on the panel decides whether the active row shows the keyboard ring.
    useLayoutEffect(() => { if (open && panelRef.current) panelRef.current.dataset.nav = nav; }, [open, nav, mode]);
    // The mode can change under an open list (a phone rotating): focus follows the model.
    const lastMode = useRef(mode);
    useEffect(() => {
      if (lastMode.current === mode) return;
      lastMode.current = mode;
      if (open && mode === "anchored" && triggerRef.current) triggerRef.current.focus({ preventScroll: true });
    }, [mode, open]);

    const triggerClass = `pk-trigger${size === "sm" ? " pk-trigger--sm" : ""}${invalid ? " is-invalid" : ""}${selected ? "" : " is-placeholder"}${className ? ` ${className}` : ""}`;
    return (
      <>
        <button type="button" ref={triggerRef} id={tid} className={triggerClass}
                role="combobox" aria-haspopup="listbox" aria-expanded={open}
                aria-controls={open ? listId : undefined}
                aria-labelledby={labelId} aria-label={labelId ? undefined : ariaLabel}
                aria-describedby={describedBy} aria-invalid={invalid || undefined}
                aria-activedescendant={open && mode === "anchored" && active >= 0 ? optId(active) : undefined}
                disabled={disabled} onClick={toggle} onKeyDown={onTriggerKeyDown} onBlur={onTriggerBlur}
                onMouseDown={() => { if (!open) prevFocus.current = document.activeElement; }}>
          <span className="pk-trigger__value">
            {selected && selected.avatarUrl ? <Avatar option={selected} size={24} />
              : selected && selected.icon ? <Icon name={selected.icon} size="sm" className="pk-trigger__icon" /> : null}
            <span className="pk-trigger__text">{selected ? (renderValue ? renderValue(selected) : selected.label) : placeholder}</span>
          </span>
          <Icon name="chevronDown" size="sm" className="pk-trigger__chev" />
        </button>
        {open && (
          <Popover anchorRef={triggerRef} mode={mode} matchWidth panelClassName="pk-panel--select" panelRef={panelRef}
                   sheetTitle={sheetTitle || ariaLabel || (labelId && document.getElementById(labelId) ? document.getElementById(labelId).textContent : "")}
                   onPanelKeyDown={onPanelKeyDown} onDismiss={onDismiss}>
              <ul role="listbox" id={listId} ref={listRef} className="pk-listbox"
                  aria-labelledby={labelId} aria-label={labelId ? undefined : ariaLabel}
                  tabIndex={mode === "sheet" ? 0 : -1}
                  aria-activedescendant={mode === "sheet" && active >= 0 ? optId(active) : undefined}
                  onMouseDown={(e) => e.preventDefault()}
                  onKeyDown={mode === "sheet" ? onOpenKey : undefined}>
                {options.map((o, i) => (
                  <li key={String(o.value)} id={optId(i)} role="option"
                      aria-selected={i === active ? "true" : "false"}
                      aria-disabled={o.disabled ? "true" : undefined}
                      className={`pk-option${i === active ? " is-active" : ""}${o.value === value ? " is-selected" : ""}${o.disabled ? " is-disabled" : ""}`}
                      onPointerMove={(e) => { if (e.pointerType === "mouse" && active !== i) setActive(i, "pointer"); }}
                      onClick={() => choose(i)}>
                    {o.avatarUrl ? <Avatar option={o} size={28} /> : o.icon ? <Icon name={o.icon} size="sm" className="pk-option__icon" /> : null}
                    <span className="pk-option__body">
                      {renderOption ? renderOption(o, { active: i === active, selected: o.value === value }) : <>
                        <span className="pk-option__label">{o.label}</span>
                        {o.description && <span className="pk-option__desc">{o.description}</span>}
                      </>}
                    </span>
                    <Icon name="check" size="sm" className="pk-option__check" />
                  </li>
                ))}
                {!options.length && <li className="pk-empty" role="presentation">{emptyText}</li>}
                {action && (
                  <li id={optId(options.length)} role="option" aria-selected={active === options.length ? "true" : "false"}
                      className={`pk-option pk-option--action${active === options.length ? " is-active" : ""}`}
                      onPointerMove={(e) => { if (e.pointerType === "mouse" && active !== options.length) setActive(options.length, "pointer"); }}
                      onClick={runAction}>
                    {action.icon && <Icon name={action.icon} size="sm" className="pk-option__icon" />}
                    <span className="pk-option__body"><span className="pk-option__label">{action.label}</span></span>
                  </li>
                )}
              </ul>
          </Popover>
        )}
      </>
    );
  };
  PSA.Select = Select;

  // ---------- calendar (internal): WAI-ARIA date picker dialog grid ----------
  const monthOf = (k) => k.slice(0, 7);
  const monthOutFor = (yy, i, minKey, maxKey) => {
    const mk = `${yy}-${pad2(i + 1)}`;
    return !!((minKey && mk < monthOf(minKey)) || (maxKey && mk > monthOf(maxKey)));
  };
  const prevOffFor = (view, k, yy, minKey) => {
    if (!minKey) return false;
    if (view === "days") { const { y, m } = parseKey(k); return addDays(key(y, m, 1), -1) < minKey; }
    return `${yy - 1}-12` < monthOf(minKey);
  };
  const nextOffFor = (view, k, yy, maxKey) => {
    if (!maxKey) return false;
    if (view === "days") { const { y, m } = parseKey(k); return addDays(key(y, m, daysInMonth(y, m)), 1) > maxKey; }
    return `${yy + 1}-01` > monthOf(maxKey);
  };

  const CalendarPanel = ({ selectedKey, rangeStart, rangeEnd, initialFocusKey, minKey, maxKey, isDisabled, todayKey, onPick, apiRef }) => {
    const titleId = useUid("pk-cal-title");
    const clamp = (k) => athens.clampKey(k, minKey, maxKey);
    const [view, setView] = useState("days");
    const [focusKey, setFocusKey] = useState(() => clamp(initialFocusKey && parseKey(initialFocusKey) ? initialFocusKey : todayKey));
    const [yearShown, setYearShown] = useState(() => parseKey(focusKey || todayKey).y);
    const [monthFocus, setMonthFocus] = useState(0);
    const [liveText, setLiveText] = useState("");
    const rootRef = useRef(null);
    const pending = useRef(null);

    const { y, m } = parseKey(focusKey);
    const monthYear = athens.fmt.monthYear(y, m);
    const blocked = (k) => !!((minKey && k < minKey) || (maxKey && k > maxKey) || (isDisabled && isDisabled(k)));
    const rows = useMemo(() => { const g = monthGrid(y, m); return Array.from({ length: 6 }, (_, r) => g.slice(r * 7, r * 7 + 7)); }, [y, m]);
    const monthOut = (i) => monthOutFor(yearShown, i, minKey, maxKey);
    const prevDisabled = prevOffFor(view, focusKey, yearShown, minKey);
    const nextDisabled = nextOffFor(view, focusKey, yearShown, maxKey);

    // Focus follows keyboard moves; buttons that move the month keep their own focus.
    useLayoutEffect(() => {
      const p = pending.current;
      if (!p || !rootRef.current) return;
      pending.current = null;
      const root = rootRef.current;
      const el = p === "cell" ? root.querySelector(`td[data-date="${focusKey}"]`)
        : p === "month" ? root.querySelector(`.pk-month[data-i="${monthFocus}"]`)
        : root.querySelector(".pk-cal__title");
      if (el) el.focus({ preventScroll: true });
    });
    // Initial focus after the layer is shown (a passive effect runs after the popover's layout effect).
    useEffect(() => {
      const el = rootRef.current && rootRef.current.querySelector(`td[data-date="${focusKey}"]`);
      if (el) el.focus({ preventScroll: true });
    }, []);
    const seenMonth = useRef(monthYear);
    useEffect(() => { if (seenMonth.current !== monthYear) { seenMonth.current = monthYear; setLiveText(monthYear); } }, [monthYear]);
    const seenYear = useRef(yearShown);
    useEffect(() => { if (seenYear.current !== yearShown) { seenYear.current = yearShown; if (view === "months") setLiveText(String(yearShown)); } }, [yearShown, view]);
    // A draft that moves day on its own (a time bump past midnight) takes the grid with it.
    const seenSel = useRef(selectedKey);
    useEffect(() => {
      if (seenSel.current === selectedKey) return;
      seenSel.current = selectedKey;
      if (selectedKey && parseKey(selectedKey) && selectedKey !== focusKey) setFocusKey(clamp(selectedKey));
    }, [selectedKey]);
    useLayoutEffect(() => {
      if (!apiRef) return;
      apiRef.current = { view, backToDays: () => { pending.current = "title"; setView("days"); } };
    });

    const moveTo = (k) => {
      const next = clamp(k);
      if (next === focusKey) return;
      pending.current = "cell";
      setFocusKey(next);
    };
    const onGridKeyDown = (e) => {
      let next;
      switch (e.key) {
        case "ArrowRight": next = addDays(focusKey, 1); break;
        case "ArrowLeft": next = addDays(focusKey, -1); break;
        case "ArrowDown": next = addDays(focusKey, 7); break;
        case "ArrowUp": next = addDays(focusKey, -7); break;
        case "Home": next = addDays(focusKey, -weekday(focusKey)); break;
        case "End": next = addDays(focusKey, 6 - weekday(focusKey)); break;
        case "PageDown": next = addMonths(focusKey, e.shiftKey ? 12 : 1); break;
        case "PageUp": next = addMonths(focusKey, e.shiftKey ? -12 : -1); break;
        case "Enter":
        case " ":
          if (e.metaKey || e.ctrlKey) return;
          e.preventDefault();
          if (!blocked(focusKey)) onPick(focusKey, { via: "keyboard" });
          return;
        default: return;
      }
      e.preventDefault();
      moveTo(next);
    };
    // A click focuses the cell (tabindex -1), so the roving focus follows it; a blocked day in view
    // takes focus but is never picked. A day from another month switches the grid and keeps focus.
    const pickPointer = (k) => {
      const sameMonth = monthOf(k) === monthOf(focusKey);
      if (blocked(k)) { if (sameMonth && k !== focusKey) setFocusKey(k); return; }
      if (k !== focusKey) { if (!sameMonth) pending.current = "cell"; setFocusKey(k); }
      onPick(k, { via: "pointer" });
    };

    const nearestEnabled = (yy, from) => {
      for (let dist = 0; dist < 12; dist++) {
        for (const i of [from - dist, from + dist]) if (i >= 0 && i < 12 && !monthOutFor(yy, i, minKey, maxKey)) return i;
      }
      return from;
    };
    const changeYear = (dir) => {
      const ny = yearShown + dir;
      if (dir < 0 ? prevOffFor("months", focusKey, yearShown, minKey) : nextOffFor("months", focusKey, yearShown, maxKey)) return false;
      setYearShown(ny);
      setMonthFocus((mf) => nearestEnabled(ny, mf));
      return true;
    };
    const prev = () => {
      if (view === "days") {
        const nk = clamp(addMonths(focusKey, -1));
        if (prevOffFor("days", nk, yearShown, minKey)) pending.current = "title";
        setFocusKey(nk);
      } else if (changeYear(-1) && prevOffFor("months", focusKey, yearShown - 1, minKey)) {
        pending.current = "title";
      }
    };
    const next = () => {
      if (view === "days") {
        const nk = clamp(addMonths(focusKey, 1));
        if (nextOffFor("days", nk, yearShown, maxKey)) pending.current = "title";
        setFocusKey(nk);
      } else if (changeYear(1) && nextOffFor("months", focusKey, yearShown + 1, maxKey)) {
        pending.current = "title";
      }
    };
    const toggleView = () => {
      if (view === "months") { setView("days"); return; }
      const sel = selectedKey && parseKey(selectedKey);
      const from = sel && sel.y === y ? sel.m - 1 : m - 1;
      setYearShown(y);
      setMonthFocus(nearestEnabled(y, from));
      pending.current = "month";
      setView("months");
    };
    const chooseMonth = (i) => {
      if (monthOut(i)) return;
      const d = parseKey(focusKey).d;
      pending.current = "cell";
      setFocusKey(clamp(key(yearShown, i + 1, Math.min(d, daysInMonth(yearShown, i + 1)))));
      setView("days");
    };
    const onMonthsKeyDown = (e) => {
      const enabled = (i) => i >= 0 && i < 12 && !monthOut(i);
      const walk = (dir) => { for (let i = monthFocus + dir; i >= 0 && i < 12; i += dir) if (enabled(i)) return i; return monthFocus; };
      let to = null;
      switch (e.key) {
        case "ArrowRight": to = walk(1); break;
        case "ArrowLeft": to = walk(-1); break;
        case "ArrowDown": to = enabled(monthFocus + 3) ? monthFocus + 3 : monthFocus; break;
        case "ArrowUp": to = enabled(monthFocus - 3) ? monthFocus - 3 : monthFocus; break;
        case "Home": to = [...Array(12).keys()].find(enabled); break;
        case "End": to = [...Array(12).keys()].reverse().find(enabled); break;
        case "PageUp": case "PageDown":
          e.preventDefault();
          if (changeYear(e.key === "PageUp" ? -1 : 1)) pending.current = "month";
          return;
        case "Enter": case " ": e.preventDefault(); chooseMonth(monthFocus); return;
        case "Escape": e.preventDefault(); e.stopPropagation(); pending.current = "title"; setView("days"); return;
        default: return;
      }
      e.preventDefault();
      if (to == null) return;
      pending.current = "month";
      setMonthFocus(to);
      if (to === monthFocus && rootRef.current) { const el = rootRef.current.querySelector(`.pk-month[data-i="${to}"]`); if (el) el.focus({ preventScroll: true }); }
    };

    const todayMonth = todayKey ? monthOf(todayKey) : "";
    const selMonth = selectedKey ? monthOf(selectedKey) : "";
    return (
      <div className="pk-cal" ref={rootRef}>
        <div className="pk-cal__head">
          <button type="button" className="pk-iconbtn" aria-label={view === "days" ? "Προηγούμενος μήνας" : "Προηγούμενο έτος"} disabled={prevDisabled} onClick={prev}><Icon name="chevronLeft" /></button>
          <button type="button" className="pk-cal__title" aria-expanded={view === "months"}
                  aria-label={view === "days" ? `Αλλαγή μήνα και έτους, ${monthYear}` : `Επιστροφή στις ημέρες, ${yearShown}`} onClick={toggleView}>
            <span id={titleId}>{view === "days" ? monthYear : yearShown}</span><Icon name="chevronDown" size="sm" />
          </button>
          <button type="button" className="pk-iconbtn" aria-label={view === "days" ? "Επόμενος μήνας" : "Επόμενο έτος"} disabled={nextDisabled} onClick={next}><Icon name="chevronRight" /></button>
        </div>
        <div className="sr-only" aria-live="polite">{liveText}</div>
        {view === "days" ? (
          <table role="grid" className="pk-grid" aria-labelledby={titleId} onKeyDown={onGridKeyDown}>
            <thead><tr>{athens.fmt.weekdays.map((w) => <th key={w.short} scope="col" abbr={w.long}>{w.short}</th>)}</tr></thead>
            <tbody>{rows.map((week, r) => (
              <tr key={r}>{week.map((k) => (
                <td key={k} data-date={k} tabIndex={k === focusKey ? 0 : -1}
                    aria-selected={k === selectedKey || (rangeStart && k >= rangeStart && k <= (rangeEnd || rangeStart)) ? "true" : undefined}
                    aria-current={k === todayKey ? "date" : undefined}
                    aria-disabled={blocked(k) ? "true" : undefined}
                    aria-label={athens.fmt.fullDate(k)}
                    className={`pk-day${rangeStart && k > rangeStart && k < rangeEnd ? " is-in-range" : ""}${monthOf(k) === monthOf(focusKey) ? "" : " is-outside"}`}
                    onClick={() => pickPointer(k)}>
                  <span aria-hidden="true">{Number(k.slice(8))}</span>
                </td>
              ))}</tr>
            ))}</tbody>
          </table>
        ) : (
          <div className="pk-months" role="group" aria-label={`Μήνες του ${yearShown}`} onKeyDown={onMonthsKeyDown}>
            {Array.from({ length: 12 }, (_, i) => {
              const mk = `${yearShown}-${pad2(i + 1)}`;
              return (
                <button key={i} type="button" data-i={i} className={`pk-month${mk === todayMonth ? " is-current" : ""}`}
                        tabIndex={i === monthFocus ? 0 : -1} aria-pressed={mk === selMonth} disabled={monthOut(i)}
                        onClick={() => chooseMonth(i)}>{athens.fmt.monthName(yearShown, i + 1)}</button>
              );
            })}
          </div>
        )}
      </div>
    );
  };

  // ---------- PSA.DatePicker ----------
  const DatePicker = ({
    value, onChange, min, max, isDateDisabled, placeholder = "Επιλέξτε ημερομηνία", disabled = false,
    id, labelId, ariaLabel, describedBy, invalid = false, sheetTitle, clearable = false,
  }) => {
    const uid = useUid("pk-date");
    const tid = id || uid;
    const panelId = `${tid}-panel`;
    const mode = PSA.usePickerMode();
    const [open, setOpen] = useState(false);
    const triggerRef = useRef(null);
    const panelRef = useRef(null);
    const calApi = useRef(null);
    const todayKey = athens.todayKey();
    const current = value && parseKey(value) ? value : null;
    const valueText = current ? athens.fmt.fullDate(current) : placeholder;
    const focusTrigger = () => { if (triggerRef.current) triggerRef.current.focus({ preventScroll: true }); };
    const commit = (k) => {
      if (k !== value && onChange) onChange(k);
      setOpen(false);
      focusTrigger();
    };
    const todayBlocked = !!((min && todayKey < min) || (max && todayKey > max) || (isDateDisabled && isDateDisabled(todayKey)));
    const onPanelKeyDown = (e) => {
      if (e.key === "Escape") {
        e.preventDefault(); e.stopPropagation();
        const api = calApi.current;
        if (api && api.view === "months") api.backToDays();
        else { setOpen(false); focusTrigger(); }
        return;
      }
      PSA.trapTab(e, panelRef.current);
    };
    const onDismiss = (reason) => {
      const inside = !!(panelRef.current && panelRef.current.contains(document.activeElement));
      setOpen(false);
      focusAfterDismiss(reason, triggerRef.current, inside);
    };
    return (
      <>
        <button type="button" ref={triggerRef} id={tid}
                className={`pk-trigger${invalid ? " is-invalid" : ""}${current ? "" : " is-placeholder"}`}
                aria-haspopup="dialog" aria-expanded={open} aria-controls={open ? panelId : undefined}
                aria-labelledby={labelId ? `${labelId} ${tid}-value` : undefined}
                aria-label={labelId ? undefined : ariaLabel ? `${ariaLabel} ${valueText}` : undefined}
                aria-describedby={describedBy} aria-invalid={invalid || undefined}
                disabled={disabled} onClick={() => setOpen((o) => !o)}>
          <Icon name="calendar" size="sm" className="pk-trigger__icon" />
          <span className="pk-trigger__text" aria-hidden="true">{current ? athens.fmt.shortDate(current) : placeholder}</span>
          <span className="sr-only" id={`${tid}-value`}>{valueText}</span>
          <Icon name="chevronDown" size="sm" className="pk-trigger__chev" />
        </button>
        {open && (
          <Popover anchorRef={triggerRef} mode={mode} panelId={panelId} panelRole="dialog" ariaModal ariaLabel="Επιλογή ημερομηνίας"
                   sheetTitle={sheetTitle || ariaLabel || "Επιλογή ημερομηνίας"} panelClassName="pk-panel--cal" panelRef={panelRef}
                   onPanelKeyDown={onPanelKeyDown} onDismiss={onDismiss}>
            <CalendarPanel selectedKey={current} initialFocusKey={athens.clampKey(current || todayKey, min, max)} minKey={min} maxKey={max}
                           isDisabled={isDateDisabled} todayKey={todayKey} apiRef={calApi} onPick={(k) => commit(k)} />
            {(clearable || !todayBlocked) && (
              <div className="pk-foot">
                {clearable && <button type="button" className="btn btn--quiet btn--sm pk-foot__start" onClick={() => commit(null)}>Καθαρισμός</button>}
                {!todayBlocked && <button type="button" className="btn btn--sm" onClick={() => commit(todayKey)}>Σήμερα</button>}
              </div>
            )}
          </Popover>
        )}
      </>
    );
  };
  PSA.DatePicker = DatePicker;

  // Two date selections form one inclusive range; dismissal cancels a partial selection.
  PSA.DateRangePicker = ({ from, to, onChange, id, ariaLabel = "Διάστημα αλλαγών" }) => {
    const mode = PSA.usePickerMode();
    const [open, setOpen] = useState(false);
    const [start, setStart] = useState(null);
    const triggerRef = useRef(null), panelRef = useRef(null), calApi = useRef(null);
    const panelId = `${id}-panel`;
    const text = from && to ? `${athens.fmt.shortDate(from)} – ${athens.fmt.shortDate(to)}` : "Όλες οι ημερομηνίες";
    const close = () => { setOpen(false); triggerRef.current?.focus({ preventScroll: true }); };
    const pick = k => {
      if (!start) { setStart(k); return; }
      onChange(k < start ? { from: k, to: start } : { from: start, to: k });
      close();
    };
    return <>
      <button type="button" id={id} ref={triggerRef} className="pk-trigger" aria-label={`${ariaLabel}: ${text}`}
              aria-haspopup="dialog" aria-expanded={open} aria-controls={open ? panelId : undefined}
              onClick={() => { setStart(null); setOpen(!open); }}>
        <Icon name="calendar" size="sm" /><span className="pk-trigger__text">{text}</span><Icon name="chevronDown" size="sm" />
      </button>
      {open && <Popover anchorRef={triggerRef} mode={mode} panelId={panelId} panelRole="dialog" ariaModal ariaLabel={ariaLabel}
          sheetTitle={ariaLabel} panelClassName="pk-panel--cal" panelRef={panelRef}
          onDismiss={reason => { setOpen(false); focusAfterDismiss(reason, triggerRef.current, !!panelRef.current?.contains(document.activeElement)); }}
          onPanelKeyDown={e => {
            if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); if (calApi.current?.view === "months") calApi.current.backToDays(); else close(); }
            else PSA.trapTab(e, panelRef.current);
          }}>
        <p className="pk-range-hint" aria-live="polite">{start ? `Από ${athens.fmt.shortDate(start)} · Επιλέξτε την τελευταία ημέρα` : "Επιλέξτε την πρώτη και την τελευταία ημέρα"}</p>
        <CalendarPanel selectedKey={start} rangeStart={start || from} rangeEnd={start || to}
          initialFocusKey={from || athens.todayKey()} todayKey={athens.todayKey()} apiRef={calApi} onPick={pick} />
        <div className="pk-foot">
          <button type="button" className="btn btn--quiet btn--sm pk-foot__start" onClick={() => { onChange({ from: null, to: null }); close(); }}>Καθαρισμός</button>
          <button type="button" className="btn btn--sm" onClick={close}>Ακύρωση</button>
        </div>
      </Popover>}
    </>;
  };

  // ---------- time segment (internal): APG spinbutton on a text input ----------
  const TimeSegment = ({ kind, n, step, inputRef, nextRef, invalid, errorId, onSet, onApply, onCancel }) => {
    const max = kind === "h" ? 23 : 59;
    const buf = useRef(null);
    const bufTimer = useRef(null);
    const hold = useRef({ t: 0, i: 0 });
    const clearBuf = () => { buf.current = null; clearTimeout(bufTimer.current); };
    const keepBuf = (d) => { buf.current = d; clearTimeout(bufTimer.current); bufTimer.current = setTimeout(() => { buf.current = null; }, 1500); };
    const up = (v) => {
      if (kind === "h") return (v + 1) % 24;
      const x = (Math.floor(v / step) + 1) * step;
      return x >= 60 ? 0 : x;
    };
    const down = (v) => {
      if (kind === "h") return (v + 23) % 24;
      const x = Math.ceil(v / step) * step - step;
      return x < 0 ? 60 - step : x;
    };
    const focusNext = () => { if (nextRef && nextRef.current) nextRef.current.focus({ preventScroll: true }); };

    const feed = (ch) => {
      if (ch === ":" || ch === ".") { if (kind === "h") { clearBuf(); focusNext(); } return; }
      if (!/^\d$/.test(ch)) return;
      const d = Number(ch);
      if (kind === "h") {
        if (buf.current == null) {
          onSet(d);
          if (d > 2) { clearBuf(); focusNext(); } else keepBuf(d);
        } else {
          const both = buf.current * 10 + d;
          clearBuf();
          if (both <= 23) { onSet(both); focusNext(); } else feed(ch);
        }
      } else if (buf.current == null) {
        onSet(d);
        if (d > 5) clearBuf(); else keepBuf(d);
      } else {
        const both = buf.current * 10 + d;
        clearBuf();
        onSet(both);
      }
    };
    const feedRef = useRef(feed);
    feedRef.current = feed;
    const onSetRef = useRef(onSet);
    onSetRef.current = onSet;

    // Digits arrive through beforeinput, so every keyboard and IME types the same way.
    useEffect(() => {
      const el = inputRef.current;
      if (!el) return undefined;
      const onBefore = (e) => {
        const t = e.inputType || "";
        if (t.startsWith("insert")) { e.preventDefault(); for (const ch of String(e.data || "")) feedRef.current(ch); }
        else if (t.startsWith("delete")) { e.preventDefault(); buf.current = null; onSetRef.current(0); }
      };
      const onInput = () => {
        const digits = el.value.replace(/\D/g, "").slice(-2);
        if (digits) onSetRef.current(Math.min(max, Number(digits)));
      };
      el.addEventListener("beforeinput", onBefore);
      el.addEventListener("input", onInput);
      return () => { el.removeEventListener("beforeinput", onBefore); el.removeEventListener("input", onInput); };
    }, [kind]);
    useLayoutEffect(() => {
      const el = inputRef.current;
      if (el && document.activeElement === el) { try { el.setSelectionRange(0, 2); } catch (e) {} }
    }, [n]);
    const stopHold = () => { clearTimeout(hold.current.t); clearInterval(hold.current.i); hold.current = { t: 0, i: 0 }; };
    useEffect(() => () => { stopHold(); clearTimeout(bufTimer.current); }, []);

    const onKeyDown = (e) => {
      const k = e.key;
      const set = (v) => { e.preventDefault(); clearBuf(); onSet(v); };
      if (k === "ArrowUp") set(up);
      else if (k === "ArrowDown") set(down);
      else if (k === "PageUp") set((v) => (kind === "h" ? (v + 6) % 24 : (v + 15) % 60));
      else if (k === "PageDown") set((v) => (kind === "h" ? (v + 18) % 24 : (v + 45) % 60));
      else if (k === "Home") set(0);
      else if (k === "End") set(kind === "h" ? 23 : 60 - step);
      else if (k === "Enter") { e.preventDefault(); e.stopPropagation(); clearBuf(); onApply(); }
      else if (k === "Escape") { e.preventDefault(); e.stopPropagation(); clearBuf(); onCancel(); }
    };
    const holdToRepeat = (dir) => ({
      onPointerDown: (e) => {
        if (e.pointerType === "mouse" && e.button !== 0) return;
        e.preventDefault();
        stopHold();
        clearBuf();
        onSet(dir > 0 ? up : down);
        hold.current.t = setTimeout(() => { hold.current.i = setInterval(() => onSetRef.current(dir > 0 ? up : down), 100); }, 400);
      },
      onPointerUp: stopHold,
      onPointerCancel: stopHold,
      onPointerLeave: stopHold,
      onMouseDown: (e) => e.preventDefault(),
      // Activation without a pointer (assistive technology) still steps once.
      onClick: (e) => { if (e.detail === 0) onSet(dir > 0 ? up : down); },
    });
    const label = kind === "h" ? "Ώρες" : "Λεπτά";
    return (
      <div className="pk-spin">
        <button type="button" tabIndex={-1} className="pk-spin__step" aria-label={kind === "h" ? "Μία ώρα αργότερα" : `${step} λεπτά αργότερα`} {...holdToRepeat(1)}>
          <Icon name="chevronUp" size="sm" />
        </button>
        <input ref={inputRef} type="text" inputMode="numeric" autoComplete="off" enterKeyHint="done" maxLength={2}
               className="pk-spin__input" role="spinbutton" aria-label={label}
               aria-valuemin={0} aria-valuemax={max} aria-valuenow={n}
               aria-invalid={invalid ? "true" : undefined} aria-describedby={invalid ? errorId : undefined}
               value={pad2(n)} onChange={() => {}} onFocus={(e) => { try { e.target.setSelectionRange(0, 2); } catch (err) {} }}
               onBlur={clearBuf} onKeyDown={onKeyDown} />
        <button type="button" tabIndex={-1} className="pk-spin__step" aria-label={kind === "h" ? "Μία ώρα νωρίτερα" : `${step} λεπτά νωρίτερα`} {...holdToRepeat(-1)}>
          <Icon name="chevronDown" size="sm" />
        </button>
      </div>
    );
  };

  // ---------- PSA.DateTimePicker ----------
  // Shows and edits Greek time; hands back epoch ms. Nothing reaches onChange before Εφαρμογή.
  const DateTimePicker = ({
    value, onChange, min, max, minuteStep = 5, defaultTime = { h: 9, min: 0 },
    placeholder = "Επιλέξτε ημερομηνία και ώρα", disabled = false, id, labelId, ariaLabel, describedBy, invalid = false, sheetTitle,
  }) => {
    const uid = useUid("pk-dt");
    const tid = id || uid;
    const panelId = `${tid}-panel`;
    const step = Math.max(1, Math.min(30, Math.round(Number(minuteStep)) || 5));
    const mode = PSA.usePickerMode();
    const [open, setOpen] = useState(false);
    const [nowTick, setNowTick] = useState(() => Date.now());
    const [draft, setDraft] = useState(null);
    const [bumpNote, setBumpNote] = useState(null);
    const triggerRef = useRef(null);
    const panelRef = useRef(null);
    const calApi = useRef(null);
    const hoursRef = useRef(null);
    const minutesRef = useRef(null);

    const floorAt = (now) => (min === "now" ? now : typeof min === "number" && Number.isFinite(min) ? min : null);
    const minMs = floorAt(nowTick);
    const maxMs = typeof max === "number" && Number.isFinite(max) ? max : null;
    const hasValue = typeof value === "number" && Number.isFinite(value);
    const zoneMismatch = !athens.deviceMatches();
    const msOf = (d) => toMs({ ...parseKey(d.key), h: d.h, min: d.min });
    const bump = (d, floor) => {
      if (floor == null || d.key !== dateKey(floor) || msOf(d).ms > floor) return { draft: d, note: null };
      const p = parts(athens.ceilToStep(floor + 60000, step));
      return { draft: { key: key(p.y, p.m, p.d), h: p.h, min: p.min }, note: `Η ώρα άλλαξε σε ${pad2(p.h)}:${pad2(p.min)}, την πρώτη διαθέσιμη.` };
    };

    useEffect(() => {
      if (!open || min !== "now") return undefined;
      const t = setInterval(() => setNowTick(Date.now()), 30000);
      return () => clearInterval(t);
    }, [open, min]);

    const focusTrigger = () => { if (triggerRef.current) triggerRef.current.focus({ preventScroll: true }); };
    const openPicker = () => {
      const now = Date.now();
      const floor = floorAt(now);
      setNowTick(now);
      let d, note = null;
      if (hasValue) {
        const p = parts(value);
        d = { key: key(p.y, p.m, p.d), h: p.h, min: p.min };
      } else {
        const b = bump({ key: dateKey(Math.max(now, floor == null ? now : floor)), h: defaultTime.h, min: defaultTime.min }, floor);
        d = b.draft; note = b.note;
      }
      setDraft(d);
      setBumpNote(note);
      setOpen(true);
    };
    const close = () => setOpen(false);
    const cancel = () => { close(); focusTrigger(); };

    const r = draft ? msOf(draft) : null;
    let error = null;
    if (r && minMs != null && r.ms <= minMs) error = `Αυτή η ώρα έχει ήδη περάσει. Διαλέξτε από τις ${athens.fmt.time(athens.ceilToStep(minMs + 60000, step))} και μετά.`;
    else if (r && maxMs != null && r.ms > maxMs) error = "Η ημερομηνία είναι εκτός των επιτρεπτών ορίων.";
    const dstNote = r && r.shifted ? `Εκείνη τη μέρα η ώρα αλλάζει και η ${pad2(draft.h)}:${pad2(draft.min)} δεν υπάρχει· θα οριστεί ${athens.fmt.time(r.ms)}.` : null;
    const note = dstNote || bumpNote;

    const apply = () => {
      if (!draft) return;
      const now = Date.now();
      const floor = floorAt(now);
      if (min === "now") setNowTick(now);
      const res = msOf(draft);
      if ((floor != null && res.ms <= floor) || (maxMs != null && res.ms > maxMs)) return;
      close();
      if (onChange) onChange(res.ms);
      focusTrigger();
    };
    const setField = (field, v) => {
      setDraft((d) => (d ? { ...d, [field]: typeof v === "function" ? v(d[field]) : v } : d));
      setBumpNote(null);
    };
    const pickDay = (k, { via }) => {
      const b = bump({ ...draft, key: k }, minMs);
      setDraft(b.draft);
      setBumpNote(b.note);
      if (via === "keyboard" && hoursRef.current) hoursRef.current.focus({ preventScroll: true });
    };
    const onPanelKeyDown = (e) => {
      if (e.key === "Escape") {
        e.preventDefault(); e.stopPropagation();
        const api = calApi.current;
        if (api && api.view === "months") api.backToDays(); else cancel();
        return;
      }
      if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); apply(); return; }
      PSA.trapTab(e, panelRef.current);
    };
    const onDismiss = (reason) => {
      const inside = !!(panelRef.current && panelRef.current.contains(document.activeElement));
      close();
      focusAfterDismiss(reason, triggerRef.current, inside);
    };

    const valueText = hasValue ? `${athens.fmt.fullDate(dateKey(value))}, ${athens.fmt.time(value)}` : placeholder;
    const zoneId = `${tid}-zone`;
    const described = [describedBy, zoneMismatch ? zoneId : null].filter(Boolean).join(" ") || undefined;
    const errorId = `${panelId}-error`;
    return (
      <>
        <button type="button" ref={triggerRef} id={tid}
                className={`pk-trigger${invalid ? " is-invalid" : ""}${hasValue ? "" : " is-placeholder"}`}
                aria-haspopup="dialog" aria-expanded={open} aria-controls={open ? panelId : undefined}
                aria-labelledby={labelId ? `${labelId} ${tid}-value` : undefined}
                aria-label={labelId ? undefined : ariaLabel ? `${ariaLabel} ${valueText}` : undefined}
                aria-describedby={described} aria-invalid={invalid || undefined}
                disabled={disabled} onClick={() => (open ? close() : openPicker())}>
          <Icon name="calendar" size="sm" className="pk-trigger__icon" />
          <span className="pk-trigger__text" aria-hidden="true">
            {hasValue ? <>{athens.fmt.shortDate(dateKey(value))}, <span className="pk-mono">{athens.fmt.time(value)}</span></> : placeholder}
          </span>
          <span className="sr-only" id={`${tid}-value`}>{valueText}</span>
          {zoneMismatch && <span className="sr-only" id={zoneId}>Οι ώρες είναι ώρα Ελλάδας.</span>}
          <Icon name="chevronDown" size="sm" className="pk-trigger__chev" />
        </button>
        {open && draft && (
          <Popover anchorRef={triggerRef} mode={mode} panelId={panelId} panelRole="dialog" ariaModal ariaLabel="Επιλογή ημερομηνίας και ώρας"
                   sheetTitle={sheetTitle || ariaLabel || "Επιλογή ημερομηνίας και ώρας"} panelClassName="pk-panel--cal" panelRef={panelRef}
                   onPanelKeyDown={onPanelKeyDown} onDismiss={onDismiss}>
            <CalendarPanel selectedKey={draft.key} initialFocusKey={draft.key}
                           minKey={minMs != null ? dateKey(minMs) : undefined} maxKey={maxMs != null ? dateKey(maxMs) : undefined}
                           todayKey={athens.todayKey()} apiRef={calApi} onPick={pickDay} />
            <div className="pk-time" role="group" aria-labelledby={`${panelId}-time`}>
              <div className="pk-time__label"><span id={`${panelId}-time`}>Ώρα</span><span className="pk-time__zone">ώρα Ελλάδας</span></div>
              <TimeSegment kind="h" n={draft.h} step={step} inputRef={hoursRef} nextRef={minutesRef} invalid={!!error} errorId={errorId}
                           onSet={(v) => setField("h", v)} onApply={apply} onCancel={cancel} />
              <span className="pk-time__sep" aria-hidden="true">:</span>
              <TimeSegment kind="min" n={draft.min} step={step} inputRef={minutesRef} invalid={!!error} errorId={errorId}
                           onSet={(v) => setField("min", v)} onApply={apply} onCancel={cancel} />
            </div>
            <p className="pk-summary">{athens.fmt.fullDate(draft.key)}, <span className="pk-mono">{pad2(draft.h)}:{pad2(draft.min)}</span></p>
            {note && <p className="pk-note" aria-live="polite">{note}</p>}
            {error && <p className="pk-error" id={errorId}><Icon name="warning" size="sm" />{error}</p>}
            {zoneMismatch && <p className="pk-note">Η συσκευή σας είναι σε άλλη ζώνη ώρας· οι ώρες εδώ είναι ώρα Ελλάδας.</p>}
            <div className="pk-foot">
              <button type="button" className="btn btn--sm" onClick={cancel}>Άκυρο</button>
              <button type="button" className="btn btn--primary btn--sm" disabled={!!error} onClick={apply}>Εφαρμογή</button>
            </div>
          </Popover>
        )}
      </>
    );
  };
  PSA.DateTimePicker = DateTimePicker;
})();
