Files
app/src/admin/plan.css
BOHA fdf180809d fix(plan): make Plán prací usable on mobile + dev port → 3050
Mobile work-plan (Plán prací) responsive fixes — desktop unchanged:
- Toolbar stacks into clean full-width rows on ≤640px (nav group,
  centered range label, full-width segmented view toggle, full-width
  category button); drops the fixed 230px label min-width and the
  margin-left:auto that made the toolbar ragged on phones.
- Grid scroll container: 100dvh-based max-height, momentum scroll, and
  overscroll containment so a fling does not bounce the whole page.
- ≤480px tightens columns (date col 72px via the colgroup <col>, person
  cols 132px) so ~2 people are comfortably visible at 360–390px.
- Touch (@media hover:none): faint persistent + on empty editable
  cells, since the hover-reveal add hint never fires on a phone.

Dev: `npm run dev` (backend) now listens on 3050 instead of 3000 to
avoid colliding with other local apps; CLAUDE.md dev-port note updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:16:00 +02:00

1153 lines
32 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================================
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 <col> element in <colgroup> is what actually pins column width
under `table-layout: auto`. The first <col> 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
360390px, and shrink the date "stamp" a touch. The colgroup <col> 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);
}