Root cause: bootstrap.php rotates the CSRF token on every successful POST,
but most API endpoints called echo json_encode() directly instead of
apiRespond() — so the rotated token was never returned to the client.
The next POST from the same page sent the now-invalid old token → 403.
Refreshing the page loaded a fresh token, making it work once.
Fixes:
- assign_ticket.php, watch_ticket.php: switch to apiRespond()
- saved_filters.php, user_preferences.php: replace all echo json_encode
calls with apiRespond() (19 and 12 call sites respectively)
- base.js: both apiFetch() and _apiFetchAuth() now update window.CSRF_TOKEN
whenever a response includes a csrf_token field, keeping the client
permanently in sync with server-side rotations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Optimistic locking:
- TicketView now includes updated_at in window.ticketData
- ticket.js saveTicket() sends expected_updated_at on every save so
the server can detect concurrent edits
- On conflict response, shows a clear toast: "ticket was modified by
someone else while you were editing — reload to see latest version"
- On success, syncs window.ticketData.updated_at from server response
so subsequent saves use the correct lock key
- update_ticket.php now returns updated_at in success response
Visibility audit log:
- updateVisibility() result is now checked; on success, logs a delta
entry to the audit trail with from/to visibility and groups so the
timeline shows who changed visibility and when
Full ticket export:
- export_tickets.php now accepts format=full with a single ticket_id
- Produces a JSON file containing ticket fields, flat comment list
(with author, timestamps, text), and the full audit timeline
- Access-controlled: respects canUserAccessTicket() before exporting
- EXPORT button added to ticket toolbar linking directly to the endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug fixes:
- bulk-delete action called undefined bulkDelete() — wired to the
existing showBulkDeleteModal() so the confirmation modal actually shows
UX:
- Template loader now checks for existing title/description and asks
for confirmation before overwriting user-typed content
- Visibility select shows a dynamic hint paragraph that updates when
the user changes the selection (public/internal/confidential)
Architecture:
- TICKET_STATUSES added to config as single source of truth; all
hardcoded ['Open','Pending','In Progress','Closed'] arrays in
DashboardView now read from config; bulk-status modal in dashboard.js
reads window.TICKET_STATUSES (set from PHP) with array fallback
- ASSET_VERSION now auto-computed from max mtime of dashboard/ticket
CSS+JS files so browsers always pick up changes on deploy; manual
override still available via ASSET_VERSION in .env
- Removed 10 dead standalone stat methods from StatsModel (getOpenTicketCount,
getClosedTicketCount, getTicketsByPriority, etc.) — all superseded by
the consolidated fetchAllStats() queries, never called externally
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P1-A: Fix CSP - add fonts.googleapis.com to style-src, fonts.gstatic.com to font-src
P1-B: CSRF token rotation - add rotateToken() to CsrfMiddleware; bootstrap.php rotates
after successful validation and stores in $GLOBALS['_new_csrf_token']; add
apiRespond() helper to append token to responses; lt.api interceptor in
layout_footer.php auto-updates window.CSRF_TOKEN from responses
P1-C: Styled 403/404 error views with TDS layout instead of raw text; index.php now
uses requireAdmin() helper eliminating 7 duplicated guard blocks (P3-D)
P2-A: Remove duplicate JS-generated keyboard help modal from keyboard-shortcuts.js;
'?' key now routes to static #lt-keys-help modal in footer
P2-B: Asset versioning driven by config ASSET_VERSION key; base.css and base.js get
?v= cache-busting in layout_header.php
P2-C: Add data-theme="dark" to <html> tag to prevent FOUC on light-mode users
P2-E: Escape status value in dashboard.js hover preview class attribute via lt.escHtml()
P2-F: Replace bespoke showLoadingOverlay() with lt-spinner / lt-loading-text from
base.css; add .lt-loading-overlay wrapper CSS to dashboard.css
P2-G: Add keyboard-shortcuts.js to all 7 admin views so J/K nav and ? help work
P3-A: APP_NAME, APP_SUBTITLE, APP_VERSION driven from config.php; layout header/footer
use config values instead of hardcoded strings
P3-G: Replace custom initTableSorting() with lt.sortTable.init() which manages aria-sort
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- bulk_operation.php: replace is_numeric() with strict int cast+equality to reject scientific notation
- AttachmentModel.php: fix bind_param type strings (s→i for integer ticket IDs)
- CommentModel.php: use strict !== comparison with (int) cast for user_id ownership checks
- ticket.js: replace all non-TDS class names (text-amber→lt-text-amber, btn→lt-btn variants, etc.)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CSS:
- ticket.css: use combined .comment.thread-depth-N selectors to resolve the
margin-left conflict between .comment-reply and .thread-depth-N classes
dashboard.js:
- Remove legacy initStatusFilter() (superseded by TDS v1.2 sidebar filters)
- Remove initTableSorting() call (client-side sort conflicts with server ?sort=)
- Remove quickSave() + saveTicket() (old hamburger-menu ticket page functions)
- Remove global loadTemplate() (duplicate of IIFE-scoped version in CreateTicketView)
- Remove generateSkeletonRows/Comments/Stats helpers (never called, used
unregistered CSS class names like .skeleton-row-tr)
- Remove "force dark mode" lines that overrode the user theme preference
- Fix non-TDS CSS classes in modal templates: text-center → style, text-green →
lt-text-cyan, mb-half → lt-mb-xs, modal-warning-text → lt-text-danger
Admin views:
- RecurringTicketsView: replace innerHTML += loop with createElement/appendChild
(avoids serial DOM re-parsing on each iteration)
- AuditLogView: add htmlspecialchars() to action_type option values (consistency)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
submitReply() built a replyDiv.innerHTML template literal using
data.user_name (API response) without escaping — an attacker-controlled
display name could inject arbitrary HTML. Fix: wrap all API-sourced
string values in lt.escHtml() within the template.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
showReplyForm() read userName from data-user attribute (decoded by
the browser from HTML entities) and injected it unsanitized into
insertAdjacentHTML() — any HTML special chars would be parsed as markup.
Fix: wrap with lt.escHtml() before interpolation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- base.css: add .lt-main.lt-container combined selector (specificity 0,2,0)
to prevent responsive .lt-container padding shorthand from overriding
the fixed-header clearance padding-top — affected all viewports < 1280px
- base.css: add .is-hidden { display: none !important } globally; it was
only defined in ticket.css so dashboard ticketPreview popup rendered
as a green box at 0,0 on page load instead of being hidden
- CreateTicketView.php: add dashboard.css to pageStyles so create-ticket-
meta-grid, lt-form-hint, visibility-groups-list, duplicate-list classes
are available (they were undefined when only ticket.css was loaded)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- base.css: add --lt-border/--lt-surface aliases so dashboard.css respects
theme instead of using hardcoded fallback colors
- base.css: add lt-select-sm/lt-input-sm compact size variants (used in 15+
places), lt-msg-danger alias for lt-msg-error, lt-form-hint--warn,
lt-font-mono utility class
- audit_log.php: cap ?limit= at 500 to prevent DoS via oversized queries
- ApiKeysView.php: replace deprecated execCommand('copy') with lt.copy();
add integer casts on api_key_id in id attr and data-id
- AuditLogView.php: rebuild pagination with windowed prev/next/ellipsis
pattern matching DashboardView; integer cast on user_id select option
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- base.css: add width:100%+min-width:0 to .lt-main so flex column body
doesn't shrink content due to margin:0 auto from .lt-container
- layout_header.php: restructure mobile nav drawer to match web_template
exactly (nav-drawer-links nav, direct <a> links, section div, no ul/li
wrapper, overlay after drawer); fix lt-nav-overlay id mismatch with
base.js; rename lt-header-username -> lt-header-user (matches CSS);
add JSON_HEX_TAG to all inline json_encode calls (closes </script> XSS)
- base.css: add lt-kv-row/label/value aliases (display:contents pattern
used in web_template v1.2 kv-grid); add lt-badge-sm variant
- Admin views: add missing .catch() on editField/editRecurring/loadUsers;
add JSON_HEX_TAG to json_encode in TemplatesView/WorkflowDesignerView
- TicketView: add JSON_HEX_TAG to all ticket-data json_encode calls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- layout_footer.php: add lt-footer with context-sensitive keyboard hint bar
([ ~ ] HOME | [ / ] SEARCH | [ + ] NEW | [ * ] CFG | [ ? ] HELP)
Context adapts for dashboard, ticket, and admin pages
- layout_footer.php: wire show-keyboard-help and open-settings for all pages
- base.css: body { display:flex; flex-direction:column } + lt-main { flex:1 }
so footer sticks to bottom of viewport on short pages
- base.css: add lt-flex-gap-xs/sm/md/lg and lt-flex-align-start/center/end
(were used across all views but never defined — causing broken layouts)
- base.css: add --lt-danger/amber/cyan/success/text-primary CSS variables
(referenced in ticket.css and dashboard.css fallbacks but never declared)
- base.css: add lt-text-danger/warning/success/info/primary utility classes
(used in TicketView, DashboardView, admin views but not defined in base.css)
- DashboardView.php: remove ascii-banner.js (loaded but never called)
- TemplatesView.php: fix priority badge from lt-p* to lt-chip component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full application redesign using Terminal Design System v1.2 (lt-* class
system). Introduces shared layout_header/footer partials, upgrades
base.css/base.js to TDS v1.2, and rewrites all views (Dashboard, Ticket,
CreateTicket, and all 7 admin views) with lt-frame, lt-table, lt-modal,
lt-stats-grid, lt-kv-grid, and data-action event delegation patterns.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DashboardView.php:
- Table status column: replace status-{slug} with lt-status lt-status-{slug} for consistent [● Status] bracket decoration from base.css
- Table priority column: replace raw number with lt-priority lt-p{N} empty span for [▲▲ P1 CRITICAL] style badges
dashboard.js:
- Kanban card priority badge: replace card-priority p{N} with lt-priority lt-p{N} to use the design system badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ticket.js: replace custom formatFileSize() with lt.bytes.format() from web_template base.js; remove the now-redundant local function
- DashboardView.php: add id="tickets-table" and wire lt.tableNav.init() for j/k/Enter keyboard row navigation
- DashboardView.php: add lt-stat-card class + data-filter-key/data-filter-val to open/critical/closed stat cards; wire lt.statsFilter.init() + window.lt_onStatFilter so clicking a stat card filters the ticket list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
utils.js is loaded on all pages (dashboard, ticket, admin views) before dashboard.js.
Moving the canonical definition there and removing the guard + the copy in dashboard.js
eliminates the redundant redefinition on every page load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Security fixes:
- add_comment.php: verify canUserAccessTicket() before allowing comment creation
- assign_ticket.php: use canUserAccessTicket() to prevent info leakage via 403 vs 404
- check_duplicates.php: apply getVisibilityFilter() so confidential ticket titles are not exposed in duplicate search results
- ticket_dependencies.php: verify ticket access on GET before returning dependency data
Route registration:
- Register 7 previously missing API endpoints in index.php: custom_fields, saved_filters, audit_log, user_preferences, download_attachment, clone_ticket, health
Frontend:
- ticket.js: fill empty catch block and empty else block in addComment() with proper error toasts
Documentation:
- README.md: document all API endpoints and update project structure listing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace style.display with .is-hidden classList in ApiKeysView, CustomFieldsView, RecurringTicketsView
- Convert boot overlay fade-out from style.opacity to .boot-overlay--fade-out CSS class
- Add .boot-overlay--fade-out rule to dashboard.css
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch tab show/hide from style.display to .tab-content.active CSS class
- Convert visibilityGroupsField, markdownPreview, uploadProgress to use .is-hidden class
- Replace comment text div style.display with classList.add/remove('is-hidden')
- Add .is-hidden utility class to ticket.css
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace stat-card cursor:pointer inline style with CSS rule
- Convert view toggle (table/card) to use .is-hidden CSS class
- Convert bulk-actions and export-dropdown to use .is-visible class
- Add .is-hidden/.is-visible utility rules to dashboard.css
- Remove duplicate lt.keys.initDefaults() call from dashboard.js
- Remove redundant setTimeout from view mode restore
- Add lt.keys.initDefaults() to dashboard.js (was missing entirely)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace all 8 showToast() calls in ApiKeysView.php with lt.toast.*
— all toast calls in the codebase now use lt.toast directly
- Add .duplicate-list, .duplicate-meta, .duplicate-hint CSS classes to
dashboard.css; replace inline styles in duplicate detection JS with them
- Add dashboardAutoRefresh() using lt.autoRefresh — reloads page every
5 minutes, skipping if a modal is open or user is typing in an input
- Add REFRESH button to dashboard header that triggers lt.autoRefresh.now()
for immediate manual refresh with timer restart
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add .inline-error and .inline-warning utility classes to dashboard.css
with correctly-matched terminal palette rgba values (replaces off-palette
rgba(231,76,60,0.1) and rgba(241,196,15,0.1))
- Add .key-generated-alert class for the new API key display frame
- Add base .dependency-item, .dependency-group h4, .dependency-item a,
.dependency-title, .btn-small overrides to ticket.css
- Remove all inline styles from the dependency list template in ticket.js
— layout, colors, and sizing now come from CSS classes
- Update CreateTicketView.php and ApiKeysView.php to use the new classes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add data-ts attributes to TicketView.php: ticket created/updated
header, comment dates (inner span to preserve edited indicator),
and all activity timeline dates
- Add initRelativeTimes() to ticket.js using lt.time.ago(); runs on
DOMContentLoaded and every 60s to keep relative times current
- Attachment dates now use lt.time.ago() with full date in title attr
and ts-cell span for periodic refresh
- Replace all 11 showToast() calls in ticket.js with lt.toast.* directly,
removing reliance on the backwards-compat shim for these paths
- Add span.ts-cell and td.ts-cell CSS to both dashboard.css and ticket.css:
dotted underline + cursor:help signals the title tooltip is available
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add data-ts attributes to table and card view date cells so JS can
convert them to relative time ("2h ago") while keeping the full date
in the title attribute for hover tooltips
- Add initRelativeTimes() in dashboard.js using lt.time.ago(); runs on
DOMContentLoaded and refreshes every 60s so times stay current
- Fix table sort for date columns to read data-ts attribute instead of
text content (which is now relative and not sortable as a date)
- Update README: add base.css/base.js/utils.js to project structure,
fix ascii-banner.js description, expand keyboard shortcuts table,
add developer notes for lt.time and boot sequence behavior
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove collapsible ASCII banner from dashboard (was cluttering the UI)
- Show ASCII banner in the boot overlay on first session visit, above
the boot messages, with a 400ms pause before messages begin
- Add scroll fade indicator (green-tinted gradient edges) to .table-wrapper
so users can see when the table is horizontally scrollable
- Fix null guards for tab switcher in ticket.js (tabEl, activeBtn)
- Fix Reset → RESET uppercase in AuditLogView and UserActivityView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Set .table-wrapper to overflow-x: auto with touch scrolling support
- Add min-width: 900px to table to trigger scroll before columns collapse
- Set .ascii-frame-outer overflow-x: visible to avoid clipping conflict
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CSS fixes:
- Fix [ ] brackets appearing below button text by replacing display:inline-flex
with display:inline-block + white-space:nowrap on .btn — removes cross-browser
flex pseudo-element inconsistency as root cause
- Remove conflicting .btn::before ripple block (position:absolute was overriding
bracket content positioning)
- Remove overflow:hidden from .btn which was clipping bracket content
- Fix body::after duplicate rule causing GPU layer blink (second position:fixed
rule re-created compositor layer, overriding display:none suppression)
- Replace all transition:all with scoped property transitions in dashboard.css,
ticket.css, base.css (prevents full CSS property evaluation on every hover)
- Convert pulse-warning/pulse-critical keyframes from box-shadow to opacity
animation (GPU-composited, eliminates CPU repaints at 60fps)
- Fix mobile *::before/*::after blanket content:none rule — now targets only
decorative frame glyphs, preserving button brackets and status indicators
- Remove --terminal-green-dim override that broke .lt-btn hover backgrounds
JS fixes:
- Fix all lt.lt.toast.* double-prefix instances in dashboard.js
- Add null guard before .appendChild() on bulkAssignUser select
- Replace all remaining emoji with terminal bracket notation (dashboard.js,
ticket.js, markdown.js)
- Migrate all toast.*() shim calls to lt.toast.* across all JS files
View fixes:
- Remove hardcoded [ ] brackets from .btn buttons (CSS now adds them)
- Replace all emoji with terminal bracket notation in all views and admin views
- Add missing CSP nonces to AuditLogView.php and UserActivityView.php script tags
- Bump CSS version strings to ?v=20260319b for cache busting
Security fixes:
- update_ticket.php: add authorization check (non-admins can only edit their own
or assigned tickets)
- add_comment.php: validate and cast ticket_id to integer with 400 response
- clone_ticket.php: fix unconditional session_start(), add ticket ID validation,
add internal ticket access check
- bulk_operation.php: add HTTP 401/403 status codes on auth failures
- upload_attachment.php: fix missing $conn arg in AttachmentModel constructor
- assign_ticket.php: add ticket existence check and permission verification
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rules: transform/opacity = GPU composited (fine). box-shadow/text-shadow on
hover = CPU repaint (removed). Static box-shadow/text-shadow = painted once (fine).
- Buttons (.btn, .btn-base, button, .btn-primary): add will-change:transform
for pre-promotion, add transform:translateY(-1px) on hover (GPU, no repaint),
scope transition to include transform, remove box-shadow/text-shadow from hover
- Stat cards: add will-change:transform, add transform:translateY(-2px) on hover
- Priority badges: replace filter:blur(6px) ::after pseudo-element (permanent GPU
layer per badge, ~20 on screen at once) with static box-shadow:0 0 6px currentColor
on the badge itself — painted once, never changes, zero compositor overhead
- Links: replace opacity-transition ::after underline (lazy GPU layer creation on
hover) with text-decoration:underline on hover (pure CPU paint, no GPU layer)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Chrome promotes ALL position:fixed elements to GPU compositing layers for scroll
performance, regardless of whether they have animations. The body::before scanline
overlay (position:fixed, z-index:9999, full-viewport) and body::after watermark
(position:fixed) were both on GPU layers. Every CPU repaint from any hover state
change required a compositor re-blend pass → one-frame blink at compositor sync.
Fixes:
- Move scanlines from body::before (position:fixed) into body { background-image }
— same visual, no separate element, no GPU layer promotion
- Set body::before { display:none } and body::after { display:none } in both
dashboard.css and base.css
- Remove animation:matrix-rain from .stat-card:hover::before — background-position
animation is not GPU-composited, caused CPU repaints every frame while hovered
plus GPU texture uploads when animation started/stopped on cursor enter/exit
- Scope a { transition: all } → transition: color in base.css
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: removing 'animation' from dashboard.css body::before did NOT disable
the scanline — it just stopped overriding base.css which still had
'animation: scanline 8s linear infinite'. CSS cascade means the base.css value
remained active. Fixed by setting 'animation: none' explicitly in dashboard.css.
Also fix base.css (used by all pages including ticket page):
- Set body::before animation: none (removes GPU compositing layer from scanline)
- Change corner-pulse/subtle-pulse/pulse-glow/pulse-red keyframes from text-shadow
and box-shadow animations to opacity (GPU composited, zero CPU repaint overhead)
- Change exec-running-pulse from box-shadow to opacity
- Remove box-shadow from .lt-table tr:hover, .lt-card:hover, .lt-stat-card:hover
- Remove text-shadow/box-shadow/transform from .lt-btn:hover and variants
- Remove text-shadow from a:hover
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Change pulse-glow keyframes from text-shadow animation to opacity (GPU composited,
eliminates 60fps CPU repaint that was the likely root cause of the persistent blink)
- Remove box-shadow from .quick-action-btn:hover; scope transition: all → background/color
- Remove box-shadow + background gradient + transform:translateY from .stat-card:hover;
scope transition: all → border-color only
- Remove .stat-card::after transition and hover background change
- Remove duplicate .stat-card:hover transform:translateY block
- Remove box-shadow from .clear-search-btn:hover; scope transition: all → background/color
- Remove text-shadow from th transition (th:hover never changes text-shadow)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove body::before scanline animation (transform: translateY promoted it to a
GPU compositing layer; CPU repaints from hover states required compositor re-blend,
causing one-frame blink at compositor sync boundary)
- Remove text-shadow and transform: translateY(-2px) from .btn-primary:hover/.create-ticket:hover
- Scope .btn-primary transition from 'all' to specific composited properties
- Remove box-shadow: inset from .banner-toggle:hover
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These non-composited properties force CPU repaints of the surrounding
paint region (the section itself) every time hover enters or exits.
With the fixed overlay (body::before scanline), each such repaint
requires the compositor to re-blend the layer, visible as a blink.
Removed from dashboard.css:
- btn/button/btn-base:hover: box-shadow + text-shadow
- th:hover: text-shadow
- ticket-link:hover: text-shadow
- pagination button:hover: box-shadow + transform + transition:all
- ticket-card-row:hover: box-shadow + transition:all -> background only
- .btn ripple rule: transition:all -> specific properties
- ascii-frame-outer: removed will-change/translateZ (GPU upload worse)
Removed from ticket.css:
- metadata-select:hover: box-shadow; transition:all -> border-color
- comment:hover: box-shadow
- btn:hover: box-shadow
- mention:hover: text-shadow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The body::before scanline overlay (position:fixed, z-index:9999) requires
the compositor to re-blend over the section every time a CPU repaint
happens inside it. Hover state entry/exit triggers these repaints, causing
a visible blink as the compositor flushes.
Fixes:
- Add will-change:transform + transform:translateZ(0) to ascii-frame-outer
to promote it to its own GPU compositing layer, isolating its repaints
from the scanline compositing pass
- Convert corner-pulse and subtle-pulse from text-shadow (CPU repaint)
to opacity (GPU composited) to eliminate continuous repaint pressure
inside the section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Four root causes removed:
- body { transition: all } — forced browser to check all CSS properties
on every hover event across the entire page
- a:not(.btn)::after underline: width+box-shadow transition replaced with
opacity transition — width repaints paint layer, box-shadow forced parent
section repaint; opacity is GPU-composited and doesn't repaint ancestors
- .ticket-link:hover { transform: translateX } — created/destroyed GPU
compositor layer on every ticket ID hover; removed, scoped transition
to specific non-layout properties
- .btn:hover { transform: translateY } in ticket.css — same layer issue
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JS mouseenter/mouseleave handlers were setting row.style.backgroundColor
inline, fighting with the CSS tr:hover rule. On mouseleave both fired
simultaneously causing a double repaint / blink. Removed the redundant
JS handlers — the CSS tr:hover transition already handles this cleanly.
Also removed body flicker animation from base.css (was still present
after being removed from dashboard.css).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove pulse-glow-box animation and translateY from button:hover
(infinite animation stopping abruptly caused a flash on mouse-leave)
- Scope button transition from 'all' to specific visual properties
(prevents transform/layout changes from triggering on hover exit)
- Scope th transition from 'all' to background-color + text-shadow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove .ascii-frame-outer:hover flicker animation (caused article to
shake/blink every time cursor entered the ticket container)
- Remove body flicker animation (caused full page blink every 30s)
- Remove deploy.sh (deployment now handled by Gitea CI/CD)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>