// Journal2.jsx — «bok»-utgaven av journalen (aktiv journal, nås via #/journal)
// «Planlegger»-oppslag: venstresiden har bokmerke-faner («I dag» / «Siste
// sider»), månedskalender, dagens trackere (logbook_trackers) og fullførte
// oppgaver/rutiner (todo_tasks + routines/routine_entries). Høyresiden er
// skrivesiden med håndskrevet datolinje i rød blekk.
// Datohjelpere (jToKey, jFromKey, jSameDay, J_MONTH, J_DAY_FULL,
// J_MOOD_EMOJIS, jt, getJLang, jGetTrackerIcon) er definert øverst i filen.
const {
  useState: useJ2State,
  useEffect: useJ2Effect,
  useRef: useJ2Ref,
  useMemo: useJ2Memo,
} = React;

// ── Delte hjelpere (tidligere definert i Journal.jsx) ────────────
// Journal.jsx er fjernet; disse små hjelperne følger nå med journalen.
function getJLang() { return localStorage.getItem('el-lang') || 'no'; }

const J_DAY_SHORT = {
  no: ['Søn', 'Man', 'Tir', 'Ons', 'Tor', 'Fre', 'Lør'],
  en: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
};
const J_DAY_FULL = {
  no: ['Søndag', 'Mandag', 'Tirsdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lørdag'],
  en: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
};
const J_MONTH = {
  no: ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember'],
  en: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
};
const J_MOOD_EMOJIS = ['😔', '😕', '😐', '🙂', '😄'];
const J_MOODS = {
  no: ['Trist', 'Ned', 'Nøytral', 'Bra', 'Topp'],
  en: ['Sad', 'Down', 'Neutral', 'Good', 'Great'],
};
function jt(lang) { return { moods: J_MOODS[lang] || J_MOODS.no }; }

function jToKey(d) {
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const dd = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${dd}`;
}
function jFromKey(k) {
  const [y, m, d] = k.split('-').map(Number);
  return new Date(y, m - 1, d);
}
function jSameDay(a, b) {
  return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
}

function jGetTrackerIcon(id, name) {
  const stored = localStorage.getItem(`el-tracker-icon-${id}`);
  if (stored) return stored;
  const n = (name || '').toLowerCase();
  if (n.includes('søvn') || n.includes('sleep')) return '🌙';
  if (n.includes('energi') || n.includes('energy')) return '⚡';
  if (n.includes('vann') || n.includes('water')) return '💧';
  if (n.includes('steg') || n.includes('step')) return '🏃';
  if (n.includes('stress')) return '😰';
  if (n.includes('fokus') || n.includes('focus')) return '🎯';
  return '📊';
}

const J2_I18N = {
  no: {
    loading: 'Åpner boken…',
    volume: 'Journal',
    moodLabel: 'Dagens stemning',
    noText: '(uten tekst)',
    placeholder: 'I dag så …',
    titlePlaceholder: 'Tittel på dagens side',
    words: 'ord',
    saved: 'Lagret i boken',
    saving: 'Blekket tørker…',
    prevDay: 'Forrige side',
    nextDay: 'Neste side',
    today: 'I dag',
    gotoToday: '↩ Til i dag',
    page: 'Side',
    tabToday: 'I dag',
    tabPages: 'Oversikt',
    toc: 'Innholdsfortegnelse',
    registerTitle: 'Oversikt',
    registerEmpty: 'Boken er fortsatt blank. Skriv din første side →',
    detailsBtn: 'Se detaljer',
    backToWrite: 'Tilbake til skriving',
    detailsTitle: 'Dagens oversikt',
    trend7: 'siste 7 dager',
    moodShort: 'Stemning',
    historyTitle: 'Utvikling',
    histLabel: 'Historikk',
    histTodayHint: 'Juster dagens verdi',
    backToDay: 'Tilbake til dagen',
    noReg: 'Ingen registrering',
    ofTarget: 'av',
    trackersLabel: 'Dagens trackere',
    noTrackers: 'Ingen trackere ennå — trykk på pennen for å legge til.',
    editTrackers: 'Rediger trackere',
    doneEditing: 'Ferdig',
    newTracker: 'Ny tracker…',
    archiveTracker: 'Arkiver tracker',
    typeCounter: 'Teller',
    typeScale: 'Skala 1–10',
    typeCheckbox: 'Avkrysning',
    typeText: 'Tekst',
    completedLabel: 'Fullført i dag',
    tasksLabel: 'Oppgaver',
    routinesLabel: 'Rutiner',
    goalsLabel: 'Mål',
    projectsLabel: 'Prosjekter',
    completedTitle: 'Fullført i dag',
    routineDevTitle: 'Utvikling',
    noTasksDay: 'Ingen oppgaver denne dagen.',
    noRoutinesDay: 'Ingen rutiner denne dagen.',
    noGoalsDone: 'Ingen mål-oppgaver fullført.',
    noProjDone: 'Ingen prosjekt-oppgaver fullført.',
    nothingToday: 'Ingen oppgaver eller rutiner denne dagen.',
    taskTag: 'oppgave',
    routineTag: 'rutine',
    onlyToday: 'Oppgaver kan bare hakes av på dagens side',
    tabClose: 'Lukk',
    closeBook: 'Lukk boken',
    openBook: 'Åpne boken',
    enterPin: 'PIN-kode',
    wrongPin: 'Feil PIN-kode',
    setPin: 'Sett PIN-kode',
    savePin: 'Lagre PIN',
    removePin: 'Fjern PIN-kode',
    pinHint: '4–6 sifre',
    cancel: 'Avbryt',
    themeLeather: 'Skinn',
    themeLab: 'Loggbok',
    themeGreen: 'Notatbok',
    settings: 'Innstillinger',
    bookColor: 'Bokfarge',
    customColor: 'Egen farge',
    fontLabel: 'Skrift',
    fontSerif: 'Klassisk',
    fontHand: 'Håndskrift',
    fontSans: 'Moderne',
  },
  en: {
    loading: 'Opening the book…',
    volume: 'Journal',
    moodLabel: "Today's mood",
    noText: '(no text)',
    placeholder: 'Today I …',
    titlePlaceholder: "Title of today's page",
    words: 'words',
    saved: 'Saved in the book',
    saving: 'Ink is drying…',
    prevDay: 'Previous page',
    nextDay: 'Next page',
    today: 'Today',
    gotoToday: '↩ To today',
    page: 'Page',
    tabToday: 'Today',
    tabPages: 'Overview',
    toc: 'Table of contents',
    registerTitle: 'Overview',
    registerEmpty: 'The book is still blank. Write your first page →',
    detailsBtn: 'See details',
    backToWrite: 'Back to writing',
    detailsTitle: 'Day overview',
    trend7: 'last 7 days',
    moodShort: 'Mood',
    historyTitle: 'Trend',
    histLabel: 'History',
    histTodayHint: 'Adjust today’s value',
    backToDay: 'Back to the day',
    noReg: 'No entry',
    ofTarget: 'of',
    trackersLabel: "Today's trackers",
    noTrackers: 'No trackers yet — tap the pen to add one.',
    editTrackers: 'Edit trackers',
    doneEditing: 'Done',
    newTracker: 'New tracker…',
    archiveTracker: 'Archive tracker',
    typeCounter: 'Counter',
    typeScale: 'Scale 1–10',
    typeCheckbox: 'Checkbox',
    typeText: 'Text',
    completedLabel: 'Completed today',
    tasksLabel: 'Tasks',
    routinesLabel: 'Routines',
    goalsLabel: 'Goals',
    projectsLabel: 'Projects',
    completedTitle: 'Completed today',
    routineDevTitle: 'Trend',
    noTasksDay: 'No tasks on this day.',
    noRoutinesDay: 'No routines on this day.',
    noGoalsDone: 'No goal tasks completed.',
    noProjDone: 'No project tasks completed.',
    nothingToday: 'No tasks or routines on this day.',
    taskTag: 'task',
    routineTag: 'routine',
    onlyToday: "Tasks can only be checked off on today's page",
    tabClose: 'Close',
    closeBook: 'Close the book',
    openBook: 'Open the book',
    enterPin: 'PIN code',
    wrongPin: 'Wrong PIN code',
    setPin: 'Set PIN code',
    savePin: 'Save PIN',
    removePin: 'Remove PIN code',
    pinHint: '4–6 digits',
    cancel: 'Cancel',
    themeLeather: 'Leather',
    themeLab: 'Logbook',
    themeGreen: 'Notebook',
    settings: 'Settings',
    bookColor: 'Book colour',
    customColor: 'Custom colour',
    fontLabel: 'Font',
    fontSerif: 'Classic',
    fontHand: 'Handwriting',
    fontSans: 'Modern',
  },
};

function j2t(lang) { return J2_I18N[lang] || J2_I18N.no; }

const J2_CAL_DOW = {
  no: ['M', 'T', 'O', 'T', 'F', 'L', 'S'],
  en: ['M', 'T', 'W', 'T', 'F', 'S', 'S'],
};

// Antall rader (månedsoverskrifter + innslag) per side i Oversikt-oppslaget
const J2_REG_PER_PAGE = 10;

// Pseudo-tracker for stemning, så den oppfører seg som de andre trackerne
const J2_MOOD_ID = '__mood__';

// PIN-lås: samme Supabase-tabell (logbook_pin_settings) og localStorage-
// nøkkel som v1-loggboken (scripts/logbook.js), så de to holder seg i sync.
const J2_PIN_KEY = 'easylife/logbook-pin';
// «Opplåst nylig»-tidsstempel: når boken er låst opp med PIN, kan man hoppe
// mellom sidene i Easylife i 15 minutter uten å taste koden på nytt. «Lukk»
// nullstiller stempelet, så boken låser seg igjen umiddelbart.
const J2_UNLOCK_KEY = 'el-j2-unlocked-at';
const J2_GRACE_MS = 15 * 60 * 1000;
function j2WithinGrace() {
  try {
    const t = +localStorage.getItem(J2_UNLOCK_KEY) || 0;
    return t > 0 && (Date.now() - t) < J2_GRACE_MS;
  } catch (e) { return false; }
}
function j2MarkUnlocked() {
  try { localStorage.setItem(J2_UNLOCK_KEY, String(Date.now())); } catch (e) { /* ignorer */ }
}
function j2ClearUnlock() {
  try { localStorage.removeItem(J2_UNLOCK_KEY); } catch (e) { /* ignorer */ }
}
const J2_THEME_KEY = 'el-j2-theme';
const J2_THEMES = ['leather', 'lab', 'green', 'custom'];
// Faste ekstra-farger i innstillingskortet (i tillegg til de tre forhåndsdefinerte
// temaene og en fri fargevelger).
const J2_CUSTOM_PRESETS = ['#7a2e3b', '#2e4a7a', '#6a4f8a', '#2f6e5e', '#3a3a40'];
const J2_CUSTOM_COLOR_KEY = 'el-j2-custom-color';
// Skrifttype for selve skrivingen i boken. 'serif' = standarden (Fraunces).
const J2_FONT_KEY = 'el-j2-font';
const J2_FONTS = ['serif', 'hand', 'sans'];
const J2_FONT_OPTS = [
  { key: 'serif', ff: "'Fraunces', serif" },
  { key: 'hand',  ff: "'Caveat', cursive" },
  { key: 'sans',  ff: "'Plus Jakarta Sans', sans-serif" },
];

// ── Fargehjelpere for egendefinert bokfarge ──
// Lager en koordinert omslagspalett (+ lesbart blekk) ut fra én grunnfarge.
function j2HexToRgb(hex) {
  const h = String(hex || '').replace('#', '');
  const v = h.length === 3 ? h.split('').map(c => c + c).join('') : h;
  const n = parseInt(v || '463322', 16);
  return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
}
function j2Shade(hex, amt) {
  // amt < 0 → mørkere mot svart, amt > 0 → lysere mot hvitt
  const { r, g, b } = j2HexToRgb(hex);
  const t = amt < 0 ? 0 : 255, p = Math.abs(amt);
  const f = c => Math.round((t - c) * p + c);
  const to = c => Math.max(0, Math.min(255, f(c))).toString(16).padStart(2, '0');
  return `#${to(r)}${to(g)}${to(b)}`;
}
function j2CustomVars(hex) {
  const { r, g, b } = j2HexToRgb(hex);
  const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
  const ink = j2Shade(hex, lum > 0.45 ? -0.55 : -0.2);
  return {
    '--j2c-a': hex,
    '--j2c-b': j2Shade(hex, -0.18),
    '--j2c-c': j2Shade(hex, -0.4),
    '--j2c-bd': lum > 0.6 ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.22)',
    '--j2cov-text': lum > 0.62 ? '#3a352c' : '#f3eee2',
    '--j2rib-a': ink, '--j2rib-b': j2Shade(ink, 0.18), '--j2rib-c': j2Shade(ink, -0.15),
    '--j2p-a': '#faf6ec', '--j2p-b': '#f4eedd', '--j2p-c': '#ece3cd',
    '--j2ink': ink,
    '--j2tab-a': j2Shade(hex, -0.18), '--j2tab-b': j2Shade(hex, -0.4),
  };
}

function j2ReadLocalPin() {
  try {
    const raw = localStorage.getItem(J2_PIN_KEY);
    if (raw) return JSON.parse(raw);
  } catch (e) { /* korrupt lokal verdi — ignorer */ }
  return null;
}

function j2WriteLocalPin(settings) {
  try { localStorage.setItem(J2_PIN_KEY, JSON.stringify(settings)); } catch (e) { /* full storage — ignorer */ }
}

// Skrivefelt for PIN: bevisst type="text" (ikke password) med autoComplete
// av, slik at Google/nettleserens passordlagring aldri tilbyr seg å lagre
// koden. Maskeringen gjøres med CSS (-webkit-text-security) i stedet.
function J2PinInput({ value, onChange, onEnter, error, placeholder, autoFocus }) {
  const inputRef = useJ2Ref(null);
  // Aktiver feltet med en gang det vises, så koden kan tastes direkte uten å
  // måtte klikke i feltet først (gjelder opplåsing av en allerede satt PIN).
  useJ2Effect(() => {
    if (autoFocus && inputRef.current) {
      try { inputRef.current.focus(); } catch (e) { /* ignorer */ }
    }
  }, [autoFocus]);
  return (
    <input
      ref={inputRef}
      className={`j2-pin-input${error ? ' err' : ''}`}
      type="text"
      inputMode="numeric"
      pattern="[0-9]*"
      autoComplete="off"
      autoCorrect="off"
      autoCapitalize="off"
      spellCheck={false}
      name="j2-bookcode"
      data-lpignore="true"
      data-1p-ignore="true"
      data-bwignore="true"
      data-form-type="other"
      maxLength={6}
      placeholder={placeholder}
      value={value}
      onChange={e => onChange(e.target.value.replace(/\D/g, ''))}
      onKeyDown={e => { if (e.key === 'Enter') onEnter(); }}
    />
  );
}

const J2_DEMO_TRACKERS = [
  { id: 'demo-trk-1', name: 'Vann', type: 'counter' },
  { id: 'demo-trk-2', name: 'Energi', type: 'scale' },
  { id: 'demo-trk-3', name: 'Trening', type: 'checkbox' },
  { id: 'demo-trk-4', name: 'Søvn', type: 'scale' },
  { id: 'demo-trk-5', name: 'Lesing', type: 'checkbox' },
  { id: 'demo-trk-6', name: 'Skritt', type: 'counter' },
];

function j2DayOfYear(d) {
  const start = new Date(d.getFullYear(), 0, 0);
  return Math.floor((d - start) / 86400000);
}

function j2FmtDate(d, lang) {
  const months = J_MONTH[lang] || J_MONTH.no;
  if (lang === 'en') return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
  return `${d.getDate()}. ${months[d.getMonth()]} ${d.getFullYear()}`;
}

function j2HasContent(v) {
  return !!((v.text || '').replace(/<[^>]*>/g, '').trim() || v.heading || v.mood != null);
}

// Skrevet innhold (tekst eller tittel). Brukt i innholdsfortegnelsen, som KUN
// skal liste dager man faktisk har skrevet på — ikke dager med bare
// tracker-/stemnings­registrering.
function j2HasText(v) {
  return !!((v.text || '').replace(/<[^>]*>/g, '').trim() || (v.heading || '').trim());
}

// ── Skrivefelt med linjert papir ─────────────────────────────────
function J2Editor({ text, onChange, placeholder }) {
  const ref = useJ2Ref(null);
  const wrapRef = useJ2Ref(null);
  const thumbRef = useJ2Ref(null);

  useJ2Effect(() => {
    if (ref.current) ref.current.innerHTML = text || '';
  }, []);

  // Elegant scroll-håndtak: boken «forlenger seg» som en scroll, og et slankt
  // sepia-håndtak viser hvor i teksten man er — kun når innholdet er lengre enn
  // én side. Håndtaket ligger i scroll-laget, så vi forskyver det med scrollTop
  // for å holde det fast i synsfeltet (uten ekstra wrapper).
  function updateThumb() {
    const w = wrapRef.current, t = thumbRef.current;
    if (!w || !t) return;
    const { scrollHeight, clientHeight, scrollTop } = w;
    if (scrollHeight <= clientHeight + 6) { t.style.opacity = '0'; return; }
    const pad = 6;
    const trackH = clientHeight - pad * 2;
    const thumbH = Math.max(30, trackH * (clientHeight / scrollHeight));
    const maxOffset = trackH - thumbH;
    const ratio = scrollTop / (scrollHeight - clientHeight);
    const offset = pad + maxOffset * ratio;
    t.style.opacity = '1';
    t.style.height = thumbH + 'px';
    t.style.top = (scrollTop + offset) + 'px';
  }

  useJ2Effect(() => {
    updateThumb();
    const w = wrapRef.current;
    if (!w) return;
    const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(updateThumb) : null;
    if (ro) ro.observe(w);
    return () => ro && ro.disconnect();
  }, [text]);

  return (
    <div className="j2-editor-wrap">
      {/* På mobil er .j2-editor-scroll absolutt-posisjonert (inset:0), så den
          får fast høyde av forelderen og ruller i stedet for å vokse med
          innholdet. På desktop er den statisk (uendret oppførsel). */}
      <div className="j2-editor-scroll" ref={wrapRef} onScroll={updateThumb}>
        {!text && <p className="j2-placeholder">{placeholder}</p>}
        <div
          ref={ref}
          className="j2-editor"
          contentEditable
          suppressContentEditableWarning
          onInput={() => { onChange(ref.current?.innerHTML || ''); updateThumb(); }}
        />
        <span className="j2-scrollthumb" ref={thumbRef} aria-hidden="true" style={{ opacity: 0 }} />
      </div>
    </div>
  );
}

// ── Månedskalender på papir ──────────────────────────────────────
function J2Cal({ month, onMonth, selected, today, entries, onPick, lang }) {
  const months = J_MONTH[lang] || J_MONTH.no;
  const dow = J2_CAL_DOW[lang] || J2_CAL_DOW.no;
  const first = new Date(month.getFullYear(), month.getMonth(), 1);
  const offset = (first.getDay() + 6) % 7; // mandag først
  const daysInMonth = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate();
  const cells = [];
  for (let i = 0; i < offset; i++) cells.push(null);
  for (let d = 1; d <= daysInMonth; d++) cells.push(d);

  function nav(step) {
    onMonth(new Date(month.getFullYear(), month.getMonth() + step, 1));
  }

  return (
    <div className="j2-cal">
      <div className="j2-cal-head">
        <button className="j2-cal-nav" onClick={() => nav(-1)} title="‹"><i className="ti ti-chevron-left"></i></button>
        <span className="j2-cal-month">{months[month.getMonth()]} {month.getFullYear()}</span>
        <button className="j2-cal-nav" onClick={() => nav(1)} title="›"><i className="ti ti-chevron-right"></i></button>
      </div>
      <div className="j2-cal-grid">
        {dow.map((l, i) => <span key={'h' + i} className="j2-cal-dow">{l}</span>)}
        {cells.map((d, i) => {
          if (d == null) return <span key={'e' + i} />;
          const date = new Date(month.getFullYear(), month.getMonth(), d);
          const k = jToKey(date);
          const isFuture = date > today && !jSameDay(date, today);
          const cls = ['j2-cal-day'];
          if (jSameDay(date, selected)) cls.push('sel');
          if (jSameDay(date, today)) cls.push('today');
          if (isFuture) cls.push('future');
          return (
            <button
              key={k}
              className={cls.join(' ')}
              disabled={isFuture}
              onClick={() => onPick(date)}
            >
              {d}
              {entries[k] && j2HasContent(entries[k]) && <span className="j2-cal-dot" />}
            </button>
          );
        })}
      </div>
    </div>
  );
}

// ── Felles «verdi i ring»-logikk for alle tracker-typer ──────────
// Gir samme visuelle språk overalt: tall for skala/teller, hake for
// avkrysning, emoji for stemning, anførselstegn for tekst.
function j2CircleData(type, value) {
  if (type === 'mood') {
    const has = value != null;
    return { content: has ? J_MOOD_EMOJIS[value] : '·', pct: has ? (value + 1) * 20 : 0, empty: !has, emoji: true };
  }
  if (type === 'scale') {
    const has = value != null && value !== '';
    return { content: has ? value : '–', pct: has ? value * 10 : 0, empty: !has };
  }
  if (type === 'counter') {
    const has = value != null && value !== '';
    return { content: has ? value : '–', pct: has ? Math.min(100, value * 10) : 0, empty: !has };
  }
  if (type === 'checkbox') {
    return { content: value ? '✓' : '–', pct: value ? 100 : 0, empty: !value };
  }
  const has = value != null && value !== '';
  return { content: has ? '❝' : '–', pct: has ? 100 : 0, empty: !has };
}

// ── Kompakt editor for valgt dag på utviklingssiden ──────────────
function J2HistEditor({ type, value, onSet }) {
  if (type === 'counter') {
    return (
      <div className="j2-hist-edit-ctl">
        <button className="j2-stepbtn" onClick={() => onSet(Math.max(0, (value || 0) - 1))}>−</button>
        <span className="j2-trk-bigval">{value || 0}</span>
        <button className="j2-stepbtn" onClick={() => onSet((value || 0) + 1)}>+</button>
      </div>
    );
  }
  if (type === 'checkbox') {
    return (
      <button className={`j2-hist-toggle${value ? ' on' : ''}`} onClick={() => onSet(!value)}>
        {value ? '✓' : '○'}
      </button>
    );
  }
  if (type === 'text') {
    return (
      <input
        className="j2-trk-text"
        defaultValue={value || ''}
        onBlur={e => onSet(e.target.value)}
        onKeyDown={e => { if (e.key === 'Enter') e.target.blur(); }}
      />
    );
  }
  // skala 1–10
  return (
    <div className="j2-hist-edit-ctl j2-hist-scale">
      {Array.from({ length: 10 }, (_, i) => (
        <button
          key={i}
          className={`j2-sdot${value === i + 1 ? ' on' : ''}`}
          onClick={() => onSet(i + 1)}
          title={String(i + 1)}
        >{i + 1}</button>
      ))}
    </div>
  );
}

// ── Sammendrags-flis: ring med resultat + navn under ─────────────
// Brukes i den rolige oversikten på venstresiden («kun resultatet»).
function J2SumTile({ name, type, value, pct, fraction, small, empty, emoji, onClick, title }) {
  // To moduser: tracker (type+value → ring) eller brøk (value+pct, f.eks 2/4)
  const c = type ? j2CircleData(type, value) : { content: value, pct: pct || 0, empty, emoji };
  return (
    <button className="j2-sum-tile" onClick={onClick} title={title || name}>
      <span className={`j2-sum-ring r-md${c.emoji ? ' emo' : ''}`} style={{ '--p': c.pct }}>
        <span className={`${small || fraction ? 'sm' : ''}${c.empty ? ' dim' : ''}`}>{c.content}</span>
      </span>
      <span className="j2-sum-name">{name}</span>
    </button>
  );
}

// ── Redigeringspanel for trackere ────────────────────────────────
function J2TrackerManage({ trackers, onRename, onArchive, onAdd, T }) {
  const [type, setType] = useJ2State('counter');
  return (
    <div className="j2-mg">
      {trackers.map(t => (
        <div key={t.id} className="j2-mg-row">
          <span className="j2-trk-ico">{jGetTrackerIcon(t.id, t.name)}</span>
          <input
            className="j2-mg-input"
            defaultValue={t.name}
            onBlur={e => { const v = e.target.value.trim(); if (v && v !== t.name) onRename(t.id, v); }}
            onKeyDown={e => { if (e.key === 'Enter') e.target.blur(); }}
          />
          <button className="j2-mg-del" onClick={() => onArchive(t.id)} title={T.archiveTracker}>
            <i className="ti ti-archive"></i>
          </button>
        </div>
      ))}
      <div className="j2-mg-add">
        <select className="j2-mg-type" value={type} onChange={e => setType(e.target.value)}>
          <option value="counter">{T.typeCounter}</option>
          <option value="scale">{T.typeScale}</option>
          <option value="checkbox">{T.typeCheckbox}</option>
          <option value="text">{T.typeText}</option>
        </select>
        <QuickAdd size="sm" placeholder={T.newTracker} onAdd={name => onAdd(name, type)} />
      </div>
    </div>
  );
}

// ══════════════════════════════════════════════════════════════════
// Journal2 — hovedside
// ══════════════════════════════════════════════════════════════════
function Journal2() {
  const today = new Date();
  const [selected, setSelected] = useJ2State(today);
  const [entries, setEntries] = useJ2State({});
  const [loading, setLoading] = useJ2State(true);
  const [saveState, setSaveState] = useJ2State('idle'); // idle | saving | saved
  const [lang, setLang] = useJ2State(() => getJLang());
  const [flipDir, setFlipDir] = useJ2State(0); // animasjonsretning ved sideblaing
  const [isNarrow, setIsNarrow] = useJ2State(() => window.innerWidth < 900);
  const [view, setView] = useJ2State('today'); // oppslaget: 'today' | 'overview'
  const [rightMode, setRightMode] = useJ2State('write'); // høyresiden i dag-oppslag: 'write' | 'details' | 'history'
  const [histTrk, setHistTrk] = useJ2State(null); // valgt tracker for utviklingssiden (kan være mood-pseudo)
  const [histPage, setHistPage] = useJ2State(0); // hvor langt tilbake i tid utviklingssiden blar
  const [histData, setHistData] = useJ2State(null); // { trackerId, days:[{key,val}] }
  const [histReturn, setHistReturn] = useJ2State('write'); // hvor utviklingssiden blar tilbake til
  const [regPage, setRegPage] = useJ2State(0); // oppslag-indeks i Oversikt (0 = side 1+2)
  const [calMonth, setCalMonth] = useJ2State(() => new Date(today.getFullYear(), today.getMonth(), 1));
  const [trackers, setTrackers] = useJ2State([]);
  const [routineDefs, setRoutineDefs] = useJ2State([]);
  const [dayData, setDayData] = useJ2State({ vals: {}, tasks: [], routineDone: {}, goalsDone: [], projDone: [] });
  const [manage, setManage] = useJ2State(false);
  // På mobil starter boken alltid LUKKET i midten (og auto-åpnes nedenfor hvis
  // ingen PIN er satt). På desktop beholdes dagens oppførsel (åpen med en gang).
  const [phase, setPhase] = useJ2State(() => (window.innerWidth < 900 ? 'closed' : 'open')); // open | closing | closed | opening
  const [theme, setTheme] = useJ2State(() => {
    const t = localStorage.getItem(J2_THEME_KEY);
    return J2_THEMES.includes(t) ? t : 'leather';
  });
  const [customColor, setCustomColor] = useJ2State(
    () => localStorage.getItem(J2_CUSTOM_COLOR_KEY) || '#6a4f8a'
  );
  const [font, setFont] = useJ2State(() => {
    const f = localStorage.getItem(J2_FONT_KEY);
    return J2_FONTS.includes(f) ? f : 'serif';
  });
  const [settingsOpen, setSettingsOpen] = useJ2State(false);
  const [pinSettings, setPinSettings] = useJ2State({ enabled: false, pin: '' });
  const [pinInput, setPinInput] = useJ2State('');
  const [pinError, setPinError] = useJ2State(false);
  const [pinMode, setPinMode] = useJ2State('idle'); // idle | set
  const pendingRef = useJ2Ref(null);
  const saveTimerRef = useJ2Ref(null);
  const flushSaveRef = useJ2Ref(null);
  const dayCacheRef = useJ2Ref({});
  const histCacheRef = useJ2Ref({});

  const T = j2t(lang);
  const dayFull = J_DAY_FULL[lang] || J_DAY_FULL.no;

  useJ2Effect(() => {
    const onResize = () => setIsNarrow(window.innerWidth < 900);
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);

  // Mobil: skjul app-toppen (logo + Jobb/Privat) og lås skjermen KUN når boken
  // er åpen (fullskjerm-skriving). Når boken er lukket vises den normale app-
  // banneren (samme som ellers i appen). CSS-en er scopet til smal skjerm, så
  // desktop påvirkes ikke.
  // j2-lock ligger på HELE journal-siden (også lukket bok): den låser body-
  // scroll på mobil slik at boken ikke kan dras opp under toppbanneret —
  // eventuell overflow ruller inni .j2-stage i stedet.
  useJ2Effect(() => {
    document.body.classList.add('j2-lock');
    if (phase === 'closed') {
      document.body.classList.remove('j2-mobile');
    } else {
      document.body.classList.add('j2-mobile');
    }
    return () => document.body.classList.remove('j2-mobile', 'j2-lock');
  }, [phase]);

  // Boken åpnes ikke lenger automatisk — brukeren slår den opp selv via
  // «Åpne boken» på omslaget (gjelder både mobil og desktop).

  useJ2Effect(() => {
    const handler = e => setLang(e.detail || getJLang());
    window.addEventListener('el-lang-change', handler);
    return () => window.removeEventListener('el-lang-change', handler);
  }, []);

  // Flush usagrede endringer når siden forlates
  useJ2Effect(() => {
    const flush = () => flushSaveRef.current && flushSaveRef.current();
    const onVis = () => { if (document.visibilityState === 'hidden') flush(); };
    document.addEventListener('visibilitychange', onVis);
    window.addEventListener('pagehide', flush);
    return () => {
      document.removeEventListener('visibilitychange', onVis);
      window.removeEventListener('pagehide', flush);
      flush();
    };
  }, []);

  useJ2Effect(() => { loadData(); }, []);

  async function loadData() {
    setLoading(true);
    try {
      if (window.EASYLIFE_DEMO?.enabled) {
        const cached = localStorage.getItem('el-journal-v2-demo');
        setEntries(cached ? JSON.parse(cached) : {});
        setTrackers(J2_DEMO_TRACKERS);
        setRoutineDefs((window.EASYLIFE_DEMO.routines || []).map(r => ({ id: r.id, name: r.name })));
        const localPin = j2ReadLocalPin();
        if (localPin) {
          setPinSettings({ enabled: !!localPin.enabled, pin: localPin.pin || '' });
          if (localPin.enabled) setPhase(j2WithinGrace() ? 'open' : 'closed');
        }
        setLoading(false);
        return;
      }
      const uid = await window.ensureCurrentUserId();
      if (!uid) { setEntries({}); setLoading(false); return; }

      const [{ data: logs }, { data: moods }, { data: trackerDefs }, { data: rDefs }, pinRes] = await Promise.all([
        supabaseClient.from('logbook_entries')
          .select('entry_date, heading, notes')
          .eq('user_id', uid)
          .order('entry_date', { ascending: false })
          .limit(120),
        supabaseClient.from('mood_entries')
          .select('entry_date, mood')
          .eq('user_id', uid)
          .order('entry_date', { ascending: false })
          .limit(120),
        supabaseClient.from('logbook_trackers')
          .select('id, name, type, created_at')
          .eq('user_id', uid)
          .is('archived_at', null)
          .order('created_at', { ascending: true }),
        supabaseClient.from('routines')
          .select('id, name, position')
          .is('archived_at', null)
          .order('position', { ascending: true })
          .limit(30),
        supabaseClient.from('logbook_pin_settings')
          .select('pin_code, pin_enabled')
          .eq('user_id', uid)
          .maybeSingle(),
      ]);

      const map = {};
      (logs || []).forEach(r => {
        map[r.entry_date] = { ...(map[r.entry_date] || {}), text: r.notes || '', heading: r.heading || '' };
      });
      const moodMap = { low: 0, meh: 1, okay: 2, good: 3, great: 4, very_sad: 0, sad: 1, neutral: 2, happy: 3, very_happy: 4 };
      (moods || []).forEach(r => {
        const idx = typeof r.mood === 'number' ? r.mood : moodMap[String(r.mood)];
        if (idx != null) map[r.entry_date] = { ...(map[r.entry_date] || {}), mood: idx };
      });
      setEntries(map);
      setTrackers(trackerDefs || []);
      setRoutineDefs(rDefs || []);

      // PIN: Supabase er fasit, lokal kopi er fallback (samme som v1)
      const pinRow = pinRes && !pinRes.error ? pinRes.data : null;
      const pin = pinRow
        ? { enabled: !!pinRow.pin_enabled, pin: pinRow.pin_code || '' }
        : (j2ReadLocalPin() || { enabled: false, pin: '' });
      setPinSettings(pin);
      j2WriteLocalPin(pin);
      // Innen 15-minutters-vinduet etter en opplåsing åpnes boken direkte, så
      // man kan hoppe mellom Easylife-sidene uten å taste PIN på nytt.
      if (pin.enabled) setPhase(j2WithinGrace() ? 'open' : 'closed');
    } catch (e) {
      console.warn('[Journal2] load failed', e);
      setEntries({});
    } finally {
      setLoading(false);
    }
  }

  const key = jToKey(selected);
  const entry = entries[key] || {};
  const isToday = jSameDay(selected, today);

  // ── Dagsdata: trackerverdier, oppgaver og rutinestatus ──────────
  useJ2Effect(() => {
    if (loading) return;
    loadDay(key);
  }, [key, trackers, loading]);

  // Utviklingsdata for én tracker lastes når utviklingssiden er åpen
  useJ2Effect(() => {
    if (loading || rightMode !== 'history' || !histTrk) return;
    loadHist(histTrk, key, histPage);
  }, [rightMode, histTrk, histPage, key, loading]);

  // Kalenderen følger valgt dato
  useJ2Effect(() => {
    setCalMonth(m => (m.getFullYear() === selected.getFullYear() && m.getMonth() === selected.getMonth())
      ? m : new Date(selected.getFullYear(), selected.getMonth(), 1));
  }, [key]);

  async function loadDay(k) {
    const cached = dayCacheRef.current[k];
    if (cached) { setDayData(cached); return; }

    if (window.EASYLIFE_DEMO?.enabled) {
      const isT = k === jToKey(today);
      const dvals = {};
      trackers.forEach(t => {
        let h = 11; (t.id + k).split('').forEach(c => { h = (h * 31 + c.charCodeAt(0)) >>> 0; });
        const ty = t.type || 'scale';
        dvals[t.id] = ty === 'checkbox' ? h % 3 !== 0 : ty === 'counter' ? (h % 8) + 1 : (h % 10) + 1;
      });
      const data = {
        vals: dvals,
        tasks: isT
          ? (window.EASYLIFE_DEMO.tasks || []).map(t => ({ id: t.id, name: t.name, done: !!t.completed }))
          : [],
        routineDone: isT
          ? Object.fromEntries((window.EASYLIFE_DEMO.routines || []).filter(r => r.completed).map(r => [r.id, true]))
          : {},
        goalsDone: isT
          ? (window.EASYLIFE_DEMO.goalTodosDone || []).map(g => ({ id: g.id, name: g.name }))
          : [],
        projDone: isT
          ? (window.EASYLIFE_DEMO.projectTodosDone || []).map(p => ({ id: p.id, name: p.name }))
          : [],
      };
      dayCacheRef.current[k] = data;
      setDayData(data);
      return;
    }

    try {
      const uid = await window.ensureCurrentUserId();
      if (!uid) return;
      const next = jFromKey(k);
      next.setDate(next.getDate() + 1);
      const nextK = jToKey(next);
      const ids = trackers.map(t => t.id);
      const rids = routineDefs.map(r => r.id);

      const [tvRes, taskRes, reRes, goalRes, projRes] = await Promise.all([
        ids.length
          ? supabaseClient.from('logbook_tracker_entries')
              .select('tracker_id, value_scale, value_text, value_counter, value_checkbox')
              .eq('user_id', uid)
              .eq('entry_date', k)
              .in('tracker_id', ids)
          : Promise.resolve({ data: [] }),
        supabaseClient.from('todo_tasks')
          .select('id, name, completed_at, due_date, position')
          .or(`due_date.eq.${k},and(completed_at.gte.${k},completed_at.lt.${nextK})`)
          .order('position', { ascending: true }),
        rids.length
          ? supabaseClient.from('routine_entries')
              .select('routine_id, status')
              .in('routine_id', rids)
              .eq('entry_date', k)
          : Promise.resolve({ data: [] }),
        // Mål-oppgaver (delmål-todos) fullført denne dagen — RLS begrenser til egne mål
        supabaseClient.from('goal_milestone_todos')
          .select('id, title, completed_at')
          .gte('completed_at', k)
          .lt('completed_at', nextK),
        // Prosjekt-oppgaver fullført denne dagen — RLS begrenser til egne prosjekter
        supabaseClient.from('project_todos')
          .select('id, name, completed_at')
          .gte('completed_at', k)
          .lt('completed_at', nextK),
      ]);

      const typeOf = {};
      trackers.forEach(t => { typeOf[t.id] = t.type || 'scale'; });
      const vals = {};
      (tvRes.data || []).forEach(r => {
        const type = typeOf[r.tracker_id];
        vals[r.tracker_id] = type === 'scale' ? r.value_scale
          : type === 'counter' ? r.value_counter
          : type === 'checkbox' ? r.value_checkbox
          : r.value_text;
      });
      const data = {
        vals,
        tasks: (taskRes.data || []).map(t => ({ id: t.id, name: t.name, done: !!t.completed_at })),
        routineDone: Object.fromEntries((reRes.data || []).filter(e => e.status === 'done').map(e => [e.routine_id, true])),
        goalsDone: (goalRes.data || []).map(g => ({ id: g.id, name: g.title })),
        projDone: (projRes.data || []).map(p => ({ id: p.id, name: p.name })),
      };
      dayCacheRef.current[k] = data;
      setDayData(data);
    } catch (e) {
      console.warn('[Journal2] day load failed', e);
    }
  }

  // Alle dager (stigende) i kalendermåneden `page` måneder før valgt dag
  function j2HistMonth(anchorK, page) {
    const a = jFromKey(anchorK);
    const first = new Date(a.getFullYear(), a.getMonth() - page, 1);
    const y = first.getFullYear(), mo = first.getMonth();
    const n = new Date(y, mo + 1, 0).getDate();
    const days = [];
    for (let d = 1; d <= n; d++) days.push(jToKey(new Date(y, mo, d)));
    return { y, mo, days };
  }

  // Last per-dag-verdier for ÉN tracker (eller stemnings-pseudoen) for én måned
  async function loadHist(trk, anchorK, page) {
    const cacheKey = `${trk.id}|${anchorK.slice(0, 7)}|${page}`;
    const cached = histCacheRef.current[cacheKey];
    if (cached) { setHistData(cached); return; }
    const { y, mo, days } = j2HistMonth(anchorK, page);
    const type = trk.type || 'scale';
    const meta = { trackerId: trk.id, page, y, mo };

    // Stemning leses fra journalinnslagene vi allerede har i minne
    if (trk.id === J2_MOOD_ID) {
      const data = { ...meta, days: days.map(k => ({ key: k, val: entries[k]?.mood ?? null })) };
      histCacheRef.current[cacheKey] = data;
      setHistData(data);
      return;
    }

    // Rutiner: per-dag-status fra routine_entries (behandles som avkrysning)
    if (trk.source === 'routine') {
      if (window.EASYLIFE_DEMO?.enabled) {
        const data = {
          ...meta,
          days: days.map(k => {
            let h = 5; (trk.id + k).split('').forEach(c => { h = (h * 31 + c.charCodeAt(0)) >>> 0; });
            return { key: k, val: h % 3 !== 0 ? true : null };
          }),
        };
        histCacheRef.current[cacheKey] = data;
        setHistData(data);
        return;
      }
      try {
        const { data: rows } = await supabaseClient.from('routine_entries')
          .select('entry_date, status')
          .eq('routine_id', trk.id)
          .gte('entry_date', days[0])
          .lte('entry_date', days[days.length - 1]);
        const byDay = {};
        (rows || []).forEach(r => { if (r.status === 'done') byDay[r.entry_date] = true; });
        const data = { ...meta, days: days.map(k => ({ key: k, val: byDay[k] ? true : null })) };
        histCacheRef.current[cacheKey] = data;
        setHistData(data);
      } catch (e) {
        console.warn('[Journal2] routine history load failed', e);
      }
      return;
    }

    if (window.EASYLIFE_DEMO?.enabled) {
      const data = {
        ...meta,
        days: days.map(k => {
          let h = 7;
          (trk.id + k).split('').forEach(c => { h = (h * 31 + c.charCodeAt(0)) >>> 0; });
          // ~25 % av dagene er bevisst uten registrering (tom sirkel)
          if (h % 4 === 0) return { key: k, val: null };
          const val = type === 'checkbox' ? h % 5 !== 0 : type === 'counter' ? (h % 6) + 1 : (h % 10) + 1;
          return { key: k, val };
        }),
      };
      histCacheRef.current[cacheKey] = data;
      setHistData(data);
      return;
    }

    try {
      const uid = await window.ensureCurrentUserId();
      if (!uid) return;
      const { data: rows } = await supabaseClient.from('logbook_tracker_entries')
        .select('entry_date, value_scale, value_text, value_counter, value_checkbox')
        .eq('user_id', uid)
        .eq('tracker_id', trk.id)
        .gte('entry_date', days[0])
        .lte('entry_date', days[days.length - 1]);
      const byDay = {};
      (rows || []).forEach(r => {
        byDay[r.entry_date] = type === 'scale' ? r.value_scale
          : type === 'counter' ? r.value_counter
          : type === 'checkbox' ? r.value_checkbox
          : r.value_text;
      });
      const data = { ...meta, days: days.map(k => ({ key: k, val: byDay[k] ?? null })) };
      histCacheRef.current[cacheKey] = data;
      setHistData(data);
    } catch (e) {
      console.warn('[Journal2] history load failed', e);
    }
  }

  function patchDay(patch) {
    setDayData(prev => {
      const next = { ...prev, ...patch };
      dayCacheRef.current[key] = next;
      return next;
    });
  }

  async function setTrackerVal(tracker, val) {
    patchDay({ vals: { ...dayData.vals, [tracker.id]: val } });
    // Hold utviklingssiden i synk med den nye verdien
    histCacheRef.current = {};
    setHistData(prev => (prev && prev.trackerId === tracker.id)
      ? { ...prev, days: prev.days.map(d => d.key === key ? { ...d, val } : d) }
      : prev);
    if (window.EASYLIFE_DEMO?.enabled) return;
    try {
      const uid = await window.ensureCurrentUserId();
      if (!uid) return;
      const type = tracker.type || 'scale';
      await supabaseClient.from('logbook_tracker_entries').upsert({
        user_id: uid,
        tracker_id: tracker.id,
        entry_date: key,
        value_scale: type === 'scale' ? val : null,
        value_text: type === 'text' ? val : null,
        value_counter: type === 'counter' ? val : null,
        value_checkbox: type === 'checkbox' ? !!val : null,
      }, { onConflict: 'tracker_id,entry_date' });
    } catch (e) {
      console.warn('[Journal2] tracker save failed', e);
    }
  }

  async function toggleTask(t) {
    if (!isToday) return;
    const newVal = t.done ? null : new Date().toISOString();
    patchDay({ tasks: dayData.tasks.map(x => x.id === t.id ? { ...x, done: !t.done } : x) });
    if (window.EASYLIFE_DEMO?.enabled) return;
    try {
      await supabaseClient.from('todo_tasks').update({ completed_at: newVal }).eq('id', t.id);
    } catch (e) {
      console.warn('[Journal2] task toggle failed', e);
    }
  }

  async function toggleRoutine(rid) {
    const wasDone = !!dayData.routineDone[rid];
    const nextDone = { ...dayData.routineDone };
    if (wasDone) delete nextDone[rid]; else nextDone[rid] = true;
    patchDay({ routineDone: nextDone });
    if (window.EASYLIFE_DEMO?.enabled) return;
    try {
      const uid = await window.ensureCurrentUserId();
      if (!uid) return;
      if (wasDone) {
        await supabaseClient.from('routine_entries').delete().eq('routine_id', rid).eq('entry_date', key);
      } else {
        await supabaseClient.from('routine_entries')
          .upsert({ routine_id: rid, user_id: uid, entry_date: key, status: 'done' }, { onConflict: 'routine_id,entry_date' });
      }
    } catch (e) {
      console.warn('[Journal2] routine toggle failed', e);
    }
  }

  // ── Rediger trackere (definisjoner) ─────────────────────────────
  async function addTracker(name, type) {
    if (window.EASYLIFE_DEMO?.enabled) {
      setTrackers(prev => [...prev, { id: 'demo-trk-' + Date.now(), name, type }]);
      return;
    }
    try {
      const uid = await window.ensureCurrentUserId();
      if (!uid) return;
      const { data, error } = await supabaseClient.from('logbook_trackers')
        .insert({ user_id: uid, name, type })
        .select()
        .single();
      if (!error && data) setTrackers(prev => [...prev, data]);
    } catch (e) {
      console.warn('[Journal2] add tracker failed', e);
    }
  }

  async function renameTracker(id, name) {
    setTrackers(prev => prev.map(t => t.id === id ? { ...t, name } : t));
    if (window.EASYLIFE_DEMO?.enabled) return;
    try {
      await supabaseClient.from('logbook_trackers').update({ name }).eq('id', id);
    } catch (e) {
      console.warn('[Journal2] rename tracker failed', e);
    }
  }

  async function archiveTracker(id) {
    setTrackers(prev => prev.filter(t => t.id !== id));
    if (window.EASYLIFE_DEMO?.enabled) return;
    try {
      await supabaseClient.from('logbook_trackers')
        .update({ archived_at: new Date().toISOString() })
        .eq('id', id);
    } catch (e) {
      console.warn('[Journal2] archive tracker failed', e);
    }
  }

  // ── Lukk/åpne boken + PIN-lås ───────────────────────────────────
  function closeBook() {
    flushSave();
    // «Lukk» låser boken igjen umiddelbart — nullstill 15-min-vinduet.
    j2ClearUnlock();
    setPinInput(''); setPinError(false); setPinMode('idle');
    setPhase('closing');
    setTimeout(() => setPhase('closed'), 470);
  }

  function openBook() {
    if (pinSettings.enabled && pinInput !== pinSettings.pin) {
      setPinError(true);
      return;
    }
    // Start 15-min-vinduet ved hver vellykket opplåsing (kun når PIN er aktiv).
    if (pinSettings.enabled) j2MarkUnlocked();
    setPinInput(''); setPinError(false);
    setPhase('opening');
    setTimeout(() => setPhase('open'), 520);
  }

  // Auto-lås når 15-min-vinduet løper ut mens boken står åpen.
  useJ2Effect(() => {
    if (!pinSettings.enabled || phase !== 'open') return;
    const unlockedAt = +localStorage.getItem(J2_UNLOCK_KEY) || Date.now();
    const remaining = J2_GRACE_MS - (Date.now() - unlockedAt);
    if (remaining <= 0) { closeBook(); return; }
    const id = setTimeout(() => closeBook(), remaining);
    return () => clearTimeout(id);
  }, [pinSettings.enabled, phase]);

  // Mobil «logg»-oppslag: oversiktssirkler. Klikk på en sirkel blar videre til
  // detaljer (oppgaver/rutiner) eller utvikling (trackere/stemning).
  function openOverview() { flushSave(); setFlipDir(1); setRightMode('overview'); }
  function closeOverview() { setFlipDir(-1); setRightMode('write'); }
  // Innholdsfortegnelse (TOC) som egen mobilside, nås fra oversikten.
  function openRegister() { flushSave(); setFlipDir(1); setRightMode('register'); }
  function closeRegister() { setFlipDir(-1); setRightMode('overview'); }

  async function persistPin(settings) {
    j2WriteLocalPin(settings);
    if (window.EASYLIFE_DEMO?.enabled) return;
    try {
      const uid = await window.ensureCurrentUserId();
      if (!uid) return;
      await supabaseClient.from('logbook_pin_settings').upsert({
        user_id: uid,
        pin_code: settings.pin,
        pin_enabled: settings.enabled,
        updated_at: new Date().toISOString(),
      }, { onConflict: 'user_id' });
    } catch (e) {
      console.warn('[Journal2] pin save failed', e);
    }
  }

  function savePin() {
    const code = pinInput.trim();
    if (!/^\d{4,6}$/.test(code)) { setPinError(true); return; }
    const s = { enabled: true, pin: code };
    setPinSettings(s);
    setPinMode('idle'); setPinInput(''); setPinError(false);
    persistPin(s);
  }

  function removePin() {
    if (pinInput !== pinSettings.pin) { setPinError(true); return; }
    const s = { enabled: false, pin: '' };
    setPinSettings(s);
    setPinInput(''); setPinError(false);
    persistPin(s);
  }

  function pickTheme(t) {
    setTheme(t);
    try { localStorage.setItem(J2_THEME_KEY, t); } catch (e) { /* ignorer */ }
  }

  function pickCustomColor(c) {
    setCustomColor(c);
    setTheme('custom');
    try {
      localStorage.setItem(J2_CUSTOM_COLOR_KEY, c);
      localStorage.setItem(J2_THEME_KEY, 'custom');
    } catch (e) { /* ignorer */ }
  }

  function pickFont(f) {
    setFont(f);
    try { localStorage.setItem(J2_FONT_KEY, f); } catch (e) { /* ignorer */ }
  }

  // ── Journaltekst/stemning (som før) ─────────────────────────────
  async function persistEntry(e, entryKey) {
    if (window.EASYLIFE_DEMO?.enabled) return;
    const uid = await window.ensureCurrentUserId();
    if (!uid) return;
    try {
      if ('text' in e || 'heading' in e) {
        await supabaseClient.from('logbook_entries')
          .upsert({ user_id: uid, entry_date: entryKey, heading: e.heading || '', notes: e.text || '' }, { onConflict: 'user_id,entry_date' });
      }
      if ('mood' in e && e.mood != null) {
        const moodNames = ['low', 'meh', 'okay', 'good', 'great'];
        await supabaseClient.from('mood_entries')
          .upsert({ user_id: uid, entry_date: entryKey, mood: moodNames[e.mood] || 'okay' }, { onConflict: 'user_id,entry_date' });
      }
      setSaveState('saved');
      setTimeout(() => setSaveState(s => (s === 'saved' ? 'idle' : s)), 2200);
    } catch (err) {
      console.warn('[Journal2] save failed', err);
      setSaveState('idle');
    }
  }

  function flushSave() {
    if (saveTimerRef.current) { clearTimeout(saveTimerRef.current); saveTimerRef.current = null; }
    const p = pendingRef.current;
    pendingRef.current = null;
    if (p) persistEntry(p.entry, p.key);
  }
  flushSaveRef.current = flushSave;

  function update(patch) {
    const next = { ...entries, [key]: { ...entry, ...patch } };
    setEntries(next);
    if (window.EASYLIFE_DEMO?.enabled) {
      localStorage.setItem('el-journal-v2-demo', JSON.stringify(next));
      return;
    }
    if ('text' in patch || 'heading' in patch) {
      setSaveState('saving');
      pendingRef.current = { key, entry: next[key] };
      clearTimeout(saveTimerRef.current);
      saveTimerRef.current = setTimeout(() => {
        const p = pendingRef.current;
        pendingRef.current = null;
        saveTimerRef.current = null;
        if (p) persistEntry(p.entry, p.key);
      }, 900);
    } else {
      setSaveState('saving');
      persistEntry({ ...next[key], ...patch }, key);
    }
  }

  function goTo(d, dir) {
    flushSave();
    setFlipDir(dir);
    setSelected(d);
  }
  // Bla forbi tomme dager: finn neste/forrige dag med faktisk skrevet innhold
  // (jf. kalender-prikkene, som bruker samme j2HasContent-sjekk). Fremover
  // stopper søket senest på dagens dato — man skal aldri kunne bla inn i
  // fremtiden. Finnes ingen dag med innhold innenfor søkevinduet, faller vi
  // tilbake til å bla én dag av gangen som før.
  function findAdjacentEntryDay(from, dir) {
    const todayKey = jToKey(today);
    const d = new Date(from);
    for (let i = 0; i < 3650; i++) {
      d.setDate(d.getDate() + dir);
      const k = jToKey(d);
      if (dir > 0 && k >= todayKey) return new Date(d);
      if (entries[k] && j2HasContent(entries[k])) return new Date(d);
    }
    return null;
  }
  function goPrev() {
    const d = findAdjacentEntryDay(selected, -1) || new Date(selected.getFullYear(), selected.getMonth(), selected.getDate() - 1);
    goTo(d, -1);
  }
  function goNext() {
    if (isToday) return;
    const d = findAdjacentEntryDay(selected, 1) || new Date(selected.getFullYear(), selected.getMonth(), selected.getDate() + 1);
    goTo(d, 1);
  }

  // ── Mobil: sveip venstre/høyre for å bla mellom dager ──
  // Aktiv kun på de dag-baserte sidene (skriv / oppgaver / rutiner / fullført),
  // ikke i oversikt-/register-visningen som har sin egen navigasjon.
  const j2SwipeRef = useJ2Ref({ active: false, startX: 0, startY: 0, axis: null });
  const j2SwipeEnabled = isNarrow && (rightMode === 'write' || rightMode === 'tasks' || rightMode === 'routines' || rightMode === 'completed');
  function j2HandleTouchStart(e) {
    if (!j2SwipeEnabled) return;
    const t = e.touches[0];
    j2SwipeRef.current = { active: true, startX: t.clientX, startY: t.clientY, axis: null };
  }
  function j2HandleTouchMove(e) {
    if (!j2SwipeRef.current.active) return;
    const t = e.touches[0];
    const dx = t.clientX - j2SwipeRef.current.startX;
    const dy = t.clientY - j2SwipeRef.current.startY;
    if (!j2SwipeRef.current.axis && (Math.abs(dx) > 8 || Math.abs(dy) > 8)) {
      j2SwipeRef.current.axis = Math.abs(dx) > Math.abs(dy) ? 'x' : 'y';
    }
  }
  function j2HandleTouchEnd(e) {
    const s = j2SwipeRef.current;
    if (!s.active) return;
    if (s.axis === 'x') {
      const t = e.changedTouches[0];
      const dx = t.clientX - s.startX;
      const threshold = 50;
      if (dx < -threshold) goNext();
      else if (dx > threshold) goPrev();
    }
    j2SwipeRef.current = { active: false, startX: 0, startY: 0, axis: null };
  }
  function pickDay(d) {
    if (view === 'overview') { setView('today'); setRightMode('write'); }
    // På mobil nås kalenderen/TOC fra egne sider — gå tilbake til skrivesiden.
    if (isNarrow) setRightMode('write');
    goTo(d, d < selected ? -1 : 1);
  }

  // Detalj-sider: hver «Fullført i dag»-flis blar over til sin egen side.
  function openDetailMode(mode) {
    if (rightMode === mode) return;
    flushSave();
    setFlipDir(1);
    setRightMode(mode);
  }
  function openTasks() { openDetailMode('tasks'); }
  function openRoutines() { openDetailMode('routines'); }
  function openCompleted() { openDetailMode('completed'); }
  function closeDetail() {
    flushSave();
    setFlipDir(-1);
    // På mobil nås detaljene fra «logg»-oversikten, så vi blar tilbake dit.
    setRightMode(isNarrow ? 'overview' : 'write');
  }
  // Ett nivå dypere: utviklingssiden for én tracker, rutine eller stemning.
  // `back` sier hvilken side «tilbake» skal bla til.
  function openHistory(trk, back) {
    setFlipDir(1);
    setHistTrk(trk);
    setHistPage(0);
    setHistData(null);
    setHistReturn(back || (isNarrow ? 'overview' : 'write'));
    setRightMode('history');
  }
  function closeHistory() {
    setFlipDir(-1);
    setHistTrk(null);
    setRightMode(histReturn);
  }
  function histGo(step) {
    const next = Math.max(0, histPage + step); // 0 = nyeste vindu, høyere = lenger tilbake
    if (next === histPage) return;
    setFlipDir(step > 0 ? -1 : 1);
    setHistPage(next);
  }

  const wordCount = (entry.text || '').replace(/<[^>]*>/g, '').trim().split(/\s+/).filter(Boolean).length;

  // Oversikt: alle sider med innhold som flat liste (månedsoverskrifter
  // er egne rader), delt opp i boksider à J2_REG_PER_PAGE rader.
  const regItems = useJ2Memo(() => {
    const list = Object.entries(entries)
      .filter(([, v]) => j2HasText(v))
      .sort(([a], [b]) => b.localeCompare(a));
    const items = [];
    let lastMk = null;
    list.forEach(([k, v]) => {
      const mk = k.slice(0, 7);
      if (mk !== lastMk) { items.push({ type: 'month', mk }); lastMk = mk; }
      items.push({ type: 'entry', k, v });
    });
    return items;
  }, [entries]);

  const regPagesArr = useJ2Memo(() => {
    const pages = [];
    for (let i = 0; i < regItems.length; i += J2_REG_PER_PAGE) pages.push(regItems.slice(i, i + J2_REG_PER_PAGE));
    return pages.length ? pages : [[]];
  }, [regItems]);
  const regSpreads = Math.max(1, Math.ceil(regPagesArr.length / 2));

  function regGo(step) {
    const next = Math.min(regSpreads - 1, Math.max(0, regPage + step));
    if (next === regPage) return;
    setFlipDir(step > 0 ? 1 : -1);
    setRegPage(next);
  }

  const moodLabels = jt(lang).moods;
  const dayShort = J_DAY_SHORT[lang] || J_DAY_SHORT.no;
  const months = J_MONTH[lang] || J_MONTH.no;

  if (loading) {
    return (
      <div className="v2-page loading-spinner">
        <i className="ti ti-loader-2"></i>
        {T.loading}
      </div>
    );
  }

  // Oppgaver + rutiner for valgt dag (detaljsiden viser dem som lister)
  const taskItems = dayData.tasks.map(t => ({ key: 't' + t.id, kind: 'task', name: t.name, done: t.done, onToggle: () => toggleTask(t) }));
  const routineItems = routineDefs.map(r => ({ key: 'r' + r.id, kind: 'routine', name: r.name, done: !!dayData.routineDone[r.id], onToggle: () => toggleRoutine(r.id) }));
  const taskCount = taskItems.length;
  const taskDone = taskItems.filter(t => t.done).length;
  const routineDone = routineItems.filter(r => r.done).length;
  const goalsDoneCount = (dayData.goalsDone || []).length;
  const projDoneCount = (dayData.projDone || []).length;
  const isOverview = view === 'overview';

  // Stemning behandles som en tracker, slik at den får samme sirkel og
  // utviklingsside som de andre. Den ligger først i listen.
  const moodTrk = { id: J2_MOOD_ID, name: T.moodShort, type: 'mood' };
  const valOf = trk => (
    trk.id === J2_MOOD_ID ? (entry.mood ?? null)
      : trk.source === 'routine' ? !!dayData.routineDone[trk.id]
      : dayData.vals[trk.id]
  );

  // Mobil-navigasjon: ALLE sekundærknapper (Oversikt/I dag/tilbake) OG «Lukk»
  // ligger nå på TOPPEN. «Lukk» skyves helt til høyre (rett etter de andre
  // knappene) og holder seg klar av det røde bokmerket. Trackersiden har i
  // tillegg en midtstilt innholdsfortegnelse-knapp nederst (mobTocBar).
  const mobClose = (
    <button className="j2-mobar-btn j2-mobar-close" onClick={closeBook}>
      <i className="ti ti-book"></i> {T.tabClose}
    </button>
  );
  const mobTop = (content) => (
    <div className="j2-mtop">
      {content}
      {mobClose}
    </div>
  );
  // Midtstilt innholdsfortegnelse-knapp nederst (kun trackersiden).
  const mobTocBar = (btn) => <div className="j2-mtoc">{btn}</div>;

  // ── Venstre side, fane «I dag»: kalender + trackere + fullført ──
  const todayPage = (
    <React.Fragment>
      <div className="j2-top">
        <div className="j2-db">
          <div className="j2-db-wd">{dayFull[selected.getDay()]}</div>
          <div className="j2-db-num">{selected.getDate()}</div>
          <div className="j2-db-my">{months[selected.getMonth()]} {selected.getFullYear()}</div>
          {isToday
            ? <div className="j2-today-stamp">{T.today}</div>
            : <button className="j2-goto-today" onClick={() => pickDay(today)}>{T.gotoToday}</button>}
        </div>
        <div className="j2-top-divider" aria-hidden="true" />
        <J2Cal
          month={calMonth}
          onMonth={setCalMonth}
          selected={selected}
          today={today}
          entries={entries}
          onPick={pickDay}
          lang={lang}
        />
      </div>

      <div className="j2-flourish j2-flourish-sm">❦</div>

      <div className="j2-sec-head">
        <p className="j2-label">{T.trackersLabel}</p>
        <button
          className={`j2-iconbtn${manage ? ' on' : ''}`}
          onClick={() => setManage(!manage)}
          title={manage ? T.doneEditing : T.editTrackers}
        >
          <i className={`ti ti-${manage ? 'check' : 'pencil'}`}></i>
        </button>
      </div>
      {manage ? (
        <J2TrackerManage
          trackers={trackers}
          onRename={renameTracker}
          onArchive={archiveTracker}
          onAdd={addTracker}
          T={T}
        />
      ) : (
        <div className="j2-sum-grid">
          <J2SumTile name={T.moodShort} type="mood" value={entry.mood ?? null} onClick={() => openHistory(moodTrk)} />
          {trackers.map(t => (
            <J2SumTile
              key={t.id}
              name={t.name}
              type={t.type || 'scale'}
              value={dayData.vals[t.id]}
              onClick={() => openHistory(t)}
            />
          ))}
        </div>
      )}

      <div className="j2-flourish j2-flourish-sm">❦</div>

      <div className="j2-sec-head">
        <p className="j2-label">{T.completedLabel}</p>
      </div>
      <div className="j2-sum-grid">
        <J2SumTile
          name={T.tasksLabel}
          fraction
          value={`${taskDone}/${taskCount}`}
          pct={taskCount ? (taskDone / taskCount) * 100 : 0}
          empty={!taskCount}
          onClick={openTasks}
        />
        <J2SumTile
          name={T.routinesLabel}
          fraction
          value={`${routineDone}/${routineDefs.length}`}
          pct={routineDefs.length ? (routineDone / routineDefs.length) * 100 : 0}
          empty={!routineDefs.length}
          onClick={openRoutines}
        />
        <J2SumTile
          name={T.goalsLabel}
          small
          value={String(goalsDoneCount)}
          pct={goalsDoneCount ? 100 : 0}
          empty={!goalsDoneCount}
          onClick={openCompleted}
        />
        <J2SumTile
          name={T.projectsLabel}
          small
          value={String(projDoneCount)}
          pct={projDoneCount ? 100 : 0}
          empty={!projDoneCount}
          onClick={openCompleted}
        />
      </div>
    </React.Fragment>
  );

  // ── Oversikt: rader for én bokside (månedsoverskrifter + innslag) ──
  function regRows(items) {
    return items.map(it => {
      if (it.type === 'month') {
        const [y, m] = it.mk.split('-');
        return <div key={'m' + it.mk} className="j2-reg-month">{months[parseInt(m, 10) - 1]} {y}</div>;
      }
      const d = jFromKey(it.k);
      const v = it.v;
      const preview = v.heading || (v.text || '').replace(/<[^>]*>/g, '').trim().slice(0, 38) || T.noText;
      return (
        <button key={it.k} className="j2-toc-row" onClick={() => pickDay(d)}>
          <span className="j2-toc-title">{preview}</span>
          <span className="j2-toc-dots" />
          <span className="j2-toc-date">{dayShort[d.getDay()].toLowerCase()} {d.getDate()}/{d.getMonth() + 1}</span>
        </button>
      );
    });
  }

  // Oversikt-oppslaget: venstre bokside (side 1, 3, 5 …)
  const overviewLeft = (
    <React.Fragment>
      <div className="j2-colophon">
        <span className="j2-colophon-line" />
        <span className="j2-colophon-text">{T.registerTitle}</span>
        <span className="j2-colophon-line" />
      </div>
      <div className="j2-reg">
        {regItems.length === 0 && <p className="j2-toc-empty">{T.registerEmpty}</p>}
        {regRows(regPagesArr[regPage * 2] || [])}
      </div>
    </React.Fragment>
  );

  // Faner som ligger MELLOM de stablede sidekantene (egne lag bak sidene):
  // «Lukk» som horisontal fane på toppen, «I dag»/«Oversikt» på fore-kanten.
  const bookTabs = (
    <React.Fragment>
      <button className="j2-tab j2-tab-top" onClick={closeBook} title={T.closeBook}>{T.tabClose}</button>
      <div className="j2-tabs">
        <button
          className={`j2-tab${!isOverview ? ' on' : ''}`}
          onClick={() => { setView('today'); setRightMode('write'); }}
        >{T.tabToday}</button>
        <button
          className={`j2-tab${isOverview ? ' on' : ''}`}
          onClick={() => { setView('overview'); setRegPage(0); }}
        >{T.tabPages}</button>
      </div>
    </React.Fragment>
  );

  const leftPage = (
    <div className="j2-page j2-page-left" key={view + (isOverview ? '-' + regPage : '')}>
      {isOverview ? overviewLeft : todayPage}
      <div className="j2-pageno">— {T.page} {isOverview ? regPage * 2 + 1 : j2DayOfYear(selected)} —</div>
      <button
        className="j2-corner j2-corner-left"
        onClick={isOverview ? () => regGo(-1) : goPrev}
        disabled={isOverview && regPage === 0}
        title={T.prevDay}
      >
        <i className="ti ti-chevron-left"></i>
      </button>
    </div>
  );

  // ── Høyre side: selve skrivesiden ──
  // På smale skjermer vises bare denne siden — da får den en kompakt
  // dato- og stemningslinje øverst, og «forrige side»-hjørnet flyttes hit.
  const writePage = (
    <div className="j2-page j2-page-right" key={key + flipDir} data-flip={flipDir}>
      <div className="j2-ribbon" aria-hidden="true" />
      {isNarrow ? (
        mobTop(
          <button className="j2-mobar-btn" onClick={openOverview}>
            <i className="ti ti-layout-grid"></i> {T.tabPages}
          </button>
        )
      ) : (
        <div className="j2-dateline">
          <span className="j2-dateline-text">{dayFull[selected.getDay()]} {j2FmtDate(selected, lang)} <span className="j2-dateline-orn">✦</span></span>
          <span className="j2-dateline-page">{T.page} {j2DayOfYear(selected) + 1}</span>
        </div>
      )}
      {isNarrow ? (
        /* Dagens dato er standard-overskriften (placeholder). Den lille
           dato-etiketten til høyre vises KUN når man har skrevet en egen tittel. */
        <div className="j2-title-row">
          <input
            className="j2-title j2-title-inline"
            value={entry.heading || ''}
            placeholder={`${dayFull[selected.getDay()]} ${j2FmtDate(selected, lang)}`}
            onChange={e => update({ heading: e.target.value })}
          />
          {(entry.heading || '').trim() && (
            <span className="j2-title-date">{dayShort[selected.getDay()]} {selected.getDate()}.{months[selected.getMonth()]}</span>
          )}
        </div>
      ) : (
        <input
          className="j2-title"
          value={entry.heading || ''}
          placeholder={T.titlePlaceholder}
          onChange={e => update({ heading: e.target.value })}
        />
      )}
      <J2Editor key={key} text={entry.text || ''} onChange={t => update({ text: t })} placeholder={T.placeholder} />
      <div className="j2-page-foot">
        <span className="j2-words">{wordCount} {T.words}</span>
        <span className={`j2-save-state ${saveState}`}>
          {saveState === 'saving' ? T.saving : saveState === 'saved' ? `✓ ${T.saved}` : ''}
        </span>
      </div>
      {isNarrow && (
        <button className="j2-corner j2-corner-left" onClick={goPrev} title={T.prevDay}>
          <i className="ti ti-chevron-left"></i>
        </button>
      )}
      <button className="j2-corner j2-corner-right" onClick={goNext} disabled={isToday} title={T.nextDay}>
        <i className="ti ti-chevron-right"></i>
      </button>
    </div>
  );

  // ── Felles skall for detalj-sidene (oppgaver / rutiner / fullført) ──
  //    Rutiner og oppgaver har nå hver sin side, og «Fullført i dag» har
  //    en egen side med mål- og prosjekt-overskrifter.
  function detailShell(titleText, body, pageKey) {
    return (
      <div className="j2-page j2-page-right" key={pageKey + key + flipDir} data-flip={flipDir}>
        <div className="j2-ribbon" aria-hidden="true" />
        {isNarrow ? (
          mobTop(
            <button className="j2-mobar-btn" onClick={closeDetail}>
              <i className="ti ti-arrow-left"></i> {T.tabPages}
            </button>
          )
        ) : null}
        {isNarrow ? (
          <div className="j2-mov-date">
            <span className="j2-mov-wd">{titleText}</span>
            <span className="j2-mov-full">{j2FmtDate(selected, lang)}</span>
          </div>
        ) : (
          <div className="j2-dateline">
            <span className="j2-dateline-text">{titleText} <span className="j2-dateline-orn">✦</span></span>
            <span className="j2-dateline-page">{dayFull[selected.getDay()]} {selected.getDate()}/{selected.getMonth() + 1}</span>
          </div>
        )}

        {body}

        {!isNarrow && <button className="j2-more-btn j2-details-btn" onClick={closeDetail}>↩ {T.backToWrite}</button>}

        <div className="j2-pageno">— {T.page} {j2DayOfYear(selected) + 1} —</div>
        {isNarrow && (
          <button className="j2-corner j2-corner-left" onClick={goPrev} title={T.prevDay}>
            <i className="ti ti-chevron-left"></i>
          </button>
        )}
        <button className="j2-corner j2-corner-right" onClick={goNext} disabled={isToday} title={T.nextDay}>
          <i className="ti ti-chevron-right"></i>
        </button>
      </div>
    );
  }

  // Oppgaver for dagen — egen side, kan hakes av på dagens side
  const tasksPage = detailShell(T.tasksLabel, (
    <div className="j2-rt-wrap">
      <div className="j2-det2-head">
        <span className="j2-det2-title">{T.tasksLabel}</span>
        <span className="j2-det2-count">{taskDone}/{taskCount}</span>
      </div>
      {taskItems.length === 0
        ? <p className="j2-det2-empty">{T.noTasksDay}</p>
        : (
          <div className="j2-rt-list">
            {taskItems.map(item => (
              <div key={item.key} className={`j2-rt-row${item.done ? ' done' : ''}`}>
                <button className={`j2-rt-check${item.done ? ' on' : ''}`} onClick={item.onToggle} disabled={!isToday}>
                  {item.done ? '✓' : ''}
                </button>
                <span className="j2-rt-name j2-rt-name-static">{item.name}</span>
              </div>
            ))}
          </div>
        )}
    </div>
  ), 'tasks-');

  // Rutiner for dagen — egen side. Klikk på en rutine åpner utvikling pr. mnd.
  const routinesPage = detailShell(T.routinesLabel, (
    <div className="j2-rt-wrap">
      <div className="j2-det2-head">
        <span className="j2-det2-title">{T.routinesLabel}</span>
        <span className="j2-det2-count">{routineDone}/{routineDefs.length}</span>
      </div>
      {routineDefs.length === 0
        ? <p className="j2-det2-empty">{T.noRoutinesDay}</p>
        : (
          <div className="j2-rt-list">
            {routineDefs.map(r => {
              const done = !!dayData.routineDone[r.id];
              return (
                <div key={r.id} className={`j2-rt-row${done ? ' done' : ''}`}>
                  <button className={`j2-rt-check${done ? ' on' : ''}`} onClick={() => toggleRoutine(r.id)}>
                    {done ? '✓' : ''}
                  </button>
                  <button
                    className="j2-rt-name"
                    onClick={() => openHistory({ id: r.id, name: r.name, type: 'checkbox', source: 'routine' }, 'routines')}
                  >
                    {r.name}
                    <i className="ti ti-chevron-right j2-rt-chev"></i>
                  </button>
                </div>
              );
            })}
          </div>
        )}
    </div>
  ), 'routines-');

  // «Fullført i dag» — mål- og prosjekt-oppgaver fullført denne dagen
  const completedPage = detailShell(T.completedTitle, (
    <div className="j2-det2">
      <div className="j2-det2-col">
        <div className="j2-det2-head">
          <span className="j2-det2-title">{T.goalsLabel}</span>
          <span className="j2-det2-count">{goalsDoneCount}</span>
        </div>
        {goalsDoneCount === 0
          ? <p className="j2-det2-empty">{T.noGoalsDone}</p>
          : (dayData.goalsDone || []).map(g => (
              <div key={g.id} className="j2-acc-item"><span className="j2-acc-tick">✓</span>{g.name}</div>
            ))}
      </div>
      <div className="j2-det2-col j2-det2-col-2">
        <div className="j2-det2-head">
          <span className="j2-det2-title">{T.projectsLabel}</span>
          <span className="j2-det2-count">{projDoneCount}</span>
        </div>
        {projDoneCount === 0
          ? <p className="j2-det2-empty">{T.noProjDone}</p>
          : (dayData.projDone || []).map(p => (
              <div key={p.id} className="j2-acc-item"><span className="j2-acc-tick">✓</span>{p.name}</div>
            ))}
      </div>
    </div>
  ), 'completed-');

  // ── Utviklingssiden: én tracker, per-dag-sirkler for én måned ──
  const histType = histTrk ? (histTrk.type || 'mood') : 'scale';
  const histReady = histData && histData.trackerId === (histTrk && histTrk.id);
  const histDaysArr = histReady ? histData.days : [];
  // Tre kolonner gjør at hele måneden får plass uten scroll på mobil
  const histThird = Math.ceil(histDaysArr.length / 3);
  const histCol1 = histDaysArr.slice(0, histThird);
  const histCol2 = histDaysArr.slice(histThird, histThird * 2);
  const histCol3 = histDaysArr.slice(histThird * 2);
  const histMonthLabel = histReady ? `${months[histData.mo]} ${histData.y}` : '';

  function histDayCircle(d) {
    const dt = jFromKey(d.key);
    const future = dt > today && !jSameDay(dt, today);
    const isSel = jSameDay(dt, selected);
    const has = histType === 'checkbox' ? d.val === true : (d.val != null && d.val !== '');
    const cd = j2CircleData(histType, d.val);
    // tre tilstander: registrert (fylt), forventet men tom (omriss), ikke forventet (grå dato)
    const state = has ? 'has' : future ? 'idle' : 'miss';
    return (
      <div key={d.key} className={`j2-hday s-${state}${isSel ? ' sel' : ''}`}>
        <span className={`j2-hday-ring${cd.emoji ? ' emo' : ''}`} style={{ '--p': cd.pct }}>
          <span>{state === 'idle' ? '' : (has ? cd.content : '')}</span>
        </span>
        <span className="j2-hday-date">{dayShort[dt.getDay()].toLowerCase()} {dt.getDate()}.</span>
      </div>
    );
  }

  const historyPage = (
    <div className="j2-page j2-page-right" key={'hist-' + (histTrk && histTrk.id) + histPage + flipDir} data-flip={flipDir}>
      <div className="j2-ribbon" aria-hidden="true" />
      {isNarrow ? (
        mobTop(
          <button className="j2-mobar-btn" onClick={closeHistory}>
            <i className="ti ti-arrow-left"></i> {T.tabPages}
          </button>
        )
      ) : null}
      <div className="j2-dateline">
        <span className="j2-dateline-text">{histTrk ? histTrk.name : ''} · {T.historyTitle} <span className="j2-dateline-orn">✦</span></span>
      </div>

      {/* «I dag»-blokk: juster/endre verdien for valgt dag */}
      {histTrk && (
        <div className="j2-hist-today">
          <div className="j2-hist-today-top">
            <span className="j2-hist-today-h">{isToday ? T.today : j2FmtDate(selected, lang)}</span>
            <span className="j2-hist-today-hint">{T.histTodayHint}</span>
          </div>
          <div className="j2-hist-today-ctl">
            {histTrk.id === J2_MOOD_ID ? (
              <div className="j2-mood-pick">
                {J_MOOD_EMOJIS.map((emoji, i) => (
                  <button
                    key={i}
                    className={`j2-mood-opt${entry.mood === i ? ' sel' : ''}`}
                    onClick={() => {
                      const nv = entry.mood === i ? null : i;
                      update({ mood: nv });
                      // Speil verdien umiddelbart i månedslisten under
                      setHistData(prev => (prev && prev.trackerId === J2_MOOD_ID)
                        ? { ...prev, days: prev.days.map(d => d.key === key ? { ...d, val: nv } : d) }
                        : prev);
                    }}
                    title={moodLabels[i]}
                  >{emoji}</button>
                ))}
              </div>
            ) : histTrk.source === 'routine' ? (
              <J2HistEditor
                type="checkbox"
                value={valOf(histTrk)}
                onSet={() => {
                  const nv = !valOf(histTrk);
                  toggleRoutine(histTrk.id);
                  // Speil verdien umiddelbart i månedslisten under
                  histCacheRef.current = {};
                  setHistData(prev => (prev && prev.trackerId === histTrk.id)
                    ? { ...prev, days: prev.days.map(d => d.key === key ? { ...d, val: nv ? true : null } : d) }
                    : prev);
                }}
              />
            ) : (
              <J2HistEditor type={histTrk.type || 'scale'} value={valOf(histTrk)} onSet={v => setTrackerVal(histTrk, v)} />
            )}
          </div>
        </div>
      )}

      <div className="j2-hist-rule" aria-hidden="true" />

      {/* Historikk for valgt måned */}
      <div className="j2-hist-monthhead">
        <span className="j2-hist-eyebrow">{T.histLabel}</span>
        <span className="j2-hist-month">{histMonthLabel}</span>
      </div>
      <div className="j2-hcols j2-hcols-3">
        <div className="j2-hcol">{histCol1.map(histDayCircle)}</div>
        <div className="j2-hcol">{histCol2.map(histDayCircle)}</div>
        <div className="j2-hcol">{histCol3.map(histDayCircle)}</div>
      </div>

      {/* «Tilbake» ligger nederst — likt med «Fullført i dag»-sidene. */}
      {!isNarrow && <button className="j2-more-btn j2-details-btn" onClick={closeHistory}>↩ {histReturn === 'routines' ? T.routinesLabel : T.backToWrite}</button>}

      <div className="j2-pageno">— {histMonthLabel} —</div>
      <button className="j2-corner j2-corner-left" onClick={() => histGo(1)} title={T.prevDay}>
        <i className="ti ti-chevron-left"></i>
      </button>
      <button className="j2-corner j2-corner-right" onClick={() => histGo(-1)} disabled={histPage === 0} title={T.nextDay}>
        <i className="ti ti-chevron-right"></i>
      </button>
    </div>
  );

  // ── Høyre side i Oversikt: side 2, 4, 6 … av registeret ──
  const overviewRightPage = (
    <div className="j2-page j2-page-right" key={'ov-' + regPage + flipDir} data-flip={flipDir}>
      <div className="j2-ribbon" aria-hidden="true" />
      <div className="j2-colophon">
        <span className="j2-colophon-line" />
        <span className="j2-colophon-text">❦</span>
        <span className="j2-colophon-line" />
      </div>
      <div className="j2-reg">
        {regRows(regPagesArr[regPage * 2 + 1] || [])}
      </div>
      <div className="j2-pageno">— {T.page} {regPage * 2 + 2} —</div>
      <button
        className="j2-corner j2-corner-right"
        onClick={() => regGo(1)}
        disabled={regPage >= regSpreads - 1}
        title={T.nextDay}
      >
        <i className="ti ti-chevron-right"></i>
      </button>
    </div>
  );

  // ── Mobil «logg»-oppslag: oversiktssirkler (som desktop-venstresiden) ──
  // Klikk på en stemnings-/tracker-sirkel → utvikling; oppgaver/rutiner → detaljer.
  const mobileOverviewPage = (
    <div className="j2-page j2-page-right" key={'movw-' + key + flipDir} data-flip={flipDir}>
      <div className="j2-ribbon" aria-hidden="true" />
      {mobTop(
        <button className="j2-mobar-btn" onClick={closeOverview}>
          <i className="ti ti-pencil"></i> {T.tabToday}
        </button>
      )}

      <div className="j2-flourish j2-flourish-sm">❦</div>
      <p className="j2-label">{T.trackersLabel}</p>
      <div className="j2-sum-grid">
        <J2SumTile name={T.moodShort} type="mood" value={entry.mood ?? null} onClick={() => openHistory(moodTrk)} />
        {trackers.map(t => (
          <J2SumTile
            key={t.id}
            name={t.name}
            type={t.type || 'scale'}
            value={dayData.vals[t.id]}
            onClick={() => openHistory(t)}
          />
        ))}
      </div>

      <div className="j2-flourish j2-flourish-sm">❦</div>
      <p className="j2-label">{T.completedLabel}</p>
      <div className="j2-sum-grid">
        <J2SumTile
          name={T.tasksLabel}
          fraction
          value={`${taskDone}/${taskCount}`}
          pct={taskCount ? (taskDone / taskCount) * 100 : 0}
          empty={!taskCount}
          onClick={openTasks}
        />
        <J2SumTile
          name={T.routinesLabel}
          fraction
          value={`${routineDone}/${routineDefs.length}`}
          pct={routineDefs.length ? (routineDone / routineDefs.length) * 100 : 0}
          empty={!routineDefs.length}
          onClick={openRoutines}
        />
        <J2SumTile
          name={T.goalsLabel}
          small
          value={String(goalsDoneCount)}
          pct={goalsDoneCount ? 100 : 0}
          empty={!goalsDoneCount}
          onClick={openCompleted}
        />
        <J2SumTile
          name={T.projectsLabel}
          small
          value={String(projDoneCount)}
          pct={projDoneCount ? 100 : 0}
          empty={!projDoneCount}
          onClick={openCompleted}
        />
      </div>

      <div className="j2-pageno">— {T.page} {j2DayOfYear(selected) + 1} —</div>
      {mobTocBar(
        <button className="j2-mobar-btn" onClick={openRegister}>
          <i className="ti ti-list-numbers"></i> {T.toc}
        </button>
      )}
    </div>
  );

  // ── Mobil innholdsfortegnelse (TOC): full liste over skrevne sider ──
  const mobileRegisterPage = (
    <div className="j2-page j2-page-right" key={'movreg-' + flipDir} data-flip={flipDir}>
      <div className="j2-ribbon" aria-hidden="true" />
      {mobTop(
        <button className="j2-mobar-btn" onClick={closeRegister}>
          <i className="ti ti-arrow-left"></i> {T.tabPages}
        </button>
      )}
      <div className="j2-colophon">
        <span className="j2-colophon-line" />
        <span className="j2-colophon-text">{T.toc}</span>
        <span className="j2-colophon-line" />
      </div>
      <div className="j2-reg">
        {regItems.length === 0 && <p className="j2-toc-empty">{T.registerEmpty}</p>}
        {regRows(regItems)}
      </div>
    </div>
  );

  const rightPage = isOverview && !isNarrow
    ? overviewRightPage
    : rightMode === 'history' ? historyPage
    : rightMode === 'tasks' ? tasksPage
    : rightMode === 'routines' ? routinesPage
    : rightMode === 'completed' ? completedPage
    : (isNarrow && rightMode === 'register') ? mobileRegisterPage
    : (isNarrow && rightMode === 'overview') ? mobileOverviewPage
    : writePage;

  // ── Lukket bok: omslag med PIN-lås og innstillinger ──
  const themeNames = { leather: T.themeLeather, lab: T.themeLab, green: T.themeGreen };
  // Felles klasse/inline-stil for bokfarge (tema eller egendefinert) + skrift.
  const bookFontClass = `j2-font-${font}`;
  const bookCustomStyle = theme === 'custom' ? j2CustomVars(customColor) : undefined;
  const usingCustomPreset = theme === 'custom' && J2_CUSTOM_PRESETS.includes(customColor);

  // Innstillingskort (bokfarge + skrift) — åpnes fra en diskré knapp nederst.
  const settingsCard = settingsOpen && (
    <div className="j2-settings-scrim" onClick={() => setSettingsOpen(false)}>
      <div className="j2-settings-card" onClick={e => e.stopPropagation()}>
        <div className="j2-settings-head">
          <span className="j2-settings-title">{T.settings}</span>
          <button className="j2-settings-close" onClick={() => setSettingsOpen(false)} aria-label={T.cancel}>
            <i className="ti ti-x"></i>
          </button>
        </div>

        <div className="j2-settings-sec">
          <span className="j2-settings-lbl">{T.bookColor}</span>
          <div className="j2-settings-colors">
            {[
              { key: 'leather', bg: 'linear-gradient(150deg,#4a3725,#241810)' },
              { key: 'lab',     bg: 'linear-gradient(150deg,#f0efe9,#c6c2b6)' },
              { key: 'green',   bg: 'linear-gradient(150deg,#3c644d,#1b3325)' },
            ].map(o => (
              <button key={o.key} type="button"
                className={`j2-set-sw${theme === o.key ? ' on' : ''}`}
                style={{ background: o.bg }} title={themeNames[o.key]}
                aria-label={themeNames[o.key]}
                onClick={() => pickTheme(o.key)} />
            ))}
            {J2_CUSTOM_PRESETS.map(c => (
              <button key={c} type="button"
                className={`j2-set-sw${theme === 'custom' && customColor === c ? ' on' : ''}`}
                style={{ background: c }} aria-label={c}
                onClick={() => pickCustomColor(c)} />
            ))}
            <label className={`j2-set-sw j2-set-sw-pick${theme === 'custom' && !usingCustomPreset ? ' on' : ''}`}
              title={T.customColor}
              style={theme === 'custom' && !usingCustomPreset ? { background: customColor } : undefined}>
              <i className="ti ti-color-picker"></i>
              <input type="color" value={customColor} onChange={e => pickCustomColor(e.target.value)} />
            </label>
          </div>
        </div>

        <div className="j2-settings-sec">
          <span className="j2-settings-lbl">{T.fontLabel}</span>
          <div className="j2-settings-fonts">
            {J2_FONT_OPTS.map(o => (
              <button key={o.key} type="button"
                className={`j2-set-font${font === o.key ? ' on' : ''}`}
                onClick={() => pickFont(o.key)}>
                <span className="j2-set-font-pre" style={{ fontFamily: o.ff }}>Aa</span>
                <span className="j2-set-font-l">{T['font' + o.key.charAt(0).toUpperCase() + o.key.slice(1)]}</span>
              </button>
            ))}
          </div>
        </div>
      </div>
    </div>
  );

  const closedBook = (
    <div className={`j2-closed j2-theme-${theme} ${bookFontClass}`} style={bookCustomStyle}>
      <div className="j2-ribbon" aria-hidden="true" />
      <span className="j2-closed-spinebar" aria-hidden="true" />
      <div className="j2-closed-inner">
        <div className="j2-closed-brand">{T.volume}</div>
        <div className="j2-closed-vol">{today.getFullYear()}</div>
        <div className="j2-closed-flourish">❦</div>

        {pinSettings.enabled ? (
          <div className="j2-cov-lock">
            <i className="ti ti-lock j2-cov-lockicon"></i>
            <J2PinInput
              value={pinInput}
              onChange={v => { setPinInput(v); setPinError(false); }}
              onEnter={openBook}
              error={pinError}
              placeholder={T.enterPin}
              autoFocus
            />
            {pinError && <div className="j2-pin-err">{T.wrongPin}</div>}
            <button className="j2-cov-btn" onClick={openBook}>
              <i className="ti ti-book"></i> {T.openBook}
            </button>
            <button className="j2-cov-link" onClick={removePin}>{T.removePin}</button>
          </div>
        ) : (
          <div className="j2-cov-lock">
            <button className="j2-cov-btn" onClick={openBook}>
              <i className="ti ti-book"></i> {T.openBook}
            </button>
            {pinMode === 'set' ? (
              <React.Fragment>
                <J2PinInput
                  value={pinInput}
                  onChange={v => { setPinInput(v); setPinError(false); }}
                  onEnter={savePin}
                  error={pinError}
                  placeholder={T.pinHint}
                />
                {pinError && <div className="j2-pin-err">{T.pinHint}</div>}
                <div className="j2-cov-row">
                  <button className="j2-cov-link" onClick={savePin}>{T.savePin}</button>
                  <button className="j2-cov-link" onClick={() => { setPinMode('idle'); setPinInput(''); setPinError(false); }}>{T.cancel}</button>
                </div>
              </React.Fragment>
            ) : (
              <button className="j2-cov-link" onClick={() => setPinMode('set')}>
                <i className="ti ti-lock"></i> {T.setPin}
              </button>
            )}
          </div>
        )}
      </div>
      <div className="j2-settings-row">
        <button className="j2-settings-btn" onClick={() => setSettingsOpen(true)}
          title={T.settings} aria-label={T.settings}>
          <i className="ti ti-adjustments-horizontal"></i>
        </button>
      </div>
    </div>
  );

  return (
    <div className="v2-page j2-stage">
      {phase === 'closed' ? closedBook : (
        <div className={`j2-book j2-theme-${theme} ${bookFontClass}${isNarrow ? ' j2-single' : ''}`} style={bookCustomStyle}
          onTouchStart={j2HandleTouchStart} onTouchMove={j2HandleTouchMove} onTouchEnd={j2HandleTouchEnd}>
          <div className={`j2-cover${phase === 'closing' ? ' j2-closing' : ''}${phase === 'opening' ? ' j2-opening' : ''}`}>
            {!isNarrow && bookTabs}
            {!isNarrow && leftPage}
            {!isNarrow && <div className="j2-spine" aria-hidden="true" />}
            {rightPage}
          </div>
        </div>
      )}
      {settingsCard}
    </div>
  );
}
