diff --git a/src/admin/components/PlanGrid.tsx b/src/admin/components/PlanGrid.tsx
index c7ba23a..fe55e4a 100644
--- a/src/admin/components/PlanGrid.tsx
+++ b/src/admin/components/PlanGrid.tsx
@@ -1,5 +1,7 @@
import { useMemo } from "react";
import type { CSSProperties } from "react";
+import { styled } from "@mui/material/styles";
+import Box from "@mui/material/Box";
import {
GridData,
ResolvedCell,
@@ -10,6 +12,356 @@ import {
import type { Project } from "../lib/queries/projects";
import PlanRangeChips from "./PlanRangeChips";
import { LoadingState } from "../ui";
+import { fonts } from "../theme";
+
+/**
+ * The planner surface, fully MUI/theme-driven (replaces the bespoke plan.css
+ * grid styling). All colors come from the theme palette via `theme.vars`, so it
+ * adapts to light/dark automatically; category colors come from the DB and are
+ * injected per cell as the `--cat-color` CSS variable. Class names from the JSX
+ * below are styled here as scoped nested selectors.
+ */
+const PlanGridRoot = styled(Box)(({ theme }) => ({
+ position: "relative",
+ overflow: "auto",
+ WebkitOverflowScrolling: "touch",
+ overscrollBehavior: "contain",
+ background: theme.vars!.palette.background.default,
+ border: `1px solid ${theme.vars!.palette.divider}`,
+ borderRadius: 14,
+ boxShadow: theme.shadows[2],
+ maxHeight: "calc(100dvh - 240px)",
+ isolation: "isolate",
+
+ "& .plan-grid": {
+ position: "relative",
+ zIndex: 1,
+ borderCollapse: "separate",
+ borderSpacing: 0,
+ width: "100%",
+ fontSize: "12.5px",
+ color: theme.vars!.palette.text.primary,
+ fontFamily: fonts.body,
+ },
+ "& col.plan-grid-date-col": { width: 88 },
+ "& tbody tr td + td": {
+ borderLeft: `1px solid ${theme.vars!.palette.divider}`,
+ },
+
+ // Sticky header
+ "& thead th": {
+ position: "sticky",
+ top: 0,
+ zIndex: 3,
+ padding: "14px 12px 12px",
+ background: theme.vars!.palette.background.paper,
+ color: theme.vars!.palette.text.primary,
+ borderBottom: `1px solid ${theme.vars!.palette.divider}`,
+ textAlign: "left",
+ fontWeight: 600,
+ whiteSpace: "nowrap",
+ },
+ "& thead th.plan-grid-date-col": {
+ left: 0,
+ zIndex: 4,
+ minWidth: 88,
+ padding: "12px 10px 10px 14px",
+ fontSize: "10.5px",
+ fontWeight: 700,
+ letterSpacing: "0.18em",
+ textTransform: "uppercase",
+ color: theme.vars!.palette.text.secondary,
+ },
+ "& .plan-person-head": {
+ display: "inline-flex",
+ alignItems: "center",
+ gap: 9,
+ minWidth: 0,
+ },
+ "& .plan-person-dot": {
+ flexShrink: 0,
+ width: 9,
+ height: 9,
+ borderRadius: "50%",
+ background: theme.vars!.palette.action.disabled,
+ },
+ "& .plan-person-name": {
+ display: "inline-flex",
+ flexDirection: "column",
+ minWidth: 0,
+ lineHeight: 1.1,
+ },
+ "& .plan-person-name strong": {
+ fontWeight: 600,
+ fontSize: 13,
+ color: theme.vars!.palette.text.primary,
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ maxWidth: 180,
+ },
+ "& .plan-person-name small": {
+ fontSize: 10,
+ fontWeight: 500,
+ color: theme.vars!.palette.text.disabled,
+ letterSpacing: "0.04em",
+ textTransform: "uppercase",
+ fontFamily: fonts.mono,
+ marginTop: 1,
+ },
+
+ // Sticky date column
+ "& tbody td.plan-grid-date-col": {
+ position: "sticky",
+ left: 0,
+ zIndex: 1,
+ background: theme.vars!.palette.background.paper,
+ fontWeight: 500,
+ color: theme.vars!.palette.text.secondary,
+ whiteSpace: "nowrap",
+ minWidth: 88,
+ padding: "9px 10px 9px 14px",
+ borderRight: `1px solid ${theme.vars!.palette.divider}`,
+ borderBottom: `1px solid ${theme.vars!.palette.divider}`,
+ },
+ "& .plan-date-stamp": {
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "flex-start",
+ lineHeight: 1.1,
+ gap: 1,
+ },
+ "& .plan-date-daynum": {
+ fontFamily: fonts.mono,
+ fontSize: 17,
+ fontWeight: 600,
+ color: theme.vars!.palette.text.primary,
+ fontVariantNumeric: "tabular-nums",
+ lineHeight: 1.05,
+ },
+ "& .plan-date-dow": {
+ fontSize: 10,
+ fontWeight: 600,
+ letterSpacing: "0.14em",
+ textTransform: "uppercase",
+ color: theme.vars!.palette.text.disabled,
+ lineHeight: 1.3,
+ },
+
+ // Today
+ "& tbody tr.is-today td.plan-grid-date-col": {
+ background: theme.vars!.palette.background.paper,
+ "&::before": {
+ content: '""',
+ position: "absolute",
+ left: 0,
+ top: 6,
+ bottom: 6,
+ width: 3,
+ borderRadius: "0 2px 2px 0",
+ background: theme.vars!.palette.primary.main,
+ boxShadow: `0 0 14px rgba(${theme.vars!.palette.primary.mainChannel} / 0.3)`,
+ },
+ "& .plan-date-daynum": { color: theme.vars!.palette.primary.main },
+ },
+ "& tbody tr.is-today td:not(.plan-grid-date-col)": {
+ background: `rgba(${theme.vars!.palette.primary.mainChannel} / 0.06)`,
+ },
+
+ // Body cells
+ "& tbody td": {
+ padding: 0,
+ borderBottom: `1px solid ${theme.vars!.palette.divider}`,
+ verticalAlign: "stretch",
+ minWidth: 168,
+ background: theme.vars!.palette.background.paper,
+ },
+
+ // Weekend
+ "& tr.plan-grid-weekend td": {
+ background: `rgba(${theme.vars!.palette.text.primaryChannel} / 0.03)`,
+ },
+ "& tr.plan-grid-weekend td.plan-grid-date-col": {
+ background: theme.vars!.palette.background.paper,
+ color: theme.vars!.palette.text.disabled,
+ "& .plan-date-dow": { color: theme.vars!.palette.warning.main },
+ "& .plan-date-daynum": { color: theme.vars!.palette.text.secondary },
+ },
+
+ // The cell button + marker tape
+ "& .plan-cell": {
+ position: "relative",
+ display: "block",
+ width: "100%",
+ height: "100%",
+ minHeight: 60,
+ textAlign: "left",
+ background: "transparent",
+ border: 0,
+ borderRadius: 0,
+ padding: "9px 11px 10px 16px",
+ cursor: "pointer",
+ font: "inherit",
+ color: "inherit",
+ transition: "background 150ms ease, transform 150ms ease",
+ "&::before": {
+ content: '""',
+ position: "absolute",
+ left: 4,
+ top: 8,
+ bottom: 8,
+ width: 3,
+ borderRadius: 2,
+ background: `var(--cat-color, ${theme.vars!.palette.divider})`,
+ transition:
+ "background 150ms ease, box-shadow 150ms ease, transform 150ms ease",
+ },
+ "&:hover": { background: theme.vars!.palette.action.hover },
+ "&:hover::before": {
+ transform: "scaleX(1.4)",
+ boxShadow: "0 0 12px currentColor",
+ },
+ "&:active": { transform: "translateY(0.5px)" },
+ "&:focus-visible": {
+ outline: "none",
+ boxShadow: `inset 0 0 0 2px ${theme.vars!.palette.primary.main}, inset 0 0 0 4px ${theme.vars!.palette.background.paper}`,
+ },
+ },
+ "& .plan-cell--readonly": {
+ cursor: "default",
+ "&:hover": { background: "transparent" },
+ "&:hover::before": { transform: "none", boxShadow: "none" },
+ },
+ // A read-only cell that HAS data is still clickable (view mode).
+ "& .plan-cell--readonly:not(.plan-cell--empty):not(.plan-cell--past)": {
+ cursor: "pointer",
+ "&:hover": { background: theme.vars!.palette.action.hover },
+ "&:hover::before": {
+ transform: "scaleX(1.4)",
+ boxShadow: "0 0 12px currentColor",
+ },
+ },
+ "& .plan-cell--past": {
+ cursor: "default",
+ opacity: 0.55,
+ "&:hover": { background: "transparent" },
+ "&:hover::before": { transform: "none", boxShadow: "none" },
+ },
+ // Empty cells: dashed tape + a "+" hover hint.
+ "& .plan-cell--empty::before": {
+ background: "transparent",
+ border: `1px dashed ${theme.vars!.palette.divider}`,
+ width: 2,
+ left: 5,
+ },
+ "& .plan-cell--empty:hover::before": {
+ background: theme.vars!.palette.primary.main,
+ borderColor: theme.vars!.palette.primary.main,
+ transform: "scaleX(1.6)",
+ },
+ "& .plan-cell--empty::after": {
+ content: '"+"',
+ position: "absolute",
+ inset: 0,
+ display: "grid",
+ placeItems: "center",
+ color: theme.vars!.palette.text.disabled,
+ opacity: 0,
+ fontSize: 18,
+ fontWeight: 300,
+ pointerEvents: "none",
+ transition: "opacity 150ms ease",
+ },
+ "& .plan-cell--empty:hover::after": {
+ opacity: 0.7,
+ color: theme.vars!.palette.primary.main,
+ },
+ "& .plan-cell--empty.plan-cell--readonly::after": { content: "none" },
+
+ // One-shot mutation pulse
+ "@keyframes plan-cell-pulse": {
+ "0%": {
+ boxShadow: `inset 0 0 0 2px ${theme.vars!.palette.primary.main}, 0 0 0 0 rgba(${theme.vars!.palette.primary.mainChannel} / 0.4)`,
+ },
+ "60%": {
+ boxShadow: `inset 0 0 0 2px ${theme.vars!.palette.primary.main}, 0 0 0 6px transparent`,
+ },
+ "100%": { boxShadow: "inset 0 0 0 2px transparent, 0 0 0 0 transparent" },
+ },
+ "& .plan-cell--pulse": { animation: "plan-cell-pulse 600ms ease-out" },
+
+ // Chips inside a cell
+ "& .plan-chip-block": {
+ display: "flex",
+ flexDirection: "column",
+ gap: 4,
+ textAlign: "left",
+ width: "100%",
+ minWidth: 0,
+ },
+ "& .plan-chip-line": {
+ display: "flex",
+ alignItems: "center",
+ gap: 7,
+ flexWrap: "wrap",
+ minWidth: 0,
+ },
+ "& .plan-chip": {
+ display: "inline-flex",
+ alignItems: "center",
+ gap: 5,
+ borderRadius: 4,
+ padding: "2px 7px",
+ fontSize: "10.5px",
+ fontWeight: 600,
+ letterSpacing: "0.04em",
+ textTransform: "uppercase",
+ color: `var(--cat-color, ${theme.vars!.palette.text.primary})`,
+ background: `color-mix(in srgb, var(--cat-color, transparent) 12%, ${theme.vars!.palette.background.paper})`,
+ border: `1px solid color-mix(in srgb, var(--cat-color, ${theme.vars!.palette.divider}) 35%, transparent)`,
+ whiteSpace: "nowrap",
+ },
+ "& .plan-chip-project": {
+ fontFamily: fonts.mono,
+ fontSize: "12.5px",
+ fontWeight: 600,
+ color: theme.vars!.palette.text.primary,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ maxWidth: "100%",
+ },
+ "& .plan-cell-note": {
+ display: "-webkit-box",
+ WebkitLineClamp: 2,
+ WebkitBoxOrient: "vertical",
+ overflow: "hidden",
+ marginTop: 1,
+ fontSize: "11.5px",
+ color: theme.vars!.palette.text.secondary,
+ lineHeight: 1.35,
+ fontStyle: "italic",
+ },
+
+ "@media (max-width: 480px)": {
+ "& col.plan-grid-date-col": { width: 72 },
+ "& tbody td": { minWidth: 132 },
+ "& .plan-cell": { minHeight: 56 },
+ },
+ // Touch: show a faint persistent "+" on empty editable cells.
+ "@media (hover: none)": {
+ "& .plan-cell--empty:not(.plan-cell--readonly)::after": {
+ opacity: 0.26,
+ color: theme.vars!.palette.text.disabled,
+ },
+ },
+ "@media (prefers-reduced-motion: reduce)": {
+ "& .plan-cell, & .plan-cell::before, & .plan-cell::after": {
+ transition: "none",
+ },
+ "& .plan-cell--pulse": { animation: "none" },
+ },
+}));
interface Props {
data: GridData | undefined;
@@ -120,7 +472,7 @@ export default function PlanGrid({
: undefined;
return (
-
+
{/* The colgroup is what actually controls column width in a
table — `min-width` on `| `/` | ` is just a floor that
@@ -251,6 +603,6 @@ export default function PlanGrid({
})}
|
-
+
);
}
diff --git a/src/admin/plan.css b/src/admin/plan.css
index 9feba9e..34a26d4 100644
--- a/src/admin/plan.css
+++ b/src/admin/plan.css
@@ -1,1109 +1,17 @@
/* ============================================================================
- Plán prací (work schedule) — editorial / atelier aesthetic
- A dispatcher's desk: paper, ink, color-coded marker tape.
- Designed to scale to 20+ people columns without losing legibility.
- ============================================================================ */
+ Plán prací — leftover bespoke styles.
-/* ---- Theme tokens for the plan surface ----------------------------------- */
+ The planner GRID is now fully MUI/theme-driven (see PlanGrid's styled()
+ wrapper), and the toolbar / range label / banner / day-choice modal were
+ migrated to MUI components. Only the category-manager modal rows remain
+ here; they keep a single paper-rule token for their swatch/divider borders.
+ ============================================================================ */
:root {
- /* Marker tape — these are the *plan*-specific category hues.
- Tuned for the same Czech workflow categories in the chip classes. */
- --plan-tape-work: #2563eb; /* deep blue */
- --plan-tape-preparation: #0d9488; /* teal */
- --plan-tape-travel: #ca8a04; /* mustard */
- --plan-tape-leave: #16a34a; /* green */
- --plan-tape-sick: #dc2626; /* red */
- --plan-tape-training: #7c3aed; /* violet */
- --plan-tape-other: #6b7280; /* graphite */
-
- /* Paper / ink tones (overridden in [data-theme] blocks). */
- --plan-paper: #f7f4ee;
--plan-paper-rule: rgba(20, 20, 24, 0.08);
- --plan-paper-soft: #fbf9f4;
- --plan-ink: #1a1a1a;
- --plan-ink-muted: #6b6b6b;
- --plan-ink-faint: #9a958c;
- --plan-today-bar: #c73030;
- --plan-today-fill: rgba(199, 48, 48, 0.06);
- --plan-today-glow: rgba(199, 48, 48, 0.18);
- --plan-weekend-fill: rgba(20, 20, 24, 0.025);
- --plan-weekend-stripe: rgba(20, 20, 24, 0.04);
- --plan-column-rule: rgba(20, 20, 24, 0.06);
- --plan-row-rule: rgba(20, 20, 24, 0.07);
- --plan-shadow:
- 0 1px 0 rgba(20, 20, 24, 0.04), 0 12px 32px -16px rgba(20, 20, 24, 0.12);
- --plan-cell-bg: #ffffff;
- --plan-cell-bg-hover: #fffdf8;
- --plan-cell-shadow: 0 1px 0 rgba(20, 20, 24, 0.03);
- --plan-header-bg: #f1ede5;
- --plan-header-ink: #1a1a1a;
- --plan-person-dot: #d1cdbf;
-
- /* Type scale (works with the app's existing --font-body / --font-mono). */
- --plan-font-display: var(--font-mono);
- --plan-font-body: var(--font-body);
}
-
[data-theme="dark"] {
- --plan-paper: #0d0d0e;
- --plan-paper-soft: #131316;
--plan-paper-rule: rgba(255, 255, 255, 0.06);
- --plan-ink: #f4f1ea;
- --plan-ink-muted: #9b9b9b;
- --plan-ink-faint: #5d5d5d;
- --plan-today-bar: #ff5a5f;
- --plan-today-fill: rgba(255, 90, 95, 0.07);
- --plan-today-glow: rgba(255, 90, 95, 0.25);
- --plan-weekend-fill: rgba(255, 255, 255, 0.015);
- --plan-weekend-stripe: rgba(255, 255, 255, 0.025);
- --plan-column-rule: rgba(255, 255, 255, 0.05);
- --plan-row-rule: rgba(255, 255, 255, 0.05);
- --plan-shadow:
- 0 1px 0 rgba(0, 0, 0, 0.4), 0 18px 40px -20px rgba(0, 0, 0, 0.6);
- --plan-cell-bg: #16161a;
- --plan-cell-bg-hover: #1c1c22;
- --plan-cell-shadow: 0 1px 0 rgba(0, 0, 0, 0.5);
- --plan-header-bg: #131316;
- --plan-header-ink: #f4f1ea;
- --plan-person-dot: #2c2c30;
-}
-
-/* ============================================================================
- Page surface — give the planner room to breathe
- ============================================================================ */
-
-.plan-work-page {
- position: relative;
-}
-
-/* Soft paper-grain behind the grid. Doesn't tile from an image — just two
- layered radial gradients in the theme paper tone. Adds warmth in light
- mode, depth in dark. */
-.plan-grid-wrap {
- position: relative;
- overflow: auto;
- /* Smooth momentum scroll on touch, and keep scroll chaining contained so a
- fling inside the grid doesn't bounce the whole page (or trigger
- pull-to-refresh) on mobile. */
- -webkit-overflow-scrolling: touch;
- overscroll-behavior: contain;
- background: var(--plan-paper);
- border: 1px solid var(--plan-paper-rule);
- border-radius: 14px;
- box-shadow: var(--plan-shadow);
- max-height: calc(100vh - 240px);
- max-height: calc(100dvh - 240px);
- isolation: isolate;
-}
-
-.plan-grid-wrap::before {
- content: "";
- position: absolute;
- inset: 0;
- pointer-events: none;
- background:
- radial-gradient(
- 1100px 320px at 0% 0%,
- color-mix(in srgb, var(--plan-tape-work) 5%, transparent),
- transparent 60%
- ),
- radial-gradient(
- 900px 280px at 100% 100%,
- color-mix(in srgb, var(--plan-tape-training) 5%, transparent),
- transparent 60%
- );
- z-index: 0;
-}
-
-.plan-grid-wrap::after {
- /* Very subtle horizontal "rule" lines to evoke ruled paper — drawn via
- repeating gradient so the table doesn't have to ship extra DOM. */
- content: "";
- position: absolute;
- inset: 0;
- pointer-events: none;
- background-image: repeating-linear-gradient(
- to bottom,
- transparent 0,
- transparent 55px,
- var(--plan-paper-rule) 55px,
- var(--plan-paper-rule) 56px
- );
- opacity: 0.35;
- mix-blend-mode: multiply;
- z-index: 0;
-}
-
-[data-theme="dark"] .plan-grid-wrap::after {
- mix-blend-mode: screen;
- opacity: 0.18;
-}
-
-/* Placeholder while fetching — matches wrap height so the toolbar doesn't jump. */
-.plan-grid-placeholder {
- min-height: 320px;
- border: 1px dashed var(--plan-paper-rule);
- border-radius: 14px;
- background: var(--plan-paper);
- opacity: 0.6;
-}
-
-/* ============================================================================
- Grid table
- ============================================================================ */
-
-.plan-grid {
- position: relative;
- z-index: 1;
- border-collapse: separate;
- border-spacing: 0;
- width: 100%;
- font-size: 12.5px;
- color: var(--plan-ink);
- font-family: var(--plan-font-body);
-}
-
-/* The element in is what actually pins column width
- under `table-layout: auto`. The first in PlanGrid.tsx carries
- `plan-grid-date-col` — sizing it to 88px here makes the date column
- that width, full stop (the `min-width` on the th/td is just a floor
- and the auto-layout algorithm happily stretches it otherwise). */
-.plan-grid col.plan-grid-date-col {
- width: 88px;
-}
-
-/* ---- Column rules (vertical) — drawn via background on the *first* cell
- of each row so we don't ship extra DOM. */
-.plan-grid tbody tr td + td {
- border-left: 1px solid var(--plan-column-rule);
-}
-
-/* ---- Sticky header ------------------------------------------------------ */
-
-.plan-grid thead th {
- position: sticky;
- top: 0;
- z-index: 3;
- padding: 14px 12px 12px;
- background: var(--plan-header-bg);
- color: var(--plan-header-ink);
- border-bottom: 1px solid var(--plan-paper-rule);
- text-align: left;
- font-weight: 600;
- white-space: nowrap;
- font-family: var(--plan-font-body);
- letter-spacing: -0.005em;
- /* A double-rule under the header — typographer's "French rule". */
- box-shadow:
- inset 0 -1px 0 var(--plan-paper-rule),
- inset 0 -3px 0 color-mix(in srgb, var(--plan-paper-rule) 60%, transparent);
-}
-
-/* The date column header reads like a section label, not a column name. */
-.plan-grid thead th.plan-grid-date-col {
- left: 0;
- z-index: 4;
- min-width: 88px;
- padding: 12px 10px 10px 14px;
- font-size: 10.5px;
- font-weight: 700;
- letter-spacing: 0.18em;
- text-transform: uppercase;
- color: var(--plan-ink-muted);
- font-family: var(--plan-font-body);
-}
-
-/* Person column header — avatar dot + name (two-line) */
-.plan-grid thead th .plan-person-head {
- display: inline-flex;
- align-items: center;
- gap: 9px;
- min-width: 0;
-}
-
-.plan-grid thead th .plan-person-dot {
- flex-shrink: 0;
- width: 9px;
- height: 9px;
- border-radius: 50%;
- background: var(--plan-person-dot);
- box-shadow: inset 0 0 0 2px var(--plan-header-bg);
-}
-
-.plan-grid thead th .plan-person-name {
- display: inline-flex;
- flex-direction: column;
- min-width: 0;
- line-height: 1.1;
-}
-
-.plan-grid thead th .plan-person-name strong {
- font-weight: 600;
- font-size: 13px;
- color: var(--plan-header-ink);
- letter-spacing: -0.005em;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 180px;
-}
-
-.plan-grid thead th .plan-person-name small {
- font-size: 10px;
- font-weight: 500;
- color: var(--plan-ink-faint);
- letter-spacing: 0.04em;
- text-transform: uppercase;
- font-family: var(--plan-font-display);
- margin-top: 1px;
-}
-
-/* ---- Date column cells (sticky left) ----------------------------------- */
-
-.plan-grid tbody td.plan-grid-date-col {
- position: sticky;
- left: 0;
- z-index: 1;
- background: var(--plan-paper-soft);
- font-weight: 500;
- color: var(--plan-ink-muted);
- white-space: nowrap;
- /* Override the inherited `tbody td { min-width: 168px }` — the date
- column is just a "stamp" with the day number and weekday name. The
- month is already shown in the page range label at the top, so the
- cell no longer carries a "· 06" suffix. 88px fits "Pondělí" with
- comfortable padding on either side. */
- min-width: 88px;
- padding: 9px 10px 9px 14px;
- border-right: 1px solid var(--plan-paper-rule);
- border-bottom: 1px solid var(--plan-row-rule);
- font-family: var(--plan-font-body);
- /* The date column is the "tab" — let it sit on its own little slab. */
- box-shadow: inset -1px 0 0 var(--plan-paper-rule);
-}
-
-.plan-grid tbody td.plan-grid-date-col .plan-date-stamp {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- line-height: 1.1;
- gap: 1px;
-}
-
-.plan-grid tbody td.plan-grid-date-col .plan-date-daynum {
- font-family: var(--plan-font-display);
- font-size: 17px;
- font-weight: 600;
- color: var(--plan-ink);
- letter-spacing: -0.01em;
- font-variant-numeric: tabular-nums;
- line-height: 1.05;
-}
-
-.plan-grid tbody td.plan-grid-date-col .plan-date-dow {
- font-size: 10px;
- font-weight: 600;
- letter-spacing: 0.14em;
- text-transform: uppercase;
- color: var(--plan-ink-faint);
- /* Slightly looser leading so Czech diacritics (ě, í, ý, á) on full
- weekday names have headroom and don't visually clip. */
- line-height: 1.3;
-}
-
-/* Mark "today" with a heavy red rule on the left of the date cell. The
- cell is sticky, so the bar stays put while the rest of the row scrolls. */
-.plan-grid tbody tr.is-today td.plan-grid-date-col {
- /* Sticky column must stay opaque or horizontally-scrolled cells show
- through it. Layer the (translucent) today tint over the opaque paper
- base instead of replacing it. */
- background:
- linear-gradient(var(--plan-today-fill), var(--plan-today-fill)),
- var(--plan-paper-soft);
- position: sticky;
-}
-
-.plan-grid tbody tr.is-today td.plan-grid-date-col::before {
- content: "";
- position: absolute;
- left: 0;
- top: 6px;
- bottom: 6px;
- width: 3px;
- border-radius: 0 2px 2px 0;
- background: var(--plan-today-bar);
- box-shadow: 0 0 14px var(--plan-today-glow);
-}
-
-.plan-grid tbody tr.is-today td.plan-grid-date-col .plan-date-daynum {
- color: var(--plan-today-bar);
-}
-
-/* The rest of the cells in today's row get a faint warm wash so the row
- reads as "the focal row" without being loud. */
-.plan-grid tbody tr.is-today td:not(.plan-grid-date-col) {
- background: var(--plan-today-fill);
-}
-
-/* ---- Body cells --------------------------------------------------------- */
-
-.plan-grid tbody td {
- padding: 0;
- border-bottom: 1px solid var(--plan-row-rule);
- vertical-align: stretch;
- min-width: 168px;
- background: var(--plan-cell-bg);
-}
-
-/* ---- Weekend treatment --------------------------------------------------
- The rule was a hard tinted background; now we use a soft paper-stripe
- (the ::after paper grain is on the wrap, so the row needs its own
- gentle wash that doesn't fight the grain). */
-.plan-grid tr.plan-grid-weekend td {
- background: var(--plan-weekend-fill);
-}
-
-.plan-grid tr.plan-grid-weekend td.plan-grid-date-col {
- /* Opaque base under the translucent weekend tint so the sticky column
- doesn't reveal scrolled cells behind it. */
- background:
- linear-gradient(var(--plan-weekend-fill), var(--plan-weekend-fill)),
- var(--plan-paper-soft);
- color: var(--plan-ink-faint);
-}
-
-.plan-grid tr.plan-grid-weekend td.plan-grid-date-col .plan-date-dow {
- color: var(--plan-tape-travel);
-}
-
-.plan-grid tr.plan-grid-weekend td.plan-grid-date-col .plan-date-daynum {
- color: var(--plan-ink-muted);
-}
-
-/* Weekend empty cells: the row already reads as weekend (date column
- tints the DOW amber, the row gets the soft weekend fill). We
- deliberately leave the `::after` slot free here so the base
- `.plan-cell--empty::after` "+" hover affordance keeps working —
- a higher-specificity `::after` here would lock the content to "—"
- and break the hover hint. */
-
-/* ============================================================================
- The cell itself — the marker chip
- ============================================================================ */
-
-.plan-cell {
- position: relative;
- display: block;
- width: 100%;
- height: 100%;
- min-height: 60px;
- text-align: left;
- background: transparent;
- border: 0;
- border-radius: 0;
- padding: 9px 11px 10px 16px; /* extra left padding for the tape */
- cursor: pointer;
- font: inherit;
- color: inherit;
- transition:
- background var(--motion-fast) ease,
- transform var(--motion-fast) ease;
-}
-
-/* The colored "marker tape" on the left edge of every filled cell.
- Drawn with a thin gradient on the cell's left padding box. The actual
- color comes from a category-specific class added on the chip — but to
- keep the tape present even when the cell is empty (so empty cells
- don't feel "broken"), we draw a faint rule for the empty state. */
-.plan-cell::before {
- content: "";
- position: absolute;
- left: 4px;
- top: 8px;
- bottom: 8px;
- width: 3px;
- border-radius: 2px;
- background: var(--cat-color, var(--plan-paper-rule));
- transition:
- background var(--motion-fast) ease,
- box-shadow var(--motion-fast) ease,
- transform var(--motion-fast) ease;
-}
-
-.plan-cell:hover {
- background: var(--plan-cell-bg-hover);
-}
-
-.plan-cell:hover::before {
- transform: scaleX(1.4);
- box-shadow: 0 0 12px currentColor;
-}
-
-.plan-cell:active {
- transform: translateY(0.5px);
-}
-
-.plan-cell:focus-visible {
- outline: none;
- box-shadow:
- inset 0 0 0 2px var(--accent-color),
- inset 0 0 0 4px var(--plan-paper);
-}
-
-.plan-cell--readonly {
- cursor: default;
-}
-.plan-cell--readonly:hover {
- background: transparent;
-}
-.plan-cell--readonly:hover::before {
- transform: none;
- box-shadow: none;
-}
-
-/* A read-only cell that HAS a record is still clickable — it opens the
- read-only detail view (see PlanWork.onCellClick: a cell with data routes
- to kind: "view" when !canEdit). So keep the hover affordance and pointer
- cursor for those, mirroring the editable cell's hover. Empty read-only
- cells and past cells stay inert (nothing to view / cannot act). The
- :not() chain out-specifies the readonly-kills-hover rules above. */
-.plan-cell--readonly:not(.plan-cell--empty):not(.plan-cell--past) {
- cursor: pointer;
-}
-.plan-cell--readonly:not(.plan-cell--empty):not(.plan-cell--past):hover {
- background: var(--plan-cell-bg-hover);
-}
-.plan-cell--readonly:not(.plan-cell--empty):not(
- .plan-cell--past
- ):hover::before {
- transform: scaleX(1.4);
- box-shadow: 0 0 12px currentColor;
-}
-
-/* Past-day cells: even if the user has edit permission, the server
- rejects create/edit on past dates (see assertNotPastDate in
- plan.service.ts) so we lock the cell to view-only in the UI. Visually
- dim them and drop the "click to add" affordance so the user doesn't
- expect to be able to act on them. */
-.plan-cell--past {
- cursor: default;
- opacity: 0.55;
-}
-.plan-cell--past:hover {
- background: transparent;
-}
-.plan-cell--past:hover::before {
- transform: none;
- box-shadow: none;
- background: var(--plan-paper-rule);
- border-color: var(--plan-paper-rule);
-}
-.plan-cell--past:hover::after {
- /* Suppress the "+" hover hint on past empty cells — clicking does
- nothing useful there (no record to view, no record to create). */
- opacity: 0 !important;
- color: var(--plan-ink-faint) !important;
-}
-
-/* "Empty" cells get a subtle add-icon affordance on hover, so the user
- knows clicking creates something. We do this without changing the DOM. */
-.plan-cell--empty::before {
- background: transparent;
- border: 1px dashed var(--plan-paper-rule);
- width: 2px;
- left: 5px;
-}
-
-.plan-cell--empty:hover {
- background: var(--plan-cell-bg-hover);
-}
-.plan-cell--empty:hover::before {
- background: var(--accent-color);
- border-color: var(--accent-color);
- transform: scaleX(1.6);
- box-shadow: 0 0 10px var(--accent-glow, rgba(214, 48, 49, 0.4));
-}
-
-.plan-cell--empty::after {
- /* Tiny "+" hint that fades in on hover. */
- content: "+";
- position: absolute;
- inset: 0;
- display: grid;
- place-items: center;
- color: var(--plan-ink-faint);
- opacity: 0;
- font-size: 18px;
- font-weight: 300;
- pointer-events: none;
- transition: opacity var(--motion-fast) ease;
-}
-.plan-cell--empty:hover::after {
- opacity: 0.7;
- color: var(--accent-color);
-}
-
-/* View-only (and otherwise locked) empty cells must NOT advertise an "add"
- action: no "+" glyph and no hover highlight. The click is already a no-op
- for these — PlanWork's onCellClick only opens the create modal when
- canEdit. Two-class selectors out-specify the single-class hover rules
- above. */
-.plan-cell--empty.plan-cell--readonly::after {
- content: none;
-}
-.plan-cell--empty.plan-cell--readonly:hover {
- background: transparent;
-}
-.plan-cell--empty.plan-cell--readonly:hover::after {
- opacity: 0;
-}
-
-/* ============================================================================
- Per-cell pulse — fired by mutations (create/edit/delete).
-
- Replaces the old "full-grid re-animates after a mutation" behavior. The
- mutation's onSuccess in usePlanWork patches the affected cell into the
- React Query cache, PlanWork bumps `lastMutated`, and PlanGrid applies
- `plan-cell--pulse` to that single cell. The keyframe runs once for
- 600ms, then PlanWork clears `lastMutated` (which removes the class).
-
- We use an outline + box-shadow on the button and a glow on the
- marker-tape `::before` — no layout shift, no remount, no global
- animation. The marker-tape glow uses `currentColor` so it picks up
- the category color via the existing :has() rules.
- ============================================================================ */
-
-@keyframes plan-cell-pulse {
- 0% {
- box-shadow:
- inset 0 0 0 2px var(--accent-color),
- inset 0 0 0 4px var(--plan-paper),
- 0 0 0 0 var(--accent-glow, rgba(214, 48, 49, 0.4));
- }
- 60% {
- box-shadow:
- inset 0 0 0 2px var(--accent-color),
- inset 0 0 0 4px var(--plan-paper),
- 0 0 0 6px transparent;
- }
- 100% {
- box-shadow:
- inset 0 0 0 2px transparent,
- inset 0 0 0 4px transparent,
- 0 0 0 0 transparent;
- }
-}
-
-@keyframes plan-cell-tape-pulse {
- 0% {
- box-shadow:
- 0 0 0 0 currentColor,
- 0 0 12px currentColor;
- }
- 100% {
- box-shadow:
- 0 0 0 4px transparent,
- 0 0 0 transparent;
- }
-}
-
-.plan-cell--pulse {
- animation: plan-cell-pulse 600ms ease-out;
-}
-
-.plan-cell--pulse::before {
- /* The tape is a single ::before pseudo-element. We give it a one-shot
- glow that matches the category color via `currentColor` (set by the
- cell's text color). The animation runs alongside the existing
- :hover transform on the tape; both apply to the same pseudo-element
- and stack. */
- animation: plan-cell-tape-pulse 600ms ease-out;
-}
-
-/* ============================================================================
- Chips — the actual data inside a cell
- ============================================================================ */
-
-.plan-chip-block {
- display: flex;
- flex-direction: column;
- gap: 4px;
- text-align: left;
- width: 100%;
- min-width: 0;
-}
-
-.plan-chip-line {
- display: flex;
- align-items: center;
- gap: 7px;
- flex-wrap: wrap;
- min-width: 0;
-}
-
-.plan-chip {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- border-radius: 4px;
- padding: 2px 7px;
- font-size: 10.5px;
- font-weight: 600;
- letter-spacing: 0.04em;
- text-transform: uppercase;
- font-family: var(--plan-font-body);
- /* Category chip is small, monoline. The heavy color lives on the tape
- on the cell edge — the chip is the *text* version of the same color. */
- color: var(--cat-color, var(--plan-ink));
- background: color-mix(
- in srgb,
- var(--cat-color, var(--plan-paper-soft)) 10%,
- var(--plan-paper)
- );
- border: 1px solid
- color-mix(
- in srgb,
- var(--cat-color, var(--plan-paper-rule)) 35%,
- transparent
- );
- white-space: nowrap;
-}
-
-[data-theme="dark"] .plan-chip {
- background: color-mix(
- in srgb,
- var(--cat-color, var(--plan-cell-bg)) 16%,
- var(--plan-paper)
- );
-}
-
-.plan-chip-project {
- font-family: var(--plan-font-display);
- font-size: 12.5px;
- font-weight: 600;
- color: var(--plan-ink);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 100%;
- letter-spacing: -0.005em;
-}
-
-.plan-cell-note {
- display: block;
- margin-top: 1px;
- font-size: 11.5px;
- color: var(--plan-ink-muted);
- line-height: 1.35;
- font-style: italic;
- font-family: var(--plan-font-body);
- /* Show first two lines max, with a soft fade for the third */
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- position: relative;
-}
-
-.plan-cell-note::before {
- content: "“";
- margin-right: 1px;
- color: var(--plan-ink-faint);
- font-style: normal;
-}
-.plan-cell-note::after {
- content: "”";
- margin-left: 1px;
- color: var(--plan-ink-faint);
- font-style: normal;
-}
-
-/* ============================================================================
- Toolbar
- ============================================================================ */
-
-.plan-toolbar {
- display: flex;
- gap: 12px;
- align-items: center;
- margin-top: 1.25rem;
- margin-bottom: 1.25rem;
- flex-wrap: wrap;
-}
-
-/* Nav button group (Dnes / ← / →). On desktop it's an inline-flex with the
- same 12px gap the toolbar used between the three loose buttons before, so
- the layout is unchanged. On mobile it becomes a full-width row (see the
- ≤640px block below). */
-.plan-toolbar-nav {
- display: inline-flex;
- align-items: center;
- gap: 12px;
-}
-
-.plan-view-toggle {
- margin-left: auto;
- display: inline-flex;
- gap: 2px;
- padding: 3px;
- background: var(--bg-tertiary);
- border: 1px solid var(--border-color);
- border-radius: 999px;
- box-shadow: inset 0 1px 0 var(--border-color);
-}
-
-.plan-view-toggle .admin-btn {
- border-radius: 999px;
- padding: 5px 14px;
- font-size: 12px;
- letter-spacing: 0.01em;
-}
-
-.plan-range-label-slot {
- display: inline-flex;
- align-items: baseline;
- gap: 6px;
- /* Wide enough for "Týden 23 (1.6. – 7.6. 2026)" — the longest
- range label — so the slot doesn't collapse during AnimatePresence
- exit/enter transitions. Without this, the toolbar jumps when the
- label width changes between week and month views. */
- min-width: 230px;
- color: var(--plan-ink);
- font-weight: 500;
- font-variant-numeric: tabular-nums;
- overflow: hidden;
- font-family: var(--font-body);
- position: relative;
-}
-
-.plan-range-label {
- white-space: nowrap;
- font-size: 14px;
- font-weight: 600;
- letter-spacing: -0.005em;
- color: var(--plan-ink);
-}
-
-/* ============================================================================
- Banner (read-only notice)
- ============================================================================ */
-
-.plan-banner {
- padding: 12px 16px;
- margin-bottom: 16px;
- background: var(--info-soft);
- border: 1px solid color-mix(in srgb, var(--info) 25%, transparent);
- border-left: 3px solid var(--info);
- border-radius: 10px;
- color: var(--text-primary);
- font-size: 13.5px;
- display: flex;
- align-items: center;
- gap: 10px;
- line-height: 1.45;
-}
-
-.plan-banner-icon {
- display: inline-grid;
- place-items: center;
- width: 22px;
- height: 22px;
- border-radius: 50%;
- background: color-mix(in srgb, var(--info) 18%, transparent);
- color: var(--info);
- font-weight: 700;
- font-style: italic;
- font-family: var(--font-display, var(--font-mono));
- font-size: 12px;
- flex-shrink: 0;
-}
-
-.plan-banner code {
- background: transparent;
- padding: 0;
-}
-
-/* ============================================================================
- Day-in-range choice panel (modal)
- ============================================================================ */
-
-.plan-modal-actions {
- display: flex;
- gap: 8px;
- justify-content: flex-end;
- margin-top: 12px;
- margin-bottom: 0;
-}
-
-.plan-modal-intro {
- margin: 0 0 14px;
- font-size: 14px;
- line-height: 1.55;
- color: var(--text-secondary);
-}
-.plan-modal-intro strong {
- color: var(--text-primary);
- font-weight: 600;
- font-variant-numeric: tabular-nums;
-}
-
-.plan-choices {
- list-style: none;
- margin: 0;
- padding: 0;
- display: flex;
- flex-direction: column;
- gap: 10px;
-}
-
-.plan-choice {
- display: flex;
- align-items: center;
- gap: 14px;
- padding: 14px 16px;
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: 10px;
- transition:
- border-color var(--motion-fast) ease,
- background var(--motion-fast) ease,
- transform var(--motion-fast) ease;
-}
-
-.plan-choice:hover {
- border-color: var(--border-color-hover);
- background: var(--bg-tertiary);
- transform: translateY(-1px);
-}
-
-.plan-choice-primary {
- border-left: 3px solid var(--accent-color);
-}
-
-.plan-choice-body {
- flex: 1;
- min-width: 0;
-}
-
-.plan-choice-title {
- font-size: 14px;
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 2px;
-}
-
-.plan-choice-desc {
- font-size: 12.5px;
- line-height: 1.45;
- color: var(--text-secondary);
-}
-
-.plan-choice .admin-btn {
- flex-shrink: 0;
-}
-
-.plan-modal-actions--solo {
- justify-content: center;
- margin-top: 14px;
- padding-top: 12px;
- border-top: 1px solid var(--border-color);
-}
-.plan-modal-actions--solo .admin-btn-ghost {
- width: 100%;
-}
-
-@media (max-width: 480px) {
- .plan-choice {
- flex-direction: column;
- align-items: stretch;
- gap: 10px;
- }
- .plan-choice .admin-btn {
- width: 100%;
- }
-}
-
-/* ============================================================================
- Scale handling — when there are many person columns
- ============================================================================ */
-
-@media (min-width: 1280px) {
- .plan-grid thead th .plan-person-name strong {
- max-width: 140px;
- }
-}
-
-/* On very wide grids (20+ people) — tighten the cell min-width so the
- viewport doesn't need aggressive horizontal scrolling. */
-@media (min-width: 1600px) {
- .plan-grid tbody td {
- min-width: 142px;
- }
-}
-
-@media (min-width: 2000px) {
- .plan-grid tbody td {
- min-width: 120px;
- }
- .plan-grid {
- font-size: 12px;
- }
-}
-
-/* On narrow viewports, the grid's main use is the "I'm working today" view.
- Drop the desktop date-column "tab" treatment to a compact inline one. */
-@media (max-width: 720px) {
- .plan-grid tbody td.plan-grid-date-col {
- /* Matches desktop (no `· 06` suffix to fit), just tighter padding
- for cramped screens. */
- min-width: 80px;
- padding: 8px 8px;
- }
- .plan-grid tbody td.plan-grid-date-col .plan-date-daynum {
- font-size: 15px;
- }
- .plan-grid tbody td.plan-grid-date-col .plan-date-dow {
- font-size: 9px;
- letter-spacing: 0.1em;
- }
- .plan-grid tbody td {
- min-width: 140px;
- }
-}
-
-/* ----------------------------------------------------------------------------
- Mobile toolbar — stack into clean full-width rows.
-
- The desktop toolbar is one wrap-row with a fixed-width range label and a
- `margin-left:auto` view toggle; on a phone that collapses into a ragged,
- gappy mess. Here we lay it out as tidy stacked rows: nav buttons on row 1,
- the range label centered on its own row, a full-width segmented view toggle,
- then the category-manager button full-width. Source order already produces
- this stack once each block is forced to 100% width — no `order` juggling.
- ---------------------------------------------------------------------------- */
-@media (max-width: 640px) {
- .plan-toolbar {
- gap: 8px;
- margin-top: 1rem;
- margin-bottom: 1rem;
- }
-
- /* Row 1: Dnes (grows) + the two arrow buttons (compact). */
- .plan-toolbar-nav {
- display: flex;
- flex: 1 1 100%;
- gap: 8px;
- }
- .plan-toolbar-nav .admin-btn {
- flex: 1 1 auto;
- }
- /* The ← / → buttons carry an aria-label; "Dnes" does not — keep the arrows
- compact and let "Dnes" take the remaining width. */
- .plan-toolbar-nav .admin-btn[aria-label] {
- flex: 0 0 auto;
- min-width: 56px;
- }
-
- /* Row 2: range label, full width, centered — drop the desktop min-width. */
- .plan-range-label-slot {
- flex: 1 1 100%;
- min-width: 0;
- justify-content: center;
- }
- .plan-range-label {
- font-size: 15px;
- }
-
- /* Row 3: view toggle as a full-width segmented control, two equal halves. */
- .plan-view-toggle {
- flex: 1 1 100%;
- margin-left: 0;
- border-radius: 12px;
- }
- .plan-view-toggle .admin-btn {
- flex: 1 1 0;
- }
-
- /* Row 4: category-manager button, full width. */
- .plan-toolbar-cat {
- flex: 1 1 100%;
- }
-
- /* Keep the whole grid inside the viewport so the toolbar stays put and the
- sticky header / date column do the orienting (rather than the page
- scrolling the header out of view). */
- .plan-grid-wrap {
- max-height: calc(100dvh - 300px);
- min-height: 280px;
- border-radius: 12px;
- }
-}
-
-/* ----------------------------------------------------------------------------
- Small phones — tighten columns so ~2 people are comfortably visible at
- 360–390px, and shrink the date "stamp" a touch. The colgroup width is
- what actually pins the date column (min-width on the cells is only a floor),
- so it must be set here too, not just on the cells.
- ---------------------------------------------------------------------------- */
-@media (max-width: 480px) {
- .plan-grid col.plan-grid-date-col {
- width: 72px;
- }
- .plan-grid thead th.plan-grid-date-col {
- min-width: 72px;
- padding-left: 10px;
- padding-right: 8px;
- }
- .plan-grid tbody td.plan-grid-date-col {
- min-width: 72px;
- padding: 8px 8px 8px 10px;
- }
- .plan-grid tbody td.plan-grid-date-col .plan-date-daynum {
- font-size: 14px;
- }
- .plan-grid tbody td {
- min-width: 132px;
- }
- .plan-cell {
- min-height: 56px;
- padding: 8px 9px 9px 14px;
- }
-}
-
-/* ----------------------------------------------------------------------------
- Touch devices — the "+" add-hint and the tape-grow are hover-only, so they
- never surface on a phone. Show a faint, persistent "+" on empty *editable*
- cells so tapping-to-add is discoverable. Read-only and past cells already
- suppress the glyph (content:none / the readonly chain), so they stay inert.
- ---------------------------------------------------------------------------- */
-@media (hover: none) {
- .plan-cell--empty:not(.plan-cell--readonly)::after {
- opacity: 0.26;
- color: var(--plan-ink-faint);
- }
-}
-
-/* Respect users who don't want a moving target — kill the hover transforms
- and the rule-line animation. */
-@media (prefers-reduced-motion: reduce) {
- .plan-cell,
- .plan-cell::before,
- .plan-cell::after,
- .plan-cell-note {
- transition: none !important;
- }
- .plan-cell:hover::before {
- transform: none;
- }
- .plan-grid-wrap::after {
- display: none;
- }
- /* Kill the mutation pulse animation. */
- .plan-cell--pulse,
- .plan-cell--pulse::before {
- animation: none !important;
- }
}
/* Category manager modal rows */