diff --git a/.gitignore b/.gitignore
index 84d48d86e28c87459c1983a44d436fb11249ce53..559118e173e0e9bd525e9f5286d2cdbf066d3845 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,4 @@ validation-status.json
 # Folders to ignore
 bower_components
 node_modules
+.build*
diff --git a/.travis.yml b/.travis.yml
index 99e274d3a2e760fb1a8c48fbe3bdd92698edea75..b54316e551865e0b723fa24a0aa862985c427ec3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -16,6 +16,8 @@ install:
   - npm install -g grunt-cli
   - ./test-infra/s3_cache.py download npm-modules
   - if [ "$TWBS_TEST" = validate-html ] && [ $TWBS_DO_VALIDATOR -ne 0 ]; then ./test-infra/s3_cache.py download rubygems; fi
+  # Install Meteor preemptively - https://github.com/MeteorPackaging/grunt-meteor#travisyml
+  - curl https://install.meteor.com | /bin/sh
 after_script:
   - if [ "$TRAVIS_REPO_SLUG" != twbs-savage/bootstrap ] && [ "$TWBS_TEST" = core ]; then ./test-infra/s3_cache.py upload npm-modules; fi
   - if [ "$TRAVIS_REPO_SLUG" != twbs-savage/bootstrap ] && [ "$TWBS_TEST" = validate-html ] && [ $TWBS_DO_VALIDATOR -ne 0 ]; then ./test-infra/s3_cache.py upload rubygems; fi
diff --git a/Gruntfile.js b/Gruntfile.js
index daa63a5923f585d54807434b5e8820184c0fac6a..913c26a78a353f636290862cc78e99eec253ac36 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -52,7 +52,8 @@ module.exports = function (grunt) {
     // Task configuration.
     clean: {
       dist: 'dist',
-      docs: 'docs/dist'
+      docs: 'docs/dist',
+      meteor: ['.build.*', 'versions.json', 'package.js']
     },
 
     jshint: {
@@ -384,6 +385,15 @@ module.exports = function (grunt) {
     exec: {
       npmUpdate: {
         command: 'npm update'
+      },
+      // These tasks require Meteor to be installed: curl https://install.meteor.com/ | sh;
+      meteorTest: {
+        // the -noglyph(icons) package only runs a subset of the tests from package.js, so skip it
+        command: 'cp grunt/meteor/package.js .; node_modules/.bin/spacejam --mongo-url mongodb:// test-packages ./'
+      },
+      meteorPublish: {
+        // publish both packages
+        command: 'cp grunt/meteor/package.js .; meteor publish; cp grunt/meteor/package-noglyph.js package.js; meteor publish'
       }
     },
 
@@ -456,6 +466,11 @@ module.exports = function (grunt) {
   grunt.registerTask('less-compile', ['less:compileCore', 'less:compileTheme']);
   grunt.registerTask('dist-css', ['less-compile', 'autoprefixer:core', 'autoprefixer:theme', 'usebanner', 'csscomb:dist', 'cssmin:minifyCore', 'cssmin:minifyTheme']);
 
+  // Meteor tasks
+  grunt.registerTask('meteor-test', ['exec:meteorTest', 'clean:meteor']);
+  grunt.registerTask('meteor-publish', ['exec:meteorPublish', 'clean:meteor']);
+  grunt.registerTask('meteor', ['exec:meteorTest', 'exec:meteorPublish', 'clean:meteor']);
+
   // Full distribution task.
   grunt.registerTask('dist', ['clean:dist', 'dist-css', 'copy:fonts', 'dist-js']);
 
@@ -507,4 +522,5 @@ module.exports = function (grunt) {
       done();
     });
   });
+
 };
diff --git a/grunt/meteor/README.md b/grunt/meteor/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..5bcc473afcc9bf0a130e5e5d1c0dab82bca3123e
--- /dev/null
+++ b/grunt/meteor/README.md
@@ -0,0 +1,52 @@
+[Bootstrap](http://getbootstrap.com) packaged for [Meteor.js](http://meteor.com).
+
+
+# Usage
+
+```sh
+meteor add twbs:bootstrap
+```
+
+Features requiring JavaScript (such as drop-downs) or custom jQuery plugins like tooltip or popover should work automatically.
+If they don't work in templates other than `body`, make sure to run the initialization code in `Template.<yourtemplate>.rendered`:
+
+```js
+Template.foo.rendered = function () {
+  this.$('[data-toggle="dropdown"]').dropdown();
+  this.$('[data-toggle="tooltip"]').tooltip();
+  this.$('[data-toggle="popover"]').popover();
+}
+```
+
+For performance reasons, [the Tooltip and Popover data-apis are opt-in](http://getbootstrap.com/javascript/#popovers).
+Above, we initialize them in the limited scope of the template DOM.
+
+
+
+# Package features
+
+* Opt-in jQuery plugins are enabled, as long as you use the `data-toggle` attribute in your HTML.
+  [Tooltips and popovers](http://getbootstrap.com/javascript/#popovers) "just work".
+* No need for CSS override files - Meteor will automatically "convert relative URLs to absolute URLs
+  when merging CSS files" [since v0.8.1](https://github.com/meteor/meteor/blob/b96c5d7962a9e59b9efaeb93eb81020e0548e378/History.md#v081)
+  so CSS `@font-face src url('../fonts/...')` will be resolved to the correct `/packages/.../fonts/...` path.
+* Tests that fonts are downloadable.
+* Tests that [all jQuery custom plugins](http://getbootstrap.com/javascript/) instantiate correctly.
+* Visual checks for plugins, including dropdown, popover, tooltip, are included.
+
+
+# Versions
+
+There are two versions of this package:
+
+* [twbs:bootstrap](https://atmospherejs.com/twbs/bootstrap) - the CSS, JS including [all jQuery plugins](http://getbootstrap.com/javascript/),
+  and the Glyphicons font are included.
+* [twbs:bootstrap-noglyph](https://atmospherejs.com/twbs/bootstrap-noglyph) - Only the Bootstrap .CSS and .JS files (including the plugins) are
+  packaged. Useful if you plan to use a different icon set instead of Glyphicons.
+
+If you need more detailed control on the files, or to use `.less`, see [Nemo64's package](https://github.com/Nemo64/meteor-bootstrap).
+
+
+# Issues
+
+If you encounter a Meteor-related issue while using this package, please CC @dandv when you [file it](https://github.com/twbs/bootstrap/issues).
diff --git a/grunt/meteor/init.js b/grunt/meteor/init.js
new file mode 100644
index 0000000000000000000000000000000000000000..7a915c9e250005cade1ed8b876d603de46315ab6
--- /dev/null
+++ b/grunt/meteor/init.js
@@ -0,0 +1,6 @@
+Meteor.startup(function () {
+  Template.body.rendered = function () {
+    $('[data-toggle="tooltip"]').tooltip();
+    $('[data-toggle="popover"]').popover();
+  };
+});
diff --git a/grunt/meteor/package-noglyph.js b/grunt/meteor/package-noglyph.js
new file mode 100644
index 0000000000000000000000000000000000000000..a63b6437afa1dfbd86be288079816625341eb0b4
--- /dev/null
+++ b/grunt/meteor/package-noglyph.js
@@ -0,0 +1,32 @@
+// package metadata file for Meteor.js
+'use strict'
+
+var packageName = 'twbs:bootstrap-noglyph'  // http://atmospherejs.com/twbs/bootstrap-noglyph
+var where = 'client'  // where to install: 'client' or 'server'. For both, pass nothing.
+
+var packageJson = JSON.parse(Npm.require("fs").readFileSync('package.json'))
+
+Package.describe({
+  name: packageName,
+  summary: 'Bootstrap without the Glyphicons font (official): the most popular HTML/CSS/JS responsive framework',  // limited to 100 characters
+  version: packageJson.version,
+  git: 'https://github.com/twbs/bootstrap.git',
+  documentation: 'grunt/meteor/README.md'
+})
+
+Package.onUse(function (api) {
+  api.versionsFrom(['METEOR@0.9.0', 'METEOR@1.0'])
+  api.use('jquery')  // required by Bootstrap's JavaScript
+  api.addFiles([
+    'dist/css/bootstrap.css',
+    'dist/js/bootstrap.js',
+    'grunt/meteor/init.js'
+  ], where)
+})
+
+Package.onTest(function (api) {
+  api.use(packageName, where)
+  api.use(['tinytest', 'http'], where)
+
+  api.addFiles('grunt/meteor/test-noglyph.js', where)
+})
diff --git a/grunt/meteor/package.js b/grunt/meteor/package.js
new file mode 100644
index 0000000000000000000000000000000000000000..2095a0e411935ebfbe65c0cdd3ac44fa1ebf4536
--- /dev/null
+++ b/grunt/meteor/package.js
@@ -0,0 +1,38 @@
+// package metadata file for Meteor.js
+'use strict'
+
+var packageName = 'twbs:bootstrap'  // http://atmospherejs.com/twbs/bootstrap
+var where = 'client'  // where to install: 'client' or 'server'. For both, pass nothing.
+
+var packageJson = JSON.parse(Npm.require("fs").readFileSync('package.json'))
+
+Package.describe({
+  name: packageName,
+  summary: 'Bootstrap (official): the most popular HTML/CSS/JS framework for responsive, mobile first projects',  // limited to 100 characters
+  version: packageJson.version,
+  git: 'https://github.com/twbs/bootstrap.git',
+  documentation: 'grunt/meteor/README.md'
+})
+
+Package.onUse(function (api) {
+  api.versionsFrom(['METEOR@0.9.0', 'METEOR@1.0'])
+  api.use('jquery')  // required by Bootstrap's JavaScript
+  api.addFiles([
+    // we bundle all font files, but the client will request only one of them via the CSS @font-face rule
+    'dist/fonts/glyphicons-halflings-regular.eot',   // IE8 or older
+    'dist/fonts/glyphicons-halflings-regular.svg',   // SVG fallback for iOS < 5 - http://caniuse.com/#feat=svg-fonts, http://stackoverflow.com/a/11002874/1269037
+    'dist/fonts/glyphicons-halflings-regular.ttf',   // Android Browers 4.1, 4.3 - http://caniuse.com/#feat=ttf
+    'dist/fonts/glyphicons-halflings-regular.woff',  // Supported by all modern browsers
+    'dist/fonts/glyphicons-halflings-regular.woff2', // Most modern font format
+    'dist/css/bootstrap.css',
+    'dist/js/bootstrap.js',
+    'grunt/meteor/init.js'
+  ], where)
+})
+
+Package.onTest(function (api) {
+  api.use(packageName, where)
+  api.use(['tinytest', 'http'], where)
+
+  api.addFiles('grunt/meteor/test.js', where)
+})
diff --git a/grunt/meteor/test-noglyph.js b/grunt/meteor/test-noglyph.js
new file mode 100644
index 0000000000000000000000000000000000000000..e972788143181fbf56bcb4567fb7b983e1e9706e
--- /dev/null
+++ b/grunt/meteor/test-noglyph.js
@@ -0,0 +1,41 @@
+'use strict';  // TWBS code style mandates no semicolons - https://github.com/twbs/bootstrap/blob/master/CONTRIBUTING.md#js
+
+var plugins = ['affix', 'alert', 'button', 'carousel', 'collapse', 'dropdown', 'modal', 'popover', 'scrollspy', 'tab', 'tooltip']
+
+// test plugins
+plugins.forEach(function (plugin) {
+  Tinytest.add('Plugin - ' + plugin, function (test) {
+    test.instanceOf($(document.body)[plugin], Function, 'instantiated correctly')
+  })
+})
+
+// visual check
+plugins.forEach(function (plugin) {
+
+  Tinytest.addAsync('Visual check - ' + plugin, function (test, done) {
+    var bootstrapDropZone = document.createElement('div')
+    document.body.appendChild(bootstrapDropZone)
+
+
+    // TODO ideally we'd get the htmls straight from this repo, but no idea how to do this from TinyTest - http://stackoverflow.com/questions/27180892/pull-an-html-file-into-a-tinytest
+    HTTP.get('http://rawgit.com/twbs/bootstrap/master/js/tests/visual/' + plugin + '.html', function callback(error, result) {
+      if (error) {
+        test.fail('Error getting the test file. Do we have an Internet connection to rawgit.com?')
+      } else {
+        // [^] matches across newlines. Stay within the container div, or else the fragment will attempt to load resources on its own.
+        bootstrapDropZone.innerHTML = result.content.match(/<div[^]+<\/div>/)
+        test.ok({message: 'Test passed if the display looks OK *and* clicking dropdowns/popovers/tooltips works.'})
+        // Only does anything after loading the 'dropdown' plugin test. No idea why it's necessary though.
+        $('[data-toggle="dropdown"]').dropdown()
+        // toltips and popovers are initialized by the package, but on Template.body.rendered, which is not available in this Tinytest
+        $('[data-toggle="tooltip"]').tooltip()
+        $('[data-toggle="popover"]').popover()
+        // don't initialize the modals because that messes up the Tinytest runner HTML
+      }
+
+      done()
+    })
+
+  })
+
+})
diff --git a/grunt/meteor/test.js b/grunt/meteor/test.js
new file mode 100644
index 0000000000000000000000000000000000000000..171834c4128ad4406d6eb8dd274f7d06ba331770
--- /dev/null
+++ b/grunt/meteor/test.js
@@ -0,0 +1,82 @@
+'use strict';  // TWBS code style mandates no semicolons - https://github.com/twbs/bootstrap/blob/master/CONTRIBUTING.md#js
+
+var packageName  // there seems to be no official way of finding out the name of the very package we're testing - http://stackoverflow.com/questions/27180709/in-a-tinytest-test-file-how-do-i-get-the-name-of-the-package
+
+// Check that the font files are downloadable. Meteor places assets at /packages/<packageName>/.
+['eot', 'svg', 'ttf', 'woff', 'woff2'].forEach(function (font) {
+  Tinytest.addAsync(font + ' fonts are shipped', function (test, done) {
+
+    // curiously enough, the 'local-test:...' package isn't loaded into Package before calling Tinytest, so we can't do this determination outside this loop
+    if (!packageName)
+      Object.keys(Package).forEach(function(p) {
+        if (p.search(/local-test/) > -1)
+          packageName = p.replace('local-test:', '')  // we should stop the loop, but forEach can't do that
+      })
+
+    if (!packageName) {
+      test.exception({message: 'Package not quite loaded... go figure'})
+      return
+    }
+
+    var packagePath = packageName.replace(':', '_')  // e.g. twbs_bootstrap
+
+    HTTP.get(
+      '/packages/' + packagePath + '/dist/fonts/glyphicons-halflings-regular.' + font,
+      {
+         headers: {
+           'Cache-Control': 'no-cache'  // because Meteor has cached fonts even after they were removed from package.js (!) - https://github.com/meteor/meteor/issues/3196
+         }
+      },
+      function callback(error, result) {
+        if (error) {
+          test.fail({message: 'Font failed to load'})
+        } else {
+          // if the file is 404, Meteor will redirect to / and return the Meteor.js boilerplate
+          test.isTrue(result.content.length > 15000, font + ' font could not be downloaded')
+        }
+
+        done()
+      }
+    )
+  })
+})
+
+var plugins = ['affix', 'alert', 'button', 'carousel', 'collapse', 'dropdown', 'modal', 'popover', 'scrollspy', 'tab', 'tooltip']
+
+// test plugins
+plugins.forEach(function (plugin) {
+  Tinytest.add('Plugin - ' + plugin, function (test) {
+    test.instanceOf($(document.body)[plugin], Function, 'instantiated correctly')
+  })
+})
+
+// visual check
+plugins.forEach(function (plugin) {
+
+  Tinytest.addAsync('Visual check - ' + plugin, function (test, done) {
+    var bootstrapDropZone = document.createElement('div')
+    document.body.appendChild(bootstrapDropZone)
+
+
+    // TODO ideally we'd get the htmls straight from this repo, but no idea how to do this from TinyTest - http://stackoverflow.com/questions/27180892/pull-an-html-file-into-a-tinytest
+    HTTP.get('http://rawgit.com/twbs/bootstrap/master/js/tests/visual/' + plugin + '.html', function callback(error, result) {
+      if (error) {
+        test.fail('Error getting the test file. Do we have an Internet connection to rawgit.com?')
+      } else {
+        // [^] matches across newlines. Stay within the container div, or else the fragment will attempt to load resources on its own.
+        bootstrapDropZone.innerHTML = result.content.match(/<div[^]+<\/div>/)
+        test.ok({message: 'Test passed if the display looks OK *and* clicking dropdowns/popovers/tooltips works.'})
+        // Only does anything after loading the 'dropdown' plugin test. No idea why it's necessary though.
+        $('[data-toggle="dropdown"]').dropdown()
+        // toltips and popovers are initialized by the package, but on Template.body.rendered, which is not available in this Tinytest
+        $('[data-toggle="tooltip"]').tooltip()
+        $('[data-toggle="popover"]').popover()
+        // don't initialize the modals because that messes up the Tinytest runner HTML
+      }
+
+      done()
+    })
+
+  })
+
+})
diff --git a/package.json b/package.json
index c2af0b5242ce3c1774aa99c29db322e7f015e364..95815c5c8d5cc73e4866c0e48c19b5406ea7f4c2 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,8 @@
     "load-grunt-tasks": "~3.0.0",
     "markdown-it": "^3.0.4",
     "npm-shrinkwrap": "^200.0.0",
-    "time-grunt": "~1.0.0"
+    "time-grunt": "~1.0.0",
+    "spacejam": "^1.1.1"
   },
   "engines": {
     "node": "~0.10.1"