katja's git: oeffisearch

fast and simple tripplanner

commit f4857cf8ea9c89ac7d586c2c249f416f4e7338b9
parent 98c55ad4a7d0a29b0f22100a551db843ebac20b9
Author: Katja (ctucx) <git@ctu.cx>
Date: Thu, 10 Apr 2025 21:17:24 +0200

completely rewrite app to use web-components via lit-element
47 files changed, 3397 insertions(+), 3245 deletions(-)
R
src/ds100.json -> ds100.json
|
0
M
flake.nix
|
2
+-
M
package.json
|
9
++++-----
M
pnpm-lock.yaml
|
332
++++++++++++++++++++-----------------------------------------------------------
A
src/LitOverlay.js
|
88
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M
src/app_functions.js
|
193
+++++++++++++++++++++++++++++++++----------------------------------------------
D
src/assets/icons.css
|
83
-------------------------------------------------------------------------------
M
src/assets/index.html
|
28
++++++++++------------------
D
src/assets/style.css
|
815
-------------------------------------------------------------------------------
M
src/coach-sequence/index.js
|
4
++--
M
src/dataStorage.js
|
44
++++++++++++--------------------------------
M
src/departuresView.js
|
225
++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
A
src/ds100.js
|
24
++++++++++++++++++++++++
A
src/footerComponent.js
|
22
++++++++++++++++++++++
M
src/formatters.js
|
60
++++++++++++++++++------------------------------------------
M
src/hafasClient.js
|
74
+++++++++++++++++++++++++++++---------------------------------------------
M
src/helpers.js
|
19
+++++++++----------
M
src/journeyView.js
|
492
++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
A
src/journeysCanvas.js
|
453
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M
src/journeysView.js
|
406
++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
D
src/journeysViewCanvas.js
|
418
-------------------------------------------------------------------------------
M
src/languages.js
|
14
++++++++++----
M
src/main.js
|
114
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
D
src/overlays.js
|
53
-----------------------------------------------------
D
src/router.js
|
31
-------------------------------
M
src/searchView.js
|
954
+++++++++++++++++++++++++++++++++++++++----------------------------------------
M
src/settings.js
|
92
+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
M
src/settingsView.js
|
269
+++++++++++++++++++++++++++++++++++++++++--------------------------------------
M
src/shim/cross-fetch.js
|
1
+
A
src/styles.js
|
17
+++++++++++++++++
A
src/styles/base.css
|
47
+++++++++++++++++++++++++++++++++++++++++++++++
A
src/styles/buttonInput.css
|
151
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/styles/card.css
|
81
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/styles/departuresView.css
|
7
+++++++
A
src/styles/flexbox.css
|
29
+++++++++++++++++++++++++++++
A
src/styles/footer.css
|
18
++++++++++++++++++
A
src/styles/header.css
|
86
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/styles/helpers.css
|
16
++++++++++++++++
A
src/styles/icons.css
|
95
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/styles/journeyView.css
|
45
+++++++++++++++++++++++++++++++++++++++++++++
A
src/styles/journeysView.css
|
15
+++++++++++++++
A
src/styles/overlays.css
|
88
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/styles/searchView.css
|
164
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/styles/settingsView.css
|
25
+++++++++++++++++++++++++
M
src/templates.js
|
87
+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
M
src/tripView.js
|
275
++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
M
webpack.config.js
|
77
++++++++++++++++++++++++++++++++++++++---------------------------------------
diff --git a/src/ds100.json b/ds100.json
diff --git a/flake.nix b/flake.nix
@@ -51,7 +51,7 @@
 
         pnpmDeps = final.pnpm.fetchDeps {
           inherit (finalAttrs) pname version src;
-          hash = "sha256-F0jSEOoosHQlKdV3gh2ZlICFxdlEN1aamnUHoqI645o=";
+          hash = "sha256-DS62XKpud8mQ5ZnNncLlCAkl9zHGSC9YBM7igSt06Lg=";
         };
 
         env.GIT_VERSION    = if (inputs.self.sourceInfo ? shortRev) then inputs.self.sourceInfo.shortRev else "dirty";
diff --git a/package.json b/package.json
@@ -14,18 +14,17 @@
     "buffer": "^6.0.3",
     "db-vendo-client": "^6.8.0",
     "hafas-client": "^6.3.4",
+    "ics": "^3.8.1",
     "idb": "^8.0.1",
-    "lit-html": "^3.2.1"
+    "lit": "^3.2.1",
+    "zustand": "^5.0.3"
   },
   "devDependencies": {
     "@principalstudio/html-webpack-inject-preload": "^1.2.7",
     "copy-webpack-plugin": "^12.0.2",
-    "css-loader": "^7.1.2",
-    "css-minimizer-webpack-plugin": "^7.0.0",
     "git-revision-webpack-plugin": "^5.0.0",
     "html-webpack-plugin": "^5.6.3",
-    "mini-css-extract-plugin": "^2.9.2",
-    "style-loader": "^4.0.0",
+    "lit-css-loader": "^3.0.1",
     "webpack": "^5.97.1",
     "webpack-cli": "^6.0.1",
     "webpack-dev-server": "^5.2.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
@@ -6,7 +6,7 @@ settings:
 
 patchedDependencies:
   hafas-client:
-    hash: mko7uu5feywjq6c7qe5c22nnzi
+    hash: jzfq2zvnydenrytq7tcgcly4hm
     path: patches/hafas-client.patch
 
 importers:

@@ -24,16 +24,19 @@ importers:
         version: 6.8.0
       hafas-client:
         specifier: ^6.3.4
-        version: 6.3.4(patch_hash=mko7uu5feywjq6c7qe5c22nnzi)
+        version: 6.3.4(patch_hash=jzfq2zvnydenrytq7tcgcly4hm)
       ics:
         specifier: ^3.8.1
         version: 3.8.1
       idb:
         specifier: ^8.0.1
         version: 8.0.2
-      lit-html:
+      lit:
         specifier: ^3.2.1
         version: 3.2.1
+      zustand:
+        specifier: ^5.0.3
+        version: 5.0.3
     devDependencies:
       '@principalstudio/html-webpack-inject-preload':
         specifier: ^1.2.7

@@ -41,24 +44,15 @@ importers:
       copy-webpack-plugin:
         specifier: ^12.0.2
         version: 12.0.2(webpack@5.98.0)
-      css-loader:
-        specifier: ^7.1.2
-        version: 7.1.2(webpack@5.98.0)
-      css-minimizer-webpack-plugin:
-        specifier: ^7.0.0
-        version: 7.0.2(webpack@5.98.0)
       git-revision-webpack-plugin:
         specifier: ^5.0.0
         version: 5.0.0(webpack@5.98.0)
       html-webpack-plugin:
         specifier: ^5.6.3
         version: 5.6.3(webpack@5.98.0)
-      mini-css-extract-plugin:
-        specifier: ^2.9.2
-        version: 2.9.2(webpack@5.98.0)
-      style-loader:
-        specifier: ^4.0.0
-        version: 4.0.0(webpack@5.98.0)
+      lit-css-loader:
+        specifier: ^3.0.1
+        version: 3.0.1(postcss@8.5.3)
       webpack:
         specifier: ^5.97.1
         version: 5.98.0(webpack-cli@6.0.1)

@@ -581,14 +575,6 @@ packages:
     resolution: {integrity: sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==}
     engines: {node: '>=14.17.0'}
 
-  '@jest/schemas@29.6.3':
-    resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
-    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
-  '@jest/types@29.6.3':
-    resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==}
-    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
   '@jridgewell/gen-mapping@0.3.8':
     resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
     engines: {node: '>=6.0.0'}

@@ -631,6 +617,12 @@ packages:
   '@leichtgewicht/ip-codec@2.0.5':
     resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
 
+  '@lit-labs/ssr-dom-shim@1.3.0':
+    resolution: {integrity: sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==}
+
+  '@lit/reactive-element@2.0.4':
+    resolution: {integrity: sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==}
+
   '@nodelib/fs.scandir@2.1.5':
     resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
     engines: {node: '>= 8'}

@@ -650,6 +642,9 @@ packages:
       html-webpack-plugin: ^4.0.0 || ^5.0.0
       webpack: ^4.0.0 || ^5.0.0
 
+  '@pwrs/lit-css@3.0.1':
+    resolution: {integrity: sha512-N3oac0XYqKEEoWMT7y02EDxoyM++805V+nXh3rEBGV2IW2Y0NjK4hepLea65HGFlu+ezSK48XAJYD4xAyvVsaQ==}
+
   '@rollup/plugin-babel@5.3.1':
     resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
     engines: {node: '>= 10.0.0'}

@@ -699,9 +694,6 @@ packages:
       rollup:
         optional: true
 
-  '@sinclair/typebox@0.27.8':
-    resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
-
   '@sindresorhus/merge-streams@2.3.0':
     resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
     engines: {node: '>=18'}

@@ -752,15 +744,6 @@ packages:
   '@types/http-proxy@1.17.16':
     resolution: {integrity: sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==}
 
-  '@types/istanbul-lib-coverage@2.0.6':
-    resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
-
-  '@types/istanbul-lib-report@3.0.3':
-    resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==}
-
-  '@types/istanbul-reports@3.0.4':
-    resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==}
-
   '@types/json-schema@7.0.15':
     resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
 

@@ -803,12 +786,6 @@ packages:
   '@types/ws@8.18.1':
     resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
 
-  '@types/yargs-parser@21.0.3':
-    resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
-
-  '@types/yargs@17.0.33':
-    resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==}
-
   '@webassemblyjs/ast@1.14.1':
     resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
 

@@ -1060,10 +1037,6 @@ packages:
     resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==}
     engines: {node: '>=6.0'}
 
-  ci-info@3.9.0:
-    resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
-    engines: {node: '>=8'}
-
   cipher-base@1.0.6:
     resolution: {integrity: sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==}
     engines: {node: '>= 0.10'}

@@ -1173,43 +1146,6 @@ packages:
     peerDependencies:
       postcss: ^8.0.9
 
-  css-loader@7.1.2:
-    resolution: {integrity: sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==}
-    engines: {node: '>= 18.12.0'}
-    peerDependencies:
-      '@rspack/core': 0.x || 1.x
-      webpack: ^5.27.0
-    peerDependenciesMeta:
-      '@rspack/core':
-        optional: true
-      webpack:
-        optional: true
-
-  css-minimizer-webpack-plugin@7.0.2:
-    resolution: {integrity: sha512-nBRWZtI77PBZQgcXMNqiIXVshiQOVLGSf2qX/WZfG8IQfMbeHUMXaBWQmiiSTmPJUflQxHjZjzAmuyO7tpL2Jg==}
-    engines: {node: '>= 18.12.0'}
-    peerDependencies:
-      '@parcel/css': '*'
-      '@swc/css': '*'
-      clean-css: '*'
-      csso: '*'
-      esbuild: '*'
-      lightningcss: '*'
-      webpack: ^5.0.0
-    peerDependenciesMeta:
-      '@parcel/css':
-        optional: true
-      '@swc/css':
-        optional: true
-      clean-css:
-        optional: true
-      csso:
-        optional: true
-      esbuild:
-        optional: true
-      lightningcss:
-        optional: true
-
   css-select@4.3.0:
     resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
 

@@ -1737,12 +1673,6 @@ packages:
   ics@3.8.1:
     resolution: {integrity: sha512-UqQlfkajfhrS4pUGQfGIJMYz/Jsl/ob3LqcfEhUmLbwumg+ZNkU0/6S734Vsjq3/FYNpEcZVKodLBoe+zBM69g==}
 
-  icss-utils@5.1.0:
-    resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==}
-    engines: {node: ^10 || ^12 || >= 14}
-    peerDependencies:
-      postcss: ^8.1.0
-
   idb@7.1.1:
     resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
 

@@ -1954,18 +1884,10 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
 
-  jest-util@29.7.0:
-    resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==}
-    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
   jest-worker@27.5.1:
     resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
     engines: {node: '>= 10.13.0'}
 
-  jest-worker@29.7.0:
-    resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==}
-    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
   js-tokens@4.0.0:
     resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
 

@@ -2018,13 +1940,26 @@ packages:
     resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
     engines: {node: '>=14'}
 
+  lit-css-loader@3.0.1:
+    resolution: {integrity: sha512-SiX7fe9R1nAd5SFzS0zgWovq0IjTyNOfIspWc2+U3eHfQSfLKtCLymJyidnIcm9nHp2f1uDxnvGDfjNI0WNYmA==}
+
+  lit-element@4.1.1:
+    resolution: {integrity: sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==}
+
   lit-html@3.2.1:
     resolution: {integrity: sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==}
 
+  lit@3.2.1:
+    resolution: {integrity: sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==}
+
   loader-runner@4.3.0:
     resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
     engines: {node: '>=6.11.5'}
 
+  loader-utils@3.3.1:
+    resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==}
+    engines: {node: '>= 12.13.0'}
+
   locate-path@5.0.0:
     resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
     engines: {node: '>=8'}

@@ -2113,12 +2048,6 @@ packages:
     engines: {node: '>=4'}
     hasBin: true
 
-  mini-css-extract-plugin@2.9.2:
-    resolution: {integrity: sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==}
-    engines: {node: '>= 12.13.0'}
-    peerDependencies:
-      webpack: ^5.0.0
-
   minimalistic-assert@1.0.1:
     resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
 

@@ -2380,30 +2309,6 @@ packages:
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-modules-extract-imports@3.1.0:
-    resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==}
-    engines: {node: ^10 || ^12 || >= 14}
-    peerDependencies:
-      postcss: ^8.1.0
-
-  postcss-modules-local-by-default@4.2.0:
-    resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==}
-    engines: {node: ^10 || ^12 || >= 14}
-    peerDependencies:
-      postcss: ^8.1.0
-
-  postcss-modules-scope@3.2.1:
-    resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==}
-    engines: {node: ^10 || ^12 || >= 14}
-    peerDependencies:
-      postcss: ^8.1.0
-
-  postcss-modules-values@4.0.0:
-    resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==}
-    engines: {node: ^10 || ^12 || >= 14}
-    peerDependencies:
-      postcss: ^8.1.0
-
   postcss-normalize-charset@7.0.0:
     resolution: {integrity: sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==}
     engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}

@@ -2682,11 +2587,6 @@ packages:
     resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
     hasBin: true
 
-  semver@7.7.1:
-    resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
-    engines: {node: '>=10'}
-    hasBin: true
-
   send@0.19.0:
     resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
     engines: {node: '>= 0.8.0'}

@@ -2843,12 +2743,6 @@ packages:
     resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==}
     engines: {node: '>=10'}
 
-  style-loader@4.0.0:
-    resolution: {integrity: sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==}
-    engines: {node: '>= 18.12.0'}
-    peerDependencies:
-      webpack: ^5.27.0
-
   stylehacks@7.0.4:
     resolution: {integrity: sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==}
     engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}

@@ -3232,6 +3126,24 @@ packages:
   yup@1.6.1:
     resolution: {integrity: sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==}
 
+  zustand@5.0.3:
+    resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==}
+    engines: {node: '>=12.20.0'}
+    peerDependencies:
+      '@types/react': '>=18.0.0'
+      immer: '>=9.0.6'
+      react: '>=18.0.0'
+      use-sync-external-store: '>=1.2.0'
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      immer:
+        optional: true
+      react:
+        optional: true
+      use-sync-external-store:
+        optional: true
+
 snapshots:
 
   '@ampproject/remapping@2.3.0':

@@ -3891,19 +3803,6 @@ snapshots:
 
   '@discoveryjs/json-ext@0.6.3': {}
 
-  '@jest/schemas@29.6.3':
-    dependencies:
-      '@sinclair/typebox': 0.27.8
-
-  '@jest/types@29.6.3':
-    dependencies:
-      '@jest/schemas': 29.6.3
-      '@types/istanbul-lib-coverage': 2.0.6
-      '@types/istanbul-reports': 3.0.4
-      '@types/node': 22.14.0
-      '@types/yargs': 17.0.33
-      chalk: 4.1.2
-
   '@jridgewell/gen-mapping@0.3.8':
     dependencies:
       '@jridgewell/set-array': 1.2.1

@@ -3944,6 +3843,12 @@ snapshots:
 
   '@leichtgewicht/ip-codec@2.0.5': {}
 
+  '@lit-labs/ssr-dom-shim@1.3.0': {}
+
+  '@lit/reactive-element@2.0.4':
+    dependencies:
+      '@lit-labs/ssr-dom-shim': 1.3.0
+
   '@nodelib/fs.scandir@2.1.5':
     dependencies:
       '@nodelib/fs.stat': 2.0.5

@@ -3961,6 +3866,12 @@ snapshots:
       html-webpack-plugin: 5.6.3(webpack@5.98.0)
       webpack: 5.98.0(webpack-cli@6.0.1)
 
+  '@pwrs/lit-css@3.0.1(postcss@8.5.3)':
+    dependencies:
+      cssnano: 7.0.6(postcss@8.5.3)
+    transitivePeerDependencies:
+      - postcss
+
   '@rollup/plugin-babel@5.3.1(@babel/core@7.26.10)(rollup@2.79.2)':
     dependencies:
       '@babel/core': 7.26.10

@@ -4009,8 +3920,6 @@ snapshots:
     optionalDependencies:
       rollup: 2.79.2
 
-  '@sinclair/typebox@0.27.8': {}
-
   '@sindresorhus/merge-streams@2.3.0': {}
 
   '@surma/rollup-plugin-off-main-thread@2.2.3':

@@ -4076,16 +3985,6 @@ snapshots:
     dependencies:
       '@types/node': 22.14.0
 
-  '@types/istanbul-lib-coverage@2.0.6': {}
-
-  '@types/istanbul-lib-report@3.0.3':
-    dependencies:
-      '@types/istanbul-lib-coverage': 2.0.6
-
-  '@types/istanbul-reports@3.0.4':
-    dependencies:
-      '@types/istanbul-lib-report': 3.0.3
-
   '@types/json-schema@7.0.15': {}
 
   '@types/mime@1.3.5': {}

@@ -4131,12 +4030,6 @@ snapshots:
     dependencies:
       '@types/node': 22.14.0
 
-  '@types/yargs-parser@21.0.3': {}
-
-  '@types/yargs@17.0.33':
-    dependencies:
-      '@types/yargs-parser': 21.0.3
-
   '@webassemblyjs/ast@1.14.1':
     dependencies:
       '@webassemblyjs/helper-numbers': 1.13.2

@@ -4446,8 +4339,6 @@ snapshots:
 
   chrome-trace-event@1.0.4: {}
 
-  ci-info@3.9.0: {}
-
   cipher-base@1.0.6:
     dependencies:
       inherits: 2.0.4

@@ -4557,29 +4448,6 @@ snapshots:
     dependencies:
       postcss: 8.5.3
 
-  css-loader@7.1.2(webpack@5.98.0):
-    dependencies:
-      icss-utils: 5.1.0(postcss@8.5.3)
-      postcss: 8.5.3
-      postcss-modules-extract-imports: 3.1.0(postcss@8.5.3)
-      postcss-modules-local-by-default: 4.2.0(postcss@8.5.3)
-      postcss-modules-scope: 3.2.1(postcss@8.5.3)
-      postcss-modules-values: 4.0.0(postcss@8.5.3)
-      postcss-value-parser: 4.2.0
-      semver: 7.7.1
-    optionalDependencies:
-      webpack: 5.98.0(webpack-cli@6.0.1)
-
-  css-minimizer-webpack-plugin@7.0.2(webpack@5.98.0):
-    dependencies:
-      '@jridgewell/trace-mapping': 0.3.25
-      cssnano: 7.0.6(postcss@8.5.3)
-      jest-worker: 29.7.0
-      postcss: 8.5.3
-      schema-utils: 4.3.0
-      serialize-javascript: 6.0.2
-      webpack: 5.98.0(webpack-cli@6.0.1)
-
   css-select@4.3.0:
     dependencies:
       boolbase: 1.0.0

@@ -5114,7 +4982,7 @@ snapshots:
 
   graceful-fs@4.2.11: {}
 
-  hafas-client@6.3.4(patch_hash=mko7uu5feywjq6c7qe5c22nnzi):
+  hafas-client@6.3.4(patch_hash=jzfq2zvnydenrytq7tcgcly4hm):
     dependencies:
       '@derhuerst/br2nl': 1.0.0
       '@derhuerst/round-robin-scheduler': 1.0.4

@@ -5259,10 +5127,6 @@ snapshots:
       runes2: 1.1.4
       yup: 1.6.1
 
-  icss-utils@5.1.0(postcss@8.5.3):
-    dependencies:
-      postcss: 8.5.3
-
   idb@7.1.1: {}
 
   idb@8.0.2: {}

@@ -5457,28 +5321,12 @@ snapshots:
       filelist: 1.0.4
       minimatch: 3.1.2
 
-  jest-util@29.7.0:
-    dependencies:
-      '@jest/types': 29.6.3
-      '@types/node': 22.14.0
-      chalk: 4.1.2
-      ci-info: 3.9.0
-      graceful-fs: 4.2.11
-      picomatch: 2.3.1
-
   jest-worker@27.5.1:
     dependencies:
       '@types/node': 22.14.0
       merge-stream: 2.0.0
       supports-color: 8.1.1
 
-  jest-worker@29.7.0:
-    dependencies:
-      '@types/node': 22.14.0
-      jest-util: 29.7.0
-      merge-stream: 2.0.0
-      supports-color: 8.1.1
-
   js-tokens@4.0.0: {}
 
   jsesc@3.0.2: {}

@@ -5514,12 +5362,33 @@ snapshots:
 
   lilconfig@3.1.3: {}
 
+  lit-css-loader@3.0.1(postcss@8.5.3):
+    dependencies:
+      '@pwrs/lit-css': 3.0.1(postcss@8.5.3)
+      loader-utils: 3.3.1
+    transitivePeerDependencies:
+      - postcss
+
+  lit-element@4.1.1:
+    dependencies:
+      '@lit-labs/ssr-dom-shim': 1.3.0
+      '@lit/reactive-element': 2.0.4
+      lit-html: 3.2.1
+
   lit-html@3.2.1:
     dependencies:
       '@types/trusted-types': 2.0.7
 
+  lit@3.2.1:
+    dependencies:
+      '@lit/reactive-element': 2.0.4
+      lit-element: 4.1.1
+      lit-html: 3.2.1
+
   loader-runner@4.3.0: {}
 
+  loader-utils@3.3.1: {}
+
   locate-path@5.0.0:
     dependencies:
       p-locate: 4.1.0

@@ -5592,12 +5461,6 @@ snapshots:
 
   mime@1.6.0: {}
 
-  mini-css-extract-plugin@2.9.2(webpack@5.98.0):
-    dependencies:
-      schema-utils: 4.3.0
-      tapable: 2.2.1
-      webpack: 5.98.0(webpack-cli@6.0.1)
-
   minimalistic-assert@1.0.1: {}
 
   minimatch@3.1.2:

@@ -5829,27 +5692,6 @@ snapshots:
       postcss: 8.5.3
       postcss-selector-parser: 6.1.2
 
-  postcss-modules-extract-imports@3.1.0(postcss@8.5.3):
-    dependencies:
-      postcss: 8.5.3
-
-  postcss-modules-local-by-default@4.2.0(postcss@8.5.3):
-    dependencies:
-      icss-utils: 5.1.0(postcss@8.5.3)
-      postcss: 8.5.3
-      postcss-selector-parser: 7.1.0
-      postcss-value-parser: 4.2.0
-
-  postcss-modules-scope@3.2.1(postcss@8.5.3):
-    dependencies:
-      postcss: 8.5.3
-      postcss-selector-parser: 7.1.0
-
-  postcss-modules-values@4.0.0(postcss@8.5.3):
-    dependencies:
-      icss-utils: 5.1.0(postcss@8.5.3)
-      postcss: 8.5.3
-
   postcss-normalize-charset@7.0.0(postcss@8.5.3):
     dependencies:
       postcss: 8.5.3

@@ -6141,8 +5983,6 @@ snapshots:
 
   semver@6.3.1: {}
 
-  semver@7.7.1: {}
-
   send@0.19.0:
     dependencies:
       debug: 2.6.9

@@ -6374,10 +6214,6 @@ snapshots:
 
   strip-comments@2.0.1: {}
 
-  style-loader@4.0.0(webpack@5.98.0):
-    dependencies:
-      webpack: 5.98.0(webpack-cli@6.0.1)
-
   stylehacks@7.0.4(postcss@8.5.3):
     dependencies:
       browserslist: 4.24.4

@@ -6893,3 +6729,5 @@ snapshots:
       tiny-case: 1.0.3
       toposort: 2.0.2
       type-fest: 2.19.0
+
+  zustand@5.0.3: {}
diff --git a/src/LitOverlay.js b/src/LitOverlay.js
@@ -0,0 +1,88 @@
+import { LitElement, html, nothing } from 'lit';
+import { t } from './languages.js';
+
+import { flexboxStyles, overlaysStyles, buttonInputStyles, iconStyles } from './styles.js';
+
+export class LitOverlay extends LitElement {
+	static properties = {
+		overlayType:    { state: true },
+		overlayTitle:   { state: true },
+		overlayVisible: { state: true },
+		overlayContent: { state: true },
+	};
+
+	static styles = [
+		flexboxStyles,
+		overlaysStyles,
+		buttonInputStyles,
+		iconStyles
+	];
+
+	constructor () {
+		super();
+		this.overlayType    = 'plain';
+		this.overlayVisible = false;
+		this.overlayContent = null;
+		this.overlayTitle   = null;
+	}
+
+	showLoaderOverlay = ()            => this.showOverlay('loader');
+	showDialogOverlay = (title, body) => this.showOverlay('dialog', body, title);
+	showAlertOverlay  = text          => this.showOverlay('alert', text);
+	showSelectOverlay = items         => this.showOverlay('select', items);
+	hideOverlay       = () => { this.overlayVisible = false; }
+	showOverlay       = (type, content, title) => {
+		this.overlayType    = type;
+		this.overlayContent = content;
+		this.overlayTitle   = title;
+		this.overlayVisible = true;
+	}
+
+	overlayHandler = event => event.target === event.currentTarget && this.overlayType !== 'loader' ? this.hideOverlay() : true;
+
+	render () {
+		let overlayContent;
+
+		if (this.overlayVisible) {
+			switch (this.overlayType) {
+				case 'loader':
+					overlayContent = html`<div class="spinner"></div>`;
+					break;
+				case 'dialog':
+					overlayContent = html`
+						<div class="modal dialog">
+							<div class="header flex-row">
+								<h4>${t(this.overlayTitle)}</h4>
+								<div class="icon-close" title="${t('close')}" @click=${this.hideOverlay}></div>
+							</div>
+							<div class="body">${this.overlayContent}</div>
+						</div>
+					`;
+					break;
+				case 'select':
+					overlayContent = html`
+						<div class="modal select flex-column">
+							${this.overlayContent.map(item => html`<a class="button color" @click=${item.action}>${t(item.label)}</a>`)}
+							<a class="button color" @click=${() => { this.hideOverlay();}}>Close</a>
+						</div>
+					`;
+					break;
+				case 'alert':
+					overlayContent = html`
+						<div class="modal alert" style="overflow:auto">
+							${this.overlayContent}<br><button class="color" style="float:right" @click=${this.hideOverlay}>OK</button>
+						</div>
+					`;
+					break;
+				default:
+					overlayContent = this.overlayContent;
+					break;
+			}
+		}
+
+		return [
+			this.renderContent(),
+			!this.overlayVisible ? nothing : html`<div class="overlay" @click=${this.overlayHandler}>${overlayContent}</div>`
+		];
+	}
+}
diff --git a/src/app_functions.js b/src/app_functions.js
@@ -1,113 +1,98 @@
-import { html } from 'lit-html';
 import { db } from './dataStorage.js';
-import { go } from './router.js';
-import { settings, subscribeSettings } from './settings.js';
-import { showLoader, hideOverlay, showModal, showAlertModal } from './overlays.js';
-import { languages } from './languages.js';
-import { isEmptyObject, generateSlug, loyaltyCardToString, loyaltyCardFromString } from './helpers.js';
-import { formatDateTime } from './formatters.js';
+import { settingsState } from './settings.js';
+import { generateSlug, loyaltyCardToString, loyaltyCardFromString } from './helpers.js';
 import { getHafasClient, client } from './hafasClient.js';
 import { trainsearchToHafas, hafasToTrainsearch } from './refresh_token/index.js';
 
-import { default as ds100 } from './ds100.json';
-
-let ds100R = {};
-
 const journeySettings = () => {
-	return {
+	let params = {
 		stopovers: true,
 		polylines: false,
 		tickets:   true,
-		language:  settings.language,
+		language:  settingsState.language,
 	};
 
-	if (settings.profile !== 'db') {
-		params.accessibility = settings.accessibility;
-		params.walkingSpeed  = settings.walkingSpeed;
-	} else {
-		const card = loyaltyCardFromString(settings.loyaltyCard)
-		params.loyaltyCard   = card;
-		params.ageGroup      = settings.ageGroup;
+	if (settingsState.profile === 'db') {
+		params.loyaltyCard = settingsState.loyaltyCard;
+		params.ageGroup    = settingsState.ageGroup;
 	}
 
 	return params;
 };
 
+const processJourneys = journeys => journeys.forEach(journey => processJourney(journey));
+const processJourney  = journey  => journey.legs.forEach(leg => processLeg(leg));
 
-export const getFrom = journeys => journeys[0].legs[0].origin;
-export const getTo   = journeys => journeys[0].legs[journeys[0].legs.length-1].destination;
-
-export const addJourneys = async data => {
-	if (!data) return false;
-
-	const historyEntry = {
-		profile:   data.profile,
-		fromPoint: getFrom(data.journeys),
-		viaPoint:  data.params.via,
-		toPoint:   getTo(data.journeys),
-		slug:      data.slug
-	};
+export const processLeg = leg => {
+	const elements = [ 'plannedDeparture', 'plannedArrival', 'plannedWhen', 'departure', 'arrival', 'when' ];
 
-	const journeyEntries = data.journeys.map(j => {
-		return {
-			...j,
-			settings: data.settings,
-			slug: data.slug,
-		};
+	elements.forEach(element => {
+		if (leg[element]) leg[element] = new Date(leg[element]);
 	});
 
-	if (typeof data.params.loyaltyCard === 'object') data.params.loyaltyCard = loyaltyCardToString(data.params.loyaltyCard);
-
-	const journeysOverviewEntry = {
-		...data,
-		journeys: data.journeys.map(j => j.refreshToken),
-	};
-
-	await db.addJourneys(journeyEntries, journeysOverviewEntry, historyEntry);
+	if (leg.stopovers) leg.stopovers.forEach(stopover => {
+		elements.forEach(element => {
+			if (stopover[element]) stopover[element] = new Date(stopover[element]);
+		});	
+	});
 };
 
-const processJourneys = data    => { for (const journey of data.journeys) processJourney(journey) };
-const processJourney  = journey => { for (const leg of journey.legs) processLeg(leg) };
+export const getFromPoint = journeys => journeys[0].legs[0].origin;
+export const getToPoint   = journeys => journeys[0].legs[journeys[0].legs.length-1].destination;
 
-export const processLeg = leg => {
-	if (leg.plannedDeparture) leg.plannedDeparture = new Date(leg.plannedDeparture);
-	if (leg.plannedArrival) leg.plannedArrival = new Date(leg.plannedArrival);
-	if (leg.plannedWhen) leg.plannedWhen = new Date(leg.plannedWhen);
-	if (leg.departure) leg.departure = new Date(leg.departure);
-	if (leg.arrival) leg.arrival = new Date(leg.arrival);
-	if (leg.when) leg.when = new Date(leg.when);
-	for (const stopover of (leg.stopovers || [])) {
-		if (stopover.plannedDeparture) stopover.plannedDeparture = new Date(stopover.plannedDeparture);
-		if (stopover.plannedArrival) stopover.plannedArrival = new Date(stopover.plannedArrival);
-		if (stopover.plannedWhen) stopover.plannedWhen = new Date(stopover.plannedWhen);
-		if (stopover.departure) stopover.departure = new Date(stopover.departure);
-		if (stopover.arrival) stopover.arrival = new Date(stopover.arrival);
-		if (stopover.when) stopover.when = new Date(stopover.when);
-	}
-};
-
-export const newJourneys = async (params) => {
+ export const newJourneys = async params => {
 	const { from, to, ...moreOpts } = params;
 	let data;
 
-	data = await client.journeys(from, to, { ...journeySettings(), ...moreOpts });
+	const journeySettingsObj = journeySettings();
+	if (typeof journeySettingsObj.loyaltyCard === 'string') journeySettingsObj.loyaltyCard = loyaltyCardFromString(journeySettingsObj.loyaltyCard);
 
-	for (const journey of data.journeys) {
-		journey.refreshToken = hafasToTrainsearch(journey.refreshToken);
-	}
+	data = await client.journeys(from, to, { ...moreOpts, ...journeySettingsObj });
+
+	data.journeys.forEach((journey, index, journeys) => {
+		journeys[index].refreshToken = hafasToTrainsearch(journey.refreshToken);
+	});
 
 	data.slug        = generateSlug();
 	data.indexOffset = 0;
 	data.params      = params;
-	data.settings    = journeySettings();
-	data.profile     = settings.profile;
+	data.settings    = journeySettingsObj;
+	data.profile     = settingsState.profile;
 
 	await addJourneys(data);
-	processJourneys(data);
+
+	processJourneys(data.journeys);
 
 	return data;
 };
 
+export const addJourneys = async data => {
+	if (!data) return false;
+
+	if (typeof data.settings.loyaltyCard === 'object') data.settings.loyaltyCard = loyaltyCardToString(data.settings.loyaltyCard);
+
+	const journeyEntries = data.journeys.map(j => ({
+		...j,
+		settings: data.settings,
+		slug: data.slug,
+	}));
+
+	const journeysOverviewEntry = {
+		...data,
+		journeys: data.journeys.map(j => j.refreshToken),
+	};
+
+	const historyEntry = {
+		profile:   data.profile,
+		fromPoint: getFromPoint(data.journeys),
+		viaPoint:  data.params.via,
+		toPoint:   getToPoint(data.journeys),
+		slug:      data.slug
+	};
+
+	await db.addJourneys(journeyEntries, journeysOverviewEntry, historyEntry);
+};
+
 export const getJourneys = async slug => {
 	let data = await db.getJourneysOverview(slug);
 

@@ -115,22 +100,22 @@ export const getJourneys = async slug => {
 
 	return {
 		...data,
-		journeys: await Promise.all(data.journeys.map(x => getJourney(x, data.profile))),
+		journeys: await Promise.all(data.journeys.map(refreshToken => getJourney(data.profile, refreshToken))),
 	};
 };
 
 export const getMoreJourneys = async (slug, mode) => {
 	const saved  = await db.getJourneysOverview(slug);
-	const params = { ...saved.params, ...journeySettings() };
+	const params = { ...journeySettings(), ...saved.params };
 
 	if (typeof params.loyaltyCard === 'string') params.loyaltyCard = loyaltyCardFromString(params.loyaltyCard);
 
 	params[mode+'Than'] = saved[mode+'Ref'];
 
 	let { departure, arrival, from, to, ...moreOpt } = params;
+
 	const [newData, ...existingJourneys] = await Promise.all(
-		[ client.journeys(from, to, moreOpt) ]
-			.concat(saved.journeys.map(x => getJourney(x, saved.profile)))
+		[ client.journeys(from, to, moreOpt) ].concat(saved.journeys.map(refreshToken => getJourney(saved.profile, refreshToken)))
 	);
 
 	const res = {

@@ -156,56 +141,40 @@ export const getMoreJourneys = async (slug, mode) => {
 
 export const refreshJourneys = async (slug) => {
 	const saved = await db.getJourneysOverview(slug);
-	await Promise.all(saved.journeys.map(x => refreshJourney(x, saved.profile || "db")));
+	await Promise.all(saved.journeys.map(refreshToken => refreshJourney(saved.profile, refreshToken)));
 };
 
-export const getJourney = async (refreshToken, profile) => {
+export const getJourney = async (profile, refreshToken) => {
 	let journeyObject = await db.getJourney(refreshToken);
 
-	if (!journeyObject || JSON.stringify(journeyObject.settings) != JSON.stringify(journeySettings()))
-		journeyObject = await refreshJourney(refreshToken, profile);
+	if (!journeyObject || JSON.stringify(journeyObject.settings) != JSON.stringify(journeySettings())) journeyObject = await refreshJourney(profile, refreshToken);
 
 	processJourney(journeyObject);
 
 	return journeyObject;
 };
 
-export const refreshJourney = async (refreshToken, profile) => {
-	const client        = await getHafasClient(profile || settings.profile || "db");
-	const [saved, data] = await Promise.all([
-		db.getJourney(refreshToken),
-		client.refreshJourney(trainsearchToHafas(refreshToken), journeySettings())
-	]);
-
-	const {journey} = data;
-
-	journey.settings     = journeySettings();
-	journey.refreshToken = hafasToTrainsearch(journey.refreshToken);
-
-	if (saved) journey.slug = saved.slug;
+export const refreshJourney = async (profile, refreshToken) => {
+	const client = await getHafasClient(profile);
 
-	db.updateJourney(journey);
+	const params = journeySettings();
+	params.loyaltyCard = loyaltyCardFromString(params.loyaltyCard);
 
-	return journey;
-};
+	const [ saved, data ] = await Promise.all([
+		db.getJourney(refreshToken),
+		client.refreshJourney(trainsearchToHafas(refreshToken), params)
+	]);
 
-export const ds100Name = (id) => {
-	if (!settings.showDS100) return null;
-	if (!ds100[Number(id)])  return null;
+	const journeyObject = data.journey;
 
-	return ds100[Number(id)];
-};
+	journeyObject.refreshToken = hafasToTrainsearch(journeyObject.refreshToken);
+	journeyObject.settings     = journeySettings();
 
-export const ds100Reverse = (name) => {
-	if (!settings.showDS100) return null;
+	if (saved) journeyObject.slug = saved.slug;
 
-	if (isEmptyObject(ds100R)) {
-		for (let [id, names] of Object.entries(ds100)) {
-			for (let name of names.split(", ")) ds100R[name] = id;
-		}
-	}
+	db.updateJourney(journeyObject);
 
-	if (!ds100R[name]) return null;
+	processJourney(journeyObject);
 
-	return ds100R[name];
+	return journeyObject;
 };
diff --git a/src/assets/icons.css b/src/assets/icons.css
@@ -1,83 +0,0 @@
-.icon-back {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z" fill="white"/></svg>');
-}
-
-.icon-reload {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M17.65 6.35A7.96 7.96 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4z" fill="white"/></svg>');
-}
-
-.icon-close {
-	content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="30" height="30"><path d="M5.293 5.293a1 1 0 0 1 1.414 0L12 10.586l5.293-5.293a1 1 0 1 1 1.414 1.414L13.414 12l5.293 5.293a1 1 0 0 1-1.414 1.414L12 13.414l-5.293 5.293a1 1 0 0 1-1.414-1.414L10.586 12 5.293 6.707a1 1 0 0 1 0-1.414" fill="white"/></svg>');
-}
-
-.icon-hint {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M5 3h14a2 2 0 0 1 2 2v14c0 .53-.21 1.04-.59 1.41-.37.38-.88.59-1.41.59H5c-.53 0-1.04-.21-1.41-.59C3.21 20.04 3 19.53 3 19V5c0-1.11.89-2 2-2m6 6h2V7h-2zm3 8v-2h-1v-4h-3v2h1v2h-1v2z"/></svg>');
-}
-
-.icon-status {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2m8 10V7h-2v6zm0 4v-2h-2v2z"/></svg>');
-}
-
-.icon-warning {
-	content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M13 13h-2V7h2m-2 8h2v2h-2m4.73-14H8.27L3 8.27v7.46L8.27 21h7.46L21 15.73V8.27z"/></svg>');
-}
-
-.icon-other {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m1 17h-2v-2h2zm2.07-7.75-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25"/></svg>');
-}
-
-.icon-arrow1 {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>');
-}
-
-.icon-arrow2 {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/></svg>');
-}
-
-.icon-swap {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M16 17.01V10h-2v7.01h-3L15 21l4-3.99zM9 3 5 6.99h3V14h2V6.99h3z"/></svg>');
-}
-
-.icon-clock {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M12 20a8 8 0 0 0 8-8 8 8 0 0 0-8-8 8 8 0 0 0-8 8 8 8 0 0 0 8 8m0-18a10 10 0 0 1 10 10 10 10 0 0 1-10 10C6.47 22 2 17.5 2 12A10 10 0 0 1 12 2m.5 5v5.25l4.5 2.67-.75 1.23L11 13V7z"/></svg>');
-}
-
-.icon-settings {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6"/></svg>');
-}
-
-.icon-walk-fast {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M13.49 5.48c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2m-3.6 13.9 1-4.4 2.1 2v6h2v-7.5l-2.1-2 .6-3c1.3 1.5 3.3 2.5 5.5 2.5v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1l-5.2 2.2v4.7h2v-3.4l1.8-.7-1.6 8.1-4.9-1-.4 2z"/></svg>');
-}
-
-.icon-walk {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M13.5 5.5c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2M9.8 8.9 7 23h2.1l1.8-8 2.1 2v6h2v-7.5l-2.1-2 .6-3C14.8 12 16.8 13 19 13v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1L6 8.3V13h2V9.6z"/></svg>');
-}
-
-.icon-weelchair {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><circle cx="12" cy="4" r="2"/><path d="M19 13v-2c-1.54.02-3.09-.75-4.07-1.83l-1.29-1.43c-.17-.19-.38-.34-.61-.45-.01 0-.01-.01-.02-.01H13c-.35-.2-.75-.3-1.19-.26C10.76 7.11 10 8.04 10 9.09V15c0 1.1.9 2 2 2h5v5h2v-5.5c0-1.1-.9-2-2-2h-3v-3.45c1.29 1.07 3.25 1.94 5 1.95m-6.17 5c-.41 1.16-1.52 2-2.83 2-1.66 0-3-1.34-3-3 0-1.31.84-2.41 2-2.83V12.1a5 5 0 1 0 5.9 5.9z"/></svg>');
-}
-
-.icon-bike {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M5 20.5A3.5 3.5 0 0 1 1.5 17 3.5 3.5 0 0 1 5 13.5 3.5 3.5 0 0 1 8.5 17 3.5 3.5 0 0 1 5 20.5M5 12a5 5 0 0 0-5 5 5 5 0 0 0 5 5 5 5 0 0 0 5-5 5 5 0 0 0-5-5m9.8-2H19V8.2h-3.2l-1.94-3.27c-.29-.5-.86-.83-1.46-.83-.47 0-.9.19-1.2.5L7.5 8.29C7.19 8.6 7 9 7 9.5c0 .63.33 1.16.85 1.47L11.2 13v5H13v-6.5l-2.25-1.65 2.32-2.35m5.93 13a3.5 3.5 0 0 1-3.5-3.5 3.5 3.5 0 0 1 3.5-3.5 3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m0-8.5a5 5 0 0 0-5 5 5 5 0 0 0 5 5 5 5 0 0 0 5-5 5 5 0 0 0-5-5m-3-7.2c1 0 1.8-.8 1.8-1.8S17 1.2 16 1.2 14.2 2 14.2 3 15 4.8 16 4.8"/></svg>');
-}
-
-.icon-seat {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M9 19h6v2H9c-2.76 0-5-2.24-5-5V7h2v9c0 1.66 1.34 3 3 3m1.42-13.59c.78-.78.78-2.05 0-2.83s-2.05-.78-2.83 0-.78 2.05 0 2.83c.78.79 2.04.79 2.83 0M11.5 9c0-1.1-.9-2-2-2H9c-1.1 0-2 .9-2 2v6c0 1.66 1.34 3 3 3h5.07l3.5 3.5L20 20.07 14.93 15H11.5z"/></svg>');
-}
-
-.icon-table {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M3 9h14V7H3zm0 4h14v-2H3zm0 4h14v-2H3zm16 0h2v-2h-2zm0-10v2h2V7zm0 6h2v-2h-2z" fill="white" /></svg>');
-}
-
-.icon-canvas {
-	content: url('data:image/svg+xml;utf8,<svg version="1.1" viewBox="0 0 24 24" height="24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m11 5v14h2v-14zm-4-2v14h2v-14zm10 4h-2v14h2z" fill="white"/></svg>');
-}
-
-.icon-dots {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M12 16a2 2 0 0 1 2 2 2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2 2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2 2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2" fill="white"/></svg>');
-}
-
-.icon-share {
-	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3 3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66 0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08" fill="white"/></svg>');
-}
diff --git a/src/assets/index.html b/src/assets/index.html
@@ -1,32 +1,24 @@
 <!DOCTYPE html>
 <html>
 	<head>
-		<title><%= htmlWebpackPlugin.options.title %></title>
+		<title></title>
 		<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
 		<meta name="theme-color" content="#333">
 		<meta name="description" content="Plan your public transport journeys">
 
 		<link rel="apple-touch-icon" href="favicon.png">
 		<link rel="manifest" href="manifest.json">
-		<style>
-			body {
-				background-color: #333;
-			}
-
-			#overlay {
-				position: fixed;
-				display: flex;
-				top: 0;
-				left: 0;
-				height: 100vh;
-				width: 100vw;
-				background-color: rgba(0, 0, 0, .7);
-			}
-		</style>
 	</head>
 	<body>
-		<div id="content"></div>
-		<div id="overlay">
+		<div style="
+			position: fixed;
+			display: flex;
+			top: 0;
+			left: 0;
+			height: 100vh;
+			width: 100vw;
+			background-color: #333;
+		">
 			<noscript style="margin: auto;">JavaScript is required to use <%= htmlWebpackPlugin.options.title %></noscript>
 			<svg style="margin: auto; width: 50vmin; height: 50vmin;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28"><rect rx="4" height="28" width="28" fill="green"/><path d="M14 5.5c-4 0-8 .5-8 4V19c0 1.93 1.57 3.5 3.5 3.5L8 24v.5h2.23l2-2H16l2 2h2V24l-1.5-1.5c1.93 0 3.5-1.57 3.5-3.5V9.5c0-3.5-3.58-4-8-4m-4.5 15c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5m3.5-7H8v-4h5zm2 0v-4h5v4zm3.5 7c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5" fill="white"/></svg>
 		</div>
diff --git a/src/assets/style.css b/src/assets/style.css
@@ -1,815 +0,0 @@
-@import url('./icons.css');
-
-font-face {
-	font-weight: normal;
-	font-tyle: normal;
-}
-
-:root {
-	overscroll-behavior-y: none;
-}
-
-* {
-	font-family: sans-serif;
-	box-sizing: border-box;
-	border-collapse: collapse;
-}
-
-
-body {
-	margin: 0;
-	background-color: #333;
-}
-
-a {
-	color: inherit;
-}
-
-.invisible {
-	visibility: hidden !important;
-}
-
-.hidden {
-	display: none !important;
-}
-
-.flipped {
-	transform-origin: center center;
-	transform: rotate(180deg);
-}
-
-.flex-row {
-	display: flex;
-	flex-direction: row;
-}
-
-.flex-column {
-	display: flex;
-	flex-direction: column;
-}
-
-.flex-center {
-	display: flex;
-	justify-content: center;
-	align-items: center;
-}
-
-.center {
-	margin: auto;
-}
-
-.spinner {
-	margin: calc(50vh - 60px) auto;
-	border: 5px solid rgba(255, 255, 255, .4);
-	border-top: 5px solid white;
-	border-radius: 50%;
-	width: 120px;
-	height: 120px;
-	animation: spin 2s linear infinite;
-}
-
-.spinning {
-	animation: spin 2s linear infinite;
-}
-
-@keyframes spin {
-	0% { transform: rotate(0deg); }
-	100% { transform: rotate(360deg); }
-}
-
-.container {
-	margin: 0 auto;
-	max-width: 1000px;
-}
-
-.header-container {
-	position: sticky;
-	top: 0;
-	z-index: 10;
-
-	header {
-		display: flex;
-		flex-direction: row;
-		justify-content: center;
-		color: white;
-		background-color: #33691E;
-		border-bottom: 1px solid rgba(255, 255, 255, .3);
-
-		.container {
-			max-width: 1000px;
-			width: 80vw;
-			margin: 0;
-		}
-
-		h3 {
-			margin-right: 1.5em;
-		}
-
-		.icon-reload {
-			float: right;
-		}
-
-		.icon-back,
-		.icon-reload,
-		.icon-share,
-		.icon-dots {
-			cursor: pointer;
-			width: 32px;
-			height: 32px;
-			margin: 12px;
-			user-select: none;
-		}
-
-		.mode-changers {
-			margin-top: auto;
-			margin-left: auto;
-			height: max-content;
-
-			a {
-				border-bottom: 3px solid transparent;
-				align-items: center;
-				display: flex;
-				padding: 0 1em;
-				cursor: pointer;
-				text-decoration: none;
-				width: max-content;
-		
-			 	span {
-					font-weight: bold;
-					margin: 1em .4em;
-				}
-			}
-
-			a.active {
-				border-bottom: 3px solid white;
-			}
-		}
-	}
-
-}
-
-footer {
-	color: #ddd;
-	padding: 2em;
-	width: max-content;
-
-	a {
-		text-decoration: none;
-	}
-
-	a:after {
-		margin: 0 8px;
-		content: "•";
-	}
-
-	:last-child:after {
-		content: none;
-	}
-}
-
-.card {
-	overflow-x: auto;
-
-	table {
-		border-bottom: 1px solid rgba(0, 0, 0, 0.3);
-		width: 100%;
-		background-color: white;
-		min-width: 390px;
-		max-width: 1000px;
-	}
-
-	table a {
-		padding: 5px 3px;
-		text-decoration: none;
-	}
-	
-	thead {
-		.center {
-			padding: 5px 3px;
-		}
-
-		tr:not(:last-child) {
-			background-color: #eee;
-		}
-	}
-
-	tbody {
-		tr {
-			cursor: pointer;
-			border-top: 1px solid #ccc;
-		}
-
-		tr:hover {
-			background-color: #ddd;
-		}
-
-		tr:hover td {
-			background-color: transparent;
-		}
-	}
-
-	td, th {
-		text-align: center;
-		overflow: hidden;
-	}
-
-	th {
-		padding: 10px 5px;
-	}
-
-	th.station {
-		width: 60%;
-	}
-
-	.train-details {
-		flex-wrap: wrap;
-		padding: 0 !important;
-	
-		div {
-			margin: .4em 2em;
-		}
-	}
-
-	.cancelled {
-		text-decoration-line: line-through;
-	}
-	
-	.cancelled-text {
-		font-weight: bold;
-		color: red !important;
-	}
-}
-
-.settingsView {
-	.flex-row,
-	.flex-column {
-		padding: 1em;
-		border-bottom: 1px solid rgba(0, 0, 0, .4);
-	}
-
-	.flex-row {
-		align-items: center;
-	}
-
-	.flex-row:last-child {
-		padding: .5em;
-		border-bottom: unset;
-	    justify-content: right;
-	}
-
-	label {
-		padding: .1em;
-	}
-
-	span {
-		padding-bottom: .25em;
-	}
-
-	select {
-		width: 65%;
-	}
-
-	select,
-	.flex-row div {
-		margin-left: auto;
-	}
-}
-
-.searchView {
-	.title {
-		padding-top: 3em;
-
-		h1 {
-			color: white;
-			font-weight: normal;
-			margin: .7em .3em .5em .3em;
-		}
-
-		h1:hover {
-			-webkit-text-fill-color: transparent;
-			-webkit-background-clip: text !important;
-			background: linear-gradient(90deg, #b4dcff 20%, pink 20%, pink 40%, white 40%, white 60%, pink 60%, pink 80%, #b4dcff 80%, #b4dcff 100%);
-		}
-	}
-
-	.title::before {
-		content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28"><rect rx="4" height="28" width="28" fill="green"/><path d="M14 5.5c-4 0-8 .5-8 4V19c0 1.93 1.57 3.5 3.5 3.5L8 24v.5h2.23l2-2H16l2 2h2V24l-1.5-1.5c1.93 0 3.5-1.57 3.5-3.5V9.5c0-3.5-3.58-4-8-4m-4.5 15c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5m3.5-7H8v-4h5zm2 0v-4h5v4zm3.5 7c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5" fill="white"/></svg>');
-		width: 50px;
-		height: 50px;
-		margin: -0.7em .3em -0.5em -0.3em;
-	}
-
-	form {
-		color: white;
-
-		#time, #date, #datetime {
-			flex-grow: 1;
-		}
-
-		#from, #to, #via {
-			width: 100%;
-		}
-
-		.button.icon-arrow1,
-		.button.icon-arrow2,
-		.button.icon-swap {
-			padding: .3em .5em;
-		}
-
-		.button.now {
-			display: flex;
-			justify-content: center;
-			align-items: center;
-			user-select: none;
-			padding: 0 10px;
-		}
-
-		button[type="submit"],
-		.button.icon-settings {
-			height: 32px;
-		}
-
-		.filler {
-			flex: auto;
-		}
-
-		.button.icon-settings {
-			width: 32px;
-			padding: 3px;
-		}
-		
-		button[type="submit"]{
-			display: flex;
-			align-items: center;
-			font-size: 20px;
-			padding: 8px;
-		}
-
-		button[type="submit"]::after {
-			width: 24px;
-			height: 24px;
-			margin-left: 5px;
-			content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2 4.5 20.29l.71.71L12 18l6.79 3 .71-.71z" fill="green"/></svg>');
-		}
-	}
-
-	.suggestions {
-		position: relative;
-		overflow: visible;
-		z-index: 100;
-		height: 0;
-		margin-left: 4px;
-		margin-right: 3.23rem;
-
-		p {
-			font-size: 1.2em;
-			background-color: white;
-			color: black;
-			margin: 0;
-			border-top: 1px solid rgba(0, 0, 0, .2);
-			padding: .3em .6em;
-			cursor: pointer;
-		}
-
-		p:first-child {
-			margin-top: -4px;
-		}
-
-		p:hover {
-			background-color: #d3d3d3;
-		}
-
-		#fromSelected,
-		#viaSelected,
-		#toSelected {
-			background-color: #bfbfbf !important;
-		}
-	}
-
-	.history {
-		padding: 0 4px;
-		overflow: hidden;
-		user-select: none;
-
-		.flex-row {
-			justify-content: space-between;
-			cursor: pointer;
-			padding: .3em .6em .3em .3em;
-			margin: 0;
-			background-color: white;
-			color: black;
-			font-size: 1.2em;
-			border-bottom: 1px solid rgba(0, 0, 0, .2);
-		}
-
-		.flex-row:last-child {
-			border-bottom: unset;
-		}
-
-		.via {
-			font-size: smaller;	
-			font-weight: 200;
-		}
-
-		.from,
-		.to {
-			width: 40%;
-		}
-
-		.to {
-			text-align: right;
-		}
-
-		small::after {
-			content: "\a";
-			white-space: pre;
-		}
-
-		.icon-arrow1 {
-			width: 25px;
-		}
-	}
-}
-
-.journeyView {
-	tbody:not(:last-child) {
-		border-bottom: 1px solid rgba(0, 0, 0, .2);
-	}
-
-	thead>tr:nth-child(2) {
-		border-bottom: 2px solid #ccc;
-	}
-
-	p {
-		color: white;
-		width: 100%;
-	}
-
-	p::before {
-		filter: drop-shadow( 0 0 5px rgba(0, 0, 0, .6) );
-		margin-right: 4px;
-		vertical-align: sub;
-	}
-
-	p.change,
-	p.walk,
-	p.transfer {
-		text-shadow: 0 0 15px rgba(0, 0, 0, .6);
-		text-align: center;
-	}
-
-	p.change::before {
-		content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M9 3 5 6.99h3V14h2V6.99h3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99z" fill="white"/></svg>');
-	}
-
-	p.walk::before {
-		content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M13.5 5.5c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2M9.8 8.9 7 23h2.1l1.8-8 2.1 2v6h2v-7.5l-2.1-2 .6-3C14.8 12 16.8 13 19 13v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1L6 8.3V13h2V9.6z" fill="white"/></svg>');
-	}
-
-	p.transfer::before {
-		content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="m21.71 11.29-9-9a.996.996 0 0 0-1.41 0l-9 9a.996.996 0 0 0 0 1.41l9 9c.39.39 1.02.39 1.41 0l9-9a.996.996 0 0 0 0-1.41M14 14.5V12h-4v3H8v-4c0-.55.45-1 1-1h5V7.5l3.5 3.5z" fill="white"/></svg>');
-	}
-
-	.link {
-		vertical-align: bottom;
-		cursor: pointer;
-		max-inline-size: 26px;
-		margin: 0 .3em;
-	}
-}
-
-.departuresView {
-	tbody td:nth-child(2) {
-		text-align: unset;
-	}
-
-	tbody td {
-		padding: 5px 3px;
-	}
-}
-
-.remarksView {
-	.flex-row {
-		align-items: center;
-		flex-wrap: nowrap;
-		padding: .5em;
-		border-bottom: 1px solid rgba(0, 0, 0, .4);
-	}
-
-	.flex-row:last-child {
-		border-bottom: unset;
-	}
-
-	span[class^="icon-"] {
-		align-self: start;
-		padding-right: .3em;
-	}
-}
-
-
-#overlay {
-	z-index: 100;
-	backdrop-filter: blur(10px);
-
-	.modal {
-		z-index: 1050;
-		margin: auto;
-		background-color: white;
-		width: max-content;
-		padding: 15px;
-		-webkit-overflow-scrolling: touch;
-	}
-
-	.modal.alert button {
-		float: right;
-	}
-
-	.modal.select {
-		a {
-			width: 100%;
-			margin: 5px 0;
-			text-align: center;
-		}
-
-		a:first-child {
-			margin-top: unset;
-		}
-
-		a:last-child {
-			margin-bottom: unset;
-		}
-	}
-
-	.modal.dialog {
-		padding: unset;
-
-		.body {
-			max-height: 70vh;
-			overflow: scroll;
-		}
-
-		.header {
-			justify-content: space-between;
-			background-color: #33691E;
-			color: white;
-			padding: 15px;
-
-			h4 {
-				margin: 0;
-	 		}
-
-			.icon-close {
-				margin: -15px;
-				padding: 10px;
-				border-left: 1px solid rgba(0, 0, 0, .4);
-				cursor: pointer;
-			}
-
-			.icon-close:hover {
-				background: rgba(0, 0, 0, .4);
-			}
-		}
-	}
-}
-
-input,
-button,
-.button,
-.selector {
-	margin: 4px;
-}
-
-input[type="datetime-local"],
-input[type="date"],
-input[type="time"],
-input[type="text"] {
-	cursor: pointer;
-	box-sizing: border-box;
-	padding: .3em .5em;
-	font-size: 1.5em;
-	padding: 7px;
-	border: none;
-	outline: none;
-	background-color: white;
-	color: black;
-	border-radius: 0;
-}
-
-input[type="checkbox"] {
-	transform: scale(1.5);
-}
-
-button,
-.button,
-.selector label {
-	background-color: white;
-	color: black;
-	cursor: pointer;
-	user-select: none;
-}
-
-button, .button {
-	width: max-content;
-	padding: 8px 20px;
-	transition: background-color 300ms;
-	border: none;
-}
-
-button:hover, .button:hover {
-	background-color: #d3d3d3;
-}
-
-button.color, .button.color {
-	background-color: #43a047;
-	color: white;
-}
-
-button.color:hover, .button.color:hover {
-	background-color: #388e3c;
-}
-
-.arrowButton {
-	cursor: pointer;
-	user-select: none;
-	height: 72px;
-	width: 72px;
-	margin: 0 auto;
-	transition: transform 150ms;
-	filter: invert();
-}
-
-.selector {
-	display: flex;
-
-	input {
-		display: none;
-	}
-
-	input + label {
-		background: #d3d3d3;
-	}
-
-	input:checked + label {
-		background: white
-	}
-
-	label {
-		display: flex;
-		justify-content: center;
-		align-items: center;
-		user-select: none;
-		padding: 0 10px;
-	}
-
-	label:after {
-		font-size: .9rem;
-		color: black;
-		text-align: center;
-		line-height: .9rem;
-		margin-top: 2px;
-	}
-
-	div:not(:last-child),
-	label:not(:last-child) {
-		border-right: 1px solid #bbb;
-	}
-
-	.icon-ice,
-	.icon-ic,
-	.icon-icice,
-	.icon-dzug,
-	.icon-regional {
-		font-style: italic;
-	}
-
-	.icon-tram:after,
-	.con-bus:after,
-	.icon-ferry:after,
-	.icon-taxi:after {
-		font-size: 0.6rem;
-	}
-
-	.icon-ice:after      { content: 'ICE'; }
-	.icon-ic:after       { content: 'IC'; }
-	.icon-icice:after    { content: 'IC ICE'; }
-	.icon-dzug:after     { content: 'D'; }
-	.icon-regional:after { content: 'NV'; }
-	.icon-suburban:after { content: 'S'; }
-	.icon-subway:after   { content: 'U'; }
-	.icon-tram:after     { content: 'Tram'; }
-	.icon-bus:after      { content: 'Bus'; }
-	.icon-ferry:after    { content: 'Ferry'; }
-	.icon-taxi:after     { content: 'Taxi'; }
-}
-
-.selector.rectangular label {
-	height: 32px;
-	width: 32px;
-	padding: 3px;
-	font-weight: bold;
-	overflow: hidden;
-}
-
-@media (max-width: 650px) {
-	.filler {
-		flex: unset !important;
-	}
-
-	button[type="submit"]{
-		flex-basis: 100%;
-		justify-content: center;
-	}
-
-	header {
-		padding-top: 10px;
-
-		.mode-changers {
-			margin: auto;
-		}
-
-		h3 {
-			margin: 8px 0;
-		}
-	}
-
-	#overlay {
-		.modal.dialog {
-			width: 100vw;
-			height: 100vh;
-		}
-	}
-
-}
-
-@media (max-width: 799px) {
-	header {
-		.icon-back {
-			left: 10px;
-		}
-	}
-
-	.searchView {
-		padding: 10px;
-	}
-
-	.flex-row {
-		flex-wrap: wrap;
-	}
-
-	.flex-row.nowrap {
-		flex-wrap: unset;
-	}
-
-	.journeysView {
-		.arrowButton {
-			margin: 15px auto;
-		}
-	}
-
-	.arrowButton {
-		width: 48px;
-		height: 48px;
-	}
-}
-
-@media (min-width: 800px) {
-	.searchView {
-		form,
-		.history {
-			width: 80vw;
-			max-width: 700px;
-			color: white;
-		}
-
-		#date {
-			width: 50%;
-		}
-
-		#time {
-			width: 30%;
-		}
-	}
-	
-	.journeysView {
-		.arrowButton.flipped {
-			margin-top: 45px;
-		}
-
-		table {
-			margin: 15px auto;
-		}
-	}
-
-	table {
-		overflow: hidden;
-		border: none;
-		margin: 50px auto;
-		width: 80vw;
-	}
-
-	#overlay {
-		.modal.dialog {
-			width: 600px;
-		}
-	}
-}
diff --git a/src/coach-sequence/index.js b/src/coach-sequence/index.js
@@ -1,7 +1,7 @@
 import { padZeros, sleep } from '../helpers.js';
 import { mapInformation } from './DB/DBMapping.js';
 
-const dbCoachSequenceTimeout    = 1000;
+const dbCoachSequenceTimeout    = 1500;
 export const coachSequenceCache = {};
 
 const rawDBCoachSequence = async (category, number, evaNumber, date, retry = 2) => {

@@ -17,7 +17,7 @@ const rawDBCoachSequence = async (category, number, evaNumber, date, retry = 2) 
 
 		return await fetch(`/db/vehicle-sequence?${searchParams}`).then(x => x.json());
 	} catch (e) {
-		sleep(dbCoachSequenceTimeout);
+		await sleep(dbCoachSequenceTimeout);
 		if (retry) return rawDBCoachSequence(category, number, evaNumber, date, retry - 1);
 	}
 }
diff --git a/src/dataStorage.js b/src/dataStorage.js
@@ -4,11 +4,8 @@ export let db;
 
 const dbName = !isDevServer ? 'trainsearch' : 'trainsearch_dev';
 
-class IDBStorage {
-
-	constructor(idb) {
-		this.idb = idb;
-	}
+class DataStorage {
+	constructor(idb) { this.idb = idb; }
 
 	static async open() {
 		const idb = await openDB(dbName, 3, {

@@ -25,6 +22,7 @@ class IDBStorage {
 						db.createObjectStore('journeys', {keyPath: 'slug'});
 						db.createObjectStore('journeysHistory', {autoIncrement: true});
 						db.createObjectStore('settings');
+
 					case 1:
 						/*
 						 * in database scheme v2, there are the following data stores:

@@ -102,7 +100,7 @@ class IDBStorage {
 			}
 		});
 
-		return new IDBStorage(idb);
+		return new DataStorage(idb);
 	}
 
 	async addJourneys(journeyEntries, overviewEntry, historyEntry) {

@@ -111,9 +109,7 @@ class IDBStorage {
 		const journeysOverviewStore = tx.objectStore('journeysOverview');
 		const journeysHistoryStore  = tx.objectStore('journeysHistory');
 
-		let proms = journeyEntries.map(j => {
-			journeyStore.put(j);
-		});
+		let proms = journeyEntries.map(j => journeyStore.put(j));
 
 		if (overviewEntry.historyEntryId === undefined)
 			overviewEntry.historyEntryId = await journeysHistoryStore.put(historyEntry);

@@ -140,30 +136,15 @@ class IDBStorage {
 	}
 
 	async addHistoryEntry(entry) {
-		await this.idb.put('journeysHistory', entry);
+		return await this.idb.put('journeysHistory', entry);
 	}
 
-	async updateHistoryEntry(key, entry) {
-		await this.idb.put('journeysHistory', entry, key);
-	}
-
-	async getHistoryEntry(key) {
-		return await this.idb.get('journeysHistory', key);
-	}
-
-
-	async getSettings() {
-		return await this.idb.get('settings', 'settings');
+	async updateHistoryEntry(id, entry) {
+		return await this.idb.put('journeysHistory', entry, id);
 	}
 
-	async modifySettings(prevSettings, callback) {
-		const tx = this.idb.transaction('settings', 'readwrite');
-		const newSettings = callback(prevSettings);
-		await Promise.all([
-			tx.store.put(newSettings, 'settings'),
-			tx.done
-		]);
-		return newSettings;
+	async getHistoryEntry(id) {
+		return await this.idb.get('journeysHistory', id);
 	}
 
 	async getJourneysOverview(slug) {

@@ -171,8 +152,7 @@ class IDBStorage {
 	}
 
 	async getJourney(refreshToken) {
-		const journeyObject  = await this.idb.get('journey', refreshToken);
-		return journeyObject;
+		return await this.idb.get('journey', refreshToken);
 	}
 
 	async updateJourney(data) {

@@ -183,7 +163,7 @@ class IDBStorage {
 export const clearDataStorage = async () => await deleteDB(dbName);
 export const initDataStorage  = async () => {
 	try {
-		db = await IDBStorage.open();
+		db = await DataStorage.open();
 	} catch(e) {
 		console.log("IndexedDB initialization failed: ", e);
 		alert("IndexedDB initialization failed!");
diff --git a/src/departuresView.js b/src/departuresView.js
@@ -1,114 +1,139 @@
-import { html, nothing, render } from 'lit-html';
-import { settings } from './settings.js';
+import { LitElement, html, nothing } from 'lit';
+import { LitOverlay } from './LitOverlay.js';
+
+import { sleep, queryBackgroundColor, setThemeColor } from './helpers.js';
 import { platformTemplate, timeTemplate } from './templates.js';
-import { ElementById, setThemeColor, queryBackgroundColor } from './helpers.js';
 import { processLeg } from './app_functions.js';
-import { formatDateTime, formatDuration, formatPrice } from './formatters.js';
-import { showAlertModal, showLoader, hideOverlay, showModal } from './overlays.js';
-import { go } from './router.js';
-import { getHafasClient, client } from './hafasClient.js';
+import { getHafasClient } from './hafasClient.js';
 import { t } from './languages.js';
 
-const departuresTemplate = (data, profile, stopId, when) => {
-	let changes = 0;
-	let lastArrival;
-
-	return html`
-		<div class="header-container">
-			<header>
-				<a id="back" class="icon-back invisible" title="${t('back')}" @click=${() => history.back()}></a>
-				<div class="container">
-					<h3>Departures from ${data.name}</h3>
-				</div>
-				<a id="reload" class="icon-reload" title="${t("reload")}" @click=${() => refreshDeparturesView(profile, stopId, when)}></a>
-			</header>
-		</div>
-		<div class="container departuresView">
-			<div class="card">
-			<table>
-				<thead>
-					<tr>
-						<th>Time</th>
-						<th class="station"></th>
-						<th>${t('platform')}</th>
-					</tr>
-				</thead>
-				<tbody>
-					${(data.departures || []).map(departure => html`
-						<tr @click=${() => go(`/t/${profile}/${departure.tripId}`)}>
-							<td class="${departure.cancelled ? 'cancelled' : nothing}"><span>${timeTemplate(departure)}</span></td>
-							<td class="${departure.cancelled ? 'cancelled' : nothing}"><span>${departure.line.name}${departure.direction ? html` → ${departure.direction}` : nothing}</span></td>
-							${departure.cancelled ? html`
-								<td><span class="cancelled-text">${t('cancelled-ride')}</span></td>
-							` : html`
-								<td>${platformTemplate(departure)}</td>
-							`}
-						</tr>
-					`)}
-				</tbody>
-			</table>
-			</div>
-		</div>
-	`;
-};
-
-export const departuresView = async (match, isUpdate) => {
-	if (!isUpdate) showLoader();
-
-	let profile, stopId, when, data;
-
-	try {
-		profile = match[0];
-		stopId  = match[1];
-
-		if (match[2]) when = new Date(parseInt(match[2].substring(1)));
-
-		const client = await getHafasClient(profile);
-		const [ {departures}, stopInfo ] = await Promise.all([
-			client.departures(stopId, { when }),
-			client.stop(stopId),
-		]);
-
-		for (let departure of departures) {
-			processLeg(departure);
-		};
-
-		data = { ...stopInfo, departures };
-	} catch(e) {
-		showAlertModal(e.toString());
-		throw e;
+import { baseStyles, helperStyles, flexboxStyles, iconStyles, headerStyles, cardStyles, departuresViewStyles } from './styles.js';
+
+class DeparturesView extends LitOverlay {
+	static styles = [
+		baseStyles,
+		helperStyles,
+		flexboxStyles,
+		iconStyles,
+		headerStyles,
+		cardStyles,
+		departuresViewStyles
+	];
+
+	static properties = {
+		profile:    { attribute: true },
+		stopId:     { attribute: true },
+		when:       { attribute: true, type: Number },
+		viewState:  { state: true },
+		isUpdating: { state: true }
+	};
+
+	constructor(...args) {
+		super(...args);
+
+		this.viewState  = null;
+		this.isUpdating = false;		
 	}
 
-	hideOverlay();
-
-	render(departuresTemplate(data, profile, stopId, when), ElementById('content'));
-	setThemeColor(queryBackgroundColor('header'));
+	async connectedCallback() {
+		super.connectedCallback();
+		await sleep(100);
 
-	if (history.length > 0) ElementById('back').classList.remove('invisible');
-};
+		setThemeColor(queryBackgroundColor(this.renderRoot, 'header'));
+	}
 
-const refreshDeparturesView = async (profile, stopId, when) => {
-	document.querySelector('.icon-reload').classList.add('spinning');
+	async updated(previous) {
+		super.updated(previous);
 
-	let data;
+		if (isDevServer) console.info('updated(): ', previous);
 
-	try {
-		const client = await getHafasClient(profile);
-		const [ {departures}, stopInfo ] = await Promise.all([
-			client.departures(stopId, { when }),
-			client.stop(stopId),
-		]);
+		if (previous.has('stopId')) {
+			this.viewState  = null;
+			await this.updateViewState();
+		}
+	}
 
-		for (let departure of departures) {
-			processLeg(departure);
-		};
+	renderContent () {
+		return this.viewState !== null ? html`
+			<div class="header-container">
+				<header>
+					<a class="icon-back ${history.length !== 1 ? '': 'invisible'}" title="${t('back')}" @click=${() => history.back()}></a>
+					<div class="container">
+						<h3>Departures from ${this.viewState.name}</h3>
+					</div>
+					<a class="icon-reload ${this.isUpdating ? 'spinning' : ''}" title="${t("reload")}" @click=${this.updateViewState}></a>
+				</header>
+			</div>
+			<div class="container">
+				<div class="card">
+				<table>
+					<thead>
+						<tr>
+							<th>Time</th>
+							<th class="station"></th>
+							<th>${t('platform')}</th>
+						</tr>
+					</thead>
+					<tbody>
+						${(this.viewState.departures || []).map(departure => html`
+							<tr @click=${() => window.location = `#/t/${this.profile}/${departure.tripId}`}>
+								<td class="${departure.cancelled ? 'cancelled' : nothing}">
+									<span>${timeTemplate(departure)}</span>
+								</td>
+								<td class="${departure.cancelled ? 'cancelled' : nothing}">
+									<span>${departure.line.name}${departure.direction ? html` → ${departure.direction}` : nothing}</span>
+								</td>
+								${departure.cancelled ? html`
+									<td><span class="cancelled-text">${t('cancelled-ride')}</span></td>
+								` : html`
+									<td>${platformTemplate(departure)}</td>
+								`}
+							</tr>
+						`)}
+					</tbody>
+				</table>
+				</div>
+			</div>
+			<footer-component></footer-component>
+		` : html`
+			<div class="header-container">
+				<header>
+					<a class="icon-back ${history.length !== 1 ? '': 'invisible'}" title="${t('back')}" @click=${() => history.back()}></a>
+					<div class="container">
+						<h3>Departures from ...</h3>
+					</div>
+					<a class="icon-reload ${this.isUpdating ? 'spinning' : ''}" title="${t("reload")}" @click=${this.updateViewState}></a>
+				</header>
+			</div>
+			<div class="spinner"></div>
+		`;
+	}
 
-		data = { ...stopInfo, departures };
-	} catch(e) {
-		showAlertModal(e.toString());
-		throw e;
+	updateViewState = async () => {
+		this.isUpdating = true;
+		try {
+			const when   = this.when;
+			const client = await getHafasClient(this.profile);
+
+			const [ {departures}, stopInfo ] = await Promise.all([
+				client.departures(this.stopId, { when }),
+				client.stop(this.stopId),
+			]);
+
+			departures.forEach(departure => processLeg(departure));
+
+			this.viewState = {
+				...stopInfo,
+				departures
+			};
+
+			if (isDevServer) console.info('viewState: ', this.viewState);
+		} catch(e) {
+			this.showAlertOverlay(e.toString());
+			console.error(e);
+		}
+		this.isUpdating = false;
 	}
+}
 
-	render(departuresTemplate(data, profile, stopId, when), ElementById('content'));
-	document.querySelector('.icon-reload').classList.remove('spinning');
-};
+customElements.define('departures-view', DeparturesView);
diff --git a/src/ds100.js b/src/ds100.js
@@ -0,0 +1,24 @@
+import { isEmptyObject } from './helpers.js';
+import { default as ds100 } from '../ds100.json';
+
+let ds100R = {};
+
+export const initDS100R = () => Object.keys(ds100).forEach(
+	(id) => (ds100[id].split(', ').forEach(
+		name => (ds100R[name] = id)
+	))
+);
+
+
+export const getDS100byIBNR = id => {
+	if (!ds100[Number(id)]) return null;
+
+	return ds100[Number(id)];
+};
+
+export const getIBNRbyDS100 = name => {
+	if (isEmptyObject(ds100R)) initDS100R();
+	if (!ds100R[name]) return null;
+
+	return ds100R[name];
+};
diff --git a/src/footerComponent.js b/src/footerComponent.js
@@ -0,0 +1,22 @@
+import { LitElement, html } from 'lit';
+
+import { baseStyles, helperStyles, footerStyles } from './styles.js';
+
+class FooterElement extends LitElement {
+	static styles = [
+		baseStyles,
+		helperStyles,
+		footerStyles,
+	];
+
+	render() {
+		return html`
+			<footer class="center">
+				<a href="https://git.ctu.cx/trainsearch" title="commit ${COMMIT} from ${COMMITDATE}">Source-Code (${VERSION})</a>
+				<a href="https://ctu.cx/imprint.html">Imprint</a>
+			</footer>
+		`;
+	}
+}
+
+customElements.define('footer-component', FooterElement);
diff --git a/src/formatters.js b/src/formatters.js
@@ -1,17 +1,19 @@
-import { ds100Name } from './app_functions.js';
+import { getDS100byIBNR } from './ds100.js';
 import { padZeros } from './helpers.js';
-import { languages } from './languages.js';
+import { settingsState } from './settings.js';
 
-export const formatName = (point) => {
+export const formatPoint = point => {
 	switch (point.type) {
 		case 'stop':
 		case 'station':
-			let nameHTML = point.name;
+			let station = point.name;
 
-			const ds100 = ds100Name(point.id);
-			if (ds100 !== null) nameHTML += ` (${ds100})`;
+			if (settingsState.showDS100) {
+				const ds100 = getDS100byIBNR(point.id);
+				if (ds100 !== null) station += ` (${ds100})`;
+			}
 
-			return nameHTML;
+			return station;
 
 		case 'location':
 			if (point.address) return point.address;

@@ -22,36 +24,10 @@ export const formatName = (point) => {
 	};
 };
 
-export const formatDateTime = (date, format) => {
-	if (format != null) {
-		switch (format) {
-			case 'full':
-				return padZeros(date.getHours()) + ':' + padZeros(date.getMinutes()) + ', ' + date.getDate() + '.' + (date.getMonth() + 1) + '.' + date.getFullYear();
-				break;
+export const formatDate = date => `${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()}`;
+export const formatTime = date => `${padZeros(date.getHours())}:${padZeros(date.getMinutes())}`;
 
-			case 'date':
-				return date.getDate() + '.' + (date.getMonth() + 1) + '.' + date.getFullYear();
-				break;
-
-			case 'time':
-				return `${padZeros(date.getHours())}:${padZeros(date.getMinutes())}`;
-				break;
-
-			default:
-				return false;
-				break;
-		}
-
-	}
-
-	if (date.toLocaleDateString() !== new Date().toLocaleDateString()) {
-		return padZeros(date.getHours()) + ':' + padZeros(date.getMinutes()) + ', ' + date.getDate() + '.' + (date.getMonth() + 1) + '.';
-	} else {
-		return padZeros(date.getHours()) + ':' + padZeros(date.getMinutes());
-	}
-};
-
-export const formatDuration = (duration) => {
+export const formatDuration = duration => {
 	const mins = duration / 60000;
 	const h    = Math.floor(mins / 60);
 	const m    = mins % 60;

@@ -97,13 +73,13 @@ export const formatPrice = price => {
 export const formatTrainTypes = info => {
 	const counts = {};
 
-	for (let group of info.sequence?.groups) {
+	info.sequence?.groups.forEach(group => {
 		const name = group.baureihe?.name;
 
-		if (!name) continue;
+		if (!name) return;
 
 		counts[name] = (counts[name] ? counts[name] : 0) + 1;
-	}
+	});
 
 	return Object.entries(counts).map(([name, count]) => {
 		let text = "";

@@ -120,7 +96,9 @@ export const formatTrainTypes = info => {
 	}).join(" + ");
 };
 
-export const formatLineAdditionalName = (line) => {
+export const formatLineDisplayName = line => line?.name || line?.operator?.name || "???";
+
+export const formatLineAdditionalName = line => {
 	if (!line.name) return null;
 	const splitName = line.name.split(' ');
 

@@ -130,5 +108,3 @@ export const formatLineAdditionalName = (line) => {
 		return null;
 	}
 };
-
-export const formatLineDisplayName = (line) => line?.name || line?.operator?.name || "???";
diff --git a/src/hafasClient.js b/src/hafasClient.js
@@ -1,54 +1,38 @@
-import { createClient as createVendoClient } from "db-vendo-client";
-import { profile as dbnavProfile } from "db-vendo-client/p/dbnav/index.js";
-
-const clients = {};
-let   createHafasClient;
-
-export let client;
-
-export const initHafasClient = async profileName => {
-	client = await getHafasClient(profileName);
-};
+import { createClient as createVendoClient } from 'db-vendo-client';
+import { profile as dbNavProfile } from 'db-vendo-client/p/dbnav/index.js';
+
+let   createHafasClient = null; 
+const clients  = {};
+
+export let   client;
+export const profiles = {
+	db:    { name: "DB",     backend: 'vendo', profile: dbNavProfile },
+	bvg:   { name: "BVG",    backend: 'hafas', profile: (await import('hafas-client/p/bvg/index.js')).profile },
+	nahsh: { name: "NAH.SH", backend: 'hafas', profile: (await import('hafas-client/p/nahsh/index.js')).profile },
+	rmv:   { name: "RMV",    backend: 'hafas', profile: (await import('hafas-client/p/rmv/index.js')).profile },
+	oebb:  { name: "ÖBB",    backend: 'hafas', profile: (await import('hafas-client/p/oebb/index.js')).profile },
+}
 
+export const getDefaultProfile = () => 'db';
 export const getHafasClient = async profileName => {
-	if (!clients[profileName]) {
-		let profile;
-
-		switch(profileName) {
-			case "db":
-				clients[profileName] = createVendoClient(dbnavProfile, "trainsearch", {enrichStations: false});
-				console.info("initialized vendo client");
-				return clients[profileName];
-
-			case "bvg":
-				const { profile: bvgProfile } = await import('hafas-client/p/bvg/index.js');
-				profile = bvgProfile;
-				break;
+	if (!profiles[profileName]) throw "Unknown profile name: " + profileName;
 
-			case "nahsh":
-				const { profile: nahshProfile } = await import('hafas-client/p/nahsh/index.js');
-				profile = nahshProfile;
-				break;
-
-			case "rmv":
-				const { profile: rmvProfile } = await import('hafas-client/p/rmv/index.js');
-				profile = rmvProfile;
-				break;
-
-			case "oebb":
-				const { profile: oebbProfile } = await import('hafas-client/p/oebb/index.js');
-				profile = oebbProfile;
-				break;
-
-			default:
-				throw "Unknown profile name: " + profileName;
+	if (!clients[profileName]) {
+		if (profiles[profileName].backend === 'vendo') {
+			clients[profileName] = createVendoClient(profiles[profileName].profile, APPNAME, {enrichStations: false});
+			if (isDevServer) console.info('initialized vendo client with profile ' + profileName);
 		}
 
-		if (!createHafasClient) createHafasClient = (await import("hafas-client")).createClient;
-
-		clients[profileName] = createHafasClient(profile, "trainsearch");
+		if (profiles[profileName].backend === 'hafas') {
+			if ( createHafasClient === null) createHafasClient = (await import('hafas-client')).createClient;
+			clients[profileName] = createHafasClient(profiles[profileName].profile, APPNAME);
+			if (isDevServer) console.info('initialized hafas client with profile ' + profileName);
+		}
 	}
 
-	console.info("initialized hafas client with profile " + profileName);
 	return clients[profileName];
 }
+
+export const initHafasClient = async profileName => {
+	client = await getHafasClient(profileName);
+};
diff --git a/src/helpers.js b/src/helpers.js
@@ -19,7 +19,8 @@ const loyaltyCardsReverse = {
 };
 
 export const sleep         = delay   => new Promise((resolve) => setTimeout(resolve, delay));
-
+export const isEmptyObject = obj     => Object.keys(obj).length === 0;
+export const padZeros      = str     => (('00' + str).slice(-2));
 export const ElementById   = id      => document.getElementById(id);
 
 export const showElement   = element => element.classList.remove('hidden');

@@ -29,14 +30,8 @@ export const elementHidden = element => element.classList.contains('hidden');
 export const unflipElement = element => element.classList.remove('flipped');
 export const flipElement   = element => element.classList.add('flipped');
 
-export const setThemeColor        = color => document.querySelector('meta[name="theme-color"]').setAttribute('content', color);
-export const queryBackgroundColor = query => window.getComputedStyle(document.querySelector(query)).getPropertyValue('background-color');
-
-export const isEmptyObject = (obj) => Object.keys(obj).length === 0;
-
-export const padZeros = str => {
-	return ('00' + str).slice(-2);
-};
+export const setThemeColor        = color           => document.querySelector('meta[name="theme-color"]').setAttribute('content', color);
+export const queryBackgroundColor = (target, query) => window.getComputedStyle(target.querySelector(query)).getPropertyValue('background-color');
 
 export const isValidDate = date => {
 	const matches = /^(\d{4})[-\/](\d{2})[-\/](\d{2})$/.exec(date);

@@ -53,7 +48,11 @@ export const isValidDate = date => {
 		   composedDate.getFullYear() == y;
 };
 
-export const loyaltyCardToString   = loyaltyCard => `${loyaltyCardsReverse[loyaltyCard.type.toString()]}-${loyaltyCard.discount}-${loyaltyCard.class}`;
+export const loyaltyCardToString   = loyaltyCard =>
+	loyaltyCardsReverse[loyaltyCard.type.toString()] !== 'NONE' ?
+	`${loyaltyCardsReverse[loyaltyCard.type.toString()]}-${loyaltyCard.discount}-${loyaltyCard.class}`
+	: 'NONE';
+
 export const loyaltyCardFromString = string => {
 	const splitedString = string.split('-');
 	if (splitedString[0] === 'NONE') return { type: loyaltyCards[splitedString[0]] };
diff --git a/src/journeyView.js b/src/journeyView.js
@@ -1,220 +1,352 @@
-import { html, nothing, render } from 'lit-html';
-import { cachedCoachSequence } from './coach-sequence/index.js';
+import { LitElement, html, css, nothing } from 'lit';
+import { LitOverlay } from './LitOverlay.js';
+import { createEvents } from 'ics';
+
+import { sleep, queryBackgroundColor, setThemeColor } from './helpers.js';
+import { db } from './dataStorage.js';
 import { settings } from './settings.js';
-import { remarksModalTemplate, platformTemplate, stopTemplate, timeTemplate, footerTemplate } from './templates.js';
-import { ElementById, setThemeColor, queryBackgroundColor } from './helpers.js';
-import { getJourney, refreshJourney } from './app_functions.js';
-import { formatName, formatDateTime, formatDuration, formatPrice, formatTrainTypes, formatLineAdditionalName, formatLineDisplayName } from './formatters.js';
-import { showAlertModal, showLoader, hideOverlay, showModal } from './overlays.js';
-import { go } from './router.js';
 import { t } from './languages.js';
-import { db } from './dataStorage.js';
-import { showSelectModal } from './overlays.js';
-
-const legTemplate = (leg, profile) => {
-	const remarks        = leg.remarks || [];
-	const remarksStatus  = remarks.some((obj) => obj.type === 'status');
-	const remarksWarning = remarks.some((obj) => obj.type === 'warning');
-	const remarksIcon    = remarksWarning ? 'icon-warning' : (remarksStatus ? 'icon-status' : 'icon-hint');
-
-	return html`
-		${leg.walking ? html`
-			<p class="walk">${t(leg.distance === null ? 'walkinfo' : 'walkinfo_meters', formatName(leg.destination), leg.distance)}</p>
-		` : leg.transfer ? html`
-			<p class="transfer">${t('transferinfo', formatName(leg.destination))}</p>
-		` : leg.change ? html`
-			<p class="change">${t('changeinfo', formatDuration(leg.duration))}</p>
-		` : html`
-			<div class="card">
-				<table>
-					<thead>
-						<tr>
-							<td colspan="4">
-								<div class="flex-center"><a href="#/t/${profile}/${leg.tripId}">${formatLineDisplayName(leg.line)}${leg.direction ? html` → ${leg.direction}` : nothing}</a>
-								${leg.cancelled ? html`<b class="cancelled-text">${t('cancelled-ride')}</b>` : nothing}
-								${!!remarks.length ? html`
-									<a class="link ${remarksIcon}" @click=${() => showModal(t('remarks'), remarksModalTemplate(remarks))}></a>
-								` : nothing}</div>
-							</td>
-						</tr>
-						<tr>
-							<td colspan="4">
-								<div class="train-details flex-center">
-									${formatLineAdditionalName(leg.line) ? html`
-										<div>
-											Trip: ${formatLineAdditionalName(leg.line)}
-										</div>
-									` : nothing}
-									${leg.line.trainType ? html`
-										<div>
-											Train type: ${leg.line.trainType}
-										</div>
-									` : nothing}
-									${(leg.arrival && leg.departure) ? html`
-										<div>
-											${t('duration')}: ${formatDuration(leg.arrival - leg.departure)}
-										</div>
-									` : nothing}
-									${leg.loadFactor ? html`
-										<div>
-											${t("load-"+leg.loadFactor)}
-										</div>
-									` : nothing}
-								</div>
-							</td>
-						</tr>
-						<tr>
-							<th>${t('arrival')}</th>
-							<th>${t('departure')}</th>
-							<th class="station">${t('station')}</th>
-							<th>${t('platform')}</th>
-						</tr>
-					</thead>
-					<tbody>
-						${(leg.stopovers || []).map(stop => html`
-							<tr class="stop ${stop.cancelled ? 'cancelled' : nothing}">
-								<td><span>${timeTemplate(stop, 'arrival')}</span></td>
-								<td><span>${timeTemplate(stop, 'departure')}</span></td>
-								<td>${stopTemplate(profile, stop.stop)}</td>
-								<td><span>${platformTemplate(stop)}</span></td>
-							</tr>
-						`)}
-					</tbody>
-				</table>
-			</div>
-		`}
-	`;
-};
+import { getJourney, refreshJourney } from './app_functions.js';
+import { remarksModal, platformTemplate, stopTemplate, timeTemplate } from './templates.js';
+import { formatPoint, formatDate, formatDuration, formatPrice, formatTrainTypes, formatLineAdditionalName, formatLineDisplayName } from './formatters.js';
+import { cachedCoachSequence } from './coach-sequence/index.js';
 
-const journeyTemplate = (data, profile) => {
-	let duration = null;
+import { baseStyles, helperStyles, flexboxStyles, headerStyles, iconStyles, cardStyles, journeyViewStyles } from './styles.js';
 
-	if (data.legs[data.legs.length - 1].arrival && data.legs[0].departure)
-		duration = data.legs[data.legs.length - 1].arrival - data.legs[0].departure;
+class JourneyView extends LitOverlay {
+	static styles = [
+		super.styles,
+		baseStyles,
+		helperStyles,
+		flexboxStyles,
+		iconStyles,
+		headerStyles,
+		cardStyles,
+		journeyViewStyles
+	];
 
-	const legs = [];
-	let changes = 0;
-	let lastArrival;
+	static properties = {
+		profile:       { attribute: true },
+		refreshToken:  { attribute: true, converter: (value) =>  decodeURIComponent(value)},
+		viewState:     { state: true },
+		settingsState: { state: true },
+		isUpdating:    { state: true }
+	};
 
-	for (const leg of data.legs) {
-		if (!leg.walking && !leg.transfer) {
+	constructor (...args) {
+		super(...args);
 
-			// add change
-			if (lastArrival) {
-				let duration = null;
+		this.viewState     = null;
+		this.settingsState = settings.getState();
+		this.isUpdating    = false;		
+	}
 
-				if (leg.departure && lastArrival) {
-					duration = leg.departure - lastArrival;
-				}
+	async connectedCallback () {
+		super.connectedCallback();
+		await sleep(100);
 
-				legs.push({
-					change: true,
-					duration,
-				});
-			}
-			changes++;
+		setThemeColor(queryBackgroundColor(this.renderRoot, 'header'));
+
+		if (this.viewState === null) await this.updateViewState();
+
+		this._unsubscribeSettingsState = settings.subscribe((state) => {
+			this.settingsState = state;
+			this.performUpdate();
+		});
+	}
 
-			lastArrival = leg.arrival;
-		} else if (legs.length) {
+	disconnectedCallback () {
+		super.disconnectedCallback();
+		this.viewState = null;
 
-			// if this is a walking leg and it is the first one, we don't want to
-			// insert a 0 minutes change entry for this
-			lastArrival = leg.arrival;
+		if (typeof this._unsubscribeSettingsState === 'function') {
+			this._unsubscribeSettingsState();
+			this._unsubscribeSettingsState = undefined;
 		}
+	}
+
+	async updated (previous) {
+		super.updated(previous);
+
+		if (isDevServer) console.info('updated(): ', previous);
+
+		if (previous.has('refreshToken')) {
+			this.viewState = null;
+			await this.updateViewState();
+		}
+	}
+
+	renderContent () {
+		return html`
+			<div class="header-container">
+				<header>
+					<a id="back" class="icon-back ${history.length !== 1 ? '': 'invisible'}" title="${t('back')}" @click=${() => history.back()}></a>
+					<div class="container">
+						<a class="icon-reload ${this.isUpdating ? 'spinning' : ''}" title="${t("reload")}" @click=${this.refreshJourney}></a>
+						${this.viewState !== null ? html`
+						<h3>${formatPoint(this.viewState.legs[0].origin)} → ${formatPoint(this.viewState.legs[this.viewState.legs.length - 1].destination)}</h3>
+						<p><b>${t('duration')}: ${formatDuration(this.viewState.duration)} | ${t('changes')}: ${this.viewState.changes-1} | ${t('date')}: ${formatDate(this.viewState.legs[0].plannedDeparture)}${this.settingsState.showPrices && this.viewState.price ? html` | ${t('price')}: <td><span>${formatPrice(this.viewState.price)}</span></td>` : nothing}</b></p>
+						` : html`
+						<h3>... → ...</h3>
+						<p><b>${t('duration')}: ... | ${t('changes')}: ... | ${t('date')}: ...</b></p>
+						`}
+					</div>
+					<a class="icon-dots" title="${t("more")}" @click=${this.moreModal}></a>
+				</header>
+			</div>
 
-		legs.push(leg);
+			${this.viewState !== null ? html`
+			<div class="container journeyView">
+				${this.viewState.legs.map(leg => this.legTemplate(leg))}
+			</div>
+			<footer-component></footer-component>
+			` : html`<div class="spinner"></div>`}
+		`;
 	}
 
-	return html`
-		<div class="header-container">
-			<header>
-				${data.slug ? html`<a class="icon-back" href="#/${data.slug}/${settings.journeysViewMode}" title="${t('back')}"></a>` : nothing}
-				<div class="container">
-					<a class="icon-reload" title="${t("reload")}" @click=${() => refreshJourneyView(profile, data.refreshToken)}></a>
-					<h3>${formatName(data.legs[0].origin)} → ${formatName(data.legs[data.legs.length - 1].destination)}</h3>
-					<p><b>${t('duration')}: ${formatDuration(duration)} | ${t('changes')}: ${changes-1} | ${t('date')}: ${formatDateTime(data.legs[0].plannedDeparture, 'date')}${settings.showPrices && settings.profile === 'db' && data.price ? html` | ${t('price')}: <td><span>${formatPrice(data.price)}</span></td>` : nothing}</b></p>
+	legTemplate (leg) {
+		if (leg.walking) {
+			return html`<p class="walk">${t(leg.distance === null ? 'walkinfo' : 'walkinfo_meters', formatPoint(leg.destination), leg.distance)}</p>`;
+		} else if (leg.transfer) {
+			return html`<p class="transfer">${t('transferinfo', formatPoint(leg.destination))}</p>`;
+		} else if (leg.change) {
+			return html`<p class="change">${t('changeinfo', formatDuration(leg.duration))}</p>`;
+		} else {
+			return html`
+				<div class="card">
+					<table>
+						<thead>
+							<tr>
+								<td colspan="4">
+									<div class="flex-center"><a href="#/t/${this.profile}/${leg.tripId}">${formatLineDisplayName(leg.line)}${leg.direction ? html` → ${leg.direction}` : nothing}</a>
+									${leg.cancelled ? html`<b class="cancelled-text">${t('cancelled-ride')}</b>` : nothing}
+									${leg.remarks.length !== 0 ? html`
+										<a class="link ${leg.remarksIcon}" @click=${() => remarksModal(this, leg.remarks)}></a>
+									` : nothing}</div>
+								</td>
+							</tr>
+							<tr>
+								<td colspan="4">
+									<div class="train-details flex-center">
+										${formatLineAdditionalName(leg.line) ? html`<div>Trip: ${formatLineAdditionalName(leg.line)}</div>` : nothing}
+										${leg.line.trainType                 ? html`<div>${t('trainType')}: ${leg.line.trainType}</div>` : nothing}
+										${(leg.arrival && leg.departure)     ? html`<div>${t('duration')}: ${formatDuration(leg.arrival - leg.departure)}</div>` : nothing}
+										${leg.loadFactor                     ? html`<div>${t(`load-${leg.loadFactor}`)}</div>` : nothing}
+									</div>
+								</td>
+							</tr>
+							<tr>
+								<th>${t('arrival')}</th>
+								<th>${t('departure')}</th>
+								<th class="station">${t('station')}</th>
+								<th>${t('platform')}</th>
+							</tr>
+						</thead>
+						<tbody>
+							${(leg.stopovers || []).map(stop => html`
+								<tr class="stop ${stop.cancelled ? 'cancelled' : nothing}">
+									<td><span>${timeTemplate(stop, 'arrival')}</span></td>
+									<td><span>${timeTemplate(stop, 'departure')}</span></td>
+									<td>${stopTemplate(this.profile, stop.stop)}</td>
+									<td><span>${platformTemplate(stop)}</span></td>
+								</tr>
+							`)}
+						</tbody>
+					</table>
 				</div>
-				<a class="icon-dots" title="${t("more")}" @click=${() => moreModal(profile, data.refreshToken)}></a>
-			</header>
-		</div>
+			`;
+		}
+	}
 
-		<div class="container journeyView">
-			${legs.map(leg => legTemplate(leg, profile))}
-		</div>
+	async updateViewState (viewState ) {
+		this.isUpdating = true;
+		try {
+			if (viewState === undefined) viewState = await getJourney(this.profile, this.refreshToken);
 
-		${footerTemplate}
-	`;
-};
+			if (viewState.slug) {
+				let overviewObject = await db.getJourneysOverview(viewState.slug);
+				let historyObject  = await db.getHistoryEntry(overviewObject.historyEntryId);
 
-export const journeyView = async (match, isUpdate) => {
-	if (!isUpdate) showLoader();
-	let profile, refreshToken, journeyObject;
+				historyObject.lastSelectedJourneyId = viewState.refreshToken;
+
+				await db.updateHistoryEntry(overviewObject.historyEntryId, historyObject);
+			}
 
-	try {
-		profile       = match[0];
-		refreshToken  = decodeURIComponent(match[1]);
-		journeyObject = await getJourney(refreshToken, profile);
 
-		if (journeyObject.slug) {
-			let overviewObject = await db.getJourneysOverview(journeyObject.slug);
-			let historyObject  = await db.getHistoryEntry(overviewObject.historyEntryId);
+			viewState.changes     = 0;
+			viewState.duration    = null;
 
-			historyObject.lastSelectedJourneyId = journeyObject.refreshToken;
+			if (viewState.legs[viewState.legs.length - 1].arrival && viewState.legs[0].departure)
+				viewState.duration = viewState.legs[viewState.legs.length - 1].arrival - viewState.legs[0].departure;
 
-			await db.updateHistoryEntry(overviewObject.historyEntryId, historyObject);
+			let lastArrival;
+			viewState.legs = viewState.legs.flatMap(leg => {
+				if (!leg.remarks) leg.remarks = [];
+
+				let legs = [];
+
+				const remarksStatus  = leg.remarks.some((obj) => obj.type === 'status');
+				const remarksWarning = leg.remarks.some((obj) => obj.type === 'warning');
+				leg.remarksIcon      = remarksWarning ? 'icon-warning' : (remarksStatus ? 'icon-status' : 'icon-hint');
+
+				if (!leg.walking && !leg.transfer) {
+					// add change
+					if (lastArrival) {
+						let duration = null;
+
+						if (leg.departure && lastArrival) duration = leg.departure - lastArrival;
+
+						legs.push({
+							change: true,
+							duration,
+						});
+					}
+					viewState.changes++;
+
+					lastArrival = leg.arrival;
+				} else if (legs.length) {
+					// if this is a walking leg and it is the first one, we don't want to
+					// insert a 0 minutes change entry for this
+					lastArrival = leg.arrival;
+				}
+
+				legs.push(leg);
+
+				return legs
+			});
+
+			this.viewState = viewState;
+
+			//fetch train types after setting the viewState
+			for (const leg of this.viewState.legs) {
+				if (leg.line && leg.line.name) {
+					const [category, number] = leg.line.name.split(' ');
+					const info               = await cachedCoachSequence(category, leg.line.fahrtNr || number, leg.origin.id, leg.plannedDeparture);
+
+					if (info) leg.line.trainType = formatTrainTypes(info);
+					this.requestUpdate();
+				}
+			}
+
+			if (isDevServer) console.info('viewState: ', this.viewState);
+		} catch(e) {
+			this.showAlertOverlay(e.toString());
+			console.error(e);
 		}
-	} catch(e) {
-		console.error(e);
-		await showAlertModal(e.toString());
-		go('/');
-		return;
+		this.isUpdating = false;
 	}
 
-	for (const leg of journeyObject.legs) {
-		if (leg.line && leg.line.name) {
-			const [category, number] = leg.line.name.split(' ');
-			const info               = await cachedCoachSequence(category, leg.line.fahrtNr || number, leg.origin.id, leg.plannedDeparture);
+	async refreshJourney () {
+		if (this.isUpdating !== false) return false;
 
-			if (info) leg.line.trainType = formatTrainTypes(info);
+		this.isUpdating = true;
+		try {
+			const data = await refreshJourney(this.profile, this.refreshToken);
+			await this.updateViewState(data);
+		} catch(e) {
+			this.showAlertOverlay(e.toString());
+			console.error(e);
 		}
+		this.isUpdating = false;
 	}
 
-	hideOverlay();
+	async moreModal () {
+		const options = [
+			{ 'label': !navigator.canShare ? 'copyURL' : 'shareURL', 'action': async () => await this.shareAction()    },
+		];
+
+		if (isDevServer) options.push({ 'label': 'addCalendar', 'action': async () => await this.calendarAction() });
 
-	render(journeyTemplate(journeyObject, profile), ElementById('content'));
-	setThemeColor(queryBackgroundColor('header'));
-};
+		if (this.profile === 'db') options.push({ 'label': 'tickets', 'action': async () => await this.showTicketsModal() });
 
-const refreshJourneyView = async (profile, refreshToken) => {
-	document.querySelector('.icon-reload').classList.add('spinning');
+		this.showSelectOverlay(options);
+	};
 
-	try {
-		await refreshJourney(refreshToken, profile);
-	} catch(e) {
-		showAlertModal(e.toString());
-		document.querySelector('.icon-reload').classList.remove('spinning');
-		throw e;
+	async shareAction () {
+		try {
+			await navigator.share({ url: window.location });
+		} catch (error) {
+			await navigator.clipboard.writeText(window.location);
+			this.showAlertOverlay('URL has been copied to clipboard.');
+		}
 	}
 
-	journeyView([profile, refreshToken], true);
-	document.querySelector('.icon-reload').classList.remove('spinning');
-};
+	async showTicketsModal () {
+		try {
+			this.showLoaderOverlay();
+			await this.refreshJourney();
 
-const moreModal = (profile, refreshToken) => {
-	const options = [
-		{ 'label': !navigator.canShare ? t('copyURL') : t('shareURL'), 'action': () => { shareAction(); hideOverlay(); }},
-	];
+			if (this.viewState.tickets === undefined) {
+				await showAlertOverlay('No ticket data available');
+				return false;
+			}
+
+			this.showDialogOverlay('tickets', html`
+				${this.viewState.tickets.map(ticket => html`
+				<div class="flex-row">
+					<div class="icon-ticket"></div>
+					<div>
+						<b>${ticket.name} <small>${ticket.firstClass ? `1. ${t('class')}` : nothing}</small></b><br>
+						<small>${ticket.addDataTicketDetails}</small>
+						<br>
+						<b>${ticket.priceObj.amount / 100}</b>
+					</div>
+				</div>
+				`)}
+				<style>${css`
+					.flex-row{
+						flex-wrap: nowrap;
+						padding: .5em;
+						border-bottom: 1px solid rgba(0, 0, 0, .4);
+					}
+
+					.flex-row:last-child {
+						border-bottom: unset;
+					}
+
+					.icon-ticket {
+						align-self: start;
+						padding-right: .3em;
+					}
+				`}</style>
+			`);
+		} catch(e) {
+			this.showAlertOverlay(e.toString());
+			console.error(e);
+		}
+	};
 
-	showSelectModal(options);
-};
+	async calendarAction () {
+		let events = [];
 
-const shareAction = async () => {
-	try {
-		await navigator.share({
-			url: window.location,
+		this.viewState.legs.forEach(leg => {
+			if (!leg.line) return;
+
+			const departureStop = leg.origin.location
+
+			events.push({
+				productId:   APPNAME,
+				created:     (new Date).getTime(),
+				start:       leg.plannedDeparture.getTime(),
+				end:         leg.plannedArrival.getTime(),
+				title:       `${leg.line.name} → ${formatPoint(leg.destination)}`,
+				url:         window.location.href,
+				geo:         { lat: departureStop.latitude , lon: departureStop.longitude },
+				description: `${leg.line.name} → ${leg.direction}\n
+				dep ${timeTemplate(departureStop, 'departure')} ${formatPoint(departureStop)}`,
+			});
 		});
-	} catch (error) {
-		navigator.clipboard.writeText(window.location);
-	}
-};
 
+		const file = await new Promise((resolve, reject) => {
+			const { error, value } = createEvents(events);
+
+			if (error) reject(error);
+
+			resolve(new File([value], 'event.ics', { type: 'text/calendar' }));
+		});
+
+		window.location = URL.createObjectURL(file);
+	};
+}
+
+customElements.define('journey-view', JourneyView);
diff --git a/src/journeysCanvas.js b/src/journeysCanvas.js
@@ -0,0 +1,453 @@
+import { LitElement, html, css } from 'lit';
+import { LitOverlay } from './LitOverlay.js';
+
+import { sleep } from './helpers.js';
+import { formatTrainTypes, formatLineDisplayName, formatTime } from './formatters.js'
+import { cachedCoachSequence, coachSequenceCache, coachSequenceCacheKey } from './coach-sequence/index.js';
+
+export class JourneysCanvas extends LitOverlay {
+	static properties = {
+		canvasState: { state: true },
+	};
+
+	static styles = [
+		super.styles,
+		css`
+		canvas {
+			display: block;
+		}
+		`
+	];
+
+	static colors = {
+		fill: {
+			'tram': '#cc5555',
+			'subway': '#5555cc',
+			'suburban': '#55aa55',
+			'nationalExpress': '#fff',
+			'national': '#fff',
+			'regionalExpress': '#888',
+			'regional': '#888',
+			'bus': '#aa55aa',
+			default: '#888'
+		},
+		text: {
+			'nationalExpress': '#ee3333',
+			'national': '#ee3333',
+			default: '#fff'
+		},
+		cancelFill: {
+			'tram': '#fff',
+			default: '#cc4444ff',
+		},
+		icon: {
+			'walk': 'directions_walk',
+			'transfer': 'directions_transfer',
+			'subway': 'directions_subway',
+			'bus': 'directions_bus',
+			'tram': 'tram',
+			default: 'train'
+		},
+		loadFactor: {
+			'low-to-medium':      [ '#777', '#ccc', '#ccc' ],
+			'high':               [ '#777', '#777', '#ccc' ],
+			'very-high':          [ '#ee8800', '#ee8800', '#ee8800'  ],
+			'exceptionally-high': [ '#cc3300', '#cc3300', '#cc3300' ],
+		}
+	};
+
+	constructor (...args) {
+		super(...args);
+
+		this.canvasElement = null;
+		this.canvasContext = null;
+		this.canvasState = {
+			handlersConnected: false,
+			textCache: {},
+			animationInterval: null,
+			offsetX: 0,
+			dpr: (window.devicePixelRatio || 1),
+			lastAnimationUpdate: 0,
+			firstDeparture: 0,
+			scaleFactor: 0,
+			lastArrival: 0,
+			rectWidth: 0,
+			padding: 0,
+			rectWidthWithPadding: 0
+		};
+
+		this.canvasState.offsetX = (window.innerWidth / this.canvasState.dpr) > 600 ? 140 : 80;
+	};
+
+	updated (previous) {
+		super.updated(previous);
+		if (isDevServer) console.info('canvasState: ', this.canvasState);
+	}
+
+	getCanvas = () => html`<canvas @mousedown=${this.mouseDownHandler} @touchstart=${this.mouseDownHandler}></canvas>`;
+
+	connectCanvas = () => {
+		if (!this.canvasState.handlersConnected) {
+			window.addEventListener('mouseup',   this.mouseUpHandler);
+			window.addEventListener('touchend',  this.mouseUpHandler);
+			window.addEventListener('mousemove', this.mouseMoveHandler);
+			window.addEventListener('touchmove', this.mouseMoveHandler);
+			window.addEventListener('resize',    this.resizeHandler);
+			window.addEventListener('zoom',      this.resizeHandler);
+			this.canvasState.handlersConnected = true;
+		}
+
+		if (this.canvasElement === null) {
+			const canvasElement = this.renderRoot.querySelector('canvas');
+
+			if (canvasElement !== null) {
+				this.canvasElement = canvasElement;
+				this.canvasContext = this.canvasElement.getContext('2d');
+				this.resizeHandler();
+			}
+		}
+	}
+
+	disconnectCanvas = () => {
+		window.removeEventListener('mouseup',   this.mouseUpHandler);
+		window.removeEventListener('touchend',  this.mouseUpHandler);
+		window.removeEventListener('mousemove', this.mouseMoveHandler);
+		window.removeEventListener('touchmove', this.mouseMoveHandler);
+		window.removeEventListener('resize',    this.resizeHandler);
+		window.removeEventListener('zoom',      this.resizeHandler);
+		this.canvasState.handlersConnected = false;
+		this.canvasElement = null;
+		this.canvasContext = null;
+	}
+
+	resizeHandler = () => {
+		if (this.canvasContext === null) return true;
+
+		this.canvasState.dpr = window.devicePixelRatio || 1;
+
+		this.canvasState.rectWidth            = (window.innerWidth / this.canvasState.dpr) > 600 ? 100 : 80;
+		this.canvasState.padding              = (window.innerWidth / this.canvasState.dpr) > 600 ? 20 : 5;
+		this.canvasState.rectWidthWithPadding = this.canvasState.rectWidth + 2 * this.canvasState.padding;
+
+		const rect = this.renderRoot.querySelector('header').getBoundingClientRect();
+		this.canvasElement.width  = window.innerWidth * this.canvasState.dpr;
+		this.canvasElement.height = (window.innerHeight - rect.height) * this.canvasState.dpr;
+
+		this.canvasContext.restore();
+		this.canvasContext.save();
+		this.canvasContext.scale(this.canvasState.dpr, this.canvasState.dpr);
+
+		this.canvasState.lastAnimationUpdate = 0;
+		this.renderCanvas();
+	};
+
+	mouseUpHandler = event => {
+		if (this.canvasState.dragging && this.canvasState.isClick) {
+			const x = event.x || event.changedTouches[0].pageX;
+			event.preventDefault();
+
+			const num = Math.floor((x - this.canvasState.offsetX + 2 * this.canvasState.padding) / this.canvasState.rectWidthWithPadding) + this.viewState.indexOffset;
+			if (num >= 0) {
+				if (num < this.viewState.journeys.length) {
+					const j = this.viewState.journeys[num];
+					window.location = `#/j/${this.viewState.profile}/${j.refreshToken}`;
+				} else if (this.viewState.laterRef) {
+					this.moreJourneys('later');
+				}
+			} else if (this.viewState.earlierRef) {
+				this.moreJourneys('earlier');
+			}
+		}
+
+		this.canvasState.dragging = false;
+		this.canvasState.isClick  = false;
+	}
+
+	mouseDownHandler = event => {
+		const x = event.x || event.changedTouches[0].pageX;
+		this.canvasState.dragStartMouse = x;
+		this.canvasState.dragStartOffset = this.canvasState.offsetX;
+		this.canvasState.dragging = true;
+		this.canvasState.isClick = true;
+	}
+
+	mouseMoveHandler = event => {
+		if (this.canvasState.dragging) {
+			event.preventDefault();
+			const x = event.x || event.changedTouches[0].pageX;
+			this.canvasState.offsetX = this.canvasState.dragStartOffset - (this.canvasState.dragStartMouse - x);
+			if (Math.abs(this.canvasState.dragStartMouse - x) > 20) this.canvasState.isClick = false;
+			this.renderCanvas();
+			return true;
+		}
+	}
+
+	makeTextCache = (text, color, fixedHeight) => {
+		const cacheCanvas        = document.createElement('canvas');
+		const cacheCanvasContext = cacheCanvas.getContext('2d');
+		let height, width;
+
+		cacheCanvasContext.shadowColor = '#00000080';
+
+		if (fixedHeight) {
+			height = 15;
+			cacheCanvasContext.font = `${height}px sans-serif`;
+			width = cacheCanvasContext.measureText(text).width;
+		} else {
+			const measureAccuracy = 50;
+			cacheCanvasContext.font = `${measureAccuracy}px sans-serif`;
+
+			width  = this.canvasState.rectWidth - 10;
+			height = Math.abs(measureAccuracy * (width / (1 - cacheCanvasContext.measureText(text).width)));
+		}
+
+		cacheCanvas.width  = width * this.canvasState.dpr;
+		cacheCanvas.height = Math.ceil(height * 1.5) * this.canvasState.dpr;
+		cacheCanvasContext.scale(this.canvasState.dpr, this.canvasState.dpr);
+
+		cacheCanvasContext.font      = `${height}px sans-serif`;
+		cacheCanvasContext.fillStyle = color;
+		cacheCanvasContext.fillText(text, 0, height);
+
+		return cacheCanvas;
+	};
+
+	getTextCache = (text, color, fixedHeight) => {
+		const index = `${text}|${color}|${this.canvasState.rectWidth}|${this.canvasState.dpr}|${fixedHeight}`;
+		if (!this.canvasState.textCache[index]) this.canvasState.textCache[index] = this.makeTextCache(text, color, fixedHeight);
+
+		return this.canvasState.textCache[index];
+	};
+
+	getColor = (type, leg, num) => {
+		if (type !== 'loadFactor') {
+			const product = leg.line?.product || 'walk';
+			return JourneysCanvas.colors[type][product] || JourneysCanvas.colors[type].default;
+		} else {
+			return JourneysCanvas.colors[type][leg][num];
+		}
+	}
+
+	getCoachSequences = async () => {
+		if (this.viewState.profile !== 'db') return;
+
+		this.isUpdating = true;
+		for (const journey of this.viewState.journeys) {
+			for (const leg of journey.legs) {
+				if (!leg.line) continue;
+				const [category, number] = leg.line.name.split(" ");
+				await cachedCoachSequence(category, leg.line.fahrtNr || number, leg.origin.id, leg.plannedDeparture);
+				this.renderCanvas();
+			}
+		}
+		this.isUpdating = false;
+	}
+
+	getTrainTypeTexts = leg => {
+		if (!leg.line || !leg.line.name) return [];
+		const [category, number] = leg.line.name.split(" ");
+		const key = coachSequenceCacheKey(category, leg.line.fahrtNr || number, leg.origin.id, leg.plannedDeparture);
+		if (!key) return [];
+		const info = coachSequenceCache[key];
+		if (!info || info instanceof Promise) return [];
+
+		return formatTrainTypes(info).split(" + ");
+	};
+
+	renderCanvas = () => {
+		if (this.canvasContext === null) return;
+
+		const drawButton = mode => {
+			const buttonPath = new Path2D('M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z');
+
+			this.canvasContext.fillStyle = '#fff';
+			this.canvasContext.shadowColor = '#00000080';
+			this.canvasContext.save();
+			this.canvasContext.scale(3, 3);
+			if (mode === 'earlier') this.canvasContext.translate(x / 3 - 15, this.canvasElement.height / this.canvasState.dpr / 6 - 24);
+			if (mode === 'later')   this.canvasContext.translate(x / 3 + 5, this.canvasElement.height / this.canvasState.dpr / 6);
+			if (mode === 'earlier') this.canvasContext.rotate(-Math.PI*1.5);
+			if (mode === 'later')   this.canvasContext.rotate(Math.PI*1.5);
+			this.canvasContext.fill(buttonPath);
+			this.canvasContext.restore();
+			this.canvasContext.beginPath();
+			if (mode === 'earlier') this.canvasContext.arc(x - 80, this.canvasElement.height / this.canvasState.dpr / 2 - 35,50,0,2*Math.PI);
+			if (mode === 'later') this.canvasContext.arc(x + 50, this.canvasElement.height / this.canvasState.dpr / 2 - 35,50,0,2*Math.PI);
+			this.canvasContext.fillStyle = '#ffffff40';
+			this.canvasContext.fill();
+			this.canvasContext.strokeStyle = '#00000020';
+			this.canvasContext.stroke();
+		}
+
+		this.canvasContext.clearRect(0, 0, this.canvasElement.width / this.canvasState.dpr, this.canvasElement.height / this.canvasState.dpr);
+		let x = this.canvasState.offsetX - this.viewState.indexOffset * this.canvasState.rectWidthWithPadding, y;
+
+		const firstVisibleJourney = Math.max(0, Math.floor((-x + this.canvasState.padding) / this.canvasState.rectWidthWithPadding));
+		const numVisibleJourneys = Math.ceil(this.canvasElement.width / this.canvasState.dpr / this.canvasState.rectWidthWithPadding);
+		const visibleJourneys = this.viewState.journeys.slice(firstVisibleJourney, firstVisibleJourney + numVisibleJourneys);
+
+		if (!visibleJourneys.length) return;
+
+		const targetFirstDeparture = Number(visibleJourneys[0].legs[0].plannedDeparture);
+		const targetLastArrival = Math.max.apply(Math,
+			visibleJourneys.map(journey => journey.legs[journey.legs.length-1].plannedArrival)
+				.concat(visibleJourneys.map(journey => journey.legs[journey.legs.length-1].arrival)
+				));
+		const targetScaleFactor = 1 / (targetLastArrival - targetFirstDeparture) * (this.canvasElement.height - 64 * this.canvasState.dpr) / this.canvasState.dpr;
+
+		const now    = new Date();
+		const factor = Math.min(.3, (now - this.canvasState.lastAnimationUpdate) / 20);
+
+		if (!this.canvasState.lastAnimationUpdate) {
+			this.canvasState.firstDeparture = Number(targetFirstDeparture);
+			this.canvasState.lastArrival    = Number(targetLastArrival);
+			this.canvasState.scaleFactor    = targetScaleFactor;
+		} else {
+			this.canvasState.firstDeparture = this.canvasState.firstDeparture + (targetFirstDeparture - this.canvasState.firstDeparture) * factor;
+			this.canvasState.lastArrival    = this.canvasState.lastArrival    + (targetLastArrival    - this.canvasState.lastArrival)    * factor;
+			this.canvasState.scaleFactor    = this.canvasState.scaleFactor    + (targetScaleFactor    - this.canvasState.scaleFactor)    * factor;
+		}
+
+		this.canvasState.lastAnimationUpdate = now;
+
+		if (Math.abs(this.canvasState.scaleFactor - targetScaleFactor) > 1
+		  || Math.abs(this.canvasState.firstDeparture - targetFirstDeparture) > 1
+		  || Math.abs(this.canvasState.lastArrival - targetLastArrival) > 1
+		) {
+			if (!this.canvasState.animationInterval) this.canvasState.animationInterval = setInterval(() => this.renderCanvas(), 16.6);
+		} else {
+			if (this.canvasState.animationInterval) {
+				clearInterval(this.canvasState.animationInterval);
+				this.canvasState.animationInterval = null;
+			}
+		}
+
+		let time = this.viewState.journeys[0].legs[0].plannedDeparture;
+
+		this.canvasContext.font = `${(window.innerWidth / this.canvasState.dpr) > 600 ? 20 : 15}px sans-serif`;
+		this.canvasContext.fillStyle = '#aaa';
+		while (time < this.canvasState.lastArrival) {
+			const y = (time - this.canvasState.firstDeparture) * this.canvasState.scaleFactor + 32;
+			this.canvasContext.fillText(formatTime(time), (window.innerWidth / this.canvasState.dpr) > 600 ? 30 : 10, y);
+			this.canvasContext.fillRect(0, y, this.canvasElement.width / this.canvasState.dpr, 1);
+			time = new Date(Number(time) + 3600000);//Math.floor(120/scaleFactor));
+		}
+
+		this.canvasContext.fillStyle = '#fa5';
+		y = (new Date() - this.canvasState.firstDeparture) * this.canvasState.scaleFactor + 32;
+		this.canvasContext.fillRect(0, y-2, this.canvasElement.width / this.canvasState.dpr, 5);
+
+		if (this.viewState.earlierRef) drawButton('earlier');
+
+		this.viewState.journeys.forEach(journey => {
+			journey.legs.reverse();
+
+			journey.legs.forEach(leg => {
+				if (Math.abs(leg.departureDelay) > 60 || Math.abs(leg.arrivalDelay) > 60) {
+					const duration = (leg.plannedArrival - leg.plannedDeparture) * this.canvasState.scaleFactor;
+
+					y = (leg.plannedDeparture - this.canvasState.firstDeparture) * this.canvasState.scaleFactor + 32;
+
+					this.canvasContext.fillStyle   = '#44444480';
+					this.canvasContext.strokeStyle = '#ffffff80';
+					this.canvasContext.fillRect(x - this.canvasState.padding, y, this.canvasState.rectWidth, duration);
+					this.canvasContext.strokeRect(x - this.canvasState.padding, y, this.canvasState.rectWidth, duration);
+				}
+			});
+
+			x += this.canvasState.rectWidthWithPadding;
+		});
+
+		x = this.canvasState.offsetX - this.viewState.indexOffset * this.canvasState.rectWidthWithPadding;
+
+		this.viewState.journeys.forEach(journey => {
+			let xOffset = 0;
+			let nextLeg;
+
+			journey.legs.forEach(leg => {
+				if (nextLeg && nextLeg.departure < leg.arrival) xOffset -= 5;
+
+				x += xOffset;
+				const duration = ((leg.arrival || leg.plannedArrival) - (leg.departure || leg.plannedDeparture)) * this.canvasState.scaleFactor;
+
+				y = ((leg.departure || leg.plannedDeparture) - this.canvasState.firstDeparture) * this.canvasState.scaleFactor + 32;
+
+				this.canvasContext.shadowColor = '#00000060';
+				if (!this.settingsState.disableCanvasBlur) this.canvasContext.shadowBlur = 5;
+
+				if (leg.walking || leg.transfer) {
+					this.canvasContext.fillStyle = '#777';
+					this.canvasContext.fillRect(x + this.canvasState.rectWidth / 2 - this.canvasState.rectWidth / 10, y, this.canvasState.rectWidth / 5, duration);
+				} else {
+					this.canvasContext.fillStyle = this.getColor('fill', leg);
+					this.canvasContext.fillRect(x, y, this.canvasState.rectWidth, duration);
+					this.canvasContext.strokeStyle = this.getColor('text', leg);
+					this.canvasContext.strokeRect(x, y, this.canvasState.rectWidth, duration);
+				}
+				if (!this.settingsState.disableCanvasBlur) this.canvasContext.shadowBlur = 0;
+
+				let preRenderedText = this.getTextCache(formatLineDisplayName(leg.line), this.getColor('text', leg));
+				let offset = duration / 2;
+				if ((offset + preRenderedText.height / this.canvasState.dpr) < duration - 5) {
+					this.canvasContext.scale(1 / this.canvasState.dpr, 1 / this.canvasState.dpr);
+					this.canvasContext.drawImage(preRenderedText, this.canvasState.dpr * (x + 5), Math.floor(this.canvasState.dpr * (y + offset) - preRenderedText.height / 2.3));
+					this.canvasContext.scale(this.canvasState.dpr, this.canvasState.dpr);
+					offset += preRenderedText.height / this.canvasState.dpr / 1.3 + 5;
+				}
+
+				this.getTrainTypeTexts(leg).forEach(typeText => {
+					const preRenderedTypeText = this.getTextCache(typeText, '#555');
+					if ((offset + preRenderedText.height / this.canvasState.dpr) < duration - 5) {
+						this.canvasContext.scale(1 / this.canvasState.dpr, 1 / this.canvasState.dpr);
+						this.canvasContext.drawImage(preRenderedTypeText, this.canvasState.dpr * (x + 5), Math.floor(this.canvasState.dpr * (y + offset) - preRenderedTypeText.height / 2.3));
+						this.canvasContext.scale(this.canvasState.dpr, this.canvasState.dpr);
+						offset += preRenderedText.height / this.canvasState.dpr / 2;
+					}
+				});
+
+				if (leg.cancelled) {
+					this.canvasContext.beginPath();
+					this.canvasContext.moveTo(x, y);
+					this.canvasContext.lineTo(x + this.canvasState.rectWidth, y + duration);
+					this.canvasContext.strokeStyle = this.getColor('cancelFill', leg);
+					this.canvasContext.lineWidth = 5;
+					this.canvasContext.stroke();
+					this.canvasContext.lineWidth = 1;
+				}
+
+				/* draw journey start and end time */
+				// note: leg order is reversed at this point in time
+				const times = [];
+				if (journey.legs.indexOf(leg) == journey.legs.length - 1) times.push([leg.departure || leg.plannedDeparture, y - 9.5]);
+				if (journey.legs.indexOf(leg) == 0) times.push([leg.arrival || leg.plannedArrival, y + duration + 7.5]);
+				times.forEach(([time, y]) => {
+					preRenderedText = this.getTextCache(formatTime(time), '#fff', 15);
+					this.canvasContext.scale(1 / this.canvasState.dpr, 1 / this.canvasState.dpr);
+					this.canvasContext.drawImage(preRenderedText, Math.ceil(this.canvasState.dpr * (x + ((this.canvasState.rectWidth - preRenderedText.width/this.canvasState.dpr)) / 2)), this.canvasState.dpr * (y - 7.5));
+					this.canvasContext.scale(this.canvasState.dpr, this.canvasState.dpr);
+				});
+
+				if (leg.loadFactor && duration > 20) {
+					this.canvasContext.shadowColor = '#00000090';
+					if (!this.settingsState.disableCanvasBlur) this.canvasContext.shadowBlur = 2;
+					[ "#777", "#aaa", "#aaa" ];
+					for (let i = 0; i < 3; i++) {
+						this.canvasContext.beginPath();
+						this.canvasContext.fillStyle = this.getColor('loadFactor', leg.loadFactor, i);
+						this.canvasContext.arc(x + (i + 3) * this.canvasState.rectWidth / 8, y + duration - 9.5, 5, 0, 2 * Math.PI, false);
+						this.canvasContext.fill();
+					}
+					if (!this.settingsState.disableCanvasBlur) this.canvasContext.shadowBlur = 0;
+				}
+
+				x -= xOffset;
+				nextLeg = leg;
+			});
+
+			journey.legs.reverse();
+			x += this.canvasState.rectWidthWithPadding;
+		});
+		
+		if (this.viewState.laterRef) drawButton('later');
+	}
+}
diff --git a/src/journeysView.js b/src/journeysView.js
@@ -1,190 +1,262 @@
-import { html, nothing, render } from 'lit-html';
-import { ElementById, setThemeColor, queryBackgroundColor, padZeros } from './helpers.js';
-import { getJourneys, getMoreJourneys, refreshJourneys, getFrom, getTo } from './app_functions.js';
-import { formatName, formatDuration, formatFromTo, formatPrice } from './formatters.js';
-import { timeTemplate, footerTemplate } from './templates.js';
-import { settings, modifySettings } from './settings.js';
-import { setupCanvas } from './journeysViewCanvas.js';
-import { go } from './router.js';
-import { showAlertModal, showLoader, hideOverlay } from './overlays.js';
+import { html, css, nothing } from 'lit';
+
+import { sleep, queryBackgroundColor, setThemeColor } from './helpers.js';
+import { getJourneys, getMoreJourneys, refreshJourneys, getFromPoint, getToPoint } from './app_functions.js';
+import { formatPoint, formatDuration, formatPrice } from './formatters.js';
+import { timeTemplate } from './templates.js';
+import { settings } from './settings.js';
 import { t } from './languages.js';
 
-const journeysTemplate = (data) => html`
-	<div class="header-container">
-		<header id="header">
-			<a class="icon-back" href="#/" title="${t('back')}"></a>
-			<div class="container flex-row">
-				<div>
-					<h3>${t('from')}: ${formatName(getFrom(data.journeys))}</h3>
-					<h3>${t('to')}: ${formatName(getTo(data.journeys))}</h3>
-				</div>
-				<div class="mode-changers flex-row">
-					<a href="#/${data.slug}/table" class="${settings.journeysViewMode === 'table' ? 'active' : ''}">
-						<div class="icon-table"></div>
-						<span>${t('table-view')}</span>
-					</a>
-					<a href="#/${data.slug}/canvas" class="${settings.journeysViewMode === 'canvas' ? 'active' : ''}">
-						<div class="icon-canvas"></div>
-						<span>${t('canvas-view')}</span>
-					</a>
-				</div>
-			</div>
-			<a class="icon-reload" title="${t("reload")}" @click=${() => refreshJourneysView(data.slug)}></a>
-		</header>
-	</div>
-
-	${settings.journeysViewMode === 'canvas' ? html`
-
-	<div id="journeysCanvas">
-		<canvas id="canvas"></canvas>
-	</div>
-
-	` : nothing}
-
-	${settings.journeysViewMode === 'table' ? html`
-
-	<div class="container journeysView">
-		${data.earlierRef ? html`<a class="arrowButton icon-arrow2 flipped flex-center" title="${t('label_earlier')}" @click=${() => moreJourneys(data.slug, 'earlier')}></a>` : nothing}
-
-		<div class="card">
-		<table>
-			<thead>
-				<tr>
-					<th>${t('departure')}</th>
-					<th>${t('arrival')}</th>
-					<th>${t('duration')}</th>
-					<th>${t('changes')}</th>
-					<th>${t('products')}</th>
-					${settings.showPrices && settings.profile === 'db' ? html`<th>${t('price')}</th>` : nothing}
-					<th></th>
-				</tr>
-			</thead>
-			<tbody>
-				${Object.entries(data.journeys).map(([key, value]) => journeyOverviewTemplate(data.profile || "db", value, data.slug, key - data.indexOffset))}
-			</tbody>
-		</table>
-		</div>
-
-		${data.laterRef ? html`<a class="arrowButton icon-arrow2 flex-center" title="${t('label_later')}" @click=${() => moreJourneys(data.slug, 'later')}></a>` : nothing}
-	</div>
-
-	${footerTemplate}
-	` : nothing}
-`;
-
-const journeyOverviewTemplate = (profile, entry, slug, key) => {
-	const firstLeg = entry.legs[0];
-	const lastLeg = entry.legs[entry.legs.length - 1];
-
-	let changes = 0;
-	const products = {};
-	let productsString = '';
-	const changesDuration = 0;
-	let cancelled = false;
-
-	const duration = Number(lastLeg.arrival || lastLeg.plannedArrival) - Number(firstLeg.departure || firstLeg.plannedDeparture);
-
-	for (const leg of entry.legs) {
-		if (leg.cancelled) cancelled = true;
-		if (leg.walking || leg.transfer) continue;
-
-		changes = changes+1;
-
-		if (leg.line) {
-			const productName = leg.line.name.split(' ')[0];
-			if (!products[productName]) products[productName] = [];
-		}
-		//if (leg.line && leg.line.trainTypeShort) products[leg.line.productName].push(leg.line.trainTypeShort);
+import { baseStyles, helperStyles, flexboxStyles, buttonInputStyles, iconStyles, headerStyles, cardStyles, journeysViewStyles } from './styles.js';
+
+import { JourneysCanvas } from './journeysCanvas.js';
+
+export class JourneysView extends JourneysCanvas {
+	static styles = [
+		super.styles,
+		baseStyles,
+		helperStyles,
+		flexboxStyles,
+		iconStyles,
+		headerStyles,
+		buttonInputStyles,
+		cardStyles,
+		journeysViewStyles
+	];
+
+	static properties = {
+		slug:          { attribute: true },
+		mode:          { attribute: true },
+		viewState:     { state:     true },
+		settingsState: { state:     true },
+		isUpdating:    { state:     true },
+	};
+
+	constructor (...args) {
+		super(...args);
+
+		this.viewState     = null;
+		this.isUpdating    = false;
+		this.settingsState = settings.getState();
 	}
 
-	productsString = Object.entries(products).map(([prod, types]) => {
-		if (types.length >= 2) {
-			prod += ' (' + types.join(', ') + ')';
-		} else if (types.length) {
-			prod += ' ' + types[0];
-		}
-		return prod;
-	}).join(', ');
-
-	return html`
-	<tr @click=${() => go(`/j/${profile}/${entry.refreshToken}`)}>
-		<td class="${cancelled ? 'cancelled' : ''}">${timeTemplate(firstLeg, 'departure')}</td>
-		${cancelled ? html`
-			<td><span class="cancelled-text">${t('cancelled-ride')}</span></td>
-		` : html`
-			<td>${timeTemplate(lastLeg, 'arrival')}</td>
-		`}
-		<td class="${cancelled ? 'cancelled' : ''}" title="${changesDuration > 0 ? 'including '+formatDuration(changesDuration)+' transfer durations' : ''}">${formatDuration(duration)}</td>
-		<td>${changes-1}</td>
-		<td>${productsString}</td>
-		${settings.showPrices && settings.profile === 'db' ? html`<td>${formatPrice(entry.price)}</td>` : nothing}
-		<td><a class="icon-arrow1"></a></td>
-	</tr>`;
-};
-
-export const journeysView = async (match, isUpdate) => {
-	const slug = match[0];
-	const mode = match[1];
-
-	if (settings.journeysViewMode != mode) {
-		await modifySettings(settings => {
-			settings.journeysViewMode = mode;
-			return settings;
+	async connectedCallback() {
+		super.connectedCallback();
+		await sleep(100);
+
+		setThemeColor(queryBackgroundColor(this.renderRoot, 'header'));
+
+		if (this.viewState === null) await this.updateViewState();
+		if (this.mode === 'canvas') this.connectCanvas();
+
+		this._unsubscribeSettingsState = settings.subscribe((state) => {
+			this.settingsState = state;
+			this.performUpdate();
 		});
 	}
 
-	let data;
+	disconnectedCallback () {
+		super.disconnectedCallback();
 
-	if (!isUpdate) showLoader();
+		if (this.mode === 'canvas') this.disconnectCanvas();
+		this.viewState = null;
 
-	try {
-		data = await getJourneys(slug);
-	} catch(e) {
-		await showAlertModal(e.toString());
-		go('/');
-		throw e;
+		if (typeof this._unsubscribeSettingsState === 'function') {
+			this._unsubscribeSettingsState();
+			this._unsubscribeSettingsState = undefined;
+		}
 	}
 
-	if (!data) {
-		await showAlertModal(html`journeys overview id invalid. <br />journeys overview links can not be shared across devices in ${APPNAME}.`);
-		go('/');
-		return;
+	async updated (previous) {
+		if (isDevServer) console.info('updated(): ', previous);
+
+		if (previous.has('mode') && this.settingsState.journeysViewMode !== this.mode) this.settingsState.setJourneysViewMode(this.mode);
+
+		if (previous.has('slug')) await this.updateViewState();
+
+		if (previous.has('mode')) {
+			if (this.mode === 'canvas') this.connectCanvas();
+			if (previous.get('mode') === 'canvas' && this.mode !== 'canvas') this.disconnectCanvas();
+		}
+
+		if (this.mode === 'canvas') {	
+			if (this.canvas === null) this.connectCanvas();
+			if (previous.has('viewState') && this.viewState !== null) {
+				this.renderCanvas();
+				await this.getCoachSequences();
+			}
+		}
+
+		await super.updated(previous);
 	}
 
-	hideOverlay();
+	renderContent () {
+		return html`
+			<div class="header-container">
+				<header>
+					<a id="back" class="icon-back" title="${t('back')}" href="#/"></a>
+					<div class="container flex-row">
+						<div>
+							<h3>${t('from')}: ${this.viewState !== null ? formatPoint(getFromPoint(this.viewState.journeys)) : '...'}</h3>
+							<h3>${t('to')}:   ${this.viewState !== null ? formatPoint(getToPoint(this.viewState.journeys))   : '...'}</h3>
+						</div>
+						<div class="mode-changers flex-row">
+							<a href="#/${this.slug}/table" class="${this.settingsState.journeysViewMode === 'table' ? 'active' : ''}">
+								<div class="icon-table"></div>
+								<span>${t('table-view')}</span>
+							</a>
+							<a href="#/${this.slug}/canvas" class="${this.settingsState.journeysViewMode === 'canvas' ? 'active' : ''}">
+								<div class="icon-canvas"></div>
+								<span>${t('canvas-view')}</span>
+							</a>
+						</div>
+					</div>
+					<a class="icon-reload ${this.isUpdating ? 'spinning' : ''}" title="${t("reload")}" @click=${this.refreshJourneys}></a>
+				</header>
+			</div>
 
-	render(journeysTemplate(data), ElementById('content'));
-	setThemeColor(queryBackgroundColor('header'));
+			${this.viewState !== null ? html`
+			${this.settingsState.journeysViewMode === 'canvas' ? this.getCanvas() : nothing}
+			${this.settingsState.journeysViewMode === 'table'  ? html`
+			<div class="container journeysView">
+				${this.viewState.earlierRef ? html`
+				<a class="arrowButton icon-arrow2 flipped flex-center" title="${t('label_earlier')}" @click=${() => this.moreJourneys('earlier')}></a>
+				` : nothing}
+
+				<div class="card">
+				<table>
+					<thead>
+						<tr>
+							<th>${t('departure')}</th>
+							<th>${t('arrival')}</th>
+							<th>${t('duration')}</th>
+							<th>${t('changes')}</th>
+							<th>${t('products')}</th>
+							${this.settingsState.showPrices && this.viewState.profile === 'db' ? html`<th>${t('price')}</th>` : nothing}
+							<th></th>
+						</tr>
+					</thead>
+					<tbody>
+						${this.viewState.journeys.map(journey => this.journeyTemplate(journey))}
+					</tbody>
+				</table>
+				</div>
 
-	if (settings.journeysViewMode === 'canvas')
-		return setupCanvas(data, isUpdate);
-};
+				${this.viewState.laterRef ? html`
+				<a class="arrowButton icon-arrow2 flex-center" title="${t('label_later')}" @click=${() => this.moreJourneys('later')}></a>
+				` : nothing}
+			</div>
+			<footer-component></footer-component>
+			` : nothing}
+			` : html`<div class="spinner"></div>`}
+		`;
+	}
 
-export const moreJourneys = async (slug, mode) => {
-	showLoader();
+	journeyTemplate (journey) {
+		const firstLeg = journey.legs[0];
+		const lastLeg  = journey.legs[journey.legs.length - 1];
+
+		return html`
+			<tr @click=${() => window.location = `#/j/${this.viewState.profile}/${journey.refreshToken}`}>
+				<td class="${!journey.cancelled ? nothing : 'cancelled'}">${timeTemplate(firstLeg, 'departure')}</td>
+				${journey.cancelled ? html`
+				<td><span class="cancelled-text">${t('cancelled-ride')}</span></td>
+				` : html`
+				<td>${timeTemplate(lastLeg, 'arrival')}</td>
+				`}
+				<td class="${!journey.cancelled ? nothing : 'cancelled'}" title="${journey.changesDuration > 0 ? 'including '+formatDuration(journey.changesDuration)+' transfer durations' : ''}">${formatDuration(journey.duration)}</td>
+				<td>${journey.changes-1}</td>
+				<td>${journey.products}</td>
+				${this.settingsState.showPrices && this.viewState.profile === 'db' ? html`
+				<td>${formatPrice(journey.price)}</td>
+				` : nothing}
+				<td><a class="icon-arrow1"></a></td>
+			</tr>
+		`;
+	}
 
-	try {
-		await getMoreJourneys(slug, mode);
-	} catch(e) {
-		showAlertModal(e.toString());
-		throw e;
+	async updateViewState () {
+		try {
+			let viewState = await getJourneys(this.slug);
+
+			if (!viewState) {
+				this.showAlertOverlay(html`journeys overview id invalid. <br>journeys overview links can not be shared across devices.`);
+				window.location = '#/';
+				return;
+			}
+
+			viewState.journeys.forEach((journey, index, journeys) => {
+				const firstLeg = journey.legs[0];
+				const lastLeg  = journey.legs[journey.legs.length - 1];
+				const products = {};
+
+				journeys[index].duration        = Number(lastLeg.arrival || lastLeg.plannedArrival) - Number(firstLeg.departure || firstLeg.plannedDeparture);
+				journeys[index].changesDuration = 0;
+				journeys[index].cancelled       = false;
+				journeys[index].changes         = 0;
+
+				journey.legs.forEach(leg => {
+					if (leg.cancelled) journeys[index].cancelled = true;
+					if (leg.walking || leg.transfer) return;
+
+					journeys[index].changes++;
+
+					if (leg.line) {
+						const productName = leg.line.name.split(' ')[0];
+						if (!products[productName]) products[productName] = [];
+					}
+					//if (leg.line && leg.line.trainTypeShort) products[leg.line.productName].push(leg.line.trainTypeShort);
+				})
+
+				journeys[index].products = Object.entries(products).map(([prod, types]) => {
+					if (types.length >= 2) {
+						prod += ' (' + types.join(', ') + ')';
+					} else if (types.length) {
+						prod += ' ' + types[0];
+					}
+					return prod;
+				}).join(', ');
+			});
+
+			this.viewState = viewState;
+			if (isDevServer) console.info('viewState: ', this.viewState);
+		} catch(e) {
+			this.showAlertOverlay(e.toString());
+			console.error(e);
+		}
 	}
 
-	hideOverlay();
-	journeysView([slug, settings.journeysViewMode], true);
-};
+	async refreshJourneys () {
+		if (this.isUpdating !== false) return false;
+
+		try {
+			this.isUpdating = true;
+			await refreshJourneys(this.slug);
+			this.isUpdating = false;
 
-const refreshJourneysView = async (slug) => {
-	document.querySelector('.icon-reload').classList.add('spinning');
+			await this.updateViewState();
+		} catch(e) {
+			this.isUpdating = false;
+			this.showAlertOverlay(e.toString());
+			console.error(e);
+		}
+	}
 
-	try {
-		await refreshJourneys(slug, true);
-	} catch(e) {
-		showAlertModal(e.toString());
-		document.querySelector('.icon-reload').classList.remove('spinning');
-		throw e;
+	async moreJourneys (mode) {
+		this.showLoaderOverlay();
+		try {
+			this.isUpdating = true;
+			await getMoreJourneys(this.slug, mode);
+			this.isUpdating = false;
+
+			await this.updateViewState();
+		} catch(e) {
+			this.isUpdating = false;
+			this.showAlertOverlay(e.toString());
+			console.error(e);
+		}
+		this.hideOverlay();
 	}
+}
 
-	journeysView([slug, settings.journeysViewMode], true);
-	document.querySelector('.icon-reload').classList.remove('spinning');
-};
+customElements.define('journeys-view', JourneysView);
diff --git a/src/journeysViewCanvas.js b/src/journeysViewCanvas.js
@@ -1,418 +0,0 @@
-import { moreJourneys } from './journeysView.js';
-import { go } from './router.js';
-import { padZeros } from './helpers.js';
-import { formatTrainTypes, formatLineDisplayName, formatDateTime } from './formatters.js'
-import { cachedCoachSequence, coachSequenceCache, coachSequenceCacheKey } from './coach-sequence/index.js';
-
-
-const colorFor = (leg, type) => {
-	const product = leg.line?.product || 'walk';
-	return colors[type][product] || colors[type].default;
-};
-
-const loadFactorColors = {
-	'low-to-medium':      [ '#777', '#ccc', '#ccc' ],
-	'high':               [ '#777', '#777', '#ccc' ],
-	'very-high':          [ '#ee8800', '#ee8800', '#ee8800'  ],
-	'exceptionally-high': [ '#cc3300', '#cc3300', '#cc3300' ],
-};
-
-const flatten = (arr) => [].concat(...arr);
-
-const colors = {
-	fill: {
-		'tram': '#cc5555',
-		'subway': '#5555cc',
-		'suburban': '#55aa55',
-		'nationalExpress': '#fff',
-		'national': '#fff',
-		'regionalExpress': '#888',
-		'regional': '#888',
-		'bus': '#aa55aa',
-		default: '#888'
-	},
-	text: {
-		'nationalExpress': '#ee3333',
-		'national': '#ee3333',
-		default: '#fff'
-	},
-	cancelFill: {
-		'tram': '#fff',
-		default: '#cc4444ff',
-	},
-	icon: {
-		'walk': 'directions_walk',
-		'transfer': 'directions_transfer',
-		'subway': 'directions_subway',
-		'bus': 'directions_bus',
-		'tram': 'tram',
-		default: 'train'
-	}
-};
-
-let rectWidth, padding, rectWidthWithPadding, canvas, ctx;
-let dpr = window.devicePixelRatio || 1;
-
-const canvasState = {
-	offsetX: 0,
-};
-
-let textCache = {};
-
-const typeTextsFor = leg => {
-	if (!leg.line || !leg.line.name) return [];
-	const [category, number] = leg.line.name.split(" ");
-	const key = coachSequenceCacheKey(category, leg.line.fahrtNr || number, leg.origin.id, leg.plannedDeparture);
-	if (!key) return [];
-	const info = coachSequenceCache[key];
-	if (!info || info instanceof Promise) {
-		return [];
-	}
-	return formatTrainTypes(info).split(" + ");
-};
-
-export const setupCanvas = (data, isUpdate) => {
-	if (!isUpdate) canvasState.offsetX = (window.innerWidth / dpr) > 600 ? 140 : 80;
-	canvas = document.getElementById('canvas');
-	ctx = canvas.getContext('2d');
-	if (data) {
-		canvasState.data = {
-			...data,
-			journeys: Object.keys(data.journeys).sort((a, b) => Number(a) - Number(b)).map(k => data.journeys[k])
-		};
-
-		(async () => {
-			for (const journey of canvasState.data.journeys) {
-				for (const leg of journey.legs) {
-					if (!leg.line) continue;
-					const [category, number] = leg.line.name.split(" ");
-					if (data.profile === "db") await cachedCoachSequence(category, leg.line.fahrtNr || number, leg.origin.id, leg.plannedDeparture);
-					setupCanvas(null, true);
-				}
-			}
-		}) ();
-	}
-
-	canvas.addEventListener('mousedown', mouseDownHandler, {passive: true});
-	canvas.addEventListener('touchstart', mouseDownHandler, {passive: true});
-	window.addEventListener('mouseup', mouseUpHandler);
-	window.addEventListener('touchend', mouseUpHandler);
-	window.addEventListener('mousemove', mouseMoveHandler);
-	window.addEventListener('touchmove', mouseMoveHandler);
-	window.addEventListener('resize', resizeHandler);
-	window.addEventListener('zoom', resizeHandler);
-	resizeHandler();
-
-	return {
-		unload: () => {
-			canvas.removeEventListener('mousedown', mouseDownHandler);
-			canvas.removeEventListener('touchstart', mouseDownHandler);
-			window.removeEventListener('mouseup', mouseUpHandler);
-			window.removeEventListener('touchend', mouseUpHandler);
-			window.removeEventListener('mousemove', mouseMoveHandler);
-			window.removeEventListener('touchmove', mouseMoveHandler);
-			window.removeEventListener('resize', resizeHandler);
-			window.removeEventListener('zoom', resizeHandler);
-		},
-	};
-};
-
-const getTextCache = (text, color, fixedHeight) => {
-	const index = `${text}|${color}|${rectWidth}|${dpr}|${fixedHeight}`;
-	if (!textCache[index]) {
-		textCache[index] = makeTextCache(text, color, fixedHeight);
-	}
-	return textCache[index];
-};
-
-const makeTextCache = (text, color, fixedHeight) => {
-	const canvas = document.createElement('canvas');
-	const ctx = canvas.getContext('2d');
-	ctx.shadowColor = '#00000080';
-
-	let height, width;
-	if (fixedHeight) {
-		height = 15;
-		ctx.font = `${height}px sans-serif`;
-		width = ctx.measureText(text).width;
-	} else {
-		const measureAccuracy = 50;
-		ctx.font = `${measureAccuracy}px sans-serif`;
-		width = rectWidth - 10;
-		height = Math.abs(measureAccuracy * (width / (1 - ctx.measureText(text).width)));
-	}
-
-	canvas.width = width * dpr;
-	canvas.height = Math.ceil(height * 1.5) * dpr;
-	ctx.scale(dpr, dpr);
-
-	ctx.font = `${height}px sans-serif`;
-	ctx.fillStyle = color;
-	ctx.fillText(text, 0, height);
-
-	return canvas;
-};
-
-let lastAnimationUpdate = 0, firstDeparture = 0, scaleFactor = 0, lastArrival = 0;
-let animationInterval;
-const renderJourneys = () => {
-	ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
-	let x = canvasState.offsetX - canvasState.data.indexOffset * rectWidthWithPadding, y;
-
-	const firstVisibleJourney = Math.max(0, Math.floor((-x + padding) / rectWidthWithPadding));
-	const numVisibleJourneys = Math.ceil(canvas.width / dpr / rectWidthWithPadding);
-	const visibleJourneys = canvasState.data.journeys.slice(firstVisibleJourney, firstVisibleJourney + numVisibleJourneys);
-
-	if (!visibleJourneys.length) return;
-
-	const targetFirstDeparture = Number(visibleJourneys[0].legs[0].plannedDeparture);
-	const targetLastArrival = Math.max.apply(Math,
-		visibleJourneys.map(journey => journey.legs[journey.legs.length-1].plannedArrival)
-			.concat(visibleJourneys.map(journey => journey.legs[journey.legs.length-1].arrival)
-			));
-	const targetScaleFactor = 1/(targetLastArrival - targetFirstDeparture) * (canvas.height - 64 * dpr) / dpr;
-
-	const now = new Date();
-	const factor = Math.min(.3, (now - lastAnimationUpdate) / 20);
-	if (!lastAnimationUpdate) {
-		firstDeparture = Number(targetFirstDeparture);
-		lastArrival = Number(targetLastArrival);
-		scaleFactor = targetScaleFactor;
-	} else {
-		firstDeparture = firstDeparture + (targetFirstDeparture - firstDeparture) * factor;
-		lastArrival = lastArrival + (targetLastArrival - lastArrival) * factor;
-		scaleFactor = scaleFactor + (targetScaleFactor - scaleFactor) * factor;
-	}
-	lastAnimationUpdate = now;
-
-	if (Math.abs(scaleFactor - targetScaleFactor) > 1
-	  || Math.abs(firstDeparture - targetFirstDeparture) > 1
-	  || Math.abs(lastArrival - targetLastArrival) > 1
-	) {
-		if (!animationInterval) animationInterval = setInterval(() => renderJourneys(), 16.6);
-	} else {
-		if (animationInterval) {
-			clearInterval(animationInterval);
-			animationInterval = null;
-		}
-	}
-
-	let time = canvasState.data.journeys[0].legs[0].plannedDeparture;
-
-	ctx.font = `${(window.innerWidth / dpr) > 600 ? 20 : 15}px sans-serif`;
-	ctx.fillStyle = '#aaa';
-	while (time < lastArrival) {
-		const y = (time - firstDeparture) * scaleFactor + 32;
-		ctx.fillText(formatDateTime(time, 'time'), (window.innerWidth / dpr) > 600 ? 30 : 10, y);
-		ctx.fillRect(0, y, canvas.width / dpr, 1);
-		time = new Date(Number(time) + 3600000);//Math.floor(120/scaleFactor));
-	}
-	ctx.fillStyle = '#fa5';
-	y = (new Date() - firstDeparture) * scaleFactor + 32;
-	ctx.fillRect(0, y-2, canvas.width / dpr, 5);
-
-	const p = new Path2D('M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z');
-	if (canvasState.data.earlierRef) {
-		ctx.fillStyle = '#fff';
-		ctx.shadowColor = '#00000080';
-		ctx.save();
-		ctx.scale(3, 3);
-		ctx.translate(x / 3 - 15, canvas.height / dpr / 6 - 24);
-		ctx.rotate(-Math.PI*1.5);
-		ctx.fill(p);
-		ctx.restore();
-		ctx.beginPath();
-		ctx.arc(x - 80,canvas.height / dpr / 2 - 35,50,0,2*Math.PI);
-		ctx.fillStyle = '#ffffff40';
-		ctx.fill();
-		ctx.strokeStyle = '#00000020';
-		ctx.stroke();
-	}
-
-	for (const journey of canvasState.data.journeys) {
-		journey.legs.reverse();
-		for (const leg of journey.legs) {
-			if (Math.abs(leg.departureDelay) > 60 || Math.abs(leg.arrivalDelay) > 60) {
-				const duration = (leg.plannedArrival - leg.plannedDeparture) * scaleFactor;
-
-				y = (leg.plannedDeparture - firstDeparture) * scaleFactor + 32;
-
-				ctx.fillStyle = '#44444480';
-				ctx.strokeStyle = '#ffffff80';
-				ctx.fillRect(x-padding, y, rectWidth, duration);
-				ctx.strokeRect(x-padding, y, rectWidth, duration);
-			}
-		}
-		x += rectWidthWithPadding;
-	}
-
-	x = canvasState.offsetX - canvasState.data.indexOffset * rectWidthWithPadding;
-
-	for (const journey of canvasState.data.journeys) {
-		let xOffset = 0;
-		let nextLeg;
-		for (const leg of journey.legs) {
-			if (nextLeg && nextLeg.departure < leg.arrival) {
-				xOffset -= 5;
-			}
-
-			x += xOffset;
-			const duration = ((leg.arrival || leg.plannedArrival) - (leg.departure || leg.plannedDeparture)) * scaleFactor;
-
-			y = ((leg.departure || leg.plannedDeparture) - firstDeparture) * scaleFactor + 32;
-
-			ctx.shadowColor = '#00000060';
-			//ctx.shadowBlur = 5;
-
-			if (leg.walking || leg.transfer) {
-				ctx.fillStyle = '#777';
-				ctx.fillRect(x + rectWidth / 2 - rectWidth / 10, y, rectWidth / 5, duration);
-			} else {
-				ctx.fillStyle = colorFor(leg, 'fill');
-				ctx.fillRect(x, y, rectWidth, duration);
-				ctx.strokeStyle = colorFor(leg, 'text');
-				ctx.strokeRect(x, y, rectWidth, duration);
-			}
-			//ctx.shadowBlur = 0;
-
-			let preRenderedText = getTextCache(formatLineDisplayName(leg.line), colorFor(leg, 'text'));
-			let offset = duration / 2;
-			if ((offset + preRenderedText.height / dpr) < duration - 5) {
-				ctx.scale(1 / dpr, 1 / dpr);
-				ctx.drawImage(preRenderedText, dpr * (x + 5), Math.floor(dpr * (y + offset) - preRenderedText.height / 2.3));
-				ctx.scale(dpr, dpr);
-				offset += preRenderedText.height / dpr / 1.3 + 5;
-			}
-			const typeTexts = typeTextsFor(leg);
-			for (const typeText of typeTexts) {
-				const preRenderedTypeText = getTextCache(typeText, '#555');
-				if ((offset + preRenderedText.height / dpr) < duration - 5) {
-					ctx.scale(1 / dpr, 1 / dpr);
-					ctx.drawImage(preRenderedTypeText, dpr * (x + 5), Math.floor(dpr * (y + offset) - preRenderedTypeText.height / 2.3));
-					ctx.scale(dpr, dpr);
-					offset += preRenderedText.height / dpr / 1.3;
-				}
-			}
-
-			if (leg.cancelled) {
-				ctx.beginPath();
-				ctx.moveTo(x, y);
-				ctx.lineTo(x + rectWidth, y + duration);
-				ctx.strokeStyle = colorFor(leg, 'cancelFill');
-				ctx.lineWidth = 5;
-				ctx.stroke();
-				ctx.lineWidth = 1;
-			}
-
-			/* draw journey start and end time */
-			let time;
-			// note: leg order is reversed at this point in time
-			const times = [];
-			if (journey.legs.indexOf(leg) == journey.legs.length - 1) times.push([leg.departure || leg.plannedDeparture, y - 9.5]);
-			if (journey.legs.indexOf(leg) == 0) times.push([leg.arrival || leg.plannedArrival, y + duration + 7.5]);
-			for (const [time, y] of times) {
-				preRenderedText = getTextCache(formatDateTime(time, 'time'), '#fff', 15);
-				ctx.scale(1 / dpr, 1 / dpr);
-				ctx.drawImage(preRenderedText, Math.ceil(dpr * (x + ((rectWidth - preRenderedText.width/dpr)) / 2)), dpr * (y - 7.5));
-				ctx.scale(dpr, dpr);
-			}
-
-			if (leg.loadFactor && duration > 20) {
-				ctx.shadowColor = '#00000090';
-				//ctx.shadowBlur = 2;
-				[ "#777", "#aaa", "#aaa" ];
-				for (let i = 0; i < 3; i++) {
-					ctx.beginPath();
-					ctx.fillStyle = loadFactorColors[leg.loadFactor][i];
-					ctx.arc(x + (i + 3) * rectWidth / 8, y + duration - 9.5, 5, 0, 2 * Math.PI, false);
-					ctx.fill();
-				}
-				//ctx.shadowBlur = 0;
-			}
-			x -= xOffset;
-			nextLeg = leg;
-		}
-
-		journey.legs.reverse();
-
-		x += rectWidthWithPadding;
-	}
-	if (canvasState.data.laterRef) {
-		ctx.fillStyle = '#fff';
-		ctx.shadowColor = '#00000080';
-		ctx.save();
-		ctx.scale(3, 3);
-		ctx.translate(x / 3 + 5, canvas.height / dpr / 6);
-		ctx.rotate(Math.PI*1.5);
-		ctx.fill(p);
-		ctx.restore();
-		ctx.beginPath();
-		ctx.arc(x + 50,canvas.height / dpr / 2 - 35,50,0,2*Math.PI);
-		ctx.fillStyle = '#ffffff40';
-		ctx.fill();
-		ctx.strokeStyle = '#00000020';
-		ctx.stroke();
-	}
-};
-
-const resizeHandler = () => {
-	dpr = window.devicePixelRatio || 1;
-	if (!document.getElementById('canvas')) return;
-
-	rectWidth = (window.innerWidth / dpr) > 600 ? 100 : 80;
-	padding = (window.innerWidth / dpr) > 600 ? 20 : 5;
-	rectWidthWithPadding = rectWidth + 2 * padding;
-
-	const rect = document.getElementById('header').getBoundingClientRect();
-	canvas.width = window.innerWidth * dpr;
-	canvas.height = (window.innerHeight - rect.height) * dpr;
-	canvas.style.width = `${window.innerWidth}px`;
-	canvas.style.height = `${window.innerHeight - rect.height - 4}px`;
-
-	ctx.restore();
-	ctx.save();
-	ctx.scale(dpr, dpr);
-
-	lastAnimationUpdate = 0;
-	renderJourneys();
-};
-
-const mouseUpHandler = (evt) => {
-	const x = evt.x || evt.changedTouches[0].pageX;
-	if (canvasState.dragging && canvasState.isClick) {
-		evt.preventDefault();
-		const num = Math.floor((x - canvasState.offsetX + 2 * padding) / rectWidthWithPadding) + canvasState.data.indexOffset;
-		if (num >= 0) {
-			if (num < canvasState.data.journeys.length) {
-				const j = canvasState.data.journeys[num];
-				go(`/j/${canvasState.data.profile || "db"}/${j.refreshToken}`);
-			} else if (canvasState.data.laterRef) {
-				moreJourneys(canvasState.data.slug, 'later');
-			}
-		} else if (canvasState.data.earlierRef) {
-			moreJourneys(canvasState.data.slug, 'earlier');
-		}
-	}
-
-	canvasState.dragging = false;
-	canvasState.isClick = false;
-};
-
-const mouseDownHandler = (evt) => {
-	const x = evt.x || evt.changedTouches[0].pageX;
-	canvasState.dragStartMouse = x;
-	canvasState.dragStartOffset = canvasState.offsetX;
-	canvasState.dragging = true;
-	canvasState.isClick = true;
-};
-
-const mouseMoveHandler = (evt) => {
-	if (canvasState.dragging) {
-		evt.preventDefault();
-		const x = evt.x || evt.changedTouches[0].pageX;
-		canvasState.offsetX = canvasState.dragStartOffset - (canvasState.dragStartMouse - x);
-		if (Math.abs(canvasState.dragStartMouse - x) > 20) canvasState.isClick = false;
-		renderJourneys();
-		return true;
-	}
-};
diff --git a/src/languages.js b/src/languages.js
@@ -1,4 +1,4 @@
-import { settings } from './settings.js';
+import { settingsState } from './settings.js';
 
 export const getDefaultLanguage = () => {
 	const userLang = navigator.language || navigator.userLanguage;

@@ -11,7 +11,7 @@ export const getDefaultLanguage = () => {
 export const getLanguages = () => Object.keys(languages);
 
 export const t = (key, ...params) => {
-	let translation = languages[settings.language][key];
+	let translation = languages[settingsState.language][key];
 
 	if (!translation) translation = languages['en'][key]
 	if (!translation) return key;

@@ -105,7 +105,10 @@ const languages = {
 		'ageGroupYoung':       'Jung',
 		'ageGroupAdult':       'Erwachsen',
 		'ageGroupSenior':      'Senior',
+		'tickets':             'Tickets',
 		'now':                 'Jetzt',
+		'minTransferTime':     'Umstiegszeit (Minuten)',
+		'trainType':           'Zugtyp',
 	},
 
 	'nl': {

@@ -243,11 +246,11 @@ const languages = {
 		'combineDateTime':   'Use combined DateTime-input',
 		'titleSetDateTimeNow': 'Set Date & Time to now',
 		'titleBikeFriendly':   'Bicycle transport possible',
-		'loyaltyCard':         'Discount Card',
+		'loyaltyCard':         'Discount card',
 		'loyaltyCardNone':     'No discount card',
 		'class':               'Class',
 		'titleNoTransfers':    'only direct connections',
-		'lastSelectedJourney': 'Last selected Journey',
+		'lastSelectedJourney': 'Last selected journey',
 		'walkingSpeed':        'Walking speed',
 		'walkingSpeedSlow':    'slow',
 		'walkingSpeedNormal':  'normal',

@@ -259,6 +262,9 @@ const languages = {
 		'ageGroupYoung':       'Young',
 		'ageGroupAdult':       'Adult',
 		'ageGroupSenior':      'Senior',
+		'tickets':             'Tickets',
 		'now':                 'Now',
+		'minTransferTime':     'Transfer time (Minutes)',
+		'trainType':           'Train type',
 	}
 };
diff --git a/src/main.js b/src/main.js
@@ -1,39 +1,93 @@
-import { route, go, start } from './router.js';
-import { ElementById } from './helpers.js';
-import { showAlertModal, hideOverlay } from './overlays.js';
-import { initSettings, settings } from './settings.js';
-import { initDataStorage } from './dataStorage.js';
-import { initHafasClient } from './hafasClient.js';
-import { searchView } from './searchView.js';
-import { journeysView } from './journeysView.js';
-import { journeyView } from './journeyView.js';
-import { tripView } from './tripView.js';
-import { departuresView } from './departuresView.js';
+import { LitElement, html, render } from 'lit';
+import { cache } from 'lit/directives/cache.js';
 
-import './assets/style.css';
+import { initDataStorage }  from './dataStorage.js';
+import { initHafasClient }  from './hafasClient.js';
+import { initSettingsState, settingsState } from './settings.js'
 
+import { baseStyles, helperStyles } from './styles.js';
+
+import './searchView.js';
+import './journeysView.js';
+import './journeyView.js';
+import './tripView.js';
+import './departuresView.js';
+import './settingsView.js';
+import './footerComponent.js';
+
+class Oeffisearch extends LitElement {
+	static properties = {
+		outlet: { state: true },
+	};
+
+	routes = [
+		{
+			pattern: /^\/$/,
+			render: () => html`<search-view></search-view>`
+		},
+		{
+			pattern: /^\/([a-zA-Z0-9]+)\/([a-z]+)$/,
+			render: param => html`<journeys-view slug="${param[0]}" mode="${param[1]}"></journeys-view>`
+		},
+		{
+			pattern: /^\/j\/([a-z]+)\/(.+)$/,
+			render: param => html`<journey-view profile="${param[0]}" refreshToken="${param[1]}"></journey-view>`
+		},
+		{
+			pattern: /^\/t\/([a-z]+)\/(.+)$/,
+			render: param => html`<trip-view profile="${param[0]}" refreshToken="${param[1]}"></trip-view>`
+		},
+		{
+			pattern: /^\/d\/([a-z]+)\/([^/]+)(\/[0-9]+)?$/,
+			render: param => html`<departures-view profile="${param[0]}" stopId="${param[1]}" when="${param[2]}"></departures-view>`
+		}
+	];
+
+	constructor (...args) {
+		super(...args);
+		this.outlet = html``;
+		if (!window.location.hash.length) window.location = '#/';
+	}
+
+	routeHandler = () => {
+		const dest = window.location.hash.slice(1);
+		this.routes.forEach(route => {
+			const match = route.pattern.exec(dest);
+			if (match !== null) this.outlet = route.render(match.slice(1));
+		});
+	}
+
+	connectedCallback () {
+		super.connectedCallback();
+		window.addEventListener('hashchange', this.routeHandler);
+		this.routeHandler();
+	}
+
+	disconnectedCallback () {
+		super.disconnectedCallback();
+		window.removeEventListener('hashchange', this.routeHandler);
+	}
+
+	render = () => cache(this.outlet);
+}
+
+customElements.define('oeffi-search', Oeffisearch);
 
 (async () => {
+	await initSettingsState();
 	await initDataStorage();
-	await initSettings();
-	await initHafasClient(settings.profile || "db");
-
-	hideOverlay();
-	ElementById('overlay').innerHTML = '';
-
-	route(/^\/$/, searchView);
-	route(/^\/([a-zA-Z0-9]+)\/([a-z]+)$/, journeysView);
-	route(/^\/j\/([a-z]+)\/(.+)$/, journeyView);
-	route(/^\/t\/([a-z]+)\/(.+)$/, tripView);
-	route(/^\/d\/([a-z]+)\/([^/]+)(\/[0-9]+)?$/, departuresView);
-	route(/^.*$/, async () => {
-		await showAlertModal('Route not found');
-		go('/');
-	});
+	await initHafasClient(settingsState.profile);
+
+	const style = document.createElement('style');
+	style.type = 'text/css';
+	style.appendChild(document.createTextNode([
+		baseStyles,
+		helperStyles
+	].join('')));
 
-	if (!window.location.hash.length) go('/');
-	start();
-}) ();
+	document.head.appendChild(style);
+	document.body.innerHTML = '<oeffi-search></oeffi-search>';
+})();
 
 if (!isDevServer) {
 	window.addEventListener('load', () => {
diff --git a/src/overlays.js b/src/overlays.js
@@ -1,53 +0,0 @@
-import { html, render } from 'lit-html';
-import { ElementById, showElement, hideElement } from './helpers.js';
-
-const overlayElement = ElementById('overlay');
-
-export const showAlertModal = (text) => {
-	showElement(overlayElement);
-	return new Promise(resolve => {
-		render(html`
-			<div class="modal alert">
-				${text}
-				<br><button class="color" id="alertButton" @click=${() => { hideOverlay(); resolve(); }}>OK</button>
-			</div>
-		`, overlayElement);
-		ElementById('alertButton').focus();
-	});
-};
-
-export const showSelectModal = (items) => {
-	showElement(overlayElement);
-	return new Promise(resolve => {
-		render(html`
-			<div class="modal select flex-column">
-				${items.map(
-					(item) => html`<a class="button color" @click=${item.action}>${item.label}</a>`
-				)}
-				<a class="button color" @click=${() => { hideOverlay(); resolve(); }}>Close</a>
-			</div>
-		`, overlayElement);
-	});
-};
-
-export const showModal = (title, content) => {
-	showElement(overlayElement);
-	return new Promise(resolve => {
-		render(html`
-			<div class="modal dialog">
-				<div class="header flex-row">
-					<h4>${title}</h4>
-					<div class="icon-close" title="Close" @click=${() => { hideOverlay(); resolve(); }}></div>
-				</div>
-				<div class="body">${content}</div>
-			</div>
-		`, overlayElement);
-	});
-};
-
-export const showLoader = () => {
-	showElement(overlayElement);
-	render(html`<div class="spinner"></div>`, overlayElement);
-};
-
-export const hideOverlay = () => hideElement(overlayElement);
diff --git a/src/router.js b/src/router.js
@@ -1,31 +0,0 @@
-const routes = [];
-let currentRoute;
-
-export const route = (pattern, handler) => {
-	routes.push({
-		pattern: pattern,
-		handler: handler
-	});
-};
-
-export const go = (dest) => {
-	window.location.hash = '#' + dest;
-};
-
-export const start = async () => {
-	const dest = window.location.hash.slice(1);
-
-	if (currentRoute && currentRoute.unload) currentRoute.unload();
-
-	for (const route of routes) {
-		const match = route.pattern.exec(dest);
-
-		if (!match) continue;
-
-		currentRoute = await route.handler(match.slice(1));
-
-		return;
-	}
-};
-
-window.addEventListener('hashchange', start);
diff --git a/src/searchView.js b/src/searchView.js
@@ -1,574 +1,560 @@
-import { html, nothing, render } from 'lit-html';
-import { ElementById, hideElement, showElement, elementHidden, flipElement, unflipElement, setThemeColor, queryBackgroundColor, padZeros, isValidDate, loyaltyCardFromString } from './helpers.js';
+import { LitElement, html, nothing } from 'lit';
+import { LitOverlay } from './LitOverlay.js';
+
 import { db } from './dataStorage.js';
-import { getJourneys, newJourneys, ds100Reverse } from './app_functions.js';
-import { formatName, formatFromTo } from './formatters.js';
-import { modifySettings, settings } from './settings.js';
-import { go } from './router.js';
-import { showAlertModal, showSelectModal, showLoader, hideOverlay} from './overlays.js';
-import { footerTemplate } from './templates.js';
 import { t } from './languages.js';
-import { showSettings } from './settingsView.js';
 import { client } from './hafasClient.js';
+import { getIBNRbyDS100 } from './ds100.js';
+import { formatPoint } from './formatters.js';
+import { newJourneys } from './app_functions.js';
+import { padZeros, sleep, queryBackgroundColor, setThemeColor } from './helpers.js';
+
+import { baseStyles, helperStyles, flexboxStyles, buttonInputStyles, iconStyles, searchViewStyles } from './styles.js';
+import { settings } from './settings.js';
+
+class SearchView extends LitOverlay {
+	static properties = {
+		settingsState: { state: true },
+		date:          { state: true },
+		numEnter:      { state: true },
+		noTransfers:   { state: true },
+		isArrival:     { state: true },
+		showHistory:   { state: true },
+		history:       { state: true },
+		location:      { state: true },
+	};
 
-const viewState = {
-	currDate:      new Date(),
-	numEnter:      0,
-	noTransfers:   false,
-	isArrival:     false,
-	dateValue:     '',
-	timeValue:     '',
-	dateTimeValue: '',
-	fromValue:     '',
-	viaValue:      '',
-	toValue:       '',
-	suggestions:   {
-		from: {},
-		via: {},
-		to: {},
-	},
-};
-
-viewState.dateValue     = `${viewState.currDate.getFullYear()}-${padZeros(viewState.currDate.getMonth()+1)}-${padZeros(viewState.currDate.getDate())}`;
-viewState.timeValue     = `${padZeros(viewState.currDate.getHours())}:${padZeros(viewState.currDate.getMinutes())}`;
-viewState.dateTimeValue = `${viewState.dateValue}T${viewState.timeValue}`;
-
-const productIcons = {
-	// DB
-	"nationalExpress": "icon-ice",
-	"national":        "icon-ic",
-	"regionalExpress": "icon-dzug",
-
-	// vbb, bvg
-	"express": "icon-icice",
-
-	// nahsh
-	"interregional": "icon-dzug",
-	"onCall":        "icon-taxi",
-
-	// SNCB
-	"intercity-p": "icon-ic",
-	"s-train":     "icon-suburban",
-
-	// RMV
-	"express-train":       "icon-ice",
-	"long-distance-train": "icon-ic",
-	"regiona-train":       "icon-regional",
-	"s-bahn":              "icon-suburban",
-	"u-bahn":              "icon-subway",
-	"watercraft":          "icon-ferry",
-	"ast":                 "icon-taxi",
-
-	// VRN
-	"regional-train":      "icon-regional",
-	"urban-train":         "icon-suburban",
-	"dial-a-ride":         "icon-taxi",
-//	"long-distance-train": "icon-icice",
-};
-
-const iconFor = id => {
-	return productIcons[id] || `icon-${id}`;
-};
-
-const searchTemplate = (journeysHistory) => html`
-	<div class="container searchView">
-		<div class="title flex-center">
-			<h1>${APPNAME}</h1>
-		</div>
-
-		<form class="center" id="form" @submit=${submitHandler}>
-			<div class="flex-row nowrap">
-				<input type="text" name="from" id="from" title="${t('from')}" placeholder="${t('from')}" value="${viewState.fromValue}" autocomplete="off"
-					@focus=${focusHandler} @blur=${blurHandler} @keydown=${keydownHandler} @keyup=${keyupHandler} @input=${loadSuggestions} required>
-				<div class="button icon-arrow2 ${!settings.showVia ? '' : 'flipped'}" id="viaButton" title="${t('via')}" @click=${toggleVia}></div>
-			</div>
-			<div class="suggestions" id="fromSuggestions"></div>
+	static styles = [
+		super.styles,
+		baseStyles,
+		helperStyles,
+		flexboxStyles,
+		buttonInputStyles,
+		iconStyles,
+		searchViewStyles
+	];
 
-			<div class="flex-row nowrap ${!settings.showVia ? 'hidden' : ''}" id="viaRow">
-				<input type="text" name="via" id="via" title="${t('via')}" placeholder="${t('via')}" value="${viewState.viaValue}" autocomplete="off"
-					@focus=${focusHandler} @blur=${blurHandler} @keydown=${keydownHandler} @keyup=${keyupHandler} @input=${loadSuggestions}>
-				<div class="button icon-arrow2 invisible"></div>
-			</div>
-			<div class="suggestions" id="viaSuggestions"></div>
+	constructor (...args) {
+		super(...args);
+
+		this.settingsState = settings.getState();
+		this.date          = new CustomDate();
+		this.numEnter      = 0;
+		this.noTransfers   = false,
+		this.isArrival     = false,
+		this.showHistory   = false;
+		this.history       = [];
+		this.location      = {
+			from: {
+				value: '',
+				suggestionsVisible: false,
+				suggestionSelected: null,
+				suggestion: null,
+				suggestions: [],
+			},
+			to: {
+				value: '',
+				suggestionsVisible: false,
+				suggestionSelected: null,
+				suggestion: null,
+				suggestions: [],
+			},
+			via: {
+				value: '',
+				suggestionsVisible: false,
+				suggestionSelected: null,
+				suggestion: null,
+				suggestions: [],
+			},
+		};
+	}
 
+	async connectedCallback() {
+		super.connectedCallback();
+		await sleep(200);
 
-			<div class="flex-row nowrap">
-				<input type="text" name="to" id="to" title="${t('to')}" placeholder="${t('to')}" value="${viewState.toValue}" autocomplete="off"
-					@focus=${focusHandler} @blur=${blurHandler} @keydown=${keydownHandler} @keyup=${keyupHandler} @input=${loadSuggestions} required>
-				<div class="button icon-swap" title="${t('swap')}" @click=${swapFromTo}></div>
-			</div>
-			<div class="suggestions" id="toSuggestions"></div>
-
-			<div class="flex-row">
-				<div class="selector">
-					<input type="radio" id="departure" name="deparr" ?checked=${!viewState.isArrival}>
-					<label for="departure">${t('departure')}</label>
-					<input type="radio" id="arrival" name="deparr" ?checked=${viewState.isArrival}>
-					<label for="arrival">${t('arrival')}</label>
-				</div>
+		setThemeColor(queryBackgroundColor(document, 'body'));
 
-				<div class="button now" title="${t('titleSetDateTimeNow')}" @click=${setDateTimeNow}>${t('now')}</div>
-				<input type="datetime-local" name="datetime" id="datetime" title="${t('date')} & ${t('time')}" value="${viewState.dateTimeValue}" class="${!settings.combineDateTime ? 'hidden' : nothing}" required>
-				<input type="time"           name="time"     id="time"     title="${t('time')}"                value="${viewState.timeValue}"     class="${!settings.combineDateTime ? nothing : 'hidden'}" required>
-				<input type="date"           name="date"     id="date"     title="${t('date')}"                value="${viewState.dateValue}"     class="${!settings.combineDateTime ? nothing : 'hidden'}" required>
-			</div>
+		await this.updateHistoryState();
 
-			<div class="flex-row">
-				<div class="selector rectangular">
-					${client.profile.products.map(prod => html`
-						<input type="checkbox" id="${prod.id}" name="${prod.id}" checked>
-						<label class="${iconFor(prod.id)}" for="${prod.id}" title="${t('product')}: ${prod.name}"></label>
-					`)}
-				</div>
+		this._unsubscribeSettingsState = settings.subscribe((state) => {
+			this.settingsState = state;
+			this.performUpdate();
+		});
+	}
+
+	disconnectedCallback () {
+		super.disconnectedCallback();
 
-				<div class="selector rectangular ${settings.profile === 'db' ? 'hidden' : ''}">
-					<input type="radio" id="accessibilityNone" name="accessibility" value="none" ?checked=${settings.accessibility === 'none'}>
-					<label class="icon-walk-fast" for="accessibilityNone" title="${t('accessibility')}: ${t('access_none')}"></label>
+		if (typeof this._unsubscribeSettingsState === 'function') {
+			this._unsubscribeSettingsState();
+			this._unsubscribeSettingsState = undefined;
+		}
+	}
 
-					<input type="radio" id="accessibilityPartial" name="accessibility" value="partial" ?checked=${settings.accessibility === 'partial'}>
-					<label class="icon-walk" for="accessibilityPartial" title="${t('accessibility')}: ${t('access_partial')}"></label>
+	async updated (previous) {
+		super.updated(previous);
 
-					<input type="radio" id="accessibilityComplete" name="accessibility" value="complete" ?checked=${settings.accessibility === 'complete'}>
-					<label class="icon-weelchair" for="accessibilityComplete" title="${t('accessibility')}: ${t('access_full')}"></label>
-				</div>
+		if (isDevServer) console.info('updated(): ', previous);
 
-				<div class="selector rectangular">
-					<input type="checkbox" id="bikeFriendly" name="bikeFriendly" ?checked=${settings.bikeFriendly}>
-					<label class="icon-bike" for="bikeFriendly" title="${t('titleBikeFriendly')}"></label>
-				</div>
+		if (previous.has('history') && previous.get('history') === undefined) await this.updateHistoryState()
 
-				<div class="selector rectangular">
-					<input type="checkbox" id="noTransfers" name="noTransfers" ?checked=${viewState.noTransfers}>
-					<label class="icon-seat" for="noTransfers" title="${t('titleNoTransfers')}"></label>
-				</div>
+		if (
+			previous.has('settingsState') &&
+			previous.get('settingsState') !== undefined &&
+			previous.get('settingsState').profile !== this.settingsState.profile
+		) await this.profileChangeHandler();
 
-				<div class="filler"></div>
+		if (isDevServer) console.info('settingsState: ', this.settingsState);
+	}
 
-				<div class="button icon-settings" title="${t('settings')}" @click=${showSettings}></div>
-				<button type="submit" tabindex="0" id="submit">${t('search')}</button>
-			</div>
+	async firstUpdated () {
+		await sleep(500);
+		this.renderRoot.querySelector('input[name=from]').focus();
+	}
+
+	renderContent () {
+		return html`
+			<div class="container">
+				<div class="title flex-center"><h1>${APPNAME}</h1></div>
+
+				<form class="center" id="form" @submit=${this.submitHandler}>
+					${['from', 'via', 'to'].map(name => html`
+					<div class="flex-row nowrap ${name === 'via' && !this.settingsState.showVia ? 'hidden' : ''}">
+						<input type="text" class="flex-grow" name="${name}" title="${t(name)}" placeholder="${t(name)}" .value=${this.location[name].value}
+						@focus=${this.focusHandler}
+						@blur=${this.blurHandler}
+						@keydown=${this.keydownHandler}
+						@keyup=${this.keyupHandler}
+						@input=${this.inputHandler} 
+						autocomplete="off"  ?required=${name !== 'via'}>
+
+						${name !== 'from' ? nothing : html`
+							<div class="button icon-arrow2 ${this.settingsState.showVia ? 'flipped' : ''}" title="${t('via')}"
+							@click=${this.settingsState.toggleShowVia}></div>
+						`}
+						${name !== 'via' ? nothing : html`<div class="button invisible"></div>`}
+						${name !== 'to' ? nothing : html`<div class="button icon-swap" title="${t('swap')}" @click=${this.swapFromTo}></div>`}
+					</div>
+
+					<div class="suggestions ${this.location[name].suggestionsVisible ? '' : 'hidden'}">
+					${this.location[name].suggestions.map((suggestion, index) => html`
+						<p class="${index !== this.location[name].suggestion ? nothing : 'selected'}"
+						@click=${(event) => this.setSuggestion(name, index, event.pointerType)}
+						@mouseover=${(event) => this.mouseOverHandler(event, name)}
+						@mouseout=${(event) => this.mouseOutHandler(event, name)}>${formatPoint(suggestion)}</p>
+					`)}
+					</div>
+					`)}
 
-			${journeysHistory.length ? html`
-				<div id="historyButton" class="arrowButton icon-arrow2" title="History" @click=${toggleHistory}></div>
-			` : nothing}
-		</form>
-
-		<div id="history" class="history center hidden">
-			${journeysHistory.map(element => html`
-			<div class="flex-row" @click="${() => {journeysHistoryAction(journeysHistory, element);}}">
-				<div class="from">
-					<small>${t('from')}:</small>
-					${formatName(element.fromPoint)}
-					${element.viaPoint ? html`
-						<div class="via">${t('via')} ${formatName(element.viaPoint)}</div>
+					<div class="flex-row">
+						<div class="selector">
+							<input type="radio" id="departure" name="isArrival" value="0" @change=${this.changeHandler} .checked=${!this.isArrival}>
+							<label for="departure">${t('departure')}</label>
+							<input type="radio" id="arrival" name="isArrival" value="1" @change=${this.changeHandler} .checked=${this.isArrival}>
+							<label for="arrival">${t('arrival')}</label>
+						</div>
+
+						<div class="button now" title="${t('titleSetDateTimeNow')}" @click=${this.resetDate}>${t('now')}</div>
+						${!this.settingsState.combineDateTime ? html`
+						<input type="time" name="time" title="${t('time')}" class="flex-grow" @change=${this.changeHandler} .value=${this.date.formatTime()} required>
+						<input type="date" name="date" title="${t('date')}" class="flex-grow" @change=${this.changeHandler} .value=${this.date.formatDate()} required>
+						` : html`
+						<input type="datetime-local" name="dateTime" title="${t('date')} & ${t('time')}" class="flex-grow"
+						@change=${this.changeHandler} .value=${this.date.formatDateTime()} required>
+						`}
+					</div>
+
+					<div class="flex-row">
+						<div class="selector rectangular">
+						${client.profile.products.map(product => html`
+							<input type="checkbox" name="${product.id}" id="${product.id}"
+							@change=${(event) => this.settingsState.toggleProduct(event.target.name)} .checked=${this.settingsState.products[product.id] ?? true}>
+							<label class="${this.iconForProduct(product.id)}" for="${product.id}" title="${t('product')}: ${product.name}"></label>
+						`)}
+						</div>
+
+						<div class="selector rectangular ${settings.profile === 'db' ? 'hidden' : ''}">
+							<input type="radio" id="accessibilityNone" name="accessibility" value="none"
+							@change=${this.changeHandler} .checked=${this.settingsState.accessibility === 'none'}>
+							<label class="icon-walk-fast" for="accessibilityNone" title="${t('accessibility')}: ${t('access_none')}"></label>
+
+							<input type="radio" id="accessibilityPartial" name="accessibility" value="partial"
+							@change=${this.changeHandler} .checked=${this.settingsState.accessibility === 'partial'}>
+							<label class="icon-walk" for="accessibilityPartial" title="${t('accessibility')}: ${t('access_partial')}"></label>
+
+							<input type="radio" id="accessibilityComplete" name="accessibility" value="complete"
+							@change=${this.changeHandler} .checked=${this.settingsState.accessibility === 'complete'}>
+							<label class="icon-weelchair" for="accessibilityComplete" title="${t('accessibility')}: ${t('access_full')}"></label>
+						</div>
+
+						<div class="selector rectangular">
+							<input type="checkbox" id="bikeFriendly" name="bikeFriendly" @change=${this.settingsState.toggleBikeFriendly} .checked=${this.settingsState.bikeFriendly}>
+							<label class="icon-bike" for="bikeFriendly" title="${t('titleBikeFriendly')}"></label>
+						</div>
+
+						<div class="selector rectangular">
+							<input type="checkbox" id="noTransfers" name="noTransfers"   @change=${this.changeHandler} .checked=${this.noTransfers}>
+							<label class="icon-seat" for="noTransfers" title="${t('titleNoTransfers')}"></label>
+						</div>
+
+						<div class="filler"></div>
+
+						<div class="button icon-settings" title="${t('settings')}" @click=${this.showSettings}></div>
+						<button type="submit" tabindex="0" id="submit">${t('search')}</button>
+					</div>
+
+					${this.history.length !== 0 ? html`
+					<div id="historyButton" class="arrowButton icon-arrow2 ${!this.showHistory ? '' : 'flipped'}" title="History" @click=${this.toggleHistory}></div>
 					` : nothing}
+				</form>
+
+				${!this.showHistory ? nothing : html`
+				<div id="history" class="history center">
+					${this.history.map((element, index) => html`
+					<div class="flex-row" @click="${() => this.journeysHistoryAction(index)}">
+						<div class="from">
+							<small>${t('from')}:</small>
+							${formatPoint(element.fromPoint)}
+							${element.viaPoint ? html`<div class="via">${t('via')} ${formatPoint(element.viaPoint)}</div>` : nothing}
+						</div>
+						<div class="icon-arrow1"></div>
+						<div class="to">
+							<small>${t('to')}:</small>
+							${formatPoint(element.toPoint)}
+						</div>
+					</div>
+					`)}
 				</div>
-				<div class="icon-arrow1"></div>
-				<div class="to">
-					<small>${t('to')}:</small>
-					${formatName(element.toPoint)}
-				</div>
+				`}
+				<footer-component></footer-component>
 			</div>
-			`)}
-		</div>
-	</div>
-
-	${footerTemplate}
-`;
-
-const journeysHistoryAction = (journeysHistory, element) => {
-	const options = [
-		{'label': t('setfromto'),           'action': () => { setFromHistory(element.key); hideOverlay(); }},
-		{'label': t('journeyoverview'),     'action': () => go(`/${element.slug}/${settings.journeysViewMode}`)}
-	];
-
-	if (element.lastSelectedJourneyId !== undefined)
-		options.push({'label': t('lastSelectedJourney'), 'action': () => go(`/j/${settings.profile}/${element.lastSelectedJourneyId}`)})
-
-	showSelectModal(options);
-};
-
-export const searchView = async (clearInputs) => {
-	const journeysHistory = (await db.getHistory(settings.profile)).slice().reverse();
-
-	render(searchTemplate(journeysHistory), ElementById('content'));
-	setThemeColor();
-
-	if (clearInputs === true) {
-		viewState.fromValue = '';
-		viewState.viaValue  = '';
-		viewState.toValue   = '';
-		viewState.suggestions.from = {};
-		viewState.suggestions.via  = {};
-		viewState.suggestions.to   = {};
-		ElementById('from').value = '';
-		ElementById('via').value = '';
-		ElementById('to').value = '';
-		ElementById('fromSuggestions').textContent = '';
-		ElementById('viaSuggestions').textContent = '';
-		ElementById('toSuggestions').textContent = '';
+	    `;
 	}
 
-	for (const [product, enabled] of Object.entries(settings.products)) {
-		const element = ElementById(product);
-		if (!element) continue;
-
-		element.checked = enabled;
+	swapFromTo = () => {
+		this.location.from = [this.location.to, this.location.to = this.location.from][0];
+		this.requestUpdate();
 	}
 
-	ElementById('from').focus();
-};
+	resetDate = () => {
+		this.date = new CustomDate(); 
+		this.requestUpdate();
+	}
 
-const submitHandler = async (event) => {
-	event.preventDefault();
+	showSettings = () => this.showDialogOverlay('settings', html`<settings-view></settings-view>`);
 
-	await modifySettings(settings => {
-		settings.products      = readProductSelection(settings);
-		settings.accessibility = document.querySelector('input[name="accessibility"]:checked').value;
-		settings.bikeFriendly  = ElementById('bikeFriendly').checked
-		return settings;
-	});
+	updateHistoryState = async () => this.history = (await db.getHistory(this.settingsState.profile)).slice().reverse();
+	toggleHistory      = ()       => this.showHistory = !this.showHistory;
 
+	journeysHistoryAction = num => {
+		const element = this.history[num];
 
-	viewState.fromValue     = ElementById('from').value.trim();
-	viewState.viaValue      = ElementById('via').value.trim();
-	viewState.toValue       = ElementById('to').value.trim();
-	viewState.dateValue     = ElementById('date').value.trim();
-	viewState.timeValue     = ElementById('time').value.trim();
-	viewState.dateTimeValue = ElementById('datetime').value.trim();
-	viewState.isArrival     = ElementById('arrival').checked;
-	viewState.noTransfers   = ElementById('noTransfers').checked;
+		const options = [
+			{'label': t('setfromto'),       'action': () => { this.setFromHistory(element.key); this.hideOverlay(); }},
+			{'label': t('journeyoverview'), 'action': () => { window.location = `#/${element.slug}/${this.settingsState.journeysViewMode}`; this.hideOverlay(); }}
+		];
 
+		if (element.lastSelectedJourneyId !== undefined) {
+			options.push({
+				'label':  t('lastSelectedJourney'),
+				'action': () => { window.location = `#/j/${this.settingsState.profile}/${element.lastSelectedJourneyId}`; this.hideOverlay(); }
+			});
+		}
 
-	// check if From or To empty
-	if (viewState.fromValue === '' || viewState.toValue   === '') {
-		showAlertModal('At least From and To need to be filled!');
-		return false;
+		this.showSelectOverlay(options);
 	}
 
+	 setFromHistory = async id => {
+		const entry = await db.getHistoryEntry(id);
+		if (!entry) return;
 
-	// date and time
-	if (!settings.combineDateTime) {
-		viewState.dateTimeValue = `${viewState.dateValue}T${viewState.timeValue}`;
-	} else {
-		const splitedDateTimeValue = viewState.dateTimeValue.split('T');
-		viewState.dateValue = splitedDateTimeValue[0];
-		viewState.timeValue = splitedDateTimeValue[1];
-	};
+		[ 'from', 'via', 'to' ].forEach(mode => {
+			if ( entry[`${mode}Point`] === null) return false;
+			if (mode === 'via') this.settingsState.setShowVia(true);
 
-	if (viewState.dateValue !== '') {
-		if (!isValidDate(viewState.dateValue)) {
-			showAlertModal('Invalid date');
-			return false;
-		}
-	} else {
-		viewState.dateValue = `${viewState.currDate.getFullYear()}-${padZeros(viewState.currDate.getMonth()+1)}-${padZeros(viewState.currDate.getDate())}`;
+			this.location[mode].value              = formatPoint(entry[`${mode}Point`]);
+			this.location[mode].suggestionSelected = entry[`${mode}Point`];
+		});
+
+		this.requestUpdate();
 	}
 
-	if (viewState['timeValue'] !== '') {
-		if (!new RegExp('([0-1][0-9]|2[0-3]):([0-5][0-9])').test(viewState.timeValue)) {
-			showAlertModal('Invalid time');
-			return false;
-		}
-	} else {
-		viewState['timeValue'] = `${padZeros(viewState.currDate.getHours())}:${padZeros(viewState.currDate.getMinutes())}`;
+	iconForProduct = id => {
+		const productIcons = {
+			// DB
+			"nationalExpress": "icon-ice",
+			"national":        "icon-ic",
+			"regionalExpress": "icon-dzug",
+
+			// BVG
+			"express": "icon-icice",
+
+			// nahsh
+			"interregional": "icon-dzug",
+			"onCall":        "icon-taxi",
+
+			// SNCB
+			"intercity-p": "icon-ic",
+			"s-train":     "icon-suburban",
+
+			// RMV
+			"express-train":       "icon-ice",
+			"long-distance-train": "icon-ic",
+			"regiona-train":       "icon-regional",
+			"s-bahn":              "icon-suburban",
+			"u-bahn":              "icon-subway",
+			"watercraft":          "icon-ferry",
+			"ast":                 "icon-taxi",
+		};
+
+		return productIcons[id] || `icon-${id}`;
 	}
 
-	const splitedDate = viewState.dateValue.split('-');
-	const splitedTime = viewState.timeValue.split(':');
-	const timestamp   = Math.round(new Date(splitedDate[0], splitedDate[1]-1, splitedDate[2], splitedTime[0], splitedTime[1]).getTime()/1000);
+	profileChangeHandler = async () => {
+		[ 'from', 'via','to' ].forEach(name => {
+			this.location[name] = {
+				value: '',
+				suggestionsVisible: false,
+				suggestionSelected: null,
+				suggestion: null,
+				suggestions: [],
+			};
+		});
+
+		await this.updateHistoryState();
+	}
 
+	submitHandler = async event => {
+		event.preventDefault();
 
-	// from
-	let from = '';
+		const params = {
+			from: null,
+			to: null,
+			via: null,
+			bike:         this.settingsState.bikeFriendly,
+			transferTime: this.settingsState.transferTime,
+			results:  6,
+			products: {},
+		};
+
+		await Promise.all([ 'from', 'via','to' ].map(async mode => {
+			if (this.location[mode].value !== '') {
+				if (mode === 'via' && this.settingsState.showVia === false) return false;
+				if (this.location[mode].suggestionSelected !== null) {
+					params[mode] = this.location[mode].suggestionSelected;
+				} else {
+					if (this.location[mode].suggestions.length !== 0) {
+						params[mode] = this.location[mode].suggestions[0]
+					} else {
+						const data = await client.locations(this.location[mode].value, {'results': 1});
+						if (!data[0]) return false;
+						params[mode] = data[0];
+					}
+				}
+			}
+		}));
 
-	if (Object.entries(viewState.suggestions.from).length !== 0) {
-		from = viewState.suggestions.from;
-	} else {
-		const data = await client.locations(viewState.fromValue, {'results': 1});
+		if (params.from === null || params.to === null) return false;
 
-		if (!data[0]) {
-			showAlertModal('Invalid From');
+		if (formatPoint(params.from) === formatPoint(params.to) && params.via === null) {
+			this.showAlertOverlay('From and To are the same place.');
 			return false;
-		}
-
-		from = data[0];
-	}
+		};
 
+		client.profile.products.forEach(product => {
+			params.products[product.id] = this.settingsState.products[product.id] ?? true;
+		});
 
-	// via
-	let via  = '';
+		if (this.noTransfers) params.transfers = 0;
 
-	if (viewState.viaValue == '') {
-		via = null;
-	} else if (Object.entries(viewState.suggestions.via).length !== 0) {
-		via = viewState.suggestions.via;
-	} else {
-		const data = await client.locations(viewState.viaValue, {'results': 1});
+		if (!this.isArrival)  params.departure = this.date.getTime();
+		else                  params.arrival   = this.date.getTime();
 
-		if (!data[0]) {
-			showAlertModal('Invalid Via');
-			return false;
+		if (this.settingsState.profile !== 'db') {
+			params.accessibility = this.settingsState.accessibility;
+			params.walkingSpeed  = this.settingsState.walkingSpeed;
 		}
 
-		via = data[0];
-	}
-
-
-	// to
-	let to   = '';
+		if (isDevServer) console.info('submitHandler(): ',params);
 
-	if (Object.entries(viewState.suggestions.to).length !== 0) {
-		to = viewState.suggestions.to;
-	} else {
-		const data = await client.locations(viewState.toValue, {'results': 1});
+		this.showLoaderOverlay();
 
-		if (!data[0]) {
-			showAlertModal('Invalid To');
-			return false;
+		let responseData;
+		try {
+			responseData = await newJourneys(params);
+		} catch(e) {
+			this.showAlertOverlay(e.toString());
+			console.error(e);
 		}
 
-		to = data[0];
-	}
-
-	if (formatName(to) === formatName(from) && via === null) {
-		showAlertModal('From and To are the same place.');
-		return false;
-	};
-
+		this.hideOverlay();
 
-	const params = {
-		from,
-		to,
-		results: 6,
-		bike: settings.bikeFriendly,
-		products: settings.products,
-	};
+		window.location = `#/${responseData.slug}/${this.settingsState.journeysViewMode}`;
+	}
 
-	if (via)                   params.via       = via;
+	mouseOverHandler (event, name) { this.location[name].suggestionsFocused = true; }
+	mouseOutHandler  (event, name) { this.location[name].suggestionsFocused = false; }
 
-	if (viewState.noTransfers) params.transfers = 0;
+	focusHandler = event => {
+		const name = event.target.name;
 
-	if (!viewState.isArrival)  params.departure = timestamp * 1000;
-	else                       params.arrival   = timestamp * 1000;
+		this.location[name].suggestionsVisible = true;
+		this.requestUpdate();
+	}
 
-	showLoader();
+	blurHandler = event => {
+		const name = event.target.name;
 
-	let responseData;
-	try {
-		responseData = await newJourneys(params);
-	} catch(e) {
-		showAlertModal(e.toString());
-		throw e;
+		if (!this.location[name].suggestionsFocused) {
+			this.location[name].suggestionsVisible = false;
+			this.requestUpdate();
+		}
 	}
 
-	hideOverlay();
+	focusNextElement = currentElementId => {
+		switch (currentElementId) {
+			case 'from':
+				this.renderRoot.querySelector('input[name=to]').focus();
 
-	go(`/${responseData.slug}/${settings.journeysViewMode}`);
-};
+				if (this.settingsState.showVia) this.renderRoot.querySelector('input[name=via]').focus();
+				break;
 
-const toggleHistory = () => {
-	const historyElement = ElementById('history');
-	const buttonElement  = ElementById('historyButton');
+			case 'via':
+				this.renderRoot.querySelector('input[name=to]').focus();
+				break;
 
-	if (!elementHidden(historyElement)) {
-		unflipElement(buttonElement);
-		hideElement(historyElement);
-	} else {
-		flipElement(buttonElement);
-		showElement(historyElement);
+			case 'to':
+				this.renderRoot.querySelector('[type=submit]').focus();
+				break;
+		}
 	}
-}
 
-const toggleVia = async mode => {
-	const rowElement    = ElementById('viaRow');
-	const buttonElement = ElementById('viaButton');
-
-	if (mode === 'show' || elementHidden(rowElement)) {
-		flipElement(buttonElement);
-		showElement(rowElement);
-	} else {
-		unflipElement(buttonElement);
-		hideElement(rowElement);
-		ElementById('via').value = '';
+	setSuggestion = (name, num, pointerType) => {
+		this.location[name].value              = formatPoint(this.location[name].suggestions[num]);
+		this.location[name].suggestionSelected = this.location[name].suggestions[num];
+		this.location[name].suggestionsVisible = false;
+		this.requestUpdate();
+		if (pointerType !== '') this.focusNextElement(name);
 	}
 
-	await modifySettings(settings => {
-		settings.showVia = !elementHidden(rowElement);
-		return settings;
-	});
-}
-
-const swapFromTo = () => {
-	viewState.suggestions.from = [viewState.suggestions.to, viewState.suggestions.to = viewState.suggestions.from][0];
-
-	const from = ElementById('from');
-	const to   = ElementById('to');
-
-	from.value = [to.value, to.value = from.value][0];
-};
-
-const setFromHistory = async id => {
-	const entry = (await db.getHistoryEntry(id));
+	keyupHandler = event => {
+		const name  = event.target.name;
+		const value = event.target.value;
 
-	if (!entry) return;
+		if (event.key !== 'Enter') return true;
 
-	setSuggestion(entry.fromPoint, 'from');
-
-	if (entry.viaPoint !== undefined) {
-		setSuggestion(entry.viaPoint, 'via');
-		toggleVia('show');
+		if (this.numEnter === 2 && value === formatPoint(this.location[name].suggestionSelected)) {
+			this.numEnter = 0;
+			this.focusNextElement(name);
+		}
 	};
 
-	setSuggestion(entry.toPoint, 'to');
-};
-
-const setDateTimeNow = () => {
-	viewState.currDate            = new Date();
-	viewState.dateValue           = `${viewState.currDate.getFullYear()}-${padZeros(viewState.currDate.getMonth()+1)}-${padZeros(viewState.currDate.getDate())}`;
-	viewState.timeValue           = `${padZeros(viewState.currDate.getHours())}:${padZeros(viewState.currDate.getMinutes())}`;
-	viewState.dateTimeValue       = `${viewState.dateValue}T${viewState.timeValue}`;
-	ElementById('date').value     = viewState.dateValue;
-	ElementById('time').value     = viewState.timeValue;
-	ElementById('datetime').value = viewState.dateTimeValue;
-};
-
-const readProductSelection = settings => {
-	const productsMap = {};
-
-	for (const prod of client.profile.products) {
-		productsMap[prod.id] = ElementById(prod.id).checked;
-	}
-
-	return productsMap;
-};
-
-const showSuggestions = (id) => {
-	viewState.numEnter = 0;
-	showElement(ElementById(`${id}Suggestions`));
-};
-
-const hideSuggestions = (id) => {
-	const suggestionsElement = ElementById(`${id}Suggestions`);
-
-	hideElement(suggestionsElement);
-	suggestionsElement.classList.remove('mouseover');
-};
-
-const suggestionsTemplate = (data, inputId) => data.map((element, index) => html`
-	<p id="${index == 0 ? inputId+'Selected' : ''}" class="suggestion"
-		@mouseover=${mouseOverHandler} @mouseout=${mouseOutHandler} @click=${(event) => setSuggestion(encodeURI(JSON.stringify(element)), inputId, event.pointerType)}>${formatName(element)}</p>
-`);
-
-const loadSuggestions = async (event) => {
-	const elementId    = event.target.id;
-	const elementValue = event.target.value.trim();
-
-	viewState.suggestions[elementId] = {};
+	keydownHandler = event => {
+		const name = event.target.name;
 
-	if (elementValue === '') return;
-
-	let results;
-	const ds100Result = ds100Reverse(elementValue);
-
-	if (ds100Result !== null) {
-		results = await client.locations(ds100Result, {'results': 1});
-	} else {
-		results = await client.locations(elementValue, {'results': 10})
-	}
+		if (this.location[name].suggestions.length == 0) return true;
+	
+		if (event.key === 'Enter') {
+			event.preventDefault();
+			this.numEnter++;
+			this.setSuggestion(name, this.location[name].suggestion);
+			return true;
+		};
+
+		if (!this.location[name].suggestionsVisible) {
+			this.numEnter = 0;
+			this.location[name].suggestionsVisible = true;
+			this.requestUpdate();
+			return true;
+		}
 
-	render(suggestionsTemplate(results, elementId), ElementById(`${elementId}Suggestions`));
-};
+		if (['Escape', 'Tab'].includes(event.key)) {
+			this.location[name].suggestionsVisible = false;
+		}
 
-const focusNextElement = (currentElementId) => {
-	switch (currentElementId) {
-		case 'from':
-			ElementById('to').focus();
+		if (['ArrowUp', 'ArrowDown'].includes(event.key) && !event.shiftKey) {
+			event.preventDefault();
 
-			if (!elementHidden(ElementById('via'))) {
-				ElementById('via').focus();
+			const numSuggesttions = this.location[name].suggestions.length-1;
+
+			if (event.key === 'ArrowUp') {
+				if (this.location[name].suggestion === 0) {
+					this.location[name].suggestion = numSuggesttions;
+				} else {
+					this.location[name].suggestion--;
+				}
+			} else {
+				if (this.location[name].suggestion === numSuggesttions) {
+					this.location[name].suggestion = 0;
+				} else {
+					this.location[name].suggestion++;
+				}
 			}
-			break;
-
-		case 'via':
-			ElementById('to').focus();
-			break;
-
-		case 'to':
-			hideSuggestions(currentElementId);
-			ElementById('submit').focus();
-			break;
+		}
+		this.requestUpdate();
 	}
-};
 
-const setSuggestion = (data, inputId, pointerType) => {
-	if (typeof data === 'string') {
-		data = JSON.parse(decodeURI(data));
+	inputHandler = async event => {
+		const name  = event.target.name;
+		const value = event.target.value.trim();
+
+		if (['from', 'via', 'to'].includes(name)) { 
+			if (value === '') return;
+
+			this.location[name].value = value;
+		
+			let   suggestions;
+			const ds100Result = getIBNRbyDS100(value);
+		
+			if (ds100Result !== null) { suggestions = await client.locations(ds100Result, {'results':  1}); }
+			else                      { suggestions = await client.locations(value,       {'results': 10}); }
+
+			this.location[name].suggestionSelected = null;
+			this.location[name].suggestion         = 0;
+			this.location[name].suggestions        = suggestions;
+			this.requestUpdate();
+		};
 	}
 
-	ElementById(inputId).value     = formatName(data);
-	viewState.suggestions[inputId] = data;
-
-	hideSuggestions(inputId);
-	if (pointerType !== '') focusNextElement(inputId);
-};
-
-const mouseOverHandler = (event) => event.target.parentElement.classList.add('mouseover');
-const mouseOutHandler  = (event) => event.target.parentElement.classList.remove('mouseover');
-
-const focusHandler = (event) => showSuggestions(event.target.id);
-const blurHandler  = (event) => {
-	if (!ElementById(`${event.target.id}Suggestions`).classList.contains('mouseover')) hideSuggestions(event.target.id);
-};
+	changeHandler = event => {
+		const name  = event.target.name;
+		const value = event.target.value;
 
-const keyupHandler = (event) => {
-	const eventElement = event.target;
+		if (name === 'accessibility') this.settingsState.setAccessibility(value);
+		if (name === 'noTransfers')   this.noTransfers = !this.noTransfers;
+		if (name === 'isArrival')     this.isArrival = !this.isArrival;
+		if (name === 'dateTime')      this.date.setDateTime(value);
+		if (name === 'date')          this.date.setDate(value);
+		if (name === 'time')          this.date.setTime(value);
 
-	if (event.key !== 'Enter') return true;
-
-	if (viewState.numEnter === 2 && eventElement.value === formatName(viewState.suggestions[eventElement.id])) {
-		viewState.numEnter = 0;
-		focusNextElement(eventElement.id);
+		this.requestUpdate();
 	}
-};
-
-const keydownHandler = (event) => {
-	const eventElement           = event.target;
-	const firstSuggestionElement = document.querySelector(`#${eventElement.id}Suggestions>p:first-child`);
-	const lastSuggestionElement  = document.querySelector(`#${eventElement.id}Suggestions>p:last-child`);
-	const selectedElement        = ElementById(`${eventElement.id}Selected`);
+}
 
-	if (selectedElement === null) return true;
+class CustomDate extends Date {
+	formatDate()     { return `${this.getFullYear()}-${padZeros(this.getMonth()+1)}-${padZeros(this.getDate())}`; }
+	formatTime()     { return `${padZeros(this.getHours())}:${padZeros(this.getMinutes())}`; }
+	formatDateTime() { return `${this.formatDate()}T${this.formatTime()}`; }
 
-	if (event.key === 'Enter') {
-		event.preventDefault();
-		selectedElement.click();
-		viewState.numEnter++;
-		return true;
-	};
-
-	if (elementHidden(ElementById(`${eventElement.id}Suggestions`))) {
-		showSuggestions(eventElement.id);
-		return true;
+	setDate(value) {
+		const splitValue = value.split('-');
+		if (splitValue.length !== 3) return false;
+		this.setFullYear(splitValue[0], splitValue[1]-1, splitValue[2]);
 	}
 
-	switch (event.key) {
-		case 'Escape':
-		case 'Tab':
-			hideSuggestions(eventElement.id);
-			break;
-
-		case 'ArrowUp':
-		case 'ArrowDown':
-			event.preventDefault();
-
-			if (event.shiftKey) break;
-
-			let nextElement                          = selectedElement.nextElementSibling     ?? firstSuggestionElement;
-			if (event.key === 'ArrowUp') nextElement = selectedElement.previousElementSibling ?? lastSuggestionElement;
+	setTime(value) {
+		const splitValue = value.split(':');
+		if (splitValue.length !== 2) return false;
+		this.setHours(splitValue[0], splitValue[1]);
+	}
 
-			selectedElement.id = '';
-			nextElement.id     = `${eventElement.id}Selected`;
-			break;
+	setDateTime(value) {
+		const splitValue = value.split('T');
+		if (splitValue.length !== 2) return false;
+		this.setDate(splitValue[0]);
+		this.setTime(splitValue[1]);
 	}
-};
+}
+
+customElements.define('search-view', SearchView);
diff --git a/src/settings.js b/src/settings.js
@@ -1,47 +1,59 @@
-import { db } from './dataStorage.js';
-import { getDefaultLanguage } from './languages.js';
+import { createStore } from 'zustand/vanilla';
+import { persist, createJSONStorage } from 'zustand/middleware'
 
-const subscribers     = [];
-const defaultSettings = {
-	language: getDefaultLanguage(),
-	profile: "db",
-	products: {
-		'nationalExpress': true,
-		'national': true,
-		'regionalExpress': true,
-		'regional': true,
-		'suburban': true,
-		'bus': true,
-		'ferry': true,
-		'subway': true,
-		'tram': true,
-		'taxi': true
-	},
-	accessibility: 'none',
-	walkingSpeed: 'normal',
-	bikeFriendly: false,
-	loyaltyCard: 'NONE',
-	ageGroup: 'E',
-	journeysViewMode: 'canvas',
-	combineDateTime: false,
-	showPrices: true,
-	showDS100: true,
-	showVia: false,
-};
+import { db, initDataStorage } from './dataStorage.js';
+import { getDefaultLanguage } from './languages.js';
+import { getDefaultProfile } from './hafasClient.js';
 
-export let settings;
+export let   settingsState;
+export const settings = createStore()(
+	persist(
+		(set, get) => ({
+			language: getDefaultLanguage(),
+			profile: getDefaultProfile(),
+			products: {},
+			accessibility: 'none',
+			walkingSpeed: 'normal',
+			transferTime: 0,
+			loyaltyCard: 'NONE',
+			ageGroup: 'E',
+			journeysViewMode: 'canvas',
+			bikeFriendly: false,
+			combineDateTime: false,
+			showVia: false,
+			showPrices: true,
+			showDS100: true,
+			disableCanvasBlur: false,
 
-export const subscribeSettings = cb => subscribers.push(cb);
+			setJourneysViewMode: (journeysViewMode) => set({ journeysViewMode }),
+			setLanguage:         (language)         => set({ language }),
+			setProfile:          (profile)          => set({ profile }),
+			setTransferTime:     (transferTime)     => set({ transferTime }),
+			setAgeGroup:         (ageGroup)         => set({ ageGroup }),
+			setLoyaltyCard:      (loyaltyCard)      => set({ loyaltyCard }),
+			setAccessibility:    (accessibility)    => set({ accessibility }),
+			setShowVia:          (showVia)          => set({ showVia }),
 
-export const initSettings = async () => {
-	settings = (await db.getSettings()) || defaultSettings;
+			toggleCombineDateTime: ()    => set((state) => ({ combineDateTime: !state.combineDateTime })),
+			toggleShowPrices:      ()    => set((state) => ({ showPrices:      !state.showPrices })),
+			toggleShowDS100:       ()    => set((state) => ({ showDS100:       !state.showDS100 })),
+			toggleShowVia:         ()    => set((state) => ({ showVia:         !state.showVia })),
+			toggleBikeFriendly:    ()    => set((state) => ({ bikeFriendly:    !state.bikeFriendly })),
+			toggleProduct:         (key) => set((state) => {
+				state.products[key] = !state.products[key];
+				return { products: state.products };
+			})
+		}),
+		{
+			name: 'settings'
+		}
+	)
+)
 
-	for (const cb of subscribers) await cb();
-};
+export const initSettingsState = async () => {
+	settingsState = settings.getState();
 
-export const modifySettings = async callback => {
-	const newSettings = await db.modifySettings(JSON.parse(JSON.stringify(settings)), callback);
-	Object.freeze(newSettings);
-	settings = newSettings;
-	for (const cb of subscribers) await cb();
+	settings.subscribe((state) => {
+		settingsState = state;
+	});
 };
diff --git a/src/settingsView.js b/src/settingsView.js
@@ -1,133 +1,144 @@
-import { html, render } from 'lit-html';
-import { db, clearDataStorage } from './dataStorage.js';
-import { modifySettings, settings } from './settings.js';
-import { showModal, hideOverlay } from './overlays.js';
-import { ElementById, hideElement, showElement } from './helpers.js';
-import { t, getLanguages } from './languages.js';
-import { searchView } from './searchView.js';
-import { initHafasClient } from './hafasClient.js';
-
-export const showSettings = async () => {
-	showModal(t('settings'), settingsTemplate());
-	profileChangeHandler(settings.profile);
-};
-
-const settingsTemplate = () => html`
-	<div class="settingsView">
-		<div class="flex-row">
-			<label for="language">${t('language')}:</label>
-			<select id="language">
-				${getLanguages().map(lang => html`
-					<option value="${lang}" ?selected=${settings.language === lang}>${t(lang)}</option>
-				`)}
-			</select>
-		</div>
-
-		<div class="flex-row">
-			<label for="profile">${t('datasource')}:</label>
-			<select id="profile" @change="${(event) => {profileChangeHandler(event.target.value)}}">
-				<option value="db"    ?selected=${settings.profile === 'db'}>DB</option>
-				<option value="nahsh" ?selected=${settings.profile === 'nahsh'}>NAH.SH</option>
-				<option value="rmv"   ?selected=${settings.profile === 'rmv'}>RMV</option>
-				<option value="bvg"   ?selected=${settings.profile === 'bvg'}>BVG</option>
-				<option value="oebb"  ?selected=${settings.profile === 'oebb'}>ÖBB</option>
-			</select>
-		</div>
-
-		<div class="flex-row" id="walkingSpeedElement">
-			<label for="walkingSpeed">${t('walkingSpeed')}:</label>
-			<select id="walkingSpeed">
-				<option value="slow"   ?selected=${settings.walkingSpeed === 'slow'}>${t('walkingSpeedSlow')}</option>
-				<option value="normal" ?selected=${settings.walkingSpeed === 'normal'}>${t('walkingSpeedNormal')}</option>
-				<option value="fast"   ?selected=${settings.walkingSpeed === 'fast'}>${t('walkingSpeedFast')}</option>
-			</select>
-		</div>
-
-		<div class="flex-row" id="ageGroupElement">
-			<label for="ageGroup">${t('ageGroup')}:</label>
-			<select id="ageGroup">
-				<option value="K" ?selected=${settings.ageGroup === 'K'}>${t('ageGroupChild')} (7-14)</option>
-				<option value="Y" ?selected=${settings.ageGroup === 'Y'}>${t('ageGroupYoung')} (15-26)</option>
-				<option value="E" ?selected=${settings.ageGroup === 'E'}>${t('ageGroupAdult')} (27-64)</option>
-				<option value="S" ?selected=${settings.ageGroup === 'S'}>${t('ageGroupSenior')} (65+)</option>
-			</select>
-		</div>
-
-		<div class="flex-row" id="loyaltyCardElement">
-			<label for="loyaltyCard">${t('loyaltyCard')}:</label>
-			<select id="loyaltyCard">
-				<option value="NONE"          ?selected=${settings.loyaltyCard === 'NONE'}>${t('loyaltyCardNone')}</option>
-				<option value="BAHNCARD-25-2" ?selected=${settings.loyaltyCard === 'BAHNCARD-25-2'}>BahnCard 25, 2. ${t("class")}</option>
-				<option value="BAHNCARD-25-1" ?selected=${settings.loyaltyCard === 'BAHNCARD-25-1'}>BahnCard 25, 1. ${t("class")}</option>
-				<option value="BAHNCARD-50-2" ?selected=${settings.loyaltyCard === 'BAHNCARD-50-2'}>BahnCard 50, 2. ${t("class")}</option>
-				<option value="BAHNCARD-50-1" ?selected=${settings.loyaltyCard === 'BAHNCARD-50-1'}>BahnCard 50, 1. ${t("class")}</option>
-				<option value="BAHNCARD-100-2" ?selected=${settings.loyaltyCard === 'BAHNCARD-100-2'}>BahnCard 100, 2. ${t("class")}</option>
-				<option value="BAHNCARD-100-1" ?selected=${settings.loyaltyCard === 'BAHNCARD-100-1'}>BahnCard 100, 1. ${t("class")}</option>
-			</select>
-		</div>
-
-		<div class="flex-column">
-			<span>${t('options')}:</span>
-			<label id="showPricesElement"><input type="checkbox" id="showPrices" ?checked=${settings.showPrices}> ${t('show-prices')}<br></label>
-			<label id="showDS100Element"><input type="checkbox" id="showDS100" ?checked=${settings.showDS100}> ${t('showds100')}<br></label>
-			<label><input type="checkbox" id="combineDateTime" ?checked=${settings.combineDateTime}> ${t('combineDateTime')}</label>
-		</div>
-
-		<div class="flex-row">
-			<button class="color" id="clear" @click=${clearStorage}>${t('clearstorage')}</button>
-			<button class="color" id="save" @click=${saveSettings}>${t('save')}</button>
-		</div>
-	</div>
-`;
-
-const profileChangeHandler = (profile) => {
-	switch (profile) {
-		case 'db':
-			showElement(ElementById('showPricesElement'));
-			showElement(ElementById('loyaltyCardElement'));
-			showElement(ElementById('ageGroupElement'));
-			hideElement(ElementById('walkingSpeedElement'));
-			break;
-
-		default:
-			showElement(ElementById('walkingSpeedElement'));
-			hideElement(ElementById('showPricesElement'));
-			hideElement(ElementById('loyaltyCardElement'));
-			hideElement(ElementById('ageGroupElement'));
-			break;
+import { LitElement, html, nothing } from 'lit';
+
+import { sleep, queryBackgroundColor, setThemeColor } from './helpers.js';
+import { getLanguages, t } from './languages.js';
+import { settings } from './settings.js';
+import { initHafasClient, profiles } from './hafasClient.js';
+import { clearDataStorage } from './dataStorage.js';
+
+import { baseStyles, helperStyles, flexboxStyles, buttonInputStyles, settingsViewStyles } from './styles.js';
+
+class SettingsView extends LitElement {
+	static styles = [
+		baseStyles,
+		helperStyles,
+		flexboxStyles,
+		buttonInputStyles,
+		settingsViewStyles
+	];
+
+	static properties = {
+		viewState: { state: true },
 	};
-};
 
-const clearStorage = async () => {
-	clearDataStorage();
-
-	caches.keys().then(names => {
-		for (let name of names) caches.delete(name);
-	});
-
-	location.reload();
-};
-
-const saveSettings = async () => {
-	const profile     = ElementById('profile').value;
-	let   clearInputs = false;
-
-	if (profile !== settings.profile) clearInputs = true;
-
-	await modifySettings(settings => {
-		settings.combineDateTime = ElementById('combineDateTime').checked;
-		settings.showDS100       = ElementById('showDS100').checked;
-		settings.showPrices      = ElementById('showPrices').checked;
-		settings.language        = ElementById('language').value;
-		settings.profile         = profile;
-		settings.loyaltyCard     = ElementById('loyaltyCard').value;
-		settings.walkingSpeed    = ElementById('walkingSpeed').value;
-		settings.ageGroup        = ElementById('ageGroup').value;
+	constructor (...args) {
+		super(...args);
+		this.viewState = settings.getState();
+	}
+
+	connectedCallback () {
+		super.connectedCallback();
+
+		this._unsubscribeViewState = settings.subscribe((state, prev) => {
+			this.viewState = state;
+			this.performUpdate();
+		});
+	}
+
+	disconnectedCallback () {
+		super.disconnectedCallback();
+
+		if (typeof this._unsubscribeViewState === 'function') {
+			this._unsubscribeViewState();
+			this._unsubscribeViewState = undefined;
+		}
+	}
+
+
+	render () {
+		return html`
+			<div class="flex-row">
+				<label for="language">${t('language')}:</label>
+				<select id="language" @change=${this.changeHandler}>
+					${getLanguages().map(lang => html`<option value="${lang}" ?selected=${this.viewState.language === lang}>${t(lang)}</option>`)}
+				</select>
+			</div>
+
+			<div class="flex-row">
+				<label for="profile">${t('datasource')}:</label>
+				<select id="profile" @change=${this.changeHandler}>
+					${Object.keys(profiles).map(profile => html`<option value="${profile}" ?selected=${this.viewState.profile === profile}>${profiles[profile].name}</option>`)}
+				</select>
+			</div>
+
+			<div class="flex-row ${this.viewState.profile !== 'db' ? '' : 'hidden'}">
+				<label for="walkingSpeed">${t('walkingSpeed')}:</label>
+				<select id="walkingSpeed" @change=${this.changeHandler}>
+					<option value="slow"   ?selected=${this.viewState.walkingSpeed === 'slow'}>${t('walkingSpeedSlow')}</option>
+					<option value="normal" ?selected=${this.viewState.walkingSpeed === 'normal'}>${t('walkingSpeedNormal')}</option>
+					<option value="fast"   ?selected=${this.viewState.walkingSpeed === 'fast'}>${t('walkingSpeedFast')}</option>
+				</select>
+			</div>
+
+			<div class="flex-row ${this.viewState.profile !== 'db' ? 'hidden' : ''}">
+				<label for="ageGroup">${t('ageGroup')}:</label>
+				<select id="ageGroup" @change=${this.changeHandler}>
+					<option value="K" ?selected=${this.viewState.ageGroup === 'K'}>${t('ageGroupChild')} (7-14)</option>
+					<option value="Y" ?selected=${this.viewState.ageGroup === 'Y'}>${t('ageGroupYoung')} (15-26)</option>
+					<option value="E" ?selected=${this.viewState.ageGroup === 'E'}>${t('ageGroupAdult')} (27-64)</option>
+					<option value="S" ?selected=${this.viewState.ageGroup === 'S'}>${t('ageGroupSenior')} (65+)</option>
+				</select>
+			</div>
+
+			<div class="flex-row ${this.viewState.profile !== 'db' ? 'hidden' : ''}">
+				<label for="loyaltyCard">${t('loyaltyCard')}:</label>
+				<select id="loyaltyCard" @change=${this.changeHandler}>
+					<option value="NONE"           ?selected=${this.viewState.loyaltyCard === 'NONE'}>${t('loyaltyCardNone')}</option>
+					<option value="BAHNCARD-25-2"  ?selected=${this.viewState.loyaltyCard === 'BAHNCARD-25-2'}>BahnCard 25, 2. ${t("class")}</option>
+					<option value="BAHNCARD-25-1"  ?selected=${this.viewState.loyaltyCard === 'BAHNCARD-25-1'}>BahnCard 25, 1. ${t("class")}</option>
+					<option value="BAHNCARD-50-2"  ?selected=${this.viewState.loyaltyCard === 'BAHNCARD-50-2'}>BahnCard 50, 2. ${t("class")}</option>
+					<option value="BAHNCARD-50-1"  ?selected=${this.viewState.loyaltyCard === 'BAHNCARD-50-1'}>BahnCard 50, 1. ${t("class")}</option>
+					<option value="BAHNCARD-100-2" ?selected=${this.viewState.loyaltyCard === 'BAHNCARD-100-2'}>BahnCard 100, 2. ${t("class")}</option>
+					<option value="BAHNCARD-100-1" ?selected=${this.viewState.loyaltyCard === 'BAHNCARD-100-1'}>BahnCard 100, 1. ${t("class")}</option>
+				</select>
+			</div>
+
+			<div class="flex-row">
+				<label for="transferTime">${t('minTransferTime')}:</label>
+				<input type="number" id="transferTime" min="0" max="99" @change=${this.changeHandler} .value="${this.viewState.transferTime}">
+			</div>
+
+			<div class="flex-column">
+				<span>${t('options')}:</span><br>
+				<label><input type="checkbox" id="showDS100"       @change=${this.changeHandler} ?checked=${this.viewState.showDS100}> ${t('showds100')}<br></label>
+				<label><input type="checkbox" id="combineDateTime" @change=${this.changeHandler} ?checked=${this.viewState.combineDateTime}> ${t('combineDateTime')}<br></label>
+				<label class="${this.viewState.profile !== 'db' ? 'hidden' : nothing}">
+					<input type="checkbox" id="showPrices" @change=${this.changeHandler} ?checked=${this.viewState.showPrices}> ${t('show-prices')}
+				</label>
+			</div>
+
+			<div class="flex-row">
+				<div class="button color icon-trashcan" title="${t('clearstorage')}" @click=${this.clearStorage}></div>
+			</div>
+		`;
+	}
+
+	changeHandler = async event => {
+		const id    = event.target.id;
+		const value = event.target.value;
+		
+		if (id === 'language')        this.viewState.setLanguage(value);
+		if (id === 'transferTime')    this.viewState.setTransferTime(parseInt(value));
+		if (id === 'ageGroup')        this.viewState.setAgeGroup(value);
+		if (id === 'loyaltyCard')     this.viewState.setLoyaltyCard(value);
+		if (id === 'showDS100')       this.viewState.toggleShowDS100();
+		if (id === 'showPrices')      this.viewState.toggleShowPrices();
+		if (id === 'combineDateTime') this.viewState.toggleCombineDateTime();
+		if (id === 'profile') {
+			this.viewState.setProfile(value);
+			await initHafasClient(this.viewState.profile);
+		}
+	}
+
+	clearStorage = () => {
+		clearDataStorage();
+		localStorage.clear();
+
+		caches.keys().then(names => names.forEach(name => caches.delete(name)));
+
+		location.reload();
+	};
 
-		return settings;
-	});
+}
 
-	await initHafasClient(settings.profile);
-	searchView(clearInputs);
-	hideOverlay();
-};
+customElements.define('settings-view', SettingsView);
diff --git a/src/shim/cross-fetch.js b/src/shim/cross-fetch.js
@@ -5,6 +5,7 @@ export const fetch = (resource, options) => {
 		resource = new Request(resource);
 	}
 
+	if (isDevServer) console.log('fetch(): ', resource, options);
 
 	const replacement = {
 		'https://app.vendo.noncd.db.de/mob/location/search':       '/db/vendo/locations',
diff --git a/src/styles.js b/src/styles.js
@@ -0,0 +1,17 @@
+import baseStyles from './styles/base.css' assert { type: 'css' };
+import helperStyles from './styles/helpers.css' assert { type: 'css' };
+import flexboxStyles from './styles/flexbox.css' assert { type: 'css' };
+import buttonInputStyles from './styles/buttonInput.css' assert { type: 'css' };
+import headerStyles from './styles/header.css' assert { type: 'css' };
+import cardStyles from './styles/card.css' assert { type: 'css' };
+import iconStyles from './styles/icons.css' assert { type: 'css' };
+import overlaysStyles from './styles/overlays.css' assert { type: 'css' };
+import searchViewStyles from './styles/searchView.css' assert { type: 'css' };
+import journeysViewStyles from './styles/journeysView.css' assert { type: 'css' };
+import journeyViewStyles from './styles/journeyView.css' assert { type: 'css' };
+import departuresViewStyles from './styles/departuresView.css' assert { type: 'css' };
+import settingsViewStyles from './styles/settingsView.css' assert { type: 'css' };
+import footerStyles from './styles/footer.css' assert { type: 'css' };
+
+export { baseStyles, helperStyles, flexboxStyles, buttonInputStyles, headerStyles, cardStyles, iconStyles, overlaysStyles, footerStyles };
+export { searchViewStyles, journeysViewStyles, journeyViewStyles, departuresViewStyles, settingsViewStyles };
diff --git a/src/styles/base.css b/src/styles/base.css
@@ -0,0 +1,47 @@
+font-face {
+	font-weight: normal;
+	font-tyle: normal;
+}
+
+:root {
+	overscroll-behavior-y: none;
+}
+
+body {
+	margin: 0;
+	background-color: #333;
+}
+
+* {
+	font-family: sans-serif;
+	box-sizing: border-box;
+	border-collapse: collapse;
+}
+
+a {
+	color: inherit;
+}
+
+.container {
+	margin: 0 auto;
+	max-width: 1000px;
+}
+
+.spinner {
+	margin: calc(50vh - 120px) auto;
+	border: 5px solid rgba(255, 255, 255, .4);
+	border-top: 5px solid white;
+	border-radius: 50%;
+	width: 120px;
+	height: 120px;
+	animation: spin 2s linear infinite;
+}
+
+.spinning {
+	animation: spin 2s linear infinite;
+}
+
+@keyframes spin {
+	0% { transform: rotate(0deg); }
+	100% { transform: rotate(360deg); }
+}	
diff --git a/src/styles/buttonInput.css b/src/styles/buttonInput.css
@@ -0,0 +1,151 @@
+input,
+button,
+.button,
+.selector {
+	margin: 4px;
+}
+
+input[type="datetime-local"],
+input[type="date"],
+input[type="time"],
+input[type="text"] {
+	cursor: pointer;
+	box-sizing: border-box;
+	padding: .3em .5em;
+	font-size: 1.5em;
+	padding: 7px;
+	border: none;
+	outline: none;
+	background-color: white;
+	color: black;
+	border-radius: 0;
+}
+
+input[type="checkbox"] {
+	transform: scale(1.5);
+}
+
+button,
+.button,
+.selector label {
+	background-color: white;
+	color: black;
+	cursor: pointer;
+	user-select: none;
+}
+
+button, .button {
+	width: max-content;
+	padding: 8px 20px;
+	transition: background-color 300ms;
+	border: none;
+}
+
+button:hover, .button:hover {
+	background-color: #d3d3d3;
+}
+
+button.color, .button.color {
+	background-color: #43a047;
+	color: white;
+}
+
+button.color:hover, .button.color:hover {
+	background-color: #388e3c;
+}
+
+.arrowButton {
+	cursor: pointer;
+	user-select: none;
+	height: 72px;
+	width: 72px;
+	margin: 0 auto;
+	transition: transform 150ms;
+	filter: invert();
+}
+
+.selector {
+	display: flex;
+
+	input {
+		display: none;
+	}
+
+	input + label {
+		background: #d3d3d3;
+	}
+
+	input:checked + label {
+		background: white
+	}
+
+	label {
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		user-select: none;
+		padding: 0 10px;
+	}
+
+	label:after {
+		font-size: .9rem;
+		color: black;
+		text-align: center;
+		line-height: .9rem;
+		margin-top: 2px;
+	}
+
+	div:not(:last-child),
+	label:not(:last-child) {
+		border-right: 1px solid #bbb;
+	}
+
+	.icon-ice,
+	.icon-ic,
+	.icon-icice,
+	.icon-dzug,
+	.icon-regional {
+		font-style: italic;
+	}
+
+	.icon-tram:after,
+	.con-bus:after,
+	.icon-ferry:after,
+	.icon-taxi:after {
+		font-size: 0.6rem;
+	}
+
+	.icon-ice:after      { content: 'ICE'; }
+	.icon-ic:after       { content: 'IC'; }
+	.icon-icice:after    { content: 'IC ICE'; }
+	.icon-dzug:after     { content: 'D'; }
+	.icon-regional:after { content: 'NV'; }
+	.icon-suburban:after { content: 'S'; }
+	.icon-subway:after   { content: 'U'; }
+	.icon-tram:after     { content: 'Tram'; }
+	.icon-bus:after      { content: 'Bus'; }
+	.icon-ferry:after    { content: 'Ferry'; }
+	.icon-taxi:after     { content: 'Taxi'; }
+}
+
+.selector.rectangular label {
+	height: 32px;
+	width: 32px;
+	padding: 3px;
+	font-weight: bold;
+	overflow: hidden;
+}
+
+@media (max-width: 650px) {
+	button[type="submit"]{
+		flex-basis: 100%;
+		justify-content: center;
+	}
+}
+
+@media (max-width: 799px) {
+	.arrowButton {
+		width: 48px;
+		height: 48px;
+	}
+}
diff --git a/src/styles/card.css b/src/styles/card.css
@@ -0,0 +1,81 @@
+.card {
+	overflow-x: auto;
+
+	table {
+		border-bottom: 1px solid rgba(0, 0, 0, 0.3);
+		width: 100%;
+		background-color: white;
+		min-width: 390px;
+		max-width: 1000px;
+	}
+
+	table a {
+		padding: 5px 3px;
+		text-decoration: none;
+	}
+	
+	thead {
+		.center {
+			padding: 5px 3px;
+		}
+
+		tr:not(:last-child) {
+			background-color: #eee;
+		}
+	}
+
+	tbody {
+		tr {
+			cursor: pointer;
+			border-top: 1px solid #ccc;
+		}
+
+		tr:hover {
+			background-color: #ddd;
+		}
+
+		tr:hover td {
+			background-color: transparent;
+		}
+	}
+
+	td, th {
+		text-align: center;
+		overflow: hidden;
+	}
+
+	th {
+		padding: 10px 5px;
+	}
+
+	th.station {
+		width: 60%;
+	}
+
+	.train-details {
+		flex-wrap: wrap;
+		padding: 0 !important;
+	
+		div {
+			margin: .4em 2em;
+		}
+	}
+
+	.cancelled {
+		text-decoration-line: line-through;
+	}
+	
+	.cancelled-text {
+		font-weight: bold;
+		color: red !important;
+	}
+}
+
+@media (min-width: 800px) {
+	table {
+		overflow: hidden;
+		border: none;
+		margin: 50px auto;
+		width: 80vw;
+	}
+}
diff --git a/src/styles/departuresView.css b/src/styles/departuresView.css
@@ -0,0 +1,7 @@
+tbody td:nth-child(2) {
+	text-align: unset;
+}
+
+tbody td {
+	padding: 5px 3px;
+}
diff --git a/src/styles/flexbox.css b/src/styles/flexbox.css
@@ -0,0 +1,29 @@
+.flex-row {
+	display: flex;
+	flex-direction: row;
+}
+
+.flex-column {
+	display: flex;
+	flex-direction: column;
+}
+
+.flex-center {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+}
+
+.flex-grow {
+	flex-grow: 1;
+}
+
+@media (max-width: 799px) {
+	.flex-row {
+		flex-wrap: wrap;
+	}
+
+	.flex-row.nowrap {
+		flex-wrap: unset;
+	}
+}
diff --git a/src/styles/footer.css b/src/styles/footer.css
@@ -0,0 +1,18 @@
+footer {
+	color: #ddd;
+	padding: 2em;
+	width: max-content;
+
+	a {
+		text-decoration: none;
+	}
+
+	a:after {
+		margin: 0 8px;
+		content: "•";
+	}
+
+	:last-child:after {
+		content: none;
+	}
+}
diff --git a/src/styles/header.css b/src/styles/header.css
@@ -0,0 +1,86 @@
+.header-container {
+	position: sticky;
+	top: 0;
+	z-index: 10;
+
+	header {
+		display: flex;
+		flex-direction: row;
+		justify-content: center;
+		color: white;
+		background-color: #33691E;
+		border-bottom: 1px solid rgba(255, 255, 255, .3);
+
+		.container {
+			max-width: 1000px;
+			width: 80vw;
+			margin: 0;
+		}
+
+		h3 {
+			margin-right: 1.5em;
+		}
+
+		.icon-reload {
+			float: right;
+		}
+
+		.icon-back,
+		.icon-reload,
+		.icon-share,
+		.icon-dots {
+			cursor: pointer;
+			width: 32px;
+			height: 32px;
+			margin: 12px;
+			user-select: none;
+		}
+
+		.mode-changers {
+			margin-top: auto;
+			margin-left: auto;
+			height: max-content;
+
+			a {
+				border-bottom: 3px solid transparent;
+				align-items: center;
+				display: flex;
+				padding: 0 1em;
+				cursor: pointer;
+				text-decoration: none;
+				width: max-content;
+		
+			 	span {
+					font-weight: bold;
+					margin: 1em .4em;
+				}
+			}
+
+			a.active {
+				border-bottom: 3px solid white;
+			}
+		}
+	}
+}
+
+@media (max-width: 650px) {
+	header {
+		padding-top: 10px;
+
+		.mode-changers {
+			margin: auto;
+		}
+
+		h3 {
+			margin: 8px 0;
+		}
+	}
+}
+
+@media (max-width: 799px) {
+	header {
+		.icon-back {
+			left: 10px;
+		}
+	}
+}
diff --git a/src/styles/helpers.css b/src/styles/helpers.css
@@ -0,0 +1,16 @@
+.invisible {
+	visibility: hidden !important;
+}
+
+.hidden {
+	display: none !important;
+}
+
+.flipped {
+	transform-origin: center center;
+	transform: rotate(180deg);
+}
+
+.center {
+	margin: auto;
+}
diff --git a/src/styles/icons.css b/src/styles/icons.css
@@ -0,0 +1,95 @@
+.icon-back {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z" fill="white"/></svg>');
+}
+
+.icon-reload {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M17.65 6.35A7.96 7.96 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4z" fill="white"/></svg>');
+}
+
+.icon-close {
+	content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="30" height="30"><path d="M5.293 5.293a1 1 0 0 1 1.414 0L12 10.586l5.293-5.293a1 1 0 1 1 1.414 1.414L13.414 12l5.293 5.293a1 1 0 0 1-1.414 1.414L12 13.414l-5.293 5.293a1 1 0 0 1-1.414-1.414L10.586 12 5.293 6.707a1 1 0 0 1 0-1.414" fill="white"/></svg>');
+}
+
+.icon-hint {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M5 3h14a2 2 0 0 1 2 2v14c0 .53-.21 1.04-.59 1.41-.37.38-.88.59-1.41.59H5c-.53 0-1.04-.21-1.41-.59C3.21 20.04 3 19.53 3 19V5c0-1.11.89-2 2-2m6 6h2V7h-2zm3 8v-2h-1v-4h-3v2h1v2h-1v2z"/></svg>');
+}
+
+.icon-status {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2m8 10V7h-2v6zm0 4v-2h-2v2z"/></svg>');
+}
+
+.icon-warning {
+	content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M13 13h-2V7h2m-2 8h2v2h-2m4.73-14H8.27L3 8.27v7.46L8.27 21h7.46L21 15.73V8.27z"/></svg>');
+}
+
+.icon-other {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m1 17h-2v-2h2zm2.07-7.75-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25"/></svg>');
+}
+
+.icon-arrow1 {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>');
+}
+
+.icon-arrow2 {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/></svg>');
+}
+
+.icon-swap {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M16 17.01V10h-2v7.01h-3L15 21l4-3.99zM9 3 5 6.99h3V14h2V6.99h3z"/></svg>');
+}
+
+.icon-clock {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M12 20a8 8 0 0 0 8-8 8 8 0 0 0-8-8 8 8 0 0 0-8 8 8 8 0 0 0 8 8m0-18a10 10 0 0 1 10 10 10 10 0 0 1-10 10C6.47 22 2 17.5 2 12A10 10 0 0 1 12 2m.5 5v5.25l4.5 2.67-.75 1.23L11 13V7z"/></svg>');
+}
+
+.icon-settings {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6"/></svg>');
+}
+
+.icon-walk-fast {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M13.49 5.48c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2m-3.6 13.9 1-4.4 2.1 2v6h2v-7.5l-2.1-2 .6-3c1.3 1.5 3.3 2.5 5.5 2.5v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1l-5.2 2.2v4.7h2v-3.4l1.8-.7-1.6 8.1-4.9-1-.4 2z"/></svg>');
+}
+
+.icon-walk {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M13.5 5.5c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2M9.8 8.9 7 23h2.1l1.8-8 2.1 2v6h2v-7.5l-2.1-2 .6-3C14.8 12 16.8 13 19 13v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1L6 8.3V13h2V9.6z"/></svg>');
+}
+
+.icon-weelchair {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><circle cx="12" cy="4" r="2"/><path d="M19 13v-2c-1.54.02-3.09-.75-4.07-1.83l-1.29-1.43c-.17-.19-.38-.34-.61-.45-.01 0-.01-.01-.02-.01H13c-.35-.2-.75-.3-1.19-.26C10.76 7.11 10 8.04 10 9.09V15c0 1.1.9 2 2 2h5v5h2v-5.5c0-1.1-.9-2-2-2h-3v-3.45c1.29 1.07 3.25 1.94 5 1.95m-6.17 5c-.41 1.16-1.52 2-2.83 2-1.66 0-3-1.34-3-3 0-1.31.84-2.41 2-2.83V12.1a5 5 0 1 0 5.9 5.9z"/></svg>');
+}
+
+.icon-bike {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M5 20.5A3.5 3.5 0 0 1 1.5 17 3.5 3.5 0 0 1 5 13.5 3.5 3.5 0 0 1 8.5 17 3.5 3.5 0 0 1 5 20.5M5 12a5 5 0 0 0-5 5 5 5 0 0 0 5 5 5 5 0 0 0 5-5 5 5 0 0 0-5-5m9.8-2H19V8.2h-3.2l-1.94-3.27c-.29-.5-.86-.83-1.46-.83-.47 0-.9.19-1.2.5L7.5 8.29C7.19 8.6 7 9 7 9.5c0 .63.33 1.16.85 1.47L11.2 13v5H13v-6.5l-2.25-1.65 2.32-2.35m5.93 13a3.5 3.5 0 0 1-3.5-3.5 3.5 3.5 0 0 1 3.5-3.5 3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m0-8.5a5 5 0 0 0-5 5 5 5 0 0 0 5 5 5 5 0 0 0 5-5 5 5 0 0 0-5-5m-3-7.2c1 0 1.8-.8 1.8-1.8S17 1.2 16 1.2 14.2 2 14.2 3 15 4.8 16 4.8"/></svg>');
+}
+
+.icon-seat {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M9 19h6v2H9c-2.76 0-5-2.24-5-5V7h2v9c0 1.66 1.34 3 3 3m1.42-13.59c.78-.78.78-2.05 0-2.83s-2.05-.78-2.83 0-.78 2.05 0 2.83c.78.79 2.04.79 2.83 0M11.5 9c0-1.1-.9-2-2-2H9c-1.1 0-2 .9-2 2v6c0 1.66 1.34 3 3 3h5.07l3.5 3.5L20 20.07 14.93 15H11.5z"/></svg>');
+}
+
+.icon-table {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M3 9h14V7H3zm0 4h14v-2H3zm0 4h14v-2H3zm16 0h2v-2h-2zm0-10v2h2V7zm0 6h2v-2h-2z" fill="white" /></svg>');
+}
+
+.icon-canvas {
+	content: url('data:image/svg+xml;utf8,<svg version="1.1" viewBox="0 0 24 24" height="24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m11 5v14h2v-14zm-4-2v14h2v-14zm10 4h-2v14h2z" fill="white"/></svg>');
+}
+
+.icon-dots {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M12 16a2 2 0 0 1 2 2 2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2 2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2 2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2" fill="white"/></svg>');
+}
+
+.icon-ticket {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M13 8.5h-2v-2h2zm0 4.5h-2v-2h2zm0 4.5h-2v-2h2zm9-7.5V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v4a2 2 0 0 1 2 2 2 2 0 0 1-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 1-2-2 2 2 0 0 1 2-2"/></svg>');
+}
+
+.icon-plus {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6z"/></svg>');
+}
+
+.icon-minus {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M19 13H5v-2h14z"/></svg>');
+}
+
+.icon-trashcan {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M9 3v1H4v2h1v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1V4h-5V3zM7 6h10v13H7zm2 2v9h2V8zm4 0v9h2V8z" fill="white"/></svg>');
+}
diff --git a/src/styles/journeyView.css b/src/styles/journeyView.css
@@ -0,0 +1,44 @@
+tbody:not(:last-child) {
+	border-bottom: 1px solid rgba(0, 0, 0, .2);
+}
+
+thead>tr:nth-child(2) {
+	border-bottom: 2px solid #ccc;
+}
+
+p {
+	color: white;
+	width: 100%;
+}
+
+p::before {
+	filter: drop-shadow( 0 0 5px rgba(0, 0, 0, .6) );
+	margin-right: 4px;
+	vertical-align: sub;
+}
+
+p.change,
+p.walk,
+p.transfer {
+	text-shadow: 0 0 15px rgba(0, 0, 0, .6);
+	text-align: center;
+}
+
+p.change::before {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M9 3 5 6.99h3V14h2V6.99h3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99z" fill="white"/></svg>');
+}
+
+p.walk::before {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="M13.5 5.5c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2M9.8 8.9 7 23h2.1l1.8-8 2.1 2v6h2v-7.5l-2.1-2 .6-3C14.8 12 16.8 13 19 13v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1L6 8.3V13h2V9.6z" fill="white"/></svg>');
+}
+
+p.transfer::before {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><path d="m21.71 11.29-9-9a.996.996 0 0 0-1.41 0l-9 9a.996.996 0 0 0 0 1.41l9 9c.39.39 1.02.39 1.41 0l9-9a.996.996 0 0 0 0-1.41M14 14.5V12h-4v3H8v-4c0-.55.45-1 1-1h5V7.5l3.5 3.5z" fill="white"/></svg>');
+}
+
+.link {
+	vertical-align: bottom;
+	cursor: pointer;
+	max-inline-size: 26px;
+	margin: 0 .3em;
+}+
\ No newline at end of file
diff --git a/src/styles/journeysView.css b/src/styles/journeysView.css
@@ -0,0 +1,15 @@
+@media (max-width: 799px) {
+	.arrowButton {
+		margin: 15px auto;
+	}
+}
+
+@media (min-width: 800px) {
+	.arrowButton.flipped {
+		margin-top: 45px;
+	}
+
+	table {
+		margin: 15px auto;
+	}
+}
diff --git a/src/styles/overlays.css b/src/styles/overlays.css
@@ -0,0 +1,87 @@
+.overlay {
+	position: fixed;
+	display: flex;
+	top: 0;
+	left: 0;
+	height: 100vh;
+	width: 100vw;
+	background-color: rgba(0, 0, 0, .7);
+	z-index: 100;
+	backdrop-filter: blur(10px);
+
+	.content {
+		z-index: 1050;
+		margin: auto;
+		width: max-content;
+		-webkit-overflow-scrolling: touch;
+	}
+}
+
+.modal {
+	z-index: 1050;
+	margin: auto;
+	background-color: white;
+	width: max-content;
+	padding: 15px;
+	-webkit-overflow-scrolling: touch;
+}
+
+.modal.select {
+	a {
+		width: 100%;
+		margin: 5px 0;
+		text-align: center;
+	}
+
+	a:first-child {
+		margin-top: unset;
+	}
+
+	a:last-child {
+		margin-bottom: unset;
+	}
+}
+
+.modal.dialog {
+	padding: unset;
+
+	.body {
+		max-height: 70vh;
+		overflow: scroll;
+	}
+
+	.header {
+		justify-content: space-between;
+		background-color: #33691E;
+		color: white;
+		padding: 15px;
+
+		h4 {
+			margin: 0;
+ 		}
+
+		.icon-close {
+			margin: -15px;
+			padding: 10px;
+			border-left: 1px solid rgba(0, 0, 0, .4);
+			cursor: pointer;
+		}
+
+		.icon-close:hover {
+			background: rgba(0, 0, 0, .4);
+		}
+	}
+}
+
+@media (max-width: 650px) {
+	.modal.dialog {
+		width: 100vw;
+		height: 100vh;
+	}
+}
+
+@media (min-width: 800px) {
+	.modal.dialog {
+		width: 600px;
+	}
+}+
\ No newline at end of file
diff --git a/src/styles/searchView.css b/src/styles/searchView.css
@@ -0,0 +1,164 @@
+.title {
+	padding-top: 3em;
+
+	h1 {
+		color: white;
+		font-weight: normal;
+		margin: .7em .3em .5em .3em;
+	}
+
+	h1:hover {
+		-webkit-text-fill-color: transparent;
+		-webkit-background-clip: text !important;
+		background: linear-gradient(90deg, #b4dcff 20%, pink 20%, pink 40%, white 40%, white 60%, pink 60%, pink 80%, #b4dcff 80%, #b4dcff 100%);
+	}
+}
+
+.title::before {
+	content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28"><rect rx="4" height="28" width="28" fill="green"/><path d="M14 5.5c-4 0-8 .5-8 4V19c0 1.93 1.57 3.5 3.5 3.5L8 24v.5h2.23l2-2H16l2 2h2V24l-1.5-1.5c1.93 0 3.5-1.57 3.5-3.5V9.5c0-3.5-3.58-4-8-4m-4.5 15c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5m3.5-7H8v-4h5zm2 0v-4h5v4zm3.5 7c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5" fill="white"/></svg>');
+	width: 50px;
+	height: 50px;
+	margin: -0.7em .3em -0.5em -0.3em;
+}
+
+form {
+	color: white;
+
+	.button.icon-arrow1,
+	.button.icon-arrow2,
+	.button.icon-plus,
+	.button.icon-minus,
+	.button.icon-swap {
+		padding: .3em .5em;
+	}
+
+	.button.now {
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		user-select: none;
+		padding: 0 10px;
+	}
+
+	button[type="submit"],
+	.button.icon-settings {
+		height: 32px;
+	}
+
+	.filler {
+		flex: auto;
+	}
+
+	.button.icon-settings {
+		width: 32px;
+		padding: 3px;
+	}
+
+	button[type="submit"]{
+		display: flex;
+		align-items: center;
+		font-size: 20px;
+		padding: 8px;
+	}
+
+	button[type="submit"]::after {
+		width: 24px;
+		height: 24px;
+		margin-left: 5px;
+		content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2 4.5 20.29l.71.71L12 18l6.79 3 .71-.71z" fill="green"/></svg>');
+	}
+}
+
+.suggestions {
+	position: relative;
+	overflow: visible;
+	z-index: 100;
+	height: 0;
+	margin-left: 4px;
+	margin-right: 3.23rem;
+
+	p {
+		font-size: 1.2em;
+		background-color: white;
+		color: black;
+		margin: 0;
+		border-top: 1px solid rgba(0, 0, 0, .2);
+		padding: .3em .6em;
+		cursor: pointer;
+	}
+
+	p:first-child {
+		margin-top: -4px;
+	}
+
+	p:hover {
+		background-color: #d3d3d3;
+	}
+
+	p.selected {
+		background-color: #bfbfbf !important;
+	}
+}
+
+.history {
+	padding: 0 4px;
+	overflow: hidden;
+	user-select: none;
+
+	.flex-row {
+		justify-content: space-between;
+		cursor: pointer;
+		padding: .3em .6em .3em .3em;
+		margin: 0;
+		background-color: white;
+		color: black;
+		font-size: 1.2em;
+		border-bottom: 1px solid rgba(0, 0, 0, .2);
+	}
+
+	.flex-row:last-child {
+		border-bottom: unset;
+	}
+
+	.via {
+		font-size: smaller;
+		font-weight: 200;
+	}
+
+	.from,
+	.to {
+		width: 40%;
+	}
+
+	.to {
+		text-align: right;
+	}
+
+	small {
+		display: block;
+	}
+
+	.icon-arrow1 {
+		width: 25px;
+	}
+}
+
+@media (max-width: 650px) {
+	.filler {
+		flex: unset !important;
+	}
+}
+
+@media (max-width: 799px) {
+	.container {
+		padding: 10px;
+	}
+}
+
+@media (min-width: 800px) {
+	form, .history {
+		width: 80vw;
+		max-width: 700px;
+		color: white;
+	}
+}
diff --git a/src/styles/settingsView.css b/src/styles/settingsView.css
@@ -0,0 +1,25 @@
+.flex-row,
+.flex-column {
+	padding: 1em;
+	border-bottom: 1px solid rgba(0, 0, 0, .4);
+}
+
+.flex-row:last-child {
+	padding: .5em;
+	border-bottom: unset;
+    justify-content: right;
+}
+
+label {
+	padding: .1em;
+}
+
+span {
+	padding-bottom: .25em;
+}
+
+select, input[type=number]{
+	margin: 0;
+	margin-left: auto;
+	width: 65%;
+}
diff --git a/src/templates.js b/src/templates.js
@@ -1,25 +1,45 @@
-import { html, nothing } from 'lit-html';
-import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
-import { formatDateTime, lineAdditionalName } from './formatters.js';
-import { showModal } from './overlays.js';
-import { settings } from './settings.js';
-import { ds100Name } from './app_functions.js';
-import { t } from './languages.js';
+import { html, css, nothing } from 'lit';
+import { unsafeHTML } from 'lit/directives/unsafe-html.js';
 
-export const remarksModalTemplate = (remarks) => html`
-	<div class="remarksView">
-		${remarks.map(remark => html`
-			<div class="flex-row">
-				<span class="icon-${remark.type}"></span>
-				<span>${unsafeHTML(remark.text)}</span>
-			</div>
-		`)}
-	</div>
-`;
+import { formatTime } from './formatters.js';
+import { settingsState } from './settings.js';
+import { getDS100byIBNR } from './ds100.js';
+
+
+export const remarksModal = (element, remarks) => element.showDialogOverlay('remarks', [].concat(
+	remarks.map(remark => html`
+		<div class="flex-row">
+			<span class="icon-${remark.type}"></span>
+			<span>${unsafeHTML(remark.text)}</span>
+		</div>
+	`),
+	[ html`<style>${css`
+		.flex-row {
+			align-items: center;
+			flex-wrap: nowrap;
+			padding: .5em;
+			border-bottom: 1px solid rgba(0, 0, 0, .4);
+		}
+
+		.flex-row:last-child {
+			border-bottom: unset;
+		}
+
+		span[class^="icon-"] {
+			align-self: start;
+			padding-right: .3em;
+		}
+	`}</style>`]
+));
 
 export const stopTemplate = (profile, stop) => {
-	const ds100 = ds100Name(stop.id);
-	return html`<a class="flex-center" href="#/d/${profile}/${stop.id}">${stop.name} ${ds100 !== null ? ` (${ds100})` : nothing}</a>`;
+	let stopName = stop.name;
+	if (settingsState.showDS100) {
+		const ds100 = getDS100byIBNR(stop.id);
+		if (ds100 !== null) stopName += ` (${ds100})`;
+	}
+
+	return html`<a class="flex-center" href="#/d/${profile}/${stop.id}">${stopName}</a>`;
 }
 
 export const platformTemplate = (data) => {

@@ -61,26 +81,20 @@ export const timeTemplate = (data, mode) => {
 			arrival: 'arrivalDelay',
 		},
 	};
+
 	const getField = fieldName => data[fieldsMap[fieldName][mode] || fieldName];
 
-	const time = getField('when') || getField('plannedWhen');
-	if (!time) return '-';
+	let   timeStr;
+	const dateObj = getField('when') || getField('plannedWhen');
+	if (!dateObj) return '-';
+
+	if (dateObj.toLocaleDateString() !== new Date().toLocaleDateString()) {
+		timeStr = `${formatTime(dateObj)}, ${dateObj.getDate()}.${dateObj.getMonth() + 1}.`;
+	} else {
+		timeStr = formatTime(dateObj);
+	}
 
 	const delayMinutes = Math.round(getField('delay') / 60);
 
-	return html`
-		${delayMinutes != 0 ? html`
-			${formatDateTime(time)} <b>(${delayMinutes > 0 ? '+' : ''}${delayMinutes})</b>
-		` : html`
-			${formatDateTime(time)}
-		`}
-	`;
+	return html`${timeStr}${delayMinutes != 0 ? html` <b>(${delayMinutes > 0 ? '+' : ''}${delayMinutes})</b>` : nothing}`;
 };
-
-
-export const footerTemplate = html`
-	<footer class="center">
-		<a href="https://git.ctu.cx/trainsearch" title="commit ${COMMIT} from ${COMMITDATE}">Source-Code (${VERSION})</a>
-		<a href="https://ctu.cx/imprint.html">Imprint</a>
-	</footer>
-`;-
\ No newline at end of file
diff --git a/src/tripView.js b/src/tripView.js
@@ -1,143 +1,176 @@
-import { html, nothing, render } from 'lit-html';
-import { settings } from './settings.js';
-import { remarksModalTemplate, platformTemplate, stopTemplate, timeTemplate } from './templates.js';
-import { ElementById, showElement, setThemeColor, queryBackgroundColor } from './helpers.js';
+import { LitElement, html, nothing } from 'lit';
+import { LitOverlay } from './LitOverlay.js';
+
+import { sleep, queryBackgroundColor, setThemeColor } from './helpers.js';
+import { cachedCoachSequence } from './coach-sequence/index.js';
+import { remarksModal, platformTemplate, stopTemplate, timeTemplate } from './templates.js';
 import { processLeg } from './app_functions.js';
-import { formatDateTime, formatDuration, formatPrice, formatLineAdditionalName, formatLineDisplayName } from './formatters.js';
-import { showAlertModal, showLoader, hideOverlay, showModal } from './overlays.js';
-import { go } from './router.js';
-import { getHafasClient, client } from './hafasClient.js';
+import { formatDuration, formatLineAdditionalName, formatLineDisplayName, formatTrainTypes } from './formatters.js';
+import { getHafasClient } from './hafasClient.js';
 import { t } from './languages.js';
 
-const tripTemplate = (data, profile, refreshToken) => {
-	let changes = 0;
-	let lastArrival;
+import { baseStyles, helperStyles, flexboxStyles, iconStyles, headerStyles, cardStyles, journeyViewStyles } from './styles.js';
+
+class TripView extends LitOverlay {
+	static styles = [
+		super.styles,
+		baseStyles,
+		helperStyles,
+		flexboxStyles,
+		iconStyles,
+		headerStyles,
+		cardStyles,
+		journeyViewStyles
+	];
+
+	static properties = {
+		profile:      { attribute: true },
+		refreshToken: { attribute: true, converter: (value) =>  decodeURIComponent(value)},
+		viewState:    { state: true },
+		isUpdating:   { state: true }
+	};
+
+	constructor (...args) {
+		super(...args);
+
+		this.viewState  = null;
+		this.isUpdating = false;		
+	}
+
+	async connectedCallback () {
+		super.connectedCallback();
+		await sleep(100);
+
+		setThemeColor(queryBackgroundColor(this.renderRoot, 'header'));
+	}
+
+	async updated (previous) {
+		super.updated(previous);
 
-	const remarks        = data.remarks || [];
-	const remarksStatus  = remarks.some((obj) => obj.type === 'status');
-	const remarksWarning = remarks.some((obj) => obj.type === 'warning');
-	const remarksIcon    = remarksWarning ? 'icon-warning' : (remarksStatus ? 'icon-status' : 'icon-hint');
+		if (isDevServer) console.info('updated(): ', previous);
 
-	let bahnExpertUrl = null;
-	if (data.line && (data.line.product == 'nationalExpress' || data.line.product == 'national' || data.line.product == 'regionalExpress' || data.line.product == 'regional')) {
-		const trainName = formatLineAdditionalName(data.line) || data.line?.name;
-		if (trainName) {
-			bahnExpertUrl = 'https://bahn.expert/details/' + encodeURIComponent(trainName) + '/' + Number(data.plannedDeparture);
+		if (previous.has('refreshToken')) {
+			this.viewState  = null;
+			await this.updateViewState();
 		}
 	}
 
-	return html`
-		<div class="header-container">
-			<header>
-				<a id="back" class="icon-back hidden" title="${t('back')}" @click=${() => history.back()}>$</a>
-				<div class="container">
-					<h3>Trip of ${formatLineDisplayName(data.line)} to ${data.direction}</h3>
-				</div>
-				<a class="icon-reload" title="${t('title')}" @click=${() => refreshTripView(profile, refreshToken)}></a>
-			</header>
-		</div>
-
-		<div class="container journeyView">
-			<div class="card">
-				<table>
-					<thead>
-						<tr>
-							<td colspan="4">
-								<div class="center">${bahnExpertUrl ? html`
-									<a href="${bahnExpertUrl}">${formatLineDisplayName(data.line)}${data.direction ? html` → ${data.direction}` : ''}</a>
-								` : html `
-									${formatLineDisplayName(data.line)}${data.direction ? html` → ${data.direction}` : ''}
-								`}
-								${data.cancelled ? html`<b class="cancelled-text">${t('cancelled-ride')}</b>` : ''}
-								${!!remarks.length ? html`<a class="link ${remarksIcon}" @click=${() => showModal(t('remarks'), remarksModalTemplate(remarks))}></a>` : nothing}</div>
-							</td>
-						</tr>
-						<tr>
-							<td colspan="4">
-								<div class="train-details center">
-									${formatLineAdditionalName(data.line) ? html`
-										<div>
-											Trip: ${formatLineAdditionalName(data.line)}
-										</div>
-									` : nothing}
-									${data.line.trainType ? html`
-										<div>
-											Train type: ${data.line.trainType}
-										</div>
-									` : nothing}
-									<div ${data.cancelled ? 'cancelled' : ''}">
-										${t('duration')}: ${formatDuration(data.arrival - (data.departure ? data.departure : data.plannedDeparture))} ${data.departure ? '' : ('(' + t('planned') + ')')}
+	renderContent () {
+		return this.viewState !== null ? html`
+			<div class="header-container">
+				<header>
+					<a id="back" class="icon-back ${history.length !== 1 ? '': 'invisible'}" title="${t('back')}" @click=${() => history.back()}></a>
+					<div class="container">
+						<h3>Trip of ${formatLineDisplayName(this.viewState.trip.line)} to ${this.viewState.trip.direction}</h3>
+					</div>
+					<a class="icon-reload ${this.isUpdating ? 'spinning' : ''}" title="${t("reload")}" @click=${this.updateViewState}></a>
+				</header>
+			</div>
+
+			<div class="container">
+				<div class="card">
+					<table>
+						<thead>
+							<tr>
+								<td colspan="4">
+									<div class="center">
+										${this.viewState.trip.bahnExpertUrl ? html`
+										<a href="${this.viewState.trip.bahnExpertUrl}">${formatLineDisplayName(this.viewState.trip.line)}${this.viewState.trip.direction ? html` → ${this.viewState.trip.direction}` : ''}</a>
+										` : html `
+										${formatLineDisplayName(this.viewState.trip.line)}${this.viewState.trip.direction ? html` → ${this.viewState.trip.direction}` : ''}
+										`}
+										${this.viewState.trip.cancelled ? html`<b class="cancelled-text">${t('cancelled-ride')}</b>` : ''}
+										${this.viewState.trip.remarks.length !== 0 ? html`
+										<a class="link ${this.viewState.trip.remarksIcon}" @click=${() => remarksModal(this, this.viewState.trip.remarks)}></a>
+										` : nothing}
 									</div>
-									${data.loadFactor ? html`
-										<div
-											${t("load-"+data.loadFactor)}
+								</td>
+							</tr>
+							<tr>
+								<td colspan="4">
+									<div class="train-details flex-center">
+										${formatLineAdditionalName(this.viewState.trip.line) ? html`<div>Trip: ${formatLineAdditionalName(this.viewState.trip.line)}</div>` : nothing}
+										${this.viewState.trip.line.trainType ? html`<div>Train type: ${this.viewState.trip.line.trainType}</div>` : nothing}
+										<div class="${!this.viewState.trip.cancelled ? nothing : 'cancelled'}">
+											${t('duration')}: ${formatDuration(this.viewState.trip.arrival - (this.viewState.trip.departure ? this.viewState.trip.departure : this.viewState.trip.plannedDeparture))} ${this.viewState.trip.departure ? '' : ('(' + t('planned') + ')')}
 										</div>
-									` : nothing}
-								</div>
-							</td>
-						</tr>
-						<tr>
-							<th>${t('arrival')}</th>
-							<th>${t('departure')}</th>
-							<th class="station">${t('station')}</th>
-							<th>${t('platform')}</th>
-						</tr>
-					</thead>
-					<tbody>
-						${(data.stopovers || []).map(stop => html`
-							<tr class="${stop.cancelled ? 'cancelled' : ''}">
-								<td>${timeTemplate(stop, 'arrival')}</td>
-								<td>${timeTemplate(stop, 'departure')}</td>
-								<td>${stopTemplate(profile, stop.stop)}</td>
-								<td>${platformTemplate(stop)}</td>
+										${this.viewState.trip.loadFactor ? html`<div>${t("load-"+this.viewState.trip.loadFactor)}</div>` : nothing}
+									</div>
+								</td>
+							</tr>
+							<tr>
+								<th>${t('arrival')}</th>
+								<th>${t('departure')}</th>
+								<th class="station">${t('station')}</th>
+								<th>${t('platform')}</th>
 							</tr>
-						`)}
-					</tbody>
-				</table>
+						</thead>
+						<tbody>
+							${(this.viewState.trip.stopovers || []).map(stop => html`
+								<tr class="${stop.cancelled ? 'cancelled' : ''}">
+									<td>${timeTemplate(stop, 'arrival')}</td>
+									<td>${timeTemplate(stop, 'departure')}</td>
+									<td>${stopTemplate(this.profile, stop.stop)}</td>
+									<td>${platformTemplate(stop)}</td>
+								</tr>
+							`)}
+						</tbody>
+					</table>
+				</div>
+			</div>
+			<footer-component></footer-component>
+		` : html`
+			<div class="header-container">
+				<header>
+					<a class="icon-back ${history.length !== 1 ? '': 'invisible'}" title="${t('back')}" @click=${() => history.back()}></a>
+					<div class="container">
+						<h3>Trip of ... to ...</h3>
+					</div>
+					<a class="icon-reload ${this.isUpdating ? 'spinning' : ''}" title="${t("reload")}" @click=${this.updateViewState}></a>
+				</header>
 			</div>
-		</div>
-	`;
-};
-
-export const tripView = async (match, isUpdate) => {
-	if (!isUpdate) showLoader();
-	let profile, refreshToken, data;
-
-	try {
-		profile = match[0];
-		refreshToken = decodeURIComponent(match[1]);
-		const client = await getHafasClient(profile);
-		data = await client.trip(refreshToken, {stopovers: true});
-		processLeg(data.trip);
-	} catch(e) {
-		showAlertModal(e.toString());
-		throw e;
+			<div class="spinner"></div>
+		`;
 	}
 
-	hideOverlay();
+	updateViewState = async () => {
+		this.isUpdating = true;
+		try {
+			const client = await getHafasClient(this.profile);
+
+			let viewState = await client.trip(this.refreshToken, {stopovers: true});
 
-	render(tripTemplate(data.trip, profile, refreshToken), ElementById('content'));
-	setThemeColor(queryBackgroundColor('header'));
+			processLeg(viewState.trip);
 
-	console.log(data)
+			if (!viewState.trip.remarks) viewState.trip.remarks = [];
 
-	if (history.length > 0) showElement(ElementById('back'));
-};
+			const remarksStatus  = viewState.trip.remarks.some((obj) => obj.type === 'status');
+			const remarksWarning = viewState.trip.remarks.some((obj) => obj.type === 'warning');
+			viewState.trip.remarksIcon = remarksWarning ? 'icon-warning' : (remarksStatus ? 'icon-status' : 'icon-hint');
 
-const refreshTripView = async (profile, refreshToken) => {
-	document.querySelector('.icon-reload').classList.add('spinning');
+			viewState.trip.bahnExpertUrl = null;
+			if (viewState.trip.line && [ 'nationalExpress', 'national', 'regionalExpress', 'regional' ].includes(viewState.trip.line.product)) {
+				const trainName = formatLineAdditionalName(viewState.trip.line) || viewState.trip.line?.name;
+				if (trainName) viewState.trip.bahnExpertUrl = 'https://bahn.expert/details/' + encodeURIComponent(trainName) + '/' + Number(viewState.trip.plannedDeparture);
+			}
 
-	let data;
+			this.viewState = viewState;
 
-	try {
-		const client = await getHafasClient(profile);
-		data = await client.trip(refreshToken, {stopovers: true});
-		processLeg(data.trip);
-	} catch(e) {
-		showAlertModal(e.toString());
-		throw e;
+			if (this.viewState.trip.line && this.viewState.trip.line.name) {
+				const [category, number] = this.viewState.trip.line.name.split(' ');
+				const info               = await cachedCoachSequence(category, this.viewState.trip.line.fahrtNr || number, this.viewState.trip.origin.id, this.viewState.trip.plannedDeparture);
+
+				if (info) this.viewState.trip.line.trainType = formatTrainTypes(info);
+				this.requestUpdate();
+			}
+
+			if (isDevServer) console.info('viewState: ',this.viewState);
+		} catch(e) {
+			this.showAlertOverlay(e.toString());
+			console.error(e);
+		}
+		this.isUpdating = false;
 	}
+}
 
-	render(tripTemplate(data.trip, profile, refreshToken), ElementById('content'));
-	document.querySelector('.icon-reload').classList.remove('spinning');
-};
+customElements.define('trip-view', TripView);
diff --git a/webpack.config.js b/webpack.config.js
@@ -5,8 +5,6 @@ import CopyPlugin from 'copy-webpack-plugin';
 import WorkboxPlugin from 'workbox-webpack-plugin';
 import HtmlWebpackPlugin from 'html-webpack-plugin';
 import HtmlWebpackInjectPreload from '@principalstudio/html-webpack-inject-preload';
-import MiniCssExtractPlugin from 'mini-css-extract-plugin';
-import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
 import { GitRevisionPlugin } from 'git-revision-webpack-plugin';
 
 const __filename        = fileURLToPath(import.meta.url);

@@ -20,40 +18,21 @@ const appName           = "Öffisearch";
 export default {
 	mode: 'production',
 	entry: './src/main.js',
-	output: {
-		path: dist,
-		filename: "[name].js"
-	},
-	module: {
-		rules: [{
-			test: /\.css$/,
-			use: [
-				isDevServer ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader',
-			],
-		}],
-	},
 	performance: {
 		hints: false,
 	},
 	optimization: {
 		minimize: true,
-		minimizer: [
-			`...`,
-			new CssMinimizerPlugin(),
-		],
-	},
-	devServer: {
-		static: dist,
-		client: {
-			progress: true,
-		},
-		proxy: [{
-			context: [ '/hafas', '/db' ],
-			target: 'https://oeffi.katja.wtf',
-			changeOrigin: true,
-		}],
 	},
 	plugins: [
+		new webpack.DefinePlugin({
+			'process.env': '{}',
+			'isDevServer': isDevServer,
+			'APPNAME':     JSON.stringify(appName),
+			'VERSION':     JSON.stringify(process.env.GIT_VERSION    ? process.env.GIT_VERSION    : gitRevisionPlugin.version()),
+			'COMMIT':      JSON.stringify(process.env.GIT_COMMIT     ? process.env.GIT_COMMIT     : gitRevisionPlugin.commithash()),
+			'COMMITDATE':  JSON.stringify(process.env.GIT_COMMITDATE ? process.env.GIT_COMMITDATE : gitRevisionPlugin.lastcommitdatetime()),
+		}),
 		new webpack.NormalModuleReplacementPlugin(/(node:|https-proxy-agent|cross-fetch|db-hafas-stations)/, (resource) => {
 			const newRequest = {
 				'node:buffer':       'buffer',

@@ -66,14 +45,6 @@ export default {
 				resource.request = newRequest;
 			}
 		}),
-		new webpack.DefinePlugin({
-			'process.env': '{}',
-			'isDevServer': isDevServer,
-			'APPNAME':     JSON.stringify(appName),
-			'VERSION':     JSON.stringify(process.env.GIT_VERSION    ? process.env.GIT_VERSION    : gitRevisionPlugin.version()),
-			'COMMIT':      JSON.stringify(process.env.GIT_COMMIT     ? process.env.GIT_COMMIT     : gitRevisionPlugin.commithash()),
-			'COMMITDATE':  JSON.stringify(process.env.GIT_COMMITDATE ? process.env.GIT_COMMITDATE : gitRevisionPlugin.lastcommitdatetime()),
-		}),
 		new webpack.ProvidePlugin({
 			Buffer: ['buffer', 'Buffer'],
 		}),

@@ -108,7 +79,6 @@ export default {
 			}],
 		}),
 		].concat(isDevServer ? [] : [
-		new MiniCssExtractPlugin(),
 		new WorkboxPlugin.GenerateSW({
 			clientsClaim: true,
 			skipWaiting: true,

@@ -130,5 +100,34 @@ export default {
 			'tls':    false,
 			'net':    false,
 		}
-	}
+	},
+	module: {
+		rules: [{
+			test: /\.css$/,
+			loader: 'lit-css-loader',
+			options: {
+				cssnano: {
+					autoprefixer: true,
+					zindex: true,
+					mergeIdents: true,
+					reduceIdents: true
+				}
+			}
+		}],
+	},
+	output: {
+		path: dist,
+		filename: "[name].js"
+	},
+	devServer: {
+		static: dist,
+		client: {
+			progress: true,
+		},
+		proxy: [{
+			context: [ '/hafas', '/db' ],
+			target: 'https://oeffi.katja.wtf',
+			changeOrigin: true,
+		}],
+	},
 };