/* ============================================================
   FTrack — bundle gerado (IIFE-wrap por módulo, deps via window).
   NÃO editar à mão: edite os arquivos-fonte e regenere.
   ============================================================ */


/* ===== tweaks-panel.jsx ===== */
;(function(){
// @ds-adherence-ignore -- omelette starter scaffold (raw elements/hex/px by design)

/* BEGIN USAGE */
// tweaks-panel.jsx
// Reusable Tweaks shell + form-control helpers.
// Exports (to window): useTweaks, TweaksPanel, TweakSection, TweakRow, TweakSlider,
//   TweakToggle, TweakRadio, TweakSelect, TweakText, TweakNumber, TweakColor, TweakButton.
//
// Owns the host protocol (listens for __activate_edit_mode / __deactivate_edit_mode,
// posts __edit_mode_available / __edit_mode_set_keys / __edit_mode_dismissed) so
// individual prototypes don't re-roll it. Ships a consistent set of controls so you
// don't hand-draw <input type="range">, segmented radios, steppers, etc.
//
// Usage (in an HTML file that loads React + Babel):
//
//   const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
//     "primaryColor": "#D97757",
//     "palette": ["#D97757", "#29261b", "#f6f4ef"],
//     "fontSize": 16,
//     "density": "regular",
//     "dark": false
//   }/*EDITMODE-END*/;
//
//   function App() {
//     const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
//     return (
//       <div style={{ fontSize: t.fontSize, color: t.primaryColor }}>
//         Hello
//         <TweaksPanel>
//           <TweakSection label="Typography" />
//           <TweakSlider label="Font size" value={t.fontSize} min={10} max={32} unit="px"
//                        onChange={(v) => setTweak('fontSize', v)} />
//           <TweakRadio  label="Density" value={t.density}
//                        options={['compact', 'regular', 'comfy']}
//                        onChange={(v) => setTweak('density', v)} />
//           <TweakSection label="Theme" />
//           <TweakColor  label="Primary" value={t.primaryColor}
//                        options={['#D97757', '#2A6FDB', '#1F8A5B', '#7A5AE0']}
//                        onChange={(v) => setTweak('primaryColor', v)} />
//           <TweakColor  label="Palette" value={t.palette}
//                        options={[['#D97757', '#29261b', '#f6f4ef'],
//                                  ['#475569', '#0f172a', '#f1f5f9']]}
//                        onChange={(v) => setTweak('palette', v)} />
//           <TweakToggle label="Dark mode" value={t.dark}
//                        onChange={(v) => setTweak('dark', v)} />
//         </TweaksPanel>
//       </div>
//     );
//   }
//
// TweakRadio is the segmented control for 2–3 short options (auto-falls-back to
// TweakSelect past ~16/~10 chars per label); reach for TweakSelect directly when
// options are many or long. For color tweaks always curate 3-4 options rather than
// a free picker; an option can also be a whole 2–5 color palette (the stored value
// is the array). The Tweak* controls are a floor, not a ceiling — build custom
// controls inside the panel if a tweak calls for UI they don't cover.
/* END USAGE */
// ─────────────────────────────────────────────────────────────────────────────

const __TWEAKS_STYLE = `
  .twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px;
    max-height:calc(100vh - 32px);display:flex;flex-direction:column;
    transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom right;
    background:rgba(250,249,247,.78);color:#29261b;
    -webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%);
    border:.5px solid rgba(255,255,255,.6);border-radius:14px;
    box-shadow:0 1px 0 rgba(255,255,255,.5) inset,0 12px 40px rgba(0,0,0,.18);
    font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}
  .twk-hd{display:flex;align-items:center;justify-content:space-between;
    padding:10px 8px 10px 14px;cursor:move;user-select:none}
  .twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em}
  .twk-x{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55);
    width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1}
  .twk-x:hover{background:rgba(0,0,0,.06);color:#29261b}
  .twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px;
    overflow-y:auto;overflow-x:hidden;min-height:0;
    scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent}
  .twk-body::-webkit-scrollbar{width:8px}
  .twk-body::-webkit-scrollbar-track{background:transparent;margin:2px}
  .twk-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px;
    border:2px solid transparent;background-clip:content-box}
  .twk-body::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25);
    border:2px solid transparent;background-clip:content-box}
  .twk-row{display:flex;flex-direction:column;gap:5px}
  .twk-row-h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px}
  .twk-lbl{display:flex;justify-content:space-between;align-items:baseline;
    color:rgba(41,38,27,.72)}
  .twk-lbl>span:first-child{font-weight:500}
  .twk-val{color:rgba(41,38,27,.5);font-variant-numeric:tabular-nums}

  .twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
    color:rgba(41,38,27,.45);padding:10px 0 0}
  .twk-sect:first-child{padding-top:0}

  .twk-field{appearance:none;box-sizing:border-box;width:100%;min-width:0;height:26px;padding:0 8px;
    border:.5px solid rgba(0,0,0,.1);border-radius:7px;
    background:rgba(255,255,255,.6);color:inherit;font:inherit;outline:none}
  .twk-field:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)}
  select.twk-field{padding-right:22px;
    background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='rgba(0,0,0,.5)' d='M0 0h10L5 6z'/></svg>");
    background-repeat:no-repeat;background-position:right 8px center}

  .twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:6px 0;
    border-radius:999px;background:rgba(0,0,0,.12);outline:none}
  .twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
    width:14px;height:14px;border-radius:50%;background:#fff;
    border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
  .twk-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;
    background:#fff;border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}

  .twk-seg{position:relative;display:flex;padding:2px;border-radius:8px;
    background:rgba(0,0,0,.06);user-select:none}
  .twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px;
    background:rgba(255,255,255,.9);box-shadow:0 1px 2px rgba(0,0,0,.12);
    transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s}
  .twk-seg.dragging .twk-seg-thumb{transition:none}
  .twk-seg button{appearance:none;position:relative;z-index:1;flex:1;border:0;
    background:transparent;color:inherit;font:inherit;font-weight:500;min-height:22px;
    border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2;
    overflow-wrap:anywhere}

  .twk-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px;
    background:rgba(0,0,0,.15);transition:background .15s;cursor:default;padding:0}
  .twk-toggle[data-on="1"]{background:#34c759}
  .twk-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;
    background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
  .twk-toggle[data-on="1"] i{transform:translateX(14px)}

  .twk-num{display:flex;align-items:center;box-sizing:border-box;min-width:0;height:26px;padding:0 0 0 8px;
    border:.5px solid rgba(0,0,0,.1);border-radius:7px;background:rgba(255,255,255,.6)}
  .twk-num-lbl{font-weight:500;color:rgba(41,38,27,.6);cursor:ew-resize;
    user-select:none;padding-right:8px}
  .twk-num input{flex:1;min-width:0;height:100%;border:0;background:transparent;
    font:inherit;font-variant-numeric:tabular-nums;text-align:right;padding:0 8px 0 0;
    outline:none;color:inherit;-moz-appearance:textfield}
  .twk-num input::-webkit-inner-spin-button,.twk-num input::-webkit-outer-spin-button{
    -webkit-appearance:none;margin:0}
  .twk-num-unit{padding-right:8px;color:rgba(41,38,27,.45)}

  .twk-btn{appearance:none;height:26px;padding:0 12px;border:0;border-radius:7px;
    background:rgba(0,0,0,.78);color:#fff;font:inherit;font-weight:500;cursor:default}
  .twk-btn:hover{background:rgba(0,0,0,.88)}
  .twk-btn.secondary{background:rgba(0,0,0,.06);color:inherit}
  .twk-btn.secondary:hover{background:rgba(0,0,0,.1)}

  .twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:22px;
    border:.5px solid rgba(0,0,0,.1);border-radius:6px;padding:0;cursor:default;
    background:transparent;flex-shrink:0}
  .twk-swatch::-webkit-color-swatch-wrapper{padding:0}
  .twk-swatch::-webkit-color-swatch{border:0;border-radius:5.5px}
  .twk-swatch::-moz-color-swatch{border:0;border-radius:5.5px}

  .twk-chips{display:flex;gap:6px}
  .twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:46px;
    padding:0;border:0;border-radius:6px;overflow:hidden;cursor:default;
    box-shadow:0 0 0 .5px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.06);
    transition:transform .12s cubic-bezier(.3,.7,.4,1),box-shadow .12s}
  .twk-chip:hover{transform:translateY(-1px);
    box-shadow:0 0 0 .5px rgba(0,0,0,.18),0 4px 10px rgba(0,0,0,.12)}
  .twk-chip[data-on="1"]{box-shadow:0 0 0 1.5px rgba(0,0,0,.85),
    0 2px 6px rgba(0,0,0,.15)}
  .twk-chip>span{position:absolute;top:0;bottom:0;right:0;width:34%;
    display:flex;flex-direction:column;box-shadow:-1px 0 0 rgba(0,0,0,.1)}
  .twk-chip>span>i{flex:1;box-shadow:0 -1px 0 rgba(0,0,0,.1)}
  .twk-chip>span>i:first-child{box-shadow:none}
  .twk-chip svg{position:absolute;top:6px;left:6px;width:13px;height:13px;
    filter:drop-shadow(0 1px 1px rgba(0,0,0,.3))}
`;

// ── useTweaks ───────────────────────────────────────────────────────────────
// Single source of truth for tweak values. setTweak persists via the host
// (__edit_mode_set_keys → host rewrites the EDITMODE block on disk).
function useTweaks(defaults) {
  const [values, setValues] = React.useState(defaults);
  // Accepts either setTweak('key', value) or setTweak({ key: value, ... }) so a
  // useState-style call doesn't write a "[object Object]" key into the persisted
  // JSON block.
  const setTweak = React.useCallback((keyOrEdits, val) => {
    const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null
      ? keyOrEdits : { [keyOrEdits]: val };
    setValues((prev) => ({ ...prev, ...edits }));
    window.parent.postMessage({ type: '__edit_mode_set_keys', edits }, '*');
    // Same-window signal so in-page listeners (deck-stage rail thumbnails)
    // can react — the parent message only reaches the host, not peers.
    window.dispatchEvent(new CustomEvent('tweakchange', { detail: edits }));
  }, []);
  return [values, setTweak];
}

// ── TweaksPanel ─────────────────────────────────────────────────────────────
// Floating shell. Registers the protocol listener BEFORE announcing
// availability — if the announce ran first, the host's activate could land
// before our handler exists and the toolbar toggle would silently no-op.
// The close button posts __edit_mode_dismissed so the host's toolbar toggle
// flips off in lockstep; the host echoes __deactivate_edit_mode back which
// is what actually hides the panel.
function TweaksPanel({ title = 'Tweaks', children }) {
  const [open, setOpen] = React.useState(false);
  const dragRef = React.useRef(null);
  const offsetRef = React.useRef({ x: 16, y: 16 });
  const PAD = 16;

  const clampToViewport = React.useCallback(() => {
    const panel = dragRef.current;
    if (!panel) return;
    const w = panel.offsetWidth, h = panel.offsetHeight;
    const maxRight = Math.max(PAD, window.innerWidth - w - PAD);
    const maxBottom = Math.max(PAD, window.innerHeight - h - PAD);
    offsetRef.current = {
      x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)),
      y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y)),
    };
    panel.style.right = offsetRef.current.x + 'px';
    panel.style.bottom = offsetRef.current.y + 'px';
  }, []);

  React.useEffect(() => {
    if (!open) return;
    clampToViewport();
    if (typeof ResizeObserver === 'undefined') {
      window.addEventListener('resize', clampToViewport);
      return () => window.removeEventListener('resize', clampToViewport);
    }
    const ro = new ResizeObserver(clampToViewport);
    ro.observe(document.documentElement);
    return () => ro.disconnect();
  }, [open, clampToViewport]);

  React.useEffect(() => {
    const onMsg = (e) => {
      const t = e?.data?.type;
      if (t === '__activate_edit_mode') setOpen(true);
      else if (t === '__deactivate_edit_mode') setOpen(false);
    };
    window.addEventListener('message', onMsg);
    window.parent.postMessage({ type: '__edit_mode_available' }, '*');
    return () => window.removeEventListener('message', onMsg);
  }, []);

  const dismiss = () => {
    setOpen(false);
    window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*');
  };

  const onDragStart = (e) => {
    const panel = dragRef.current;
    if (!panel) return;
    const r = panel.getBoundingClientRect();
    const sx = e.clientX, sy = e.clientY;
    const startRight = window.innerWidth - r.right;
    const startBottom = window.innerHeight - r.bottom;
    const move = (ev) => {
      offsetRef.current = {
        x: startRight - (ev.clientX - sx),
        y: startBottom - (ev.clientY - sy),
      };
      clampToViewport();
    };
    const up = () => {
      window.removeEventListener('mousemove', move);
      window.removeEventListener('mouseup', up);
    };
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', up);
  };

  if (!open) return null;
  return (
    <>
      <style>{__TWEAKS_STYLE}</style>
      <div ref={dragRef} className="twk-panel" data-omelette-chrome=""
           style={{ right: offsetRef.current.x, bottom: offsetRef.current.y }}>
        <div className="twk-hd" onMouseDown={onDragStart}>
          <b>{title}</b>
          <button className="twk-x" aria-label="Close tweaks"
                  onMouseDown={(e) => e.stopPropagation()}
                  onClick={dismiss}>✕</button>
        </div>
        <div className="twk-body">
          {children}
        </div>
      </div>
    </>
  );
}

// ── Layout helpers ──────────────────────────────────────────────────────────

function TweakSection({ label, children }) {
  return (
    <>
      <div className="twk-sect">{label}</div>
      {children}
    </>
  );
}

function TweakRow({ label, value, children, inline = false }) {
  return (
    <div className={inline ? 'twk-row twk-row-h' : 'twk-row'}>
      <div className="twk-lbl">
        <span>{label}</span>
        {value != null && <span className="twk-val">{value}</span>}
      </div>
      {children}
    </div>
  );
}

// ── Controls ────────────────────────────────────────────────────────────────

function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = '', onChange }) {
  return (
    <TweakRow label={label} value={`${value}${unit}`}>
      <input type="range" className="twk-slider" min={min} max={max} step={step}
             value={value} onChange={(e) => onChange(Number(e.target.value))} />
    </TweakRow>
  );
}

function TweakToggle({ label, value, onChange }) {
  return (
    <div className="twk-row twk-row-h">
      <div className="twk-lbl"><span>{label}</span></div>
      <button type="button" className="twk-toggle" data-on={value ? '1' : '0'}
              role="switch" aria-checked={!!value}
              onClick={() => onChange(!value)}><i /></button>
    </div>
  );
}

function TweakRadio({ label, value, options, onChange }) {
  const trackRef = React.useRef(null);
  const [dragging, setDragging] = React.useState(false);
  // The active value is read by pointer-move handlers attached for the lifetime
  // of a drag — ref it so a stale closure doesn't fire onChange for every move.
  const valueRef = React.useRef(value);
  valueRef.current = value;

  // Segments wrap mid-word once per-segment width runs out. The track is
  // ~248px (280 panel − 28 body pad − 4 seg pad), each button loses 12px
  // to its own padding, and 11.5px system-ui averages ~6.3px/char — so 2
  // options fit ~16 chars each, 3 fit ~10. Past that (or >3 options), fall
  // back to a dropdown rather than wrap.
  const labelLen = (o) => String(typeof o === 'object' ? o.label : o).length;
  const maxLen = options.reduce((m, o) => Math.max(m, labelLen(o)), 0);
  const fitsAsSegments = maxLen <= ({ 2: 16, 3: 10 }[options.length] ?? 0);
  if (!fitsAsSegments) {
    // <select> emits strings — map back to the original option value so the
    // fallback stays type-preserving (numbers, booleans) like the segment path.
    const resolve = (s) => {
      const m = options.find((o) => String(typeof o === 'object' ? o.value : o) === s);
      return m === undefined ? s : typeof m === 'object' ? m.value : m;
    };
    return <TweakSelect label={label} value={value} options={options}
                        onChange={(s) => onChange(resolve(s))} />;
  }
  const opts = options.map((o) => (typeof o === 'object' ? o : { value: o, label: o }));
  const idx = Math.max(0, opts.findIndex((o) => o.value === value));
  const n = opts.length;

  const segAt = (clientX) => {
    const r = trackRef.current.getBoundingClientRect();
    const inner = r.width - 4;
    const i = Math.floor(((clientX - r.left - 2) / inner) * n);
    return opts[Math.max(0, Math.min(n - 1, i))].value;
  };

  const onPointerDown = (e) => {
    setDragging(true);
    const v0 = segAt(e.clientX);
    if (v0 !== valueRef.current) onChange(v0);
    const move = (ev) => {
      if (!trackRef.current) return;
      const v = segAt(ev.clientX);
      if (v !== valueRef.current) onChange(v);
    };
    const up = () => {
      setDragging(false);
      window.removeEventListener('pointermove', move);
      window.removeEventListener('pointerup', up);
    };
    window.addEventListener('pointermove', move);
    window.addEventListener('pointerup', up);
  };

  return (
    <TweakRow label={label}>
      <div ref={trackRef} role="radiogroup" onPointerDown={onPointerDown}
           className={dragging ? 'twk-seg dragging' : 'twk-seg'}>
        <div className="twk-seg-thumb"
             style={{ left: `calc(2px + ${idx} * (100% - 4px) / ${n})`,
                      width: `calc((100% - 4px) / ${n})` }} />
        {opts.map((o) => (
          <button key={o.value} type="button" role="radio" aria-checked={o.value === value}>
            {o.label}
          </button>
        ))}
      </div>
    </TweakRow>
  );
}

function TweakSelect({ label, value, options, onChange }) {
  return (
    <TweakRow label={label}>
      <select className="twk-field" value={value} onChange={(e) => onChange(e.target.value)}>
        {options.map((o) => {
          const v = typeof o === 'object' ? o.value : o;
          const l = typeof o === 'object' ? o.label : o;
          return <option key={v} value={v}>{l}</option>;
        })}
      </select>
    </TweakRow>
  );
}

function TweakText({ label, value, placeholder, onChange }) {
  return (
    <TweakRow label={label}>
      <input className="twk-field" type="text" value={value} placeholder={placeholder}
             onChange={(e) => onChange(e.target.value)} />
    </TweakRow>
  );
}

function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) {
  const clamp = (n) => {
    if (min != null && n < min) return min;
    if (max != null && n > max) return max;
    return n;
  };
  const startRef = React.useRef({ x: 0, val: 0 });
  const onScrubStart = (e) => {
    e.preventDefault();
    startRef.current = { x: e.clientX, val: value };
    const decimals = (String(step).split('.')[1] || '').length;
    const move = (ev) => {
      const dx = ev.clientX - startRef.current.x;
      const raw = startRef.current.val + dx * step;
      const snapped = Math.round(raw / step) * step;
      onChange(clamp(Number(snapped.toFixed(decimals))));
    };
    const up = () => {
      window.removeEventListener('pointermove', move);
      window.removeEventListener('pointerup', up);
    };
    window.addEventListener('pointermove', move);
    window.addEventListener('pointerup', up);
  };
  return (
    <div className="twk-num">
      <span className="twk-num-lbl" onPointerDown={onScrubStart}>{label}</span>
      <input type="number" value={value} min={min} max={max} step={step}
             onChange={(e) => onChange(clamp(Number(e.target.value)))} />
      {unit && <span className="twk-num-unit">{unit}</span>}
    </div>
  );
}

// Relative-luminance contrast pick — checkmarks drawn over a swatch need to
// read on both #111 and #fafafa without per-option configuration. Hex input
// only (#rgb / #rrggbb); named or rgb()/hsl() colors fall through to "light".
function __twkIsLight(hex) {
  const h = String(hex).replace('#', '');
  const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, '0');
  const n = parseInt(x.slice(0, 6), 16);
  if (Number.isNaN(n)) return true;
  const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
  return r * 299 + g * 587 + b * 114 > 148000;
}

const __TwkCheck = ({ light }) => (
  <svg viewBox="0 0 14 14" aria-hidden="true">
    <path d="M3 7.2 5.8 10 11 4.2" fill="none" strokeWidth="2.2"
          strokeLinecap="round" strokeLinejoin="round"
          stroke={light ? 'rgba(0,0,0,.78)' : '#fff'} />
  </svg>
);

// TweakColor — curated color/palette picker. Each option is either a single
// hex string or an array of 1-5 hex strings; the card adapts — a lone color
// renders solid, a palette renders colors[0] as the hero (left ~2/3) with the
// rest stacked in a sharp column on the right. onChange emits the
// option in the shape it was passed (string stays string, array stays array).
// Without options it falls back to the native color input for back-compat.
function TweakColor({ label, value, options, onChange }) {
  if (!options || !options.length) {
    return (
      <div className="twk-row twk-row-h">
        <div className="twk-lbl"><span>{label}</span></div>
        <input type="color" className="twk-swatch" value={value}
               onChange={(e) => onChange(e.target.value)} />
      </div>
    );
  }
  // Native <input type=color> emits lowercase hex per the HTML spec, so
  // compare case-insensitively. String() guards JSON.stringify(undefined),
  // which returns the primitive undefined (no .toLowerCase).
  const key = (o) => String(JSON.stringify(o)).toLowerCase();
  const cur = key(value);
  return (
    <TweakRow label={label}>
      <div className="twk-chips" role="radiogroup">
        {options.map((o, i) => {
          const colors = Array.isArray(o) ? o : [o];
          const [hero, ...rest] = colors;
          const sup = rest.slice(0, 4);
          const on = key(o) === cur;
          return (
            <button key={i} type="button" className="twk-chip" role="radio"
                    aria-checked={on} data-on={on ? '1' : '0'}
                    aria-label={colors.join(', ')} title={colors.join(' · ')}
                    style={{ background: hero }}
                    onClick={() => onChange(o)}>
              {sup.length > 0 && (
                <span>
                  {sup.map((c, j) => <i key={j} style={{ background: c }} />)}
                </span>
              )}
              {on && <__TwkCheck light={__twkIsLight(hero)} />}
            </button>
          );
        })}
      </div>
    </TweakRow>
  );
}

function TweakButton({ label, onClick, secondary = false }) {
  return (
    <button type="button" className={secondary ? 'twk-btn secondary' : 'twk-btn'}
            onClick={onClick}>{label}</button>
  );
}

Object.assign(window, {
  useTweaks, TweaksPanel, TweakSection, TweakRow,
  TweakSlider, TweakToggle, TweakRadio, TweakSelect,
  TweakText, TweakNumber, TweakColor, TweakButton,
});

})();

/* ===== data.js ===== */
;(function(){
/* ============ FTrack — mock data (pesca esportiva · Amazônia) ============ */
(function () {
  // seeded RNG (mulberry32) — estável entre reloads
  function mulberry32(a) {
    return function () {
      a |= 0; a = (a + 0x6D2B79F5) | 0;
      let t = Math.imul(a ^ (a >>> 15), 1 | a);
      t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
  }
  const rng = mulberry32(20260604);
  const rand = (min, max) => min + rng() * (max - min);
  const randi = (min, max) => Math.floor(rand(min, max + 1));
  const pick = (arr) => arr[Math.floor(rng() * arr.length)];
  const wpick = (arr) => { // weighted pick: [item, weight]
    const tot = arr.reduce((s, x) => s + x[1], 0);
    let r = rng() * tot;
    for (const [item, w] of arr) { if ((r -= w) <= 0) return item; }
    return arr[0][0];
  };

  // ---- Espécies (alvos da pesca esportiva amazônica) ----
  const SPECIES = [
    { id: 'tuc-acu', nome: 'Tucunaré-açu', cientifico: 'Cichla temensis', cor: '#0369a1', cat: 'Predador', pesoMin: 1.5, pesoMax: 12.5, compMin: 45, compMax: 98, soltura: 0.93 },
    { id: 'tuc-paca', nome: 'Tucunaré-paca', cientifico: 'Cichla pinima', cor: '#0891b2', cat: 'Predador', pesoMin: 0.8, pesoMax: 6.0, compMin: 35, compMax: 72, soltura: 0.94 },
    { id: 'tuc-borb', nome: 'Tucunaré-borboleta', cientifico: 'Cichla orinocensis', cor: '#06b6d4', cat: 'Predador', pesoMin: 0.6, pesoMax: 4.2, compMin: 30, compMax: 60, soltura: 0.95 },
    { id: 'pirarucu', nome: 'Pirarucu', cientifico: 'Arapaima gigas', cor: '#7c3aed', cat: 'Gigante', pesoMin: 25, pesoMax: 110, compMin: 130, compMax: 245, soltura: 0.99 },
    { id: 'aruana', nome: 'Aruanã', cientifico: 'Osteoglossum bicirrhosum', cor: '#0d9488', cat: 'Predador', pesoMin: 1.0, pesoMax: 4.5, compMin: 55, compMax: 90, soltura: 0.9 },
    { id: 'traira', nome: 'Traíra', cientifico: 'Hoplias malabaricus', cor: '#65a30d', cat: 'Predador', pesoMin: 0.7, pesoMax: 5.0, compMin: 30, compMax: 65, soltura: 0.7 },
    { id: 'bicuda', nome: 'Bicuda', cientifico: 'Boulengerella cuvieri', cor: '#ca8a04', cat: 'Predador', pesoMin: 0.5, pesoMax: 3.2, compMin: 40, compMax: 80, soltura: 0.85 },
    { id: 'cachorra', nome: 'Cachorra', cientifico: 'Hydrolycus scomberoides', cor: '#dc2626', cat: 'Predador', pesoMin: 1.0, pesoMax: 8.0, compMin: 45, compMax: 95, soltura: 0.88 },
    { id: 'matrinxa', nome: 'Matrinxã', cientifico: 'Brycon amazonicus', cor: '#ea580c', cat: 'Escama', pesoMin: 0.8, pesoMax: 4.0, compMin: 35, compMax: 60, soltura: 0.6 },
    { id: 'pirapitinga', nome: 'Pirapitinga', cientifico: 'Piaractus brachypomus', cor: '#d97706', cat: 'Escama', pesoMin: 2.0, pesoMax: 12.0, compMin: 40, compMax: 75, soltura: 0.55 },
    { id: 'tambaqui', nome: 'Tambaqui', cientifico: 'Colossoma macropomum', cor: '#b45309', cat: 'Escama', pesoMin: 3.0, pesoMax: 22.0, compMin: 45, compMax: 90, soltura: 0.5 },
    { id: 'jau', nome: 'Jaú', cientifico: 'Zungaro zungaro', cor: '#475569', cat: 'Bagre', pesoMin: 8.0, pesoMax: 65.0, compMin: 70, compMax: 150, soltura: 0.92 },
    { id: 'pintado', nome: 'Pintado', cientifico: 'Pseudoplatystoma corruscans', cor: '#64748b', cat: 'Bagre', pesoMin: 4.0, pesoMax: 40.0, compMin: 60, compMax: 130, soltura: 0.88 },
    { id: 'piraiba', nome: 'Piraíba', cientifico: 'Brachyplatystoma filamentosum', cor: '#334155', cat: 'Gigante', pesoMin: 15, pesoMax: 95.0, compMin: 90, compMax: 200, soltura: 0.97 },
  ];

  const SPECIES_WEIGHTS = [
    ['tuc-acu', 30], ['tuc-paca', 18], ['tuc-borb', 12], ['pirarucu', 3], ['aruana', 6],
    ['traira', 5], ['bicuda', 5], ['cachorra', 7], ['matrinxa', 4], ['pirapitinga', 3],
    ['tambaqui', 3], ['jau', 2], ['pintado', 3], ['piraiba', 1.5],
  ];

  // ---- Operações (lodges / barcos) ----
  const OPERATIONS = [
    { id: 'op-rio-negro', nome: 'Lodge Rio Negro', tipo: 'Lodge', base: 'Barcelos, AM', barcos: 6, ativa: true },
    { id: 'op-marie', nome: 'Untamed Marié', tipo: 'Barco-hotel', base: 'Santa Isabel, AM', barcos: 4, ativa: true },
    { id: 'op-xeriuini', nome: 'Xeriuíni Expedições', tipo: 'Barco-hotel', base: 'Caracaraí, RR', barcos: 3, ativa: true },
    { id: 'op-agua-boa', nome: 'Água Boa Amazon', tipo: 'Lodge', base: 'Novo Airão, AM', barcos: 8, ativa: true },
    { id: 'op-thaimacu', nome: 'Pousada Thaimaçu', tipo: 'Pousada', base: 'Jacareacanga, PA', barcos: 5, ativa: true },
    { id: 'op-kalua', nome: 'Barco Kaluana', tipo: 'Barco-hotel', base: 'Manaus, AM', barcos: 2, ativa: false },
    { id: 'op-otter', nome: 'Otter Expeditions', tipo: 'Barco-hotel', base: 'Barcelos, AM', barcos: 3, ativa: true },
  ];
  const OP_WEIGHTS = [
    ['op-rio-negro', 24], ['op-marie', 14], ['op-xeriuini', 10], ['op-agua-boa', 22],
    ['op-thaimacu', 13], ['op-kalua', 6], ['op-otter', 11],
  ];

  // ---- Guias ----
  const NOMES = ['João', 'Pedro', 'Raimundo', 'Antônio', 'Manoel', 'Francisco', 'Sebastião', 'José', 'Carlos', 'Edmilson', 'Adriano', 'Wesley', 'Cleiton', 'Marcos', 'Rogério', 'Valdir', 'Nélson', 'Aldo', 'Genilson', 'Ronaldo', 'Daniel', 'Fábio', 'Igor', 'Lucas'];
  const SOBRENOMES = ['da Silva', 'Souza', 'Ferreira', 'Lima', 'Pereira', 'Nogueira', 'Cordeiro', 'da Costa', 'Mendes', 'Tavares', 'Barreto', 'Rodrigues', 'Albuquerque', 'Bentes', 'Vasconcelos', 'Pinto', 'Maciel', 'Brandão'];
  const GUIDES = [];
  for (let i = 0; i < 28; i++) {
    const nome = `${pick(NOMES)} ${pick(SOBRENOMES)}`;
    GUIDES.push({
      id: 'g-' + (i + 1),
      nome,
      iniciais: nome.split(' ').map(p => p[0]).filter(c => c === c.toUpperCase()).slice(0, 2).join(''),
      operacaoId: wpick(OP_WEIGHTS),
      desde: 2018 + randi(0, 6),
      ativo: rng() > 0.12,
      telefone: `(92) 9${randi(8000, 9999)}-${randi(1000, 9999)}`,
    });
  }

  // ---- Locais (rios / igarapés) com âncora geográfica ----
  const LOCAIS = [
    { nome: 'Rio Negro', lat: -0.93, lng: -62.92 },
    { nome: 'Rio Marié', lat: -0.42, lng: -66.05 },
    { nome: 'Rio Xeriuíni', lat: 0.65, lng: -61.55 },
    { nome: 'Rio Branco', lat: 1.82, lng: -61.12 },
    { nome: 'Rio Aracá', lat: 0.38, lng: -63.20 },
    { nome: 'Rio Demini', lat: 0.55, lng: -62.78 },
    { nome: 'Igarapé do Daraá', lat: -0.21, lng: -64.83 },
    { nome: 'Rio Cururu', lat: -7.55, lng: -57.95 },
    { nome: 'Rio São Benedito', lat: -8.40, lng: -57.95 },
    { nome: 'Lago do Jaú', lat: -1.92, lng: -61.60 },
    { nome: 'Paraná do Cuieiras', lat: -2.65, lng: -60.50 },
    { nome: 'Rio Unini', lat: -1.70, lng: -61.45 },
  ];

  const ISCAS = ['Zara', 'Helicóptero', 'Spinnerbait', 'Jig', 'Plug de superfície', 'Hot dog', 'Colher', 'Isca viva', 'Soft', 'Popper'];

  // ---- Registros ----
  function gauss() { // soft normal in [0,1]
    return (rng() + rng() + rng()) / 3;
  }
  const N = 4180;
  const RECORDS = [];
  const START = new Date('2024-01-08').getTime();
  const END = new Date('2026-06-02').getTime();
  for (let i = 0; i < N; i++) {
    const spId = wpick(SPECIES_WEIGHTS);
    const sp = SPECIES.find(s => s.id === spId);
    const g = gauss();
    const peso = +(sp.pesoMin + (sp.pesoMax - sp.pesoMin) * g * g).toFixed(1);
    const comp = Math.round(sp.compMin + (sp.compMax - sp.compMin) * (0.4 * g + 0.6 * rng()));
    // sazonalidade: seca amazônica (jul–nov) concentra capturas
    let ts;
    do {
      ts = START + rng() * (END - START);
      const m = new Date(ts).getMonth(); // 0=jan
      const seasonal = (m >= 6 && m <= 10) ? 0.85 : 0.32;
      if (rng() < seasonal) break;
    } while (true);
    const d = new Date(ts);
    d.setHours(randi(5, 17), randi(0, 59), 0, 0);
    const loc = pick(LOCAIS);
    const guia = pick(GUIDES);
    RECORDS.push({
      id: i + 1,
      code: 'FT-' + String(i + 1).padStart(5, '0'),
      data: d.toISOString(),
      especieId: sp.id,
      peso,
      comprimento: comp,
      guiaId: guia.id,
      operacaoId: guia.operacaoId,
      local: loc.nome,
      lat: +(loc.lat + rand(-0.18, 0.18)).toFixed(4),
      lng: +(loc.lng + rand(-0.18, 0.18)).toFixed(4),
      isca: pick(ISCAS),
    });
  }
  RECORDS.sort((a, b) => new Date(b.data) - new Date(a.data));

  // ---- Lookups + agregados úteis ----
  const speciesById = Object.fromEntries(SPECIES.map(s => [s.id, s]));
  const guidesById = Object.fromEntries(GUIDES.map(g => [g.id, g]));
  const opsById = Object.fromEntries(OPERATIONS.map(o => [o.id, o]));

  // contagens por espécie / guia / operação (sobre todos os registros)
  const countBy = (key) => RECORDS.reduce((m, r) => { m[r[key]] = (m[r[key]] || 0) + 1; return m; }, {});
  const bySpecies = countBy('especieId');
  const byGuide = countBy('guiaId');
  const byOp = countBy('operacaoId');
  SPECIES.forEach(s => { s.capturas = bySpecies[s.id] || 0; });
  GUIDES.forEach(g => { g.capturas = byGuide[g.id] || 0; });
  OPERATIONS.forEach(o => { o.capturas = byOp[o.id] || 0; o.guias = GUIDES.filter(g => g.operacaoId === o.id).length; });

  window.FT = {
    SPECIES, OPERATIONS, GUIDES, RECORDS, LOCAIS, ISCAS,
    speciesById, guidesById, opsById,
  };
})();

})();

/* ===== icons.jsx ===== */
;(function(){
/* ============ FTrack — ícones (line, estilo lucide) ============ */
const ICON_PATHS = {
  dashboard: 'M3 3h8v8H3zM13 3h8v5h-8zM13 12h8v9h-8zM3 14h8v7H3z',
  fish: 'M6.5 12c.94-3.46 4.94-6 8.5-6 3.56 0 6.06 2.54 7 6-.94 3.47-3.44 6-7 6s-7.56-2.53-8.5-6zM18 12h.01M2 16s1.5-2 4-2M2 8s1.5 2 4 2',
  ship: 'M12 10.19V4M5 10.5V4a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v6.5M3 14l1.5 5.5a2 2 0 0 0 2 .5h11a2 2 0 0 0 2-.5L21 14M3 14a3 3 0 0 0 4 0 3 3 0 0 0 4 0 3 3 0 0 0 4 0 3 3 0 0 0 4 0',
  list: 'M3 5h18M3 12h18M3 19h18',
  table: 'M3 4h18v16H3zM3 10h18M3 16h18M9 4v16',
  users: 'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75',
  search: 'M11 11m-8 0a8 8 0 1 0 16 0 8 8 0 1 0-16 0M21 21l-4.3-4.3',
  bell: 'M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10.3 21a1.94 1.94 0 0 0 3.4 0',
  settings: 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z',
  chevronDown: 'M6 9l6 6 6-6',
  chevronRight: 'M9 18l6-6-6-6',
  chevronLeft: 'M15 18l-6-6 6-6',
  chevronsLeft: 'M11 17l-5-5 5-5M18 17l-5-5 5-5',
  chevronsRight: 'M13 17l5-5-5-5M6 17l5-5-5-5',
  filter: 'M22 3H2l8 9.46V19l4 2v-8.54z',
  plus: 'M12 5v14M5 12h14',
  download: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3',
  more: 'M12 12h.01M19 12h.01M5 12h.01',
  x: 'M18 6L6 18M6 6l12 12',
  check: 'M20 6L9 17l-5-5',
  sort: 'M11 5h10M11 9h7M11 13h4M3 17l3 3 3-3M6 18V4',
  arrowUp: 'M12 19V5M5 12l7-7 7 7',
  arrowDown: 'M12 5v14M19 12l-7 7-7-7',
  calendar: 'M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z',
  mapPin: 'M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0zM12 10m-3 0a3 3 0 1 0 6 0 3 3 0 1 0-6 0',
  trendUp: 'M22 7l-8.5 8.5-5-5L2 17M16 7h6v6',
  trendDown: 'M22 17l-8.5-8.5-5 5L2 7M16 17h6v-6',
  scale: 'M12 3v18M7 7l-5 9a5 3 0 0 0 10 0zM17 7l5 9a5 3 0 0 1-10 0zM7 7h10M5 21h14',
  ruler: 'M3 21l18-18M14.5 3.5l6 6M3.5 14.5l6 6M7 11l2 2M11 7l2 2M11 15l2 2M15 11l2 2',
  logout: 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9',
  eye: 'M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7zM12 12m-3 0a3 3 0 1 0 6 0 3 3 0 1 0-6 0',
  columns: 'M3 3h18v18H3zM12 3v18',
  sliders: 'M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6',
  waves: 'M2 6c.6.5 1.2 1 2.5 1C7 7 7 5 9.5 5c2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1M2 12c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1M2 18c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1',
  target: 'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0M12 12m-6 0a6 6 0 1 0 12 0 6 6 0 1 0-12 0M12 12m-2 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0',
  award: 'M12 15a7 7 0 1 0 0-14 7 7 0 0 0 0 14zM8.21 13.89L7 23l5-3 5 3-1.21-9.12',
  menu: 'M3 6h18M3 12h18M3 18h18',
  lock: 'M5 11h14a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2zM7 11V7a5 5 0 0 1 10 0v4',
  user: 'M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
  mail: 'M22 6l-10 7L2 6M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z',
  arrowRight: 'M5 12h14M12 5l7 7-7 7',
  hash: 'M4 9h16M4 15h16M10 3L8 21M16 3l-2 18',
  dot: 'M12 12m-4 0a4 4 0 1 0 8 0 4 4 0 1 0-8 0',
  ruler2: 'M3 8h18v8H3zM7 8v4M11 8v4M15 8v4M19 8v4',
  clock: 'M12 12m-9 0a9 9 0 1 0 18 0 9 9 0 1 0-18 0M12 7v5l3 2',
  refresh: 'M3 12a9 9 0 0 1 15-6.7L21 8M21 3v5h-5M21 12a9 9 0 0 1-15 6.7L3 16M3 21v-5h5',
  trophy: 'M6 9H4.5a2.5 2.5 0 0 1 0-5H6M18 9h1.5a2.5 2.5 0 0 0 0-5H18M4 22h16M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22M18 2H6v7a6 6 0 0 0 12 0V2z',
  upload: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12',
  pencil: 'M17 3a2.85 2.85 0 0 1 4 4L7.5 20.5 2 22l1.5-5.5z',
  fishSimple: 'M2 16s9-3 9-9c0 0 3 2 3 6s-3 6-3 6M11 7s2 0 3 1M16 10c2-1 5 0 6 2-1 2-4 3-6 2',
  fileText: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M8 13h8M8 17h8M8 9h2',
  printer: 'M6 9V2h12v7M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2M6 14h12v8H6z',
  shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z',
  building: 'M3 21h18M5 21V7l8-4v18M19 21V11l-6-4M9 9v.01M9 12v.01M9 15v.01M9 18v.01',
  fileCheck: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M9 15l2 2 4-4',
  send: 'M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z',
  info: 'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0M12 16v-4M12 8h.01',
  chartBar: 'M3 3v18h18M7 16v-5M12 16V8M17 16v-3',
};

function Icon({ name, size = 16, className = '', style = {}, strokeWidth = 1.75 }) {
  const d = ICON_PATHS[name] || ICON_PATHS.dot;
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
      stroke="currentColor" strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round"
      className={className} style={{ flexShrink: 0, ...style }} aria-hidden="true">
      {d.split('M').filter(Boolean).map((seg, i) => <path key={i} d={'M' + seg} />)}
    </svg>
  );
}

window.Icon = Icon;

})();

/* ===== ui.jsx ===== */
;(function(){
/* ============ FTrack — primitivos de UI (shadcn-like) ============ */
const { useState, useRef, useEffect, useMemo, useCallback } = React;

// ---------- formatadores PT-BR ----------
const nf0 = new Intl.NumberFormat('pt-BR');
const nf1 = new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 1, maximumFractionDigits: 1 });
const fmtInt = (n) => nf0.format(Math.round(n));
const fmtKg = (n) => nf1.format(n) + ' kg';
const MESES = ['jan', 'fev', 'mar', 'abr', 'mai', 'jun', 'jul', 'ago', 'set', 'out', 'nov', 'dez'];
const MESES_FULL = ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
function fmtDate(iso) {
  const d = new Date(iso);
  return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`;
}
function fmtDateTime(iso) {
  const d = new Date(iso);
  return `${fmtDate(iso)} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}
function fmtTime(iso) {
  const d = new Date(iso);
  return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}

// ---------- Button ----------
function Button({ children, variant = 'default', size = 'md', icon, iconRight, className = '', style = {}, ...rest }) {
  const base = {
    display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6,
    fontWeight: 500, whiteSpace: 'nowrap', borderRadius: 'var(--radius-sm)',
    border: '1px solid transparent', transition: 'background .12s, border-color .12s, box-shadow .12s, color .12s',
    cursor: 'pointer', userSelect: 'none',
  };
  const sizes = {
    sm: { height: 28, padding: '0 9px', fontSize: 12.5 },
    md: { height: 34, padding: '0 13px', fontSize: 13.5 },
    lg: { height: 40, padding: '0 18px', fontSize: 14 },
    icon: { height: 32, width: 32, padding: 0, fontSize: 13.5 },
    iconSm: { height: 28, width: 28, padding: 0 },
  };
  const variants = {
    default: { background: 'hsl(var(--primary))', color: 'hsl(var(--primary-foreground))', boxShadow: 'var(--shadow-sm)' },
    secondary: { background: 'hsl(var(--background))', color: 'hsl(var(--foreground))', borderColor: 'hsl(var(--border))', boxShadow: 'var(--shadow-sm)' },
    ghost: { background: 'transparent', color: 'hsl(var(--muted-foreground))' },
    ghostDark: { background: 'transparent', color: 'hsl(var(--foreground))' },
    outline: { background: 'transparent', color: 'hsl(var(--foreground))', borderColor: 'hsl(var(--border))' },
    danger: { background: 'hsl(var(--danger-bg))', color: 'hsl(var(--danger))', borderColor: 'hsl(var(--danger) / .2)' },
  };
  const [hover, setHover] = useState(false);
  const hoverStyle = hover ? {
    default: { background: 'hsl(var(--primary-700))' },
    secondary: { background: 'hsl(var(--muted))' },
    ghost: { background: 'hsl(var(--muted))', color: 'hsl(var(--foreground))' },
    ghostDark: { background: 'hsl(var(--muted))' },
    outline: { background: 'hsl(var(--muted))' },
    danger: { background: 'hsl(var(--danger) / .12)' },
  }[variant] : {};
  return (
    <button className={'focusable ' + className}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{ ...base, ...sizes[size], ...variants[variant], ...hoverStyle, ...style }} {...rest}>
      {icon && <Icon name={icon} size={size === 'sm' ? 13 : 14.5} />}
      {children}
      {iconRight && <Icon name={iconRight} size={size === 'sm' ? 13 : 14.5} />}
    </button>
  );
}

// ---------- Badge / chip ----------
function Badge({ children, tone = 'neutral', dot, style = {}, className = '' }) {
  const tones = {
    neutral: { bg: 'hsl(var(--muted))', fg: 'hsl(var(--muted-foreground))', bd: 'hsl(var(--border))' },
    primary: { bg: 'hsl(var(--primary-50))', fg: 'hsl(var(--primary-700))', bd: 'hsl(var(--primary) / .18)' },
    success: { bg: 'hsl(var(--success-bg))', fg: 'hsl(var(--success))', bd: 'hsl(var(--success) / .2)' },
    warning: { bg: 'hsl(var(--warning-bg))', fg: 'hsl(var(--warning))', bd: 'hsl(var(--warning) / .25)' },
    danger: { bg: 'hsl(var(--danger-bg))', fg: 'hsl(var(--danger))', bd: 'hsl(var(--danger) / .2)' },
  };
  const t = tones[tone] || tones.neutral;
  return (
    <span className={className} style={{
      display: 'inline-flex', alignItems: 'center', gap: 5, height: 19, padding: '0 7px',
      borderRadius: 5, background: t.bg, color: t.fg, border: `1px solid ${t.bd}`,
      fontSize: 11.5, fontWeight: 500, lineHeight: 1, whiteSpace: 'nowrap', ...style,
    }}>
      {dot && <span style={{ width: 6, height: 6, borderRadius: 99, background: dot, flexShrink: 0 }} />}
      {children}
    </span>
  );
}

// ---------- SpeciesDot ----------
function SpeciesDot({ cor, size = 8 }) {
  return <span style={{ width: size, height: size, borderRadius: 99, background: cor, flexShrink: 0, display: 'inline-block', boxShadow: `0 0 0 2px ${cor}22` }} />;
}

// ---------- Avatar (iniciais) ----------
function Avatar({ name, iniciais, size = 24, cor }) {
  const hue = useMemo(() => {
    let h = 0; for (const c of (name || '')) h = (h * 31 + c.charCodeAt(0)) % 360; return h;
  }, [name]);
  const bg = cor || `hsl(${hue} 42% 92%)`;
  const fg = cor ? '#fff' : `hsl(${hue} 45% 32%)`;
  return (
    <span style={{
      width: size, height: size, borderRadius: 99, background: bg, color: fg,
      display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
      fontSize: size * 0.4, fontWeight: 600, flexShrink: 0, letterSpacing: '-.02em',
    }}>{iniciais || (name || '?').slice(0, 2)}</span>
  );
}

// ---------- Card ----------
function Card({ children, className = '', style = {}, pad = true, ...rest }) {
  return (
    <div className={className} style={{
      background: 'hsl(var(--card))', border: '1px solid hsl(var(--border))',
      borderRadius: 'var(--radius)', boxShadow: 'var(--shadow-sm)',
      ...(pad ? { padding: 16 } : {}), ...style,
    }} {...rest}>{children}</div>
  );
}

// ---------- Input ----------
function Input({ icon, className = '', style = {}, wrapStyle = {}, ...rest }) {
  return (
    <div style={{ position: 'relative', display: 'flex', alignItems: 'center', ...wrapStyle }}>
      {icon && <Icon name={icon} size={14.5} style={{ position: 'absolute', left: 9, color: 'hsl(var(--muted-foreground))', pointerEvents: 'none' }} />}
      <input className={'focusable ' + className} style={{
        height: 34, width: '100%', borderRadius: 'var(--radius-sm)', border: '1px solid hsl(var(--input))',
        background: 'hsl(var(--background))', color: 'hsl(var(--foreground))', fontSize: 13.5,
        padding: icon ? '0 10px 0 29px' : '0 10px', transition: 'border-color .12s, box-shadow .12s', ...style,
      }} {...rest} />
    </div>
  );
}

// ---------- Checkbox ----------
function Checkbox({ checked, indeterminate, onChange, size = 15 }) {
  return (
    <span onClick={(e) => { e.stopPropagation(); onChange && onChange(!checked); }}
      className="focusable" tabIndex={0}
      onKeyDown={(e) => { if (e.key === ' ') { e.preventDefault(); onChange && onChange(!checked); } }}
      style={{
        width: size, height: size, borderRadius: 4, flexShrink: 0, cursor: 'pointer',
        border: `1.5px solid ${checked || indeterminate ? 'hsl(var(--primary))' : 'hsl(var(--border))'}`,
        background: checked || indeterminate ? 'hsl(var(--primary))' : 'hsl(var(--background))',
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center', transition: 'background .1s, border-color .1s',
      }}>
      {checked && <Icon name="check" size={size - 4} style={{ color: '#fff' }} strokeWidth={3} />}
      {indeterminate && !checked && <span style={{ width: size - 7, height: 2, background: '#fff', borderRadius: 2 }} />}
    </span>
  );
}

// ---------- Select (custom dropdown) ----------
function Select({ value, options, onChange, size = 'md', placeholder = 'Selecionar', className = '', style = {}, align = 'left', minWidth = 0 }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h);
  }, []);
  const cur = options.find(o => (o.value ?? o) === value);
  const label = cur ? (cur.label ?? cur) : placeholder;
  const h = size === 'sm' ? 28 : 34;
  return (
    <div ref={ref} style={{ position: 'relative', ...style }}>
      <button className="focusable" onClick={() => setOpen(o => !o)} style={{
        height: h, minWidth, display: 'inline-flex', alignItems: 'center', gap: 7, justifyContent: 'space-between',
        padding: size === 'sm' ? '0 8px' : '0 10px', borderRadius: 'var(--radius-sm)',
        border: '1px solid hsl(var(--input))', background: 'hsl(var(--background))',
        color: cur ? 'hsl(var(--foreground))' : 'hsl(var(--muted-foreground))',
        fontSize: size === 'sm' ? 12.5 : 13.5, fontWeight: 450, width: '100%',
      }}>
        <span style={{ display: 'flex', alignItems: 'center', gap: 7, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
          {cur && cur.dot && <SpeciesDot cor={cur.dot} />}{label}
        </span>
        <Icon name="chevronDown" size={14} style={{ color: 'hsl(var(--muted-foreground))', opacity: .8 }} />
      </button>
      {open && (
        <div className="anim-pop" style={{
          position: 'absolute', top: h + 4, [align]: 0, zIndex: 60, minWidth: Math.max(minWidth, 160),
          background: 'hsl(var(--popover))', border: '1px solid hsl(var(--border))', borderRadius: 'var(--radius)',
          boxShadow: 'var(--shadow-lg)', padding: 4, maxHeight: 300, overflowY: 'auto',
        }}>
          {options.map((o, i) => {
            const v = o.value ?? o, l = o.label ?? o;
            const sel = v === value;
            return (
              <div key={i} onClick={() => { onChange(v); setOpen(false); }} style={{
                display: 'flex', alignItems: 'center', gap: 8, padding: '6px 8px', borderRadius: 5, cursor: 'pointer',
                fontSize: 13, color: 'hsl(var(--foreground))', background: sel ? 'hsl(var(--muted))' : 'transparent',
              }} onMouseEnter={(e) => e.currentTarget.style.background = 'hsl(var(--muted))'}
                onMouseLeave={(e) => e.currentTarget.style.background = sel ? 'hsl(var(--muted))' : 'transparent'}>
                {o.dot && <SpeciesDot cor={o.dot} />}
                <span style={{ flex: 1 }}>{l}</span>
                {sel && <Icon name="check" size={14} style={{ color: 'hsl(var(--primary))' }} />}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// ---------- Popover genérico (anchored) ----------
function Popover({ trigger, children, align = 'left', width = 240, panelStyle = {} }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h);
  }, []);
  return (
    <div ref={ref} style={{ position: 'relative' }}>
      {trigger(open, () => setOpen(o => !o))}
      {open && (
        <div className="anim-pop" style={{
          position: 'absolute', top: '100%', marginTop: 6, [align]: 0, zIndex: 70, width,
          background: 'hsl(var(--popover))', border: '1px solid hsl(var(--border))', borderRadius: 'var(--radius)',
          boxShadow: 'var(--shadow-lg)', ...panelStyle,
        }}>{typeof children === 'function' ? children(() => setOpen(false)) : children}</div>
      )}
    </div>
  );
}

// ---------- KPI stat ----------
function Stat({ label, value, sub, delta, deltaTone, icon, accent }) {
  return (
    <Card style={{ padding: 15, display: 'flex', flexDirection: 'column', gap: 10 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <span style={{ fontSize: 12.5, color: 'hsl(var(--muted-foreground))', fontWeight: 500 }}>{label}</span>
        {icon && (
          <span style={{ width: 28, height: 28, borderRadius: 7, display: 'grid', placeItems: 'center',
            background: accent ? 'hsl(var(--primary-50))' : 'hsl(var(--muted))', color: accent ? 'hsl(var(--primary))' : 'hsl(var(--muted-foreground))' }}>
            <Icon name={icon} size={15} />
          </span>
        )}
      </div>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
        <span className="tnum" style={{ fontSize: 26, fontWeight: 600, letterSpacing: '-.02em', lineHeight: 1 }}>{value}</span>
        {sub && <span style={{ fontSize: 13, color: 'hsl(var(--muted-foreground))' }}>{sub}</span>}
      </div>
      {delta != null && (
        <div style={{ display: 'flex', alignItems: 'center', gap: 5, fontSize: 12, fontWeight: 500,
          color: deltaTone === 'down' ? 'hsl(var(--danger))' : 'hsl(var(--success))' }}>
          <Icon name={deltaTone === 'down' ? 'trendDown' : 'trendUp'} size={13} />
          <span>{delta}</span>
          <span style={{ color: 'hsl(var(--muted-foreground))', fontWeight: 450 }}>vs. período anterior</span>
        </div>
      )}
    </Card>
  );
}

// ---------- Section header ----------
function PageHeader({ title, subtitle, actions, children }) {
  return (
    <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, marginBottom: 18, flexWrap: 'wrap' }}>
      <div>
        <h1 style={{ fontSize: 21, fontWeight: 600, letterSpacing: '-.02em' }}>{title}</h1>
        {subtitle && <p style={{ fontSize: 13.5, color: 'hsl(var(--muted-foreground))', marginTop: 3 }}>{subtitle}</p>}
        {children}
      </div>
      {actions && <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>{actions}</div>}
    </div>
  );
}

// ---------- DatePicker (presets + calendário, intervalo) ----------
const DP_PRESETS = [
  { key: 'all', label: 'Todo período' },
  { key: '7d', label: 'Últimos 7 dias' },
  { key: '30d', label: 'Últimos 30 dias' },
  { key: '90d', label: 'Últimos 90 dias' },
  { key: '12m', label: 'Últimos 12 meses' },
  { key: 'ytd', label: 'Este ano' },
];
function dpRange(key, refDate) {
  const ref = new Date(refDate);
  const end = ref.getTime();
  const days = { '7d': 7, '30d': 30, '90d': 90, '12m': 365 }[key];
  if (days) return { from: end - days * 864e5, to: end };
  if (key === 'ytd') return { from: new Date(ref.getFullYear(), 0, 1).getTime(), to: end };
  return { from: null, to: null };
}
function DatePicker({ value, onChange, refDate, align = 'left' }) {
  // value: { key, from, to } — key='custom' usa from/to
  const [viewM, setViewM] = useState(() => { const d = new Date(value.to || refDate); return new Date(d.getFullYear(), d.getMonth(), 1); });
  const [pendFrom, setPendFrom] = useState(null);
  const label = useMemo(() => {
    if (value.key && value.key !== 'custom') return DP_PRESETS.find(p => p.key === value.key)?.label || 'Período';
    if (value.from && value.to) return `${fmtDate(new Date(value.from).toISOString())} – ${fmtDate(new Date(value.to).toISOString())}`;
    return 'Período personalizado';
  }, [value]);
  const isActive = value.key && value.key !== 'all';

  const buildDays = (base) => {
    const y = base.getFullYear(), m = base.getMonth();
    const first = new Date(y, m, 1);
    const startDow = (first.getDay() + 6) % 7; // semana começa seg
    const daysInM = new Date(y, m + 1, 0).getDate();
    const cells = [];
    for (let i = 0; i < startDow; i++) cells.push(null);
    for (let d = 1; d <= daysInM; d++) cells.push(new Date(y, m, d));
    return cells;
  };

  return (
    <Popover width="auto" align={align} panelStyle={{ width: 'auto' }} trigger={(open, toggle) => (
      <button onClick={toggle} className="focusable" style={{
        display: 'inline-flex', alignItems: 'center', gap: 7, height: 32, padding: '0 11px',
        borderRadius: 'var(--radius-sm)', border: `1px solid ${isActive ? 'hsl(var(--primary) / .4)' : 'hsl(var(--input))'}`,
        background: isActive ? 'hsl(var(--primary-50))' : 'hsl(var(--background))', color: 'hsl(var(--foreground))',
        fontSize: 13, fontWeight: 450, whiteSpace: 'nowrap',
      }}>
        <Icon name="calendar" size={14.5} style={{ color: isActive ? 'hsl(var(--primary))' : 'hsl(var(--muted-foreground))' }} />
        {label}
        <Icon name="chevronDown" size={13} style={{ color: 'hsl(var(--muted-foreground))', opacity: .8 }} />
      </button>
    )}>
      {(close) => (
        <div style={{ display: 'flex' }}>
          {/* presets */}
          <div style={{ width: 168, borderRight: '1px solid hsl(var(--border))', padding: 5, display: 'flex', flexDirection: 'column', gap: 1 }}>
            {DP_PRESETS.map(p => {
              const sel = value.key === p.key;
              return (
                <button key={p.key} onClick={() => { onChange({ key: p.key, ...dpRange(p.key, refDate) }); close(); }}
                  style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', height: 31, padding: '0 9px', borderRadius: 5, border: 'none', background: sel ? 'hsl(var(--primary-50))' : 'transparent', color: sel ? 'hsl(var(--primary-700))' : 'hsl(var(--foreground))', fontSize: 12.5, fontWeight: sel ? 600 : 450, cursor: 'pointer', textAlign: 'left' }}
                  onMouseEnter={(e) => { if (!sel) e.currentTarget.style.background = 'hsl(var(--muted))'; }} onMouseLeave={(e) => { if (!sel) e.currentTarget.style.background = 'transparent'; }}>
                  {p.label}{sel && <Icon name="check" size={14} style={{ color: 'hsl(var(--primary))' }} />}
                </button>
              );
            })}
          </div>
          {/* calendário */}
          <div style={{ padding: 12, width: 244 }}>
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
              <button style={dpNav} onClick={() => setViewM(d => new Date(d.getFullYear(), d.getMonth() - 1, 1))}><Icon name="chevronLeft" size={16} /></button>
              <span style={{ fontSize: 13, fontWeight: 600 }}>{MESES_FULL[viewM.getMonth()]} {viewM.getFullYear()}</span>
              <button style={dpNav} onClick={() => setViewM(d => new Date(d.getFullYear(), d.getMonth() + 1, 1))}><Icon name="chevronRight" size={16} /></button>
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', gap: 1, marginBottom: 3 }}>
              {['S', 'T', 'Q', 'Q', 'S', 'S', 'D'].map((d, i) => <div key={i} style={{ textAlign: 'center', fontSize: 10.5, fontWeight: 600, color: 'hsl(var(--muted-foreground))', height: 22, lineHeight: '22px' }}>{d}</div>)}
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', gap: 1 }}>
              {buildDays(viewM).map((d, i) => {
                if (!d) return <div key={i} />;
                const ts = d.getTime();
                const inRange = value.from && value.to && ts >= value.from && ts <= value.to + 864e5 - 1;
                const isEdge = (value.from && new Date(value.from).toDateString() === d.toDateString()) || (value.to && new Date(value.to).toDateString() === d.toDateString());
                const isPend = pendFrom && new Date(pendFrom).toDateString() === d.toDateString();
                return (
                  <button key={i} onClick={() => {
                    if (pendFrom == null) { setPendFrom(ts); }
                    else { const a = Math.min(pendFrom, ts), b = Math.max(pendFrom, ts); onChange({ key: 'custom', from: a, to: b }); setPendFrom(null); close(); }
                  }}
                    style={{ height: 28, borderRadius: 5, border: 'none', cursor: 'pointer', fontSize: 12, fontWeight: (isEdge || isPend) ? 600 : 450,
                      background: (isEdge || isPend) ? 'hsl(var(--primary))' : inRange ? 'hsl(var(--primary-50))' : 'transparent',
                      color: (isEdge || isPend) ? '#fff' : 'hsl(var(--foreground))' }}
                    onMouseEnter={(e) => { if (!isEdge && !inRange && !isPend) e.currentTarget.style.background = 'hsl(var(--muted))'; }}
                    onMouseLeave={(e) => { if (!isEdge && !inRange && !isPend) e.currentTarget.style.background = 'transparent'; }}>{d.getDate()}</button>
                );
              })}
            </div>
            <div style={{ fontSize: 11, color: 'hsl(var(--muted-foreground))', marginTop: 9, textAlign: 'center' }}>
              {pendFrom ? 'Selecione a data final' : 'Clique para definir um intervalo'}
            </div>
          </div>
        </div>
      )}
    </Popover>
  );
}
const dpNav = { width: 26, height: 26, borderRadius: 5, border: 'none', background: 'transparent', color: 'hsl(var(--muted-foreground))', display: 'grid', placeItems: 'center', cursor: 'pointer' };

// ---------- Dialog / Modal ----------
function Dialog({ open, onClose, title, desc, icon, children, footer, width = 480 }) {
  useEffect(() => {
    if (!open) return;
    const h = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', h);
    return () => document.removeEventListener('keydown', h);
  }, [open, onClose]);
  if (!open) return null;
  return (
    <div onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }} style={{
      position: 'fixed', inset: 0, zIndex: 300, background: 'hsl(240 10% 8% / .42)', backdropFilter: 'blur(2px)',
      display: 'grid', placeItems: 'center', padding: 24, animation: 'ft-pop .14s ease both',
    }}>
      <div style={{ width, maxWidth: '100%', maxHeight: '90vh', display: 'flex', flexDirection: 'column', background: 'hsl(var(--background))', borderRadius: 'var(--radius)', border: '1px solid hsl(var(--border))', boxShadow: 'var(--shadow-lg)', animation: 'ft-fade-in .2s cubic-bezier(.16,1,.3,1) both' }}>
        <div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: '17px 20px', borderBottom: '1px solid hsl(var(--border))' }}>
          {icon && <div style={{ width: 34, height: 34, borderRadius: 8, background: 'hsl(var(--primary-50))', color: 'hsl(var(--primary))', display: 'grid', placeItems: 'center', flexShrink: 0 }}><Icon name={icon} size={18} /></div>}
          <div style={{ flex: 1, minWidth: 0 }}>
            <h2 style={{ fontSize: 16, fontWeight: 600, letterSpacing: '-.01em' }}>{title}</h2>
            {desc && <p style={{ fontSize: 13, color: 'hsl(var(--muted-foreground))', marginTop: 3 }}>{desc}</p>}
          </div>
          <button onClick={onClose} style={{ ...iconBtn, width: 30, height: 30 }}><Icon name="x" size={17} /></button>
        </div>
        <div style={{ padding: 20, overflowY: 'auto' }}>{children}</div>
        {footer && <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 9, padding: '14px 20px', borderTop: '1px solid hsl(var(--border))', background: 'hsl(var(--muted) / .4)' }}>{footer}</div>}
      </div>
    </div>
  );
}

// ---------- Form field (para modais) ----------
function FormField({ label, hint, required, children, span }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 6, gridColumn: span ? `span ${span}` : undefined }}>
      <label style={{ fontSize: 12.5, fontWeight: 500 }}>{label}{required && <span style={{ color: 'hsl(var(--danger))', marginLeft: 3 }}>*</span>}</label>
      {children}
      {hint && <span style={{ fontSize: 11.5, color: 'hsl(var(--muted-foreground))' }}>{hint}</span>}
    </div>
  );
}

Object.assign(window, {
  useState, useRef, useEffect, useMemo, useCallback,
  nf0, nf1, fmtInt, fmtKg, MESES, MESES_FULL, fmtDate, fmtDateTime, fmtTime,
  Button, Badge, SpeciesDot, Avatar, Card, Input, Checkbox, Select, Popover, Stat, PageHeader,
  DatePicker, dpRange, DP_PRESETS, Dialog, FormField,
});

})();

/* ===== charts.jsx ===== */
;(function(){
/* ============ FTrack — gráficos (SVG leve) ============ */

// ---------- Tooltip flutuante ----------
function useChartTip() {
  const [tip, setTip] = useState(null); // {x,y,content}
  const node = tip ? (
    <div style={{
      position: 'fixed', left: tip.x, top: tip.y, transform: 'translate(-50%,-115%)',
      background: 'hsl(240 10% 12%)', color: '#fff', padding: '6px 9px', borderRadius: 6,
      fontSize: 12, fontWeight: 500, pointerEvents: 'none', zIndex: 200, whiteSpace: 'nowrap',
      boxShadow: 'var(--shadow-lg)', lineHeight: 1.45,
    }}>{tip.content}</div>
  ) : null;
  return [node, setTip];
}

// ---------- BarChart (vertical, mensal) ----------
function BarChart({ data, height = 220, color = 'hsl(var(--primary))', valueFmt = fmtInt, barWidth = 26 }) {
  const [tip, setTip] = useChartTip();
  const max = Math.max(...data.map(d => d.value), 1);
  const ticks = 4;
  return (
    <div style={{ position: 'relative' }}>
      {tip}
      <div style={{ display: 'flex', height }}>
        {/* y axis */}
        <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between', paddingRight: 8, paddingBottom: 22 }}>
          {Array.from({ length: ticks + 1 }).map((_, i) => (
            <span key={i} className="tnum" style={{ fontSize: 10.5, color: 'hsl(var(--muted-foreground))', textAlign: 'right' }}>
              {fmtInt(max * (ticks - i) / ticks)}
            </span>
          ))}
        </div>
        {/* plot */}
        <div style={{ flex: 1, position: 'relative' }}>
          {/* gridlines */}
          <div style={{ position: 'absolute', inset: 0, bottom: 22, display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
            {Array.from({ length: ticks + 1 }).map((_, i) => (
              <div key={i} style={{ borderTop: '1px solid hsl(var(--border))', opacity: i === ticks ? 1 : .6 }} />
            ))}
          </div>
          {/* bars */}
          <div style={{ position: 'absolute', inset: 0, bottom: 22, display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 3, padding: '0 2px' }}>
            {data.map((d, i) => {
              const hpct = (d.value / max) * 100;
              return (
                <div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end', height: '100%' }}
                  onMouseMove={(e) => setTip({ x: e.clientX, y: e.clientY, content: `${d.label}: ${valueFmt(d.value)}` })}
                  onMouseLeave={() => setTip(null)}>
                  <div style={{
                    width: '100%', maxWidth: barWidth, height: `${hpct}%`, minHeight: d.value > 0 ? 2 : 0,
                    background: d.highlight ? color : 'hsl(var(--primary) / .82)', borderRadius: '4px 4px 0 0',
                    transition: 'background .12s', cursor: 'default',
                  }} />
                </div>
              );
            })}
          </div>
          {/* x labels */}
          <div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height: 22, display: 'flex', justifyContent: 'space-between', gap: 3, paddingTop: 6 }}>
            {data.map((d, i) => (
              <span key={i} style={{ flex: 1, textAlign: 'center', fontSize: 10.5, color: 'hsl(var(--muted-foreground))', overflow: 'hidden', whiteSpace: 'nowrap' }}>{d.short ?? d.label}</span>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

// ---------- Donut ----------
function Donut({ data, size = 168, thickness = 26, centerLabel, centerSub }) {
  const [tip, setTip] = useChartTip();
  const total = data.reduce((s, d) => s + d.value, 0) || 1;
  const r = (size - thickness) / 2;
  const c = 2 * Math.PI * r;
  let acc = 0;
  return (
    <div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', width: size, height: size }}>
      {tip}
      <svg width={size} height={size} style={{ transform: 'rotate(-90deg)' }}>
        <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="hsl(var(--muted))" strokeWidth={thickness} />
        {data.map((d, i) => {
          const frac = d.value / total;
          const dash = frac * c;
          const off = acc * c;
          acc += frac;
          return (
            <circle key={i} cx={size / 2} cy={size / 2} r={r} fill="none" stroke={d.cor} strokeWidth={thickness}
              strokeDasharray={`${dash} ${c - dash}`}
              strokeDashoffset={-off} style={{ transition: 'stroke-width .12s' }}
              onMouseMove={(e) => setTip({ x: e.clientX, y: e.clientY, content: `${d.label}: ${fmtInt(d.value)} (${Math.round(frac * 100)}%)` })}
              onMouseLeave={() => setTip(null)} />
          );
        })}
      </svg>
      {centerLabel != null && (
        <div style={{ position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', textAlign: 'center' }}>
          <div>
            <div className="tnum" style={{ fontSize: 24, fontWeight: 600, letterSpacing: '-.02em' }}>{centerLabel}</div>
            {centerSub && <div style={{ fontSize: 11.5, color: 'hsl(var(--muted-foreground))', marginTop: 2 }}>{centerSub}</div>}
          </div>
        </div>
      )}
    </div>
  );
}

// ---------- Ranking horizontal ----------
function RankBars({ data, valueFmt = fmtInt, max: maxProp }) {
  const max = maxProp ?? Math.max(...data.map(d => d.value), 1);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 11 }}>
      {data.map((d, i) => (
        <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          <div style={{ width: 130, display: 'flex', alignItems: 'center', gap: 7, flexShrink: 0, overflow: 'hidden' }}>
            {d.dot && <SpeciesDot cor={d.dot} />}
            {d.avatar}
            <span style={{ fontSize: 12.5, fontWeight: 450, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{d.label}</span>
          </div>
          <div style={{ flex: 1, height: 18, background: 'hsl(var(--muted))', borderRadius: 4, overflow: 'hidden', position: 'relative' }}>
            <div style={{ position: 'absolute', inset: 0, width: `${(d.value / max) * 100}%`, background: d.cor || 'hsl(var(--primary) / .8)', borderRadius: 4, transition: 'width .5s cubic-bezier(.16,1,.3,1)' }} />
          </div>
          <span className="tnum" style={{ width: 52, textAlign: 'right', fontSize: 12.5, fontWeight: 500, flexShrink: 0 }}>{valueFmt(d.value)}</span>
        </div>
      ))}
    </div>
  );
}

// ---------- Sparkline (área) ----------
function Sparkline({ data, width = 120, height = 34, color = 'hsl(var(--primary))' }) {
  const max = Math.max(...data, 1), min = Math.min(...data, 0);
  const rng = max - min || 1;
  const pts = data.map((v, i) => [i / (data.length - 1) * width, height - ((v - min) / rng) * (height - 4) - 2]);
  const line = pts.map((p, i) => `${i ? 'L' : 'M'}${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' ');
  const area = `${line} L${width} ${height} L0 ${height} Z`;
  const id = useMemo(() => 'sp' + Math.random().toString(36).slice(2, 7), []);
  return (
    <svg width={width} height={height} style={{ display: 'block' }}>
      <defs><linearGradient id={id} x1="0" y1="0" x2="0" y2="1">
        <stop offset="0%" stopColor={color} stopOpacity="0.18" /><stop offset="100%" stopColor={color} stopOpacity="0" />
      </linearGradient></defs>
      <path d={area} fill={`url(#${id})`} />
      <path d={line} fill="none" stroke={color} strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round" />
    </svg>
  );
}

// ---------- Mapa de calor geográfico (capturas por coordenada) ----------
function HeatMap({ records, locais, height = 320, onPointHover }) {
  const [tip, setTip] = useChartTip();
  // bounding box a partir dos locais
  const lats = locais.map(l => l.lat), lngs = locais.map(l => l.lng);
  const pad = 0.6;
  const minLat = Math.min(...lats) - pad, maxLat = Math.max(...lats) + pad;
  const minLng = Math.min(...lngs) - pad, maxLng = Math.max(...lngs) + pad;
  const W = 1000, H = 620;
  const proj = (lat, lng) => [
    ((lng - minLng) / (maxLng - minLng)) * W,
    H - ((lat - minLat) / (maxLat - minLat)) * H,
  ];
  // agregação por local
  const agg = useMemo(() => {
    const m = {};
    for (const r of records) { m[r.local] = (m[r.local] || 0) + 1; }
    return locais.map(l => ({ ...l, n: m[l.nome] || 0 })).filter(l => l.n > 0);
  }, [records, locais]);
  const maxN = Math.max(...agg.map(a => a.n), 1);

  return (
    <div style={{ position: 'relative', width: '100%', height, borderRadius: 'var(--radius)', overflow: 'hidden', border: '1px solid hsl(var(--border))', background: 'linear-gradient(160deg, #f0f6fb, #e7f0f6)' }}>
      {tip}
      <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMid slice" width="100%" height="100%" style={{ display: 'block' }}>
        <defs>
          <radialGradient id="heatblob" cx="50%" cy="50%" r="50%">
            <stop offset="0%" stopColor="hsl(8 88% 55%)" stopOpacity="0.62" />
            <stop offset="38%" stopColor="hsl(30 95% 55%)" stopOpacity="0.42" />
            <stop offset="70%" stopColor="hsl(48 95% 58%)" stopOpacity="0.22" />
            <stop offset="100%" stopColor="hsl(48 95% 58%)" stopOpacity="0" />
          </radialGradient>
          <pattern id="grat" width={W / 8} height={H / 6} patternUnits="userSpaceOnUse">
            <path d={`M ${W / 8} 0 L 0 0 0 ${H / 6}`} fill="none" stroke="hsl(201 40% 70%)" strokeOpacity="0.25" strokeWidth="1" />
          </pattern>
        </defs>
        {/* graticula */}
        <rect width={W} height={H} fill="url(#grat)" />
        {/* heat blobs */}
        {agg.map((a, i) => {
          const [x, y] = proj(a.lat, a.lng);
          const rad = 70 + (a.n / maxN) * 140;
          return <circle key={'b' + i} cx={x} cy={y} r={rad} fill="url(#heatblob)" />;
        })}
        {/* pontos + rótulos */}
        {agg.map((a, i) => {
          const [x, y] = proj(a.lat, a.lng);
          const sz = 4 + (a.n / maxN) * 7;
          return (
            <g key={'p' + i} style={{ cursor: 'pointer' }}
              onMouseMove={(e) => setTip({ x: e.clientX, y: e.clientY, content: `${a.nome} · ${fmtInt(a.n)} capturas` })}
              onMouseLeave={() => setTip(null)}>
              <circle cx={x} cy={y} r={sz + 4} fill="#fff" fillOpacity="0.5" />
              <circle cx={x} cy={y} r={sz} fill="hsl(201 96% 32%)" stroke="#fff" strokeWidth="2" />
            </g>
          );
        })}
      </svg>
      {/* labels HTML overlay para nitidez */}
      <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
        {agg.map((a, i) => {
          const [x, y] = proj(a.lat, a.lng);
          return (
            <span key={'l' + i} style={{
              position: 'absolute', left: `${(x / W) * 100}%`, top: `${(y / H) * 100}%`,
              transform: 'translate(10px,-50%)', fontSize: 11, fontWeight: 600, color: 'hsl(201 90% 24%)',
              textShadow: '0 1px 3px #fff, 0 0 4px #fff', whiteSpace: 'nowrap',
            }}>{a.nome}</span>
          );
        })}
      </div>
      {/* legenda */}
      <div style={{ position: 'absolute', right: 12, bottom: 12, display: 'flex', alignItems: 'center', gap: 8, background: 'hsl(0 0% 100% / .82)', backdropFilter: 'blur(4px)', padding: '6px 10px', borderRadius: 7, border: '1px solid hsl(var(--border))', fontSize: 11, color: 'hsl(var(--muted-foreground))' }}>
        <span>Menos</span>
        <div style={{ width: 90, height: 8, borderRadius: 99, background: 'linear-gradient(90deg, hsl(48 95% 65%), hsl(30 95% 55%), hsl(8 88% 52%))' }} />
        <span>Mais capturas</span>
      </div>
    </div>
  );
}

Object.assign(window, { BarChart, Donut, RankBars, Sparkline, HeatMap, useChartTip });

})();

/* ===== shell.jsx ===== */
;(function(){
/* ============ FTrack — shell: sidebar + topbar ============ */

const NAV = [
  { group: null, items: [
    { id: 'dashboard', label: 'Dashboard', icon: 'dashboard' },
    { id: 'registros', label: 'Registros', icon: 'table', badge: () => fmtInt(window.FT.RECORDS.length) },
    { id: 'relatorios', label: 'Relatórios', icon: 'fileText' },
  ]},
  { group: 'Cadastros', items: [
    { id: 'especies', label: 'Espécies', icon: 'fish' },
    { id: 'operacoes', label: 'Operações', icon: 'ship' },
    { id: 'usuarios', label: 'Usuários', icon: 'users' },
  ]},
];

function Logo({ compact }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 9 }}>
      <div style={{ width: 30, height: 30, borderRadius: 8, background: 'hsl(var(--primary))', display: 'grid', placeItems: 'center', color: '#fff', flexShrink: 0, boxShadow: 'var(--shadow-sm)' }}>
        <Icon name="fishSimple" size={18} strokeWidth={2} />
      </div>
      {!compact && (
        <div style={{ lineHeight: 1.1 }}>
          <div style={{ fontWeight: 700, fontSize: 15, letterSpacing: '-.02em' }}>FTrack</div>
          <div style={{ fontSize: 10.5, color: 'hsl(var(--muted-foreground))', fontWeight: 500 }}>Pesca esportiva</div>
        </div>
      )}
    </div>
  );
}

function NavItem({ item, active, onClick }) {
  const [hover, setHover] = useState(false);
  return (
    <button onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{
        display: 'flex', alignItems: 'center', gap: 9, width: '100%', height: 33, padding: '0 9px',
        borderRadius: 'var(--radius-sm)', border: 'none', textAlign: 'left',
        background: active ? 'hsl(var(--primary-50))' : hover ? 'hsl(var(--muted))' : 'transparent',
        color: active ? 'hsl(var(--primary-700))' : 'hsl(var(--foreground))',
        fontSize: 13.5, fontWeight: active ? 600 : 450, position: 'relative', transition: 'background .1s',
      }}>
      {active && <span style={{ position: 'absolute', left: -8, top: '50%', transform: 'translateY(-50%)', width: 3, height: 18, borderRadius: 99, background: 'hsl(var(--primary))' }} />}
      <Icon name={item.icon} size={16.5} style={{ color: active ? 'hsl(var(--primary))' : 'hsl(var(--muted-foreground))' }} />
      <span style={{ flex: 1 }}>{item.label}</span>
      {item.badge && <span className="tnum" style={{ fontSize: 11, fontWeight: 500, color: 'hsl(var(--muted-foreground))', background: active ? 'hsl(var(--background))' : 'hsl(var(--muted))', padding: '1px 6px', borderRadius: 5 }}>{item.badge()}</span>}
    </button>
  );
}

function Sidebar({ current, onNavigate, user, onLogout }) {
  return (
    <aside style={{
      width: 'var(--sidebar-w)', flexShrink: 0, height: '100%', background: 'hsl(var(--background))',
      borderRight: '1px solid hsl(var(--border))', display: 'flex', flexDirection: 'column',
    }}>
      <div style={{ height: 'var(--topbar-h)', display: 'flex', alignItems: 'center', padding: '0 16px', borderBottom: '1px solid hsl(var(--border))' }}>
        <Logo />
      </div>
      <nav style={{ flex: 1, overflowY: 'auto', padding: '12px 12px' }}>
        {NAV.map((sec, si) => (
          <div key={si} style={{ marginBottom: 14 }}>
            {sec.group && <div style={{ fontSize: 10.5, fontWeight: 600, letterSpacing: '.06em', textTransform: 'uppercase', color: 'hsl(var(--muted-foreground))', padding: '0 9px', marginBottom: 6 }}>{sec.group}</div>}
            <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
              {sec.items.map(it => <NavItem key={it.id} item={it} active={current === it.id} onClick={() => onNavigate(it.id)} />)}
            </div>
          </div>
        ))}
      </nav>
      <div style={{ padding: 10, borderTop: '1px solid hsl(var(--border))' }}>
        <Popover align="left" width={210} trigger={(open, toggle) => (
          <button onClick={toggle} style={{ display: 'flex', alignItems: 'center', gap: 9, width: '100%', padding: '7px 8px', borderRadius: 'var(--radius-sm)', border: 'none', background: open ? 'hsl(var(--muted))' : 'transparent', cursor: 'pointer' }}>
            <Avatar name={user.nome} iniciais={user.iniciais} cor="hsl(var(--primary))" size={28} />
            <div style={{ flex: 1, textAlign: 'left', lineHeight: 1.2, overflow: 'hidden' }}>
              <div style={{ fontSize: 12.5, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{user.nome}</div>
              <div style={{ fontSize: 11, color: 'hsl(var(--muted-foreground))' }}>{user.papel}</div>
            </div>
            <Icon name="chevronDown" size={14} style={{ color: 'hsl(var(--muted-foreground))' }} />
          </button>
        )}>
          {(close) => (
            <div style={{ padding: 5 }}>
              {[['user', 'Meu perfil'], ['settings', 'Configurações']].map(([ic, lb]) => (
                <button key={lb} onClick={close} style={menuItemStyle}><Icon name={ic} size={15} style={{ color: 'hsl(var(--muted-foreground))' }} />{lb}</button>
              ))}
              <div style={{ height: 1, background: 'hsl(var(--border))', margin: '5px 0' }} />
              <button onClick={() => { close(); onLogout(); }} style={{ ...menuItemStyle, color: 'hsl(var(--danger))' }}><Icon name="logout" size={15} />Sair</button>
            </div>
          )}
        </Popover>
      </div>
    </aside>
  );
}
const menuItemStyle = { display: 'flex', alignItems: 'center', gap: 9, width: '100%', padding: '7px 8px', borderRadius: 5, border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 13, color: 'hsl(var(--foreground))', textAlign: 'left' };

function Topbar({ title, onSearch }) {
  return (
    <header style={{
      height: 'var(--topbar-h)', flexShrink: 0, borderBottom: '1px solid hsl(var(--border))',
      background: 'hsl(var(--background) / .82)', backdropFilter: 'blur(8px)', position: 'sticky', top: 0, zIndex: 40,
      display: 'flex', alignItems: 'center', gap: 14, padding: '0 18px',
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 7, fontSize: 13, color: 'hsl(var(--muted-foreground))' }}>
        <span>FTrack</span><Icon name="chevronRight" size={13} />
        <span style={{ color: 'hsl(var(--foreground))', fontWeight: 500 }}>{title}</span>
      </div>
      <div style={{ flex: 1 }} />
      <div style={{ width: 250 }}>
        <Input icon="search" placeholder="Buscar registros, guias…" style={{ height: 32, fontSize: 13 }} onChange={(e) => onSearch && onSearch(e.target.value)} />
      </div>
      <button className="focusable" style={iconBtn}><Icon name="refresh" size={16} /></button>
      <button className="focusable" style={{ ...iconBtn, position: 'relative' }}>
        <Icon name="bell" size={16} />
        <span style={{ position: 'absolute', top: 6, right: 6, width: 6, height: 6, borderRadius: 99, background: 'hsl(var(--danger))', border: '1.5px solid hsl(var(--background))' }} />
      </button>
    </header>
  );
}
const iconBtn = { width: 32, height: 32, borderRadius: 'var(--radius-sm)', border: '1px solid transparent', background: 'transparent', color: 'hsl(var(--muted-foreground))', display: 'grid', placeItems: 'center', cursor: 'pointer' };

Object.assign(window, { Sidebar, Topbar, Logo, iconBtn });

})();

/* ===== login.jsx ===== */
;(function(){
/* ============ FTrack — tela de Login ============ */
function LoginScreen({ onLogin }) {
  const [email, setEmail] = useState('admin@ftrack.com.br');
  const [senha, setSenha] = useState('demo1234');
  const [loading, setLoading] = useState(false);
  const submit = (e) => {
    e.preventDefault();
    setLoading(true);
    setTimeout(() => { setLoading(false); onLogin(); }, 650);
  };
  return (
    <div style={{ height: '100%', display: 'flex', background: 'hsl(var(--background))' }}>
      {/* esquerda — formulário */}
      <div style={{ flex: '1 1 50%', display: 'flex', flexDirection: 'column', padding: '32px 40px', minWidth: 0 }}>
        <Logo />
        <div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', maxWidth: 360, margin: '0 auto', width: '100%' }}>
          <div className="anim-fade">
            <h1 style={{ fontSize: 26, fontWeight: 600, letterSpacing: '-.025em' }}>Entrar</h1>
            <p style={{ fontSize: 14, color: 'hsl(var(--muted-foreground))', marginTop: 6 }}>Acesse o painel de gestão de capturas da sua operação.</p>
          </div>
          <form onSubmit={submit} style={{ marginTop: 28, display: 'flex', flexDirection: 'column', gap: 15 }}>
            <Field label="E-mail">
              <Input icon="mail" type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="voce@operacao.com" autoComplete="username" />
            </Field>
            <Field label="Senha" aside={<a href="#" onClick={(e) => e.preventDefault()} style={{ fontSize: 12.5, color: 'hsl(var(--primary))', fontWeight: 500, textDecoration: 'none' }}>Esqueci a senha</a>}>
              <Input icon="lock" type="password" value={senha} onChange={(e) => setSenha(e.target.value)} placeholder="••••••••" autoComplete="current-password" />
            </Field>
            <label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, color: 'hsl(var(--muted-foreground))', cursor: 'pointer', userSelect: 'none' }}>
              <Checkbox checked={true} onChange={() => {}} /> Manter conectado neste dispositivo
            </label>
            <Button type="submit" size="lg" iconRight={loading ? null : 'arrowRight'} style={{ width: '100%', marginTop: 4 }} disabled={loading}>
              {loading ? 'Entrando…' : 'Entrar'}
            </Button>
          </form>
          <div style={{ marginTop: 22, padding: 12, borderRadius: 'var(--radius)', background: 'hsl(var(--muted))', border: '1px dashed hsl(var(--border))' }}>
            <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '.05em', textTransform: 'uppercase', color: 'hsl(var(--muted-foreground))', marginBottom: 5 }}>Acesso de demonstração</div>
            <div className="mono" style={{ fontSize: 12, color: 'hsl(var(--foreground))', lineHeight: 1.6 }}>admin@ftrack.com.br · demo1234</div>
          </div>
          <p style={{ fontSize: 12.5, color: 'hsl(var(--muted-foreground))', marginTop: 22 }}>
            Não tem conta? <a href="#" onClick={(e) => e.preventDefault()} style={{ color: 'hsl(var(--primary))', fontWeight: 500, textDecoration: 'none' }}>Fale com sua operação</a>
          </p>
        </div>
        <div style={{ fontSize: 12, color: 'hsl(var(--muted-foreground))', textAlign: 'center' }}>© 2026 FTrack · Gestão de pesca esportiva</div>
      </div>

      {/* direita — painel de marca */}
      <div style={{ flex: '1 1 50%', position: 'relative', background: 'linear-gradient(155deg, hsl(202 90% 26%), hsl(201 96% 18%))', display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', padding: 40, overflow: 'hidden' }}>
        {/* placeholder de foto (listrado) */}
        <svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', opacity: 0.16 }} aria-hidden="true">
          <defs>
            <pattern id="loginStripe" width="14" height="14" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
              <rect width="14" height="14" fill="transparent" /><line x1="0" y1="0" x2="0" y2="14" stroke="#fff" strokeWidth="6" />
            </pattern>
          </defs>
          <rect width="100%" height="100%" fill="url(#loginStripe)" />
        </svg>
        <div style={{ position: 'absolute', top: 28, left: 40, display: 'inline-flex', alignItems: 'center', gap: 7, color: 'hsl(0 0% 100% / .7)', fontSize: 12, fontFamily: 'var(--font-mono)' }}>
          <Icon name="eye" size={14} /> [ foto: pesca esportiva no Rio Negro ]
        </div>
        <div style={{ position: 'relative', color: '#fff', maxWidth: 440 }} className="anim-fade">
          <div style={{ display: 'flex', gap: 7, marginBottom: 20 }}>
            {[0, 1, 2].map(i => <span key={i} style={{ width: 26, height: 4, borderRadius: 99, background: i === 0 ? '#fff' : 'hsl(0 0% 100% / .3)' }} />)}
          </div>
          <h2 style={{ fontSize: 30, fontWeight: 600, letterSpacing: '-.025em', lineHeight: 1.15 }}>Cada captura, registrada e mensurável.</h2>
          <p style={{ fontSize: 15, color: 'hsl(0 0% 100% / .82)', marginTop: 14, lineHeight: 1.6 }}>
            Seus guias registram peso, espécie e coordenadas direto do barco. Você acompanha tudo aqui — em tempo real.
          </p>
          <div style={{ display: 'flex', gap: 28, marginTop: 28 }}>
            {[['4.180', 'capturas em 2025'], ['14', 'espécies-alvo'], ['7', 'operações ativas']].map(([n, l]) => (
              <div key={l}>
                <div className="tnum" style={{ fontSize: 24, fontWeight: 700, letterSpacing: '-.02em' }}>{n}</div>
                <div style={{ fontSize: 12, color: 'hsl(0 0% 100% / .7)', marginTop: 2 }}>{l}</div>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

function Field({ label, aside, children }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <label style={{ fontSize: 13, fontWeight: 500 }}>{label}</label>
        {aside}
      </div>
      {children}
    </div>
  );
}

Object.assign(window, { LoginScreen });

})();

/* ===== dashboard.jsx ===== */
;(function(){
/* ============ FTrack — Dashboard ============ */
function useDashData() {
  return useMemo(() => {
    const R = window.FT.RECORDS, S = window.FT.speciesById, G = window.FT.guidesById;
    const total = R.length;
    const pesoMedio = R.reduce((s, r) => s + r.peso, 0) / total;
    const pesoTotal = R.reduce((s, r) => s + r.peso, 0);
    const maiorPeixe = R.reduce((a, b) => b.peso > a.peso ? b : a);

    // por mês (últimos 12)
    const ref = new Date(R[0].data); // mais recente
    const months = [];
    for (let i = 11; i >= 0; i--) {
      const d = new Date(ref.getFullYear(), ref.getMonth() - i, 1);
      months.push({ key: `${d.getFullYear()}-${d.getMonth()}`, label: `${MESES[d.getMonth()]}/${String(d.getFullYear()).slice(2)}`, short: MESES[d.getMonth()], value: 0 });
    }
    const mIdx = Object.fromEntries(months.map((m, i) => [m.key, i]));
    R.forEach(r => { const d = new Date(r.data); const k = `${d.getFullYear()}-${d.getMonth()}`; if (k in mIdx) months[mIdx[k]].value++; });
    months[months.length - 1].highlight = true;

    // espécies
    const spArr = window.FT.SPECIES.map(s => ({ ...s })).sort((a, b) => b.capturas - a.capturas);
    const topSp = spArr[0];

    // guias top
    const topGuias = window.FT.GUIDES.slice().sort((a, b) => b.capturas - a.capturas).slice(0, 6);

    // sparkline últimos 12
    const spark = months.map(m => m.value);

    return { total, pesoMedio, pesoTotal, maiorPeixe, months, spArr, topSp, topGuias, spark, S, G };
  }, []);
}

function Panel({ title, sub, children, action, style = {}, bodyStyle = {} }) {
  return (
    <Card pad={false} style={{ display: 'flex', flexDirection: 'column', ...style }}>
      {(title || action) && (
        <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 10, padding: '13px 16px', borderBottom: '1px solid hsl(var(--border))' }}>
          <div>
            <h3 style={{ fontSize: 14, fontWeight: 600 }}>{title}</h3>
            {sub && <p style={{ fontSize: 12, color: 'hsl(var(--muted-foreground))', marginTop: 2 }}>{sub}</p>}
          </div>
          {action}
        </div>
      )}
      <div style={{ padding: 16, flex: 1, ...bodyStyle }}>{children}</div>
    </Card>
  );
}

function RecentTable({ data, rows = 8 }) {
  const R = window.FT.RECORDS.slice(0, rows);
  return (
    <div>
      {R.map((r, i) => {
        const sp = data.S[r.especieId], g = data.G[r.guiaId];
        return (
          <div key={r.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '7px 0', borderBottom: i < R.length - 1 ? '1px solid hsl(var(--border))' : 'none' }}>
            <SpeciesDot cor={sp.cor} size={9} />
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 13, fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{sp.nome}</div>
              <div style={{ fontSize: 11.5, color: 'hsl(var(--muted-foreground))' }}>{r.local} · {g.nome.split(' ')[0]}</div>
            </div>
            <div style={{ textAlign: 'right' }}>
              <div className="tnum" style={{ fontSize: 13, fontWeight: 600 }}>{fmtKg(r.peso)}</div>
              <div className="tnum" style={{ fontSize: 11, color: 'hsl(var(--muted-foreground))' }}>{fmtTime(r.data)}</div>
            </div>
          </div>
        );
      })}
    </div>
  );
}

function SpeciesLegend({ spArr, total }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 7, flex: 1 }}>
      {spArr.slice(0, 6).map(s => (
        <div key={s.id} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5 }}>
          <SpeciesDot cor={s.cor} />
          <span style={{ flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{s.nome}</span>
          <span className="tnum" style={{ fontWeight: 600 }}>{Math.round(s.capturas / total * 100)}%</span>
        </div>
      ))}
    </div>
  );
}

function Dashboard({ layout = 'padrao', onNavigate }) {
  const d = useDashData();
  const donutData = d.spArr.slice(0, 7).map(s => ({ label: s.nome, value: s.capturas, cor: s.cor }));
  const period = (
    <Select size="sm" value="12m" onChange={() => {}} options={[
      { value: '12m', label: 'Últimos 12 meses' }, { value: '6m', label: 'Últimos 6 meses' }, { value: 'ytd', label: 'Este ano' },
    ]} minWidth={150} align="right" />
  );

  const kpis = (
    <>
      <Stat label="Total de capturas" value={fmtInt(d.total)} delta="+12,4%" deltaTone="up" icon="fish" accent />
      <Stat label="Peso médio" value={fmtKg(d.pesoMedio).replace(' kg', '')} sub="kg" delta="+0,3 kg" deltaTone="up" icon="scale" />
      <Stat label="Espécie mais comum" value={d.topSp.nome} sub={`${fmtInt(d.topSp.capturas)}`} icon="award" />
      <Stat label="Maior captura" value={fmtKg(d.maiorPeixe.peso).replace(' kg', '')} sub={`kg · ${d.S[d.maiorPeixe.especieId].nome}`} icon="trophy" />
    </>
  );

  const heatPanel = (
    <Panel title="Mapa de calor de capturas" sub="Concentração por ponto de pesca (lat/long)" style={{ height: '100%' }}
      action={<Badge tone="primary" dot="hsl(var(--primary))">{window.FT.LOCAIS.length} pontos</Badge>}>
      <HeatMap records={window.FT.RECORDS} locais={window.FT.LOCAIS} height={layout === 'mapa' ? 420 : 300} />
    </Panel>
  );
  const monthsPanel = (
    <Panel title="Capturas por mês" sub="Sazonalidade — pico na seca amazônica (jul–nov)"
      action={<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'hsl(var(--muted-foreground))' }}><Sparkline data={d.spark} width={70} height={22} /></div>}>
      <BarChart data={d.months} height={layout === 'compacto' ? 180 : 230} />
    </Panel>
  );
  const donutPanel = (
    <Panel title="Distribuição por espécie" style={{ height: '100%' }} action={<a onClick={() => onNavigate('especies')} style={linkStyle}>Ver todas</a>}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 20 }}>
        <Donut data={donutData} centerLabel={fmtInt(d.total)} centerSub="capturas" />
        <SpeciesLegend spArr={d.spArr} total={d.total} />
      </div>
    </Panel>
  );
  const guidesPanel = (
    <Panel title="Ranking de guias" sub="Capturas registradas" style={{ height: '100%' }} action={<a onClick={() => onNavigate('usuarios')} style={linkStyle}>Ver todos</a>}>
      <RankBars data={d.topGuias.map(g => ({ label: g.nome, value: g.capturas, avatar: <Avatar name={g.nome} iniciais={g.iniciais} size={20} /> }))} />
    </Panel>
  );
  const recentPanel = (
    <Panel title="Registros recentes" style={{ height: '100%' }} action={<a onClick={() => onNavigate('registros')} style={linkStyle}>Ver tudo</a>}>
      <RecentTable data={d} rows={layout === 'mapa' ? 6 : 7} />
    </Panel>
  );

  return (
    <div className="anim-fade">
      <PageHeader title="Dashboard" subtitle="Visão geral das capturas da temporada" actions={<>{period}<Button variant="secondary" size="md" icon="download">Exportar</Button></>} />

      {/* KPIs */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 14, marginBottom: 14 }}>{kpis}</div>

      {layout === 'padrao' && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
          <div style={{ display: 'grid', gridTemplateColumns: '1.55fr 1fr', gap: 14 }}>
            {monthsPanel}{donutPanel}
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: '1.55fr 1fr', gap: 14, alignItems: 'stretch' }}>
            {heatPanel}
            <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>{guidesPanel}{recentPanel}</div>
          </div>
        </div>
      )}

      {layout === 'compacto' && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 14 }}>
            <div style={{ gridColumn: 'span 1' }}>{donutPanel}</div>
            <div style={{ gridColumn: 'span 2' }}>{monthsPanel}</div>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 14 }}>
            {guidesPanel}{recentPanel}{heatPanel}
          </div>
        </div>
      )}

      {layout === 'mapa' && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
          {heatPanel}
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 14 }}>
            {monthsPanel}{donutPanel}
            <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>{recentPanel}</div>
          </div>
          {guidesPanel}
        </div>
      )}
    </div>
  );
}

const linkStyle = { fontSize: 12.5, color: 'hsl(var(--primary))', fontWeight: 500, cursor: 'pointer', textDecoration: 'none' };

Object.assign(window, { Dashboard });

})();

/* ===== records.jsx ===== */
;(function(){
/* ============ FTrack — Registros (tabela ultra-compacta, virtualizada) ============ */

// ---------- Filtro facetado (popover multi-seleção com contagem) ----------
function FacetFilter({ label, icon, options, selected, onChange }) {
  const [q, setQ] = useState('');
  const filt = options.filter(o => o.label.toLowerCase().includes(q.toLowerCase()));
  const count = selected.size;
  return (
    <Popover width={244} trigger={(open, toggle) => (
      <button onClick={toggle} className="focusable" style={{
        display: 'inline-flex', alignItems: 'center', gap: 6, height: 32, padding: '0 10px',
        borderRadius: 'var(--radius-sm)', border: `1px solid ${count ? 'hsl(var(--primary) / .4)' : 'hsl(var(--border))'}`,
        background: count ? 'hsl(var(--primary-50))' : 'hsl(var(--background))', color: 'hsl(var(--foreground))',
        fontSize: 13, fontWeight: 450, borderStyle: count ? 'solid' : 'dashed',
      }}>
        <Icon name={icon || 'plus'} size={14} style={{ color: count ? 'hsl(var(--primary))' : 'hsl(var(--muted-foreground))' }} />
        {label}
        {count > 0 && <><span style={{ width: 1, height: 14, background: 'hsl(var(--border))', margin: '0 2px' }} /><span style={{ display: 'inline-flex', gap: 4 }}>{count <= 2 ? [...selected].slice(0, 2).map(s => <Badge key={s} tone="primary">{options.find(o => o.value === s)?.label || s}</Badge>) : <Badge tone="primary">{count} selec.</Badge>}</span></>}
      </button>
    )}>
      {(close) => (
        <div>
          <div style={{ padding: 8, borderBottom: '1px solid hsl(var(--border))' }}>
            <Input icon="search" placeholder={`Filtrar ${label.toLowerCase()}…`} value={q} onChange={(e) => setQ(e.target.value)} style={{ height: 30, fontSize: 12.5 }} autoFocus />
          </div>
          <div style={{ maxHeight: 260, overflowY: 'auto', padding: 4 }}>
            {filt.map(o => {
              const on = selected.has(o.value);
              return (
                <div key={o.value} onClick={() => { const n = new Set(selected); on ? n.delete(o.value) : n.add(o.value); onChange(n); }}
                  style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 7px', borderRadius: 5, cursor: 'pointer' }}
                  onMouseEnter={(e) => e.currentTarget.style.background = 'hsl(var(--muted))'} onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
                  <Checkbox checked={on} onChange={() => {}} size={14} />
                  {o.dot && <SpeciesDot cor={o.dot} />}
                  {o.avatar}
                  <span style={{ flex: 1, fontSize: 12.5, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{o.label}</span>
                  <span className="tnum" style={{ fontSize: 11, color: 'hsl(var(--muted-foreground))' }}>{fmtInt(o.count)}</span>
                </div>
              );
            })}
            {filt.length === 0 && <div style={{ padding: 14, textAlign: 'center', fontSize: 12.5, color: 'hsl(var(--muted-foreground))' }}>Nenhum resultado</div>}
          </div>
          {count > 0 && (
            <div style={{ borderTop: '1px solid hsl(var(--border))', padding: 6 }}>
              <button onClick={() => onChange(new Set())} style={{ width: '100%', height: 28, border: 'none', background: 'transparent', color: 'hsl(var(--foreground))', fontSize: 12.5, borderRadius: 5, cursor: 'pointer' }}
                onMouseEnter={(e) => e.currentTarget.style.background = 'hsl(var(--muted))'} onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>Limpar seleção</button>
            </div>
          )}
        </div>
      )}
    </Popover>
  );
}

const STATUS_REMOVED = true;

function Registros({ density = 'ultra', onNew }) {
  const FT = window.FT;
  const ALL = FT.RECORDS;
  const maxTs = new Date(ALL[0].data).getTime();
  const [search, setSearch] = useState('');
  const [fSpecies, setFSpecies] = useState(new Set());
  const [fOp, setFOp] = useState(new Set());
  const [fGuia, setFGuia] = useState(new Set());
  const [range, setRange] = useState({ key: 'all', from: null, to: null });
  const [sort, setSort] = useState({ key: 'data', dir: 'desc' });
  const [selected, setSelected] = useState(new Set());
  const [scrollTop, setScrollTop] = useState(0);
  const scrollRef = useRef(null);

  const rowH = density === 'ultra' ? 30 : 36;

  // contagens p/ facetas (dataset completo)
  const speciesOpts = FT.SPECIES.map(s => ({ value: s.id, label: s.nome, dot: s.cor, count: s.capturas })).sort((a, b) => b.count - a.count);
  const opOpts = FT.OPERATIONS.map(o => ({ value: o.id, label: o.nome, count: o.capturas })).sort((a, b) => b.count - a.count);
  const guiaOpts = FT.GUIDES.map(g => ({ value: g.id, label: g.nome, avatar: <Avatar name={g.nome} iniciais={g.iniciais} size={18} />, count: g.capturas })).sort((a, b) => b.count - a.count);

  // filtragem
  const filtered = useMemo(() => {
    let fromTs = null, toTs = null;
    if (range.key === 'custom') { fromTs = range.from; toTs = range.to + 864e5 - 1; }
    else if (range.key !== 'all') { const r = dpRange(range.key, maxTs); fromTs = r.from; toTs = r.to; }
    const q = search.trim().toLowerCase();
    let res = ALL.filter(r => {
      if (fSpecies.size && !fSpecies.has(r.especieId)) return false;
      if (fOp.size && !fOp.has(r.operacaoId)) return false;
      if (fGuia.size && !fGuia.has(r.guiaId)) return false;
      const ts = new Date(r.data).getTime();
      if (fromTs && ts < fromTs) return false;
      if (toTs && ts > toTs) return false;
      if (q) {
        const sp = FT.speciesById[r.especieId], g = FT.guidesById[r.guiaId];
        const hay = `${r.code} ${sp.nome} ${g.nome} ${r.local} ${r.isca}`.toLowerCase();
        if (!hay.includes(q)) return false;
      }
      return true;
    });
    const dir = sort.dir === 'asc' ? 1 : -1;
    const getv = (r) => {
      switch (sort.key) {
        case 'data': return new Date(r.data).getTime();
        case 'especie': return FT.speciesById[r.especieId].nome;
        case 'guia': return FT.guidesById[r.guiaId].nome;
        case 'operacao': return FT.opsById[r.operacaoId].nome;
        case 'local': return r.local;
        default: return r[sort.key];
      }
    };
    res = res.slice().sort((a, b) => { const va = getv(a), vb = getv(b); return va < vb ? -dir : va > vb ? dir : 0; });
    return res;
  }, [search, fSpecies, fOp, fGuia, range, sort]);

  // virtualização
  const VH = 100000; // computed dynamically below by container; approximate buffer
  const containerH = (scrollRef.current && scrollRef.current.clientHeight) || 560;
  const visCount = Math.ceil(containerH / rowH) + 8;
  const start = Math.max(0, Math.floor(scrollTop / rowH) - 4);
  const slice = filtered.slice(start, start + visCount);

  const activeFilters = fSpecies.size + fOp.size + fGuia.size + (range.key !== 'all' ? 1 : 0);
  const clearAll = () => { setFSpecies(new Set()); setFOp(new Set()); setFGuia(new Set()); setRange({ key: 'all', from: null, to: null }); setSearch(''); };

  // colunas — largura mínima + peso de expansão (fr) para preencher a linha toda
  const cols = [
    { id: 'check', w: 34, header: <Checkbox size={14} checked={selected.size > 0 && selected.size === filtered.length} indeterminate={selected.size > 0 && selected.size < filtered.length} onChange={(v) => setSelected(v ? new Set(filtered.map(r => r.id)) : new Set())} /> },
    { id: 'code', w: 80, label: '#', sortable: true, mono: true },
    { id: 'data', min: 120, grow: 1, label: 'Data / hora', sortable: true },
    { id: 'especie', min: 150, grow: 1.6, label: 'Espécie', sortable: true },
    { id: 'peso', w: 76, label: 'Peso', align: 'right', sortable: true },
    { id: 'comprimento', w: 70, label: 'Comp.', align: 'right', sortable: true },
    { id: 'guia', min: 140, grow: 1.5, label: 'Guia', sortable: true },
    { id: 'operacao', min: 138, grow: 1.4, label: 'Operação', sortable: true },
    { id: 'local', min: 120, grow: 1.2, label: 'Local', sortable: true },
    { id: 'coord', min: 120, grow: 1.1, label: 'Coordenadas' },
    { id: 'isca', min: 92, grow: 1, label: 'Isca' },
    { id: 'act', w: 40, label: '' },
  ];
  const [hiddenCols, setHiddenCols] = useState(new Set());
  const visCols = cols.filter(c => !hiddenCols.has(c.id));
  const gridTemplate = visCols.map(c => c.w != null ? c.w + 'px' : `minmax(${c.min}px, ${c.grow}fr)`).join(' ');
  const minW = visCols.reduce((s, c) => s + (c.w != null ? c.w : c.min), 0);

  const toggleSort = (key) => setSort(s => s.key === key ? { key, dir: s.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: key === 'data' || key === 'peso' || key === 'comprimento' ? 'desc' : 'asc' });

  const renderCell = (c, r) => {
    const sp = FT.speciesById[r.especieId], g = FT.guidesById[r.guiaId], op = FT.opsById[r.operacaoId];
    switch (c.id) {
      case 'check': return <Checkbox size={14} checked={selected.has(r.id)} onChange={(v) => { const n = new Set(selected); v ? n.add(r.id) : n.delete(r.id); setSelected(n); }} />;
      case 'code': return <span className="mono" style={{ fontSize: 11.5, color: 'hsl(var(--muted-foreground))' }}>{r.code}</span>;
      case 'data': return <span className="tnum" style={{ fontSize: 12 }}>{fmtDate(r.data)} <span style={{ color: 'hsl(var(--muted-foreground))' }}>{fmtTime(r.data)}</span></span>;
      case 'especie': return <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7, minWidth: 0 }}><SpeciesDot cor={sp.cor} /><span style={{ fontSize: 12.5, fontWeight: 450, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{sp.nome}</span></span>;
      case 'peso': return <span className="tnum" style={{ fontSize: 12.5, fontWeight: 600 }}>{nf1.format(r.peso)}</span>;
      case 'comprimento': return <span className="tnum" style={{ fontSize: 12.5, color: 'hsl(var(--muted-foreground))' }}>{r.comprimento}</span>;
      case 'guia': return <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, minWidth: 0 }}><Avatar name={g.nome} iniciais={g.iniciais} size={20} /><span style={{ fontSize: 12.5, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{g.nome}</span></span>;
      case 'operacao': return <span style={{ fontSize: 12.5, color: 'hsl(var(--muted-foreground))', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{op.nome}</span>;
      case 'local': return <span style={{ fontSize: 12.5, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.local}</span>;
      case 'coord': return <span className="mono" style={{ fontSize: 11, color: 'hsl(var(--muted-foreground))' }}>{r.lat.toFixed(3)}, {r.lng.toFixed(3)}</span>;
      case 'isca': return <span style={{ fontSize: 12, color: 'hsl(var(--muted-foreground))', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.isca}</span>;
      case 'act': return <button style={{ ...iconBtn, width: 24, height: 24 }} onClick={(e) => e.stopPropagation()}><Icon name="more" size={15} /></button>;
      default: return null;
    }
  };

  return (
    <div className="anim-fade" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
      <PageHeader title="Registros de captura" subtitle={`${fmtInt(filtered.length)} de ${fmtInt(ALL.length)} registros`}
        actions={<><Button variant="secondary" size="md" icon="upload">Importar</Button><Button size="md" icon="plus" onClick={() => onNew && onNew('registro')}>Novo registro</Button></>} />

      {/* toolbar de filtros */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10, flexWrap: 'wrap' }}>
        <DatePicker value={range} onChange={setRange} refDate={maxTs} />
        <div style={{ width: 220 }}><Input icon="search" placeholder="Buscar código, guia, local…" value={search} onChange={(e) => setSearch(e.target.value)} style={{ height: 32 }} /></div>
        <FacetFilter label="Espécie" icon="fish" options={speciesOpts} selected={fSpecies} onChange={setFSpecies} />
        <FacetFilter label="Operação" icon="ship" options={opOpts} selected={fOp} onChange={setFOp} />
        <FacetFilter label="Guia" icon="user" options={guiaOpts} selected={fGuia} onChange={setFGuia} />
        {activeFilters > 0 && <Button variant="ghost" size="sm" icon="x" onClick={clearAll}>Limpar ({activeFilters})</Button>}
        <div style={{ flex: 1 }} />
        <ColumnToggle cols={cols.filter(c => c.label)} hidden={hiddenCols} onChange={setHiddenCols} />
        <Button variant="secondary" size="sm" icon="download">Exportar</Button>
      </div>

      {/* tabela */}
      <Card pad={false} style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minHeight: 0 }}>
        <div ref={scrollRef} onScroll={(e) => setScrollTop(e.target.scrollTop)} style={{ flex: 1, overflow: 'auto', minHeight: 0 }}>
          <div style={{ minWidth: minW, width: '100%' }}>
            {/* header */}
            <div style={{ display: 'grid', gridTemplateColumns: gridTemplate, position: 'sticky', top: 0, zIndex: 10, background: 'hsl(var(--muted) / .6)', backdropFilter: 'blur(6px)', borderBottom: '1px solid hsl(var(--border))', height: 34 }}>
              {visCols.map(c => (
                <div key={c.id} onClick={() => c.sortable && toggleSort(c.id)} style={{
                  display: 'flex', alignItems: 'center', gap: 4, justifyContent: c.align === 'right' ? 'flex-end' : 'flex-start',
                  padding: '0 10px', fontSize: 11.5, fontWeight: 600, color: 'hsl(var(--muted-foreground))',
                  cursor: c.sortable ? 'pointer' : 'default', userSelect: 'none', letterSpacing: '.01em',
                }}>
                  {c.header || c.label}
                  {c.sortable && sort.key === c.id && <Icon name={sort.dir === 'asc' ? 'arrowUp' : 'arrowDown'} size={12} style={{ color: 'hsl(var(--primary))' }} />}
                </div>
              ))}
            </div>
            {/* corpo virtualizado */}
            <div style={{ height: filtered.length * rowH, position: 'relative' }}>
              <div style={{ position: 'absolute', top: start * rowH, left: 0, right: 0 }}>
                {slice.map((r, i) => {
                  const sel = selected.has(r.id);
                  return (
                    <div key={r.id} style={{
                      display: 'grid', gridTemplateColumns: gridTemplate, height: rowH, alignItems: 'center',
                      borderBottom: '1px solid hsl(var(--border) / .6)', background: sel ? 'hsl(var(--primary-50))' : (start + i) % 2 ? 'hsl(var(--muted) / .35)' : 'transparent',
                      cursor: 'default', transition: 'background .08s',
                    }}
                      onMouseEnter={(e) => { if (!sel) e.currentTarget.style.background = 'hsl(var(--muted))'; }}
                      onMouseLeave={(e) => { if (!sel) e.currentTarget.style.background = (start + i) % 2 ? 'hsl(var(--muted) / .35)' : 'transparent'; }}>
                      {visCols.map(c => (
                        <div key={c.id} style={{ padding: '0 10px', display: 'flex', alignItems: 'center', justifyContent: c.align === 'right' ? 'flex-end' : 'flex-start', minWidth: 0, overflow: 'hidden' }}>
                          {renderCell(c, r)}
                        </div>
                      ))}
                    </div>
                  );
                })}
              </div>
            </div>
            {filtered.length === 0 && <div style={{ padding: 60, textAlign: 'center', color: 'hsl(var(--muted-foreground))' }}><Icon name="search" size={28} style={{ opacity: .4 }} /><p style={{ marginTop: 10, fontSize: 13.5 }}>Nenhum registro encontrado com esses filtros.</p></div>}
          </div>
        </div>
        {/* footer */}
        <div style={{ height: 40, flexShrink: 0, borderTop: '1px solid hsl(var(--border))', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 14px', background: 'hsl(var(--background))' }}>
          <span style={{ fontSize: 12.5, color: 'hsl(var(--muted-foreground))' }} className="tnum">
            {selected.size > 0 ? `${fmtInt(selected.size)} selecionado(s)` : `Exibindo ${fmtInt(filtered.length)} registro(s)`}
          </span>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            {selected.size > 0 && <><Button variant="ghost" size="sm" icon="download">Exportar seleção</Button><Button variant="danger" size="sm" icon="x" onClick={() => setSelected(new Set())}>Limpar</Button></>}
            <span style={{ fontSize: 12, color: 'hsl(var(--muted-foreground))' }} className="tnum">Linha {fmtInt(Math.min(start + 1, filtered.length))}–{fmtInt(Math.min(start + visCount, filtered.length))}</span>
          </div>
        </div>
      </Card>
    </div>
  );
}

function ColumnToggle({ cols, hidden, onChange }) {
  return (
    <Popover align="right" width={200} trigger={(open, toggle) => (
      <Button variant="secondary" size="sm" icon="columns" onClick={toggle}>Colunas</Button>
    )}>
      <div style={{ padding: 5 }}>
        {cols.map(c => {
          const vis = !hidden.has(c.id);
          return (
            <div key={c.id} onClick={() => { const n = new Set(hidden); vis ? n.add(c.id) : n.delete(c.id); onChange(n); }}
              style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '6px 8px', borderRadius: 5, cursor: 'pointer', fontSize: 13 }}
              onMouseEnter={(e) => e.currentTarget.style.background = 'hsl(var(--muted))'} onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
              <Checkbox checked={vis} onChange={() => {}} size={14} />{c.label}
            </div>
          );
        })}
      </div>
    </Popover>
  );
}

Object.assign(window, { Registros });

})();

/* ===== registries.jsx ===== */
;(function(){
/* ============ FTrack — Cadastros: Espécies · Operações · Usuários ============ */

// ---------------- ESPÉCIES ----------------
function EspeciesScreen({ onNavigate, onNew }) {
  const FT = window.FT;
  const [q, setQ] = useState('');
  const [cat, setCat] = useState('all');
  const cats = ['all', ...Array.from(new Set(FT.SPECIES.map(s => s.cat)))];
  const maxCap = Math.max(...FT.SPECIES.map(s => s.capturas));
  const list = FT.SPECIES.filter(s => (cat === 'all' || s.cat === cat) && (s.nome.toLowerCase().includes(q.toLowerCase()) || s.cientifico.toLowerCase().includes(q.toLowerCase()))).sort((a, b) => b.capturas - a.capturas);
  const catTone = { 'Predador': 'primary', 'Gigante': 'danger', 'Bagre': 'neutral', 'Escama': 'warning' };
  return (
    <div className="anim-fade">
      <PageHeader title="Espécies" subtitle={`${FT.SPECIES.length} espécies-alvo cadastradas`} actions={<Button size="md" icon="plus" onClick={() => onNew && onNew('especie')}>Nova espécie</Button>} />
      <div style={{ display: 'flex', gap: 8, marginBottom: 16, flexWrap: 'wrap' }}>
        <div style={{ width: 240 }}><Input icon="search" placeholder="Buscar espécie…" value={q} onChange={(e) => setQ(e.target.value)} style={{ height: 32 }} /></div>
        <div style={{ display: 'flex', gap: 5, background: 'hsl(var(--muted))', padding: 3, borderRadius: 'var(--radius-sm)' }}>
          {cats.map(c => (
            <button key={c} onClick={() => setCat(c)} style={{
              height: 26, padding: '0 11px', borderRadius: 5, border: 'none', fontSize: 12.5, fontWeight: 500, cursor: 'pointer',
              background: cat === c ? 'hsl(var(--background))' : 'transparent', color: cat === c ? 'hsl(var(--foreground))' : 'hsl(var(--muted-foreground))',
              boxShadow: cat === c ? 'var(--shadow-sm)' : 'none',
            }}>{c === 'all' ? 'Todas' : c}</button>
          ))}
        </div>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 14 }}>
        {list.map(s => (
          <Card key={s.id} pad={false} style={{ overflow: 'hidden' }}>
            <div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: 15 }}>
              <div style={{ width: 46, height: 46, borderRadius: 10, background: s.cor + '18', display: 'grid', placeItems: 'center', flexShrink: 0 }}>
                <Icon name="fishSimple" size={24} style={{ color: s.cor }} strokeWidth={1.8} />
              </div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
                  <h3 style={{ fontSize: 15, fontWeight: 600 }}>{s.nome}</h3>
                  <Badge tone={catTone[s.cat] || 'neutral'}>{s.cat}</Badge>
                </div>
                <p style={{ fontSize: 12, color: 'hsl(var(--muted-foreground))', fontStyle: 'italic', marginTop: 2 }}>{s.cientifico}</p>
              </div>
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', borderTop: '1px solid hsl(var(--border))' }}>
              {[['Capturas', fmtInt(s.capturas)], ['Peso típ.', `${nf1.format(s.pesoMin)}–${nf1.format(s.pesoMax)} kg`], ['Comp. típ.', `${s.compMin}–${s.compMax} cm`]].map(([l, v], i) => (
                <div key={l} style={{ padding: '10px 12px', borderLeft: i ? '1px solid hsl(var(--border))' : 'none' }}>
                  <div style={{ fontSize: 10.5, color: 'hsl(var(--muted-foreground))', fontWeight: 500, textTransform: 'uppercase', letterSpacing: '.04em' }}>{l}</div>
                  <div className="tnum" style={{ fontSize: 14, fontWeight: 600, marginTop: 3 }}>{v}</div>
                </div>
              ))}
            </div>
            <div style={{ height: 4, background: 'hsl(var(--muted))' }}>
              <div style={{ height: '100%', width: `${(s.capturas / maxCap) * 100}%`, background: s.cor }} />
            </div>
          </Card>
        ))}
      </div>
    </div>
  );
}

// ---------------- OPERAÇÕES ----------------
function OperacoesScreen({ onNew }) {
  const FT = window.FT;
  const [q, setQ] = useState('');
  const list = FT.OPERATIONS.filter(o => o.nome.toLowerCase().includes(q.toLowerCase())).sort((a, b) => b.capturas - a.capturas);
  const tipoTone = { 'Lodge': 'primary', 'Barco-hotel': 'neutral', 'Pousada': 'warning' };
  return (
    <div className="anim-fade">
      <PageHeader title="Operações" subtitle={`${FT.OPERATIONS.filter(o => o.ativa).length} ativas · ${FT.OPERATIONS.length} no total`} actions={<Button size="md" icon="plus" onClick={() => onNew && onNew('operacao')}>Nova operação</Button>} />
      <div style={{ width: 240, marginBottom: 16 }}><Input icon="search" placeholder="Buscar operação…" value={q} onChange={(e) => setQ(e.target.value)} style={{ height: 32 }} /></div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(330px, 1fr))', gap: 14 }}>
        {list.map(o => (
          <Card key={o.id} style={{ padding: 0, overflow: 'hidden' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16 }}>
              <div style={{ width: 44, height: 44, borderRadius: 10, background: 'hsl(var(--primary-50))', display: 'grid', placeItems: 'center', flexShrink: 0 }}>
                <Icon name="ship" size={22} style={{ color: 'hsl(var(--primary))' }} />
              </div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                  <h3 style={{ fontSize: 15, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{o.nome}</h3>
                  <Badge tone={o.ativa ? 'success' : 'neutral'} dot={o.ativa ? 'hsl(var(--success))' : undefined}>{o.ativa ? 'Ativa' : 'Inativa'}</Badge>
                </div>
                <div style={{ display: 'flex', alignItems: 'center', gap: 7, fontSize: 12, color: 'hsl(var(--muted-foreground))', marginTop: 3 }}>
                  <Icon name="mapPin" size={13} />{o.base}
                </div>
              </div>
              <button style={{ ...iconBtn }}><Icon name="more" size={16} /></button>
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', borderTop: '1px solid hsl(var(--border))' }}>
              {[['Tipo', <Badge tone={tipoTone[o.tipo]}>{o.tipo}</Badge>], ['Barcos', fmtInt(o.barcos)], ['Guias', fmtInt(o.guias)], ['Capturas', fmtInt(o.capturas)]].map(([l, v], i) => (
                <div key={l} style={{ padding: '11px 12px', borderLeft: i ? '1px solid hsl(var(--border))' : 'none' }}>
                  <div style={{ fontSize: 10.5, color: 'hsl(var(--muted-foreground))', fontWeight: 500, textTransform: 'uppercase', letterSpacing: '.04em' }}>{l}</div>
                  <div className="tnum" style={{ fontSize: 14, fontWeight: 600, marginTop: 4 }}>{v}</div>
                </div>
              ))}
            </div>
          </Card>
        ))}
      </div>
    </div>
  );
}

// ---------------- USUÁRIOS ----------------
function UsuariosScreen({ onNew }) {
  const FT = window.FT;
  const [q, setQ] = useState('');
  const [fOp, setFOp] = useState('all');
  const [fStatus, setFStatus] = useState('all');
  // monta lista de usuários: admin + guias
  const users = useMemo(() => {
    const arr = [{ id: 'u-admin', nome: 'Marina Couto', iniciais: 'MC', papel: 'Administrador', operacaoId: null, desde: 2021, ativo: true, telefone: '(92) 99812-4471', capturas: 0 }];
    FT.GUIDES.forEach((g, i) => arr.push({ ...g, papel: i % 9 === 0 ? 'Gestor' : 'Guia' }));
    return arr;
  }, []);
  const list = users.filter(u =>
    (fOp === 'all' || u.operacaoId === fOp) &&
    (fStatus === 'all' || (fStatus === 'ativo' ? u.ativo : !u.ativo)) &&
    u.nome.toLowerCase().includes(q.toLowerCase())
  ).sort((a, b) => b.capturas - a.capturas);
  const papelTone = { 'Administrador': 'danger', 'Gestor': 'primary', 'Guia': 'neutral' };

  const cols = [
    { id: 'nome', w: '2fr', label: 'Usuário' },
    { id: 'papel', w: '1fr', label: 'Papel' },
    { id: 'op', w: '1.4fr', label: 'Operação' },
    { id: 'desde', w: '0.8fr', label: 'Desde' },
    { id: 'tel', w: '1.2fr', label: 'Telefone' },
    { id: 'cap', w: '0.9fr', label: 'Capturas', align: 'right' },
    { id: 'status', w: '0.9fr', label: 'Status' },
    { id: 'act', w: '44px', label: '' },
  ];
  const gt = cols.map(c => typeof c.w === 'number' ? c.w + 'px' : c.w).join(' ');

  return (
    <div className="anim-fade">
      <PageHeader title="Usuários" subtitle={`${users.filter(u => u.ativo).length} ativos · guias, gestores e administradores`} actions={<Button size="md" icon="plus" onClick={() => onNew && onNew('usuario')}>Convidar usuário</Button>} />
      <div style={{ display: 'flex', gap: 8, marginBottom: 14, flexWrap: 'wrap' }}>
        <div style={{ width: 240 }}><Input icon="search" placeholder="Buscar por nome…" value={q} onChange={(e) => setQ(e.target.value)} style={{ height: 32 }} /></div>
        <Select size="sm" value={fOp} onChange={setFOp} minWidth={150} options={[{ value: 'all', label: 'Todas operações' }, ...FT.OPERATIONS.map(o => ({ value: o.id, label: o.nome }))]} />
        <Select size="sm" value={fStatus} onChange={setFStatus} minWidth={120} options={[{ value: 'all', label: 'Todos status' }, { value: 'ativo', label: 'Ativos' }, { value: 'inativo', label: 'Inativos' }]} />
      </div>
      <Card pad={false} style={{ overflow: 'hidden' }}>
        <div style={{ overflowX: 'auto' }}>
          <div style={{ minWidth: 880 }}>
            <div style={{ display: 'grid', gridTemplateColumns: gt, height: 38, borderBottom: '1px solid hsl(var(--border))', background: 'hsl(var(--muted) / .5)' }}>
              {cols.map(c => <div key={c.id} style={{ display: 'flex', alignItems: 'center', justifyContent: c.align === 'right' ? 'flex-end' : 'flex-start', padding: '0 14px', fontSize: 11.5, fontWeight: 600, color: 'hsl(var(--muted-foreground))' }}>{c.label}</div>)}
            </div>
            {list.map((u, i) => {
              const op = u.operacaoId ? FT.opsById[u.operacaoId] : null;
              return (
                <div key={u.id} style={{ display: 'grid', gridTemplateColumns: gt, height: 48, alignItems: 'center', borderBottom: i < list.length - 1 ? '1px solid hsl(var(--border) / .6)' : 'none' }}
                  onMouseEnter={(e) => e.currentTarget.style.background = 'hsl(var(--muted) / .5)'} onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '0 14px', minWidth: 0 }}>
                    <Avatar name={u.nome} iniciais={u.iniciais} size={30} cor={u.papel === 'Administrador' ? 'hsl(var(--primary))' : undefined} />
                    <div style={{ minWidth: 0 }}>
                      <div style={{ fontSize: 13, fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{u.nome}</div>
                      <div className="mono" style={{ fontSize: 11, color: 'hsl(var(--muted-foreground))' }}>{u.nome.toLowerCase().replace(/[^a-z ]/g, '').split(' ').slice(0, 2).join('.')}@ftrack</div>
                    </div>
                  </div>
                  <div style={{ padding: '0 14px' }}><Badge tone={papelTone[u.papel]}>{u.papel}</Badge></div>
                  <div style={{ padding: '0 14px', fontSize: 12.5, color: 'hsl(var(--muted-foreground))', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{op ? op.nome : '—'}</div>
                  <div className="tnum" style={{ padding: '0 14px', fontSize: 12.5 }}>{u.desde}</div>
                  <div className="mono" style={{ padding: '0 14px', fontSize: 11.5, color: 'hsl(var(--muted-foreground))' }}>{u.telefone}</div>
                  <div className="tnum" style={{ padding: '0 14px', fontSize: 12.5, fontWeight: 600, textAlign: 'right' }}>{u.capturas ? fmtInt(u.capturas) : '—'}</div>
                  <div style={{ padding: '0 14px' }}><Badge tone={u.ativo ? 'success' : 'neutral'} dot={u.ativo ? 'hsl(var(--success))' : undefined}>{u.ativo ? 'Ativo' : 'Inativo'}</Badge></div>
                  <div style={{ padding: '0 8px' }}><button style={{ ...iconBtn, width: 26, height: 26 }}><Icon name="more" size={15} /></button></div>
                </div>
              );
            })}
          </div>
        </div>
      </Card>
    </div>
  );
}

Object.assign(window, { EspeciesScreen, OperacoesScreen, UsuariosScreen });

})();

/* ===== modals.jsx ===== */
;(function(){
/* ============ FTrack — Modais de cadastro + Toast ============ */

// ---------- Toast (canto inferior direito) ----------
function useToast() {
  const [toasts, setToasts] = useState([]);
  const push = useCallback((msg, tone = 'success') => {
    const id = Math.random().toString(36).slice(2);
    setToasts(t => [...t, { id, msg, tone }]);
    setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3400);
  }, []);
  const node = (
    <div style={{ position: 'fixed', bottom: 18, right: 18, zIndex: 400, display: 'flex', flexDirection: 'column', gap: 8, pointerEvents: 'none' }}>
      {toasts.map(t => (
        <div key={t.id} className="anim-fade" style={{
          display: 'flex', alignItems: 'center', gap: 10, minWidth: 280, maxWidth: 380, padding: '11px 14px',
          background: 'hsl(var(--background))', border: '1px solid hsl(var(--border))', borderRadius: 'var(--radius)',
          boxShadow: 'var(--shadow-lg)', pointerEvents: 'auto',
        }}>
          <span style={{ width: 26, height: 26, borderRadius: 7, flexShrink: 0, display: 'grid', placeItems: 'center', background: 'hsl(var(--success-bg))', color: 'hsl(var(--success))' }}><Icon name="fileCheck" size={15} /></span>
          <span style={{ fontSize: 13, fontWeight: 450, lineHeight: 1.4 }}>{t.msg}</span>
        </div>
      ))}
    </div>
  );
  return [node, push];
}

// ---------- Helpers de formulário ----------
const inputCls = { height: 34 };
function fieldGrid(children, cols = 2) {
  return <div style={{ display: 'grid', gridTemplateColumns: `repeat(${cols}, 1fr)`, gap: 14 }}>{children}</div>;
}

// ---------- Form: Espécie ----------
function EspecieForm({ onChange }) {
  const [f, setF] = useState({ nome: '', cientifico: '', cat: 'Predador', cor: '#0369a1', pesoMin: '', pesoMax: '', compMin: '', compMax: '' });
  useEffect(() => onChange(f), [f]);
  const set = (k) => (e) => setF(s => ({ ...s, [k]: e.target ? e.target.value : e }));
  const CORES = ['#0369a1', '#0891b2', '#06b6d4', '#7c3aed', '#0d9488', '#65a30d', '#ca8a04', '#dc2626', '#ea580c', '#475569'];
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      {fieldGrid(<>
        <FormField label="Nome popular" required><Input value={f.nome} onChange={set('nome')} placeholder="Ex: Tucunaré-açu" style={inputCls} /></FormField>
        <FormField label="Categoria" required><Select value={f.cat} onChange={(v) => setF(s => ({ ...s, cat: v }))} options={['Predador', 'Gigante', 'Bagre', 'Escama']} /></FormField>
      </>)}
      <FormField label="Nome científico"><Input value={f.cientifico} onChange={set('cientifico')} placeholder="Ex: Cichla temensis" style={{ ...inputCls, fontStyle: 'italic' }} /></FormField>
      {fieldGrid(<>
        <FormField label="Peso típico (kg)" hint="mínimo – máximo">
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}><Input type="number" value={f.pesoMin} onChange={set('pesoMin')} placeholder="0,5" style={inputCls} /><span style={{ color: 'hsl(var(--muted-foreground))' }}>–</span><Input type="number" value={f.pesoMax} onChange={set('pesoMax')} placeholder="12,0" style={inputCls} /></div>
        </FormField>
        <FormField label="Comprimento típico (cm)" hint="mínimo – máximo">
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}><Input type="number" value={f.compMin} onChange={set('compMin')} placeholder="30" style={inputCls} /><span style={{ color: 'hsl(var(--muted-foreground))' }}>–</span><Input type="number" value={f.compMax} onChange={set('compMax')} placeholder="98" style={inputCls} /></div>
        </FormField>
      </>)}
      <FormField label="Cor no painel" hint="usada nos gráficos e indicadores">
        <div style={{ display: 'flex', gap: 7, flexWrap: 'wrap' }}>
          {CORES.map(c => (
            <button key={c} onClick={() => setF(s => ({ ...s, cor: c }))} style={{ width: 28, height: 28, borderRadius: 7, background: c, border: f.cor === c ? '2px solid hsl(var(--foreground))' : '2px solid transparent', cursor: 'pointer', outline: f.cor === c ? '2px solid hsl(var(--background))' : 'none', outlineOffset: -4 }} />
          ))}
        </div>
      </FormField>
    </div>
  );
}

// ---------- Form: Operação ----------
function OperacaoForm({ onChange }) {
  const [f, setF] = useState({ nome: '', tipo: 'Lodge', cidade: '', uf: 'AM', barcos: '', cnpj: '', ativa: true });
  useEffect(() => onChange(f), [f]);
  const set = (k) => (e) => setF(s => ({ ...s, [k]: e.target ? e.target.value : e }));
  const UFS = ['AM', 'PA', 'RR', 'RO', 'AC', 'MT', 'TO'];
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <FormField label="Nome da operação" required><Input icon="ship" value={f.nome} onChange={set('nome')} placeholder="Ex: Lodge Rio Negro" style={inputCls} /></FormField>
      {fieldGrid(<>
        <FormField label="Tipo" required><Select value={f.tipo} onChange={(v) => setF(s => ({ ...s, tipo: v }))} options={['Lodge', 'Barco-hotel', 'Pousada']} /></FormField>
        <FormField label="CNPJ"><Input value={f.cnpj} onChange={set('cnpj')} placeholder="00.000.000/0001-00" style={inputCls} className="mono" /></FormField>
      </>)}
      {fieldGrid(<>
        <FormField label="Cidade / base" required><Input icon="mapPin" value={f.cidade} onChange={set('cidade')} placeholder="Barcelos" style={inputCls} /></FormField>
        <FormField label="UF"><Select value={f.uf} onChange={(v) => setF(s => ({ ...s, uf: v }))} options={UFS} /></FormField>
      </>)}
      {fieldGrid(<>
        <FormField label="Nº de barcos"><Input type="number" value={f.barcos} onChange={set('barcos')} placeholder="6" style={inputCls} /></FormField>
        <FormField label="Situação">
          <ToggleRow on={f.ativa} onToggle={() => setF(s => ({ ...s, ativa: !s.ativa }))} label={f.ativa ? 'Operação ativa' : 'Operação inativa'} />
        </FormField>
      </>)}
    </div>
  );
}

// ---------- Form: Usuário ----------
function UsuarioForm({ onChange }) {
  const FT = window.FT;
  const [f, setF] = useState({ nome: '', email: '', papel: 'Guia', operacaoId: FT.OPERATIONS[0].id, telefone: '', ativo: true });
  useEffect(() => onChange(f), [f]);
  const set = (k) => (e) => setF(s => ({ ...s, [k]: e.target ? e.target.value : e }));
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '4px 0 6px' }}>
        <Avatar name={f.nome || '?'} size={42} cor={f.papel === 'Administrador' ? 'hsl(var(--primary))' : undefined} />
        <div style={{ fontSize: 12.5, color: 'hsl(var(--muted-foreground))' }}>O usuário receberá um convite por e-mail para acessar o app de registro.</div>
      </div>
      {fieldGrid(<>
        <FormField label="Nome completo" required><Input icon="user" value={f.nome} onChange={set('nome')} placeholder="Ex: João da Silva" style={inputCls} /></FormField>
        <FormField label="Papel" required><Select value={f.papel} onChange={(v) => setF(s => ({ ...s, papel: v }))} options={['Guia', 'Gestor', 'Administrador']} /></FormField>
      </>)}
      <FormField label="E-mail" required><Input icon="mail" type="email" value={f.email} onChange={set('email')} placeholder="joao@operacao.com" style={inputCls} /></FormField>
      {fieldGrid(<>
        <FormField label="Operação" required><Select value={f.operacaoId} onChange={(v) => setF(s => ({ ...s, operacaoId: v }))} options={FT.OPERATIONS.map(o => ({ value: o.id, label: o.nome }))} /></FormField>
        <FormField label="Telefone"><Input value={f.telefone} onChange={set('telefone')} placeholder="(92) 99999-9999" style={inputCls} className="mono" /></FormField>
      </>)}
    </div>
  );
}

// ---------- Form: Registro de captura ----------
function RegistroForm({ onChange }) {
  const FT = window.FT;
  const [f, setF] = useState({ especieId: FT.SPECIES[0].id, peso: '', comprimento: '', guiaId: FT.GUIDES[0].id, local: FT.LOCAIS[0].nome, isca: FT.ISCAS[0], data: new Date().toISOString().slice(0, 10), lat: '', lng: '' });
  useEffect(() => onChange(f), [f]);
  const set = (k) => (e) => setF(s => ({ ...s, [k]: e.target ? e.target.value : e }));
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <FormField label="Espécie" required>
        <Select value={f.especieId} onChange={(v) => setF(s => ({ ...s, especieId: v }))} options={FT.SPECIES.map(s => ({ value: s.id, label: s.nome, dot: s.cor }))} />
      </FormField>
      {fieldGrid(<>
        <FormField label="Peso (kg)" required><Input type="number" icon="scale" value={f.peso} onChange={set('peso')} placeholder="4,2" style={inputCls} /></FormField>
        <FormField label="Comprimento (cm)"><Input type="number" icon="ruler" value={f.comprimento} onChange={set('comprimento')} placeholder="62" style={inputCls} /></FormField>
      </>)}
      {fieldGrid(<>
        <FormField label="Guia" required><Select value={f.guiaId} onChange={(v) => setF(s => ({ ...s, guiaId: v }))} options={FT.GUIDES.filter(g => g.ativo).map(g => ({ value: g.id, label: g.nome }))} /></FormField>
        <FormField label="Data" required><Input type="date" value={f.data} onChange={set('data')} style={inputCls} /></FormField>
      </>)}
      {fieldGrid(<>
        <FormField label="Local de captura"><Select value={f.local} onChange={(v) => setF(s => ({ ...s, local: v }))} options={FT.LOCAIS.map(l => l.nome)} /></FormField>
        <FormField label="Isca"><Select value={f.isca} onChange={(v) => setF(s => ({ ...s, isca: v }))} options={FT.ISCAS} /></FormField>
      </>)}
      {fieldGrid(<>
        <FormField label="Latitude"><Input value={f.lat} onChange={set('lat')} placeholder="-0.9320" style={inputCls} className="mono" /></FormField>
        <FormField label="Longitude"><Input value={f.lng} onChange={set('lng')} placeholder="-62.9200" style={inputCls} className="mono" /></FormField>
      </>)}
    </div>
  );
}

// ---------- Toggle row ----------
function ToggleRow({ on, onToggle, label }) {
  return (
    <button onClick={onToggle} style={{ height: 34, display: 'flex', alignItems: 'center', gap: 9, padding: '0 11px', borderRadius: 'var(--radius-sm)', border: '1px solid hsl(var(--input))', background: 'hsl(var(--background))', cursor: 'pointer', fontSize: 13, color: 'hsl(var(--foreground))' }}>
      <span style={{ width: 32, height: 18, borderRadius: 99, background: on ? 'hsl(var(--primary))' : 'hsl(var(--border))', position: 'relative', flexShrink: 0, transition: 'background .15s' }}>
        <span style={{ position: 'absolute', top: 2, left: on ? 16 : 2, width: 14, height: 14, borderRadius: 99, background: '#fff', transition: 'left .15s', boxShadow: 'var(--shadow-sm)' }} />
      </span>
      {label}
    </button>
  );
}

// ---------- Modal config ----------
const MODALS = {
  especie: { title: 'Nova espécie', desc: 'Cadastre uma espécie-alvo da operação.', icon: 'fish', width: 540, Form: EspecieForm, cta: 'Cadastrar espécie', toast: 'Espécie cadastrada com sucesso.' },
  operacao: { title: 'Nova operação', desc: 'Lodge, barco-hotel ou pousada.', icon: 'ship', width: 520, Form: OperacaoForm, cta: 'Cadastrar operação', toast: 'Operação cadastrada com sucesso.' },
  usuario: { title: 'Convidar usuário', desc: 'Guia, gestor ou administrador.', icon: 'users', width: 520, Form: UsuarioForm, cta: 'Enviar convite', toast: 'Convite enviado por e-mail.' },
  registro: { title: 'Novo registro de captura', desc: 'Lançamento manual de uma captura.', icon: 'plus', width: 560, Form: RegistroForm, cta: 'Salvar registro', toast: 'Registro de captura salvo.' },
};

// ---------- Modal host (escuta eventos globais) ----------
function ModalHost() {
  const [active, setActive] = useState(null);
  const [toastNode, pushToast] = useToast();
  const formRef = useRef({});
  useEffect(() => {
    const h = (e) => setActive(e.detail);
    window.addEventListener('ft:open-modal', h);
    return () => window.removeEventListener('ft:open-modal', h);
  }, []);
  const cfg = active ? MODALS[active] : null;
  const close = () => setActive(null);
  const save = () => { close(); if (cfg) pushToast(cfg.toast); };
  return (
    <>
      {cfg && (
        <Dialog open onClose={close} title={cfg.title} desc={cfg.desc} icon={cfg.icon} width={cfg.width}
          footer={<><Button variant="secondary" onClick={close}>Cancelar</Button><Button icon="check" onClick={save}>{cfg.cta}</Button></>}>
          <cfg.Form onChange={(v) => { formRef.current = v; }} />
        </Dialog>
      )}
      {toastNode}
    </>
  );
}
window.openModal = (name) => window.dispatchEvent(new CustomEvent('ft:open-modal', { detail: name }));

Object.assign(window, { ModalHost, useToast });

})();

/* ===== reports.jsx ===== */
;(function(){
/* ============ FTrack — Relatórios (foco IBAMA) ============ */

const REPORT_TYPES = [
  { value: 'captura', label: 'Relatório de Atividade de Pesca' },
  { value: 'especie', label: 'Quantitativo por espécie' },
  { value: 'esforco', label: 'Esforço de pesca por operação' },
];

function useReportData(operacaoId, range) {
  const FT = window.FT;
  return useMemo(() => {
    const maxTs = new Date(FT.RECORDS[0].data).getTime();
    let fromTs = null, toTs = null;
    if (range.key === 'custom') { fromTs = range.from; toTs = range.to + 864e5 - 1; }
    else if (range.key !== 'all') { const r = dpRange(range.key, maxTs); fromTs = r.from; toTs = r.to; }
    const recs = FT.RECORDS.filter(r => {
      if (operacaoId !== 'all' && r.operacaoId !== operacaoId) return false;
      const ts = new Date(r.data).getTime();
      if (fromTs && ts < fromTs) return false;
      if (toTs && ts > toTs) return false;
      return true;
    });
    // por espécie
    const bySp = {};
    recs.forEach(r => {
      const s = bySp[r.especieId] || (bySp[r.especieId] = { n: 0, peso: 0, comp: 0 });
      s.n++; s.peso += r.peso; s.comp += r.comprimento;
    });
    const linhas = Object.entries(bySp).map(([id, v]) => ({
      sp: FT.speciesById[id], n: v.n, pesoTotal: v.peso, pesoMedio: v.peso / v.n, compMedio: v.comp / v.n,
    })).sort((a, b) => b.n - a.n);
    const totN = recs.length;
    const totPeso = recs.reduce((s, r) => s + r.peso, 0);
    const guias = new Set(recs.map(r => r.guiaId)).size;
    const dias = new Set(recs.map(r => r.data.slice(0, 10))).size;
    const locais = new Set(recs.map(r => r.local)).size;
    return { recs, linhas, totN, totPeso, guias, dias, locais, fromTs, toTs };
  }, [operacaoId, range]);
}

function fmtPeriodo(fromTs, toTs) {
  const f = fromTs ? fmtDate(new Date(fromTs).toISOString()) : '—';
  const t = toTs ? fmtDate(new Date(toTs).toISOString()) : fmtDate(new Date().toISOString());
  return `${f} a ${t}`;
}

function ReportDoc({ tipo, operacao, data, range }) {
  const hoje = fmtDate(new Date().toISOString());
  const protocolo = 'IBAMA-PA/' + (operacao === 'all' ? 'CONSOLIDADO' : operacao.toUpperCase().replace('OP-', '')) + '-' + new Date().getFullYear();
  const op = operacao === 'all' ? null : window.FT.opsById[operacao];
  return (
    <div id="report-sheet" style={{ background: '#fff', width: 794, margin: '0 auto', padding: '48px 56px', color: '#1a1a1a', fontSize: 12.5, lineHeight: 1.55, boxShadow: 'var(--shadow-lg)', border: '1px solid hsl(var(--border))' }}>
      {/* cabeçalho oficial */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 16, paddingBottom: 16, borderBottom: '2px solid #1a1a1a' }}>
        <div style={{ width: 52, height: 52, flexShrink: 0, display: 'grid', placeItems: 'center', border: '1.5px solid #1a1a1a', borderRadius: 6 }}>
          <Icon name="shield" size={30} style={{ color: '#1a6b3a' }} strokeWidth={1.5} />
        </div>
        <div style={{ flex: 1, textAlign: 'center' }}>
          <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '.04em', textTransform: 'uppercase' }}>Ministério do Meio Ambiente e Mudança do Clima</div>
          <div style={{ fontSize: 12.5, fontWeight: 700 }}>IBAMA — Instituto Brasileiro do Meio Ambiente e dos Recursos Naturais Renováveis</div>
          <div style={{ fontSize: 10.5, color: '#555', marginTop: 2 }}>Sistema de Controle de Pesca Amadora e Esportiva</div>
        </div>
        <div style={{ width: 52, flexShrink: 0 }} />
      </div>

      <h1 style={{ fontSize: 16, fontWeight: 700, textAlign: 'center', margin: '22px 0 4px', letterSpacing: '.01em' }}>
        {tipo === 'captura' ? 'RELATÓRIO DE ATIVIDADE DE PESCA AMADORA/ESPORTIVA' : tipo === 'especie' ? 'RELATÓRIO QUANTITATIVO POR ESPÉCIE' : 'RELATÓRIO DE ESFORÇO DE PESCA'}
      </h1>
      <div style={{ textAlign: 'center', fontSize: 11, color: '#555', marginBottom: 22 }}>Protocolo nº {protocolo} · Emitido em {hoje}</div>

      {/* dados da operação */}
      <table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: 18, fontSize: 11.5 }}>
        <tbody>
          {[
            ['Operação / Estabelecimento', op ? op.nome : 'Todas as operações (consolidado)'],
            ['Tipo', op ? op.tipo : '—'],
            ['Município-base / UF', op ? op.base : '—'],
            ['CNPJ', op ? '12.345.678/0001-90' : '—'],
            ['Período de apuração', fmtPeriodo(data.fromTs, data.toTs)],
            ['Responsável técnico', 'Marina Couto — Administrador'],
          ].map(([k, v], i) => (
            <tr key={i}>
              <td style={{ border: '1px solid #ccc', padding: '6px 10px', background: '#f4f4f4', fontWeight: 600, width: '34%' }}>{k}</td>
              <td style={{ border: '1px solid #ccc', padding: '6px 10px' }}>{v}</td>
            </tr>
          ))}
        </tbody>
      </table>

      {/* resumo */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 0, marginBottom: 20, border: '1px solid #ccc' }}>
        {[['Exemplares', fmtInt(data.totN)], ['Peso total (kg)', nf1.format(data.totPeso)], ['Dias de atividade', fmtInt(data.dias)], ['Guias envolvidos', fmtInt(data.guias)]].map(([l, v], i) => (
          <div key={l} style={{ padding: '10px 12px', borderLeft: i ? '1px solid #ccc' : 'none', textAlign: 'center' }}>
            <div style={{ fontSize: 9.5, color: '#666', textTransform: 'uppercase', letterSpacing: '.04em' }}>{l}</div>
            <div className="tnum" style={{ fontSize: 17, fontWeight: 700, marginTop: 3 }}>{v}</div>
          </div>
        ))}
      </div>

      {/* tabela de espécies */}
      <div style={{ fontSize: 12, fontWeight: 700, marginBottom: 7 }}>Demonstrativo de capturas por espécie</div>
      <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11 }}>
        <thead>
          <tr style={{ background: '#1a6b3a', color: '#fff' }}>
            {['Nome popular', 'Nome científico', 'Exemplares', 'Peso total (kg)', 'Peso médio (kg)', 'Comp. médio (cm)'].map((h, i) => (
              <th key={i} style={{ padding: '7px 9px', textAlign: i < 2 ? 'left' : 'right', fontWeight: 600, fontSize: 10.5, borderRight: i < 5 ? '1px solid rgba(255,255,255,.2)' : 'none' }}>{h}</th>
            ))}
          </tr>
        </thead>
        <tbody>
          {data.linhas.map((l, i) => (
            <tr key={l.sp.id} style={{ background: i % 2 ? '#f7f9f8' : '#fff' }}>
              <td style={{ padding: '6px 9px', border: '1px solid #ddd' }}>
                <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}><span style={{ width: 8, height: 8, borderRadius: 99, background: l.sp.cor }} />{l.sp.nome}</span>
              </td>
              <td style={{ padding: '6px 9px', border: '1px solid #ddd', fontStyle: 'italic', color: '#444' }}>{l.sp.cientifico}</td>
              <td className="tnum" style={{ padding: '6px 9px', border: '1px solid #ddd', textAlign: 'right', fontWeight: 600 }}>{fmtInt(l.n)}</td>
              <td className="tnum" style={{ padding: '6px 9px', border: '1px solid #ddd', textAlign: 'right' }}>{nf1.format(l.pesoTotal)}</td>
              <td className="tnum" style={{ padding: '6px 9px', border: '1px solid #ddd', textAlign: 'right' }}>{nf1.format(l.pesoMedio)}</td>
              <td className="tnum" style={{ padding: '6px 9px', border: '1px solid #ddd', textAlign: 'right' }}>{fmtInt(l.compMedio)}</td>
            </tr>
          ))}
          <tr style={{ background: '#eef2f0', fontWeight: 700 }}>
            <td style={{ padding: '7px 9px', border: '1px solid #ccc' }} colSpan={2}>TOTAL GERAL</td>
            <td className="tnum" style={{ padding: '7px 9px', border: '1px solid #ccc', textAlign: 'right' }}>{fmtInt(data.totN)}</td>
            <td className="tnum" style={{ padding: '7px 9px', border: '1px solid #ccc', textAlign: 'right' }}>{nf1.format(data.totPeso)}</td>
            <td style={{ border: '1px solid #ccc' }} colSpan={2} />
          </tr>
        </tbody>
      </table>

      {/* declaração */}
      <p style={{ fontSize: 11, color: '#333', marginTop: 22, textAlign: 'justify' }}>
        Declaro, para os devidos fins e sob as penas da lei, que as informações constantes neste relatório são verídicas e
        refletem fielmente a atividade de pesca amadora/esportiva conduzida no período de apuração indicado, em conformidade
        com a Instrução Normativa IBAMA aplicável e demais normas de ordenamento pesqueiro vigentes.
      </p>

      {/* assinaturas */}
      <div style={{ display: 'flex', gap: 60, marginTop: 48, justifyContent: 'center' }}>
        {['Responsável pela operação', 'Responsável técnico'].map(l => (
          <div key={l} style={{ textAlign: 'center', flex: 1, maxWidth: 240 }}>
            <div style={{ borderTop: '1px solid #1a1a1a', paddingTop: 6, fontSize: 10.5, color: '#555' }}>{l}</div>
          </div>
        ))}
      </div>
      <div style={{ textAlign: 'center', fontSize: 9.5, color: '#999', marginTop: 30 }}>
        Documento gerado eletronicamente por FTrack · {hoje} · página 1 de 1
      </div>
    </div>
  );
}

function RelatoriosScreen() {
  const FT = window.FT;
  const [tipo, setTipo] = useState('captura');
  const [operacao, setOperacao] = useState('all');
  const [range, setRange] = useState({ key: 'ytd', ...dpRange('ytd', new Date(FT.RECORDS[0].data).getTime()) });
  const maxTs = new Date(FT.RECORDS[0].data).getTime();
  const data = useReportData(operacao, range);
  const [toastNode, pushToast] = useToast();

  return (
    <div className="anim-fade">
      <PageHeader title="Relatórios" subtitle="Geração de relatórios oficiais para prestação de contas ao IBAMA"
        actions={<><Button variant="secondary" size="md" icon="download" onClick={() => pushToast('Relatório exportado em CSV.')}>CSV</Button><Button size="md" icon="printer" onClick={() => { pushToast('Enviando para impressão…'); setTimeout(() => window.print(), 400); }}>Imprimir / PDF</Button></>} />

      <div style={{ display: 'grid', gridTemplateColumns: '300px 1fr', gap: 18, alignItems: 'start' }}>
        {/* configuração */}
        <div style={{ position: 'sticky', top: 0, display: 'flex', flexDirection: 'column', gap: 14 }}>
          <Card style={{ padding: 0, overflow: 'hidden' }}>
            <div style={{ padding: '13px 16px', borderBottom: '1px solid hsl(var(--border))', display: 'flex', alignItems: 'center', gap: 8 }}>
              <Icon name="sliders" size={16} style={{ color: 'hsl(var(--muted-foreground))' }} />
              <h3 style={{ fontSize: 14, fontWeight: 600 }}>Configuração</h3>
            </div>
            <div style={{ padding: 16, display: 'flex', flexDirection: 'column', gap: 15 }}>
              <FormField label="Tipo de relatório">
                <Select value={tipo} onChange={setTipo} options={REPORT_TYPES} minWidth={260} />
              </FormField>
              <FormField label="Operação">
                <Select value={operacao} onChange={setOperacao} minWidth={260} options={[{ value: 'all', label: 'Todas (consolidado)' }, ...FT.OPERATIONS.map(o => ({ value: o.id, label: o.nome }))]} />
              </FormField>
              <FormField label="Período de apuração">
                <DatePicker value={range} onChange={setRange} refDate={maxTs} />
              </FormField>
            </div>
          </Card>

          <Card style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 10 }}>
            <div style={{ fontSize: 11.5, fontWeight: 600, color: 'hsl(var(--muted-foreground))', textTransform: 'uppercase', letterSpacing: '.04em' }}>Resumo do período</div>
            {[['Exemplares', fmtInt(data.totN)], ['Peso total', fmtKg(data.totPeso)], ['Espécies', fmtInt(data.linhas.length)], ['Pontos de pesca', fmtInt(data.locais)], ['Dias de atividade', fmtInt(data.dias)]].map(([l, v]) => (
              <div key={l} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 13 }}>
                <span style={{ color: 'hsl(var(--muted-foreground))' }}>{l}</span>
                <span className="tnum" style={{ fontWeight: 600 }}>{v}</span>
              </div>
            ))}
          </Card>

          <div style={{ display: 'flex', gap: 8, alignItems: 'flex-start', padding: '11px 13px', background: 'hsl(var(--primary-50))', border: '1px solid hsl(var(--primary) / .2)', borderRadius: 'var(--radius)' }}>
            <Icon name="info" size={15} style={{ color: 'hsl(var(--primary))', flexShrink: 0, marginTop: 1 }} />
            <span style={{ fontSize: 11.5, color: 'hsl(var(--primary-700))', lineHeight: 1.5 }}>Pré-visualização do documento ao lado. Use “Imprimir / PDF” para gerar o arquivo final assinável.</span>
          </div>
        </div>

        {/* documento */}
        <div id="report-scroll" style={{ overflowX: 'auto', paddingBottom: 8 }}>
          <ReportDoc tipo={tipo} operacao={operacao} data={data} range={range} />
        </div>
      </div>
      {toastNode}
    </div>
  );
}

Object.assign(window, { RelatoriosScreen });

})();

/* ===== app.jsx ===== */
;(function(){
/* ============ FTrack — app raiz ============ */
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "dashLayout": "padrao",
  "accent": "oceano",
  "tableDensity": "ultra"
}/*EDITMODE-END*/;

const ACCENTS = {
  oceano: { p: '201 96% 32%', p7: '202 90% 26%', p50: '204 100% 97%', ring: '201 96% 32%', label: 'Azul oceano' },
  verde: { p: '162 88% 26%', p7: '163 90% 20%', p50: '152 76% 96%', ring: '162 88% 26%', label: 'Verde água' },
  slate: { p: '215 25% 35%', p7: '216 30% 27%', p50: '214 32% 96%', ring: '215 25% 35%', label: 'Slate' },
  indigo: { p: '243 60% 50%', p7: '244 55% 42%', p50: '226 100% 97%', ring: '243 60% 50%', label: 'Índigo' },
};

const USER = { nome: 'Marina Couto', iniciais: 'MC', papel: 'Administrador' };
const SCREEN_TITLE = { dashboard: 'Dashboard', registros: 'Registros', relatorios: 'Relatórios', especies: 'Espécies', operacoes: 'Operações', usuarios: 'Usuários' };

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [authed, setAuthed] = useState(() => sessionStorage.getItem('ft_auth') === '1');
  const [screen, setScreen] = useState(() => localStorage.getItem('ft_screen') || 'dashboard');

  // aplica accent
  useEffect(() => {
    const a = ACCENTS[t.accent] || ACCENTS.oceano;
    const r = document.documentElement.style;
    r.setProperty('--primary', a.p); r.setProperty('--primary-700', a.p7);
    r.setProperty('--primary-50', a.p50); r.setProperty('--ring', a.ring);
  }, [t.accent]);

  const navigate = (s) => { setScreen(s); localStorage.setItem('ft_screen', s); };
  const login = () => { setAuthed(true); sessionStorage.setItem('ft_auth', '1'); };
  const logout = () => { setAuthed(false); sessionStorage.removeItem('ft_auth'); };

  const panel = (
    <TweaksPanel>
      <TweakSection label="Layout do dashboard" />
      <TweakRadio label="Arranjo" value={t.dashLayout} options={[
        { value: 'padrao', label: 'Padrão' }, { value: 'compacto', label: 'Compacto' }, { value: 'mapa', label: 'Foco no mapa' },
      ]} onChange={(v) => setTweak('dashLayout', v)} />
      <TweakSection label="Tabela de registros" />
      <TweakRadio label="Densidade" value={t.tableDensity} options={[
        { value: 'ultra', label: 'Ultra' }, { value: 'confortavel', label: 'Confortável' },
      ]} onChange={(v) => setTweak('tableDensity', v)} />
      <TweakSection label="Tema" />
      <TweakColor label="Cor de destaque" value={'hsl(' + (ACCENTS[t.accent] || ACCENTS.oceano).p + ')'}
        options={Object.keys(ACCENTS).map(k => 'hsl(' + ACCENTS[k].p + ')')}
        onChange={(v) => { const k = Object.keys(ACCENTS).find(k => 'hsl(' + ACCENTS[k].p + ')' === v); if (k) setTweak('accent', k); }} />
    </TweaksPanel>
  );

  if (!authed) {
    return <>{<LoginScreen onLogin={login} />}{panel}</>;
  }

  let content;
  if (screen === 'dashboard') content = <Dashboard layout={t.dashLayout} onNavigate={navigate} />;
  else if (screen === 'registros') content = <Registros density={t.tableDensity} onNew={window.openModal} />;
  else if (screen === 'relatorios') content = <RelatoriosScreen />;
  else if (screen === 'especies') content = <EspeciesScreen onNavigate={navigate} onNew={window.openModal} />;
  else if (screen === 'operacoes') content = <OperacoesScreen onNew={window.openModal} />;
  else if (screen === 'usuarios') content = <UsuariosScreen onNew={window.openModal} />;

  const fullBleed = screen === 'registros';

  return (
    <div style={{ display: 'flex', height: '100%', overflow: 'hidden' }}>
      <Sidebar current={screen} onNavigate={navigate} user={USER} onLogout={logout} />
      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, height: '100%' }}>
        <Topbar title={SCREEN_TITLE[screen]} />
        <main key={screen} style={{ flex: 1, overflow: fullBleed ? 'hidden' : 'auto', padding: fullBleed ? '20px 22px' : '22px 24px', display: fullBleed ? 'flex' : 'block', flexDirection: 'column', minHeight: 0 }}>
          <div style={{ maxWidth: fullBleed ? 'none' : 1400, margin: '0 auto', width: '100%', height: fullBleed ? '100%' : 'auto', display: fullBleed ? 'flex' : 'block', flexDirection: 'column', minHeight: 0 }}>
            {content}
          </div>
        </main>
      </div>
      {panel}
      <ModalHost />
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);

})();
