diff --git a/content.js b/content.js
index 0b8bdf43e94beb7871e63d0ac83ed65ac2fb122a..df1c082573311d8eba7b745f31ec6751fd5e0bd1 100644
--- a/content.js
+++ b/content.js
@@ -277,28 +277,17 @@ let Content = {
   note_array: {
     instructions: "Define <code>notes</code> to be an array that contains 7 \"note arrays\" that are pairs of note pitch and it's length, e.g. <code>[\"C5\",\"8n\"]</code>. The array must first have four sixteenth (16n) notes A4, B4, C#4, A4. Then, TWICE the eighth (8n) note E5. And finally, the half (2n) note C#4.",
     initialJs: 'let notes;',
-    postExecuteJs: ';\n'
-    + 'let notesTimed = [];'
-    + 'display.cmd([0,1,2,3,4,5,6].map(i => { return "notes[" + i + "]"; }).join(", "));\n'
-    + 'for (var i = 0; i < 7; i++) {\n'
-    + '  display.res(JSON.stringify((notes || [])[i]), (notes || [])[i]);\n'
-    + '  if (notes && notes[i] && notes[i].length > 1) {\n'
-    + '   notesTimed.push(notes[i]);\n'
-    + '  }\n'
-    + '}\n'
-    + 'document.write("<script src=\\"https://cdnjs.cloudflare.com/ajax/libs/tone/14.5.41/Tone.js\\"></"+"script>");\n'
-    + 'function music() {\n'
-    + '  var synth = new Tone.Synth().toDestination();\n'
-    + '  var pattern = new Tone.Pattern(function(time, note) {\n'
-    + '    synth.triggerAttackRelease(note[0], note[1]);\n'
-    + '  }, notesTimed);\n'
-    + '  pattern.start(0);\n'
-    + '  Tone.Transport.bpm.value = 160;\n'
-    + '  Tone.Transport.start();\n'
-    + '}\n'
-    + 'if (notesTimed.length > 0) {\n'
-    + '  document.write("<button onclick=\\"music();\\">Play music</button>");\n'
-    + '}\n',
+    postExecuteJs: ';'
+      + 'window["sequence"] = [];\n'
+      + 'display.cmd([0,1,2,3,4,5,6].map(i => { return "notes[" + i + "]"; }).join(", "));\n'
+      + 'for (var i = 0; i < 7; i++) {\n'
+      + '  display.res(JSON.stringify((notes || [])[i]), (notes || [])[i]);\n'
+      + '  if (notes && notes[i] && notes[i].length > 1) {\n'
+      + '    window["sequence"].push(notes[i]);\n'
+      + '  }\n'
+      + '}\n',
+    postExecuteHtml: '<button class="tone-play-sequence">Play music</button>\n',
+    postExecuteScript: '/static/webdev/augment-tone.js',
     executeAtStart: false,
     points: function ($element, config, accessor) {
       var correct = [["A4","16n"],["B4","16n"],["C#4","16n"],["A4","16n"],["E5","8n"],["E5","8n"],["C#4","2n"]];
@@ -579,6 +568,46 @@ let Content = {
     order: 14
   },
 
+  adjust_css_classes: {
+    instructions: 'The function named <code>onPlay</code> below is called with the id of a button that was just clicked. '
+      + 'Your task is to set a CSS class <code>last-played</code> to this button that was just clicked. '
+      + 'In addition, you must make it the only button that has this CSS class.',
+    preExecuteHtml: '<div id="keyboard" class="tone-keyboard">\n'
+      + '  <button id="C4">C4</button>\n'
+      + '  <button id="C#4" class="black">C#4</button>\n'
+      + '  <button id="D4">D4</button>\n'
+      + '  <button id="D#4" class="black">D#4</button>\n'
+      + '  <button id="E4">E4</button>\n'
+      + '</div>\n',
+    initialJs: 'function onPlay(id) {\n  console.log(id);\n}\n',
+    postExecuteJs: ';'
+      + 'document.querySelectorAll("#keyboard button").forEach(function (button) {\n'
+      + '  button.addEventListener("click", function (event) {\n'
+      + '    onPlay(event.target.getAttribute("id"));\n'
+      + '    display.showCode("keyboard");\n'
+      + '  });\n'
+      + '});\n'
+      + 'display.showCode("keyboard");\n',
+    postExecuteScript: '/static/webdev/augment-tone.js',
+    executeAtStart: true,
+    points: function ($element, config, accessor) {
+      let doc = accessor.doc();
+      let p = 0;
+      ['C4', 'D4', 'D#4', 'C#4', 'E4'].forEach(function (id) {
+        let b = doc.getElementById(id);
+        b.click();
+        p += b.classList.contains('last-played') ? 1 : 0;
+        p += doc.querySelectorAll('.last-played').length == 1 ? 1 : 0;
+      });
+      return { points: p };
+    },
+    maxPoints: 10,
+    title: "Adjust CSS classes",
+    description: "Set CSS classes for elements based on a given element id.",
+    concepts: ["JavaScript", "getElementById", "childNodes", "classes", "modify"],
+    order: 15
+  },
+
   events: {
     instructions: `Now we want to upgrade our keyboard. We would like to show the
     note names on the buttons only when the mouse pointer enters the button, and remove them
@@ -636,12 +665,11 @@ for(let i = 0; i < buttonsIds.length; i++) {
         return { points: p };
     },
     maxPoints: 10,
-    title: "modify the elements on mouseenter and mouseleave events",
+    title: "Modify the elements on mouseenter and mouseleave events",
     description: "",
     concepts: ["JavaScript", "mouseenter", "mouseleave"],
-    order: 15
+    order: 16
   }
-
 };
 
 module.exports = Content;
diff --git a/static/webdev-editor.css b/static/webdev-editor.css
index cac9d6bfdc4f1144a975861b142571d2bd8f7912..de856ce9b93522b8b903b4fe972ae5061a6f7549 100644
--- a/static/webdev-editor.css
+++ b/static/webdev-editor.css
@@ -40,6 +40,12 @@ body.execute ul#display > li.error {
 body.execute ul#display > li.error::before {
   content: "тип ";
 }
+body.execute textarea.show-code {
+  display: block;
+  width: 98%;
+  margin: 0 auto;
+}
+
 body.execute button {
   border: 1px solid silver;
   border-radius: 0.5em;
@@ -51,3 +57,25 @@ body.execute button {
   font-size: 90%;
   font-weight: bold;
 }
+
+body.execute .tone-keyboard {
+  text-align: center;
+  padding-top: 20px;
+}
+body.execute .tone-keyboard button {
+  width: 50px;
+  height: 120px;
+  padding: 2px;
+  background-color: #F0F0F0;
+}
+body.execute .tone-keyboard button.black {
+  height: 100px;
+  transform: translate(0, -20px);
+  background-color: #333;
+  color: #FFF;
+  border-color: #000;
+}
+body.execute .tone-keyboard button.last-played {
+  border-left-width: 10px;
+  border-top-width: 3px;
+}
diff --git a/static/webdev-editor.js b/static/webdev-editor.js
index ca9ba49120bcc1af7a3a8ef18b48ec169b22ac64..ed1fdbae0b05e2cd20249cea4753212440325ec8 100644
--- a/static/webdev-editor.js
+++ b/static/webdev-editor.js
@@ -71,10 +71,9 @@ ACOSWebdev.prototype.extendGrade = function (eventOrMutations, cb) {
       }, 0);
     }
   };
-  this.editorExecute();
-  setTimeout(function () {
+  this.editorExecute(function () {
     cb(self.config.points(self.$element, self.config, accessor));
-  }, 1000);
+  });
 };
 
 ACOSWebdev.prototype.extendProtocolFeedback = function (feedback) {
@@ -83,7 +82,7 @@ ACOSWebdev.prototype.extendProtocolFeedback = function (feedback) {
   return '<pre><code>' + this.editor.getValue() + '</code></pre><div>' + $out.html() + '</div>';
 };
 
-ACOSWebdev.prototype.editorExecute = function () {
+ACOSWebdev.prototype.editorExecute = function (cb) {
   var $iframe = $('<iframe src="about:blank"></iframe>');
   this.$editorOutput.empty().append($iframe);
   $iframe.get(0).contentWindow.contents = '<!DOCTYPE html>\n'
@@ -92,10 +91,31 @@ ACOSWebdev.prototype.editorExecute = function () {
     + '<link href="/static/webdev-editor/webdev-editor.css" rel="stylesheet">\n'
     + '<script src="/static/webdev-editor/webdev-execute.js"></script>\n'
     + '</head>\n<body class="execute">\n'
+    + (this.config.preExecuteScript ? ('<script src="' + this.config.preExecuteScript + '"></script>\n') : '')
     + (this.config.preExecuteHtml || '')
-    + '<script>' + (this.config.preExecuteJs || '')
+    + '<script>'
+    + 'try {\n'
+    + (this.config.preExecuteJs || '')
     + this.editor.getValue()
-    + (this.config.postExecuteJs || '') + '</script>\n'
+    + (this.config.postExecuteJs || '')
+    + '} catch (error) {\n'
+    + '  display.err(error.message);\n'
+    + '  throw error;\n'
+    + '}\n'
+    + 'window.postMessage({state: "done"}, "*");\n'
+    + 'window.parent.postMessage({state: "done"}, "*");\n'
+    + '</script>\n'
+    + (this.config.postExecuteHtml || '')
+    + (this.config.postExecuteScript ? ('<script src="' + this.config.postExecuteScript + '"></script>\n') : '')
     + '</body>\n</html>\n';
+  function onDone(event) {
+    if (event.data.state == 'done') {
+      window.removeEventListener('message', onDone);
+      var h = $iframe.get(0).contentWindow.document.body.scrollHeight;
+      $iframe.css('height', h + 10 + 'px');
+      cb();
+    }
+  }
+  window.addEventListener('message', onDone);
   $iframe.attr('src', 'javascript:window["contents"]');
 };
diff --git a/static/webdev-execute.js b/static/webdev-execute.js
index 51307aa1e2d44b1a115b3f02006fd7295a8a9c81..03f5a6cce4225751ba659853a1da82a676735e1a 100644
--- a/static/webdev-execute.js
+++ b/static/webdev-execute.js
@@ -1,6 +1,7 @@
 var display = (function () {
 
   var element = undefined;
+  var code = undefined;
 
   function addLine(html, cls, args) {
     if (element === undefined) {
@@ -28,6 +29,21 @@ var display = (function () {
     },
     res: function (html, args) {
       addLine(html, 'res', args);
+    },
+    err: function (html) {
+      addLine(html, 'error text-danger');
+    },
+    showCode: function (id) {
+      let element = document.getElementById(id);
+      if (code === undefined) {
+        code = document.createElement('textarea');
+        code.classList.add('show-code');
+        code.disabled = true;
+        document.body.insertBefore(code, element);
+      }
+      code.style.height = null;
+      code.textContent = element.outerHTML;
+      code.style.height = code.scrollHeight + 3 + 'px';
     }
   };
 })();