Commit Diff


commit - 1f11edf54013c82402ffbc058de59e9668c52d10
commit + 20f66d3e663eca1889354da880458264430f4296
blob - /dev/null
blob + bf73340068cfa7919881cbe6ec81ab3a04d9d18a (mode 644)
Binary files /dev/null and assets/left.webp differ
blob - /dev/null
blob + 5ca9fd161b584d7e202af278ce91fe1a28ea0744 (mode 644)
Binary files /dev/null and assets/right.webp differ
blob - d9fb2294a30528aac890909fc858c5f7f6cecf3c
blob + 21f0426df1c2b249c91f69dcf4278280ed828cf2
--- flake.lock
+++ flake.lock
@@ -51,11 +51,11 @@
     },
     "nixpkgs_2": {
       "locked": {
-        "lastModified": 1783915482,
-        "narHash": "sha256-FmieJB8/OUvNxbkboi7+IGfIuSXY3nF/hZQm8kD0r50=",
+        "lastModified": 1785602060,
+        "narHash": "sha256-z7D96eESRM4CPV/XtwpwFn8IDdfLAmxz6lVWrGYXvR4=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "6cdc7fc76e8bf7fde9fa43a849fcaaa70e230dee",
+        "rev": "a5cbcfe954791221bfffe2307f7d1a1bf61a871e",
         "type": "github"
       },
       "original": {
blob - fa4250e86cb91b61f1403c67598d0f928f29971f
blob + 7eb18dc8f5f34813c0ceca1d765a862f5df6c185
--- src/Main.purs
+++ src/Main.purs
@@ -108,6 +108,10 @@ routeRequest env contexts req url path allUsers res = 
     serveAsset "image/png" "assets/favicon.png" res
   "/cover.webp" ->
     serveAsset "image/webp" "assets/cover.webp" res
+  "/gutter-left.webp" ->
+    serveAsset "image/webp" "assets/left.webp" res
+  "/gutter-right.webp" ->
+    serveAsset "image/webp" "assets/right.webp" res
   "/" ->
     serveIndex regEnabled allUsers "" res
   "/register" ->
blob - 161c64f63caa4c654cf4e243905cfea12725b77e
blob + d7b603af4cde642bc457bef5c84c662913e9fa2f
--- src/Registrations.purs
+++ src/Registrations.purs
@@ -48,6 +48,8 @@ reservedSlugs =
   , "client.js"
   , "favicon.png"
   , "cover.webp"
+  , "gutter-left.webp"
+  , "gutter-right.webp"
   , "cover"
   , "metrics"
   , "proxy"
blob - 1f99cfd0d81d875b4ed6319d26fca553ed0891cd
blob + e019ebda3e58402c9a341929d6b9b9af09bb263f
--- src/State.elm
+++ src/State.elm
@@ -49,7 +49,6 @@ init flags url navKey =
       , allUsers = flags.allUsers
       , searchInput = ""
       , activeSearch = Nothing
-      , tabsVisible = True
       , page = page
       , registrationEnabled = flags.registrationEnabled
       , regSlug = ""
@@ -127,8 +126,12 @@ update msg model =
         UrlRequested urlRequest ->
             case urlRequest of
                 Browser.Internal url ->
-                    ( model, Nav.pushUrl model.navKey (Url.toString url) )
+                    if String.isEmpty url.path then
+                        ( model, Cmd.none )
 
+                    else
+                        ( model, Nav.pushUrl model.navKey (Url.toString url) )
+
                 Browser.External href ->
                     ( model, Nav.load href )
 
@@ -190,8 +193,11 @@ update msg model =
 
                             else
                                 targetOffset
+
+                        shouldLoad =
+                            model.page /= MainPage || userChanged || model.offset /= finalOffset || model.activeFilter /= finalFilter || model.activeSearch /= finalSearch
                     in
-                    if model.page /= MainPage || userChanged || model.offset /= finalOffset || model.activeFilter /= finalFilter || model.activeSearch /= finalSearch then
+                    if shouldLoad then
                         let
                             newModel =
                                 { model
@@ -227,7 +233,7 @@ update msg model =
                         )
 
                     else
-                        ( { model | activeTab = ListensTab, page = MainPage }, Cmd.none )
+                        ( { model | page = MainPage }, Cmd.none )
 
         Tick time ->
             if model.page == MainPage && not model.loading then
@@ -339,9 +345,6 @@ update msg model =
                     Cmd.none
             )
 
-        ToggleTabs ->
-            ( { model | tabsVisible = not model.tabsVisible }, Cmd.none )
-
         SetStatsPeriod period ->
             ( { model
                 | statsPeriod = period
@@ -492,6 +495,9 @@ update msg model =
         UnhoverCover ->
             ( { model | hoveredCover = Nothing }, Cmd.none )
 
+        CoverBackgroundClick ->
+            ( { model | hoveredCover = Nothing }, Cmd.none )
+
         ExpandSection section ->
             ( { model | expandedSections = Set.insert section model.expandedSections }, Cmd.none )
 
blob - a77bc9f485b85c13813cc966cb16a396212d47b6
blob + 39c343b9a4af4e29e2d61256a1efd6f142255451
--- src/Templates.purs
+++ src/Templates.purs
@@ -21,285 +21,481 @@ indexHtml registrationEnabled userSlug allUsers =
     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
     <link href="https://fonts.googleapis.com/css2?family=Intel+One+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
     <style>
+        :root {
+            color-scheme: dark;
+            --bg: #212529;
+            --bg-alt: #2b3035;
+            --bg-dark: #343a40;
+            --bg-inactive: #343a40;
+            --fg: #ced4da;
+            --fg-alt: #6c757d;
+            --fg-alt-dark: #adb5bd;
+            --fg-light: #495057;
+            --fg-intense: #f8f9fa;
+            --highlight: #2b375c;
+            --cyan: #9bf6ff;
+            --red: #ffadad;
+            --red-light: #4a2f33;
+            --yellow: #fdffb6;
+            --green: #caffbf;
+            --green-light: #2f4a35;
+            --blue: #9bb1ff;
+            --purple: #ffc6ff;
+            --border: var(--fg-light);
+            --space-xs: 0.25em;
+            --space-sm: 0.4em;
+            --space-md: 0.6em;
+            --space-lg: 1em;
+            --window-pad: 0.5em 1.15em;
+            --gutter: 25%;
+            --radius: 0;
+        }
+
+        * {
+            box-sizing: border-box;
+        }
+
+        html, body {
+            height: 100%;
+        }
+
         body {
             font-family: 'Intel One Mono', 'Courier New', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
-            background: oklch(0.194 0.0345 225.31);
-            color: oklch(0.91 0.012 225.31);
+            font-size: 16px;
+            line-height: 1.5;
+            background-color: var(--bg);
+            color: var(--fg);
             margin: 0;
-            line-height: 1.6;
+            padding: 0 var(--gutter);
         }
 
-        ::selection {
-            background: oklch(0.42 0.04 225.31);
-            color: oklch(0.91 0.012 225.31);
-        }
-
+        /* The app window: inset from the gutters so content never sits on an edge. */
         .container {
-            max-width: 800px;
-            margin: 0 auto;
-            padding: 24px 20px;
+            padding: var(--window-pad);
         }
 
+        /* Artwork filling the left and right gutters, pinned to the viewport. */
+        body::before,
+        body::after {
+            content: "";
+            position: fixed;
+            top: 0;
+            bottom: 0;
+            width: var(--gutter);
+            background-position: center;
+            background-repeat: no-repeat;
+            background-size: cover;
+            pointer-events: none;
+            z-index: -1;
+        }
+
+        body::before {
+            left: 0;
+            background-image: url("/gutter-left.webp");
+        }
+
+        body::after {
+            right: 0;
+            background-image: url("/gutter-right.webp");
+        }
+
+        /* Narrow viewports can't spare 30% a side; fall back to a flat gutter. */
+        @media (max-width: 60em) {
+            :root {
+                --gutter: var(--space-lg);
+            }
+
+            body::before,
+            body::after {
+                display: none;
+            }
+        }
+
+        a {
+            color: var(--blue);
+            text-decoration: none;
+        }
+
+        a:hover {
+            text-decoration: underline;
+        }
+
+        a:visited {
+            color: var(--purple);
+        }
+
         h1 {
-            color: oklch(0.91 0.012 225.31);
-            margin: 0 0 20px 0;
-            font-size: 24px;
-            border-bottom: 2px solid oklch(0.42 0.04 225.31);
-            padding-bottom: 8px;
+            font-size: 1.25em;
+            margin: 0 0 var(--space-xs) 0;
+            color: var(--fg);
         }
 
-        h1.site-header {
-            border-bottom: none;
-            padding-bottom: 0;
-            line-height: 0;
+        h2 {
+            font-size: 1em;
+            margin: var(--space-md) 0 var(--space-sm) 0;
+            color: var(--fg);
+        }
+
+        header {
+            margin-bottom: var(--space-md);
+            border-bottom: solid 0.1em var(--border);
+            padding-bottom: var(--space-sm);
+        }
+
+        .bar {
+            display: flex;
+            flex-wrap: wrap;
+            gap: var(--space-sm);
+            align-items: center;
+            margin-bottom: var(--space-md);
+        }
+
+        input,
+        button,
+        .tab-btn,
+        .tab-btn:visited {
+            background-color: var(--bg-alt);
+            color: var(--fg);
+            border: solid 0.1em var(--border);
+            padding: var(--space-xs) var(--space-sm);
+            font: inherit;
+            border-radius: var(--radius);
+            line-height: 1.4;
+        }
+
+        input {
+            flex: 1;
+            min-width: 12em;
+        }
+
+        input::placeholder {
+            color: var(--fg-alt);
+        }
+
+        input:focus {
+            outline: none;
+            border-color: var(--cyan);
+        }
+
+        button,
+        .tab-btn {
             cursor: pointer;
+            text-decoration: none;
+            display: inline-flex;
+            align-items: center;
+            gap: var(--space-xs);
         }
 
-        h1.site-header img {
-            display: block;
+        button:hover,
+        .tab-btn:hover {
+            background-color: var(--highlight);
+            border-color: var(--cyan);
+            text-decoration: none;
+        }
+
+        button:disabled {
+            opacity: 0.5;
+            cursor: not-allowed;
+        }
+
+        button.active,
+        button.on,
+        .tab-btn.active,
+        .tab-btn.on {
+            background-color: var(--cyan);
+            color: var(--bg);
+            border-color: var(--cyan);
+        }
+
+        button .n,
+        .tab-btn .n {
+            color: var(--fg-alt);
+        }
+
+        button.active .n,
+        button.on .n,
+        .tab-btn.active .n,
+        .tab-btn.on .n {
+            color: var(--bg);
+        }
+
+        table {
+            border-collapse: collapse;
             width: 100%;
-            height: 80px;
-            object-fit: cover;
-            border-radius: 4px;
-            border: 1px solid oklch(0.42 0.04 225.31);
+            border: solid 0.1em var(--border);
         }
 
-        ul {
-            list-style: none;
+        th,
+        td {
+            border: solid 0.1em var(--border);
+            padding: var(--space-sm) var(--space-md);
+            text-align: left;
+            vertical-align: top;
+        }
+
+        th {
+            background-color: var(--bg-dark);
+            color: var(--fg);
+            cursor: pointer;
+            user-select: none;
+            position: sticky;
+            top: 0;
+            white-space: nowrap;
+        }
+
+        th:hover {
+            background-color: var(--highlight);
+        }
+
+        tbody tr:nth-child(even) {
+            background-color: var(--bg-alt);
+        }
+
+        tbody tr:hover {
+            background-color: var(--highlight);
+        }
+
+        .d {
+            color: var(--fg-alt);
+            white-space: nowrap;
+            width: 1%;
+        }
+
+        .track-cell {
+            position: relative;
+        }
+
+        .track-line {
+            display: flex;
+            flex-wrap: wrap;
+            align-items: baseline;
+            gap: var(--space-sm);
+        }
+
+        .track-link,
+        .artist-link,
+        .album-link,
+        .label-link {
+            background: none;
+            border: none;
             padding: 0;
-            margin: 0 0 20px 0;
+            color: var(--fg);
+            font: inherit;
+            cursor: pointer;
+            text-align: left;
+            user-select: text;
         }
 
-        li {
-            background: oklch(0.27 0.038 225.31);
-            border: 1px solid oklch(0.42 0.04 225.31);
-            border-radius: 4px;
-            padding: 5px 15px 5px 15px;
-            margin-bottom: 5px;
-            display: flex;
-            justify-content: space-between;
-            align-items: center;
-            box-shadow: 4px 4px 0px oklch(0.42 0.04 225.31);
-        }
-
-        li.success {
-            background: oklch(0.27 0.038 225.31);
-            border-color: oklch(0.42 0.04 225.31);
-            box-shadow: 4px 4px 0px oklch(0.42 0.04 225.31);
-        }
-
-        .track-info {
-            flex: 1;
-        }
-
-        .track-name {
-            font-weight: bold;
-            font-size: 16px;
-            color: oklch(0.91 0.012 225.31);
-        }
-
-        .track-artist {
-            font-size: 14px;
-            color: oklch(0.70 0.035 225.31);
-            margin-top: 1px;
-        }
-
-        .track-time {
-            font-size: 12px;
-            color: oklch(0.74 0.075 225.31);
-            margin-top: 2px;
-        }
-
         .track-link {
-            background: none;
-            border: none;
-            padding: 0;
-            color: oklch(0.91 0.012 225.31);
             font-weight: bold;
-            font-size: 16px;
-            font-family: inherit;
-            cursor: pointer;
-            text-align: left;
-            user-select: text;
         }
 
-        .artist-link {
-            background: none;
-            border: none;
-            padding: 0;
-            color: oklch(0.70 0.035 225.31);
-            font-size: 14px;
-            font-family: inherit;
-            cursor: pointer;
-            text-align: left;
-            user-select: text;
+        .artist-link,
+        .album-link,
+        .label-link {
+            color: var(--fg-alt);
         }
 
-        .album-link {
-            background: none;
-            border: none;
-            padding: 0;
-            color: oklch(0.74 0.075 225.31);
-            text-decoration: underline;
-            font-family: inherit;
-            font-size: inherit;
-            cursor: pointer;
-        }
-
-        .album-link:hover {
-            color: oklch(0.78 0.13 357.86);
-        }
-
+        .album-link,
         .label-link {
-            background: none;
-            border: none;
-            padding: 0;
-            color: oklch(0.74 0.075 225.31);
             text-decoration: underline;
-            font-family: inherit;
-            font-size: inherit;
-            cursor: pointer;
         }
 
+        .track-link:hover,
+        .artist-link:hover,
+        .album-link:hover,
         .label-link:hover {
-            color: oklch(0.78 0.13 357.86);
+            color: var(--blue);
         }
 
+        .track-artist {
+            margin-top: var(--space-xs);
+        }
+
+        .track-meta {
+            display: flex;
+            flex-wrap: wrap;
+            align-items: baseline;
+            gap: var(--space-sm);
+            margin-top: var(--space-xs);
+            color: var(--fg-alt);
+            font-size: 0.9em;
+            position: relative;
+            padding-right: 8em;
+        }
+
+        .track-release {
+            color: var(--fg-alt);
+        }
+
+        .genre-tag {
+            display: inline-block;
+            font-size: 0.85em;
+            color: var(--green);
+            position: absolute;
+            bottom: 0;
+            right: 0;
+        }
+
+        .cover-cell {
+            width: 76px;
+            padding: var(--space-sm);
+            vertical-align: middle;
+        }
+
+        .cover-cell .track-cover {
+            width: 60px;
+            height: 60px;
+            object-fit: cover;
+            border: solid 0.1em var(--border);
+            background: var(--bg-alt);
+            cursor: pointer;
+            display: block;
+        }
+
+        .cover-wrapper {
+            display: none;
+        }
+
+        .cover-overlay {
+            position: fixed;
+            inset: 0;
+            background-color: rgba(0, 0, 0, 0.75);
+            z-index: 90;
+        }
+
         .track-cover {
             width: 60px;
             height: 60px;
-            border-radius: 4px;
             object-fit: cover;
-            background: rgba(255, 255, 255, 0.05);
-            transition: transform 0.2s ease-in-out;
+            border: solid 0.1em var(--border);
+            background: var(--bg-alt);
             cursor: pointer;
         }
 
         .track-cover.zoomed {
-            transform: scale(5.0);
-            z-index: 10;
-            position: relative;
-            box-shadow: 0 8px 16px rgba(0, 0, 0, 0.5);
+            position: fixed;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+            width: 320px;
+            height: 320px;
+            z-index: 100;
+            border-color: var(--cyan);
         }
 
-        .loading {
-            padding: 20px;
-            color: oklch(0.74 0.075 225.31);
-            text-align: center;
+        .similar-btn {
+            font-size: 0.75em;
+            padding: var(--space-xs) var(--space-sm);
         }
 
+        .similar-row td {
+            padding: 0;
+        }
+
+        .similar-panel {
+            padding: var(--space-sm) var(--space-md);
+            background-color: var(--bg-alt);
+            border-top: solid 0.1em var(--border);
+        }
+
+        .similar-track {
+            display: flex;
+            align-items: center;
+            gap: var(--space-sm);
+            padding: var(--space-xs) 0;
+        }
+
+        .similar-track-info {
+            flex: 1;
+            min-width: 0;
+        }
+
+        .similar-track-name {
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+        }
+
+        .similar-track-artist {
+            color: var(--fg-alt);
+            font-size: 0.9em;
+        }
+
+        .similar-score {
+            color: var(--fg-alt);
+            font-size: 0.85em;
+        }
+
+        .similar-link {
+            font-size: 0.85em;
+            color: var(--blue);
+        }
+
+        .similar-link:hover {
+            color: var(--cyan);
+        }
+
+        .similar-loading,
+        .similar-error,
+        .similar-empty {
+            color: var(--fg-alt);
+            font-size: 0.9em;
+        }
+
+        .loading,
         .error {
-            padding: 20px;
-            color: oklch(0.78 0.085 225.31);
-            text-align: center;
+            padding: var(--space-md) 0;
+            color: var(--fg-alt);
         }
 
-        .small {
-            font-size: 12px;
-            color: oklch(0.74 0.075 225.31);
-            margin-top: 20px;
-        }
-
-        .small a {
-            color: oklch(0.70 0.035 225.31);
-            text-decoration: none;
-        }
-
-        .small a:hover {
-            color: oklch(0.78 0.13 357.86);
-            text-decoration: underline;
-        }
-
         .pagination {
             display: flex;
-            justify-content: center;
-            gap: 20px;
-            margin-top: 20px;
+            flex-wrap: wrap;
+            gap: var(--space-sm);
+            align-items: center;
+            margin: var(--space-md) 0;
         }
 
-        .page-btn {
-            background: oklch(0.27 0.038 225.31);
-            border: 1px solid oklch(0.42 0.04 225.31);
-            color: oklch(0.91 0.012 225.31);
-            padding: 8px 16px;
-            border-radius: 4px;
-            cursor: pointer;
-            font-family: inherit;
-            box-shadow: 2px 2px 0px oklch(0.42 0.04 225.31);
-        }
-
-        .page-btn:hover {
-            background: oklch(0.32 0.045 225.31);
-            border-color: oklch(0.52 0.05 225.31);
-        }
-
-        .page-btn:disabled {
-            opacity: 0.5;
-            cursor: not-allowed;
-        }
-
         .page-indicator {
+            color: var(--fg-alt);
+            font-size: 0.9em;
+            padding: 0 var(--space-sm);
+        }
+
+        .filter-banner {
             display: flex;
+            flex-wrap: wrap;
+            gap: var(--space-sm);
             align-items: center;
-            font-size: 14px;
-            color: oklch(0.74 0.075 225.31);
+            margin-bottom: var(--space-md);
+            padding: var(--space-sm) var(--space-md);
+            background-color: var(--bg-alt);
+            border: solid 0.1em var(--border);
+            color: var(--fg-alt);
         }
 
-        .tabs {
-            display: flex;
-            gap: 10px;
-            margin-top: 5px;
-            margin-bottom: 20px;
+        .filter-label strong {
+            color: var(--fg);
         }
 
-        .tab-btn {
-            background: none;
-            border: 1px solid oklch(0.42 0.04 225.31);
-            color: oklch(0.74 0.075 225.31);
-            padding: 6px 14px;
-            border-radius: 4px;
-            cursor: pointer;
-            font-family: inherit;
-            font-size: 12px;
-            text-decoration: none;
-            display: inline-block;
-        }
-
-        .tab-btn.active {
-            background: oklch(0.27 0.038 225.31);
-            color: oklch(0.91 0.012 225.31);
-            box-shadow: 2px 2px 0px oklch(0.42 0.04 225.31);
-        }
-
-        .tab-btn:hover {
-            color: oklch(0.91 0.012 225.31);
-            background: oklch(0.42 0.04 225.31);
-        }
-
         .stats-section {
-            margin-bottom: 30px;
+            margin-bottom: var(--space-lg);
         }
 
-        .stats-section h2 {
-            font-size: 11px;
-            color: oklch(0.74 0.075 225.31);
-            text-transform: uppercase;
-            letter-spacing: 3px;
-            margin: 0 0 10px 0;
-            border-bottom: 1px solid oklch(0.42 0.04 225.31);
-            padding-bottom: 5px;
+        .stats-table {
+            margin-bottom: var(--space-sm);
         }
 
-        .stat-row {
-            display: flex;
-            justify-content: space-between;
-            align-items: center;
-            padding: 5px 8px;
-            margin-bottom: 3px;
+        .stats-count-header,
+        .stat-count {
+            text-align: right;
+            color: var(--fg-alt);
+        }
+
+        .stat-name-cell {
             position: relative;
-            border-radius: 2px;
             overflow: hidden;
-            font-size: 13px;
         }
 
         .stat-bar {
@@ -307,397 +503,83 @@ indexHtml registrationEnabled userSlug allUsers =
             left: 0;
             top: 0;
             height: 100%;
-            background: oklch(0.27 0.038 225.31);
-            border-right: 1px solid oklch(0.42 0.04 225.31);
+            background-color: var(--bg-inactive);
             z-index: 0;
         }
 
         .stat-name {
             position: relative;
             z-index: 1;
-            color: oklch(0.91 0.012 225.31);
-            flex: 1;
-            padding-right: 10px;
-            white-space: nowrap;
-            overflow: hidden;
-            text-overflow: ellipsis;
         }
 
-        .stat-count {
-            position: relative;
-            z-index: 1;
-            color: oklch(0.74 0.075 225.31);
-            font-size: 12px;
-            flex-shrink: 0;
-        }
-
-        .stats-empty {
-            color: oklch(0.74 0.075 225.31);
-            font-size: 13px;
-            padding: 10px 0;
-        }
-
-        .cover-wrapper {
-            display: flex;
-            flex-direction: column;
-            align-items: center;
-	    padding-top: 5px;
-            margin-left: 15px;
-            gap: 4px;
-            flex-shrink: 0;
-            position: relative;
-        }
-
-        .genre-tag {
-            display: none;
-        }
-
-
-
         .stat-row.clickable {
             cursor: pointer;
         }
 
         .stat-row.clickable:hover .stat-name {
-            color: oklch(0.70 0.035 225.31);
+            color: var(--blue);
         }
 
-        .filter-banner {
-            display: flex;
-            align-items: center;
-            gap: 10px;
-            background: oklch(0.27 0.038 225.31);
-            border: 1px solid oklch(0.42 0.04 225.31);
-            border-radius: 4px;
-            padding: 8px 12px;
-            margin-bottom: 12px;
-            font-size: 13px;
-            color: oklch(0.74 0.075 225.31);
+        .stats-empty {
+            color: var(--fg-alt);
+            font-size: 0.9em;
+            padding: var(--space-sm) 0;
         }
 
-        .filter-label {
-            flex: 1;
-        }
-
-        .filter-label strong {
-            color: oklch(0.91 0.012 225.31);
-        }
-
-        .filter-clear {
-            background: none;
-            border: 1px solid oklch(0.42 0.04 225.31);
-            color: oklch(0.74 0.075 225.31);
-            padding: 2px 8px;
-            border-radius: 3px;
-            cursor: pointer;
-            font-family: inherit;
-            font-size: 12px;
-        }
-
-        .filter-clear:hover {
-            color: oklch(0.91 0.012 225.31);
-            border-color: oklch(0.52 0.05 225.31);
-            background: oklch(0.32 0.045 225.31);
-        }
-
-        .show-all-btn {
-            background: none;
-            border: none;
-            color: oklch(0.74 0.075 225.31);
-            cursor: pointer;
-            font-family: inherit;
-            font-size: 12px;
-            padding: 4px 0;
-            text-decoration: underline;
-        }
-
-        .show-all-btn:hover {
-            color: oklch(0.91 0.012 225.31);
-            background: oklch(0.32 0.045 225.31);
-        }
-
         .period-selector {
             display: flex;
-            gap: 6px;
-            margin-bottom: 16px;
+            flex-wrap: wrap;
+            gap: var(--space-sm);
+            margin-bottom: var(--space-md);
         }
 
-        .period-btn {
-            background: none;
-            border: 1px solid oklch(0.42 0.04 225.31);
-            color: oklch(0.74 0.075 225.31);
-            padding: 4px 12px;
-            border-radius: 3px;
-            cursor: pointer;
-            font-family: inherit;
-            font-size: 12px;
-        }
-
-        .period-btn:hover {
-            color: oklch(0.91 0.012 225.31);
-            border-color: oklch(0.7 0.1274 357.86);
-        }
-
-        .period-btn.active {
-            background: oklch(0.5 0.1274 357.86);
-            border-color: oklch(0.7 0.1274 357.86);
-            color: oklch(0.97 0.01 357.86);
-        }
-
         .custom-range {
             display: flex;
-            gap: 8px;
-            margin-top: 8px;
+            flex-wrap: wrap;
+            gap: var(--space-sm);
             align-items: center;
+            margin-bottom: var(--space-md);
         }
 
         .custom-range-input {
-            background: oklch(0.27 0.038 225.31);
-            border: 1px solid oklch(0.42 0.04 225.31);
-            color: oklch(0.91 0.012 225.31);
-            padding: 4px 8px;
-            border-radius: 3px;
-            font-family: inherit;
-            font-size: 12px;
-            width: 220px;
+            width: 16em;
         }
 
-        .custom-range-input::placeholder {
-            color: oklch(0.74 0.075 225.31);
-            opacity: 0.7;
-        }
-
-        .custom-range-input:focus {
-            outline: none;
-            border-color: oklch(0.91 0.012 225.31);
-        }
-
         .custom-range-input.error {
-            border-color: oklch(0.68 0.11 225.31);
+            border-color: var(--red);
         }
 
         .custom-range-error {
-            color: oklch(0.68 0.11 225.31);
-            font-size: 12px;
-            margin-top: 4px;
+            color: var(--red);
+            font-size: 0.9em;
         }
 
-        .similar-btn {
-            background: none;
-            border: 1px solid oklch(0.42 0.04 225.31);
-            color: oklch(0.74 0.075 225.31);
-            padding: 2px 8px;
-            border-radius: 3px;
+        .show-all-btn {
+            background-color: var(--bg-alt);
+            border: solid 0.1em var(--border);
+            color: var(--fg);
+            padding: var(--space-xs) var(--space-sm);
             cursor: pointer;
-            font-family: inherit;
-            font-size: 10px;
-            margin-top: 4px;
-            transition: all 0.2s ease;
+            font: inherit;
         }
 
-        .similar-btn:hover {
-            background: oklch(0.32 0.045 225.31);
-            color: oklch(0.91 0.012 225.31);
-            border-color: oklch(0.7 0.1274 357.86);
+        .show-all-btn:hover {
+            background-color: var(--highlight);
+            border-color: var(--cyan);
         }
 
-        .similar-btn.active {
-            background: oklch(0.5 0.1274 357.86);
-            color: oklch(0.97 0.01 357.86);
-            border-color: oklch(0.7 0.1274 357.86);
-            box-shadow: 2px 2px 0px oklch(0.7 0.1274 357.86);
-        }
-
-        .similar-panel {
-            margin-top: 10px;
-            background: oklch(0.27 0.038 225.31);
-            border: 1px solid oklch(0.42 0.04 225.31);
-            border-radius: 4px;
-            padding: 10px;
-            box-shadow: 2px 2px 0px oklch(0.42 0.04 225.31);
-        }
-
-        .similar-panel-header {
-            font-size: 11px;
-            color: oklch(0.74 0.075 225.31);
-            text-transform: uppercase;
-            letter-spacing: 1px;
-            margin-bottom: 8px;
-            font-weight: bold;
-        }
-
-        .similar-loading {
-            color: oklch(0.74 0.075 225.31);
-            font-size: 12px;
-            text-align: center;
-            padding: 8px;
-        }
-
-        .similar-error {
-            color: oklch(0.78 0.085 225.31);
-            font-size: 12px;
-            text-align: center;
-            padding: 8px;
-        }
-
-        .similar-empty {
-            color: oklch(0.74 0.075 225.31);
-            font-size: 12px;
-            text-align: center;
-            padding: 8px;
-        }
-
-        .similar-track {
-            display: flex;
-            justify-content: space-between;
-            align-items: center;
-            padding: 6px 8px;
-            margin-bottom: 4px;
-            background: oklch(0.27 0.038 225.31);
-            border-radius: 3px;
-            border-left: 2px solid oklch(0.42 0.04 225.31);
-        }
-
-        .similar-track:last-child {
-            margin-bottom: 0;
-        }
-
-        .similar-track-info {
-            flex: 1;
-            min-width: 0;
-        }
-
-        .similar-track-name {
-            font-size: 12px;
-            color: oklch(0.91 0.012 225.31);
-            font-weight: 500;
-            white-space: nowrap;
-            overflow: hidden;
-            text-overflow: ellipsis;
-        }
-
-        .similar-track-artist {
-            font-size: 11px;
-            color: oklch(0.70 0.035 225.31);
-            margin-top: 1px;
-            white-space: nowrap;
-            overflow: hidden;
-            text-overflow: ellipsis;
-        }
-
-        .similar-score {
-            font-size: 10px;
-            color: oklch(0.74 0.075 225.31);
-            background: oklch(0.27 0.038 225.31);
-            border: 1px solid oklch(0.42 0.04 225.31);
-            padding: 2px 6px;
-            border-radius: 2px;
-            margin-right: 8px;
-            font-weight: 500;
-        }
-
-        .similar-link {
-            color: oklch(0.74 0.075 225.31);
-            text-decoration: none;
-            font-size: 12px;
-            background: oklch(0.27 0.038 225.31);
-            border: 1px solid oklch(0.42 0.04 225.31);
-            padding: 2px 6px;
-            border-radius: 2px;
-            transition: all 0.2s ease;
-        }
-
-        .similar-link:hover {
-            color: oklch(0.78 0.13 357.86);
-            background: oklch(0.42 0.04 225.31);
-        }
-
-        .tracks-with-similar {
-            display: flex;
-            flex-direction: column;
-            gap: 10px;
-        }
-
-        .track-with-similar-container {
-            display: flex;
-            flex-direction: column;
-            gap: 10px;
-        }
-
-        .track-item {
-            list-style: none;
-            padding: 0;
-            margin: 0;
-        }
-
-        .track-item li {
-            margin: 0;
-        }
-
-        .search-bar {
-            display: flex;
-            gap: 8px;
-            margin-bottom: 12px;
-            align-items: center;
-        }
-
-        .search-input {
-            flex: 1;
-            background: oklch(0.27 0.038 225.31);
-            border: 1px solid oklch(0.42 0.04 225.31);
-            color: oklch(0.91 0.012 225.31);
-            padding: 6px 10px;
-            border-radius: 4px;
-            font-family: inherit;
-            font-size: 13px;
-        }
-
-        .search-input::placeholder {
-            color: oklch(0.74 0.075 225.31);
-            opacity: 0.7;
-        }
-
-        .search-input:focus {
-            outline: none;
-            border-color: oklch(0.91 0.012 225.31);
-        }
-
-        .search-btn {
-            background: oklch(0.32 0.045 225.31);
-            border: 1px solid oklch(0.52 0.05 225.31);
-            color: oklch(0.91 0.012 225.31);
-            padding: 6px 12px;
-            border-radius: 4px;
-            cursor: pointer;
-            font-family: inherit;
-            font-size: 12px;
-        }
-
-        .search-btn:hover {
-            background: oklch(0.42 0.05 225.31);
-            border-color: oklch(0.52 0.05 225.31);
-        }
-
-        .search-bar:focus-within .search-btn {
-            background: oklch(0.5 0.1274 357.86);
-            border-color: oklch(0.7 0.1274 357.86);
-            color: oklch(0.97 0.01 357.86);
-        }
-
         .about-lead {
-            color: oklch(0.70 0.035 225.31);
-            font-size: 13px;
-            line-height: 1.8;
-            margin: 0 0 30px 0;
+            color: var(--fg-alt);
+            font-size: 0.9em;
+            line-height: 1.6;
         }
 
         .about-link {
-            color: oklch(0.74 0.075 225.31);
-            text-decoration: underline;
+            color: var(--blue);
         }
 
         .about-link:hover {
-            color: oklch(0.78 0.13 357.86);
+            color: var(--cyan);
         }
 
         .about-users {
@@ -707,173 +589,133 @@ indexHtml registrationEnabled userSlug allUsers =
         }
 
         .about-users li {
-            background: none;
-            border: none;
-            border-radius: 0;
-            padding: 4px 0;
-            margin-bottom: 0;
-            display: block;
-            box-shadow: none;
+            padding: var(--space-xs) 0;
         }
 
-        .about-users a {
-            color: oklch(0.74 0.075 225.31);
-            text-decoration: none;
-            font-size: 13px;
-        }
-
-        .about-users a:hover {
-            color: oklch(0.78 0.13 357.86);
-            text-decoration: underline;
-        }
-
         .reg-form {
-            display: flex;
-            flex-direction: column;
-            gap: 12px;
-            max-width: 480px;
-            margin: 20px 0;
+            max-width: 32em;
         }
 
         .reg-field {
             display: flex;
             flex-direction: column;
-            gap: 4px;
+            gap: var(--space-xs);
+            margin-bottom: var(--space-md);
         }
 
         .reg-label {
-            font-size: 12px;
-            color: oklch(0.74 0.075 225.31);
+            color: var(--fg-alt);
+            font-size: 0.9em;
         }
 
         .reg-input {
-            background: oklch(0.27 0.038 225.31);
-            border: 1px solid oklch(0.42 0.04 225.31);
-            color: oklch(0.91 0.012 225.31);
-            padding: 6px 10px;
-            border-radius: 4px;
-            font-family: inherit;
-            font-size: 13px;
+            width: 100%;
         }
 
-        .reg-input.error {
-            border-color: oklch(0.62 0.17 25);
-        }
-
-        .reg-btn {
-            background: oklch(0.32 0.045 225.31);
-            border: 1px solid oklch(0.52 0.05 225.31);
-            color: oklch(0.91 0.012 225.31);
-            padding: 8px 14px;
-            border-radius: 4px;
-            font-family: inherit;
-            font-size: 13px;
+        .reg-btn,
+        .approve-btn,
+        .deny-btn {
+            background-color: var(--bg-alt);
+            border: solid 0.1em var(--border);
+            color: var(--fg);
+            padding: var(--space-xs) var(--space-sm);
+            font: inherit;
             cursor: pointer;
-            align-self: flex-start;
         }
 
-        .reg-btn:hover {
-            background: oklch(0.38 0.05 225.31);
+        .reg-btn:hover,
+        .approve-btn:hover,
+        .deny-btn:hover {
+            background-color: var(--highlight);
+            border-color: var(--cyan);
         }
 
-        .reg-btn:disabled {
-            opacity: 0.5;
-            cursor: default;
+        .approve-btn {
+            background-color: var(--green-light);
+            border-color: var(--green);
+            color: var(--fg);
         }
 
+        .approve-btn:hover {
+            background-color: var(--green);
+            border-color: var(--green);
+            color: var(--bg);
+        }
+
+        .deny-btn {
+            background-color: var(--red-light);
+            border-color: var(--red);
+            color: var(--fg);
+        }
+
+        .deny-btn:hover {
+            background-color: var(--red);
+            border-color: var(--red);
+            color: var(--bg);
+        }
+
         .reg-error {
-            color: oklch(0.72 0.15 25);
-            font-size: 13px;
+            color: var(--red);
+            font-size: 0.9em;
         }
 
         .reg-success {
-            color: oklch(0.78 0.13 150);
-            font-size: 13px;
+            color: var(--green);
+            font-size: 0.9em;
         }
 
         .admin-list {
             list-style: none;
             padding: 0;
-            margin: 16px 0;
-            display: flex;
-            flex-direction: column;
-            gap: 10px;
+            margin: var(--space-sm) 0 var(--space-md) 0;
         }
 
         .admin-item {
-            background: oklch(0.24 0.035 225.31);
-            border: 1px solid oklch(0.36 0.04 225.31);
-            border-radius: 6px;
-            padding: 12px;
+            display: flex;
+            flex-wrap: wrap;
+            gap: var(--space-sm);
+            align-items: center;
+            justify-content: space-between;
+            padding: var(--space-sm) var(--space-md);
+            border: solid 0.1em var(--border);
+            background-color: var(--bg-alt);
         }
 
-        .admin-item-meta {
-            font-size: 13px;
-            margin-bottom: 8px;
-        }
-
         .admin-item-slug {
-            font-weight: 700;
-            color: oklch(0.88 0.05 225.31);
+            font-weight: bold;
+            color: var(--fg);
         }
 
         .admin-item-sub {
-            color: oklch(0.7 0.05 225.31);
-            font-size: 12px;
+            color: var(--fg-alt);
+            font-size: 0.9em;
         }
 
-        .admin-actions {
-            display: flex;
-            gap: 8px;
-        }
-
-        .approve-btn {
-            background: oklch(0.34 0.07 150);
-            border: 1px solid oklch(0.5 0.09 150);
-            color: oklch(0.94 0.02 150);
-            padding: 5px 12px;
-            border-radius: 4px;
-            font-family: inherit;
-            font-size: 12px;
-            cursor: pointer;
-        }
-
-        .deny-btn {
-            background: oklch(0.32 0.07 25);
-            border: 1px solid oklch(0.5 0.1 25);
-            color: oklch(0.94 0.02 25);
-            padding: 5px 12px;
-            border-radius: 4px;
-            font-family: inherit;
-            font-size: 12px;
-            cursor: pointer;
-        }
-
         .token-box {
-            background: oklch(0.27 0.038 225.31);
-            border: 1px solid oklch(0.42 0.04 225.31);
-            border-radius: 4px;
-            padding: 10px;
-            margin: 12px 0;
-            font-size: 12px;
+            border: solid 0.1em var(--border);
+            padding: var(--space-sm);
+            margin: var(--space-sm) 0;
+            background-color: var(--bg-alt);
+            font-size: 0.85em;
             word-break: break-all;
+            color: var(--fg);
         }
+
+        ::selection {
+            background-color: var(--highlight);
+            color: var(--fg-intense);
+        }
     </style>
 </head>
 <body>
     <div id="app"></div>
     <script src="/client.js"></script>
     <script>
-        var userSlug = '""" <> userSlug
-      <>
+        var userSlug = '""" <> userSlug <>
         """';
-        var allUsers = """
-      <> usersJson
-      <>
+        var allUsers = """ <> usersJson <>
         """;
-        var registrationEnabled = """
-      <> registrationEnabledJson
-      <>
+        var registrationEnabled = """ <> registrationEnabledJson <>
         """;
         var adminToken = window.localStorage.getItem('corpusAdminToken') || '';
         var app = Elm.Client.init({
blob - 7b4a00c9d4f0fec9c01568dbfaa534371735cc84
blob + 8cc781c6abb7e2cb96b5ba0337fc7d90d8794a6d
--- src/Types.elm
+++ src/Types.elm
@@ -150,7 +150,6 @@ type alias Model =
     , allUsers : List UserInfo
     , searchInput : String
     , activeSearch : Maybe String
-    , tabsVisible : Bool
     , page : Page
     , registrationEnabled : Bool
 
@@ -185,7 +184,6 @@ type Msg
     | NextPage
     | PrevPage
     | SwitchTab Tab
-    | ToggleTabs
     | SetStatsPeriod Period
     | OpenCustomInput
     | UpdateCustomInput String
@@ -194,6 +192,7 @@ type Msg
     | ClearFilter
     | HoverCover Int
     | UnhoverCover
+    | CoverBackgroundClick
     | ExpandSection String
     | ShowAllSection String
     | CollapseSection String
blob - c7564af396d76e666866be86292c0b91b173198b
blob + e53876ca46d78093bcca958c0d6192f5984e0045
--- src/View.elm
+++ src/View.elm
@@ -2,7 +2,7 @@ module View exposing (view)
 
 import Browser
 import Dict exposing (Dict)
-import Html exposing (Html, a, button, div, h2, img, input, li, p, span, strong, text, ul)
+import Html exposing (Html, a, button, div, h1, h2, header, img, input, li, p, span, strong, table, tbody, td, text, th, thead, tr, ul)
 import Html.Attributes as Attr exposing (class, disabled, href, placeholder, src, style, target, type_, value)
 import Html.Events exposing (on, onClick, onInput)
 import Json.Decode as D
@@ -21,175 +21,169 @@ view model =
     { title = "scrobbler"
     , body =
         [ div [ class "container" ]
-            (case model.page of
-                RegisterPage ->
-                    [ renderRegister model ]
+            (header [] [ renderNav model ]
+                :: (case model.page of
+                        RegisterPage ->
+                            [ renderRegister model ]
 
-                AdminPage ->
-                    [ renderAdmin model ]
+                        AdminPage ->
+                            [ renderAdmin model ]
 
-                MainPage ->
-                    [ renderMain model ]
+                        MainPage ->
+                            [ renderMain model ]
+                   )
             )
         ]
     }
 
 
+renderNav : Model -> Html Msg
+renderNav model =
+    let
+        navClass : Bool -> Html.Attribute Msg
+        navClass active =
+            class
+                (if active then
+                    "tab-btn active"
+
+                 else
+                    "tab-btn"
+                )
+
+        navLink : String -> Bool -> String -> Html Msg
+        navLink label active href_ =
+            a [ navClass active, href href_ ] [ text label ]
+
+        navTab : String -> Tab -> Html Msg
+        navTab label tab =
+            if model.page == MainPage then
+                button [ navClass (model.activeTab == tab), onClick (SwitchTab tab) ] [ text label ]
+
+            else
+                navLink label False "/"
+    in
+    div [ class "bar" ]
+        [ navTab "listens" ListensTab
+        , navTab "stats" StatsTab
+        , navTab "about" AboutTab
+        , if model.registrationEnabled then
+            navLink "join" (model.page == RegisterPage) "/register"
+
+          else
+            text ""
+        ]
+
+
 renderMain : Model -> Html Msg
 renderMain model =
     div []
-            [ if model.tabsVisible then
-                div [ class "tabs" ]
-                [ a
-                    [ class
-                        ("tab-btn"
-                            ++ (if model.activeTab == ListensTab then
-                                    " active"
-
-                                else
-                                    ""
-                               )
-                        )
-                    , href
-                        (if model.userSlug == "" then
-                            "/"
-
-                         else
-                            "/u/" ++ model.userSlug
-                        )
-                    ]
-                    [ text "listens" ]
-                , button
-                    [ class
-                        ("tab-btn"
-                            ++ (if model.activeTab == StatsTab then
-                                    " active"
-
-                                else
-                                    ""
-                               )
-                        )
-                    , onClick (SwitchTab StatsTab)
-                    ]
-                    [ text "stats" ]
-                , if model.registrationEnabled then
-                    a [ class "tab-btn", href "/register" ] [ text "join" ]
-
-                  else
-                    text ""
-                , button
-                    [ class
-                        ("tab-btn"
-                            ++ (if model.activeTab == AboutTab then
-                                    " active"
-
-                                else
-                                    ""
-                               )
-                        )
-                    , onClick (SwitchTab AboutTab)
-                    ]
-                    [ text "about" ]
-                ]
-
-              else
-                text ""
-            , case model.activeTab of
-                ListensTab ->
-                    div []
-                        [ div [ class "search-bar" ]
-                            [ input
-                                [ type_ "text"
-                                , class "search-input"
-                                , placeholder "search by track, artist, album or label"
-                                , value model.searchInput
-                                , onInput UpdateSearchInput
-                                , onEnter SubmitSearch
-                                ]
-                                []
-                            , button [ class "search-btn", onClick SubmitSearch ] [ text "search" ]
-                            , case model.activeSearch of
-                                Just _ ->
-                                    button [ class "filter-clear", onClick ClearSearch ] [ text "✕ clear" ]
-
-                                Nothing ->
-                                    text ""
+        [ case model.activeTab of
+            ListensTab ->
+                div []
+                    [ div [ class "bar" ]
+                        [ input
+                            [ type_ "search"
+                            , class "search-input"
+                            , placeholder ""
+                            , value model.searchInput
+                            , onInput UpdateSearchInput
+                            , onEnter SubmitSearch
                             ]
-                        , case model.activeFilter of
+                            []
+                        , button [ class "search-btn", onClick SubmitSearch ] [ text "search" ]
+                        , case model.activeSearch of
+                            Just _ ->
+                                button [ class "filter-clear", onClick ClearSearch ] [ text "✕ clear" ]
+
                             Nothing ->
                                 text ""
+                        ]
+                    , case model.activeFilter of
+                        Nothing ->
+                            text ""
 
-                            Just { field, value } ->
-                                div [ class "filter-banner" ]
-                                    [ span [ class "filter-label" ]
-                                        [ text (field ++ ": ")
-                                        , strong [] [ text value ]
-                                        ]
-                                    , button [ class "filter-clear", onClick ClearFilter ]
-                                        [ text "✕ clear" ]
+                        Just { field, value } ->
+                            div [ class "filter-banner" ]
+                                [ span [ class "filter-label" ]
+                                    [ text (field ++ ": ")
+                                    , strong [] [ text value ]
                                     ]
-                        , renderContent model
-                        , div [ class "pagination" ]
-                            [ button
-                                [ class "page-btn"
-                                , disabled (model.offset == 0 || model.loading)
-                                , onClick PrevPage
+                                , button [ class "filter-clear", onClick ClearFilter ]
+                                    [ text "✕ clear" ]
                                 ]
-                                [ text "Previous" ]
-                            , div [ class "page-indicator" ]
-                                [ text ("Page " ++ String.fromInt (model.offset // model.limit + 1)) ]
-                            , button
-                                [ class "page-btn"
-                                , disabled (List.length model.listens < model.limit || model.loading)
-                                , onClick NextPage
-                                ]
-                                [ text "Next" ]
+                    , renderContent model
+                    , renderCoverOverlay model.hoveredCover
+                    , div [ class "pagination" ]
+                        [ button
+                            [ class "page-btn"
+                            , disabled (model.offset == 0 || model.loading)
+                            , onClick PrevPage
                             ]
+                            [ text "Previous" ]
+                        , div [ class "page-indicator" ]
+                            [ text ("Page " ++ String.fromInt (model.offset // model.limit + 1)) ]
+                        , button
+                            [ class "page-btn"
+                            , disabled (List.length model.listens < model.limit || model.loading)
+                            , onClick NextPage
+                            ]
+                            [ text "Next" ]
                         ]
+                    ]
 
-                StatsTab ->
-                    div []
-                        [ renderPeriodSelector model.statsPeriod model.showCustomInput model.customInput model.customError
-                        , renderStatsView model.expandedSections model.loadedSections model.stats
-                        ]
+            StatsTab ->
+                div []
+                    [ renderPeriodSelector model.statsPeriod model.showCustomInput model.customInput model.customError
+                    , renderStatsView model.expandedSections model.loadedSections model.stats
+                    ]
 
-                AboutTab ->
-                    renderAboutView model.userSlug model.allUsers
-            ]
+            AboutTab ->
+                renderAboutView model.userSlug model.allUsers
+        ]
 
 
+renderCoverOverlay : Maybe Int -> Html Msg
+renderCoverOverlay hovered =
+    case hovered of
+        Nothing ->
+            text ""
+
+        Just _ ->
+            div [ class "cover-overlay", onClick CoverBackgroundClick ] []
+
+
 renderContent : Model -> Html Msg
 renderContent model =
     if model.loading && List.isEmpty model.listens then
-        ul [] [ li [ class "loading" ] [ text "⏳" ] ]
+        div [ class "loading" ] [ text "⏳" ]
 
     else
         case model.error of
             Just err ->
-                ul [] [ li [ class "error" ] [ text err ] ]
+                div [ class "error" ] [ text err ]
 
             Nothing ->
-                div [ class "tracks-with-similar" ]
-                    (List.indexedMap
-                        (\idx listen ->
-                            let
-                                artist =
-                                    Maybe.withDefault "" listen.artistName
+                table []
+                    [ tbody []
+                        (List.indexedMap
+                            (\idx listen ->
+                                let
+                                    artist =
+                                        Maybe.withDefault "" listen.artistName
 
-                                trackName =
-                                    Maybe.withDefault "" listen.trackName
+                                    trackName =
+                                        Maybe.withDefault "" listen.trackName
 
-                                key =
-                                    String.fromInt idx ++ "\t" ++ artist ++ "\t" ++ trackName
-                            in
-                            div [ class "track-with-similar-container" ]
-                                [ ul [ class "track-item" ]
-                                    [ renderListen model.userSlug model.currentTime model.failedCovers model.hoveredCover model.similarStates idx listen ]
-                                , renderSimilarPanel model.similarStates key
-                                ]
+                                    key =
+                                        String.fromInt idx ++ "\t" ++ artist ++ "\t" ++ trackName
+                                in
+                                renderListen model.userSlug model.currentTime model.failedCovers model.hoveredCover model.similarStates idx listen
+                                    :: renderSimilarRow key model.similarStates
+                            )
+                            model.listens
+                            |> List.concat
                         )
-                        model.listens
-                    )
+                    ]
 
 
 renderListen : String -> Maybe Time.Posix -> Set String -> Maybe Int -> Dict String SimilarState -> Int -> Listen -> Html Msg
@@ -236,13 +230,62 @@ renderListen userSlug currentTime failedCovers hovered
         isActive =
             Dict.member key similarStates
     in
-    li [ class "success" ]
-        [ div [ class "track-info" ]
-            [ div [ class "track-name" ]
-                [ button [ class "track-link", onClick (FilterBy "track" (Maybe.withDefault "Unknown Track" listen.trackName)) ] [ text (Maybe.withDefault "Unknown Track" listen.trackName) ] ]
-            , div [ class "track-artist" ] [ button [ class "artist-link", onClick (FilterBy "artist" artist) ] [ text artist ] ]
-            , div [ class "track-time" ]
-                [ span []
+    tr [ class "listen-row" ]
+        [ td [ class "d" ]
+            [ if Set.member coverUrl failedCovers then
+                text ""
+
+              else
+                div [ class "cover-cell" ]
+                    [ img
+                        [ class
+                            ("track-cover"
+                                ++ (if isZoomed then
+                                        " zoomed"
+
+                                    else
+                                        ""
+                                   )
+                            )
+                        , src coverUrl
+                        , Attr.alt release
+                        , on "error" (D.succeed (ImageError coverUrl))
+                        , onClick
+                            (if isZoomed then
+                                UnhoverCover
+
+                             else
+                                HoverCover idx
+                            )
+                        ]
+                        []
+                    ]
+            ]
+        , td [ class "track-cell" ]
+            [ div [ class "track-line" ]
+                [ if artist /= "" && trackName /= "" then
+                    button
+                        [ class
+                            ("similar-btn"
+                                ++ (if isActive then
+                                        " active"
+
+                                    else
+                                        ""
+                                   )
+                            )
+                        , onClick (FetchSimilar idx artist trackName)
+                        ]
+                        [ text "similar" ]
+
+                  else
+                    text ""
+                , button [ class "track-link", onClick (FilterBy "track" (Maybe.withDefault "Unknown Track" listen.trackName)) ] [ text (Maybe.withDefault "Unknown Track" listen.trackName) ]
+                ]
+            , div [ class "track-artist" ]
+                [ button [ class "artist-link", onClick (FilterBy "artist" artist) ] [ text artist ] ]
+            , div [ class "track-meta" ]
+                [ div [ class "track-release" ]
                     (button [ class "album-link", onClick (FilterBy "album" release) ] [ text release ]
                         :: (case listen.label of
                                 Just l ->
@@ -253,86 +296,86 @@ renderListen userSlug currentTime failedCovers hovered
                                 Nothing ->
                                     []
                            )
-                        ++ [ text (" • " ++ timeAgo currentTime listen.listenedAt) ]
                     )
+                , case listen.genre of
+                    Just g ->
+                        div [ class "genre-tag" ] [ text g ]
+
+                    Nothing ->
+                        text ""
                 ]
-            ]
-        , div [ class "cover-wrapper" ]
-            [ if Set.member coverUrl failedCovers then
-                text ""
-
-              else
-                img
-                    [ class
-                        ("track-cover"
-                            ++ (if isZoomed then
-                                    " zoomed"
-
-                                else
-                                    ""
-                               )
-                        )
-                    , src coverUrl
-                    , Attr.alt release
-                    , on "error" (D.succeed (ImageError coverUrl))
-                    , onClick
-                        (if isZoomed then
-                            UnhoverCover
-
-                         else
-                            HoverCover idx
-                        )
-                    ]
-                    []
-            , case listen.genre of
-                Just g ->
-                    div [ class "genre-tag" ] [ text g ]
-
-                Nothing ->
+            , div [ class "cover-wrapper" ]
+                [ if Set.member coverUrl failedCovers then
                     text ""
-            , if artist /= "" && trackName /= "" then
-                button
-                    [ class
-                        ("similar-btn"
-                            ++ (if isActive then
-                                    " active"
 
-                                else
-                                    ""
-                               )
-                        )
-                    , onClick (FetchSimilar idx artist trackName)
-                    ]
-                    [ text "similar" ]
+                  else
+                    img
+                        [ class
+                            ("track-cover"
+                                ++ (if isZoomed then
+                                        " zoomed"
 
-              else
-                text ""
+                                    else
+                                        ""
+                                   )
+                            )
+                        , src coverUrl
+                        , Attr.alt release
+                        , on "error" (D.succeed (ImageError coverUrl))
+                        , onClick
+                            (if isZoomed then
+                                UnhoverCover
+
+                             else
+                                HoverCover idx
+                            )
+                        ]
+                        []
+                ]
             ]
         ]
 
 
-renderSimilarPanel : Dict String SimilarState -> String -> Html Msg
-renderSimilarPanel similarStates key =
+renderSimilarRow : String -> Dict String SimilarState -> List (Html Msg)
+renderSimilarRow key similarStates =
     case Dict.get key similarStates of
         Nothing ->
-            text ""
+            []
 
         Just SimilarLoading ->
-            div [ class "similar-panel" ]
-                [ div [ class "similar-loading" ] [ text "⏳" ] ]
+            [ tr [ class "similar-row" ]
+                [ td [ class "d" ] []
+                , td []
+                    [ div [ class "similar-panel" ]
+                        [ div [ class "similar-loading" ] [ text "⏳" ] ]
+                    ]
+                ]
+            ]
 
         Just (SimilarError err) ->
-            div [ class "similar-panel" ]
-                [ div [ class "similar-error" ] [ text err ] ]
+            [ tr [ class "similar-row" ]
+                [ td [ class "d" ] []
+                , td []
+                    [ div [ class "similar-panel" ]
+                        [ div [ class "similar-error" ] [ text err ] ]
+                    ]
+                ]
+            ]
 
         Just (SimilarLoaded tracks) ->
-            div [ class "similar-panel" ]
-                [ if List.isEmpty tracks then
-                    div [ class "similar-empty" ] [ text "no similar songs found" ]
+            [ tr [ class "similar-row" ]
+                [ td [ class "d" ] []
+                , td []
+                    [ div [ class "similar-panel" ]
+                        [ if List.isEmpty tracks then
+                            div [ class "similar-empty" ] [ text "no similar songs found" ]
 
-                  else
-                    div [] (List.map renderSimilarTrack tracks)
+                          else
+                            div [] (List.map renderSimilarTrack tracks)
+                        ]
+                    ]
                 ]
+            ]
 
 
 renderSimilarTrack : SimilarTrack -> Html Msg
@@ -352,7 +395,7 @@ renderSimilarTrack track =
         , case track.videoUri of
             Just link ->
                 a [ class "similar-link", href link, target "_blank" ]
-                    [ text "▶" ]
+                    [ text "play" ]
 
             Nothing ->
                 text ""
@@ -437,7 +480,15 @@ renderStatSection expandedSections loadedSections mFie
             div [ class "stats-empty" ] [ text "beyond here lies nothing" ]
 
           else
-            ul [] (List.map (renderStatEntry maxCount mField) visible)
+            table [ class "stats-table" ]
+                [ thead []
+                    [ tr []
+                        [ th [] [ text "name" ]
+                        , th [ class "stats-count-header" ] [ text "plays" ]
+                        ]
+                    ]
+                , tbody [] (List.map (renderStatEntry maxCount mField) visible)
+                ]
         , footer
         ]
 
@@ -456,10 +507,12 @@ renderStatEntry maxCount mField entry =
                 Nothing ->
                     [ class "stat-row" ]
     in
-    li rowAttrs
-        [ div [ class "stat-bar", style "width" (String.fromInt barPct ++ "%") ] []
-        , span [ class "stat-name" ] [ text entry.name ]
-        , span [ class "stat-count" ] [ text (String.fromInt entry.count) ]
+    tr rowAttrs
+        [ td [ class "stat-name-cell" ]
+            [ div [ class "stat-bar", style "width" (String.fromInt barPct ++ "%") ] []
+            , span [ class "stat-name" ] [ text entry.name ]
+            ]
+        , td [ class "stat-count" ] [ text (String.fromInt entry.count) ]
         ]