/* ============================================================================
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.
============================================================================ */
/* ---- Theme tokens for the plan surface ----------------------------------- */
: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 */
.plan-cat-manager {
display: flex;
flex-direction: column;
gap: 8px;
}
.plan-cat-row {
display: flex;
align-items: center;
gap: 8px;
}
.plan-cat-row--inactive {
opacity: 0.55;
}
.plan-cat-row .admin-form-input {
flex: 1;
min-width: 0;
}
.plan-cat-color {
width: 34px;
height: 34px;
padding: 0;
border: 1px solid var(--plan-paper-rule);
border-radius: 6px;
background: none;
cursor: pointer;
flex: 0 0 auto;
}
.plan-cat-row--new {
margin-top: 6px;
padding-top: 10px;
border-top: 1px dashed var(--plan-paper-rule);
}
/* The per-row delete trigger reads quieter than the solid danger confirm
button it turns into; keep it an outline until the user commits. */
.plan-cat-delete.admin-btn-danger {
background: transparent;
color: var(--danger);
border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent);
}
.plan-cat-delete.admin-btn-danger:hover:not(:disabled) {
background: color-mix(in srgb, var(--danger) 12%, transparent);
color: var(--danger);
}