/* tugtile — markdown kanban (prototype, read-only renderer) */ /* Squircle (continuous-curvature) corners — progressive enhancement. corner-shape is Chromium-only as of mid-2026; Safari/WebKit & Firefox ignore it and keep the border-radius arc, so this is ZERO visual regression. Requires a non-zero border-radius (still set per-surface below). Token lives here because corner-shape is NOT inherited but a custom property IS. Applied only to finite-radius card/button/panel/input surfaces — never pills/circles. */ :root { --corner: squircle; } :where( .tugtile-brand, .tugtile__lane, .tugtile__tile, .tugtile__num, .tugtile__popup, .tugtile__popup-d, .tugtile__popup-t, .tugtile__add-btn, .tugtile-prompt-field, .tugtile-iconbtn, .tugtile-ed-find-i, .tugtile-viewcycle, .tugtile__archivebar, .tugtile__lane-chevron, .tugtile-archive-row, .tugtile__addcol-btn ) { corner-shape: var(--corner); } .tugtile { height: 100%; padding: 0; position: relative; /* Contain the view-swap snapshot overlay */ } /* Search/Undo/Redo live in the native back/forward container (repurposed by setupLeftActions), so they line up with the left-sidebar toggle above. */ .tugtile-leftacts { display: flex; align-items: center; gap: 2px; padding-left: 4px; } /* Nudge right to line up the first icon with the left-sidebar toggle above (auto-measure couldn't find the toggle on iPad) */ /* iPad only, Obsidian sets `.is-mobile .view-action { margin: auto 0 }` (specificity (0,2,0)) on these addAction icons, which misaligns Search/Undo/Redo. This (0,3,0) compound out-specifies it and resets the margin without !important — the children always carry both classes (addAction → .clickable-icon.view-action). */ .tugtile-leftacts > .clickable-icon.view-action { margin: 0; } /* Board⇄table swap: the outgoing snapshot slides up & out while the new view rises from below (two panels shuffling). */ .tugtile-viewsnap { position: absolute; inset: 0; z-index: 3; background: var(--background-primary); overflow: hidden; pointer-events: none; animation: tugtile-view-out 0.38s cubic-bezier(.7, 0, .3, 1) forwards; /* ease-in-out: slow → fast → slow */ } @keyframes tugtile-view-out { from { transform: translateY(0); } to { transform: translateY(-100%); } } .tugtile--switching .tugtile__board, .tugtile--switching .tugtile__table-wrap { animation: tugtile-view-in 0.38s cubic-bezier(.7, 0, .3, 1); } @keyframes tugtile-view-in { from { transform: translateY(100%); } to { transform: translateY(0); } } /* Phone: the centered control (view-cycle · lock) sits in a top-bar in the content, not the cramped header */ /* Solid bg + z-index above the view-swap snapshot (z-index 3): the bar stays put on top while board/table slide beneath it, so the up-slide animation never leaks through the control row. */ .tugtile__ctlbar { flex: 0 0 auto; width: 100%; box-sizing: border-box; display: flex; justify-content: center; padding: 2px 8px 8px; position: relative; z-index: 4; background: var(--background-primary); } /* Phone: kill the view-content top padding so the control bar sits flush against the header — otherwise that strip sits ABOVE the bar and the view-swap snapshot (which covers the whole content) leaks through it. */ body.is-phone .workspace-leaf-content[data-type="tugtile-board"] .view-content { padding: 0; } /* All sides: the left/right padding inset the content so the full-width bar still left gaps on both edges */ /* Greyed-out (disabled-looking) header action — used for undo/redo when their stack is empty, instead of hiding the button */ .tugtile-act-off { opacity: 0.35; pointer-events: none; } /* Dimmed brand suffix appended to the centered header title: " · tugtile-ing" */ /* Centered header controls: [view-cycle] · [lock]. Sized up (font-size) to roughly match the right-hand toolbar icons. */ .tugtile-headerctl { display: inline-flex; align-items: center; gap: 6px; vertical-align: middle; font-size: 18px; } .tugtile-sep { color: var(--text-faint); } /* Neutral separator, NOT a button — equal button padding + the flex gap keep its two sides symmetric */ /* Brand suffix + lock icon = one button (t('brandSuffix') + padlock) that toggles the read-only lock */ .tugtile-brand { display: inline-flex; align-items: center; gap: 5px; color: var(--text-faint); font-weight: var(--font-normal); cursor: pointer; border-radius: var(--radius-s); padding: 2px 6px; } .tugtile-brand:hover { background: var(--background-modifier-hover); color: var(--text-muted); } /* TileEditModal (tugtile's big editor) control strip — scoped to the modal so marktile's own strip is untouched. A uniform gray bar spanning the full width: ✕ pinned far left, ✓ far right, the mode·lock centered between. */ .tugtile-edit-modal-full .tugtile__ctlbar { background: var(--background-secondary); } .tugtile-edit-modal-full .tugtile-headerctl { display: flex; width: 100%; } .tugtile-edit-modal-full .tugtile-headerctl > .tugtile-iconbtn:first-child { margin-right: auto; } /* ✕ → far left */ .tugtile-edit-modal-full .tugtile-headerctl > .tugtile-iconbtn:last-child { margin-left: auto; } /* ✓ → far right */ .tugtile-lock-icon { display: inline-flex; } .tugtile-lock-icon svg { width: 0.9em; height: 0.9em; } /* Locked board (read-only): hide add/edit affordances, disable checkboxes, drop drag cursors. View-only actions (collapse, view switch, search) still work. */ .tugtile--locked .tugtile__add, .tugtile--locked .tugtile__addcol, .tugtile--locked .tugtile__tile-more, .tugtile--locked .tugtile__lane-more { display: none; } .tugtile--locked .tugtile__check, .tugtile--locked .tugtile__row-title input { pointer-events: none; } .tugtile--locked .tugtile__tile, .tugtile--locked .tugtile__lane-head { cursor: default; } .tugtile__empty { padding: 16px; color: var(--text-muted); } .tugtile__board { display: flex; gap: 12px; align-items: flex-start; height: 100%; overflow-x: auto; padding: 12px; } .tugtile__lane { flex: 0 0 auto; width: var(--tugtile-lane-width, 300px); /* Uniform fixed column width (configurable in settings); all lanes line up evenly */ max-height: 100%; display: flex; flex-direction: column; background: var(--background-secondary); border-radius: var(--radius-m); padding: 8px; overflow: hidden; /* Cleanly clips content into the changing lane width during collapse/expand (dragging uses a clone on body and is unaffected) */ --tg-d1: 0.26s; /* Stage 1: lane narrows + horizontal title tucks into the chevron (collapse) / widens + title grows back (expand) */ --tg-d2: 0.24s; /* Stage 2: vertical title grows down out of the chevron (collapse) / retracts up into it (expand) */ transition: width var(--tg-d1) cubic-bezier(.4, 0, .2, 1); /* Smooth width change on collapse/expand */ } .tugtile__lane-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 4px 0 8px; /* Horizontal 0 → header rails line up with the card column (cards sit at the lane's own 8px padding on both sides) */ font-weight: 700; cursor: grab; /* Header acts as the drag handle for the lane */ } .tugtile__lane-head:active { cursor: grabbing; } .tugtile__lane--ghost { opacity: 0.4; } .tugtile__lane--chosen { box-shadow: 0 0 0 2px var(--interactive-accent); } .tugtile__lane--drag { /* Slightly tilts and lifts the lane when dragging the entire column */ rotate: 2deg; box-shadow: 0 16px 36px rgba(0, 0, 0, 0.5); } .tugtile__lane-count { flex: 0 0 auto; opacity: 0.55; font-size: 0.85em; transition: opacity var(--tg-d1) ease; } .tugtile__lane-count--over { /* WIP: Warning indicator when exceeding limits */ color: var(--text-error); opacity: 1; font-weight: 700; } .tugtile--hide-counts .tugtile__lane-count { display: none; } .tugtile__list { display: flex; flex-direction: column; gap: 8px; overflow-y: auto; /* Scrollbar gutter without changing the card layout: the list extends 8px to the right (eating the lane's right padding) via the negative margin, while padding-right keeps the cards in their original position. The iOS/iPadOS overlay scrollbar then sits in that right padding instead of on top of the cards. */ margin-right: -8px; padding-right: 8px; } .tugtile__tile:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--interactive-accent); } /* keyboard-focused card (arrow keys move it) */ .tugtile__tile { position: relative; background: var(--background-primary); border: 1px solid var(--background-modifier-border); border-radius: var(--radius-m); padding: 8px; cursor: grab; } .tugtile__tile:active { cursor: grabbing; } .tugtile__tile:hover { border-color: var(--background-modifier-border-hover, var(--interactive-accent)); } /* Dragging state (corresponding to the Sortable class in main.js) */ /* Placeholder left behind: faded out with a dashed border, similar to a Trello card slot */ .tugtile__tile--ghost { opacity: 0.4; background: var(--background-modifier-hover); border-style: dashed; } .tugtile__tile--ghost > * { visibility: hidden; } .tugtile__tile--chosen { box-shadow: inset 0 0 0 2px var(--interactive-accent); /* Inner ring — an outer ring's left/right 2px gets clipped by the list's overflow (cards are full-width with no horizontal room) */ } /* The card following the cursor feels "picked up": slightly tilted, scaled up, with a floating shadow. Uses standalone rotate/scale properties (not transform) to avoid overwriting translations applied by Sortable. */ .tugtile__tile--drag { opacity: 0.97; rotate: 3deg; scale: 1.03; box-shadow: 0 14px 32px rgba(0, 0, 0, 0.5); cursor: grabbing; } /* Index number: a rounded inline chip prepended to the card text. Key detail: keeping font-size unscaled (matching body text size) aligns the number and adjacent text to the same baseline, achieving natural vertical alignment without magic numbers, font, line-height, or scale issues. The background chip sits snug around the number (slight horizontal, minimal vertical padding), centering itself on the first line and wrapping naturally. */ .tugtile__num { display: inline-block; margin-right: 0.4em; padding: 0 0.4em; background: var(--interactive-accent); color: var(--text-on-accent); border-radius: var(--radius-s); font-weight: 700; line-height: 1.25; } /* Floating control area in the top-right: checkbox + ⋯. Using float allows the first line of text to wrap around it, keeping content aligned to the top without wasting space. */ .tugtile__tile-ctrls { float: right; display: flex; align-items: center; gap: 6px; margin: 0 0 2px 8px; } .tugtile__tile-body { padding-top: 0.15em; /* Subtle top padding for breathing room (kept smaller than the control height so the first line still wraps around it) */ overflow-wrap: anywhere; /* Alphanumeric text/URLs do not overflow the lane */ contain: layout paint; /* The body's internal layout is self-contained, so animating the tile's height doesn't re-flow the rendered markdown every frame — kills the stutter on content-heavy tiles */ } /* Long card collapse: hides the full text when folded, showing only the first line of the title (Approach C, without clipping) */ .tugtile__tile-title { display: none; padding-top: 0.15em; font-weight: 600; } .tugtile__tile-title > :first-child { margin-top: 0; } .tugtile__tile-title > :last-child { margin-bottom: 0; } .tugtile__tile--folded .tugtile__tile-body { display: none; } .tugtile__tile--folded .tugtile__tile-title { display: block; } /* On expand the tile height snaps (one reflow), then the body slides + fades in on the GPU — no per-frame layout, so it stays smooth no matter how much content the tile holds. */ .tugtile__tile--revealing .tugtile__tile-body { animation: tugtile-body-reveal 0.24s cubic-bezier(.4, 0, .2, 1) both; } @keyframes tugtile-body-reveal { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }.tugtile__tile-body > :first-child { margin-top: 0; } .tugtile__tile-body > :last-child { margin-bottom: 0; } /* Date and time picker popup */ .tugtile__popup { position: fixed; z-index: 9999; background: var(--background-primary); border: 1px solid var(--background-modifier-border); border-radius: var(--radius-m); box-shadow: var(--shadow-l); padding: 8px; user-select: none; } .tugtile__popup-head { display: flex; align-items: center; justify-content: space-between; font-weight: 600; padding: 2px 4px 8px; } .tugtile__popup-nav { cursor: pointer; padding: 0 8px; display: inline-flex; align-items: center; } .tugtile__popup-nav svg { width: 1.15em; height: 1.15em; } .tugtile__popup-nav:hover { color: var(--text-accent); } .tugtile__popup-grid { display: grid; grid-template-columns: repeat(7, 2em); gap: 2px; text-align: center; } .tugtile__popup-w { opacity: 0.5; font-size: 0.8em; } .tugtile__popup-d { cursor: pointer; border-radius: var(--radius-s); padding: 3px 0; line-height: 1.4; } .tugtile__popup-d:hover { background: var(--background-modifier-hover); } .tugtile__popup-d--today { color: var(--text-accent); font-weight: 700; } .tugtile__popup--time { max-height: 240px; overflow-y: auto; padding: 4px; } .tugtile__popup-t { cursor: pointer; padding: 4px 16px; border-radius: var(--radius-s); } .tugtile__popup-t:hover { background: var(--background-modifier-hover); } /* Tag chips at the bottom of the card (when move-tags-to-card-footer is enabled) */ .tugtile__tags { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 4px; } .tugtile__tag { font-size: 0.78em; padding: 1px 7px; border-radius: 999px; background: var(--background-modifier-hover); color: var(--text-accent); cursor: pointer; } .tugtile__tag:hover { filter: brightness(1.1); } /* Date/time chips at the bottom of the card */ .tugtile__date { margin-top: 6px; font-size: 0.78em; color: var(--text-muted); display: flex; flex-wrap: wrap; align-items: center; gap: 8px; /* separates the date and time chips (replaces the old full-width-space spacer) */ } .tugtile__date-d, .tugtile__date-t { display: inline-flex; align-items: center; gap: 3px; } .tugtile__date svg { width: 1em; height: 1em; } /* Lucide calendar/clock, sized to the chip text */ .tugtile__archivebar-i { display: inline-flex; } .tugtile__archivebar svg { width: 1.1em; height: 1.1em; } /* Lucide archive icon (was a 📥 emoji) */ /* Date status colors: red for overdue, accent for today, yellow for upcoming (≤7 days), and green for safe (>7 days); colored whenever a date exists */ .tugtile__date--overdue { color: var(--color-red); font-weight: 600; } .tugtile__date--today { color: var(--text-accent); font-weight: 600; } .tugtile__date--soon { color: var(--color-yellow); font-weight: 600; } .tugtile__date--safe { color: var(--color-green); } .tugtile__date--unknown { color: var(--text-muted); } /* Neutral color used when the date cannot be parsed */ /* Freeze container scrolling during dragging (adding .is-dragging in main.js onStart) to prevent native mobile scrolling from fighting the drag gesture */ .tugtile.is-dragging .tugtile__board, .tugtile.is-dragging .tugtile__list { /* (0,2,0) already out-specifies the base .tugtile__board/__list (0,1,0) in this file, so no !important needed */ overflow: hidden; touch-action: none; } /* Add card button and input */ .tugtile__add { padding-top: 8px; /* Match the 8px gap between cards so the add button keeps the same vertical rhythm */ } .tugtile__add-btn { width: 100%; text-align: left; background: transparent; border: 1px dashed var(--background-modifier-border); border-radius: var(--radius-m); color: var(--text-muted); padding: 8px; /* Match card padding and the add-lane button (consistent 8px rhythm); left-aligned "+" lines up with the card number chip */ cursor: pointer; } .tugtile__add-btn:hover { background: var(--background-modifier-hover); color: var(--text-normal); } /* ⋯ More actions */ .tugtile__tile-more { color: var(--text-faint); line-height: 1; cursor: pointer; opacity: 0.3; display: inline-flex; align-items: center; } .tugtile__tile-more svg { width: 1em; height: 1em; } .tugtile__tile-more:hover { opacity: 1; color: var(--text-normal); } /* Card checkbox */ .tugtile__check { margin: 0; flex: none; cursor: pointer; } /* "Undo" button for toast notifications (to restore deleted/archived items in one tap) */ .tugtile-toast-undo { margin-left: 10px; font-weight: 700; text-decoration: underline; cursor: pointer; color: var(--text-accent); } /* Focused editing sheet (integrated with iOS 26 rounded sheet design; hides native ✕ to use our own left ❌ and right ✓) */ .tugtile-edit-modal-full { width: 100vw; max-width: 100vw; height: 100dvh; /* Dynamic viewport: shrinks with the on-screen keyboard so the editor stays inside the visible area and the textarea can scroll. dvh is supported on every Obsidian-supported renderer (Electron + iOS/Android webview), so no 100vh fallback is needed. */ max-height: 100dvh; border-radius: 0; /* Full-screen focus editor — nothing of the board shows behind */ /* Inset content past the iPad status bar / notch / home indicator; env() = 0 on desktop. The solid background still bleeds into these strips. */ padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); overflow: hidden; transform-origin: center; animation: tugtile-pop-in 0.4s cubic-bezier(.34, 1.56, .64, 1); /* Keep the bounce — full-screen ≠ unrefined */ } /* Backdrop darkens to solid FAST (so the board vanishes quickly, not lingering behind the bounce); zero container padding */ .modal-container.tugtile-edit-host { padding: 0; align-items: flex-start; } /* Top-pinned, so shrinking the editor to the visual viewport (keyboard) keeps it above the keyboard. Class set in TileEditModal.onOpen — a direct container target, no parent-match selector. */ /* The status-bar / home-indicator strips show --background-secondary (white in light, grey in dark) and sit outside the modal's reach. So instead of fighting to cover them, dye the WHOLE editor --background-secondary to match — seamless in both modes (the old --background-primary only matched in light mode). */ .modal-container.tugtile-edit-host .modal-bg { background: var(--background-secondary); opacity: 1; animation: tugtile-bg-in 0.12s ease both; } .tugtile-edit-modal-full, .tugtile-edit-modal-full .tugtile-edit-modal { background: var(--background-secondary); } /* macOS: push the editor down so its top bar (✕) isn't covered by the window traffic lights (class is .mod-macos) */ .mod-macos .tugtile-edit-modal-full { padding-top: 28px; } @keyframes tugtile-bg-in { from { opacity: 0; } to { opacity: 1; } } .tugtile-edit-modal-full.tugtile-ed-closing { animation: tugtile-pop-out 0.28s ease-in forwards; /* Exit transition: reverse scaling animation */ } /* Opacity reaches 1 early (~0.1s) so the editor covers the board fast — the scale still bounces to settle. Fixes the "laggy / see-through during the animation" feel. */ @keyframes tugtile-pop-in { 0% { opacity: 0; transform: scale(0.92); } 35% { opacity: 1; } 100% { opacity: 1; transform: scale(1); } } @keyframes tugtile-pop-out { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.92); } } /* In-content text bars (search · rename · add-lane) — none of these use an Obsidian Modal any more. The view renders a strip at the top of its content: .tugtile-searchbar is persistent (toggled by .tugtile--searching), .tugtile-promptbar is transient (built on demand for rename / add-lane). Both reuse the .tugtile-prompt-row/ -field/-input styles below, and the JS freezes the board height while they're open so the virtual keyboard can't collapse it into a black gap. On phone they take over the .tugtile__ctlbar slot. */ .tugtile-searchbar { display: none; } .tugtile--searching .tugtile-searchbar, .tugtile-promptbar { flex: 0 0 auto; padding: 4px 8px 8px; background: var(--background-primary); } .tugtile--searching .tugtile-searchbar { display: block; } body.is-phone .tugtile--searching .tugtile__ctlbar, body.is-phone .tugtile--prompting .tugtile__ctlbar { display: none; } .tugtile-prompt-row { display: flex; align-items: center; gap: 8px; } /* The input is wrapped in a rounded "field" so a leading icon (search) can sit inside it */ .tugtile-prompt-field { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; box-sizing: border-box; height: 40px; padding: 0 12px; background: var(--background-modifier-form-field); border: 1px solid var(--background-modifier-border); border-radius: var(--radius-m); overflow: hidden; /* Clip the input to the rounded box so it can never poke out of the corners */ } .tugtile-prompt-field:focus-within { border-color: var(--interactive-accent); } .tugtile-prompt-fieldicon { flex: 0 0 auto; display: inline-flex; align-items: center; color: var(--text-muted); } .tugtile-prompt-fieldicon svg { width: 1.15em; height: 1.15em; } /* The input is our OWN contenteditable element (not a native ), so Obsidian's input chrome never applies — no overrides, no !important. The .tugtile-prompt-field IS the visible box; this is the bare single-line text surface inside it. plaintext-only keeps paste/IME clean (no rich markup, CJK-safe). */ .tugtile-prompt-input { flex: 1; min-width: 0; outline: none; white-space: pre; overflow-x: auto; overflow-y: hidden; /* single line: scroll sideways, never wrap */ font-size: 16px; color: var(--text-normal); /* 16px prevents iOS auto-zoom on focus */ -webkit-user-modify: read-write-plaintext-only; } .tugtile-prompt-input::-webkit-scrollbar { display: none; } /* hide the horizontal scrollbar on the one-line field */ .tugtile-prompt-input:empty::before { content: attr(data-placeholder); color: var(--text-faint); pointer-events: none; /* placeholder text isn't selectable / doesn't block the caret */ } /* .tugtile-prompt-x / -ok inherit from .tugtile-iconbtn (the ✓ colour override lives up there) */ .tugtile-edit-modal-full .modal-close-button { display: none; } /* Hides the native top-right ✕ button in favor of our custom ❌ button with safety checks */ .tugtile-edit-modal { display: flex; flex-direction: column; height: 100%; min-height: 0; padding: 0; background: var(--background-primary); /* Opaque: in the marktile pane there's no modal-bg behind it, so without this the area the iOS keyboard reveals shows through as a black gap */ } /* marktile lives in a leaf, not a modal — fill the pane opaquely and use the dynamic viewport so the editor shrinks WITH the on-screen keyboard instead of leaving a black void below it. */ .workspace-leaf-content[data-type="marktile-editor"] .view-content { padding: 0; background: var(--background-primary); } .workspace-leaf-content[data-type="marktile-editor"] .tugtile-ed-scroll { max-height: 100dvh; } .tugtile-ed-bar { display: flex; align-items: center; gap: 6px; padding: 8px 10px; border-bottom: 1px solid var(--background-modifier-border); } /* ── Single source of truth for EVERY icon button ────────────────────────────────────────────── editor ✕/✓, toolbar tools, find/replace, search ✕/✓. 40×40, flat, non-flex centering. Two hard rules baked in here so they can never drift again and re-break iPad: 1) NO display:flex on the button — iPad WebKit blanks an inline svg that's a flex item. 2) the svg is nested in a (done in JS) — iPad WebKit won't render an svg that's a direct button child. So a size rule on the svg is safe. Per-button classes below only override what's genuinely unique (colour of ✓, hover colour of ✕). */ .tugtile-iconbtn { flex: 0 0 auto; -webkit-appearance: none; appearance: none; position: relative; width: 40px; height: 40px; border: none; border-radius: var(--radius-m); background: transparent; color: var(--text-muted); cursor: pointer; } /* The icon's svg is wrapped in a (the iPad svg-in-button fix). Absolutely-centre that span — true pixel-centre, and NO flex on the button (flex on a