/* 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__add-input, .tugtile__add-ok, .tugtile-prompt-field, .tugtile-iconbtn, .tugtile-ed-find-i, .tugtile__tile-edit, .tugtile__lane-rename, .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) */ .tugtile-leftacts > * { margin: 0 !important; } /* Reset Obsidian's per-action margins so Search/Undo/Redo are evenly spaced and aligned */ /* 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); } } /* iOS26 border radius fix (merged from the old kanban-card-radius snippet): iOS26 stretches the core variable --input-radius into a pill shape, deforming any input box or button using it. We override it locally within tugtile's scope (kanban view and the two modals) instead of applying it globally to the body, keeping this OSS plugin clean. Remove once Obsidian/iOS is fixed. */ .tugtile, .tugtile-edit-modal-full, .tugtile-prompt-modal, .tugtile-archive-modal { --input-radius: var(--radius-m) !important; } /* 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 { 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; } /* 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 { overflow: hidden !important; touch-action: none !important; } /* 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); } .tugtile__add-input { width: 100%; min-height: 4em; resize: vertical; border-radius: var(--radius-m); } .tugtile__add-ok { background: var(--interactive-accent); color: var(--text-on-accent); border: none; border-radius: var(--radius-s); padding: 4px 12px; cursor: pointer; } /* ⋯ 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); } /* Submit button for mobile editing */ .tugtile__edit-actions { display: flex; justify-content: flex-end; margin-top: 6px; } /* 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: 100vh; height: 100dvh; /* Dynamic viewport: shrinks with the on-screen keyboard so the editor stays inside the visible area and the textarea can scroll */ max-height: 100vh; 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:has(.tugtile-edit-modal-full) { padding: 0; align-items: flex-start; } /* Top-pinned, so shrinking the editor to the visual viewport (keyboard) keeps it above the keyboard */ /* 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:has(.tugtile-edit-modal-full) .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); } } /* Small input modal (search / rename / add lane): sit near the top like Spotlight, so the virtual keyboard doesn't cover it. */ .modal-container:has(.tugtile-prompt-modal) { align-items: flex-start; } .tugtile-prompt-modal { margin-top: 10vh; transform-origin: center; animation: tugtile-pop-in 0.4s cubic-bezier(.34, 1.56, .64, 1); /* Make the modal PANEL invisible — we stop fighting its box/padding/alignment and just float the search row (field + buttons, which carry their own backgrounds) over a frosted backdrop. */ background: transparent !important; box-shadow: none !important; border: none !important; } /* Phone: sit below the header toolbar instead of colliding with it (this one was safe; the box/padding overrides that broke the layout — clobbering Obsidian's mobile safe-area — have been removed). */ body.is-phone .tugtile-prompt-modal { margin-top: calc(env(safe-area-inset-top) + 64px); } .tugtile-prompt-modal.tugtile-ed-closing { animation: tugtile-pop-out 0.28s ease-in forwards; } .tugtile-prompt-modal .modal-close-button { display: none; } /* Hides Obsidian's native ✕ button to use our custom ❌ button instead */ /* Our own inner spacing — replaces Obsidian's 16px .modal-content padding. NEVER touch .modal's box: that carries the mobile safe-area insets (overriding it clipped the buttons off-screen). justify-content:center vertically centres the one-line row, so any leftover modal height is balanced top/bottom (no top-heavy gap). */ .tugtile-prompt-modal .modal-content { padding: 8px; justify-content: center; } /* Dim the board behind the search modal (Obsidian forces the prompt backdrop transparent — opacity:0 !important — so without this the board shows at full brightness and crowds the field on a narrow phone). */ .modal-container:has(.tugtile-prompt-modal) .modal-bg { opacity: 1 !important; background-color: rgba(0, 0, 0, 0.4); -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px); } .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 bare — the .tugtile-prompt-field IS the visible box. High-specificity selector + appearance:none + !important on every state to fully strip Obsidian's input chrome (the focus border/box-shadow was bigger than the field and broke the nesting). */ input.tugtile-prompt-input, input.tugtile-prompt-input:hover, input.tugtile-prompt-input:focus, input.tugtile-prompt-input:focus-visible, input.tugtile-prompt-input:active { flex: 1; min-width: 0; width: auto; -webkit-appearance: none; appearance: none; background: transparent !important; border: 0 !important; border-radius: 0 !important; outline: 0 !important; box-shadow: none !important; padding: 0 !important; margin: 0 !important; height: 100%; min-height: 0; font-size: 16px; color: var(--text-normal); /* 16px prevents iOS auto-zoom on focus */ } /* .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