/* ============================================================================ 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); }