Merge client from www into release.go.playground/client.
diff --git a/client/content/playgrounds/code/fortune/ex0-go/src/client/client.go b/client/content/playgrounds/code/fortune/ex0-go/src/client/client.go
new file mode 100644
index 0000000..140e270
--- /dev/null
+++ b/client/content/playgrounds/code/fortune/ex0-go/src/client/client.go
@@ -0,0 +1,34 @@
+// index=3
+package main
+
+import (
+	"fmt"
+	"time"
+
+	_ "v.io/core/veyron/profiles"
+	"v.io/core/veyron2"
+
+	"fortune"
+)
+
+func main() {
+	// Initialize Vanadium.
+	ctx, shutdown := veyron2.Init()
+	defer shutdown()
+
+	// Create a new stub that binds to address without
+	// using the name service.
+	stub := fortune.FortuneClient("fortune")
+
+	// Issue a Get() RPC.
+	// We do this in a loop to give the server time to start up.
+	var fortune string
+	for {
+		var err error
+		if fortune, err = stub.Get(ctx); err == nil {
+			break
+		}
+		time.Sleep(100 * time.Millisecond)
+	}
+	fmt.Println(fortune)
+}
diff --git a/client/content/playgrounds/code/fortune/ex0-go/src/fortune/fortune.vdl b/client/content/playgrounds/code/fortune/ex0-go/src/fortune/fortune.vdl
new file mode 100644
index 0000000..a823cab
--- /dev/null
+++ b/client/content/playgrounds/code/fortune/ex0-go/src/fortune/fortune.vdl
@@ -0,0 +1,10 @@
+// index=3
+package fortune
+
+type Fortune interface {
+	// Returns a random fortune.
+	Get() (Fortune string | error)
+
+	// Adds a fortune to the set used by Get().
+	Add(Fortune string) error
+}
diff --git a/client/content/playgrounds/code/fortune/ex0-go/src/server/server.go b/client/content/playgrounds/code/fortune/ex0-go/src/server/server.go
new file mode 100644
index 0000000..3b0966e
--- /dev/null
+++ b/client/content/playgrounds/code/fortune/ex0-go/src/server/server.go
@@ -0,0 +1,78 @@
+// index=1
+package main
+
+import (
+	"fmt"
+	"math/rand"
+
+	"v.io/core/veyron/lib/signals"
+	"v.io/core/veyron/profiles"
+	vflag "v.io/core/veyron/security/flag"
+	"v.io/core/veyron2"
+	"v.io/core/veyron2/ipc"
+
+	"fortune"
+)
+
+// The Fortuned implementation.
+type fortuned struct {
+	// The set of all fortunes.
+	fortunes []string
+
+	// Used to pick a random index in 'fortunes'.
+	random *rand.Rand
+}
+
+// Initialize server state.
+func newFortuned() *fortuned {
+	return &fortuned{
+		fortunes: []string{
+			"You will reach the height of success in whatever you do.",
+			"You have remarkable power which you are not using.",
+			"Everything will now come your way.",
+		},
+		random: rand.New(rand.NewSource(99)),
+	}
+}
+
+// Methods that get called by RPC requests.
+func (f *fortuned) Get(_ ipc.ServerContext) (Fortune string, err error) {
+	return f.fortunes[f.random.Intn(len(f.fortunes))], nil
+}
+
+func (f *fortuned) Add(_ ipc.ServerContext, Fortune string) error {
+	f.fortunes = append(f.fortunes, Fortune)
+	return nil
+}
+
+// Main - Set everything up.
+func main() {
+	// Initialize Vanadium.
+	ctx, shutdown := veyron2.Init()
+	defer shutdown()
+	log := veyron2.GetLogger(ctx)
+
+	// Create a new instance of the runtime's server functionality.
+	server, err := veyron2.NewServer(ctx)
+	if err != nil {
+		log.Panic("failure creating server: ", err)
+	}
+
+	// Create the fortune server stub.
+	fortuneServer := fortune.FortuneServer(newFortuned())
+
+	// Create an endpoint and begin listening.
+	if endpoint, err := server.Listen(profiles.LocalListenSpec); err == nil {
+		fmt.Printf("Listening at: %v\n", endpoint)
+	} else {
+		log.Panic("error listening to service: ", err)
+	}
+
+	// Start the fortune server at "fortune".
+	if err := server.Serve("fortune", fortuneServer, vflag.NewAuthorizerOrDie()); err != nil {
+		log.Panic("error serving service: ", err)
+	}
+
+	// Wait forever.
+	<-signals.ShutdownOnSignals(ctx)
+}
diff --git a/client/content/playgrounds/code/fortune/ex0-js/client.js b/client/content/playgrounds/code/fortune/ex0-js/client.js
new file mode 100644
index 0000000..7b61c14
--- /dev/null
+++ b/client/content/playgrounds/code/fortune/ex0-js/client.js
@@ -0,0 +1,41 @@
+var veyron = require('veyron');
+var context = veyron.context;
+
+/**
+ * Create a Vanadium runtime using the configuration defined in config.js,
+ * and bind it to the bakery/cookie/fortune service.
+ */
+veyron.init(function(err, rt){
+  if (err) { return error(err); }
+
+  var ctx = new context.Context();
+
+  retryBindTo(ctx, rt, function(err, fortuneService) {
+    if (err) { return error(err); }
+
+    fortuneService.getRandomFortune(ctx, function(err, fortune) {
+      if (err) { return error(err); }
+
+      console.log(fortune);
+      process.exit(0);
+    });
+  });
+});
+
+function retryBindTo(ctx, rt, cb) {
+  rt.bindTo(ctx, 'bakery/cookie/fortune', function(err, fortuneService) {
+    if (err) {
+      // Try again in 100ms
+      return setTimeout(function(){
+        retryBindTo(ctx, rt, cb);
+      }, 100);
+    }
+
+    cb(null, fortuneService);
+  });
+}
+
+function error(err) {
+  console.error(err);
+  process.exit(1);
+}
diff --git a/client/content/playgrounds/code/fortune/ex0-js/server.js b/client/content/playgrounds/code/fortune/ex0-js/server.js
new file mode 100644
index 0000000..f3ab264
--- /dev/null
+++ b/client/content/playgrounds/code/fortune/ex0-js/server.js
@@ -0,0 +1,61 @@
+var veyron = require('veyron');
+
+/**
+ * 1) Implement a simple fortune service
+ */
+
+var fortuneService = {
+  // List of fortunes
+  fortunes: [],
+
+  numFortunesServed: 0,
+
+  // Gets a random fortune
+  getRandomFortune: function(ctx) {
+    var numExistingfortunes = this.fortunes.length;
+    if(numExistingfortunes === 0) {
+      throw new Error('Sorry! No fortune available :(');
+    }
+    var randomIndex = Math.floor(Math.random() * numExistingfortunes);
+    var fortune = this.fortunes[randomIndex];
+    this.numFortunesServed++;
+    console.info('Serving:', fortune);
+    return fortune;
+  },
+
+  // Adds a new fortune
+  addNewFortune: function(ctx, fortune) {
+    if(!fortune || fortune.trim() === '') {
+      throw new Error('Sorry! Can\'t add empty or null fortune!');
+    }
+    console.info('Adding:', fortune);
+    this.fortunes.push(fortune);
+  }
+};
+
+/**
+ * 2) Publish the fortune service
+ */
+
+// Create a Vanadium runtime using the configuration
+veyron.init().then(function(rt){
+  // Serve the fortune server under a name. Serve returns a Promise object
+  rt.serve('bakery/cookie/fortune', fortuneService).then(function() {
+    console.log('Fortune server serving under: bakery/cookie/fortune \n');
+  }).catch(function(err) {
+    console.error('Failed to serve the fortune server because: \n', err);
+    process.exit(1);
+  });
+}).catch(function(err) {
+  console.error('Failed to start the fortune server because:', err);
+  process.exit(1);
+});
+
+// Let's add a few fortunes to start with
+[
+  'The fortune you seek is in another cookie.',
+  'Everything will now come your way.',
+  'Conquer your fears or they will conquer you.'
+].forEach(function(fortune) {
+  fortuneService.addNewFortune(null, fortune);
+});
diff --git a/client/content/playgrounds/fortune.md b/client/content/playgrounds/fortune.md
new file mode 100644
index 0000000..1b13cd1
--- /dev/null
+++ b/client/content/playgrounds/fortune.md
@@ -0,0 +1,78 @@
+= yaml =
+title: Playground example - Fortune
+status: draft
+sort: 05
+= yaml =
+
+<span style="background-color:red">
+TODO(jregan): plan is to insert one or more of these as appropriate in tutorials
+</span>
+
+(Taken from: https://docs.google.com/a/google.com/document/d/189JXetSjHc980LuSl88Y7_VtG4tyZ0iggILEWD2_JKc/edit#)
+
+This is an example of a simple Fortune application in Vanadium.  It has two
+parts, a client and a server.  The client can either request a fortune from the
+server, or add a new fortune to the server.
+
+
+## Interface Definition
+
+The remote interface exposed by the server is defined in a .vdl file.  Here is
+an example Fortune service:
+
+    package fortune
+
+    type Fortune interface {
+      // Returns a random fortune.
+      Get() (Fortune string | error)
+
+      // Adds a fortune to the set used by Get().
+      Add(Fortune string) error
+    }
+
+The services exposes two methods - `Get` which returns a random fortune string
+from a fortune repository held by the service, and `Add` that adds the provided
+fortune string to the repository.
+
+
+## Implementation
+
+### Server
+
+<div class="lang-go">
+The server implements the `Get` and `Add` methods defined in the vdl interface.
+
+Inside the `main()` function, we create a new Vanadium runtime with
+`r := rt.New()`, and use the runtime to create a new server with
+`r.NewServer()`.
+
+We use the `fortune` package generated from the vdl along with the fortuned
+implementation to create a new fortune server.
+
+The server listens for a tcp connection on localhost, and mounts itself on the
+mounttable with the name "fortune".
+</div>
+
+<!--
+<span class="lang-js">TODO(nlacasse): describe the js server</span>
+-->
+
+### Client
+
+<div class="lang-go">
+The client binds to the fortune server with a `.FortuneClient("fortune")` call,
+and then issues a `Get` request.  We do the `.Get` request in a loop to give
+the server a chance to start up.
+</div>
+
+<!--
+<span class="lang-js">TODO(nlacasse): describe the js client</span>
+-->
+
+### Code
+
+<div class="lang-go playground" data-srcdir="/fortune/ex0-go"></div>
+
+<!--
+<div class="lang-js playground" data-srcdir="/fortune/ex0-js"></div>
+-->
diff --git a/client/playground/client/editor.js b/client/playground/client/editor.js
new file mode 100644
index 0000000..7918791
--- /dev/null
+++ b/client/playground/client/editor.js
@@ -0,0 +1,80 @@
+module.exports = Editor;
+
+// NOTE(sadovsky): We considered both Ace and CodeMirror, but CodeMirror appears
+// to be incompatible with Bootstrap and has other weird issues, e.g. its
+// injected editor divs can obscure other DOM elements.
+
+var ace = require('brace');
+require('brace/mode/javascript');
+require('brace/mode/golang');
+require('brace/theme/monokai');
+
+// Mercury widget that wraps a code editor.
+// * type: Type of code. Currently either 'js' or 'go'.
+// * text: Initial code.
+function Editor(type, text) {
+  this.type_ = type;
+  this.text_ = text;
+  this.aceEditor_ = null;
+}
+
+// This tells Mercury to treat Editor as a widget and not try to render its
+// internals.
+Editor.prototype.type = 'Widget';
+
+Editor.prototype.init = function() {
+  console.log('EditorWidget.init');
+  var el = document.createElement('div');
+  this.mount(el, this.type_, this.text_);
+  return el;
+};
+
+Editor.prototype.update = function() {
+  console.log('EditorWidget.update');
+};
+
+Editor.prototype.getText = function() {
+  return this.aceEditor_.getValue();
+};
+
+Editor.prototype.reset = function() {
+  // The '-1' argument puts the cursor at the document start.
+  this.aceEditor_.setValue(this.text_, -1);
+};
+
+// Creates a new Ace editor instance and mounts it on a DOM node.
+// * el: The DOM node to mount on.
+// * type: Type of code. Currently either 'js' or 'go'.
+// * text: Initial code.
+Editor.prototype.mount = function(el, type, text) {
+  var editor = this.aceEditor_ = ace.edit(el);
+  editor.setTheme('ace/theme/monokai');
+  // https://github.com/ajaxorg/ace/wiki/Configuring-Ace
+  editor.setOptions({
+    fontFamily: 'Source Code Pro',
+    fontSize: '13px'
+  });
+
+  var session = editor.getSession();
+  switch (type) {
+  case 'go':
+    session.setMode('ace/mode/golang');
+    break;
+  case 'vdl':
+    session.setMode('ace/mode/golang');
+    break;
+  case 'json':
+    session.setMode('ace/mode/javascript');
+    break;
+  case 'js':
+    session.setMode('ace/mode/javascript');
+    break;
+  default:
+    throw new Error('Language type not supported: ' + type);
+  }
+
+  session.setValue(text);
+
+  // Disable syntax checking. The UI is annoying and only works for JS anyways.
+  session.setOption('useWorker', false);
+};
diff --git a/client/playground/client/embedded.js b/client/playground/client/embedded.js
new file mode 100644
index 0000000..168bdbd
--- /dev/null
+++ b/client/playground/client/embedded.js
@@ -0,0 +1,294 @@
+module.exports = EmbeddedPlayground;
+
+var _ = require('lodash');
+var http = require('http');
+var mercury = require('mercury');
+var moment = require('moment');
+var path = require('path');
+var url = require('url');
+
+var Editor = require('./editor');
+
+var m = mercury;
+var h = mercury.h;
+
+// Shows each file in a tab.
+// * el: The DOM element to mount on.
+// * id: Identifier for this playground instance, used in debug messages.
+// * files: List of {name, body}, as written by bundler.
+function EmbeddedPlayground(el, id, files) {
+  this.id_ = id;
+  this.files_ = _.map(files, function(file) {
+    return _.assign({}, file, {
+      basename: path.basename(file.name),
+      type: path.extname(file.name).substr(1)
+    });
+  });
+  this.editors_ = _.map(this.files_, function(file) {
+    return new Editor(file.type, file.body);
+  });
+  this.state_ = m.struct({
+    activeTab: m.value(0),
+    nextRunId: m.value(0),
+    running: m.value(false),
+    hasRun: m.value(false),
+    consoleEvents: m.value([])
+  });
+  mercury.app(el, this.state_, this.render_.bind(this));
+}
+
+EmbeddedPlayground.prototype.renderTopBar_ = function(state) {
+  var that = this;
+
+  var tabs = _.map(this.files_, function(file, i) {
+    var selector = 'div.tab';
+    if (i === state.activeTab) {
+      selector += '.active';
+    }
+    return h(selector, {
+      'ev-click': function() {
+        that.state_.activeTab.set(i);
+      }
+    }, file.basename);
+  });
+
+  var runBtn = h('button.btn', {
+    'ev-click': that.run.bind(that)
+  }, 'Run');
+  var resetBtn = h('button.btn', {
+    'ev-click': that.reset.bind(that)
+  }, 'Reset');
+
+  return h('div.top-bar', [h('div', tabs), h('div.btns', [runBtn, resetBtn])]);
+};
+
+EmbeddedPlayground.prototype.renderEditors_ = function(state) {
+  var editors = _.map(this.editors_, function(editor, i) {
+    var properties = {};
+    if (i !== state.activeTab) {
+      // Use "visibility: hidden" rather than "display: none" because the latter
+      // causes the editor to initialize lazily and thus flicker when it's first
+      // opened.
+      properties['style'] = {visibility: 'hidden'};
+    }
+    return h('div.editor', properties, editor);
+  });
+
+  return h('div.editors', editors);
+};
+
+function renderConsoleEvent(event) {
+  var children = [];
+  if (event.Timestamp) {
+    // Convert UTC to local time.
+    var t = moment(event.Timestamp / 1e6);
+    children.push(h('span.timestamp', t.format('H:mm:ss.SSS') + ' '));
+  }
+  if (event.File) {
+    children.push(h('span.filename', path.basename(event.File) + ': '));
+  }
+  // A single trailing newline is always ignored.
+  // Ignoring the last character, check if there are any newlines in message.
+  if (event.Message.slice(0, -1).indexOf('\n') !== -1) {
+    // Multiline messages are marked with U+23CE and started in a new line.
+    children.push('\u23ce'/* U+23CE RETURN SYMBOL */, h('br'));
+  }
+  children.push(h('span.message.' + (event.Stream || 'unknown'),
+                  event.Message));
+  return h('div', children);
+}
+
+EmbeddedPlayground.prototype.renderConsole_ = function(state) {
+  if (state.hasRun) {
+    return h('div.console.open', [
+      h('div.text', _.map(state.consoleEvents, renderConsoleEvent))
+    ]);
+  }
+  return h('div.console');
+};
+
+EmbeddedPlayground.prototype.render_ = function(state) {
+  return h('div.pg', [
+    this.renderTopBar_(state),
+    this.renderEditors_(state),
+    this.renderConsole_(state)
+  ]);
+};
+
+// Sends the files to the backend, then injects the response in the console.
+EmbeddedPlayground.prototype.run = function() {
+  if (this.state_.running()) {
+    console.log('Already running', this.id_);
+    return;
+  }
+  var runId = this.state_.nextRunId();
+
+  // TODO(sadovsky): Visually disable the "Run" button or change it to a "Stop"
+  // button.
+  this.state_.running.set(true);
+  this.state_.hasRun.set(true);
+  this.state_.consoleEvents.set([{Message: 'Running...'}]);
+
+  var myUrl = url.parse(window.location.href, true);
+  var pgaddr = myUrl.query.pgaddr;
+  if (pgaddr) {
+    console.log('Using pgaddr', pgaddr);
+  } else {
+    pgaddr = 'https://staging.v.io/playground';
+  }
+  var compileUrl = pgaddr + '/compile';
+  if (myUrl.query.debug === '1') {
+    compileUrl += '?debug=1';
+  }
+
+  var editors = this.editors_;
+  var reqData = {
+    files: _.map(this.files_, function(file, i) {
+      var editor = editors[i];
+      return {
+        Name: file.name,
+        Body: editor.getText()
+      };
+    }),
+    Identities: []
+  };
+
+  // TODO(sadovsky): To deal with cached responses, shift timestamps (based on
+  // current time) and introduce a fake delay. Also, switch to streaming
+  // messages, for usability.
+  var that = this, state = this.state_;
+
+  // If the user stops the current run or resets the playground, functions
+  // wrapped with ifRunActive become no-ops.
+  var ifRunActive = function(cb) {
+    return function() {
+      if (runId === state.nextRunId()) {
+        cb.apply(this, arguments);
+      }
+    };
+  };
+
+  var appendToConsole = function(events) {
+    state.consoleEvents.set(state.consoleEvents().concat(events));
+  };
+  var makeEvent = function(stream, message) {
+    return {Stream: stream, Message: message};
+  };
+
+  var urlp = url.parse(compileUrl);
+
+  var options = {
+    method: 'POST',
+    protocol: urlp.protocol,
+    hostname: urlp.hostname,
+    port: urlp.port || (urlp.protocol === 'https:' ? '443' : '80'),
+    path: urlp.path,
+    // TODO(ivanpi): Change once deployed.
+    withCredentials: false,
+    headers: {
+      'accept': 'application/json',
+      'content-type': 'application/json'
+    }
+  };
+
+  var req = http.request(options);
+
+  var watchdog = null;
+  // The heartbeat function clears the existing timeout (if any) and, if the run
+  // is still active, starts a new timeout.
+  var heartbeat = function() {
+    if (watchdog !== null) {
+      clearTimeout(watchdog);
+    }
+    watchdog = null;
+    ifRunActive(function() {
+      // TODO(ivanpi): Reduce timeout duration when server heartbeat is added.
+      watchdog = setTimeout(function() {
+        process.nextTick(ifRunActive(function() {
+          req.destroy();
+          appendToConsole(makeEvent('syserr', 'Server response timed out.'));
+        }));
+      }, 10500);
+    })();
+  };
+
+  var endRunIfActive = ifRunActive(function() {
+    that.endRun_();
+    // Cleanup watchdog timer.
+    heartbeat();
+  });
+
+  // error and close callbacks call endRunIfActive in the next tick to ensure
+  // that if both events are triggered, both are executed before the run is
+  // ended by either.
+  req.on('error', ifRunActive(function(err) {
+    console.error('Connection error: ' + err.message + '\n' + err.stack);
+    appendToConsole(makeEvent('syserr', 'Error connecting to server.'));
+    process.nextTick(endRunIfActive);
+  }));
+
+  // Holds partial prefix of next response line.
+  var partialLine = '';
+
+  req.on('response', ifRunActive(function(res) {
+    heartbeat();
+    if (res.statusCode !== 0 && res.statusCode !== 200) {
+      appendToConsole(makeEvent('syserr', 'HTTP status ' + res.statusCode));
+    }
+    res.on('data', ifRunActive(function(chunk) {
+      heartbeat();
+      // Each complete line is one JSON Event.
+      var eventsJson = (partialLine + chunk).split('\n');
+      partialLine = eventsJson.pop();
+      var events = [];
+      _.forEach(eventsJson, function(line) {
+        // Ignore empty lines.
+        line = line.trim();
+        if (line) {
+          var ev;
+          try {
+            ev = JSON.parse(line);
+          } catch (err) {
+            console.error('Error parsing line: ' + line + '\n' + err.message);
+            events.push(makeEvent('syserr', 'Error parsing server response.'));
+            endRunIfActive();
+            return false;
+          }
+          events.push(ev);
+        }
+      });
+      appendToConsole(events);
+    }));
+  }));
+
+  req.on('close', ifRunActive(function() {
+    // Sanity check: partialLine should be empty when connection is closed.
+    partialLine = partialLine.trim();
+    if (partialLine) {
+      console.error('Connection closed without newline after: ' + partialLine);
+      appendToConsole(makeEvent('syserr', 'Error parsing server response.'));
+    }
+    process.nextTick(endRunIfActive);
+  }));
+
+  req.write(JSON.stringify(reqData));
+  req.end();
+
+  // Start watchdog.
+  heartbeat();
+};
+
+// Clears the console and resets all editors to their original contents.
+EmbeddedPlayground.prototype.reset = function() {
+  this.state_.consoleEvents.set([]);
+  _.forEach(this.editors_, function(editor) {
+    editor.reset();
+  });
+  this.endRun_();
+  this.state_.hasRun.set(false);
+};
+
+EmbeddedPlayground.prototype.endRun_ = function() {
+  this.state_.nextRunId.set(this.state_.nextRunId() + 1);
+  this.state_.running.set(false);
+};
diff --git a/client/playground/client/index.js b/client/playground/client/index.js
new file mode 100644
index 0000000..3a7562f
--- /dev/null
+++ b/client/playground/client/index.js
@@ -0,0 +1,36 @@
+var _ = require('lodash');
+var path = require('path');
+var request = require('superagent');
+
+var EmbeddedPlayground = require('./embedded');
+
+_.forEach(document.querySelectorAll('.playground'), function(el) {
+  var srcdir = el.getAttribute('data-srcdir');
+  console.log('Creating playground', srcdir);
+
+  fetchBundle(srcdir, function(err, bundle) {
+    if (err) {
+      el.innerHTML = '<div class="error"><p>Playground error.' +
+        '<br>Bundle not found: <strong>' + srcdir + '</strong></p></div>';
+      return;
+    }
+    new EmbeddedPlayground(el, srcdir, bundle.files);  // jshint ignore:line
+  });
+});
+
+function fetchBundle(loc, cb) {
+  var basePath = '/playgrounds/code/';
+  console.log('Fetching bundle', loc);
+  request
+    .get(path.join(basePath, loc, 'bundle.json'))
+    .accept('json')
+    .end(function(err, res) {
+      if (err) {
+        return cb(err);
+      }
+      if (res.error) {
+        return cb(res.error);
+      }
+      cb(null, res.body);
+    });
+}
diff --git a/client/stylesheets/playground.css b/client/stylesheets/playground.css
new file mode 100644
index 0000000..eacc394
--- /dev/null
+++ b/client/stylesheets/playground.css
@@ -0,0 +1,132 @@
+/* CSS rules for elements inside EmbeddedPlayground instances. */
+
+.pg {
+  width: 100%;
+}
+
+.pg .top-bar {
+  position: relative;
+}
+
+.pg .btn,
+.pg .tab {
+  cursor: pointer;
+  display: inline-block;
+  font-size: 14px;
+  font-weight: normal;
+  padding: 6px 12px;
+  user-select: none;
+}
+
+.pg .tab {
+  background-color: #bbb;
+  margin-right: 2px;
+}
+
+.pg .tab.active {
+  background-color: #ddd;
+}
+
+.pg .btns {
+  position: absolute;
+  top: 0;
+  right: 0;
+}
+
+.pg .btn {
+  background-color: #0277bd;
+  color: #fff;
+  border: 0;
+  margin-left: 2px;
+}
+
+/* TODO(sadovsky): Add .no-touch once we integrate Modernizr. */
+.pg .btn:hover {
+  background-color: #01579b;
+}
+
+.pg .btn:focus,
+.pg .btn:active {
+  outline: 0;
+}
+
+.pg .btn:active {
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+  box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+}
+
+/* TODO(sadovsky): Make this reactive. */
+.pg .editors {
+  position: relative;
+  border-top: 4px solid #ddd;
+  height: 200px;
+}
+
+.pg .editor {
+  position: absolute;  /* stack editors one on top of the other */
+  width: 100%;
+  height: 100%;
+}
+
+.pg .ace_editor {
+  width: 100%;
+  height: 100%;
+}
+
+.pg .console {
+  background-color: #ddd;
+  width: 100%;
+  height: 4px;  /* matches .editors border-top; expands on run */
+  transition: height 0.2s;
+}
+
+.pg .console .text {
+  color: #222;
+  font-family: "Source Code Pro", monospace;
+  font-size: 13px;
+  overflow-y: auto;
+  padding: 8px;
+}
+
+.pg .console .text .timestamp {
+  color: #9e9e9e;
+}
+
+.pg .console .text .filename {
+  font-weight: bold;
+}
+
+/* Known bug: Chrome doesn't show tabs; refer to
+   https://code.google.com/p/chromium/issues/detail?id=398274 */
+.pg .console .text .message {
+  white-space: pre-wrap;
+  tab-size: 4;
+}
+
+.pg .console .text .stderr {
+  color: #d01716;
+}
+
+.pg .console .text .svc-stderr {
+  color: #d01716;
+}
+
+.pg .console .text .debug {
+  color: #455ede;
+}
+
+.pg .console .text .syserr {
+  color: #d01716;
+  font-weight: bold;
+}
+
+.pg .console.open {
+  height: 200px;
+  overflow: auto;
+}
+
+.clearfix {
+  display: table;
+  clear: both;
+  content: "";
+}
diff --git a/client/test/playground_test.sh b/client/test/playground_test.sh
new file mode 100755
index 0000000..33aebe9
--- /dev/null
+++ b/client/test/playground_test.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# Tests that all embedded playgrounds execute successfully.
+
+# To debug playground compile errors you can build examples locally, e.g.:
+# $ cd content/playgrounds/code/fortune/ex0-go/src
+# $ GOPATH=$(dirname $(pwd)) VDLPATH=$(dirname $(pwd)) v23 go install ./...
+
+source "$(go list -f {{.Dir}} v.io/core/shell/lib)/shell_test.sh"
+source "$(go list -f {{.Dir}} v.io/playground)/lib/pg_test_util.sh"
+
+main() {
+  local -r WWWDIR="$(pwd)"
+  cd "${shell_test_WORK_DIR}"
+
+  setup_environment
+
+  build_go_binaries
+  install_vanadium_js
+  install_pgbundle
+
+  local -r EXAMPLE_DIRS=$(find "${WWWDIR}/content/playgrounds/code" -maxdepth 2 -mindepth 2)
+  for d in $EXAMPLE_DIRS; do
+    echo -e "\n\n>>>>> Test ${d}\n\n"
+    test_pg_example "${d}" "-v=false" || shell_test::fail "${d}: failed to run"
+    # TODO(sadovsky): Make this "clean exit" check more robust.
+    grep -q "\"Exited cleanly.\"" builder.out || shell_test::fail "${d}: did not exit cleanly"
+    rm -f builder.out
+  done
+
+  shell_test::pass
+}
+
+main "$@"