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>
1153 lines
32 KiB
CSS
1153 lines
32 KiB
CSS
/* ============================================================================
|
||
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
|
||
360–390px, 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);
|
||
}
|