diff --git a/app/escriptorium/context_processors.py b/app/escriptorium/context_processors.py
new file mode 100644
index 0000000000000000000000000000000000000000..5c226b31c36778f6b5d5cb7d7de0e460a24fa622
--- /dev/null
+++ b/app/escriptorium/context_processors.py
@@ -0,0 +1,7 @@
+from django.conf import settings
+
+
+def enable_cookie_consent(request):
+    return {'ENABLE_COOKIE_CONSENT': getattr(settings,
+                                             'ENABLE_COOKIE_CONSENT',
+                                             True)}
diff --git a/app/escriptorium/settings.py b/app/escriptorium/settings.py
index 222e9f0b9fd0f8f2285bcef3322674d535442625..a019a3270f3f6688d5b2fb59b8093d763b7b487a 100644
--- a/app/escriptorium/settings.py
+++ b/app/escriptorium/settings.py
@@ -95,6 +95,7 @@ TEMPLATES = [
                 'django.template.context_processors.request',
                 'django.contrib.auth.context_processors.auth',
                 'django.contrib.messages.context_processors.messages',
+                'escriptorium.context_processors.enable_cookie_consent'
             ],
         },
     },
@@ -302,6 +303,9 @@ THUMBNAIL_ALIASES = {
 #     'jpeg': '/usr/bin/jpegoptim -S200 {filename}'
 # }
 
+
+ENABLE_COOKIE_CONSENT = os.getenv('ENABLE_COOKIE_CONSENT', True)
+
 VERSIONING_DEFAULT_SOURCE = 'eScriptorium'
 
 VERSION_DATE = os.getenv('VERSION_DATE', '<development>')
diff --git a/app/escriptorium/templates/base.html b/app/escriptorium/templates/base.html
index abfeca7273ece25e6d9c60c698c017babab89ea4..780cdb3ddb69f4975df0009f0690dde9bd3ccbf3 100644
--- a/app/escriptorium/templates/base.html
+++ b/app/escriptorium/templates/base.html
@@ -97,7 +97,7 @@
     {% block scripts %}
     <script src="{% static 'main.js' %}" type="text/javascript"></script>
     <script type="text/javascript">
-      const DEBUG = {% if debug %}true{% else %}false{% endif %};
+     const DEBUG = {% if debug %}true{% else %}false{% endif %};
     </script>
     {% if user.is_authenticated %} {# no need to open a socket for anonymous users, bots etc #}
       <script type="text/javascript">
@@ -108,7 +108,12 @@
       {% include 'includes/messages.html' %}
       {% if user.onboarding %}
       <script type="text/javascript">
-        bootOnboarding();
+       bootOnboarding();
+      </script>
+      {% endif %}
+      {% if ENABLE_COOKIE_CONSENT %}
+      <script type="text/javascript">
+          userProfile.getCookieConsent();
       </script>
       {% endif %}
       {% endif %}
diff --git a/app/requirements-dev.txt b/app/requirements-dev.txt
new file mode 100644
index 0000000000000000000000000000000000000000..17bb39607363af5893cd80f7583da88daaee047c
--- /dev/null
+++ b/app/requirements-dev.txt
@@ -0,0 +1,2 @@
+django-debug-toolbar==3.2
+django-extensions==3.1.0
diff --git a/docker-compose.override.yml_example b/docker-compose.override.yml_example
index 789e18900dfbb920c69c1362847d0dd7f0102862..519b21d512d6bd5e98a89c8eb850afe67e480eb1 100644
--- a/docker-compose.override.yml_example
+++ b/docker-compose.override.yml_example
@@ -45,9 +45,13 @@ services:
       ## if need be to correspond to the volume below
       ### and uncomment this block and the port 443
       # volumes:
-      #   - ${PWD}/nginx/ssl.conf:/etc/nginx/conf.d/nginx.conf
-      #   - ${PWD}/nginx/ssl_certificates.conf:/etc/nginx/conf.d/ssl_certificates.conf
-      #   - ${PWD}/nginx/certs/:/etc/certs/
+      # - type: bind
+      #   source: $PWD/nginx/ssl.conf
+      #   target: /etc/nginx/conf.d/nginx.conf
+      # - type: bind
+      #   source: $PWD/nginx/ssl_certificates.conf
+      #   target: /etc/nginx/conf.d/ssl_certificates.conf
+      # - $PWD/nginx/certs/:/etc/certs/
 
     flower:
       restart: always
diff --git a/docker-compose.yml b/docker-compose.yml
index 76df44507314d876d3efbdd1e608ff93f3946b16..b7bbb53b52f4ed4e023f75f5efd68eb81f49f84b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -40,7 +40,9 @@ services:
     environment:
       - SERVERNAME=${DOMAIN:-localhost}
     volumes:
-      - ${PWD}/nginx/nginx.conf:/etc/nginx/conf.d/nginx.conf
+      - type: bind
+        source: $PWD/nginx/nginx.conf
+        target: /etc/nginx/conf.d/nginx.conf
       - static:/usr/src/app/static
       - media:/usr/src/app/media
     ports:
@@ -67,6 +69,8 @@ services:
 
   celery-main:
     <<: *app
+    environment:
+      - OMP_NUM_THREADS=1
     command: "celery worker -l INFO -E -A escriptorium -Ofair --prefetch-multiplier 1 -Q default -c ${CELERY_MAIN_CONC:-10} --max-tasks-per-child=10"
 
   celery-low-priority:
diff --git a/front/package-lock.json b/front/package-lock.json
index 97ef7a1c092e6e572b861b5c5827ad06444e976b..02b562ba47ed3693ed2ead761efe0a053371d484 100644
--- a/front/package-lock.json
+++ b/front/package-lock.json
@@ -828,9 +828,9 @@
       "dev": true
     },
     "intro.js": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/intro.js/-/intro.js-0.6.0.tgz",
-      "integrity": "sha1-D82p9DZOIBiX4RKJ4A3S5S4col8="
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/intro.js/-/intro.js-3.3.1.tgz",
+      "integrity": "sha512-Y+iMbIzVQou9PDRP5miKa7NNQeUkSA6XFp+atnGiTarEi2jghthSLK+9JUvdM6QvQ9G+bpOEfKd+DNtWJlGb9w=="
     },
     "is-core-module": {
       "version": "2.2.0",
diff --git a/front/package.json b/front/package.json
index a55f8701f4953337afc00112aa378faeac93a60b..fc2db0948f2a178370a44c8b87261a3b387f1766 100644
--- a/front/package.json
+++ b/front/package.json
@@ -16,7 +16,7 @@
     "diff": "^4.0.1",
     "dropzone": "^5.5.1",
     "file-loader": "^6.2.0",
-    "intro.js": "^0.6.0",
+    "intro.js": "^3.3.1",
     "jquery": "^3.3.1",
     "jquery-ui-dist": "^1.12.1",
     "js-cookie": "^2.2.0",
diff --git a/front/src/baseline.editor.js b/front/src/baseline.editor.js
index 6089f213824cf152f84dad19f9c1e0eeb18e8167..d5764f89bdeab8f087f21be21d37aaf6d6f201ea 100644
--- a/front/src/baseline.editor.js
+++ b/front/src/baseline.editor.js
@@ -181,7 +181,8 @@ class SegmenterLine {
         }
 
         this.tooltipText = this.type;
-        this.segmenter.attachTooltip(this, this.baselinePath);
+        let tooltipTarget = this.baseline !== null ? this.baselinePath : this.maskPath;
+        this.segmenter.attachTooltip(this, tooltipTarget);
 
         this.refresh();
     }
@@ -874,8 +875,6 @@ export class Segmenter {
         // this.raster.position = view.center;
         // this.img.style.display = 'hidden';
 
-        tool.onMouseDown = this.onMouseDown.bind(this);
-
         // hide the tooltip on leaving the area
         this.canvas.addEventListener('mouseleave', function() {
             this.tooltip.style.display = 'none';
@@ -883,6 +882,8 @@ export class Segmenter {
 
         this.tool = tool;
         this.tool.activate();
+        this.resetToolEvents();
+
         this.loaded = true;
     }
 
@@ -958,14 +959,32 @@ export class Segmenter {
     bindRegionEvents(region) {
         region.polygonPath.onMouseDown = function(event) {
             if (event.event.ctrlKey ||
-                this.selecting ||
+                this.spliting ||
+                // this.selecting ||
                 isRightClick(event.event) ||
                 this.mode != 'regions') return;
-            this.selecting = region;
+
+            // if what we are clicking on is already selected,
+            // check there isn't something below
+            if (region.selected) {
+                let hit;
+                for (let i=0; i<this.regions.length; i++) {
+                    if (this.regions[i] != region) {
+                        hit = this.regions[i].polygonPath.hitTest(event.point);
+                        if (hit) {
+                            this.selecting = this.regions[i];
+                            break;
+                        }
+                    }
+                }
+                if (!hit) this.selecting = region;
+            } else {
+                this.selecting = region;
+            }
 
             var dragging = region.polygonPath.getNearestLocation(event.point).segment;
             this.tool.onMouseDrag = function(event) {
-                this.selecting = false;
+                this.selecting = region;
                 if (!event.event.shiftKey) {
                     this.movePointInView(dragging.point, event.delta);
                 }
@@ -984,6 +1003,7 @@ export class Segmenter {
                 }
             }
             this.tool.onMouseUp = function(event) {
+                this.onMouseUp(event);
                 this.resetToolEvents();
                 this.updateRegionsFromCanvas();
             }.bind(this);
@@ -1001,10 +1021,12 @@ export class Segmenter {
         if (line.baselinePath) {
             line.baselinePath.onMouseDown = function(event) {
                 if (event.event.ctrlKey ||
+                    this.spliting ||
                     isRightClick(event.event) ||
                     this.mode != 'lines' ||
                     this.selecting) return;
                 this.selecting = line;
+
                 var hit = line.baselinePath.hitTest(event.point, {
 	            segments: true,
 	            tolerance: 20
@@ -1028,6 +1050,7 @@ export class Segmenter {
                 }.bind(this);
 
                 this.tool.onMouseUp = function(event) {
+                    this.onMouseUp(event);
                     this.resetToolEvents();
                     line.updateDataFromCanvas();
                 }.bind(this);
@@ -1069,6 +1092,7 @@ export class Segmenter {
         if (line.maskPath) {
             line.maskPath.onMouseDown = function(event) {
                 if (event.event.ctrlKey ||
+                    this.spliting ||
                     isRightClick(event.event) ||
                     this.selecting ||
                     this.mode != 'lines') return;
@@ -1095,6 +1119,7 @@ export class Segmenter {
                 }.bind(this);
 
                 this.tool.onMouseUp = function(event) {
+                    this.onMouseUp(event);
                     this.resetToolEvents();
                     line.updateDataFromCanvas();
                 }.bind(this);
@@ -1116,9 +1141,9 @@ export class Segmenter {
 
     resetToolEvents() {
         this.tool.onMouseDown = this.onMouseDown.bind(this);
+        this.tool.onMouseUp = null; //this.onMouseUp.bind(this);
         this.tool.onMouseDrag = this.onMouseDrag.bind(this);
         this.tool.onMouseMove = null;
-        this.tool.onMouseUp = null;
     }
 
     attachTooltip(obj, target) {
@@ -1201,39 +1226,14 @@ export class Segmenter {
 
     onMouseDown(event) {
         if (isRightClick(event.event)) return;
-        if (this.selecting) {
-            if (this.mode == 'regions') {
-                // if what we are selecting is already selected, check there isn't something below
-                for (let i=0; i<this.regions.length; i++) {
-                    if (this.selecting.selected && this.regions[i] != this.selecting) {
-                        let hit = this.regions[i].polygonPath.hitTest(event.point);
-                        if (hit) {
-                            this.selecting = this.regions[i];
-                            break;
-                        }
-                    }
-                }
-            }
-            // selection
-            if (event.event.shiftKey) {
-                this.selecting.toggleSelect();
-                this.startLassoSelection(event);
-            } else {
-                this.selecting.select();
-                this.purgeSelection(this.selecting);
-            }
-            this.trigger('baseline-editor:selection', {target: this.selecting, selection: this.selection});
-            this.selecting = null;
-        } else {
+        if (!this.selecting) {
             if (event.event.ctrlKey) return;
             if (this.spliting) {
                 this.startCuter(event);
             } else if (event.event.shiftKey) {
                 // lasso selection tool
                 this.startLassoSelection(event);
-            } else if (this.hasSelection()) {
-                this.purgeSelection();
-            } else if (this.mode == 'regions') {
+            }  else if (this.mode == 'regions') {
                 this.startNewRegion(event);
             }  else {  // mode = 'lines'
                 // create a new line
@@ -1242,6 +1242,23 @@ export class Segmenter {
         }
     }
 
+    onMouseUp(event) {
+        if (this.selecting) {
+            // selection
+            if (event.event.shiftKey) {
+                this.selecting.toggleSelect();
+                this.startLassoSelection(event);
+            } else {
+                this.selecting.select();
+                this.purgeSelection(this.selecting);
+            }
+            this.trigger('baseline-editor:selection', {target: this.selecting, selection: this.selection});
+            this.selecting = null;
+        } else if (this.hasSelection()) {
+            this.purgeSelection();
+        }
+    }
+
     getRegionsAt(pt) {
         // returns all the regions that contains the given point pt
         return this.regions.filter(r => r.polygonPath.contains(pt));
@@ -2048,7 +2065,7 @@ export class Segmenter {
                         }
                     } else {
                         let newMask = null;
-                        // calculate the normals before splitting
+                        // calculate the normals before spliting
                         let normal1 = line.baselinePath.getNormalAt(intersections[i].offset);
                         let normal2 = line.baselinePath.getNormalAt(intersections[i+1].offset);
                         let split = line.baselinePath.splitAt(intersections[i+1]);
diff --git a/front/src/editor/store/lines.js b/front/src/editor/store/lines.js
index 65b1f66878746cab5652aca505e083c27a9668d4..a9473ff30fcc59aa48952aa3316bb57148eebbb4 100644
--- a/front/src/editor/store/lines.js
+++ b/front/src/editor/store/lines.js
@@ -96,7 +96,14 @@ export const mutations = {
             if (index < 0) return
             state.all[index] = {
                 ...state.all[index],
-                currentTrans: { ...state.all[index].currentTrans, pk: lineTrans.pk }
+                currentTrans: { ...state.all[index].currentTrans, pk: lineTrans.pk },
+                transcriptions: {
+                    ...state.all[index].transcriptions,
+                    [lineTrans.transcription]: {
+                        ...state.all[index].transcriptions[lineTrans.transcription],
+                        pk: lineTrans.pk
+                    }
+                }
             }
         }
         // Force reference update on the whole array
diff --git a/front/src/editor/store/transcriptions.js b/front/src/editor/store/transcriptions.js
index 36f73392aefc15271e343018ee35e8f615d4eb02..d273299a9854d2463c4b848c8a3b165b6979b842 100644
--- a/front/src/editor/store/transcriptions.js
+++ b/front/src/editor/store/transcriptions.js
@@ -83,13 +83,13 @@ export const actions = {
         //  then fetch all content page by page
         let fetchPage = async function(page) {
             const resp = await api.retrievePage(rootState.document.id, rootState.parts.pk, transcription, page)
-            
+
             let data = resp.data
             for (var i=0; i<data.results.length; i++) {
                 let line = rootState.lines.all.find(l=>l.pk == data.results[i].line)
                 commit('lines/setTranscriptions', { pk: line.pk, transcription: data.results[i] }, {root: true})
             }
-            if (data.next) fetchPage(page+1)
+            if (data.next) await fetchPage(page+1)
         }
         await fetchPage(1)
     },
@@ -162,4 +162,4 @@ export default {
     state: initialState(),
     mutations,
     actions
-}
\ No newline at end of file
+}
diff --git a/front/src/messages.js b/front/src/messages.js
index 8ae463aa3a22b57dd04302aa46b7c1a6284082f3..05fc0b2aa0527c78f9a135d9bbea635789838e74 100644
--- a/front/src/messages.js
+++ b/front/src/messages.js
@@ -14,10 +14,12 @@ export class Alert {
         if (this.links !== undefined) {
             for (let i=0; i<this.links.length; i++) {
                 let link = $('<div>').html('<a href="'+this.links[i].src+'" >'+this.links[i].text+'</a>');
+                if (this.links[i].cssClass) $('a', link).addClass(this.links[i].cssClass);
                 $('.additional', $new).append(link).css('display', 'block');
             }
         }
         this.$element = $new;
+        this.htmlElement = this.$element.get(0);
         $('#alerts-container').append($new);
         $new.show();
 
@@ -33,6 +35,7 @@ export class Alert {
         } else {
             alerts[id_].incrementCounter();
         }
+        return alerts[id_];
     }
 
     incrementCounter() {
diff --git a/front/src/onboarding.js b/front/src/onboarding.js
index a177d27585aed926beb14b81498e5925d0df98bb..e34c678b2b95056228d166820a020b2bac9c9bb3 100644
--- a/front/src/onboarding.js
+++ b/front/src/onboarding.js
@@ -4,24 +4,29 @@ Just set ONBOARDING_PAGE in the page before the scripts block.super eg:
 const ONBOARDING_PAGE = 'onboarding_document_form';
 
 */
-
 export function bootOnboarding() {
     if (typeof ONBOARDING_PAGE !== 'undefined') {
         var onboarding_page_done = userProfile.get(ONBOARDING_PAGE) || false;
-    
+
         if (!onboarding_page_done) {
             var intro = introJs().setOptions({'skipLabel': "Skip"});
             intro.oncomplete(function() {
                 userProfile.set(ONBOARDING_PAGE, true);
             })
-            intro.onexit(function(aa) {
-                if (!userProfile.get(ONBOARDING_PAGE, true)) {
-                    if (confirm("Are you sure you want to avoid further help?")) {
-                        exitOnboarding();
+            intro.onbeforeexit(function(aa) {
+                if (intro._currentStep<intro._introItems.length-1) {
+                    if (!userProfile.get(ONBOARDING_PAGE, true)) {
+                        if (confirm("Are you sure you want to avoid further help?")) {
+                            exitOnboarding();
+                        } else {
+                            return false;
+                        }
                     }
+                } else {
+                    userProfile.set(ONBOARDING_PAGE, true);
                 }
             });
-    
+
             //document_form
             if (ONBOARDING_PAGE == 'onboarding_document_form') {
                 intro.setOptions({
@@ -48,7 +53,7 @@ export function bootOnboarding() {
                         },
                     ]
                 });
-    
+
             } else if (ONBOARDING_PAGE == 'onboarding_images') {
                 intro.setOptions({
                     steps: [
@@ -89,7 +94,7 @@ export function bootOnboarding() {
                         }
                     ]
                 });
-    
+
             } else if (ONBOARDING_PAGE == 'onboarding_edit') {
                 intro.setOptions({
                     steps: [
@@ -125,7 +130,7 @@ export function bootOnboarding() {
                     }]
                 });
             }
-    
+
             document.addEventListener('DOMContentLoaded', function() {
                 intro.start();
             });
diff --git a/front/src/profile.js b/front/src/profile.js
index 3dc5d0b70a87d89f20ef440348017935cdd3c9a7..c1e67661555e1312f6f8a052719f468d135ef789 100644
--- a/front/src/profile.js
+++ b/front/src/profile.js
@@ -22,6 +22,20 @@ class Profile {
     get(key) {
         return this.settings[key];
     }
+
+    getCookieConsent() {
+        // get cookie consent.
+        if (!this.get('cookie-consent')) {
+            let alert = Alert.add('cookie-consent',
+                                  "eScriptorium uses cookies to store the user session and local storage to save user interface preferences.",
+                                  "warning",
+                                  [{src: '', text:'Accept', cssClass: 'btn btn-outline-dark btn-sm mt-2'}]);
+            alert.htmlElement.querySelector('.additional a').addEventListener('click', function(ev) {
+                this.set('cookie-consent', true);
+                return false;
+            }.bind(this));
+        }
+    }
 }
 
 export var userProfile = new Profile();
diff --git a/front/src/vendor.js b/front/src/vendor.js
index a10c138493cefcf1637c0873a83803c61110ddf4..03bfe26d4a0818dac562063e25e0c2f29b9fc57f 100644
--- a/front/src/vendor.js
+++ b/front/src/vendor.js
@@ -21,7 +21,7 @@ window.Dropzone = require('dropzone/dist/dropzone');
 
 // Intro.js needs to be explicitly set on window, as it's used at boot time
 // by onboarding.js
-window.introJs = require('intro.js/intro').introJs;
+window.introJs = require('intro.js');
 
 // moment needs to be explicitly set on window, as it's used at boot time
 // by trans_modal.js
diff --git a/front/vue/components/DiploPanel.vue b/front/vue/components/DiploPanel.vue
index 39fd8092018ea2eb78b91c18d693d99591b3da23..4da1265f94f744da00cf08334cadcac945091fd8 100644
--- a/front/vue/components/DiploPanel.vue
+++ b/front/vue/components/DiploPanel.vue
@@ -270,6 +270,7 @@ export default Vue.extend({
                 }
             }
 
+            this.$refs.saveNotif.classList.remove('hide');
             this.constrainLineNumber();
             e.preventDefault();
         },
@@ -339,4 +340,4 @@ export default Vue.extend({
 </script>
 
 <style scoped>
-</style>
\ No newline at end of file
+</style>
diff --git a/front/vue/components/TranscriptionModal.vue b/front/vue/components/TranscriptionModal.vue
index 1112da13443294bdb0eb174d6ddc7a4dc30d3b1a..640ce673e574cbc89aa637747007f41d7c46f5bd 100644
--- a/front/vue/components/TranscriptionModal.vue
+++ b/front/vue/components/TranscriptionModal.vue
@@ -202,7 +202,13 @@ export default Vue.extend({
             return moment.tz(this.line.currentTrans.version_updated_at, this.timeZone);
         },
         modalImgSrc() {
-            return this.$store.state.parts.image.uri;
+             if (this.$store.state.parts.image.uri.endsWith('.tif') ||
+                 this.$store.state.parts.image.uri.endsWith('.tiff')) {
+                 // can't display tifs so fallback to large thumbnail
+                 return this.$store.state.parts.image.thumbnails.large;
+             } else {
+                 return this.$store.state.parts.image.uri;
+             }
         },
         otherTranscriptions() {
             let a = Object
@@ -250,8 +256,16 @@ export default Vue.extend({
         },
 
         getLineAngle() {
-            let p1 = this.line.baseline[0];
-            let p2 = this.line.baseline[this.line.baseline.length-1];
+            let p1, p2;
+            if (this.line.baseline) {
+                p1 = this.line.baseline[0];
+                p2 = this.line.baseline[this.line.baseline.length-1];
+            } else {
+                // fake baseline from left most to right most points in mask
+                p1 = this.line.mask.reduce((minPt, curPt) => (curPt[0] < minPt[0]) ? curPt : minPt);
+                p2 = this.line.mask.reduce((maxPt, curPt) => (curPt[0] > maxPt[0]) ? curPt : maxPt);
+            }
+
             return Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180 / Math.PI;
         },
 
diff --git a/variables.env_example b/variables.env_example
index 8ee2552e6966bd0b57139107f660d38092854de3..a3dabbb55d4630114510d7c2dd6ceaab3ece3c35 100644
--- a/variables.env_example
+++ b/variables.env_example
@@ -1,9 +1,10 @@
 DOMAIN=localhost
 SECRET_KEY=changeme
 
-
+REDIS_HOST=redis
+# REDIS_PORT=6379
 SQL_HOST=db
-SQL_PORT=5432
+# SQL_PORT=5432
 # POSTGRES_USER=postgres
 # POSTGRES_PASSWORD=postgres
 # POSTGRES_DB=escriptorium