/* ELEV8 ATS - R124 record shell: the reusable frame every record screen composes as
   Topbar -> RecordBody( scrollable content, RecordRail ). RecordBody owns the desktop
   three-column contract (gray content canvas, white full-height rail) and the mobile
   rail-to-bottom-sheet behavior; RecordRail owns the resizable Details/Activity panel.
   Adopted on the candidate record first; jobs/clients/contacts/engagements follow. */

/* Wide: content column (gray canvas, own scroll) beside the rail, both full height.
   Narrow: content full-width; the rail renders inside the existing BottomSheet, opened by a
   control the SCREEN owns (railOpen/onRailClose are controlled props so the trigger can live in
   the record's subnav and take focus back on close).

   R138 - "narrow" is a property of THIS ROW, not of the window. A 480px rail inside a 1000px
   window leaves the content column the same 520px a phone would get, and a viewport query
   cannot see that; the CSS half of the R133 repair had also been silently losing the cascade,
   so the collapse never happened at any width. RecordBody now measures its own inline size with
   a ResizeObserver and hands the verdict back through `onCollapse`, because the trigger button
   belongs to the screen (candidate puts it in the tab row, consultant above the tabs) and a
   screen calling useIsMobile() itself is back to asking the viewport. `useIsMobile` seeds the
   first paint and is the fallback where ResizeObserver is missing. */
const RAIL_COLLAPSE_AT = 840; /* canonical phone breakpoint; mirrored by @container e8rec in app.css */

/* The value a screen seeds `railCollapsed` with before RecordBody's observer has measured
   anything. The viewport is the only thing knowable at that instant, and it is wrong on a narrow
   desktop window - the observer corrects it on the first frame. Exported so the breakpoint has
   ONE home: three screens had inlined the 840 next to a constant that already held it. */
function railCollapseSeed() {
  return !!(window.matchMedia && window.matchMedia('(max-width: ' + RAIL_COLLAPSE_AT + 'px)').matches);
}

/* Tells RecordRail HOW it is being rendered - beside the content, or inside the bottom sheet and
   whether that sheet is currently showing. The rail arrives as a PROP, but React context is
   positional in the rendered tree, not lexical in the file - so a provider in RecordBody's return
   still reaches it. It cannot ask useIsMobile() itself: at a 1000px window the sheet is open while
   the viewport is not mobile, which is exactly the case that produced a resize grip inside a
   bottom sheet.

   The value is an OBJECT, not the old bare boolean, because `inSheet` alone was not enough to
   answer the question the ` shortcut has to ask - "can the user see the thing this key moves?".
   In the sheet the rail stays MOUNTED while the sheet is shut, so a boolean-only context left the
   key flipping tabs on a rail that was off-screen. `null` still means standalone (a rail rendered
   outside any RecordBody), which falls back to the viewport. */
const RecordShellCtx = React.createContext(null);

function RecordBody({ rail, railOpen, onRailClose, railLabel = 'Details', onCollapse, children }) {
  const isMobile = useIsMobile();
  const hostRef = React.useRef(null);
  const [narrow, setNarrow] = React.useState(isMobile);
  /* The row's width is set by the app shell, never by its own children, so removing the rail
     cannot feed back into this measurement and oscillate. */
  React.useEffect(() => {
    const el = hostRef.current;
    if (!el || typeof ResizeObserver === 'undefined') { setNarrow(isMobile); return undefined; }
    const ro = new ResizeObserver((entries) => {
      const box = entries[0];
      const w = box.borderBoxSize && box.borderBoxSize.length ? box.borderBoxSize[0].inlineSize : box.contentRect.width;
      setNarrow(w <= RAIL_COLLAPSE_AT);
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, [isMobile]);
  React.useEffect(() => { if (onCollapse) onCollapse(narrow); }, [narrow, onCollapse]);

  /* Identity is stable per branch so the provider does not hand a fresh object to every consumer
     on every render of the record. */
  const wideCtx = React.useMemo(() => ({ inSheet: false, sheetOpen: false }), []);
  const sheetCtx = React.useMemo(() => ({ inSheet: true, sheetOpen: !!railOpen }), [railOpen]);

  if (!narrow) {
    return (
      <RecordShellCtx.Provider value={wideCtx}>
        <div className="e8-recordbody" ref={hostRef}>
          <div className="e8-content e8-rbody-content">{children}</div>
          {rail}
        </div>
      </RecordShellCtx.Provider>
    );
  }
  return (
    <RecordShellCtx.Provider value={sheetCtx}>
      <div className="e8-recordbody is-narrow" ref={hostRef}>
        <div className="e8-content e8-rbody-content">{children}</div>
      </div>
      {/* Two fixes, and both are needed. The `rail ?` guard: a record composing RecordBody purely
          for the content contract used to get an empty titled sheet mounted underneath it,
          reachable by Tab and by any caller that set railOpen.
          And `initial="full"`, not the primitive's default half detent
          (detentY(d) => d === 'full' ? 0 : H() * 0.42). Measured two ways on the candidate record
          at 390x844: the sheet's top sits at y=422 with body scrollHeight === clientHeight, so
          354px of rail is below the fold AND unscrollable; counted by element, 60 of 110 rail
          elements sat there - Contact, Notes, Tasks, Pipeline and Files, 54% of the panel -
          recoverable only by dragging the grab handle. The rail is the record's fact sheet;
          opening it half-read is not progressive disclosure, it is a hidden gesture. Fixing it
          here fixes every record type that adopts the rail, not candidates alone. */}
      {rail ? (
        <BottomSheet open={!!railOpen} onClose={onRailClose} title={railLabel} initial="full">
          <div className="e8-rrail-sheet">{rail}</div>
        </BottomSheet>
      ) : null}    </RecordShellCtx.Provider>
  );
}

/* The resizable white right rail. Width is per-persona workspace state (E8Workspace.railWidth,
   clamped 248-480): local component state carries every pointer move, the preference is
   written ONCE on pointer-up/cancel, and only when the final width actually changed - a
   per-move write would thrash storage and rerender the record on every frame. Pointer capture
   routes move/up to the grip, so there are no window listeners to leak; unmount mid-drag
   restores user-select/cursor so a record swap can never leave the shell stuck resizing. */
function RecordRail({ active = 'details', onTab, tabs, children }) {
  /* `shell` comes from RecordBody, not the viewport - see RecordShellCtx. Standalone use (a rail
     outside a RecordBody) still falls back to the viewport. */
  const shell = React.useContext(RecordShellCtx);
  const viewportMobile = useIsMobile();
  const inSheet = shell == null ? viewportMobile : shell.inSheet;
  const isMobile = inSheet;
  /* Is this rail actually on screen? Beside the content it always is. Inside the sheet it is only
     visible while the sheet is open - and it stays mounted when shut, which is the whole reason
     this has to be asked rather than assumed. Standalone rails have no sheet to be behind. */
  const visible = !inSheet || (shell == null ? true : !!shell.sheetOpen);
  const asideRef = React.useRef(null);
  const persona = workspacePersona();
  const W = window.E8Workspace;
  /* Bounds live in workspace.js (clampRailWidth) - no second copy here to drift. With no
     workspace the rail is genuinely FIXED: an earlier version dropped the bounds and merely
     rounded, which let a drag produce any width at all, including negative. Returning the
     default means resize is inert rather than unbounded. */
  const clamp = W ? W.clampRailWidth : () => 300;
  const storedWidth = W && persona ? W.get(persona.name).railWidth : 300;
  const [width, setWidth] = React.useState(storedWidth);
  const [dragging, setDragging] = React.useState(false);
  const dragRef = React.useRef(null);
  /* Mirror the live width to :root so fixed overlays outside this subtree can dodge the rail. */
  React.useEffect(() => {
    if (isMobile) { document.documentElement.style.removeProperty('--e8-rail-w'); return undefined; }
    document.documentElement.style.setProperty('--e8-rail-w', width + 'px');
    return () => document.documentElement.style.removeProperty('--e8-rail-w');
  }, [width, isMobile]);

  /* Re-sync when the persona switches (each persona persists its own width). */
  React.useEffect(() => { setWidth(storedWidth); }, [persona && persona.name]); // eslint-disable-line

  const finishDrag = React.useCallback(() => {
    const d = dragRef.current;
    if (!d) return;
    dragRef.current = null;
    setDragging(false);
    try { d.el.releasePointerCapture(d.pointerId); } catch (e) {}
    document.body.style.userSelect = d.prevUserSelect;
    document.body.style.cursor = d.prevCursor;
    if (W && persona) {
      const final = clamp(d.lastWidth);
      /* Skip the write when nothing changed - reload-persistence without write noise. */
      if (final !== W.get(persona.name).railWidth) W.setRailWidth(persona.name, final);
    }
  }, [W, persona && persona.name]); // eslint-disable-line

  /* Unmount mid-drag: restore the document, drop capture, write nothing. */
  React.useEffect(() => () => {
    const d = dragRef.current;
    if (!d) return;
    dragRef.current = null;
    try { d.el.releasePointerCapture(d.pointerId); } catch (e) {}
    document.body.style.userSelect = d.prevUserSelect;
    document.body.style.cursor = d.prevCursor;
  }, []);

  const onPointerDown = (e) => {
    if (e.pointerType === 'mouse' && e.button !== 0) return;
    const el = e.currentTarget;
    try { el.setPointerCapture(e.pointerId); } catch (err) {}
    dragRef.current = {
      pointerId: e.pointerId, el,
      startX: e.clientX, startW: width, lastWidth: width,
      prevUserSelect: document.body.style.userSelect,
      prevCursor: document.body.style.cursor,
    };
    document.body.style.userSelect = 'none';
    document.body.style.cursor = 'col-resize';
    setDragging(true);
  };
  const onPointerMove = (e) => {
    const d = dragRef.current;
    if (!d) return;
    /* The rail sits on the right - dragging its left edge leftward widens it. */
    const next = clamp(d.startW + (d.startX - e.clientX));
    d.lastWidth = next;
    setWidth(next);
  };

  const tabDefs = tabs || [{ id: 'details', label: 'Details' }, { id: 'activity', label: 'Activity' }];

  /* Roving tabindex means the previously-selected tab drops to tabIndex -1 the moment `active`
     changes. Without moving focus to the new one, an arrow press leaves focus on a now-
     unfocusable button and the next Tab escapes the strip entirely. Only steal focus when it is
     already inside the tablist, so selecting a tab by click or by backtick from elsewhere does
     not yank the caret out of whatever the user was doing. */
  const tabsRef = React.useRef(null);
  React.useEffect(() => {
    const strip = tabsRef.current;
    if (!strip || !strip.contains(document.activeElement)) return;
    const sel = strip.querySelector('[aria-selected="true"]');
    if (sel && sel !== document.activeElement) sel.focus();
  }, [active]);

  /* ` cycles the rail tabs. It used to hardcode a two-way details<->activity flip, so a third
     tab was unreachable by keyboard and the key jumped straight past it.

     The guard also has to stand down for modals. Checking INPUT/TEXTAREA/contentEditable is not
     enough: a dialog's focus can sit on a button, and the job record alone has six dialogs, so
     backtick would silently flip the rail behind them. Any open [role=dialog] or *-scrim
     suppresses it.

     R143 - that modal guard had the sheet case exactly INVERTED, and it was measured rather than
     reasoned: at 390x844 on #/candidate/c-okafor, ` with the rail sheet OPEN left the tab on
     Details (the sheet is itself a visible [role=dialog], so the guard suppressed the key), and
     ` with the sheet SHUT moved Details -> Activity. The only state in which the shortcut fired
     was the one in which nobody could see what it did. Two corrections: the rail declines the key
     outright when it is not `visible`, and the modal sweep skips any dialog that CONTAINS this
     rail - a sheet wrapped around the thing the key operates is not a modal in front of it. */
  React.useEffect(() => {
    if (!onTab || !visible) return undefined;
    const onKey = (e) => {
      if (e.key !== '`' || e.metaKey || e.ctrlKey || e.altKey) return;
      const t = e.target;
      if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT' || t.isContentEditable)) return;
      /* Only an OPEN modal suppresses the shortcut. This app mounts its sheets and panels
         unconditionally and drives them with inert/aria-hidden, so a bare presence check matches
         five permanently-present dialogs (the AI panel, mobile sheet, create sheet, capture
         overlay and capture sheet) and kills the key outright. */
      const self = asideRef.current;
      const modal = Array.prototype.slice
        .call(document.querySelectorAll('[role="dialog"], .e8-ndoc-scrim, .e8-tkc-scrim, .e8-cap-overlay, .e8-spled-scrim'))
        .some((el) => !el.hasAttribute('inert')
          && el.getAttribute('aria-hidden') !== 'true'
          && (el.offsetWidth > 0 || el.offsetHeight > 0)
          && !(self && el.contains(self)));
      if (modal) return;
      if (!tabDefs.length) return;
      e.preventDefault();
      const i = tabDefs.findIndex((x) => x.id === active);
      onTab(tabDefs[(i + 1 + tabDefs.length) % tabDefs.length].id);
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [active, onTab, visible, tabDefs.map((t) => t.id).join(',')]); // eslint-disable-line
  return (
    <aside ref={asideRef} className={'e8-rrail' + (dragging ? ' is-dragging' : '') + (isMobile ? ' is-sheet' : '')}
      /* --e8-rail-w is published so FIXED overlays (the docked note) can sit clear of the rail
         instead of covering its controls. Set on the element itself and mirrored to :root by the
         effect below, because a fixed panel is not a descendant of this aside. */
      style={isMobile ? undefined : { width }} aria-label="Record details">
      {!isMobile ? (
        <div className="e8-rrail-grip" title="Drag to resize" aria-hidden="true"
          onPointerDown={onPointerDown} onPointerMove={onPointerMove}
          onPointerUp={finishDrag} onPointerCancel={finishDrag}></div>
      ) : null}
      {/* "rail" is our word for it, not the reader's - a screen-reader user has no rail, they
          have a region of the record. */}
      <div className="e8-rrail-tabs" role="tablist" aria-label="Record sections" ref={tabsRef}
        onKeyDown={(e) => {
          if (!onTab || !tabDefs.length || (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight')) return;
          e.preventDefault();
          const i = tabDefs.findIndex((x) => x.id === active);
          const next = e.key === 'ArrowRight' ? i + 1 : i - 1;
          onTab(tabDefs[(next + tabDefs.length) % tabDefs.length].id);
        }}>
        {tabDefs.map((t) => (
          <button key={t.id} type="button" role="tab" aria-selected={active === t.id}
            id={'e8-rrail-tab-' + t.id} aria-controls="e8-rrail-panel"
            /* Roving tabindex: one stop for the whole strip, arrows move within it. */
            tabIndex={active === t.id ? 0 : -1}
            className={'e8-rrail-tab' + (active === t.id ? ' is-on' : '')}
            onClick={() => onTab && onTab(t.id)}>{t.label}</button>
        ))}
        {/* Only claim the shortcut when it is actually installed AND reachable. `viewportMobile`
            rather than `inSheet` on purpose: the sheet is also how a NARROW DESKTOP window renders
            the rail, and there the key is real and worth advertising. On a phone there is no
            backtick within reach, and the chip was rendering at 390px (display block, opacity
            .55) next to a key nobody could press. */}
        {onTab && visible && !viewportMobile
          ? <span className="e8-rrail-tabs-kbd" aria-hidden="true"><span className="e8-kbd">`</span></span> : null}
      </div>
      <div className="e8-rrail-body" id="e8-rrail-panel" role="tabpanel"
        aria-labelledby={'e8-rrail-tab-' + active}>{children}</div>
    </aside>
  );
}

/* The control that opens the rail sheet once RecordBody reports `narrow`. PLACEMENT stays with the
   screen - candidate and job sit it in the tab row, consultant above the tabs - but its look, its
   label and its dialog semantics are shell business and were not shared: candidate and job each
   hand-wrote the same `.e8-rail-trigger` button (two copies of one thing), and consultant rendered
   a `variant="secondary"` DS Button instead, so the same affordance is a quiet bordered chip on two
   records and a filled secondary button on the third.
   Three notes on what this deliberately does NOT do. It takes no ref-and-setTimeout focus dance:
   BottomSheet runs `useDialog`, which captures the opener on the opening render and refocuses it in
   the effect cleanup on close, so all three screens' hand-rolled restores are already redundant -
   and consultant's is a querySelector('button') hack that exists only because a ref on a DS Button
   is a no-op. It does not read `narrow` itself, because the screen already has it from onCollapse.
   And it does not guess the label: `children` defaults to Details but a rail whose first tab is
   something else should say so. */
/* NO OBJECT REST IN THE PARAMETER LIST, and this is a real defect rather than a style note.
   Babel compiles `{ a, b, ...rest }` to `_objectWithoutProperties(_ref, _excluded)` and emits
   `var _excluded = [...]` at the TOP LEVEL of the script. Every `.jsx` here is a separate
   `<script type="text/babel">` but they all share ONE global scope, so each file's first rest
   site declares the same global `var _excluded` and the LAST script loaded wins. `primitives.jsx`
   loads 28 files after this one (HTML order), so at runtime `_excluded` held Text's key list -
   ["size","tone","weight",...] - and this function stripped THOSE names instead of its own.
   Measured, not reasoned: with `RailTrigger` rendered, React logged `Unknown event handler
   property onOpen` and the button's props were
   ["type","className","aria-haspopup","onClick","onOpen","icon","children"].
   The bug was latent only because RailTrigger had no call site until now. Passing the extras
   through an explicit `extra` object cannot collide with anything. */
function RailTrigger({ onOpen, icon = 'info', children = 'Details', extra }) {
  return (
    <button type="button" className="e8-rail-trigger" aria-haspopup="dialog" onClick={onOpen} {...(extra || {})}>
      <span className="material-symbols-outlined" aria-hidden="true">{icon}</span>
      {children}
    </button>
  );
}

/* The rail state EVERY adopted record declares, in the same order, with the same seeds.
   Candidate, job and consultant each hand-wrote these three useStates verbatim; client and
   contact now compose them from here instead of writing a fourth and fifth copy. It is a hook,
   so it obeys the same rule as any other: call it above the record's not-found guard.

   Deliberately no focus dance on close. BottomSheet runs `useDialog`, which captures the opener
   on the opening render and refocuses it in the effect cleanup - the three hand-rolled restores
   in the older screens are already redundant, and one of them (screens-more.jsx:1064) has been
   dead since a ref landed on a function component. */
function useRecordShell(initialRailTab = 'details') {
  const [railTab, setRailTab] = React.useState(initialRailTab);
  const [railOpen, setRailOpen] = React.useState(false);
  const [railCollapsed, setRailCollapsed] = React.useState(railCollapseSeed);
  return { railTab, setRailTab, railOpen, setRailOpen, railCollapsed, setRailCollapsed };
}

/* The record's tab strip and the collapsed rail's stand-in, on one line. Three screens had
   written this row three ways - `.e8-cand-tabrow`, `.e8-jrail-tabrow`, and (consultant) a
   right-aligned Row ABOVE the tabs holding a filled secondary Button rather than the quiet chip
   the other two use. Same row, same job, so it is one component.

   It carries no class of its own on purpose: this stream may not add CSS, and a class name with
   no rule behind it is worse than none. `window.Row` + `.e8-ui-grow` already express exactly
   what `.e8-cand-tabrow` does (`display:flex; align-items:center; gap:12px` with the tabs at
   `flex:1; min-width:0`), through primitives instead of a fourth namespace. */
function RecordTabRow({ collapsed, onOpenRail, railLabel = 'Details', railIcon = 'info', triggerExtra, children }) {
  return (
    <window.Row gap={12}>
      <div className="e8-ui-grow">{children}</div>
      {collapsed && onOpenRail
        ? <RailTrigger onOpen={onOpenRail} icon={railIcon} extra={triggerExtra}>{railLabel}</RailTrigger>
        : null}
    </window.Row>
  );
}

/* Uppercase caption + rows; `action` is an optional right-aligned control. */
function RailSection({ title, count, action, children }) {
  return (
    <div className="e8-rrail-sec">
      {title ? (
        <div className="e8-rrail-sec-head">
          <span className="e8-rrail-sec-title">{title}{count != null && count !== 0 ? <span className="e8-rrail-sec-n tnum">{count}</span> : null}</span>
          {action ? <span className="e8-rrail-sec-act">{action}</span> : null}
        </div>
      ) : null}
      {children}
    </div>
  );
}

/* One label · value row. PRESENTATIONAL ONLY - it owns no store knowledge. The caller passes
   `onCommit(next)` which does the actual write + undo + activity event; `validate(next)`
   returns an error string to block a commit (Enter shows the error and stays editing; blur
   with an invalid value reverts rather than trapping focus). `display` overrides how a value
   renders without changing what is edited; `meta` is quiet right-aligned context. */
/* TYPED EDITORS. `options` turns the editor into a real dropdown instead of a text box you are
   expected to type a code into - job Priority was `validate: '1 urgent - 4 low'`, i.e. a select
   pretending to be a number field. A NATIVE <select> on purpose: keyboard, type-ahead, the mobile
   wheel and screen-reader semantics all come free, where a bespoke combobox has to earn each one.
   Options are ['A','B'] or [{value,label}] - the second because Priority stores 1-4 and shows
   Urgent/High/Medium/Low. `commit()` is untouched: <select>.value is the same API as <input>.value. */
const railOpts = (options) => (options || []).map((o) => (o && typeof o === 'object' ? o : { value: o, label: o }));

function RailFact({ label, value, display, editable, validate, onCommit, meta, placeholder, options }) {
  const [editing, setEditing] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const inputRef = React.useRef(null);
  const raw = value == null ? '' : String(value);
  const commit = (fromBlur) => {
    const next = inputRef.current ? inputRef.current.value.trim() : '';
    const problem = validate ? validate(next) : null;
    if (problem) {
      if (fromBlur) { setEditing(false); setErr(null); return; } /* revert quietly */
      setErr(problem);
      return;
    }
    setEditing(false);
    setErr(null);
    if (onCommit && next !== raw) onCommit(next);
  };
  return (
    <div className="e8-rrail-fact">
      <span className="e8-rrail-fact-k">{label}</span>
      {editing ? (
        <span className="e8-rrail-fact-editwrap">
          {options ? (
            /* Commit on CHANGE: picking from a list is the whole gesture, so requiring Enter
               afterwards would be a second step for no reason. Escape still abandons. */
            <select ref={inputRef} className="e8-rrail-fact-input" autoFocus defaultValue={raw}
              aria-label={label} aria-invalid={err ? true : undefined}
              onChange={() => commit(false)}
              onKeyDown={(e) => { if (e.key === 'Escape') { e.preventDefault(); setEditing(false); setErr(null); } }}
              onBlur={() => commit(true)}>
              {raw === '' ? <option value="">{placeholder || 'Select…'}</option> : null}
              {railOpts(options).map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
              {/* A value no longer in the list still shows, rather than silently blanking the
                  record when someone edits the options behind it. */}
              {raw !== '' && !railOpts(options).some((o) => String(o.value) === raw)
                ? <option value={raw}>{raw} (not in list)</option> : null}
            </select>
          ) : (
          <input ref={inputRef} className="e8-rrail-fact-input" autoFocus defaultValue={raw}
            aria-label={label} aria-invalid={err ? true : undefined}
            onKeyDown={(e) => {
              if (e.key === 'Enter') { e.preventDefault(); commit(false); }
              else if (e.key === 'Escape') { e.preventDefault(); setEditing(false); setErr(null); }
            }}
            onBlur={() => commit(true)} />
          )}
          {err ? <span className="e8-rrail-fact-err" role="alert">{err}</span> : null}
        </span>
      ) : editable ? (
        <button type="button" className="e8-rrail-fact-v is-edit"
          title={raw === '' ? 'Edit ' + String(label).toLowerCase() : raw + ' — click to edit'}
          onClick={() => setEditing(true)}>
          {raw === '' ? <span className="e8-rrail-fact-empty">{placeholder || 'Add'}</span> : (display != null ? display : raw)}
        </button>
      ) : (
        /* title carries the full value: the column ellipsises, and a truncated address or name
           with no tooltip is simply unreadable. */
        <span className="e8-rrail-fact-v" title={raw === '' ? undefined : raw}>{display != null ? display : (raw === '' ? '—' : raw)}</span>
      )}
      {meta ? <span className="e8-rrail-fact-meta">{meta}</span> : null}
    </div>
  );
}

/* "All details · N" disclosure - the usually-noise tail, one click away. Per the handoff, the
   label flips to `openLabel` ("Fewer details") while open, and the count shows only closed. */
/* ONE zero rule for the whole rail. RailSection suppressed a 0 count and RailFold printed one, so
   the same rail could show a bare "Client contacts" heading above "All details · 0" - two
   components, two answers, one screen. Zero is the absence of a number here, not a number. */
function RailFold({ label = 'All details', openLabel = 'Fewer details', count, defaultOpen = false, children }) {
  const [open, setOpen] = React.useState(defaultOpen);
  const n = count != null && count !== 0 ? count : null;
  return (
    <div className="e8-rrail-fold">
      <button type="button" className="e8-rrail-fold-btn" aria-expanded={open} onClick={() => setOpen((v) => !v)}>
        <span>{open ? openLabel : label + (n != null ? ' · ' + n : '')}</span>
        <span className="material-symbols-outlined" aria-hidden="true">unfold_more</span>
      </button>
      {open ? <div className="e8-rrail-fold-body">{children}</div> : null}
    </div>
  );
}

Object.assign(window, { RecordBody, RecordRail, RailTrigger, RecordTabRow, useRecordShell, RailSection, RailFact, RailFold, RAIL_COLLAPSE_AT, railCollapseSeed });
