heartwood every commit a ring

Refresh theme, drop PWA, and harden response headers

9f1de9f2 by Isaac Bythewood · 23 days ago

Refresh theme, drop PWA, and harden response headers

- Replace synthwave palette with an earthy green/amber/terracotta theme
  and rework component and page styles to match.
- Remove service worker, workbox, and next-pwa leftovers; unregister any
  stale service workers on load and update README accordingly.
- Add security headers (CSP, X-Frame-Options, Referrer-Policy,
  Permissions-Policy) via next.config.js.
- Refactor entry, sidebar, log, and summary pages with expanded l10n
  strings and updated hotkey mappings.
- Drop unused uuid dependency and refresh favicon/logo assets.
modified README.md
@@ -16,9 +16,9 @@ https://timelite.bythewood.me/## Why?I want to casually track the time I spend on things without any overhead ofsigning into a service or even being online. Timelite is a progressive web appand works just fine without an internet connection, all data is stored locallyand I don't track anything.signing into a service or even being online. All data stays in your browser(IndexedDB via localForage), there is no backend, and nothing is tracked orsent anywhere.Timelite has been a pretty great companion for me with more aggressive teammanagement software solutions that sometimes don't allow me to swap between
@@ -84,3 +84,7 @@ that you can run:The `up` implies that you want to start the server again, `--build` will rebuildthe container and `-d`, as stated above, starts us in detached mode so you canset it and forget it.If you previously used an older version and now see stale service-worker /filesystem errors in the console, the app now unregisters any old serviceworkers on load. A hard refresh (Ctrl+Shift+R) will clear them after one visit.
modified bun.lock
@@ -16,7 +16,6 @@        "react-localization": "^2.0.6",        "react-toastify": "^11.0.5",        "react-transition-group": "^4.2.1",        "uuid": "^13.0.0",      },    },  },
@@ -167,8 +166,6 @@    "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],    "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],    "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],  }}
modified components/HotKeysMapping.js
@@ -36,6 +36,8 @@ const HotKeysMapping = (props) => {  const router = useRouter();  const onLogPage = () => router.pathname === "/log";  const handlers = {    RESET: () => dispatch({ type: "NEW_TIMER" }),    ADD_LOG: (event) => {
@@ -54,45 +56,39 @@ const HotKeysMapping = (props) => {    ABOUT_PAGE: () => router.push("/about"),    SUMMARY_PAGE: () => router.push("/summary"),    CLEAR_LOG: () => {      dispatch({ type: "CLEAR_LOG" });      if (state.log.length === 0) return;      contextStrings.setLanguage(state.language);      if (!window.confirm(contextStrings.confirmClearLog)) return;      dispatch({ type: "CLEAR_LOG" });      toast.error(contextStrings.resetLog);    },    LOG_NEXT: (event) => {      event.preventDefault();      if (        window.location.href.substr(window.location.href.length - 3) == "log"      ) {        dispatch({ type: "LOG_EDIT_TOGLE", edit: false });      if (onLogPage()) {        dispatch({ type: "TOGGLE_EDITION", edit: false });        dispatch({ type: "NEXT_LOG_ITEM" });      }    },    LOG_PREVIOUS: (event) => {      event.preventDefault();      if (        window.location.href.substr(window.location.href.length - 3) == "log"      ) {        dispatch({ type: "LOG_EDIT_TOGLE", edit: false });      if (onLogPage()) {        dispatch({ type: "TOGGLE_EDITION", edit: false });        dispatch({ type: "PREVIOUS_LOG_ITEM" });      }    },    LOG_EDIT: (event) => {      event.preventDefault();      if (!state.logSelectedEntry) return;      if (window.location.href.substr(window.location.href.length - 3) == "log")        dispatch({ type: "TOGGLE_EDITION", edit: true });      if (onLogPage()) dispatch({ type: "TOGGLE_EDITION", edit: true });    },    LOG_DELETE_SINGLE: (event) => {      event.preventDefault();      if (!state.logSelectedEntry) return;      if (        window.location.href.substr(window.location.href.length - 3) == "log"      ) {        dispatch({ type: "REMOVE_LOG" });        contextStrings.setLanguage(state.language);        toast.error(contextStrings.deletedEntry);      }      if (!onLogPage()) return;      contextStrings.setLanguage(state.language);      if (!window.confirm(contextStrings.confirmDeleteEntry)) return;      dispatch({ type: "REMOVE_LOG" });      toast.error(contextStrings.deletedEntry);    },  };
modified components/context.js
@@ -2,9 +2,11 @@ import React, { useEffect } from "react";import { useReducer, createContext } from "react";import PropTypes from "prop-types";import localForage from "localforage";import { v4 as uuid } from "uuid";import strings from "../l10n/context";const newId = () =>  typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"    ? crypto.randomUUID()    : `id-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;const initialState = {  note: "",
@@ -20,11 +22,8 @@ const Context = createContext();const reducer = (state, action) => {  let newState = {};  strings.setLanguage(state.language);  switch (action.type) {    case "LOCALDATA_READY":      strings.setLanguage(action.localdata.language);      return { ...action.localdata };    case "SET_LANGUAGE":      newState = {
@@ -53,16 +52,14 @@ const reducer = (state, action) => {        timer: new Date(),        log: [          {            id: uuid(),            id: newId(),            start: state.timer,            end: new Date(),            note: state.note,            tags: state.note              .split(" ")              .filter((word) => word.startsWith("#"))              .map((word) => {                return word.toLowerCase();              }),              .split(/\s+/)              .filter((word) => word.startsWith("#") && word.length > 1)              .map((word) => word.toLowerCase()),          },          ...state.log,        ],
@@ -162,7 +159,6 @@ const ContextProvider = ({ children }) => {  const value = { state, dispatch };  useEffect(() => {    // Only use localForage in browser environment to avoid SSR errors    if (typeof window !== "undefined") {      localForage        .getItem("context")
@@ -170,7 +166,6 @@ const ContextProvider = ({ children }) => {          if (value !== null)            dispatch({ type: "LOCALDATA_READY", localdata: value });        })        // Handle any remaining errors gracefully        .catch(() => {});    }  }, []);
modified components/entry.js
@@ -7,149 +7,213 @@ import { Context } from "../components/context";import styles from "../styles/components/entry.module.css";const extractTags = (text) =>  text    .split(/\s+/)    .filter((word) => word.startsWith("#") && word.length > 1)    .map((word) => word.toLowerCase());const toDatetimeLocal = (date) => {  const d = date instanceof Date ? date : new Date(date);  const pad = (n) => String(n).padStart(2, "0");  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;};const Entry = React.forwardRef(  ({ entry, removeEntry, isSelected, style }, forwardedRef) => {  const { state, dispatch } = useContext(Context);  const { register, handleSubmit } = useForm();  const focusedEntry = useRef(null);  const setRefs = useCallback(    (node) => {      focusedEntry.current = node;      if (typeof forwardedRef === "function") {        forwardedRef(node);      } else if (forwardedRef && "current" in forwardedRef) {        forwardedRef.current = node;      }    },    [forwardedRef]  );  const onSubmit = (data) => {    dispatch({      type: "EDIT_LOG",      entry: {        ...entry,        note: data.note,        tags: data.note          .split(" ")          .filter((word) => word.startsWith("#"))          .map((word) => {            return word.toLowerCase();          }),    const { state, dispatch } = useContext(Context);    const { register, handleSubmit, reset } = useForm({      defaultValues: {        note: entry.note,        start: toDatetimeLocal(entry.start),        end: toDatetimeLocal(entry.end),      },    });    dispatch({ type: "TOGGLE_EDITION", edit: false, submited: true });  };    const focusedEntry = useRef(null);    const isEditing = state.edit && isSelected == entry.id;    useEffect(() => {      if (isEditing) {        reset({          note: entry.note,          start: toDatetimeLocal(entry.start),          end: toDatetimeLocal(entry.end),        });      }    }, [isEditing, entry.id, entry.note, entry.start, entry.end, reset]);  useEffect(() => {    if (isSelected == entry.id && focusedEntry.current) {      focusedEntry.current.focus();      focusedEntry.current.scrollIntoView({ behavior: "smooth" });    const setRefs = useCallback(      (node) => {        focusedEntry.current = node;        if (typeof forwardedRef === "function") {          forwardedRef(node);        } else if (forwardedRef && "current" in forwardedRef) {          forwardedRef.current = node;        }      },      [forwardedRef]    );    const onSubmit = (data) => {      const start = new Date(data.start);      const end = new Date(data.end);      if (isNaN(start) || isNaN(end) || end < start) {        dispatch({ type: "TOGGLE_EDITION", edit: false });        return;      }      dispatch({        type: "EDIT_LOG",        entry: {          ...entry,          start,          end,          note: data.note,          tags: extractTags(data.note),        },      });      dispatch({ type: "TOGGLE_EDITION", edit: false });    };    useEffect(() => {      if (isSelected == entry.id && focusedEntry.current) {        focusedEntry.current.focus();        focusedEntry.current.scrollIntoView({ behavior: "smooth", block: "nearest" });      }    }, [isSelected, entry.id]);    const containerClasses = [styles.entryContainer];    if (isSelected == entry.id) containerClasses.push(styles.selected);    if (isEditing) {      containerClasses.push(styles.zoom);      containerClasses.push(styles.editing);    }  });  const highlight = isSelected == entry.id ? { filter: "invert(1)" } : {};  const combinedStyle = { ...(style || {}), ...highlight };  const containerClasses = [styles.entryContainer];  if (state.edit) containerClasses.push(styles.zoom);  return (    <div  style={combinedStyle}      className={containerClasses.join(" ")}      ref={setRefs}      tabIndex={-1}    >      {state.edit && isSelected == entry.id ? (        <form className={styles.entryForm} onSubmit={handleSubmit(onSubmit)}>          <div className={styles.entryTime}>            {timeString(entry.end - entry.start)}            <span>{entry.start.toLocaleTimeString()}</span>          </div>          <div className={styles.entryNote}>            <input              {...register("note")}              className={styles.entryNoteInput}              name="note"              autoFocus              value={state.log.find((x) => x.id == entry.id).note || ""}              onChange={(e) =>                dispatch({                  type: "EDIT_LOG",                  entry: {                    ...entry,                    note: e.target.value,                    tags: e.target.value                      .split(" ")                      .filter((word) => word.startsWith("#"))                      .map((word) => {                        return word.toLowerCase();                      }),                  },                })              }            />          </div>          <button            className={`${styles.entryButton} ${styles.entrySubmit}`}            type="submit"          >          </button>          <button            className={`${styles.entryButton} ${styles.entryRemove}`}            type="button"            onClick={() => dispatch({ type: "TOGGLE_EDITION", edit: false })}          >            x          </button>        </form>      ) : (        <>          <div className={styles.entryTime}>            {timeString(entry.end - entry.start)}            <span>{entry.start.toLocaleTimeString()}</span>          </div>          <div            className={`${styles.entryNote} ${entry.note.length === 0 ? styles.entryNoteEmpty : ""}`.trim()}          >            {entry.note}            {entry.tags.length > 0 && (              <small>                {entry.tags                  .map((tag) => {                    return tag;                  })                  .join(", ")}              </small>            )}          </div>          <button            className={`${styles.entryButton} ${styles.entryEdit}`}            onClick={() => {              dispatch({ type: "SELECT_LOG_ITEM", id: entry.id });              dispatch({ type: "TOGGLE_EDITION", edit: true });            }}          >            _          </button>          <button            className={`${styles.entryButton} ${styles.entryRemove}`}            onClick={() => {              dispatch({ type: "SELECT_LOG_ITEM", id: "" });              removeEntry(entry.id);            }}          >            x          </button>        </>      )}    </div>  );    return (      <div        style={style}        className={containerClasses.join(" ")}        ref={setRefs}        tabIndex={-1}      >        {isEditing ? (          <form className={styles.entryForm} onSubmit={handleSubmit(onSubmit)}>            <div className={`${styles.entryTime} ${styles.entryTimeEditing}`}>              <div className={styles.entryDuration}>                {timeString(entry.end - entry.start)}              </div>              <label className={styles.entryTimeRow}>                <span className={styles.entryTimeRowLabel}>From</span>                <input                  type="datetime-local"                  step="1"                  aria-label="Start time"                  className={styles.entryTimeInput}                  {...register("start")}                />              </label>              <label className={styles.entryTimeRow}>                <span className={styles.entryTimeRowLabel}>To</span>                <input                  type="datetime-local"                  step="1"                  aria-label="End time"                  className={styles.entryTimeInput}                  {...register("end")}                />              </label>            </div>            <div className={styles.entryNote}>              <input                {...register("note")}                className={styles.entryNoteInput}                autoFocus                aria-label="Note"                placeholder="Note with #tags"              />            </div>            <button              className={`${styles.entryButton} ${styles.entrySubmit}`}              type="submit"              aria-label="Save"              title="Save (Enter)"            >              <IconCheck />            </button>            <button              className={`${styles.entryButton} ${styles.entryRemove}`}              type="button"              aria-label="Cancel"              title="Cancel"              onClick={() => dispatch({ type: "TOGGLE_EDITION", edit: false })}            >              <IconX />            </button>          </form>        ) : (          <>            <div className={styles.entryTime}>              {timeString(entry.end - entry.start)}              <span>{entry.start.toLocaleTimeString()}</span>            </div>            <div              className={`${styles.entryNote} ${entry.note.length === 0 ? styles.entryNoteEmpty : ""}`.trim()}            >              {entry.note}              {entry.tags.length > 0 && (                <small>{entry.tags.join(", ")}</small>              )}            </div>            <button              className={`${styles.entryButton} ${styles.entryEdit}`}              aria-label="Edit"              onClick={() => {                dispatch({ type: "SELECT_LOG_ITEM", id: entry.id });                dispatch({ type: "TOGGLE_EDITION", edit: true });              }}            >              <IconPencil />            </button>            <button              className={`${styles.entryButton} ${styles.entryRemove}`}              aria-label="Delete"              onClick={() => {                dispatch({ type: "SELECT_LOG_ITEM", id: "" });                removeEntry(entry.id);              }}            >              <IconTrash />            </button>          </>        )}      </div>    );  });const IconPencil = () => (  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">    <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 0 0 0-1.41l-2.34-2.34a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>  </svg>);const IconTrash = () => (  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">    <path d="M6 19a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>  </svg>);const IconCheck = () => (  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">    <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z"/>  </svg>);const IconX = () => (  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">    <path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>  </svg>);Entry.propTypes = {  entry: PropTypes.object,  removeEntry: PropTypes.func,
modified components/sidebar.js
@@ -14,7 +14,23 @@ const Sidebar = () => {  return (    <aside className={styles.side}>      <div className={styles.title}>{strings.name}</div>      <Link href="/" className={styles.brand} aria-label={strings.name}>        <span className={styles.brandMark} aria-hidden="true">          <svg viewBox="0 0 24 24" width="22" height="22">            <circle              cx="12"              cy="12"              r="9.5"              fill="none"              stroke="currentColor"              strokeWidth="1.5"              opacity="0.9"            />            <circle cx="12" cy="12" r="4" fill="currentColor" />          </svg>        </span>        <span className={styles.brandText}>{strings.name}</span>      </Link>      <div className={styles.pages}>        <Link          href="/"
@@ -47,72 +63,67 @@ const Sidebar = () => {export default Sidebar;const HelpIcon = () => (  <svg    xmlns="http://www.w3.org/2000/svg"    height="22"    viewBox="0 0 24 24"    width="22"    fill="currentColor"    aria-hidden="true"  >    <path d="M0 0h24v24H0V0z" fill="none" />    <path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z" />  </svg>);const HelpIcon = () => {  return (    <svg      xmlns="http://www.w3.org/2000/svg"      height="24px"      viewBox="0 0 24 24"      width="24px"      fill="#FFFFFF"    >      <path d="M0 0h24v24H0V0z" fill="none" />      <path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z" />    </svg>  );};const TimerIcon = () => {  return (    <svg      xmlns="http://www.w3.org/2000/svg"      height="24px"      viewBox="0 0 24 24"      width="24px"      fill="#000000"    >      <path d="M0 0h24v24H0V0z" fill="none" />      <path d="M15.07 1.01h-6v2h6v-2zm-4 13h2v-6h-2v6zm8.03-6.62l1.42-1.42c-.43-.51-.9-.99-1.41-1.41l-1.42 1.42C16.14 4.74 14.19 4 12.07 4c-4.97 0-9 4.03-9 9s4.02 9 9 9 9-4.03 9-9c0-2.11-.74-4.06-1.97-5.61zm-7.03 12.62c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z" />    </svg>  );};const TimerIcon = () => (  <svg    xmlns="http://www.w3.org/2000/svg"    height="22"    viewBox="0 0 24 24"    width="22"    fill="currentColor"    aria-hidden="true"  >    <path d="M0 0h24v24H0V0z" fill="none" />    <path d="M15.07 1.01h-6v2h6v-2zm-4 13h2v-6h-2v6zm8.03-6.62l1.42-1.42c-.43-.51-.9-.99-1.41-1.41l-1.42 1.42C16.14 4.74 14.19 4 12.07 4c-4.97 0-9 4.03-9 9s4.02 9 9 9 9-4.03 9-9c0-2.11-.74-4.06-1.97-5.61zm-7.03 12.62c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z" />  </svg>);const LogIcon = () => {  return (    <svg      xmlns="http://www.w3.org/2000/svg"      height="24px"      viewBox="0 0 24 24"      width="24px"      fill="#000000"    >      <path d="M0 0h24v24H0V0z" fill="none" />      <path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z" />    </svg>  );};const LogIcon = () => (  <svg    xmlns="http://www.w3.org/2000/svg"    height="22"    viewBox="0 0 24 24"    width="22"    fill="currentColor"    aria-hidden="true"  >    <path d="M0 0h24v24H0V0z" fill="none" />    <path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z" />  </svg>);const SummaryIcon = () => {  return (    <svg      xmlns="http://www.w3.org/2000/svg"      enableBackground="new 0 0 24 24"      height="24px"      viewBox="0 0 24 24"      width="24px"      fill="#000000"    >const SummaryIcon = () => (  <svg    xmlns="http://www.w3.org/2000/svg"    enableBackground="new 0 0 24 24"    height="22"    viewBox="0 0 24 24"    width="22"    fill="currentColor"    aria-hidden="true"  >    <g>      <rect fill="none" height="24" width="24" />      <g>        <rect fill="none" height="24" width="24" />        <g>          <path d="M19,3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V5C21,3.9,20.1,3,19,3z M19,19H5V5h14V19z" />          <rect height="5" width="2" x="7" y="12" />          <rect height="10" width="2" x="15" y="7" />          <rect height="3" width="2" x="11" y="14" />          <rect height="2" width="2" x="11" y="10" />        </g>        <path d="M19,3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V5C21,3.9,20.1,3,19,3z M19,19H5V5h14V19z" />        <rect height="5" width="2" x="7" y="12" />        <rect height="10" width="2" x="15" y="7" />        <rect height="3" width="2" x="11" y="14" />        <rect height="2" width="2" x="11" y="10" />      </g>    </svg>  );};    </g>  </svg>);
modified l10n/about.js
@@ -8,10 +8,12 @@ const strings = new LocalizedStrings({    sectionTimer: "Timer",    sectionNavigation: "Navigation",    sectionLog: "Log",    sectionLanguage: "Language",    keyReset: "Reset timer",    keyAddLog: "Add log entry",    keyTimerPage: "Move to timer page",    keyLogPage: "Move to log page",    keySummaryPage: "Move to summary page",    keyAboutPage: "Move to about page",    keyNextLogEntry: "Move to next log entry",    keyPreviousLogEntry: "Move to previous log entry",
@@ -26,10 +28,12 @@ const strings = new LocalizedStrings({    sectionTimer: "タイマー",    sectionNavigation: "ナビゲーション",    sectionLog: "ログ",    sectionLanguage: "言語",    keyReset: "タイマーのリセット",    keyAddLog: "ログエントリを追加する",    keyTimerPage: "タイマーページに移動",    keyLogPage: "ログページに移動",    keySummaryPage: "サマリーページに移動",    keyAboutPage: "についてページに移動",    keyNextLogEntry: "次のログエントリに移動",    keyPreviousLogEntry: "前のログエントリに移動します",
@@ -44,10 +48,12 @@ const strings = new LocalizedStrings({    sectionTimer: "Zegar",    sectionNavigation: "Nawigacja",    sectionLog: "Dziennik",    sectionLanguage: "Język",    keyReset: "Zerowanie zegara",    keyAddLog: "Dodaj wpis do dziennika",    keyTimerPage: "Przejdź na stronę zegara",    keyLogPage: "Przejdź na stronę dziennika",    keySummaryPage: "Przejdź na stronę podsumowania",    keyAboutPage: "Przejdź na stronę o timelite",    keyNextLogEntry: "Przejdź do następnego wpisu",    keyPreviousLogEntry: "Przejdź do poprzedniego wpisu",
modified l10n/context.js
@@ -7,6 +7,9 @@ const strings = new LocalizedStrings({    editedEntry: "You've edited an entry.",    deletedEntry: "You've deleted an entry.",    resetLog: "You've reset your log.",    confirmClearLog: "Clear all log entries? This cannot be undone.",    confirmClearTag: "Delete all entries with this tag? This cannot be undone.",    confirmDeleteEntry: "Delete this entry?",  },  jp: {    loaded: "ローカルストレージから状態をロードしました。",
@@ -14,6 +17,9 @@ const strings = new LocalizedStrings({    editedEntry: "エントリを編集しました。",    deletedEntry: "エントリを削除しました。",    resetLog: "ログをリセットしました。",    confirmClearLog: "すべてのログエントリを削除しますか?元に戻せません。",    confirmClearTag: "このタグのすべてのエントリを削除しますか?元に戻せません。",    confirmDeleteEntry: "このエントリを削除しますか?",  },  pl: {    loaded: "Wczytano stan z lokalnego zapisu.",
@@ -21,6 +27,9 @@ const strings = new LocalizedStrings({    editedEntry: "Zmieniłeś wpis.",    deletedEntry: "Skasowałeś wpis.",    resetLog: "Wyczyściłeś dziennik.",    confirmClearLog: "Usunąć wszystkie wpisy dziennika? Tej operacji nie można cofnąć.",    confirmClearTag: "Usunąć wszystkie wpisy z tym tagiem? Tej operacji nie można cofnąć.",    confirmDeleteEntry: "Usunąć ten wpis?",  },});
modified l10n/log.js
@@ -2,34 +2,46 @@ import LocalizedStrings from "react-localization";const strings = new LocalizedStrings({  en: {    pageTitle: "Log",    total: "Total",    subtotal: "Subtotal",    start: "Start Time",    entries: "Entries",    nothing: "Your log is empty.",    emptyHint: "Add a tagged note from the timer page to see entries here.",    nothingFiltered: "No entries match this tag.",    tags: "Tags",    show: "Show All",    clear: "Clear",    export: "Export",    export: "Export CSV",  },  jp: {    pageTitle: "ログ",    total: "合計",    subtotal: "小計",    start: "開始",    entries: "エントリ数",    nothing: "ログは空です。",    emptyHint: "タイマーページからタグ付きメモを追加するとここに表示されます。",    nothingFiltered: "このタグに一致するエントリはありません。",    tags: "タグ",    show: "すべて",    clear: "すべてクリア",    export: "書き出す",    export: "CSVに書き出す",  },  pl: {    pageTitle: "Dziennik",    total: "Razem",    subtotal: "Podsuma",    start: "Czas początkowy",    entries: "Wpisy",    nothing: "Twój dziennik nie ma wpisów.",    emptyHint: "Dodaj notatkę z tagiem ze strony zegara, aby zobaczyć wpisy.",    nothingFiltered: "Brak wpisów z tym tagiem.",    tags: "Tagi",    show: "Pokaż wszystko",    clear: "Wyczyść",    export: "Wyeksportuj",    export: "Eksportuj CSV",  },});
modified l10n/summary.js
@@ -2,28 +2,61 @@ import LocalizedStrings from "react-localization";const strings = new LocalizedStrings({  en: {    title: "Summary",    totalHours: "Total number of hours spent across all tags.",    varHours: (n) => `${n} hours`,    tagHours: "The number of hours you've spent per-tag.",    numHours: "# of hours",    logEmpty: "Your log is empty.",    pageTitle: "Summary",    empty: "Your log is empty.",    emptyHint:      "Start the timer and add a tagged note to see your stats aggregate here.",    // Metric tile labels    statTotal: "Total Hours",    statEntries: "Entries",    statToday: "Today",    statWeek: "This Week",    statLongest: "Longest Session",    statTags: "Unique Tags",    // Section labels    sectionPerTag: "Hours per tag",    sectionPerDay: "Hours per day · last 14 days",    // Chart legend    numHours: "hours",    // Unit suffixes / fallback    hoursSuffix: "h",    minsSuffix: "m",    none: "—",  },  jp: {    title: "概要",    totalHours: "すべてのタグで費やされた合計時間数。",    varHours: (n) => `${n} 時間`,    tagHours: "タグごとに費やした時間数。",    numHours: "時間数",    logEmpty: "ログは空です。",    pageTitle: "概要",    empty: "ログは空です。",    emptyHint: "タイマーを開始してタグ付きメモを追加すると、統計がここに表示されます。",    statTotal: "合計時間",    statEntries: "エントリ数",    statToday: "今日",    statWeek: "今週",    statLongest: "最長セッション",    statTags: "タグ数",    sectionPerTag: "タグごとの時間",    sectionPerDay: "日ごとの時間 · 過去14日間",    numHours: "時間",    hoursSuffix: "時",    minsSuffix: "分",    none: "—",  },  pl: {    title: "Streszczenie",    totalHours: "Całkowita liczba godzin spędzonych na wszystkich tagach.",    varHours: (n) => `${n} godzin`,    tagHours: "Liczba godzin spędzonych na tagu.",    numHours: "Liczba godzin",    logEmpty: "Twój dziennik nie ma wpisów.",    pageTitle: "Streszczenie",    empty: "Twój dziennik nie ma wpisów.",    emptyHint:      "Uruchom zegar i dodaj notatkę z tagiem, aby zobaczyć tutaj statystyki.",    statTotal: "Razem godzin",    statEntries: "Wpisy",    statToday: "Dzisiaj",    statWeek: "Ten tydzień",    statLongest: "Najdłuższa sesja",    statTags: "Unikalne tagi",    sectionPerTag: "Godziny na tag",    sectionPerDay: "Godziny na dzień · ostatnie 14 dni",    numHours: "godzin",    hoursSuffix: "g",    minsSuffix: "m",    none: "—",  },});
modified next.config.js
@@ -1,7 +1,40 @@/** @type {import('next').NextConfig} */const securityHeaders = [  { key: "X-Content-Type-Options", value: "nosniff" },  { key: "X-Frame-Options", value: "SAMEORIGIN" },  { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },  {    key: "Permissions-Policy",    value: "camera=(), microphone=(), geolocation=(), interest-cohort=()",  },  {    key: "Content-Security-Policy",    value: [      "default-src 'self'",      "script-src 'self' 'unsafe-inline' 'unsafe-eval'",      "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",      "img-src 'self' data: blob:",      "font-src 'self' data: https://fonts.gstatic.com",      "connect-src 'self' blob:",      "manifest-src 'self'",      "base-uri 'self'",      "form-action 'self'",      "frame-ancestors 'self'",    ].join("; "),  },];const nextConfig = {  devIndicators: {    position: 'top-right',    position: "top-right",  },  async headers() {    return [      {        source: "/:path*",        headers: securityHeaders,      },    ];  },};
modified package.json
@@ -17,7 +17,6 @@    "react-is": "^19.2.4",    "react-localization": "^2.0.6",    "react-toastify": "^11.0.5",    "react-transition-group": "^4.2.1",    "uuid": "^13.0.0"    "react-transition-group": "^4.2.1"  }}
modified pages/_app.js
@@ -1,12 +1,11 @@import React from "react";import React, { useEffect } from "react";import { TransitionGroup, CSSTransition } from "react-transition-group";import { ToastContainer, toast } from "react-toastify";import { ToastContainer } from "react-toastify";import "react-toastify/dist/ReactToastify.css";import { ContextProvider } from "../components/context";import HotKeysMapping from "../components/HotKeysMapping";import L10n from "../components/l10n";import Sidebar from "../components/sidebar";import "../styles/globals.css";
@@ -14,11 +13,26 @@ import "../styles/globals.css";const MyApp = ({ Component, pageProps, router }) => {  const pageNodeRef = React.useMemo(() => React.createRef(), [router.route]);  // Clean up any stale service worker registered by the old Workbox sw.js  // that used to ship with this project. Without this, browsers that  // visited an earlier version keep a dead SW cached and log filesystem  // / precache errors on every load.  useEffect(() => {    if (typeof window === "undefined" || !("serviceWorker" in navigator))      return;    navigator.serviceWorker      .getRegistrations()      .then((regs) => regs.forEach((r) => r.unregister()))      .catch(() => {});    if (window.caches && caches.keys) {      caches.keys().then((keys) => keys.forEach((k) => caches.delete(k))).catch(() => {});    }  }, []);  return (    <ContextProvider>      <HotKeysMapping>  <ToastContainer position="top-right" />        <L10n />        <ToastContainer position="top-right" />        <TransitionGroup component={null}>          <CSSTransition            key={router.route}
modified pages/_document.js
@@ -10,8 +10,18 @@ class MyDocument extends Document {            name="description"            content="Why is it 5 AM? Isn't there something simple I can use to track what I'm doing with all this time?"          />          <meta name="theme-color" content="#0D0221" />          <meta name="theme-color" content="#0E0D0A" />          <link rel="manifest" href="/manifest.json" />          <link rel="preconnect" href="https://fonts.googleapis.com" />          <link            rel="preconnect"            href="https://fonts.gstatic.com"            crossOrigin="anonymous"          />          <link            rel="stylesheet"            href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap"          />        </Head>        <body>          <Main />
modified pages/about.js
@@ -1,6 +1,7 @@import React, { useContext } from "react";import Page from "../components/page";import L10n from "../components/l10n";import { Context } from "../components/context";import strings from "../l10n/about";
@@ -22,20 +23,21 @@ const About = () => {        <div className={styles.githubLogo}>          <svg            xmlns="http://www.w3.org/2000/svg"            width="50"            height="50"            fill="white"            className="bi bi-github"            width="32"            height="32"            fill="currentColor"            viewBox="0 0 16 16"          >            <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z" />          </svg>        </div>      </a>      <div className={styles.grid}>        <main className={styles.main}>          <h1 className={styles.heading}>{strings.title}</h1>          <blockquote className={styles.blockquote}>{strings.quote}</blockquote>      <div className="page-grid">        <main className="page-main">          <h1 className="page-title">{strings.title}</h1>          <blockquote className={styles.blockquote}>            {strings.quote}          </blockquote>          <a            className={styles.creator}            href="https://www.isaacbythewood.com/"
@@ -46,26 +48,29 @@ const About = () => {          </a>          <div className={styles.descriptionContainer}>            <div className={styles.keysColumn}>              <h2 className={styles.sectionTitle}>{strings.sectionTimer}</h2>              <h2 className="section-label">{strings.sectionTimer}</h2>              <div className={styles.keysDescription}>                <span>⎇+r</span> {strings.keyReset}              </div>              <div className={styles.keysDescription}>                <span>⎇+a</span> {strings.keyAddLog}              </div>              <h2 className={styles.sectionTitle}>{strings.sectionNavigation}</h2>              <h2 className="section-label">{strings.sectionNavigation}</h2>              <div className={styles.keysDescription}>                <span>⎇+t</span> {strings.keyTimerPage}              </div>              <div className={styles.keysDescription}>                <span>⎇+l</span> {strings.keyLogPage}              </div>              <div className={styles.keysDescription}>                <span>⎇+s</span> {strings.keySummaryPage}              </div>              <div className={styles.keysDescription}>                <span>⎇+o</span> {strings.keyAboutPage}              </div>            </div>            <div className={styles.keysColumn}>              <h2 className={styles.sectionTitle}>{strings.sectionLog}</h2>              <h2 className="section-label">{strings.sectionLog}</h2>              <div className={styles.keysDescription}>                <span>↓</span> {strings.keyNextLogEntry}              </div>
@@ -83,6 +88,11 @@ const About = () => {              </div>            </div>          </div>          <h2 className="section-label">{strings.sectionLanguage}</h2>          <div className={styles.languageRow}>            <L10n />          </div>        </main>      </div>    </Page>
modified pages/log.js
@@ -1,4 +1,4 @@import React, { useContext, useState, useRef } from "react";import React, { useContext, useState, useRef, useMemo } from "react";import { TransitionGroup, CSSTransition } from "react-transition-group";import { CSVLink } from "react-csv";import { toast } from "react-toastify";
@@ -12,6 +12,22 @@ import { timeString } from "../utils/time";import styles from "../styles/pages/log.module.css";const CSV_HEADERS = [  { label: "id", key: "id" },  { label: "start", key: "start" },  { label: "end", key: "end" },  { label: "duration_seconds", key: "duration_seconds" },  { label: "note", key: "note" },  { label: "tags", key: "tags" },];const sanitizeCell = (value) => {  if (value == null) return "";  const str = String(value);  if (/^[=+\-@\t\r]/.test(str)) return `'${str}`;  return str;};const Log = () => {  const { state, dispatch } = useContext(Context);  const [filter, setFilter] = useState({ type: "SHOW_ALL" });
@@ -48,59 +64,119 @@ const Log = () => {    }, 0);  };  const csvData = useMemo(    () =>      state.log.map((entry) => {        const start =          entry.start instanceof Date ? entry.start : new Date(entry.start);        const end = entry.end instanceof Date ? entry.end : new Date(entry.end);        return {          id: entry.id,          start: start.toISOString(),          end: end.toISOString(),          duration_seconds: Math.round((end - start) / 1000),          note: sanitizeCell(entry.note),          tags: sanitizeCell((entry.tags || []).join(" ")),        };      }),    [state.log]  );  const removeEntry = (id) => {    contextStrings.setLanguage(state.language);    if (!window.confirm(contextStrings.confirmDeleteEntry)) return;    if (getVisibleEntries(state.log, filter).length === 1) {      setFilter({ type: "SHOW_ALL" });    }    dispatch({ type: "REMOVE_LOG", id: id });    toast.error(contextStrings.deletedEntry);  };  const clearAll = () => {    contextStrings.setLanguage(state.language);    if (!window.confirm(contextStrings.confirmClearLog)) return;    dispatch({ type: "CLEAR_LOG" });    toast.error(contextStrings.resetLog);  };  const clearTag = () => {    contextStrings.setLanguage(state.language);    if (!window.confirm(contextStrings.confirmClearTag)) return;    dispatch({ type: "CLEAR_TAG", tag: filter.tag });    setFilter({ type: "SHOW_ALL" });    toast.error(contextStrings.deletedEntry);  };  const visibleEntries = getVisibleEntries(state.log, filter);  const isEmpty = state.log.length === 0;  if (isEmpty) {    return (      <Page title="Log">        <div className="page-grid">          <main className="page-main">            <h1 className="page-title">{strings.pageTitle}</h1>          </main>        </div>        <div className="empty-state">          <p className="empty-state-title">{strings.nothing}</p>          <p className="empty-state-hint">{strings.emptyHint}</p>        </div>      </Page>    );  }  const startTimeLabel = state.log[state.log.length - 1].start.toLocaleTimeString();  return (    <Page title="Log">      <div className={styles.grid}>        {state.log.length > 0 && (          <aside className={styles.details}>            <div className={styles.total}>              <span>{strings.start}</span>              {state.log[state.log.length - 1].start.toLocaleTimeString()}      <div className="page-grid">        <main className="page-main">          <h1 className="page-title">{strings.pageTitle}</h1>          <div className={styles.tiles}>            <div className={styles.tile}>              <span className={styles.tileLabel}>{strings.start}</span>              <span className={styles.tileValue}>{startTimeLabel}</span>            </div>            <div className={styles.total}>              <span>{strings.subtotal}</span>              {timeString(getVisibleTotalMilliseconds())}            <div className={styles.tile}>              <span className={styles.tileLabel}>{strings.subtotal}</span>              <span className={styles.tileValue}>                {timeString(getVisibleTotalMilliseconds())}              </span>            </div>            <div className={styles.total}>              <span>{strings.total}</span>              {timeString(getTotalMilliseconds())}            <div className={styles.tile}>              <span className={styles.tileLabel}>{strings.total}</span>              <span className={styles.tileValue}>                {timeString(getTotalMilliseconds())}              </span>            </div>            <div className={styles.tile}>              <span className={styles.tileLabel}>{strings.entries}</span>              <span className={styles.tileValue}>{state.log.length}</span>            </div>            <CSVLink              className={styles.csvButton}              data={state.log}              filename="timelite-export.csv"              className={styles.tileExport}              data={csvData}              headers={CSV_HEADERS}              filename={`timelite-export-${new Date()                .toISOString()                .slice(0, 10)}.csv`}            >              {strings.export}              <span className={styles.tileLabel}>{strings.export}</span>              <span className={styles.tileExportGlyph}>↓</span>            </CSVLink>          </aside>        )}        <main          className={`${styles.main} ${            getVisibleEntries(state.log, filter).length === 0              ? styles.mainEmpty              : ""          }`.trim()}          tabIndex="1"        >          </div>          {getTags(state.log).length > 0 && (            <div className={styles.topBar}>              <div className={styles.filters}>                <span>{strings.tags}</span>                <span className={styles.filtersLabel}>{strings.tags}</span>                {getTags(state.log).map((tag) => {                  const active = filter.type === "SHOW_TAG" && filter.tag === tag;                  return (                    <button                      className={styles.filterButton}                      className={`${styles.filterButton} ${active ? styles.filterButtonActive : ""}`.trim()}                      key={tag}                      onClick={() => setFilter({ type: "SHOW_TAG", tag: tag })}                    >
@@ -108,72 +184,60 @@ const Log = () => {                    </button>                  );                })}                <button                  className={styles.filterButton}                  onClick={() => setFilter({ type: "SHOW_ALL" })}                >                  {strings.show}                </button>                {filter.type === "SHOW_TAG" && (                  <button                    className={styles.filterButton}                    onClick={() => setFilter({ type: "SHOW_ALL" })}                  >                    {strings.show}                  </button>                )}              </div>              {filter.tag ? (                <button                  className={styles.reset}                  onClick={() => {                    dispatch({ type: "CLEAR_TAG", tag: filter.tag });                    setFilter({ type: "SHOW_ALL" });                    contextStrings.setLanguage(state.language);                    toast.error(contextStrings.deletedEntry);                  }}                >                <button className={styles.reset} onClick={clearTag}>                  {strings.clear} {filter.tag}                </button>              ) : (                <button                  className={styles.reset}                  onClick={() => {                    dispatch({ type: "CLEAR_LOG" });                    contextStrings.setLanguage(state.language);                    toast.error(contextStrings.resetLog);                  }}                >                <button className={styles.reset} onClick={clearAll}>                  {strings.clear}                </button>              )}            </div>          )}          {visibleEntries.length > 0 ? (            <>              <TransitionGroup component={null}>                {visibleEntries.map((entry, index) => {                  const timeout = (index + 1) * 250;                  const transitionDelay = index * 125;                  let nodeRef = nodeRefs.current.get(entry.id);                  if (!nodeRef) {                    nodeRef = React.createRef();                    nodeRefs.current.set(entry.id, nodeRef);                  }                  return (                    <CSSTransition                      key={entry.id}                      appear                      timeout={{ appear: timeout, enter: 250, exit: 250 }}                      classNames="fade"                      nodeRef={nodeRef}                    >                      <Entry                        ref={nodeRef}                        style={{ transitionDelay: `${transitionDelay}ms` }}                        entry={entry}                        removeEntry={removeEntry}                        isSelected={state.logSelectedEntry}                      />                    </CSSTransition>                  );                })}              </TransitionGroup>            </>            <TransitionGroup component={null}>              {visibleEntries.map((entry, index) => {                const timeout = (index + 1) * 250;                const transitionDelay = index * 125;                let nodeRef = nodeRefs.current.get(entry.id);                if (!nodeRef) {                  nodeRef = React.createRef();                  nodeRefs.current.set(entry.id, nodeRef);                }                return (                  <CSSTransition                    key={entry.id}                    appear                    timeout={{ appear: timeout, enter: 250, exit: 250 }}                    classNames="fade"                    nodeRef={nodeRef}                  >                    <Entry                      ref={nodeRef}                      style={{ transitionDelay: `${transitionDelay}ms` }}                      entry={entry}                      removeEntry={removeEntry}                      isSelected={state.logSelectedEntry}                    />                  </CSSTransition>                );              })}            </TransitionGroup>          ) : (            <div className={styles.nothing}>{strings.nothing}</div>            <div className={styles.nothingFiltered}>              {strings.nothingFiltered}            </div>          )}        </main>      </div>
modified pages/summary.js
@@ -1,4 +1,4 @@import React, { useEffect, useRef, useContext } from "react";import React, { useEffect, useRef, useContext, useMemo } from "react";import Chart from "chart.js/auto";import Page from "../components/page";
@@ -7,99 +7,291 @@ import strings from "../l10n/summary";import styles from "../styles/pages/summary.module.css";const PALETTE = [  "#6B9E78",  "#C9A84C",  "#C47055",  "#7EAAB8",  "#7DB88C",  "#DDC06A",];const MS = {  minute: 60 * 1000,  hour: 60 * 60 * 1000,  day: 24 * 60 * 60 * 1000,};const formatDuration = (ms, s) => {  if (!ms) return "0" + s.hoursSuffix;  const hours = ms / MS.hour;  if (hours >= 1) return `${hours.toFixed(hours >= 10 ? 0 : 1)}${s.hoursSuffix}`;  const mins = Math.round(ms / MS.minute);  return `${mins}${s.minsSuffix}`;};const formatHours = (ms) => Math.round((ms / MS.hour) * 100) / 100;const startOfDay = (d) => {  const x = new Date(d);  x.setHours(0, 0, 0, 0);  return x;};const Summary = () => {  const { state } = useContext(Context);  strings.setLanguage(state.language);  const canvasRef = useRef(null);  const getLabels = (entries) => {    let tags = [];    entries.map((entry) => tags.push(...entry.tags));    return [...new Set(tags)];  };  const getDatasets = (entries) => {    const labels = getLabels(entries);    let datasets = [];    labels.map((label) => {      const labeledEntries = [        ...entries.filter((entry) => entry.tags.includes(label)),      ];      let totalTime = 0;      labeledEntries.map((entry) => {        totalTime += entry.end - entry.start;      });      datasets.push(totalTime / 1000 / 60 / 60);    });    return datasets;  };  const getTotalTime = (entries) => {    let totalTime = 0;    entries.map((entry) => {      totalTime += entry.end - entry.start;  const tagCanvasRef = useRef(null);  const dayCanvasRef = useRef(null);  const log = state.log;  const hasData = log.length > 0;  // Derived stats ----------------------------------------------------------  const stats = useMemo(() => {    if (!hasData) return null;    const now = Date.now();    const today = startOfDay(now).getTime();    const weekAgo = today - 6 * MS.day;    let totalMs = 0;    let todayMs = 0;    let weekMs = 0;    let longestMs = 0;    const tagSet = new Set();    for (const e of log) {      const start = +new Date(e.start);      const end = +new Date(e.end);      const dur = end - start;      totalMs += dur;      if (dur > longestMs) longestMs = dur;      if (start >= today) todayMs += dur;      if (start >= weekAgo) weekMs += dur;      for (const t of e.tags || []) tagSet.add(t);    }    return {      totalMs,      entryCount: log.length,      todayMs,      weekMs,      longestMs,      tagCount: tagSet.size,    };  }, [log, hasData]);  // Hours per tag  const perTag = useMemo(() => {    if (!hasData) return { labels: [], data: [] };    const map = new Map();    for (const e of log) {      const dur = +new Date(e.end) - +new Date(e.start);      for (const t of e.tags || []) {        map.set(t, (map.get(t) || 0) + dur);      }    }    const entries = [...map.entries()].sort((a, b) => b[1] - a[1]);    return {      labels: entries.map(([t]) => t),      data: entries.map(([, ms]) => formatHours(ms)),    };  }, [log, hasData]);  // Hours per day over last 14 days  const perDay = useMemo(() => {    if (!hasData) return { labels: [], data: [] };    const days = 14;    const todayStart = startOfDay(Date.now()).getTime();    const buckets = new Array(days).fill(0);    const labels = new Array(days).fill(0).map((_, i) => {      const d = new Date(todayStart - (days - 1 - i) * MS.day);      return `${d.getMonth() + 1}/${d.getDate()}`;    });    totalTime = totalTime / 1000 / 60 / 60;    return Math.round(totalTime * 100) / 100;  };    for (const e of log) {      const start = +new Date(e.start);      const daysAgo = Math.floor((todayStart - startOfDay(start).getTime()) / MS.day);      if (daysAgo < 0 || daysAgo >= days) continue;      const dur = +new Date(e.end) - start;      buckets[days - 1 - daysAgo] += dur;    }    return { labels, data: buckets.map((ms) => formatHours(ms)) };  }, [log, hasData]);  // Chart: hours per tag ---------------------------------------------------  useEffect(() => {    if (state.log.length <= 0) return;    const chart = new Chart(canvasRef.current, {    if (!hasData || !tagCanvasRef.current) return;    const chart = new Chart(tagCanvasRef.current, {      type: "bar",      data: {        labels: getLabels(state.log),        labels: perTag.labels,        datasets: [          {            label: strings.numHours,            data: perTag.data,            backgroundColor: perTag.labels.map(              (_, i) => PALETTE[i % PALETTE.length] + "cc"            ),            borderColor: perTag.labels.map(              (_, i) => PALETTE[i % PALETTE.length]            ),            borderWidth: 1,            borderRadius: 2,          },        ],      },      options: {        responsive: true,        maintainAspectRatio: true,        plugins: {          legend: { display: false },          tooltip: {            backgroundColor: "#13120e",            borderColor: "rgba(107,158,120,0.3)",            borderWidth: 1,            titleFont: { family: "JetBrains Mono, monospace" },            bodyFont: { family: "JetBrains Mono, monospace" },          },        },        scales: {          x: {            ticks: {              color: "#a09890",              font: { family: "JetBrains Mono, monospace", size: 11 },            },            grid: { color: "rgba(221,215,205,0.04)" },          },          y: {            ticks: {              color: "#a09890",              font: { family: "JetBrains Mono, monospace", size: 11 },            },            grid: { color: "rgba(221,215,205,0.04)" },          },        },      },    });    return () => chart.destroy();  }, [perTag, hasData, state.language]);  // Chart: hours per day ---------------------------------------------------  useEffect(() => {    if (!hasData || !dayCanvasRef.current) return;    const chart = new Chart(dayCanvasRef.current, {      type: "line",      data: {        labels: perDay.labels,        datasets: [          {            label: strings.numHours,            data: getDatasets(state.log),            backgroundColor: [              "rgba(255, 99, 132, 0.5)",              "rgba(54, 162, 235, 0.7)",              "rgba(255, 206, 86, 0.7)",              "rgba(75, 192, 192, 0.7)",              "rgba(153, 102, 255, 0.7)",              "rgba(255, 159, 64, 0.7)",            ],            data: perDay.data,            fill: true,            tension: 0.35,            backgroundColor: "rgba(107,158,120,0.14)",            borderColor: "#6B9E78",            borderWidth: 1.5,            pointBackgroundColor: "#7DB88C",            pointBorderColor: "#0e0d0a",            pointRadius: 3,            pointHoverRadius: 5,          },        ],      },      options: {        responsive: true,        maintainAspectRatio: true,        plugins: {          legend: { display: false },          tooltip: {            backgroundColor: "#13120e",            borderColor: "rgba(107,158,120,0.3)",            borderWidth: 1,            titleFont: { family: "JetBrains Mono, monospace" },            bodyFont: { family: "JetBrains Mono, monospace" },          },        },        scales: {          x: {            ticks: {              color: "#a09890",              font: { family: "JetBrains Mono, monospace", size: 10 },            },            grid: { color: "rgba(221,215,205,0.04)" },          },          y: {            ticks: {              color: "#a09890",              font: { family: "JetBrains Mono, monospace", size: 10 },            },            grid: { color: "rgba(221,215,205,0.04)" },            beginAtZero: true,          },        },      },    });    return () => chart.destroy();  }, [perDay, hasData, state.language]);    return () => {      chart.destroy();    };  }, [state.language]);  if (!hasData) {    return (      <Page title="Summary">        <div className="page-grid">          <main className="page-main">            <h1 className="page-title">{strings.pageTitle}</h1>          </main>        </div>        <div className="empty-state">          <p className="empty-state-title">{strings.empty}</p>          <p className="empty-state-hint">{strings.emptyHint}</p>        </div>      </Page>    );  }  return (    <Page title="Summary">      <div className={styles.grid}>        <main          className={`${styles.main} ${            state.log.length <= 0 ? styles.mainEmpty : ""          }`.trim()}          tabIndex="1"        >          {state.log.length > 0 ? (            <>              <h1 className={styles.title}>{strings.title}</h1>              <p>{strings.totalHours}</p>              <div className={styles.totalTime}>                {strings.varHours(getTotalTime(state.log))}              </div>              <p>{strings.tagHours}</p>              <div className={styles.canvasWrapper}>                <canvas ref={canvasRef} />              </div>            </>          ) : (            <div className={styles.nothing}>{strings.logEmpty}</div>          )}      <div className="page-grid">        <main className="page-main">          <h1 className="page-title">{strings.pageTitle}</h1>          <div className={styles.tiles}>            <Tile label={strings.statTotal}>              {formatDuration(stats.totalMs, strings)}            </Tile>            <Tile label={strings.statToday}>              {formatDuration(stats.todayMs, strings)}            </Tile>            <Tile label={strings.statWeek}>              {formatDuration(stats.weekMs, strings)}            </Tile>            <Tile label={strings.statLongest}>              {formatDuration(stats.longestMs, strings)}            </Tile>            <Tile label={strings.statEntries}>{stats.entryCount}</Tile>            <Tile label={strings.statTags}>{stats.tagCount}</Tile>          </div>          <h2 className="section-label">{strings.sectionPerTag}</h2>          <div className={styles.chartCard}>            <canvas ref={tagCanvasRef} />          </div>          <h2 className="section-label">{strings.sectionPerDay}</h2>          <div className={styles.chartCard}>            <canvas ref={dayCanvasRef} />          </div>        </main>      </div>    </Page>  );};const Tile = ({ label, children }) => (  <div className={styles.tile}>    <span className={styles.tileLabel}>{label}</span>    <span className={styles.tileValue}>{children}</span>  </div>);export default Summary;
modified public/favicon.ico

binary file

modified public/manifest.json
@@ -1,7 +1,7 @@{  "name": "Timelite",  "short_name": "Timelite",  "background_color": "#0D0221",  "background_color": "#0E0D0A",  "display": "standalone",  "scope": "/",  "start_url": "/",
@@ -12,5 +12,5 @@      "sizes": "512x512"    }  ],  "theme_color": "#0D0221"  "theme_color": "#0E0D0A"}
modified public/static/logo.png

binary file

deleted public/sw.js
@@ -1 +0,0 @@if(!self.define){let e,s={};const n=(n,t)=>(n=new URL(n+".js",t).href,s[n]||new Promise((s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()})).then((()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e})));self.define=(t,a)=>{const c=e||("document"in self?document.currentScript.src:"")||location.href;if(s[c])return;let i={};const r=e=>n(e,c),o={module:{uri:c},exports:i,require:r};s[c]=Promise.all(t.map((e=>o[e]||r(e)))).then((e=>(a(...e),i)))}}define(["./workbox-1bb06f5e"],(function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/_G91BBERxYXwD1nJDaiiR/_buildManifest.js",revision:"91e5c64ad0fca6a30720c5df62c7480f"},{url:"/_next/static/_G91BBERxYXwD1nJDaiiR/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/3fff1979-e05f9502cbb9afd3.js",revision:"e05f9502cbb9afd3"},{url:"/_next/static/chunks/57-4ec64d5c3964051e.js",revision:"4ec64d5c3964051e"},{url:"/_next/static/chunks/798-a279a2d3464180db.js",revision:"a279a2d3464180db"},{url:"/_next/static/chunks/framework-02398e00071ab346.js",revision:"02398e00071ab346"},{url:"/_next/static/chunks/main-de624c79bb36ac66.js",revision:"de624c79bb36ac66"},{url:"/_next/static/chunks/pages/_app-b340974d47531072.js",revision:"b340974d47531072"},{url:"/_next/static/chunks/pages/_error-e4f561a102d9bb14.js",revision:"e4f561a102d9bb14"},{url:"/_next/static/chunks/pages/about-f05409bbb33b2c17.js",revision:"f05409bbb33b2c17"},{url:"/_next/static/chunks/pages/index-187063dd54b22a7c.js",revision:"187063dd54b22a7c"},{url:"/_next/static/chunks/pages/log-1e1c99e0b03c2cf5.js",revision:"1e1c99e0b03c2cf5"},{url:"/_next/static/chunks/pages/summary-aff8ab92d9317b65.js",revision:"aff8ab92d9317b65"},{url:"/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js",revision:"837c0df77fd5009c9e46d446188ecfd0"},{url:"/_next/static/chunks/webpack-ee7e63bc15b31913.js",revision:"ee7e63bc15b31913"},{url:"/_next/static/css/00e700f65670fdbd.css",revision:"00e700f65670fdbd"},{url:"/_next/static/css/0b64542f6d0b77e6.css",revision:"0b64542f6d0b77e6"},{url:"/_next/static/css/7c3dc3d95952a61f.css",revision:"7c3dc3d95952a61f"},{url:"/_next/static/css/a669697f4aac1aa0.css",revision:"a669697f4aac1aa0"},{url:"/_next/static/css/a812ef7d01f8018f.css",revision:"a812ef7d01f8018f"},{url:"/favicon.ico",revision:"09e229d772adc57c987768f35b3d4b6a"},{url:"/manifest.json",revision:"fd60d88ae5d9c197f890bc5610fd6249"},{url:"/robots.txt",revision:"1c1bcfe2c83986eeb8df4a6ecd4739fc"},{url:"/sitemap.xml",revision:"ee271b8714d0514d35ad607ec28c2616"},{url:"/static/logo.png",revision:"09e229d772adc57c987768f35b3d4b6a"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:n,state:t})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute((({url:e})=>{if(!(self.origin===e.origin))return!1;const s=e.pathname;return!s.startsWith("/api/auth/")&&!!s.startsWith("/api/")}),new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute((({url:e})=>{if(!(self.origin===e.origin))return!1;return!e.pathname.startsWith("/api/")}),new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute((({url:e})=>!(self.origin===e.origin)),new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET")}));
deleted public/workbox-1bb06f5e.js
@@ -1 +0,0 @@define(["exports"],(function(t){"use strict";try{self["workbox:core:6.6.0"]&&_()}catch(t){}const e=(t,...e)=>{let s=t;return e.length>0&&(s+=` :: ${JSON.stringify(e)}`),s};class s extends Error{constructor(t,s){super(e(t,s)),this.name=t,this.details=s}}try{self["workbox:routing:6.6.0"]&&_()}catch(t){}const n=t=>t&&"object"==typeof t?t:{handle:t};class r{constructor(t,e,s="GET"){this.handler=n(e),this.match=t,this.method=s}setCatchHandler(t){this.catchHandler=n(t)}}class i extends r{constructor(t,e,s){super((({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)}),e,s)}}class a{constructor(){this.t=new Map,this.i=new Map}get routes(){return this.t}addFetchListener(){self.addEventListener("fetch",(t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map((e=>{"string"==typeof e&&(e=[e]);const s=new Request(...e);return this.handleRequest({request:s,event:t})})));t.waitUntil(s),t.ports&&t.ports[0]&&s.then((()=>t.ports[0].postMessage(!0)))}}))}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:r,route:i}=this.findMatchingRoute({event:e,request:t,sameOrigin:n,url:s});let a=i&&i.handler;const o=t.method;if(!a&&this.i.has(o)&&(a=this.i.get(o)),!a)return;let c;try{c=a.handle({url:s,request:t,event:e,params:r})}catch(t){c=Promise.reject(t)}const h=i&&i.catchHandler;return c instanceof Promise&&(this.o||h)&&(c=c.catch((async n=>{if(h)try{return await h.handle({url:s,request:t,event:e,params:r})}catch(t){t instanceof Error&&(n=t)}if(this.o)return this.o.handle({url:s,request:t,event:e});throw n}))),c}findMatchingRoute({url:t,sameOrigin:e,request:s,event:n}){const r=this.t.get(s.method)||[];for(const i of r){let r;const a=i.match({url:t,sameOrigin:e,request:s,event:n});if(a)return r=a,(Array.isArray(r)&&0===r.length||a.constructor===Object&&0===Object.keys(a).length||"boolean"==typeof a)&&(r=void 0),{route:i,params:r}}return{}}setDefaultHandler(t,e="GET"){this.i.set(e,n(t))}setCatchHandler(t){this.o=n(t)}registerRoute(t){this.t.has(t.method)||this.t.set(t.method,[]),this.t.get(t.method).push(t)}unregisterRoute(t){if(!this.t.has(t.method))throw new s("unregister-route-but-not-found-with-method",{method:t.method});const e=this.t.get(t.method).indexOf(t);if(!(e>-1))throw new s("unregister-route-route-not-registered");this.t.get(t.method).splice(e,1)}}let o;const c=()=>(o||(o=new a,o.addFetchListener(),o.addCacheListener()),o);function h(t,e,n){let a;if("string"==typeof t){const s=new URL(t,location.href);a=new r((({url:t})=>t.href===s.href),e,n)}else if(t instanceof RegExp)a=new i(t,e,n);else if("function"==typeof t)a=new r(t,e,n);else{if(!(t instanceof r))throw new s("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});a=t}return c().registerRoute(a),a}try{self["workbox:strategies:6.6.0"]&&_()}catch(t){}const u={cacheWillUpdate:async({response:t})=>200===t.status||0===t.status?t:null},l={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},f=t=>[l.prefix,t,l.suffix].filter((t=>t&&t.length>0)).join("-"),w=t=>t||f(l.precache),d=t=>t||f(l.runtime);function p(t,e){const s=new URL(t);for(const t of e)s.searchParams.delete(t);return s.href}class y{constructor(){this.promise=new Promise(((t,e)=>{this.resolve=t,this.reject=e}))}}const g=new Set;function m(t){return"string"==typeof t?new Request(t):t}class v{constructor(t,e){this.h={},Object.assign(this,e),this.event=e.event,this.u=t,this.l=new y,this.p=[],this.m=[...t.plugins],this.v=new Map;for(const t of this.m)this.v.set(t,{});this.event.waitUntil(this.l.promise)}async fetch(t){const{event:e}=this;let n=m(t);if("navigate"===n.mode&&e instanceof FetchEvent&&e.preloadResponse){const t=await e.preloadResponse;if(t)return t}const r=this.hasCallback("fetchDidFail")?n.clone():null;try{for(const t of this.iterateCallbacks("requestWillFetch"))n=await t({request:n.clone(),event:e})}catch(t){if(t instanceof Error)throw new s("plugin-error-request-will-fetch",{thrownErrorMessage:t.message})}const i=n.clone();try{let t;t=await fetch(n,"navigate"===n.mode?void 0:this.u.fetchOptions);for(const s of this.iterateCallbacks("fetchDidSucceed"))t=await s({event:e,request:i,response:t});return t}catch(t){throw r&&await this.runCallbacks("fetchDidFail",{error:t,event:e,originalRequest:r.clone(),request:i.clone()}),t}}async fetchAndCachePut(t){const e=await this.fetch(t),s=e.clone();return this.waitUntil(this.cachePut(t,s)),e}async cacheMatch(t){const e=m(t);let s;const{cacheName:n,matchOptions:r}=this.u,i=await this.getCacheKey(e,"read"),a=Object.assign(Object.assign({},r),{cacheName:n});s=await caches.match(i,a);for(const t of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await t({cacheName:n,matchOptions:r,cachedResponse:s,request:i,event:this.event})||void 0;return s}async cachePut(t,e){const n=m(t);var r;await(r=0,new Promise((t=>setTimeout(t,r))));const i=await this.getCacheKey(n,"write");if(!e)throw new s("cache-put-with-no-response",{url:(a=i.url,new URL(String(a),location.href).href.replace(new RegExp(`^${location.origin}`),""))});var a;const o=await this.R(e);if(!o)return!1;const{cacheName:c,matchOptions:h}=this.u,u=await self.caches.open(c),l=this.hasCallback("cacheDidUpdate"),f=l?await async function(t,e,s,n){const r=p(e.url,s);if(e.url===r)return t.match(e,n);const i=Object.assign(Object.assign({},n),{ignoreSearch:!0}),a=await t.keys(e,i);for(const e of a)if(r===p(e.url,s))return t.match(e,n)}(u,i.clone(),["__WB_REVISION__"],h):null;try{await u.put(i,l?o.clone():o)}catch(t){if(t instanceof Error)throw"QuotaExceededError"===t.name&&await async function(){for(const t of g)await t()}(),t}for(const t of this.iterateCallbacks("cacheDidUpdate"))await t({cacheName:c,oldResponse:f,newResponse:o.clone(),request:i,event:this.event});return!0}async getCacheKey(t,e){const s=`${t.url} | ${e}`;if(!this.h[s]){let n=t;for(const t of this.iterateCallbacks("cacheKeyWillBeUsed"))n=m(await t({mode:e,request:n,event:this.event,params:this.params}));this.h[s]=n}return this.h[s]}hasCallback(t){for(const e of this.u.plugins)if(t in e)return!0;return!1}async runCallbacks(t,e){for(const s of this.iterateCallbacks(t))await s(e)}*iterateCallbacks(t){for(const e of this.u.plugins)if("function"==typeof e[t]){const s=this.v.get(e),n=n=>{const r=Object.assign(Object.assign({},n),{state:s});return e[t](r)};yield n}}waitUntil(t){return this.p.push(t),t}async doneWaiting(){let t;for(;t=this.p.shift();)await t}destroy(){this.l.resolve(null)}async R(t){let e=t,s=!1;for(const t of this.iterateCallbacks("cacheWillUpdate"))if(e=await t({request:this.request,response:e,event:this.event})||void 0,s=!0,!e)break;return s||e&&200!==e.status&&(e=void 0),e}}class R{constructor(t={}){this.cacheName=d(t.cacheName),this.plugins=t.plugins||[],this.fetchOptions=t.fetchOptions,this.matchOptions=t.matchOptions}handle(t){const[e]=this.handleAll(t);return e}handleAll(t){t instanceof FetchEvent&&(t={event:t,request:t.request});const e=t.event,s="string"==typeof t.request?new Request(t.request):t.request,n="params"in t?t.params:void 0,r=new v(this,{event:e,request:s,params:n}),i=this.q(r,s,e);return[i,this.D(i,r,s,e)]}async q(t,e,n){let r;await t.runCallbacks("handlerWillStart",{event:n,request:e});try{if(r=await this.U(e,t),!r||"error"===r.type)throw new s("no-response",{url:e.url})}catch(s){if(s instanceof Error)for(const i of t.iterateCallbacks("handlerDidError"))if(r=await i({error:s,event:n,request:e}),r)break;if(!r)throw s}for(const s of t.iterateCallbacks("handlerWillRespond"))r=await s({event:n,request:e,response:r});return r}async D(t,e,s,n){let r,i;try{r=await t}catch(i){}try{await e.runCallbacks("handlerDidRespond",{event:n,request:s,response:r}),await e.doneWaiting()}catch(t){t instanceof Error&&(i=t)}if(await e.runCallbacks("handlerDidComplete",{event:n,request:s,response:r,error:i}),e.destroy(),i)throw i}}function b(t){t.then((()=>{}))}function q(){return q=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var s=arguments[e];for(var n in s)({}).hasOwnProperty.call(s,n)&&(t[n]=s[n])}return t},q.apply(null,arguments)}let D,U;const x=new WeakMap,L=new WeakMap,I=new WeakMap,C=new WeakMap,E=new WeakMap;let N={get(t,e,s){if(t instanceof IDBTransaction){if("done"===e)return L.get(t);if("objectStoreNames"===e)return t.objectStoreNames||I.get(t);if("store"===e)return s.objectStoreNames[1]?void 0:s.objectStore(s.objectStoreNames[0])}return k(t[e])},set:(t,e,s)=>(t[e]=s,!0),has:(t,e)=>t instanceof IDBTransaction&&("done"===e||"store"===e)||e in t};function O(t){return t!==IDBDatabase.prototype.transaction||"objectStoreNames"in IDBTransaction.prototype?(U||(U=[IDBCursor.prototype.advance,IDBCursor.prototype.continue,IDBCursor.prototype.continuePrimaryKey])).includes(t)?function(...e){return t.apply(B(this),e),k(x.get(this))}:function(...e){return k(t.apply(B(this),e))}:function(e,...s){const n=t.call(B(this),e,...s);return I.set(n,e.sort?e.sort():[e]),k(n)}}function T(t){return"function"==typeof t?O(t):(t instanceof IDBTransaction&&function(t){if(L.has(t))return;const e=new Promise(((e,s)=>{const n=()=>{t.removeEventListener("complete",r),t.removeEventListener("error",i),t.removeEventListener("abort",i)},r=()=>{e(),n()},i=()=>{s(t.error||new DOMException("AbortError","AbortError")),n()};t.addEventListener("complete",r),t.addEventListener("error",i),t.addEventListener("abort",i)}));L.set(t,e)}(t),e=t,(D||(D=[IDBDatabase,IDBObjectStore,IDBIndex,IDBCursor,IDBTransaction])).some((t=>e instanceof t))?new Proxy(t,N):t);var e}function k(t){if(t instanceof IDBRequest)return function(t){const e=new Promise(((e,s)=>{const n=()=>{t.removeEventListener("success",r),t.removeEventListener("error",i)},r=()=>{e(k(t.result)),n()},i=()=>{s(t.error),n()};t.addEventListener("success",r),t.addEventListener("error",i)}));return e.then((e=>{e instanceof IDBCursor&&x.set(e,t)})).catch((()=>{})),E.set(e,t),e}(t);if(C.has(t))return C.get(t);const e=T(t);return e!==t&&(C.set(t,e),E.set(e,t)),e}const B=t=>E.get(t);const P=["get","getKey","getAll","getAllKeys","count"],M=["put","add","delete","clear"],W=new Map;function j(t,e){if(!(t instanceof IDBDatabase)||e in t||"string"!=typeof e)return;if(W.get(e))return W.get(e);const s=e.replace(/FromIndex$/,""),n=e!==s,r=M.includes(s);if(!(s in(n?IDBIndex:IDBObjectStore).prototype)||!r&&!P.includes(s))return;const i=async function(t,...e){const i=this.transaction(t,r?"readwrite":"readonly");let a=i.store;return n&&(a=a.index(e.shift())),(await Promise.all([a[s](...e),r&&i.done]))[0]};return W.set(e,i),i}N=(t=>q({},t,{get:(e,s,n)=>j(e,s)||t.get(e,s,n),has:(e,s)=>!!j(e,s)||t.has(e,s)}))(N);try{self["workbox:expiration:6.6.0"]&&_()}catch(t){}const S="cache-entries",K=t=>{const e=new URL(t,location.href);return e.hash="",e.href};class A{constructor(t){this._=null,this.L=t}I(t){const e=t.createObjectStore(S,{keyPath:"id"});e.createIndex("cacheName","cacheName",{unique:!1}),e.createIndex("timestamp","timestamp",{unique:!1})}C(t){this.I(t),this.L&&function(t,{blocked:e}={}){const s=indexedDB.deleteDatabase(t);e&&s.addEventListener("blocked",(t=>e(t.oldVersion,t))),k(s).then((()=>{}))}(this.L)}async setTimestamp(t,e){const s={url:t=K(t),timestamp:e,cacheName:this.L,id:this.N(t)},n=(await this.getDb()).transaction(S,"readwrite",{durability:"relaxed"});await n.store.put(s),await n.done}async getTimestamp(t){const e=await this.getDb(),s=await e.get(S,this.N(t));return null==s?void 0:s.timestamp}async expireEntries(t,e){const s=await this.getDb();let n=await s.transaction(S).store.index("timestamp").openCursor(null,"prev");const r=[];let i=0;for(;n;){const s=n.value;s.cacheName===this.L&&(t&&s.timestamp<t||e&&i>=e?r.push(n.value):i++),n=await n.continue()}const a=[];for(const t of r)await s.delete(S,t.id),a.push(t.url);return a}N(t){return this.L+"|"+K(t)}async getDb(){return this._||(this._=await function(t,e,{blocked:s,upgrade:n,blocking:r,terminated:i}={}){const a=indexedDB.open(t,e),o=k(a);return n&&a.addEventListener("upgradeneeded",(t=>{n(k(a.result),t.oldVersion,t.newVersion,k(a.transaction),t)})),s&&a.addEventListener("blocked",(t=>s(t.oldVersion,t.newVersion,t))),o.then((t=>{i&&t.addEventListener("close",(()=>i())),r&&t.addEventListener("versionchange",(t=>r(t.oldVersion,t.newVersion,t)))})).catch((()=>{})),o}("workbox-expiration",1,{upgrade:this.C.bind(this)})),this._}}class F{constructor(t,e={}){this.O=!1,this.T=!1,this.k=e.maxEntries,this.B=e.maxAgeSeconds,this.P=e.matchOptions,this.L=t,this.M=new A(t)}async expireEntries(){if(this.O)return void(this.T=!0);this.O=!0;const t=this.B?Date.now()-1e3*this.B:0,e=await this.M.expireEntries(t,this.k),s=await self.caches.open(this.L);for(const t of e)await s.delete(t,this.P);this.O=!1,this.T&&(this.T=!1,b(this.expireEntries()))}async updateTimestamp(t){await this.M.setTimestamp(t,Date.now())}async isURLExpired(t){if(this.B){const e=await this.M.getTimestamp(t),s=Date.now()-1e3*this.B;return void 0===e||e<s}return!1}async delete(){this.T=!1,await this.M.expireEntries(1/0)}}try{self["workbox:range-requests:6.6.0"]&&_()}catch(t){}async function H(t,e){try{if(206===e.status)return e;const n=t.headers.get("range");if(!n)throw new s("no-range-header");const r=function(t){const e=t.trim().toLowerCase();if(!e.startsWith("bytes="))throw new s("unit-must-be-bytes",{normalizedRangeHeader:e});if(e.includes(","))throw new s("single-range-only",{normalizedRangeHeader:e});const n=/(\d*)-(\d*)/.exec(e);if(!n||!n[1]&&!n[2])throw new s("invalid-range-values",{normalizedRangeHeader:e});return{start:""===n[1]?void 0:Number(n[1]),end:""===n[2]?void 0:Number(n[2])}}(n),i=await e.blob(),a=function(t,e,n){const r=t.size;if(n&&n>r||e&&e<0)throw new s("range-not-satisfiable",{size:r,end:n,start:e});let i,a;return void 0!==e&&void 0!==n?(i=e,a=n+1):void 0!==e&&void 0===n?(i=e,a=r):void 0!==n&&void 0===e&&(i=r-n,a=r),{start:i,end:a}}(i,r.start,r.end),o=i.slice(a.start,a.end),c=o.size,h=new Response(o,{status:206,statusText:"Partial Content",headers:e.headers});return h.headers.set("Content-Length",String(c)),h.headers.set("Content-Range",`bytes ${a.start}-${a.end-1}/${i.size}`),h}catch(t){return new Response("",{status:416,statusText:"Range Not Satisfiable"})}}function $(t,e){const s=e();return t.waitUntil(s),s}try{self["workbox:precaching:6.6.0"]&&_()}catch(t){}function z(t){if(!t)throw new s("add-to-cache-list-unexpected-type",{entry:t});if("string"==typeof t){const e=new URL(t,location.href);return{cacheKey:e.href,url:e.href}}const{revision:e,url:n}=t;if(!n)throw new s("add-to-cache-list-unexpected-type",{entry:t});if(!e){const t=new URL(n,location.href);return{cacheKey:t.href,url:t.href}}const r=new URL(n,location.href),i=new URL(n,location.href);return r.searchParams.set("__WB_REVISION__",e),{cacheKey:r.href,url:i.href}}class G{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:t,state:e})=>{e&&(e.originalRequest=t)},this.cachedResponseWillBeUsed=async({event:t,state:e,cachedResponse:s})=>{if("install"===t.type&&e&&e.originalRequest&&e.originalRequest instanceof Request){const t=e.originalRequest.url;s?this.notUpdatedURLs.push(t):this.updatedURLs.push(t)}return s}}}class V{constructor({precacheController:t}){this.cacheKeyWillBeUsed=async({request:t,params:e})=>{const s=(null==e?void 0:e.cacheKey)||this.W.getCacheKeyForURL(t.url);return s?new Request(s,{headers:t.headers}):t},this.W=t}}let J,Q;async function X(t,e){let n=null;if(t.url){n=new URL(t.url).origin}if(n!==self.location.origin)throw new s("cross-origin-copy-response",{origin:n});const r=t.clone(),i={headers:new Headers(r.headers),status:r.status,statusText:r.statusText},a=e?e(i):i,o=function(){if(void 0===J){const t=new Response("");if("body"in t)try{new Response(t.body),J=!0}catch(t){J=!1}J=!1}return J}()?r.body:await r.blob();return new Response(o,a)}class Y extends R{constructor(t={}){t.cacheName=w(t.cacheName),super(t),this.j=!1!==t.fallbackToNetwork,this.plugins.push(Y.copyRedirectedCacheableResponsesPlugin)}async U(t,e){const s=await e.cacheMatch(t);return s||(e.event&&"install"===e.event.type?await this.S(t,e):await this.K(t,e))}async K(t,e){let n;const r=e.params||{};if(!this.j)throw new s("missing-precache-entry",{cacheName:this.cacheName,url:t.url});{const s=r.integrity,i=t.integrity,a=!i||i===s;n=await e.fetch(new Request(t,{integrity:"no-cors"!==t.mode?i||s:void 0})),s&&a&&"no-cors"!==t.mode&&(this.A(),await e.cachePut(t,n.clone()))}return n}async S(t,e){this.A();const n=await e.fetch(t);if(!await e.cachePut(t,n.clone()))throw new s("bad-precaching-response",{url:t.url,status:n.status});return n}A(){let t=null,e=0;for(const[s,n]of this.plugins.entries())n!==Y.copyRedirectedCacheableResponsesPlugin&&(n===Y.defaultPrecacheCacheabilityPlugin&&(t=s),n.cacheWillUpdate&&e++);0===e?this.plugins.push(Y.defaultPrecacheCacheabilityPlugin):e>1&&null!==t&&this.plugins.splice(t,1)}}Y.defaultPrecacheCacheabilityPlugin={cacheWillUpdate:async({response:t})=>!t||t.status>=400?null:t},Y.copyRedirectedCacheableResponsesPlugin={cacheWillUpdate:async({response:t})=>t.redirected?await X(t):t};class Z{constructor({cacheName:t,plugins:e=[],fallbackToNetwork:s=!0}={}){this.F=new Map,this.H=new Map,this.$=new Map,this.u=new Y({cacheName:w(t),plugins:[...e,new V({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this.u}precache(t){this.addToCacheList(t),this.G||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this.G=!0)}addToCacheList(t){const e=[];for(const n of t){"string"==typeof n?e.push(n):n&&void 0===n.revision&&e.push(n.url);const{cacheKey:t,url:r}=z(n),i="string"!=typeof n&&n.revision?"reload":"default";if(this.F.has(r)&&this.F.get(r)!==t)throw new s("add-to-cache-list-conflicting-entries",{firstEntry:this.F.get(r),secondEntry:t});if("string"!=typeof n&&n.integrity){if(this.$.has(t)&&this.$.get(t)!==n.integrity)throw new s("add-to-cache-list-conflicting-integrities",{url:r});this.$.set(t,n.integrity)}if(this.F.set(r,t),this.H.set(r,i),e.length>0){const t=`Workbox is precaching URLs without revision info: ${e.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(t)}}}install(t){return $(t,(async()=>{const e=new G;this.strategy.plugins.push(e);for(const[e,s]of this.F){const n=this.$.get(s),r=this.H.get(e),i=new Request(e,{integrity:n,cache:r,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:s},request:i,event:t}))}const{updatedURLs:s,notUpdatedURLs:n}=e;return{updatedURLs:s,notUpdatedURLs:n}}))}activate(t){return $(t,(async()=>{const t=await self.caches.open(this.strategy.cacheName),e=await t.keys(),s=new Set(this.F.values()),n=[];for(const r of e)s.has(r.url)||(await t.delete(r),n.push(r.url));return{deletedURLs:n}}))}getURLsToCacheKeys(){return this.F}getCachedURLs(){return[...this.F.keys()]}getCacheKeyForURL(t){const e=new URL(t,location.href);return this.F.get(e.href)}getIntegrityForCacheKey(t){return this.$.get(t)}async matchPrecache(t){const e=t instanceof Request?t.url:t,s=this.getCacheKeyForURL(e);if(s){return(await self.caches.open(this.strategy.cacheName)).match(s)}}createHandlerBoundToURL(t){const e=this.getCacheKeyForURL(t);if(!e)throw new s("non-precached-url",{url:t});return s=>(s.request=new Request(t),s.params=Object.assign({cacheKey:e},s.params),this.strategy.handle(s))}}const tt=()=>(Q||(Q=new Z),Q);class et extends r{constructor(t,e){super((({request:s})=>{const n=t.getURLsToCacheKeys();for(const r of function*(t,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:s="index.html",cleanURLs:n=!0,urlManipulation:r}={}){const i=new URL(t,location.href);i.hash="",yield i.href;const a=function(t,e=[]){for(const s of[...t.searchParams.keys()])e.some((t=>t.test(s)))&&t.searchParams.delete(s);return t}(i,e);if(yield a.href,s&&a.pathname.endsWith("/")){const t=new URL(a.href);t.pathname+=s,yield t.href}if(n){const t=new URL(a.href);t.pathname+=".html",yield t.href}if(r){const t=r({url:i});for(const e of t)yield e.href}}(s.url,e)){const e=n.get(r);if(e){return{cacheKey:e,integrity:t.getIntegrityForCacheKey(e)}}}}),t.strategy)}}t.CacheFirst=class extends R{async U(t,e){let n,r=await e.cacheMatch(t);if(!r)try{r=await e.fetchAndCachePut(t)}catch(t){t instanceof Error&&(n=t)}if(!r)throw new s("no-response",{url:t.url,error:n});return r}},t.ExpirationPlugin=class{constructor(t={}){this.cachedResponseWillBeUsed=async({event:t,request:e,cacheName:s,cachedResponse:n})=>{if(!n)return null;const r=this.V(n),i=this.J(s);b(i.expireEntries());const a=i.updateTimestamp(e.url);if(t)try{t.waitUntil(a)}catch(t){}return r?n:null},this.cacheDidUpdate=async({cacheName:t,request:e})=>{const s=this.J(t);await s.updateTimestamp(e.url),await s.expireEntries()},this.X=t,this.B=t.maxAgeSeconds,this.Y=new Map,t.purgeOnQuotaError&&function(t){g.add(t)}((()=>this.deleteCacheAndMetadata()))}J(t){if(t===d())throw new s("expire-custom-caches-only");let e=this.Y.get(t);return e||(e=new F(t,this.X),this.Y.set(t,e)),e}V(t){if(!this.B)return!0;const e=this.Z(t);if(null===e)return!0;return e>=Date.now()-1e3*this.B}Z(t){if(!t.headers.has("date"))return null;const e=t.headers.get("date"),s=new Date(e).getTime();return isNaN(s)?null:s}async deleteCacheAndMetadata(){for(const[t,e]of this.Y)await self.caches.delete(t),await e.delete();this.Y=new Map}},t.NetworkFirst=class extends R{constructor(t={}){super(t),this.plugins.some((t=>"cacheWillUpdate"in t))||this.plugins.unshift(u),this.tt=t.networkTimeoutSeconds||0}async U(t,e){const n=[],r=[];let i;if(this.tt){const{id:s,promise:a}=this.et({request:t,logs:n,handler:e});i=s,r.push(a)}const a=this.st({timeoutId:i,request:t,logs:n,handler:e});r.push(a);const o=await e.waitUntil((async()=>await e.waitUntil(Promise.race(r))||await a)());if(!o)throw new s("no-response",{url:t.url});return o}et({request:t,logs:e,handler:s}){let n;return{promise:new Promise((e=>{n=setTimeout((async()=>{e(await s.cacheMatch(t))}),1e3*this.tt)})),id:n}}async st({timeoutId:t,request:e,logs:s,handler:n}){let r,i;try{i=await n.fetchAndCachePut(e)}catch(t){t instanceof Error&&(r=t)}return t&&clearTimeout(t),!r&&i||(i=await n.cacheMatch(e)),i}},t.RangeRequestsPlugin=class{constructor(){this.cachedResponseWillBeUsed=async({request:t,cachedResponse:e})=>e&&t.headers.has("range")?await H(t,e):e}},t.StaleWhileRevalidate=class extends R{constructor(t={}){super(t),this.plugins.some((t=>"cacheWillUpdate"in t))||this.plugins.unshift(u)}async U(t,e){const n=e.fetchAndCachePut(t).catch((()=>{}));e.waitUntil(n);let r,i=await e.cacheMatch(t);if(i);else try{i=await n}catch(t){t instanceof Error&&(r=t)}if(!i)throw new s("no-response",{url:t.url,error:r});return i}},t.cleanupOutdatedCaches=function(){self.addEventListener("activate",(t=>{const e=w();t.waitUntil((async(t,e="-precache-")=>{const s=(await self.caches.keys()).filter((s=>s.includes(e)&&s.includes(self.registration.scope)&&s!==t));return await Promise.all(s.map((t=>self.caches.delete(t)))),s})(e).then((t=>{})))}))},t.clientsClaim=function(){self.addEventListener("activate",(()=>self.clients.claim()))},t.precacheAndRoute=function(t,e){!function(t){tt().precache(t)}(t),function(t){const e=tt();h(new et(e,t))}(e)},t.registerRoute=h}));
modified site.config.js
@@ -1,10 +1,13 @@const theme = {  colors: {    one: "#0D0221",    two: "#261447",    three: "#D40078",    four: "#FF3864",    five: "#FF6C11",    bg: "#0E0D0A",    bgDeep: "#090806",    surface: "#13120E",    green: "#6B9E78",    greenBright: "#7DB88C",    amber: "#C9A84C",    terracotta: "#C47055",    text: "#DDD7CD",  },  breakpoint: "max-width: 1023.99px",};
modified styles/components/entry.module.css
@@ -1,17 +1,34 @@.entryContainer {  background-color: #ffffff;  color: #000000;  margin-bottom: 15px;  background-color: var(--surface);  color: var(--text);  margin-bottom: 10px;  display: grid;  grid-template-columns: 150px 1fr 50px 50px;  align-items: center;  border-bottom: 1px solid var(--color-four);  transition-duration: 250ms;  transition-property: transform;  grid-template-columns: 170px 1fr 44px 44px;  align-items: stretch;  border: 1px solid var(--border);  border-radius: var(--radius);  transition:    border-color 200ms ease,    box-shadow 200ms ease,    transform 180ms ease;  overflow: hidden;}.entryContainer.editing {  grid-template-columns: 300px 1fr 44px 44px;}.entryContainer:hover {  border-color: var(--border-green);}.selected {  border-color: var(--green);  box-shadow: 0 0 0 1px var(--green), 0 0 24px var(--green-dim);}.zoom {  transform: scale(1.05);  transform: scale(1.01);}@media (max-width: 1023.99px) {
@@ -28,43 +45,148 @@.entryNoteInput {  border: none;  padding: 12px 0;  font-size: 1em;  padding: 10px 0;  font-size: 0.95em;  width: 100%;  border-bottom: 2px solid var(--color-three);  border-bottom: 1px solid var(--border-green);  background: transparent;  color: inherit;  color: var(--text-bright);  outline: none;  letter-spacing: 0.02em;}.entryNoteInput:focus-visible {  border-bottom-color: var(--green);  box-shadow: 0 1px 0 0 var(--green);}.entryTime {  font-weight: bold;  font-size: 1.3em;  padding: 15px;  font-weight: 600;  font-size: 1.1em;  font-variant-numeric: tabular-nums;  letter-spacing: 0.02em;  padding: 12px 14px;  height: 100%;  background-color: var(--color-four);  color: #ffffff;  background-color: var(--bg-deep);  color: var(--green-bright);  text-align: center;  display: flex;  justify-content: center;  align-items: center;  box-sizing: border-box;  flex-direction: column;  gap: 4px;  border-right: 1px solid var(--border);  min-width: 0;}.entryTimeEditing {  align-items: stretch;  gap: 8px;  text-align: left;  padding: 12px;}.entryDuration {  font-size: 0.95em;  letter-spacing: 0.04em;  text-align: center;  color: var(--green-bright);  font-weight: 700;  padding-bottom: 6px;  border-bottom: 1px solid var(--border);}.entryTime span {  font-size: 0.7em;  opacity: 0.8;  font-weight: lighter;  opacity: 0.7;  font-weight: 400;  color: var(--text-muted);  text-transform: uppercase;  letter-spacing: 0.1em;}.entryTimeRow {  display: flex;  align-items: center;  gap: 6px;  width: 100%;}.entryTimeRowLabel {  font-size: 0.58em;  font-weight: 600;  text-transform: uppercase;  letter-spacing: 0.18em;  color: var(--text-muted);  min-width: 28px;  text-align: right;  flex-shrink: 0;}.entryTimeInput {  flex: 1 1 auto;  font-size: 0.72em;  font-family: var(--font-mono);  font-weight: 400;  padding: 4px 6px;  border: 1px solid var(--border-green);  background: var(--surface-2);  color: var(--text);  border-radius: 2px;  width: 100%;  min-width: 0;  box-sizing: border-box;  color-scheme: dark;}.entryTimeInput:focus-visible {  outline: none;  border-color: var(--green);  box-shadow: 0 0 0 2px var(--green-dim);}/* Trim and recolor the native calendar picker indicator so the seconds   digit never collides with it on Chromium. */.entryTimeInput::-webkit-calendar-picker-indicator {  filter: invert(0.55) sepia(0.2) saturate(3) hue-rotate(80deg);  padding: 0;  margin-left: 4px;  cursor: pointer;  opacity: 0.6;}.entryTimeInput::-webkit-calendar-picker-indicator:hover {  opacity: 1;}@media (max-width: 1023.99px) {  .entryContainer,  .entryContainer.editing {    grid-template-columns: 1fr 1fr;    grid-template-rows: auto auto auto;  }  .entryTime {    grid-column: 1 / span 2;    border-right: 0;    border-bottom: 1px solid var(--border);  }  .entryTimeEditing {    padding: 14px 16px;  }  .entryTimeRow {    gap: 10px;  }  .entryTimeRowLabel {    min-width: 32px;  }}.entryNote {  padding: 15px;  padding: 14px 18px;  align-self: center;  font-size: 0.92em;  letter-spacing: 0.01em;}.entryNoteEmpty {
@@ -73,8 +195,11 @@.entryNote small {  display: block;  color: gray;  margin-top: 5px;  color: var(--green-bright);  opacity: 0.7;  margin-top: 6px;  font-size: 0.72em;  letter-spacing: 0.08em;}@media (max-width: 1023.99px) {
@@ -84,28 +209,55 @@}.entryButton {  font-size: 1.3em;  font-weight: bolder;  color: #ffffff;  font-size: 1em;  cursor: pointer;  border: 0;  border-left: 1px solid var(--border);  margin: 0;  padding: 15px;  padding: 0;  height: 100%;  background: transparent;  color: var(--text-muted);  display: flex;  align-items: center;  justify-content: center;  transition:    background 180ms ease,    color 180ms ease;}.entryButton:hover {  background: var(--green-dim);  color: var(--green-bright);}.entryButton:focus-visible {  outline: none;  background: var(--green-dim);  box-shadow: inset 0 0 0 1px var(--green);}@media (max-width: 1023.99px) {  .entryButton {    padding: 5px;    padding: 10px;  }}.entrySubmit,.entryEdit {  background: var(--color-four);.entrySubmit {  color: var(--green-bright);}.entrySubmit:hover {  background: var(--green-dim);  color: var(--green-hi);}.entryEdit:hover {  color: var(--amber-bright);  background: var(--amber-dim);}.entryRemove {  background: var(--color-five);.entryRemove:hover {  color: var(--terracotta-bright);  background: var(--terracotta-dim);}
modified styles/components/l10n.module.css
@@ -1,19 +1,33 @@.select {  position: fixed;  bottom: 5px;  right: 5px;  z-index: 2;  background-color: var(--color-two);  color: #ffffff;  padding: 5px;  border: none;  background: var(--surface);  color: var(--text-bright);  padding: 8px 14px;  border: 1px solid var(--border-green);  border-radius: var(--radius);  font-family: var(--font-mono);  font-size: 0.85em;  letter-spacing: 0.06em;  cursor: pointer;  transition:    color 180ms ease,    border-color 180ms ease,    background 180ms ease;  min-width: 180px;}@media (max-width: 1023.99px) {  .select {    top: 5px;    right: 5px;    text-align: center;    bottom: auto;  }.select:hover {  color: var(--green-bright);  border-color: var(--border-green-hover);  background: var(--surface-2);}.select:focus-visible {  outline: none;  border-color: var(--green);  box-shadow: 0 0 0 3px var(--green-dim);}.select option {  background: var(--surface);  color: var(--text);}
modified styles/components/sidebar.module.css
@@ -3,57 +3,136 @@  left: 0;  top: 0;  bottom: 0;  width: 50px;  background-color: #ffffff;  width: 60px;  background-color: var(--bg-deep);  border-right: 1px solid var(--border);  display: flex;  justify-content: space-between;  flex-direction: column;  align-items: center;  padding: 14px 0;  z-index: 10;}@media (max-width: 1023.99px) {  .side {    width: 100%;    height: 50px;    height: 56px;    bottom: 0;    left: 0;    top: auto;    flex-direction: row;    align-items: center;    justify-content: space-between;    padding: 0 12px;    border-right: 0;    border-top: 1px solid var(--border);  }}.title {  color: var(--color-one);/* Brand mark + vertical wordmark */.brand {  display: flex;  flex-direction: column;  align-items: center;  gap: 16px;  text-decoration: none;  color: var(--text-muted);  transition: color 200ms ease;  padding-top: 4px;}.brand:hover {  color: var(--green-bright);}.brandMark {  display: flex;  align-items: center;  justify-content: center;  width: 32px;  height: 32px;  color: var(--green-bright);  filter: drop-shadow(0 0 6px rgba(107, 158, 120, 0.35));  transition:    filter 220ms ease,    transform 220ms ease;}.brandMark svg {  display: block;}.brandMark svg circle:last-child {  transform-origin: center;  animation: brandPulse 2.2s ease-in-out infinite;}.brand:hover .brandMark {  filter: drop-shadow(0 0 10px rgba(107, 158, 120, 0.6));  transform: scale(1.05);}@keyframes brandPulse {  0%,  100% {    opacity: 1;    transform: scale(1);  }  50% {    opacity: 0.55;    transform: scale(0.82);  }}.brandText {  font-size: 1.3em;  font-weight: 700;  text-transform: uppercase;  font-size: 1.7em;  font-weight: 900;  text-align: center;  padding: 15px 0;  white-space: nowrap;  letter-spacing: 0.14em;  color: var(--green-bright);  writing-mode: vertical-rl;  text-orientation: mixed;  line-height: 1.7em;  line-height: 1;  white-space: nowrap;  text-shadow: 0 0 10px var(--green-glow);}@media (max-width: 1023.99px) {  .title {    padding: 0 15px;    font-size: 1.4em;  .brand {    flex-direction: row;    gap: 10px;    padding-top: 0;  }  .brandMark {    width: 26px;    height: 26px;  }  .brandMark svg {    width: 18px;    height: 18px;  }  .brandText {    font-size: 0.8em;    writing-mode: horizontal-tb;    text-orientation: mixed;    letter-spacing: 0.2em;  }}.pages {  display: flex;  flex-direction: column;  padding: 15px;  gap: 4px;  align-items: center;}@media (max-width: 1023.99px) {  .pages {    flex-direction: row;    padding: 5px;    gap: 2px;  }}
@@ -61,59 +140,61 @@  text-decoration: none;  position: relative;  display: flex;  margin-bottom: 1rem;  align-items: center;  justify-content: center;  width: 40px;  height: 40px;  border-radius: var(--radius);  color: var(--text-muted);  transition:    color 300ms,    font-size 300ms,    transform 300ms;  font-size: 2em;  font-weight: 100;  color: rgba(0, 0, 0, 0.5);    color 200ms ease,    background 200ms ease;}.pageLink svg {  width: 20px;  height: 20px;  fill: currentColor;}.pageLink:hover {  transform: scale(1.5);  color: var(--green-bright);  background: var(--green-dim);}.pageLinkActive {  color: rgba(0, 0, 0, 1);  color: var(--green-bright);  background: var(--green-dim);  box-shadow: inset 2px 0 0 var(--green);}@media (max-width: 1023.99px) {  .pageLink {    font-size: 1.4em;    margin-bottom: 0;    margin-right: 15px;  }  .pageLink:hover {    transform: none;    font-size: 1.5em;  .pageLinkActive {    box-shadow: inset 0 2px 0 var(--green);  }}.about {  text-align: center;  padding: 10px 0;  display: block;  display: flex;  justify-content: center;  align-items: center;  width: 40px;  height: 40px;  border-radius: var(--radius);  text-decoration: none;  font-family: monospace;  background: var(--color-two);  color: #ffffff;  line-height: 0;  transition: transform 300ms;  color: var(--text-muted);  transition:    color 200ms ease,    background 200ms ease;}.about:hover {  transform: scale(1.5);.about svg {  width: 20px;  height: 20px;  fill: currentColor;}@media (max-width: 1023.99px) {  .about {    padding: 0 15px;    display: flex;    justify-content: center;    align-items: center;    height: 50px;  }.about:hover {  color: var(--amber);  background: var(--amber-dim);}
modified styles/components/timer.module.css
@@ -1,95 +1,149 @@.time {  font-size: 10em;  font-size: 8em;  text-align: center;  font-weight: lighter;  font-weight: 300;  font-variant-numeric: tabular-nums;  letter-spacing: 0.02em;  color: var(--green-bright);  text-shadow: 0 0 24px var(--green-glow);  margin-bottom: 1rem;  position: relative;}.time::before,.time::after {  content: "";  display: block;  width: 80px;  height: 1px;  margin: 0.5rem auto;  background: linear-gradient(90deg, transparent, var(--green) 50%, transparent);  opacity: 0.7;}@media (max-width: 1023.99px) {  .time {    font-size: 5em;    font-size: 3.8em;  }  .time::before,  .time::after {    width: 50px;  }}.inputs {  text-align: center;  margin-top: 1.5rem;}.note {  width: 600px;  width: 560px;  max-width: calc(100vw - 80px);  text-align: center;  margin-top: 40px;  border: 0;  background: rgba(255, 255, 255, 0.7);  color: #000000;  padding: 15px 30px;  font-size: 1.6em;  transform: scale(1);  border: 1px solid var(--border-green);  background: var(--surface);  color: var(--text);  padding: 14px 20px;  font-size: 0.95em;  letter-spacing: 0.04em;  border-radius: var(--radius);  transition:    transform 250ms,    background 250ms;    border-color 200ms ease,    box-shadow 200ms ease,    background 200ms ease;}.note:hover {  border-color: var(--border-green-hover);}.note:hover,.note:focus {  transform: scale(1.1);  background: rgba(255, 255, 255, 1);  z-index: 3;  position: relative;  outline: none;  border-color: var(--green);  background: var(--surface-2);  box-shadow: 0 0 0 3px var(--green-dim);}.note::placeholder {  font-size: 0.7em;  font-size: 0.85em;  text-transform: uppercase;  font-weight: 100;  font-weight: 500;  letter-spacing: 0.15em;  color: var(--text-dim);}@media (max-width: 1023.99px) {  .note {    padding: 10px 20px;    font-size: 1.2em;    width: 280px;    margin-top: 20px;    padding: 12px 16px;    font-size: 0.9em;    width: 300px;  }}.buttons {  text-align: center;  display: flex;  gap: 12px;  justify-content: center;  margin-top: 16px;  width: 560px;  max-width: calc(100vw - 80px);  margin-left: auto;  margin-right: auto;}.button {  color: #ffffff;  padding: 15px 30px;  font-size: 1em;  letter-spacing: 2px;  width: 100%;  flex: 1;  padding: 12px 20px;  font-size: 0.78em;  letter-spacing: 0.16em;  cursor: pointer;  text-transform: uppercase;  font-weight: 700;  border: none;  transform: scale(1);  transition: transform 250ms;  font-weight: 600;  background: transparent;  border-radius: var(--radius);  transition:    background 180ms ease,    color 180ms ease,    border-color 180ms ease,    box-shadow 180ms ease;}.button:hover,.button:focus {  transform: scale(1.1);  z-index: 3;  position: relative;.button:focus-visible {  outline: none;  box-shadow: 0 0 0 3px var(--green-dim);}@media (max-width: 1023.99px) {  .buttons {    width: 300px;  }  .button {    font-size: 0.9em;    padding: 15px 25px;    padding: 11px 16px;    font-size: 0.72em;    letter-spacing: 0.12em;  }}.resetButton {  background: var(--color-one);  color: var(--text-muted);  border: 1px solid var(--border);}.resetButton:hover {  color: var(--terracotta-bright);  border-color: var(--terracotta);  background: var(--terracotta-dim);}.addButton {  background: var(--color-two);  color: var(--green-bright);  border: 1px solid var(--green);  background: var(--green-dim);}.addButton:hover {  color: var(--green-hi);  background: rgba(107, 158, 120, 0.22);  border-color: var(--green-bright);  box-shadow: 0 0 12px var(--green-glow);}
modified styles/globals.css
@@ -1,10 +1,45 @@:root {  --color-one: #0d0221;  --color-two: #261447;  --color-three: #d40078;  --color-four: #ff3864;  --color-five: #ff6c11;  /* Warm-dark biohacker palette */  --bg: #0e0d0a;  --bg-deep: #090806;  --surface: #13120e;  --surface-2: #18160f;  --border: rgba(221, 215, 205, 0.06);  --border-green: rgba(107, 158, 120, 0.2);  --border-green-hover: rgba(107, 158, 120, 0.35);  --text: #ddd7cd;  --text-bright: #ede8e0;  --text-muted: #a09890;  --text-dim: #665f56;  --green: #6b9e78;  --green-bright: #7db88c;  --green-hi: #95cca2;  --green-dim: rgba(107, 158, 120, 0.12);  --green-glow: rgba(107, 158, 120, 0.3);  --amber: #c9a84c;  --amber-bright: #ddc06a;  --amber-dim: rgba(201, 168, 76, 0.1);  --terracotta: #c47055;  --terracotta-bright: #e38871;  --terracotta-dim: rgba(196, 112, 85, 0.1);  /* Legacy tokens kept so any missed reference still resolves to the new palette */  --color-one: var(--bg);  --color-two: var(--surface);  --color-three: var(--green);  --color-four: var(--green-bright);  --color-five: var(--amber);  --radius: 3px;  --radius-lg: 6px;  --breakpoint-desktop-max: 1023.99px;  --font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code", "SF Mono",    Menlo, Consolas, ui-monospace, monospace;}* {
@@ -18,43 +53,71 @@ body {}body {  font-family:    -apple-system,    BlinkMacSystemFont,    "Segoe UI",    Helvetica,    Arial,    sans-serif,    "Apple Color Emoji",    "Segoe UI Emoji",    "Segoe UI Symbol";  color: #ffffff;  background-color: var(--color-one);  font-family: var(--font-mono);  color: var(--text);  background-color: var(--bg);  min-height: 100vh;  width: 100%;  overflow-x: hidden;  text-shadow: rgba(0, 0, 0, 0.01) 0 0 1px;  font-size: 15px;  line-height: 1.6;  letter-spacing: 0.01em;  -webkit-font-smoothing: antialiased;  text-rendering: optimizeLegibility;  /* Subtle grid background for that operator-console feel */  background-image:    radial-gradient(      circle at 15% 10%,      rgba(107, 158, 120, 0.04) 0%,      transparent 45%    ),    radial-gradient(      circle at 85% 85%,      rgba(201, 168, 76, 0.025) 0%,      transparent 50%    ),    linear-gradient(rgba(107, 158, 120, 0.015) 1px, transparent 1px),    linear-gradient(90deg, rgba(107, 158, 120, 0.015) 1px, transparent 1px);  background-size: auto, auto, 40px 40px, 40px 40px;  background-position: 0 0, 0 0, -1px -1px, -1px -1px;  background-attachment: fixed;}input,button,select,textarea {  font-family:    -apple-system,    BlinkMacSystemFont,    "Segoe UI",    Helvetica,    Arial,    sans-serif,    "Apple Color Emoji",    "Segoe UI Emoji",    "Segoe UI Symbol";  font-family: var(--font-mono);  color: inherit;}a {  color: inherit;  color: var(--green-bright);  text-decoration: none;  transition: color 200ms ease;}a:hover {  color: var(--green-hi);}:focus {  outline: none;}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible {  outline: 1px solid var(--green);  outline-offset: 2px;  box-shadow: 0 0 0 3px var(--green-dim);}::selection {  background: var(--green-dim);  color: var(--green-hi);}.page-transition {
@@ -96,7 +159,7 @@ a {.fade-appear,.fade-enter {  opacity: 0;  transform: translateX(-100px);  transform: translateX(-40px);}.fade-appear-active,
@@ -114,8 +177,164 @@ a {.fade-exit-active {  opacity: 0;  transform: translateX(100px);  transform: translateX(40px);  transition-duration: 250ms;  transition-property: opacity, transform;  transition-delay: 0ms !important;}/* Toastify overrides so it matches the dark warm palette */.Toastify__toast {  font-family: var(--font-mono) !important;  font-size: 0.85rem !important;  border-radius: var(--radius) !important;  background: var(--surface) !important;  color: var(--text) !important;  border: 1px solid var(--border-green);  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.45);}.Toastify__toast--success {  border-left: 3px solid var(--green);}.Toastify__toast--error {  border-left: 3px solid var(--terracotta);}.Toastify__progress-bar--success {  background: var(--green) !important;}.Toastify__progress-bar--error {  background: var(--terracotta) !important;}/* ----- Shared page primitives (used by log, summary, about) ----- *//* Outer page grid: fixed sidebar + fluid main + right gutter.   All content pages use this for consistent horizontal alignment. */.page-grid {  display: grid;  grid-template-columns: 60px minmax(0, 1fr) 60px;  width: 100%;  min-height: 100vh;}@media (max-width: 1023.99px) {  .page-grid {    grid-template-columns: 0 minmax(0, 1fr) 0;  }}.page-main {  grid-column: 2;  width: 100%;  max-width: 1100px;  justify-self: start;  padding: 52px 40px 120px;  box-sizing: border-box;}@media (max-width: 1023.99px) {  .page-main {    padding: 32px 20px 120px;  }}/* Page title bar: uppercase green with glow + hairline underline.   Use on every content page's top to signal location. */.page-title {  font-weight: 700;  text-transform: uppercase;  font-size: 1.8em;  letter-spacing: 0.22em;  color: var(--green-bright);  text-shadow: 0 0 16px var(--green-glow);  margin: 0 0 1.5rem;  padding-bottom: 0.8rem;  border-bottom: 1px solid var(--border);}@media (max-width: 1023.99px) {  .page-title {    font-size: 1.3em;  }}/* Section label: small uppercase tracked label used above chart / section */.section-label {  font-size: 0.72em;  font-weight: 600;  text-transform: uppercase;  letter-spacing: 0.22em;  color: var(--text-muted);  margin: 2rem 0 0.75rem;}.section-label:first-child {  margin-top: 0;}/* Empty state: consistent centered message used when a page has no data.   Positioned to fill the viewport so it renders identically across pages. */.empty-state {  position: fixed;  top: 0;  left: 0;  right: 0;  bottom: 0;  display: flex;  flex-direction: column;  align-items: center;  justify-content: center;  gap: 16px;  text-align: center;  padding: 20px;  pointer-events: none;}.empty-state > * {  pointer-events: auto;}.empty-state-title {  font-weight: 500;  font-size: 1.15em;  letter-spacing: 0.18em;  text-transform: uppercase;  color: var(--text-muted);  margin: 0;  padding-bottom: 14px;  position: relative;}.empty-state-title::after {  content: "";  position: absolute;  width: 80px;  left: 50%;  transform: translateX(-50%);  bottom: 0;  height: 1px;  background: linear-gradient(    90deg,    transparent,    var(--green) 50%,    transparent  );}.empty-state-hint {  font-size: 0.78em;  color: var(--text-dim);  letter-spacing: 0.06em;  max-width: 320px;  line-height: 1.6;}@media (max-width: 1023.99px) {  .empty-state-title {    font-size: 0.9em;  }}
modified styles/pages/about.module.css
@@ -1,7 +1,7 @@@keyframes fadeRight {  from {    opacity: 0;    transform: translateX(-100px);    transform: translateX(-40px);  }  to {    opacity: 1;
@@ -9,203 +9,197 @@  }}@keyframes scaleRight {  from {    transform: scaleX(0);  }  to {    transform: scaleX(1);  }}@keyframes scaleLeft {  from {    transform: scaleX(1);  }  to {    transform: scaleX(0);  }}.githubLink {  position: absolute;  top: 24px;  right: 24px;  text-decoration: none;  z-index: 5;}.githubLogo {  position: absolute;  top: 20px;  right: 20px;  width: 50px;  height: 50px;  opacity: 0.5;  transition-property: opacity;  transition-duration: 250ms;  width: 32px;  height: 32px;  color: var(--text-muted);  opacity: 0.6;  transition:    opacity 200ms ease,    color 200ms ease,    transform 200ms ease;}.githubLogo:hover {.githubLink:hover .githubLogo {  opacity: 1;}@media (max-width: 1023.99px) {  .githubLogo {    font-size: 3em;    right: unset;    left: 20px;  }}.grid {  display: grid;  grid-template-columns: 20% 1fr 20%;  grid-template-rows: repeat(3, 1fr);  width: 100%;  height: 100vh;}@media (max-width: 1023.99px) {  .grid {    grid-template-columns: 5% 1fr 5%;  }}.main {  grid-area: 2 / 2;  display: flex;  justify-content: space-between;  flex-direction: column;}.heading {  font-size: 4em;  font-weight: lighter;  margin-top: 0;  opacity: 0;  animation: fadeRight 1000ms ease-out forwards;}@media (max-width: 1023.99px) {  .heading {    font-size: 3em;  }  color: var(--green-bright);  transform: rotate(6deg);}.blockquote {  font-size: 1.5em;  margin: 0 auto 40px auto;  font-size: 1.35em;  margin: 4rem auto 3.5rem auto;  padding: 0 2rem;  position: relative;  max-width: 500px;  max-width: 520px;  color: var(--text);  line-height: 1.6;  letter-spacing: 0.01em;  opacity: 0;  animation: fadeRight 1000ms ease-out 500ms forwards;  animation: fadeRight 600ms ease-out forwards;}.blockquote::before {  content: "”";  position: absolute;  top: 0;  top: -1.5rem;  right: 0;  font-size: 5em;  opacity: 0.1;  font-size: 6em;  opacity: 0.12;  line-height: 0.5em;  color: var(--green-bright);  pointer-events: none;}@media (max-width: 1023.99px) {  .blockquote {    font-size: 1.2em;    font-size: 1.05em;    margin: 2.5rem auto 2.5rem auto;    padding: 0 1rem;  }  .blockquote::before {    font-size: 4em;  }}.creator {  font-size: 1.5em;  display: block;  margin-left: auto;  opacity: 0.7;  color: #ffffff;  margin-right: 2rem;  margin-bottom: 4rem;  width: fit-content;  font-size: 1.25em;  color: var(--text-bright);  text-decoration: none;  transition: opacity 250ms;  transition: color 200ms ease;  position: relative;  opacity: 0;  animation: fadeRight 1000ms ease-out 1000ms forwards;  animation: fadeRight 600ms ease-out 300ms forwards;  padding-bottom: 6px;}.creator::before {.creator::after {  content: "";  position: absolute;  left: 0;  right: 0;  bottom: -10px;  bottom: 0;  height: 2px;  background: var(--color-three);  transform-origin: left;  animation: scaleLeft 300ms forwards;  background: var(--green);  opacity: 0.25;  pointer-events: none;}.creator::after {.creator::before {  content: "";  position: absolute;  left: 0;  right: 0;  bottom: -10px;  bottom: 0;  height: 2px;  background: var(--color-three);  opacity: 0.2;  background: var(--green);  transform-origin: left;  transform: scaleX(1);  animation: scaleLeftOut 400ms ease forwards;  pointer-events: none;}.creator:hover {  color: var(--green-bright);}.creator:hover::before {  animation: scaleRight 300ms forwards;  animation: scaleRightIn 300ms ease forwards;}@keyframes scaleLeftOut {  from {    transform: scaleX(1);    transform-origin: right;  }  to {    transform: scaleX(0);    transform-origin: right;  }}@keyframes scaleRightIn {  from {    transform: scaleX(0);    transform-origin: left;  }  to {    transform: scaleX(1);    transform-origin: left;  }}@media (max-width: 1023.99px) {  .creator {    font-size: 1.2em;    font-size: 1.05em;  }}.descriptionContainer {  display: flex;  margin-top: 2em;  display: grid;  grid-template-columns: 1fr 1fr;  gap: 3rem;  margin-top: 1rem;}@media (max-width: 1023.99px) {  .descriptionContainer {    display: none;    grid-template-columns: 1fr;    gap: 1.5rem;  }}.keysColumn {  flex: 1;}.sectionTitle {  font-size: 1.5em;  position: relative;  text-align: left;  max-width: 500px;  opacity: 0;  margin-bottom: 0;  animation: fadeRight 1000ms ease-out 1500ms forwards;  display: flex;  flex-direction: column;  gap: 6px;}.keysDescription {  font-size: 1em;  position: relative;  font-size: 0.88em;  color: var(--text);  display: flex;  align-items: center;  gap: 12px;  margin: 0;  padding: 4px 0;  opacity: 0;  animation: fadeRight 1000ms ease-out 2000ms forwards;  margin-bottom: 0;  animation: fadeRight 600ms ease-out 400ms forwards;}.keysDescription span {  width: 45px;  display: inline-block;  background: rgba(255, 255, 255, 0.3);  font-family: monospace;  min-width: 58px;  text-align: center;  font-size: 1.1em;  padding: 3px 8px;  background: var(--surface);  border: 1px solid var(--border-green);  border-radius: 2px;  font-family: var(--font-mono);  font-size: 0.85em;  color: var(--green-bright);  letter-spacing: 0.04em;}@media (max-width: 1023.99px) {  .keysDescription {    font-size: 1.2em;    font-size: 0.82em;  }}.languageRow {  margin-top: 0.5rem;  margin-bottom: 2rem;}
modified styles/pages/index.module.css
@@ -1,7 +1,7 @@@keyframes fadeUp {  from {    opacity: 0;    transform: translateY(100px);    transform: translateY(40px);  }  to {    opacity: 1;
@@ -10,39 +10,52 @@}.background {  position: absolute;  position: fixed;  z-index: -1;  top: 0;  right: 0;  bottom: 0;  left: 0;  background-image:    linear-gradient(#0d0221, transparent),    linear-gradient(to top left, #580215, transparent),    linear-gradient(to top right, #210d00, transparent);  background-blend-mode: screen;  background-size: cover;  background:    radial-gradient(      circle at 20% 15%,      rgba(107, 158, 120, 0.08) 0%,      transparent 55%    ),    radial-gradient(      circle at 80% 85%,      rgba(201, 168, 76, 0.05) 0%,      transparent 60%    ),    radial-gradient(      circle at 50% 50%,      rgba(196, 112, 85, 0.04) 0%,      transparent 70%    );  pointer-events: none;}.grid {  display: grid;  grid-template-columns: 20% 1fr 20%;  grid-template-rows: repeat(3, 1fr);  grid-template-columns: 60px 1fr 80px;  grid-template-rows: 1fr;  width: 100%;  height: 100vh;  min-height: 100vh;}@media (max-width: 1023.99px) {  .grid {    grid-template-columns: 5% 1fr 5%;    grid-template-columns: 0 1fr 0;  }}.main {  grid-area: 2 / 2;  grid-area: 1 / 2;  justify-content: center;  align-items: center;  display: flex;  flex-direction: column;  animation: fadeUp 500ms forwards;  min-height: 100vh;  padding: 48px 24px;  animation: fadeUp 500ms ease-out forwards;}
modified styles/pages/log.module.css
@@ -1,179 +1,171 @@.grid {.tiles {  display: grid;  grid-template-columns: 250px 1fr;  width: 100%;  min-height: 100vh;  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));  gap: 10px;  margin-bottom: 28px;}@media (max-width: 1023.99px) {  .grid {    grid-template-columns: 1fr;    grid-auto-rows: min-content;  }}.details {  position: fixed;.tile {  position: relative;  background: var(--surface);  border: 1px solid var(--border);  border-radius: var(--radius);  padding: 14px 16px;  display: flex;  flex-direction: column;  justify-content: center;  grid-column: 1;  width: 250px;  height: 100vh;  padding: 15px;  box-sizing: border-box;  background-color: #e2e2e2;  color: var(--color-one);  gap: 6px;  transition: border-color 180ms ease;}@media (max-width: 1023.99px) {  .details {    position: relative;    grid-column: 1;    height: auto;    margin-bottom: 50px;    width: 100%;  }.tile:hover {  border-color: var(--border-green);}.tileLabel {  font-size: 0.65em;  font-weight: 600;  text-transform: uppercase;  letter-spacing: 0.22em;  color: var(--text-muted);}.main {  grid-column: 2;  min-height: 100vh;  padding: 50px;  box-sizing: border-box;.tileValue {  font-size: 1.5em;  font-variant-numeric: tabular-nums;  font-weight: 700;  color: var(--text-bright);  letter-spacing: 0.01em;}.mainEmpty {  grid-column: 1 / span 2;.tileExport {  position: relative;  background: transparent;  border: 1px solid var(--border-green);  border-radius: var(--radius);  padding: 14px 16px;  display: flex;  align-items: center;  justify-content: center;  flex-direction: column;  align-items: flex-start;  gap: 6px;  color: var(--green-bright);  text-decoration: none;  transition:    background 180ms ease,    border-color 180ms ease,    color 180ms ease;}@media (max-width: 1023.99px) {  .main {    grid-column: 1;    padding-top: 0;    min-height: auto;    padding-bottom: 100px;  }.tileExport:hover {  background: var(--green-dim);  border-color: var(--green);  color: var(--green-hi);}  .mainEmpty {    grid-column: 1;    height: 100vh;  }.tileExportGlyph {  font-size: 1.5em;  font-weight: 700;  line-height: 1;  letter-spacing: 0;}.topBar {  display: flex;  justify-content: space-between;  margin-bottom: 40px;  align-items: center;  gap: 16px;  margin-bottom: 20px;  padding-bottom: 18px;  border-bottom: 1px solid var(--border);}@media (max-width: 1023.99px) {  .topBar {    flex-direction: column;    align-items: stretch;  }}.filters {  display: block;  display: flex;  flex-wrap: wrap;  align-items: center;  gap: 8px;}.filters span {  font-size: 0.8em;.filtersLabel {  font-size: 0.68em;  text-transform: uppercase;  font-weight: lighter;  display: block;  font-weight: 600;  letter-spacing: 0.22em;  color: var(--text-muted);  margin-right: 4px;}.filterButton {  background: var(--color-three);  border-bottom: 1px solid var(--color-four);  padding: 6px 9px;  color: #ffffff;  border: none;  margin-right: 10px;  margin-top: 10px;  background: rgba(221, 215, 205, 0.04);  color: var(--text-muted);  border: 1px solid rgba(221, 215, 205, 0.1);  padding: 6px 12px;  font-family: var(--font-mono);  font-size: 0.78em;  letter-spacing: 0.02em;  border-radius: 2px;  cursor: pointer;  transition:    background 180ms ease,    color 180ms ease,    border-color 180ms ease;}.reset {  font-size: 1.1em;  border: 0;  background-color: var(--color-five);  border-bottom: 1px solid var(--color-four);  padding: 10px 25px;  letter-spacing: 1px;  text-transform: uppercase;  color: #ffffff;  font-weight: bolder;  cursor: pointer;.filterButton:hover {  background: var(--green-dim);  color: var(--green-bright);  border-color: var(--border-green);}@media (max-width: 1023.99px) {  .reset {    font-size: 0.8em;    padding: 8px 15px;    margin-top: 15px;  }}.csvButton {  text-align: center;  padding: 5px 0;  display: block;  text-decoration: none;  font-family: monospace;  background: var(--color-one);  color: #ffffff;  margin: 5px;}@media (max-width: 1023.99px) {  .csvButton {    padding: 10px 0;  }.filterButtonActive {  background: var(--green-dim);  color: var(--green-bright);  border-color: var(--border-green);}.nothing {  font-weight: 100;  font-size: 2.5em;  text-align: center;  position: relative;.reset {  font-size: 0.72em;  letter-spacing: 0.14em;  text-transform: uppercase;  font-weight: 600;  color: var(--terracotta-bright);  background: var(--terracotta-dim);  border: 1px solid rgba(196, 112, 85, 0.22);  padding: 6px 14px;  border-radius: 2px;  cursor: pointer;  transition:    background 180ms ease,    color 180ms ease,    border-color 180ms ease;}.nothing::after {  content: "";  position: absolute;  width: 100%;  left: 0;  bottom: -15px;  height: 3px;  background-color: var(--color-three);.reset:hover {  background: rgba(196, 112, 85, 0.2);  color: #f09a85;  border-color: var(--terracotta);}@media (max-width: 1023.99px) {  .nothing {    font-size: 1.4em;  .reset {    align-self: flex-start;  }}.total {  font-weight: bolder;  font-size: 2em;  padding: 5px;  margin-bottom: 20px;}.total:last-child {  margin-bottom: 0;}.total span {  font-size: 0.4em;.nothingFiltered {  padding: 48px 16px;  text-align: center;  color: var(--text-muted);  font-size: 0.85em;  letter-spacing: 0.1em;  text-transform: uppercase;  font-weight: lighter;  display: block;  border: 1px dashed var(--border);  border-radius: var(--radius);  background: rgba(221, 215, 205, 0.02);}
modified styles/pages/summary.module.css
@@ -1,66 +1,51 @@.grid {.tiles {  display: grid;  grid-template-columns: 20% 1fr 20%;  width: 100%;  height: 100vh;  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));  gap: 10px;  margin-bottom: 32px;}@media (max-width: 1023.99px) {  .grid {    grid-template-columns: 5% 1fr 5%;  }}.main {  grid-area: 1 / 2;}.mainEmpty {  grid-area: 1 / 2;.tile {  position: relative;  background: var(--surface);  border: 1px solid var(--border);  border-radius: var(--radius);  padding: 14px 16px;  display: flex;  align-items: center;  justify-content: center;  flex-direction: column;  gap: 6px;  transition: border-color 180ms ease;}.title {  font-weight: 300;  text-transform: uppercase;  font-size: 6em;  margin-bottom: 2rem;}.canvasWrapper {  background-color: #ffffff;  padding: 0.25rem;  margin-bottom: 2rem;.tile:hover {  border-color: var(--border-green);}.totalTime {  font-size: 4em;  font-weight: 900;  margin-bottom: 3rem;.tileLabel {  font-size: 0.65em;  font-weight: 600;  text-transform: uppercase;  letter-spacing: 0.22em;  color: var(--text-muted);}.nothing {  font-weight: 100;  font-size: 2.5em;  text-align: center;  position: relative;.tileValue {  font-size: 1.6em;  font-variant-numeric: tabular-nums;  font-weight: 700;  color: var(--text-bright);  letter-spacing: 0.01em;}.nothing::after {  content: "";  position: absolute;  width: 100%;  left: 0;  bottom: -15px;  height: 3px;  background-color: var(--color-three);.chartCard {  background: var(--surface);  border: 1px solid var(--border);  border-radius: var(--radius);  padding: 16px;  margin-bottom: 20px;  transition: border-color 180ms ease;}@media (max-width: 1023.99px) {  .nothing {    font-size: 1.4em;  }.chartCard:hover {  border-color: var(--border-green);}