diff --git a/.travis.yml b/.travis.yml
index f36af0aa4fabb52ac87dbf05f11e21a65e801569..47e28bf232a89899dbd2049c7c15f78ad70d352f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,9 +6,9 @@ before_install:
 install:
   - if [ "$TWBS_TEST" = validate-html ]; then time gem install jekyll; fi
   - time npm install -g grunt-cli
-  - time ./test-infra/node_modules_cache.py download || time npm install
+  - time ./test-infra/node_modules_cache.py download package.json ./node_modules || time npm install
 after_script:
-  - if [ "$TWBS_TEST" = core ]; then time ./test-infra/node_modules_cache.py upload; fi
+  - if [ "$TWBS_TEST" = core ]; then time ./test-infra/node_modules_cache.py upload package.json ./node_modules; fi
 env:
   global:
     - SAUCE_USERNAME: bootstrap
diff --git a/Gruntfile.js b/Gruntfile.js
index c4c3ced58f2c7348dd4a39ec69259a1adf18d45e..d350ce3ad6583bd0acd279c3b3fdebcee5b7fae3 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -193,6 +193,8 @@ module.exports = function (grunt) {
 
     validation: {
       options: {
+        charset: 'utf-8',
+        doctype: 'HTML5',
         reset: true,
         relaxerror: [
           'Bad value X-UA-Compatible for attribute http-equiv on element meta.',
diff --git a/components.html b/components.html
index 4afb2cd005307c84d1ba5dc1be19f6368fec6b83..8e61cf5265fcd9d9ed14697b22ca71c458f48b74 100644
--- a/components.html
+++ b/components.html
@@ -1552,7 +1552,7 @@ base_url: "../"
 
 
     <h2 id="input-groups-basic">Basic example</h2>
-    <p>Place one add-on or button on either side of an input. You may also place one on both sides of an input. <strong class="text-danger">We do not support mutiple add-ons on a single side.</strong></p>
+    <p>Place one add-on or button on either side of an input. You may also place one on both sides of an input. <strong class="text-danger">We do not support multiple add-ons on a single side.</strong></p>
     <form class="bs-example bs-example-form" role="form">
       <div class="input-group">
         <span class="input-group-addon">@</span>
diff --git a/css.html b/css.html
index 4220b998cc1698433daf3f22cb9859084f63fab0..cd233ef2ab23fa2ee7da6b0de87a098233ac015d 100644
--- a/css.html
+++ b/css.html
@@ -1965,31 +1965,31 @@ For example, <code>&lt;section&gt;</code> should be wrapped as inline.
     <div class="bs-example">
       <form role="form">
         <div class="form-group has-success">
-          <label class="control-label" for="inputSuccess">Input with success</label>
-          <input type="text" class="form-control" id="inputSuccess">
+          <label class="control-label" for="inputSuccess1">Input with success</label>
+          <input type="text" class="form-control" id="inputSuccess1">
         </div>
         <div class="form-group has-warning">
-          <label class="control-label" for="inputWarning">Input with warning</label>
-          <input type="text" class="form-control" id="inputWarning">
+          <label class="control-label" for="inputWarning1">Input with warning</label>
+          <input type="text" class="form-control" id="inputWarning1">
         </div>
         <div class="form-group has-error">
-          <label class="control-label" for="inputError">Input with error</label>
-          <input type="text" class="form-control" id="inputError">
+          <label class="control-label" for="inputError1">Input with error</label>
+          <input type="text" class="form-control" id="inputError1">
         </div>
       </form>
     </div><!-- /.bs-example -->
 {% highlight html %}
 <div class="form-group has-success">
-  <label class="control-label" for="inputSuccess">Input with success</label>
-  <input type="text" class="form-control" id="inputSuccess">
+  <label class="control-label" for="inputSuccess1">Input with success</label>
+  <input type="text" class="form-control" id="inputSuccess1">
 </div>
 <div class="form-group has-warning">
-  <label class="control-label" for="inputWarning">Input with warning</label>
-  <input type="text" class="form-control" id="inputWarning">
+  <label class="control-label" for="inputWarning1">Input with warning</label>
+  <input type="text" class="form-control" id="inputWarning1">
 </div>
 <div class="form-group has-error">
-  <label class="control-label" for="inputError">Input with error</label>
-  <input type="text" class="form-control" id="inputError">
+  <label class="control-label" for="inputError1">Input with error</label>
+  <input type="text" class="form-control" id="inputError1">
 </div>
 {% endhighlight %}
 
@@ -1998,36 +1998,36 @@ For example, <code>&lt;section&gt;</code> should be wrapped as inline.
     <div class="bs-example">
       <form role="form">
         <div class="form-group has-success has-feedback">
-          <label class="control-label" for="inputSuccess">Input with success</label>
-          <input type="text" class="form-control" id="inputSuccess">
+          <label class="control-label" for="inputSuccess2">Input with success</label>
+          <input type="text" class="form-control" id="inputSuccess2">
           <span class="glyphicon glyphicon-ok form-control-feedback"></span>
         </div>
         <div class="form-group has-warning has-feedback">
-          <label class="control-label" for="inputWarning">Input with warning</label>
-          <input type="text" class="form-control" id="inputWarning">
+          <label class="control-label" for="inputWarning2">Input with warning</label>
+          <input type="text" class="form-control" id="inputWarning2">
           <span class="glyphicon glyphicon-warning-sign form-control-feedback"></span>
         </div>
         <div class="form-group has-error has-feedback">
-          <label class="control-label" for="inputError">Input with error</label>
-          <input type="text" class="form-control" id="inputError">
+          <label class="control-label" for="inputError2">Input with error</label>
+          <input type="text" class="form-control" id="inputError2">
           <span class="glyphicon glyphicon-remove form-control-feedback"></span>
         </div>
       </form>
     </div>
 {% highlight html %}
 <div class="form-group has-success has-feedback">
-  <label class="control-label" for="inputSuccess">Input with success</label>
-  <input type="text" class="form-control" id="inputSuccess">
+  <label class="control-label" for="inputSuccess2">Input with success</label>
+  <input type="text" class="form-control" id="inputSuccess2">
   <span class="glyphicon glyphicon-ok form-control-feedback"></span>
 </div>
 <div class="form-group has-warning has-feedback">
-  <label class="control-label" for="inputWarning">Input with warning</label>
-  <input type="text" class="form-control" id="inputWarning">
+  <label class="control-label" for="inputWarning2">Input with warning</label>
+  <input type="text" class="form-control" id="inputWarning2">
   <span class="glyphicon glyphicon-warning-sign form-control-feedback"></span>
 </div>
 <div class="form-group has-error has-feedback">
-  <label class="control-label" for="inputError">Input with error</label>
-  <input type="text" class="form-control" id="inputError">
+  <label class="control-label" for="inputError2">Input with error</label>
+  <input type="text" class="form-control" id="inputError2">
   <span class="glyphicon glyphicon-remove form-control-feedback"></span>
 </div>
 {% endhighlight %}
@@ -2036,9 +2036,9 @@ For example, <code>&lt;section&gt;</code> should be wrapped as inline.
     <div class="bs-example">
       <form class="form-horizontal" role="form">
         <div class="form-group has-success has-feedback">
-          <label class="control-label col-sm-3" for="inputSuccess">Input with success</label>
+          <label class="control-label col-sm-3" for="inputSuccess3">Input with success</label>
           <div class="col-sm-9">
-            <input type="text" class="form-control" id="inputSuccess">
+            <input type="text" class="form-control" id="inputSuccess3">
             <span class="glyphicon glyphicon-ok form-control-feedback"></span>
           </div>
         </div>
@@ -2047,9 +2047,9 @@ For example, <code>&lt;section&gt;</code> should be wrapped as inline.
 {% highlight html %}
 <form class="form-horizontal" role="form">
   <div class="form-group has-success has-feedback">
-    <label class="control-label col-sm-3" for="inputSuccess">Input with success</label>
+    <label class="control-label col-sm-3" for="inputSuccess3">Input with success</label>
     <div class="col-sm-9">
-      <input type="text" class="form-control" id="inputSuccess">
+      <input type="text" class="form-control" id="inputSuccess3">
       <span class="glyphicon glyphicon-ok form-control-feedback"></span>
     </div>
   </div>
@@ -2059,8 +2059,8 @@ For example, <code>&lt;section&gt;</code> should be wrapped as inline.
     <div class="bs-example">
       <form class="form-inline" role="form">
         <div class="form-group has-success has-feedback">
-          <label class="control-label" for="inputSuccess">Input with success</label>
-          <input type="text" class="form-control" id="inputSuccess">
+          <label class="control-label" for="inputSuccess4">Input with success</label>
+          <input type="text" class="form-control" id="inputSuccess4">
           <span class="glyphicon glyphicon-ok form-control-feedback"></span>
         </div>
       </form>
@@ -2068,8 +2068,8 @@ For example, <code>&lt;section&gt;</code> should be wrapped as inline.
 {% highlight html %}
 <form class="form-inline" role="form">
   <div class="form-group has-success has-feedback">
-    <label class="control-label" for="inputSuccess">Input with success</label>
-    <input type="text" class="form-control" id="inputSuccess">
+    <label class="control-label" for="inputSuccess4">Input with success</label>
+    <input type="text" class="form-control" id="inputSuccess4">
     <span class="glyphicon glyphicon-ok form-control-feedback"></span>
   </div>
 </form>
@@ -3006,7 +3006,7 @@ a {
 {% endhighlight %}
 
     <h3 id="less-mixins-box-shadow">Box (Drop) shadows</h3>
-    <p>If your target audience is using the latest and greatest browsers and devices, be sure to just use the <code>box-shadow</code> property on it's own. If you need support for older Android (pre-v4) and iOS devices (pre-iOS 5), use of the mixin to pick up the required <code>-webkit</code> prefix.</p>
+    <p>If your target audience is using the latest and greatest browsers and devices, be sure to just use the <code>box-shadow</code> property on its own. If you need support for older Android (pre-v4) and iOS devices (pre-iOS 5), use the mixin to pick up the required <code>-webkit</code> prefix.</p>
     <p>Be sure to use <code>rgba()</code> colors in your box shadows so they blend as seamlessly as possible with backgrounds.</p>
 {% highlight css %}
 .box-shadow(@shadow: 0 1px 3px rgba(0,0,0,.25)) {
diff --git a/getting-started.html b/getting-started.html
index c43abe197999e049d715456c891c8cf859531cdb..286058ba4f676e5e14fd611e1acc5d96e24c0fca 100644
--- a/getting-started.html
+++ b/getting-started.html
@@ -884,8 +884,8 @@ if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
 {% highlight html %}
 <script>
 var nua = navigator.userAgent;
-var is_android = ((nua.indexOf('Mozilla/5.0') > -1 && nua.indexOf('Android ') > -1 && nua.indexOf('AppleWebKit') > -1) && !(nua.indexOf('Chrome') > -1));
-if(is_android) {
+var isAndroid = (nua.indexOf('Mozilla/5.0') > -1 && nua.indexOf('Android ') > -1 && nua.indexOf('AppleWebKit') > -1 && nua.indexOf('Chrome') === -1);
+if (isAndroid) {
   $('select.form-control').removeClass('form-control').css('width', '100%');
 }
 </script>
diff --git a/javascript.html b/javascript.html
index d83358a3974a68eadfc2478e70160c95f4879d70..c330a47921f2cf05147efabdfae444b93b168aee 100644
--- a/javascript.html
+++ b/javascript.html
@@ -245,7 +245,7 @@ $('#myModal').on('show.bs.modal', function (e) {
 <!-- Large modal -->
 <button class="btn btn-primary" data-toggle="modal" data-target=".bs-modal-lg">Large modal</button>
 
-<div class="modal fade bs-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+<div class="modal fade bs-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true">
   <div class="modal-dialog modal-lg">
     <div class="modal-content">
       ...
@@ -256,7 +256,7 @@ $('#myModal').on('show.bs.modal', function (e) {
 <!-- Small modal -->
 <button class="btn btn-primary" data-toggle="modal" data-target=".bs-modal-sm">Small modal</button>
 
-<div class="modal fade bs-modal-sm" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+<div class="modal fade bs-modal-sm" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
   <div class="modal-dialog modal-sm">
     <div class="modal-content">
       ...
@@ -266,13 +266,13 @@ $('#myModal').on('show.bs.modal', function (e) {
 {% endhighlight %}
 
     <!--  Modal content for the above example -->
-    <div class="modal fade bs-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+    <div class="modal fade bs-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true">
       <div class="modal-dialog modal-lg">
         <div class="modal-content">
 
           <div class="modal-header">
             <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-            <h4 class="modal-title" id="myModalLabel">Large modal</h4>
+            <h4 class="modal-title" id="myLargeModalLabel">Large modal</h4>
           </div>
           <div class="modal-body">
             ...
@@ -280,13 +280,13 @@ $('#myModal').on('show.bs.modal', function (e) {
         </div><!-- /.modal-content -->
       </div><!-- /.modal-dialog -->
     </div><!-- /.modal -->
-    <div class="modal fade bs-modal-sm" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+    <div class="modal fade bs-modal-sm" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
       <div class="modal-dialog modal-sm">
         <div class="modal-content">
 
           <div class="modal-header">
             <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-            <h4 class="modal-title" id="myModalLabel">Small modal</h4>
+            <h4 class="modal-title" id="mySmallModalLabel">Small modal</h4>
           </div>
           <div class="modal-body">
             ...
@@ -2053,7 +2053,7 @@ $('#myCarousel').on('slide.bs.carousel', function () {
     <p>The affix plugin toggles between three classes, each representing a particular state: <code>.affix</code>, <code>.affix-top</code>, and <code>.affix-bottom</code>. You must provide the styles for these classes yourself (independent of this plugin) to handle the actual positions.</p>
     <p>Here's how the affix plugin works:</p>
     <ol>
-      <li>To start, the plugin adds <code>.affix-top</code> to indicate the element is in it's top-most position. At this point no CSS positioning is required.</li>
+      <li>To start, the plugin adds <code>.affix-top</code> to indicate the element is in its top-most position. At this point no CSS positioning is required.</li>
       <li>Scrolling past the element you want affixed should trigger the actual affixing. This is where <code>.affix</code> replaces <code>.affix-top</code> and sets <code>position: fixed;</code> (provided by Bootstrap's code CSS).</li>
       <li>If a bottom offset is defined, scrolling past that should replace <code>.affix</code> with <code>.affix-bottom</code>. Since offsets are optional, setting one requires you to set the appropriate CSS. In this case, add <code>position: absolute;</code> when necessary. The plugin uses the data attribute or JavaScript option to determine where to position the element from there.</li>
     </ol>
diff --git a/less/forms.less b/less/forms.less
index 530257499fb7ca2bb809aeed9e1f230f0ce07cf9..aefa5a462c858a6642d0d138eb611e8e2205e9d3 100644
--- a/less/forms.less
+++ b/less/forms.less
@@ -265,7 +265,7 @@ input[type="checkbox"],
   // Feedback icon (requires .glyphicon classes)
   .form-control-feedback {
     position: absolute;
-    top: (@line-height-computed + 5); // Height of the `label` and it's margin
+    top: (@line-height-computed + 5); // Height of the `label` and its margin
     right: 0;
     display: block;
     width: @input-height-base;
diff --git a/less/navbar.less b/less/navbar.less
index d96f85e30635c086c9d8a972c4ce932c32a46488..621772fbbfb464bd870ca9554eb7931285c81816 100644
--- a/less/navbar.less
+++ b/less/navbar.less
@@ -212,7 +212,7 @@
 
 // Navbar nav links
 //
-// Builds on top of the `.nav` components with it's own modifier class to make
+// Builds on top of the `.nav` components with its own modifier class to make
 // the nav the full height of the horizontal nav (above 768px).
 
 .navbar-nav {
diff --git a/less/navs.less b/less/navs.less
index e4ac1445952df8bbdd3d14adbc63a97917e2fc38..9e729b39fe5e31b062c5e9644ae2fb58d0b829ec 100644
--- a/less/navs.less
+++ b/less/navs.less
@@ -91,7 +91,7 @@
       }
     }
 
-    // Active state, and it's :hover to override normal :hover
+    // Active state, and its :hover to override normal :hover
     &.active > a {
       &,
       &:hover,
diff --git a/less/panels.less b/less/panels.less
index 1b72cebd160666ef56d996adbbd7645d4efa272c..ef878686721ea42e7fa004b785d3d68cc4386dc7 100644
--- a/less/panels.less
+++ b/less/panels.less
@@ -95,15 +95,17 @@
     border: 0;
     margin-bottom: 0;
   }
-  > .table-striped > tbody > tr:last-child,
-  > .table-responsive > .table-striped > tbody > tr:last-child {
-    td:first-child,
-    th:first-child {
-      border-bottom-left-radius: (@panel-border-radius - 1);
-    }
-    td:last-child,
-    th:last-child {
-      border-bottom-left-radius: (@panel-border-radius - 1);
+  > .table-striped,
+  > .table-responsive > .table-striped {
+    > tbody > tr:last-child {
+      td:first-child,
+      th:first-child {
+        border-bottom-left-radius: (@panel-border-radius - 1);
+      }
+      td:last-child,
+      th:last-child {
+        border-bottom-left-radius: (@panel-border-radius - 1);
+      }
     }
   }
 }
@@ -120,7 +122,7 @@
   }
 }
 
-// Within heading, strip any `h*` tag of it's default margins for spacing.
+// Within heading, strip any `h*` tag of its default margins for spacing.
 .panel-title {
   margin-top: 0;
   margin-bottom: 0;
diff --git a/less/type.less b/less/type.less
index 9d032b2681c18ce8484935a08ca24d47a6f669dd..5373975d2fa53c10b11ba6af1af5dd2760513adf 100644
--- a/less/type.less
+++ b/less/type.less
@@ -126,7 +126,7 @@ cite    { font-style: normal; }
 // For now we'll leave these alongside the text classes until v4 when we can
 // safely shift things around (per SemVer rules).
 .bg-primary {
-  // Given the contrast here, this is the only class to have it's color inverted
+  // Given the contrast here, this is the only class to have its color inverted
   // automatically.
   color: #fff;
   background-color: @brand-primary;
diff --git a/test-infra/node_modules_cache.py b/test-infra/node_modules_cache.py
index 9d9272fcc1620d4ea9c5c8f848bb430fcb40a6ea..6acddb38a765044ae273bca0014b780975b1ac52 100755
--- a/test-infra/node_modules_cache.py
+++ b/test-infra/node_modules_cache.py
@@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals, print_function, divisi
 
 from sys import argv
 from os import environ, stat, remove as _delete_file
-from os.path import isfile
+from os.path import isfile, dirname, basename, abspath
 from hashlib import sha256
 from subprocess import check_call as run
 
@@ -12,7 +12,6 @@ from boto.s3.key import Key
 from boto.exception import S3ResponseError
 
 
-NODE_MODULES_TARBALL = 'node_modules.tar.gz'
 NEED_TO_UPLOAD_MARKER = '.need-to-upload'
 BYTES_PER_MB = 1024 * 1024
 try:
@@ -25,7 +24,9 @@ def _sha256_of_file(filename):
     hasher = sha256()
     with open(filename, 'rb') as input_file:
         hasher.update(input_file.read())
-    return hasher.hexdigest()
+    file_hash = hasher.hexdigest()
+    print('sha256({}) = {}'.format(filename, file_hash))
+    return file_hash
 
 
 def _delete_file_quietly(filename):
@@ -35,52 +36,71 @@ def _delete_file_quietly(filename):
         pass
 
 
-def _tarball_size():
-    kib = stat(NODE_MODULES_TARBALL).st_size // BYTES_PER_MB
+def _tarball_size(directory):
+    kib = stat(_tarball_filename_for(directory)).st_size // BYTES_PER_MB
     return "{} MiB".format(kib)
 
 
+def _tarball_filename_for(directory):
+    return abspath('./{}.tar.gz'.format(basename(directory)))
+
+
+def _create_tarball(directory):
+    print("Creating tarball of {}...".format(directory))
+    run(['tar', '-czf', _tarball_filename_for(directory), '-C', dirname(directory), basename(directory)])
+
+
+def _extract_tarball(directory):
+    print("Extracting tarball of {}...".format(directory))
+    run(['tar', '-xzf', _tarball_filename_for(directory), '-C', dirname(directory)])
+
+
+def download(directory):
+    _delete_file_quietly(NEED_TO_UPLOAD_MARKER)
+    try:
+        print("Downloading {} tarball from S3...".format(basename(directory)))
+        key.get_contents_to_filename(_tarball_filename_for(directory))
+    except S3ResponseError as err:
+        open(NEED_TO_UPLOAD_MARKER, 'a').close()
+        print(err)
+        raise SystemExit("Cached {} download failed!".format(basename(directory)))
+    print("Downloaded {}.".format(_tarball_size(directory)))
+    _extract_tarball(directory)
+    print("{} successfully installed from cache.".format(directory))
+
+
+def upload(directory):
+    _create_tarball(directory)
+    print("Uploading {} tarball to S3... ({})".format(basename(directory), _tarball_size(directory)))
+    key.set_contents_from_filename(_tarball_filename_for(directory))
+    print("{} cache successfully updated.".format(directory))
+    _delete_file_quietly(NEED_TO_UPLOAD_MARKER)
+
+
 if __name__ == '__main__':
     # Uses environment variables:
     #   AWS_ACCESS_KEY_ID - AWS Access Key ID
     #   AWS_SECRET_ACCESS_KEY - AWS Secret Access Key
     argv.pop(0)
-    if len(argv) != 1:
-        raise SystemExit("USAGE: node_modules_cache.py <download | upload>")
-    mode = argv.pop()
+    if len(argv) != 3:
+        raise SystemExit("USAGE: node_modules_cache.py <download | upload> <dependencies file> <directory>")
+    mode, dependencies_file, directory = argv
 
     conn = S3Connection()
     bucket = conn.lookup(BUCKET_NAME)
     if bucket is None:
         raise SystemExit("Could not access bucket!")
 
-    package_json_hash = _sha256_of_file('package.json')
-    print('sha256(package.json) = ' + package_json_hash)
+    dependencies_file_hash = _sha256_of_file(dependencies_file)
 
-    key = Key(bucket, package_json_hash)
+    key = Key(bucket, dependencies_file_hash)
     key.storage_class = 'REDUCED_REDUNDANCY'
 
     if mode == 'download':
-        _delete_file_quietly(NEED_TO_UPLOAD_MARKER)
-        try:
-            print("Downloading tarball from S3...")
-            key.get_contents_to_filename(NODE_MODULES_TARBALL)
-        except S3ResponseError as err:
-            open(NEED_TO_UPLOAD_MARKER, 'a').close()
-            print(err)
-            raise SystemExit("Cached node_modules download failed!")
-        print("Downloaded {}.".format(_tarball_size()))
-        print("Extracting tarball...")
-        run(['tar', 'xzf', NODE_MODULES_TARBALL])
-        print("node_modules successfully installed from cache.")
+        download(directory)
     elif mode == 'upload':
-        if isfile(NEED_TO_UPLOAD_MARKER):
-            print("Creating tarball...")
-            run(['tar', 'czf', NODE_MODULES_TARBALL, 'node_modules'])
-            print("Uploading tarball to S3... ({})".format(_tarball_size()))
-            key.set_contents_from_filename(NODE_MODULES_TARBALL)
-            print("node_modules cache successfully updated.")
-            _delete_file_quietly(NEED_TO_UPLOAD_MARKER)
+        if isfile(NEED_TO_UPLOAD_MARKER):  # FIXME
+            upload(directory)
         else:
             print("No need to upload anything.")
     else: