diff --git a/.gitignore b/.gitignore
index 10bd2523..aa1e3097 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,6 @@ srtm/
 frontend/static/js/archives.js
 frontend/static/js/flyxc.js
 frontend/static/js/status.js
-frontend/static/js/tracking.js
\ No newline at end of file
+frontend/static/js/tracking.js
+frontend/static/js/xc-score-worker.js
+
diff --git a/.vscode/launch.json b/.vscode/launch.json
index dcd56b43..9512caa4 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -4,6 +4,13 @@
   // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
   "version": "0.2.0",
   "configurations": [
+    {
+      "name": "Launch Chrome",
+      "request": "launch",
+      "type": "pwa-chrome",
+      "url": "http://localhost:8080/?track=https%3A%2F%2Fparapente.ffvl.fr%2Fsites%2Fparapente.ffvl.fr%2Ffiles%2Figcfiles%2F2020-06-10-igcfile-276534-200361.igc&s=20&l=xc&p=u%7BpiGiuik%40%3FvfbB",
+      "webRoot": "${workspaceFolder}/frontend/static"
+    },
     {
       "type": "node",
       "request": "attach",
@@ -12,6 +19,6 @@
       "outFiles": [
         "${workspaceFolder}/**/*.js"
       ],
-    }
+    },
   ]
 }
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index b778bcb4..a63b8d57 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -93,6 +93,14 @@
         "@ui5/webcomponents-base": "0.20.0"
       }
     },
+    "collections": {
+      "version": "5.1.11",
+      "resolved": "https://registry.npmjs.org/collections/-/collections-5.1.11.tgz",
+      "integrity": "sha512-WbBP7RRuAwnZHNTcDsUEHZxcowEWMfI0r2iN0okRS147IAvJsR6EAi1+b/C0ytmCtGne456azwgZ1RZsBxWSiw==",
+      "requires": {
+        "weak-map": "~1.0.x"
+      }
+    },
     "cookiesjs": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/cookiesjs/-/cookiesjs-3.0.3.tgz",
@@ -136,6 +144,24 @@
       "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
       "dev": true
     },
+    "flatbush": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/flatbush/-/flatbush-3.3.0.tgz",
+      "integrity": "sha512-F3EzQvKpdmXUbFwWxLKBpytOFEGYQMCTBLuqZ4GEajFOEAvnOIBiyxW3OFSZXIOtpCS8teN6bFEpNZtnVXuDQA==",
+      "requires": {
+        "flatqueue": "^1.2.0"
+      }
+    },
+    "flatqueue": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/flatqueue/-/flatqueue-1.2.1.tgz",
+      "integrity": "sha512-X86TpWS1rGuY7m382HuA9vngLeDuWA9lJvhEG+GfgKMV5onSvx5a71cl7GMbXzhWtlN9dGfqOBrpfqeOtUfGYQ=="
+    },
+    "flight-recorder-manufacturers": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/flight-recorder-manufacturers/-/flight-recorder-manufacturers-1.1.0.tgz",
+      "integrity": "sha1-ZmOdXT8zzawUfj/jWnARqJm1Bfk="
+    },
     "geolib": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/geolib/-/geolib-3.3.1.tgz",
@@ -146,6 +172,25 @@
       "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
       "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
     },
+    "igc-parser": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/igc-parser/-/igc-parser-0.5.0.tgz",
+      "integrity": "sha512-XpuPcl7MTu3H+FPjOpH21AQhZWOeaqFpk/T5M5r63ROxRpLF9idgb6SS7/NyQHXOe1L1pdgDKIErKfkxqp90OA==",
+      "requires": {
+        "flight-recorder-manufacturers": "^1.0.1"
+      }
+    },
+    "igc-xc-score": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/igc-xc-score/-/igc-xc-score-1.5.0.tgz",
+      "integrity": "sha512-hWfx1IfxuDyCPKdco/8T87PRcl1paKmPbOGlp5zzUNuzKBCdBxuDY1UtU0kfMLE2B3fnlnzNofpJ/qlAsKFzCQ==",
+      "requires": {
+        "collections": "^5.1.11",
+        "flatbush": "^3.3.0",
+        "igc-parser": "^0.5.0",
+        "rbush": "^3.0.1"
+      }
+    },
     "js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -191,6 +236,19 @@
       "resolved": "https://registry.npmjs.org/pwa-helpers/-/pwa-helpers-0.9.1.tgz",
       "integrity": "sha512-4sP/C9sSxQ3w80AATmvCEI3R+MHzCwr2RSZEbLyMkeJgV3cRk7ySZRUrQnBDSA7A0/z6dkYtjuXlkhN1ZFw3iA=="
     },
+    "quickselect": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
+      "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw=="
+    },
+    "rbush": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz",
+      "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==",
+      "requires": {
+        "quickselect": "^2.0.0"
+      }
+    },
     "redux": {
       "version": "4.0.5",
       "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
@@ -255,6 +313,11 @@
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/url-search-params-polyfill/-/url-search-params-polyfill-5.1.0.tgz",
       "integrity": "sha512-yjFY7uw2xRf9e8Mg4ZVkZwtp8dMCC4cbBkEIZiTDpuSY2WJ9+Quw0wRhxncv32qaMQwmBQT+P847rO8PrFhhDA=="
+    },
+    "weak-map": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.5.tgz",
+      "integrity": "sha1-eWkVhNmGB/UHC9O3CkDmuyLkAes="
     }
   }
 }
diff --git a/frontend/package.json b/frontend/package.json
index efb2efbd..0d63130b 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -16,6 +16,7 @@
     "d3-array": "^2.4.0",
     "d3-scale-chromatic": "^1.5.0",
     "geolib": "^3.3.1",
+    "igc-xc-score": "^1.5.0",
     "lit-element": "^2.3.1",
     "lit-html": "^1.2.1",
     "pbf": "^3.2.1",
diff --git a/frontend/src/viewer/components/path-element.ts b/frontend/src/viewer/components/path-element.ts
index 5e4edf0b..37e2c479 100644
--- a/frontend/src/viewer/components/path-element.ts
+++ b/frontend/src/viewer/components/path-element.ts
@@ -2,14 +2,18 @@ import { CSSResult, LitElement, PropertyValues, TemplateResult, css, customEleme
 import { Measure, Point } from '../logic/score/measure';
 import { RootState, store } from '../store';
 import { setDistance, setLeague, setScore, setSpeed } from '../actions/map';
+import { Track } from '../logic/map';
 
-import { CircuitType } from '../logic/score/scorer';
+import { CircuitType, Score } from '../logic/score/scorer';
 import { ClosingSector } from '../gm/closing-sector';
 import { FaiSectors } from '../gm/fai-sectors';
 import { LEAGUES } from '../logic/score/league/leagues';
 import { PlannerElement } from './planner-element';
 import { connect } from 'pwa-helpers';
 import { formatUnit } from '../logic/units';
+//import { isMobileDevice } from '../logic/util';
+import { BRecord, RecordExtensions } from 'igc-parser';
+import { Solution } from 'igc-xc-score';
 import { Units } from '../reducers/map';
 
 const ROUTE_STROKE_COLORS = {
@@ -19,6 +23,13 @@ const ROUTE_STROKE_COLORS = {
   [CircuitType.FAI_TRIANGLE]: '#ffff00',
 };
 
+const SCORE_STROKE_COLORS = {
+  [CircuitType.OPEN_DISTANCE]: '#b22222',
+  [CircuitType.OUT_AND_RETURN]: '#b22222',
+  [CircuitType.FLAT_TRIANGLE]: '#cd5c5c',
+  [CircuitType.FAI_TRIANGLE]: '#cd5c5c',
+};
+
 const CIRCUIT_SHORT_NAME = {
   [CircuitType.OPEN_DISTANCE]: 'od',
   [CircuitType.OUT_AND_RETURN]: 'oar',
@@ -37,10 +48,27 @@ const WAYPOINT_FORMATS: { [id: string]: string } = {
 @customElement('path-ctrl-element')
 export class PathCtrlElement extends connect(store)(LitElement) {
   line: google.maps.Polyline | null = null;
+  scoring: {
+    path: google.maps.Polyline | null;
+    closing: google.maps.Polyline | null;
+  } = { path: null, closing: null };
 
   @property({ attribute: false })
   expanded = false;
 
+  
+  @property({ attribute: false })
+  tracks: Track[] | null = null;
+
+  @property({ attribute: false })
+  currentTrack: number | null = null;
+
+  @property({ attribute: false })
+  worker: Worker | null = null;
+
+  @property({ attribute: false })
+  measureIcon: string = 'img/measuring.svg';
+
   @property({ attribute: false })
   units: Units | null = null;
 
@@ -104,6 +132,8 @@ export class PathCtrlElement extends connect(store)(LitElement) {
       this.speed = state.map.speed;
       this.league = state.map.league;
       this.units = state.map.units;
+      this.tracks = state.map.tracks;
+      this.currentTrack = state.map.currentTrack;
     }
   }
 
@@ -176,14 +206,10 @@ export class PathCtrlElement extends connect(store)(LitElement) {
       } else {
         const line = this.line as google.maps.Polyline;
         line.setMap(null);
-        if (this.flight) {
-          this.flight.setMap(null);
-        }
-        if (this.closingSector) {
-          this.closingSector.setMap(null);
-        }
-        if (this.faiSectors) {
-          this.faiSectors.setMap(null);
+        for (let e of [this.flight, this.closingSector, this.faiSectors, this.scoring.closing, this.scoring.path]) {
+          if (e) {
+            e.setMap(null);
+          }
         }
         store.dispatch(setScore(null));
         google.maps.event.removeListener(this.onAddPoint as google.maps.MapsEventListener);
@@ -333,13 +359,116 @@ export class PathCtrlElement extends connect(store)(LitElement) {
     }
   }
 
+  protected launchScoring(): void {
+    if (this.tracks && this.currentTrack !== null && this.tracks[this.currentTrack]) {
+      const fixes: BRecord[] = [];
+      for (let i = 0; i < this.tracks[this.currentTrack].fixes.lat.length; i++)
+        // Keep this to the bare minimum needed for igc-xc-score
+        fixes[i] = {
+          timestamp: this.tracks[this.currentTrack].fixes.ts[i],
+          latitude: this.tracks[this.currentTrack].fixes.lat[i],
+          longitude: this.tracks[this.currentTrack].fixes.lon[i],
+          pressureAltitude: this.tracks[this.currentTrack].fixes.alt[i],
+          gpsAltitude: this.tracks[this.currentTrack].fixes.alt[i],
+          valid: true,
+          extensions: {} as RecordExtensions,
+          fixAccuracy: null,
+          time: '',
+          enl: null
+        };
+      if (this.worker !== null)
+        this.worker.terminate();
+      this.worker = new Worker('js/xc-score-worker.js');
+      this.worker.onmessage = this.updateScore.bind(this);
+      this.worker.postMessage({ msg: 'xc-score-start', flight: JSON.stringify(fixes), league: this.league });
+      this.measureIcon = 'img/pacman.svg';
+    }
+  }
+
+  protected updateScore(msg: any): void {
+    if (msg.data.msg && (msg.data.msg === 'xc-score-result' || msg.data.msg === 'xc-score-progress')) {
+      const r = JSON.parse(msg.data.r) as Solution;
+      let t: CircuitType;
+      let closedCircuit: boolean = false;
+      switch (r.opt.scoring.code) {
+        case 'tri':
+          t = CircuitType.FLAT_TRIANGLE;
+          closedCircuit = true;
+          break;
+        case 'fai':
+          t = CircuitType.FAI_TRIANGLE;
+          closedCircuit = true;
+          break;
+        default:
+        case 'od':
+          t = CircuitType.OPEN_DISTANCE;
+          break;
+      }
+      const score: Score = {
+        points: r.score ? r.score : 0,
+        distance: r.scoreInfo ? r.scoreInfo.distance * 1000 : 0,
+        closingRadius: r.scoreInfo && r.scoreInfo.cp ? r.scoreInfo.cp.d : 0,
+        multiplier: r.opt.scoring.multiplier,
+        circuit: t,
+        indexes: [0]
+      }
+
+      if (this.scoring.path !== null)
+        this.scoring.path.setMap(null);
+      if (this.scoring.closing !== null)
+        this.scoring.closing.setMap(null);
+      let turnPoints: google.maps.LatLng[] = [];
+      if (r.scoreInfo && r.scoreInfo.tp) {
+        turnPoints = r.scoreInfo.tp.map(p => new google.maps.LatLng(p.y, p.x));
+        if (closedCircuit) {
+          /* Triangle -> first point is also last */
+          turnPoints.push(turnPoints[0]);
+        } else {
+          /* Open distance -> ep.start and ep.finish are actually first and fifth turnpoint */
+          if (r.scoreInfo && r.scoreInfo.ep) {
+            turnPoints.unshift(new google.maps.LatLng(r.scoreInfo.ep.start.y, r.scoreInfo.ep.start.x));
+            turnPoints.push(new google.maps.LatLng(r.scoreInfo.ep.finish.y, r.scoreInfo.ep.finish.x));
+          }
+        }
+        this.scoring.path = new google.maps.Polyline({
+          map: this.map as google.maps.Map,
+          path: turnPoints,
+          strokeColor: SCORE_STROKE_COLORS[score.circuit],
+          strokeWeight: 4,
+          zIndex: 1000,
+        });
+      }
+      let closingPoints: google.maps.LatLng[] = [];
+      if (r.scoreInfo && r.scoreInfo.cp && closedCircuit) {
+        closingPoints = [new google.maps.LatLng(r.scoreInfo.cp.in.y, r.scoreInfo.cp.in.x),
+          new google.maps.LatLng(r.scoreInfo.cp.out.y, r.scoreInfo.cp.out.x)];
+        this.scoring.closing = new google.maps.Polyline({
+          map: this.map as google.maps.Map,
+          path: closingPoints,
+          strokeColor: SCORE_STROKE_COLORS['Out and return'],
+          strokeWeight: 4,
+          zIndex: 1000,
+        });
+      }
+
+      store.dispatch(setScore(score));
+      if (msg.data.msg === 'xc-score-result')
+        this.measureIcon = 'img/measuring.svg';
+    }
+  }
+
   protected render(): TemplateResult {
     // Update the URL on re-rendering
     this.getQrText();
     return this.units
       ? html`
           <link rel="stylesheet" href="https://kit-free.fontawesome.com/releases/latest/css/free.min.css" />
-          <span .hidden=${!this.expanded}>${formatUnit(this.distance, this.units.distance)}</span>
+          <span .hidden=${!this.expanded}>
+            <i class="fas fa-2x" style="cursor: pointer" @click=${this.launchScoring}>
+              <img width="32" height="32" style="vertical-align: middle;" src="${this.measureIcon}" />
+            </i>
+            ${formatUnit(this.distance, this.units.distance)}
+          </span>
           <i class="fas fa-ruler fa-2x" style="cursor: pointer" @click=${this.toggleExpanded}></i>
 
           <ui5-dialog id="share-dialog" header-text="Share">
diff --git a/frontend/src/viewer/components/tracking-element.ts b/frontend/src/viewer/components/tracking-element.ts
index 37c0d4dd..c91d9760 100644
--- a/frontend/src/viewer/components/tracking-element.ts
+++ b/frontend/src/viewer/components/tracking-element.ts
@@ -67,7 +67,8 @@ export class TrackingElement extends connect(store)(LitElement) {
           this.features = map.data.addGeoJson(geoJson);
           features.forEach((f) => map.data.remove(f));
         }
-      });
+      })
+      .catch((e) => { console.error(e); return null; });
   }
 
   protected setupListener(map: google.maps.Map): void {
diff --git a/frontend/src/viewer/logic/util.ts b/frontend/src/viewer/logic/util.ts
new file mode 100644
index 00000000..9e8608bf
--- /dev/null
+++ b/frontend/src/viewer/logic/util.ts
@@ -0,0 +1,3 @@
+export function isMobileDevice() {
+    return (typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1);
+}
\ No newline at end of file
diff --git a/frontend/src/workers/xc-score-worker.ts b/frontend/src/workers/xc-score-worker.ts
new file mode 100644
index 00000000..44bdbbf6
--- /dev/null
+++ b/frontend/src/workers/xc-score-worker.ts
@@ -0,0 +1,42 @@
+import { solver, scoringRules } from 'igc-xc-score';
+import { IGCFile } from 'igc-parser';
+
+/* The joy of JS multithreading */
+function filterFunc(o: any): object {
+  const r: any = {};
+  for (let k of Object.keys(o)) {
+    if (typeof o[k] === 'function' || typeof o[k] === 'undefined' || o[k] === null)
+      continue;
+    else if (o[k] instanceof Array)
+      r[k] = o[k];
+    else if (typeof o[k] === 'object')
+      r[k] = filterFunc(o[k]);
+    else
+      r[k] = o[k];
+  }
+  return r;
+}
+
+self.onmessage = function (msg: any) {
+  if (msg.data.msg = 'xc-score-start') {
+    const flight = <IGCFile>{ fixes: JSON.parse(msg.data.flight) };
+    let rules: object | undefined = undefined;
+    switch (msg.data.league) {
+      case 'xc':
+        rules = scoringRules.XContest;
+        break;
+      case 'fr':
+        rules = scoringRules.FFVL;
+        break;
+    }
+    if (rules === undefined)
+      return;
+    const it = solver(flight, rules, { maxcycle: 1000 });
+    let s = it.next();
+    while (!s.done) {
+      self.postMessage({ msg: 'xc-score-progress', r: JSON.stringify(filterFunc(s.value)) });
+      s = it.next();
+    }
+    self.postMessage({ msg: 'xc-score-result', r: JSON.stringify(filterFunc(s.value)) });
+  }
+};
\ No newline at end of file
diff --git a/frontend/static/img/measuring.svg b/frontend/static/img/measuring.svg
new file mode 100644
index 00000000..6f91ae5d
--- /dev/null
+++ b/frontend/static/img/measuring.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 125" enable-background="new 0 0 100 100" xml:space="preserve"><g><path d="M90.129,88.626H70.792c0,0,15.829-16.607,16.355-17.283c1.927-2.465,1.768-7.104-0.24-9.996   c-2.338-3.916-7.416-9.338-15.354-14.192c-0.093-0.065-37.969-22.113-37.969-22.113c-0.087-0.051-0.149-0.065-0.229-0.105   c-0.133-0.068-0.441-0.197-0.459-0.202c-1.766-0.646-3.811-0.07-4.938,1.544c-1.341,1.924-0.869,4.571,1.055,5.911l23.993,16.723   c-1.201,1.261-2.696,3.604-2.696,6.396v20.726l-21.144,12.37c-0.2,0.116-0.347,0.23-0.47,0.342   c-1.764,1.363-2.299,3.843-1.142,5.819c0.838,1.432,2.343,2.229,3.89,2.229c0.772,0,1.556-0.199,2.271-0.618l23.373-13.674   c1.938-1.237,2.181-3.231,2.184-3.249l2.176-15.218c1.173,0.488,2.436,1.113,3.615,1.898c1.243,0.83,2.476,1.966,3.568,3.084   L56.172,88.771c-0.578,0.873-1.868,2.401-1.142,5.618c0.466,2.078,2.851,3.912,4.77,3.912h30.329c2.671,0,4.839-2.162,4.839-4.844   C94.968,90.79,92.8,88.626,90.129,88.626z"/><circle cx="39.758" cy="56.051" r="9.173"/></g><g><path d="M40.473,11.409c-0.008-0.133-0.012-0.268-0.012-0.403c0-5.445-4.429-9.874-9.872-9.874h-11.79   c-5.445,0-9.875,4.429-9.875,9.874v87.79H23.57v-87.79c0-1.927,0.781-3.672,2.043-4.942c0.131,0.143,0.261,0.287,0.397,0.431   c1.126,1.186,2.098,2.21,2.098,4.511c0,5.444,4.43,9.875,9.876,9.875h9.378v-0.006c0.04,0.001,0.079,0.006,0.119,0.006   c4.614,0,8.674-3.269,9.649-7.774l0.376-1.733L40.473,11.409z M20.714,11.006V95.94H11.78v-4.799h6.197v-2.939H11.78v-4.821h3.386   v-2.939H11.78v-4.822h6.197V72.68H11.78v-4.82h3.386V64.92H11.78v-4.822h6.197v-2.939H11.78v-4.82h3.386v-2.939H11.78v-4.823h6.197   v-2.94H11.78v-4.822h3.386v-2.939H11.78V29.05h6.197v-2.94H11.78v-4.821h3.386v-2.94H11.78v-4.823h6.197v-2.94h-6.154   c0.222-3.671,3.25-6.598,6.977-6.598h4.85C21.837,5.779,20.714,8.265,20.714,11.006z M37.984,18.025   c-3.871,0-7.021-3.149-7.021-7.019c0-3.44-1.666-5.195-2.882-6.477c-0.014-0.016-0.027-0.029-0.041-0.045   c0.792-0.311,1.648-0.496,2.548-0.496c3.869,0,7.017,3.148,7.017,7.018c0,2.744,1.127,5.228,2.94,7.019H37.984z M47.481,18.025   c-2.696,0-5.042-1.527-6.218-3.762l12.449-0.027C52.524,16.508,50.132,18.025,47.481,18.025z"/></g></svg>
\ No newline at end of file
diff --git a/frontend/static/img/pacman.svg b/frontend/static/img/pacman.svg
new file mode 100644
index 00000000..418c821d
--- /dev/null
+++ b/frontend/static/img/pacman.svg
@@ -0,0 +1 @@
+<svg width="200px"  height="200px"  xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="lds-pacman" style="background: none;"><g style="display:block"><circle cx="76.8" cy="50" r="4" ng-attr-fill="{{config.c2}}" fill="black"><animate attributeName="cx" calcMode="linear" values="95;35" keyTimes="0;1" dur="1" begin="-0.67s" repeatCount="indefinite"></animate><animate attributeName="fill-opacity" calcMode="linear" values="0;1;1" keyTimes="0;0.2;1" dur="1" begin="-0.67s" repeatCount="indefinite"></animate></circle><circle cx="37.2" cy="50" r="4" ng-attr-fill="{{config.c2}}" fill="black"><animate attributeName="cx" calcMode="linear" values="95;35" keyTimes="0;1" dur="1" begin="-0.33s" repeatCount="indefinite"></animate><animate attributeName="fill-opacity" calcMode="linear" values="0;1;1" keyTimes="0;0.2;1" dur="1" begin="-0.33s" repeatCount="indefinite"></animate></circle><circle cx="57" cy="50" r="4" ng-attr-fill="{{config.c2}}" fill="black"><animate attributeName="cx" calcMode="linear" values="95;35" keyTimes="0;1" dur="1" begin="0s" repeatCount="indefinite"></animate><animate attributeName="fill-opacity" calcMode="linear" values="0;1;1" keyTimes="0;0.2;1" dur="1" begin="0s" repeatCount="indefinite"></animate></circle></g><g ng-attr-transform="translate({{config.showBeanOffset}} 0)" transform="translate(-15 0)"><path d="M50 50L20 50A30 30 0 0 0 80 50Z" ng-attr-fill="{{config.c1}}" fill="black" transform="rotate(33 50 50)"><animateTransform attributeName="transform" type="rotate" calcMode="linear" values="0 50 50;45 50 50;0 50 50" keyTimes="0;0.5;1" dur="1s" begin="0s" repeatCount="indefinite"></animateTransform></path><path d="M50 50L20 50A30 30 0 0 1 80 50Z" ng-attr-fill="{{config.c1}}" fill="black" transform="rotate(-33 50 50)"><animateTransform attributeName="transform" type="rotate" calcMode="linear" values="0 50 50;-45 50 50;0 50 50" keyTimes="0;0.5;1" dur="1s" begin="0s" repeatCount="indefinite"></animateTransform></path></g></svg>
\ No newline at end of file
diff --git a/rollup.config.js b/rollup.config.js
index 1baf4d44..8d574f14 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -68,9 +68,10 @@ export default [
   buildFrontEnd('frontend/src/archives/archives.ts'),
   buildFrontEnd('frontend/src/tracking/tracking.ts'),
   buildFrontEnd('frontend/src/status/status.ts'),
+  buildFrontEnd('frontend/src/workers/xc-score-worker.ts', true),
 ];
 
-function buildFrontEnd(input) {
+function buildFrontEnd(input, worker) {
   return {
     input,
 
@@ -95,7 +96,9 @@ function buildFrontEnd(input) {
       minifyHTML(),
       resolve(),
       cjs(),
-      typescript(),
+      typescript({
+        tsconfigOverride: worker ? { compilerOptions: { lib: ['webworker'] } } : undefined
+      }),
       prod && terser({ output: { comments: false } }),
     ],
   };