Index: /main/trunk/greenstone3/web/interfaces/default/js/utility_scripts.js
===================================================================
--- /main/trunk/greenstone3/web/interfaces/default/js/utility_scripts.js	(revision 37030)
+++ /main/trunk/greenstone3/web/interfaces/default/js/utility_scripts.js	(revision 37031)
@@ -11,5 +11,5 @@
 */
 function urlEncodeChar(single_char_string) {
-    /*var hex = Number(single_char_string.charCodeAt(0)).toString(16);
+    /*let hex = Number(single_char_string.charCodeAt(0)).toString(16);
     var str = "" + hex;
     str = "%" + str.toUpperCase();
@@ -292,28 +292,51 @@
 
 function loadAudio(audio, sectionData) {
-   var speakerObjects = [];
-   var uniqueSpeakers;
+   let editMode = false;
+   let currentRegion = {speaker: '', start: '', end: ''};
+   let currentRegions = [];
+
+   // let speakerObjects = [];
+   // let tempSpeakerObjects = [];
+   // let uniqueSpeakers;
    const inputFile = sectionData;
-   var itemType;
-
-   // var accentColour = "#4CA72D";
-   var accentColour = "#F8C537";
-   var regionTransparency = "59";
-
-   var waveformContainer = document.getElementById("waveform");
+   let itemType;
+
+   let dualMode = false;
+   let secondaryLoaded = false;
+
+   let editsMade = false;
+   let undoLevel = 0;
+   let undoStates = [];
+   let prevUndoState = "";
+   let tempZoomSave = 0;
+   let isZooming;
+
+   let accentColour = "#66d640";
+   // let accentColour = "#F8C537";
+   let regionTransparency = "50";
+
+   let waveformContainer = document.getElementById("waveform");
    
-   var wavesurfer = WaveSurfer.create({ // wavesurfer options
+   let wavesurfer = WaveSurfer.create({ // wavesurfer options
       container: waveformContainer,
       backend: "MediaElement",
-      backgroundColor: "rgb(54, 73, 78)",
+      backgroundColor: "rgb(40, 54, 58)",
+      // backgroundColor: "rgb(24, 36, 39)",
       waveColor: "white",
       progressColor: accentColour,
-      barWidth: 2,
+      // progressColor: "grey",
+      // barWidth: 2,
       barHeight: 1.2,
-      barGap: 2,
-      barRadius: 1,
+      // barGap: 2,
+      // barRadius: 1,
       cursorColor: 'black',
+      cursorWidth: 2,
+      normalize: true, // normalizes by maximum peak
       plugins: [
-         WaveSurfer.regions.create(),
+         WaveSurfer.regions.create({
+            // formatTimeCallback: function(a, b) {
+            //    return "TEST";
+            // }
+         }),
          WaveSurfer.timeline.create({
             container: "#wave-timeline",
@@ -329,5 +352,5 @@
                 'background-color': '#000',
                 color: '#fff',
-                padding: '0.2em',
+                padding: '0.25rem',
                 'font-size': '12px'
             },
@@ -341,13 +364,109 @@
    // wavesurfer events
 
-   wavesurfer.on('region-click', function(region, e) { // play region audio on click
+   wavesurfer.on('region-click', handleRegionClick);
+
+   function handleRegionClick(region, e) { 
       e.stopPropagation();
-      handleRegionColours(region, true);
-      wavesurfer.play(region.start); // plays from start of region
-      // region.play(); // plays region only
+      if (!editMode) { // play region audio on click
+         wavesurfer.play(region.start); // plays from start of region
+      } else { // select / deselect current region
+         if (region.element.classList.contains("region-top")) caretClicked("primary-caret");
+         else if (region.element.classList.contains("region-bottom")) caretClicked("secondary-caret");
+         prevUndoState = "";
+
+         if (!e.ctrlKey && !e.shiftKey) {
+            currentRegions = [];
+            if (getCurrentRegionIndex() != -1 && isCurrentRegion(region)) {
+               removeCurrentRegion(); // deselect current region on click
+            } else {
+               currentRegion = region;
+               currentRegion.speaker = currentRegion.attributes.label;
+               region.play(); // start and stop to move play cursor to beginning of region
+               wavesurfer.playPause();
+            }
+         } else if (e.ctrlKey) { // control was held during click
+            if (currentRegions.length == 0 && isCurrentRegion(region)) {
+               removeCurrentRegion();
+            } else if (getCurrentRegionIndex() != -1 && isInCurrentRegions(region)) {
+               const removeIndex = getIndexInCurrentRegions(region);
+               if (removeIndex != -1) currentRegions.splice(removeIndex, 1);
+               if (currentRegions.length > 0 && isCurrentRegion(region)) { // change current region if removed
+                  currentRegion = currentRegions[0];
+                  // currentRegions = [];
+               }
+            } else {
+               if (currentRegions.length < 1) currentRegions.push(currentRegion);
+               if (getIndexInCurrentRegions(region) == -1) currentRegions.push(region); // add if it doesn't already exist
+               currentRegion = region;
+               currentRegion.speaker = currentRegion.attributes.label;
+               region.play();
+               wavesurfer.playPause();
+            }
+            if (currentRegions.length == 1)  currentRegions = []; // clear selected regions if there is only one
+         } else if (e.shiftKey) { // shift was held during click
+            if (getCurrentRegionIndex() != -1 && getIndexOfRegion(region) != -1) {
+               if (currentRegions && currentRegions.length > 0) {
+                  if (Math.max(...getCurrentRegionsIndexes()) < getIndexOfRegion(region)) { // shifting forwards / down
+                     currentRegions = currSpeakerSet.tempSpeakerObjects.slice(Math.min(...getCurrentRegionsIndexes()), getIndexOfRegion(region)+1);
+                  } else { // shifting backwards / up
+                     currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getIndexOfRegion(region), Math.max(...getCurrentRegionsIndexes())+1);
+                  }
+               } else {
+                  if (getCurrentRegionIndex() < getIndexOfRegion(region)) { // shifting forwards / down
+                     currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getCurrentRegionIndex(), getIndexOfRegion(region)+1);
+                  } else { // shifting backwards / up
+                     currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getIndexOfRegion(region), getCurrentRegionIndex()+1);
+                  }
+               }
+            }
+         }
+         if (speakerCheckbox.checked) { currentRegions = getRegionsWithSpeaker(currentRegion.speaker) } 
+         reloadRegionsAndChapters();
+      }
+   }
+
+   function getIndexInCurrentRegions(region) {
+      for (const reg of currentRegions) {
+         const regSpeaker = reg.attributes ? reg.attributes.label : reg.speaker;
+         if (reg.start == region.start && reg.end == region.end && regSpeaker == region.attributes.label) {
+            return currentRegions.indexOf(reg);
+         }
+      }
+      return -1;
+   }
+
+   function getIndexOfRegion(region) {
+      for (const reg of currSpeakerSet.tempSpeakerObjects) {
+         if (reg.start == region.start && reg.end == region.end && reg.speaker == region.attributes.label) {
+            return currSpeakerSet.tempSpeakerObjects.indexOf(reg);
+         }
+      }
+      return -1;
+   }
+
+   wavesurfer.on('region-mouseenter', function(region) { // region hover effects
+      handleRegionColours(region, true); 
+      hoverSpeaker.innerHTML = region.attributes.label;  
+      hoverSpeaker.style.marginLeft = parseInt(region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
+      if (!isInCurrentRegions(region)) {
+         removeRegionBounds();
+         drawRegionBounds(region, waveform.scrollLeft, "black");
+      }
+      if (isCurrentRegion(region)) drawRegionBounds(region, waveform.scrollLeft);
    });
-
-   wavesurfer.on('region-mouseenter', function(region) { handleRegionColours(region, true); });
-   wavesurfer.on('region-mouseleave', function(region) { if (!(wavesurfer.getCurrentTime() <= region.end && wavesurfer.getCurrentTime() >= region.start)) handleRegionColours(region, false); });
+   wavesurfer.on('region-mouseleave', function(region) { 
+      if (!(wavesurfer.getCurrentTime() <= region.end && wavesurfer.getCurrentTime() >= region.start)) handleRegionColours(region, false); 
+      removeRegionBounds();
+      if (currentRegion.speaker && getCurrentRegionIndex() != -1) { 
+         hoverSpeaker.innerHTML = currentRegion.speaker;
+         hoverSpeaker.style.marginLeft = parseInt(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
+         let currIndexes = getCurrentRegionsIndexes();
+         for (let i = 0; i < currIndexes.length; i++) {
+            drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, waveform.scrollLeft, "black");
+         }
+         drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, waveform.scrollLeft);
+      }
+      if (!currentRegion.speaker) hoverSpeaker.innerHTML = "";
+   });
    wavesurfer.on('region-in', function(region) { 
       handleRegionColours(region, true); 
@@ -359,7 +478,9 @@
       }
    });
-   wavesurfer.on('region-out', function(region) { handleRegionColours(region, false); });
-
-   var loader = document.createElement("span"); // loading audio element
+   wavesurfer.on('region-out', function(region) { handleRegionColours(region, false) });
+   wavesurfer.on('region-update-end', handleRegionEdit); // end of click-drag event
+   // wavesurfer.on('region-update-end', (region, e) => { handleRegionEdit(region, e)} ); // end of click-drag event
+
+   let loader = document.createElement("span"); // loading audio element
    loader.innerHTML = "Loading audio";
    loader.id = "waveform-loader";
@@ -369,5 +490,5 @@
       if (inputFile.endsWith("csv")) { // diarization if csv
          itemType = "chapter";
-         loadCSVFile(inputFile, ["speaker", "start", "end"]);
+         loadCSVFile(inputFile, ["speaker", "start", "end"], primarySet);
       } else if (inputFile.endsWith("json")) { // transcription if json
          itemType = "word";
@@ -377,8 +498,10 @@
       }
       loader.remove(); // remove load text
+      chapters.style.cursor = "pointer";
+      waveform.className = "audio-scroll";
    });
    
    function downloadURI(loc, name) {
-      var link = document.createElement("a");
+      let link = document.createElement("a");
       link.download = name;
       link.href = loc;
@@ -387,44 +510,178 @@
 
    // toolbar elements & event handlers
-   var chapters = document.getElementById("chapters");
-   var chapterButton = document.getElementById("chapterButton");
-   var backButton = document.getElementById("backButton");
-   var playPauseButton = document.getElementById("playPauseButton");
-   var forwardButton = document.getElementById("forwardButton");
-   var downloadButton = document.getElementById("downloadButton");
-   var muteButton = document.getElementById("muteButton");
-   var zoomSlider = document.getElementById("slider");
+   const audioContainer = document.getElementById("audioContainer");
+   const dualModeCheckbox = document.getElementById("dual-mode-checkbox");
+   const waveform = document.getElementsByTagName("wave")[0];
+   const primaryCaret = document.getElementById("primary-caret");
+   const secondaryCaret = document.getElementById("secondary-caret");
+   const chapters = document.getElementById("chapters");
+   const editPanel = document.getElementById("edit-panel");
+   const chapterButton = document.getElementById("chapterButton");
+   const zoomOutButton = document.getElementById("zoomOutButton");
+   const zoomSlider = document.getElementById("zoom-slider");
+   const zoomInButton = document.getElementById("zoomInButton");
+   const backButton = document.getElementById("backButton");
+   const playPauseButton = document.getElementById("playPauseButton");
+   const forwardButton = document.getElementById("forwardButton");
+   const editButton = document.getElementById("editButton");
+   const downloadButton = document.getElementById("downloadButton");
+   const muteButton = document.getElementById("muteButton");
+   const volumeSlider = document.getElementById("volume-slider");
+   const fullscreenButton = document.getElementById("fullscreenButton");
+   const speakerCheckbox = document.getElementById("change-all-checkbox");
+   const changeAllLabel = document.getElementById("change-all-label");
+   const speakerInput = document.getElementById("speaker-input");
+   const startTimeInput = document.getElementById("start-time-input");
+   const endTimeInput = document.getElementById("end-time-input");
+   const removeButton = document.getElementById("remove-button");
+   const createButton = document.getElementById("create-button");
+   const discardButton = document.getElementById("discard-button");
+   const undoButton = document.getElementById("undo-button");
+   const redoButton = document.getElementById("redo-button");
+   const saveButton = document.getElementById("save-button");
+   const hoverSpeaker = document.getElementById("hover-speaker");
+
+   audioContainer.addEventListener('fullscreenchange', (e) => { fullscreenChanged() });
+   dualModeCheckbox.addEventListener("change", dualModeChanged);
+   waveform.addEventListener('scroll', (e) => { waveformScrolled() })
+   primaryCaret.addEventListener("click", (e) => caretClicked(e.target.id));
+   secondaryCaret.addEventListener("click", (e) => caretClicked(e.target.id));
    chapters.style.height = "0px";
-   chapterButton.addEventListener("click", function() { toggleChapters(); });
-   backButton.addEventListener("click", function() { wavesurfer.skipBackward(); });
-   playPauseButton.addEventListener("click", function() { wavesurfer.playPause() });
-   forwardButton.addEventListener("click", function() { wavesurfer.skipForward(); });
-   // audio = /greenstone3/library/sites/localsite/collect/tiriana-audio/index/assoc/HASHa6b7.dir/Te_Kakano_CH09_B.mp3
-   downloadButton.addEventListener("click", function() { downloadURI(audio, audio.split(".dir/")[1]) }); 
-   muteButton.addEventListener("click", function() { wavesurfer.toggleMute(); });
+   editPanel.style.height = "0px";
+   chapterButton.addEventListener("click", () => { toggleChapters() });
+   zoomOutButton.addEventListener("click", () => { zoomSlider.stepDown(); zoomSlider.dispatchEvent(new Event("input")) });
+   zoomInButton.addEventListener("click", () => { zoomSlider.stepUp(); zoomSlider.dispatchEvent(new Event("input")) });
+   backButton.addEventListener("click", () => { wavesurfer.skipBackward(); });
+   playPauseButton.addEventListener("click", () => { wavesurfer.playPause() });
+   forwardButton.addEventListener("click", () => { wavesurfer.skipForward(); });
+   editButton.addEventListener("click", toggleEditMode); 
+   downloadButton.addEventListener("click", () => { downloadURI(audio, audio.split(".dir/")[1]) }); 
+   muteButton.addEventListener("click", () => { wavesurfer.toggleMute() });
+   volumeSlider.style["accent-color"] = accentColour; 
+   fullscreenButton.addEventListener("click", toggleFullscreen);
    zoomSlider.style["accent-color"] = accentColour; 
+   speakerCheckbox.addEventListener("change", speakerCheckboxChanged);
+   speakerInput.addEventListener("input", speakerChange);
+   speakerInput.addEventListener("blur", speakerInputUnfocused);
+   createButton.addEventListener("click", createNewRegion);
+   removeButton.addEventListener("click", removeRegion);
+   discardButton.addEventListener("click", discardRegionChanges);
+   undoButton.addEventListener("click", undo);
+   redoButton.addEventListener("click", redo);
+   saveButton.addEventListener("click", saveRegionChanges);
+   document.querySelectorAll('input[type=number]').forEach(e => {
+      e.onchange = (e) => { changeStartEndTime(e) }; // updates speaker objects when number input(s) are changed
+      e.onblur = () => { prevUndoState = "" }; 
+   }); 
+   audioContainer.addEventListener("keyup", keyPressed);
+
+   function keyPressed(e) {
+      if (e.target.tagName !== "INPUT") {
+         if (e.code === "Backspace" || e.code === "Delete") removeRegion();
+         else if (e.code === "Space") wavesurfer.playPause();
+         else if (e.code === "ArrowLeft") wavesurfer.skipBackward();
+         else if (e.code === "ArrowRight") wavesurfer.skipForward();
+      }
+   }
+
+   function dualModeChanged(e) { // on dualmode checkbox value change
+      dualMode = e.target.checked; 
+      currSpeakerSet = primarySet;
+      reloadRegionsAndChapters();
+      if (dualMode) {
+         if (!secondaryLoaded) {
+            loadCSVFile(inputFile.replace(".csv", "-2.csv"), ["speaker", "start", "end"], secondarySet);
+            secondaryLoaded = true; // ensure secondarySet doesn't get re-read > once
+         }
+         document.getElementById("caret-container").style.display = "flex";
+      } else {
+         document.getElementById("caret-container").style.display = "none";
+      }
+      currSpeakerSet = primarySet;
+   } 
 
    // path to toolbar images
-   var interface_bootstrap_images = "interfaces/" + gs.xsltParams.interface_name + "/images/bootstrap/";
-
-   wavesurfer.on("play", function() { playPauseButton.src = interface_bootstrap_images + "pause.svg"; });
-   wavesurfer.on("pause", function() { playPauseButton.src = interface_bootstrap_images + "play.svg"; });
+   let interface_bootstrap_images = "interfaces/" + gs.xsltParams.interface_name + "/images/bootstrap/";
+
+   function caretClicked(id) {
+      wavesurfer.clearRegions();
+      // flashChapters();
+      if (id === "primary-caret") {
+         currSpeakerSet = primarySet;
+         swapCarets(true);
+      } else if (id === "secondary-caret") {
+         currSpeakerSet = secondarySet;
+         swapCarets(false);
+      }
+      $(".region-top").remove();
+      $(".region-bottom").remove();
+      populateChapters(primarySet);
+      populateChapters(secondarySet);
+   }
+
+   function swapCarets(toPrimary) {
+      const currCaretIsPrimary = primaryCaret.src.includes("fill") ? true : false;
+      if ((toPrimary && !currCaretIsPrimary) || (!toPrimary && currCaretIsPrimary)) removeCurrentRegion(); // ensure currentRegion is only removed if changing speakerSet
+      if (toPrimary) {
+         primaryCaret.src = interface_bootstrap_images + "caret-right-fill.svg";
+         secondaryCaret.src = interface_bootstrap_images + "caret-right.svg";
+      } else {
+         primaryCaret.src = interface_bootstrap_images + "caret-right.svg";
+         secondaryCaret.src = interface_bootstrap_images + "caret-right-fill.svg";
+      }
+   }
+
+   wavesurfer.on("play", () => { playPauseButton.src = interface_bootstrap_images + "pause.svg"; });
+   wavesurfer.on("pause", () => { playPauseButton.src = interface_bootstrap_images + "play.svg"; });
    wavesurfer.on("mute", function(mute) { 
       if (mute) {
          muteButton.src = interface_bootstrap_images + "mute.svg"; 
          muteButton.style.opacity = 0.6;
+         volumeSlider.value = 0;
       }
       else {
          muteButton.src = interface_bootstrap_images + "unmute.svg"; 
          muteButton.style.opacity = 1;
+         volumeSlider.value = 1;
       } 
    });
 
-   zoomSlider.oninput = function () { // slider changes waveform zoom
+   volumeSlider.addEventListener("input", function() {
+      wavesurfer.setVolume(this.value);
+      if (this.value == 0) {
+         muteButton.src = interface_bootstrap_images + "mute.svg"; 
+         muteButton.style.opacity = 0.6;
+      } else {
+         muteButton.src = interface_bootstrap_images + "unmute.svg"; 
+         muteButton.style.opacity = 1;
+      }
+   });
+
+   zoomSlider.addEventListener("input", function() { // slider changes waveform zoom
       wavesurfer.zoom(Number(this.value) / 4); 
-   };
+      if (currentRegion.speaker && getCurrentRegionIndex() != -1) { 
+         hoverSpeaker.innerHTML = currentRegion.speaker;
+         hoverSpeaker.style.marginLeft = parseInt(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
+         removeRegionBounds();
+         let currIndexes = getCurrentRegionsIndexes();
+         for (let i = 0; i < currIndexes.length; i++) {
+            drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, waveform.scrollLeft, "black");
+         }
+         drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, waveform.scrollLeft);
+      }
+      let handles = document.getElementsByClassName("wavesurfer-handle");
+      if (this.value < 20) {
+         for (const handle of handles) {
+            handle.style.setProperty("width", "1px", "important");
+         }
+      } else {
+         for (const handle of handles) {
+            handle.style.setProperty("width", "3px", "important");
+         }
+      }
+   });
    wavesurfer.zoom(zoomSlider.value / 4); // set default zoom point
 
-   var toggleChapters = function() { // show & hide chapter section
+   let toggleChapters = function() { // show & hide chapter section
       if (chapters.style.height == "0px") {
          chapters.style.height = "30vh";
@@ -434,5 +691,16 @@
    }
 
-   function loadCSVFile(filename, manualHeader) { // based around: https://stackoverflow.com/questions/7431268/how-to-read-data-from-csv-file-using-javascript
+   function SpeakerSet(isSecondary, uniqueSpeakers, speakerObjects, tempSpeakerObjects) {
+      this.isSecondary = isSecondary;
+      this.uniqueSpeakers = uniqueSpeakers;
+      this.speakerObjects = speakerObjects;
+      this.tempSpeakerObjects = tempSpeakerObjects;
+   }
+   let primarySet = new SpeakerSet(false, [], [], []);
+   let secondarySet = new SpeakerSet(true, [], [], []);
+   let currSpeakerSet = primarySet;
+
+   function loadCSVFile(filename, manualHeader, speakerSet) { // based on: https://stackoverflow.com/questions/7431268/how-to-read-data-from-csv-file-using-javascript
+      // if (speakerSet) currSpeakerSet = speakerSet; // if parameter is given, set
       $.ajax({
          type: "GET",
@@ -440,9 +708,9 @@
          dataType: "text",
       }).then(function(data) {
-         var dataLines = data.split(/\r\n|\n/);
-         var headers;
-         var startIndex;
-         uniqueSpeakers = []; // used for obtaining unique colours
-         speakerObjects = []; // list of speaker items
+         let dataLines = data.split(/\r\n|\n/);
+         let headers;
+         let startIndex;
+         speakerSet.uniqueSpeakers = []; // used for obtaining unique colours
+         speakerSet.speakerObjects = []; // list of speaker items
 
          if (manualHeader) { // headers for columns can be provided if not existent in csv
@@ -454,52 +722,83 @@
          }
 
-         for (var i = startIndex; i < dataLines.length; i++) {
-            var data = dataLines[i].split(',');
+         for (let i = startIndex; i < dataLines.length; i++) {
+            let data = dataLines[i].split(',');
             if (data.length == headers.length) {
-               var item = {};
-               for (var j = 0; j < headers.length; j++) {
+               let item = {};
+               for (let j = 0; j < headers.length; j++) {
                   item[headers[j]] = data[j];
-                  if (j == 0) {
-                     if (!uniqueSpeakers.includes(data[j])) {
-                        uniqueSpeakers.push(data[j]);
-                     }
+                  if (j == 0 && !speakerSet.uniqueSpeakers.includes(data[j])) {
+                     speakerSet.uniqueSpeakers.push(data[j]);
                   }
                }
-               speakerObjects.push(item);
+               speakerSet.speakerObjects.push(item);
             }
          }
-         populateChapters(speakerObjects);
+         speakerSet.tempSpeakerObjects = cloneSpeakerObjectArray(speakerSet.speakerObjects);
+         populateChapters(speakerSet);
+         resetUndoStates(); // undo stack init
       });
    }
 
    function populateChapters(data) { // populates chapter section and adds regions to waveform
-      // colorbrewer is a web tool for guidance in choosing map colour schemes based on a variety of settings.
+      // colorbrewer is a web tool for guidance in choosing map colour schemes based on a letiety of settings.
       // this colour scheme is designed for qualitative data
 
-      if (uniqueSpeakers.length > 8) colourbrewerset = colorbrewer.Set2[8];
-      else if (uniqueSpeakers.length < 3) colourbrewerset = colorbrewer.Set2[3];
-      else  colourbrewerset = colorbrewer.Set2[uniqueSpeakers.length];
-
-      for (var i = 0; i < data.length; i++) {
-         var chapter = document.createElement("div");
-         var speakerLetter = getLetter(data[i]);
+      if (data.uniqueSpeakers.length > 8) colourbrewerset = colorbrewer.Set2[8];
+      else if (data.uniqueSpeakers.length < 3) colourbrewerset = colorbrewer.Set2[3];
+      else  colourbrewerset = colorbrewer.Set2[data.uniqueSpeakers.length];
+
+      let dataIsSelected = false;
+
+      if ((!data.isSecondary && primaryCaret.src.includes("fill")) || (data.isSecondary && secondaryCaret.src.includes("fill"))) dataIsSelected = true;
+      if (dataIsSelected || !dualMode) chapters.innerHTML = ""; // clear chapter div for re-population
+      data.tempSpeakerObjects = sortSpeakerObjectsByStart(data.tempSpeakerObjects); // sort speakerObjects by start time
+
+      for (let i = 0; i < data.tempSpeakerObjects.length; i++) {
+         let chapter = document.createElement("div"); 
+         chapter.classList.add("chapter");
          chapter.id = "chapter" + i;
-         chapter.classList.add("chapter");
-         chapter.innerHTML = "Speaker " + speakerLetter + "<span class='speakerTime' id='" + "chapter" + i + "'>" + minutize(data[i].start) + " - " + minutize(data[i].end) + "</span>";
-         chapter.addEventListener("click", e => { chapterClicked(e.target.id) });
-         chapter.addEventListener("mouseover", e => { chapterEnter(e.target.id) });
-         chapter.addEventListener("mouseleave", e => { chapterLeave(e.target.id) });
-         chapters.appendChild(chapter);
-         // console.log("index: " + uniqueSpeakers.indexOf(data[i].speaker)%8);
-         // console.log("colour: " + colourbrewerset[uniqueSpeakers.indexOf(data[i].speaker)%8]);
-         wavesurfer.addRegion({
+         let speakerName = data.tempSpeakerObjects[i].speaker;
+         let speakerTime = document.createElement("span"); 
+         speakerTime.classList.add("speakerTime"); 
+         speakerTime.innerHTML = minutize(data.tempSpeakerObjects[i].start) + " - " + minutize(data.tempSpeakerObjects[i].end) + "s";
+         chapter.innerHTML = speakerName;
+         chapter.appendChild(speakerTime);
+         chapter.addEventListener("click", chapterClicked);
+         chapter.addEventListener("mouseover", e => { chapterEnter(Array.from(e.target.parentElement.children).indexOf(e.target)) });
+         chapter.addEventListener("mouseleave", e => { chapterLeave(Array.from(e.target.parentElement.children).indexOf(e.target)) });
+
+         let selected = false;
+         let dummyRegion = { start: data.tempSpeakerObjects[i].start, end: data.tempSpeakerObjects[i].end };
+
+         if ((dataIsSelected || !dualMode) && (isCurrentRegion(dummyRegion) || isInCurrentRegions(dummyRegion))) {
+            chapter.classList.add("selected-chapter");
+            selected = true;
+         }
+
+         if (dataIsSelected || !dualMode) chapters.appendChild(chapter);
+
+         let associatedReg = wavesurfer.addRegion({ // create associated wavesurfer region
             id: "region" + i,
-            start: data[i].start,
-            end: data[i].end,
-            drag: false,
-            resize: false,
-            color: colourbrewerset[uniqueSpeakers.indexOf(data[i].speaker)%8] + regionTransparency,
+            start: data.tempSpeakerObjects[i].start,
+            end: data.tempSpeakerObjects[i].end,
+            drag: editMode,
+            resize: editMode,
+            attributes: {
+               label: speakerName,
+            },
+            color: colourbrewerset[data.uniqueSpeakers.indexOf(data.tempSpeakerObjects[i].speaker)%8] + regionTransparency,
+            ...(selected) && {color: "rgba(255,50,50,0.5)"},
          });
-      }
+         data.tempSpeakerObjects[i].region = associatedReg;
+      }
+
+      let regions = document.getElementsByTagName("region");
+      if (dualMode) {
+         if (document.getElementsByClassName("region-top").length === 0) for (const reg of regions) reg.classList.add("region-top"); 
+         else for (const rego of regions) if (!rego.classList.contains("region-top")) rego.classList.add("region-bottom");
+      }
+      if (editMode) for (const reg of regions) reg.style.setProperty("z-index", "3", "important");
+      else for (const reg of regions) reg.style.setProperty("z-index", "1", "important");
    }
 
@@ -513,16 +812,16 @@
 
    function populateWords(data) { // populates word section and adds regions to waveform
-      var transcription = data.transcription;
-      var words = data.words;
-      var wordContainer = document.createElement("div");
+      let transcription = data.transcription;
+      let words = data.words;
+      let wordContainer = document.createElement("div");
       wordContainer.id = "word-container";
-      for (var i = 0; i < words.length; i++) {
-         var word = document.createElement("span");
+      for (let i = 0; i < words.length; i++) {
+         let word = document.createElement("span");
          word.id = "word" + i;
          word.classList.add("word");
          word.innerHTML = transcription.split(" ")[i];
          word.addEventListener("click", e => { wordClicked(data, e.target.id) });
-         word.addEventListener("mouseover", e => { chapterEnter(e.target.id) });
-         word.addEventListener("mouseleave", e => { chapterLeave(e.target.id) });
+         word.addEventListener("mouseover", e => { chapterEnter(Array.from(e.target.parentElement.children).indexOf(e.target)) });
+         word.addEventListener("mouseleave", e => { chapterLeave(Array.from(e.target.parentElement.children).indexOf(e.target)) });
          wordContainer.appendChild(word);
          wavesurfer.addRegion({
@@ -538,51 +837,76 @@
    }
 
-   var chapterClicked = function(id) { // plays audio from start of chapter
-      var index = id.replace("chapter", "");
-      var start = speakerObjects[index].start;
-      var end = speakerObjects[index].end;
-      // wavesurfer.play(start, end);
+   let chapterClicked = function(e) { // plays audio from start of chapter
+      let index = e.target.id.replace("chapter", "");
+      let clickedRegion = currSpeakerSet.tempSpeakerObjects[index].region;
+      handleRegionClick(clickedRegion, e);
+   }
+
+   function wordClicked(data, id) { // plays audio from start of word
+      let index = id.replace("word", "");
+      let start = data.words[index].startTime;
       wavesurfer.play(start);
    }
 
-   function wordClicked(data, id) { // plays audio from start of word
-      var index = id.replace("word", "");
-      var start = data.words[index].startTime;
-      var end = data.words[index].endTime;
-      // wavesurfer.play(start, end);
-      wavesurfer.play(start);
-   }
-
-   function chapterEnter(id) {
-      regionEnter(wavesurfer.regions.list["region" + id.replace(itemType, "")]);
-   }
-
-   function chapterLeave(id) {
-      regionLeave(wavesurfer.regions.list["region" + id.replace(itemType, "")]);
+   function chapterEnter(idx) {
+      let reg = currSpeakerSet.tempSpeakerObjects[idx].region;
+      regionEnter(reg);
+      hoverSpeaker.innerHTML = reg.attributes.label;  
+      hoverSpeaker.style.marginLeft = parseInt(reg.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
+      if (!isInCurrentRegions(reg)) {
+         removeRegionBounds();
+         drawRegionBounds(reg, waveform.scrollLeft, "black");
+      }
+   }
+
+   function chapterLeave(idx) {
+      regionLeave(currSpeakerSet.tempSpeakerObjects[idx].region);
+      removeRegionBounds();
+      hoverSpeaker.innerHTML = "";
+      if (currentRegion.speaker && getCurrentRegionIndex() != -1) { 
+         hoverSpeaker.innerHTML = currentRegion.speaker;
+         hoverSpeaker.style.marginLeft = parseInt(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
+         let currIndexes = getCurrentRegionsIndexes();
+         for (let i = 0; i < currIndexes.length; i++) {
+            drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, waveform.scrollLeft, "black");
+         }
+         drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, waveform.scrollLeft);
+      }
    }
 
    function handleRegionColours(region, highlight) { // handles region, chapter & word colours
-      var colour;
-      if (highlight) {
-         colour = "rgb(101, 116, 116)";
-         regionEnter(region);
+      if (!dualMode || (region.element.classList.contains("region-top") && primaryCaret.src.includes("fill")) || region.element.classList.contains("region-bottom") && secondaryCaret.src.includes("fill")) {
+         let colour;
+         if (highlight) {
+            colour = "rgb(101, 116, 116)";
+            regionEnter(region);
+         } else {
+            colour = "";
+            regionLeave(region);
+         }
+         if (isCurrentRegion(region) || isInCurrentRegions(region)) {
+            colour = "rgba(255, 50, 50, 0.5)";
+         }
+         let regionIndex = region.id.replace("region","");
+         let corrItem = document.getElementById(itemType + regionIndex);
+         corrItem.style.backgroundColor = colour; // updates chapter background (not region)
+      }
+   }
+
+   function regionEnter(region) {
+      if (isCurrentRegion(region) || isInCurrentRegions(region)) {
+         if (region.element.classList.contains("region-top") && !currSpeakerSet.isSecondary) region.update({ color: "rgba(255, 50, 50, 0.5)" });
       } else {
-         colour = "";
-         regionLeave(region);
-      }
-      var regionIndex = region.id.replace("region","");
-      var corrItem = document.getElementById(itemType + regionIndex);
-      corrItem.style.backgroundColor = colour;
-   }
-
-   function regionEnter(region) {
-      region.update({ color: "rgba(255, 255, 255, 0.35)" });
+         region.update({ color: "rgba(255, 255, 255, 0.35)" });
+      }
    }
 
    function regionLeave(region) { 
       if (itemType == "chapter") {
-         if (!(wavesurfer.getCurrentTime() + 0.1 < region.end && wavesurfer.getCurrentTime() > region.start)) {
-            var index = region.id.replace("region", "");
-            region.update({ color: colourbrewerset[uniqueSpeakers.indexOf(speakerObjects[index].speaker)%8] + regionTransparency });
+         if (isCurrentRegion(region) || isInCurrentRegions(region)) {
+            region.update({ color: "rgba(255, 50, 50, 0.5)" });
+         } else if (!(wavesurfer.getCurrentTime() + 0.1 < region.end && wavesurfer.getCurrentTime() > region.start)) {
+            let index = region.id.replace("region", "");
+            region.update({ color: colourbrewerset[currSpeakerSet.uniqueSpeakers.indexOf(currSpeakerSet.tempSpeakerObjects[index].speaker)%8] + regionTransparency });
          }
       } else {
@@ -592,18 +916,649 @@
 
    function minutize(num) { // converts seconds to m:ss for chapters & waveform hover
-      var seconds = Math.round(num % 60);
-      if (seconds.toString().length == 1) seconds = "0" + seconds;
-      return Math.floor(num / 60) + ":" + seconds;
+      // return (num - (num %= 60)) / 60 + (9 < num ? ':' : ':0') + ~~num; // https://stackoverflow.com/questions/3733227/javascript-seconds-to-minutes-and-seconds
+
+      let date = new Date(null);
+      date.setSeconds(num);
+      return date.toTimeString().split(" ")[0].substring(3);
    }
 
    function getLetter(val) {
-      var speakerNum = parseInt(val.speaker.replace("SPEAKER_",""));
+      // return val.replace("SPEAKER_","");
+      let speakerNum = parseInt(val.replace("SPEAKER_",""));
       return String.fromCharCode(65 + speakerNum); // 'A' == UTF-16 65
    }
+
+
+
+   // edit functionality
+
+   function toggleEditMode() { // toggles edit panel and redraws regions with resize handles
+      toggleEditPanel();
+      updateRegionEditPanel();
+   }
+
+   function toggleEditPanel() { // show & hide edit panel
+      currentRegion.speaker = '';
+      currentRegion.start = '';
+      currentRegion.end = '';
+      currentRegions = [];
+      removeRegionBounds();
+      hoverSpeaker.innerHTML = "";
+      if (editPanel.style.height == "0px") {
+         if (chapters.style.height == "0px") chapters.style.height = "30vh"; // expands chapter panel
+         editPanel.style.height = "30vh";
+         editPanel.style.padding = "1rem";
+         setRegionEditMode(true);
+      } else { 
+         editPanel.style.height = "0px"; 
+         editPanel.style.padding = "0px"; 
+         setRegionEditMode(false);
+      }
+   }
+
+   function setRegionEditMode(state) {
+      editMode = state;
+      chapters.innerHTML = '';
+      wavesurfer.clearRegions();
+      populateChapters(currSpeakerSet);
+   }
+
+   function handleRegionEdit(region, e) { 
+      if (e.target.localName === "region" || e.target.localName === "handle") {
+         if (region.element.classList.contains("region-top")) { currSpeakerSet = primarySet; swapCarets(true) }
+         else if (region.element.classList.contains("region-bottom")) { currSpeakerSet = secondarySet; swapCarets(false) }
+         editsMade = true;
+         currentRegion = region;
+         region.play();
+         wavesurfer.pause();
+         let regionIndex = getCurrentRegionIndex();
+         currentRegion.speaker = currSpeakerSet.tempSpeakerObjects[regionIndex].speaker; 
+         currSpeakerSet.tempSpeakerObjects[regionIndex].region = region;
+         currSpeakerSet.tempSpeakerObjects[regionIndex].start = region.start;
+         currSpeakerSet.tempSpeakerObjects[regionIndex].end = region.end;
+         reloadRegionsAndChapters();
+         handleSameSpeakerOverlap(getCurrentRegionIndex()); // recalculate index in case start pos has changed
+         addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "dragdrop", getCurrentRegionIndex());
+         editPanel.click(); // fixes buttons needing to be clicked twice (unknown cause!)
+      } else console.log("resizing too fast, selected region not updated.");
+   }
+
+   function handleSameSpeakerOverlap(regionIdx) { // consumes/merges same-speaker regions with overlapping bounds
+      let draggedRegion = currSpeakerSet.tempSpeakerObjects[regionIdx]; // regionIdx may point to a different region within the for-loop after adjustments, so defined here
+      let draggedRegionSpeaker = draggedRegion.speaker;
+      for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
+         if (currSpeakerSet.tempSpeakerObjects[i].speaker === draggedRegionSpeaker && !regionsMatch(draggedRegion, currSpeakerSet.tempSpeakerObjects[i])) { // ensure speaker name match
+            if (currSpeakerSet.tempSpeakerObjects[i].start < draggedRegion.end && draggedRegion.start < currSpeakerSet.tempSpeakerObjects[i].end) { // ensure overlap
+               draggedRegion.start = Math.min(currSpeakerSet.tempSpeakerObjects[i].start, draggedRegion.start);
+               draggedRegion.end = Math.max(currSpeakerSet.tempSpeakerObjects[i].end, draggedRegion.end);
+               currentRegion = draggedRegion;
+               currSpeakerSet.tempSpeakerObjects.splice(i, 1); // remove consumed region
+               i = -1; // reset for loop to support multiple consumptions 
+            } 
+         }
+      }
+      reloadRegionsAndChapters();
+   }
+
+   function updateRegionEditPanel() { // updates edit panel content/inputs
+      if (currentRegion && currentRegion.speaker == "") { 
+         removeButton.classList.add("disabled");
+         speakerInput.classList.add("disabled");
+         speakerCheckbox.classList.add("disabled");
+         speakerCheckbox.disabled = true;
+         disableStartEndInputs();
+         speakerInput.readOnly = true;
+         speakerInput.value = "";
+      } else { 
+         removeButton.classList.remove("disabled");
+         speakerInput.classList.remove("disabled");
+         speakerCheckbox.classList.remove("disabled");
+         if (!isZooming) speakerCheckbox.disabled = false; 
+         enableStartEndInputs();
+         speakerInput.readOnly = false;
+      }
+      if (editsMade) {
+         discardButton.classList.remove("disabled");
+         saveButton.classList.remove("disabled");
+      } else {
+         discardButton.classList.add("disabled");
+         saveButton.classList.add("disabled");
+      }
+      if (speakerCheckbox.checked) {
+         // changeAllLabel.innerHTML = "Change all (x" + currentRegions.length + ")";
+         disableStartEndInputs();
+      }
+      if (currentRegion && currentRegion.speaker != "") {
+         speakerInput.value = currentRegion.speaker;
+         setInputInSeconds(startTimeInput, currentRegion.start);
+         setInputInSeconds(endTimeInput, currentRegion.end);
+      }
+   }
+
+   function createNewRegion() { // adds a new region to the waveform
+      const speaker = "NEW_SPEAKER"; // default name
+      let offset = 0;
+      if (!currSpeakerSet.uniqueSpeakers.includes(speaker)) { currSpeakerSet.uniqueSpeakers.push(speaker) }
+      else { offset = 5 * getRegionsWithSpeaker(speaker).length } // offset new region if multiple new regions are created. TODO: check region has different start time
+      const start = offset + wavesurfer.getCurrentTime();
+      const end = offset + wavesurfer.getCurrentTime() + 15;
+      currSpeakerSet.tempSpeakerObjects.push({speaker: speaker, start: start, end: end});
+      editsMade = true;
+      currentRegions = [];
+      currentRegion = getRegionFromProps({speaker: speaker, start: start, end: end});
+      reloadRegionsAndChapters();
+      addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "create", getCurrentRegionIndex());
+   }
+
+   function getRegionFromProps(props) { // find region using speaker, start & end time
+      for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
+         if (currSpeakerSet.tempSpeakerObjects[i].speaker === props.speaker && currSpeakerSet.tempSpeakerObjects[i].start === props.start && currSpeakerSet.tempSpeakerObjects[i].end === props.end) {
+            return currSpeakerSet.tempSpeakerObjects[i];
+         } 
+      }
+      console.log("getRegionFromProps failed to find matching region");
+   }
+
+   function removeRegion() { // removes currently selected region or regions
+      if (!removeButton.classList.contains("disabled")) {
+         if (getCurrentRegionIndex() != -1) { // if currentRegion has been set 
+            let currentRegionIndex = getCurrentRegionIndex();
+            let currentRegionIndexes = getCurrentRegionsIndexes();
+            for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
+               if (isCurrentRegion(currSpeakerSet.tempSpeakerObjects[i].region)) {
+                  // if (!currentRegion.region) currentRegion.remove(); // remove from wavesurfer.regions.list
+                  currSpeakerSet.tempSpeakerObjects.splice(i, 1); // remove from tempSpeakerObjects
+                  // else currentRegion.region.remove(); // remove if region was just added
+                  editsMade = true;
+                  if (i >= 0) i--; // decrement index for side-by-side regions
+                  if (!speakerCheckbox.checked && currentRegions.length < 1) {
+                     removeCurrentRegion();
+                     reloadRegionsAndChapters();
+                     addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "remove", currentRegionIndex);
+                     return; // jump out of for loop
+                  }
+               } else if (isInCurrentRegions(currSpeakerSet.tempSpeakerObjects[i])) { 
+                  currSpeakerSet.tempSpeakerObjects.splice(i, 1);
+                  if (i >= 0) i--;
+               }
+            }
+            removeCurrentRegion();
+            reloadRegionsAndChapters();
+            addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "remove", currentRegionIndex, currentRegionIndexes); // multiple regions removed
+         } else { console.log("no region selected") }
+      }
+   }
+
+   function regionsMatch(reg1, reg2) {
+      if (reg1.start == reg2.start && reg1.end == reg2.end) return true;
+      return false;
+   }
+
+   function isCurrentRegion(region) {
+      if (regionsMatch(currentRegion, region)) return true;
+      return false;
+   }
+
+   function isInCurrentRegions(region) {
+      if (currentRegions != []) {
+         for (let i = 0; i < currentRegions.length; i++) {
+            if (currentRegions[i].start == region.start && currentRegions[i].end == region.end) {
+               return true;
+            }
+         }
+      }
+      return false;
+   }
+
+   function getCurrentRegionIndex() { // returns the index of currently selected region
+      for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
+         if (isCurrentRegion(currSpeakerSet.tempSpeakerObjects[i].region)) { return i }
+      }
+      // if (dualMode) {
+      //    for (let i = 0; i < secondarySet.tempSpeakerObjects.length; i++) {
+      //       if (isCurrentRegion(secondarySet.tempSpeakerObjects[i].region)) { return i }
+      //    }
+      // }
+      return -1;
+   }
+
+   function getCurrentRegionsIndexes() { // returns the indexes of currently selected regions
+      let indexes = [];
+      for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
+         if (isInCurrentRegions(currSpeakerSet.tempSpeakerObjects[i].region)) { indexes.push(i) }
+      }
+      return indexes;
+   }
+
+   function removeCurrentRegion() { // removes current region, regions and bound markers
+      currentRegion = {speaker: '', start: '', end: ''};
+      currentRegions = [];
+      removeRegionBounds();
+      hoverSpeaker.innerHTML = "";
+   }
+
+   function getRegionsWithSpeaker(speaker) { // returns all regions with the given speaker name
+      let out = [];
+      for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
+         if (currSpeakerSet.tempSpeakerObjects[i].speaker === speaker) { out.push(currSpeakerSet.tempSpeakerObjects[i]) }
+      }
+      return out;
+   }
+
+   function sortSpeakerObjectsByStart(speakerOb) { // sorts the speaker object array by start time
+      return speakerOb.sort(function(a,b) {
+         return a.start - b.start;
+      });
+   }
+
+   function speakerChange() { // speaker input name onInput handler
+      const newSpeaker = speakerInput.value;
+      if (newSpeaker && newSpeaker != "") {
+         speakerInput.style.border = "2px solid transparent";
+         if (getCurrentRegionIndex() != -1) { // if a region is selected
+            if (!currSpeakerSet.uniqueSpeakers.includes(newSpeaker)) { currSpeakerSet.uniqueSpeakers.push(newSpeaker) }
+            if (currentRegions && currentRegions.length < 1) { currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].speaker = newSpeaker } // single change
+            else if (currentRegions && currentRegions.length > 1) { // multiple changes
+               for (idx of getCurrentRegionsIndexes()) currSpeakerSet.tempSpeakerObjects[idx].speaker = newSpeaker;
+            } 
+            speakerInput.value = "";
+            currentRegion.speaker = newSpeaker;
+            editsMade = true;
+            reloadRegionsAndChapters();
+            addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "speaker-change", getCurrentRegionIndex(), getCurrentRegionsIndexes());
+         } else { console.log("no region selected") }
+      } else { console.log("no text in speaker input"); speakerInput.style.border = "2px solid firebrick"; }
+   }
+
+   function speakerInputUnfocused() {
+      prevUndoState = "";
+      if (speakerInput.value == "" && !speakerInput.classList.contains("disabled")) {
+         speakerInput.style.border = "2px solid firebrick";
+         window.alert("Speaker input cannot be left empty. Please enter a speaker name.");
+         setTimeout(() => speakerInput.focus(), 10); // timeout needed otherwise input isn't selected
+      } else speakerInput.style.border = "2px transparent";
+   }
+
+   function speakerCheckboxChanged() { // "Change all" toggled
+      if (speakerCheckbox.checked) {
+         if (!isZooming) {
+            tempZoomSave = zoomSlider.value;
+            zoomTo(0); // zoom out to encompass all selected regions
+         }
+         let uniqueSelectedSpeakers;
+         if (currentRegions && currentRegions.length > 0) { // if more than one region selected
+            uniqueSelectedSpeakers = [... new Set(currentRegions.map(a => a.speaker))]; // gets unique speakers in currentRegions
+            uniqueSelectedSpeakers.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
+         } else uniqueSelectedSpeakers = [currentRegion.speaker];
+         currentRegions = [];
+         for (const speaker of uniqueSelectedSpeakers) {
+            for (const region of getRegionsWithSpeaker(speaker)) currentRegions.push(region);
+         }
+         reloadRegionsAndChapters();
+      } else {
+         if (!isZooming) {
+            zoomTo(tempZoomSave / 4);  // zoom back in to previous level
+         }
+         currentRegions = []; // this will lose track of previously selected region*s*
+         // changeAllLabel.innerHTML = "Change all";
+         reloadRegionsAndChapters();
+      }
+   }
+
+   function enableStartEndInputs() { // removes the 'disabled' tag from all time inputs
+      for (idx in startTimeInput.childNodes) { startTimeInput.childNodes[idx].disabled = false }
+      for (idx in endTimeInput.childNodes) { endTimeInput.childNodes[idx].disabled = false }
+   }
+
+   function disableStartEndInputs() { // adds the 'disabled' tag to all time inputs
+      for (idx in startTimeInput.childNodes) { startTimeInput.childNodes[idx].disabled = true; startTimeInput.childNodes[idx].value = 0; }
+      for (idx in endTimeInput.childNodes) { endTimeInput.childNodes[idx].disabled = true; endTimeInput.childNodes[idx].value = 0; }
+   }
+
+   function zoomTo(dest) { // (smoothly?) zooms wavesurfer waveform to destination
+      isZooming = true;
+      speakerCheckbox.disabled = true;
+      let isOut = false;
+      if (dest == 0) isOut = true;
+      zoomInterval = setInterval(() => {
+         if (isOut) {
+            if (zoomSlider.value != 0) {
+               if (zoomSlider.value > 50) zoomSlider.value -= 30; // ramp up for finer adjustments
+               else zoomSlider.stepDown(); 
+               wavesurfer.zoom(zoomSlider.value / 4); 
+            } else {
+               clearInterval(zoomInterval);
+               isZooming = false;
+               speakerCheckbox.disabled = false;
+               zoomSlider.dispatchEvent(new Event("input"));
+            }
+         } else {
+            if (zoomSlider.value / 4 < dest) {
+               if (zoomSlider.value > 50) zoomSlider.value += 30; // ramp up for finer adjustments
+               else zoomSlider.stepUp(); 
+               wavesurfer.zoom(zoomSlider.value / 4);
+            } else {
+               clearInterval(zoomInterval);
+               isZooming = false;
+               speakerCheckbox.disabled = false;
+               zoomSlider.dispatchEvent(new Event("input"));
+            }
+         }
+      }, 10); // interval
+      
+   }
+
+   function saveRegionChanges() { // saves tempSpeakerObjects to speakerObjects
+      if (!saveButton.classList.contains("disabled")) {
+         currSpeakerSet.speakerObjects = cloneSpeakerObjectArray(currSpeakerSet.tempSpeakerObjects);
+         editsMade = false;
+         removeCurrentRegion();
+         reloadRegionsAndChapters();
+         console.log("saved changes");
+      }
+   }
+
+   function discardRegionChanges() { // resets tempSpeakerObjects to speakerObjects
+      if (!discardButton.classList.contains("disabled")) {
+         let confirm = window.confirm("Are you sure you want to discard changes?");
+         if (confirm) {
+            currSpeakerSet.tempSpeakerObjects = cloneSpeakerObjectArray(currSpeakerSet.speakerObjects);
+            editsMade = false;
+            removeCurrentRegion();
+            resetUndoStates();
+            reloadRegionsAndChapters();
+            console.log("discarded changes");
+         }
+      }
+   }
+
+   function reloadRegionsAndChapters() { // redraws edit panel, chapter list, wavesurfer regions 
+      updateRegionEditPanel();
+      wavesurfer.clearRegions();
+      $(".region-top").remove();
+      $(".region-bottom").remove();
+      populateChapters(primarySet);
+      if (dualMode) { 
+         populateChapters(secondarySet);
+         currSpeakerSet = primarySet;
+      }
+      updateCurrSpeakerSet();
+      if (currentRegion.speaker && getCurrentRegionIndex() != -1) { 
+         hoverSpeaker.innerHTML = currentRegion.speaker;
+         hoverSpeaker.style.marginLeft = parseInt(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
+         removeRegionBounds();
+         let currIndexes = getCurrentRegionsIndexes();
+         for (let i = 0; i < currIndexes.length; i++) {
+            drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, waveform.scrollLeft, "black");
+         }
+         drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, waveform.scrollLeft);
+      }
+      if (currentRegions.length < 1) { 
+         removeButton.innerHTML = "Remove Selected Region";
+         enableStartEndInputs();
+      } else {
+         removeButton.innerHTML = "Remove Selected Regions (x" + currentRegions.length + ")";
+         disableStartEndInputs();
+         const uniqueSelectedSpeakers = [... new Set(currentRegions.map(a => a.speaker))]; // gets unique speakers in currentRegions
+         uniqueSelectedSpeakers.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
+         // console.log(uniqueSelectedSpeakers); // CLG
+         speakerInput.value = uniqueSelectedSpeakers.join(", ");
+      }
+   }
+
+   function changeStartEndTime(e) { // start/end time input handler
+      let newStart = getTimeInSecondsFromInput(startTimeInput);
+      let newEnd = getTimeInSecondsFromInput(endTimeInput);
+      let duration = Math.floor(wavesurfer.getDuration()); // total duration of current audio
+
+      if (getCurrentRegionIndex() != -1) { // if there is a selected region
+         if (newEnd <= newStart) newStart = newEnd - 1; // when start time > end time, push region forward
+         if (newEnd <= 0) newEnd = 1;
+         if (newStart < 0) newStart = 0; // ensures region start doesn't go < 0s
+         if (newEnd > duration) newEnd = duration; // ensures region start doesn't go > duration
+         
+         setInputInSeconds(startTimeInput, newStart);
+         setInputInSeconds(endTimeInput, newEnd);
+
+         let currRegIdx = getCurrentRegionIndex();
+         currSpeakerSet.tempSpeakerObjects[currRegIdx].start = newStart;
+         currSpeakerSet.tempSpeakerObjects[currRegIdx].end = newEnd;
+         currentRegion.start = newStart;
+         currentRegion.end = newEnd;
+         editsMade = true;
+         reloadRegionsAndChapters();
+         handleSameSpeakerOverlap(currRegIdx);
+         addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "change-time", getCurrentRegionIndex());
+      } else { 
+         console.log("no region selected");
+         setInputInSeconds(startTimeInput, 0);
+         setInputInSeconds(endTimeInput, 0);
+      }
+   }
+
+   function getTimeInSecondsFromInput(input) { // returns time in seconds from start or end input
+      let hours = input.children[0].valueAsNumber;
+      let mins = input.children[1].valueAsNumber;
+      let secs = input.children[2].valueAsNumber;
+      return (hours * 3600) + (mins * 60) + secs;
+   }
+
+   function setInputInSeconds(input, seconds) { // sets start or end input time when given seconds
+      let date = new Date(null);
+      date.setMilliseconds(seconds * 1000);
+      input.children[0].value = date.getHours() % 12;
+      input.children[1].value = date.getMinutes();
+      input.children[2].value = date.getSeconds() + "." + date.getMilliseconds();
+      
+      document.querySelectorAll('input[type=number]').forEach(e => {
+         e.value = Math.round(e.valueAsNumber * 10) / 10; // to 1dp
+         if (e.classList.contains("seconds") && !e.value.includes(".")) e.value = e.value + ".0";
+         else if (e.value.length === 1) e.value = '0' + e.value; // 0 on left for hrs & mins
+         if (e.value.length === 3) e.value = '0' + e.value; // 0 on the left (doesn't work on FF)
+         // if (e.value.length < 4) e.value = e.value.slice(0, 4); // always 4 digits (3 numbers, 1 fullstop) 
+         // if (!e.value) e.value = '00'; // avoiding letters on FF
+      });      
+   }
+
+   function addUndoState(state, secState, isSec, type, currRegIdx, currRegIdxs) { // adds a new state to the undoStates stack
+      let newState = cloneSpeakerObjectArray(state.tempSpeakerObjects); // clone method removes references
+      let newSecState = cloneSpeakerObjectArray(secState.tempSpeakerObjects); // clone method removes references
+      undoButton.classList.remove("disabled");
+      undoStates = undoStates.slice(0, undoLevel + 1); // trim to current level if undos have already been made
+      undoStates.push({state: newState, secState: newSecState, isSec: isSec, currentRegionIndex: currRegIdx, currentRegionIndexes: currRegIdxs, type: type});
+      if ((type === "change-time" && prevUndoState === "change-time") || (type === "speaker-change" && prevUndoState === "speaker-change")) { // checks if similar change was made previously
+         undoStates.splice(-2, 1); // remove second-to-last item in undoStates stack (merge last two changes into one to avoid multiple small edits)
+         prevUndoState = type;
+      } else undoLevel++;
+      prevUndoState = type;
+      redoButton.classList.add("disabled");
+      // console.log(undoStates.at(-1));
+   }
+
+   function undo() { // undo action: go back one state in the undoStates stack
+      if (!undoButton.classList.contains("disabled")) { // ensure there exist states to undo to
+         if (undoLevel - 1 < 0) console.log("ran out of undos");
+         else {            
+            let adjustedUndoLevel = undoLevel-1;
+            primarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[adjustedUndoLevel].state.slice(0)); // slice & clone removes potential references between arrays
+            if (dualMode && undoStates[adjustedUndoLevel].secState && undoStates[adjustedUndoLevel].secState.length > 0) { // if secondary undoState exists
+               secondarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[adjustedUndoLevel].secState.slice(0)); // slice & clone removes potential references between arrays
+            }
+            editsMade = true;
+            
+            let selectedSpeakerSet;
+            
+            // handle currentRegion change
+            removeCurrentRegion();  
+            if (undoStates[undoLevel] && undoStates[undoLevel].type && undoStates[undoLevel].type == "remove") { // if destination state type is remove
+               selectedSpeakerSet = (undoStates[undoLevel].isSec) ? secondarySet : primarySet;
+               if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
+               else caretClicked("primary-caret");
+               currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel].currentRegionIndex]; // restore previous current state
+               // console.log("undo-ing to index " + undoStates[undoLevel].currentRegionIndex);
+            } else if (undoStates[undoLevel-1].currentRegionIndex) {
+               if (!dualMode) selectedSpeakerSet = primarySet;
+               else {
+                  selectedSpeakerSet = (undoStates[undoLevel-1].isSec) ? secondarySet : primarySet;
+                  if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
+                  else caretClicked("primary-caret");
+               }
+               currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel-1].currentRegionIndex]; // restore previous current state
+               // console.log("undo-ing to index " + undoStates[undoLevel-1].currentRegionIndex);
+            }
+            // handle currentRegions change NEEDS REVISION xxxxx
+            if (undoStates[undoLevel-1].currentRegionIndexes && undoStates[undoLevel-1].currentRegionIndexes.length > 1) {
+               for (const idx of undoStates[undoLevel-1].currentRegionIndexes) currentRegions.push(currSpeakerSet.tempSpeakerObjects[idx]);
+
+               // currentRegions = getRegionsWithSpeaker(currentRegion.speaker);
+               // if (!speakerCheckbox.checked) speakerCheckbox.click(); // manually fires onChange event
+            }
+            reloadRegionsAndChapters();
+            undoLevel--; // decrement undoLevel
+            if (undoLevel - 1 < 0) undoButton.classList.add("disabled");
+            else undoButton.classList.remove("disabled");
+         }
+         if (undoLevel < undoStates.length) redoButton.classList.remove("disabled");
+      }
+   }
+
+   function redo() { // redo action: go forward one state in the undoStates stack
+      if (!redoButton.classList.contains("disabled")) { // ensure there exist states to redo to
+         if (undoLevel + 1 > undoStates.length - 1) console.log("ran out of redos");
+         else {
+            primarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[undoLevel+1].state.slice(0)); // set primary to new state
+            secondarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[undoLevel+1].secState.slice(0)); // set secondary to new state
+            editsMade = true;
+            let selectedSpeakerSet;
+
+            // handle currentRegion change
+            if (undoLevel+1 <= undoStates.length-1) {
+               removeCurrentRegion();
+               if (undoStates[undoLevel+2] && undoStates[undoLevel+2].type && undoStates[undoLevel+2].type == "remove") {
+                  selectedSpeakerSet = (undoStates[undoLevel+2].isSec) ? secondarySet : primarySet;
+                  if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret"); 
+                  else caretClicked("primary-caret");
+                  currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel+2].currentRegionIndex];
+               } else {
+                  selectedSpeakerSet = (undoStates[undoLevel+1].isSec) ? secondarySet : primarySet;
+                  if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret"); 
+                  else caretClicked("primary-caret");
+                  currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel+1].currentRegionIndex];
+               }
+
+               // console.log("redo-ing to index " + undoStates[undoLevel+1].currentRegionIndex);
+               if (undoStates[undoLevel+1].currentRegionIndexes && undoStates[undoLevel+1].currentRegionIndexes.length > 1) {
+                  for (const idx of undoStates[undoLevel-1].currentRegionIndexes) currentRegions.push(currSpeakerSet.tempSpeakerObjects[idx]);
+                  // currentRegions = getRegionsWithSpeaker(currentRegion.speaker);
+                  // if (!speakerCheckbox.checked) speakerCheckbox.click(); // ensures onchange event is fired
+               }
+            }
+            
+            reloadRegionsAndChapters();
+            undoLevel++; // increment undoLevel
+            if (undoLevel + 1 > undoStates.length - 1) redoButton.classList.add("disabled");
+            else redoButton.classList.remove("disabled");
+         }
+         if (undoLevel < undoStates.length) undoButton.classList.remove("disabled");
+         // console.log("new undoLevel: " + undoLevel);
+      }
+   }
+
+   function resetUndoStates() { // clear undo history
+      undoStates = [{state: cloneSpeakerObjectArray(primarySet.tempSpeakerObjects), secState: cloneSpeakerObjectArray(secondarySet.tempSpeakerObjects)}];
+      undoLevel = 0;
+      undoButton.classList.add("disabled");
+      redoButton.classList.add("disabled");
+   }
+
+   function waveformScrolled() { // waveform scroll handler
+      if (currentRegion.speaker && getCurrentRegionIndex() != -1) { // updates region bound markers if selected region exists
+         hoverSpeaker.innerHTML = currentRegion.speaker;
+         hoverSpeaker.style.marginLeft = parseInt(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
+         removeRegionBounds();
+         let currIndexes = getCurrentRegionsIndexes();
+         for (let i = 0; i < currIndexes.length; i++) {
+            drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, waveform.scrollLeft, "black");
+         }
+         drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, waveform.scrollLeft);
+      }
+   }
+
+   function drawRegionBounds(region, scrollPos) { // draws on canvas to show bounds of hovered/selected region
+      const hoverSpeakerCanvas = document.createElement("canvas");
+      let colour = "black";
+      hoverSpeakerCanvas.id = "hover-speaker-canvas";
+      hoverSpeakerCanvas.classList.add("region-bounds");
+      hoverSpeakerCanvas.width = audioContainer.clientWidth; // max width of drawn bounds
+      const ctx = hoverSpeakerCanvas.getContext("2d");
+
+      ctx.translate(0.5, 0.5); // fixes lineWidth inconsistency
+      ctx.lineWidth = 1;
+      if (currentRegions && currentRegions.length < 1 && isCurrentRegion(region)) {
+         colour = "FireBrick";
+         ctx.lineWidth = 3;
+      }
+      ctx.strokeStyle = colour;
+      ctx.beginPath();
+      ctx.moveTo(parseInt(region.element.style.left.slice(0, -2)) - scrollPos, 28);
+      ctx.lineTo(parseInt(region.element.style.left.slice(0, -2)) - scrollPos, 20);
+      ctx.lineTo(parseInt(region.element.style.left.slice(0, -2)) + parseInt(region.element.style.width.slice(0, -2)) - scrollPos, 20);
+      ctx.lineTo(parseInt(region.element.style.left.slice(0, -2)) + parseInt(region.element.style.width.slice(0, -2)) - scrollPos, 28);
+      ctx.stroke();
+      audioContainer.prepend(hoverSpeakerCanvas);
+   }
+
+   function removeRegionBounds() { // remove all region bound markers
+      let canvases = document.getElementsByClassName('region-bounds');
+      while (canvases[0]) canvases[0].parentNode.removeChild(canvases[0]);
+   }
+
+   function updateCurrSpeakerSet() {
+      if (primaryCaret.src.includes("fill")) currSpeakerSet = primarySet;
+      else if (secondaryCaret.src.includes("fill")) currSpeakerSet = secondarySet;
+   }
+
+   function cloneSpeakerObjectArray(inputArray) { // clones speakerObjectArray without references (wavesurfer regions)
+      let output = [];
+      for (let i = 0; i < inputArray.length; i++) { output.push({speaker: inputArray[i].speaker, start: inputArray[i].start, end: inputArray[i].end }) }
+      return output;
+   }
+
+   function flashInput(valid) { // flashes background of input to show validity of input
+      if (valid) speakerInput.style.backgroundColor = "rgb(50,255,50)"; 
+      else speakerInput.style.backgroundColor = "rgb(255,50,50)";
+      setTimeout(() => { speakerInput.style.backgroundColor = "rgb(255,255,255)" }, 750);
+   }
+
+   function flashChapters() {
+      chapters.style.backgroundColor = "rgb(66, 84, 88)";
+      setTimeout(() => { chapters.style.backgroundColor = "rgb(40, 54, 58)" }, 500);
+   }
+
+   function fullscreenChanged() { // fullscreen onChange handler, increases waveform height & adjusts padding/margin
+      if (!audioContainer.classList.contains("fullscreen")) {
+         audioContainer.classList.add("fullscreen");
+         wavesurfer.setHeight(175);
+      } else  {
+         audioContainer.classList.remove("fullscreen");
+         wavesurfer.setHeight(128);
+      }
+   }
+
+   function toggleFullscreen() { // toggles fullscreen mode of audio player/editor
+      if ((document.fullscreenElement && document.fullscreenElement !== null) ||
+        (document.webkitFullscreenElement && document.webkitFullscreenElement !== null) ||
+        (document.mozFullScreenElement && document.mozFullScreenElement !== null) ||
+        (document.msFullscreenElement && document.msFullscreenElement !== null)) {
+         document.exitFullscreen();
+      } else {
+         audioContainer.requestFullscreen();
+      }
+   }
 }
 
 function formatAudioDuration(duration) {
-   console.log(duration);
-   var [hrs, mins, secs, ms] = duration.replace(".", ":").split(":");
+   // console.log('duration: ' + duration);
+   let [hrs, mins, secs, ms] = duration.replace(".", ":").split(":");
    return hrs + ":" + mins + ":" + secs;
 }
Index: /main/trunk/greenstone3/web/interfaces/default/style/core.css
===================================================================
--- /main/trunk/greenstone3/web/interfaces/default/style/core.css	(revision 37030)
+++ /main/trunk/greenstone3/web/interfaces/default/style/core.css	(revision 37031)
@@ -1392,6 +1392,16 @@
 #audioContainer {
     width: 100%;
-    /* background-color: rgb(24, 36, 39); */
-	scrollbar-color: white transparent;
+		overflow: hidden;
+		font-family: 'Courier New', monospace;
+}
+
+#audioContainer::backdrop {
+	background-color: rgb(245, 243, 229);
+}
+
+.fullscreen {
+	padding-top: 20vh;
+	padding-left: 5vw;
+	padding-right: 5vw;
 }
 
@@ -1401,5 +1411,5 @@
 
 #toolbar {
-	background-color: rgb(24, 36, 39);
+	background-color: rgb(20, 30, 32);
     position: relative;
     display: flex;
@@ -1426,9 +1436,9 @@
 
 #audioContainer img:hover {
-    filter: invert(0.6);
+    filter: invert(0.5);
 }
 
 #audioContainer img:active {
-    filter: invert(0.5);
+    filter: invert(0.4);
 }
 
@@ -1438,9 +1448,23 @@
     max-height: 30vh;
     font-size: 14px;
-    background-color: rgb(54, 73, 78);
+    background-color: rgb(40, 54, 58);
     color: white;
     overflow-y: scroll;
-    transition: height 0.4s ease;
+		/* transition: background-color 0.4s ease-in-out;
+    transition: height 0.4s ease; */
+		transition: 0.3s ease-in-out;
+		cursor: wait;
+		user-select: none;
 } 
+
+/* .audio-scroll::-webkit-scrollbar {
+    height: 8px;
+    width: 8px;
+    background: rgb(24, 36, 39);
+}
+
+.audio-scroll::-webkit-scrollbar-thumb {
+    background: #66d640;
+} */
 
 .chapter {
@@ -1450,6 +1474,7 @@
     border-top-right-radius: 5px;
     border-top-left-radius: 5px;
-    padding: 0.5em;
+    padding: 0.5rem;
     transition: 0.1s ease;
+		
 }
 
@@ -1459,18 +1484,57 @@
 }
 
+.selected-chapter {
+	background-color: rgba(255, 50, 50, 0.5);
+}
+
+.selected-chapter:hover {
+	background-color: rgba(255, 100, 100, 0.5);
+}
+
 #playPauseButton {
-    padding-left: 1em;
-    padding-right: 1em;
-}
-
-#downloadButton {
-	transform: scale(0.8);
-	padding-right: 0.75em;
-}
-
-.wavesurfer-region {
-    cursor: pointer !important;
-    /* border: 1px solid rgba(255, 255, 255, 0.3) !important; */
-    transition: 0.1s ease;
+    padding-left: 1rem;
+    padding-right: 1rem;
+}
+
+#downloadButton, #editButton, #fullscreenButton {
+	transform: scale(0.75);
+	padding-right: 0.75rem;
+}
+
+#zoomInButton, #zoomOutButton {
+	transform: scale(0.75);
+}
+
+#muteButton {
+	padding-right: 0.25rem;
+}
+
+#volume-slider {
+	position: absolute;
+	display: none;
+	cursor: pointer;
+	z-index: 10;
+	height: 8rem;
+	margin-top: -8rem;
+	padding-left: 1rem; 
+	padding-right: 1rem;
+	margin-left: -1rem;
+	box-shadow: 0 0 15px;
+}
+
+#volume-container {
+	position: relative;
+	display: flex;
+	flex-direction: row;
+}
+
+#volume-container:hover #volume-slider {
+	display: inline;
+}
+
+canvas {
+	/* transition: width 0.5s ease; */
+	/* z-index: 4 !important; */
+	pointer-events: none;
 }
 
@@ -1479,27 +1543,24 @@
 }
 
-#slider {
-    width: 10em;
-    margin-left: 0.5em;
-    margin-right: 0.5em;
+#zoom-slider {
+    width: 10rem;
+    margin-left: 0.5rem;
+    margin-right: 0.5rem;
 	cursor: pointer;
 }
 
-#zoomIcon {
-    width: 1.2em !important;
-}
-
 #chapterButton {
-	padding-left: 0.2em;
-	padding-right: 0.75em;
+	padding-left: 0.2rem;
+	padding-right: 0.75rem;
 }
 
 #wave-timeline {
-    background-color: rgb(54, 73, 78);
+    background-color: rgb(40, 54, 58);
+    /* background-color: rgb(24, 36, 39); */
 }
 
 #waveform-loader {
 	color: white;
-	padding: 0.2em;
+	padding: 0.2rem;
 }
 
@@ -1535,9 +1596,9 @@
     flex-wrap: wrap;
     justify-content: space-evenly;
-    padding: 0.5em;
+    padding: 0.5rem;
 }
 
 .word {
-    margin-right: 0.5em;
+    margin-right: 0.5rem;
     cursor: pointer;
 }
@@ -1555,9 +1616,9 @@
 
 #tapeDetails td {
-	padding: 0.2em;
+	padding: 0.2rem;
 }
 
 #tapeDetails td:nth-child(1) {
-	padding-right: 6em;
+	padding-right: 6rem;
 }
 
@@ -1578,4 +1639,219 @@
 .metadataTable td {
     padding: 2px;
+<<<<<<< .mine
+}
+
+/* edit functionality */
+#edit-panel {
+	width: 50%;
+	height: 0px;
+	position: relative;
+	right: 0;
+	max-height: 30vh;
+	font-size: 15px;
+	background-color: rgb(40, 54, 58);
+	color: white;
+	overflow-y: auto;
+	transition: height 0.4s ease;
+	box-sizing: border-box; /* ensures padding doesn't modify width */
+	font-family: 'Courier New', monospace;
+	font-size: 0.85rem;
+	border-left: 1px solid rgb(24, 36, 39);
+
+	display: flex;
+	flex-direction: column;
+	flex-wrap: nowrap;
+	justify-content: space-between;
+}
+
+#edit-panel button {
+	padding: 0.5rem;
+	/* border: 1px solid white; */
+	cursor: pointer;
+	background-color: #F8C537;
+	transition: 0.1s ease-in-out;
+	font-size: 15px;
+	font-family: 'Courier New', monospace;
+}
+
+#edit-panel h3 {
+	font-size: 1.2rem;
+	margin: 0;
+}
+
+#edit-panel button:hover {
+	background-color: #af8b26;
+}
+
+.flex-row {
+	display: flex;
+	flex-direction: row;
+	flex-wrap: nowrap;
+}
+
+#edit-panel input[type='text'] {
+	width: 50%;
+  /* border: 1px solid white; */
+  outline: none;
+	font-size: 15px;
+	margin-right: 0.5rem;
+	margin-bottom: 0.25rem;
+	transition: 0.25s ease-in;
+	background-color: white;
+}
+
+#edit-panel input::placeholder {
+	color: black;
+	opacity: 0.75;
+}
+
+#edit-panel input[type='checkbox'] {
+	transform: scale(1.25);
+	margin-right: 0.5rem;
+	margin-top: -0.25rem;
+}
+
+#edit-panel input[type='number'],
+.time-label {
+	border: none;
+  text-align: center;
+  width: 1.5rem;
+  padding: 0;
+	margin: 0 !important;
+	font-size: 15px;
+}
+
+.time-label {
+	background-color: transparent !important;
+	font-family: 'Courier New', Courier, monospace !important;
+	font-size: 14px !important;
+}
+
+.time-label-container {
+	width: 20%;
+	background-color: transparent;
+  display: inline-flex;
+}
+
+#edit-panel input[type='number']:focus {
+  border: none;
+  outline: none;
+}
+
+#change-all-label {
+	/* width: 100%; */
+}
+
+#save-discard {
+	/* margin-bottom: 1rem; */
+	justify-content: space-between;
+}
+
+#audio-dropdowns {
+	width: 100%;
+	display: flex;
+	flex-direction: row;
+	flex-wrap: nowrap;
+	justify-content: space-between;
+}
+
+#hover-speaker {
+	height: 2rem;
+	overflow: hidden;
+	font-family: 'Courier New', monospace;
+}
+
+#hover-speaker-canvas {
+	position: absolute;
+	overflow: hidden;
+}
+
+#region-details {
+	
+}
+
+.time-picker {
+	background-color: white;
+  display: inline-flex;
+  border: 1px solid #ccc;
+  color: #555;
+	margin-bottom: 0.25rem;
+}
+
+.no-arrows {
+  -moz-appearance: textfield;
+	-webkit-appearance: textfield;
+	appearance: textfield;
+}
+
+/* input::-webkit-outer-spin-button,
+input::-webkit-inner-spin-button {
+	opacity: 0.5;
+	pointer-events: none;
+} */
+ 
+.seconds {
+  width: 3.25rem !important;
+}
+
+.disabled {
+	/* background-color: #888 !important; */
+	opacity: 0.5;
+	filter: grayscale(100%);
+	-webkit-filter: grayscale(100%);
+	cursor: not-allowed !important;
+}
+
+.wavesurfer-region {
+    cursor: pointer !important;
+    transition: background-color 0.1s ease;
+		z-index: 1 !important;
+		height: 70% !important;
+		top: 15% !important;
+		/* transition: width 2s ease; */
+}
+
+.wavesurfer-handle {
+	/* background-color: rgb(255, 0, 0) !important; */
+	/* z-index: 20 !important; */
+	width: 3px !important;
+}
+
+.region-top {
+  height: 30% !important;
+  top: 10% !important;
+}
+
+.region-bottom {
+	height: 30% !important;
+	top: 60% !important;
+}
+
+#caret-container {
+	position: absolute;
+	/* background-color: #090; */
+	height: 128px; /* match waveform */
+	width: 20px; /* match gs_content padding */
+	flex-direction: column;
+	justify-content: space-around;
+	left: -4px; /* padding */
+	cursor: pointer;
+	display: none;
+}
+
+#caret-container img {
+	filter: none;
+	opacity: 0.85;
+}
+
+#caret-container img:hover {
+	filter: none;
+	opacity: 1;
+}
+
+#selected-header {
+	justify-content: space-between;
+	padding-top: 0.5rem;
+	padding-bottom: 0.5rem;
 }
 
@@ -1583,2 +1859,3 @@
     color: black;
 }
+
