WebUI: Import Polymer3 in third_party/polymer.

Specifically:
 - Add new third_party/polymer/v3_0/ folder.
 - Add package.json, package-lock.json and rsync_exclude.txt.
 - Add reproduce.sh script to update the v3 version.
 - Add chromium.patch with some initial local modifications.
 - Update existing scripts polymer_grdp_to_txt.py and
   txt_to_polymer_grdp.py to work for v3 as well.
 - Update shared_resources_data_source.cc to properly map
   chrome://resources/polymer/v3_0/ requests to files.

Note: Given that Polymer 3 usage in WebUI is still at an early
exploration phase, its resources are only included in the build for
optimize_webui=false, to not affect release builds for now, while
allowing making progress on other fronts like
 - type checking
 - bundling
 - HTML-to-JS automation
 - testing JS module based code wit js2gtest infra

Bug: 965770
Test: Visit chrome://resources/polymer/v3_0/paper-button/paper-button.js
Change-Id: I9a6f2c0d9c59d9569bd0f0f4d09ce94a1270cebd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1610718
Commit-Queue: Demetrios Papadopoulos <dpapad@chromium.org>
Reviewed-by: Dan Beam <dbeam@chromium.org>
Cr-Commit-Position: refs/heads/master@{#663383}
diff --git a/content/browser/webui/shared_resources_data_source.cc b/content/browser/webui/shared_resources_data_source.cc
index 3517622..cce9341 100644
--- a/content/browser/webui/shared_resources_data_source.cc
+++ b/content/browser/webui/shared_resources_data_source.cc
@@ -66,6 +66,8 @@
   std::map<std::string, std::string> aliases = {
       {"../../../third_party/polymer/v1_0/components-chromium/",
        "polymer/v1_0/"},
+      {"../../../third_party/polymer/v3_0/components-chromium/",
+       "polymer/v3_0/"},
       {"../../../third_party/web-animations-js/sources/",
        "polymer/v1_0/web-animations-js/"},
       {"../../views/resources/default_100_percent/common/", "images/apps/"},
diff --git a/third_party/polymer/README.chromium b/third_party/polymer/README.chromium
index f6d300f..afbdc26 100644
--- a/third_party/polymer/README.chromium
+++ b/third_party/polymer/README.chromium
@@ -55,3 +55,8 @@
 Be sure to add the .bower.json file to the repository as it includes the
 revision information of the polymer component.
 Also be sure that you listed all the added packages in bower.json.
+
+Polymer 3 notes
+Polymer 3 resides under v3_0/, and uses NPM instead of Bower. To restore the
+contents of the 'components-chromium' directory from scratch, run
+./v3_0/reproduce.sh on a Linux machine.
diff --git a/third_party/polymer/v3_0/chromium.patch b/third_party/polymer/v3_0/chromium.patch
new file mode 100644
index 0000000..1a873a7
--- /dev/null
+++ b/third_party/polymer/v3_0/chromium.patch
@@ -0,0 +1,39 @@
+diff --git a/components-chromium/font-roboto/roboto.js b/components-chromium/font-roboto/roboto.js
+index 02aebd81b0df..342d26d6f797 100644
+--- a/components-chromium/font-roboto/roboto.js
++++ b/components-chromium/font-roboto/roboto.js
+@@ -17,7 +17,6 @@ if (!window.polymerSkipLoadingFontRoboto) {
+   link.rel = 'stylesheet';
+   link.type = 'text/css';
+   link.crossOrigin = 'anonymous';
+-  link.href =
+-      'https://fonts.googleapis.com/css?family=Roboto+Mono:400,700|Roboto:400,300,300italic,400italic,500,500italic,700,700italic';
++  link.href = 'chrome://resources/css/roboto.css';
+   document.head.appendChild(link);
+ }
+diff --git a/components-chromium/polymer/lib/legacy/legacy-element-mixin.js b/components-chromium/polymer/lib/legacy/legacy-element-mixin.js
+index 21fa65c0208d..bd591b6dd341 100644
+--- a/components-chromium/polymer/lib/legacy/legacy-element-mixin.js
++++ b/components-chromium/polymer/lib/legacy/legacy-element-mixin.js
+@@ -10,7 +10,6 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
+ import '../../../shadycss/entrypoints/apply-shim.js';
+ import { ElementMixin } from '../mixins/element-mixin.js';
+ import { GestureEventListeners } from '../mixins/gesture-event-listeners.js';
+-import { DirMixin } from '../mixins/dir-mixin.js';
+ import { dedupingMixin } from '../utils/mixin.js';
+ import '../utils/render-status.js';
+ import '../utils/unresolved.js';
+@@ -42,11 +41,10 @@ export const LegacyElementMixin = dedupingMixin((base) => {
+    * @constructor
+    * @implements {Polymer_ElementMixin}
+    * @implements {Polymer_GestureEventListeners}
+-   * @implements {Polymer_DirMixin}
+    * @extends {HTMLElement}
+    * @private
+    */
+-  const legacyElementBase = DirMixin(GestureEventListeners(ElementMixin(base)));
++  const legacyElementBase = GestureEventListeners(ElementMixin(base));
+ 
+   /**
+    * Map of simple names to touch action names
+
diff --git a/third_party/polymer/v3_0/components-chromium/font-roboto/roboto.js b/third_party/polymer/v3_0/components-chromium/font-roboto/roboto.js
new file mode 100644
index 0000000..342d26d
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/font-roboto/roboto.js
@@ -0,0 +1,22 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+
+export {}; // ensure this file can only be parsed as a module.
+
+// Give the user the choice to opt out of font loading.
+if (!window.polymerSkipLoadingFontRoboto) {
+  const link = document.createElement('link');
+  link.rel = 'stylesheet';
+  link.type = 'text/css';
+  link.crossOrigin = 'anonymous';
+  link.href = 'chrome://resources/css/roboto.css';
+  document.head.appendChild(link);
+}
diff --git a/third_party/polymer/v3_0/components-chromium/iron-a11y-announcer/iron-a11y-announcer.js b/third_party/polymer/v3_0/components-chromium/iron-a11y-announcer/iron-a11y-announcer.js
new file mode 100644
index 0000000..93b9e37
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-a11y-announcer/iron-a11y-announcer.js
@@ -0,0 +1,111 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+
+/**
+`iron-a11y-announcer` is a singleton element that is intended to add a11y
+to features that require on-demand announcement from screen readers. In
+order to make use of the announcer, it is best to request its availability
+in the announcing element.
+
+Example:
+
+    Polymer({
+
+      is: 'x-chatty',
+
+      attached: function() {
+        // This will create the singleton element if it has not
+        // been created yet:
+        Polymer.IronA11yAnnouncer.requestAvailability();
+      }
+    });
+
+After the `iron-a11y-announcer` has been made available, elements can
+make announces by firing bubbling `iron-announce` events.
+
+Example:
+
+    this.fire('iron-announce', {
+      text: 'This is an announcement!'
+    }, { bubbles: true });
+
+Note: announcements are only audible if you have a screen reader enabled.
+
+@group Iron Elements
+@demo demo/index.html
+*/
+export const IronA11yAnnouncer = Polymer({
+  _template: html`
+    <style>
+      :host {
+        display: inline-block;
+        position: fixed;
+        clip: rect(0px,0px,0px,0px);
+      }
+    </style>
+    <div aria-live$="[[mode]]">[[_text]]</div>
+`,
+
+  is: 'iron-a11y-announcer',
+
+  properties: {
+
+    /**
+     * The value of mode is used to set the `aria-live` attribute
+     * for the element that will be announced. Valid values are: `off`,
+     * `polite` and `assertive`.
+     */
+    mode: {type: String, value: 'polite'},
+
+    _text: {type: String, value: ''}
+  },
+
+  created: function() {
+    if (!IronA11yAnnouncer.instance) {
+      IronA11yAnnouncer.instance = this;
+    }
+
+    document.body.addEventListener(
+        'iron-announce', this._onIronAnnounce.bind(this));
+  },
+
+  /**
+   * Cause a text string to be announced by screen readers.
+   *
+   * @param {string} text The text that should be announced.
+   */
+  announce: function(text) {
+    this._text = '';
+    this.async(function() {
+      this._text = text;
+    }, 100);
+  },
+
+  _onIronAnnounce: function(event) {
+    if (event.detail && event.detail.text) {
+      this.announce(event.detail.text);
+    }
+  }
+});
+
+IronA11yAnnouncer.instance = null;
+
+IronA11yAnnouncer.requestAvailability = function() {
+  if (!IronA11yAnnouncer.instance) {
+    IronA11yAnnouncer.instance = document.createElement('iron-a11y-announcer');
+  }
+
+  document.body.appendChild(IronA11yAnnouncer.instance);
+};
diff --git a/third_party/polymer/v3_0/components-chromium/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js b/third_party/polymer/v3_0/components-chromium/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js
new file mode 100644
index 0000000..dae57c4
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js
@@ -0,0 +1,484 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+/**
+ * Chrome uses an older version of DOM Level 3 Keyboard Events
+ *
+ * Most keys are labeled as text, but some are Unicode codepoints.
+ * Values taken from:
+ * http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set
+ */
+var KEY_IDENTIFIER = {
+  'U+0008': 'backspace',
+  'U+0009': 'tab',
+  'U+001B': 'esc',
+  'U+0020': 'space',
+  'U+007F': 'del'
+};
+
+/**
+ * Special table for KeyboardEvent.keyCode.
+ * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even better
+ * than that.
+ *
+ * Values from:
+ * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode#Value_of_keyCode
+ */
+var KEY_CODE = {
+  8: 'backspace',
+  9: 'tab',
+  13: 'enter',
+  27: 'esc',
+  33: 'pageup',
+  34: 'pagedown',
+  35: 'end',
+  36: 'home',
+  32: 'space',
+  37: 'left',
+  38: 'up',
+  39: 'right',
+  40: 'down',
+  46: 'del',
+  106: '*'
+};
+
+/**
+ * MODIFIER_KEYS maps the short name for modifier keys used in a key
+ * combo string to the property name that references those same keys
+ * in a KeyboardEvent instance.
+ */
+var MODIFIER_KEYS = {
+  'shift': 'shiftKey',
+  'ctrl': 'ctrlKey',
+  'alt': 'altKey',
+  'meta': 'metaKey'
+};
+
+/**
+ * KeyboardEvent.key is mostly represented by printable character made by
+ * the keyboard, with unprintable keys labeled nicely.
+ *
+ * However, on OS X, Alt+char can make a Unicode character that follows an
+ * Apple-specific mapping. In this case, we fall back to .keyCode.
+ */
+var KEY_CHAR = /[a-z0-9*]/;
+
+/**
+ * Matches a keyIdentifier string.
+ */
+var IDENT_CHAR = /U\+/;
+
+/**
+ * Matches arrow keys in Gecko 27.0+
+ */
+var ARROW_KEY = /^arrow/;
+
+/**
+ * Matches space keys everywhere (notably including IE10's exceptional name
+ * `spacebar`).
+ */
+var SPACE_KEY = /^space(bar)?/;
+
+/**
+ * Matches ESC key.
+ *
+ * Value from: http://w3c.github.io/uievents-key/#key-Escape
+ */
+var ESC_KEY = /^escape$/;
+
+/**
+ * Transforms the key.
+ * @param {string} key The KeyBoardEvent.key
+ * @param {Boolean} [noSpecialChars] Limits the transformation to
+ * alpha-numeric characters.
+ */
+function transformKey(key, noSpecialChars) {
+  var validKey = '';
+  if (key) {
+    var lKey = key.toLowerCase();
+    if (lKey === ' ' || SPACE_KEY.test(lKey)) {
+      validKey = 'space';
+    } else if (ESC_KEY.test(lKey)) {
+      validKey = 'esc';
+    } else if (lKey.length == 1) {
+      if (!noSpecialChars || KEY_CHAR.test(lKey)) {
+        validKey = lKey;
+      }
+    } else if (ARROW_KEY.test(lKey)) {
+      validKey = lKey.replace('arrow', '');
+    } else if (lKey == 'multiply') {
+      // numpad '*' can map to Multiply on IE/Windows
+      validKey = '*';
+    } else {
+      validKey = lKey;
+    }
+  }
+  return validKey;
+}
+
+function transformKeyIdentifier(keyIdent) {
+  var validKey = '';
+  if (keyIdent) {
+    if (keyIdent in KEY_IDENTIFIER) {
+      validKey = KEY_IDENTIFIER[keyIdent];
+    } else if (IDENT_CHAR.test(keyIdent)) {
+      keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
+      validKey = String.fromCharCode(keyIdent).toLowerCase();
+    } else {
+      validKey = keyIdent.toLowerCase();
+    }
+  }
+  return validKey;
+}
+
+function transformKeyCode(keyCode) {
+  var validKey = '';
+  if (Number(keyCode)) {
+    if (keyCode >= 65 && keyCode <= 90) {
+      // ascii a-z
+      // lowercase is 32 offset from uppercase
+      validKey = String.fromCharCode(32 + keyCode);
+    } else if (keyCode >= 112 && keyCode <= 123) {
+      // function keys f1-f12
+      validKey = 'f' + (keyCode - 112 + 1);
+    } else if (keyCode >= 48 && keyCode <= 57) {
+      // top 0-9 keys
+      validKey = String(keyCode - 48);
+    } else if (keyCode >= 96 && keyCode <= 105) {
+      // num pad 0-9
+      validKey = String(keyCode - 96);
+    } else {
+      validKey = KEY_CODE[keyCode];
+    }
+  }
+  return validKey;
+}
+
+/**
+ * Calculates the normalized key for a KeyboardEvent.
+ * @param {KeyboardEvent} keyEvent
+ * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key
+ * transformation to alpha-numeric chars. This is useful with key
+ * combinations like shift + 2, which on FF for MacOS produces
+ * keyEvent.key = @
+ * To get 2 returned, set noSpecialChars = true
+ * To get @ returned, set noSpecialChars = false
+ */
+function normalizedKeyForEvent(keyEvent, noSpecialChars) {
+  // Fall back from .key, to .detail.key for artifical keyboard events,
+  // and then to deprecated .keyIdentifier and .keyCode.
+  if (keyEvent.key) {
+    return transformKey(keyEvent.key, noSpecialChars);
+  }
+  if (keyEvent.detail && keyEvent.detail.key) {
+    return transformKey(keyEvent.detail.key, noSpecialChars);
+  }
+  return transformKeyIdentifier(keyEvent.keyIdentifier) ||
+      transformKeyCode(keyEvent.keyCode) || '';
+}
+
+function keyComboMatchesEvent(keyCombo, event) {
+  // For combos with modifiers we support only alpha-numeric keys
+  var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
+  return keyEvent === keyCombo.key &&
+      (!keyCombo.hasModifiers ||
+       (!!event.shiftKey === !!keyCombo.shiftKey &&
+        !!event.ctrlKey === !!keyCombo.ctrlKey &&
+        !!event.altKey === !!keyCombo.altKey &&
+        !!event.metaKey === !!keyCombo.metaKey));
+}
+
+function parseKeyComboString(keyComboString) {
+  if (keyComboString.length === 1) {
+    return {combo: keyComboString, key: keyComboString, event: 'keydown'};
+  }
+  return keyComboString.split('+')
+      .reduce(function(parsedKeyCombo, keyComboPart) {
+        var eventParts = keyComboPart.split(':');
+        var keyName = eventParts[0];
+        var event = eventParts[1];
+
+        if (keyName in MODIFIER_KEYS) {
+          parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
+          parsedKeyCombo.hasModifiers = true;
+        } else {
+          parsedKeyCombo.key = keyName;
+          parsedKeyCombo.event = event || 'keydown';
+        }
+
+        return parsedKeyCombo;
+      }, {combo: keyComboString.split(':').shift()});
+}
+
+function parseEventString(eventString) {
+  return eventString.trim().split(' ').map(function(keyComboString) {
+    return parseKeyComboString(keyComboString);
+  });
+}
+
+/**
+ * `Polymer.IronA11yKeysBehavior` provides a normalized interface for processing
+ * keyboard commands that pertain to [WAI-ARIA best
+ * practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding). The
+ * element takes care of browser differences with respect to Keyboard events and
+ * uses an expressive syntax to filter key presses.
+ *
+ * Use the `keyBindings` prototype property to express what combination of keys
+ * will trigger the callback. A key binding has the format
+ * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or
+ * `"KEY:EVENT": "callback"` are valid as well). Some examples:
+ *
+ *      keyBindings: {
+ *        'space': '_onKeydown', // same as 'space:keydown'
+ *        'shift+tab': '_onKeydown',
+ *        'enter:keypress': '_onKeypress',
+ *        'esc:keyup': '_onKeyup'
+ *      }
+ *
+ * The callback will receive with an event containing the following information
+ * in `event.detail`:
+ *
+ *      _onKeydown: function(event) {
+ *        console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab"
+ *        console.log(event.detail.key); // KEY only, e.g. "tab"
+ *        console.log(event.detail.event); // EVENT, e.g. "keydown"
+ *        console.log(event.detail.keyboardEvent); // the original KeyboardEvent
+ *      }
+ *
+ * Use the `keyEventTarget` attribute to set up event handlers on a specific
+ * node.
+ *
+ * See the [demo source
+ * code](https://github.com/PolymerElements/iron-a11y-keys-behavior/blob/master/demo/x-key-aware.html)
+ * for an example.
+ *
+ * @demo demo/index.html
+ * @polymerBehavior
+ */
+export const IronA11yKeysBehavior = {
+  properties: {
+    /**
+     * The EventTarget that will be firing relevant KeyboardEvents. Set it to
+     * `null` to disable the listeners.
+     * @type {?EventTarget}
+     */
+    keyEventTarget: {
+      type: Object,
+      value: function() {
+        return this;
+      }
+    },
+
+    /**
+     * If true, this property will cause the implementing element to
+     * automatically stop propagation on any handled KeyboardEvents.
+     */
+    stopKeyboardEventPropagation: {type: Boolean, value: false},
+
+    _boundKeyHandlers: {
+      type: Array,
+      value: function() {
+        return [];
+      }
+    },
+
+    // We use this due to a limitation in IE10 where instances will have
+    // own properties of everything on the "prototype".
+    _imperativeKeyBindings: {
+      type: Object,
+      value: function() {
+        return {};
+      }
+    }
+  },
+
+  observers: ['_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'],
+
+
+  /**
+   * To be used to express what combination of keys  will trigger the relative
+   * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}`
+   * @type {!Object}
+   */
+  keyBindings: {},
+
+  registered: function() {
+    this._prepKeyBindings();
+  },
+
+  attached: function() {
+    this._listenKeyEventListeners();
+  },
+
+  detached: function() {
+    this._unlistenKeyEventListeners();
+  },
+
+  /**
+   * Can be used to imperatively add a key binding to the implementing
+   * element. This is the imperative equivalent of declaring a keybinding
+   * in the `keyBindings` prototype property.
+   *
+   * @param {string} eventString
+   * @param {string} handlerName
+   */
+  addOwnKeyBinding: function(eventString, handlerName) {
+    this._imperativeKeyBindings[eventString] = handlerName;
+    this._prepKeyBindings();
+    this._resetKeyEventListeners();
+  },
+
+  /**
+   * When called, will remove all imperatively-added key bindings.
+   */
+  removeOwnKeyBindings: function() {
+    this._imperativeKeyBindings = {};
+    this._prepKeyBindings();
+    this._resetKeyEventListeners();
+  },
+
+  /**
+   * Returns true if a keyboard event matches `eventString`.
+   *
+   * @param {KeyboardEvent} event
+   * @param {string} eventString
+   * @return {boolean}
+   */
+  keyboardEventMatchesKeys: function(event, eventString) {
+    var keyCombos = parseEventString(eventString);
+    for (var i = 0; i < keyCombos.length; ++i) {
+      if (keyComboMatchesEvent(keyCombos[i], event)) {
+        return true;
+      }
+    }
+    return false;
+  },
+
+  _collectKeyBindings: function() {
+    var keyBindings = this.behaviors.map(function(behavior) {
+      return behavior.keyBindings;
+    });
+
+    if (keyBindings.indexOf(this.keyBindings) === -1) {
+      keyBindings.push(this.keyBindings);
+    }
+
+    return keyBindings;
+  },
+
+  _prepKeyBindings: function() {
+    this._keyBindings = {};
+
+    this._collectKeyBindings().forEach(function(keyBindings) {
+      for (var eventString in keyBindings) {
+        this._addKeyBinding(eventString, keyBindings[eventString]);
+      }
+    }, this);
+
+    for (var eventString in this._imperativeKeyBindings) {
+      this._addKeyBinding(
+          eventString, this._imperativeKeyBindings[eventString]);
+    }
+
+    // Give precedence to combos with modifiers to be checked first.
+    for (var eventName in this._keyBindings) {
+      this._keyBindings[eventName].sort(function(kb1, kb2) {
+        var b1 = kb1[0].hasModifiers;
+        var b2 = kb2[0].hasModifiers;
+        return (b1 === b2) ? 0 : b1 ? -1 : 1;
+      })
+    }
+  },
+
+  _addKeyBinding: function(eventString, handlerName) {
+    parseEventString(eventString).forEach(function(keyCombo) {
+      this._keyBindings[keyCombo.event] =
+          this._keyBindings[keyCombo.event] || [];
+
+      this._keyBindings[keyCombo.event].push([keyCombo, handlerName]);
+    }, this);
+  },
+
+  _resetKeyEventListeners: function() {
+    this._unlistenKeyEventListeners();
+
+    if (this.isAttached) {
+      this._listenKeyEventListeners();
+    }
+  },
+
+  _listenKeyEventListeners: function() {
+    if (!this.keyEventTarget) {
+      return;
+    }
+    Object.keys(this._keyBindings).forEach(function(eventName) {
+      var keyBindings = this._keyBindings[eventName];
+      var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
+
+      this._boundKeyHandlers.push(
+          [this.keyEventTarget, eventName, boundKeyHandler]);
+
+      this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
+    }, this);
+  },
+
+  _unlistenKeyEventListeners: function() {
+    var keyHandlerTuple;
+    var keyEventTarget;
+    var eventName;
+    var boundKeyHandler;
+
+    while (this._boundKeyHandlers.length) {
+      // My kingdom for block-scope binding and destructuring assignment..
+      keyHandlerTuple = this._boundKeyHandlers.pop();
+      keyEventTarget = keyHandlerTuple[0];
+      eventName = keyHandlerTuple[1];
+      boundKeyHandler = keyHandlerTuple[2];
+
+      keyEventTarget.removeEventListener(eventName, boundKeyHandler);
+    }
+  },
+
+  _onKeyBindingEvent: function(keyBindings, event) {
+    if (this.stopKeyboardEventPropagation) {
+      event.stopPropagation();
+    }
+
+    // if event has been already prevented, don't do anything
+    if (event.defaultPrevented) {
+      return;
+    }
+
+    for (var i = 0; i < keyBindings.length; i++) {
+      var keyCombo = keyBindings[i][0];
+      var handlerName = keyBindings[i][1];
+      if (keyComboMatchesEvent(keyCombo, event)) {
+        this._triggerKeyHandler(keyCombo, handlerName, event);
+        // exit the loop if eventDefault was prevented
+        if (event.defaultPrevented) {
+          return;
+        }
+      }
+    }
+  },
+
+  _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
+    var detail = Object.create(keyCombo);
+    detail.keyboardEvent = keyboardEvent;
+    var event =
+        new CustomEvent(keyCombo.event, {detail: detail, cancelable: true});
+    this[handlerName].call(this, event);
+    if (event.defaultPrevented) {
+      keyboardEvent.preventDefault();
+    }
+  }
+};
diff --git a/third_party/polymer/v3_0/components-chromium/iron-a11y-keys/iron-a11y-keys.js b/third_party/polymer/v3_0/components-chromium/iron-a11y-keys/iron-a11y-keys.js
new file mode 100644
index 0000000..a84490a
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-a11y-keys/iron-a11y-keys.js
@@ -0,0 +1,170 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {IronA11yKeysBehavior} from '../iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+/**
+`iron-a11y-keys` provides a cross-browser interface for processing
+keyboard commands. The interface adheres to [WAI-ARIA best
+practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding).
+It uses an expressive syntax to filter key presses.
+
+## Basic usage
+
+The sample code below is a portion of a custom element. The goal is to call
+the `onEnter` method whenever the `paper-input` element is in focus and
+the `Enter` key is pressed.
+
+    <iron-a11y-keys id="a11y" target="[[target]]" keys="enter"
+                        on-keys-pressed="onEnter"></iron-a11y-keys>
+    <paper-input id="input"
+                 placeholder="Type something. Press enter. Check console."
+                 value="{{userInput::input}}"></paper-input>
+
+The custom element declares an `iron-a11y-keys` element that is bound to a
+property called `target`. The `target` property
+needs to evaluate to the `paper-input` node. `iron-a11y-keys` registers
+an event handler for the target node using Polymer's [annotated event handler
+syntax](https://www.polymer-project.org/1.0/docs/devguide/events.html#annotated-listeners).
+`{{userInput::input}}` sets the `userInput` property to the user's input on each
+keystroke.
+
+The last step is to link the two elements within the custom element's
+registration.
+
+    ...
+    properties: {
+      userInput: {
+        type: String,
+        notify: true,
+      },
+      target: {
+        type: Object,
+        value: function() {
+          return this.$.input;
+        }
+      },
+    },
+    onEnter: function() {
+      console.log(this.userInput);
+    }
+    ...
+
+## The `keys` attribute
+
+The `keys` attribute expresses what combination of keys triggers the event.
+
+The attribute accepts a space-separated, plus-sign-concatenated
+set of modifier keys and some common keyboard keys.
+
+The common keys are: `a-z`, `0-9` (top row and number pad), `*` (shift 8 and
+number pad), `F1-F12`, `Page Up`, `Page Down`, `Left Arrow`, `Right Arrow`,
+`Down Arrow`, `Up Arrow`, `Home`, `End`, `Escape`, `Space`, `Tab`, `Enter`.
+
+The modifier keys are: `Shift`, `Control`, `Alt`, `Meta`.
+
+All keys are expected to be lowercase and shortened. E.g.
+`Left Arrow` is `left`, `Page Down` is `pagedown`, `Control` is `ctrl`,
+`F1` is `f1`, `Escape` is `esc`, etc.
+
+### Grammar
+
+Below is the
+[EBNF](http://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_Form) Grammar
+of the `keys` attribute.
+
+    modifier = "shift" | "ctrl" | "alt" | "meta";
+    ascii = ? /[a-z0-9]/ ? ;
+    fnkey = ? f1 through f12 ? ;
+    arrow = "up" | "down" | "left" | "right" ;
+    key = "tab" | "esc" | "space" | "*" | "pageup" | "pagedown" |
+          "home" | "end" | arrow | ascii | fnkey;
+    event = "keypress" | "keydown" | "keyup";
+    keycombo = { modifier, "+" }, key, [ ":", event ] ;
+    keys = keycombo, { " ", keycombo } ;
+
+### Example
+
+Given the following value for `keys`:
+
+`ctrl+shift+f7 up pagedown esc space alt+m`
+
+The event is fired if any of the following key combinations are fired:
+`Control` and `Shift` and `F7` keys, `Up Arrow` key, `Page Down` key,
+`Escape` key, `Space` key, `Alt` and `M` keys.
+
+### WAI-ARIA Slider Example
+
+The following is an example of the set of keys that fulfills WAI-ARIA's
+"slider" role [best
+practices](http://www.w3.org/TR/wai-aria-practices/#slider):
+
+    <iron-a11y-keys target="[[target]]" keys="left pagedown down"
+                    on-keys-pressed="decrement"></iron-a11y-keys>
+    <iron-a11y-keys target="[[target]]" keys="right pageup up"
+                    on-keys-pressed="increment"></iron-a11y-keys>
+    <iron-a11y-keys target="[[target]]" keys="home"
+                    on-keys-pressed="setMin"></iron-a11y-keys>
+    <iron-a11y-keys target="[[target]]" keys="end"
+                    on-keys-pressed="setMax"></iron-a11y-keys>
+
+The `target` properties must evaluate to a node. See the basic usage
+example above.
+
+Each of the values for the `on-keys-pressed` attributes must evalute
+to methods. The `increment` method should move the slider a set amount
+toward the maximum value. `decrement` should move the slider a set amount
+toward the minimum value. `setMin` should move the slider to the minimum
+value. `setMax` should move the slider to the maximum value.
+
+@demo demo/index.html
+*/
+
+
+
+Polymer({
+  is: 'iron-a11y-keys',
+
+  behaviors: [IronA11yKeysBehavior],
+
+  properties: {
+    /** @type {?Node} */
+    target: {type: Object, observer: '_targetChanged'},
+
+    /**
+     * Space delimited list of keys where each key follows the format:
+     * `[MODIFIER+]*KEY[:EVENT]`.
+     * e.g. `keys="space ctrl+shift+tab enter:keyup"`.
+     * More detail can be found in the "Grammar" section of the documentation
+     */
+    keys: {type: String, reflectToAttribute: true, observer: '_keysChanged'}
+  },
+
+  attached: function() {
+    if (!this.target) {
+      this.target = this.parentNode;
+    }
+  },
+
+  _targetChanged: function(target) {
+    this.keyEventTarget = target;
+  },
+
+  _keysChanged: function() {
+    this.removeOwnKeyBindings();
+    this.addOwnKeyBinding(this.keys, '_fireKeysPressed');
+  },
+
+  _fireKeysPressed: function(event) {
+    this.fire('keys-pressed', event.detail, {});
+  }
+});
diff --git a/third_party/polymer/v3_0/components-chromium/iron-behaviors/iron-button-state.js b/third_party/polymer/v3_0/components-chromium/iron-behaviors/iron-button-state.js
new file mode 100644
index 0000000..a163674
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-behaviors/iron-button-state.js
@@ -0,0 +1,212 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+import './iron-control-state.js';
+
+import {IronA11yKeysBehavior} from '../iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+
+/**
+ * @demo demo/index.html
+ * @polymerBehavior IronButtonState
+ */
+export const IronButtonStateImpl = {
+
+  properties: {
+
+    /**
+     * If true, the user is currently holding down the button.
+     */
+    pressed: {
+      type: Boolean,
+      readOnly: true,
+      value: false,
+      reflectToAttribute: true,
+      observer: '_pressedChanged'
+    },
+
+    /**
+     * If true, the button toggles the active state with each tap or press
+     * of the spacebar.
+     */
+    toggles: {type: Boolean, value: false, reflectToAttribute: true},
+
+    /**
+     * If true, the button is a toggle and is currently in the active state.
+     */
+    active:
+        {type: Boolean, value: false, notify: true, reflectToAttribute: true},
+
+    /**
+     * True if the element is currently being pressed by a "pointer," which
+     * is loosely defined as mouse or touch input (but specifically excluding
+     * keyboard input).
+     */
+    pointerDown: {type: Boolean, readOnly: true, value: false},
+
+    /**
+     * True if the input device that caused the element to receive focus
+     * was a keyboard.
+     */
+    receivedFocusFromKeyboard: {type: Boolean, readOnly: true},
+
+    /**
+     * The aria attribute to be set if the button is a toggle and in the
+     * active state.
+     */
+    ariaActiveAttribute: {
+      type: String,
+      value: 'aria-pressed',
+      observer: '_ariaActiveAttributeChanged'
+    }
+  },
+
+  listeners: {down: '_downHandler', up: '_upHandler', tap: '_tapHandler'},
+
+  observers:
+      ['_focusChanged(focused)', '_activeChanged(active, ariaActiveAttribute)'],
+
+  /**
+   * @type {!Object}
+   */
+  keyBindings: {
+    'enter:keydown': '_asyncClick',
+    'space:keydown': '_spaceKeyDownHandler',
+    'space:keyup': '_spaceKeyUpHandler',
+  },
+
+  _mouseEventRe: /^mouse/,
+
+  _tapHandler: function() {
+    if (this.toggles) {
+      // a tap is needed to toggle the active state
+      this._userActivate(!this.active);
+    } else {
+      this.active = false;
+    }
+  },
+
+  _focusChanged: function(focused) {
+    this._detectKeyboardFocus(focused);
+
+    if (!focused) {
+      this._setPressed(false);
+    }
+  },
+
+  _detectKeyboardFocus: function(focused) {
+    this._setReceivedFocusFromKeyboard(!this.pointerDown && focused);
+  },
+
+  // to emulate native checkbox, (de-)activations from a user interaction fire
+  // 'change' events
+  _userActivate: function(active) {
+    if (this.active !== active) {
+      this.active = active;
+      this.fire('change');
+    }
+  },
+
+  _downHandler: function(event) {
+    this._setPointerDown(true);
+    this._setPressed(true);
+    this._setReceivedFocusFromKeyboard(false);
+  },
+
+  _upHandler: function() {
+    this._setPointerDown(false);
+    this._setPressed(false);
+  },
+
+  /**
+   * @param {!KeyboardEvent} event .
+   */
+  _spaceKeyDownHandler: function(event) {
+    var keyboardEvent = event.detail.keyboardEvent;
+    var target = dom(keyboardEvent).localTarget;
+
+    // Ignore the event if this is coming from a focused light child, since that
+    // element will deal with it.
+    if (this.isLightDescendant(/** @type {Node} */ (target)))
+      return;
+
+    keyboardEvent.preventDefault();
+    keyboardEvent.stopImmediatePropagation();
+    this._setPressed(true);
+  },
+
+  /**
+   * @param {!KeyboardEvent} event .
+   */
+  _spaceKeyUpHandler: function(event) {
+    var keyboardEvent = event.detail.keyboardEvent;
+    var target = dom(keyboardEvent).localTarget;
+
+    // Ignore the event if this is coming from a focused light child, since that
+    // element will deal with it.
+    if (this.isLightDescendant(/** @type {Node} */ (target)))
+      return;
+
+    if (this.pressed) {
+      this._asyncClick();
+    }
+    this._setPressed(false);
+  },
+
+  // trigger click asynchronously, the asynchrony is useful to allow one
+  // event handler to unwind before triggering another event
+  _asyncClick: function() {
+    this.async(function() {
+      this.click();
+    }, 1);
+  },
+
+  // any of these changes are considered a change to button state
+
+  _pressedChanged: function(pressed) {
+    this._changedButtonState();
+  },
+
+  _ariaActiveAttributeChanged: function(value, oldValue) {
+    if (oldValue && oldValue != value && this.hasAttribute(oldValue)) {
+      this.removeAttribute(oldValue);
+    }
+  },
+
+  _activeChanged: function(active, ariaActiveAttribute) {
+    if (this.toggles) {
+      this.setAttribute(this.ariaActiveAttribute, active ? 'true' : 'false');
+    } else {
+      this.removeAttribute(this.ariaActiveAttribute);
+    }
+    this._changedButtonState();
+  },
+
+  _controlStateChanged: function() {
+    if (this.disabled) {
+      this._setPressed(false);
+    } else {
+      this._changedButtonState();
+    }
+  },
+
+  // provide hook for follow-on behaviors to react to button-state
+
+  _changedButtonState: function() {
+    if (this._buttonStateChanged) {
+      this._buttonStateChanged();  // abstract
+    }
+  }
+
+};
+
+/** @polymerBehavior */
+export const IronButtonState = [IronA11yKeysBehavior, IronButtonStateImpl];
diff --git a/third_party/polymer/v3_0/components-chromium/iron-behaviors/iron-control-state.js b/third_party/polymer/v3_0/components-chromium/iron-behaviors/iron-control-state.js
new file mode 100644
index 0000000..0d7d3a1
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-behaviors/iron-control-state.js
@@ -0,0 +1,104 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+
+/**
+ * @demo demo/index.html
+ * @polymerBehavior
+ */
+export const IronControlState = {
+
+  properties: {
+
+    /**
+     * If true, the element currently has focus.
+     */
+    focused: {
+      type: Boolean,
+      value: false,
+      notify: true,
+      readOnly: true,
+      reflectToAttribute: true
+    },
+
+    /**
+     * If true, the user cannot interact with this element.
+     */
+    disabled: {
+      type: Boolean,
+      value: false,
+      notify: true,
+      observer: '_disabledChanged',
+      reflectToAttribute: true
+    },
+
+    /**
+     * Value of the `tabindex` attribute before `disabled` was activated.
+     * `null` means the attribute was not present.
+     * @type {?string|undefined}
+     */
+    _oldTabIndex: {type: String},
+
+    _boundFocusBlurHandler: {
+      type: Function,
+      value: function() {
+        return this._focusBlurHandler.bind(this);
+      }
+    }
+  },
+
+  observers: ['_changedControlState(focused, disabled)'],
+
+  /**
+   * @return {void}
+   */
+  ready: function() {
+    this.addEventListener('focus', this._boundFocusBlurHandler, true);
+    this.addEventListener('blur', this._boundFocusBlurHandler, true);
+  },
+
+  _focusBlurHandler: function(event) {
+    // Polymer takes care of retargeting events.
+    this._setFocused(event.type === 'focus');
+    return;
+  },
+
+  _disabledChanged: function(disabled, old) {
+    this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
+    this.style.pointerEvents = disabled ? 'none' : '';
+    if (disabled) {
+      // Read the `tabindex` attribute instead of the `tabIndex` property.
+      // The property returns `-1` if there is no `tabindex` attribute.
+      // This distinction is important when restoring the value because
+      // leaving `-1` hides shadow root children from the tab order.
+      this._oldTabIndex = this.getAttribute('tabindex');
+      this._setFocused(false);
+      this.tabIndex = -1;
+      this.blur();
+    } else if (this._oldTabIndex !== undefined) {
+      if (this._oldTabIndex === null) {
+        this.removeAttribute('tabindex');
+      } else {
+        this.setAttribute('tabindex', this._oldTabIndex);
+      }
+    }
+  },
+
+  _changedControlState: function() {
+    // _controlStateChanged is abstract, follow-on behaviors may implement it
+    if (this._controlStateChanged) {
+      this._controlStateChanged();
+    }
+  }
+
+};
diff --git a/third_party/polymer/v3_0/components-chromium/iron-collapse/iron-collapse.js b/third_party/polymer/v3_0/components-chromium/iron-collapse/iron-collapse.js
new file mode 100644
index 0000000..75372db
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-collapse/iron-collapse.js
@@ -0,0 +1,273 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import {IronResizableBehavior} from '../iron-resizable-behavior/iron-resizable-behavior.js';
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+import {Base} from '../polymer/polymer-legacy.js';
+
+/**
+`iron-collapse` creates a collapsible block of content.  By default, the content
+will be collapsed.  Use `opened` or `toggle()` to show/hide the content.
+
+    <button on-click="toggle">toggle collapse</button>
+
+    <iron-collapse id="collapse">
+      <div>Content goes here...</div>
+    </iron-collapse>
+
+    ...
+
+    toggle: function() {
+      this.$.collapse.toggle();
+    }
+
+`iron-collapse` adjusts the max-height/max-width of the collapsible element to
+show/hide the content.  So avoid putting padding/margin/border on the
+collapsible directly, and instead put a div inside and style that.
+
+    <style>
+      .collapse-content {
+        padding: 15px;
+        border: 1px solid #dedede;
+      }
+    </style>
+
+    <iron-collapse>
+      <div class="collapse-content">
+        <div>Content goes here...</div>
+      </div>
+    </iron-collapse>
+
+### Styling
+
+The following custom properties and mixins are available for styling:
+
+Custom property | Description | Default
+----------------|-------------|----------
+`--iron-collapse-transition-duration` | Animation transition duration | `300ms`
+
+@group Iron Elements
+@hero hero.svg
+@demo demo/index.html
+@element iron-collapse
+*/
+Polymer({
+  _template: html`
+    <style>
+      :host {
+        display: block;
+        transition-duration: var(--iron-collapse-transition-duration, 300ms);
+        /* Safari 10 needs this property prefixed to correctly apply the custom property */
+        -webkit-transition-duration: var(--iron-collapse-transition-duration, 300ms);
+        overflow: visible;
+      }
+
+      :host(.iron-collapse-closed) {
+        display: none;
+      }
+
+      :host(:not(.iron-collapse-opened)) {
+        overflow: hidden;
+      }
+    </style>
+
+    <slot></slot>
+`,
+
+  is: 'iron-collapse',
+  behaviors: [IronResizableBehavior],
+
+  properties: {
+
+    /**
+     * If true, the orientation is horizontal; otherwise is vertical.
+     *
+     * @attribute horizontal
+     */
+    horizontal: {type: Boolean, value: false, observer: '_horizontalChanged'},
+
+    /**
+     * Set opened to true to show the collapse element and to false to hide it.
+     *
+     * @attribute opened
+     */
+    opened:
+        {type: Boolean, value: false, notify: true, observer: '_openedChanged'},
+
+    /**
+     * When true, the element is transitioning its opened state. When false,
+     * the element has finished opening/closing.
+     *
+     * @attribute transitioning
+     */
+    transitioning: {type: Boolean, notify: true, readOnly: true},
+
+    /**
+     * Set noAnimation to true to disable animations.
+     *
+     * @attribute noAnimation
+     */
+    noAnimation: {type: Boolean},
+
+    /**
+     * Stores the desired size of the collapse body.
+     * @private
+     */
+    _desiredSize: {type: String, value: ''}
+  },
+
+  get dimension() {
+    return this.horizontal ? 'width' : 'height';
+  },
+
+  /**
+   * `maxWidth` or `maxHeight`.
+   * @private
+   */
+  get _dimensionMax() {
+    return this.horizontal ? 'maxWidth' : 'maxHeight';
+  },
+
+  /**
+   * `max-width` or `max-height`.
+   * @private
+   */
+  get _dimensionMaxCss() {
+    return this.horizontal ? 'max-width' : 'max-height';
+  },
+
+  hostAttributes: {
+    role: 'group',
+    'aria-hidden': 'true',
+  },
+
+  listeners: {transitionend: '_onTransitionEnd'},
+
+  /**
+   * Toggle the opened state.
+   *
+   * @method toggle
+   */
+  toggle: function() {
+    this.opened = !this.opened;
+  },
+
+  show: function() {
+    this.opened = true;
+  },
+
+  hide: function() {
+    this.opened = false;
+  },
+
+  /**
+   * Updates the size of the element.
+   * @param {string} size The new value for `maxWidth`/`maxHeight` as css property value, usually `auto` or `0px`.
+   * @param {boolean=} animated if `true` updates the size with an animation, otherwise without.
+   */
+  updateSize: function(size, animated) {
+    // Consider 'auto' as '', to take full size.
+    size = size === 'auto' ? '' : size;
+
+    var willAnimate = animated && !this.noAnimation && this.isAttached &&
+        this._desiredSize !== size;
+
+    this._desiredSize = size;
+
+    this._updateTransition(false);
+    // If we can animate, must do some prep work.
+    if (willAnimate) {
+      // Animation will start at the current size.
+      var startSize = this._calcSize();
+      // For `auto` we must calculate what is the final size for the animation.
+      // After the transition is done, _transitionEnd will set the size back to
+      // `auto`.
+      if (size === '') {
+        this.style[this._dimensionMax] = '';
+        size = this._calcSize();
+      }
+      // Go to startSize without animation.
+      this.style[this._dimensionMax] = startSize;
+      // Force layout to ensure transition will go. Set scrollTop to itself
+      // so that compilers won't remove it.
+      this.scrollTop = this.scrollTop;
+      // Enable animation.
+      this._updateTransition(true);
+      // If final size is the same as startSize it will not animate.
+      willAnimate = (size !== startSize);
+    }
+    // Set the final size.
+    this.style[this._dimensionMax] = size;
+    // If it won't animate, call transitionEnd to set correct classes.
+    if (!willAnimate) {
+      this._transitionEnd();
+    }
+  },
+
+  /**
+   * enableTransition() is deprecated, but left over so it doesn't break
+   * existing code. Please use `noAnimation` property instead.
+   *
+   * @method enableTransition
+   * @deprecated since version 1.0.4
+   */
+  enableTransition: function(enabled) {
+    Base._warn(
+        '`enableTransition()` is deprecated, use `noAnimation` instead.');
+    this.noAnimation = !enabled;
+  },
+
+  _updateTransition: function(enabled) {
+    this.style.transitionDuration = (enabled && !this.noAnimation) ? '' : '0s';
+  },
+
+  _horizontalChanged: function() {
+    this.style.transitionProperty = this._dimensionMaxCss;
+    var otherDimension =
+        this._dimensionMax === 'maxWidth' ? 'maxHeight' : 'maxWidth';
+    this.style[otherDimension] = '';
+    this.updateSize(this.opened ? 'auto' : '0px', false);
+  },
+
+  _openedChanged: function() {
+    this.setAttribute('aria-hidden', !this.opened);
+
+    this._setTransitioning(true);
+    this.toggleClass('iron-collapse-closed', false);
+    this.toggleClass('iron-collapse-opened', false);
+    this.updateSize(this.opened ? 'auto' : '0px', true);
+
+    // Focus the current collapse.
+    if (this.opened) {
+      this.focus();
+    }
+  },
+
+  _transitionEnd: function() {
+    this.style[this._dimensionMax] = this._desiredSize;
+    this.toggleClass('iron-collapse-closed', !this.opened);
+    this.toggleClass('iron-collapse-opened', this.opened);
+    this._updateTransition(false);
+    this.notifyResize();
+    this._setTransitioning(false);
+  },
+
+  _onTransitionEnd: function(event) {
+    if (dom(event).rootTarget === this) {
+      this._transitionEnd();
+    }
+  },
+
+  _calcSize: function() {
+    return this.getBoundingClientRect()[this.dimension] + 'px';
+  }
+});
diff --git a/third_party/polymer/v3_0/components-chromium/iron-dropdown/iron-dropdown-scroll-manager.js b/third_party/polymer/v3_0/components-chromium/iron-dropdown/iron-dropdown-scroll-manager.js
new file mode 100644
index 0000000..4f52ea1a
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-dropdown/iron-dropdown-scroll-manager.js
@@ -0,0 +1,16 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import * as ironScrollManager from '../iron-overlay-behavior/iron-scroll-manager.js';
+
+/**
+ * IronDropdownScrollManager is deprecated, use IronScrollManager instead.
+ */
+export {ironScrollManager as IronDropdownScrollManager};
diff --git a/third_party/polymer/v3_0/components-chromium/iron-dropdown/iron-dropdown.js b/third_party/polymer/v3_0/components-chromium/iron-dropdown/iron-dropdown.js
new file mode 100644
index 0000000..288e624
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-dropdown/iron-dropdown.js
@@ -0,0 +1,279 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {IronA11yKeysBehavior} from '../iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
+import {IronControlState} from '../iron-behaviors/iron-control-state.js';
+import {IronOverlayBehavior, IronOverlayBehaviorImpl} from '../iron-overlay-behavior/iron-overlay-behavior.js';
+import {NeonAnimationRunnerBehavior} from '../neon-animation/neon-animation-runner-behavior.js';
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+
+/**
+`<iron-dropdown>` is a generalized element that is useful when you have
+hidden content (`dropdown-content`) that is revealed due to some change in
+state that should cause it to do so.
+
+Note that this is a low-level element intended to be used as part of other
+composite elements that cause dropdowns to be revealed.
+
+Examples of elements that might be implemented using an `iron-dropdown`
+include comboboxes, menubuttons, selects. The list goes on.
+
+The `<iron-dropdown>` element exposes attributes that allow the position
+of the `dropdown-content` relative to the `dropdown-trigger` to be
+configured.
+
+    <iron-dropdown horizontal-align="right" vertical-align="top">
+      <div slot="dropdown-content">Hello!</div>
+    </iron-dropdown>
+
+In the above example, the `<div>` assigned to the `dropdown-content` slot will
+be hidden until the dropdown element has `opened` set to true, or when the
+`open` method is called on the element.
+
+@demo demo/index.html
+*/
+Polymer({
+  _template: html`
+    <style>
+      :host {
+        position: fixed;
+      }
+
+      #contentWrapper ::slotted(*) {
+        overflow: auto;
+      }
+
+      #contentWrapper.animating ::slotted(*) {
+        overflow: hidden;
+        pointer-events: none;
+      }
+    </style>
+
+    <div id="contentWrapper">
+      <slot id="content" name="dropdown-content"></slot>
+    </div>
+`,
+
+  is: 'iron-dropdown',
+
+  behaviors: [
+    IronControlState,
+    IronA11yKeysBehavior,
+    IronOverlayBehavior,
+    NeonAnimationRunnerBehavior
+  ],
+
+  properties: {
+    /**
+     * The orientation against which to align the dropdown content
+     * horizontally relative to the dropdown trigger.
+     * Overridden from `Polymer.IronFitBehavior`.
+     */
+    horizontalAlign: {type: String, value: 'left', reflectToAttribute: true},
+
+    /**
+     * The orientation against which to align the dropdown content
+     * vertically relative to the dropdown trigger.
+     * Overridden from `Polymer.IronFitBehavior`.
+     */
+    verticalAlign: {type: String, value: 'top', reflectToAttribute: true},
+
+    /**
+     * An animation config. If provided, this will be used to animate the
+     * opening of the dropdown. Pass an Array for multiple animations.
+     * See `neon-animation` documentation for more animation configuration
+     * details.
+     */
+    openAnimationConfig: {type: Object},
+
+    /**
+     * An animation config. If provided, this will be used to animate the
+     * closing of the dropdown. Pass an Array for multiple animations.
+     * See `neon-animation` documentation for more animation configuration
+     * details.
+     */
+    closeAnimationConfig: {type: Object},
+
+    /**
+     * If provided, this will be the element that will be focused when
+     * the dropdown opens.
+     */
+    focusTarget: {type: Object},
+
+    /**
+     * Set to true to disable animations when opening and closing the
+     * dropdown.
+     */
+    noAnimations: {type: Boolean, value: false},
+
+    /**
+     * By default, the dropdown will constrain scrolling on the page
+     * to itself when opened.
+     * Set to true in order to prevent scroll from being constrained
+     * to the dropdown when it opens.
+     * This property is a shortcut to set `scrollAction` to lock or refit.
+     * Prefer directly setting the `scrollAction` property.
+     */
+    allowOutsideScroll:
+        {type: Boolean, value: false, observer: '_allowOutsideScrollChanged'}
+  },
+
+  listeners: {'neon-animation-finish': '_onNeonAnimationFinish'},
+
+  observers: [
+    '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign, verticalOffset, horizontalOffset)'
+  ],
+
+  /**
+   * The element that is contained by the dropdown, if any.
+   */
+  get containedElement() {
+    // Polymer 2.x returns slot.assignedNodes which can contain text nodes.
+    var nodes = dom(this.$.content).getDistributedNodes();
+    for (var i = 0, l = nodes.length; i < l; i++) {
+      if (nodes[i].nodeType === Node.ELEMENT_NODE) {
+        return nodes[i];
+      }
+    }
+  },
+
+  ready: function() {
+    // Ensure scrollAction is set.
+    if (!this.scrollAction) {
+      this.scrollAction = this.allowOutsideScroll ? 'refit' : 'lock';
+    }
+    this._readied = true;
+  },
+
+  attached: function() {
+    if (!this.sizingTarget || this.sizingTarget === this) {
+      this.sizingTarget = this.containedElement || this;
+    }
+  },
+
+  detached: function() {
+    this.cancelAnimation();
+  },
+
+  /**
+   * Called when the value of `opened` changes.
+   * Overridden from `IronOverlayBehavior`
+   */
+  _openedChanged: function() {
+    if (this.opened && this.disabled) {
+      this.cancel();
+    } else {
+      this.cancelAnimation();
+      this._updateAnimationConfig();
+      IronOverlayBehaviorImpl._openedChanged.apply(this, arguments);
+    }
+  },
+
+  /**
+   * Overridden from `IronOverlayBehavior`.
+   */
+  _renderOpened: function() {
+    if (!this.noAnimations && this.animationConfig.open) {
+      this.$.contentWrapper.classList.add('animating');
+      this.playAnimation('open');
+    } else {
+      IronOverlayBehaviorImpl._renderOpened.apply(this, arguments);
+    }
+  },
+
+  /**
+   * Overridden from `IronOverlayBehavior`.
+   */
+  _renderClosed: function() {
+    if (!this.noAnimations && this.animationConfig.close) {
+      this.$.contentWrapper.classList.add('animating');
+      this.playAnimation('close');
+    } else {
+      IronOverlayBehaviorImpl._renderClosed.apply(this, arguments);
+    }
+  },
+
+  /**
+   * Called when animation finishes on the dropdown (when opening or
+   * closing). Responsible for "completing" the process of opening or
+   * closing the dropdown by positioning it or setting its display to
+   * none.
+   */
+  _onNeonAnimationFinish: function() {
+    this.$.contentWrapper.classList.remove('animating');
+    if (this.opened) {
+      this._finishRenderOpened();
+    } else {
+      this._finishRenderClosed();
+    }
+  },
+
+  /**
+   * Constructs the final animation config from different properties used
+   * to configure specific parts of the opening and closing animations.
+   */
+  _updateAnimationConfig: function() {
+    // Update the animation node to be the containedElement.
+    var animationNode = this.containedElement;
+    var animations = [].concat(this.openAnimationConfig || [])
+                         .concat(this.closeAnimationConfig || []);
+    for (var i = 0; i < animations.length; i++) {
+      animations[i].node = animationNode;
+    }
+    this.animationConfig = {
+      open: this.openAnimationConfig,
+      close: this.closeAnimationConfig
+    };
+  },
+
+  /**
+   * Updates the overlay position based on configured horizontal
+   * and vertical alignment.
+   */
+  _updateOverlayPosition: function() {
+    if (this.isAttached) {
+      // This triggers iron-resize, and iron-overlay-behavior will call refit if
+      // needed.
+      this.notifyResize();
+    }
+  },
+
+  /**
+   * Sets scrollAction according to the value of allowOutsideScroll.
+   * Prefer setting directly scrollAction.
+   */
+  _allowOutsideScrollChanged: function(allowOutsideScroll) {
+    // Wait until initial values are all set.
+    if (!this._readied) {
+      return;
+    }
+    if (!allowOutsideScroll) {
+      this.scrollAction = 'lock';
+    } else if (!this.scrollAction || this.scrollAction === 'lock') {
+      this.scrollAction = 'refit';
+    }
+  },
+
+  /**
+   * Apply focus to focusTarget or containedElement
+   */
+  _applyFocus: function() {
+    var focusTarget = this.focusTarget || this.containedElement;
+    if (focusTarget && this.opened && !this.noAutoFocus) {
+      focusTarget.focus();
+    } else {
+      IronOverlayBehaviorImpl._applyFocus.apply(this, arguments);
+    }
+  }
+});
diff --git a/third_party/polymer/v3_0/components-chromium/iron-fit-behavior/iron-fit-behavior.js b/third_party/polymer/v3_0/components-chromium/iron-fit-behavior/iron-fit-behavior.js
new file mode 100644
index 0000000..48d9b70
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-fit-behavior/iron-fit-behavior.js
@@ -0,0 +1,688 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+
+/**
+`Polymer.IronFitBehavior` fits an element in another element using `max-height`
+and `max-width`, and optionally centers it in the window or another element.
+
+The element will only be sized and/or positioned if it has not already been
+sized and/or positioned by CSS.
+
+CSS properties            | Action
+--------------------------|-------------------------------------------
+`position` set            | Element is not centered horizontally or vertically
+`top` or `bottom` set     | Element is not vertically centered
+`left` or `right` set     | Element is not horizontally centered
+`max-height` set          | Element respects `max-height`
+`max-width` set           | Element respects `max-width`
+
+`Polymer.IronFitBehavior` can position an element into another element using
+`verticalAlign` and `horizontalAlign`. This will override the element's css
+position.
+
+    <div class="container">
+      <iron-fit-impl vertical-align="top" horizontal-align="auto">
+        Positioned into the container
+      </iron-fit-impl>
+    </div>
+
+Use `noOverlap` to position the element around another element without
+overlapping it.
+
+    <div class="container">
+      <iron-fit-impl no-overlap vertical-align="auto" horizontal-align="auto">
+        Positioned around the container
+      </iron-fit-impl>
+    </div>
+
+Use `horizontalOffset, verticalOffset` to offset the element from its
+`positionTarget`; `Polymer.IronFitBehavior` will collapse these in order to
+keep the element within `fitInto` boundaries, while preserving the element's
+CSS margin values.
+
+    <div class="container">
+      <iron-fit-impl vertical-align="top" vertical-offset="20">
+        With vertical offset
+      </iron-fit-impl>
+    </div>
+
+@demo demo/index.html
+@polymerBehavior
+*/
+export const IronFitBehavior = {
+
+  properties: {
+
+    /**
+     * The element that will receive a `max-height`/`width`. By default it is
+     * the same as `this`, but it can be set to a child element. This is useful,
+     * for example, for implementing a scrolling region inside the element.
+     * @type {!Element}
+     */
+    sizingTarget: {
+      type: Object,
+      value: function() {
+        return this;
+      }
+    },
+
+    /**
+     * The element to fit `this` into.
+     */
+    fitInto: {type: Object, value: window},
+
+    /**
+     * Will position the element around the positionTarget without overlapping
+     * it.
+     */
+    noOverlap: {type: Boolean},
+
+    /**
+     * The element that should be used to position the element. If not set, it
+     * will default to the parent node.
+     * @type {!Element}
+     */
+    positionTarget: {type: Element},
+
+    /**
+     * The orientation against which to align the element horizontally
+     * relative to the `positionTarget`. Possible values are "left", "right",
+     * "center", "auto".
+     */
+    horizontalAlign: {type: String},
+
+    /**
+     * The orientation against which to align the element vertically
+     * relative to the `positionTarget`. Possible values are "top", "bottom",
+     * "middle", "auto".
+     */
+    verticalAlign: {type: String},
+
+    /**
+     * If true, it will use `horizontalAlign` and `verticalAlign` values as
+     * preferred alignment and if there's not enough space, it will pick the
+     * values which minimize the cropping.
+     */
+    dynamicAlign: {type: Boolean},
+
+    /**
+     * A pixel value that will be added to the position calculated for the
+     * given `horizontalAlign`, in the direction of alignment. You can think
+     * of it as increasing or decreasing the distance to the side of the
+     * screen given by `horizontalAlign`.
+     *
+     * If `horizontalAlign` is "left" or "center", this offset will increase or
+     * decrease the distance to the left side of the screen: a negative offset
+     * will move the dropdown to the left; a positive one, to the right.
+     *
+     * Conversely if `horizontalAlign` is "right", this offset will increase
+     * or decrease the distance to the right side of the screen: a negative
+     * offset will move the dropdown to the right; a positive one, to the left.
+     */
+    horizontalOffset: {type: Number, value: 0, notify: true},
+
+    /**
+     * A pixel value that will be added to the position calculated for the
+     * given `verticalAlign`, in the direction of alignment. You can think
+     * of it as increasing or decreasing the distance to the side of the
+     * screen given by `verticalAlign`.
+     *
+     * If `verticalAlign` is "top" or "middle", this offset will increase or
+     * decrease the distance to the top side of the screen: a negative offset
+     * will move the dropdown upwards; a positive one, downwards.
+     *
+     * Conversely if `verticalAlign` is "bottom", this offset will increase
+     * or decrease the distance to the bottom side of the screen: a negative
+     * offset will move the dropdown downwards; a positive one, upwards.
+     */
+    verticalOffset: {type: Number, value: 0, notify: true},
+
+    /**
+     * Set to true to auto-fit on attach.
+     */
+    autoFitOnAttach: {type: Boolean, value: false},
+
+    /** @type {?Object} */
+    _fitInfo: {type: Object}
+  },
+
+  get _fitWidth() {
+    var fitWidth;
+    if (this.fitInto === window) {
+      fitWidth = this.fitInto.innerWidth;
+    } else {
+      fitWidth = this.fitInto.getBoundingClientRect().width;
+    }
+    return fitWidth;
+  },
+
+  get _fitHeight() {
+    var fitHeight;
+    if (this.fitInto === window) {
+      fitHeight = this.fitInto.innerHeight;
+    } else {
+      fitHeight = this.fitInto.getBoundingClientRect().height;
+    }
+    return fitHeight;
+  },
+
+  get _fitLeft() {
+    var fitLeft;
+    if (this.fitInto === window) {
+      fitLeft = 0;
+    } else {
+      fitLeft = this.fitInto.getBoundingClientRect().left;
+    }
+    return fitLeft;
+  },
+
+  get _fitTop() {
+    var fitTop;
+    if (this.fitInto === window) {
+      fitTop = 0;
+    } else {
+      fitTop = this.fitInto.getBoundingClientRect().top;
+    }
+    return fitTop;
+  },
+
+  /**
+   * The element that should be used to position the element,
+   * if no position target is configured.
+   */
+  get _defaultPositionTarget() {
+    var parent = dom(this).parentNode;
+
+    if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
+      parent = parent.host;
+    }
+
+    return parent;
+  },
+
+  /**
+   * The horizontal align value, accounting for the RTL/LTR text direction.
+   */
+  get _localeHorizontalAlign() {
+    if (this._isRTL) {
+      // In RTL, "left" becomes "right".
+      if (this.horizontalAlign === 'right') {
+        return 'left';
+      }
+      if (this.horizontalAlign === 'left') {
+        return 'right';
+      }
+    }
+    return this.horizontalAlign;
+  },
+
+  /**
+   * True if the element should be positioned instead of centered.
+   * @private
+   */
+  get __shouldPosition() {
+    return (this.horizontalAlign || this.verticalAlign) && this.positionTarget;
+  },
+
+  attached: function() {
+    // Memoize this to avoid expensive calculations & relayouts.
+    // Make sure we do it only once
+    if (typeof this._isRTL === 'undefined') {
+      this._isRTL = window.getComputedStyle(this).direction == 'rtl';
+    }
+    this.positionTarget = this.positionTarget || this._defaultPositionTarget;
+    if (this.autoFitOnAttach) {
+      if (window.getComputedStyle(this).display === 'none') {
+        setTimeout(function() {
+          this.fit();
+        }.bind(this));
+      } else {
+        // NOTE: shadydom applies distribution asynchronously
+        // for performance reasons webcomponents/shadydom#120
+        // Flush to get correct layout info.
+        window.ShadyDOM && ShadyDOM.flush();
+        this.fit();
+      }
+    }
+  },
+
+  detached: function() {
+    if (this.__deferredFit) {
+      clearTimeout(this.__deferredFit);
+      this.__deferredFit = null;
+    }
+  },
+
+  /**
+   * Positions and fits the element into the `fitInto` element.
+   */
+  fit: function() {
+    this.position();
+    this.constrain();
+    this.center();
+  },
+
+  /**
+   * Memoize information needed to position and size the target element.
+   * @suppress {deprecated}
+   */
+  _discoverInfo: function() {
+    if (this._fitInfo) {
+      return;
+    }
+    var target = window.getComputedStyle(this);
+    var sizer = window.getComputedStyle(this.sizingTarget);
+
+    this._fitInfo = {
+      inlineStyle: {
+        top: this.style.top || '',
+        left: this.style.left || '',
+        position: this.style.position || ''
+      },
+      sizerInlineStyle: {
+        maxWidth: this.sizingTarget.style.maxWidth || '',
+        maxHeight: this.sizingTarget.style.maxHeight || '',
+        boxSizing: this.sizingTarget.style.boxSizing || ''
+      },
+      positionedBy: {
+        vertically: target.top !== 'auto' ?
+            'top' :
+            (target.bottom !== 'auto' ? 'bottom' : null),
+        horizontally: target.left !== 'auto' ?
+            'left' :
+            (target.right !== 'auto' ? 'right' : null)
+      },
+      sizedBy: {
+        height: sizer.maxHeight !== 'none',
+        width: sizer.maxWidth !== 'none',
+        minWidth: parseInt(sizer.minWidth, 10) || 0,
+        minHeight: parseInt(sizer.minHeight, 10) || 0
+      },
+      margin: {
+        top: parseInt(target.marginTop, 10) || 0,
+        right: parseInt(target.marginRight, 10) || 0,
+        bottom: parseInt(target.marginBottom, 10) || 0,
+        left: parseInt(target.marginLeft, 10) || 0
+      }
+    };
+  },
+
+  /**
+   * Resets the target element's position and size constraints, and clear
+   * the memoized data.
+   */
+  resetFit: function() {
+    var info = this._fitInfo || {};
+    for (var property in info.sizerInlineStyle) {
+      this.sizingTarget.style[property] = info.sizerInlineStyle[property];
+    }
+    for (var property in info.inlineStyle) {
+      this.style[property] = info.inlineStyle[property];
+    }
+
+    this._fitInfo = null;
+  },
+
+  /**
+   * Equivalent to calling `resetFit()` and `fit()`. Useful to call this after
+   * the element or the `fitInto` element has been resized, or if any of the
+   * positioning properties (e.g. `horizontalAlign, verticalAlign`) is updated.
+   * It preserves the scroll position of the sizingTarget.
+   */
+  refit: function() {
+    var scrollLeft = this.sizingTarget.scrollLeft;
+    var scrollTop = this.sizingTarget.scrollTop;
+    this.resetFit();
+    this.fit();
+    this.sizingTarget.scrollLeft = scrollLeft;
+    this.sizingTarget.scrollTop = scrollTop;
+  },
+
+  /**
+   * Positions the element according to `horizontalAlign, verticalAlign`.
+   */
+  position: function() {
+    if (!this.__shouldPosition) {
+      // needs to be centered, and it is done after constrain.
+      return;
+    }
+    this._discoverInfo();
+
+    this.style.position = 'fixed';
+    // Need border-box for margin/padding.
+    this.sizingTarget.style.boxSizing = 'border-box';
+    // Set to 0, 0 in order to discover any offset caused by parent stacking
+    // contexts.
+    this.style.left = '0px';
+    this.style.top = '0px';
+
+    var rect = this.getBoundingClientRect();
+    var positionRect = this.__getNormalizedRect(this.positionTarget);
+    var fitRect = this.__getNormalizedRect(this.fitInto);
+
+    var margin = this._fitInfo.margin;
+
+    // Consider the margin as part of the size for position calculations.
+    var size = {
+      width: rect.width + margin.left + margin.right,
+      height: rect.height + margin.top + margin.bottom
+    };
+
+    var position = this.__getPosition(
+        this._localeHorizontalAlign,
+        this.verticalAlign,
+        size,
+        rect,
+        positionRect,
+        fitRect);
+
+    var left = position.left + margin.left;
+    var top = position.top + margin.top;
+
+    // We first limit right/bottom within fitInto respecting the margin,
+    // then use those values to limit top/left.
+    var right = Math.min(fitRect.right - margin.right, left + rect.width);
+    var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height);
+
+    // Keep left/top within fitInto respecting the margin.
+    left = Math.max(
+        fitRect.left + margin.left,
+        Math.min(left, right - this._fitInfo.sizedBy.minWidth));
+    top = Math.max(
+        fitRect.top + margin.top,
+        Math.min(top, bottom - this._fitInfo.sizedBy.minHeight));
+
+    // Use right/bottom to set maxWidth/maxHeight, and respect
+    // minWidth/minHeight.
+    this.sizingTarget.style.maxWidth =
+        Math.max(right - left, this._fitInfo.sizedBy.minWidth) + 'px';
+    this.sizingTarget.style.maxHeight =
+        Math.max(bottom - top, this._fitInfo.sizedBy.minHeight) + 'px';
+
+    // Remove the offset caused by any stacking context.
+    this.style.left = (left - rect.left) + 'px';
+    this.style.top = (top - rect.top) + 'px';
+  },
+
+  /**
+   * Constrains the size of the element to `fitInto` by setting `max-height`
+   * and/or `max-width`.
+   */
+  constrain: function() {
+    if (this.__shouldPosition) {
+      return;
+    }
+    this._discoverInfo();
+
+    var info = this._fitInfo;
+    // position at (0px, 0px) if not already positioned, so we can measure the
+    // natural size.
+    if (!info.positionedBy.vertically) {
+      this.style.position = 'fixed';
+      this.style.top = '0px';
+    }
+    if (!info.positionedBy.horizontally) {
+      this.style.position = 'fixed';
+      this.style.left = '0px';
+    }
+
+    // need border-box for margin/padding
+    this.sizingTarget.style.boxSizing = 'border-box';
+    // constrain the width and height if not already set
+    var rect = this.getBoundingClientRect();
+    if (!info.sizedBy.height) {
+      this.__sizeDimension(
+          rect, info.positionedBy.vertically, 'top', 'bottom', 'Height');
+    }
+    if (!info.sizedBy.width) {
+      this.__sizeDimension(
+          rect, info.positionedBy.horizontally, 'left', 'right', 'Width');
+    }
+  },
+
+  /**
+   * @protected
+   * @deprecated
+   */
+  _sizeDimension: function(rect, positionedBy, start, end, extent) {
+    this.__sizeDimension(rect, positionedBy, start, end, extent);
+  },
+
+  /**
+   * @private
+   */
+  __sizeDimension: function(rect, positionedBy, start, end, extent) {
+    var info = this._fitInfo;
+    var fitRect = this.__getNormalizedRect(this.fitInto);
+    var max = extent === 'Width' ? fitRect.width : fitRect.height;
+    var flip = (positionedBy === end);
+    var offset = flip ? max - rect[end] : rect[start];
+    var margin = info.margin[flip ? start : end];
+    var offsetExtent = 'offset' + extent;
+    var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent];
+    this.sizingTarget.style['max' + extent] =
+        (max - margin - offset - sizingOffset) + 'px';
+  },
+
+  /**
+   * Centers horizontally and vertically if not already positioned. This also
+   * sets `position:fixed`.
+   */
+  center: function() {
+    if (this.__shouldPosition) {
+      return;
+    }
+    this._discoverInfo();
+
+    var positionedBy = this._fitInfo.positionedBy;
+    if (positionedBy.vertically && positionedBy.horizontally) {
+      // Already positioned.
+      return;
+    }
+    // Need position:fixed to center
+    this.style.position = 'fixed';
+    // Take into account the offset caused by parents that create stacking
+    // contexts (e.g. with transform: translate3d). Translate to 0,0 and
+    // measure the bounding rect.
+    if (!positionedBy.vertically) {
+      this.style.top = '0px';
+    }
+    if (!positionedBy.horizontally) {
+      this.style.left = '0px';
+    }
+    // It will take in consideration margins and transforms
+    var rect = this.getBoundingClientRect();
+    var fitRect = this.__getNormalizedRect(this.fitInto);
+    if (!positionedBy.vertically) {
+      var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2;
+      this.style.top = top + 'px';
+    }
+    if (!positionedBy.horizontally) {
+      var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2;
+      this.style.left = left + 'px';
+    }
+  },
+
+  __getNormalizedRect: function(target) {
+    if (target === document.documentElement || target === window) {
+      return {
+        top: 0,
+        left: 0,
+        width: window.innerWidth,
+        height: window.innerHeight,
+        right: window.innerWidth,
+        bottom: window.innerHeight
+      };
+    }
+    return target.getBoundingClientRect();
+  },
+
+  __getOffscreenArea: function(position, size, fitRect) {
+    var verticalCrop = Math.min(0, position.top) +
+        Math.min(0, fitRect.bottom - (position.top + size.height));
+    var horizontalCrop = Math.min(0, position.left) +
+        Math.min(0, fitRect.right - (position.left + size.width));
+    return Math.abs(verticalCrop) * size.width +
+        Math.abs(horizontalCrop) * size.height;
+  },
+
+
+  __getPosition: function(
+      hAlign, vAlign, size, sizeNoMargins, positionRect, fitRect) {
+    // All the possible configurations.
+    // Ordered as top-left, top-right, bottom-left, bottom-right.
+    var positions = [
+      {
+        verticalAlign: 'top',
+        horizontalAlign: 'left',
+        top: positionRect.top + this.verticalOffset,
+        left: positionRect.left + this.horizontalOffset
+      },
+      {
+        verticalAlign: 'top',
+        horizontalAlign: 'right',
+        top: positionRect.top + this.verticalOffset,
+        left: positionRect.right - size.width - this.horizontalOffset
+      },
+      {
+        verticalAlign: 'bottom',
+        horizontalAlign: 'left',
+        top: positionRect.bottom - size.height - this.verticalOffset,
+        left: positionRect.left + this.horizontalOffset
+      },
+      {
+        verticalAlign: 'bottom',
+        horizontalAlign: 'right',
+        top: positionRect.bottom - size.height - this.verticalOffset,
+        left: positionRect.right - size.width - this.horizontalOffset
+      }
+    ];
+
+    if (this.noOverlap) {
+      // Duplicate.
+      for (var i = 0, l = positions.length; i < l; i++) {
+        var copy = {};
+        for (var key in positions[i]) {
+          copy[key] = positions[i][key];
+        }
+        positions.push(copy);
+      }
+      // Horizontal overlap only.
+      positions[0].top = positions[1].top += positionRect.height;
+      positions[2].top = positions[3].top -= positionRect.height;
+      // Vertical overlap only.
+      positions[4].left = positions[6].left += positionRect.width;
+      positions[5].left = positions[7].left -= positionRect.width;
+    }
+
+    // Consider auto as null for coding convenience.
+    vAlign = vAlign === 'auto' ? null : vAlign;
+    hAlign = hAlign === 'auto' ? null : hAlign;
+
+    if (!hAlign || hAlign === 'center') {
+      positions.push({
+        verticalAlign: 'top',
+        horizontalAlign: 'center',
+        top: positionRect.top + this.verticalOffset +
+            (this.noOverlap ? positionRect.height : 0),
+        left: positionRect.left - sizeNoMargins.width / 2 +
+            positionRect.width / 2 + this.horizontalOffset
+      });
+      positions.push({
+        verticalAlign: 'bottom',
+        horizontalAlign: 'center',
+        top: positionRect.bottom - size.height - this.verticalOffset -
+            (this.noOverlap ? positionRect.height : 0),
+        left: positionRect.left - sizeNoMargins.width / 2 +
+            positionRect.width / 2 + this.horizontalOffset
+      });
+    }
+
+    if (!vAlign || vAlign === 'middle') {
+      positions.push({
+        verticalAlign: 'middle',
+        horizontalAlign: 'left',
+        top: positionRect.top - sizeNoMargins.height / 2 +
+            positionRect.height / 2 + this.verticalOffset,
+        left: positionRect.left + this.horizontalOffset +
+            (this.noOverlap ? positionRect.width : 0)
+      });
+      positions.push({
+        verticalAlign: 'middle',
+        horizontalAlign: 'right',
+        top: positionRect.top - sizeNoMargins.height / 2 +
+            positionRect.height / 2 + this.verticalOffset,
+        left: positionRect.right - size.width - this.horizontalOffset -
+            (this.noOverlap ? positionRect.width : 0)
+      });
+    }
+
+    if (vAlign === 'middle' && hAlign === 'center') {
+      positions.push({
+        verticalAlign: 'middle',
+        horizontalAlign: 'center',
+        top: positionRect.top - sizeNoMargins.height / 2 +
+            positionRect.height / 2 + this.verticalOffset,
+        left: positionRect.left - sizeNoMargins.width / 2 +
+            positionRect.width / 2 + this.horizontalOffset
+      });
+    }
+
+    var position;
+    for (var i = 0; i < positions.length; i++) {
+      var candidate = positions[i];
+      var vAlignOk = candidate.verticalAlign === vAlign;
+      var hAlignOk = candidate.horizontalAlign === hAlign;
+
+      // If both vAlign and hAlign are defined, return exact match.
+      // For dynamicAlign and noOverlap we'll have more than one candidate, so
+      // we'll have to check the offscreenArea to make the best choice.
+      if (!this.dynamicAlign && !this.noOverlap && vAlignOk && hAlignOk) {
+        position = candidate;
+        break;
+      }
+
+      // Align is ok if alignment preferences are respected. If no preferences,
+      // it is considered ok.
+      var alignOk = (!vAlign || vAlignOk) && (!hAlign || hAlignOk);
+
+      // Filter out elements that don't match the alignment (if defined).
+      // With dynamicAlign, we need to consider all the positions to find the
+      // one that minimizes the cropped area.
+      if (!this.dynamicAlign && !alignOk) {
+        continue;
+      }
+
+      candidate.offscreenArea =
+          this.__getOffscreenArea(candidate, size, fitRect);
+      // If not cropped and respects the align requirements, keep it.
+      // This allows to prefer positions overlapping horizontally over the
+      // ones overlapping vertically.
+      if (candidate.offscreenArea === 0 && alignOk) {
+        position = candidate;
+        break;
+      }
+      position = position || candidate;
+      var diff = candidate.offscreenArea - position.offscreenArea;
+      // Check which crops less. If it crops equally, check if at least one
+      // align setting is ok.
+      if (diff < 0 || (diff === 0 && (vAlignOk || hAlignOk))) {
+        position = candidate;
+      }
+    }
+
+    return position;
+  }
+
+};
diff --git a/third_party/polymer/v3_0/components-chromium/iron-flex-layout/iron-flex-layout-classes.js b/third_party/polymer/v3_0/components-chromium/iron-flex-layout/iron-flex-layout-classes.js
new file mode 100644
index 0000000..54edcb4
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-flex-layout/iron-flex-layout-classes.js
@@ -0,0 +1,443 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+
+/*
+A set of layout classes that let you specify layout properties directly in
+markup. You must include this file in every element that needs to use them.
+
+Sample use:
+
+    import '../iron-flex-layout/iron-flex-layout-classes.js';
+
+    const template = html`
+      <style is="custom-style" include="iron-flex iron-flex-alignment"></style>
+      <style>
+        .test { width: 100px; }
+      </style>
+      <div class="layout horizontal center-center">
+        <div class="test">horizontal layout center alignment</div>
+      </div>
+    `;
+    document.body.appendChild(template.content);
+
+The following imports are available:
+ - iron-flex
+ - iron-flex-reverse
+ - iron-flex-alignment
+ - iron-flex-factors
+ - iron-positioning
+*/
+
+const template = html`
+/* Most common used flex styles*/
+<dom-module id="iron-flex">
+  <template>
+    <style>
+      .layout.horizontal,
+      .layout.vertical {
+        display: -ms-flexbox;
+        display: -webkit-flex;
+        display: flex;
+      }
+
+      .layout.inline {
+        display: -ms-inline-flexbox;
+        display: -webkit-inline-flex;
+        display: inline-flex;
+      }
+
+      .layout.horizontal {
+        -ms-flex-direction: row;
+        -webkit-flex-direction: row;
+        flex-direction: row;
+      }
+
+      .layout.vertical {
+        -ms-flex-direction: column;
+        -webkit-flex-direction: column;
+        flex-direction: column;
+      }
+
+      .layout.wrap {
+        -ms-flex-wrap: wrap;
+        -webkit-flex-wrap: wrap;
+        flex-wrap: wrap;
+      }
+
+      .layout.no-wrap {
+        -ms-flex-wrap: nowrap;
+        -webkit-flex-wrap: nowrap;
+        flex-wrap: nowrap;
+      }
+
+      .layout.center,
+      .layout.center-center {
+        -ms-flex-align: center;
+        -webkit-align-items: center;
+        align-items: center;
+      }
+
+      .layout.center-justified,
+      .layout.center-center {
+        -ms-flex-pack: center;
+        -webkit-justify-content: center;
+        justify-content: center;
+      }
+
+      .flex {
+        -ms-flex: 1 1 0.000000001px;
+        -webkit-flex: 1;
+        flex: 1;
+        -webkit-flex-basis: 0.000000001px;
+        flex-basis: 0.000000001px;
+      }
+
+      .flex-auto {
+        -ms-flex: 1 1 auto;
+        -webkit-flex: 1 1 auto;
+        flex: 1 1 auto;
+      }
+
+      .flex-none {
+        -ms-flex: none;
+        -webkit-flex: none;
+        flex: none;
+      }
+    </style>
+  </template>
+</dom-module>
+/* Basic flexbox reverse styles */
+<dom-module id="iron-flex-reverse">
+  <template>
+    <style>
+      .layout.horizontal-reverse,
+      .layout.vertical-reverse {
+        display: -ms-flexbox;
+        display: -webkit-flex;
+        display: flex;
+      }
+
+      .layout.horizontal-reverse {
+        -ms-flex-direction: row-reverse;
+        -webkit-flex-direction: row-reverse;
+        flex-direction: row-reverse;
+      }
+
+      .layout.vertical-reverse {
+        -ms-flex-direction: column-reverse;
+        -webkit-flex-direction: column-reverse;
+        flex-direction: column-reverse;
+      }
+
+      .layout.wrap-reverse {
+        -ms-flex-wrap: wrap-reverse;
+        -webkit-flex-wrap: wrap-reverse;
+        flex-wrap: wrap-reverse;
+      }
+    </style>
+  </template>
+</dom-module>
+/* Flexbox alignment */
+<dom-module id="iron-flex-alignment">
+  <template>
+    <style>
+      /**
+       * Alignment in cross axis.
+       */
+      .layout.start {
+        -ms-flex-align: start;
+        -webkit-align-items: flex-start;
+        align-items: flex-start;
+      }
+
+      .layout.center,
+      .layout.center-center {
+        -ms-flex-align: center;
+        -webkit-align-items: center;
+        align-items: center;
+      }
+
+      .layout.end {
+        -ms-flex-align: end;
+        -webkit-align-items: flex-end;
+        align-items: flex-end;
+      }
+
+      .layout.baseline {
+        -ms-flex-align: baseline;
+        -webkit-align-items: baseline;
+        align-items: baseline;
+      }
+
+      /**
+       * Alignment in main axis.
+       */
+      .layout.start-justified {
+        -ms-flex-pack: start;
+        -webkit-justify-content: flex-start;
+        justify-content: flex-start;
+      }
+
+      .layout.center-justified,
+      .layout.center-center {
+        -ms-flex-pack: center;
+        -webkit-justify-content: center;
+        justify-content: center;
+      }
+
+      .layout.end-justified {
+        -ms-flex-pack: end;
+        -webkit-justify-content: flex-end;
+        justify-content: flex-end;
+      }
+
+      .layout.around-justified {
+        -ms-flex-pack: distribute;
+        -webkit-justify-content: space-around;
+        justify-content: space-around;
+      }
+
+      .layout.justified {
+        -ms-flex-pack: justify;
+        -webkit-justify-content: space-between;
+        justify-content: space-between;
+      }
+
+      /**
+       * Self alignment.
+       */
+      .self-start {
+        -ms-align-self: flex-start;
+        -webkit-align-self: flex-start;
+        align-self: flex-start;
+      }
+
+      .self-center {
+        -ms-align-self: center;
+        -webkit-align-self: center;
+        align-self: center;
+      }
+
+      .self-end {
+        -ms-align-self: flex-end;
+        -webkit-align-self: flex-end;
+        align-self: flex-end;
+      }
+
+      .self-stretch {
+        -ms-align-self: stretch;
+        -webkit-align-self: stretch;
+        align-self: stretch;
+      }
+
+      .self-baseline {
+        -ms-align-self: baseline;
+        -webkit-align-self: baseline;
+        align-self: baseline;
+      }
+
+      /**
+       * multi-line alignment in main axis.
+       */
+      .layout.start-aligned {
+        -ms-flex-line-pack: start;  /* IE10 */
+        -ms-align-content: flex-start;
+        -webkit-align-content: flex-start;
+        align-content: flex-start;
+      }
+
+      .layout.end-aligned {
+        -ms-flex-line-pack: end;  /* IE10 */
+        -ms-align-content: flex-end;
+        -webkit-align-content: flex-end;
+        align-content: flex-end;
+      }
+
+      .layout.center-aligned {
+        -ms-flex-line-pack: center;  /* IE10 */
+        -ms-align-content: center;
+        -webkit-align-content: center;
+        align-content: center;
+      }
+
+      .layout.between-aligned {
+        -ms-flex-line-pack: justify;  /* IE10 */
+        -ms-align-content: space-between;
+        -webkit-align-content: space-between;
+        align-content: space-between;
+      }
+
+      .layout.around-aligned {
+        -ms-flex-line-pack: distribute;  /* IE10 */
+        -ms-align-content: space-around;
+        -webkit-align-content: space-around;
+        align-content: space-around;
+      }
+    </style>
+  </template>
+</dom-module>
+/* Non-flexbox positioning helper styles */
+<dom-module id="iron-flex-factors">
+  <template>
+    <style>
+      .flex,
+      .flex-1 {
+        -ms-flex: 1 1 0.000000001px;
+        -webkit-flex: 1;
+        flex: 1;
+        -webkit-flex-basis: 0.000000001px;
+        flex-basis: 0.000000001px;
+      }
+
+      .flex-2 {
+        -ms-flex: 2;
+        -webkit-flex: 2;
+        flex: 2;
+      }
+
+      .flex-3 {
+        -ms-flex: 3;
+        -webkit-flex: 3;
+        flex: 3;
+      }
+
+      .flex-4 {
+        -ms-flex: 4;
+        -webkit-flex: 4;
+        flex: 4;
+      }
+
+      .flex-5 {
+        -ms-flex: 5;
+        -webkit-flex: 5;
+        flex: 5;
+      }
+
+      .flex-6 {
+        -ms-flex: 6;
+        -webkit-flex: 6;
+        flex: 6;
+      }
+
+      .flex-7 {
+        -ms-flex: 7;
+        -webkit-flex: 7;
+        flex: 7;
+      }
+
+      .flex-8 {
+        -ms-flex: 8;
+        -webkit-flex: 8;
+        flex: 8;
+      }
+
+      .flex-9 {
+        -ms-flex: 9;
+        -webkit-flex: 9;
+        flex: 9;
+      }
+
+      .flex-10 {
+        -ms-flex: 10;
+        -webkit-flex: 10;
+        flex: 10;
+      }
+
+      .flex-11 {
+        -ms-flex: 11;
+        -webkit-flex: 11;
+        flex: 11;
+      }
+
+      .flex-12 {
+        -ms-flex: 12;
+        -webkit-flex: 12;
+        flex: 12;
+      }
+    </style>
+  </template>
+</dom-module>
+<dom-module id="iron-positioning">
+  <template>
+    <style>
+      .block {
+        display: block;
+      }
+
+      [hidden] {
+        display: none !important;
+      }
+
+      .invisible {
+        visibility: hidden !important;
+      }
+
+      .relative {
+        position: relative;
+      }
+
+      .fit {
+        position: absolute;
+        top: 0;
+        right: 0;
+        bottom: 0;
+        left: 0;
+      }
+
+      body.fullbleed {
+        margin: 0;
+        height: 100vh;
+      }
+
+      .scroll {
+        -webkit-overflow-scrolling: touch;
+        overflow: auto;
+      }
+
+      /* fixed position */
+      .fixed-bottom,
+      .fixed-left,
+      .fixed-right,
+      .fixed-top {
+        position: fixed;
+      }
+
+      .fixed-top {
+        top: 0;
+        left: 0;
+        right: 0;
+      }
+
+      .fixed-right {
+        top: 0;
+        right: 0;
+        bottom: 0;
+      }
+
+      .fixed-bottom {
+        right: 0;
+        bottom: 0;
+        left: 0;
+      }
+
+      .fixed-left {
+        top: 0;
+        bottom: 0;
+        left: 0;
+      }
+    </style>
+  </template>
+</dom-module>
+`;
+template.setAttribute('style', 'display: none;');
+document.head.appendChild(template.content);
diff --git a/third_party/polymer/v3_0/components-chromium/iron-flex-layout/iron-flex-layout.js b/third_party/polymer/v3_0/components-chromium/iron-flex-layout/iron-flex-layout.js
new file mode 100644
index 0000000..279345c
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-flex-layout/iron-flex-layout.js
@@ -0,0 +1,435 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+
+/**
+The `<iron-flex-layout>` component provides simple ways to use
+[CSS flexible box
+layout](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Flexible_boxes),
+also known as flexbox. Note that this is an old element, that was written
+before all modern browsers had non-prefixed flex styles. As such, nowadays you
+don't really need to use this element anymore, and can use CSS flex styles
+directly in your code.
+
+This component provides two different ways to use flexbox:
+
+1. [Layout
+classes](https://github.com/PolymerElements/iron-flex-layout/tree/master/iron-flex-layout-classes.html).
+The layout class stylesheet provides a simple set of class-based flexbox rules,
+that let you specify layout properties directly in markup. You must include this
+file in every element that needs to use them.
+
+    Sample use:
+
+    ```
+    <custom-element-demo>
+      <template>
+        <script src="../webcomponentsjs/webcomponents-lite.js"></script>
+        <next-code-block></next-code-block>
+      </template>
+    </custom-element-demo>
+    ```
+
+    ```js
+    import {html} from '../polymer/lib/utils/html-tag.js';
+    import '../iron-flex-layout/iron-flex-layout-classes.js';
+
+    const template = html`
+      <style is="custom-style" include="iron-flex iron-flex-alignment"></style>
+      <style>
+        .test { width: 100px; }
+      </style>
+      <div class="layout horizontal center-center">
+        <div class="test">horizontal layout center alignment</div>
+      </div>
+    `;
+    document.body.appendChild(template.content);
+    ```
+
+2. [Custom CSS
+mixins](https://github.com/PolymerElements/iron-flex-layout/blob/master/iron-flex-layout.html).
+The mixin stylesheet includes custom CSS mixins that can be applied inside a CSS
+rule using the `@apply` function.
+
+Please note that the old [/deep/ layout
+classes](https://github.com/PolymerElements/iron-flex-layout/tree/master/classes)
+are deprecated, and should not be used. To continue using layout properties
+directly in markup, please switch to using the new `dom-module`-based
+[layout
+classes](https://github.com/PolymerElements/iron-flex-layout/tree/master/iron-flex-layout-classes.html).
+Please note that the new version does not use `/deep/`, and therefore requires
+you to import the `dom-modules` in every element that needs to use them.
+
+@group Iron Elements
+@pseudoElement iron-flex-layout
+@demo demo/index.html
+*/
+const template = html`
+<custom-style>
+  <style is="custom-style">
+    [hidden] {
+      display: none !important;
+    }
+  </style>
+</custom-style>
+<custom-style>
+  <style is="custom-style">
+    html {
+
+      --layout: {
+        display: -ms-flexbox;
+        display: -webkit-flex;
+        display: flex;
+      };
+
+      --layout-inline: {
+        display: -ms-inline-flexbox;
+        display: -webkit-inline-flex;
+        display: inline-flex;
+      };
+
+      --layout-horizontal: {
+        @apply --layout;
+
+        -ms-flex-direction: row;
+        -webkit-flex-direction: row;
+        flex-direction: row;
+      };
+
+      --layout-horizontal-reverse: {
+        @apply --layout;
+
+        -ms-flex-direction: row-reverse;
+        -webkit-flex-direction: row-reverse;
+        flex-direction: row-reverse;
+      };
+
+      --layout-vertical: {
+        @apply --layout;
+
+        -ms-flex-direction: column;
+        -webkit-flex-direction: column;
+        flex-direction: column;
+      };
+
+      --layout-vertical-reverse: {
+        @apply --layout;
+
+        -ms-flex-direction: column-reverse;
+        -webkit-flex-direction: column-reverse;
+        flex-direction: column-reverse;
+      };
+
+      --layout-wrap: {
+        -ms-flex-wrap: wrap;
+        -webkit-flex-wrap: wrap;
+        flex-wrap: wrap;
+      };
+
+      --layout-wrap-reverse: {
+        -ms-flex-wrap: wrap-reverse;
+        -webkit-flex-wrap: wrap-reverse;
+        flex-wrap: wrap-reverse;
+      };
+
+      --layout-flex-auto: {
+        -ms-flex: 1 1 auto;
+        -webkit-flex: 1 1 auto;
+        flex: 1 1 auto;
+      };
+
+      --layout-flex-none: {
+        -ms-flex: none;
+        -webkit-flex: none;
+        flex: none;
+      };
+
+      --layout-flex: {
+        -ms-flex: 1 1 0.000000001px;
+        -webkit-flex: 1;
+        flex: 1;
+        -webkit-flex-basis: 0.000000001px;
+        flex-basis: 0.000000001px;
+      };
+
+      --layout-flex-2: {
+        -ms-flex: 2;
+        -webkit-flex: 2;
+        flex: 2;
+      };
+
+      --layout-flex-3: {
+        -ms-flex: 3;
+        -webkit-flex: 3;
+        flex: 3;
+      };
+
+      --layout-flex-4: {
+        -ms-flex: 4;
+        -webkit-flex: 4;
+        flex: 4;
+      };
+
+      --layout-flex-5: {
+        -ms-flex: 5;
+        -webkit-flex: 5;
+        flex: 5;
+      };
+
+      --layout-flex-6: {
+        -ms-flex: 6;
+        -webkit-flex: 6;
+        flex: 6;
+      };
+
+      --layout-flex-7: {
+        -ms-flex: 7;
+        -webkit-flex: 7;
+        flex: 7;
+      };
+
+      --layout-flex-8: {
+        -ms-flex: 8;
+        -webkit-flex: 8;
+        flex: 8;
+      };
+
+      --layout-flex-9: {
+        -ms-flex: 9;
+        -webkit-flex: 9;
+        flex: 9;
+      };
+
+      --layout-flex-10: {
+        -ms-flex: 10;
+        -webkit-flex: 10;
+        flex: 10;
+      };
+
+      --layout-flex-11: {
+        -ms-flex: 11;
+        -webkit-flex: 11;
+        flex: 11;
+      };
+
+      --layout-flex-12: {
+        -ms-flex: 12;
+        -webkit-flex: 12;
+        flex: 12;
+      };
+
+      /* alignment in cross axis */
+
+      --layout-start: {
+        -ms-flex-align: start;
+        -webkit-align-items: flex-start;
+        align-items: flex-start;
+      };
+
+      --layout-center: {
+        -ms-flex-align: center;
+        -webkit-align-items: center;
+        align-items: center;
+      };
+
+      --layout-end: {
+        -ms-flex-align: end;
+        -webkit-align-items: flex-end;
+        align-items: flex-end;
+      };
+
+      --layout-baseline: {
+        -ms-flex-align: baseline;
+        -webkit-align-items: baseline;
+        align-items: baseline;
+      };
+
+      /* alignment in main axis */
+
+      --layout-start-justified: {
+        -ms-flex-pack: start;
+        -webkit-justify-content: flex-start;
+        justify-content: flex-start;
+      };
+
+      --layout-center-justified: {
+        -ms-flex-pack: center;
+        -webkit-justify-content: center;
+        justify-content: center;
+      };
+
+      --layout-end-justified: {
+        -ms-flex-pack: end;
+        -webkit-justify-content: flex-end;
+        justify-content: flex-end;
+      };
+
+      --layout-around-justified: {
+        -ms-flex-pack: distribute;
+        -webkit-justify-content: space-around;
+        justify-content: space-around;
+      };
+
+      --layout-justified: {
+        -ms-flex-pack: justify;
+        -webkit-justify-content: space-between;
+        justify-content: space-between;
+      };
+
+      --layout-center-center: {
+        @apply --layout-center;
+        @apply --layout-center-justified;
+      };
+
+      /* self alignment */
+
+      --layout-self-start: {
+        -ms-align-self: flex-start;
+        -webkit-align-self: flex-start;
+        align-self: flex-start;
+      };
+
+      --layout-self-center: {
+        -ms-align-self: center;
+        -webkit-align-self: center;
+        align-self: center;
+      };
+
+      --layout-self-end: {
+        -ms-align-self: flex-end;
+        -webkit-align-self: flex-end;
+        align-self: flex-end;
+      };
+
+      --layout-self-stretch: {
+        -ms-align-self: stretch;
+        -webkit-align-self: stretch;
+        align-self: stretch;
+      };
+
+      --layout-self-baseline: {
+        -ms-align-self: baseline;
+        -webkit-align-self: baseline;
+        align-self: baseline;
+      };
+
+      /* multi-line alignment in main axis */
+
+      --layout-start-aligned: {
+        -ms-flex-line-pack: start;  /* IE10 */
+        -ms-align-content: flex-start;
+        -webkit-align-content: flex-start;
+        align-content: flex-start;
+      };
+
+      --layout-end-aligned: {
+        -ms-flex-line-pack: end;  /* IE10 */
+        -ms-align-content: flex-end;
+        -webkit-align-content: flex-end;
+        align-content: flex-end;
+      };
+
+      --layout-center-aligned: {
+        -ms-flex-line-pack: center;  /* IE10 */
+        -ms-align-content: center;
+        -webkit-align-content: center;
+        align-content: center;
+      };
+
+      --layout-between-aligned: {
+        -ms-flex-line-pack: justify;  /* IE10 */
+        -ms-align-content: space-between;
+        -webkit-align-content: space-between;
+        align-content: space-between;
+      };
+
+      --layout-around-aligned: {
+        -ms-flex-line-pack: distribute;  /* IE10 */
+        -ms-align-content: space-around;
+        -webkit-align-content: space-around;
+        align-content: space-around;
+      };
+
+      /*******************************
+                Other Layout
+      *******************************/
+
+      --layout-block: {
+        display: block;
+      };
+
+      --layout-invisible: {
+        visibility: hidden !important;
+      };
+
+      --layout-relative: {
+        position: relative;
+      };
+
+      --layout-fit: {
+        position: absolute;
+        top: 0;
+        right: 0;
+        bottom: 0;
+        left: 0;
+      };
+
+      --layout-scroll: {
+        -webkit-overflow-scrolling: touch;
+        overflow: auto;
+      };
+
+      --layout-fullbleed: {
+        margin: 0;
+        height: 100vh;
+      };
+
+      /* fixed position */
+
+      --layout-fixed-top: {
+        position: fixed;
+        top: 0;
+        left: 0;
+        right: 0;
+      };
+
+      --layout-fixed-right: {
+        position: fixed;
+        top: 0;
+        right: 0;
+        bottom: 0;
+      };
+
+      --layout-fixed-bottom: {
+        position: fixed;
+        right: 0;
+        bottom: 0;
+        left: 0;
+      };
+
+      --layout-fixed-left: {
+        position: fixed;
+        top: 0;
+        bottom: 0;
+        left: 0;
+      };
+
+    }
+  </style>
+</custom-style>`;
+
+template.setAttribute('style', 'display: none;');
+document.head.appendChild(template.content);
+
+var style = document.createElement('style');
+style.textContent = '[hidden] { display: none !important; }';
+document.head.appendChild(style);
diff --git a/third_party/polymer/v3_0/components-chromium/iron-icon/iron-icon.js b/third_party/polymer/v3_0/components-chromium/iron-icon/iron-icon.js
new file mode 100644
index 0000000..001d8d5
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-icon/iron-icon.js
@@ -0,0 +1,200 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../iron-flex-layout/iron-flex-layout.js';
+
+import {IronMeta} from '../iron-meta/iron-meta.js';
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+import {Base} from '../polymer/polymer-legacy.js';
+
+/**
+
+The `iron-icon` element displays an icon. By default an icon renders as a 24px
+square.
+
+Example using src:
+
+    <iron-icon src="star.png"></iron-icon>
+
+Example setting size to 32px x 32px:
+
+    <iron-icon class="big" src="big_star.png"></iron-icon>
+
+    <style is="custom-style">
+      .big {
+        --iron-icon-height: 32px;
+        --iron-icon-width: 32px;
+      }
+    </style>
+
+The iron elements include several sets of icons. To use the default set of
+icons, import `iron-icons.js` and use the `icon` attribute to specify an icon:
+
+    <script type="module">
+      import "../iron-icons/iron-icons.js";
+    </script>
+
+    <iron-icon icon="menu"></iron-icon>
+
+To use a different built-in set of icons, import the specific
+`iron-icons/<iconset>-icons.js`, and specify the icon as `<iconset>:<icon>`.
+For example, to use a communication icon, you would use:
+
+    <script type="module">
+      import "../iron-icons/communication-icons.js";
+    </script>
+
+    <iron-icon icon="communication:email"></iron-icon>
+
+You can also create custom icon sets of bitmap or SVG icons.
+
+Example of using an icon named `cherry` from a custom iconset with the ID
+`fruit`:
+
+    <iron-icon icon="fruit:cherry"></iron-icon>
+
+See `<iron-iconset>` and `<iron-iconset-svg>` for more information about how to
+create a custom iconset.
+
+See the `iron-icons` demo to see the icons available in the various iconsets.
+
+### Styling
+
+The following custom properties are available for styling:
+
+Custom property | Description | Default
+----------------|-------------|----------
+`--iron-icon` | Mixin applied to the icon | {}
+`--iron-icon-width` | Width of the icon | `24px`
+`--iron-icon-height` | Height of the icon | `24px`
+`--iron-icon-fill-color` | Fill color of the svg icon | `currentcolor`
+`--iron-icon-stroke-color` | Stroke color of the svg icon | none
+
+@group Iron Elements
+@element iron-icon
+@demo demo/index.html
+@hero hero.svg
+@homepage polymer.github.io
+*/
+Polymer({
+  _template: html`
+    <style>
+      :host {
+        @apply --layout-inline;
+        @apply --layout-center-center;
+        position: relative;
+
+        vertical-align: middle;
+
+        fill: var(--iron-icon-fill-color, currentcolor);
+        stroke: var(--iron-icon-stroke-color, none);
+
+        width: var(--iron-icon-width, 24px);
+        height: var(--iron-icon-height, 24px);
+        @apply --iron-icon;
+      }
+
+      :host([hidden]) {
+        display: none;
+      }
+    </style>
+`,
+
+  is: 'iron-icon',
+
+  properties: {
+
+    /**
+     * The name of the icon to use. The name should be of the form:
+     * `iconset_name:icon_name`.
+     */
+    icon: {type: String},
+
+    /**
+     * The name of the theme to used, if one is specified by the
+     * iconset.
+     */
+    theme: {type: String},
+
+    /**
+     * If using iron-icon without an iconset, you can set the src to be
+     * the URL of an individual icon image file. Note that this will take
+     * precedence over a given icon attribute.
+     */
+    src: {type: String},
+
+    /**
+     * @type {!IronMeta}
+     */
+    _meta: {value: Base.create('iron-meta', {type: 'iconset'})}
+
+  },
+
+  observers: [
+    '_updateIcon(_meta, isAttached)',
+    '_updateIcon(theme, isAttached)',
+    '_srcChanged(src, isAttached)',
+    '_iconChanged(icon, isAttached)'
+  ],
+
+  _DEFAULT_ICONSET: 'icons',
+
+  _iconChanged: function(icon) {
+    var parts = (icon || '').split(':');
+    this._iconName = parts.pop();
+    this._iconsetName = parts.pop() || this._DEFAULT_ICONSET;
+    this._updateIcon();
+  },
+
+  _srcChanged: function(src) {
+    this._updateIcon();
+  },
+
+  _usesIconset: function() {
+    return this.icon || !this.src;
+  },
+
+  /** @suppress {visibility} */
+  _updateIcon: function() {
+    if (this._usesIconset()) {
+      if (this._img && this._img.parentNode) {
+        dom(this.root).removeChild(this._img);
+      }
+      if (this._iconName === '') {
+        if (this._iconset) {
+          this._iconset.removeIcon(this);
+        }
+      } else if (this._iconsetName && this._meta) {
+        this._iconset = /** @type {?Polymer.Iconset} */ (
+            this._meta.byKey(this._iconsetName));
+        if (this._iconset) {
+          this._iconset.applyIcon(this, this._iconName, this.theme);
+          this.unlisten(window, 'iron-iconset-added', '_updateIcon');
+        } else {
+          this.listen(window, 'iron-iconset-added', '_updateIcon');
+        }
+      }
+    } else {
+      if (this._iconset) {
+        this._iconset.removeIcon(this);
+      }
+      if (!this._img) {
+        this._img = document.createElement('img');
+        this._img.style.width = '100%';
+        this._img.style.height = '100%';
+        this._img.draggable = false;
+      }
+      this._img.src = this.src;
+      dom(this.root).appendChild(this._img);
+    }
+  }
+});
diff --git a/third_party/polymer/v3_0/components-chromium/iron-iconset-svg/iron-iconset-svg.js b/third_party/polymer/v3_0/components-chromium/iron-iconset-svg/iron-iconset-svg.js
new file mode 100644
index 0000000..c76a530
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-iconset-svg/iron-iconset-svg.js
@@ -0,0 +1,257 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {IronMeta} from '../iron-meta/iron-meta.js';
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+/**
+ * The `iron-iconset-svg` element allows users to define their own icon sets
+ * that contain svg icons. The svg icon elements should be children of the
+ * `iron-iconset-svg` element. Multiple icons should be given distinct id's.
+ *
+ * Using svg elements to create icons has a few advantages over traditional
+ * bitmap graphics like jpg or png. Icons that use svg are vector based so
+ * they are resolution independent and should look good on any device. They
+ * are stylable via css. Icons can be themed, colorized, and even animated.
+ *
+ * Example:
+ *
+ *     <iron-iconset-svg name="my-svg-icons" size="24">
+ *       <svg>
+ *         <defs>
+ *           <g id="shape">
+ *             <rect x="12" y="0" width="12" height="24" />
+ *             <circle cx="12" cy="12" r="12" />
+ *           </g>
+ *         </defs>
+ *       </svg>
+ *     </iron-iconset-svg>
+ *
+ * This will automatically register the icon set "my-svg-icons" to the iconset
+ * database.  To use these icons from within another element, make a
+ * `iron-iconset` element and call the `byId` method
+ * to retrieve a given iconset. To apply a particular icon inside an
+ * element use the `applyIcon` method. For example:
+ *
+ *     iconset.applyIcon(iconNode, 'car');
+ *
+ * @element iron-iconset-svg
+ * @demo demo/index.html
+ * @implements {Polymer.Iconset}
+ */
+Polymer({
+  is: 'iron-iconset-svg',
+
+  properties: {
+
+    /**
+     * The name of the iconset.
+     */
+    name: {type: String, observer: '_nameChanged'},
+
+    /**
+     * The size of an individual icon. Note that icons must be square.
+     */
+    size: {type: Number, value: 24},
+
+    /**
+     * Set to true to enable mirroring of icons where specified when they are
+     * stamped. Icons that should be mirrored should be decorated with a
+     * `mirror-in-rtl` attribute.
+     *
+     * NOTE: For performance reasons, direction will be resolved once per
+     * document per iconset, so moving icons in and out of RTL subtrees will
+     * not cause their mirrored state to change.
+     */
+    rtlMirroring: {type: Boolean, value: false},
+
+    /**
+     * Set to true to measure RTL based on the dir attribute on the body or
+     * html elements (measured on document.body or document.documentElement as
+     * available).
+     */
+    useGlobalRtlAttribute: {type: Boolean, value: false}
+  },
+
+  created: function() {
+    this._meta = new IronMeta({type: 'iconset', key: null, value: null});
+  },
+
+  attached: function() {
+    this.style.display = 'none';
+  },
+
+  /**
+   * Construct an array of all icon names in this iconset.
+   *
+   * @return {!Array} Array of icon names.
+   */
+  getIconNames: function() {
+    this._icons = this._createIconMap();
+    return Object.keys(this._icons).map(function(n) {
+      return this.name + ':' + n;
+    }, this);
+  },
+
+  /**
+   * Applies an icon to the given element.
+   *
+   * An svg icon is prepended to the element's shadowRoot if it exists,
+   * otherwise to the element itself.
+   *
+   * If RTL mirroring is enabled, and the icon is marked to be mirrored in
+   * RTL, the element will be tested (once and only once ever for each
+   * iconset) to determine the direction of the subtree the element is in.
+   * This direction will apply to all future icon applications, although only
+   * icons marked to be mirrored will be affected.
+   *
+   * @method applyIcon
+   * @param {Element} element Element to which the icon is applied.
+   * @param {string} iconName Name of the icon to apply.
+   * @return {?Element} The svg element which renders the icon.
+   */
+  applyIcon: function(element, iconName) {
+    // Remove old svg element
+    this.removeIcon(element);
+    // install new svg element
+    var svg = this._cloneIcon(
+        iconName, this.rtlMirroring && this._targetIsRTL(element));
+    if (svg) {
+      // insert svg element into shadow root, if it exists
+      var pde = dom(element.root || element);
+      pde.insertBefore(svg, pde.childNodes[0]);
+      return element._svgIcon = svg;
+    }
+    return null;
+  },
+
+  /**
+   * Remove an icon from the given element by undoing the changes effected
+   * by `applyIcon`.
+   *
+   * @param {Element} element The element from which the icon is removed.
+   */
+  removeIcon: function(element) {
+    // Remove old svg element
+    if (element._svgIcon) {
+      dom(element.root || element).removeChild(element._svgIcon);
+      element._svgIcon = null;
+    }
+  },
+
+  /**
+   * Measures and memoizes the direction of the element. Note that this
+   * measurement is only done once and the result is memoized for future
+   * invocations.
+   */
+  _targetIsRTL: function(target) {
+    if (this.__targetIsRTL == null) {
+      if (this.useGlobalRtlAttribute) {
+        var globalElement =
+            (document.body && document.body.hasAttribute('dir')) ?
+            document.body :
+            document.documentElement;
+
+        this.__targetIsRTL = globalElement.getAttribute('dir') === 'rtl';
+      } else {
+        if (target && target.nodeType !== Node.ELEMENT_NODE) {
+          target = target.host;
+        }
+
+        this.__targetIsRTL =
+            target && window.getComputedStyle(target)['direction'] === 'rtl';
+      }
+    }
+
+    return this.__targetIsRTL;
+  },
+
+  /**
+   *
+   * When name is changed, register iconset metadata
+   *
+   */
+  _nameChanged: function() {
+    this._meta.value = null;
+    this._meta.key = this.name;
+    this._meta.value = this;
+
+    this.async(function() {
+      this.fire('iron-iconset-added', this, {node: window});
+    });
+  },
+
+  /**
+   * Create a map of child SVG elements by id.
+   *
+   * @return {!Object} Map of id's to SVG elements.
+   */
+  _createIconMap: function() {
+    // Objects chained to Object.prototype (`{}`) have members. Specifically,
+    // on FF there is a `watch` method that confuses the icon map, so we
+    // need to use a null-based object here.
+    var icons = Object.create(null);
+    dom(this).querySelectorAll('[id]').forEach(function(icon) {
+      icons[icon.id] = icon;
+    });
+    return icons;
+  },
+
+  /**
+   * Produce installable clone of the SVG element matching `id` in this
+   * iconset, or `undefined` if there is no matching element.
+   *
+   * @return {Element} Returns an installable clone of the SVG element
+   * matching `id`.
+   */
+  _cloneIcon: function(id, mirrorAllowed) {
+    // create the icon map on-demand, since the iconset itself has no discrete
+    // signal to know when it's children are fully parsed
+    this._icons = this._icons || this._createIconMap();
+    return this._prepareSvgClone(this._icons[id], this.size, mirrorAllowed);
+  },
+
+  /**
+   * @param {Element} sourceSvg
+   * @param {number} size
+   * @param {Boolean} mirrorAllowed
+   * @return {Element}
+   */
+  _prepareSvgClone: function(sourceSvg, size, mirrorAllowed) {
+    if (sourceSvg) {
+      var content = sourceSvg.cloneNode(true),
+          svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
+          viewBox =
+              content.getAttribute('viewBox') || '0 0 ' + size + ' ' + size,
+          cssText =
+              'pointer-events: none; display: block; width: 100%; height: 100%;';
+
+      if (mirrorAllowed && content.hasAttribute('mirror-in-rtl')) {
+        cssText +=
+            '-webkit-transform:scale(-1,1);transform:scale(-1,1);transform-origin:center;';
+      }
+
+      svg.setAttribute('viewBox', viewBox);
+      svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
+      svg.setAttribute('focusable', 'false');
+      // TODO(dfreedm): `pointer-events: none` works around
+      // https://crbug.com/370136
+      // TODO(sjmiles): inline style may not be ideal, but avoids requiring a
+      // shadow-root
+      svg.style.cssText = cssText;
+      svg.appendChild(content).removeAttribute('id');
+      return svg;
+    }
+    return null;
+  }
+
+});
diff --git a/third_party/polymer/v3_0/components-chromium/iron-input/iron-input.js b/third_party/polymer/v3_0/components-chromium/iron-input/iron-input.js
new file mode 100644
index 0000000..3c85279
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-input/iron-input.js
@@ -0,0 +1,368 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {IronA11yAnnouncer} from '../iron-a11y-announcer/iron-a11y-announcer.js';
+import {IronValidatableBehavior} from '../iron-validatable-behavior/iron-validatable-behavior.js';
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+
+/**
+`<iron-input>` is a wrapper to a native `<input>` element, that adds two-way
+binding and prevention of invalid input. To use it, you must distribute a native
+`<input>` yourself. You can continue to use the native `input` as you would
+normally:
+
+    <iron-input>
+      <input>
+    </iron-input>
+
+    <iron-input>
+      <input type="email" disabled>
+    </iron-input>
+
+### Two-way binding
+
+By default you can only get notified of changes to a native `<input>`'s `value`
+due to user input:
+
+    <input value="{{myValue::input}}">
+
+This means that if you imperatively set the value (i.e. `someNativeInput.value =
+'foo'`), no events will be fired and this change cannot be observed.
+
+`iron-input` adds the `bind-value` property that mirrors the native `input`'s
+'`value` property; this property can be used for two-way data binding.
+`bind-value` will notify if it is changed either by user input or by script.
+
+    <iron-input bind-value="{{myValue}}">
+      <input>
+    </iron-input>
+
+Note: this means that if you want to imperatively set the native `input`'s, you
+_must_ set `bind-value` instead, so that the wrapper `iron-input` can be
+notified.
+
+### Validation
+
+`iron-input` uses the native `input`'s validation. For simplicity, `iron-input`
+has a `validate()` method (which internally just checks the distributed
+`input`'s validity), which sets an `invalid` attribute that can also be used for
+styling.
+
+To validate automatically as you type, you can use the `auto-validate`
+attribute.
+
+`iron-input` also fires an `iron-input-validate` event after `validate()` is
+called. You can use it to implement a custom validator:
+
+    var CatsOnlyValidator = {
+      validate: function(ironInput) {
+        var valid = !ironInput.bindValue || ironInput.bindValue === 'cat';
+        ironInput.invalid = !valid;
+        return valid;
+      }
+    }
+    ironInput.addEventListener('iron-input-validate', function() {
+      CatsOnly.validate(input2);
+    });
+
+You can also use an element implementing an
+[`IronValidatorBehavior`](/element/PolymerElements/iron-validatable-behavior).
+This example can also be found in the demo for this element:
+
+    <iron-input validator="cats-only">
+      <input>
+    </iron-input>
+
+### Preventing invalid input
+
+It may be desirable to only allow users to enter certain characters. You can use
+the `allowed-pattern` attribute to accomplish this. This feature is separate
+from validation, and `allowed-pattern` does not affect how the input is
+validated.
+
+    // Only allow typing digits, but a valid input has exactly 5 digits.
+    <iron-input allowed-pattern="[0-9]">
+      <input pattern="\d{5}">
+    </iron-input>
+
+@demo demo/index.html
+*/
+Polymer({
+  _template: html`
+    <style>
+      :host {
+        display: inline-block;
+      }
+    </style>
+    <slot id="content"></slot>
+`,
+
+  is: 'iron-input',
+  behaviors: [IronValidatableBehavior],
+
+  /**
+   * Fired whenever `validate()` is called.
+   *
+   * @event iron-input-validate
+   */
+
+  properties: {
+
+    /**
+     * Use this property instead of `value` for two-way data binding, or to
+     * set a default value for the input. **Do not** use the distributed
+     * input's `value` property to set a default value.
+     */
+    bindValue: {type: String, value: ''},
+
+    /**
+     * Computed property that echoes `bindValue` (mostly used for Polymer 1.0
+     * backcompatibility, if you were one-way binding to the Polymer 1.0
+     * `input is="iron-input"` value attribute).
+     */
+    value: {type: String, computed: '_computeValue(bindValue)'},
+
+    /**
+     * Regex-like list of characters allowed as input; all characters not in the
+     * list will be rejected. The recommended format should be a list of allowed
+     * characters, for example, `[a-zA-Z0-9.+-!;:]`.
+     *
+     * This pattern represents the allowed characters for the field; as the user
+     * inputs text, each individual character will be checked against the
+     * pattern (rather than checking the entire value as a whole). If a
+     * character is not a match, it will be rejected.
+     *
+     * Pasted input will have each character checked individually; if any
+     * character doesn't match `allowedPattern`, the entire pasted string will
+     * be rejected.
+     *
+     * Note: if you were using `iron-input` in 1.0, you were also required to
+     * set `prevent-invalid-input`. This is no longer needed as of Polymer 2.0,
+     * and will be set automatically for you if an `allowedPattern` is provided.
+     *
+     */
+    allowedPattern: {type: String},
+
+    /**
+     * Set to true to auto-validate the input value as you type.
+     */
+    autoValidate: {type: Boolean, value: false},
+
+    /**
+     * The native input element.
+     */
+    _inputElement: Object,
+  },
+
+  observers: ['_bindValueChanged(bindValue, _inputElement)'],
+  listeners: {'input': '_onInput', 'keypress': '_onKeypress'},
+
+  created: function() {
+    IronA11yAnnouncer.requestAvailability();
+    this._previousValidInput = '';
+    this._patternAlreadyChecked = false;
+  },
+
+  attached: function() {
+    // If the input is added at a later time, update the internal reference.
+    this._observer = dom(this).observeNodes(function(info) {
+      this._initSlottedInput();
+    }.bind(this));
+  },
+
+  detached: function() {
+    if (this._observer) {
+      dom(this).unobserveNodes(this._observer);
+      this._observer = null;
+    }
+  },
+
+  /**
+   * Returns the distributed input element.
+   */
+  get inputElement() {
+    return this._inputElement;
+  },
+
+  _initSlottedInput: function() {
+    this._inputElement = this.getEffectiveChildren()[0];
+
+    if (this.inputElement && this.inputElement.value) {
+      this.bindValue = this.inputElement.value;
+    }
+
+    this.fire('iron-input-ready');
+  },
+
+  get _patternRegExp() {
+    var pattern;
+    if (this.allowedPattern) {
+      pattern = new RegExp(this.allowedPattern);
+    } else {
+      switch (this.inputElement.type) {
+        case 'number':
+          pattern = /[0-9.,e-]/;
+          break;
+      }
+    }
+    return pattern;
+  },
+
+  /**
+   * @suppress {checkTypes}
+   */
+  _bindValueChanged: function(bindValue, inputElement) {
+    // The observer could have run before attached() when we have actually
+    // initialized this property.
+    if (!inputElement) {
+      return;
+    }
+
+    if (bindValue === undefined) {
+      inputElement.value = null;
+    } else if (bindValue !== inputElement.value) {
+      this.inputElement.value = bindValue;
+    }
+
+    if (this.autoValidate) {
+      this.validate();
+    }
+
+    // manually notify because we don't want to notify until after setting value
+    this.fire('bind-value-changed', {value: bindValue});
+  },
+
+  _onInput: function() {
+    // Need to validate each of the characters pasted if they haven't
+    // been validated inside `_onKeypress` already.
+    if (this.allowedPattern && !this._patternAlreadyChecked) {
+      var valid = this._checkPatternValidity();
+      if (!valid) {
+        this._announceInvalidCharacter(
+            'Invalid string of characters not entered.');
+        this.inputElement.value = this._previousValidInput;
+      }
+    }
+    this.bindValue = this._previousValidInput = this.inputElement.value;
+    this._patternAlreadyChecked = false;
+  },
+
+  _isPrintable: function(event) {
+    // What a control/printable character is varies wildly based on the browser.
+    // - most control characters (arrows, backspace) do not send a `keypress`
+    // event
+    //   in Chrome, but the *do* on Firefox
+    // - in Firefox, when they do send a `keypress` event, control chars have
+    //   a charCode = 0, keyCode = xx (for ex. 40 for down arrow)
+    // - printable characters always send a keypress event.
+    // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the
+    // keyCode
+    //   always matches the charCode.
+    // None of this makes any sense.
+
+    // For these keys, ASCII code == browser keycode.
+    var anyNonPrintable = (event.keyCode == 8) ||  // backspace
+        (event.keyCode == 9) ||                    // tab
+        (event.keyCode == 13) ||                   // enter
+        (event.keyCode == 27);                     // escape
+
+    // For these keys, make sure it's a browser keycode and not an ASCII code.
+    var mozNonPrintable = (event.keyCode == 19) ||  // pause
+        (event.keyCode == 20) ||                    // caps lock
+        (event.keyCode == 45) ||                    // insert
+        (event.keyCode == 46) ||                    // delete
+        (event.keyCode == 144) ||                   // num lock
+        (event.keyCode == 145) ||                   // scroll lock
+        (event.keyCode > 32 &&
+         event.keyCode < 41) ||  // page up/down, end, home, arrows
+        (event.keyCode > 111 && event.keyCode < 124);  // fn keys
+
+    return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable);
+  },
+
+  _onKeypress: function(event) {
+    if (!this.allowedPattern && this.inputElement.type !== 'number') {
+      return;
+    }
+    var regexp = this._patternRegExp;
+    if (!regexp) {
+      return;
+    }
+
+    // Handle special keys and backspace
+    if (event.metaKey || event.ctrlKey || event.altKey) {
+      return;
+    }
+
+    // Check the pattern either here or in `_onInput`, but not in both.
+    this._patternAlreadyChecked = true;
+
+    var thisChar = String.fromCharCode(event.charCode);
+    if (this._isPrintable(event) && !regexp.test(thisChar)) {
+      event.preventDefault();
+      this._announceInvalidCharacter(
+          'Invalid character ' + thisChar + ' not entered.');
+    }
+  },
+
+  _checkPatternValidity: function() {
+    var regexp = this._patternRegExp;
+    if (!regexp) {
+      return true;
+    }
+    for (var i = 0; i < this.inputElement.value.length; i++) {
+      if (!regexp.test(this.inputElement.value[i])) {
+        return false;
+      }
+    }
+    return true;
+  },
+
+  /**
+   * Returns true if `value` is valid. The validator provided in `validator`
+   * will be used first, then any constraints.
+   * @return {boolean} True if the value is valid.
+   */
+  validate: function() {
+    if (!this.inputElement) {
+      this.invalid = false;
+      return true;
+    }
+
+    // Use the nested input's native validity.
+    var valid = this.inputElement.checkValidity();
+
+    // Only do extra checking if the browser thought this was valid.
+    if (valid) {
+      // Empty, required input is invalid
+      if (this.required && this.bindValue === '') {
+        valid = false;
+      } else if (this.hasValidator()) {
+        valid = IronValidatableBehavior.validate.call(this, this.bindValue);
+      }
+    }
+
+    this.invalid = !valid;
+    this.fire('iron-input-validate');
+    return valid;
+  },
+
+  _announceInvalidCharacter: function(message) {
+    this.fire('iron-announce', {text: message});
+  },
+
+  _computeValue: function(bindValue) {
+    return bindValue;
+  }
+});
diff --git a/third_party/polymer/v3_0/components-chromium/iron-list/iron-list.js b/third_party/polymer/v3_0/components-chromium/iron-list/iron-list.js
new file mode 100644
index 0000000..62f6768
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-list/iron-list.js
@@ -0,0 +1,1960 @@
+/**
+@license
+Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+import '../iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
+
+import {IronResizableBehavior} from '../iron-resizable-behavior/iron-resizable-behavior.js';
+import {IronScrollTargetBehavior} from '../iron-scroll-target-behavior/iron-scroll-target-behavior.js';
+import {OptionalMutableDataBehavior} from '../polymer/lib/legacy/mutable-data-behavior.js';
+import {Polymer as Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+import {Templatizer} from '../polymer/lib/legacy/templatizer-behavior.js';
+import {animationFrame, idlePeriod, microTask} from '../polymer/lib/utils/async.js';
+import {Debouncer} from '../polymer/lib/utils/debounce.js';
+import {enqueueDebouncer, flush} from '../polymer/lib/utils/flush.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+import {matches, translate} from '../polymer/lib/utils/path.js';
+import {TemplateInstanceBase} from '../polymer/lib/utils/templatize.js';
+
+var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
+var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
+var DEFAULT_PHYSICAL_COUNT = 3;
+var HIDDEN_Y = '-10000px';
+var SECRET_TABINDEX = -100;
+
+/**
+
+`iron-list` displays a virtual, 'infinite' list. The template inside
+the iron-list element represents the DOM to create for each list item.
+The `items` property specifies an array of list item data.
+
+For performance reasons, not every item in the list is rendered at once;
+instead a small subset of actual template elements *(enough to fill the
+viewport)* are rendered and reused as the user scrolls. As such, it is important
+that all state of the list template is bound to the model driving it, since the
+view may be reused with a new model at any time. Particularly, any state that
+may change as the result of a user interaction with the list item must be bound
+to the model to avoid view state inconsistency.
+
+### Sizing iron-list
+
+`iron-list` must either be explicitly sized, or delegate scrolling to an
+explicitly sized parent. By "explicitly sized", we mean it either has an
+explicit CSS `height` property set via a class or inline style, or else is sized
+by other layout means (e.g. the `flex` or `fit` classes).
+
+#### Flexbox - [jsbin](https://jsbin.com/vejoni/edit?html,output)
+
+```html
+<template is="x-list">
+  <style>
+    :host {
+      display: block;
+      height: 100vh;
+      display: flex;
+      flex-direction: column;
+    }
+
+    iron-list {
+      flex: 1 1 auto;
+    }
+  </style>
+  <app-toolbar>App name</app-toolbar>
+  <iron-list items="[[items]]">
+    <template>
+      <div>
+        ...
+      </div>
+    </template>
+  </iron-list>
+</template>
+```
+#### Explicit size - [jsbin](https://jsbin.com/vopucus/edit?html,output)
+```html
+<template is="x-list">
+  <style>
+    :host {
+      display: block;
+    }
+
+    iron-list {
+      height: 100vh; /* don't use % values unless the parent element is sized.
+*\/
+    }
+  </style>
+  <iron-list items="[[items]]">
+    <template>
+      <div>
+        ...
+      </div>
+    </template>
+  </iron-list>
+</template>
+```
+#### Main document scrolling -
+[jsbin](https://jsbin.com/wevirow/edit?html,output)
+```html
+<head>
+  <style>
+    body {
+      height: 100vh;
+      margin: 0;
+      display: flex;
+      flex-direction: column;
+    }
+
+    app-toolbar {
+      position: fixed;
+      top: 0;
+      left: 0;
+      right: 0;
+    }
+
+    iron-list {
+      /* add padding since the app-toolbar is fixed at the top *\/
+      padding-top: 64px;
+    }
+  </style>
+</head>
+<body>
+  <app-toolbar>App name</app-toolbar>
+  <iron-list scroll-target="document">
+    <template>
+      <div>
+        ...
+      </div>
+    </template>
+  </iron-list>
+</body>
+```
+
+`iron-list` must be given a `<template>` which contains exactly one element. In
+the examples above we used a `<div>`, but you can provide any element (including
+custom elements).
+
+### Template model
+
+List item templates should bind to template models of the following structure:
+
+```js
+{
+  index: 0,        // index in the item array
+  selected: false, // true if the current item is selected
+  tabIndex: -1,    // a dynamically generated tabIndex for focus management
+  item: {}         // user data corresponding to items[index]
+}
+```
+
+Alternatively, you can change the property name used as data index by changing
+the `indexAs` property. The `as` property defines the name of the variable to
+add to the binding scope for the array.
+
+For example, given the following `data` array:
+
+##### data.json
+
+```js
+[
+  {"name": "Bob"},
+  {"name": "Tim"},
+  {"name": "Mike"}
+]
+```
+
+The following code would render the list (note the name property is bound from
+the model object provided to the template scope):
+
+```html
+<iron-ajax url="data.json" last-response="{{data}}" auto></iron-ajax>
+<iron-list items="[[data]]" as="item">
+  <template>
+    <div>
+      Name: [[item.name]]
+    </div>
+  </template>
+</iron-list>
+```
+
+### Grid layout
+
+`iron-list` supports a grid layout in addition to linear layout by setting
+the `grid` attribute.  In this case, the list template item must have both fixed
+width and height (e.g. via CSS). Based on this, the number of items
+per row are determined automatically based on the size of the list viewport.
+
+### Accessibility
+
+`iron-list` automatically manages the focus state for the items. It also
+provides a `tabIndex` property within the template scope that can be used for
+keyboard navigation. For example, users can press the up and down keys to move
+to previous and next items in the list:
+
+```html
+<iron-list items="[[data]]" as="item">
+  <template>
+    <div tabindex$="[[tabIndex]]">
+      Name: [[item.name]]
+    </div>
+  </template>
+</iron-list>
+```
+
+### Styling
+
+You can use the `--iron-list-items-container` mixin to style the container of
+items:
+
+```css
+iron-list {
+ --iron-list-items-container: {
+    margin: auto;
+  };
+}
+```
+
+### Resizing
+
+`iron-list` lays out the items when it receives a notification via the
+`iron-resize` event. This event is fired by any element that implements
+`IronResizableBehavior`.
+
+By default, elements such as `iron-pages`, `paper-tabs` or `paper-dialog` will
+trigger this event automatically. If you hide the list manually (e.g. you use
+`display: none`) you might want to implement `IronResizableBehavior` or fire
+this event manually right after the list became visible again. For example:
+
+```js
+document.querySelector('iron-list').fire('iron-resize');
+```
+
+### When should `<iron-list>` be used?
+
+`iron-list` should be used when a page has significantly more DOM nodes than the
+ones visible on the screen. e.g. the page has 500 nodes, but only 20 are visible
+at a time. This is why we refer to it as a `virtual` list. In this case, a
+`dom-repeat` will still create 500 nodes which could slow down the web app, but
+`iron-list` will only create 20.
+
+However, having an `iron-list` does not mean that you can load all the data at
+once. Say you have a million records in the database, you want to split the data
+into pages so you can bring in a page at the time. The page could contain 500
+items, and iron-list will only render 20.
+
+@group Iron Element
+@element iron-list
+@demo demo/index.html
+
+*/
+Polymer({
+  _template: html`
+    <style>
+      :host {
+        display: block;
+      }
+
+      @media only screen and (-webkit-max-device-pixel-ratio: 1) {
+        :host {
+          will-change: transform;
+        }
+      }
+
+      #items {
+        @apply --iron-list-items-container;
+        position: relative;
+      }
+
+      :host(:not([grid])) #items > ::slotted(*) {
+        width: 100%;
+      }
+
+      #items > ::slotted(*) {
+        box-sizing: border-box;
+        margin: 0;
+        position: absolute;
+        top: 0;
+        will-change: transform;
+      }
+    </style>
+
+    <array-selector id="selector" items="{{items}}" selected="{{selectedItems}}" selected-item="{{selectedItem}}"></array-selector>
+
+    <div id="items">
+      <slot></slot>
+    </div>
+`,
+
+  is: 'iron-list',
+
+  properties: {
+
+    /**
+     * An array containing items determining how many instances of the template
+     * to stamp and that that each template instance should bind to.
+     */
+    items: {type: Array},
+
+    /**
+     * The name of the variable to add to the binding scope for the array
+     * element associated with a given template instance.
+     */
+    as: {type: String, value: 'item'},
+
+    /**
+     * The name of the variable to add to the binding scope with the index
+     * for the row.
+     */
+    indexAs: {type: String, value: 'index'},
+
+    /**
+     * The name of the variable to add to the binding scope to indicate
+     * if the row is selected.
+     */
+    selectedAs: {type: String, value: 'selected'},
+
+    /**
+     * When true, the list is rendered as a grid. Grid items must have
+     * fixed width and height set via CSS. e.g.
+     *
+     * ```html
+     * <iron-list grid>
+     *   <template>
+     *      <div style="width: 100px; height: 100px;"> 100x100 </div>
+     *   </template>
+     * </iron-list>
+     * ```
+     */
+    grid: {
+      type: Boolean,
+      value: false,
+      reflectToAttribute: true,
+      observer: '_gridChanged'
+    },
+
+    /**
+     * When true, tapping a row will select the item, placing its data model
+     * in the set of selected items retrievable via the selection property.
+     *
+     * Note that tapping focusable elements within the list item will not
+     * result in selection, since they are presumed to have their * own action.
+     */
+    selectionEnabled: {type: Boolean, value: false},
+
+    /**
+     * When `multiSelection` is false, this is the currently selected item, or
+     * `null` if no item is selected.
+     */
+    selectedItem: {type: Object, notify: true},
+
+    /**
+     * When `multiSelection` is true, this is an array that contains the
+     * selected items.
+     */
+    selectedItems: {type: Object, notify: true},
+
+    /**
+     * When `true`, multiple items may be selected at once (in this case,
+     * `selected` is an array of currently selected items).  When `false`,
+     * only one item may be selected at a time.
+     */
+    multiSelection: {type: Boolean, value: false},
+
+    /**
+     * The offset top from the scrolling element to the iron-list element.
+     * This value can be computed using the position returned by
+     * `getBoundingClientRect()` although it's preferred to use a constant value
+     * when possible.
+     *
+     * This property is useful when an external scrolling element is used and
+     * there's some offset between the scrolling element and the list. For
+     * example: a header is placed above the list.
+     */
+    scrollOffset: {type: Number, value: 0}
+  },
+
+  observers: [
+    '_itemsChanged(items.*)',
+    '_selectionEnabledChanged(selectionEnabled)',
+    '_multiSelectionChanged(multiSelection)',
+    '_setOverflow(scrollTarget, scrollOffset)'
+  ],
+
+  behaviors: [
+    Templatizer,
+    IronResizableBehavior,
+    IronScrollTargetBehavior,
+    OptionalMutableDataBehavior
+  ],
+
+  /**
+   * The ratio of hidden tiles that should remain in the scroll direction.
+   * Recommended value ~0.5, so it will distribute tiles evenly in both
+   * directions.
+   */
+  _ratio: 0.5,
+
+  /**
+   * The padding-top value for the list.
+   */
+  _scrollerPaddingTop: 0,
+
+  /**
+   * This value is the same as `scrollTop`.
+   */
+  _scrollPosition: 0,
+
+  /**
+   * The sum of the heights of all the tiles in the DOM.
+   */
+  _physicalSize: 0,
+
+  /**
+   * The average `offsetHeight` of the tiles observed till now.
+   */
+  _physicalAverage: 0,
+
+  /**
+   * The number of tiles which `offsetHeight` > 0 observed until now.
+   */
+  _physicalAverageCount: 0,
+
+  /**
+   * The Y position of the item rendered in the `_physicalStart`
+   * tile relative to the scrolling list.
+   */
+  _physicalTop: 0,
+
+  /**
+   * The number of items in the list.
+   */
+  _virtualCount: 0,
+
+  /**
+   * The estimated scroll height based on `_physicalAverage`
+   */
+  _estScrollHeight: 0,
+
+  /**
+   * The scroll height of the dom node
+   */
+  _scrollHeight: 0,
+
+  /**
+   * The height of the list. This is referred as the viewport in the context of
+   * list.
+   */
+  _viewportHeight: 0,
+
+  /**
+   * The width of the list. This is referred as the viewport in the context of
+   * list.
+   */
+  _viewportWidth: 0,
+
+  /**
+   * An array of DOM nodes that are currently in the tree
+   * @type {?Array<!TemplateInstanceBase>}
+   */
+  _physicalItems: null,
+
+  /**
+   * An array of heights for each item in `_physicalItems`
+   * @type {?Array<number>}
+   */
+  _physicalSizes: null,
+
+  /**
+   * A cached value for the first visible index.
+   * See `firstVisibleIndex`
+   * @type {?number}
+   */
+  _firstVisibleIndexVal: null,
+
+  /**
+   * A cached value for the last visible index.
+   * See `lastVisibleIndex`
+   * @type {?number}
+   */
+  _lastVisibleIndexVal: null,
+
+  /**
+   * The max number of pages to render. One page is equivalent to the height of
+   * the list.
+   */
+  _maxPages: 2,
+
+  /**
+   * The currently focused physical item.
+   */
+  _focusedItem: null,
+
+  /**
+   * The virtual index of the focused item.
+   */
+  _focusedVirtualIndex: -1,
+
+  /**
+   * The physical index of the focused item.
+   */
+  _focusedPhysicalIndex: -1,
+
+  /**
+   * The the item that is focused if it is moved offscreen.
+   * @private {?TemplatizerNode}
+   */
+  _offscreenFocusedItem: null,
+
+  /**
+   * The item that backfills the `_offscreenFocusedItem` in the physical items
+   * list when that item is moved offscreen.
+   */
+  _focusBackfillItem: null,
+
+  /**
+   * The maximum items per row
+   */
+  _itemsPerRow: 1,
+
+  /**
+   * The width of each grid item
+   */
+  _itemWidth: 0,
+
+  /**
+   * The height of the row in grid layout.
+   */
+  _rowHeight: 0,
+
+  /**
+   * The cost of stamping a template in ms.
+   */
+  _templateCost: 0,
+
+  /**
+   * Needed to pass event.model property to declarative event handlers -
+   * see polymer/polymer#4339.
+   */
+  _parentModel: true,
+
+  /**
+   * The bottom of the physical content.
+   */
+  get _physicalBottom() {
+    return this._physicalTop + this._physicalSize;
+  },
+
+  /**
+   * The bottom of the scroll.
+   */
+  get _scrollBottom() {
+    return this._scrollPosition + this._viewportHeight;
+  },
+
+  /**
+   * The n-th item rendered in the last physical item.
+   */
+  get _virtualEnd() {
+    return this._virtualStart + this._physicalCount - 1;
+  },
+
+  /**
+   * The height of the physical content that isn't on the screen.
+   */
+  get _hiddenContentSize() {
+    var size =
+        this.grid ? this._physicalRows * this._rowHeight : this._physicalSize;
+    return size - this._viewportHeight;
+  },
+
+  /**
+   * The parent node for the _userTemplate.
+   */
+  get _itemsParent() {
+    return dom(dom(this._userTemplate).parentNode);
+  },
+
+  /**
+   * The maximum scroll top value.
+   */
+  get _maxScrollTop() {
+    return this._estScrollHeight - this._viewportHeight + this._scrollOffset;
+  },
+
+  /**
+   * The largest n-th value for an item such that it can be rendered in
+   * `_physicalStart`.
+   */
+  get _maxVirtualStart() {
+    var virtualCount = this._convertIndexToCompleteRow(this._virtualCount);
+    return Math.max(0, virtualCount - this._physicalCount);
+  },
+
+  set _virtualStart(val) {
+    val = this._clamp(val, 0, this._maxVirtualStart);
+    if (this.grid) {
+      val = val - (val % this._itemsPerRow);
+    }
+    this._virtualStartVal = val;
+  },
+
+  get _virtualStart() {
+    return this._virtualStartVal || 0;
+  },
+
+  /**
+   * The k-th tile that is at the top of the scrolling list.
+   */
+  set _physicalStart(val) {
+    val = val % this._physicalCount;
+    if (val < 0) {
+      val = this._physicalCount + val;
+    }
+    if (this.grid) {
+      val = val - (val % this._itemsPerRow);
+    }
+    this._physicalStartVal = val;
+  },
+
+  get _physicalStart() {
+    return this._physicalStartVal || 0;
+  },
+
+  /**
+   * The k-th tile that is at the bottom of the scrolling list.
+   */
+  get _physicalEnd() {
+    return (this._physicalStart + this._physicalCount - 1) %
+        this._physicalCount;
+  },
+
+  set _physicalCount(val) {
+    this._physicalCountVal = val;
+  },
+
+  get _physicalCount() {
+    return this._physicalCountVal || 0;
+  },
+
+  /**
+   * An optimal physical size such that we will have enough physical items
+   * to fill up the viewport and recycle when the user scrolls.
+   *
+   * This default value assumes that we will at least have the equivalent
+   * to a viewport of physical items above and below the user's viewport.
+   */
+  get _optPhysicalSize() {
+    return this._viewportHeight === 0 ? Infinity :
+                                        this._viewportHeight * this._maxPages;
+  },
+
+  /**
+   * True if the current list is visible.
+   */
+  get _isVisible() {
+    return Boolean(this.offsetWidth || this.offsetHeight);
+  },
+
+  /**
+   * Gets the index of the first visible item in the viewport.
+   *
+   * @type {number}
+   */
+  get firstVisibleIndex() {
+    var idx = this._firstVisibleIndexVal;
+    if (idx == null) {
+      var physicalOffset = this._physicalTop + this._scrollOffset;
+
+      idx = this._iterateItems(function(pidx, vidx) {
+        physicalOffset += this._getPhysicalSizeIncrement(pidx);
+
+        if (physicalOffset > this._scrollPosition) {
+          return this.grid ? vidx - (vidx % this._itemsPerRow) : vidx;
+        }
+        // Handle a partially rendered final row in grid mode
+        if (this.grid && this._virtualCount - 1 === vidx) {
+          return vidx - (vidx % this._itemsPerRow);
+        }
+      }) ||
+          0;
+      this._firstVisibleIndexVal = idx;
+    }
+    return idx;
+  },
+
+  /**
+   * Gets the index of the last visible item in the viewport.
+   *
+   * @type {number}
+   */
+  get lastVisibleIndex() {
+    var idx = this._lastVisibleIndexVal;
+    if (idx == null) {
+      if (this.grid) {
+        idx = Math.min(
+            this._virtualCount,
+            this.firstVisibleIndex + this._estRowsInView * this._itemsPerRow -
+                1);
+      } else {
+        var physicalOffset = this._physicalTop + this._scrollOffset;
+        this._iterateItems(function(pidx, vidx) {
+          if (physicalOffset < this._scrollBottom) {
+            idx = vidx;
+          }
+          physicalOffset += this._getPhysicalSizeIncrement(pidx);
+        });
+      }
+      this._lastVisibleIndexVal = idx;
+    }
+    return idx;
+  },
+
+  get _defaultScrollTarget() {
+    return this;
+  },
+
+  get _virtualRowCount() {
+    return Math.ceil(this._virtualCount / this._itemsPerRow);
+  },
+
+  get _estRowsInView() {
+    return Math.ceil(this._viewportHeight / this._rowHeight);
+  },
+
+  get _physicalRows() {
+    return Math.ceil(this._physicalCount / this._itemsPerRow);
+  },
+
+  get _scrollOffset() {
+    return this._scrollerPaddingTop + this.scrollOffset;
+  },
+
+  ready: function() {
+    this.addEventListener('focus', this._didFocus.bind(this), true);
+  },
+
+  attached: function() {
+    this._debounce('_render', this._render, animationFrame);
+    // `iron-resize` is fired when the list is attached if the event is added
+    // before attached causing unnecessary work.
+    this.listen(this, 'iron-resize', '_resizeHandler');
+    this.listen(this, 'keydown', '_keydownHandler');
+  },
+
+  detached: function() {
+    this.unlisten(this, 'iron-resize', '_resizeHandler');
+    this.unlisten(this, 'keydown', '_keydownHandler');
+  },
+
+  /**
+   * Set the overflow property if this element has its own scrolling region
+   */
+  _setOverflow: function(scrollTarget) {
+    this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : '';
+    this.style.overflowY = scrollTarget === this ? 'auto' : '';
+    // Clear cache.
+    this._lastVisibleIndexVal = null;
+    this._firstVisibleIndexVal = null;
+    this._debounce('_render', this._render, animationFrame);
+  },
+
+  /**
+   * Invoke this method if you dynamically update the viewport's
+   * size or CSS padding.
+   *
+   * @method updateViewportBoundaries
+   */
+  updateViewportBoundaries: function() {
+    var styles = window.getComputedStyle(this);
+    this._scrollerPaddingTop =
+        this.scrollTarget === this ? 0 : parseInt(styles['padding-top'], 10);
+    this._isRTL = Boolean(styles.direction === 'rtl');
+    this._viewportWidth = this.$.items.offsetWidth;
+    this._viewportHeight = this._scrollTargetHeight;
+    this.grid && this._updateGridMetrics();
+  },
+
+  /**
+   * Recycles the physical items when needed.
+   */
+  _scrollHandler: function() {
+    var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop));
+    var delta = scrollTop - this._scrollPosition;
+    var isScrollingDown = delta >= 0;
+    // Track the current scroll position.
+    this._scrollPosition = scrollTop;
+    // Clear indexes for first and last visible indexes.
+    this._firstVisibleIndexVal = null;
+    this._lastVisibleIndexVal = null;
+    // Random access.
+    if (Math.abs(delta) > this._physicalSize && this._physicalSize > 0) {
+      delta = delta - this._scrollOffset;
+      var idxAdjustment =
+          Math.round(delta / this._physicalAverage) * this._itemsPerRow;
+      this._virtualStart = this._virtualStart + idxAdjustment;
+      this._physicalStart = this._physicalStart + idxAdjustment;
+      // Estimate new physical offset.
+      this._physicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
+          this._physicalAverage;
+      this._update();
+    } else if (this._physicalCount > 0) {
+      var reusables = this._getReusables(isScrollingDown);
+      if (isScrollingDown) {
+        this._physicalTop = reusables.physicalTop;
+        this._virtualStart = this._virtualStart + reusables.indexes.length;
+        this._physicalStart = this._physicalStart + reusables.indexes.length;
+      } else {
+        this._virtualStart = this._virtualStart - reusables.indexes.length;
+        this._physicalStart = this._physicalStart - reusables.indexes.length;
+      }
+      this._update(
+          reusables.indexes, isScrollingDown ? null : reusables.indexes);
+      this._debounce(
+          '_increasePoolIfNeeded',
+          this._increasePoolIfNeeded.bind(this, 0),
+          microTask);
+    }
+  },
+
+  /**
+   * Returns an object that contains the indexes of the physical items
+   * that might be reused and the physicalTop.
+   *
+   * @param {boolean} fromTop If the potential reusable items are above the scrolling region.
+   */
+  _getReusables: function(fromTop) {
+    var ith, lastIth, offsetContent, physicalItemHeight;
+    var idxs = [];
+    var protectedOffsetContent = this._hiddenContentSize * this._ratio;
+    var virtualStart = this._virtualStart;
+    var virtualEnd = this._virtualEnd;
+    var physicalCount = this._physicalCount;
+    var top = this._physicalTop + this._scrollOffset;
+    var bottom = this._physicalBottom + this._scrollOffset;
+    var scrollTop = this._scrollTop;
+    var scrollBottom = this._scrollBottom;
+
+    if (fromTop) {
+      ith = this._physicalStart;
+      lastIth = this._physicalEnd;
+      offsetContent = scrollTop - top;
+    } else {
+      ith = this._physicalEnd;
+      lastIth = this._physicalStart;
+      offsetContent = bottom - scrollBottom;
+    }
+    while (true) {
+      physicalItemHeight = this._getPhysicalSizeIncrement(ith);
+      offsetContent = offsetContent - physicalItemHeight;
+      if (idxs.length >= physicalCount ||
+          offsetContent <= protectedOffsetContent) {
+        break;
+      }
+      if (fromTop) {
+        // Check that index is within the valid range.
+        if (virtualEnd + idxs.length + 1 >= this._virtualCount) {
+          break;
+        }
+        // Check that the index is not visible.
+        if (top + physicalItemHeight >= scrollTop - this._scrollOffset) {
+          break;
+        }
+        idxs.push(ith);
+        top = top + physicalItemHeight;
+        ith = (ith + 1) % physicalCount;
+      } else {
+        // Check that index is within the valid range.
+        if (virtualStart - idxs.length <= 0) {
+          break;
+        }
+        // Check that the index is not visible.
+        if (top + this._physicalSize - physicalItemHeight <= scrollBottom) {
+          break;
+        }
+        idxs.push(ith);
+        top = top - physicalItemHeight;
+        ith = (ith === 0) ? physicalCount - 1 : ith - 1;
+      }
+    }
+    return {indexes: idxs, physicalTop: top - this._scrollOffset};
+  },
+
+  /**
+   * Update the list of items, starting from the `_virtualStart` item.
+   * @param {!Array<number>=} itemSet
+   * @param {!Array<number>=} movingUp
+   */
+  _update: function(itemSet, movingUp) {
+    if ((itemSet && itemSet.length === 0) || this._physicalCount === 0) {
+      return;
+    }
+    this._manageFocus();
+    this._assignModels(itemSet);
+    this._updateMetrics(itemSet);
+    // Adjust offset after measuring.
+    if (movingUp) {
+      while (movingUp.length) {
+        var idx = movingUp.pop();
+        this._physicalTop -= this._getPhysicalSizeIncrement(idx);
+      }
+    }
+    this._positionItems();
+    this._updateScrollerSize();
+  },
+
+  /**
+   * Creates a pool of DOM elements and attaches them to the local dom.
+   *
+   * @param {number} size Size of the pool
+   */
+  _createPool: function(size) {
+    this._ensureTemplatized();
+    var i, inst;
+    var physicalItems = new Array(size);
+    for (i = 0; i < size; i++) {
+      inst = this.stamp(null);
+      // TODO(blasten):
+      // First element child is item; Safari doesn't support children[0]
+      // on a doc fragment. Test this to see if it still matters.
+      physicalItems[i] = inst.root.querySelector('*');
+      this._itemsParent.appendChild(inst.root);
+    }
+    return physicalItems;
+  },
+
+  _isClientFull: function() {
+    return this._scrollBottom != 0 &&
+        this._physicalBottom - 1 >= this._scrollBottom &&
+        this._physicalTop <= this._scrollPosition;
+  },
+
+  /**
+   * Increases the pool size.
+   */
+  _increasePoolIfNeeded: function(count) {
+    var nextPhysicalCount = this._clamp(
+        this._physicalCount + count,
+        DEFAULT_PHYSICAL_COUNT,
+        this._virtualCount - this._virtualStart);
+    nextPhysicalCount = this._convertIndexToCompleteRow(nextPhysicalCount);
+    if (this.grid) {
+      var correction = nextPhysicalCount % this._itemsPerRow;
+      if (correction && nextPhysicalCount - correction <= this._physicalCount) {
+        nextPhysicalCount += this._itemsPerRow;
+      }
+      nextPhysicalCount -= correction;
+    }
+    var delta = nextPhysicalCount - this._physicalCount;
+    var nextIncrease = Math.round(this._physicalCount * 0.5);
+
+    if (delta < 0) {
+      return;
+    }
+    if (delta > 0) {
+      var ts = window.performance.now();
+      // Concat arrays in place.
+      [].push.apply(this._physicalItems, this._createPool(delta));
+      // Push 0s into physicalSizes. Can't use Array.fill because IE11 doesn't
+      // support it.
+      for (var i = 0; i < delta; i++) {
+        this._physicalSizes.push(0);
+      }
+      this._physicalCount = this._physicalCount + delta;
+      // Update the physical start if it needs to preserve the model of the
+      // focused item. In this situation, the focused item is currently rendered
+      // and its model would have changed after increasing the pool if the
+      // physical start remained unchanged.
+      if (this._physicalStart > this._physicalEnd &&
+          this._isIndexRendered(this._focusedVirtualIndex) &&
+          this._getPhysicalIndex(this._focusedVirtualIndex) <
+              this._physicalEnd) {
+        this._physicalStart = this._physicalStart + delta;
+      }
+      this._update();
+      this._templateCost = (window.performance.now() - ts) / delta;
+      nextIncrease = Math.round(this._physicalCount * 0.5);
+    }
+    // The upper bounds is not fixed when dealing with a grid that doesn't
+    // fill it's last row with the exact number of items per row.
+    if (this._virtualEnd >= this._virtualCount - 1 || nextIncrease === 0) {
+      // Do nothing.
+    } else if (!this._isClientFull()) {
+      this._debounce(
+          '_increasePoolIfNeeded',
+          this._increasePoolIfNeeded.bind(this, nextIncrease),
+          microTask);
+    } else if (this._physicalSize < this._optPhysicalSize) {
+      // Yield and increase the pool during idle time until the physical size is
+      // optimal.
+      this._debounce(
+          '_increasePoolIfNeeded',
+          this._increasePoolIfNeeded.bind(
+              this,
+              this._clamp(
+                  Math.round(50 / this._templateCost), 1, nextIncrease)),
+          idlePeriod);
+    }
+  },
+
+  /**
+   * Renders the a new list.
+   */
+  _render: function() {
+    if (!this.isAttached || !this._isVisible) {
+      return;
+    }
+    if (this._physicalCount !== 0) {
+      var reusables = this._getReusables(true);
+      this._physicalTop = reusables.physicalTop;
+      this._virtualStart = this._virtualStart + reusables.indexes.length;
+      this._physicalStart = this._physicalStart + reusables.indexes.length;
+      this._update(reusables.indexes);
+      this._update();
+      this._increasePoolIfNeeded(0);
+    } else if (this._virtualCount > 0) {
+      // Initial render
+      this.updateViewportBoundaries();
+      this._increasePoolIfNeeded(DEFAULT_PHYSICAL_COUNT);
+    }
+  },
+
+  /**
+   * Templetizes the user template.
+   */
+  _ensureTemplatized: function() {
+    if (this.ctor) {
+      return;
+    }
+    this._userTemplate = this.queryEffectiveChildren('template');
+    if (!this._userTemplate) {
+      console.warn('iron-list requires a template to be provided in light-dom');
+    }
+    var instanceProps = {};
+    instanceProps.__key__ = true;
+    instanceProps[this.as] = true;
+    instanceProps[this.indexAs] = true;
+    instanceProps[this.selectedAs] = true;
+    instanceProps.tabIndex = true;
+    this._instanceProps = instanceProps;
+    this.templatize(this._userTemplate, this.mutableData);
+  },
+
+  _gridChanged: function(newGrid, oldGrid) {
+    if (typeof oldGrid === 'undefined')
+      return;
+    this.notifyResize();
+    flush();
+    newGrid && this._updateGridMetrics();
+  },
+
+  /**
+   * Called when the items have changed. That is, reassignments
+   * to `items`, splices or updates to a single item.
+   */
+  _itemsChanged: function(change) {
+    if (change.path === 'items') {
+      this._virtualStart = 0;
+      this._physicalTop = 0;
+      this._virtualCount = this.items ? this.items.length : 0;
+      this._physicalIndexForKey = {};
+      this._firstVisibleIndexVal = null;
+      this._lastVisibleIndexVal = null;
+      this._physicalCount = this._physicalCount || 0;
+      this._physicalItems = this._physicalItems || [];
+      this._physicalSizes = this._physicalSizes || [];
+      this._physicalStart = 0;
+      if (this._scrollTop > this._scrollOffset) {
+        this._resetScrollPosition(0);
+      }
+      this._removeFocusedItem();
+      this._debounce('_render', this._render, animationFrame);
+    } else if (change.path === 'items.splices') {
+      this._adjustVirtualIndex(change.value.indexSplices);
+      this._virtualCount = this.items ? this.items.length : 0;
+      // Only blur if at least one item is added or removed.
+      var itemAddedOrRemoved = change.value.indexSplices.some(function(splice) {
+        return splice.addedCount > 0 || splice.removed.length > 0;
+      });
+      if (itemAddedOrRemoved) {
+        // Only blur activeElement if it is a descendant of the list (#505,
+        // #507).
+        var activeElement = this._getActiveElement();
+        if (this.contains(activeElement)) {
+          activeElement.blur();
+        }
+      }
+      // Render only if the affected index is rendered.
+      var affectedIndexRendered =
+          change.value.indexSplices.some(function(splice) {
+            return splice.index + splice.addedCount >= this._virtualStart &&
+                splice.index <= this._virtualEnd;
+          }, this);
+      if (!this._isClientFull() || affectedIndexRendered) {
+        this._debounce('_render', this._render, animationFrame);
+      }
+    } else if (change.path !== 'items.length') {
+      this._forwardItemPath(change.path, change.value);
+    }
+  },
+
+  _forwardItemPath: function(path, value) {
+    path = path.slice(6);  // 'items.'.length == 6
+    var dot = path.indexOf('.');
+    if (dot === -1) {
+      dot = path.length;
+    }
+    var isIndexRendered;
+    var pidx;
+    var inst;
+    var offscreenInstance = this.modelForElement(this._offscreenFocusedItem);
+    var vidx = parseInt(path.substring(0, dot), 10);
+    isIndexRendered = this._isIndexRendered(vidx);
+    if (isIndexRendered) {
+      pidx = this._getPhysicalIndex(vidx);
+      inst = this.modelForElement(this._physicalItems[pidx]);
+    } else if (offscreenInstance) {
+      inst = offscreenInstance;
+    }
+
+    if (!inst || inst[this.indexAs] !== vidx) {
+      return;
+    }
+    path = path.substring(dot + 1);
+    path = this.as + (path ? '.' + path : '');
+    inst._setPendingPropertyOrPath(path, value, false, true);
+    inst._flushProperties && inst._flushProperties(true);
+    // TODO(blasten): V1 doesn't do this and it's a bug
+    if (isIndexRendered) {
+      this._updateMetrics([pidx]);
+      this._positionItems();
+      this._updateScrollerSize();
+    }
+  },
+
+  /**
+   * @param {!Array<!Object>} splices
+   */
+  _adjustVirtualIndex: function(splices) {
+    splices.forEach(function(splice) {
+      // deselect removed items
+      splice.removed.forEach(this._removeItem, this);
+      // We only need to care about changes happening above the current position
+      if (splice.index < this._virtualStart) {
+        var delta = Math.max(
+            splice.addedCount - splice.removed.length,
+            splice.index - this._virtualStart);
+        this._virtualStart = this._virtualStart + delta;
+        if (this._focusedVirtualIndex >= 0) {
+          this._focusedVirtualIndex = this._focusedVirtualIndex + delta;
+        }
+      }
+    }, this);
+  },
+
+  _removeItem: function(item) {
+    this.$.selector.deselect(item);
+    // remove the current focused item
+    if (this._focusedItem &&
+        this.modelForElement(this._focusedItem)[this.as] === item) {
+      this._removeFocusedItem();
+    }
+  },
+
+  /**
+   * Executes a provided function per every physical index in `itemSet`
+   * `itemSet` default value is equivalent to the entire set of physical
+   * indexes.
+   *
+   * @param {!function(number, number)} fn
+   * @param {!Array<number>=} itemSet
+   */
+  _iterateItems: function(fn, itemSet) {
+    var pidx, vidx, rtn, i;
+
+    if (arguments.length === 2 && itemSet) {
+      for (i = 0; i < itemSet.length; i++) {
+        pidx = itemSet[i];
+        vidx = this._computeVidx(pidx);
+        if ((rtn = fn.call(this, pidx, vidx)) != null) {
+          return rtn;
+        }
+      }
+    } else {
+      pidx = this._physicalStart;
+      vidx = this._virtualStart;
+      for (; pidx < this._physicalCount; pidx++, vidx++) {
+        if ((rtn = fn.call(this, pidx, vidx)) != null) {
+          return rtn;
+        }
+      }
+      for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) {
+        if ((rtn = fn.call(this, pidx, vidx)) != null) {
+          return rtn;
+        }
+      }
+    }
+  },
+
+  /**
+   * Returns the virtual index for a given physical index
+   *
+   * @param {number} pidx Physical index
+   * @return {number}
+   */
+  _computeVidx: function(pidx) {
+    if (pidx >= this._physicalStart) {
+      return this._virtualStart + (pidx - this._physicalStart);
+    }
+    return this._virtualStart + (this._physicalCount - this._physicalStart) +
+        pidx;
+  },
+
+  /**
+   * Assigns the data models to a given set of items.
+   * @param {!Array<number>=} itemSet
+   */
+  _assignModels: function(itemSet) {
+    this._iterateItems(function(pidx, vidx) {
+      var el = this._physicalItems[pidx];
+      var item = this.items && this.items[vidx];
+      if (item != null) {
+        var inst = this.modelForElement(el);
+        inst.__key__ = null;
+        this._forwardProperty(inst, this.as, item);
+        this._forwardProperty(
+            inst, this.selectedAs, this.$.selector.isSelected(item));
+        this._forwardProperty(inst, this.indexAs, vidx);
+        this._forwardProperty(
+            inst, 'tabIndex', this._focusedVirtualIndex === vidx ? 0 : -1);
+        this._physicalIndexForKey[inst.__key__] = pidx;
+        inst._flushProperties && inst._flushProperties(true);
+        el.removeAttribute('hidden');
+      } else {
+        el.setAttribute('hidden', '');
+      }
+    }, itemSet);
+  },
+
+  /**
+   * Updates the height for a given set of items.
+   *
+   * @param {!Array<number>=} itemSet
+   */
+  _updateMetrics: function(itemSet) {
+    // Make sure we distributed all the physical items
+    // so we can measure them.
+    flush();
+
+    var newPhysicalSize = 0;
+    var oldPhysicalSize = 0;
+    var prevAvgCount = this._physicalAverageCount;
+    var prevPhysicalAvg = this._physicalAverage;
+
+    this._iterateItems(function(pidx, vidx) {
+      oldPhysicalSize += this._physicalSizes[pidx];
+      this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight;
+      newPhysicalSize += this._physicalSizes[pidx];
+      this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0;
+    }, itemSet);
+
+    if (this.grid) {
+      this._updateGridMetrics();
+      this._physicalSize =
+          Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight;
+    } else {
+      oldPhysicalSize = (this._itemsPerRow === 1) ?
+          oldPhysicalSize :
+          Math.ceil(this._physicalCount / this._itemsPerRow) * this._rowHeight;
+      this._physicalSize =
+          this._physicalSize + newPhysicalSize - oldPhysicalSize;
+      this._itemsPerRow = 1;
+    }
+    // Update the average if it measured something.
+    if (this._physicalAverageCount !== prevAvgCount) {
+      this._physicalAverage = Math.round(
+          ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) /
+          this._physicalAverageCount);
+    }
+  },
+
+  _updateGridMetrics: function() {
+    this._itemWidth = this._physicalCount > 0 ?
+        this._physicalItems[0].getBoundingClientRect().width :
+        200;
+    this._rowHeight =
+        this._physicalCount > 0 ? this._physicalItems[0].offsetHeight : 200;
+    this._itemsPerRow = this._itemWidth ?
+        Math.floor(this._viewportWidth / this._itemWidth) :
+        this._itemsPerRow;
+  },
+
+  /**
+   * Updates the position of the physical items.
+   */
+  _positionItems: function() {
+    this._adjustScrollPosition();
+
+    var y = this._physicalTop;
+
+    if (this.grid) {
+      var totalItemWidth = this._itemsPerRow * this._itemWidth;
+      var rowOffset = (this._viewportWidth - totalItemWidth) / 2;
+
+      this._iterateItems(function(pidx, vidx) {
+        var modulus = vidx % this._itemsPerRow;
+        var x = Math.floor((modulus * this._itemWidth) + rowOffset);
+        if (this._isRTL) {
+          x = x * -1;
+        }
+        this.translate3d(x + 'px', y + 'px', 0, this._physicalItems[pidx]);
+        if (this._shouldRenderNextRow(vidx)) {
+          y += this._rowHeight;
+        }
+      });
+    } else {
+      this._iterateItems(function(pidx, vidx) {
+        this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]);
+        y += this._physicalSizes[pidx];
+      });
+    }
+  },
+
+  _getPhysicalSizeIncrement: function(pidx) {
+    if (!this.grid) {
+      return this._physicalSizes[pidx];
+    }
+    if (this._computeVidx(pidx) % this._itemsPerRow !== this._itemsPerRow - 1) {
+      return 0;
+    }
+    return this._rowHeight;
+  },
+
+  /**
+   * Returns, based on the current index,
+   * whether or not the next index will need
+   * to be rendered on a new row.
+   *
+   * @param {number} vidx Virtual index
+   * @return {boolean}
+   */
+  _shouldRenderNextRow: function(vidx) {
+    return vidx % this._itemsPerRow === this._itemsPerRow - 1;
+  },
+
+  /**
+   * Adjusts the scroll position when it was overestimated.
+   */
+  _adjustScrollPosition: function() {
+    var deltaHeight = this._virtualStart === 0 ?
+        this._physicalTop :
+        Math.min(this._scrollPosition + this._physicalTop, 0);
+    // Note: the delta can be positive or negative.
+    if (deltaHeight !== 0) {
+      this._physicalTop = this._physicalTop - deltaHeight;
+      var scrollTop = this._scrollTop;
+      // juking scroll position during interial scrolling on iOS is no bueno
+      if (!IOS_TOUCH_SCROLLING && scrollTop > 0) {
+        this._resetScrollPosition(scrollTop - deltaHeight);
+      }
+    }
+  },
+
+  /**
+   * Sets the position of the scroll.
+   */
+  _resetScrollPosition: function(pos) {
+    if (this.scrollTarget && pos >= 0) {
+      this._scrollTop = pos;
+      this._scrollPosition = this._scrollTop;
+    }
+  },
+
+  /**
+   * Sets the scroll height, that's the height of the content,
+   *
+   * @param {boolean=} forceUpdate If true, updates the height no matter what.
+   */
+  _updateScrollerSize: function(forceUpdate) {
+    if (this.grid) {
+      this._estScrollHeight = this._virtualRowCount * this._rowHeight;
+    } else {
+      this._estScrollHeight =
+          (this._physicalBottom +
+           Math.max(
+               this._virtualCount - this._physicalCount - this._virtualStart,
+               0) *
+               this._physicalAverage);
+    }
+    forceUpdate = forceUpdate || this._scrollHeight === 0;
+    forceUpdate = forceUpdate ||
+        this._scrollPosition >= this._estScrollHeight - this._physicalSize;
+    forceUpdate = forceUpdate ||
+        this.grid && this.$.items.style.height < this._estScrollHeight;
+    // Amortize height adjustment, so it won't trigger large repaints too often.
+    if (forceUpdate ||
+        Math.abs(this._estScrollHeight - this._scrollHeight) >=
+            this._viewportHeight) {
+      this.$.items.style.height = this._estScrollHeight + 'px';
+      this._scrollHeight = this._estScrollHeight;
+    }
+  },
+
+  /**
+   * Scroll to a specific item in the virtual list regardless
+   * of the physical items in the DOM tree.
+   *
+   * @method scrollToItem
+   * @param {(Object)} item The item to be scrolled to
+   */
+  scrollToItem: function(item) {
+    return this.scrollToIndex(this.items.indexOf(item));
+  },
+
+  /**
+   * Scroll to a specific index in the virtual list regardless
+   * of the physical items in the DOM tree.
+   *
+   * @method scrollToIndex
+   * @param {number} idx The index of the item
+   */
+  scrollToIndex: function(idx) {
+    if (typeof idx !== 'number' || idx < 0 || idx > this.items.length - 1) {
+      return;
+    }
+    flush();
+    // Items should have been rendered prior scrolling to an index.
+    if (this._physicalCount === 0) {
+      return;
+    }
+    idx = this._clamp(idx, 0, this._virtualCount - 1);
+    // Update the virtual start only when needed.
+    if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) {
+      this._virtualStart =
+          this.grid ? (idx - this._itemsPerRow * 2) : (idx - 1);
+    }
+    this._manageFocus();
+    this._assignModels();
+    this._updateMetrics();
+    // Estimate new physical offset.
+    this._physicalTop = Math.floor(this._virtualStart / this._itemsPerRow) *
+        this._physicalAverage;
+
+    var currentTopItem = this._physicalStart;
+    var currentVirtualItem = this._virtualStart;
+    var targetOffsetTop = 0;
+    var hiddenContentSize = this._hiddenContentSize;
+    // scroll to the item as much as we can.
+    while (currentVirtualItem < idx && targetOffsetTop <= hiddenContentSize) {
+      targetOffsetTop =
+          targetOffsetTop + this._getPhysicalSizeIncrement(currentTopItem);
+      currentTopItem = (currentTopItem + 1) % this._physicalCount;
+      currentVirtualItem++;
+    }
+    this._updateScrollerSize(true);
+    this._positionItems();
+    this._resetScrollPosition(
+        this._physicalTop + this._scrollOffset + targetOffsetTop);
+    this._increasePoolIfNeeded(0);
+    // clear cached visible index.
+    this._firstVisibleIndexVal = null;
+    this._lastVisibleIndexVal = null;
+  },
+
+  /**
+   * Reset the physical average and the average count.
+   */
+  _resetAverage: function() {
+    this._physicalAverage = 0;
+    this._physicalAverageCount = 0;
+  },
+
+  /**
+   * A handler for the `iron-resize` event triggered by `IronResizableBehavior`
+   * when the element is resized.
+   */
+  _resizeHandler: function() {
+    this._debounce('_render', function() {
+      // clear cached visible index.
+      this._firstVisibleIndexVal = null;
+      this._lastVisibleIndexVal = null;
+      if (this._isVisible) {
+        this.updateViewportBoundaries();
+        // Reinstall the scroll event listener.
+        this.toggleScrollListener(true);
+        this._resetAverage();
+        this._render();
+      } else {
+        // Uninstall the scroll event listener.
+        this.toggleScrollListener(false);
+      }
+    }, animationFrame);
+  },
+
+  /**
+   * Selects the given item.
+   *
+   * @method selectItem
+   * @param {Object} item The item instance.
+   */
+  selectItem: function(item) {
+    return this.selectIndex(this.items.indexOf(item));
+  },
+
+  /**
+   * Selects the item at the given index in the items array.
+   *
+   * @method selectIndex
+   * @param {number} index The index of the item in the items array.
+   */
+  selectIndex: function(index) {
+    if (index < 0 || index >= this._virtualCount) {
+      return;
+    }
+    if (!this.multiSelection && this.selectedItem) {
+      this.clearSelection();
+    }
+    if (this._isIndexRendered(index)) {
+      var model = this.modelForElement(
+          this._physicalItems[this._getPhysicalIndex(index)]);
+      if (model) {
+        model[this.selectedAs] = true;
+      }
+      this.updateSizeForIndex(index);
+    }
+    this.$.selector.selectIndex(index);
+  },
+
+  /**
+   * Deselects the given item.
+   *
+   * @method deselect
+   * @param {Object} item The item instance.
+   */
+  deselectItem: function(item) {
+    return this.deselectIndex(this.items.indexOf(item));
+  },
+
+  /**
+   * Deselects the item at the given index in the items array.
+   *
+   * @method deselectIndex
+   * @param {number} index The index of the item in the items array.
+   */
+  deselectIndex: function(index) {
+    if (index < 0 || index >= this._virtualCount) {
+      return;
+    }
+    if (this._isIndexRendered(index)) {
+      var model = this.modelForElement(
+          this._physicalItems[this._getPhysicalIndex(index)]);
+      model[this.selectedAs] = false;
+      this.updateSizeForIndex(index);
+    }
+    this.$.selector.deselectIndex(index);
+  },
+
+  /**
+   * Selects or deselects a given item depending on whether the item
+   * has already been selected.
+   *
+   * @method toggleSelectionForItem
+   * @param {Object} item The item object.
+   */
+  toggleSelectionForItem: function(item) {
+    return this.toggleSelectionForIndex(this.items.indexOf(item));
+  },
+
+  /**
+   * Selects or deselects the item at the given index in the items array
+   * depending on whether the item has already been selected.
+   *
+   * @method toggleSelectionForIndex
+   * @param {number} index The index of the item in the items array.
+   */
+  toggleSelectionForIndex: function(index) {
+    var isSelected = this.$.selector.isIndexSelected ?
+        this.$.selector.isIndexSelected(index) :
+        this.$.selector.isSelected(this.items[index]);
+    isSelected ? this.deselectIndex(index) : this.selectIndex(index);
+  },
+
+  /**
+   * Clears the current selection in the list.
+   *
+   * @method clearSelection
+   */
+  clearSelection: function() {
+    this._iterateItems(function(pidx, vidx) {
+      this.modelForElement(this._physicalItems[pidx])[this.selectedAs] = false;
+    });
+    this.$.selector.clearSelection();
+  },
+
+  /**
+   * Add an event listener to `tap` if `selectionEnabled` is true,
+   * it will remove the listener otherwise.
+   */
+  _selectionEnabledChanged: function(selectionEnabled) {
+    var handler = selectionEnabled ? this.listen : this.unlisten;
+    handler.call(this, this, 'tap', '_selectionHandler');
+  },
+
+  /**
+   * Select an item from an event object.
+   */
+  _selectionHandler: function(e) {
+    var model = this.modelForElement(e.target);
+    if (!model) {
+      return;
+    }
+    var modelTabIndex, activeElTabIndex;
+    var target = dom(e).path[0];
+    var activeEl = this._getActiveElement();
+    var physicalItem =
+        this._physicalItems[this._getPhysicalIndex(model[this.indexAs])];
+    // Safari does not focus certain form controls via mouse
+    // https://bugs.webkit.org/show_bug.cgi?id=118043
+    if (target.localName === 'input' || target.localName === 'button' ||
+        target.localName === 'select') {
+      return;
+    }
+    // Set a temporary tabindex
+    modelTabIndex = model.tabIndex;
+    model.tabIndex = SECRET_TABINDEX;
+    activeElTabIndex = activeEl ? activeEl.tabIndex : -1;
+    model.tabIndex = modelTabIndex;
+    // Only select the item if the tap wasn't on a focusable child
+    // or the element bound to `tabIndex`
+    if (activeEl && physicalItem !== activeEl &&
+        physicalItem.contains(activeEl) &&
+        activeElTabIndex !== SECRET_TABINDEX) {
+      return;
+    }
+    this.toggleSelectionForItem(model[this.as]);
+  },
+
+  _multiSelectionChanged: function(multiSelection) {
+    this.clearSelection();
+    this.$.selector.multi = multiSelection;
+  },
+
+  /**
+   * Updates the size of a given list item.
+   *
+   * @method updateSizeForItem
+   * @param {Object} item The item instance.
+   */
+  updateSizeForItem: function(item) {
+    return this.updateSizeForIndex(this.items.indexOf(item));
+  },
+
+  /**
+   * Updates the size of the item at the given index in the items array.
+   *
+   * @method updateSizeForIndex
+   * @param {number} index The index of the item in the items array.
+   */
+  updateSizeForIndex: function(index) {
+    if (!this._isIndexRendered(index)) {
+      return null;
+    }
+    this._updateMetrics([this._getPhysicalIndex(index)]);
+    this._positionItems();
+    return null;
+  },
+
+  /**
+   * Creates a temporary backfill item in the rendered pool of physical items
+   * to replace the main focused item. The focused item has tabIndex = 0
+   * and might be currently focused by the user.
+   *
+   * This dynamic replacement helps to preserve the focus state.
+   */
+  _manageFocus: function() {
+    var fidx = this._focusedVirtualIndex;
+
+    if (fidx >= 0 && fidx < this._virtualCount) {
+      // if it's a valid index, check if that index is rendered
+      // in a physical item.
+      if (this._isIndexRendered(fidx)) {
+        this._restoreFocusedItem();
+      } else {
+        this._createFocusBackfillItem();
+      }
+    } else if (this._virtualCount > 0 && this._physicalCount > 0) {
+      // otherwise, assign the initial focused index.
+      this._focusedPhysicalIndex = this._physicalStart;
+      this._focusedVirtualIndex = this._virtualStart;
+      this._focusedItem = this._physicalItems[this._physicalStart];
+    }
+  },
+
+  /**
+   * Converts a random index to the index of the item that completes it's row.
+   * Allows for better order and fill computation when grid == true.
+   */
+  _convertIndexToCompleteRow: function(idx) {
+    // when grid == false _itemPerRow can be unset.
+    this._itemsPerRow = this._itemsPerRow || 1;
+    return this.grid ? Math.ceil(idx / this._itemsPerRow) * this._itemsPerRow :
+                       idx;
+  },
+
+  _isIndexRendered: function(idx) {
+    return idx >= this._virtualStart && idx <= this._virtualEnd;
+  },
+
+  _isIndexVisible: function(idx) {
+    return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex;
+  },
+
+  _getPhysicalIndex: function(vidx) {
+    return (this._physicalStart + (vidx - this._virtualStart)) %
+        this._physicalCount;
+  },
+
+  focusItem: function(idx) {
+    this._focusPhysicalItem(idx);
+  },
+
+  _focusPhysicalItem: function(idx) {
+    if (idx < 0 || idx >= this._virtualCount) {
+      return;
+    }
+    this._restoreFocusedItem();
+    // scroll to index to make sure it's rendered
+    if (!this._isIndexRendered(idx)) {
+      this.scrollToIndex(idx);
+    }
+    var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)];
+    var model = this.modelForElement(physicalItem);
+    var focusable;
+    // set a secret tab index
+    model.tabIndex = SECRET_TABINDEX;
+    // check if focusable element is the physical item
+    if (physicalItem.tabIndex === SECRET_TABINDEX) {
+      focusable = physicalItem;
+    }
+    // search for the element which tabindex is bound to the secret tab index
+    if (!focusable) {
+      focusable = dom(physicalItem)
+                      .querySelector('[tabindex="' + SECRET_TABINDEX + '"]');
+    }
+    // restore the tab index
+    model.tabIndex = 0;
+    // focus the focusable element
+    this._focusedVirtualIndex = idx;
+    focusable && focusable.focus();
+  },
+
+  _removeFocusedItem: function() {
+    if (this._offscreenFocusedItem) {
+      this._itemsParent.removeChild(this._offscreenFocusedItem);
+    }
+    this._offscreenFocusedItem = null;
+    this._focusBackfillItem = null;
+    this._focusedItem = null;
+    this._focusedVirtualIndex = -1;
+    this._focusedPhysicalIndex = -1;
+  },
+
+  _createFocusBackfillItem: function() {
+    var fpidx = this._focusedPhysicalIndex;
+
+    if (this._offscreenFocusedItem || this._focusedVirtualIndex < 0) {
+      return;
+    }
+    if (!this._focusBackfillItem) {
+      // Create a physical item.
+      var inst = this.stamp(null);
+      this._focusBackfillItem = inst.root.querySelector('*');
+      this._itemsParent.appendChild(inst.root);
+    }
+    // Set the offcreen focused physical item.
+    this._offscreenFocusedItem = this._physicalItems[fpidx];
+    this.modelForElement(this._offscreenFocusedItem).tabIndex = 0;
+    this._physicalItems[fpidx] = this._focusBackfillItem;
+    this._focusedPhysicalIndex = fpidx;
+    // Hide the focused physical.
+    this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem);
+  },
+
+  _restoreFocusedItem: function() {
+    if (!this._offscreenFocusedItem || this._focusedVirtualIndex < 0) {
+      return;
+    }
+    // Assign models to the focused index.
+    this._assignModels();
+    // Get the new physical index for the focused index.
+    var fpidx = this._focusedPhysicalIndex =
+        this._getPhysicalIndex(this._focusedVirtualIndex);
+
+    var onScreenItem = this._physicalItems[fpidx];
+    if (!onScreenItem) {
+      return;
+    }
+    var onScreenInstance = this.modelForElement(onScreenItem);
+    var offScreenInstance = this.modelForElement(this._offscreenFocusedItem);
+    // Restores the physical item only when it has the same model
+    // as the offscreen one. Use key for comparison since users can set
+    // a new item via set('items.idx').
+    if (onScreenInstance[this.as] === offScreenInstance[this.as]) {
+      // Flip the focus backfill.
+      this._focusBackfillItem = onScreenItem;
+      onScreenInstance.tabIndex = -1;
+      // Restore the focused physical item.
+      this._physicalItems[fpidx] = this._offscreenFocusedItem;
+      // Hide the physical item that backfills.
+      this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem);
+    } else {
+      this._removeFocusedItem();
+      this._focusBackfillItem = null;
+    }
+    this._offscreenFocusedItem = null;
+  },
+
+  _didFocus: function(e) {
+    var targetModel = this.modelForElement(e.target);
+    var focusedModel = this.modelForElement(this._focusedItem);
+    var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null;
+    var fidx = this._focusedVirtualIndex;
+    if (!targetModel) {
+      return;
+    }
+    if (focusedModel === targetModel) {
+      // If the user focused the same item, then bring it into view if it's not
+      // visible.
+      if (!this._isIndexVisible(fidx)) {
+        this.scrollToIndex(fidx);
+      }
+    } else {
+      this._restoreFocusedItem();
+      // Restore tabIndex for the currently focused item.
+      if (focusedModel) {
+        focusedModel.tabIndex = -1;
+      }
+      // Set the tabIndex for the next focused item.
+      targetModel.tabIndex = 0;
+      fidx = targetModel[this.indexAs];
+      this._focusedVirtualIndex = fidx;
+      this._focusedPhysicalIndex = this._getPhysicalIndex(fidx);
+      this._focusedItem = this._physicalItems[this._focusedPhysicalIndex];
+      if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) {
+        this._update();
+      }
+    }
+  },
+
+  _keydownHandler: function(e) {
+    switch (e.keyCode) {
+      case /* ARROW_DOWN */ 40:
+        if (this._focusedVirtualIndex < this._virtualCount - 1)
+          e.preventDefault();
+        this._focusPhysicalItem(
+            this._focusedVirtualIndex + (this.grid ? this._itemsPerRow : 1));
+        break;
+      case /* ARROW_RIGHT */ 39:
+        if (this.grid)
+          this._focusPhysicalItem(
+              this._focusedVirtualIndex + (this._isRTL ? -1 : 1));
+        break;
+      case /* ARROW_UP */ 38:
+        if (this._focusedVirtualIndex > 0)
+          e.preventDefault();
+        this._focusPhysicalItem(
+            this._focusedVirtualIndex - (this.grid ? this._itemsPerRow : 1));
+        break;
+      case /* ARROW_LEFT */ 37:
+        if (this.grid)
+          this._focusPhysicalItem(
+              this._focusedVirtualIndex + (this._isRTL ? 1 : -1));
+        break;
+      case /* ENTER */ 13:
+        this._focusPhysicalItem(this._focusedVirtualIndex);
+        if (this.selectionEnabled)
+          this._selectionHandler(e);
+        break;
+    }
+  },
+
+  _clamp: function(v, min, max) {
+    return Math.min(max, Math.max(min, v));
+  },
+
+  _debounce: function(name, cb, asyncModule) {
+    this._debouncers = this._debouncers || {};
+    this._debouncers[name] =
+        Debouncer.debounce(this._debouncers[name], asyncModule, cb.bind(this));
+    enqueueDebouncer(this._debouncers[name]);
+  },
+
+  _forwardProperty: function(inst, name, value) {
+    inst._setPendingProperty(name, value);
+  },
+
+  /* Templatizer bindings for v2 */
+  _forwardHostPropV2: function(prop, value) {
+    (this._physicalItems || [])
+        .concat([this._offscreenFocusedItem, this._focusBackfillItem])
+        .forEach(function(item) {
+          if (item) {
+            this.modelForElement(item).forwardHostProp(prop, value);
+          }
+        }, this);
+  },
+
+  _notifyInstancePropV2: function(inst, prop, value) {
+    if (matches(this.as, prop)) {
+      var idx = inst[this.indexAs];
+      if (prop == this.as) {
+        this.items[idx] = value;
+      }
+      this.notifyPath(translate(this.as, 'items.' + idx, prop), value);
+    }
+  },
+
+  /* Templatizer bindings for v1 */
+  _getStampedChildren: function() {
+    return this._physicalItems;
+  },
+
+  _forwardInstancePath: function(inst, path, value) {
+    if (path.indexOf(this.as + '.') === 0) {
+      this.notifyPath(
+          'items.' + inst.__key__ + '.' + path.slice(this.as.length + 1),
+          value);
+    }
+  },
+
+  _forwardParentPath: function(path, value) {
+    (this._physicalItems || [])
+        .concat([this._offscreenFocusedItem, this._focusBackfillItem])
+        .forEach(function(item) {
+          if (item) {
+            this.modelForElement(item).notifyPath(path, value, true);
+          }
+        }, this);
+  },
+
+  _forwardParentProp: function(prop, value) {
+    (this._physicalItems || [])
+        .concat([this._offscreenFocusedItem, this._focusBackfillItem])
+        .forEach(function(item) {
+          if (item) {
+            this.modelForElement(item)[prop] = value;
+          }
+        }, this);
+  },
+
+  /* Gets the activeElement of the shadow root/host that contains the list. */
+  _getActiveElement: function() {
+    var itemsHost = this._itemsParent.node.domHost;
+    return dom(itemsHost ? itemsHost.root : document).activeElement;
+  }
+});
diff --git a/third_party/polymer/v3_0/components-chromium/iron-location/iron-location.js b/third_party/polymer/v3_0/components-chromium/iron-location/iron-location.js
new file mode 100644
index 0000000..5a402bf
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-location/iron-location.js
@@ -0,0 +1,395 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+/**
+
+The `iron-location` element manages binding to and from the current URL.
+
+iron-location is the first, and lowest level element in the Polymer team's
+routing system. This is a beta release of iron-location as we continue work
+on higher level elements, and as such iron-location may undergo breaking
+changes.
+
+#### Properties
+
+When the URL is: `/search?query=583#details` iron-location's properties will be:
+
+  - path: `'/search'`
+  - query: `'query=583'`
+  - hash: `'details'`
+
+These bindings are bidirectional. Modifying them will in turn modify the URL.
+
+iron-location is only active while it is attached to the document.
+
+#### Links
+
+While iron-location is active in the document it will intercept clicks on links
+within your site, updating the URL pushing the updated URL out through the
+databinding system. iron-location only intercepts clicks with the intent to
+open in the same window, so middle mouse clicks and ctrl/cmd clicks work fine.
+
+You can customize this behavior with the `urlSpaceRegex`.
+
+#### Dwell Time
+
+iron-location protects against accidental history spamming by only adding
+entries to the user's history if the URL stays unchanged for `dwellTime`
+milliseconds.
+
+@demo demo/index.html
+
+ */
+Polymer({
+  is: 'iron-location',
+
+  properties: {
+    /**
+     * The pathname component of the URL.
+     */
+    path: {
+      type: String,
+      notify: true,
+      value: function() {
+        return window.decodeURIComponent(window.location.pathname);
+      }
+    },
+
+    /**
+     * The query string portion of the URL.
+     */
+    query: {
+      type: String,
+      notify: true,
+      value: function() {
+        return window.location.search.slice(1);
+      }
+    },
+
+    /**
+     * The hash component of the URL.
+     */
+    hash: {
+      type: String,
+      notify: true,
+      value: function() {
+        return window.decodeURIComponent(window.location.hash.slice(1));
+      }
+    },
+
+    /**
+     * If the user was on a URL for less than `dwellTime` milliseconds, it
+     * won't be added to the browser's history, but instead will be replaced
+     * by the next entry.
+     *
+     * This is to prevent large numbers of entries from clogging up the user's
+     * browser history. Disable by setting to a negative number.
+     */
+    dwellTime: {type: Number, value: 2000},
+
+    /**
+     * A regexp that defines the set of URLs that should be considered part
+     * of this web app.
+     *
+     * Clicking on a link that matches this regex won't result in a full page
+     * navigation, but will instead just update the URL state in place.
+     *
+     * This regexp is given everything after the origin in an absolute
+     * URL. So to match just URLs that start with /search/ do:
+     *     url-space-regex="^/search/"
+     *
+     * @type {string|RegExp}
+     */
+    urlSpaceRegex: {type: String, value: ''},
+
+    /**
+     * A flag that specifies whether the spaces in query that would normally be
+     * encoded as %20 should be encoded as +.
+     *
+     * Given an example text "hello world", it is encoded in query as
+     * - "hello%20world" without the parameter
+     * - "hello+world" with the parameter
+     */
+    encodeSpaceAsPlusInQuery: {type: Boolean, value: false},
+
+    /**
+     * urlSpaceRegex, but coerced into a regexp.
+     *
+     * @type {RegExp}
+     */
+    _urlSpaceRegExp: {computed: '_makeRegExp(urlSpaceRegex)'},
+
+    _lastChangedAt: {type: Number},
+
+    _initialized: {type: Boolean, value: false}
+  },
+
+  hostAttributes: {hidden: true},
+
+  observers: ['_updateUrl(path, query, hash)'],
+
+  created: function() {
+    this.__location = window.location;
+  },
+
+  attached: function() {
+    this.listen(window, 'hashchange', '_hashChanged');
+    this.listen(window, 'location-changed', '_urlChanged');
+    this.listen(window, 'popstate', '_urlChanged');
+    this.listen(
+        /** @type {!HTMLBodyElement} */ (document.body),
+        'click',
+        '_globalOnClick');
+    // Give a 200ms grace period to make initial redirects without any
+    // additions to the user's history.
+    this._lastChangedAt = window.performance.now() - (this.dwellTime - 200);
+    this._initialized = true;
+
+    this._urlChanged();
+  },
+
+  detached: function() {
+    this.unlisten(window, 'hashchange', '_hashChanged');
+    this.unlisten(window, 'location-changed', '_urlChanged');
+    this.unlisten(window, 'popstate', '_urlChanged');
+    this.unlisten(
+        /** @type {!HTMLBodyElement} */ (document.body),
+        'click',
+        '_globalOnClick');
+    this._initialized = false;
+  },
+
+  _hashChanged: function() {
+    this.hash = window.decodeURIComponent(this.__location.hash.substring(1));
+  },
+
+  _urlChanged: function() {
+    // We want to extract all info out of the updated URL before we
+    // try to write anything back into it.
+    //
+    // i.e. without _dontUpdateUrl we'd overwrite the new path with the old
+    // one when we set this.hash. Likewise for query.
+    this._dontUpdateUrl = true;
+    this._hashChanged();
+    this.path = window.decodeURIComponent(this.__location.pathname);
+    this.query = this.__location.search.substring(1);
+    this._dontUpdateUrl = false;
+    this._updateUrl();
+  },
+
+  _getUrl: function() {
+    var partiallyEncodedPath =
+        window.encodeURI(this.path).replace(/\#/g, '%23').replace(/\?/g, '%3F');
+    var partiallyEncodedQuery = '';
+    if (this.query) {
+      partiallyEncodedQuery = '?' + this.query.replace(/\#/g, '%23');
+      if (this.encodeSpaceAsPlusInQuery) {
+        partiallyEncodedQuery = partiallyEncodedQuery.replace(/\+/g, '%2B')
+                                    .replace(/ /g, '+')
+                                    .replace(/%20/g, '+');
+      } else {
+        // required for edge
+        partiallyEncodedQuery =
+            partiallyEncodedQuery.replace(/\+/g, '%2B').replace(/ /g, '%20');
+      }
+    }
+    var partiallyEncodedHash = '';
+    if (this.hash) {
+      partiallyEncodedHash = '#' + window.encodeURI(this.hash);
+    }
+    return (
+        partiallyEncodedPath + partiallyEncodedQuery + partiallyEncodedHash);
+  },
+
+  _updateUrl: function() {
+    if (this._dontUpdateUrl || !this._initialized) {
+      return;
+    }
+
+    if (this.path === window.decodeURIComponent(this.__location.pathname) &&
+        this.query === this.__location.search.substring(1) &&
+        this.hash ===
+            window.decodeURIComponent(this.__location.hash.substring(1))) {
+      // Nothing to do, the current URL is a representation of our properties.
+      return;
+    }
+
+    var newUrl = this._getUrl();
+    // Need to use a full URL in case the containing page has a base URI.
+    var fullNewUrl =
+        new URL(newUrl, this.__location.protocol + '//' + this.__location.host)
+            .href;
+    var now = window.performance.now();
+    var shouldReplace = this._lastChangedAt + this.dwellTime > now;
+    this._lastChangedAt = now;
+
+    if (shouldReplace) {
+      window.history.replaceState({}, '', fullNewUrl);
+    } else {
+      window.history.pushState({}, '', fullNewUrl);
+    }
+
+    this.fire('location-changed', {}, {node: window});
+  },
+
+  /**
+   * A necessary evil so that links work as expected. Does its best to
+   * bail out early if possible.
+   *
+   * @param {MouseEvent} event .
+   */
+  _globalOnClick: function(event) {
+    // If another event handler has stopped this event then there's nothing
+    // for us to do. This can happen e.g. when there are multiple
+    // iron-location elements in a page.
+    if (event.defaultPrevented) {
+      return;
+    }
+
+    var href = this._getSameOriginLinkHref(event);
+
+    if (!href) {
+      return;
+    }
+
+    event.preventDefault();
+
+    // If the navigation is to the current page we shouldn't add a history
+    // entry or fire a change event.
+    if (href === this.__location.href) {
+      return;
+    }
+
+    window.history.pushState({}, '', href);
+    this.fire('location-changed', {}, {node: window});
+  },
+
+  /**
+   * Returns the absolute URL of the link (if any) that this click event
+   * is clicking on, if we can and should override the resulting full
+   * page navigation. Returns null otherwise.
+   *
+   * @param {MouseEvent} event .
+   * @return {string?} .
+   */
+  _getSameOriginLinkHref: function(event) {
+    // We only care about left-clicks.
+    if (event.button !== 0) {
+      return null;
+    }
+
+    // We don't want modified clicks, where the intent is to open the page
+    // in a new tab.
+    if (event.metaKey || event.ctrlKey) {
+      return null;
+    }
+
+    var eventPath = dom(event).path;
+    var anchor = null;
+
+    for (var i = 0; i < eventPath.length; i++) {
+      var element = eventPath[i];
+
+      if (element.tagName === 'A' && element.href) {
+        anchor = element;
+        break;
+      }
+    }
+
+    // If there's no link there's nothing to do.
+    if (!anchor) {
+      return null;
+    }
+
+    // Target blank is a new tab, don't intercept.
+    if (anchor.target === '_blank') {
+      return null;
+    }
+
+    // If the link is for an existing parent frame, don't intercept.
+    if ((anchor.target === '_top' || anchor.target === '_parent') &&
+        window.top !== window) {
+      return null;
+    }
+
+    // If the link is a download, don't intercept.
+    if (anchor.download) {
+      return null;
+    }
+
+    var href = anchor.href;
+
+    // It only makes sense for us to intercept same-origin navigations.
+    // pushState/replaceState don't work with cross-origin links.
+    var url;
+
+    if (document.baseURI != null) {
+      url = new URL(href, /** @type {string} */ (document.baseURI));
+    } else {
+      url = new URL(href);
+    }
+
+    var origin;
+
+    // IE Polyfill
+    if (this.__location.origin) {
+      origin = this.__location.origin;
+    } else {
+      origin = this.__location.protocol + '//' + this.__location.host;
+    }
+
+    var urlOrigin;
+
+    if (url.origin) {
+      urlOrigin = url.origin;
+    } else {
+      // IE always adds port number on HTTP and HTTPS on <a>.host but not on
+      // window.location.host
+      var urlHost = url.host;
+      var urlPort = url.port;
+      var urlProtocol = url.protocol;
+      var isExtraneousHTTPS = urlProtocol === 'https:' && urlPort === '443';
+      var isExtraneousHTTP = urlProtocol === 'http:' && urlPort === '80';
+
+      if (isExtraneousHTTPS || isExtraneousHTTP) {
+        urlHost = url.hostname;
+      }
+      urlOrigin = urlProtocol + '//' + urlHost;
+    }
+
+    if (urlOrigin !== origin) {
+      return null;
+    }
+
+    var normalizedHref = url.pathname + url.search + url.hash;
+
+    // pathname should start with '/', but may not if `new URL` is not supported
+    if (normalizedHref[0] !== '/') {
+      normalizedHref = '/' + normalizedHref;
+    }
+
+    // If we've been configured not to handle this url... don't handle it!
+    if (this._urlSpaceRegExp && !this._urlSpaceRegExp.test(normalizedHref)) {
+      return null;
+    }
+
+    // Need to use a full URL in case the containing page has a base URI.
+    var fullNormalizedHref = new URL(normalizedHref, this.__location.href).href;
+    return fullNormalizedHref;
+  },
+
+  _makeRegExp: function(urlSpaceRegex) {
+    return RegExp(urlSpaceRegex);
+  }
+});
diff --git a/third_party/polymer/v3_0/components-chromium/iron-location/iron-query-params.js b/third_party/polymer/v3_0/components-chromium/iron-location/iron-query-params.js
new file mode 100644
index 0000000..98e10b78
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-location/iron-query-params.js
@@ -0,0 +1,94 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+/**
+ * @demo demo/iron-query-params.html
+ */
+Polymer({
+  is: 'iron-query-params',
+
+  properties: {
+    /**
+     * @type{string|undefined}
+     */
+    paramsString: {
+      type: String,
+      notify: true,
+      observer: 'paramsStringChanged',
+    },
+
+    /**
+     * @type{Object|undefined}
+     */
+    paramsObject: {
+      type: Object,
+      notify: true,
+    },
+
+    _dontReact: {type: Boolean, value: false}
+  },
+
+  hostAttributes: {hidden: true},
+
+  observers: ['paramsObjectChanged(paramsObject.*)'],
+
+  paramsStringChanged: function() {
+    this._dontReact = true;
+    this.paramsObject = this._decodeParams(this.paramsString);
+    this._dontReact = false;
+  },
+
+  paramsObjectChanged: function() {
+    if (this._dontReact) {
+      return;
+    }
+    this.paramsString = this._encodeParams(this.paramsObject)
+                            .replace(/%3F/g, '?')
+                            .replace(/%2F/g, '/')
+                            .replace(/'/g, '%27');
+  },
+
+  _encodeParams: function(params) {
+    var encodedParams = [];
+
+    for (var key in params) {
+      var value = params[key];
+
+      if (value === '') {
+        encodedParams.push(encodeURIComponent(key));
+
+      } else if (value) {
+        encodedParams.push(
+            encodeURIComponent(key) + '=' +
+            encodeURIComponent(value.toString()));
+      }
+    }
+    return encodedParams.join('&');
+  },
+
+  _decodeParams: function(paramString) {
+    var params = {};
+    // Work around a bug in decodeURIComponent where + is not
+    // converted to spaces:
+    paramString = (paramString || '').replace(/\+/g, '%20');
+    var paramList = paramString.split('&');
+    for (var i = 0; i < paramList.length; i++) {
+      var param = paramList[i].split('=');
+      if (param[0]) {
+        params[decodeURIComponent(param[0])] =
+            decodeURIComponent(param[1] || '');
+      }
+    }
+    return params;
+  }
+});
diff --git a/third_party/polymer/v3_0/components-chromium/iron-media-query/iron-media-query.js b/third_party/polymer/v3_0/components-chromium/iron-media-query/iron-media-query.js
new file mode 100644
index 0000000..48cb839
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-media-query/iron-media-query.js
@@ -0,0 +1,110 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+
+/**
+`iron-media-query` can be used to data bind to a CSS media query.
+The `query` property is a bare CSS media query.
+The `query-matches` property is a boolean representing whether the page matches
+that media query.
+
+Example:
+
+```html
+<iron-media-query query="(min-width: 600px)" query-matches="{{queryMatches}}">
+</iron-media-query>
+```
+
+@group Iron Elements
+@demo demo/index.html
+@hero hero.svg
+@element iron-media-query
+*/
+Polymer({
+
+  is: 'iron-media-query',
+
+  properties: {
+
+    /**
+     * The Boolean return value of the media query.
+     */
+    queryMatches: {type: Boolean, value: false, readOnly: true, notify: true},
+
+    /**
+     * The CSS media query to evaluate.
+     */
+    query: {type: String, observer: 'queryChanged'},
+
+    /**
+     * If true, the query attribute is assumed to be a complete media query
+     * string rather than a single media feature.
+     */
+    full: {type: Boolean, value: false},
+
+    /**
+     * @type {function(MediaQueryList)}
+     */
+    _boundMQHandler: {
+      value: function() {
+        return this.queryHandler.bind(this);
+      }
+    },
+
+    /**
+     * @type {MediaQueryList}
+     */
+    _mq: {value: null}
+  },
+
+  attached: function() {
+    this.style.display = 'none';
+    this.queryChanged();
+  },
+
+  detached: function() {
+    this._remove();
+  },
+
+  _add: function() {
+    if (this._mq) {
+      this._mq.addListener(this._boundMQHandler);
+    }
+  },
+
+  _remove: function() {
+    if (this._mq) {
+      this._mq.removeListener(this._boundMQHandler);
+    }
+    this._mq = null;
+  },
+
+  queryChanged: function() {
+    this._remove();
+    var query = this.query;
+    if (!query) {
+      return;
+    }
+    if (!this.full && query[0] !== '(') {
+      query = '(' + query + ')';
+    }
+    this._mq = window.matchMedia(query);
+    this._add();
+    this.queryHandler(this._mq);
+  },
+
+  queryHandler: function(mq) {
+    this._setQueryMatches(mq.matches);
+  }
+
+});
diff --git a/third_party/polymer/v3_0/components-chromium/iron-meta/iron-meta.js b/third_party/polymer/v3_0/components-chromium/iron-meta/iron-meta.js
new file mode 100644
index 0000000..e32da21
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-meta/iron-meta.js
@@ -0,0 +1,210 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+
+export class IronMeta {
+  /**
+   * @param {{
+   *   type: (string|null|undefined),
+   *   key: (string|null|undefined),
+   *   value: *,
+   * }=} options
+   */
+  constructor(options) {
+    IronMeta[' '](options);
+
+    /** @type {string} */
+    this.type = (options && options.type) || 'default';
+    /** @type {string|null|undefined} */
+    this.key = options && options.key;
+    if (options && 'value' in options) {
+      /** @type {*} */
+      this.value = options.value;
+    }
+  }
+
+  /** @return {*} */
+  get value() {
+    var type = this.type;
+    var key = this.key;
+
+    if (type && key) {
+      return IronMeta.types[type] && IronMeta.types[type][key];
+    }
+  }
+
+  /** @param {*} value */
+  set value(value) {
+    var type = this.type;
+    var key = this.key;
+
+    if (type && key) {
+      type = IronMeta.types[type] = IronMeta.types[type] || {};
+      if (value == null) {
+        delete type[key];
+      } else {
+        type[key] = value;
+      }
+    }
+  }
+
+  /** @return {!Array<*>} */
+  get list() {
+    var type = this.type;
+
+    if (type) {
+      var items = IronMeta.types[this.type];
+      if (!items) {
+        return [];
+      }
+
+      return Object.keys(items).map(function(key) {
+        return metaDatas[this.type][key];
+      }, this);
+    }
+  }
+
+  /**
+   * @param {string} key
+   * @return {*}
+   */
+  byKey(key) {
+    this.key = key;
+    return this.value;
+  }
+};
+
+// This function is used to convince Closure not to remove constructor calls
+// for instances that are not held anywhere. For example, when
+// `new IronMeta({...})` is used only for the side effect of adding a value.
+IronMeta[' '] = function() {};
+
+IronMeta.types = {};
+
+var metaDatas = IronMeta.types;
+
+/**
+`iron-meta` is a generic element you can use for sharing information across the
+DOM tree. It uses [monostate pattern](http://c2.com/cgi/wiki?MonostatePattern)
+such that any instance of iron-meta has access to the shared information. You
+can use `iron-meta` to share whatever you want (or create an extension [like
+x-meta] for enhancements).
+
+The `iron-meta` instances containing your actual data can be loaded in an
+import, or constructed in any way you see fit. The only requirement is that you
+create them before you try to access them.
+
+Examples:
+
+If I create an instance like this:
+
+    <iron-meta key="info" value="foo/bar"></iron-meta>
+
+Note that value="foo/bar" is the metadata I've defined. I could define more
+attributes or use child nodes to define additional metadata.
+
+Now I can access that element (and it's metadata) from any iron-meta instance
+via the byKey method, e.g.
+
+    meta.byKey('info');
+
+Pure imperative form would be like:
+
+    document.createElement('iron-meta').byKey('info');
+
+Or, in a Polymer element, you can include a meta in your template:
+
+    <iron-meta id="meta"></iron-meta>
+    ...
+    this.$.meta.byKey('info');
+
+@group Iron Elements
+@demo demo/index.html
+@element iron-meta
+*/
+Polymer({
+
+  is: 'iron-meta',
+
+  properties: {
+
+    /**
+     * The type of meta-data.  All meta-data of the same type is stored
+     * together.
+     * @type {string}
+     */
+    type: {
+      type: String,
+      value: 'default',
+    },
+
+    /**
+     * The key used to store `value` under the `type` namespace.
+     * @type {?string}
+     */
+    key: {
+      type: String,
+    },
+
+    /**
+     * The meta-data to store or retrieve.
+     * @type {*}
+     */
+    value: {
+      type: String,
+      notify: true,
+    },
+
+    /**
+     * If true, `value` is set to the iron-meta instance itself.
+     */
+    self: {type: Boolean, observer: '_selfChanged'},
+
+    __meta: {type: Boolean, computed: '__computeMeta(type, key, value)'}
+  },
+
+  hostAttributes: {hidden: true},
+
+  __computeMeta: function(type, key, value) {
+    var meta = new IronMeta({type: type, key: key});
+
+    if (value !== undefined && value !== meta.value) {
+      meta.value = value;
+    } else if (this.value !== meta.value) {
+      this.value = meta.value;
+    }
+
+    return meta;
+  },
+
+  get list() {
+    return this.__meta && this.__meta.list;
+  },
+
+  _selfChanged: function(self) {
+    if (self) {
+      this.value = this;
+    }
+  },
+
+  /**
+   * Retrieves meta data value by key.
+   *
+   * @method byKey
+   * @param {string} key The key of the meta-data to be returned.
+   * @return {*}
+   */
+  byKey: function(key) {
+    return new IronMeta({type: this.type, key: key}).value;
+  }
+});
diff --git a/third_party/polymer/v3_0/components-chromium/iron-overlay-behavior/iron-focusables-helper.js b/third_party/polymer/v3_0/components-chromium/iron-overlay-behavior/iron-focusables-helper.js
new file mode 100644
index 0000000..40c10a0
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-overlay-behavior/iron-focusables-helper.js
@@ -0,0 +1,215 @@
+/**
+@license
+Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+
+var p = Element.prototype;
+var matches = p.matches || p.matchesSelector || p.mozMatchesSelector ||
+    p.msMatchesSelector || p.oMatchesSelector || p.webkitMatchesSelector;
+
+export const IronFocusablesHelper = {
+
+  /**
+   * Returns a sorted array of tabbable nodes, including the root node.
+   * It searches the tabbable nodes in the light and shadow dom of the chidren,
+   * sorting the result by tabindex.
+   * @param {!Node} node
+   * @return {!Array<!HTMLElement>}
+   */
+  getTabbableNodes: function(node) {
+    var result = [];
+    // If there is at least one element with tabindex > 0, we need to sort
+    // the final array by tabindex.
+    var needsSortByTabIndex = this._collectTabbableNodes(node, result);
+    if (needsSortByTabIndex) {
+      return this._sortByTabIndex(result);
+    }
+    return result;
+  },
+
+  /**
+   * Returns if a element is focusable.
+   * @param {!HTMLElement} element
+   * @return {boolean}
+   */
+  isFocusable: function(element) {
+    // From http://stackoverflow.com/a/1600194/4228703:
+    // There isn't a definite list, it's up to the browser. The only
+    // standard we have is DOM Level 2 HTML
+    // https://www.w3.org/TR/DOM-Level-2-HTML/html.html, according to which the
+    // only elements that have a focus() method are HTMLInputElement,
+    // HTMLSelectElement, HTMLTextAreaElement and HTMLAnchorElement. This
+    // notably omits HTMLButtonElement and HTMLAreaElement. Referring to these
+    // tests with tabbables in different browsers
+    // http://allyjs.io/data-tables/focusable.html
+
+    // Elements that cannot be focused if they have [disabled] attribute.
+    if (matches.call(element, 'input, select, textarea, button, object')) {
+      return matches.call(element, ':not([disabled])');
+    }
+    // Elements that can be focused even if they have [disabled] attribute.
+    return matches.call(
+        element, 'a[href], area[href], iframe, [tabindex], [contentEditable]');
+  },
+
+  /**
+   * Returns if a element is tabbable. To be tabbable, a element must be
+   * focusable, visible, and with a tabindex !== -1.
+   * @param {!HTMLElement} element
+   * @return {boolean}
+   */
+  isTabbable: function(element) {
+    return this.isFocusable(element) &&
+        matches.call(element, ':not([tabindex="-1"])') &&
+        this._isVisible(element);
+  },
+
+  /**
+   * Returns the normalized element tabindex. If not focusable, returns -1.
+   * It checks for the attribute "tabindex" instead of the element property
+   * `tabIndex` since browsers assign different values to it.
+   * e.g. in Firefox `<div contenteditable>` has `tabIndex = -1`
+   * @param {!HTMLElement} element
+   * @return {!number}
+   * @private
+   */
+  _normalizedTabIndex: function(element) {
+    if (this.isFocusable(element)) {
+      var tabIndex = element.getAttribute('tabindex') || 0;
+      return Number(tabIndex);
+    }
+    return -1;
+  },
+
+  /**
+   * Searches for nodes that are tabbable and adds them to the `result` array.
+   * Returns if the `result` array needs to be sorted by tabindex.
+   * @param {!Node} node The starting point for the search; added to `result`
+   * if tabbable.
+   * @param {!Array<!HTMLElement>} result
+   * @return {boolean}
+   * @private
+   */
+  _collectTabbableNodes: function(node, result) {
+    // If not an element or not visible, no need to explore children.
+    if (node.nodeType !== Node.ELEMENT_NODE || !this._isVisible(node)) {
+      return false;
+    }
+    var element = /** @type {!HTMLElement} */ (node);
+    var tabIndex = this._normalizedTabIndex(element);
+    var needsSort = tabIndex > 0;
+    if (tabIndex >= 0) {
+      result.push(element);
+    }
+
+    // In ShadowDOM v1, tab order is affected by the order of distrubution.
+    // E.g. getTabbableNodes(#root) in ShadowDOM v1 should return [#A, #B];
+    // in ShadowDOM v0 tab order is not affected by the distrubution order,
+    // in fact getTabbableNodes(#root) returns [#B, #A].
+    //  <div id="root">
+    //   <!-- shadow -->
+    //     <slot name="a">
+    //     <slot name="b">
+    //   <!-- /shadow -->
+    //   <input id="A" slot="a">
+    //   <input id="B" slot="b" tabindex="1">
+    //  </div>
+    // TODO(valdrin) support ShadowDOM v1 when upgrading to Polymer v2.0.
+    var children;
+    if (element.localName === 'content' || element.localName === 'slot') {
+      children = dom(element).getDistributedNodes();
+    } else {
+      // Use shadow root if possible, will check for distributed nodes.
+      children = dom(element.root || element).children;
+    }
+    for (var i = 0; i < children.length; i++) {
+      // Ensure method is always invoked to collect tabbable children.
+      needsSort = this._collectTabbableNodes(children[i], result) || needsSort;
+    }
+    return needsSort;
+  },
+
+  /**
+   * Returns false if the element has `visibility: hidden` or `display: none`
+   * @param {!HTMLElement} element
+   * @return {boolean}
+   * @private
+   */
+  _isVisible: function(element) {
+    // Check inline style first to save a re-flow. If looks good, check also
+    // computed style.
+    var style = element.style;
+    if (style.visibility !== 'hidden' && style.display !== 'none') {
+      style = window.getComputedStyle(element);
+      return (style.visibility !== 'hidden' && style.display !== 'none');
+    }
+    return false;
+  },
+
+  /**
+   * Sorts an array of tabbable elements by tabindex. Returns a new array.
+   * @param {!Array<!HTMLElement>} tabbables
+   * @return {!Array<!HTMLElement>}
+   * @private
+   */
+  _sortByTabIndex: function(tabbables) {
+    // Implement a merge sort as Array.prototype.sort does a non-stable sort
+    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
+    var len = tabbables.length;
+    if (len < 2) {
+      return tabbables;
+    }
+    var pivot = Math.ceil(len / 2);
+    var left = this._sortByTabIndex(tabbables.slice(0, pivot));
+    var right = this._sortByTabIndex(tabbables.slice(pivot));
+    return this._mergeSortByTabIndex(left, right);
+  },
+
+  /**
+   * Merge sort iterator, merges the two arrays into one, sorted by tab index.
+   * @param {!Array<!HTMLElement>} left
+   * @param {!Array<!HTMLElement>} right
+   * @return {!Array<!HTMLElement>}
+   * @private
+   */
+  _mergeSortByTabIndex: function(left, right) {
+    var result = [];
+    while ((left.length > 0) && (right.length > 0)) {
+      if (this._hasLowerTabOrder(left[0], right[0])) {
+        result.push(right.shift());
+      } else {
+        result.push(left.shift());
+      }
+    }
+
+    return result.concat(left, right);
+  },
+
+  /**
+   * Returns if element `a` has lower tab order compared to element `b`
+   * (both elements are assumed to be focusable and tabbable).
+   * Elements with tabindex = 0 have lower tab order compared to elements
+   * with tabindex > 0.
+   * If both have same tabindex, it returns false.
+   * @param {!HTMLElement} a
+   * @param {!HTMLElement} b
+   * @return {boolean}
+   * @private
+   */
+  _hasLowerTabOrder: function(a, b) {
+    // Normalize tabIndexes
+    // e.g. in Firefox `<div contenteditable>` has `tabIndex = -1`
+    var ati = Math.max(a.tabIndex, 0);
+    var bti = Math.max(b.tabIndex, 0);
+    return (ati === 0 || bti === 0) ? bti > ati : ati > bti;
+  }
+};
diff --git a/third_party/polymer/v3_0/components-chromium/iron-overlay-behavior/iron-overlay-backdrop.js b/third_party/polymer/v3_0/components-chromium/iron-overlay-behavior/iron-overlay-backdrop.js
new file mode 100644
index 0000000..8bd4927
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-overlay-behavior/iron-overlay-backdrop.js
@@ -0,0 +1,158 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+
+/*
+`iron-overlay-backdrop` is a backdrop used by `Polymer.IronOverlayBehavior`. It
+should be a singleton.
+
+### Styling
+
+The following custom properties and mixins are available for styling.
+
+Custom property | Description | Default
+-------------------------------------------|------------------------|---------
+`--iron-overlay-backdrop-background-color` | Backdrop background color | #000
+`--iron-overlay-backdrop-opacity`          | Backdrop opacity | 0.6
+`--iron-overlay-backdrop`                  | Mixin applied to `iron-overlay-backdrop`.                      | {}
+`--iron-overlay-backdrop-opened`           | Mixin applied to `iron-overlay-backdrop` when it is displayed | {}
+*/
+Polymer({
+  _template: html`
+    <style>
+      :host {
+        position: fixed;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        background-color: var(--iron-overlay-backdrop-background-color, #000);
+        opacity: 0;
+        transition: opacity 0.2s;
+        pointer-events: none;
+        @apply --iron-overlay-backdrop;
+      }
+
+      :host(.opened) {
+        opacity: var(--iron-overlay-backdrop-opacity, 0.6);
+        pointer-events: auto;
+        @apply --iron-overlay-backdrop-opened;
+      }
+    </style>
+
+    <slot></slot>
+`,
+
+  is: 'iron-overlay-backdrop',
+
+  properties: {
+
+    /**
+     * Returns true if the backdrop is opened.
+     */
+    opened: {
+      reflectToAttribute: true,
+      type: Boolean,
+      value: false,
+      observer: '_openedChanged',
+    }
+
+  },
+
+  listeners: {
+    'transitionend': '_onTransitionend',
+  },
+
+  created: function() {
+    // Used to cancel previous requestAnimationFrame calls when opened changes.
+    this.__openedRaf = null;
+  },
+
+  attached: function() {
+    this.opened && this._openedChanged(this.opened);
+  },
+
+  /**
+   * Appends the backdrop to document body if needed.
+   */
+  prepare: function() {
+    if (this.opened && !this.parentNode) {
+      dom(document.body).appendChild(this);
+    }
+  },
+
+  /**
+   * Shows the backdrop.
+   */
+  open: function() {
+    this.opened = true;
+  },
+
+  /**
+   * Hides the backdrop.
+   */
+  close: function() {
+    this.opened = false;
+  },
+
+  /**
+   * Removes the backdrop from document body if needed.
+   */
+  complete: function() {
+    if (!this.opened && this.parentNode === document.body) {
+      dom(this.parentNode).removeChild(this);
+    }
+  },
+
+  _onTransitionend: function(event) {
+    if (event && event.target === this) {
+      this.complete();
+    }
+  },
+
+  /**
+   * @param {boolean} opened
+   * @private
+   */
+  _openedChanged: function(opened) {
+    if (opened) {
+      // Auto-attach.
+      this.prepare();
+    } else {
+      // Animation might be disabled via the mixin or opacity custom property.
+      // If it is disabled in other ways, it's up to the user to call complete.
+      var cs = window.getComputedStyle(this);
+      if (cs.transitionDuration === '0s' || cs.opacity == 0) {
+        this.complete();
+      }
+    }
+
+    if (!this.isAttached) {
+      return;
+    }
+
+    // Always cancel previous requestAnimationFrame.
+    if (this.__openedRaf) {
+      window.cancelAnimationFrame(this.__openedRaf);
+      this.__openedRaf = null;
+    }
+    // Force relayout to ensure proper transitions.
+    this.scrollTop = this.scrollTop;
+    this.__openedRaf = window.requestAnimationFrame(function() {
+      this.__openedRaf = null;
+      this.toggleClass('opened', this.opened);
+    }.bind(this));
+  }
+});
diff --git a/third_party/polymer/v3_0/components-chromium/iron-overlay-behavior/iron-overlay-behavior.js b/third_party/polymer/v3_0/components-chromium/iron-overlay-behavior/iron-overlay-behavior.js
new file mode 100644
index 0000000..0f215b1
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-overlay-behavior/iron-overlay-behavior.js
@@ -0,0 +1,825 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {IronFitBehavior} from '../iron-fit-behavior/iron-fit-behavior.js';
+import {IronResizableBehavior} from '../iron-resizable-behavior/iron-resizable-behavior.js';
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+import {useShadow} from '../polymer/lib/utils/settings.js';
+
+import {IronFocusablesHelper} from './iron-focusables-helper.js';
+import {IronOverlayManager} from './iron-overlay-manager.js';
+import {pushScrollLock, removeScrollLock} from './iron-scroll-manager.js';
+
+/** @polymerBehavior */
+export const IronOverlayBehaviorImpl = {
+
+  properties: {
+
+    /**
+     * True if the overlay is currently displayed.
+     */
+    opened:
+        {observer: '_openedChanged', type: Boolean, value: false, notify: true},
+
+    /**
+     * True if the overlay was canceled when it was last closed.
+     */
+    canceled: {
+      observer: '_canceledChanged',
+      readOnly: true,
+      type: Boolean,
+      value: false
+    },
+
+    /**
+     * Set to true to display a backdrop behind the overlay. It traps the focus
+     * within the light DOM of the overlay.
+     */
+    withBackdrop: {
+      observer: '_withBackdropChanged',
+      type: Boolean,
+    },
+
+    /**
+     * Set to true to disable auto-focusing the overlay or child nodes with
+     * the `autofocus` attribute` when the overlay is opened.
+     */
+    noAutoFocus: {
+      type: Boolean,
+      value: false,
+    },
+
+    /**
+     * Set to true to disable canceling the overlay with the ESC key.
+     */
+    noCancelOnEscKey: {
+      type: Boolean,
+      value: false,
+    },
+
+    /**
+     * Set to true to disable canceling the overlay by clicking outside it.
+     */
+    noCancelOnOutsideClick: {
+      type: Boolean,
+      value: false,
+    },
+
+    /**
+     * Contains the reason(s) this overlay was last closed (see
+     * `iron-overlay-closed`). `IronOverlayBehavior` provides the `canceled`
+     * reason; implementers of the behavior can provide other reasons in
+     * addition to `canceled`.
+     */
+    closingReason: {
+      // was a getter before, but needs to be a property so other
+      // behaviors can override this.
+      type: Object,
+    },
+
+    /**
+     * Set to true to enable restoring of focus when overlay is closed.
+     */
+    restoreFocusOnClose: {
+      type: Boolean,
+      value: false,
+    },
+
+    /**
+     * Set to true to allow clicks to go through overlays.
+     * When the user clicks outside this overlay, the click may
+     * close the overlay below.
+     */
+    allowClickThrough: {
+      type: Boolean,
+    },
+
+    /**
+     * Set to true to keep overlay always on top.
+     */
+    alwaysOnTop: {
+      type: Boolean,
+    },
+
+    /**
+     * Determines which action to perform when scroll outside an opened overlay
+     * happens. Possible values: lock - blocks scrolling from happening, refit -
+     * computes the new position on the overlay cancel - causes the overlay to
+     * close
+     */
+    scrollAction: {
+      type: String,
+    },
+
+    /**
+     * Shortcut to access to the overlay manager.
+     * @private
+     * @type {!IronOverlayManagerClass}
+     */
+    _manager: {
+      type: Object,
+      value: IronOverlayManager,
+    },
+
+    /**
+     * The node being focused.
+     * @type {?Node}
+     */
+    _focusedChild: {
+      type: Object,
+    }
+
+  },
+
+  listeners: {'iron-resize': '_onIronResize'},
+
+  observers: ['__updateScrollObservers(isAttached, opened, scrollAction)'],
+
+  /**
+   * The backdrop element.
+   * @return {!Element}
+   */
+  get backdropElement() {
+    return this._manager.backdropElement;
+  },
+
+  /**
+   * Returns the node to give focus to.
+   * @return {!Node}
+   */
+  get _focusNode() {
+    return this._focusedChild || dom(this).querySelector('[autofocus]') || this;
+  },
+
+  /**
+   * Array of nodes that can receive focus (overlay included), ordered by
+   * `tabindex`. This is used to retrieve which is the first and last focusable
+   * nodes in order to wrap the focus for overlays `with-backdrop`.
+   *
+   * If you know what is your content (specifically the first and last focusable
+   * children), you can override this method to return only `[firstFocusable,
+   * lastFocusable];`
+   * @return {!Array<!Node>}
+   * @protected
+   */
+  get _focusableNodes() {
+    return IronFocusablesHelper.getTabbableNodes(this);
+  },
+
+  /**
+   * @return {void}
+   */
+  ready: function() {
+    // Used to skip calls to notifyResize and refit while the overlay is
+    // animating.
+    this.__isAnimating = false;
+    // with-backdrop needs tabindex to be set in order to trap the focus.
+    // If it is not set, IronOverlayBehavior will set it, and remove it if
+    // with-backdrop = false.
+    this.__shouldRemoveTabIndex = false;
+    // Used for wrapping the focus on TAB / Shift+TAB.
+    this.__firstFocusableNode = this.__lastFocusableNode = null;
+    // Used by to keep track of the RAF callbacks.
+    this.__rafs = {};
+    // Focused node before overlay gets opened. Can be restored on close.
+    this.__restoreFocusNode = null;
+    // Scroll info to be restored.
+    this.__scrollTop = this.__scrollLeft = null;
+    this.__onCaptureScroll = this.__onCaptureScroll.bind(this);
+    // Root nodes hosting the overlay, used to listen for scroll events on them.
+    this.__rootNodes = null;
+    this._ensureSetup();
+  },
+
+  attached: function() {
+    // Call _openedChanged here so that position can be computed correctly.
+    if (this.opened) {
+      this._openedChanged(this.opened);
+    }
+    this._observer = dom(this).observeNodes(this._onNodesChange);
+  },
+
+  detached: function() {
+    dom(this).unobserveNodes(this._observer);
+    this._observer = null;
+    for (var cb in this.__rafs) {
+      if (this.__rafs[cb] !== null) {
+        cancelAnimationFrame(this.__rafs[cb]);
+      }
+    }
+    this.__rafs = {};
+    this._manager.removeOverlay(this);
+
+    // We got detached while animating, ensure we show/hide the overlay
+    // and fire iron-overlay-opened/closed event!
+    if (this.__isAnimating) {
+      if (this.opened) {
+        this._finishRenderOpened();
+      } else {
+        // Restore the focus if necessary.
+        this._applyFocus();
+        this._finishRenderClosed();
+      }
+    }
+  },
+
+  /**
+   * Toggle the opened state of the overlay.
+   */
+  toggle: function() {
+    this._setCanceled(false);
+    this.opened = !this.opened;
+  },
+
+  /**
+   * Open the overlay.
+   */
+  open: function() {
+    this._setCanceled(false);
+    this.opened = true;
+  },
+
+  /**
+   * Close the overlay.
+   */
+  close: function() {
+    this._setCanceled(false);
+    this.opened = false;
+  },
+
+  /**
+   * Cancels the overlay.
+   * @param {Event=} event The original event
+   */
+  cancel: function(event) {
+    var cancelEvent =
+        this.fire('iron-overlay-canceled', event, {cancelable: true});
+    if (cancelEvent.defaultPrevented) {
+      return;
+    }
+
+    this._setCanceled(true);
+    this.opened = false;
+  },
+
+  /**
+   * Invalidates the cached tabbable nodes. To be called when any of the
+   * focusable content changes (e.g. a button is disabled).
+   */
+  invalidateTabbables: function() {
+    this.__firstFocusableNode = this.__lastFocusableNode = null;
+  },
+
+  _ensureSetup: function() {
+    if (this._overlaySetup) {
+      return;
+    }
+    this._overlaySetup = true;
+    this.style.outline = 'none';
+    this.style.display = 'none';
+  },
+
+  /**
+   * Called when `opened` changes.
+   * @param {boolean=} opened
+   * @protected
+   */
+  _openedChanged: function(opened) {
+    if (opened) {
+      this.removeAttribute('aria-hidden');
+    } else {
+      this.setAttribute('aria-hidden', 'true');
+    }
+
+    // Defer any animation-related code on attached
+    // (_openedChanged gets called again on attached).
+    if (!this.isAttached) {
+      return;
+    }
+
+    this.__isAnimating = true;
+
+    // Deraf for non-blocking rendering.
+    this.__deraf('__openedChanged', this.__openedChanged);
+  },
+
+  _canceledChanged: function() {
+    this.closingReason = this.closingReason || {};
+    this.closingReason.canceled = this.canceled;
+  },
+
+  _withBackdropChanged: function() {
+    // If tabindex is already set, no need to override it.
+    if (this.withBackdrop && !this.hasAttribute('tabindex')) {
+      this.setAttribute('tabindex', '-1');
+      this.__shouldRemoveTabIndex = true;
+    } else if (this.__shouldRemoveTabIndex) {
+      this.removeAttribute('tabindex');
+      this.__shouldRemoveTabIndex = false;
+    }
+    if (this.opened && this.isAttached) {
+      this._manager.trackBackdrop();
+    }
+  },
+
+  /**
+   * tasks which must occur before opening; e.g. making the element visible.
+   * @protected
+   */
+  _prepareRenderOpened: function() {
+    // Store focused node.
+    this.__restoreFocusNode = this._manager.deepActiveElement;
+
+    // Needed to calculate the size of the overlay so that transitions on its
+    // size will have the correct starting points.
+    this._preparePositioning();
+    this.refit();
+    this._finishPositioning();
+
+    // Safari will apply the focus to the autofocus element when displayed
+    // for the first time, so we make sure to return the focus where it was.
+    if (this.noAutoFocus && document.activeElement === this._focusNode) {
+      this._focusNode.blur();
+      this.__restoreFocusNode.focus();
+    }
+  },
+
+  /**
+   * Tasks which cause the overlay to actually open; typically play an
+   * animation.
+   * @protected
+   */
+  _renderOpened: function() {
+    this._finishRenderOpened();
+  },
+
+  /**
+   * Tasks which cause the overlay to actually close; typically play an
+   * animation.
+   * @protected
+   */
+  _renderClosed: function() {
+    this._finishRenderClosed();
+  },
+
+  /**
+   * Tasks to be performed at the end of open action. Will fire
+   * `iron-overlay-opened`.
+   * @protected
+   */
+  _finishRenderOpened: function() {
+    this.notifyResize();
+    this.__isAnimating = false;
+
+    this.fire('iron-overlay-opened');
+  },
+
+  /**
+   * Tasks to be performed at the end of close action. Will fire
+   * `iron-overlay-closed`.
+   * @protected
+   */
+  _finishRenderClosed: function() {
+    // Hide the overlay.
+    this.style.display = 'none';
+    // Reset z-index only at the end of the animation.
+    this.style.zIndex = '';
+    this.notifyResize();
+    this.__isAnimating = false;
+    this.fire('iron-overlay-closed', this.closingReason);
+  },
+
+  _preparePositioning: function() {
+    this.style.transition = this.style.webkitTransition = 'none';
+    this.style.transform = this.style.webkitTransform = 'none';
+    this.style.display = '';
+  },
+
+  _finishPositioning: function() {
+    // First, make it invisible & reactivate animations.
+    this.style.display = 'none';
+    // Force reflow before re-enabling animations so that they don't start.
+    // Set scrollTop to itself so that Closure Compiler doesn't remove this.
+    this.scrollTop = this.scrollTop;
+    this.style.transition = this.style.webkitTransition = '';
+    this.style.transform = this.style.webkitTransform = '';
+    // Now that animations are enabled, make it visible again
+    this.style.display = '';
+    // Force reflow, so that following animations are properly started.
+    // Set scrollTop to itself so that Closure Compiler doesn't remove this.
+    this.scrollTop = this.scrollTop;
+  },
+
+  /**
+   * Applies focus according to the opened state.
+   * @protected
+   */
+  _applyFocus: function() {
+    if (this.opened) {
+      if (!this.noAutoFocus) {
+        this._focusNode.focus();
+      }
+    } else {
+      // Restore focus.
+      if (this.restoreFocusOnClose && this.__restoreFocusNode) {
+        // If the activeElement is `<body>` or inside the overlay,
+        // we are allowed to restore the focus. In all the other
+        // cases focus might have been moved elsewhere by another
+        // component or by an user interaction (e.g. click on a
+        // button outside the overlay).
+        var activeElement = this._manager.deepActiveElement;
+        if (activeElement === document.body ||
+            dom(this).deepContains(activeElement)) {
+          this.__restoreFocusNode.focus();
+        }
+      }
+      this.__restoreFocusNode = null;
+      this._focusNode.blur();
+      this._focusedChild = null;
+    }
+  },
+
+  /**
+   * Cancels (closes) the overlay. Call when click happens outside the overlay.
+   * @param {!Event} event
+   * @protected
+   */
+  _onCaptureClick: function(event) {
+    if (!this.noCancelOnOutsideClick) {
+      this.cancel(event);
+    }
+  },
+
+  /**
+   * Keeps track of the focused child. If withBackdrop, traps focus within
+   * overlay.
+   * @param {!Event} event
+   * @protected
+   */
+  _onCaptureFocus: function(event) {
+    if (!this.withBackdrop) {
+      return;
+    }
+    var path = dom(event).path;
+    if (path.indexOf(this) === -1) {
+      event.stopPropagation();
+      this._applyFocus();
+    } else {
+      this._focusedChild = path[0];
+    }
+  },
+
+  /**
+   * Handles the ESC key event and cancels (closes) the overlay.
+   * @param {!Event} event
+   * @protected
+   */
+  _onCaptureEsc: function(event) {
+    if (!this.noCancelOnEscKey) {
+      this.cancel(event);
+    }
+  },
+
+  /**
+   * Handles TAB key events to track focus changes.
+   * Will wrap focus for overlays withBackdrop.
+   * @param {!Event} event
+   * @protected
+   */
+  _onCaptureTab: function(event) {
+    if (!this.withBackdrop) {
+      return;
+    }
+    this.__ensureFirstLastFocusables();
+    // TAB wraps from last to first focusable.
+    // Shift + TAB wraps from first to last focusable.
+    var shift = event.shiftKey;
+    var nodeToCheck =
+        shift ? this.__firstFocusableNode : this.__lastFocusableNode;
+    var nodeToSet =
+        shift ? this.__lastFocusableNode : this.__firstFocusableNode;
+    var shouldWrap = false;
+    if (nodeToCheck === nodeToSet) {
+      // If nodeToCheck is the same as nodeToSet, it means we have an overlay
+      // with 0 or 1 focusables; in either case we still need to trap the
+      // focus within the overlay.
+      shouldWrap = true;
+    } else {
+      // In dom=shadow, the manager will receive focus changes on the main
+      // root but not the ones within other shadow roots, so we can't rely on
+      // _focusedChild, but we should check the deepest active element.
+      var focusedNode = this._manager.deepActiveElement;
+      // If the active element is not the nodeToCheck but the overlay itself,
+      // it means the focus is about to go outside the overlay, hence we
+      // should prevent that (e.g. user opens the overlay and hit Shift+TAB).
+      shouldWrap = (focusedNode === nodeToCheck || focusedNode === this);
+    }
+
+    if (shouldWrap) {
+      // When the overlay contains the last focusable element of the document
+      // and it's already focused, pressing TAB would move the focus outside
+      // the document (e.g. to the browser search bar). Similarly, when the
+      // overlay contains the first focusable element of the document and it's
+      // already focused, pressing Shift+TAB would move the focus outside the
+      // document (e.g. to the browser search bar).
+      // In both cases, we would not receive a focus event, but only a blur.
+      // In order to achieve focus wrapping, we prevent this TAB event and
+      // force the focus. This will also prevent the focus to temporarily move
+      // outside the overlay, which might cause scrolling.
+      event.preventDefault();
+      this._focusedChild = nodeToSet;
+      this._applyFocus();
+    }
+  },
+
+  /**
+   * Refits if the overlay is opened and not animating.
+   * @protected
+   */
+  _onIronResize: function() {
+    if (this.opened && !this.__isAnimating) {
+      this.__deraf('refit', this.refit);
+    }
+  },
+
+  /**
+   * Will call notifyResize if overlay is opened.
+   * Can be overridden in order to avoid multiple observers on the same node.
+   * @protected
+   */
+  _onNodesChange: function() {
+    if (this.opened && !this.__isAnimating) {
+      // It might have added focusable nodes, so invalidate cached values.
+      this.invalidateTabbables();
+      this.notifyResize();
+    }
+  },
+
+  /**
+   * Updates the references to the first and last focusable nodes.
+   * @private
+   */
+  __ensureFirstLastFocusables: function() {
+    var focusableNodes = this._focusableNodes;
+    this.__firstFocusableNode = focusableNodes[0];
+    this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1];
+  },
+
+  /**
+   * Tasks executed when opened changes: prepare for the opening, move the
+   * focus, update the manager, render opened/closed.
+   * @private
+   */
+  __openedChanged: function() {
+    if (this.opened) {
+      // Make overlay visible, then add it to the manager.
+      this._prepareRenderOpened();
+      this._manager.addOverlay(this);
+      // Move the focus to the child node with [autofocus].
+      this._applyFocus();
+
+      this._renderOpened();
+    } else {
+      // Remove overlay, then restore the focus before actually closing.
+      this._manager.removeOverlay(this);
+      this._applyFocus();
+
+      this._renderClosed();
+    }
+  },
+
+  /**
+   * Debounces the execution of a callback to the next animation frame.
+   * @param {!string} jobname
+   * @param {!Function} callback Always bound to `this`
+   * @private
+   */
+  __deraf: function(jobname, callback) {
+    var rafs = this.__rafs;
+    if (rafs[jobname] !== null) {
+      cancelAnimationFrame(rafs[jobname]);
+    }
+    rafs[jobname] = requestAnimationFrame(function nextAnimationFrame() {
+      rafs[jobname] = null;
+      callback.call(this);
+    }.bind(this));
+  },
+
+  /**
+   * @param {boolean} isAttached
+   * @param {boolean} opened
+   * @param {string=} scrollAction
+   * @private
+   */
+  __updateScrollObservers: function(isAttached, opened, scrollAction) {
+    if (!isAttached || !opened || !this.__isValidScrollAction(scrollAction)) {
+      removeScrollLock(this);
+      this.__removeScrollListeners();
+    } else {
+      if (scrollAction === 'lock') {
+        this.__saveScrollPosition();
+        pushScrollLock(this);
+      }
+      this.__addScrollListeners();
+    }
+  },
+
+  /**
+   * @private
+   */
+  __addScrollListeners: function() {
+    if (!this.__rootNodes) {
+      this.__rootNodes = [];
+      // Listen for scroll events in all shadowRoots hosting this overlay only
+      // when in native ShadowDOM.
+      if (useShadow) {
+        var node = this;
+        while (node) {
+          if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE && node.host) {
+            this.__rootNodes.push(node);
+          }
+          node = node.host || node.assignedSlot || node.parentNode;
+        }
+      }
+      this.__rootNodes.push(document);
+    }
+    this.__rootNodes.forEach(function(el) {
+      el.addEventListener('scroll', this.__onCaptureScroll, {
+        capture: true,
+        passive: true,
+      });
+    }, this);
+  },
+
+  /**
+   * @private
+   */
+  __removeScrollListeners: function() {
+    if (this.__rootNodes) {
+      this.__rootNodes.forEach(function(el) {
+        el.removeEventListener('scroll', this.__onCaptureScroll, {
+          capture: true,
+          passive: true,
+        });
+      }, this);
+    }
+    if (!this.isAttached) {
+      this.__rootNodes = null;
+    }
+  },
+
+  /**
+   * @param {string=} scrollAction
+   * @return {boolean}
+   * @private
+   */
+  __isValidScrollAction: function(scrollAction) {
+    return scrollAction === 'lock' || scrollAction === 'refit' ||
+        scrollAction === 'cancel';
+  },
+
+  /**
+   * @private
+   */
+  __onCaptureScroll: function(event) {
+    if (this.__isAnimating) {
+      return;
+    }
+    // Check if scroll outside the overlay.
+    if (dom(event).path.indexOf(this) >= 0) {
+      return;
+    }
+    switch (this.scrollAction) {
+      case 'lock':
+        // NOTE: scrolling might happen if a scroll event is not cancellable, or
+        // if user pressed keys that cause scrolling (they're not prevented in
+        // order not to break a11y features like navigate with arrow keys).
+        this.__restoreScrollPosition();
+        break;
+      case 'refit':
+        this.__deraf('refit', this.refit);
+        break;
+      case 'cancel':
+        this.cancel(event);
+        break;
+    }
+  },
+
+  /**
+   * Memoizes the scroll position of the outside scrolling element.
+   * @private
+   */
+  __saveScrollPosition: function() {
+    if (document.scrollingElement) {
+      this.__scrollTop = document.scrollingElement.scrollTop;
+      this.__scrollLeft = document.scrollingElement.scrollLeft;
+    } else {
+      // Since we don't know if is the body or html, get max.
+      this.__scrollTop =
+          Math.max(document.documentElement.scrollTop, document.body.scrollTop);
+      this.__scrollLeft = Math.max(
+          document.documentElement.scrollLeft, document.body.scrollLeft);
+    }
+  },
+
+  /**
+   * Resets the scroll position of the outside scrolling element.
+   * @private
+   */
+  __restoreScrollPosition: function() {
+    if (document.scrollingElement) {
+      document.scrollingElement.scrollTop = this.__scrollTop;
+      document.scrollingElement.scrollLeft = this.__scrollLeft;
+    } else {
+      // Since we don't know if is the body or html, set both.
+      document.documentElement.scrollTop = document.body.scrollTop =
+          this.__scrollTop;
+      document.documentElement.scrollLeft = document.body.scrollLeft =
+          this.__scrollLeft;
+    }
+  },
+
+};
+
+/**
+  Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden
+  or shown, and displays on top of other content. It includes an optional
+  backdrop, and can be used to implement a variety of UI controls including
+  dialogs and drop downs. Multiple overlays may be displayed at once.
+
+  See the [demo source
+  code](https://github.com/PolymerElements/iron-overlay-behavior/blob/master/demo/simple-overlay.html)
+  for an example.
+
+  ### Closing and canceling
+
+  An overlay may be hidden by closing or canceling. The difference between close
+  and cancel is user intent. Closing generally implies that the user
+  acknowledged the content on the overlay. By default, it will cancel whenever
+  the user taps outside it or presses the escape key. This behavior is
+  configurable with the `no-cancel-on-esc-key` and the
+  `no-cancel-on-outside-click` properties. `close()` should be called explicitly
+  by the implementer when the user interacts with a control in the overlay
+  element. When the dialog is canceled, the overlay fires an
+  'iron-overlay-canceled' event. Call `preventDefault` on this event to prevent
+  the overlay from closing.
+
+  ### Positioning
+
+  By default the element is sized and positioned to fit and centered inside the
+  window. You can position and size it manually using CSS. See
+  `Polymer.IronFitBehavior`.
+
+  ### Backdrop
+
+  Set the `with-backdrop` attribute to display a backdrop behind the overlay.
+  The backdrop is appended to `<body>` and is of type `<iron-overlay-backdrop>`.
+  See its doc page for styling options.
+
+  In addition, `with-backdrop` will wrap the focus within the content in the
+  light DOM. Override the [`_focusableNodes`
+  getter](#Polymer.IronOverlayBehavior:property-_focusableNodes) to achieve a
+  different behavior.
+
+  ### Limitations
+
+  The element is styled to appear on top of other content by setting its
+  `z-index` property. You must ensure no element has a stacking context with a
+  higher `z-index` than its parent stacking context. You should place this
+  element as a child of `<body>` whenever possible.
+
+  @demo demo/index.html
+  @polymerBehavior
+ */
+export const IronOverlayBehavior =
+    [IronFitBehavior, IronResizableBehavior, IronOverlayBehaviorImpl];
+
+/**
+ * Fired after the overlay opens.
+ * @event iron-overlay-opened
+ */
+
+/**
+ * Fired when the overlay is canceled, but before it is closed.
+ * @event iron-overlay-canceled
+ * @param {Event} event The closing of the overlay can be prevented
+ * by calling `event.preventDefault()`. The `event.detail` is the original event
+ * that originated the canceling (e.g. ESC keyboard event or click event outside
+ * the overlay).
+ */
+
+/**
+ * Fired after the overlay closes.
+ * @event iron-overlay-closed
+ * @param {Event} event The `event.detail` is the `closingReason` property
+ * (contains `canceled`, whether the overlay was canceled).
+ */
diff --git a/third_party/polymer/v3_0/components-chromium/iron-overlay-behavior/iron-overlay-manager.js b/third_party/polymer/v3_0/components-chromium/iron-overlay-behavior/iron-overlay-manager.js
new file mode 100644
index 0000000..99768f4
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-overlay-behavior/iron-overlay-manager.js
@@ -0,0 +1,389 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+import './iron-overlay-backdrop.js';
+
+import {IronA11yKeysBehavior} from '../iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+import * as gestures from '../polymer/lib/utils/gestures.js';
+
+/**
+ * @struct
+ * @constructor
+ * @private
+ */
+export const IronOverlayManagerClass = function() {
+  /**
+   * Used to keep track of the opened overlays.
+   * @private {!Array<!Element>}
+   */
+  this._overlays = [];
+
+  /**
+   * iframes have a default z-index of 100,
+   * so this default should be at least that.
+   * @private {number}
+   */
+  this._minimumZ = 101;
+
+  /**
+   * Memoized backdrop element.
+   * @private {Element|null}
+   */
+  this._backdropElement = null;
+
+  // Enable document-wide tap recognizer.
+  // NOTE: Use useCapture=true to avoid accidentally prevention of the closing
+  // of an overlay via event.stopPropagation(). The only way to prevent
+  // closing of an overlay should be through its APIs.
+  // NOTE: enable tap on <html> to workaround Polymer/polymer#4459
+  // Pass no-op function because MSEdge 15 doesn't handle null as 2nd argument
+  // https://github.com/Microsoft/ChakraCore/issues/3863
+  gestures.add(document.documentElement, 'tap', function() {});
+  document.addEventListener('tap', this._onCaptureClick.bind(this), true);
+  document.addEventListener('focus', this._onCaptureFocus.bind(this), true);
+  document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true);
+};
+
+IronOverlayManagerClass.prototype = {
+
+  constructor: IronOverlayManagerClass,
+
+  /**
+   * The shared backdrop element.
+   * @return {!Element} backdropElement
+   */
+  get backdropElement() {
+    if (!this._backdropElement) {
+      this._backdropElement = document.createElement('iron-overlay-backdrop');
+    }
+    return this._backdropElement;
+  },
+
+  /**
+   * The deepest active element.
+   * @return {!Element} activeElement the active element
+   */
+  get deepActiveElement() {
+    var active = document.activeElement;
+    // document.activeElement can be null
+    // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
+    // In IE 11, it can also be an object when operating in iframes.
+    // In these cases, default it to document.body.
+    if (!active || active instanceof Element === false) {
+      active = document.body;
+    }
+    while (active.root && dom(active.root).activeElement) {
+      active = dom(active.root).activeElement;
+    }
+    return active;
+  },
+
+  /**
+   * Brings the overlay at the specified index to the front.
+   * @param {number} i
+   * @private
+   */
+  _bringOverlayAtIndexToFront: function(i) {
+    var overlay = this._overlays[i];
+    if (!overlay) {
+      return;
+    }
+    var lastI = this._overlays.length - 1;
+    var currentOverlay = this._overlays[lastI];
+    // Ensure always-on-top overlay stays on top.
+    if (currentOverlay &&
+        this._shouldBeBehindOverlay(overlay, currentOverlay)) {
+      lastI--;
+    }
+    // If already the top element, return.
+    if (i >= lastI) {
+      return;
+    }
+    // Update z-index to be on top.
+    var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
+    if (this._getZ(overlay) <= minimumZ) {
+      this._applyOverlayZ(overlay, minimumZ);
+    }
+
+    // Shift other overlays behind the new on top.
+    while (i < lastI) {
+      this._overlays[i] = this._overlays[i + 1];
+      i++;
+    }
+    this._overlays[lastI] = overlay;
+  },
+
+  /**
+   * Adds the overlay and updates its z-index if it's opened, or removes it if
+   * it's closed. Also updates the backdrop z-index.
+   * @param {!Element} overlay
+   */
+  addOrRemoveOverlay: function(overlay) {
+    if (overlay.opened) {
+      this.addOverlay(overlay);
+    } else {
+      this.removeOverlay(overlay);
+    }
+  },
+
+  /**
+   * Tracks overlays for z-index and focus management.
+   * Ensures the last added overlay with always-on-top remains on top.
+   * @param {!Element} overlay
+   */
+  addOverlay: function(overlay) {
+    var i = this._overlays.indexOf(overlay);
+    if (i >= 0) {
+      this._bringOverlayAtIndexToFront(i);
+      this.trackBackdrop();
+      return;
+    }
+    var insertionIndex = this._overlays.length;
+    var currentOverlay = this._overlays[insertionIndex - 1];
+    var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
+    var newZ = this._getZ(overlay);
+
+    // Ensure always-on-top overlay stays on top.
+    if (currentOverlay &&
+        this._shouldBeBehindOverlay(overlay, currentOverlay)) {
+      // This bumps the z-index of +2.
+      this._applyOverlayZ(currentOverlay, minimumZ);
+      insertionIndex--;
+      // Update minimumZ to match previous overlay's z-index.
+      var previousOverlay = this._overlays[insertionIndex - 1];
+      minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
+    }
+
+    // Update z-index and insert overlay.
+    if (newZ <= minimumZ) {
+      this._applyOverlayZ(overlay, minimumZ);
+    }
+    this._overlays.splice(insertionIndex, 0, overlay);
+
+    this.trackBackdrop();
+  },
+
+  /**
+   * @param {!Element} overlay
+   */
+  removeOverlay: function(overlay) {
+    var i = this._overlays.indexOf(overlay);
+    if (i === -1) {
+      return;
+    }
+    this._overlays.splice(i, 1);
+
+    this.trackBackdrop();
+  },
+
+  /**
+   * Returns the current overlay.
+   * @return {!Element|undefined}
+   */
+  currentOverlay: function() {
+    var i = this._overlays.length - 1;
+    return this._overlays[i];
+  },
+
+  /**
+   * Returns the current overlay z-index.
+   * @return {number}
+   */
+  currentOverlayZ: function() {
+    return this._getZ(this.currentOverlay());
+  },
+
+  /**
+   * Ensures that the minimum z-index of new overlays is at least `minimumZ`.
+   * This does not effect the z-index of any existing overlays.
+   * @param {number} minimumZ
+   */
+  ensureMinimumZ: function(minimumZ) {
+    this._minimumZ = Math.max(this._minimumZ, minimumZ);
+  },
+
+  focusOverlay: function() {
+    var current = /** @type {?} */ (this.currentOverlay());
+    if (current) {
+      current._applyFocus();
+    }
+  },
+
+  /**
+   * Updates the backdrop z-index.
+   */
+  trackBackdrop: function() {
+    var overlay = this._overlayWithBackdrop();
+    // Avoid creating the backdrop if there is no overlay with backdrop.
+    if (!overlay && !this._backdropElement) {
+      return;
+    }
+    this.backdropElement.style.zIndex = this._getZ(overlay) - 1;
+    this.backdropElement.opened = !!overlay;
+    // Property observers are not fired until element is attached
+    // in Polymer 2.x, so we ensure element is attached if needed.
+    // https://github.com/Polymer/polymer/issues/4526
+    this.backdropElement.prepare();
+  },
+
+  /**
+   * @return {!Array<!Element>}
+   */
+  getBackdrops: function() {
+    var backdrops = [];
+    for (var i = 0; i < this._overlays.length; i++) {
+      if (this._overlays[i].withBackdrop) {
+        backdrops.push(this._overlays[i]);
+      }
+    }
+    return backdrops;
+  },
+
+  /**
+   * Returns the z-index for the backdrop.
+   * @return {number}
+   */
+  backdropZ: function() {
+    return this._getZ(this._overlayWithBackdrop()) - 1;
+  },
+
+  /**
+   * Returns the top opened overlay that has a backdrop.
+   * @return {!Element|undefined}
+   * @private
+   */
+  _overlayWithBackdrop: function() {
+    for (var i = this._overlays.length - 1; i >= 0; i--) {
+      if (this._overlays[i].withBackdrop) {
+        return this._overlays[i];
+      }
+    }
+  },
+
+  /**
+   * Calculates the minimum z-index for the overlay.
+   * @param {Element=} overlay
+   * @private
+   */
+  _getZ: function(overlay) {
+    var z = this._minimumZ;
+    if (overlay) {
+      var z1 = Number(
+          overlay.style.zIndex || window.getComputedStyle(overlay).zIndex);
+      // Check if is a number
+      // Number.isNaN not supported in IE 10+
+      if (z1 === z1) {
+        z = z1;
+      }
+    }
+    return z;
+  },
+
+  /**
+   * @param {!Element} element
+   * @param {number|string} z
+   * @private
+   */
+  _setZ: function(element, z) {
+    element.style.zIndex = z;
+  },
+
+  /**
+   * @param {!Element} overlay
+   * @param {number} aboveZ
+   * @private
+   */
+  _applyOverlayZ: function(overlay, aboveZ) {
+    this._setZ(overlay, aboveZ + 2);
+  },
+
+  /**
+   * Returns the deepest overlay in the path.
+   * @param {!Array<!Element>=} path
+   * @return {!Element|undefined}
+   * @suppress {missingProperties}
+   * @private
+   */
+  _overlayInPath: function(path) {
+    path = path || [];
+    for (var i = 0; i < path.length; i++) {
+      if (path[i]._manager === this) {
+        return path[i];
+      }
+    }
+  },
+
+  /**
+   * Ensures the click event is delegated to the right overlay.
+   * @param {!Event} event
+   * @private
+   */
+  _onCaptureClick: function(event) {
+    var i = this._overlays.length - 1;
+    if (i === -1)
+      return;
+    var path = /** @type {!Array<!EventTarget>} */ (dom(event).path);
+    var overlay;
+    // Check if clicked outside of overlay.
+    while ((overlay = /** @type {?} */ (this._overlays[i])) &&
+           this._overlayInPath(path) !== overlay) {
+      overlay._onCaptureClick(event);
+      if (overlay.allowClickThrough) {
+        i--;
+      } else {
+        break;
+      }
+    }
+  },
+
+  /**
+   * Ensures the focus event is delegated to the right overlay.
+   * @param {!Event} event
+   * @private
+   */
+  _onCaptureFocus: function(event) {
+    var overlay = /** @type {?} */ (this.currentOverlay());
+    if (overlay) {
+      overlay._onCaptureFocus(event);
+    }
+  },
+
+  /**
+   * Ensures TAB and ESC keyboard events are delegated to the right overlay.
+   * @param {!Event} event
+   * @private
+   */
+  _onCaptureKeyDown: function(event) {
+    var overlay = /** @type {?} */ (this.currentOverlay());
+    if (overlay) {
+      if (IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
+        overlay._onCaptureEsc(event);
+      } else if (IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) {
+        overlay._onCaptureTab(event);
+      }
+    }
+  },
+
+  /**
+   * Returns if the overlay1 should be behind overlay2.
+   * @param {!Element} overlay1
+   * @param {!Element} overlay2
+   * @return {boolean}
+   * @suppress {missingProperties}
+   * @private
+   */
+  _shouldBeBehindOverlay: function(overlay1, overlay2) {
+    return !overlay1.alwaysOnTop && overlay2.alwaysOnTop;
+  }
+};
+
+export const IronOverlayManager = new IronOverlayManagerClass();
diff --git a/third_party/polymer/v3_0/components-chromium/iron-overlay-behavior/iron-scroll-manager.js b/third_party/polymer/v3_0/components-chromium/iron-overlay-behavior/iron-scroll-manager.js
new file mode 100644
index 0000000..156d1435
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-overlay-behavior/iron-scroll-manager.js
@@ -0,0 +1,378 @@
+/**
+@license
+Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+/**
+ * Used to calculate the scroll direction during touch events.
+ * @type {!Object}
+ */
+var lastTouchPosition = {pageX: 0, pageY: 0};
+/**
+ * Used to avoid computing event.path and filter scrollable nodes (better perf).
+ * @type {?EventTarget}
+ */
+var lastRootTarget = null;
+/**
+ * @type {!Array<!Node>}
+ */
+var lastScrollableNodes = [];
+/**
+ * @type {!Array<string>}
+ */
+var scrollEvents = [
+  // Modern `wheel` event for mouse wheel scrolling:
+  'wheel',
+  // Older, non-standard `mousewheel` event for some FF:
+  'mousewheel',
+  // IE:
+  'DOMMouseScroll',
+  // Touch enabled devices
+  'touchstart',
+  'touchmove'
+];
+// must be defined for modulizer
+var _boundScrollHandler;
+var currentLockingElement;
+
+/**
+ * The IronScrollManager is intended to provide a central source
+ * of authority and control over which elements in a document are currently
+ * allowed to scroll.
+ *
+ */
+`TODO(modulizer): A namespace named Polymer.IronScrollManager was
+declared here. The surrounding comments should be reviewed,
+and this string can then be deleted`;
+
+/**
+ * The current element that defines the DOM boundaries of the
+ * scroll lock. This is always the most recently locking element.
+ *
+ * @return {!Node|undefined}
+ */
+export {currentLockingElement};
+
+/**
+ * Returns true if the provided element is "scroll locked", which is to
+ * say that it cannot be scrolled via pointer or keyboard interactions.
+ *
+ * @param {!HTMLElement} element An HTML element instance which may or may
+ * not be scroll locked.
+ */
+export function elementIsScrollLocked(element) {
+  var lockingElement = currentLockingElement;
+
+  if (lockingElement === undefined) {
+    return false;
+  }
+
+  var scrollLocked;
+
+  if (_hasCachedLockedElement(element)) {
+    return true;
+  }
+
+  if (_hasCachedUnlockedElement(element)) {
+    return false;
+  }
+
+  scrollLocked = !!lockingElement && lockingElement !== element &&
+      !_composedTreeContains(lockingElement, element);
+
+  if (scrollLocked) {
+    _lockedElementCache.push(element);
+  } else {
+    _unlockedElementCache.push(element);
+  }
+
+  return scrollLocked;
+}
+
+/**
+ * Push an element onto the current scroll lock stack. The most recently
+ * pushed element and its children will be considered scrollable. All
+ * other elements will not be scrollable.
+ *
+ * Scroll locking is implemented as a stack so that cases such as
+ * dropdowns within dropdowns are handled well.
+ *
+ * @param {!HTMLElement} element The element that should lock scroll.
+ */
+export function pushScrollLock(element) {
+  // Prevent pushing the same element twice
+  if (_lockingElements.indexOf(element) >= 0) {
+    return;
+  }
+
+  if (_lockingElements.length === 0) {
+    _lockScrollInteractions();
+  }
+
+  _lockingElements.push(element);
+  currentLockingElement = _lockingElements[_lockingElements.length - 1];
+
+  _lockedElementCache = [];
+  _unlockedElementCache = [];
+}
+
+/**
+ * Remove an element from the scroll lock stack. The element being
+ * removed does not need to be the most recently pushed element. However,
+ * the scroll lock constraints only change when the most recently pushed
+ * element is removed.
+ *
+ * @param {!HTMLElement} element The element to remove from the scroll
+ * lock stack.
+ */
+export function removeScrollLock(element) {
+  var index = _lockingElements.indexOf(element);
+
+  if (index === -1) {
+    return;
+  }
+
+  _lockingElements.splice(index, 1);
+  currentLockingElement = _lockingElements[_lockingElements.length - 1];
+
+  _lockedElementCache = [];
+  _unlockedElementCache = [];
+
+  if (_lockingElements.length === 0) {
+    _unlockScrollInteractions();
+  }
+}
+
+export const _lockingElements = [];
+export let _lockedElementCache = null;
+export let _unlockedElementCache = null;
+
+export function _hasCachedLockedElement(element) {
+  return _lockedElementCache.indexOf(element) > -1;
+}
+
+export function _hasCachedUnlockedElement(element) {
+  return _unlockedElementCache.indexOf(element) > -1;
+}
+
+export function _composedTreeContains(element, child) {
+  // NOTE(cdata): This method iterates over content elements and their
+  // corresponding distributed nodes to implement a contains-like method
+  // that pierces through the composed tree of the ShadowDOM. Results of
+  // this operation are cached (elsewhere) on a per-scroll-lock basis, to
+  // guard against potentially expensive lookups happening repeatedly as
+  // a user scrolls / touchmoves.
+  var contentElements;
+  var distributedNodes;
+  var contentIndex;
+  var nodeIndex;
+
+  if (element.contains(child)) {
+    return true;
+  }
+
+  contentElements = dom(element).querySelectorAll('content,slot');
+
+  for (contentIndex = 0; contentIndex < contentElements.length;
+       ++contentIndex) {
+    distributedNodes = dom(contentElements[contentIndex]).getDistributedNodes();
+
+    for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) {
+      // Polymer 2.x returns slot.assignedNodes which can contain text nodes.
+      if (distributedNodes[nodeIndex].nodeType !== Node.ELEMENT_NODE)
+        continue;
+
+      if (_composedTreeContains(distributedNodes[nodeIndex], child)) {
+        return true;
+      }
+    }
+  }
+
+  return false;
+}
+
+export function _scrollInteractionHandler(event) {
+  // Avoid canceling an event with cancelable=false, e.g. scrolling is in
+  // progress and cannot be interrupted.
+  if (event.cancelable && _shouldPreventScrolling(event)) {
+    event.preventDefault();
+  }
+  // If event has targetTouches (touch event), update last touch position.
+  if (event.targetTouches) {
+    var touch = event.targetTouches[0];
+    lastTouchPosition.pageX = touch.pageX;
+    lastTouchPosition.pageY = touch.pageY;
+  }
+}
+
+/**
+ * @private
+ */
+export {_boundScrollHandler};
+
+export function _lockScrollInteractions() {
+  _boundScrollHandler =
+      _boundScrollHandler || _scrollInteractionHandler.bind(undefined);
+  for (var i = 0, l = scrollEvents.length; i < l; i++) {
+    // NOTE: browsers that don't support objects as third arg will
+    // interpret it as boolean, hence useCapture = true in this case.
+    document.addEventListener(
+        scrollEvents[i], _boundScrollHandler, {capture: true, passive: false});
+  }
+}
+
+export function _unlockScrollInteractions() {
+  for (var i = 0, l = scrollEvents.length; i < l; i++) {
+    // NOTE: browsers that don't support objects as third arg will
+    // interpret it as boolean, hence useCapture = true in this case.
+    document.removeEventListener(
+        scrollEvents[i], _boundScrollHandler, {capture: true, passive: false});
+  }
+}
+
+/**
+ * Returns true if the event causes scroll outside the current locking
+ * element, e.g. pointer/keyboard interactions, or scroll "leaking"
+ * outside the locking element when it is already at its scroll boundaries.
+ * @param {!Event} event
+ * @return {boolean}
+ * @private
+ */
+export function _shouldPreventScrolling(event) {
+  // Update if root target changed. For touch events, ensure we don't
+  // update during touchmove.
+  var target = dom(event).rootTarget;
+  if (event.type !== 'touchmove' && lastRootTarget !== target) {
+    lastRootTarget = target;
+    lastScrollableNodes = _getScrollableNodes(dom(event).path);
+  }
+
+  // Prevent event if no scrollable nodes.
+  if (!lastScrollableNodes.length) {
+    return true;
+  }
+  // Don't prevent touchstart event inside the locking element when it has
+  // scrollable nodes.
+  if (event.type === 'touchstart') {
+    return false;
+  }
+  // Get deltaX/Y.
+  var info = _getScrollInfo(event);
+  // Prevent if there is no child that can scroll.
+  return !_getScrollingNode(lastScrollableNodes, info.deltaX, info.deltaY);
+}
+
+/**
+ * Returns an array of scrollable nodes up to the current locking element,
+ * which is included too if scrollable.
+ * @param {!Array<!Node>} nodes
+ * @return {!Array<!Node>} scrollables
+ * @private
+ */
+export function _getScrollableNodes(nodes) {
+  var scrollables = [];
+  var lockingIndex = nodes.indexOf(currentLockingElement);
+  // Loop from root target to locking element (included).
+  for (var i = 0; i <= lockingIndex; i++) {
+    // Skip non-Element nodes.
+    if (nodes[i].nodeType !== Node.ELEMENT_NODE) {
+      continue;
+    }
+    var node = /** @type {!Element} */ (nodes[i]);
+    // Check inline style before checking computed style.
+    var style = node.style;
+    if (style.overflow !== 'scroll' && style.overflow !== 'auto') {
+      style = window.getComputedStyle(node);
+    }
+    if (style.overflow === 'scroll' || style.overflow === 'auto') {
+      scrollables.push(node);
+    }
+  }
+  return scrollables;
+}
+
+/**
+ * Returns the node that is scrolling. If there is no scrolling,
+ * returns undefined.
+ * @param {!Array<!Node>} nodes
+ * @param {number} deltaX Scroll delta on the x-axis
+ * @param {number} deltaY Scroll delta on the y-axis
+ * @return {!Node|undefined}
+ * @private
+ */
+export function _getScrollingNode(nodes, deltaX, deltaY) {
+  // No scroll.
+  if (!deltaX && !deltaY) {
+    return;
+  }
+  // Check only one axis according to where there is more scroll.
+  // Prefer vertical to horizontal.
+  var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX);
+  for (var i = 0; i < nodes.length; i++) {
+    var node = nodes[i];
+    var canScroll = false;
+    if (verticalScroll) {
+      // delta < 0 is scroll up, delta > 0 is scroll down.
+      canScroll = deltaY < 0 ?
+          node.scrollTop > 0 :
+          node.scrollTop < node.scrollHeight - node.clientHeight;
+    } else {
+      // delta < 0 is scroll left, delta > 0 is scroll right.
+      canScroll = deltaX < 0 ?
+          node.scrollLeft > 0 :
+          node.scrollLeft < node.scrollWidth - node.clientWidth;
+    }
+    if (canScroll) {
+      return node;
+    }
+  }
+}
+
+/**
+ * Returns scroll `deltaX` and `deltaY`.
+ * @param {!Event} event The scroll event
+ * @return {{deltaX: number, deltaY: number}} Object containing the
+ * x-axis scroll delta (positive: scroll right, negative: scroll left,
+ * 0: no scroll), and the y-axis scroll delta (positive: scroll down,
+ * negative: scroll up, 0: no scroll).
+ * @private
+ */
+export function _getScrollInfo(event) {
+  var info = {deltaX: event.deltaX, deltaY: event.deltaY};
+  // Already available.
+  if ('deltaX' in event) {
+    // do nothing, values are already good.
+  }
+  // Safari has scroll info in `wheelDeltaX/Y`.
+  else if ('wheelDeltaX' in event && 'wheelDeltaY' in event) {
+    info.deltaX = -event.wheelDeltaX;
+    info.deltaY = -event.wheelDeltaY;
+  }
+  // IE10 has only vertical scroll info in `wheelDelta`.
+  else if ('wheelDelta' in event) {
+    info.deltaX = 0;
+    info.deltaY = -event.wheelDelta;
+  }
+  // Firefox has scroll info in `detail` and `axis`.
+  else if ('axis' in event) {
+    info.deltaX = event.axis === 1 ? event.detail : 0;
+    info.deltaY = event.axis === 2 ? event.detail : 0;
+  }
+  // On mobile devices, calculate scroll direction.
+  else if (event.targetTouches) {
+    var touch = event.targetTouches[0];
+    // Touch moves from right to left => scrolling goes right.
+    info.deltaX = lastTouchPosition.pageX - touch.pageX;
+    // Touch moves from down to up => scrolling goes down.
+    info.deltaY = lastTouchPosition.pageY - touch.pageY;
+  }
+  return info;
+}
diff --git a/third_party/polymer/v3_0/components-chromium/iron-pages/iron-pages.js b/third_party/polymer/v3_0/components-chromium/iron-pages/iron-pages.js
new file mode 100644
index 0000000..2708d9a
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-pages/iron-pages.js
@@ -0,0 +1,73 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {IronResizableBehavior} from '../iron-resizable-behavior/iron-resizable-behavior.js';
+import {IronSelectableBehavior} from '../iron-selector/iron-selectable.js';
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+
+/**
+`iron-pages` is used to select one of its children to show. One use is to cycle
+through a list of children "pages".
+
+Example:
+
+    <iron-pages selected="0">
+      <div>One</div>
+      <div>Two</div>
+      <div>Three</div>
+    </iron-pages>
+
+    <script>
+      document.addEventListener('click', function(e) {
+        var pages = document.querySelector('iron-pages');
+        pages.selectNext();
+      });
+    </script>
+
+@group Iron Elements
+@demo demo/index.html
+*/
+Polymer({
+  _template: html`
+    <style>
+      :host {
+        display: block;
+      }
+
+      :host > ::slotted(:not(slot):not(.iron-selected)) {
+        display: none !important;
+      }
+    </style>
+
+    <slot></slot>
+`,
+
+  is: 'iron-pages',
+  behaviors: [IronResizableBehavior, IronSelectableBehavior],
+
+  properties: {
+
+    // as the selected page is the only one visible, activateEvent
+    // is both non-sensical and problematic; e.g. in cases where a user
+    // handler attempts to change the page and the activateEvent
+    // handler immediately changes it back
+    activateEvent: {type: String, value: null}
+
+  },
+
+  observers: ['_selectedPageChanged(selected)'],
+
+  _selectedPageChanged: function(selected, old) {
+    this.async(this.notifyResize);
+  }
+});
diff --git a/third_party/polymer/v3_0/components-chromium/iron-range-behavior/iron-range-behavior.js b/third_party/polymer/v3_0/components-chromium/iron-range-behavior/iron-range-behavior.js
new file mode 100644
index 0000000..1802854
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-range-behavior/iron-range-behavior.js
@@ -0,0 +1,95 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+/**
+ * `iron-range-behavior` provides the behavior for something with a minimum to
+ * maximum range.
+ *
+ * @demo demo/index.html
+ * @polymerBehavior
+ */
+export const IronRangeBehavior = {
+
+  properties: {
+
+    /**
+     * The number that represents the current value.
+     */
+    value: {type: Number, value: 0, notify: true, reflectToAttribute: true},
+
+    /**
+     * The number that indicates the minimum value of the range.
+     */
+    min: {type: Number, value: 0, notify: true},
+
+    /**
+     * The number that indicates the maximum value of the range.
+     */
+    max: {type: Number, value: 100, notify: true},
+
+    /**
+     * Specifies the value granularity of the range's value.
+     */
+    step: {type: Number, value: 1, notify: true},
+
+    /**
+     * Returns the ratio of the value.
+     */
+    ratio: {type: Number, value: 0, readOnly: true, notify: true},
+  },
+
+  observers: ['_update(value, min, max, step)'],
+
+  _calcRatio: function(value) {
+    return (this._clampValue(value) - this.min) / (this.max - this.min);
+  },
+
+  _clampValue: function(value) {
+    return Math.min(this.max, Math.max(this.min, this._calcStep(value)));
+  },
+
+  _calcStep: function(value) {
+    // polymer/issues/2493
+    value = parseFloat(value);
+
+    if (!this.step) {
+      return value;
+    }
+
+    var numSteps = Math.round((value - this.min) / this.step);
+    if (this.step < 1) {
+      /**
+       * For small values of this.step, if we calculate the step using
+       * `Math.round(value / step) * step` we may hit a precision point issue
+       * eg. 0.1 * 0.2 =  0.020000000000000004
+       * http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
+       *
+       * as a work around we can divide by the reciprocal of `step`
+       */
+      return numSteps / (1 / this.step) + this.min;
+    } else {
+      return numSteps * this.step + this.min;
+    }
+  },
+
+  _validateValue: function() {
+    var v = this._clampValue(this.value);
+    this.value = this.oldValue = isNaN(v) ? this.oldValue : v;
+    return this.value !== v;
+  },
+
+  _update: function() {
+    this._validateValue();
+    this._setRatio(this._calcRatio(this.value) * 100);
+  }
+
+};
diff --git a/third_party/polymer/v3_0/components-chromium/iron-resizable-behavior/iron-resizable-behavior.js b/third_party/polymer/v3_0/components-chromium/iron-resizable-behavior/iron-resizable-behavior.js
new file mode 100644
index 0000000..8a5317e
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-resizable-behavior/iron-resizable-behavior.js
@@ -0,0 +1,273 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+import {useShadow} from '../polymer/lib/utils/settings.js';
+
+// Contains all connected resizables that do not have a parent.
+var ORPHANS = new Set();
+
+/**
+ * `IronResizableBehavior` is a behavior that can be used in Polymer elements to
+ * coordinate the flow of resize events between "resizers" (elements that
+ *control the size or hidden state of their children) and "resizables" (elements
+ *that need to be notified when they are resized or un-hidden by their parents
+ *in order to take action on their new measurements).
+ *
+ * Elements that perform measurement should add the `IronResizableBehavior`
+ *behavior to their element definition and listen for the `iron-resize` event on
+ *themselves. This event will be fired when they become showing after having
+ *been hidden, when they are resized explicitly by another resizable, or when
+ *the window has been resized.
+ *
+ * Note, the `iron-resize` event is non-bubbling.
+ *
+ * @polymerBehavior
+ * @demo demo/index.html
+ **/
+export const IronResizableBehavior = {
+  properties: {
+    /**
+     * The closest ancestor element that implements `IronResizableBehavior`.
+     */
+    _parentResizable: {
+      type: Object,
+      observer: '_parentResizableChanged',
+    },
+
+    /**
+     * True if this element is currently notifying its descendant elements of
+     * resize.
+     */
+    _notifyingDescendant: {
+      type: Boolean,
+      value: false,
+    }
+  },
+
+  listeners: {
+    'iron-request-resize-notifications': '_onIronRequestResizeNotifications'
+  },
+
+  created: function() {
+    // We don't really need property effects on these, and also we want them
+    // to be created before the `_parentResizable` observer fires:
+    this._interestedResizables = [];
+    this._boundNotifyResize = this.notifyResize.bind(this);
+    this._boundOnDescendantIronResize = this._onDescendantIronResize.bind(this);
+  },
+
+  attached: function() {
+    this._requestResizeNotifications();
+  },
+
+  detached: function() {
+    if (this._parentResizable) {
+      this._parentResizable.stopResizeNotificationsFor(this);
+    } else {
+      ORPHANS.delete(this);
+      window.removeEventListener('resize', this._boundNotifyResize);
+    }
+
+    this._parentResizable = null;
+  },
+
+  /**
+   * Can be called to manually notify a resizable and its descendant
+   * resizables of a resize change.
+   */
+  notifyResize: function() {
+    if (!this.isAttached) {
+      return;
+    }
+
+    this._interestedResizables.forEach(function(resizable) {
+      if (this.resizerShouldNotify(resizable)) {
+        this._notifyDescendant(resizable);
+      }
+    }, this);
+
+    this._fireResize();
+  },
+
+  /**
+   * Used to assign the closest resizable ancestor to this resizable
+   * if the ancestor detects a request for notifications.
+   */
+  assignParentResizable: function(parentResizable) {
+    if (this._parentResizable) {
+      this._parentResizable.stopResizeNotificationsFor(this);
+    }
+
+    this._parentResizable = parentResizable;
+
+    if (parentResizable &&
+        parentResizable._interestedResizables.indexOf(this) === -1) {
+      parentResizable._interestedResizables.push(this);
+      parentResizable._subscribeIronResize(this);
+    }
+  },
+
+  /**
+   * Used to remove a resizable descendant from the list of descendants
+   * that should be notified of a resize change.
+   */
+  stopResizeNotificationsFor: function(target) {
+    var index = this._interestedResizables.indexOf(target);
+
+    if (index > -1) {
+      this._interestedResizables.splice(index, 1);
+      this._unsubscribeIronResize(target);
+    }
+  },
+
+  /**
+   * Subscribe this element to listen to iron-resize events on the given target.
+   *
+   * Preferred over target.listen because the property renamer does not
+   * understand to rename when the target is not specifically "this"
+   *
+   * @param {!HTMLElement} target Element to listen to for iron-resize events.
+   */
+  _subscribeIronResize: function(target) {
+    target.addEventListener('iron-resize', this._boundOnDescendantIronResize);
+  },
+
+  /**
+   * Unsubscribe this element from listening to to iron-resize events on the
+   * given target.
+   *
+   * Preferred over target.unlisten because the property renamer does not
+   * understand to rename when the target is not specifically "this"
+   *
+   * @param {!HTMLElement} target Element to listen to for iron-resize events.
+   */
+  _unsubscribeIronResize: function(target) {
+    target.removeEventListener(
+        'iron-resize', this._boundOnDescendantIronResize);
+  },
+
+  /**
+   * This method can be overridden to filter nested elements that should or
+   * should not be notified by the current element. Return true if an element
+   * should be notified, or false if it should not be notified.
+   *
+   * @param {HTMLElement} element A candidate descendant element that
+   * implements `IronResizableBehavior`.
+   * @return {boolean} True if the `element` should be notified of resize.
+   */
+  resizerShouldNotify: function(element) {
+    return true;
+  },
+
+  _onDescendantIronResize: function(event) {
+    if (this._notifyingDescendant) {
+      event.stopPropagation();
+      return;
+    }
+
+    // no need to use this during shadow dom because of event retargeting
+    if (!useShadow) {
+      this._fireResize();
+    }
+  },
+
+  _fireResize: function() {
+    this.fire('iron-resize', null, {node: this, bubbles: false});
+  },
+
+  _onIronRequestResizeNotifications: function(event) {
+    var target = /** @type {!EventTarget} */ (dom(event).rootTarget);
+    if (target === this) {
+      return;
+    }
+
+    target.assignParentResizable(this);
+    this._notifyDescendant(target);
+
+    event.stopPropagation();
+  },
+
+  _parentResizableChanged: function(parentResizable) {
+    if (parentResizable) {
+      window.removeEventListener('resize', this._boundNotifyResize);
+    }
+  },
+
+  _notifyDescendant: function(descendant) {
+    // NOTE(cdata): In IE10, attached is fired on children first, so it's
+    // important not to notify them if the parent is not attached yet (or
+    // else they will get redundantly notified when the parent attaches).
+    if (!this.isAttached) {
+      return;
+    }
+
+    this._notifyingDescendant = true;
+    descendant.notifyResize();
+    this._notifyingDescendant = false;
+  },
+
+  _requestResizeNotifications: function() {
+    if (!this.isAttached) {
+      return;
+    }
+
+    if (document.readyState === 'loading') {
+      var _requestResizeNotifications =
+          this._requestResizeNotifications.bind(this);
+      document.addEventListener(
+          'readystatechange', function readystatechanged() {
+            document.removeEventListener('readystatechange', readystatechanged);
+            _requestResizeNotifications();
+          });
+    } else {
+      this._findParent();
+
+      if (!this._parentResizable) {
+        // If this resizable is an orphan, tell other orphans to try to find
+        // their parent again, in case it's this resizable.
+        ORPHANS.forEach(function(orphan) {
+          if (orphan !== this) {
+            orphan._findParent();
+          }
+        }, this);
+
+        window.addEventListener('resize', this._boundNotifyResize);
+        this.notifyResize();
+      } else {
+        // If this resizable has a parent, tell other child resizables of
+        // that parent to try finding their parent again, in case it's this
+        // resizable.
+        this._parentResizable._interestedResizables
+            .forEach(function(resizable) {
+              if (resizable !== this) {
+                resizable._findParent();
+              }
+            }, this);
+      }
+    }
+  },
+
+  _findParent: function() {
+    this.assignParentResizable(null);
+    this.fire(
+        'iron-request-resize-notifications',
+        null,
+        {node: this, bubbles: true, cancelable: true});
+
+    if (!this._parentResizable) {
+      ORPHANS.add(this);
+    } else {
+      ORPHANS.delete(this);
+    }
+  }
+};
diff --git a/third_party/polymer/v3_0/components-chromium/iron-scroll-target-behavior/iron-scroll-target-behavior.js b/third_party/polymer/v3_0/components-chromium/iron-scroll-target-behavior/iron-scroll-target-behavior.js
new file mode 100644
index 0000000..a9d889b
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-scroll-target-behavior/iron-scroll-target-behavior.js
@@ -0,0 +1,278 @@
+/**
+@license
+Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+
+/**
+ * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll
+ * events from a designated scroll target.
+ *
+ * Elements that consume this behavior can override the `_scrollHandler`
+ * method to add logic on the scroll event.
+ *
+ * @demo demo/scrolling-region.html Scrolling Region
+ * @demo demo/document.html Document Element
+ * @polymerBehavior
+ */
+export const IronScrollTargetBehavior = {
+
+  properties: {
+
+    /**
+     * Specifies the element that will handle the scroll event
+     * on the behalf of the current element. This is typically a reference to an
+     *element, but there are a few more posibilities:
+     *
+     * ### Elements id
+     *
+     *```html
+     * <div id="scrollable-element" style="overflow: auto;">
+     *  <x-element scroll-target="scrollable-element">
+     *    <!-- Content-->
+     *  </x-element>
+     * </div>
+     *```
+     * In this case, the `scrollTarget` will point to the outer div element.
+     *
+     * ### Document scrolling
+     *
+     * For document scrolling, you can use the reserved word `document`:
+     *
+     *```html
+     * <x-element scroll-target="document">
+     *   <!-- Content -->
+     * </x-element>
+     *```
+     *
+     * ### Elements reference
+     *
+     *```js
+     * appHeader.scrollTarget = document.querySelector('#scrollable-element');
+     *```
+     *
+     * @type {HTMLElement}
+     * @default document
+     */
+    scrollTarget: {
+      type: HTMLElement,
+      value: function() {
+        return this._defaultScrollTarget;
+      }
+    }
+  },
+
+  observers: ['_scrollTargetChanged(scrollTarget, isAttached)'],
+
+  /**
+   * True if the event listener should be installed.
+   */
+  _shouldHaveListener: true,
+
+  _scrollTargetChanged: function(scrollTarget, isAttached) {
+    var eventTarget;
+
+    if (this._oldScrollTarget) {
+      this._toggleScrollListener(false, this._oldScrollTarget);
+      this._oldScrollTarget = null;
+    }
+    if (!isAttached) {
+      return;
+    }
+    // Support element id references
+    if (scrollTarget === 'document') {
+      this.scrollTarget = this._doc;
+
+    } else if (typeof scrollTarget === 'string') {
+      var domHost = this.domHost;
+
+      this.scrollTarget = domHost && domHost.$ ?
+          domHost.$[scrollTarget] :
+          dom(this.ownerDocument).querySelector('#' + scrollTarget);
+
+    } else if (this._isValidScrollTarget()) {
+      this._oldScrollTarget = scrollTarget;
+      this._toggleScrollListener(this._shouldHaveListener, scrollTarget);
+    }
+  },
+
+  /**
+   * Runs on every scroll event. Consumer of this behavior may override this
+   * method.
+   *
+   * @protected
+   */
+  _scrollHandler: function scrollHandler() {},
+
+  /**
+   * The default scroll target. Consumers of this behavior may want to customize
+   * the default scroll target.
+   *
+   * @type {Element}
+   */
+  get _defaultScrollTarget() {
+    return this._doc;
+  },
+
+  /**
+   * Shortcut for the document element
+   *
+   * @type {Element}
+   */
+  get _doc() {
+    return this.ownerDocument.documentElement;
+  },
+
+  /**
+   * Gets the number of pixels that the content of an element is scrolled
+   * upward.
+   *
+   * @type {number}
+   */
+  get _scrollTop() {
+    if (this._isValidScrollTarget()) {
+      return this.scrollTarget === this._doc ? window.pageYOffset :
+                                               this.scrollTarget.scrollTop;
+    }
+    return 0;
+  },
+
+  /**
+   * Gets the number of pixels that the content of an element is scrolled to the
+   * left.
+   *
+   * @type {number}
+   */
+  get _scrollLeft() {
+    if (this._isValidScrollTarget()) {
+      return this.scrollTarget === this._doc ? window.pageXOffset :
+                                               this.scrollTarget.scrollLeft;
+    }
+    return 0;
+  },
+
+  /**
+   * Sets the number of pixels that the content of an element is scrolled
+   * upward.
+   *
+   * @type {number}
+   */
+  set _scrollTop(top) {
+    if (this.scrollTarget === this._doc) {
+      window.scrollTo(window.pageXOffset, top);
+    } else if (this._isValidScrollTarget()) {
+      this.scrollTarget.scrollTop = top;
+    }
+  },
+
+  /**
+   * Sets the number of pixels that the content of an element is scrolled to the
+   * left.
+   *
+   * @type {number}
+   */
+  set _scrollLeft(left) {
+    if (this.scrollTarget === this._doc) {
+      window.scrollTo(left, window.pageYOffset);
+    } else if (this._isValidScrollTarget()) {
+      this.scrollTarget.scrollLeft = left;
+    }
+  },
+
+  /**
+   * Scrolls the content to a particular place.
+   *
+   * @method scroll
+   * @param {number|!{left: number, top: number}} leftOrOptions The left position or scroll options
+   * @param {number=} top The top position
+   * @return {void}
+   */
+  scroll: function(leftOrOptions, top) {
+    var left;
+
+    if (typeof leftOrOptions === 'object') {
+      left = leftOrOptions.left;
+      top = leftOrOptions.top;
+    } else {
+      left = leftOrOptions;
+    }
+
+    left = left || 0;
+    top = top || 0;
+    if (this.scrollTarget === this._doc) {
+      window.scrollTo(left, top);
+    } else if (this._isValidScrollTarget()) {
+      this.scrollTarget.scrollLeft = left;
+      this.scrollTarget.scrollTop = top;
+    }
+  },
+
+  /**
+   * Gets the width of the scroll target.
+   *
+   * @type {number}
+   */
+  get _scrollTargetWidth() {
+    if (this._isValidScrollTarget()) {
+      return this.scrollTarget === this._doc ? window.innerWidth :
+                                               this.scrollTarget.offsetWidth;
+    }
+    return 0;
+  },
+
+  /**
+   * Gets the height of the scroll target.
+   *
+   * @type {number}
+   */
+  get _scrollTargetHeight() {
+    if (this._isValidScrollTarget()) {
+      return this.scrollTarget === this._doc ? window.innerHeight :
+                                               this.scrollTarget.offsetHeight;
+    }
+    return 0;
+  },
+
+  /**
+   * Returns true if the scroll target is a valid HTMLElement.
+   *
+   * @return {boolean}
+   */
+  _isValidScrollTarget: function() {
+    return this.scrollTarget instanceof HTMLElement;
+  },
+
+  _toggleScrollListener: function(yes, scrollTarget) {
+    var eventTarget = scrollTarget === this._doc ? window : scrollTarget;
+    if (yes) {
+      if (!this._boundScrollHandler) {
+        this._boundScrollHandler = this._scrollHandler.bind(this);
+        eventTarget.addEventListener('scroll', this._boundScrollHandler);
+      }
+    } else {
+      if (this._boundScrollHandler) {
+        eventTarget.removeEventListener('scroll', this._boundScrollHandler);
+        this._boundScrollHandler = null;
+      }
+    }
+  },
+
+  /**
+   * Enables or disables the scroll event listener.
+   *
+   * @param {boolean} yes True to add the event, False to remove it.
+   */
+  toggleScrollListener: function(yes) {
+    this._shouldHaveListener = yes;
+    this._toggleScrollListener(yes, this.scrollTarget);
+  }
+
+};
diff --git a/third_party/polymer/v3_0/components-chromium/iron-scroll-threshold/iron-scroll-threshold.js b/third_party/polymer/v3_0/components-chromium/iron-scroll-threshold/iron-scroll-threshold.js
new file mode 100644
index 0000000..c592f93
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-scroll-threshold/iron-scroll-threshold.js
@@ -0,0 +1,212 @@
+/**
+@license
+Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {IronScrollTargetBehavior} from '../iron-scroll-target-behavior/iron-scroll-target-behavior.js';
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+
+/**
+`iron-scroll-threshold` is a utility element that listens for `scroll` events
+from a scrollable region and fires events to indicate when the scroller has
+reached a pre-defined limit, specified in pixels from the upper and lower bounds
+of the scrollable region. This element may wrap a scrollable region and will
+listen for `scroll` events bubbling through it from its children.  In this case,
+care should be taken that only one scrollable region with the same orientation
+as this element is contained within. Alternatively, the `scrollTarget` property
+can be set/bound to a non-child scrollable region, from which it will listen for
+events.
+
+Once a threshold has been reached, a `lower-threshold` or `upper-threshold`
+event will be fired, at which point the user may perform actions such as
+lazily-loading more data to be displayed. After any work is done, the user must
+then clear the threshold by calling the `clearTriggers` method on this element,
+after which it will begin listening again for the scroll position to reach the
+threshold again assuming the content in the scrollable region has grown. If the
+user no longer wishes to receive events (e.g. all data has been exhausted), the
+threshold property in question (e.g. `lowerThreshold`) may be set to a falsy
+value to disable events and clear the associated triggered property.
+
+### Example
+
+```html
+<iron-scroll-threshold on-lower-threshold="loadMoreData">
+  <div>content</div>
+</iron-scroll-threshold>
+```
+
+```js
+  loadMoreData: function() {
+    // load async stuff. e.g. XHR
+    asyncStuff(function done() {
+      ironScrollTheshold.clearTriggers();
+    });
+  }
+```
+
+### Using dom-repeat
+
+```html
+<iron-scroll-threshold on-lower-threshold="loadMoreData">
+  <template is="dom-repeat" items="[[items]]">
+    <div>[[index]]</div>
+  </template>
+</iron-scroll-threshold>
+```
+
+### Using iron-list
+
+```html
+<iron-scroll-threshold on-lower-threshold="loadMoreData" id="threshold">
+  <iron-list scroll-target="threshold" items="[[items]]">
+    <template>
+      <div>[[index]]</div>
+    </template>
+  </iron-list>
+</iron-scroll-threshold>
+```
+
+@group Iron Element
+@element iron-scroll-threshold
+@demo demo/scrolling-region.html Scrolling Region
+@demo demo/document.html Document Element
+*/
+Polymer({
+  _template: html`
+    <style>
+      :host {
+        display: block;
+      }
+    </style>
+
+    <slot></slot>
+`,
+
+  is: 'iron-scroll-threshold',
+
+  properties: {
+
+    /**
+     * Distance from the top (or left, for horizontal) bound of the scroller
+     * where the "upper trigger" will fire.
+     */
+    upperThreshold: {type: Number, value: 100},
+
+    /**
+     * Distance from the bottom (or right, for horizontal) bound of the scroller
+     * where the "lower trigger" will fire.
+     */
+    lowerThreshold: {type: Number, value: 100},
+
+    /**
+     * Read-only value that tracks the triggered state of the upper threshold.
+     */
+    upperTriggered: {type: Boolean, value: false, notify: true, readOnly: true},
+
+    /**
+     * Read-only value that tracks the triggered state of the lower threshold.
+     */
+    lowerTriggered: {type: Boolean, value: false, notify: true, readOnly: true},
+
+    /**
+     * True if the orientation of the scroller is horizontal.
+     */
+    horizontal: {type: Boolean, value: false}
+  },
+
+  behaviors: [IronScrollTargetBehavior],
+
+  observers:
+      ['_setOverflow(scrollTarget)', '_initCheck(horizontal, isAttached)'],
+
+  get _defaultScrollTarget() {
+    return this;
+  },
+
+  _setOverflow: function(scrollTarget) {
+    this.style.overflow = scrollTarget === this ? 'auto' : '';
+    this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : '';
+  },
+
+  _scrollHandler: function() {
+    // throttle the work on the scroll event
+    var THROTTLE_THRESHOLD = 200;
+    if (!this.isDebouncerActive('_checkTheshold')) {
+      this.debounce('_checkTheshold', function() {
+        this.checkScrollThresholds();
+      }, THROTTLE_THRESHOLD);
+    }
+  },
+
+  _initCheck: function(horizontal, isAttached) {
+    if (isAttached) {
+      this.debounce('_init', function() {
+        this.clearTriggers();
+        this.checkScrollThresholds();
+      });
+    }
+  },
+
+  /**
+   * Checks the scroll thresholds.
+   * This method is automatically called by iron-scroll-threshold.
+   *
+   * @method checkScrollThresholds
+   */
+  checkScrollThresholds: function() {
+    if (!this.scrollTarget || (this.lowerTriggered && this.upperTriggered)) {
+      return;
+    }
+    var upperScrollValue = this.horizontal ? this._scrollLeft : this._scrollTop;
+    var lowerScrollValue = this.horizontal ? this.scrollTarget.scrollWidth -
+            this._scrollTargetWidth - this._scrollLeft :
+                                             this.scrollTarget.scrollHeight -
+            this._scrollTargetHeight - this._scrollTop;
+
+    // Detect upper threshold
+    if (upperScrollValue <= this.upperThreshold && !this.upperTriggered) {
+      this._setUpperTriggered(true);
+      this.fire('upper-threshold');
+    }
+    // Detect lower threshold
+    if (lowerScrollValue <= this.lowerThreshold && !this.lowerTriggered) {
+      this._setLowerTriggered(true);
+      this.fire('lower-threshold');
+    }
+  },
+
+  checkScrollThesholds: function() {
+    // iron-scroll-threshold/issues/16
+    this.checkScrollThresholds();
+  },
+
+  /**
+   * Clear the upper and lower threshold states.
+   *
+   * @method clearTriggers
+   */
+  clearTriggers: function() {
+    this._setUpperTriggered(false);
+    this._setLowerTriggered(false);
+  }
+
+  /**
+   * Fires when the lower threshold has been reached.
+   *
+   * @event lower-threshold
+   */
+
+  /**
+   * Fires when the upper threshold has been reached.
+   *
+   * @event upper-threshold
+   */
+});
diff --git a/third_party/polymer/v3_0/components-chromium/iron-selector/iron-multi-selectable.js b/third_party/polymer/v3_0/components-chromium/iron-selector/iron-multi-selectable.js
new file mode 100644
index 0000000..9a79732
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-selector/iron-multi-selectable.js
@@ -0,0 +1,166 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {IronSelectableBehavior} from './iron-selectable.js';
+
+/**
+ * @polymerBehavior IronMultiSelectableBehavior
+ */
+export const IronMultiSelectableBehaviorImpl = {
+  properties: {
+
+    /**
+     * If true, multiple selections are allowed.
+     */
+    multi: {type: Boolean, value: false, observer: 'multiChanged'},
+
+    /**
+     * Gets or sets the selected elements. This is used instead of `selected`
+     * when `multi` is true.
+     */
+    selectedValues: {
+      type: Array,
+      notify: true,
+      value: function() {
+        return [];
+      }
+    },
+
+    /**
+     * Returns an array of currently selected items.
+     */
+    selectedItems: {
+      type: Array,
+      readOnly: true,
+      notify: true,
+      value: function() {
+        return [];
+      }
+    },
+
+  },
+
+  observers: ['_updateSelected(selectedValues.splices)'],
+
+  /**
+   * Selects the given value. If the `multi` property is true, then the selected
+   * state of the `value` will be toggled; otherwise the `value` will be
+   * selected.
+   *
+   * @method select
+   * @param {string|number} value the value to select.
+   */
+  select: function(value) {
+    if (this.multi) {
+      this._toggleSelected(value);
+    } else {
+      this.selected = value;
+    }
+  },
+
+  multiChanged: function(multi) {
+    this._selection.multi = multi;
+    this._updateSelected();
+  },
+
+  // UNUSED, FOR API COMPATIBILITY
+  get _shouldUpdateSelection() {
+    return this.selected != null ||
+        (this.selectedValues != null && this.selectedValues.length);
+  },
+
+  _updateAttrForSelected: function() {
+    if (!this.multi) {
+      IronSelectableBehavior._updateAttrForSelected.apply(this);
+    } else if (this.selectedItems && this.selectedItems.length > 0) {
+      this.selectedValues =
+          this.selectedItems
+              .map(
+                  function(selectedItem) {
+                    return this._indexToValue(this.indexOf(selectedItem));
+                  },
+                  this)
+              .filter(function(unfilteredValue) {
+                return unfilteredValue != null;
+              }, this);
+    }
+  },
+
+  _updateSelected: function() {
+    if (this.multi) {
+      this._selectMulti(this.selectedValues);
+    } else {
+      this._selectSelected(this.selected);
+    }
+  },
+
+  _selectMulti: function(values) {
+    values = values || [];
+
+    var selectedItems =
+        (this._valuesToItems(values) || []).filter(function(item) {
+          return item !== null && item !== undefined;
+        });
+
+    // clear all but the current selected items
+    this._selection.clear(selectedItems);
+
+    // select only those not selected yet
+    for (var i = 0; i < selectedItems.length; i++) {
+      this._selection.setItemSelected(selectedItems[i], true);
+    }
+
+    // Check for items, since this array is populated only when attached
+    if (this.fallbackSelection && !this._selection.get().length) {
+      var fallback = this._valueToItem(this.fallbackSelection);
+      if (fallback) {
+        this.select(this.fallbackSelection);
+      }
+    }
+  },
+
+  _selectionChange: function() {
+    var s = this._selection.get();
+    if (this.multi) {
+      this._setSelectedItems(s);
+      this._setSelectedItem(s.length ? s[0] : null);
+    } else {
+      if (s !== null && s !== undefined) {
+        this._setSelectedItems([s]);
+        this._setSelectedItem(s);
+      } else {
+        this._setSelectedItems([]);
+        this._setSelectedItem(null);
+      }
+    }
+  },
+
+  _toggleSelected: function(value) {
+    var i = this.selectedValues.indexOf(value);
+    var unselected = i < 0;
+    if (unselected) {
+      this.push('selectedValues', value);
+    } else {
+      this.splice('selectedValues', i, 1);
+    }
+  },
+
+  _valuesToItems: function(values) {
+    return (values == null) ? null : values.map(function(value) {
+      return this._valueToItem(value);
+    }, this);
+  }
+};
+
+/** @polymerBehavior */
+export const IronMultiSelectableBehavior =
+    [IronSelectableBehavior, IronMultiSelectableBehaviorImpl];
diff --git a/third_party/polymer/v3_0/components-chromium/iron-selector/iron-selectable.js b/third_party/polymer/v3_0/components-chromium/iron-selector/iron-selectable.js
new file mode 100644
index 0000000..9a4cd9e
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-selector/iron-selectable.js
@@ -0,0 +1,399 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+import {dashToCamelCase} from '../polymer/lib/utils/case-map.js';
+
+import {IronSelection} from './iron-selection.js';
+
+/**
+ * @polymerBehavior
+ */
+export const IronSelectableBehavior = {
+
+  /**
+   * Fired when iron-selector is activated (selected or deselected).
+   * It is fired before the selected items are changed.
+   * Cancel the event to abort selection.
+   *
+   * @event iron-activate
+   */
+
+  /**
+   * Fired when an item is selected
+   *
+   * @event iron-select
+   */
+
+  /**
+   * Fired when an item is deselected
+   *
+   * @event iron-deselect
+   */
+
+  /**
+   * Fired when the list of selectable items changes (e.g., items are
+   * added or removed). The detail of the event is a mutation record that
+   * describes what changed.
+   *
+   * @event iron-items-changed
+   */
+
+  properties: {
+
+    /**
+     * If you want to use an attribute value or property of an element for
+     * `selected` instead of the index, set this to the name of the attribute
+     * or property. Hyphenated values are converted to camel case when used to
+     * look up the property of a selectable element. Camel cased values are
+     * *not* converted to hyphenated values for attribute lookup. It's
+     * recommended that you provide the hyphenated form of the name so that
+     * selection works in both cases. (Use `attr-or-property-name` instead of
+     * `attrOrPropertyName`.)
+     */
+    attrForSelected: {type: String, value: null},
+
+    /**
+     * Gets or sets the selected element. The default is to use the index of the
+     * item.
+     * @type {string|number}
+     */
+    selected: {type: String, notify: true},
+
+    /**
+     * Returns the currently selected item.
+     *
+     * @type {?Object}
+     */
+    selectedItem: {type: Object, readOnly: true, notify: true},
+
+    /**
+     * The event that fires from items when they are selected. Selectable
+     * will listen for this event from items and update the selection state.
+     * Set to empty string to listen to no events.
+     */
+    activateEvent:
+        {type: String, value: 'tap', observer: '_activateEventChanged'},
+
+    /**
+     * This is a CSS selector string.  If this is set, only items that match the
+     * CSS selector are selectable.
+     */
+    selectable: String,
+
+    /**
+     * The class to set on elements when selected.
+     */
+    selectedClass: {type: String, value: 'iron-selected'},
+
+    /**
+     * The attribute to set on elements when selected.
+     */
+    selectedAttribute: {type: String, value: null},
+
+    /**
+     * Default fallback if the selection based on selected with
+     * `attrForSelected` is not found.
+     */
+    fallbackSelection: {type: String, value: null},
+
+    /**
+     * The list of items from which a selection can be made.
+     */
+    items: {
+      type: Array,
+      readOnly: true,
+      notify: true,
+      value: function() {
+        return [];
+      }
+    },
+
+    /**
+     * The set of excluded elements where the key is the `localName`
+     * of the element that will be ignored from the item list.
+     *
+     * @default {template: 1}
+     */
+    _excludedLocalNames: {
+      type: Object,
+      value: function() {
+        return {
+          'template': 1,
+          'dom-bind': 1,
+          'dom-if': 1,
+          'dom-repeat': 1,
+        };
+      }
+    }
+  },
+
+  observers: [
+    '_updateAttrForSelected(attrForSelected)',
+    '_updateSelected(selected)',
+    '_checkFallback(fallbackSelection)'
+  ],
+
+  created: function() {
+    this._bindFilterItem = this._filterItem.bind(this);
+    this._selection = new IronSelection(this._applySelection.bind(this));
+  },
+
+  attached: function() {
+    this._observer = this._observeItems(this);
+    this._addListener(this.activateEvent);
+  },
+
+  detached: function() {
+    if (this._observer) {
+      dom(this).unobserveNodes(this._observer);
+    }
+    this._removeListener(this.activateEvent);
+  },
+
+  /**
+   * Returns the index of the given item.
+   *
+   * @method indexOf
+   * @param {Object} item
+   * @returns Returns the index of the item
+   */
+  indexOf: function(item) {
+    return this.items ? this.items.indexOf(item) : -1;
+  },
+
+  /**
+   * Selects the given value.
+   *
+   * @method select
+   * @param {string|number} value the value to select.
+   */
+  select: function(value) {
+    this.selected = value;
+  },
+
+  /**
+   * Selects the previous item.
+   *
+   * @method selectPrevious
+   */
+  selectPrevious: function() {
+    var length = this.items.length;
+    var index = length - 1;
+    if (this.selected !== undefined) {
+      index = (Number(this._valueToIndex(this.selected)) - 1 + length) % length;
+    }
+    this.selected = this._indexToValue(index);
+  },
+
+  /**
+   * Selects the next item.
+   *
+   * @method selectNext
+   */
+  selectNext: function() {
+    var index = 0;
+    if (this.selected !== undefined) {
+      index =
+          (Number(this._valueToIndex(this.selected)) + 1) % this.items.length;
+    }
+    this.selected = this._indexToValue(index);
+  },
+
+  /**
+   * Selects the item at the given index.
+   *
+   * @method selectIndex
+   */
+  selectIndex: function(index) {
+    this.select(this._indexToValue(index));
+  },
+
+  /**
+   * Force a synchronous update of the `items` property.
+   *
+   * NOTE: Consider listening for the `iron-items-changed` event to respond to
+   * updates to the set of selectable items after updates to the DOM list and
+   * selection state have been made.
+   *
+   * WARNING: If you are using this method, you should probably consider an
+   * alternate approach. Synchronously querying for items is potentially
+   * slow for many use cases. The `items` property will update asynchronously
+   * on its own to reflect selectable items in the DOM.
+   */
+  forceSynchronousItemUpdate: function() {
+    if (this._observer && typeof this._observer.flush === 'function') {
+      // NOTE(bicknellr): `dom.flush` above is no longer sufficient to trigger
+      // `observeNodes` callbacks. Polymer 2.x returns an object from
+      // `observeNodes` with a `flush` that synchronously gives the callback any
+      // pending MutationRecords (retrieved with `takeRecords`). Any case where
+      // ShadyDOM flushes were expected to synchronously trigger item updates
+      // will now require calling `forceSynchronousItemUpdate`.
+      this._observer.flush();
+    } else {
+      this._updateItems();
+    }
+  },
+
+  // UNUSED, FOR API COMPATIBILITY
+  get _shouldUpdateSelection() {
+    return this.selected != null;
+  },
+
+  _checkFallback: function() {
+    this._updateSelected();
+  },
+
+  _addListener: function(eventName) {
+    this.listen(this, eventName, '_activateHandler');
+  },
+
+  _removeListener: function(eventName) {
+    this.unlisten(this, eventName, '_activateHandler');
+  },
+
+  _activateEventChanged: function(eventName, old) {
+    this._removeListener(old);
+    this._addListener(eventName);
+  },
+
+  _updateItems: function() {
+    var nodes = dom(this).queryDistributedElements(this.selectable || '*');
+    nodes = Array.prototype.filter.call(nodes, this._bindFilterItem);
+    this._setItems(nodes);
+  },
+
+  _updateAttrForSelected: function() {
+    if (this.selectedItem) {
+      this.selected = this._valueForItem(this.selectedItem);
+    }
+  },
+
+  _updateSelected: function() {
+    this._selectSelected(this.selected);
+  },
+
+  _selectSelected: function(selected) {
+    if (!this.items) {
+      return;
+    }
+
+    var item = this._valueToItem(this.selected);
+    if (item) {
+      this._selection.select(item);
+    } else {
+      this._selection.clear();
+    }
+    // Check for items, since this array is populated only when attached
+    // Since Number(0) is falsy, explicitly check for undefined
+    if (this.fallbackSelection && this.items.length &&
+        (this._selection.get() === undefined)) {
+      this.selected = this.fallbackSelection;
+    }
+  },
+
+  _filterItem: function(node) {
+    return !this._excludedLocalNames[node.localName];
+  },
+
+  _valueToItem: function(value) {
+    return (value == null) ? null : this.items[this._valueToIndex(value)];
+  },
+
+  _valueToIndex: function(value) {
+    if (this.attrForSelected) {
+      for (var i = 0, item; item = this.items[i]; i++) {
+        if (this._valueForItem(item) == value) {
+          return i;
+        }
+      }
+    } else {
+      return Number(value);
+    }
+  },
+
+  _indexToValue: function(index) {
+    if (this.attrForSelected) {
+      var item = this.items[index];
+      if (item) {
+        return this._valueForItem(item);
+      }
+    } else {
+      return index;
+    }
+  },
+
+  _valueForItem: function(item) {
+    if (!item) {
+      return null;
+    }
+    if (!this.attrForSelected) {
+      var i = this.indexOf(item);
+      return i === -1 ? null : i;
+    }
+    var propValue = item[dashToCamelCase(this.attrForSelected)];
+    return propValue != undefined ? propValue :
+                                    item.getAttribute(this.attrForSelected);
+  },
+
+  _applySelection: function(item, isSelected) {
+    if (this.selectedClass) {
+      this.toggleClass(this.selectedClass, isSelected, item);
+    }
+    if (this.selectedAttribute) {
+      this.toggleAttribute(this.selectedAttribute, isSelected, item);
+    }
+    this._selectionChange();
+    this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item});
+  },
+
+  _selectionChange: function() {
+    this._setSelectedItem(this._selection.get());
+  },
+
+  // observe items change under the given node.
+  _observeItems: function(node) {
+    return dom(node).observeNodes(function(mutation) {
+      this._updateItems();
+      this._updateSelected();
+
+      // Let other interested parties know about the change so that
+      // we don't have to recreate mutation observers everywhere.
+      this.fire(
+          'iron-items-changed', mutation, {bubbles: false, cancelable: false});
+    });
+  },
+
+  _activateHandler: function(e) {
+    var t = e.target;
+    var items = this.items;
+    while (t && t != this) {
+      var i = items.indexOf(t);
+      if (i >= 0) {
+        var value = this._indexToValue(i);
+        this._itemActivate(value, t);
+        return;
+      }
+      t = t.parentNode;
+    }
+  },
+
+  _itemActivate: function(value, item) {
+    if (!this.fire('iron-activate', {selected: value, item: item}, {
+               cancelable: true
+             })
+             .defaultPrevented) {
+      this.select(value);
+    }
+  }
+
+};
diff --git a/third_party/polymer/v3_0/components-chromium/iron-selector/iron-selection.js b/third_party/polymer/v3_0/components-chromium/iron-selector/iron-selection.js
new file mode 100644
index 0000000..ebdc9e2
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-selector/iron-selection.js
@@ -0,0 +1,107 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+export class IronSelection {
+  /**
+   * @param {!Function} selectCallback
+   * @suppress {missingProvide}
+   */
+  constructor(selectCallback) {
+    this.selection = [];
+    this.selectCallback = selectCallback;
+  }
+
+  /**
+   * Retrieves the selected item(s).
+   *
+   * @returns Returns the selected item(s). If the multi property is true,
+   * `get` will return an array, otherwise it will return
+   * the selected item or undefined if there is no selection.
+   */
+  get() {
+    return this.multi ? this.selection.slice() : this.selection[0];
+  }
+
+  /**
+   * Clears all the selection except the ones indicated.
+   *
+   * @param {Array} excludes items to be excluded.
+   */
+  clear(excludes) {
+    this.selection.slice().forEach(function(item) {
+      if (!excludes || excludes.indexOf(item) < 0) {
+        this.setItemSelected(item, false);
+      }
+    }, this);
+  }
+
+  /**
+   * Indicates if a given item is selected.
+   *
+   * @param {*} item The item whose selection state should be checked.
+   * @return {boolean} Returns true if `item` is selected.
+   */
+  isSelected(item) {
+    return this.selection.indexOf(item) >= 0;
+  }
+
+  /**
+   * Sets the selection state for a given item to either selected or deselected.
+   *
+   * @param {*} item The item to select.
+   * @param {boolean} isSelected True for selected, false for deselected.
+   */
+  setItemSelected(item, isSelected) {
+    if (item != null) {
+      if (isSelected !== this.isSelected(item)) {
+        // proceed to update selection only if requested state differs from
+        // current
+        if (isSelected) {
+          this.selection.push(item);
+        } else {
+          var i = this.selection.indexOf(item);
+          if (i >= 0) {
+            this.selection.splice(i, 1);
+          }
+        }
+        if (this.selectCallback) {
+          this.selectCallback(item, isSelected);
+        }
+      }
+    }
+  }
+
+  /**
+   * Sets the selection state for a given item. If the `multi` property
+   * is true, then the selected state of `item` will be toggled; otherwise
+   * the `item` will be selected.
+   *
+   * @param {*} item The item to select.
+   */
+  select(item) {
+    if (this.multi) {
+      this.toggle(item);
+    } else if (this.get() !== item) {
+      this.setItemSelected(this.get(), false);
+      this.setItemSelected(item, true);
+    }
+  }
+
+  /**
+   * Toggles the selection state for `item`.
+   *
+   * @param {*} item The item to toggle.
+   */
+  toggle(item) {
+    this.setItemSelected(item, !this.isSelected(item));
+  }
+};
diff --git a/third_party/polymer/v3_0/components-chromium/iron-selector/iron-selector.js b/third_party/polymer/v3_0/components-chromium/iron-selector/iron-selector.js
new file mode 100644
index 0000000..1991f58
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-selector/iron-selector.js
@@ -0,0 +1,87 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+
+import {IronMultiSelectableBehavior} from './iron-multi-selectable.js';
+
+/**
+`iron-selector` is an element which can be used to manage a list of elements
+that can be selected.  Tapping on the item will make the item selected.  The
+`selected` indicates which item is being selected.  The default is to use the
+index of the item.
+
+Example:
+
+    <iron-selector selected="0">
+      <div>Item 1</div>
+      <div>Item 2</div>
+      <div>Item 3</div>
+    </iron-selector>
+
+If you want to use the attribute value of an element for `selected` instead of
+the index, set `attrForSelected` to the name of the attribute.  For example, if
+you want to select item by `name`, set `attrForSelected` to `name`.
+
+Example:
+
+    <iron-selector attr-for-selected="name" selected="foo">
+      <div name="foo">Foo</div>
+      <div name="bar">Bar</div>
+      <div name="zot">Zot</div>
+    </iron-selector>
+
+You can specify a default fallback with `fallbackSelection` in case the
+`selected` attribute does not match the `attrForSelected` attribute of any
+elements.
+
+Example:
+
+      <iron-selector attr-for-selected="name" selected="non-existing"
+                     fallback-selection="default">
+        <div name="foo">Foo</div>
+        <div name="bar">Bar</div>
+        <div name="default">Default</div>
+      </iron-selector>
+
+Note: When the selector is multi, the selection will set to `fallbackSelection`
+iff the number of matching elements is zero.
+
+`iron-selector` is not styled. Use the `iron-selected` CSS class to style the
+selected element.
+
+Example:
+
+    <style>
+      .iron-selected {
+        background: #eee;
+      }
+    </style>
+
+    ...
+
+    <iron-selector selected="0">
+      <div>Item 1</div>
+      <div>Item 2</div>
+      <div>Item 3</div>
+    </iron-selector>
+
+@demo demo/index.html
+*/
+
+Polymer({
+
+  is: 'iron-selector',
+
+  behaviors: [IronMultiSelectableBehavior]
+
+});
diff --git a/third_party/polymer/v3_0/components-chromium/iron-test-helpers/iron-test-helpers.js b/third_party/polymer/v3_0/components-chromium/iron-test-helpers/iron-test-helpers.js
new file mode 100644
index 0000000..8fd6155
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-test-helpers/iron-test-helpers.js
@@ -0,0 +1,12 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import './mock-interactions.js';
+import './test-helpers.js';
diff --git a/third_party/polymer/v3_0/components-chromium/iron-test-helpers/mock-interactions.js b/third_party/polymer/v3_0/components-chromium/iron-test-helpers/mock-interactions.js
new file mode 100644
index 0000000..13b9380
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-test-helpers/mock-interactions.js
@@ -0,0 +1,480 @@
+/**
+ * @license
+ * Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+ * This code may only be used under the BSD style license found at
+ * http://polymer.github.io/LICENSE.txt The complete set of authors may be found
+ * at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
+ * be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
+ * Google as part of the polymer project is also subject to an additional IP
+ * rights grant found at http://polymer.github.io/PATENTS.txt
+ */
+
+import {Base} from '../polymer/polymer-legacy.js';
+
+const HAS_NEW_MOUSE = (() => {
+  let has = false;
+  try {
+    has = Boolean(new MouseEvent('x'));
+  } catch (_) {
+  }
+  return has;
+})();
+
+const HAS_NEW_TOUCH = (() => {
+  let has = false;
+  try {
+    has = Boolean(new TouchEvent('x'));
+  } catch (_) {
+  }
+  return has;
+})();
+
+/**
+ * Returns the (x,y) coordinates representing the middle of a node.
+ *
+ * @param {!Element} node An element.
+ */
+export function middleOfNode(node) {
+  const bcr = node.getBoundingClientRect();
+  return {y: bcr.top + (bcr.height / 2), x: bcr.left + (bcr.width / 2)};
+}
+
+/**
+ * Returns the (x,y) coordinates representing the top left corner of a node.
+ *
+ * @param {!Element} node An element.
+ */
+export function topLeftOfNode(node) {
+  const bcr = node.getBoundingClientRect();
+  return {y: bcr.top, x: bcr.left};
+}
+
+/**
+ * Returns a list of Touch objects that correspond to an array of positions
+ * and a target node. The Touch instances will each have a unique Touch
+ * identifier.
+ *
+ * @param {!Array<{ x: number, y: number }>} xyList A list of (x,y) coordinate
+ * objects.
+ * @param {!Element} node A target element node.
+ */
+export function makeTouches(xyList, node) {
+  let id = 0;
+
+  return xyList.map(function(xy) {
+    var touchInit =
+        {identifier: id++, target: node, clientX: xy.x, clientY: xy.y};
+
+    return HAS_NEW_TOUCH ? new window.Touch(touchInit) : touchInit;
+  });
+}
+
+/**
+ * Generates and dispatches a TouchEvent of a given type, at a specified
+ * position of a target node.
+ *
+ * @param {string} type The type of TouchEvent to generate.
+ * @param {{ x: number, y: number }} xy An (x,y) coordinate for the generated
+ * TouchEvent.
+ * @param {!Element} node The target element node for the generated
+ * TouchEvent to be dispatched on.
+ */
+export function makeSoloTouchEvent(type, xy, node) {
+  xy = xy || middleOfNode(node);
+  const touches = makeTouches([xy], node);
+  const touchEventInit = {
+    touches: touches,
+    targetTouches: touches,
+    changedTouches: touches
+  };
+  let event;
+
+  if (HAS_NEW_TOUCH) {
+    touchEventInit.bubbles = true;
+    touchEventInit.cancelable = true;
+    event = new TouchEvent(type, touchEventInit);
+  } else {
+    event = new CustomEvent(type, {
+      bubbles: true,
+      cancelable: true,
+      // Allow event to go outside a ShadowRoot.
+      composed: true
+    });
+    for (const property in touchEventInit) {
+      event[property] = touchEventInit[property];
+    }
+  }
+
+  node.dispatchEvent(event);
+}
+
+/**
+ * Fires a mouse event on a specific node, at a given set of coordinates.
+ * This event bubbles and is cancellable.
+ *
+ * @param {string} type The type of mouse event (such as 'tap' or 'down').
+ * @param {{ x: number, y: number }} xy The (x,y) coordinates the mouse event
+ * should be fired from.
+ * @param {!Element} node The node to fire the event on.
+ */
+export function makeMouseEvent(type, xy, node) {
+  const props = {
+    bubbles: true,
+    cancelable: true,
+    clientX: xy.x,
+    clientY: xy.y,
+    // Allow event to go outside a ShadowRoot.
+    composed: true,
+    // Make this a primary input.
+    buttons:
+        1  // http://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
+  };
+  let e;
+  if (HAS_NEW_MOUSE) {
+    e = new MouseEvent(type, props);
+  } else {
+    e = document.createEvent('MouseEvent');
+    e.initMouseEvent(
+        type,
+        props.bubbles,
+        props.cancelable,
+        null, /* view */
+        null, /* detail */
+        0,    /* screenX */
+        0,    /* screenY */
+        props.clientX,
+        props.clientY,
+        false, /*ctrlKey */
+        false, /*altKey */
+        false, /*shiftKey */
+        false, /*metaKey */
+        0,     /*button */
+        null /*relatedTarget*/);
+  }
+  node.dispatchEvent(e);
+}
+
+/**
+ * Simulates a mouse move action by firing a `move` mouse event on a
+ * specific node, between a set of coordinates.
+ *
+ * @param {!Element} node The node to fire the event on.
+ * @param {Object} fromXY The (x,y) coordinates the dragging should start from.
+ * @param {Object} toXY The (x,y) coordinates the dragging should end at.
+ * @param {?number=} steps Optional. The numbers of steps in the move motion.
+ *    If not specified, the default is 5.
+ */
+export function move(node, fromXY, toXY, steps) {
+  steps = steps || 5;
+  var dx = Math.round((fromXY.x - toXY.x) / steps);
+  var dy = Math.round((fromXY.y - toXY.y) / steps);
+  var xy = {x: fromXY.x, y: fromXY.y};
+  for (var i = steps; i > 0; i--) {
+    makeMouseEvent('mousemove', xy, node);
+    xy.x += dx;
+    xy.y += dy;
+  }
+  makeMouseEvent('mousemove', {x: toXY.x, y: toXY.y}, node);
+}
+
+/**
+ * Simulates a mouse dragging action originating in the middle of a specific
+ * node.
+ *
+ * @param {!Element} target The node to fire the event on.
+ * @param {?number} dx The horizontal displacement.
+ * @param {?number} dy The vertical displacement
+ * @param {?number=} steps Optional. The numbers of steps in the dragging
+ * motion. If not specified, the default is 5.
+ */
+export function track(target, dx, dy, steps) {
+  dx = dx | 0;
+  dy = dy | 0;
+  steps = steps || 5;
+  down(target);
+  var xy = middleOfNode(target);
+  var xy2 = {x: xy.x + dx, y: xy.y + dy};
+  move(target, xy, xy2, steps);
+  up(target, xy2);
+}
+
+/**
+ * Fires a `down` mouse event on a specific node, at a given set of coordinates.
+ * This event bubbles and is cancellable. If the (x,y) coordinates are
+ * not specified, the middle of the node will be used instead.
+ *
+ * @param {!Element} node The node to fire the event on.
+ * @param {{ x: number, y: number }=} xy Optional. The (x,y) coordinates the
+ * mouse event should be fired from.
+ */
+export function down(node, xy) {
+  xy = xy || middleOfNode(node);
+  makeMouseEvent('mousedown', xy, node);
+}
+
+/**
+ * Fires an `up` mouse event on a specific node, at a given set of coordinates.
+ * This event bubbles and is cancellable. If the (x,y) coordinates are
+ * not specified, the middle of the node will be used instead.
+ *
+ * @param {!Element} node The node to fire the event on.
+ * @param {{ x: number, y: number }=} xy Optional. The (x,y) coordinates the
+ * mouse event should be fired from.
+ */
+export function up(node, xy) {
+  xy = xy || middleOfNode(node);
+  makeMouseEvent('mouseup', xy, node);
+}
+
+/**
+ * Generate a click event on a given node, optionally at a given coordinate.
+ * @param {!Element} node The node to fire the click event on.
+ * @param {{ x: number, y: number }=} xy Optional. The (x,y) coordinates the
+ * mouse event should be fired from.
+ */
+export function click(node, xy) {
+  xy = xy || middleOfNode(node);
+  makeMouseEvent('click', xy, node);
+}
+
+/**
+ * Generate a touchstart event on a given node, optionally at a given
+ * coordinate.
+ * @param {!Element} node The node to fire the click event on.
+ * @param {{ x: number, y: number }=} xy Optional. The (x,y) coordinates the
+ * touch event should be fired from.
+ */
+export function touchstart(node, xy) {
+  xy = xy || middleOfNode(node);
+  makeSoloTouchEvent('touchstart', xy, node);
+}
+
+/**
+ * Generate a touchend event on a given node, optionally at a given coordinate.
+ * @param {!Element} node The node to fire the click event on.
+ * @param {{ x: number, y: number }=} xy Optional. The (x,y) coordinates the
+ * touch event should be fired from.
+ */
+export function touchend(node, xy) {
+  xy = xy || middleOfNode(node);
+  makeSoloTouchEvent('touchend', xy, node);
+}
+
+/**
+ * Simulates a complete mouse click by firing a `down` mouse event, followed
+ * by an asynchronous `up` and `tap` events on a specific node. Calls the
+ *`callback` after the `tap` event is fired.
+ *
+ * @param {!Element} target The node to fire the event on.
+ * @param {?Function=} callback Optional. The function to be called after the
+ *action ends.
+ * @param {?{
+ *   emulateTouch: boolean
+ * }=} options Optional. Configure the emulation fidelity of the mouse events.
+ */
+export function downAndUp(target, callback, options) {
+  if (options && options.emulateTouch) {
+    touchstart(target);
+    touchend(target);
+  }
+
+  down(target);
+  Base.async(function() {
+    up(target);
+    click(target);
+    callback && callback();
+  });
+}
+
+/**
+ * Fires a 'tap' mouse event on a specific node. This respects the
+ * pointer-events set on the node, and will not fire on disabled nodes.
+ *
+ * @param {!Element} node The node to fire the event on.
+ * @param {?{
+ *   emulateTouch: boolean
+ * }=} options Optional. Configure the emulation fidelity of the mouse event.
+ */
+export function tap(node, options) {
+  // Respect nodes that are disabled in the UI.
+  if (window.getComputedStyle(node)['pointer-events'] === 'none') {
+    return;
+  }
+
+  const xy = middleOfNode(node);
+
+  if (options && options.emulateTouch) {
+    touchstart(node, xy);
+    touchend(node, xy);
+  }
+
+  down(node, xy);
+  up(node, xy);
+  click(node, xy);
+}
+
+/**
+ * Focuses a node by firing a `focus` event. This event does not bubble.
+ *
+ * @param {!Element} target The node to fire the event on.
+ */
+export function focus(target) {
+  Base.fire('focus', {}, {bubbles: false, node: target});
+}
+
+/**
+ * Blurs a node by firing a `blur` event. This event does not bubble.
+ *
+ * @param {!Element} target The node to fire the event on.
+ */
+export function blur(target) {
+  Base.fire('blur', {}, {bubbles: false, node: target});
+}
+
+/**
+ * Returns a keyboard event. This event bubbles and is cancellable.
+ *
+ * @param {string} type The type of keyboard event (such as 'keyup' or
+ * 'keydown').
+ * @param {number} keyCode The keyCode for the event.
+ * @param {(string|Array<string>)=} modifiers The key modifiers for the event.
+ *     Accepted values are shift, ctrl, alt, meta.
+ * @param {string=} key The KeyboardEvent.key value for the event.
+ */
+export function keyboardEventFor(type, keyCode, modifiers, key) {
+  const event = new CustomEvent(type, {
+    detail: 0,
+    bubbles: true,
+    cancelable: true,
+    // Allow event to go outside a ShadowRoot.
+    composed: true
+  });
+
+  event.keyCode = keyCode;
+  event.code = keyCode;
+
+  modifiers = modifiers || [];
+  if (typeof modifiers === 'string') {
+    modifiers = [modifiers];
+  }
+  event.shiftKey = modifiers.indexOf('shift') !== -1;
+  event.altKey = modifiers.indexOf('alt') !== -1;
+  event.ctrlKey = modifiers.indexOf('ctrl') !== -1;
+  event.metaKey = modifiers.indexOf('meta') !== -1;
+
+  event.key = key;
+
+  return event;
+}
+
+/**
+ * Fires a keyboard event on a specific node. This event bubbles and is
+ * cancellable.
+ *
+ * @param {!Element} target The node to fire the event on.
+ * @param {string} type The type of keyboard event (such as 'keyup' or
+ * 'keydown').
+ * @param {number} keyCode The keyCode for the event.
+ * @param {(string|Array<string>)=} modifiers The key modifiers for the event.
+ *     Accepted values are shift, ctrl, alt, meta.
+ * @param {string=} key The KeyboardEvent.key value for the event.
+ */
+export function keyEventOn(target, type, keyCode, modifiers, key) {
+  target.dispatchEvent(keyboardEventFor(type, keyCode, modifiers, key));
+}
+
+/**
+ * Fires a 'keydown' event on a specific node. This event bubbles and is
+ * cancellable.
+ *
+ * @param {!Element} target The node to fire the event on.
+ * @param {number} keyCode The keyCode for the event.
+ * @param {(string|Array<string>)=} modifiers The key modifiers for the event.
+ *     Accepted values are shift, ctrl, alt, meta.
+ * @param {string=} key The KeyboardEvent.key value for the event.
+ */
+export function keyDownOn(target, keyCode, modifiers, key) {
+  keyEventOn(target, 'keydown', keyCode, modifiers, key);
+}
+
+/**
+ * Fires a 'keyup' event on a specific node. This event bubbles and is
+ * cancellable.
+ *
+ * @param {!Element} target The node to fire the event on.
+ * @param {number} keyCode The keyCode for the event.
+ * @param {(string|Array<string>)=} modifiers The key modifiers for the event.
+ *     Accepted values are shift, ctrl, alt, meta.
+ * @param {string=} key The KeyboardEvent.key value for the event.
+ */
+export function keyUpOn(target, keyCode, modifiers, key) {
+  keyEventOn(target, 'keyup', keyCode, modifiers, key);
+}
+
+/**
+ * Simulates a complete key press by firing a `keydown` keyboard event, followed
+ * by an asynchronous `keyup` event on a specific node.
+ *
+ * @param {!Element} target The node to fire the event on.
+ * @param {number} keyCode The keyCode for the event.
+ * @param {(string|Array<string>)=} modifiers The key modifiers for the event.
+ *     Accepted values are shift, ctrl, alt, meta.
+ * @param {string=} key The KeyboardEvent.key value for the event.
+ */
+export function pressAndReleaseKeyOn(target, keyCode, modifiers, key) {
+  keyDownOn(target, keyCode, modifiers, key);
+  Base.async(function() {
+    keyUpOn(target, keyCode, modifiers, key);
+  }, 1);
+}
+
+/**
+ * Simulates a complete 'enter' key press by firing a `keydown` keyboard event,
+ * followed by an asynchronous `keyup` event on a specific node.
+ *
+ * @param {!Element} target The node to fire the event on.
+ */
+export function pressEnter(target) {
+  pressAndReleaseKeyOn(target, 13);
+}
+
+/**
+ * Simulates a complete 'space' key press by firing a `keydown` keyboard event,
+ * followed by an asynchronous `keyup` event on a specific node.
+ *
+ * @param {!Element} target The node to fire the event on.
+ */
+export function pressSpace(target) {
+  pressAndReleaseKeyOn(target, 32);
+}
+
+/**
+ * This global is provided for backwards compatibility and will be removed in
+ * the next major version. All users should migrate to importing functions
+ * directly from this module instead of accessing them via the global.
+ */
+window.MockInteractions = {
+  middleOfNode,
+  topLeftOfNode,
+  makeTouches,
+  makeSoloTouchEvent,
+  makeMouseEvent,
+  move,
+  track,
+  down,
+  up,
+  click,
+  touchstart,
+  touchend,
+  downAndUp,
+  tap,
+  focus,
+  blur,
+  keyboardEventFor,
+  keyEventOn,
+  keyDownOn,
+  keyUpOn,
+  pressAndReleaseKeyOn,
+  pressEnter,
+  pressSpace,
+};
diff --git a/third_party/polymer/v3_0/components-chromium/iron-test-helpers/test-helpers.js b/third_party/polymer/v3_0/components-chromium/iron-test-helpers/test-helpers.js
new file mode 100644
index 0000000..af80627
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-test-helpers/test-helpers.js
@@ -0,0 +1,102 @@
+/**
+ * @license
+ * Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+ * This code may only be used under the BSD style license found at
+ * http://polymer.github.io/LICENSE.txt The complete set of authors may be found
+ * at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
+ * be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
+ * Google as part of the polymer project is also subject to an additional IP
+ * rights grant found at http://polymer.github.io/PATENTS.txt
+ */
+
+import {dom, flush} from '../polymer/lib/legacy/polymer.dom.js';
+
+/**
+ * Forces distribution of light children, and lifecycle callbacks on the
+ * Custom Elements polyfill. Used when testing elements that rely on their
+ * distributed children.
+ */
+export function flushAsynchronousOperations() {
+  // force distribution
+  flush();
+  // force lifecycle callback to fire on polyfill
+  window.CustomElements && window.CustomElements.takeRecords();
+}
+
+/**
+ * Stamps and renders a `dom-if` template.
+ *
+ * @param {!Element} node The node containing the template,
+ */
+export function forceXIfStamp(node) {
+  const templates = dom(node.root).querySelectorAll('template[is=dom-if]');
+  for (let tmpl, i = 0; tmpl = templates[i]; i++) {
+    tmpl.render();
+  }
+
+  flushAsynchronousOperations();
+}
+
+/**
+ * Fires a custom event on a specific node. This event bubbles and is
+ * cancellable.
+ *
+ * @param {string} type The type of event.
+ * @param {?Object} props Any custom properties the event contains.
+ * @param {!Element} node The node to fire the event on.
+ */
+export function fireEvent(type, props, node) {
+  const event = new CustomEvent(type, {bubbles: true, cancelable: true});
+  for (const p in props) {
+    event[p] = props[p];
+  }
+  node.dispatchEvent(event);
+}
+
+/**
+ * Skips a test unless a condition is met. Sample use:
+ *    function isNotIE() {
+ *      return !navigator.userAgent.match(/MSIE/i);
+ *    }
+ *    test('runs on non IE browsers', skipUnless(isNotIE, function() {
+ *      ...
+ *    });
+ *
+ * @param {Function} condition The name of a Boolean function determining if the
+ * test should be run.
+ * @param {Function} test The test to be run.
+ */
+export function skipUnless(condition, test) {
+  const isAsyncTest = !!test.length;
+
+  return (done) => {
+    if (!condition()) {
+      return done();
+    }
+
+    const result = test.call(this, done);
+
+    if (!isAsyncTest) {
+      done();
+    }
+
+    return result;
+  };
+}
+
+/**
+ * The globals below are provided for backwards compatibility and will be
+ * removed in the next major version. All users should migrate to importing
+ * functions directly from this module instead of accessing them via globals.
+ */
+window.TestHelpers = {
+  flushAsynchronousOperations,
+  forceXIfStamp,
+  fireEvent,
+  skipUnless,
+};
+
+window.flushAsynchronousOperations = flushAsynchronousOperations;
+window.forceXIfStamp = forceXIfStamp;
+window.fireEvent = fireEvent;
+window.skipUnless = skipUnless;
diff --git a/third_party/polymer/v3_0/components-chromium/iron-validatable-behavior/iron-validatable-behavior.js b/third_party/polymer/v3_0/components-chromium/iron-validatable-behavior/iron-validatable-behavior.js
new file mode 100644
index 0000000..c0cf7479
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/iron-validatable-behavior/iron-validatable-behavior.js
@@ -0,0 +1,131 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {IronMeta} from '../iron-meta/iron-meta.js';
+
+/**
+ * Singleton IronMeta instance.
+ */
+export let IronValidatableBehaviorMeta = null;
+
+/**
+ * `Use IronValidatableBehavior` to implement an element that validates
+ * user input. Use the related `IronValidatorBehavior` to add custom
+ * validation logic to an iron-input.
+ *
+ * By default, an `<iron-form>` element validates its fields when the user
+ * presses the submit button. To validate a form imperatively, call the form's
+ * `validate()` method, which in turn will call `validate()` on all its
+ * children. By using `IronValidatableBehavior`, your custom element
+ * will get a public `validate()`, which will return the validity of the
+ * element, and a corresponding `invalid` attribute, which can be used for
+ * styling.
+ *
+ * To implement the custom validation logic of your element, you must override
+ * the protected `_getValidity()` method of this behaviour, rather than
+ * `validate()`. See
+ * [this](https://github.com/PolymerElements/iron-form/blob/master/demo/simple-element.html)
+ * for an example.
+ *
+ * ### Accessibility
+ *
+ * Changing the `invalid` property, either manually or by calling `validate()`
+ * will update the `aria-invalid` attribute.
+ *
+ * @demo demo/index.html
+ * @polymerBehavior
+ */
+export const IronValidatableBehavior = {
+
+  properties: {
+    /**
+     * Name of the validator to use.
+     */
+    validator: {type: String},
+
+    /**
+     * True if the last call to `validate` is invalid.
+     */
+    invalid: {
+      notify: true,
+      reflectToAttribute: true,
+      type: Boolean,
+      value: false,
+      observer: '_invalidChanged'
+    },
+  },
+
+  registered: function() {
+    IronValidatableBehaviorMeta = new IronMeta({type: 'validator'});
+  },
+
+  _invalidChanged: function() {
+    if (this.invalid) {
+      this.setAttribute('aria-invalid', 'true');
+    } else {
+      this.removeAttribute('aria-invalid');
+    }
+  },
+
+  /* Recompute this every time it's needed, because we don't know if the
+   * underlying IronValidatableBehaviorMeta has changed. */
+  get _validator() {
+    return IronValidatableBehaviorMeta &&
+        IronValidatableBehaviorMeta.byKey(this.validator);
+  },
+
+  /**
+   * @return {boolean} True if the validator `validator` exists.
+   */
+  hasValidator: function() {
+    return this._validator != null;
+  },
+
+  /**
+   * Returns true if the `value` is valid, and updates `invalid`. If you want
+   * your element to have custom validation logic, do not override this method;
+   * override `_getValidity(value)` instead.
+
+   * @param {Object} value Deprecated: The value to be validated. By default,
+   * it is passed to the validator's `validate()` function, if a validator is
+   set.
+   * If this argument is not specified, then the element's `value` property
+   * is used, if it exists.
+   * @return {boolean} True if `value` is valid.
+   */
+  validate: function(value) {
+    // If this is an element that also has a value property, and there was
+    // no explicit value argument passed, use the element's property instead.
+    if (value === undefined && this.value !== undefined)
+      this.invalid = !this._getValidity(this.value);
+    else
+      this.invalid = !this._getValidity(value);
+    return !this.invalid;
+  },
+
+  /**
+   * Returns true if `value` is valid.  By default, it is passed
+   * to the validator's `validate()` function, if a validator is set. You
+   * should override this method if you want to implement custom validity
+   * logic for your element.
+   *
+   * @param {Object} value The value to be validated.
+   * @return {boolean} True if `value` is valid.
+   */
+
+  _getValidity: function(value) {
+    if (this.hasValidator()) {
+      return this._validator.validate(value);
+    }
+    return true;
+  }
+};
diff --git a/third_party/polymer/v3_0/components-chromium/neon-animation/animations/fade-in-animation.js b/third_party/polymer/v3_0/components-chromium/neon-animation/animations/fade-in-animation.js
new file mode 100644
index 0000000..257320c
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/neon-animation/animations/fade-in-animation.js
@@ -0,0 +1,42 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../../polymer/polymer-legacy.js';
+
+import {Polymer} from '../../polymer/lib/legacy/polymer-fn.js';
+import {NeonAnimationBehavior} from '../neon-animation-behavior.js';
+/*
+`<fade-in-animation>` animates the opacity of an element from 0 to 1.
+
+Configuration:
+```
+{
+  name: 'fade-in-animation',
+  node: <node>
+  timing: <animation-timing>
+}
+```
+*/
+Polymer({
+
+  is: 'fade-in-animation',
+
+  behaviors: [NeonAnimationBehavior],
+
+  configure: function(config) {
+    var node = config.node;
+    this._effect = new KeyframeEffect(
+        node,
+        [{'opacity': '0'}, {'opacity': '1'}],
+        this.timingFromConfig(config));
+    return this._effect;
+  }
+
+});
diff --git a/third_party/polymer/v3_0/components-chromium/neon-animation/animations/fade-out-animation.js b/third_party/polymer/v3_0/components-chromium/neon-animation/animations/fade-out-animation.js
new file mode 100644
index 0000000..83c586e
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/neon-animation/animations/fade-out-animation.js
@@ -0,0 +1,45 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../../polymer/polymer-legacy.js';
+
+import {Polymer} from '../../polymer/lib/legacy/polymer-fn.js';
+import {NeonAnimationBehavior} from '../neon-animation-behavior.js';
+/*
+`<fade-out-animation>` animates the opacity of an element from 1 to 0.
+
+Configuration:
+```
+{
+  name: 'fade-out-animation',
+  node: <node>
+  timing: <animation-timing>
+}
+```
+*/
+Polymer({
+
+  is: 'fade-out-animation',
+
+  behaviors: [NeonAnimationBehavior],
+
+  configure: function(config) {
+    var node = config.node;
+    this._effect = new KeyframeEffect(
+        node,
+        [
+          {'opacity': '1'},
+          {'opacity': '0'},
+        ],
+        this.timingFromConfig(config));
+    return this._effect;
+  }
+
+});
diff --git a/third_party/polymer/v3_0/components-chromium/neon-animation/neon-animatable-behavior.js b/third_party/polymer/v3_0/components-chromium/neon-animation/neon-animatable-behavior.js
new file mode 100644
index 0000000..14c4709
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/neon-animation/neon-animatable-behavior.js
@@ -0,0 +1,144 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+/**
+ * `NeonAnimatableBehavior` is implemented by elements containing
+ * animations for use with elements implementing
+ * `NeonAnimationRunnerBehavior`.
+ * @polymerBehavior
+ */
+export const NeonAnimatableBehavior = {
+
+  properties: {
+
+    /**
+     * Animation configuration. See README for more info.
+     */
+    animationConfig: {type: Object},
+
+    /**
+     * Convenience property for setting an 'entry' animation. Do not set
+     * `animationConfig.entry` manually if using this. The animated node is set
+     * to `this` if using this property.
+     */
+    entryAnimation: {
+      observer: '_entryAnimationChanged',
+      type: String,
+    },
+
+    /**
+     * Convenience property for setting an 'exit' animation. Do not set
+     * `animationConfig.exit` manually if using this. The animated node is set
+     * to `this` if using this property.
+     */
+    exitAnimation: {
+      observer: '_exitAnimationChanged',
+      type: String,
+    },
+
+  },
+
+  _entryAnimationChanged: function() {
+    this.animationConfig = this.animationConfig || {};
+    this.animationConfig['entry'] = [{name: this.entryAnimation, node: this}];
+  },
+
+  _exitAnimationChanged: function() {
+    this.animationConfig = this.animationConfig || {};
+    this.animationConfig['exit'] = [{name: this.exitAnimation, node: this}];
+  },
+
+  _copyProperties: function(config1, config2) {
+    // shallowly copy properties from config2 to config1
+    for (var property in config2) {
+      config1[property] = config2[property];
+    }
+  },
+
+  _cloneConfig: function(config) {
+    var clone = {isClone: true};
+    this._copyProperties(clone, config);
+    return clone;
+  },
+
+  _getAnimationConfigRecursive: function(type, map, allConfigs) {
+    if (!this.animationConfig) {
+      return;
+    }
+
+    if (this.animationConfig.value &&
+        typeof this.animationConfig.value === 'function') {
+      this._warn(this._logf(
+          'playAnimation',
+          'Please put \'animationConfig\' inside of your components \'properties\' object instead of outside of it.'));
+      return;
+    }
+
+    // type is optional
+    var thisConfig;
+    if (type) {
+      thisConfig = this.animationConfig[type];
+    } else {
+      thisConfig = this.animationConfig;
+    }
+
+    if (!Array.isArray(thisConfig)) {
+      thisConfig = [thisConfig];
+    }
+
+    // iterate animations and recurse to process configurations from child nodes
+    if (thisConfig) {
+      for (var config, index = 0; config = thisConfig[index]; index++) {
+        if (config.animatable) {
+          config.animatable._getAnimationConfigRecursive(
+              config.type || type, map, allConfigs);
+        } else {
+          if (config.id) {
+            var cachedConfig = map[config.id];
+            if (cachedConfig) {
+              // merge configurations with the same id, making a clone lazily
+              if (!cachedConfig.isClone) {
+                map[config.id] = this._cloneConfig(cachedConfig);
+                cachedConfig = map[config.id];
+              }
+              this._copyProperties(cachedConfig, config);
+            } else {
+              // put any configs with an id into a map
+              map[config.id] = config;
+            }
+          } else {
+            allConfigs.push(config);
+          }
+        }
+      }
+    }
+  },
+
+  /**
+   * An element implementing `NeonAnimationRunnerBehavior` calls this
+   * method to configure an animation with an optional type. Elements
+   * implementing `NeonAnimatableBehavior` should define the property
+   * `animationConfig`, which is either a configuration object or a map of
+   * animation type to array of configuration objects.
+   */
+  getAnimationConfig: function(type) {
+    var map = {};
+    var allConfigs = [];
+    this._getAnimationConfigRecursive(type, map, allConfigs);
+    // append the configurations saved in the map to the array
+    for (var key in map) {
+      allConfigs.push(map[key]);
+    }
+    return allConfigs;
+  }
+
+};
diff --git a/third_party/polymer/v3_0/components-chromium/neon-animation/neon-animatable.js b/third_party/polymer/v3_0/components-chromium/neon-animation/neon-animatable.js
new file mode 100644
index 0000000..215593d
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/neon-animation/neon-animatable.js
@@ -0,0 +1,46 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {IronResizableBehavior} from '../iron-resizable-behavior/iron-resizable-behavior.js';
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+
+import {NeonAnimatableBehavior} from './neon-animatable-behavior.js';
+
+/**
+`<neon-animatable>` is a simple container element implementing
+`NeonAnimatableBehavior`. This is a convenience element for use with
+`<neon-animated-pages>`.
+
+```
+<neon-animated-pages selected="0"
+                     entry-animation="slide-from-right-animation"
+                     exit-animation="slide-left-animation">
+  <neon-animatable>1</neon-animatable>
+  <neon-animatable>2</neon-animatable>
+</neon-animated-pages>
+```
+*/
+Polymer({
+  _template: html`
+    <style>
+      :host {
+        display: block;
+      }
+    </style>
+
+    <slot></slot>
+  `,
+
+  is: 'neon-animatable',
+  behaviors: [NeonAnimatableBehavior, IronResizableBehavior]
+});
diff --git a/third_party/polymer/v3_0/components-chromium/neon-animation/neon-animated-pages.js b/third_party/polymer/v3_0/components-chromium/neon-animation/neon-animated-pages.js
new file mode 100644
index 0000000..0ea3e08
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/neon-animation/neon-animated-pages.js
@@ -0,0 +1,194 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {IronResizableBehavior} from '../iron-resizable-behavior/iron-resizable-behavior.js';
+import {IronSelectableBehavior} from '../iron-selector/iron-selectable.js';
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+
+import {NeonAnimationRunnerBehavior} from './neon-animation-runner-behavior.js';
+
+/**
+Material design: [Meaningful
+transitions](https://www.google.com/design/spec/animation/meaningful-transitions.html)
+
+`neon-animated-pages` manages a set of pages and runs an animation when
+switching between them. Its children pages should implement
+`NeonAnimatableBehavior` and define `entry` and `exit` animations to be
+run when switching to or switching out of the page.
+
+@group Neon Elements
+@element neon-animated-pages
+*/
+Polymer({
+  _template: html`
+    <style>
+      :host {
+        display: block;
+        position: relative;
+      }
+
+      :host > ::slotted(*) {
+        position: absolute;
+        top: 0;
+        left: 0;
+        bottom: 0;
+        right: 0;
+      }
+
+      :host > ::slotted(:not(.iron-selected):not(.neon-animating))
+       {
+        display: none !important;
+      }
+
+      :host > ::slotted(.neon-animating) {
+        pointer-events: none;
+      }
+    </style>
+
+    <slot id="content"></slot>
+  `,
+
+  is: 'neon-animated-pages',
+
+  behaviors: [
+    IronResizableBehavior,
+    IronSelectableBehavior,
+    NeonAnimationRunnerBehavior
+  ],
+
+  properties: {
+
+    activateEvent: {type: String, value: ''},
+
+    // if true, the initial page selection will also be animated according to
+    // its animation config.
+    animateInitialSelection: {type: Boolean, value: false}
+
+  },
+
+  listeners: {
+    'iron-select': '_onIronSelect',
+    'neon-animation-finish': '_onNeonAnimationFinish'
+  },
+
+  _onIronSelect: function(event) {
+    var selectedPage = event.detail.item;
+
+    // Only consider child elements.
+    if (this.items.indexOf(selectedPage) < 0) {
+      return;
+    }
+
+    var oldPage = this._valueToItem(this._prevSelected) || false;
+    this._prevSelected = this.selected;
+
+    // on initial load and if animateInitialSelection is negated, simply display
+    // selectedPage.
+    if (!oldPage && !this.animateInitialSelection) {
+      this._completeSelectedChanged();
+      return;
+    }
+
+    this.animationConfig = [];
+
+    // configure selectedPage animations.
+    if (this.entryAnimation) {
+      this.animationConfig.push(
+          {name: this.entryAnimation, node: selectedPage});
+    } else {
+      if (selectedPage.getAnimationConfig) {
+        this.animationConfig.push({animatable: selectedPage, type: 'entry'});
+      }
+    }
+
+    // configure oldPage animations iff exists.
+    if (oldPage) {
+      // cancel the currently running animation if one is ongoing.
+      if (oldPage.classList.contains('neon-animating')) {
+        this._squelchNextFinishEvent = true;
+        this.cancelAnimation();
+        this._completeSelectedChanged();
+        this._squelchNextFinishEvent = false;
+      }
+
+      // configure the animation.
+      if (this.exitAnimation) {
+        this.animationConfig.push({name: this.exitAnimation, node: oldPage});
+      } else {
+        if (oldPage.getAnimationConfig) {
+          this.animationConfig.push({animatable: oldPage, type: 'exit'});
+        }
+      }
+
+      // display the oldPage during the transition.
+      oldPage.classList.add('neon-animating');
+    }
+
+    // display the selectedPage during the transition.
+    selectedPage.classList.add('neon-animating');
+
+    // actually run the animations.
+    if (this.animationConfig.length >= 1) {
+      // on first load, ensure we run animations only after element is attached.
+      if (!this.isAttached) {
+        this.async(function() {
+          this.playAnimation(undefined, {fromPage: null, toPage: selectedPage});
+        });
+
+      } else {
+        this.playAnimation(
+            undefined, {fromPage: oldPage, toPage: selectedPage});
+      }
+
+    } else {
+      this._completeSelectedChanged(oldPage, selectedPage);
+    }
+  },
+
+  /**
+   * @param {Object=} oldPage
+   * @param {Object=} selectedPage
+   */
+  _completeSelectedChanged: function(oldPage, selectedPage) {
+    if (selectedPage) {
+      selectedPage.classList.remove('neon-animating');
+    }
+    if (oldPage) {
+      oldPage.classList.remove('neon-animating');
+    }
+    if (!selectedPage || !oldPage) {
+      var nodes = dom(this.$.content).getDistributedNodes();
+      for (var node, index = 0; node = nodes[index]; index++) {
+        node.classList && node.classList.remove('neon-animating');
+      }
+    }
+    this.async(this._notifyPageResize);
+  },
+
+  _onNeonAnimationFinish: function(event) {
+    if (this._squelchNextFinishEvent) {
+      this._squelchNextFinishEvent = false;
+      return;
+    }
+    this._completeSelectedChanged(event.detail.fromPage, event.detail.toPage);
+  },
+
+  _notifyPageResize: function() {
+    var selectedPage = this.selectedItem || this._valueToItem(this.selected);
+    this.resizerShouldNotify = function(element) {
+      return element == selectedPage;
+    };
+    this.notifyResize();
+  }
+})
diff --git a/third_party/polymer/v3_0/components-chromium/neon-animation/neon-animation-behavior.js b/third_party/polymer/v3_0/components-chromium/neon-animation/neon-animation-behavior.js
new file mode 100644
index 0000000..33be9de
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/neon-animation/neon-animation-behavior.js
@@ -0,0 +1,88 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+/**
+ * Use `NeonAnimationBehavior` to implement an animation.
+ * @polymerBehavior
+ */
+export const NeonAnimationBehavior = {
+
+  properties: {
+
+    /**
+     * Defines the animation timing.
+     */
+    animationTiming: {
+      type: Object,
+      value: function() {
+        return {
+          duration: 500, easing: 'cubic-bezier(0.4, 0, 0.2, 1)', fill: 'both'
+        }
+      }
+    }
+
+  },
+
+  /**
+   * Can be used to determine that elements implement this behavior.
+   */
+  isNeonAnimation: true,
+
+  /**
+   * Do any animation configuration here.
+   */
+  // configure: function(config) {
+  // },
+
+  created: function() {
+    if (!document.body.animate) {
+      console.warn(
+          'No web animations detected. This element will not' +
+          ' function without a web animations polyfill.');
+    }
+  },
+
+  /**
+   * Returns the animation timing by mixing in properties from `config` to the
+   * defaults defined by the animation.
+   */
+  timingFromConfig: function(config) {
+    if (config.timing) {
+      for (var property in config.timing) {
+        this.animationTiming[property] = config.timing[property];
+      }
+    }
+    return this.animationTiming;
+  },
+
+  /**
+   * Sets `transform` and `transformOrigin` properties along with the prefixed
+   * versions.
+   */
+  setPrefixedProperty: function(node, property, value) {
+    var map = {
+      'transform': ['webkitTransform'],
+      'transformOrigin': ['mozTransformOrigin', 'webkitTransformOrigin']
+    };
+    var prefixes = map[property];
+    for (var prefix, index = 0; prefix = prefixes[index]; index++) {
+      node.style[prefix] = value;
+    }
+    node.style[property] = value;
+  },
+
+  /**
+   * Called when the animation finishes.
+   */
+  complete: function(config) {}
+
+};
diff --git a/third_party/polymer/v3_0/components-chromium/neon-animation/neon-animation-runner-behavior.js b/third_party/polymer/v3_0/components-chromium/neon-animation/neon-animation-runner-behavior.js
new file mode 100644
index 0000000..c718e87
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/neon-animation/neon-animation-runner-behavior.js
@@ -0,0 +1,157 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {NeonAnimatableBehavior} from './neon-animatable-behavior.js';
+
+/**
+ * `NeonAnimationRunnerBehavior` adds a method to run animations.
+ *
+ * @polymerBehavior NeonAnimationRunnerBehavior
+ */
+export const NeonAnimationRunnerBehaviorImpl = {
+
+  _configureAnimations: function(configs) {
+    var results = [];
+    var resultsToPlay = [];
+
+    if (configs.length > 0) {
+      for (let config, index = 0; config = configs[index]; index++) {
+        let neonAnimation = document.createElement(config.name);
+        // is this element actually a neon animation?
+        if (neonAnimation.isNeonAnimation) {
+          let result = null;
+          // Closure compiler does not work well with a try / catch here.
+          // .configure needs to be explicitly defined
+          if (!neonAnimation.configure) {
+            /**
+             * @param {Object} config
+             * @return {AnimationEffectReadOnly}
+             */
+            neonAnimation.configure = function(config) {
+              return null;
+            }
+          }
+
+          result = neonAnimation.configure(config);
+          resultsToPlay.push({
+            result: result,
+            config: config,
+            neonAnimation: neonAnimation,
+          });
+        } else {
+          console.warn(this.is + ':', config.name, 'not found!');
+        }
+      }
+    }
+
+    for (var i = 0; i < resultsToPlay.length; i++) {
+      let result = resultsToPlay[i].result;
+      let config = resultsToPlay[i].config;
+      let neonAnimation = resultsToPlay[i].neonAnimation;
+      // configuration or play could fail if polyfills aren't loaded
+      try {
+        // Check if we have an Effect rather than an Animation
+        if (typeof result.cancel != 'function') {
+          result = document.timeline.play(result);
+        }
+      } catch (e) {
+        result = null;
+        console.warn('Couldnt play', '(', config.name, ').', e);
+      }
+
+      if (result) {
+        results.push({
+          neonAnimation: neonAnimation,
+          config: config,
+          animation: result,
+        });
+      }
+    }
+
+    return results;
+  },
+
+  _shouldComplete: function(activeEntries) {
+    var finished = true;
+    for (var i = 0; i < activeEntries.length; i++) {
+      if (activeEntries[i].animation.playState != 'finished') {
+        finished = false;
+        break;
+      }
+    }
+    return finished;
+  },
+
+  _complete: function(activeEntries) {
+    for (var i = 0; i < activeEntries.length; i++) {
+      activeEntries[i].neonAnimation.complete(activeEntries[i].config);
+    }
+    for (var i = 0; i < activeEntries.length; i++) {
+      activeEntries[i].animation.cancel();
+    }
+  },
+
+  /**
+   * Plays an animation with an optional `type`.
+   * @param {string=} type
+   * @param {!Object=} cookie
+   */
+  playAnimation: function(type, cookie) {
+    var configs = this.getAnimationConfig(type);
+    if (!configs) {
+      return;
+    }
+    this._active = this._active || {};
+    if (this._active[type]) {
+      this._complete(this._active[type]);
+      delete this._active[type];
+    }
+
+    var activeEntries = this._configureAnimations(configs);
+
+    if (activeEntries.length == 0) {
+      this.fire('neon-animation-finish', cookie, {bubbles: false});
+      return;
+    }
+
+    this._active[type] = activeEntries;
+
+    for (var i = 0; i < activeEntries.length; i++) {
+      activeEntries[i].animation.onfinish = function() {
+        if (this._shouldComplete(activeEntries)) {
+          this._complete(activeEntries);
+          delete this._active[type];
+          this.fire('neon-animation-finish', cookie, {bubbles: false});
+        }
+      }.bind(this);
+    }
+  },
+
+  /**
+   * Cancels the currently running animations.
+   */
+  cancelAnimation: function() {
+    for (var k in this._active) {
+      var entries = this._active[k]
+
+                    for (var j in entries) {
+        entries[j].animation.cancel();
+      }
+    }
+
+    this._active = {};
+  }
+};
+
+/** @polymerBehavior */
+export const NeonAnimationRunnerBehavior =
+    [NeonAnimatableBehavior, NeonAnimationRunnerBehaviorImpl];
diff --git a/third_party/polymer/v3_0/components-chromium/neon-animation/neon-shared-element-animatable-behavior.js b/third_party/polymer/v3_0/components-chromium/neon-animation/neon-shared-element-animatable-behavior.js
new file mode 100644
index 0000000..9f8b10f
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/neon-animation/neon-shared-element-animatable-behavior.js
@@ -0,0 +1,35 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {NeonAnimatableBehavior} from './neon-animatable-behavior.js';
+
+/**
+ * Use `NeonSharedElementAnimatableBehavior` to implement elements
+ * containing shared element animations.
+ * @polymerBehavior NeonSharedElementAnimatableBehavior
+ */
+export const NeonSharedElementAnimatableBehaviorImpl = {
+
+  properties: {
+
+    /**
+     * A map of shared element id to node.
+     */
+    sharedElements: {type: Object, value: {}}
+
+  }
+
+};
+
+/** @polymerBehavior */
+export const NeonSharedElementAnimatableBehavior =
+    [NeonAnimatableBehavior, NeonSharedElementAnimatableBehaviorImpl];
diff --git a/third_party/polymer/v3_0/components-chromium/neon-animation/neon-shared-element-animation-behavior.js b/third_party/polymer/v3_0/components-chromium/neon-animation/neon-shared-element-animation-behavior.js
new file mode 100644
index 0000000..9343824
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/neon-animation/neon-shared-element-animation-behavior.js
@@ -0,0 +1,72 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {NeonAnimationBehavior} from './neon-animation-behavior.js';
+
+/**
+ * Use `NeonSharedElementAnimationBehavior` to implement shared element
+ * animations.
+ * @polymerBehavior NeonSharedElementAnimationBehavior
+ */
+export const NeonSharedElementAnimationBehaviorImpl = {
+
+  properties: {
+
+    /**
+     * Cached copy of shared elements.
+     */
+    sharedElements: {type: Object}
+
+  },
+
+  /**
+   * Finds shared elements based on `config`.
+   */
+  findSharedElements: function(config) {
+    var fromPage = config.fromPage;
+    var toPage = config.toPage;
+    if (!fromPage || !toPage) {
+      console.warn(
+          this.is + ':', !fromPage ? 'fromPage' : 'toPage', 'is undefined!');
+      return null;
+    };
+
+    if (!fromPage.sharedElements || !toPage.sharedElements) {
+      console.warn(
+          this.is + ':',
+          'sharedElements are undefined for',
+          !fromPage.sharedElements ? fromPage : toPage);
+      return null;
+    };
+
+    var from = fromPage.sharedElements[config.id];
+    var to = toPage.sharedElements[config.id];
+
+    if (!from || !to) {
+      console.warn(
+          this.is + ':',
+          'sharedElement with id',
+          config.id,
+          'not found in',
+          !from ? fromPage : toPage);
+      return null;
+    }
+
+    this.sharedElements = {from: from, to: to};
+    return this.sharedElements;
+  }
+
+};
+
+/** @polymerBehavior */
+export const NeonSharedElementAnimationBehavior =
+    [NeonAnimationBehavior, NeonSharedElementAnimationBehaviorImpl];
diff --git a/third_party/polymer/v3_0/components-chromium/paper-behaviors/paper-button-behavior.js b/third_party/polymer/v3_0/components-chromium/paper-behaviors/paper-button-behavior.js
new file mode 100644
index 0000000..23c9908a
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/paper-behaviors/paper-button-behavior.js
@@ -0,0 +1,90 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {IronButtonState, IronButtonStateImpl} from '../iron-behaviors/iron-button-state.js';
+import {IronControlState} from '../iron-behaviors/iron-control-state.js';
+
+import {PaperRippleBehavior} from './paper-ripple-behavior.js';
+
+/** @polymerBehavior PaperButtonBehavior */
+export const PaperButtonBehaviorImpl = {
+  properties: {
+    /**
+     * The z-depth of this element, from 0-5. Setting to 0 will remove the
+     * shadow, and each increasing number greater than 0 will be "deeper"
+     * than the last.
+     *
+     * @attribute elevation
+     * @type number
+     * @default 1
+     */
+    elevation: {type: Number, reflectToAttribute: true, readOnly: true}
+  },
+
+  observers: [
+    '_calculateElevation(focused, disabled, active, pressed, receivedFocusFromKeyboard)',
+    '_computeKeyboardClass(receivedFocusFromKeyboard)'
+  ],
+
+  hostAttributes: {role: 'button', tabindex: '0', animated: true},
+
+  _calculateElevation: function() {
+    var e = 1;
+    if (this.disabled) {
+      e = 0;
+    } else if (this.active || this.pressed) {
+      e = 4;
+    } else if (this.receivedFocusFromKeyboard) {
+      e = 3;
+    }
+    this._setElevation(e);
+  },
+
+  _computeKeyboardClass: function(receivedFocusFromKeyboard) {
+    this.toggleClass('keyboard-focus', receivedFocusFromKeyboard);
+  },
+
+  /**
+   * In addition to `IronButtonState` behavior, when space key goes down,
+   * create a ripple down effect.
+   *
+   * @param {!KeyboardEvent} event .
+   */
+  _spaceKeyDownHandler: function(event) {
+    IronButtonStateImpl._spaceKeyDownHandler.call(this, event);
+    // Ensure that there is at most one ripple when the space key is held down.
+    if (this.hasRipple() && this.getRipple().ripples.length < 1) {
+      this._ripple.uiDownAction();
+    }
+  },
+
+  /**
+   * In addition to `IronButtonState` behavior, when space key goes up,
+   * create a ripple up effect.
+   *
+   * @param {!KeyboardEvent} event .
+   */
+  _spaceKeyUpHandler: function(event) {
+    IronButtonStateImpl._spaceKeyUpHandler.call(this, event);
+    if (this.hasRipple()) {
+      this._ripple.uiUpAction();
+    }
+  }
+};
+
+/** @polymerBehavior */
+export const PaperButtonBehavior = [
+  IronButtonState,
+  IronControlState,
+  PaperRippleBehavior,
+  PaperButtonBehaviorImpl
+];
diff --git a/third_party/polymer/v3_0/components-chromium/paper-behaviors/paper-inky-focus-behavior.js b/third_party/polymer/v3_0/components-chromium/paper-behaviors/paper-inky-focus-behavior.js
new file mode 100644
index 0000000..73a141b
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/paper-behaviors/paper-inky-focus-behavior.js
@@ -0,0 +1,51 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {IronButtonState} from '../iron-behaviors/iron-button-state.js';
+import {IronControlState} from '../iron-behaviors/iron-control-state.js';
+
+import {PaperRippleBehavior} from './paper-ripple-behavior.js';
+
+/**
+ * `PaperInkyFocusBehavior` implements a ripple when the element has keyboard
+ * focus.
+ *
+ * @polymerBehavior PaperInkyFocusBehavior
+ */
+export const PaperInkyFocusBehaviorImpl = {
+  observers: ['_focusedChanged(receivedFocusFromKeyboard)'],
+
+  _focusedChanged: function(receivedFocusFromKeyboard) {
+    if (receivedFocusFromKeyboard) {
+      this.ensureRipple();
+    }
+    if (this.hasRipple()) {
+      this._ripple.holdDown = receivedFocusFromKeyboard;
+    }
+  },
+
+  _createRipple: function() {
+    var ripple = PaperRippleBehavior._createRipple();
+    ripple.id = 'ink';
+    ripple.setAttribute('center', '');
+    ripple.classList.add('circle');
+    return ripple;
+  }
+};
+
+/** @polymerBehavior */
+export const PaperInkyFocusBehavior = [
+  IronButtonState,
+  IronControlState,
+  PaperRippleBehavior,
+  PaperInkyFocusBehaviorImpl
+];
diff --git a/third_party/polymer/v3_0/components-chromium/paper-behaviors/paper-ripple-behavior.js b/third_party/polymer/v3_0/components-chromium/paper-behaviors/paper-ripple-behavior.js
new file mode 100644
index 0000000..7976f0b2
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/paper-behaviors/paper-ripple-behavior.js
@@ -0,0 +1,125 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+import '../paper-ripple/paper-ripple.js';
+
+import {IronButtonStateImpl} from '../iron-behaviors/iron-button-state.js';
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+
+/**
+ * `PaperRippleBehavior` dynamically implements a ripple when the element has
+ * focus via pointer or keyboard.
+ *
+ * NOTE: This behavior is intended to be used in conjunction with and after
+ * `IronButtonState` and `IronControlState`.
+ *
+ * @polymerBehavior PaperRippleBehavior
+ */
+export const PaperRippleBehavior = {
+  properties: {
+    /**
+     * If true, the element will not produce a ripple effect when interacted
+     * with via the pointer.
+     */
+    noink: {type: Boolean, observer: '_noinkChanged'},
+
+    /**
+     * @type {Element|undefined}
+     */
+    _rippleContainer: {
+      type: Object,
+    }
+  },
+
+  /**
+   * Ensures a `<paper-ripple>` element is available when the element is
+   * focused.
+   */
+  _buttonStateChanged: function() {
+    if (this.focused) {
+      this.ensureRipple();
+    }
+  },
+
+  /**
+   * In addition to the functionality provided in `IronButtonState`, ensures
+   * a ripple effect is created when the element is in a `pressed` state.
+   */
+  _downHandler: function(event) {
+    IronButtonStateImpl._downHandler.call(this, event);
+    if (this.pressed) {
+      this.ensureRipple(event);
+    }
+  },
+
+  /**
+   * Ensures this element contains a ripple effect. For startup efficiency
+   * the ripple effect is dynamically on demand when needed.
+   * @param {!Event=} optTriggeringEvent (optional) event that triggered the
+   * ripple.
+   */
+  ensureRipple: function(optTriggeringEvent) {
+    if (!this.hasRipple()) {
+      this._ripple = this._createRipple();
+      this._ripple.noink = this.noink;
+      var rippleContainer = this._rippleContainer || this.root;
+      if (rippleContainer) {
+        dom(rippleContainer).appendChild(this._ripple);
+      }
+      if (optTriggeringEvent) {
+        // Check if the event happened inside of the ripple container
+        // Fall back to host instead of the root because distributed text
+        // nodes are not valid event targets
+        var domContainer = dom(this._rippleContainer || this);
+        var target = dom(optTriggeringEvent).rootTarget;
+        if (domContainer.deepContains(/** @type {Node} */ (target))) {
+          this._ripple.uiDownAction(optTriggeringEvent);
+        }
+      }
+    }
+  },
+
+  /**
+   * Returns the `<paper-ripple>` element used by this element to create
+   * ripple effects. The element's ripple is created on demand, when
+   * necessary, and calling this method will force the
+   * ripple to be created.
+   */
+  getRipple: function() {
+    this.ensureRipple();
+    return this._ripple;
+  },
+
+  /**
+   * Returns true if this element currently contains a ripple effect.
+   * @return {boolean}
+   */
+  hasRipple: function() {
+    return Boolean(this._ripple);
+  },
+
+  /**
+   * Create the element's ripple effect via creating a `<paper-ripple>`.
+   * Override this method to customize the ripple element.
+   * @return {!PaperRippleElement} Returns a `<paper-ripple>` element.
+   */
+  _createRipple: function() {
+    var element = /** @type {!PaperRippleElement} */ (
+        document.createElement('paper-ripple'));
+    return element;
+  },
+
+  _noinkChanged: function(noink) {
+    if (this.hasRipple()) {
+      this._ripple.noink = noink;
+    }
+  }
+};
diff --git a/third_party/polymer/v3_0/components-chromium/paper-button/paper-button.js b/third_party/polymer/v3_0/components-chromium/paper-button/paper-button.js
new file mode 100644
index 0000000..eb9535c
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/paper-button/paper-button.js
@@ -0,0 +1,211 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../iron-flex-layout/iron-flex-layout.js';
+import '../paper-styles/element-styles/paper-material-styles.js';
+
+import {PaperButtonBehavior, PaperButtonBehaviorImpl} from '../paper-behaviors/paper-button-behavior.js';
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {html} from '../polymer/polymer-legacy.js';
+
+const template = html`
+  <style include="paper-material-styles">
+    /* Need to specify the same specificity as the styles imported from paper-material. */
+    :host {
+      @apply --layout-inline;
+      @apply --layout-center-center;
+      position: relative;
+      box-sizing: border-box;
+      min-width: 5.14em;
+      margin: 0 0.29em;
+      background: transparent;
+      -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+      -webkit-tap-highlight-color: transparent;
+      font: inherit;
+      text-transform: uppercase;
+      outline-width: 0;
+      border-radius: 3px;
+      -moz-user-select: none;
+      -ms-user-select: none;
+      -webkit-user-select: none;
+      user-select: none;
+      cursor: pointer;
+      z-index: 0;
+      padding: 0.7em 0.57em;
+
+      @apply --paper-font-common-base;
+      @apply --paper-button;
+    }
+
+    :host([elevation="1"]) {
+      @apply --paper-material-elevation-1;
+    }
+
+    :host([elevation="2"]) {
+      @apply --paper-material-elevation-2;
+    }
+
+    :host([elevation="3"]) {
+      @apply --paper-material-elevation-3;
+    }
+
+    :host([elevation="4"]) {
+      @apply --paper-material-elevation-4;
+    }
+
+    :host([elevation="5"]) {
+      @apply --paper-material-elevation-5;
+    }
+
+    :host([hidden]) {
+      display: none !important;
+    }
+
+    :host([raised].keyboard-focus) {
+      font-weight: bold;
+      @apply --paper-button-raised-keyboard-focus;
+    }
+
+    :host(:not([raised]).keyboard-focus) {
+      font-weight: bold;
+      @apply --paper-button-flat-keyboard-focus;
+    }
+
+    :host([disabled]) {
+      background: none;
+      color: #a8a8a8;
+      cursor: auto;
+      pointer-events: none;
+
+      @apply --paper-button-disabled;
+    }
+
+    :host([disabled][raised]) {
+      background: #eaeaea;
+    }
+
+
+    :host([animated]) {
+      @apply --shadow-transition;
+    }
+
+    paper-ripple {
+      color: var(--paper-button-ink-color);
+    }
+  </style>
+
+  <slot></slot>`;
+
+template.setAttribute('strip-whitespace', '');
+
+/**
+Material design:
+[Buttons](https://www.google.com/design/spec/components/buttons.html)
+
+`paper-button` is a button. When the user touches the button, a ripple effect
+emanates from the point of contact. It may be flat or raised. A raised button is
+styled with a shadow.
+
+Example:
+
+    <paper-button>Flat button</paper-button>
+    <paper-button raised>Raised button</paper-button>
+    <paper-button noink>No ripple effect</paper-button>
+    <paper-button toggles>Toggle-able button</paper-button>
+
+A button that has `toggles` true will remain `active` after being clicked (and
+will have an `active` attribute set). For more information, see the
+`IronButtonState` behavior.
+
+You may use custom DOM in the button body to create a variety of buttons. For
+example, to create a button with an icon and some text:
+
+    <paper-button>
+      <iron-icon icon="favorite"></iron-icon>
+      custom button content
+    </paper-button>
+
+To use `paper-button` as a link, wrap it in an anchor tag. Since `paper-button`
+will already receive focus, you may want to prevent the anchor tag from
+receiving focus as well by setting its tabindex to -1.
+
+    <a href="https://www.polymer-project.org/" tabindex="-1">
+      <paper-button raised>Polymer Project</paper-button>
+    </a>
+
+### Styling
+
+Style the button with CSS as you would a normal DOM element.
+
+    paper-button.fancy {
+      background: green;
+      color: yellow;
+    }
+
+    paper-button.fancy:hover {
+      background: lime;
+    }
+
+    paper-button[disabled],
+    paper-button[toggles][active] {
+      background: red;
+    }
+
+By default, the ripple is the same color as the foreground at 25% opacity. You
+may customize the color using the `--paper-button-ink-color` custom property.
+
+The following custom properties and mixins are also available for styling:
+
+Custom property | Description | Default
+----------------|-------------|----------
+`--paper-button-ink-color` | Background color of the ripple | `Based on the button's color`
+`--paper-button` | Mixin applied to the button | `{}`
+`--paper-button-disabled` | Mixin applied to the disabled button. Note that you can also use the `paper-button[disabled]` selector | `{}`
+`--paper-button-flat-keyboard-focus` | Mixin applied to a flat button after it's been focused using the keyboard | `{}`
+`--paper-button-raised-keyboard-focus` | Mixin applied to a raised button after it's been focused using the keyboard | `{}`
+
+@demo demo/index.html
+*/
+Polymer({
+  _template: template,
+
+  is: 'paper-button',
+
+  behaviors: [PaperButtonBehavior],
+
+  properties: {
+    /**
+     * If true, the button should be styled with a shadow.
+     */
+    raised: {
+      type: Boolean,
+      reflectToAttribute: true,
+      value: false,
+      observer: '_calculateElevation',
+    }
+  },
+
+  _calculateElevation: function() {
+    if (!this.raised) {
+      this._setElevation(0);
+    } else {
+      PaperButtonBehaviorImpl._calculateElevation.apply(this);
+    }
+  }
+
+  /**
+  Fired when the animation finishes.
+  This is useful if you want to wait until
+  the ripple animation finishes to perform some action.
+
+  @event transitionend
+  Event param: {{node: Object}} detail Contains the animated node.
+  */
+});
diff --git a/third_party/polymer/v3_0/components-chromium/paper-input/paper-input-addon-behavior.js b/third_party/polymer/v3_0/components-chromium/paper-input/paper-input-addon-behavior.js
new file mode 100644
index 0000000..1f7e4b5
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/paper-input/paper-input-addon-behavior.js
@@ -0,0 +1,39 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+/**
+ * Use `Polymer.PaperInputAddonBehavior` to implement an add-on for
+ * `<paper-input-container>`. A add-on appears below the input, and may display
+ * information based on the input value and validity such as a character counter
+ * or an error message.
+ * @polymerBehavior
+ */
+export const PaperInputAddonBehavior = {
+  attached: function() {
+    this.fire('addon-attached');
+  },
+
+  /**
+   * The function called by `<paper-input-container>` when the input value or
+   * validity changes.
+   * @param {{
+   *   invalid: boolean,
+   *   inputElement: (Element|undefined),
+   *   value: (string|undefined)
+   * }} state -
+   *     inputElement: The input element.
+   *     value: The input value.
+   *     invalid: True if the input value is invalid.
+   */
+  update: function(state) {}
+
+};
diff --git a/third_party/polymer/v3_0/components-chromium/paper-input/paper-input-container.js b/third_party/polymer/v3_0/components-chromium/paper-input/paper-input-container.js
new file mode 100644
index 0000000..3e724e7
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/paper-input/paper-input-container.js
@@ -0,0 +1,697 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+import '../iron-flex-layout/iron-flex-layout.js';
+import '../paper-styles/default-theme.js';
+import '../paper-styles/typography.js';
+
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+import {dashToCamelCase} from '../polymer/lib/utils/case-map.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+const template = html`
+<custom-style>
+  <style is="custom-style">
+    html {
+      --paper-input-container-shared-input-style: {
+        position: relative; /* to make a stacking context */
+        outline: none;
+        box-shadow: none;
+        padding: 0;
+        margin: 0;
+        width: 100%;
+        max-width: 100%;
+        background: transparent;
+        border: none;
+        color: var(--paper-input-container-input-color, var(--primary-text-color));
+        -webkit-appearance: none;
+        text-align: inherit;
+        vertical-align: var(--paper-input-container-input-align, bottom);
+
+        @apply --paper-font-subhead;
+      };
+    }
+  </style>
+</custom-style>
+`;
+template.setAttribute('style', 'display: none;');
+document.head.appendChild(template.content);
+
+/*
+`<paper-input-container>` is a container for a `<label>`, an `<iron-input>` or
+`<textarea>` and optional add-on elements such as an error message or character
+counter, used to implement Material Design text fields.
+
+For example:
+
+    <paper-input-container>
+      <label slot="label">Your name</label>
+      <iron-input slot="input">
+        <input>
+      </iron-input>
+      // In Polymer 1.0, you would use `<input is="iron-input" slot="input">`
+instead of the above.
+    </paper-input-container>
+
+You can style the nested `<input>` however you want; if you want it to look like
+a Material Design input, you can style it with the
+--paper-input-container-shared-input-style mixin.
+
+Do not wrap `<paper-input-container>` around elements that already include it,
+such as `<paper-input>`. Doing so may cause events to bounce infinitely between
+the container and its contained element.
+
+### Listening for input changes
+
+By default, it listens for changes on the `bind-value` attribute on its children
+nodes and perform tasks such as auto-validating and label styling when the
+`bind-value` changes. You can configure the attribute it listens to with the
+`attr-for-value` attribute.
+
+### Using a custom input element
+
+You can use a custom input element in a `<paper-input-container>`, for example
+to implement a compound input field like a social security number input. The
+custom input element should have the `paper-input-input` class, have a
+`notify:true` value property and optionally implements
+`Polymer.IronValidatableBehavior` if it is validatable.
+
+    <paper-input-container attr-for-value="ssn-value">
+      <label slot="label">Social security number</label>
+      <ssn-input slot="input" class="paper-input-input"></ssn-input>
+    </paper-input-container>
+
+
+If you're using a `<paper-input-container>` imperatively, it's important to make
+sure that you attach its children (the `iron-input` and the optional `label`)
+before you attach the `<paper-input-container>` itself, so that it can be set up
+correctly.
+
+### Validation
+
+If the `auto-validate` attribute is set, the input container will validate the
+input and update the container styling when the input value changes.
+
+### Add-ons
+
+Add-ons are child elements of a `<paper-input-container>` with the `add-on`
+attribute and implements the `Polymer.PaperInputAddonBehavior` behavior. They
+are notified when the input value or validity changes, and may implement
+functionality such as error messages or character counters. They appear at the
+bottom of the input.
+
+### Prefixes and suffixes
+These are child elements of a `<paper-input-container>` with the `prefix`
+or `suffix` attribute, and are displayed inline with the input, before or after.
+
+    <paper-input-container>
+      <div slot="prefix">$</div>
+      <label slot="label">Total</label>
+      <iron-input slot="input">
+        <input>
+      </iron-input>
+      // In Polymer 1.0, you would use `<input is="iron-input" slot="input">`
+instead of the above. <paper-icon-button slot="suffix"
+icon="clear"></paper-icon-button>
+    </paper-input-container>
+
+### Styling
+
+The following custom properties and mixins are available for styling:
+
+Custom property | Description | Default
+----------------|-------------|----------
+`--paper-input-container-color` | Label and underline color when the input is not focused | `--secondary-text-color`
+`--paper-input-container-focus-color` | Label and underline color when the input is focused | `--primary-color`
+`--paper-input-container-invalid-color` | Label and underline color when the input is is invalid | `--error-color`
+`--paper-input-container-input-color` | Input foreground color | `--primary-text-color`
+`--paper-input-container` | Mixin applied to the container | `{}`
+`--paper-input-container-disabled` | Mixin applied to the container when it's disabled | `{}`
+`--paper-input-container-label` | Mixin applied to the label | `{}`
+`--paper-input-container-label-focus` | Mixin applied to the label when the input is focused | `{}`
+`--paper-input-container-label-floating` | Mixin applied to the label when floating | `{}`
+`--paper-input-container-input` | Mixin applied to the input | `{}`
+`--paper-input-container-input-align` | The vertical-align property of the input | `bottom`
+`--paper-input-container-input-disabled` | Mixin applied to the input when the component is disabled | `{}`
+`--paper-input-container-input-focus` | Mixin applied to the input when focused | `{}`
+`--paper-input-container-input-invalid` | Mixin applied to the input when invalid | `{}`
+`--paper-input-container-input-webkit-spinner` | Mixin applied to the webkit spinner | `{}`
+`--paper-input-container-input-webkit-clear` | Mixin applied to the webkit clear button | `{}`
+`--paper-input-container-input-webkit-calendar-picker-indicator` | Mixin applied to the webkit calendar picker indicator | `{}`
+`--paper-input-container-ms-clear` | Mixin applied to the Internet Explorer clear button | `{}`
+`--paper-input-container-underline` | Mixin applied to the underline | `{}`
+`--paper-input-container-underline-focus` | Mixin applied to the underline when the input is focused | `{}`
+`--paper-input-container-underline-disabled` | Mixin applied to the underline when the input is disabled | `{}`
+`--paper-input-prefix` | Mixin applied to the input prefix | `{}`
+`--paper-input-suffix` | Mixin applied to the input suffix | `{}`
+
+This element is `display:block` by default, but you can set the `inline`
+attribute to make it `display:inline-block`.
+*/
+Polymer({
+  _template: html`
+    <style>
+      :host {
+        display: block;
+        padding: 8px 0;
+        @apply --paper-input-container;
+      }
+
+      :host([inline]) {
+        display: inline-block;
+      }
+
+      :host([disabled]) {
+        pointer-events: none;
+        opacity: 0.33;
+
+        @apply --paper-input-container-disabled;
+      }
+
+      :host([hidden]) {
+        display: none !important;
+      }
+
+      [hidden] {
+        display: none !important;
+      }
+
+      .floated-label-placeholder {
+        @apply --paper-font-caption;
+      }
+
+      .underline {
+        height: 2px;
+        position: relative;
+      }
+
+      .focused-line {
+        @apply --layout-fit;
+        border-bottom: 2px solid var(--paper-input-container-focus-color, var(--primary-color));
+
+        -webkit-transform-origin: center center;
+        transform-origin: center center;
+        -webkit-transform: scale3d(0,1,1);
+        transform: scale3d(0,1,1);
+
+        @apply --paper-input-container-underline-focus;
+      }
+
+      .underline.is-highlighted .focused-line {
+        -webkit-transform: none;
+        transform: none;
+        -webkit-transition: -webkit-transform 0.25s;
+        transition: transform 0.25s;
+
+        @apply --paper-transition-easing;
+      }
+
+      .underline.is-invalid .focused-line {
+        border-color: var(--paper-input-container-invalid-color, var(--error-color));
+        -webkit-transform: none;
+        transform: none;
+        -webkit-transition: -webkit-transform 0.25s;
+        transition: transform 0.25s;
+
+        @apply --paper-transition-easing;
+      }
+
+      .unfocused-line {
+        @apply --layout-fit;
+        border-bottom: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
+        @apply --paper-input-container-underline;
+      }
+
+      :host([disabled]) .unfocused-line {
+        border-bottom: 1px dashed;
+        border-color: var(--paper-input-container-color, var(--secondary-text-color));
+        @apply --paper-input-container-underline-disabled;
+      }
+
+      .input-wrapper {
+        @apply --layout-horizontal;
+        @apply --layout-center;
+        position: relative;
+      }
+
+      .input-content {
+        @apply --layout-flex-auto;
+        @apply --layout-relative;
+        max-width: 100%;
+      }
+
+      .input-content ::slotted(label),
+      .input-content ::slotted(.paper-input-label) {
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        font: inherit;
+        color: var(--paper-input-container-color, var(--secondary-text-color));
+        -webkit-transition: -webkit-transform 0.25s, width 0.25s;
+        transition: transform 0.25s, width 0.25s;
+        -webkit-transform-origin: left top;
+        transform-origin: left top;
+        /* Fix for safari not focusing 0-height date/time inputs with -webkit-apperance: none; */
+        min-height: 1px;
+
+        @apply --paper-font-common-nowrap;
+        @apply --paper-font-subhead;
+        @apply --paper-input-container-label;
+        @apply --paper-transition-easing;
+      }
+
+      .input-content.label-is-floating ::slotted(label),
+      .input-content.label-is-floating ::slotted(.paper-input-label) {
+        -webkit-transform: translateY(-75%) scale(0.75);
+        transform: translateY(-75%) scale(0.75);
+
+        /* Since we scale to 75/100 of the size, we actually have 100/75 of the
+        original space now available */
+        width: 133%;
+
+        @apply --paper-input-container-label-floating;
+      }
+
+      :host(:dir(rtl)) .input-content.label-is-floating ::slotted(label),
+      :host(:dir(rtl)) .input-content.label-is-floating ::slotted(.paper-input-label) {
+        right: 0;
+        left: auto;
+        -webkit-transform-origin: right top;
+        transform-origin: right top;
+      }
+
+      .input-content.label-is-highlighted ::slotted(label),
+      .input-content.label-is-highlighted ::slotted(.paper-input-label) {
+        color: var(--paper-input-container-focus-color, var(--primary-color));
+
+        @apply --paper-input-container-label-focus;
+      }
+
+      .input-content.is-invalid ::slotted(label),
+      .input-content.is-invalid ::slotted(.paper-input-label) {
+        color: var(--paper-input-container-invalid-color, var(--error-color));
+      }
+
+      .input-content.label-is-hidden ::slotted(label),
+      .input-content.label-is-hidden ::slotted(.paper-input-label) {
+        visibility: hidden;
+      }
+
+      .input-content ::slotted(input),
+      .input-content ::slotted(iron-input),
+      .input-content ::slotted(textarea),
+      .input-content ::slotted(iron-autogrow-textarea),
+      .input-content ::slotted(.paper-input-input) {
+        @apply --paper-input-container-shared-input-style;
+        /* The apply shim doesn't apply the nested color custom property,
+          so we have to re-apply it here. */
+        color: var(--paper-input-container-input-color, var(--primary-text-color));
+        @apply --paper-input-container-input;
+      }
+
+      .input-content ::slotted(input)::-webkit-outer-spin-button,
+      .input-content ::slotted(input)::-webkit-inner-spin-button {
+        @apply --paper-input-container-input-webkit-spinner;
+      }
+
+      .input-content.focused ::slotted(input),
+      .input-content.focused ::slotted(iron-input),
+      .input-content.focused ::slotted(textarea),
+      .input-content.focused ::slotted(iron-autogrow-textarea),
+      .input-content.focused ::slotted(.paper-input-input) {
+        @apply --paper-input-container-input-focus;
+      }
+
+      .input-content.is-invalid ::slotted(input),
+      .input-content.is-invalid ::slotted(iron-input),
+      .input-content.is-invalid ::slotted(textarea),
+      .input-content.is-invalid ::slotted(iron-autogrow-textarea),
+      .input-content.is-invalid ::slotted(.paper-input-input) {
+        @apply --paper-input-container-input-invalid;
+      }
+
+      .prefix ::slotted(*) {
+        display: inline-block;
+        @apply --paper-font-subhead;
+        @apply --layout-flex-none;
+        @apply --paper-input-prefix;
+      }
+
+      .suffix ::slotted(*) {
+        display: inline-block;
+        @apply --paper-font-subhead;
+        @apply --layout-flex-none;
+
+        @apply --paper-input-suffix;
+      }
+
+      /* Firefox sets a min-width on the input, which can cause layout issues */
+      .input-content ::slotted(input) {
+        min-width: 0;
+      }
+
+      .input-content ::slotted(textarea) {
+        resize: none;
+      }
+
+      .add-on-content {
+        position: relative;
+      }
+
+      .add-on-content.is-invalid ::slotted(*) {
+        color: var(--paper-input-container-invalid-color, var(--error-color));
+      }
+
+      .add-on-content.is-highlighted ::slotted(*) {
+        color: var(--paper-input-container-focus-color, var(--primary-color));
+      }
+    </style>
+
+    <div class="floated-label-placeholder" aria-hidden="true" hidden="[[noLabelFloat]]">&nbsp;</div>
+
+    <div class="input-wrapper">
+      <span class="prefix"><slot name="prefix"></slot></span>
+
+      <div class$="[[_computeInputContentClass(noLabelFloat,alwaysFloatLabel,focused,invalid,_inputHasContent)]]" id="labelAndInputContainer">
+        <slot name="label"></slot>
+        <slot name="input"></slot>
+      </div>
+
+      <span class="suffix"><slot name="suffix"></slot></span>
+    </div>
+
+    <div class$="[[_computeUnderlineClass(focused,invalid)]]">
+      <div class="unfocused-line"></div>
+      <div class="focused-line"></div>
+    </div>
+
+    <div class$="[[_computeAddOnContentClass(focused,invalid)]]">
+      <slot name="add-on"></slot>
+    </div>
+`,
+
+  is: 'paper-input-container',
+
+  properties: {
+    /**
+     * Set to true to disable the floating label. The label disappears when the
+     * input value is not null.
+     */
+    noLabelFloat: {type: Boolean, value: false},
+
+    /**
+     * Set to true to always float the floating label.
+     */
+    alwaysFloatLabel: {type: Boolean, value: false},
+
+    /**
+     * The attribute to listen for value changes on.
+     */
+    attrForValue: {type: String, value: 'bind-value'},
+
+    /**
+     * Set to true to auto-validate the input value when it changes.
+     */
+    autoValidate: {type: Boolean, value: false},
+
+    /**
+     * True if the input is invalid. This property is set automatically when the
+     * input value changes if auto-validating, or when the `iron-input-validate`
+     * event is heard from a child.
+     */
+    invalid: {observer: '_invalidChanged', type: Boolean, value: false},
+
+    /**
+     * True if the input has focus.
+     */
+    focused: {readOnly: true, type: Boolean, value: false, notify: true},
+
+    _addons: {
+      type: Array
+      // do not set a default value here intentionally - it will be initialized
+      // lazily when a distributed child is attached, which may occur before
+      // configuration for this element in polyfill.
+    },
+
+    _inputHasContent: {type: Boolean, value: false},
+
+    _inputSelector:
+        {type: String, value: 'input,iron-input,textarea,.paper-input-input'},
+
+    _boundOnFocus: {
+      type: Function,
+      value: function() {
+        return this._onFocus.bind(this);
+      }
+    },
+
+    _boundOnBlur: {
+      type: Function,
+      value: function() {
+        return this._onBlur.bind(this);
+      }
+    },
+
+    _boundOnInput: {
+      type: Function,
+      value: function() {
+        return this._onInput.bind(this);
+      }
+    },
+
+    _boundValueChanged: {
+      type: Function,
+      value: function() {
+        return this._onValueChanged.bind(this);
+      }
+    }
+  },
+
+  listeners: {
+    'addon-attached': '_onAddonAttached',
+    'iron-input-validate': '_onIronInputValidate'
+  },
+
+  get _valueChangedEvent() {
+    return this.attrForValue + '-changed';
+  },
+
+  get _propertyForValue() {
+    return dashToCamelCase(this.attrForValue);
+  },
+
+  get _inputElement() {
+    return dom(this).querySelector(this._inputSelector);
+  },
+
+  get _inputElementValue() {
+    return this._inputElement[this._propertyForValue] ||
+        this._inputElement.value;
+  },
+
+  ready: function() {
+    // Paper-input treats a value of undefined differently at startup than
+    // the rest of the time (specifically: it does not validate it at startup,
+    // but it does after that. We need to track whether the first time we
+    // encounter the value is basically this first time, so that we can validate
+    // it correctly the rest of the time. See
+    // https://github.com/PolymerElements/paper-input/issues/605
+    this.__isFirstValueUpdate = true;
+    if (!this._addons) {
+      this._addons = [];
+    }
+    this.addEventListener('focus', this._boundOnFocus, true);
+    this.addEventListener('blur', this._boundOnBlur, true);
+  },
+
+  attached: function() {
+    if (this.attrForValue) {
+      this._inputElement.addEventListener(
+          this._valueChangedEvent, this._boundValueChanged);
+    } else {
+      this.addEventListener('input', this._onInput);
+    }
+
+    // Only validate when attached if the input already has a value.
+    if (this._inputElementValue && this._inputElementValue != '') {
+      this._handleValueAndAutoValidate(this._inputElement);
+    } else {
+      this._handleValue(this._inputElement);
+    }
+  },
+
+  /** @private */
+  _onAddonAttached: function(event) {
+    if (!this._addons) {
+      this._addons = [];
+    }
+    var target = event.target;
+    if (this._addons.indexOf(target) === -1) {
+      this._addons.push(target);
+      if (this.isAttached) {
+        this._handleValue(this._inputElement);
+      }
+    }
+  },
+
+  /** @private */
+  _onFocus: function() {
+    this._setFocused(true);
+  },
+
+  /** @private */
+  _onBlur: function() {
+    this._setFocused(false);
+    this._handleValueAndAutoValidate(this._inputElement);
+  },
+
+  /** @private */
+  _onInput: function(event) {
+    this._handleValueAndAutoValidate(event.target);
+  },
+
+  /** @private */
+  _onValueChanged: function(event) {
+    var input = event.target;
+
+    // Paper-input treats a value of undefined differently at startup than
+    // the rest of the time (specifically: it does not validate it at startup,
+    // but it does after that. If this is in fact the bootup case, ignore
+    // validation, just this once.
+    if (this.__isFirstValueUpdate) {
+      this.__isFirstValueUpdate = false;
+      if (input.value === undefined || input.value === '') {
+        return;
+      }
+    }
+
+    this._handleValueAndAutoValidate(event.target);
+  },
+
+  /** @private */
+  _handleValue: function(inputElement) {
+    var value = this._inputElementValue;
+
+    // type="number" hack needed because this.value is empty until it's valid
+    if (value || value === 0 ||
+        (inputElement.type === 'number' && !inputElement.checkValidity())) {
+      this._inputHasContent = true;
+    } else {
+      this._inputHasContent = false;
+    }
+
+    this.updateAddons(
+        {inputElement: inputElement, value: value, invalid: this.invalid});
+  },
+
+  /** @private */
+  _handleValueAndAutoValidate: function(inputElement) {
+    if (this.autoValidate && inputElement) {
+      var valid;
+
+      if (inputElement.validate) {
+        valid = inputElement.validate(this._inputElementValue);
+      } else {
+        valid = inputElement.checkValidity();
+      }
+      this.invalid = !valid;
+    }
+
+    // Call this last to notify the add-ons.
+    this._handleValue(inputElement);
+  },
+
+  /** @private */
+  _onIronInputValidate: function(event) {
+    this.invalid = this._inputElement.invalid;
+  },
+
+  /** @private */
+  _invalidChanged: function() {
+    if (this._addons) {
+      this.updateAddons({invalid: this.invalid});
+    }
+  },
+
+  /**
+   * Call this to update the state of add-ons.
+   * @param {Object} state Add-on state.
+   */
+  updateAddons: function(state) {
+    for (var addon, index = 0; addon = this._addons[index]; index++) {
+      addon.update(state);
+    }
+  },
+
+  /** @private */
+  _computeInputContentClass: function(
+      noLabelFloat, alwaysFloatLabel, focused, invalid, _inputHasContent) {
+    var cls = 'input-content';
+    if (!noLabelFloat) {
+      var label = this.querySelector('label');
+
+      if (alwaysFloatLabel || _inputHasContent) {
+        cls += ' label-is-floating';
+        // If the label is floating, ignore any offsets that may have been
+        // applied from a prefix element.
+        this.$.labelAndInputContainer.style.position = 'static';
+
+        if (invalid) {
+          cls += ' is-invalid';
+        } else if (focused) {
+          cls += ' label-is-highlighted';
+        }
+      } else {
+        // When the label is not floating, it should overlap the input element.
+        if (label) {
+          this.$.labelAndInputContainer.style.position = 'relative';
+        }
+        if (invalid) {
+          cls += ' is-invalid';
+        }
+      }
+    } else {
+      if (_inputHasContent) {
+        cls += ' label-is-hidden';
+      }
+      if (invalid) {
+        cls += ' is-invalid';
+      }
+    }
+    if (focused) {
+      cls += ' focused';
+    }
+    return cls;
+  },
+
+  /** @private */
+  _computeUnderlineClass: function(focused, invalid) {
+    var cls = 'underline';
+    if (invalid) {
+      cls += ' is-invalid';
+    } else if (focused) {
+      cls += ' is-highlighted'
+    }
+    return cls;
+  },
+
+  /** @private */
+  _computeAddOnContentClass: function(focused, invalid) {
+    var cls = 'add-on-content';
+    if (invalid) {
+      cls += ' is-invalid';
+    } else if (focused) {
+      cls += ' is-highlighted'
+    }
+    return cls;
+  }
+});
diff --git a/third_party/polymer/v3_0/components-chromium/paper-input/paper-input-error.js b/third_party/polymer/v3_0/components-chromium/paper-input/paper-input-error.js
new file mode 100644
index 0000000..afd9adc
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/paper-input/paper-input-error.js
@@ -0,0 +1,107 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+import '../paper-styles/default-theme.js';
+import '../paper-styles/typography.js';
+
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+
+import {PaperInputAddonBehavior} from './paper-input-addon-behavior.js';
+
+/*
+`<paper-input-error>` is an error message for use with
+`<paper-input-container>`. The error is displayed when the
+`<paper-input-container>` is `invalid`.
+
+    <paper-input-container>
+      <input pattern="[0-9]*">
+      <paper-input-error slot="add-on">Only numbers are
+allowed!</paper-input-error>
+    </paper-input-container>
+
+### Styling
+
+The following custom properties and mixins are available for styling:
+
+Custom property | Description | Default
+----------------|-------------|----------
+`--paper-input-container-invalid-color` | The foreground color of the error | `--error-color`
+`--paper-input-error` | Mixin applied to the error | `{}`
+*/
+Polymer({
+  _template: html`
+    <style>
+      :host {
+        display: inline-block;
+        visibility: hidden;
+
+        color: var(--paper-input-container-invalid-color, var(--error-color));
+
+        @apply --paper-font-caption;
+        @apply --paper-input-error;
+        position: absolute;
+        left:0;
+        right:0;
+      }
+
+      :host([invalid]) {
+        visibility: visible;
+      }
+
+      #a11yWrapper {
+        visibility: hidden;
+      }
+
+      :host([invalid]) #a11yWrapper {
+        visibility: visible;
+      }
+    </style>
+
+    <!--
+    If the paper-input-error element is directly referenced by an
+    \`aria-describedby\` attribute, such as when used as a paper-input add-on,
+    then applying \`visibility: hidden;\` to the paper-input-error element itself
+    does not hide the error.
+
+    For more information, see:
+    https://www.w3.org/TR/accname-1.1/#mapping_additional_nd_description
+    -->
+    <div id="a11yWrapper">
+      <slot></slot>
+    </div>
+`,
+
+  is: 'paper-input-error',
+  behaviors: [PaperInputAddonBehavior],
+
+  properties: {
+    /**
+     * True if the error is showing.
+     */
+    invalid: {readOnly: true, reflectToAttribute: true, type: Boolean}
+  },
+
+  /**
+   * This overrides the update function in PaperInputAddonBehavior.
+   * @param {{
+   *   inputElement: (Element|undefined),
+   *   value: (string|undefined),
+   *   invalid: boolean
+   * }} state -
+   *     inputElement: The input element.
+   *     value: The input value.
+   *     invalid: True if the input value is invalid.
+   */
+  update: function(state) {
+    this._setInvalid(state.invalid);
+  }
+});
diff --git a/third_party/polymer/v3_0/components-chromium/paper-progress/paper-progress.js b/third_party/polymer/v3_0/components-chromium/paper-progress/paper-progress.js
new file mode 100644
index 0000000..fdb26f7
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/paper-progress/paper-progress.js
@@ -0,0 +1,342 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+import '../iron-flex-layout/iron-flex-layout.js';
+import '../paper-styles/color.js';
+
+import {IronRangeBehavior} from '../iron-range-behavior/iron-range-behavior.js';
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+
+/**
+Material design: [Progress &
+activity](https://www.google.com/design/spec/components/progress-activity.html)
+
+The progress bars are for situations where the percentage completed can be
+determined. They give users a quick sense of how much longer an operation
+will take.
+
+Example:
+
+    <paper-progress value="10"></paper-progress>
+
+There is also a secondary progress which is useful for displaying intermediate
+progress, such as the buffer level during a streaming playback progress bar.
+
+Example:
+
+    <paper-progress value="10" secondary-progress="30"></paper-progress>
+
+### Styling progress bar:
+
+To change the active progress bar color:
+
+    paper-progress {
+       --paper-progress-active-color: #e91e63;
+    }
+
+To change the secondary progress bar color:
+
+    paper-progress {
+      --paper-progress-secondary-color: #f8bbd0;
+    }
+
+To change the progress bar background color:
+
+    paper-progress {
+      --paper-progress-container-color: #64ffda;
+    }
+
+Add the class `transiting` to a paper-progress to animate the progress bar when
+the value changed. You can also customize the transition:
+
+    paper-progress {
+      --paper-progress-transition-duration: 0.08s;
+      --paper-progress-transition-timing-function: ease;
+      --paper-progress-transition-delay: 0s;
+    }
+
+To change the duration of the indeterminate cycle:
+
+    paper-progress {
+      --paper-progress-indeterminate-cycle-duration: 2s;
+    }
+
+The following mixins are available for styling:
+
+Custom property | Description | Default
+----------------|-------------|---------
+`--paper-progress-container` | Mixin applied to container | `{}`
+`--paper-progress-transition-duration` | Duration of the transition | `0.08s`
+`--paper-progress-transition-timing-function` | The timing function for the transition | `ease`
+`--paper-progress-transition-delay` | delay for the transition | `0s`
+`--paper-progress-container-color` | Color of the container | `--google-grey-300`
+`--paper-progress-active-color` | The color of the active bar | `--google-green-500`
+`--paper-progress-secondary-color` | The color of the secondary bar | `--google-green-100`
+`--paper-progress-disabled-active-color` | The color of the active bar if disabled | `--google-grey-500`
+`--paper-progress-disabled-secondary-color` | The color of the secondary bar if disabled  | `--google-grey-300`
+`--paper-progress-height` | The height of the progress bar | `4px`
+`--paper-progress-indeterminate-cycle-duration` | Duration of an indeterminate cycle | `2s`
+
+@group Paper Elements
+@element paper-progress
+@demo demo/index.html
+*/
+Polymer({
+  _template: html`
+    <style>
+      :host {
+        display: block;
+        width: 200px;
+        position: relative;
+        overflow: hidden;
+      }
+
+      :host([hidden]), [hidden] {
+        display: none !important;
+      }
+
+      #progressContainer {
+        @apply --paper-progress-container;
+        position: relative;
+      }
+
+      #progressContainer,
+      /* the stripe for the indeterminate animation*/
+      .indeterminate::after {
+        height: var(--paper-progress-height, 4px);
+      }
+
+      #primaryProgress,
+      #secondaryProgress,
+      .indeterminate::after {
+        @apply --layout-fit;
+      }
+
+      #progressContainer,
+      .indeterminate::after {
+        background: var(--paper-progress-container-color, var(--google-grey-300));
+      }
+
+      :host(.transiting) #primaryProgress,
+      :host(.transiting) #secondaryProgress {
+        -webkit-transition-property: -webkit-transform;
+        transition-property: transform;
+
+        /* Duration */
+        -webkit-transition-duration: var(--paper-progress-transition-duration, 0.08s);
+        transition-duration: var(--paper-progress-transition-duration, 0.08s);
+
+        /* Timing function */
+        -webkit-transition-timing-function: var(--paper-progress-transition-timing-function, ease);
+        transition-timing-function: var(--paper-progress-transition-timing-function, ease);
+
+        /* Delay */
+        -webkit-transition-delay: var(--paper-progress-transition-delay, 0s);
+        transition-delay: var(--paper-progress-transition-delay, 0s);
+      }
+
+      #primaryProgress,
+      #secondaryProgress {
+        @apply --layout-fit;
+        -webkit-transform-origin: left center;
+        transform-origin: left center;
+        -webkit-transform: scaleX(0);
+        transform: scaleX(0);
+        will-change: transform;
+      }
+
+      #primaryProgress {
+        background: var(--paper-progress-active-color, var(--google-green-500));
+      }
+
+      #secondaryProgress {
+        background: var(--paper-progress-secondary-color, var(--google-green-100));
+      }
+
+      :host([disabled]) #primaryProgress {
+        background: var(--paper-progress-disabled-active-color, var(--google-grey-500));
+      }
+
+      :host([disabled]) #secondaryProgress {
+        background: var(--paper-progress-disabled-secondary-color, var(--google-grey-300));
+      }
+
+      :host(:not([disabled])) #primaryProgress.indeterminate {
+        -webkit-transform-origin: right center;
+        transform-origin: right center;
+        -webkit-animation: indeterminate-bar var(--paper-progress-indeterminate-cycle-duration, 2s) linear infinite;
+        animation: indeterminate-bar var(--paper-progress-indeterminate-cycle-duration, 2s) linear infinite;
+      }
+
+      :host(:not([disabled])) #primaryProgress.indeterminate::after {
+        content: "";
+        -webkit-transform-origin: center center;
+        transform-origin: center center;
+
+        -webkit-animation: indeterminate-splitter var(--paper-progress-indeterminate-cycle-duration, 2s) linear infinite;
+        animation: indeterminate-splitter var(--paper-progress-indeterminate-cycle-duration, 2s) linear infinite;
+      }
+
+      @-webkit-keyframes indeterminate-bar {
+        0% {
+          -webkit-transform: scaleX(1) translateX(-100%);
+        }
+        50% {
+          -webkit-transform: scaleX(1) translateX(0%);
+        }
+        75% {
+          -webkit-transform: scaleX(1) translateX(0%);
+          -webkit-animation-timing-function: cubic-bezier(.28,.62,.37,.91);
+        }
+        100% {
+          -webkit-transform: scaleX(0) translateX(0%);
+        }
+      }
+
+      @-webkit-keyframes indeterminate-splitter {
+        0% {
+          -webkit-transform: scaleX(.75) translateX(-125%);
+        }
+        30% {
+          -webkit-transform: scaleX(.75) translateX(-125%);
+          -webkit-animation-timing-function: cubic-bezier(.42,0,.6,.8);
+        }
+        90% {
+          -webkit-transform: scaleX(.75) translateX(125%);
+        }
+        100% {
+          -webkit-transform: scaleX(.75) translateX(125%);
+        }
+      }
+
+      @keyframes indeterminate-bar {
+        0% {
+          transform: scaleX(1) translateX(-100%);
+        }
+        50% {
+          transform: scaleX(1) translateX(0%);
+        }
+        75% {
+          transform: scaleX(1) translateX(0%);
+          animation-timing-function: cubic-bezier(.28,.62,.37,.91);
+        }
+        100% {
+          transform: scaleX(0) translateX(0%);
+        }
+      }
+
+      @keyframes indeterminate-splitter {
+        0% {
+          transform: scaleX(.75) translateX(-125%);
+        }
+        30% {
+          transform: scaleX(.75) translateX(-125%);
+          animation-timing-function: cubic-bezier(.42,0,.6,.8);
+        }
+        90% {
+          transform: scaleX(.75) translateX(125%);
+        }
+        100% {
+          transform: scaleX(.75) translateX(125%);
+        }
+      }
+    </style>
+
+    <div id="progressContainer">
+      <div id="secondaryProgress" hidden\$="[[_hideSecondaryProgress(secondaryRatio)]]"></div>
+      <div id="primaryProgress"></div>
+    </div>
+`,
+
+  is: 'paper-progress',
+  behaviors: [IronRangeBehavior],
+
+  properties: {
+    /**
+     * The number that represents the current secondary progress.
+     */
+    secondaryProgress: {type: Number, value: 0},
+
+    /**
+     * The secondary ratio
+     */
+    secondaryRatio: {type: Number, value: 0, readOnly: true},
+
+    /**
+     * Use an indeterminate progress indicator.
+     */
+    indeterminate:
+        {type: Boolean, value: false, observer: '_toggleIndeterminate'},
+
+    /**
+     * True if the progress is disabled.
+     */
+    disabled: {
+      type: Boolean,
+      value: false,
+      reflectToAttribute: true,
+      observer: '_disabledChanged'
+    }
+  },
+
+  observers:
+      ['_progressChanged(secondaryProgress, value, min, max, indeterminate)'],
+
+  hostAttributes: {role: 'progressbar'},
+
+  _toggleIndeterminate: function(indeterminate) {
+    // If we use attribute/class binding, the animation sometimes doesn't
+    // translate properly on Safari 7.1. So instead, we toggle the class here in
+    // the update method.
+    this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress);
+  },
+
+  _transformProgress: function(progress, ratio) {
+    var transform = 'scaleX(' + (ratio / 100) + ')';
+    progress.style.transform = progress.style.webkitTransform = transform;
+  },
+
+  _mainRatioChanged: function(ratio) {
+    this._transformProgress(this.$.primaryProgress, ratio);
+  },
+
+  _progressChanged: function(
+      secondaryProgress, value, min, max, indeterminate) {
+    secondaryProgress = this._clampValue(secondaryProgress);
+    value = this._clampValue(value);
+
+    var secondaryRatio = this._calcRatio(secondaryProgress) * 100;
+    var mainRatio = this._calcRatio(value) * 100;
+
+    this._setSecondaryRatio(secondaryRatio);
+    this._transformProgress(this.$.secondaryProgress, secondaryRatio);
+    this._transformProgress(this.$.primaryProgress, mainRatio);
+
+    this.secondaryProgress = secondaryProgress;
+
+    if (indeterminate) {
+      this.removeAttribute('aria-valuenow');
+    } else {
+      this.setAttribute('aria-valuenow', value);
+    }
+    this.setAttribute('aria-valuemin', min);
+    this.setAttribute('aria-valuemax', max);
+  },
+
+  _disabledChanged: function(disabled) {
+    this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
+  },
+
+  _hideSecondaryProgress: function(secondaryRatio) {
+    return secondaryRatio === 0;
+  }
+});
diff --git a/third_party/polymer/v3_0/components-chromium/paper-ripple/paper-ripple.js b/third_party/polymer/v3_0/components-chromium/paper-ripple/paper-ripple.js
new file mode 100644
index 0000000..6969cd7
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/paper-ripple/paper-ripple.js
@@ -0,0 +1,736 @@
+/**
+@license
+Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+import {IronA11yKeysBehavior} from '../iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {dom} from '../polymer/lib/legacy/polymer.dom.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+
+var Utility = {
+  distance: function(x1, y1, x2, y2) {
+    var xDelta = (x1 - x2);
+    var yDelta = (y1 - y2);
+
+    return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
+  },
+
+  now: window.performance && window.performance.now ?
+      window.performance.now.bind(window.performance) :
+      Date.now
+};
+
+/**
+ * @param {HTMLElement} element
+ * @constructor
+ */
+function ElementMetrics(element) {
+  this.element = element;
+  this.width = this.boundingRect.width;
+  this.height = this.boundingRect.height;
+
+  this.size = Math.max(this.width, this.height);
+}
+
+ElementMetrics.prototype = {
+  get boundingRect() {
+    return this.element.getBoundingClientRect();
+  },
+
+  furthestCornerDistanceFrom: function(x, y) {
+    var topLeft = Utility.distance(x, y, 0, 0);
+    var topRight = Utility.distance(x, y, this.width, 0);
+    var bottomLeft = Utility.distance(x, y, 0, this.height);
+    var bottomRight = Utility.distance(x, y, this.width, this.height);
+
+    return Math.max(topLeft, topRight, bottomLeft, bottomRight);
+  }
+};
+
+/**
+ * @param {HTMLElement} element
+ * @constructor
+ */
+function Ripple(element) {
+  this.element = element;
+  this.color = window.getComputedStyle(element).color;
+
+  this.wave = document.createElement('div');
+  this.waveContainer = document.createElement('div');
+  this.wave.style.backgroundColor = this.color;
+  this.wave.classList.add('wave');
+  this.waveContainer.classList.add('wave-container');
+  dom(this.waveContainer).appendChild(this.wave);
+
+  this.resetInteractionState();
+}
+
+Ripple.MAX_RADIUS = 300;
+
+Ripple.prototype = {
+  get recenters() {
+    return this.element.recenters;
+  },
+
+  get center() {
+    return this.element.center;
+  },
+
+  get mouseDownElapsed() {
+    var elapsed;
+
+    if (!this.mouseDownStart) {
+      return 0;
+    }
+
+    elapsed = Utility.now() - this.mouseDownStart;
+
+    if (this.mouseUpStart) {
+      elapsed -= this.mouseUpElapsed;
+    }
+
+    return elapsed;
+  },
+
+  get mouseUpElapsed() {
+    return this.mouseUpStart ? Utility.now() - this.mouseUpStart : 0;
+  },
+
+  get mouseDownElapsedSeconds() {
+    return this.mouseDownElapsed / 1000;
+  },
+
+  get mouseUpElapsedSeconds() {
+    return this.mouseUpElapsed / 1000;
+  },
+
+  get mouseInteractionSeconds() {
+    return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
+  },
+
+  get initialOpacity() {
+    return this.element.initialOpacity;
+  },
+
+  get opacityDecayVelocity() {
+    return this.element.opacityDecayVelocity;
+  },
+
+  get radius() {
+    var width2 = this.containerMetrics.width * this.containerMetrics.width;
+    var height2 = this.containerMetrics.height * this.containerMetrics.height;
+    var waveRadius =
+        Math.min(Math.sqrt(width2 + height2), Ripple.MAX_RADIUS) * 1.1 + 5;
+
+    var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS);
+    var timeNow = this.mouseInteractionSeconds / duration;
+    var size = waveRadius * (1 - Math.pow(80, -timeNow));
+
+    return Math.abs(size);
+  },
+
+  get opacity() {
+    if (!this.mouseUpStart) {
+      return this.initialOpacity;
+    }
+
+    return Math.max(
+        0,
+        this.initialOpacity -
+            this.mouseUpElapsedSeconds * this.opacityDecayVelocity);
+  },
+
+  get outerOpacity() {
+    // Linear increase in background opacity, capped at the opacity
+    // of the wavefront (waveOpacity).
+    var outerOpacity = this.mouseUpElapsedSeconds * 0.3;
+    var waveOpacity = this.opacity;
+
+    return Math.max(0, Math.min(outerOpacity, waveOpacity));
+  },
+
+  get isOpacityFullyDecayed() {
+    return this.opacity < 0.01 &&
+        this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
+  },
+
+  get isRestingAtMaxRadius() {
+    return this.opacity >= this.initialOpacity &&
+        this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
+  },
+
+  get isAnimationComplete() {
+    return this.mouseUpStart ? this.isOpacityFullyDecayed :
+                               this.isRestingAtMaxRadius;
+  },
+
+  get translationFraction() {
+    return Math.min(
+        1, this.radius / this.containerMetrics.size * 2 / Math.sqrt(2));
+  },
+
+  get xNow() {
+    if (this.xEnd) {
+      return this.xStart + this.translationFraction * (this.xEnd - this.xStart);
+    }
+
+    return this.xStart;
+  },
+
+  get yNow() {
+    if (this.yEnd) {
+      return this.yStart + this.translationFraction * (this.yEnd - this.yStart);
+    }
+
+    return this.yStart;
+  },
+
+  get isMouseDown() {
+    return this.mouseDownStart && !this.mouseUpStart;
+  },
+
+  resetInteractionState: function() {
+    this.maxRadius = 0;
+    this.mouseDownStart = 0;
+    this.mouseUpStart = 0;
+
+    this.xStart = 0;
+    this.yStart = 0;
+    this.xEnd = 0;
+    this.yEnd = 0;
+    this.slideDistance = 0;
+
+    this.containerMetrics = new ElementMetrics(this.element);
+  },
+
+  draw: function() {
+    var scale;
+    var dx;
+    var dy;
+
+    this.wave.style.opacity = this.opacity;
+
+    scale = this.radius / (this.containerMetrics.size / 2);
+    dx = this.xNow - (this.containerMetrics.width / 2);
+    dy = this.yNow - (this.containerMetrics.height / 2);
+
+
+    // 2d transform for safari because of border-radius and overflow:hidden
+    // clipping bug. https://bugs.webkit.org/show_bug.cgi?id=98538
+    this.waveContainer.style.webkitTransform =
+        'translate(' + dx + 'px, ' + dy + 'px)';
+    this.waveContainer.style.transform =
+        'translate3d(' + dx + 'px, ' + dy + 'px, 0)';
+    this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
+    this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
+  },
+
+  /** @param {Event=} event */
+  downAction: function(event) {
+    var xCenter = this.containerMetrics.width / 2;
+    var yCenter = this.containerMetrics.height / 2;
+
+    this.resetInteractionState();
+    this.mouseDownStart = Utility.now();
+
+    if (this.center) {
+      this.xStart = xCenter;
+      this.yStart = yCenter;
+      this.slideDistance =
+          Utility.distance(this.xStart, this.yStart, this.xEnd, this.yEnd);
+    } else {
+      this.xStart = event ?
+          event.detail.x - this.containerMetrics.boundingRect.left :
+          this.containerMetrics.width / 2;
+      this.yStart = event ?
+          event.detail.y - this.containerMetrics.boundingRect.top :
+          this.containerMetrics.height / 2;
+    }
+
+    if (this.recenters) {
+      this.xEnd = xCenter;
+      this.yEnd = yCenter;
+      this.slideDistance =
+          Utility.distance(this.xStart, this.yStart, this.xEnd, this.yEnd);
+    }
+
+    this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(
+        this.xStart, this.yStart);
+
+    this.waveContainer.style.top =
+        (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px';
+    this.waveContainer.style.left =
+        (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px';
+
+    this.waveContainer.style.width = this.containerMetrics.size + 'px';
+    this.waveContainer.style.height = this.containerMetrics.size + 'px';
+  },
+
+  /** @param {Event=} event */
+  upAction: function(event) {
+    if (!this.isMouseDown) {
+      return;
+    }
+
+    this.mouseUpStart = Utility.now();
+  },
+
+  remove: function() {
+    dom(this.waveContainer.parentNode).removeChild(this.waveContainer);
+  }
+};
+
+/**
+Material design: [Surface
+reaction](https://www.google.com/design/spec/animation/responsive-interaction.html#responsive-interaction-surface-reaction)
+
+`paper-ripple` provides a visual effect that other paper elements can
+use to simulate a rippling effect emanating from the point of contact.  The
+effect can be visualized as a concentric circle with motion.
+
+Example:
+
+    <div style="position:relative">
+      <paper-ripple></paper-ripple>
+    </div>
+
+Note, it's important that the parent container of the ripple be relative
+position, otherwise the ripple will emanate outside of the desired container.
+
+`paper-ripple` listens to "mousedown" and "mouseup" events so it would display
+ripple effect when touches on it.  You can also defeat the default behavior and
+manually route the down and up actions to the ripple element.  Note that it is
+important if you call `downAction()` you will have to make sure to call
+`upAction()` so that `paper-ripple` would end the animation loop.
+
+Example:
+
+    <paper-ripple id="ripple" style="pointer-events: none;"></paper-ripple>
+    ...
+    downAction: function(e) {
+      this.$.ripple.downAction(e.detail);
+    },
+    upAction: function(e) {
+      this.$.ripple.upAction();
+    }
+
+Styling ripple effect:
+
+  Use CSS color property to style the ripple:
+
+    paper-ripple {
+      color: #4285f4;
+    }
+
+  Note that CSS color property is inherited so it is not required to set it on
+  the `paper-ripple` element directly.
+
+By default, the ripple is centered on the point of contact.  Apply the
+`recenters` attribute to have the ripple grow toward the center of its
+container.
+
+    <paper-ripple recenters></paper-ripple>
+
+You can also  center the ripple inside its container from the start.
+
+    <paper-ripple center></paper-ripple>
+
+Apply `circle` class to make the rippling effect within a circle.
+
+    <paper-ripple class="circle"></paper-ripple>
+
+@group Paper Elements
+@element paper-ripple
+@hero hero.svg
+@demo demo/index.html
+*/
+Polymer({
+  _template: html`
+    <style>
+      :host {
+        display: block;
+        position: absolute;
+        border-radius: inherit;
+        overflow: hidden;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+
+        /* See PolymerElements/paper-behaviors/issues/34. On non-Chrome browsers,
+         * creating a node (with a position:absolute) in the middle of an event
+         * handler "interrupts" that event handler (which happens when the
+         * ripple is created on demand) */
+        pointer-events: none;
+      }
+
+      :host([animating]) {
+        /* This resolves a rendering issue in Chrome (as of 40) where the
+           ripple is not properly clipped by its parent (which may have
+           rounded corners). See: http://jsbin.com/temexa/4
+
+           Note: We only apply this style conditionally. Otherwise, the browser
+           will create a new compositing layer for every ripple element on the
+           page, and that would be bad. */
+        -webkit-transform: translate(0, 0);
+        transform: translate3d(0, 0, 0);
+      }
+
+      #background,
+      #waves,
+      .wave-container,
+      .wave {
+        pointer-events: none;
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+      }
+
+      #background,
+      .wave {
+        opacity: 0;
+      }
+
+      #waves,
+      .wave {
+        overflow: hidden;
+      }
+
+      .wave-container,
+      .wave {
+        border-radius: 50%;
+      }
+
+      :host(.circle) #background,
+      :host(.circle) #waves {
+        border-radius: 50%;
+      }
+
+      :host(.circle) .wave-container {
+        overflow: hidden;
+      }
+    </style>
+
+    <div id="background"></div>
+    <div id="waves"></div>
+`,
+
+  is: 'paper-ripple',
+  behaviors: [IronA11yKeysBehavior],
+
+  properties: {
+    /**
+     * The initial opacity set on the wave.
+     *
+     * @attribute initialOpacity
+     * @type number
+     * @default 0.25
+     */
+    initialOpacity: {type: Number, value: 0.25},
+
+    /**
+     * How fast (opacity per second) the wave fades out.
+     *
+     * @attribute opacityDecayVelocity
+     * @type number
+     * @default 0.8
+     */
+    opacityDecayVelocity: {type: Number, value: 0.8},
+
+    /**
+     * If true, ripples will exhibit a gravitational pull towards
+     * the center of their container as they fade away.
+     *
+     * @attribute recenters
+     * @type boolean
+     * @default false
+     */
+    recenters: {type: Boolean, value: false},
+
+    /**
+     * If true, ripples will center inside its container
+     *
+     * @attribute recenters
+     * @type boolean
+     * @default false
+     */
+    center: {type: Boolean, value: false},
+
+    /**
+     * A list of the visual ripples.
+     *
+     * @attribute ripples
+     * @type Array
+     * @default []
+     */
+    ripples: {
+      type: Array,
+      value: function() {
+        return [];
+      }
+    },
+
+    /**
+     * True when there are visible ripples animating within the
+     * element.
+     */
+    animating:
+        {type: Boolean, readOnly: true, reflectToAttribute: true, value: false},
+
+    /**
+     * If true, the ripple will remain in the "down" state until `holdDown`
+     * is set to false again.
+     */
+    holdDown: {type: Boolean, value: false, observer: '_holdDownChanged'},
+
+    /**
+     * If true, the ripple will not generate a ripple effect
+     * via pointer interaction.
+     * Calling ripple's imperative api like `simulatedRipple` will
+     * still generate the ripple effect.
+     */
+    noink: {type: Boolean, value: false},
+
+    _animating: {type: Boolean},
+
+    _boundAnimate: {
+      type: Function,
+      value: function() {
+        return this.animate.bind(this);
+      }
+    }
+  },
+
+  get target() {
+    return this.keyEventTarget;
+  },
+
+  /**
+   * @type {!Object}
+   */
+  keyBindings: {
+    'enter:keydown': '_onEnterKeydown',
+    'space:keydown': '_onSpaceKeydown',
+    'space:keyup': '_onSpaceKeyup'
+  },
+
+  attached: function() {
+    // Set up a11yKeysBehavior to listen to key events on the target,
+    // so that space and enter activate the ripple even if the target doesn't
+    // handle key events. The key handlers deal with `noink` themselves.
+    if (this.parentNode.nodeType == 11) {  // DOCUMENT_FRAGMENT_NODE
+      this.keyEventTarget = dom(this).getOwnerRoot().host;
+    } else {
+      this.keyEventTarget = this.parentNode;
+    }
+    var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget);
+    this.listen(keyEventTarget, 'up', 'uiUpAction');
+    this.listen(keyEventTarget, 'down', 'uiDownAction');
+  },
+
+  detached: function() {
+    this.unlisten(this.keyEventTarget, 'up', 'uiUpAction');
+    this.unlisten(this.keyEventTarget, 'down', 'uiDownAction');
+    this.keyEventTarget = null;
+  },
+
+  get shouldKeepAnimating() {
+    for (var index = 0; index < this.ripples.length; ++index) {
+      if (!this.ripples[index].isAnimationComplete) {
+        return true;
+      }
+    }
+
+    return false;
+  },
+
+  simulatedRipple: function() {
+    this.downAction(null);
+
+    // Please see polymer/polymer#1305
+    this.async(function() {
+      this.upAction();
+    }, 1);
+  },
+
+  /**
+   * Provokes a ripple down effect via a UI event,
+   * respecting the `noink` property.
+   * @param {Event=} event
+   */
+  uiDownAction: function(event) {
+    if (!this.noink) {
+      this.downAction(event);
+    }
+  },
+
+  /**
+   * Provokes a ripple down effect via a UI event,
+   * *not* respecting the `noink` property.
+   * @param {Event=} event
+   */
+  downAction: function(event) {
+    if (this.holdDown && this.ripples.length > 0) {
+      return;
+    }
+
+    var ripple = this.addRipple();
+
+    ripple.downAction(event);
+
+    if (!this._animating) {
+      this._animating = true;
+      this.animate();
+    }
+  },
+
+  /**
+   * Provokes a ripple up effect via a UI event,
+   * respecting the `noink` property.
+   * @param {Event=} event
+   */
+  uiUpAction: function(event) {
+    if (!this.noink) {
+      this.upAction(event);
+    }
+  },
+
+  /**
+   * Provokes a ripple up effect via a UI event,
+   * *not* respecting the `noink` property.
+   * @param {Event=} event
+   */
+  upAction: function(event) {
+    if (this.holdDown) {
+      return;
+    }
+
+    this.ripples.forEach(function(ripple) {
+      ripple.upAction(event);
+    });
+
+    this._animating = true;
+    this.animate();
+  },
+
+  onAnimationComplete: function() {
+    this._animating = false;
+    this.$.background.style.backgroundColor = null;
+    this.fire('transitionend');
+  },
+
+  addRipple: function() {
+    var ripple = new Ripple(this);
+
+    dom(this.$.waves).appendChild(ripple.waveContainer);
+    this.$.background.style.backgroundColor = ripple.color;
+    this.ripples.push(ripple);
+
+    this._setAnimating(true);
+
+    return ripple;
+  },
+
+  removeRipple: function(ripple) {
+    var rippleIndex = this.ripples.indexOf(ripple);
+
+    if (rippleIndex < 0) {
+      return;
+    }
+
+    this.ripples.splice(rippleIndex, 1);
+
+    ripple.remove();
+
+    if (!this.ripples.length) {
+      this._setAnimating(false);
+    }
+  },
+
+  /**
+   * Deprecated. Please use animateRipple() instead.
+   *
+   * This method name conflicts with Element#animate().
+   * https://developer.mozilla.org/en-US/docs/Web/API/Element/animate.
+   *
+   * @suppress {checkTypes}
+   */
+  animate: function() {
+    if (!this._animating) {
+      return;
+    }
+    var index;
+    var ripple;
+
+    for (index = 0; index < this.ripples.length; ++index) {
+      ripple = this.ripples[index];
+
+      ripple.draw();
+
+      this.$.background.style.opacity = ripple.outerOpacity;
+
+      if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) {
+        this.removeRipple(ripple);
+      }
+    }
+
+    if (!this.shouldKeepAnimating && this.ripples.length === 0) {
+      this.onAnimationComplete();
+    } else {
+      window.requestAnimationFrame(this._boundAnimate);
+    }
+  },
+
+  /**
+   * An alias for animate() whose name does not conflict with the platform
+   * Element.animate() method.
+   */
+  animateRipple: function() {
+    return this.animate();
+  },
+
+  _onEnterKeydown: function() {
+    this.uiDownAction();
+    this.async(this.uiUpAction, 1);
+  },
+
+  _onSpaceKeydown: function() {
+    this.uiDownAction();
+  },
+
+  _onSpaceKeyup: function() {
+    this.uiUpAction();
+  },
+
+  // note: holdDown does not respect noink since it can be a focus based
+  // effect.
+  _holdDownChanged: function(newVal, oldVal) {
+    if (oldVal === undefined) {
+      return;
+    }
+    if (newVal) {
+      this.downAction();
+    } else {
+      this.upAction();
+    }
+  }
+
+  /**
+  Fired when the animation finishes.
+  This is useful if you want to wait until
+  the ripple animation finishes to perform some action.
+
+  @event transitionend
+  @param {{node: Object}} detail Contains the animated node.
+  */
+});
diff --git a/third_party/polymer/v3_0/components-chromium/paper-spinner/paper-spinner-behavior.js b/third_party/polymer/v3_0/components-chromium/paper-spinner/paper-spinner-behavior.js
new file mode 100644
index 0000000..ebc060e
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/paper-spinner/paper-spinner-behavior.js
@@ -0,0 +1,73 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+
+/** @polymerBehavior */
+export const PaperSpinnerBehavior = {
+
+  properties: {
+    /**
+     * Displays the spinner.
+     */
+    active: {
+      type: Boolean,
+      value: false,
+      reflectToAttribute: true,
+      observer: '__activeChanged'
+    },
+
+    /**
+     * Alternative text content for accessibility support.
+     * If alt is present, it will add an aria-label whose content matches alt
+     * when active. If alt is not present, it will default to 'loading' as the
+     * alt value.
+     */
+    alt: {type: String, value: 'loading', observer: '__altChanged'},
+
+    __coolingDown: {type: Boolean, value: false}
+  },
+
+  __computeContainerClasses: function(active, coolingDown) {
+    return [
+      active || coolingDown ? 'active' : '',
+      coolingDown ? 'cooldown' : ''
+    ].join(' ');
+  },
+
+  __activeChanged: function(active, old) {
+    this.__setAriaHidden(!active);
+    this.__coolingDown = !active && old;
+  },
+
+  __altChanged: function(alt) {
+    // user-provided `aria-label` takes precedence over prototype default
+    if (alt === 'loading') {
+      this.alt = this.getAttribute('aria-label') || alt;
+    } else {
+      this.__setAriaHidden(alt === '');
+      this.setAttribute('aria-label', alt);
+    }
+  },
+
+  __setAriaHidden: function(hidden) {
+    var attr = 'aria-hidden';
+    if (hidden) {
+      this.setAttribute(attr, 'true');
+    } else {
+      this.removeAttribute(attr);
+    }
+  },
+
+  __reset: function() {
+    this.active = false;
+    this.__coolingDown = false;
+  }
+};
diff --git a/third_party/polymer/v3_0/components-chromium/paper-spinner/paper-spinner-lite.js b/third_party/polymer/v3_0/components-chromium/paper-spinner/paper-spinner-lite.js
new file mode 100644
index 0000000..740be37
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/paper-spinner/paper-spinner-lite.js
@@ -0,0 +1,75 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+import '../polymer/polymer-legacy.js';
+import '../paper-styles/color.js';
+import './paper-spinner-styles.js';
+
+import {Polymer} from '../polymer/lib/legacy/polymer-fn.js';
+import {html} from '../polymer/lib/utils/html-tag.js';
+
+import {PaperSpinnerBehavior} from './paper-spinner-behavior.js';
+
+const template = html`
+  <style include="paper-spinner-styles"></style>
+
+  <div id="spinnerContainer" class-name="[[__computeContainerClasses(active, __coolingDown)]]" on-animationend="__reset" on-webkit-animation-end="__reset">
+    <div class="spinner-layer">
+      <div class="circle-clipper left">
+        <div class="circle"></div>
+      </div>
+      <div class="circle-clipper right">
+        <div class="circle"></div>
+      </div>
+    </div>
+  </div>
+`;
+template.setAttribute('strip-whitespace', '');
+
+/**
+Material design: [Progress &
+activity](https://www.google.com/design/spec/components/progress-activity.html)
+
+Element providing a single color material design circular spinner.
+
+    <paper-spinner-lite active></paper-spinner-lite>
+
+The default spinner is blue. It can be customized to be a different color.
+
+### Accessibility
+
+Alt attribute should be set to provide adequate context for accessibility. If
+not provided, it defaults to 'loading'. Empty alt can be provided to mark the
+element as decorative if alternative content is provided in another form (e.g. a
+text block following the spinner).
+
+    <paper-spinner-lite alt="Loading contacts list" active></paper-spinner-lite>
+
+### Styling
+
+The following custom properties and mixins are available for styling:
+
+Custom property | Description | Default
+----------------|-------------|----------
+`--paper-spinner-color` | Color of the spinner | `--google-blue-500`
+`--paper-spinner-stroke-width` | The width of the spinner stroke | 3px
+
+@group Paper Elements
+@element paper-spinner-lite
+@hero hero.svg
+@demo demo/index.html
+*/
+Polymer({
+  _template: template,
+
+  is: 'paper-spinner-lite',
+
+  behaviors: [PaperSpinnerBehavior]
+});
diff --git a/third_party/polymer/v3_0/components-chromium/paper-spinner/paper-spinner-styles.js b/third_party/polymer/v3_0/components-chromium/paper-spinner/paper-spinner-styles.js
new file mode 100644
index 0000000..43b6553
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/paper-spinner/paper-spinner-styles.js
@@ -0,0 +1,346 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+const $_documentContainer = document.createElement('template');
+$_documentContainer.setAttribute('style', 'display: none;');
+
+$_documentContainer.innerHTML = `<dom-module id="paper-spinner-styles">
+  <template>
+    <style>
+      /*
+      /**************************/
+      /* STYLES FOR THE SPINNER */
+      /**************************/
+
+      /*
+       * Constants:
+       *      ARCSIZE     = 270 degrees (amount of circle the arc takes up)
+       *      ARCTIME     = 1333ms (time it takes to expand and contract arc)
+       *      ARCSTARTROT = 216 degrees (how much the start location of the arc
+       *                                should rotate each time, 216 gives us a
+       *                                5 pointed star shape (it's 360/5 * 3).
+       *                                For a 7 pointed star, we might do
+       *                                360/7 * 3 = 154.286)
+       *      SHRINK_TIME = 400ms
+       */
+
+      :host {
+        display: inline-block;
+        position: relative;
+        width: 28px;
+        height: 28px;
+
+        /* 360 * ARCTIME / (ARCSTARTROT + (360-ARCSIZE)) */
+        --paper-spinner-container-rotation-duration: 1568ms;
+
+        /* ARCTIME */
+        --paper-spinner-expand-contract-duration: 1333ms;
+
+        /* 4 * ARCTIME */
+        --paper-spinner-full-cycle-duration: 5332ms;
+
+        /* SHRINK_TIME */
+        --paper-spinner-cooldown-duration: 400ms;
+      }
+
+      #spinnerContainer {
+        width: 100%;
+        height: 100%;
+
+        /* The spinner does not have any contents that would have to be
+         * flipped if the direction changes. Always use ltr so that the
+         * style works out correctly in both cases. */
+        direction: ltr;
+      }
+
+      #spinnerContainer.active {
+        -webkit-animation: container-rotate var(--paper-spinner-container-rotation-duration) linear infinite;
+        animation: container-rotate var(--paper-spinner-container-rotation-duration) linear infinite;
+      }
+
+      @-webkit-keyframes container-rotate {
+        to { -webkit-transform: rotate(360deg) }
+      }
+
+      @keyframes container-rotate {
+        to { transform: rotate(360deg) }
+      }
+
+      .spinner-layer {
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        opacity: 0;
+        white-space: nowrap;
+        color: var(--paper-spinner-color, var(--google-blue-500));
+      }
+
+      .layer-1 {
+        color: var(--paper-spinner-layer-1-color, var(--google-blue-500));
+      }
+
+      .layer-2 {
+        color: var(--paper-spinner-layer-2-color, var(--google-red-500));
+      }
+
+      .layer-3 {
+        color: var(--paper-spinner-layer-3-color, var(--google-yellow-500));
+      }
+
+      .layer-4 {
+        color: var(--paper-spinner-layer-4-color, var(--google-green-500));
+      }
+
+      /**
+       * IMPORTANT NOTE ABOUT CSS ANIMATION PROPERTIES (keanulee):
+       *
+       * iOS Safari (tested on iOS 8.1) does not handle animation-delay very well - it doesn't
+       * guarantee that the animation will start _exactly_ after that value. So we avoid using
+       * animation-delay and instead set custom keyframes for each color (as layer-2undant as it
+       * seems).
+       */
+      .active .spinner-layer {
+        -webkit-animation-name: fill-unfill-rotate;
+        -webkit-animation-duration: var(--paper-spinner-full-cycle-duration);
+        -webkit-animation-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);
+        -webkit-animation-iteration-count: infinite;
+        animation-name: fill-unfill-rotate;
+        animation-duration: var(--paper-spinner-full-cycle-duration);
+        animation-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);
+        animation-iteration-count: infinite;
+        opacity: 1;
+      }
+
+      .active .spinner-layer.layer-1 {
+        -webkit-animation-name: fill-unfill-rotate, layer-1-fade-in-out;
+        animation-name: fill-unfill-rotate, layer-1-fade-in-out;
+      }
+
+      .active .spinner-layer.layer-2 {
+        -webkit-animation-name: fill-unfill-rotate, layer-2-fade-in-out;
+        animation-name: fill-unfill-rotate, layer-2-fade-in-out;
+      }
+
+      .active .spinner-layer.layer-3 {
+        -webkit-animation-name: fill-unfill-rotate, layer-3-fade-in-out;
+        animation-name: fill-unfill-rotate, layer-3-fade-in-out;
+      }
+
+      .active .spinner-layer.layer-4 {
+        -webkit-animation-name: fill-unfill-rotate, layer-4-fade-in-out;
+        animation-name: fill-unfill-rotate, layer-4-fade-in-out;
+      }
+
+      @-webkit-keyframes fill-unfill-rotate {
+        12.5% { -webkit-transform: rotate(135deg) } /* 0.5 * ARCSIZE */
+        25%   { -webkit-transform: rotate(270deg) } /* 1   * ARCSIZE */
+        37.5% { -webkit-transform: rotate(405deg) } /* 1.5 * ARCSIZE */
+        50%   { -webkit-transform: rotate(540deg) } /* 2   * ARCSIZE */
+        62.5% { -webkit-transform: rotate(675deg) } /* 2.5 * ARCSIZE */
+        75%   { -webkit-transform: rotate(810deg) } /* 3   * ARCSIZE */
+        87.5% { -webkit-transform: rotate(945deg) } /* 3.5 * ARCSIZE */
+        to    { -webkit-transform: rotate(1080deg) } /* 4   * ARCSIZE */
+      }
+
+      @keyframes fill-unfill-rotate {
+        12.5% { transform: rotate(135deg) } /* 0.5 * ARCSIZE */
+        25%   { transform: rotate(270deg) } /* 1   * ARCSIZE */
+        37.5% { transform: rotate(405deg) } /* 1.5 * ARCSIZE */
+        50%   { transform: rotate(540deg) } /* 2   * ARCSIZE */
+        62.5% { transform: rotate(675deg) } /* 2.5 * ARCSIZE */
+        75%   { transform: rotate(810deg) } /* 3   * ARCSIZE */
+        87.5% { transform: rotate(945deg) } /* 3.5 * ARCSIZE */
+        to    { transform: rotate(1080deg) } /* 4   * ARCSIZE */
+      }
+
+      @-webkit-keyframes layer-1-fade-in-out {
+        0% { opacity: 1 }
+        25% { opacity: 1 }
+        26% { opacity: 0 }
+        89% { opacity: 0 }
+        90% { opacity: 1 }
+        to { opacity: 1 }
+      }
+
+      @keyframes layer-1-fade-in-out {
+        0% { opacity: 1 }
+        25% { opacity: 1 }
+        26% { opacity: 0 }
+        89% { opacity: 0 }
+        90% { opacity: 1 }
+        to { opacity: 1 }
+      }
+
+      @-webkit-keyframes layer-2-fade-in-out {
+        0% { opacity: 0 }
+        15% { opacity: 0 }
+        25% { opacity: 1 }
+        50% { opacity: 1 }
+        51% { opacity: 0 }
+        to { opacity: 0 }
+      }
+
+      @keyframes layer-2-fade-in-out {
+        0% { opacity: 0 }
+        15% { opacity: 0 }
+        25% { opacity: 1 }
+        50% { opacity: 1 }
+        51% { opacity: 0 }
+        to { opacity: 0 }
+      }
+
+      @-webkit-keyframes layer-3-fade-in-out {
+        0% { opacity: 0 }
+        40% { opacity: 0 }
+        50% { opacity: 1 }
+        75% { opacity: 1 }
+        76% { opacity: 0 }
+        to { opacity: 0 }
+      }
+
+      @keyframes layer-3-fade-in-out {
+        0% { opacity: 0 }
+        40% { opacity: 0 }
+        50% { opacity: 1 }
+        75% { opacity: 1 }
+        76% { opacity: 0 }
+        to { opacity: 0 }
+      }
+
+      @-webkit-keyframes layer-4-fade-in-out {
+        0% { opacity: 0 }
+        65% { opacity: 0 }
+        75% { opacity: 1 }
+        90% { opacity: 1 }
+        to { opacity: 0 }
+      }
+
+      @keyframes layer-4-fade-in-out {
+        0% { opacity: 0 }
+        65% { opacity: 0 }
+        75% { opacity: 1 }
+        90% { opacity: 1 }
+        to { opacity: 0 }
+      }
+
+      .circle-clipper {
+        display: inline-block;
+        position: relative;
+        width: 50%;
+        height: 100%;
+        overflow: hidden;
+      }
+
+      /**
+       * Patch the gap that appear between the two adjacent div.circle-clipper while the
+       * spinner is rotating (appears on Chrome 50, Safari 9.1.1, and Edge).
+       */
+      .spinner-layer::after {
+        content: '';
+        left: 45%;
+        width: 10%;
+        border-top-style: solid;
+      }
+
+      .spinner-layer::after,
+      .circle-clipper .circle {
+        box-sizing: border-box;
+        position: absolute;
+        top: 0;
+        border-width: var(--paper-spinner-stroke-width, 3px);
+        border-radius: 50%;
+      }
+
+      .circle-clipper .circle {
+        bottom: 0;
+        width: 200%;
+        border-style: solid;
+        border-bottom-color: transparent !important;
+      }
+
+      .circle-clipper.left .circle {
+        left: 0;
+        border-right-color: transparent !important;
+        -webkit-transform: rotate(129deg);
+        transform: rotate(129deg);
+      }
+
+      .circle-clipper.right .circle {
+        left: -100%;
+        border-left-color: transparent !important;
+        -webkit-transform: rotate(-129deg);
+        transform: rotate(-129deg);
+      }
+
+      .active .gap-patch::after,
+      .active .circle-clipper .circle {
+        -webkit-animation-duration: var(--paper-spinner-expand-contract-duration);
+        -webkit-animation-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);
+        -webkit-animation-iteration-count: infinite;
+        animation-duration: var(--paper-spinner-expand-contract-duration);
+        animation-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);
+        animation-iteration-count: infinite;
+      }
+
+      .active .circle-clipper.left .circle {
+        -webkit-animation-name: left-spin;
+        animation-name: left-spin;
+      }
+
+      .active .circle-clipper.right .circle {
+        -webkit-animation-name: right-spin;
+        animation-name: right-spin;
+      }
+
+      @-webkit-keyframes left-spin {
+        0% { -webkit-transform: rotate(130deg) }
+        50% { -webkit-transform: rotate(-5deg) }
+        to { -webkit-transform: rotate(130deg) }
+      }
+
+      @keyframes left-spin {
+        0% { transform: rotate(130deg) }
+        50% { transform: rotate(-5deg) }
+        to { transform: rotate(130deg) }
+      }
+
+      @-webkit-keyframes right-spin {
+        0% { -webkit-transform: rotate(-130deg) }
+        50% { -webkit-transform: rotate(5deg) }
+        to { -webkit-transform: rotate(-130deg) }
+      }
+
+      @keyframes right-spin {
+        0% { transform: rotate(-130deg) }
+        50% { transform: rotate(5deg) }
+        to { transform: rotate(-130deg) }
+      }
+
+      #spinnerContainer.cooldown {
+        -webkit-animation: container-rotate var(--paper-spinner-container-rotation-duration) linear infinite, fade-out var(--paper-spinner-cooldown-duration) cubic-bezier(0.4, 0.0, 0.2, 1);
+        animation: container-rotate var(--paper-spinner-container-rotation-duration) linear infinite, fade-out var(--paper-spinner-cooldown-duration) cubic-bezier(0.4, 0.0, 0.2, 1);
+      }
+
+      @-webkit-keyframes fade-out {
+        0% { opacity: 1 }
+        to { opacity: 0 }
+      }
+
+      @keyframes fade-out {
+        0% { opacity: 1 }
+        to { opacity: 0 }
+      }
+    </style>
+  </template>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+export {};
diff --git a/third_party/polymer/v3_0/components-chromium/paper-styles/classes/global.js b/third_party/polymer/v3_0/components-chromium/paper-styles/classes/global.js
new file mode 100644
index 0000000..2082c8f
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/paper-styles/classes/global.js
@@ -0,0 +1,97 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+/*
+Note that this file probably doesn't do what you expect it to do. It's not
+a `<style is=custom-style include="..."` type of style include, which mean
+these styles will only apply to the main document, regardless of where
+you import this file.
+*/
+
+import '../paper-styles-classes.js';
+
+import {html} from '../../polymer/lib/utils/html-tag.js';
+const template = html`<style>
+/* Mixins */
+
+/* [paper-font] */
+body {
+  font-family: 'Roboto', 'Noto', sans-serif;
+  -webkit-font-smoothing: antialiased;  /* OS X subpixel AA bleed bug */
+}
+
+/* [paper-font=display2] */
+h1 {
+  font-size: 45px;
+  font-weight: 400;
+  letter-spacing: -.018em;
+  line-height: 48px;
+}
+
+/* [paper-font=display1] */
+h2 {
+  font-size: 34px;
+  font-weight: 400;
+  letter-spacing: -.01em;
+  line-height: 40px;
+}
+
+/* [paper-font=headline] */
+h3 {
+  font-size: 24px;
+  font-weight: 400;
+  letter-spacing: -.012em;
+  line-height: 32px;
+}
+
+/* [paper-font=subhead] */
+h4 {
+  font-size: 16px;
+  font-weight: 400;
+  line-height: 24px;
+}
+
+/* [paper-font=body2] */
+h5, h6 {
+  font-size: 14px;
+  font-weight: 500;
+  line-height: 24px;
+}
+
+/* [paper-font=button] */
+a {
+  font-size: 14px;
+  font-weight: 500;
+  letter-spacing: 0.018em;
+  line-height: 24px;
+  text-transform: uppercase;
+}
+
+/* Overrides */
+
+body, a {
+  color: #212121;
+}
+
+h1, h2, h3, h4, h5, h6, p {
+  margin: 0 0 20px 0;
+}
+
+h1, h2, h3, h4, h5, h6, a {
+  text-rendering: optimizeLegibility;
+}
+
+a {
+  text-decoration: none;
+}
+
+</style>`;
+template.setAttribute('style', 'display: none;');
+document.head.appendChild(template.content);
diff --git a/third_party/polymer/v3_0/components-chromium/paper-styles/classes/shadow.js b/third_party/polymer/v3_0/components-chromium/paper-styles/classes/shadow.js
new file mode 100644
index 0000000..44dcd29
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/paper-styles/classes/shadow.js
@@ -0,0 +1,65 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+/*
+Note that this file probably doesn't do what you expect it to do. It's not
+a `<style is=custom-style include="..."` type of style include, which mean
+these styles will only apply to the main document, regardless of where
+you import this file.
+
+For a set of styles that can be applied to an element, check
+paper-styles/shadow.js.
+*/
+import {html} from '../../polymer/lib/utils/html-tag.js';
+const template = html`
+<style>
+.shadow-transition {
+  transition: box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.shadow-elevation-2dp {
+  box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
+              0 1px 5px 0 rgba(0, 0, 0, 0.12),
+              0 3px 1px -2px rgba(0, 0, 0, 0.2);
+}
+
+.shadow-elevation-3dp {
+  box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14),
+              0 1px 8px 0 rgba(0, 0, 0, 0.12),
+              0 3px 3px -2px rgba(0, 0, 0, 0.4);
+}
+
+.shadow-elevation-4dp {
+  box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14),
+              0 1px 10px 0 rgba(0, 0, 0, 0.12),
+              0 2px 4px -1px rgba(0, 0, 0, 0.4);
+}
+
+.shadow-elevation-6dp {
+  box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.14),
+              0 1px 18px 0 rgba(0, 0, 0, 0.12),
+              0 3px 5px -1px rgba(0, 0, 0, 0.4);
+}
+
+.shadow-elevation-8dp {
+  box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14),
+              0 3px 14px 2px rgba(0, 0, 0, 0.12),
+              0 5px 5px -3px rgba(0, 0, 0, 0.4);
+}
+
+.shadow-elevation-16dp {
+  box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14),
+              0  6px 30px 5px rgba(0, 0, 0, 0.12),
+              0  8px 10px -5px rgba(0, 0, 0, 0.4);
+}
+
+</style>`;
+template.setAttribute('style', 'display: none;');
+document.head.appendChild(template.content);
diff --git a/third_party/polymer/v3_0/components-chromium/paper-styles/classes/typography.js b/third_party/polymer/v3_0/components-chromium/paper-styles/classes/typography.js
new file mode 100644
index 0000000..b35f889
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/paper-styles/classes/typography.js
@@ -0,0 +1,169 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+/*
+Note that this file probably doesn't do what you expect it to do. It's not
+a `<style is=custom-style include="..."` type of style include, which mean
+these styles will only apply to the main document, regardless of where
+you import this file.
+
+For a set of styles that can be applied to an element, check
+paper-styles/typography.html.
+*/
+import '../../font-roboto/roboto.js';
+import {html} from '../../polymer/lib/utils/html-tag.js';
+const template = html`
+<style>
+
+.paper-font-display4,
+.paper-font-display3,
+.paper-font-display2,
+.paper-font-display1,
+.paper-font-headline,
+.paper-font-title,
+.paper-font-subhead,
+.paper-font-body2,
+.paper-font-body1,
+.paper-font-caption,
+.paper-font-menu,
+.paper-font-button {
+  font-family: 'Roboto', 'Noto', sans-serif;
+  -webkit-font-smoothing: antialiased;  /* OS X subpixel AA bleed bug */
+}
+
+.paper-font-code2,
+.paper-font-code1 {
+  font-family: 'Roboto Mono', 'Consolas', 'Menlo', monospace;
+  -webkit-font-smoothing: antialiased;  /* OS X subpixel AA bleed bug */
+}
+
+/* Opt for better kerning for headers &amp; other short labels. */
+.paper-font-display4,
+.paper-font-display3,
+.paper-font-display2,
+.paper-font-display1,
+.paper-font-headline,
+.paper-font-title,
+.paper-font-subhead,
+.paper-font-menu,
+.paper-font-button {
+  text-rendering: optimizeLegibility;
+}
+
+/*
+"Line wrapping only applies to Body, Subhead, Headline, and the smaller Display
+styles. All other styles should exist as single lines."
+*/
+.paper-font-display4,
+.paper-font-display3,
+.paper-font-title,
+.paper-font-caption,
+.paper-font-menu,
+.paper-font-button {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.paper-font-display4 {
+  font-size: 112px;
+  font-weight: 300;
+  letter-spacing: -.044em;
+  line-height: 120px;
+}
+
+.paper-font-display3 {
+  font-size: 56px;
+  font-weight: 400;
+  letter-spacing: -.026em;
+  line-height: 60px;
+}
+
+.paper-font-display2 {
+  font-size: 45px;
+  font-weight: 400;
+  letter-spacing: -.018em;
+  line-height: 48px;
+}
+
+.paper-font-display1 {
+  font-size: 34px;
+  font-weight: 400;
+  letter-spacing: -.01em;
+  line-height: 40px;
+}
+
+.paper-font-headline {
+  font-size: 24px;
+  font-weight: 400;
+  letter-spacing: -.012em;
+  line-height: 32px;
+}
+
+.paper-font-title {
+  font-size: 20px;
+  font-weight: 500;
+  line-height: 28px;
+}
+
+.paper-font-subhead {
+  font-size: 16px;
+  font-weight: 400;
+  line-height: 24px;
+}
+
+.paper-font-body2 {
+  font-size: 14px;
+  font-weight: 500;
+  line-height: 24px;
+}
+
+.paper-font-body1 {
+  font-size: 14px;
+  font-weight: 400;
+  line-height: 20px;
+}
+
+.paper-font-caption {
+  font-size: 12px;
+  font-weight: 400;
+  letter-spacing: 0.011em;
+  line-height: 20px;
+}
+
+.paper-font-menu {
+  font-size: 13px;
+  font-weight: 500;
+  line-height: 24px;
+}
+
+.paper-font-button {
+  font-size: 14px;
+  font-weight: 500;
+  letter-spacing: 0.018em;
+  line-height: 24px;
+  text-transform: uppercase;
+}
+
+.paper-font-code2 {
+  font-size: 14px;
+  font-weight: 700;
+  line-height: 20px;
+}
+
+.paper-font-code1 {
+  font-size: 14px;
+  font-weight: 500;
+  line-height: 20px;
+}
+
+</style>`;
+template.setAttribute('style', 'display: none;');
+document.head.appendChild(template.content);
diff --git a/third_party/polymer/v3_0/components-chromium/paper-styles/color.js b/third_party/polymer/v3_0/components-chromium/paper-styles/color.js
new file mode 100644
index 0000000..dac6530
--- /dev/null
+++ b/third_party/polymer/v3_0/components-chromium/paper-styles/color.js
@@ -0,0 +1,340 @@
+/**
+@license
+Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
+*/
+
+import '../polymer/polymer-legacy.js';
+
+import {html} from '../polymer/lib/utils/html-tag.js';
+const template = html`
+<custom-style>
+  <style is="custom-style">
+    html {
+
+      /* Material Design color palette for Google products */
+
+      --google-red-100: #f4c7c3;
+      --google-red-300: #e67c73;
+      --google-red-500: #db4437;
+      --google-red-700: #c53929;
+
+      --google-blue-100: #c6dafc;
+      --google-blue-300: #7baaf7;
+      --google-blue-500: #4285f4;
+      --google-blue-700: #3367d6;
+
+      --google-green-100: #b7e1cd;
+      --google-green-300: #57bb8a;
+      --google-green-500: #0f9d58;
+      --google-green-700: #0b8043;
+
+      --google-yellow-100: #fce8b2;
+      --google-yellow-300: #f7cb4d;
+      --google-yellow-500: #f4b400;
+      --google-yellow-700: #f09300;
+
+      --google-grey-100: #f5f5f5;
+      --google-grey-300: #e0e0e0;
+      --google-grey-500: #9e9e9e;
+      --google-grey-700: #616161;
+
+      /* Material Design color palette from online spec document */
+
+      --paper-red-50: #ffebee;
+      --paper-red-100: #ffcdd2;
+      --paper-red-200: #ef9a9a;
+      --paper-red-300: #e57373;
+      --paper-red-400: #ef5350;
+      --paper-red-500: #f44336;
+      --paper-red-600: #e53935;
+      --paper-red-700: #d32f2f;
+      --paper-red-800: #c62828;
+      --paper-red-900: #b71c1c;
+      --paper-red-a100: #ff8a80;
+      --paper-red-a200: #ff5252;
+      --paper-red-a400: #ff1744;
+      --paper-red-a700: #d50000;
+
+      --paper-pink-50: #fce4ec;
+      --paper-pink-100: #f8bbd0;
+      --paper-pink-200: #f48fb1;
+      --paper-pink-300: #f06292;
+      --paper-pink-400: #ec407a;
+      --paper-pink-500: #e91e63;
+      --paper-pink-600: #d81b60;
+      --paper-pink-700: #c2185b;
+      --paper-pink-800: #ad1457;
+      --paper-pink-900: #880e4f;
+      --paper-pink-a100: #ff80ab;
+      --paper-pink-a200: #ff4081;
+      --paper-pink-a400: #f50057;
+      --paper-pink-a700: #c51162;
+
+      --paper-purple-50: #f3e5f5;
+      --paper-purple-100: #e1bee7;
+      --paper-purple-200: #ce93d8;
+      --paper-purple-300: #ba68c8;
+      --paper-purple-400: #ab47bc;
+      --paper-purple-500: #9c27b0;
+      --paper-purple-600: #8e24aa;
+      --paper-purple-700: #7b1fa2;
+      --paper-purple-800: #6a1b9a;
+      --paper-purple-900: #4a148c;
+      --paper-purple-a100: #ea80fc;
+      --paper-purple-a200: #e040fb;
+      --paper-purple-a400: #d500f9;
+      --paper-purple-a700: #aa00ff;
+
+      --paper-deep-purple-50: #ede7f6;
+      --paper-deep-purple-100: #d1c4e9;
+      --paper-deep-purple-200: #b39ddb;
+      --paper-deep-purple-300: #9575cd;
+      --paper-deep-purple-400: #7e57c2;
+      --paper-deep-purple-500: #673ab7;
+      --paper-deep-purple-600: #5e35b1;
+      --paper-deep-purple-700: #512da8;
+      --paper-deep-purple-800: #4527a0;
+      --paper-deep-purple-900: #311b92;
+      --paper-deep-purple-a100: #b388ff;
+      --paper-deep-purple-a200: #7c4dff;
+      --paper-deep-purple-a400: #651fff;
+      --paper-deep-purple-a700: #6200ea;
+
+      --paper-indigo-50: #e8eaf6;
+      --paper-indigo-100: #c5cae9;
+      --paper-indigo-200: #9fa8da;
+      --paper-indigo-300: #7986cb;
+      --paper-indigo-400: #5c6bc0;
+      --paper-indigo-500: #3f51b5;
+      --paper-indigo-600: #3949ab;
+      --paper-indigo-700: #303f9f;
+      --paper-indigo-800: #283593;
+      --paper-indigo-900: #1a237e;
+      --paper-indigo-a100: #8c9eff;
+      --paper-indigo-a200: #536dfe;
+      --paper-indigo-a400: #3d5afe;
+      --paper-indigo-a700: #304ffe;
+
+      --paper-blue-50: #e3f2fd;
+      --paper-blue-100: #bbdefb;
+      --paper-blue-200: #90caf9;
+      --paper-blue-300: #64b5f6;
+      --paper-blue-400: #42a5f5;
+      --paper-blue-500: #2196f3;
+      --paper-blue-600: #1e88e5;
+      --paper-blue-700: #1976d2;
+      --paper-blue-800: #1565c0;
+      --paper-blue-900: #0d47a1;
+      --paper-blue-a100: #82b1ff;
+      --paper-blue-a200: #448aff;
+      --paper-blue-a400: #2979ff;
+      --paper-blue-a700: #2962ff;
+
+      --paper-light-blue-50: #e1f5fe;
+      --paper-light-blue-100: #b3e5fc;
+      --paper-light-blue-200: #81d4fa;
+      --paper-light-blue-300: #4fc3f7;
+      --paper-light-blue-400: #29b6f6;
+      --paper-light-blue-500: #03a9f4;
+      --paper-light-blue-600: #039be5;
+      --paper-light-blue-700: #0288d1;
+      --paper-light-blue-800: #0277bd;
+      --paper-light-blue-900: #01579b;
+      --paper-light-blue-a100: #80d8ff;
+      --paper-light-blue-a200: #40c4ff;
+      --paper-light-blue-a400: #00b0ff;
+      --paper-light-blue-a700: #0091ea;
+
+      --paper-cyan-50: #e0f7fa;
+      --paper-cyan-100: #b2ebf2;
+      --paper-cyan-200: #80deea;
+      --paper-cyan-300: #4dd0e1;
+      --paper-cyan-400: #26c6da;
+      --paper-cyan-500: #00bcd4;
+      --paper-cyan-600: #00acc1;
+      --paper-cyan-700: #0097a7;
+      --paper-cyan-800: #00838f;
+      --paper-cyan-900: #006064;
+      --paper-cyan-a100: #84ffff;
+      --paper-cyan-a200: #18ffff;
+      --paper-cyan-a400: #00e5ff;
+      --paper-cyan-a700: #00b8d4;
+
+      --paper-teal-50: #e0f2f1;
+      --paper-teal-100: #b2dfdb;
+      --paper-teal-200: #80cbc4;
+      --paper-teal-300: #4db6ac;
+      --paper-teal-400: #26a69a;
+      --paper-teal-500: #009688;
+      --paper-teal-600: #00897b;
+      --paper-teal-700: #00796b;
+      --paper-teal-800: #00695c;
+      --paper-teal-900: #004d40;
+      --paper-teal-a100: #a7ffeb;
+      --paper-teal-a200: #64ffda;
+      --paper-teal-a400: #1de9b6;
+      --paper-teal-a700: #00bfa5;
+
+      --paper-green-50: #e8f5e9;
+      --paper-green-100: #c8e6c9;
+      --paper-green-200: #a5d6a7;
+      --paper-green-300: #81c784;
+      --paper-green-400: #66bb6a;
+      --paper-green-500: #4caf50;
+      --paper-green-600: #43a047;
+      --paper-green-700: #388e3c;
+      --paper-green-800: #2e7d32;
+      --paper-green-900: #1b5e20;
+      --paper-green-a100: #b9f6ca;
+      --paper-green-a200: #69f0ae;
+      --paper-green-a400: #00e676;
+      --paper-green-a700: #00c853;
+
+      --paper-light-green-50: #f1f8e9;
+      --paper-light-green-100: #dcedc8;
+      --paper-light-green-200: #c5e1a5;
+      --paper-light-green-300: #aed581;
+      --paper-light-green-400: #9ccc65;
+      --paper-light-green-500: #8bc34a;
+      --paper-light-green-600: #7cb342;
+      --paper-light-green-700: #689f38;
+      --paper-light-green-800: #558b2f;
+      --paper-light-green-900: #33691e;
+      --paper-light-green-a100: #ccff90;
+      --paper-light-green-a200: #b2ff59;
+      --paper-light-green-a400: #76ff03;
+      --paper-light-green-a700: #64dd17;
+
+      --paper-lime-50: #f9fbe7;
+      --paper-lime-100: #f0f4c3;
+      --paper-lime-200: #e6ee9c;
+      --paper-lime-300: #dce775;
+      --paper-lime-400: #d4e157;
+      --paper-lime-500: #cddc39;
+      --paper-lime-600: #c0ca33;
+      --paper-lime-700: #afb42b;
+      --paper-lime-800: #9e9d24;
+      --paper-lime-900: #827717;
+      --paper-lime-a100: #f4ff81;
+      --paper-lime-a200: #eeff41;
+      --paper-lime-a400: #c6ff00;
+      --paper-lime-a700: #aeea00;
+
+      --paper-yellow-50: #fffde7;
+      --paper-yellow-100: #fff9c4;
+      --paper-yellow-200: #fff59d;
+      --paper-yellow-300: #fff176;
+      --paper-yellow-400: #ffee58;
+      --paper-yellow-500: #ffeb3b;
+      --paper-yellow-600: #fdd835;
+      --paper-yellow-700: #fbc02d;
+      --paper-yellow-800: #f9a825;
+      --paper-yellow-900: #f57f17;
+      --paper-yellow-a100: #ffff8d;
+      --paper-yellow-a200: #ffff00;
+      --paper-yellow-a400: #ffea00;
+      --paper-yellow-a700: #ffd600;
+
+      --paper-amber-50: #fff8e1;
+      --paper-amber-100: #ffecb3;
+      --paper-amber-200: #ffe082;
+      --paper-amber-300: #ffd54f;
+      --paper-amber-400: #ffca28;
+      --paper-amber-500: #ffc107;
+      --paper-amber-600: #ffb300;
+      --paper-amber-700: #ffa000;
+      --paper-amber-800: #ff8f00;
+      --paper-amber-900: #ff6f00;
+      --paper-amber-a100: #ffe57f;
+      --paper-amber-a200: #ffd740;
+      --paper-amber-a400: #ffc400;
+      --paper-amber-a700: #ffab00;
+
+      --paper-orange-50: #fff3e0;
+      --paper-orange-100: #ffe0b2;
+      --paper-orange-200: #ffcc80;
+      --paper-orange-300: #ffb74d;
+      --paper-orange-400: #ffa726;
+      --paper-orange-500: #ff9800;
+      --paper-orange-600: #fb8c00;
+      --paper-orange-700: #f57c00;
+      --paper-orange-800: #ef6c00;
+      --paper-orange-900: #e65100;
+      --paper-orange-a100: #ffd180;
+      --paper-orange-a200: #ffab40;
+      --paper-orange-a400: #ff9100;
+      --paper-orange-a700: #ff6500;
+
+      --paper-deep-orange-50: #fbe9e7;
+      --paper-deep-orange-100: #ffccbc;
+      --paper-deep-orange-200: #ffab91;
+      --paper-deep-orange-300: #ff8a65;
+      --paper-deep-orange-400: #ff7043;
+      --paper-deep-orange-500: #ff5722;
+      --paper-deep-orange-600: #f4511e;
+      --paper-deep-orange-700: #e64a19;
+      --paper-deep-orange-800: #d84315;
+      --paper-deep-orange-900: #bf360c;
+      --paper-deep-orange-a100: #ff9e80;
+      --paper-deep-orange-a200: #ff6e40;
+      --paper-deep-orange-a400: #ff3d00;
+      --paper-deep-orange-a700: #dd2c00;
+
+      --paper-brown-50: #efebe9;
+      --paper-brown-100: #d7ccc8;
+      --paper-brown-200: #bcaaa4;
+      --paper-brown-300: #a1887f;
+      --paper-brown-400: #8d6e63;
+      --paper-brown-500: #795548;
+      --paper-brown-600: #6d4c41;
+      --paper-brown-700: #5d4037;
+      --paper-brown-800: #4e342e;
+      --paper-brown-900: #3e2723;
+
+      --paper-grey-50: #fafafa;
+      --paper-grey-100: #f5f5f5;
+      --paper-grey-200: #eeeeee;
+      --paper-grey-300: #e0e0e0;
+      --paper-grey-400: #bdbdbd;
+      --paper-grey-500: #9e9e9e;
+      --paper-grey-600: #757575;
+      --paper-grey-700: #616161;
+      --paper-grey-800: #424242;
+      --paper-grey-900: #212121;
+
+      --paper-blue-grey-50: #eceff1;
+      --paper-blue-grey-100: #cfd8dc;
+      --paper-blue-grey-200: #b0bec5;
+      --paper-blue-grey-300: #90a4ae;
+      --paper-blue-grey-400: #78909c;
+      --paper-blue-grey-500: #607d8b;
+      --paper-blue-grey-600: #546e7a;
+      --paper-blue-grey-700: #455a64;
+      --paper-blue-grey-800: #37474f;
+      --paper-blue-grey-900: #263238;
+
+      /* opacity for dark text on a light background */
+      --dark-divider-opacity: 0.12;