commit 86f0ef71bf86570ad286987d70cff2f55a35b299 Author: Sami Samhuri Date: Sun Feb 5 22:01:28 2012 -0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3523299 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.tmproj +log/ +pid +public/photos/* +public/videos/* +public/js-min +public/css-min +coverage diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..6aaa039 --- /dev/null +++ b/Gemfile @@ -0,0 +1,19 @@ +source :rubygems + +gem 'rack' +gem 'rack-test' +gem 'sinatra-flash' +gem 'sinatra' +gem 'redis', '>= 2.2.0', :require => ['redis', 'redis/connection/hiredis'] +gem 'thin' +gem 'rmagick' +gem 'bcrypt-ruby' +gem 'pony' +gem 'erubis' +gem 'minitest' +gem 'activesupport' +gem 'uuid' +gem 'sinatra-cookie_thief' +gem 'rdiscount' +gem 'redis-store', :git => 'https://github.com/betastreet/redis-store.git', :branch => '1.0.x' +gem 'simplecov', :require => false diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..d7dddd1 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,83 @@ +GIT + remote: https://github.com/betastreet/redis-store.git + revision: 1d4b475fb975a8ad361744346edc7172acf8869f + branch: 1.0.x + specs: + redis-store (1.0.0.1) + redis (~> 2.2.1) + +GEM + remote: http://rubygems.org/ + specs: + activesupport (3.1.3) + multi_json (~> 1.0) + bcrypt-ruby (3.0.1) + daemons (1.1.6) + erubis (2.7.0) + eventmachine (0.12.10) + i18n (0.6.0) + macaddr (1.5.0) + systemu (>= 2.4.0) + mail (2.4.1) + i18n (>= 0.4.0) + mime-types (~> 1.16) + treetop (~> 1.4.8) + mime-types (1.17.2) + minitest (2.11.1) + multi_json (1.0.4) + polyglot (0.3.3) + pony (1.4) + mail (> 2.0) + rack (1.4.1) + rack-protection (1.2.0) + rack + rack-test (0.6.1) + rack (>= 1.0) + rdiscount (1.6.8) + redis (2.2.2) + rmagick (2.13.1) + simplecov (0.5.4) + multi_json (~> 1.0.3) + simplecov-html (~> 0.5.3) + simplecov-html (0.5.3) + sinatra (1.3.2) + rack (~> 1.3, >= 1.3.6) + rack-protection (~> 1.2) + tilt (~> 1.3, >= 1.3.3) + sinatra-cookie_thief (0.1.1) + sinatra (>= 1.0) + sinatra-flash (0.3.0) + sinatra (>= 1.0.0) + systemu (2.4.2) + thin (1.3.1) + daemons (>= 1.0.9) + eventmachine (>= 0.12.6) + rack (>= 1.0.0) + tilt (1.3.3) + treetop (1.4.10) + polyglot + polyglot (>= 0.3.1) + uuid (2.3.5) + macaddr (~> 1.0) + +PLATFORMS + ruby + +DEPENDENCIES + activesupport + bcrypt-ruby + erubis + minitest + pony + rack + rack-test + rdiscount + redis (>= 2.2.0) + redis-store! + rmagick + simplecov + sinatra + sinatra-cookie_thief + sinatra-flash + thin + uuid diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..9a77874 --- /dev/null +++ b/Rakefile @@ -0,0 +1,49 @@ +# Copyright 2012 Sami Samhuri + +require 'rake/testtask' + +Rake::TestTask.new do |t| + t.libs << 'test' + t.test_files = FileList['test/**/test-*.rb'] +end + +task 'nuke-test-data' do + require 'rubygems' + require 'bundler/setup' + require 'redis' + + redis = Redis.new + redis.keys('TEST:stormy:*').each do |key| + redis.del key + end +end + +MinifiedCSSDir = 'public/css-min' +MinifiedJSDir = 'public/js-min' + +desc "Minifies JS and CSS" +task :minify do + puts "Minifying JavaScript..." + Dir.mkdir(MinifiedJSDir) unless File.exists?(MinifiedJSDir) + Dir['public/js/*.js'].each do |path| + filename = File.basename(path) + out = File.join(MinifiedJSDir, filename) + if !File.exists?(out) || File.mtime(path) > File.mtime(out) + puts " * #{filename}" + `bin/closure <"#{path}" >"#{out}"` + end + end + + puts "Minifying CSS..." + Dir.mkdir(MinifiedCSSDir) unless File.exists?(MinifiedCSSDir) + Dir['public/css/*.css'].each do |path| + filename = File.basename(path) + out = File.join(MinifiedCSSDir, filename) + if !File.exists?(out) || File.mtime(path) > File.mtime(out) + puts " * #{filename}" + `bin/yui-compressor "#{path}" "#{out}"` + end + end + + puts "Done." +end diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..96b8aea --- /dev/null +++ b/Readme.md @@ -0,0 +1,68 @@ +# Stormy Weather + +A web server blueprint using Sinatra and Redis. + +Storm Weather is only a vague idea. Trying to nail down a lightweight blueprint for +projects that have user accounts and admin accounts. + + +## Models + +### Account Model + +### Admin Model + + +## Controllers + +### Account Controller + +### Admin Controller + +### Public Controller + + +## Helpers + +### Accounts + +### Admin + +### Authorization + +### FAQ + +### Utils + +### Views + + +## Tests + + % rake test + +The tests are pretty slow. Mocking out Redis didn't improve the speed of tests. Advice on speeding them up is greatly appreciated. + + +## Thanks + +These truly are gems. Thanks to all the maintainers. + + - ActiveSupport + - Bcrypt + - Erubis + - Pony + - Rack and Rack::Test + - RDiscount + - Redis + - Rmagick + - SimpleCov + - Sinatra + - Thin + + +## License + +[Licensed under the terms of the MIT license](http://sjs.mit-license.org). + +© 2012 Sami Samhuri <[sami@samhuri.net](mailto:sami@samhuri.net)> diff --git a/bin/closure b/bin/closure new file mode 100755 index 0000000..4b3dbe0 --- /dev/null +++ b/bin/closure @@ -0,0 +1,3 @@ +#!/bin/bash + +java -jar $(dirname $0)/compiler.jar "$@" diff --git a/bin/compiler.jar b/bin/compiler.jar new file mode 100644 index 0000000..694808c Binary files /dev/null and b/bin/compiler.jar differ diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..531d350 --- /dev/null +++ b/bin/console @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# +# Bash version: +# exec irb --readline --simple-prompt -r irb/completion -I $(dirname "$0")/../lib -r stormy + +$LOAD_PATH << File.expand_path('../lib', File.dirname(__FILE__)) +require 'stormy' +require 'stormy/models' + +include Stormy::Models + +require 'irb' +require 'irb/completion' + +ARGV << '--simple-prompt' +IRB.start diff --git a/bin/mkpass.rb b/bin/mkpass.rb new file mode 100755 index 0000000..818e489 --- /dev/null +++ b/bin/mkpass.rb @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +require 'rubygems' +require 'bundler/setup' +require 'bcrypt' + +puts BCrypt::Password.create(ARGV.first) diff --git a/bin/restart.sh b/bin/restart.sh new file mode 100755 index 0000000..7c8ada6 --- /dev/null +++ b/bin/restart.sh @@ -0,0 +1,5 @@ +#!/bin/zsh + +[[ -d /web/stormy ]] && cd /web/stormy +bin/stop.sh +bin/start.sh diff --git a/bin/start.rb b/bin/start.rb new file mode 100755 index 0000000..fd9830f --- /dev/null +++ b/bin/start.rb @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +# +# Copyright 2012 Sami Samhuri + +$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib') +require 'stormy' +require 'stormy/server' + +Stormy::Server.run! diff --git a/bin/start.sh b/bin/start.sh new file mode 100755 index 0000000..985450c --- /dev/null +++ b/bin/start.sh @@ -0,0 +1,17 @@ +#!/bin/zsh + +[[ -d "$HOME/.rbenv" ]] && export PATH="$HOME/.rbenv/shims:$PATH" + +if [[ "$RACK_ENV" = "development" ]]; then + exec shotgun -s thin -o 0.0.0.0 -p 5000 config.ru +else + [[ -d /web/stormy ]] && cd /web/stormy + [[ -d log ]] || mkdir log + RACK_ENV=production bin/start.rb >>|log/access.log 2>>|log/access.log &! + if [[ $? -eq 0 ]]; then + echo $! >|./pid + else + echo "!! Failed to start. Last bit of the log:" + tail -n 20 log/access.log + fi +fi diff --git a/bin/stop.sh b/bin/stop.sh new file mode 100755 index 0000000..66b7a0f --- /dev/null +++ b/bin/stop.sh @@ -0,0 +1,22 @@ +#!/bin/zsh + +[[ -d /web/stormy ]] && cd /web/stormy + +if [[ -r pid ]]; then + + PID=$(cat pid) + RETRIES=3 + + while [[ $RETRIES -gt 0 ]] && ps ax | grep "${PID}[ ]" >/dev/null; do + kill $PID + sleep 1 + RETRIES=$((RETRIES - 1)) + done + + if ps ax | grep "${PID}[ ]" >/dev/null; then + kill -9 $PID + sleep 1 + fi + + rm pid +fi diff --git a/bin/yui-compressor b/bin/yui-compressor new file mode 100755 index 0000000..a500120 --- /dev/null +++ b/bin/yui-compressor @@ -0,0 +1,3 @@ +#!/bin/bash + +java -jar $(dirname $0)/yuicompressor-2.4.7.jar "$1" -o "$2" diff --git a/bin/yuicompressor-2.4.7.jar b/bin/yuicompressor-2.4.7.jar new file mode 100644 index 0000000..c34800c Binary files /dev/null and b/bin/yuicompressor-2.4.7.jar differ diff --git a/config.ru b/config.ru new file mode 100755 index 0000000..d72b874 --- /dev/null +++ b/config.ru @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +# +# Copyright 2012 Sami Samhuri + +$LOAD_PATH.unshift('lib') +require 'stormy' +require 'stormy/server' + +run Stormy::Server diff --git a/lib/hash-ext.rb b/lib/hash-ext.rb new file mode 100644 index 0000000..079bf79 --- /dev/null +++ b/lib/hash-ext.rb @@ -0,0 +1,12 @@ +# Copyright 2012 Sami Samhuri + +class Hash + + def slice(*keys) + keys.inject({}) do |h, k| + h[k] = self[k] if has_key?(k) + h + end + end + +end diff --git a/lib/stormy.rb b/lib/stormy.rb new file mode 100644 index 0000000..835fce6 --- /dev/null +++ b/lib/stormy.rb @@ -0,0 +1,37 @@ +# Copyright 2012 Sami Samhuri + +require 'date' + +require 'rubygems' +require 'bundler/setup' +require 'active_support/core_ext' + +this_dir = File.dirname(__FILE__) +$LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir) + +module Stormy + + # key prefix for data stored in Redis (used for testing) + unless const_defined? :KeyPrefix + KeyPrefix = '' + end + + # public directory for project photos + unless const_defined? :PhotoDir + PhotoDir = File.expand_path('../public/photos', File.dirname(__FILE__)) + end + + # public directory for project videos + unless const_defined? :VideoDir + VideoDir = File.expand_path('../public/videos', File.dirname(__FILE__)) + end + + def self.key_prefix + @key_prefix ||= "#{KeyPrefix}stormy:" + end + + def self.key(*components) + key_prefix + components.compact.join(':') + end + +end diff --git a/lib/stormy/config.rb b/lib/stormy/config.rb new file mode 100644 index 0000000..6c0aa15 --- /dev/null +++ b/lib/stormy/config.rb @@ -0,0 +1,75 @@ +# Copyright 2012 Sami Samhuri + +require 'redis' + +module Stormy + + class Config + + DefaultConfig = { + } + + ConfigTypes = { + } + + # shared instance + def self.instance + @@instance ||= new + end + + def initialize + reload! + if config.size == 0 && DefaultConfig.size > 0 + redis.hmset(config_key, *DefaultConfig.to_a.flatten) + reload! + end + end + + def config_key + @config_key ||= Stormy.key('config') + end + + def config + @config ||= redis.hgetall(config_key) + end + + def redis + @redis ||= Redis.new + end + + def reload! + @config = nil + config + if config.size > 0 + ConfigTypes.each do |name, type| + if type == :integer + config[name] = config[name].to_i + elsif type == :boolean + config[name] = config[name] == 'true' + end + end + end + end + + def method_missing(name, *args) + name = name.to_s + # TODO: decide if we should call super for unknown names + if name.ends_with?('=') + name = name.sub(/=$/, '') + value = args.first + redis.hset(config_key, name, value) + config[name] = value + elsif config.has_key?(name) + config[name] + elsif DefaultConfig.has_key?(name) + value = DefaultConfig[name] + redis.hset(config_key, name, value) + config[name] = value + else + super(name.to_sym, *args) + end + end + + end + +end diff --git a/lib/stormy/controllers.rb b/lib/stormy/controllers.rb new file mode 100644 index 0000000..419666e --- /dev/null +++ b/lib/stormy/controllers.rb @@ -0,0 +1,5 @@ +# Copyright 2012 Sami Samhuri + +Dir[File.dirname(__FILE__) + '/controllers/*.rb'].each do |f| + require 'stormy/controllers/' + File.basename(f) +end diff --git a/lib/stormy/controllers/accounts_controller.rb b/lib/stormy/controllers/accounts_controller.rb new file mode 100644 index 0000000..1b4e5b5 --- /dev/null +++ b/lib/stormy/controllers/accounts_controller.rb @@ -0,0 +1,221 @@ +# Copyright 2012 Sami Samhuri + +module Stormy + class Server < Sinatra::Base + + get '/sign-up' do + redirect '/projects' if authorized? && production? + + title 'Sign Up' + stylesheet 'sign-up' + script 'sign-up' + @errors = session.delete(:errors) if session[:errors] + @fields = session.delete(:fields) || {} + erb :'sign-up' + end + + post '/sign-up' do + session.delete('source') if session['source'] + + fields = params.slice(*Account.fields.map { |name, options| name.to_s if options[:updatable] }.compact) + %w[email password].each do |name| + fields[name] = params[name] + end + + begin + @account = Account.create(fields) + authorize_account(@account.id) + send_verification_mail(@account, 'Welcome to Stormy Weather!') + redirect '/projects' + + rescue Account::EmailTakenError => e + flash[:warning] = "That email address is already taken." + session[:fields] = fields + session[:fields]['terms'] = params['terms'] + session[:errors] = { 'email' => 'taken' } + redirect '/sign-up' + + rescue Account::InvalidDataError => e + flash[:warning] = "There's a small problem with your info." + session[:fields] = fields + session[:fields]['terms'] = params['terms'] + session[:errors] = e.fields + if session[:errors].has_key?('hashed_password') + session[:errors]['password'] = session[:errors].delete('hashed_password') + end + redirect '/sign-up' + end + end + + get '/sign-in' do + redirect '/projects' if authorized? && production? + + title 'Sign In' + stylesheet 'sign-in' + script 'sign-in' + @email = session.delete(:email) + erb :'sign-in' + end + + post '/sign-in' do + if id = Account.check_password(params['email'], params['password']) + authorize_account(id) + if params['remember'] == 'on' + response.set_cookie('remembered', { + :value => current_account.id, + :path => '/', + :expires => Time.now + 2.weeks, + :httponly => true + }) + else + response.delete_cookie('remembered') + end + url = session.delete(:original_url) || '/projects' + redirect url + else + flash[:warning] = "Incorrect email address or password." + redirect '/sign-in' + end + end + + post '/sign-out' do + deauthorize + redirect '/' + end + + get '/forgot-password/?:email?' do |email| + title 'Forgot Password' + script 'forgot-password' + @email = email + erb :'forgot-password' + end + + post '/forgot-password' do + if params['email'].blank? + flash[:warning] = "Enter your email address so we can send you a link to reset your password." + redirect '/forgot-password' + elsif send_reset_password_mail(params['email']) + flash[:notice] = "A link to reset your password was sent to #{escape_html(params['email'])}." + redirect '/sign-in' + else + flash[:warning] = "We don't have an account for #{escape_html(params['email'])}." + redirect '/forgot-password' + end + end + + # reset password + get '/sign-in/:email/:token' do |email, token| + if id = Account.use_password_reset_token(email, token) + authorize_account(id) + title 'Reset My Password' + stylesheet 'reset-password' + script 'reset-password' + erb :'reset-password' + else + flash[:warning] = "Unknown or expired link to reset password." + redirect '/forgot-password/' + email + end + end + + post '/account/reset-password' do + authorize! + current_account.password = params['password'] + current_account.save! + redirect '/projects' + end + + get '/account' do + authorize! + title 'Account' + stylesheet 'account' + script 'jquery.jeditable' + script 'account' + script 'account-editable' + @account = current_account + erb :account + end + + post '/account/password' do + content_type :json + authorize_api! + + begin + raise Account::InvalidDataError unless params['new-password'] == params['password-confirmation'] + current_account.update_password(params['old-password'], params['new-password']) + ok + rescue Account::IncorrectPasswordError => e + fail('incorrect') + rescue Account::InvalidDataError => e + fail('invalid') + end + end + + post '/account/update' do + authorize_api! + + begin + current_account.update({ params['id'] => params['value'] }) + params['value'] + rescue Account::InvalidDataError => e + # This is lame but gives the desired result with jEditable + bad_request + end + end + + post '/account/update.json' do + content_type :json + authorize_api! + + begin + if params['id'] == 'email' + old_email = current_account.email + new_email = params['value'] + current_account.update_email(new_email) + if old_email.downcase != new_email.downcase + send_verification_mail unless current_account.email_verified? + end + else + # decode booleans + if params['id'].match(/_notifications$/) + if params['value'] == 'true' + params['value'] = true + elsif params['value'] == 'false' + params['value'] = false + end + end + current_account.update({ params['id'] => params['value'] }) + end + ok + rescue Account::EmailTakenError => e + fail('taken') + rescue Account::InvalidDataError => e + fail('invalid') + end + end + + + #################### + ### Verification ### + #################### + + get '/account/verify/:email/:token' do |email, token| + if Account.verify_email(email, token) + authorize_account(Account.id_from_email(email)) unless authorized? + flash[:notice] = "Your email address has been verified." + redirect '/account' + elsif authorized? + redirect '/account' + else + erb :'verification-failed' + end + end + + post '/account/send-email-verification' do + content_type :json + authorize_api! + send_verification_mail + ok + end + + end +end diff --git a/lib/stormy/controllers/admin_controller.rb b/lib/stormy/controllers/admin_controller.rb new file mode 100644 index 0000000..74a28a1 --- /dev/null +++ b/lib/stormy/controllers/admin_controller.rb @@ -0,0 +1,217 @@ +# Copyright 2012 Sami Samhuri + +module Stormy + class Server < Sinatra::Base + + get '/admin' do + admin_authorize! + title "Dashboard" + erb :'admin/dashboard', :layout => :'admin/layout' + end + + get '/admin/sign-in' do + title "Sign In" + script 'sign-in' + stylesheet 'sign-in' + erb :'admin/sign-in', :layout => :'admin/layout' + end + + post '/admin/sign-in' do + if id = Admin.check_password(params['email'], params['password']) + authorize_admin(id) + redirect session.delete(:original_url) || '/admin' + else + flash[:notice] = "Incorrect email address or password." + redirect '/admin/sign-in' + end + end + + post '/admin/sign-out' do + session.delete(:admin_id) + redirect '/admin' + end + + get '/admin/password' do + admin_authorize! + title 'Change password' + erb :'admin/password', :layout => :'admin/layout' + end + + post '/admin/password' do + admin_authorize! + if params['password'] == params['password_confirmation'] + current_admin.password = params['password'] + current_admin.save + flash[:notice] = "Password changed." + redirect '/admin' + else + flash[:warning] = "Passwords do not match." + redirect '/admin/password' + end + end + + + ################ + ### Accounts ### + ################ + + get '/admin/accounts' do + admin_authorize! + mark_last_listing + title "Accounts" + @accounts = Account.fetch_all.sort { |a,b| a.name <=> b.name } + erb :'admin/accounts', :layout => :'admin/layout' + end + + get '/admin/account/:email' do |email| + admin_authorize! + if @account = Account.fetch_by_email(email) + mark_last_listing + title "#{@account.name}'s Account" + script 'admin-account' + erb :'admin/account', :layout => :'admin/layout' + else + flash[:notice] = "No account with email #{email}" + redirect last_listing + end + end + + get '/admin/sign-in-as/:email' do |email| + admin_authorize! + authorize_account(Account.id_from_email(email)) + redirect '/projects' + end + + get '/admin/account/:email/delete' do |email| + admin_authorize! + if @account = Account.fetch_by_email(email) + @account.delete! + end + redirect last_listing + end + + post '/admin/account/:email' do |email| + admin_authorize! + if @account = Account.fetch_by_email(email) + email_changed = params['new_email'].present? && params['new_email'] != @account.email + fields = params.merge({ + 'email_verified' => email_changed ? true : @account.email_verified + }) + fields.delete('splat') + fields.delete('captures') + fields.delete('email') + new_email = fields.delete('new_email') + new_email = @account.email if new_email.blank? + if new_email != @account.email + begin + @account.update_email(new_email) + rescue Account::EmailTakenError => e + flash[:warning] = "That email address is already taken." + redirect '/admin/account/' + email + end + end + begin + @account.update!(fields, :validate => true) + flash[:notice] = "Account updated." + rescue Account::InvalidDataError => e + flash[:warning] = "Invalid fields: #{e.fields.inspect}" + end + redirect '/admin/account/' + new_email + else + flash[:notice] = "No account with email #{email}" + redirect last_listing + end + end + + + ################ + ### Projects ### + ################ + + get '/admin/projects' do + admin_authorize! + mark_last_listing + title "Projects" + @projects = Project.fetch_all.sort { |a,b| a.id <=> b.id } + erb :'admin/projects', :layout => :'admin/layout' + end + + get '/admin/project/:id' do |id| + admin_authorize! + if @project = Project.fetch(id) + title "Project ##{id}" + title "#{title} (#{@project.name})" if @project.name + script 'admin-project' + erb :'admin/project', :layout => :'admin/layout' + else + flash[:notice] = "No such project (ID #{id})." + redirect last_listing + end + end + + get '/admin/project/:id/delete' do |id| + admin_authorize! + Project.delete!(id) + redirect last_listing + end + + + ########### + ### FAQ ### + ########### + + get '/admin/faq' do + admin_authorize! + title 'FAQ' + @faq = faq + if @faq.blank? + @faq = <<-EOT +

1. Are you my mother?

+

Yes my son.

+ EOT + end + erb :'admin/faq', :layout => :'admin/layout' + end + + post '/admin/faq' do + admin_authorize! + self.faq = params['faq'] + flash[:notice] = "FAQ saved." + redirect '/admin/faq' + end + + + ###################### + ### Admin Accounts ### + ###################### + + get '/admin/admins' do + admin_authorize! + @admins = Admin.fetch_all.sort { |a,b| a.name <=> b.name } + @fields = session.delete(:fields) || {} + title 'Admin Accounts' + stylesheet 'admins' + erb :'admin/admins', :layout => :'admin/layout' + end + + post '/admin/admins' do + admin_authorize! + if params['password'] == params['password_confirmation'] + admin = Admin.create(params) + flash[:notice] = "Added #{params['name']} (#{params['email']}) as an admin." + else + session[:fields] = params.slice('name', 'email') + flash[:warning] = "Passwords do not match." + end + redirect '/admin/admins' + end + + get '/admin/admins/:id/delete' do |id| + admin_authorize! + Admin.delete!(id) + flash[:notice] = "Deleted." + redirect '/admin/admins' + end + + end +end diff --git a/lib/stormy/controllers/projects_controller.rb b/lib/stormy/controllers/projects_controller.rb new file mode 100644 index 0000000..c52d244 --- /dev/null +++ b/lib/stormy/controllers/projects_controller.rb @@ -0,0 +1,123 @@ +# Copyright 2011 Beta Street Media + +module Stormy + class Server < Sinatra::Base + + get '/projects' do + authorize! + @projects = current_account.sorted_projects + title 'Projects' + stylesheet 'projects' + script 'projects' + erb :projects + end + + get '/project/:id' do |id| + authorize_project!(id) + if current_project.name.blank? + title "Project ID #{id}" + else + title current_project.name + end + stylesheet 'jquery.lightbox-0.5' + script 'jquery.lightbox-0.5' + script 'jquery.dragsort' + stylesheet 'edit-project' + script 'edit-project' + + # fuck IE + if request.user_agent.match(/msie/i) + stylesheet 'uploadify' + script 'swfobject' + script 'jquery.uploadify.v2.1.4' + end + + @errors = session.delete('errors') + @project = current_project + erb :'edit-project' + end + + post '/project/update' do + id = params['id'] + if admin_authorized? + current_project(id) + else + authorize_project!(id) + end + + begin + current_project.update(params) + + flash[:notice] = "Project saved." + rescue Project::InvalidDataError => e + flash[:warning] = "There are some errors with your project." + session['errors'] = e.fields + end + + redirect '/project/' + params['id'] + end + + post '/project/add-photo' do + content_type :json + id = params['id'] + if admin_authorized? + current_project(id) + else + authorize_project_api!(id) + end + + if photo = current_project.add_photo(params['photo'][:tempfile].path) + ok({ + 'n' => current_project.count_photos, + 'photo' => photo + }) + else + fail('limit') + end + end + + # fuck IE + post '/uploadify' do + content_type :json + authorize_project_api!(params['id']) + if photo = current_project.add_photo(params['Filedata'][:tempfile].path) + ok({ + 'n' => current_project.count_photos, + 'photo' => photo + }) + else + content_type 'text/plain' + bad_request + end + end + + post '/project/remove-photo' do + content_type :json + if admin_authorized? + current_project(params['id']) + else + authorize_project_api!(params['id']) + end + + current_project.remove_photo(params['photo_id']) + ok({ + 'photos' => current_project.photos + }) + end + + post '/project/photo-order' do + content_type :json + id = params['id'] + if admin_authorized? + current_project(id) + else + authorize_project_api!(id) + end + + current_project.photo_ids = params['order'] + current_project.save! + ok + end + + end +end diff --git a/lib/stormy/controllers/public_controller.rb b/lib/stormy/controllers/public_controller.rb new file mode 100644 index 0000000..c073779 --- /dev/null +++ b/lib/stormy/controllers/public_controller.rb @@ -0,0 +1,49 @@ + +# Copyright 2012 Sami Samhuri + +module Stormy + class Server < Sinatra::Base + + get '/' do + cache_control :public, :must_revalidate, :max_age => 60 + stylesheet 'index' + stylesheet 'jquery.lightbox-0.5' + script 'jquery.lightbox-0.5' + script 'index' + erb :index + end + + get '/contact' do + cache_control :public, :must_revalidate, :max_age => 60 + title 'Contact' + stylesheet 'contact' + script 'contact' + erb :contact + end + + post '/contact' do + Pony.mail({ + :to => 'info@example.com', + :from => params['email'], + :subject => 'Stormy Weather Contact Form', + :body => params['message'] + }) + flash[:notice] = "Thanks for contacting us!" + redirect '/contact' + end + + get '/terms' do + cache_control :public, :must_revalidate, :max_age => 60 + title 'Terms of Service' + erb :terms + end + + get '/faq' do + @faq = faq + title 'Frequently Asked Questions' + stylesheet 'faq' + erb :faq + end + + end +end diff --git a/lib/stormy/helpers.rb b/lib/stormy/helpers.rb new file mode 100644 index 0000000..24505f8 --- /dev/null +++ b/lib/stormy/helpers.rb @@ -0,0 +1,5 @@ +# Copyright 2012 Sami Samhuri + +Dir[File.dirname(__FILE__) + '/helpers/*.rb'].each do |f| + require 'stormy/helpers/' + File.basename(f) +end diff --git a/lib/stormy/helpers/accounts.rb b/lib/stormy/helpers/accounts.rb new file mode 100644 index 0000000..0f1f1d5 --- /dev/null +++ b/lib/stormy/helpers/accounts.rb @@ -0,0 +1,45 @@ +# Copyright 2012 Sami Samhuri + +module Stormy + module Helpers + module Accounts + + include Stormy::Models + + def send_reset_password_mail(email) + if data = Account.reset_password(email) + body = erb(:'email/reset-password', :layout => :'email/layout', :locals => { + :name => data['name'], + :email => email, + :sign_in_url => url_for('sign-in', email, data['token']) + }) + Pony.mail({ + :to => email, + :from => 'support@example.com', + :subject => 'Reset your Stormy Weather password', + :headers => { 'Content-Type' => 'text/html' }, + :body => body + }) + data + end + end + + def send_verification_mail(account = current_account, subject = nil) + account.create_email_verification_token + body = erb(:'email/email-verification', :layout => :'email/layout', :locals => { + :name => account.first_name, + :email => account.email, + :url => url_for('account/verify', account.email, account.email_verification_token) + }) + Pony.mail({ + :to => account.email, + :from => 'support@example.com', + :subject => subject || 'Verify your Stormy Weather account', + :headers => { 'Content-Type' => 'text/html' }, + :body => body + }) + end + + end + end +end diff --git a/lib/stormy/helpers/admin.rb b/lib/stormy/helpers/admin.rb new file mode 100644 index 0000000..b35818e --- /dev/null +++ b/lib/stormy/helpers/admin.rb @@ -0,0 +1,38 @@ +# Copyright 2012 Sami Samhuri + +module Stormy + module Helpers + module Admin + + include Stormy::Models + + def num_accounts + Account.count + end + + def num_admins + Models::Admin.count + end + + def num_projects + Project.count + end + + # Used to redirect back to the most recent list of things. + # + # i.e. someone goes to /admin -> /admin/account/foo -> /admin/project/007 + # if they delete that project they should go back to /admin/account/foo + # + # however if they go /admin -> /admin/projects -> /admin/project/007 + # and then delete that project they should go back to /admin/projects + def last_listing + session.delete(:last_listing) || '/admin' + end + + def mark_last_listing(path = request.path_info) + session[:last_listing] = path + end + + end + end +end diff --git a/lib/stormy/helpers/authorization.rb b/lib/stormy/helpers/authorization.rb new file mode 100644 index 0000000..d57cff8 --- /dev/null +++ b/lib/stormy/helpers/authorization.rb @@ -0,0 +1,108 @@ +# Copyright 2012 Sami Samhuri + +module Stormy + module Helpers + module Authorization + + include Stormy::Models + + def authorize_account(id) + session[:id] = id + end + + def authorized? + if !session[:id] && id = request.cookies['remembered'] + authorize_account(id) + end + session[:id] && Account.exists?(session[:id]) + end + + def authorize! + unless authorized? + session[:original_url] = request.url + redirect '/sign-in' + end + end + + def authorize_api! + unless authorized? + content_type 'text/plain' + throw(:halt, not_authorized) + end + end + + def deauthorize + session.delete(:id) + response.delete_cookie('remembered') + end + + def current_account + if session[:id] + @current_account ||= Account.fetch(session[:id]) + end + end + + def current_project(id = nil) + if id + @current_project = Project.fetch(id) + else + @current_project + end + end + + def project_authorized? + current_project && current_account && current_project.account_id == current_account.id + end + + def authorize_project_api!(id) + authorize_api! + current_project(id) + throw(:halt, fail('no such project')) unless current_project + unless project_authorized? + content_type 'text/plain' + throw(:halt, not_authorized) + end + end + + def authorize_project!(id) + authorize! + current_project(id) + unless current_project && project_authorized? + flash[:warning] = 'No such project.' + redirect '/projects' + end + end + + def authorize_admin(id) + session[:admin_id] = id + end + + def deauthorize_admin + session.delete(:admin_id) + end + + def admin_authorized? + session[:admin_id] && Models::Admin.exists?(session[:admin_id]) + end + + def admin_authorize! + unless admin_authorized? + session[:original_url] = request.url + redirect '/admin' + end + end + + def admin_authorize_api! + unless admin_authorized? + content_type 'text/plain' + throw(:halt, not_authorized) + end + end + + def current_admin + @current_admin ||= Models::Admin.fetch(session[:admin_id]) + end + + end + end +end diff --git a/lib/stormy/helpers/faq.rb b/lib/stormy/helpers/faq.rb new file mode 100644 index 0000000..8da6cd2 --- /dev/null +++ b/lib/stormy/helpers/faq.rb @@ -0,0 +1,23 @@ +# Copyright 2012 Sami Samhuri + +module Stormy + module Helpers + module FAQ + + def faq + redis.get(faq_key) + end + + def faq=(new_faq) + redis.set(faq_key, new_faq) + end + + private + + def faq_key + @faq_key ||= Stormy.key('faq') + end + + end + end +end diff --git a/lib/stormy/helpers/utils.rb b/lib/stormy/helpers/utils.rb new file mode 100644 index 0000000..99161b4 --- /dev/null +++ b/lib/stormy/helpers/utils.rb @@ -0,0 +1,65 @@ +# Copyright 2012 Sami Samhuri + +require 'stormy/config' + +module Stormy + module Helpers + module Utils + + def config + @config ||= Stormy::Config.instance + end + + def redis + @redis ||= Redis.new + end + + def testing? + ENV['RACK_ENV'] == 'test' + end + + def production? + ENV['RACK_ENV'] == 'production' + end + + def development? + ENV['RACK_ENV'] == 'development' + end + + # JSON responses for API endpoints + + def ok(data = nil) + if data.nil? + { 'status' => 'ok' } + else + { 'status' => 'ok', 'data' => data } + end.to_json + end + + def fail(reason = nil) + if reason.nil? + { 'status' => 'fail' } + else + { 'status' => 'fail', 'reason' => reason } + end.to_json + end + + def bad_request + [400, "Bad request\n"] + end + + def not_authorized + [403, "Not authorized\n"] + end + + def base_url + @base_url ||= production? ? "http://dev.example.com:#{settings.port}/" : "http://localhost:#{settings.port}/" + end + + def url_for(*args) + "#{base_url}#{args.join('/')}" + end + + end + end +end diff --git a/lib/stormy/helpers/views.rb b/lib/stormy/helpers/views.rb new file mode 100644 index 0000000..2cbcf0f --- /dev/null +++ b/lib/stormy/helpers/views.rb @@ -0,0 +1,121 @@ +# Copyright 2012 Sami Samhuri + +require 'rdiscount' + +module Stormy + module Helpers + module Views + + def escape_html(s) + Rack::Utils::escape_html(s) + end + + def script(name) + if name.match(/^(https?:)?\/\//) + scripts << name + elsif production? + scripts << "/js-min/#{name}.js" + else + scripts << "/js/#{name}.js" + end + end + + def scripts + @page_scripts ||= [ + '//ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.min.js', + "/js#{production? ? '-min' : ''}/jquery.placeholder.js", + "/js#{production? ? '-min' : ''}/common.js" + ] + end + + def stylesheet(name) + if production? + stylesheets << "/css-min/#{name}.css" + else + stylesheets << "/css/#{name}.css" + end + end + + def stylesheets + @page_styles ||= ["/css#{production? ? '-min' : ''}/common.css"] + end + + def title(title = nil) + @page_title = title if title + @page_title + end + + def flash_message + if flash[:notice] + klass = 'notice' + message = flash[:notice] + elsif flash[:warning] + klass = 'warning' + message = flash[:warning] + elsif flash[:error] + klass = 'error' + message = flash[:error] + else + klass = flash.keys.first + message = flash[klass] if klass + end + if message + "
#{message}
" + end + end + + def format_dollars(amount, currency = 'CAD') + '%s $%.2f' % [currency, amount / 100.0] + end + + def format_date(date) + date.strftime("%B %e, %Y") + end + + def format_time(time) + time.strftime('%B %e, %Y %l:%M %p') + end + + def pad(n) + n < 10 ? "0#{n}" : "#{n}" + end + + def format_duration(duration) + mins = duration / 60 + secs = duration % 60 + "#{pad(mins)}:#{pad(secs)}" + end + + def ordinal_day(day) + th = case day + when 1 + 'st' + when 2 + 'nd' + when 3 + 'rd' + when 21 + 'st' + when 22 + 'nd' + when 23 + 'rd' + when 31 + 'st' + else + 'th' + end + "#{day}#{th}" + end + + def format_percent(percent) + "#{(100 * percent).to_i}%" + end + + def markdown(s) + RDiscount.new(s.to_s).to_html + end + + end + end +end diff --git a/lib/stormy/models.rb b/lib/stormy/models.rb new file mode 100644 index 0000000..8f0cb02 --- /dev/null +++ b/lib/stormy/models.rb @@ -0,0 +1,7 @@ +# Copyright 2012 Sami Samhuri + +require 'stormy/models/base' + +Dir[File.dirname(__FILE__) + '/models/*.rb'].each do |f| + require 'stormy/models/' + File.basename(f) +end diff --git a/lib/stormy/models/account.rb b/lib/stormy/models/account.rb new file mode 100644 index 0000000..269f7d1 --- /dev/null +++ b/lib/stormy/models/account.rb @@ -0,0 +1,226 @@ +# Copyright 2012 Sami Samhuri + +require 'bcrypt' +require 'uuid' + +module Stormy + module Models + class Account < Base + + class EmailTakenError < RuntimeError; end + class IncorrectPasswordError < RuntimeError; end + + name 'account' + + field :id, :required => true + + field :email, :type => :email, :required => true + field :first_name, :required => true, :updatable => true + field :last_name, :required => true, :updatable => true + field :phone, :type => :phone, :updatable => true + + field :hashed_password, :required => true + field :password + + field :created_timestamp, :type => :integer + field :email_verification_token, :nullify_if_blank => true + field :email_verified? + field :password_reset_token, :nullify_if_blank => true + + @@account_email_index_key = Stormy.key('index:account-email') + + + ### Class Methods + + def self.check_password(email, password) + id = id_from_email(email) + key = self.key(id) + if key + hashed_password = BCrypt::Password.new(redis.hget(key, 'hashed_password')) + id if hashed_password == password + end + end + + def self.email_taken?(email) + !! redis.hget(@@account_email_index_key, email.to_s.strip.downcase) + end + + def self.fetch_by_email(email) + if id = id_from_email(email) + fetch(id) + end + end + + def self.reset_password(email) + if key = key_from_email(email) + token = redis.hget(key, 'password_reset_token') + if token.blank? + token = UUID.generate + redis.hset(key, 'password_reset_token', token) + end + { 'name' => redis.hget(key, 'first_name'), + 'token' => token + } + end + end + + def self.use_password_reset_token(email, token) + if id = id_from_email(email) + key = key(id) + expected_token = redis.hget(key, 'password_reset_token') + if token == expected_token + redis.hdel(key, 'password_reset_token') + id + end + end + end + + def self.id_from_email(email) + redis.hget(@@account_email_index_key, email.strip.downcase) + end + + def self.verify_email(email, token) + if key = key_from_email(email) + expected_token = redis.hget(key, 'email_verification_token') + verified = token == expected_token + if verified + redis.hdel(key, 'email_verification_token') + redis.hset(key, 'email_verified', true) + end + verified + end + end + + def self.email_verified?(email) + if key = key_from_email(email) + redis.hget(key, 'email_verified') == 'true' + end + end + + + ### Private Class Methods + + def self.key_from_email(email) + key(id_from_email(email)) + end + + private_class_method :key_from_email + + + ### Instance Methods + + def initialize(fields = {}, options = {}) + super(fields, options) + + if fields['hashed_password'] + self.hashed_password = BCrypt::Password.new(fields['hashed_password']) + else + self.password = fields['password'] + end + end + + def create + raise EmailTakenError if email_taken? + + # new accounts get an id and timestamp + self.id = UUID.generate unless id.present? + self.created_timestamp = Time.now.to_i + + super + + create_email_verification_token + + # add to index + redis.hset(@@account_email_index_key, email.downcase, id) + + self + end + + def delete! + project_ids.each { |id| Project.delete!(id) } + super + redis.hdel(@@account_email_index_key, email.strip.downcase) + end + + def email_taken?(email = @email) + self.class.email_taken?(email) + end + + def create_email_verification_token + self.email_verification_token ||= UUID.generate + redis.hset(key, 'email_verification_token', email_verification_token) + email_verification_token + end + + def password + @password ||= BCrypt::Password.new(hashed_password) + end + + def password=(new_password) + if new_password.present? + self.hashed_password = BCrypt::Password.create(new_password) + @password = nil + end + end + + def name + "#{first_name} #{last_name}" + end + + def count_projects + redis.scard(project_ids_key) + end + + def project_ids + redis.smembers(project_ids_key) + end + + def projects + project_ids.map { |pid| Project.fetch(pid) } + end + + def sorted_projects + @sorted_projects ||= projects.sort { |a,b| a.created_timestamp <=> b.created_timestamp } + end + + def add_project_id(id) + redis.sadd(project_ids_key, id) + end + + def remove_project_id(id) + redis.srem(project_ids_key, id) + end + + def update_email(new_email) + new_email = new_email.strip + if email != new_email + raise EmailTakenError if new_email.downcase != email.downcase && email_taken?(new_email) + raise InvalidDataError.new({ 'email' => 'invalid' }) unless field_valid?('email', new_email) + if email.downcase != new_email.downcase + self.email_verified = false + redis.hdel(@@account_email_index_key, email.downcase) + redis.hset(@@account_email_index_key, new_email.downcase, id) + end + self.email = new_email + save! + end + end + + def update_password(old_password, new_password) + hashed_password = BCrypt::Password.new(redis.hget(key, 'hashed_password')) + raise IncorrectPasswordError unless hashed_password == old_password + raise InvalidDataError.new({ 'password' => 'missing' }) if new_password.blank? + redis.hset(key, 'hashed_password', BCrypt::Password.create(new_password)) + self.password = new_password + end + + + private + + def project_ids_key + @project_ids_key ||= "#{key}:project-ids" + end + + end + end +end \ No newline at end of file diff --git a/lib/stormy/models/admin.rb b/lib/stormy/models/admin.rb new file mode 100644 index 0000000..961073d --- /dev/null +++ b/lib/stormy/models/admin.rb @@ -0,0 +1,124 @@ +# Copyright 2012 Sami Samhuri + +require 'bcrypt' +require 'uuid' + +module Stormy + module Models + class Admin < Base + + class EmailTakenError < RuntimeError; end + class IncorrectPasswordError < RuntimeError; end + + name 'admin' + + field :id, :required => true + + field :name, :required => true, :updatable => true + field :email, :type => :email, :required => true + field :hashed_password, :required => true + field :password + + @@admin_email_index_key = Stormy.key('index:admin-email') + + + ### Class Methods + + def self.key_from_email(email) + key(id_from_email(email)) + end + + def self.check_password(email, password) + id = id_from_email(email) + key = self.key(id) + if key + hashed_password = BCrypt::Password.new(redis.hget(key, 'hashed_password')) + id if hashed_password == password + end + end + + def self.email_taken?(email) + !! redis.hget(@@admin_email_index_key, email.to_s.strip.downcase) + end + + def self.fetch_by_email(email) + key = key_from_email(email) + new(redis.hgetall(key)) if key + end + + def self.id_from_email(email) + redis.hget(@@admin_email_index_key, email.strip.downcase) + end + + + ### Instance Methods + + def initialize(fields = {}, options = {}) + super(fields, options) + + if fields['hashed_password'] + self.hashed_password = BCrypt::Password.new(fields['hashed_password']) + else + self.password = fields['password'] + end + end + + def create + raise EmailTakenError if email_taken? + + self.id = UUID.generate unless id.present? + + super + + # add to index + redis.hset(@@admin_email_index_key, @email.downcase, @id) + + self + end + + def delete! + if super + redis.hdel(@@admin_email_index_key, @email.strip.downcase) + end + end + + def email_taken?(email = @email) + self.class.email_taken?(email) + end + + def password + @password ||= BCrypt::Password.new(hashed_password) + end + + def password=(new_password) + if new_password.present? + self.hashed_password = BCrypt::Password.create(new_password) + @password = nil + end + end + + def update_email(new_email) + new_email = new_email.strip + if email != new_email + raise EmailTakenError if new_email.downcase != email.downcase && email_taken?(new_email) + raise InvalidDataError.new({ 'email' => 'invalid' }) unless field_valid?('email', new_email) + if email.downcase != new_email.downcase + redis.hdel(@@admin_email_index_key, email.downcase) + redis.hset(@@admin_email_index_key, new_email.downcase, id) + end + self.email = new_email + save! + end + end + + def update_password(old_password, new_password) + hashed_password = BCrypt::Password.new(redis.hget(key, 'hashed_password')) + raise IncorrectPasswordError unless hashed_password == old_password + raise InvalidDataError.new({ 'password' => 'missing' }) if new_password.blank? + redis.hset(key, 'hashed_password', BCrypt::Password.create(new_password)) + self.password = new_password + end + + end + end +end \ No newline at end of file diff --git a/lib/stormy/models/base.rb b/lib/stormy/models/base.rb new file mode 100644 index 0000000..28529e7 --- /dev/null +++ b/lib/stormy/models/base.rb @@ -0,0 +1,404 @@ +# Copyright 2012 Sami Samhuri + +require 'json' +require 'redis' +require 'uuid' + +module Stormy + module Models + class Base + + class InvalidDataError < RuntimeError + attr_reader :fields + def initialize(invalid_fields = {}) + @fields = invalid_fields + end + end + + def self.clean_number(number) + number.gsub(/[^\d]/, '').sub(/^1/, '') + end + + # Allows any 10 digit number in North America, or an empty field (for account creation). + PhoneNumberValidator = proc do |number| + if number.present? + clean_number(number).length == 10 + else + true + end + end + + # Liberal email address regex + EmailAddressValidator = proc { |email| email =~ /^[^@]+@[^.@]+(\.[^.@]+)+$/ } + + + # Only changed fields are persisted on save + attr_reader :changed_fields + + + ##################### + ### Class Methods ### + ##################### + + @@redis = Redis.new + + def self.redis + @@redis + end + + + # Define or retrieve the name of this model. + def self.name(name = nil) + if name + @model_name = name + end + @model_name + end + + + # Hash of all fields. + def self.fields + @fields ||= {} + end + + + # Define fields like so: + # + # field :id, :type => :integer, :required => true + # field :name, :required => true, :updatable => true + # field :verified? + # + # Defaults: { + # :type => :string, + # :required => false, + # :updatable => false, + # :validator => nil, # with some exceptions + # :default => {}, + # :nullify_if_blank => false + # } + # + # Types: :string, :integer, :boolean, :json, as well as + # :email and :phone which are string aliases with the + # appropriate validations. String fields have an option + # :nullify_if_blank that will initialize and set fields + # to `nil` if they are empty. + # + # If an `integer` is required it must be greater than zero. + # The required option has no effect on boolean fields. + # + # Fields with names ending with question mark are boolean. + # + # JSON fields accept a :default option used to initialize + # a JSON field, and also when a parse fails. + # + # Attribute accessors are defined for each field and boolean + # fields get a predicate method as well, e.g. verified? + # + # Changed fields are tracked and only changed fields are + # persisted on a `save`. + # + def self.field(name, options = {}) + if name.to_s.ends_with?('?') + options[:type] = :boolean + name = name.to_s[0..-2] + end + + name = name.to_sym + options[:type] ||= :string + + case options[:type] + when :email + options[:validator] ||= EmailAddressValidator + options[:type] = :string + when :phone + options[:validator] ||= PhoneNumberValidator + options[:type] = :string + when :json + options[:default] ||= {} + end + + fields[name] = options + define_method(name) do + instance_variable_get("@#{name}") + end + + case options[:type] + when :string + define_method("#{name}=") do |value| + s = + if options[:nullify_if_blank] && value.blank? + nil + else + value.to_s.strip + end + instance_variable_set("@#{name}", s) + changed_fields[name] = s + end + + when :integer + define_method("#{name}=") do |value| + i = value.to_i + instance_variable_set("@#{name}", i) + changed_fields[name] = i + end + + when :boolean + define_method("#{name}=") do |value| + b = value == 'true' || value == true + instance_variable_set("@#{name}", b) + changed_fields[name] = b + end + define_method("#{name}?") do + instance_variable_get("@#{name}") + end + + when :json + define_method(name) do + unless value = instance_variable_get("@#{name}") + value = options[:default].dup + send("#{name}=", value) + end + value + end + define_method("#{name}=") do |value| + obj = + if value.is_a?(String) + if value.length > 0 + JSON.parse(value) + else + options[:default].dup + end + else + value + end + instance_variable_set("@#{name}", obj) + changed_fields[name] = obj + end + + else + define_method("#{name}=") do |value| + instance_variable_set("@#{name}", value) + changed_fields[name] = value + end + end + end + + + # internal + def self.model_ids_key + @model_ids_key ||= Stormy.key("#{@model_name}-ids") + end + + + def self.create(fields = {}) + new(fields).create + end + + def self.delete!(id) + if obj = fetch(id) + obj.delete! + end + end + + def self.exists?(id) + redis.sismember(model_ids_key, id) + end + + def self.fetch(id) + if id && exists?(id) + new(redis.hgetall(key(id)), :fetched => true) + end + end + + def self.fetch_all + list_ids.map { |id| fetch(id) } + end + + def self.key(id) + Stormy.key(@model_name, id) if id + end + + def self.list_ids + redis.smembers(model_ids_key) + end + + def self.count + redis.scard(model_ids_key) + end + + + ### Instance Methods + + attr_accessor :redis + + def initialize(fields = {}, options = {}) + self.redis = self.class.redis + + fields = fields.symbolize_keys + field_names.each do |name| + send("#{name}=", fields[name]) + end + + # no changed fields yet if we have been fetched + if options[:fetched] + @changed_fields = {} + end + end + + def create + # raises if invalid + save + add_to_index + self + end + + def delete! + if redis.srem(self.class.model_ids_key, id) + redis.del(key) + end + end + + def reload! + initialize(redis.hgetall(key)) + self + end + + # Convenient defaults for performing safe updates. + def update!(fields, options = {}) + options[:validate] = false unless options.has_key?(:validate) + options[:all] = true unless options.has_key?(:all) + update(fields, options) + end + + # The `update` method only updates fields marked updatable. + # Unless you pass in :all => true, then all fields are + # updated. + # + # There's also a :validate flag. + # + def update(fields, options = {}) + options[:validate] = true unless options.has_key?(:validate) + fields.each do |name, value| + if options[:all] || field_updatable?(name) + send("#{name}=", value) + end + end + if options[:validate] + save + else + save! + end + end + + def save + validate + save! + end + + def save! + # always update JSON fields because they can be updated without our knowledge + field_names.each do |name| + if field_type(name) == :json + changed_fields[name] = send(name) + end + end + + fields = changed_fields.map do |name, value| + if field_type(name) == :json && !value.is_a?(String) + [name, JSON.fast_generate(value || field_default(name))] + else + [name, value] + end + end + if fields.length > 0 + redis.hmset(key, *fields.flatten) + end + @changed_fields = {} + end + + def validate + invalid_fields = field_names.inject({}) do |fields, name| + if field_validates?(name) + result = validate_field(name, send(name)) + fields[name] = result[:reason] unless result[:valid] + end + fields + end + if invalid_fields.length > 0 + raise InvalidDataError.new(invalid_fields.stringify_keys) + end + end + + + private + + def key + @key ||= self.class.key(self.id) + end + + def add_to_index + redis.sadd(self.class.model_ids_key, self.id) + end + + def changed_fields + @changed_fields ||= {} + end + + def clean_number(number) + self.class.clean_number(number) + end + + def field_names + self.class.fields.keys + end + + def field_type(name) + self.class.fields[name.to_sym][:type] + end + + def field_updatable?(name) + self.class.fields[name.to_sym][:updatable] + end + + def validate_field(name, value) + valid = true + reason = nil + field = self.class.fields[name.to_sym] + type = field[:type] + if field[:required] + case + when type == :string && value.blank? + valid = false + reason = 'missing' + when type == :integer && value.to_i <= 0 + valid = false + reason = 'missing' + when type == :json && value.blank? + valid = false + reason = 'missing' + end + end + if valid && validator = field[:validator] + valid = validator.call(value) + reason = 'invalid' + end + { :valid => valid, :reason => reason } + end + + def field_valid?(name, value) + result = validate_field(name, value) + result[:valid] + end + + def field_validates?(name) + field = self.class.fields[name.to_sym] + field[:required] || field[:validator] + end + + def field_default(name) + self.class.fields[name][:default] + end + + end + end +end diff --git a/lib/stormy/models/project.rb b/lib/stormy/models/project.rb new file mode 100644 index 0000000..a319b68 --- /dev/null +++ b/lib/stormy/models/project.rb @@ -0,0 +1,162 @@ +# Copyright 2012 Sami Samhuri + +require 'digest/sha1' +require 'fileutils' +require 'RMagick' + +module Stormy + module Models + class Project < Base + + # max width or height in pixels + MaxPhotoSize = 1200 + MaxPhotos = 10 + + name 'project' + + field :id, :required => true + + field :name, :required => true, :updatable => true + + field :account_id + field :created_timestamp, :type => :integer, :required => true + field :fizzled_timestamp, :type => :integer + field :funded_timestamp, :type => :integer + field :photo_ids, :type => :json, :default => [] + + @@project_name_index_key = Stormy.key('index:project-name') + + def self.fetch_by_name(name) + if id = id_from_name(name) + fetch(id) + end + end + + def self.id_from_name(name) + redis.hget(@@project_name_index_key, name.strip.downcase) + end + + def create + self.id = UUID.generate unless id.present? + self.created_timestamp = Time.now.to_i + + super + + # add to index + redis.hset(@@project_name_index_key, name.downcase, id) + + account.add_project_id(id) if account + + self + end + + def delete! + if super + remove_all_photos! + account.remove_project_id(id) if account + redis.hdel(@@project_name_index_key, name.strip.downcase) + end + end + + def funded? + funded_timestamp > 0 + end + + def funded! + self.funded_timestamp = Time.now.to_i + save! + end + + def fizzled? + fizzled_timestamp > 0 + end + + def fizzled! + self.fizzled_timestamp = Time.now.to_i + save! + end + + def count_photos + photo_ids.length + end + + def add_photo(path) + unless count_photos >= MaxPhotos + photo = Magick::Image.read(path).first + photo.auto_orient! + photo.change_geometry("#{MaxPhotoSize}x#{MaxPhotoSize}>") { |cols, rows, img| img.resize!(cols, rows) } + photo.format = 'jpg' + + FileUtils.mkdir_p(photo_dir) unless File.exists?(photo_dir) + + photo_id = Digest::SHA1.hexdigest(photo.to_blob) + photo.write(photo_path(photo_id)) { self.quality = 80 } + + photo_ids << photo_id + save! + + photo_data(photo_id) + end + end + + def remove_photo(photo_id) + path = photo_path(photo_id) + if i = photo_ids.index(photo_id) + photo_ids.delete_at(i) + end + FileUtils.rm(path) if File.exists?(path) && !photo_ids.include?(photo_id) + save! + end + + def photo_paths + photo_ids.map { |id| photo_path(id) } + end + + def photo_urls + photo_ids.map { |photo_id| "/photos/#{id}/#{photo_id}.jpg" } + end + + def photo_url(photo_id) + "/photos/#{id}/#{photo_id}.jpg" + end + + def photo_data(photo_id) + { + 'id' => photo_id, + 'url' => photo_url(photo_id) + } + end + + def photos + photo_ids.map { |id| photo_data(id) } + end + + def account + if account_id + @account ||= Account.fetch(account_id) + end + end + + + private + + def photo_dir + File.join(Stormy::PhotoDir, @id) + end + + def photo_path(id) + File.join(photo_dir, "#{id}.jpg") + end + + def photos_key + "#{key}:photos" + end + + def remove_all_photos! + FileUtils.rm_rf(photo_dir) if File.exists?(photo_dir) + self.photo_ids = [] + end + + end + end +end diff --git a/lib/stormy/server.rb b/lib/stormy/server.rb new file mode 100644 index 0000000..ac7eabd --- /dev/null +++ b/lib/stormy/server.rb @@ -0,0 +1,93 @@ +# Copyright 2012 Sami Samhuri + +require 'time' + +require 'sinatra' +require 'sinatra/cookie_thief' +require 'sinatra/flash' + +require 'erubis' +require 'json' +require 'pony' +require 'redis' +require 'redis-store' +require 'uuid' + +# Ruby extensions +require 'hash-ext' + +require 'stormy/models' +require 'stormy/controllers' +require 'stormy/helpers' + +module Stormy + + class Server < Sinatra::Base + + set :port, 5000 + + configure :production do + enable :dump_errors + + # compress responses + use Rack::Deflater + + # cache static files for an hour + set :static_cache_control, [ :must_revalidate, :max_age => 60 ] + end + + enable :logging + + # serve static files from /public, views from /views + set :public_folder, File.dirname(__FILE__) + '/../../public' + set :views, File.dirname(__FILE__) + '/../../views' + + # Automatically escape HTML + set :erb, :escape_html => true + + # disable Rack::Protection, JsonCsrf breaks things + disable :protection + + # disable cookies for static files + register Sinatra::CookieThief + + register Sinatra::Flash + + use Rack::Session::Redis, { + :httponly => true, + :secret => '38066be6a9d388626e045be2351d26918608d53c', + :expire_after => 8.hours + } + + helpers Helpers::Accounts + helpers Helpers::Admin + helpers Helpers::Authorization + helpers Helpers::FAQ + helpers Helpers::Utils + helpers Helpers::Views + + not_found do + erb :'not-found' + end + + error do + if production? + body = erb(:'email/error-notification', :layout => false, :locals => { + :account => current_account, + :project => current_project, + :admin => current_admin, + :error => env['sinatra.error'] + }) + Pony.mail({ + :to => 'admin@example.com', + :from => 'info@example.com', + :subject => "[stormy] #{@error.class}: #{@error.message}", + :headers => { 'Content-Type' => 'text/html' }, + :body => body + }) + end + erb :error + end + + end +end diff --git a/public/css/account.css b/public/css/account.css new file mode 100644 index 0000000..87d05be --- /dev/null +++ b/public/css/account.css @@ -0,0 +1,53 @@ +table#account +{ width: 70% +; margin: auto +; border-spacing: 0 0.7em +} + +th +{ text-align: right +; padding-right: 1em +; vertical-align: top +} + +th, td { color: #405e83 } + +hr +{ width: 70% +; margin: 2em auto +} + +#taken, #invalid +{ font-weight: bold +; color: #600 +; text-align: center +; padding: 0.3em 0.5em +} + +div.indented p, +div.indented form, +p.indented +{ margin-left: 10em } + +#content a { color: #405e83 } + +.verified { color: #4db269; float: right } +.unverified { color: #aa0000 } + +#sending-email-verification +{ display: none } + +table#change-password +{ display: none +; margin-left: 10em +} + +.spinner +{ display: none +; margin: 0 0.5em +} + +#password-changed +{ display: none +; margin-left: 10em +} diff --git a/public/css/admin.css b/public/css/admin.css new file mode 100644 index 0000000..b7b4400 --- /dev/null +++ b/public/css/admin.css @@ -0,0 +1,78 @@ +body +{ font-size: 1.2em +; font-family: 'Helvetica Neue', Roboto, Helvetica, Verdana, sans-serif +} + +h1 { float: left } + +#sign-in +{ text-align: center +; width: 50% +; margin: auto +; padding: 1em +; background-color: #d0d0d0 +; clear: both +} + +#sign-out +{ float: right +; font-size: 1.0em +} + +#content +{ float: right +; text-align: left +; width: 70% +; margin-right: 2em +; padding-left: 2em +; border-left: solid 2px #ddd +} + +#nav +{ width: 200px +; list-style-type: none +; float: none !important +} + +#nav li a { text-decoration: underline } + +.section { border-bottom: solid 1px #ddd } +.section:last-child { border-bottom: none } + +.subtle +{ color: #bbb +; margin-left: 20em +} + +#flash +{ border: solid 1px #bbb +; font-weight: bold +; border: solid 1px #bbb +; font-weight: bold +; padding: 0.7em 0.4em +; margin: 1em auto +; width: 70% +; font-size: 1.2em +; text-align: center +; clear: left +; border-radius: 5px +} + +.notice { background-color: #d3edc5 } +.warning { background-color: #ffc } +#flash.error { background-color: #fcc } + + +/* Accounts */ + +#accounts { border-collapse: collapse } +#accounts th { background-color: #def } +#accounts tr:nth-child(even) { background-color: #eee } + +#accounts td, +#accounts th +{ text-align: center +; padding: 0.2em 0.3em +} + +td.name { text-align: left } diff --git a/public/css/bootstrap-responsive.css b/public/css/bootstrap-responsive.css new file mode 100644 index 0000000..4b032cd --- /dev/null +++ b/public/css/bootstrap-responsive.css @@ -0,0 +1,567 @@ +/*! + * Bootstrap Responsive v2.0.0 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ +.hidden { + display: none; + visibility: hidden; +} +@media (max-width: 480px) { + .nav-collapse { + -webkit-transform: translate3d(0, 0, 0); + } + .page-header h1 small { + display: block; + line-height: 18px; + } + input[class*="span"], + select[class*="span"], + textarea[class*="span"], + .uneditable-input { + display: block; + width: 100%; + height: 28px; + /* Make inputs at least the height of their button counterpart */ + + /* Makes inputs behave like true block-level elements */ + + -webkit-box-sizing: border-box; + /* Older Webkit */ + + -moz-box-sizing: border-box; + /* Older FF */ + + -ms-box-sizing: border-box; + /* IE8 */ + + box-sizing: border-box; + /* CSS3 spec*/ + + } + .input-prepend input[class*="span"], .input-append input[class*="span"] { + width: auto; + } + input[type="checkbox"], input[type="radio"] { + border: 1px solid #ccc; + } + .form-horizontal .control-group > label { + float: none; + width: auto; + padding-top: 0; + text-align: left; + } + .form-horizontal .controls { + margin-left: 0; + } + .form-horizontal .control-list { + padding-top: 0; + } + .form-horizontal .form-actions { + padding-left: 10px; + padding-right: 10px; + } + .modal { + position: absolute; + top: 10px; + left: 10px; + right: 10px; + width: auto; + margin: 0; + } + .modal.fade.in { + top: auto; + } + .modal-header .close { + padding: 10px; + margin: -10px; + } + .carousel-caption { + position: static; + } +} +@media (max-width: 768px) { + .container { + width: auto; + padding: 0 20px; + } + .row-fluid { + width: 100%; + } + .row { + margin-left: 0; + } + .row > [class*="span"], .row-fluid > [class*="span"] { + float: none; + display: block; + width: auto; + margin: 0; + } +} +@media (min-width: 768px) and (max-width: 980px) { + .row { + margin-left: -20px; + *zoom: 1; + } + .row:before, .row:after { + display: table; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + margin-left: 20px; + } + .span1 { + width: 42px; + } + .span2 { + width: 104px; + } + .span3 { + width: 166px; + } + .span4 { + width: 228px; + } + .span5 { + width: 290px; + } + .span6 { + width: 352px; + } + .span7 { + width: 414px; + } + .span8 { + width: 476px; + } + .span9 { + width: 538px; + } + .span10 { + width: 600px; + } + .span11 { + width: 662px; + } + .span12, .container { + width: 724px; + } + .offset1 { + margin-left: 82px; + } + .offset2 { + margin-left: 144px; + } + .offset3 { + margin-left: 206px; + } + .offset4 { + margin-left: 268px; + } + .offset5 { + margin-left: 330px; + } + .offset6 { + margin-left: 392px; + } + .offset7 { + margin-left: 454px; + } + .offset8 { + margin-left: 516px; + } + .offset9 { + margin-left: 578px; + } + .offset10 { + margin-left: 640px; + } + .offset11 { + margin-left: 702px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, .row-fluid:after { + display: table; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid > [class*="span"] { + float: left; + margin-left: 2.762430939%; + } + .row-fluid > [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .span1 { + width: 5.801104972%; + } + .row-fluid .span2 { + width: 14.364640883%; + } + .row-fluid .span3 { + width: 22.928176794%; + } + .row-fluid .span4 { + width: 31.491712705%; + } + .row-fluid .span5 { + width: 40.055248616%; + } + .row-fluid .span6 { + width: 48.618784527%; + } + .row-fluid .span7 { + width: 57.182320438000005%; + } + .row-fluid .span8 { + width: 65.74585634900001%; + } + .row-fluid .span9 { + width: 74.30939226%; + } + .row-fluid .span10 { + width: 82.87292817100001%; + } + .row-fluid .span11 { + width: 91.436464082%; + } + .row-fluid .span12 { + width: 99.999999993%; + } + input.span1, textarea.span1, .uneditable-input.span1 { + width: 32px; + } + input.span2, textarea.span2, .uneditable-input.span2 { + width: 94px; + } + input.span3, textarea.span3, .uneditable-input.span3 { + width: 156px; + } + input.span4, textarea.span4, .uneditable-input.span4 { + width: 218px; + } + input.span5, textarea.span5, .uneditable-input.span5 { + width: 280px; + } + input.span6, textarea.span6, .uneditable-input.span6 { + width: 342px; + } + input.span7, textarea.span7, .uneditable-input.span7 { + width: 404px; + } + input.span8, textarea.span8, .uneditable-input.span8 { + width: 466px; + } + input.span9, textarea.span9, .uneditable-input.span9 { + width: 528px; + } + input.span10, textarea.span10, .uneditable-input.span10 { + width: 590px; + } + input.span11, textarea.span11, .uneditable-input.span11 { + width: 652px; + } + input.span12, textarea.span12, .uneditable-input.span12 { + width: 714px; + } +} +@media (max-width: 980px) { + body { + padding-top: 0; + } + .navbar-fixed-top { + position: static; + margin-bottom: 18px; + } + .navbar-fixed-top .navbar-inner { + padding: 5px; + } + .navbar .container { + width: auto; + padding: 0; + } + .navbar .brand { + padding-left: 10px; + padding-right: 10px; + margin: 0 0 0 -5px; + } + .navbar .nav-collapse { + clear: left; + } + .navbar .nav { + float: none; + margin: 0 0 9px; + } + .navbar .nav > li { + float: none; + } + .navbar .nav > li > a { + margin-bottom: 2px; + } + .navbar .nav > .divider-vertical { + display: none; + } + .navbar .nav > li > a, .navbar .dropdown-menu a { + padding: 6px 15px; + font-weight: bold; + color: #999999; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + } + .navbar .dropdown-menu li + li a { + margin-bottom: 2px; + } + .navbar .nav > li > a:hover, .navbar .dropdown-menu a:hover { + background-color: #222222; + } + .navbar .dropdown-menu { + position: static; + top: auto; + left: auto; + float: none; + display: block; + max-width: none; + margin: 0 15px; + padding: 0; + background-color: transparent; + border: none; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + } + .navbar .dropdown-menu:before, .navbar .dropdown-menu:after { + display: none; + } + .navbar .dropdown-menu .divider { + display: none; + } + .navbar-form, .navbar-search { + float: none; + padding: 9px 15px; + margin: 9px 0; + border-top: 1px solid #222222; + border-bottom: 1px solid #222222; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + } + .navbar .nav.pull-right { + float: none; + margin-left: 0; + } + .navbar-static .navbar-inner { + padding-left: 10px; + padding-right: 10px; + } + .btn-navbar { + display: block; + } + .nav-collapse { + overflow: hidden; + height: 0; + } +} +@media (min-width: 980px) { + .nav-collapse.collapse { + height: auto !important; + } +} +@media (min-width: 1200px) { + .row { + margin-left: -30px; + *zoom: 1; + } + .row:before, .row:after { + display: table; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + margin-left: 30px; + } + .span1 { + width: 70px; + } + .span2 { + width: 170px; + } + .span3 { + width: 270px; + } + .span4 { + width: 370px; + } + .span5 { + width: 470px; + } + .span6 { + width: 570px; + } + .span7 { + width: 670px; + } + .span8 { + width: 770px; + } + .span9 { + width: 870px; + } + .span10 { + width: 970px; + } + .span11 { + width: 1070px; + } + .span12, .container { + width: 1170px; + } + .offset1 { + margin-left: 130px; + } + .offset2 { + margin-left: 230px; + } + .offset3 { + margin-left: 330px; + } + .offset4 { + margin-left: 430px; + } + .offset5 { + margin-left: 530px; + } + .offset6 { + margin-left: 630px; + } + .offset7 { + margin-left: 730px; + } + .offset8 { + margin-left: 830px; + } + .offset9 { + margin-left: 930px; + } + .offset10 { + margin-left: 1030px; + } + .offset11 { + margin-left: 1130px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, .row-fluid:after { + display: table; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid > [class*="span"] { + float: left; + margin-left: 2.564102564%; + } + .row-fluid > [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .span1 { + width: 5.982905983%; + } + .row-fluid .span2 { + width: 14.529914530000001%; + } + .row-fluid .span3 { + width: 23.076923077%; + } + .row-fluid .span4 { + width: 31.623931624%; + } + .row-fluid .span5 { + width: 40.170940171000005%; + } + .row-fluid .span6 { + width: 48.717948718%; + } + .row-fluid .span7 { + width: 57.264957265%; + } + .row-fluid .span8 { + width: 65.81196581200001%; + } + .row-fluid .span9 { + width: 74.358974359%; + } + .row-fluid .span10 { + width: 82.905982906%; + } + .row-fluid .span11 { + width: 91.45299145300001%; + } + .row-fluid .span12 { + width: 100%; + } + input.span1, textarea.span1, .uneditable-input.span1 { + width: 60px; + } + input.span2, textarea.span2, .uneditable-input.span2 { + width: 160px; + } + input.span3, textarea.span3, .uneditable-input.span3 { + width: 260px; + } + input.span4, textarea.span4, .uneditable-input.span4 { + width: 360px; + } + input.span5, textarea.span5, .uneditable-input.span5 { + width: 460px; + } + input.span6, textarea.span6, .uneditable-input.span6 { + width: 560px; + } + input.span7, textarea.span7, .uneditable-input.span7 { + width: 660px; + } + input.span8, textarea.span8, .uneditable-input.span8 { + width: 760px; + } + input.span9, textarea.span9, .uneditable-input.span9 { + width: 860px; + } + input.span10, textarea.span10, .uneditable-input.span10 { + width: 960px; + } + input.span11, textarea.span11, .uneditable-input.span11 { + width: 1060px; + } + input.span12, textarea.span12, .uneditable-input.span12 { + width: 1160px; + } + .thumbnails { + margin-left: -30px; + } + .thumbnails > li { + margin-left: 30px; + } +} diff --git a/public/css/bootstrap.css b/public/css/bootstrap.css new file mode 100755 index 0000000..2ee50bf --- /dev/null +++ b/public/css/bootstrap.css @@ -0,0 +1,3932 @@ +/*! + * Bootstrap v2.0.0 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block; +} +audio, canvas, video { + display: inline-block; + *display: inline; + *zoom: 1; +} +audio:not([controls]) { + display: none; +} +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +a:hover, a:active { + outline: 0; +} +sub, sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} +sup { + top: -0.5em; +} +sub { + bottom: -0.25em; +} +img { + max-width: 100%; + height: auto; + border: 0; + -ms-interpolation-mode: bicubic; +} +button, +input, +select, +textarea { + margin: 0; + font-size: 100%; + vertical-align: middle; +} +button, input { + *overflow: visible; + line-height: normal; +} +button::-moz-focus-inner, input::-moz-focus-inner { + padding: 0; + border: 0; +} +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} +input[type="search"] { + -webkit-appearance: textfield; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} +textarea { + overflow: auto; + vertical-align: top; +} +body { + margin: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 18px; + color: #333333; + background-color: #ffffff; +} +a { + color: #0088cc; + text-decoration: none; +} +a:hover { + color: #005580; + text-decoration: underline; +} +.row { + margin-left: -20px; + *zoom: 1; +} +.row:before, .row:after { + display: table; + content: ""; +} +.row:after { + clear: both; +} +[class*="span"] { + float: left; + margin-left: 20px; +} +.span1 { + width: 60px; +} +.span2 { + width: 140px; +} +.span3 { + width: 220px; +} +.span4 { + width: 300px; +} +.span5 { + width: 380px; +} +.span6 { + width: 460px; +} +.span7 { + width: 540px; +} +.span8 { + width: 620px; +} +.span9 { + width: 700px; +} +.span10 { + width: 780px; +} +.span11 { + width: 860px; +} +.span12, .container { + width: 940px; +} +.offset1 { + margin-left: 100px; +} +.offset2 { + margin-left: 180px; +} +.offset3 { + margin-left: 260px; +} +.offset4 { + margin-left: 340px; +} +.offset5 { + margin-left: 420px; +} +.offset6 { + margin-left: 500px; +} +.offset7 { + margin-left: 580px; +} +.offset8 { + margin-left: 660px; +} +.offset9 { + margin-left: 740px; +} +.offset10 { + margin-left: 820px; +} +.offset11 { + margin-left: 900px; +} +.row-fluid { + width: 100%; + *zoom: 1; +} +.row-fluid:before, .row-fluid:after { + display: table; + content: ""; +} +.row-fluid:after { + clear: both; +} +.row-fluid > [class*="span"] { + float: left; + margin-left: 2.127659574%; +} +.row-fluid > [class*="span"]:first-child { + margin-left: 0; +} +.row-fluid .span1 { + width: 6.382978723%; +} +.row-fluid .span2 { + width: 14.89361702%; +} +.row-fluid .span3 { + width: 23.404255317%; +} +.row-fluid .span4 { + width: 31.914893614%; +} +.row-fluid .span5 { + width: 40.425531911%; +} +.row-fluid .span6 { + width: 48.93617020799999%; +} +.row-fluid .span7 { + width: 57.446808505%; +} +.row-fluid .span8 { + width: 65.95744680199999%; +} +.row-fluid .span9 { + width: 74.468085099%; +} +.row-fluid .span10 { + width: 82.97872339599999%; +} +.row-fluid .span11 { + width: 91.489361693%; +} +.row-fluid .span12 { + width: 99.99999998999999%; +} +.container { + width: 940px; + margin-left: auto; + margin-right: auto; + *zoom: 1; +} +.container:before, .container:after { + display: table; + content: ""; +} +.container:after { + clear: both; +} +.container-fluid { + padding-left: 20px; + padding-right: 20px; + *zoom: 1; +} +.container-fluid:before, .container-fluid:after { + display: table; + content: ""; +} +.container-fluid:after { + clear: both; +} +p { + margin: 0 0 9px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 18px; +} +p small { + font-size: 11px; + color: #999999; +} +.lead { + margin-bottom: 18px; + font-size: 20px; + font-weight: 200; + line-height: 27px; +} +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + font-weight: bold; + color: #333333; + text-rendering: optimizelegibility; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small { + font-weight: normal; + color: #999999; +} +h1 { + font-size: 30px; + line-height: 36px; +} +h1 small { + font-size: 18px; +} +h2 { + font-size: 24px; + line-height: 36px; +} +h2 small { + font-size: 18px; +} +h3 { + line-height: 27px; + font-size: 18px; +} +h3 small { + font-size: 14px; +} +h4, h5, h6 { + line-height: 18px; +} +h4 { + font-size: 14px; +} +h4 small { + font-size: 12px; +} +h5 { + font-size: 12px; +} +h6 { + font-size: 11px; + color: #999999; + text-transform: uppercase; +} +.page-header { + padding-bottom: 17px; + margin: 18px 0; + border-bottom: 1px solid #eeeeee; +} +.page-header h1 { + line-height: 1; +} +ul, ol { + padding: 0; + margin: 0 0 9px 25px; +} +ul ul, +ul ol, +ol ol, +ol ul { + margin-bottom: 0; +} +ul { + list-style: disc; +} +ol { + list-style: decimal; +} +li { + line-height: 18px; +} +ul.unstyled { + margin-left: 0; + list-style: none; +} +dl { + margin-bottom: 18px; +} +dt, dd { + line-height: 18px; +} +dt { + font-weight: bold; +} +dd { + margin-left: 9px; +} +hr { + margin: 18px 0; + border: 0; + border-top: 1px solid #e5e5e5; + border-bottom: 1px solid #ffffff; +} +strong { + font-weight: bold; +} +em { + font-style: italic; +} +.muted { + color: #999999; +} +abbr { + font-size: 90%; + text-transform: uppercase; + border-bottom: 1px dotted #ddd; + cursor: help; +} +blockquote { + padding: 0 0 0 15px; + margin: 0 0 18px; + border-left: 5px solid #eeeeee; +} +blockquote p { + margin-bottom: 0; + font-size: 16px; + font-weight: 300; + line-height: 22.5px; +} +blockquote small { + display: block; + line-height: 18px; + color: #999999; +} +blockquote small:before { + content: '\2014 \00A0'; +} +blockquote.pull-right { + float: right; + padding-left: 0; + padding-right: 15px; + border-left: 0; + border-right: 5px solid #eeeeee; +} +blockquote.pull-right p, blockquote.pull-right small { + text-align: right; +} +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} +address { + display: block; + margin-bottom: 18px; + line-height: 18px; + font-style: normal; +} +small { + font-size: 100%; +} +cite { + font-style: normal; +} +code, pre { + padding: 0 3px 2px; + font-family: Menlo, Monaco, "Courier New", monospace; + font-size: 12px; + color: #333333; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +code { + padding: 3px 4px; + color: #d14; + background-color: #f7f7f9; + border: 1px solid #e1e1e8; +} +pre { + display: block; + padding: 8.5px; + margin: 0 0 9px; + font-size: 12px; + line-height: 18px; + background-color: #f5f5f5; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + white-space: pre; + white-space: pre-wrap; + word-break: break-all; +} +pre.prettyprint { + margin-bottom: 18px; +} +pre code { + padding: 0; + background-color: transparent; +} +.label { + padding: 1px 3px 2px; + font-size: 9.75px; + font-weight: bold; + color: #ffffff; + text-transform: uppercase; + background-color: #999999; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.label-important { + background-color: #b94a48; +} +.label-warning { + background-color: #f89406; +} +.label-success { + background-color: #468847; +} +.label-info { + background-color: #3a87ad; +} +table { + max-width: 100%; + border-collapse: collapse; + border-spacing: 0; +} +.table { + width: 100%; + margin-bottom: 18px; +} +.table th, .table td { + padding: 8px; + line-height: 18px; + text-align: left; + border-top: 1px solid #ddd; +} +.table th { + font-weight: bold; + vertical-align: bottom; +} +.table td { + vertical-align: top; +} +.table thead:first-child tr th, .table thead:first-child tr td { + border-top: 0; +} +.table tbody + tbody { + border-top: 2px solid #ddd; +} +.table-condensed th, .table-condensed td { + padding: 4px 5px; +} +.table-bordered { + border: 1px solid #ddd; + border-collapse: separate; + *border-collapse: collapsed; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.table-bordered th + th, +.table-bordered td + td, +.table-bordered th + td, +.table-bordered td + th { + border-left: 1px solid #ddd; +} +.table-bordered thead:first-child tr:first-child th, .table-bordered tbody:first-child tr:first-child th, .table-bordered tbody:first-child tr:first-child td { + border-top: 0; +} +.table-bordered thead:first-child tr:first-child th:first-child, .table-bordered tbody:first-child tr:first-child td:first-child { + -webkit-border-radius: 4px 0 0 0; + -moz-border-radius: 4px 0 0 0; + border-radius: 4px 0 0 0; +} +.table-bordered thead:first-child tr:first-child th:last-child, .table-bordered tbody:first-child tr:first-child td:last-child { + -webkit-border-radius: 0 4px 0 0; + -moz-border-radius: 0 4px 0 0; + border-radius: 0 4px 0 0; +} +.table-bordered thead:last-child tr:last-child th:first-child, .table-bordered tbody:last-child tr:last-child td:first-child { + -webkit-border-radius: 0 0 0 4px; + -moz-border-radius: 0 0 0 4px; + border-radius: 0 0 0 4px; +} +.table-bordered thead:last-child tr:last-child th:last-child, .table-bordered tbody:last-child tr:last-child td:last-child { + -webkit-border-radius: 0 0 4px 0; + -moz-border-radius: 0 0 4px 0; + border-radius: 0 0 4px 0; +} +.table-striped tbody tr:nth-child(odd) td, .table-striped tbody tr:nth-child(odd) th { + background-color: #f9f9f9; +} +table .span1 { + float: none; + width: 44px; + margin-left: 0; +} +table .span2 { + float: none; + width: 124px; + margin-left: 0; +} +table .span3 { + float: none; + width: 204px; + margin-left: 0; +} +table .span4 { + float: none; + width: 284px; + margin-left: 0; +} +table .span5 { + float: none; + width: 364px; + margin-left: 0; +} +table .span6 { + float: none; + width: 444px; + margin-left: 0; +} +table .span7 { + float: none; + width: 524px; + margin-left: 0; +} +table .span8 { + float: none; + width: 604px; + margin-left: 0; +} +table .span9 { + float: none; + width: 684px; + margin-left: 0; +} +table .span10 { + float: none; + width: 764px; + margin-left: 0; +} +table .span11 { + float: none; + width: 844px; + margin-left: 0; +} +table .span12 { + float: none; + width: 924px; + margin-left: 0; +} +form { + margin: 0 0 18px; +} +fieldset { + padding: 0; + margin: 0; + border: 0; +} +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 27px; + font-size: 19.5px; + line-height: 36px; + color: #333333; + border: 0; + border-bottom: 1px solid #eee; +} +label, +input, +button, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: 18px; +} +label { + display: block; + margin-bottom: 5px; + color: #333333; +} +input, +textarea, +select, +.uneditable-input { + display: inline-block; + width: 210px; + height: 18px; + padding: 4px; + margin-bottom: 9px; + font-size: 13px; + line-height: 18px; + color: #555555; + border: 1px solid #ccc; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.uneditable-textarea { + width: auto; + height: auto; +} +label input, label textarea, label select { + display: block; +} +input[type="image"], input[type="checkbox"], input[type="radio"] { + width: auto; + height: auto; + padding: 0; + margin: 3px 0; + *margin-top: 0; + /* IE7 */ + + line-height: normal; + border: 0; + cursor: pointer; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +input[type="file"] { + padding: initial; + line-height: initial; + border: initial; + background-color: #ffffff; + background-color: initial; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} +input[type="button"], input[type="reset"], input[type="submit"] { + width: auto; + height: auto; +} +select, input[type="file"] { + height: 28px; + /* In IE7, the height of the select element cannot be changed by height, only font-size */ + + *margin-top: 4px; + /* For IE7, add top margin to align select with labels */ + + line-height: 28px; +} +select { + width: 220px; + background-color: #ffffff; +} +select[multiple], select[size] { + height: auto; +} +input[type="image"] { + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} +textarea { + height: auto; +} +input[type="hidden"] { + display: none; +} +.radio, .checkbox { + padding-left: 18px; +} +.radio input[type="radio"], .checkbox input[type="checkbox"] { + float: left; + margin-left: -18px; +} +.controls > .radio:first-child, .controls > .checkbox:first-child { + padding-top: 5px; +} +.radio.inline, .checkbox.inline { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; +} +.radio.inline + .radio.inline, .checkbox.inline + .checkbox.inline { + margin-left: 10px; +} +.controls > .radio.inline:first-child, .controls > .checkbox.inline:first-child { + padding-top: 0; +} +input, textarea { + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -ms-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; +} +input:focus, textarea:focus { + border-color: rgba(82, 168, 236, 0.8); + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + outline: 0; + outline: thin dotted \9; + /* IE6-8 */ + +} +input[type="file"]:focus, input[type="checkbox"]:focus, select:focus { + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.input-mini { + width: 60px; +} +.input-small { + width: 90px; +} +.input-medium { + width: 150px; +} +.input-large { + width: 210px; +} +.input-xlarge { + width: 270px; +} +.input-xxlarge { + width: 530px; +} +input[class*="span"], +select[class*="span"], +textarea[class*="span"], +.uneditable-input { + float: none; + margin-left: 0; +} +input.span1, textarea.span1, .uneditable-input.span1 { + width: 50px; +} +input.span2, textarea.span2, .uneditable-input.span2 { + width: 130px; +} +input.span3, textarea.span3, .uneditable-input.span3 { + width: 210px; +} +input.span4, textarea.span4, .uneditable-input.span4 { + width: 290px; +} +input.span5, textarea.span5, .uneditable-input.span5 { + width: 370px; +} +input.span6, textarea.span6, .uneditable-input.span6 { + width: 450px; +} +input.span7, textarea.span7, .uneditable-input.span7 { + width: 530px; +} +input.span8, textarea.span8, .uneditable-input.span8 { + width: 610px; +} +input.span9, textarea.span9, .uneditable-input.span9 { + width: 690px; +} +input.span10, textarea.span10, .uneditable-input.span10 { + width: 770px; +} +input.span11, textarea.span11, .uneditable-input.span11 { + width: 850px; +} +input.span12, textarea.span12, .uneditable-input.span12 { + width: 930px; +} +input[disabled], +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + background-color: #f5f5f5; + border-color: #ddd; + cursor: not-allowed; +} +.control-group.warning > label, .control-group.warning .help-block, .control-group.warning .help-inline { + color: #c09853; +} +.control-group.warning input, .control-group.warning select, .control-group.warning textarea { + color: #c09853; + border-color: #c09853; +} +.control-group.warning input:focus, .control-group.warning select:focus, .control-group.warning textarea:focus { + border-color: #a47e3c; + -webkit-box-shadow: 0 0 6px #dbc59e; + -moz-box-shadow: 0 0 6px #dbc59e; + box-shadow: 0 0 6px #dbc59e; +} +.control-group.warning .input-prepend .add-on, .control-group.warning .input-append .add-on { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} +.control-group.error > label, .control-group.error .help-block, .control-group.error .help-inline { + color: #b94a48; +} +.control-group.error input, .control-group.error select, .control-group.error textarea { + color: #b94a48; + border-color: #b94a48; +} +.control-group.error input:focus, .control-group.error select:focus, .control-group.error textarea:focus { + border-color: #953b39; + -webkit-box-shadow: 0 0 6px #d59392; + -moz-box-shadow: 0 0 6px #d59392; + box-shadow: 0 0 6px #d59392; +} +.control-group.error .input-prepend .add-on, .control-group.error .input-append .add-on { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} +.control-group.success > label, .control-group.success .help-block, .control-group.success .help-inline { + color: #468847; +} +.control-group.success input, .control-group.success select, .control-group.success textarea { + color: #468847; + border-color: #468847; +} +.control-group.success input:focus, .control-group.success select:focus, .control-group.success textarea:focus { + border-color: #356635; + -webkit-box-shadow: 0 0 6px #7aba7b; + -moz-box-shadow: 0 0 6px #7aba7b; + box-shadow: 0 0 6px #7aba7b; +} +.control-group.success .input-prepend .add-on, .control-group.success .input-append .add-on { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} +input:focus:required:invalid, textarea:focus:required:invalid, select:focus:required:invalid { + color: #b94a48; + border-color: #ee5f5b; +} +input:focus:required:invalid:focus, textarea:focus:required:invalid:focus, select:focus:required:invalid:focus { + border-color: #e9322d; + -webkit-box-shadow: 0 0 6px #f8b9b7; + -moz-box-shadow: 0 0 6px #f8b9b7; + box-shadow: 0 0 6px #f8b9b7; +} +.form-actions { + padding: 17px 20px 18px; + margin-top: 18px; + margin-bottom: 18px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; +} +.uneditable-input { + display: block; + background-color: #ffffff; + border-color: #eee; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + cursor: not-allowed; +} +:-moz-placeholder { + color: #999999; +} +::-webkit-input-placeholder { + color: #999999; +} +.help-block { + margin-top: 5px; + margin-bottom: 0; + color: #999999; +} +.help-inline { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; + margin-bottom: 9px; + vertical-align: middle; + padding-left: 5px; +} +.input-prepend, .input-append { + margin-bottom: 5px; + *zoom: 1; +} +.input-prepend:before, +.input-append:before, +.input-prepend:after, +.input-append:after { + display: table; + content: ""; +} +.input-prepend:after, .input-append:after { + clear: both; +} +.input-prepend input, +.input-append input, +.input-prepend .uneditable-input, +.input-append .uneditable-input { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.input-prepend input:focus, +.input-append input:focus, +.input-prepend .uneditable-input:focus, +.input-append .uneditable-input:focus { + position: relative; + z-index: 2; +} +.input-prepend .uneditable-input, .input-append .uneditable-input { + border-left-color: #ccc; +} +.input-prepend .add-on, .input-append .add-on { + float: left; + display: block; + width: auto; + min-width: 16px; + height: 18px; + margin-right: -1px; + padding: 4px 5px; + font-weight: normal; + line-height: 18px; + color: #999999; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + background-color: #f5f5f5; + border: 1px solid #ccc; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.input-prepend .active, .input-append .active { + background-color: #a9dba9; + border-color: #46a546; +} +.input-prepend .add-on { + *margin-top: 1px; + /* IE6-7 */ + +} +.input-append input, .input-append .uneditable-input { + float: left; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.input-append .uneditable-input { + border-right-color: #ccc; +} +.input-append .add-on { + margin-right: 0; + margin-left: -1px; + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.input-append input:first-child { + *margin-left: -160px; +} +.input-append input:first-child + .add-on { + *margin-left: -21px; +} +.search-query { + padding-left: 14px; + padding-right: 14px; + margin-bottom: 0; + -webkit-border-radius: 14px; + -moz-border-radius: 14px; + border-radius: 14px; +} +.form-search input, +.form-inline input, +.form-horizontal input, +.form-search textarea, +.form-inline textarea, +.form-horizontal textarea, +.form-search select, +.form-inline select, +.form-horizontal select, +.form-search .help-inline, +.form-inline .help-inline, +.form-horizontal .help-inline, +.form-search .uneditable-input, +.form-inline .uneditable-input, +.form-horizontal .uneditable-input { + display: inline-block; + margin-bottom: 0; +} +.form-search label, +.form-inline label, +.form-search .input-append, +.form-inline .input-append, +.form-search .input-prepend, +.form-inline .input-prepend { + display: inline-block; +} +.form-search .input-append .add-on, +.form-inline .input-prepend .add-on, +.form-search .input-append .add-on, +.form-inline .input-prepend .add-on { + vertical-align: middle; +} +.control-group { + margin-bottom: 9px; +} +.form-horizontal legend + .control-group { + margin-top: 18px; + -webkit-margin-top-collapse: separate; +} +.form-horizontal .control-group { + margin-bottom: 18px; + *zoom: 1; +} +.form-horizontal .control-group:before, .form-horizontal .control-group:after { + display: table; + content: ""; +} +.form-horizontal .control-group:after { + clear: both; +} +.form-horizontal .control-group > label { + float: left; + width: 140px; + padding-top: 5px; + text-align: right; +} +.form-horizontal .controls { + margin-left: 160px; +} +.form-horizontal .form-actions { + padding-left: 160px; +} +.btn { + display: inline-block; + padding: 4px 10px 4px; + font-size: 13px; + line-height: 18px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + background-color: #fafafa; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); + background-image: -moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6); + background-image: -ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); + background-image: -o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); + background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); + background-repeat: no-repeat; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); + border: 1px solid #ccc; + border-bottom-color: #bbb; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + cursor: pointer; + *margin-left: .3em; +} +.btn:first-child { + *margin-left: 0; +} +.btn:hover { + color: #333333; + text-decoration: none; + background-color: #e6e6e6; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -ms-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} +.btn:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn.active, .btn:active { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + background-color: #e6e6e6; + background-color: #d9d9d9 \9; + color: rgba(0, 0, 0, 0.5); + outline: 0; +} +.btn.disabled, .btn[disabled] { + cursor: default; + background-image: none; + background-color: #e6e6e6; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} +.btn-large { + padding: 9px 14px; + font-size: 15px; + line-height: normal; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.btn-large .icon { + margin-top: 1px; +} +.btn-small { + padding: 5px 9px; + font-size: 11px; + line-height: 16px; +} +.btn-small .icon { + margin-top: -1px; +} +.btn-primary, +.btn-primary:hover, +.btn-warning, +.btn-warning:hover, +.btn-danger, +.btn-danger:hover, +.btn-success, +.btn-success:hover, +.btn-info, +.btn-info:hover { + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + color: #ffffff; +} +.btn-primary.active, +.btn-warning.active, +.btn-danger.active, +.btn-success.active, +.btn-info.active { + color: rgba(255, 255, 255, 0.75); +} +.btn-primary { + background-color: #006dcc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -ms-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(top, #0088cc, #0044cc); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-primary:hover, +.btn-primary:active, +.btn-primary.active, +.btn-primary.disabled, +.btn-primary[disabled] { + background-color: #0044cc; +} +.btn-primary:active, .btn-primary.active { + background-color: #003399 \9; +} +.btn-warning { + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -ms-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(top, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); + border-color: #f89406 #f89406 #ad6704; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-warning:hover, +.btn-warning:active, +.btn-warning.active, +.btn-warning.disabled, +.btn-warning[disabled] { + background-color: #f89406; +} +.btn-warning:active, .btn-warning.active { + background-color: #c67605 \9; +} +.btn-danger { + background-color: #da4f49; + background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -ms-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); + background-image: linear-gradient(top, #ee5f5b, #bd362f); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0); + border-color: #bd362f #bd362f #802420; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-danger:hover, +.btn-danger:active, +.btn-danger.active, +.btn-danger.disabled, +.btn-danger[disabled] { + background-color: #bd362f; +} +.btn-danger:active, .btn-danger.active { + background-color: #942a25 \9; +} +.btn-success { + background-color: #5bb75b; + background-image: -moz-linear-gradient(top, #62c462, #51a351); + background-image: -ms-linear-gradient(top, #62c462, #51a351); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); + background-image: -webkit-linear-gradient(top, #62c462, #51a351); + background-image: -o-linear-gradient(top, #62c462, #51a351); + background-image: linear-gradient(top, #62c462, #51a351); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0); + border-color: #51a351 #51a351 #387038; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-success:hover, +.btn-success:active, +.btn-success.active, +.btn-success.disabled, +.btn-success[disabled] { + background-color: #51a351; +} +.btn-success:active, .btn-success.active { + background-color: #408140 \9; +} +.btn-info { + background-color: #49afcd; + background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -ms-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); + background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); + background-image: linear-gradient(top, #5bc0de, #2f96b4); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0); + border-color: #2f96b4 #2f96b4 #1f6377; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-info:hover, +.btn-info:active, +.btn-info.active, +.btn-info.disabled, +.btn-info[disabled] { + background-color: #2f96b4; +} +.btn-info:active, .btn-info.active { + background-color: #24748c \9; +} +button.btn, input[type="submit"].btn { + *padding-top: 2px; + *padding-bottom: 2px; +} +button.btn::-moz-focus-inner, input[type="submit"].btn::-moz-focus-inner { + padding: 0; + border: 0; +} +button.btn.large, input[type="submit"].btn.large { + *padding-top: 7px; + *padding-bottom: 7px; +} +button.btn.small, input[type="submit"].btn.small { + *padding-top: 3px; + *padding-bottom: 3px; +} +[class^="icon-"] { + display: inline-block; + width: 14px; + height: 14px; + vertical-align: text-top; + background-image: url(../img/glyphicons-halflings.png); + background-position: 14px 14px; + background-repeat: no-repeat; + *margin-right: .3em; +} +[class^="icon-"]:last-child { + *margin-left: 0; +} +.icon-white { + background-image: url(../img/glyphicons-halflings-white.png); +} +.icon-glass { + background-position: 0 0; +} +.icon-music { + background-position: -24px 0; +} +.icon-search { + background-position: -48px 0; +} +.icon-envelope { + background-position: -72px 0; +} +.icon-heart { + background-position: -96px 0; +} +.icon-star { + background-position: -120px 0; +} +.icon-star-empty { + background-position: -144px 0; +} +.icon-user { + background-position: -168px 0; +} +.icon-film { + background-position: -192px 0; +} +.icon-th-large { + background-position: -216px 0; +} +.icon-th { + background-position: -240px 0; +} +.icon-th-list { + background-position: -264px 0; +} +.icon-ok { + background-position: -288px 0; +} +.icon-remove { + background-position: -312px 0; +} +.icon-zoom-in { + background-position: -336px 0; +} +.icon-zoom-out { + background-position: -360px 0; +} +.icon-off { + background-position: -384px 0; +} +.icon-signal { + background-position: -408px 0; +} +.icon-cog { + background-position: -432px 0; +} +.icon-trash { + background-position: -456px 0; +} +.icon-home { + background-position: 0 -24px; +} +.icon-file { + background-position: -24px -24px; +} +.icon-time { + background-position: -48px -24px; +} +.icon-road { + background-position: -72px -24px; +} +.icon-download-alt { + background-position: -96px -24px; +} +.icon-download { + background-position: -120px -24px; +} +.icon-upload { + background-position: -144px -24px; +} +.icon-inbox { + background-position: -168px -24px; +} +.icon-play-circle { + background-position: -192px -24px; +} +.icon-repeat { + background-position: -216px -24px; +} +.icon-refresh { + background-position: -240px -24px; +} +.icon-list-alt { + background-position: -264px -24px; +} +.icon-lock { + background-position: -287px -24px; +} +.icon-flag { + background-position: -312px -24px; +} +.icon-headphones { + background-position: -336px -24px; +} +.icon-volume-off { + background-position: -360px -24px; +} +.icon-volume-down { + background-position: -384px -24px; +} +.icon-volume-up { + background-position: -408px -24px; +} +.icon-qrcode { + background-position: -432px -24px; +} +.icon-barcode { + background-position: -456px -24px; +} +.icon-tag { + background-position: 0 -48px; +} +.icon-tags { + background-position: -25px -48px; +} +.icon-book { + background-position: -48px -48px; +} +.icon-bookmark { + background-position: -72px -48px; +} +.icon-print { + background-position: -96px -48px; +} +.icon-camera { + background-position: -120px -48px; +} +.icon-font { + background-position: -144px -48px; +} +.icon-bold { + background-position: -167px -48px; +} +.icon-italic { + background-position: -192px -48px; +} +.icon-text-height { + background-position: -216px -48px; +} +.icon-text-width { + background-position: -240px -48px; +} +.icon-align-left { + background-position: -264px -48px; +} +.icon-align-center { + background-position: -288px -48px; +} +.icon-align-right { + background-position: -312px -48px; +} +.icon-align-justify { + background-position: -336px -48px; +} +.icon-list { + background-position: -360px -48px; +} +.icon-indent-left { + background-position: -384px -48px; +} +.icon-indent-right { + background-position: -408px -48px; +} +.icon-facetime-video { + background-position: -432px -48px; +} +.icon-picture { + background-position: -456px -48px; +} +.icon-pencil { + background-position: 0 -72px; +} +.icon-map-marker { + background-position: -24px -72px; +} +.icon-adjust { + background-position: -48px -72px; +} +.icon-tint { + background-position: -72px -72px; +} +.icon-edit { + background-position: -96px -72px; +} +.icon-share { + background-position: -120px -72px; +} +.icon-check { + background-position: -144px -72px; +} +.icon-move { + background-position: -168px -72px; +} +.icon-step-backward { + background-position: -192px -72px; +} +.icon-fast-backward { + background-position: -216px -72px; +} +.icon-backward { + background-position: -240px -72px; +} +.icon-play { + background-position: -264px -72px; +} +.icon-pause { + background-position: -288px -72px; +} +.icon-stop { + background-position: -312px -72px; +} +.icon-forward { + background-position: -336px -72px; +} +.icon-fast-forward { + background-position: -360px -72px; +} +.icon-step-forward { + background-position: -384px -72px; +} +.icon-eject { + background-position: -408px -72px; +} +.icon-chevron-left { + background-position: -432px -72px; +} +.icon-chevron-right { + background-position: -456px -72px; +} +.icon-plus-sign { + background-position: 0 -96px; +} +.icon-minus-sign { + background-position: -24px -96px; +} +.icon-remove-sign { + background-position: -48px -96px; +} +.icon-ok-sign { + background-position: -72px -96px; +} +.icon-question-sign { + background-position: -96px -96px; +} +.icon-info-sign { + background-position: -120px -96px; +} +.icon-screenshot { + background-position: -144px -96px; +} +.icon-remove-circle { + background-position: -168px -96px; +} +.icon-ok-circle { + background-position: -192px -96px; +} +.icon-ban-circle { + background-position: -216px -96px; +} +.icon-arrow-left { + background-position: -240px -96px; +} +.icon-arrow-right { + background-position: -264px -96px; +} +.icon-arrow-up { + background-position: -289px -96px; +} +.icon-arrow-down { + background-position: -312px -96px; +} +.icon-share-alt { + background-position: -336px -96px; +} +.icon-resize-full { + background-position: -360px -96px; +} +.icon-resize-small { + background-position: -384px -96px; +} +.icon-plus { + background-position: -408px -96px; +} +.icon-minus { + background-position: -433px -96px; +} +.icon-asterisk { + background-position: -456px -96px; +} +.icon-exclamation-sign { + background-position: 0 -120px; +} +.icon-gift { + background-position: -24px -120px; +} +.icon-leaf { + background-position: -48px -120px; +} +.icon-fire { + background-position: -72px -120px; +} +.icon-eye-open { + background-position: -96px -120px; +} +.icon-eye-close { + background-position: -120px -120px; +} +.icon-warning-sign { + background-position: -144px -120px; +} +.icon-plane { + background-position: -168px -120px; +} +.icon-calendar { + background-position: -192px -120px; +} +.icon-random { + background-position: -216px -120px; +} +.icon-comment { + background-position: -240px -120px; +} +.icon-magnet { + background-position: -264px -120px; +} +.icon-chevron-up { + background-position: -288px -120px; +} +.icon-chevron-down { + background-position: -313px -119px; +} +.icon-retweet { + background-position: -336px -120px; +} +.icon-shopping-cart { + background-position: -360px -120px; +} +.icon-folder-close { + background-position: -384px -120px; +} +.icon-folder-open { + background-position: -408px -120px; +} +.icon-resize-vertical { + background-position: -432px -119px; +} +.icon-resize-horizontal { + background-position: -456px -118px; +} +.btn-group { + position: relative; + *zoom: 1; + *margin-left: .3em; +} +.btn-group:before, .btn-group:after { + display: table; + content: ""; +} +.btn-group:after { + clear: both; +} +.btn-group:first-child { + *margin-left: 0; +} +.btn-group + .btn-group { + margin-left: 5px; +} +.btn-toolbar { + margin-top: 9px; + margin-bottom: 9px; +} +.btn-toolbar .btn-group { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} +.btn-group .btn { + position: relative; + float: left; + margin-left: -1px; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.btn-group .btn:first-child { + margin-left: 0; + -webkit-border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; + border-top-left-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + border-bottom-left-radius: 4px; +} +.btn-group .btn:last-child, .btn-group .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; + border-bottom-right-radius: 4px; +} +.btn-group .btn.large:first-child { + margin-left: 0; + -webkit-border-top-left-radius: 6px; + -moz-border-radius-topleft: 6px; + border-top-left-radius: 6px; + -webkit-border-bottom-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + border-bottom-left-radius: 6px; +} +.btn-group .btn.large:last-child, .btn-group .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + -moz-border-radius-topright: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + -moz-border-radius-bottomright: 6px; + border-bottom-right-radius: 6px; +} +.btn-group .btn:hover, +.btn-group .btn:focus, +.btn-group .btn:active, +.btn-group .btn.active { + z-index: 2; +} +.btn-group .dropdown-toggle:active, .btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + *padding-top: 5px; + *padding-bottom: 5px; +} +.btn-group.open { + *z-index: 1000; +} +.btn-group.open .dropdown-menu { + display: block; + margin-top: 1px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} +.btn .caret { + margin-top: 7px; + margin-left: 0; +} +.btn:hover .caret, .open.btn-group .caret { + opacity: 1; + filter: alpha(opacity=100); +} +.btn-primary .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret { + border-top-color: #ffffff; + opacity: 0.75; + filter: alpha(opacity=75); +} +.btn-small .caret { + margin-top: 4px; +} +.nav { + margin-left: 0; + margin-bottom: 18px; + list-style: none; +} +.nav > li > a { + display: block; +} +.nav > li > a:hover { + text-decoration: none; + background-color: #eeeeee; +} +.nav-list { + padding-left: 14px; + padding-right: 14px; + margin-bottom: 0; +} +.nav-list > li > a, .nav-list .nav-header { + display: block; + padding: 3px 15px; + margin-left: -15px; + margin-right: -15px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); +} +.nav-list .nav-header { + font-size: 11px; + font-weight: bold; + line-height: 18px; + color: #999999; + text-transform: uppercase; +} +.nav-list > li + .nav-header { + margin-top: 9px; +} +.nav-list .active > a, .nav-list .active > a:hover { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + background-color: #0088cc; +} +.nav-list [class^="icon-"] { + margin-right: 2px; +} +.nav-tabs, .nav-pills { + *zoom: 1; +} +.nav-tabs:before, +.nav-pills:before, +.nav-tabs:after, +.nav-pills:after { + display: table; + content: ""; +} +.nav-tabs:after, .nav-pills:after { + clear: both; +} +.nav-tabs > li, .nav-pills > li { + float: left; +} +.nav-tabs > li > a, .nav-pills > li > a { + padding-right: 12px; + padding-left: 12px; + margin-right: 2px; + line-height: 14px; +} +.nav-tabs { + border-bottom: 1px solid #ddd; +} +.nav-tabs > li { + margin-bottom: -1px; +} +.nav-tabs > li > a { + padding-top: 9px; + padding-bottom: 9px; + border: 1px solid transparent; + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} +.nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #dddddd; +} +.nav-tabs > .active > a, .nav-tabs > .active > a:hover { + color: #555555; + background-color: #ffffff; + border: 1px solid #ddd; + border-bottom-color: transparent; + cursor: default; +} +.nav-pills > li > a { + padding-top: 8px; + padding-bottom: 8px; + margin-top: 2px; + margin-bottom: 2px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.nav-pills .active > a, .nav-pills .active > a:hover { + color: #ffffff; + background-color: #0088cc; +} +.nav-stacked > li { + float: none; +} +.nav-stacked > li > a { + margin-right: 0; +} +.nav-tabs.nav-stacked { + border-bottom: 0; +} +.nav-tabs.nav-stacked > li > a { + border: 1px solid #ddd; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.nav-tabs.nav-stacked > li:first-child > a { + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} +.nav-tabs.nav-stacked > li:last-child > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} +.nav-tabs.nav-stacked > li > a:hover { + border-color: #ddd; + z-index: 2; +} +.nav-pills.nav-stacked > li > a { + margin-bottom: 3px; +} +.nav-pills.nav-stacked > li:last-child > a { + margin-bottom: 1px; +} +.nav-tabs .dropdown-menu, .nav-pills .dropdown-menu { + margin-top: 1px; + border-width: 1px; +} +.nav-pills .dropdown-menu { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.nav-tabs .dropdown-toggle .caret, .nav-pills .dropdown-toggle .caret { + border-top-color: #0088cc; + margin-top: 6px; +} +.nav-tabs .dropdown-toggle:hover .caret, .nav-pills .dropdown-toggle:hover .caret { + border-top-color: #005580; +} +.nav-tabs .active .dropdown-toggle .caret, .nav-pills .active .dropdown-toggle .caret { + border-top-color: #333333; +} +.nav > .dropdown.active > a:hover { + color: #000000; + cursor: pointer; +} +.nav-tabs .open .dropdown-toggle, .nav-pills .open .dropdown-toggle, .nav > .open.active > a:hover { + color: #ffffff; + background-color: #999999; + border-color: #999999; +} +.nav .open .caret, .nav .open.active .caret, .nav .open a:hover .caret { + border-top-color: #ffffff; + opacity: 1; + filter: alpha(opacity=100); +} +.tabs-stacked .open > a:hover { + border-color: #999999; +} +.tabbable { + *zoom: 1; +} +.tabbable:before, .tabbable:after { + display: table; + content: ""; +} +.tabbable:after { + clear: both; +} +.tabs-below .nav-tabs, .tabs-right .nav-tabs, .tabs-left .nav-tabs { + border-bottom: 0; +} +.tab-content > .tab-pane, .pill-content > .pill-pane { + display: none; +} +.tab-content > .active, .pill-content > .active { + display: block; +} +.tabs-below .nav-tabs { + border-top: 1px solid #ddd; +} +.tabs-below .nav-tabs > li { + margin-top: -1px; + margin-bottom: 0; +} +.tabs-below .nav-tabs > li > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} +.tabs-below .nav-tabs > li > a:hover { + border-bottom-color: transparent; + border-top-color: #ddd; +} +.tabs-below .nav-tabs .active > a, .tabs-below .nav-tabs .active > a:hover { + border-color: transparent #ddd #ddd #ddd; +} +.tabs-left .nav-tabs > li, .tabs-right .nav-tabs > li { + float: none; +} +.tabs-left .nav-tabs > li > a, .tabs-right .nav-tabs > li > a { + min-width: 74px; + margin-right: 0; + margin-bottom: 3px; +} +.tabs-left .nav-tabs { + float: left; + margin-right: 19px; + border-right: 1px solid #ddd; +} +.tabs-left .nav-tabs > li > a { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} +.tabs-left .nav-tabs > li > a:hover { + border-color: #eeeeee #dddddd #eeeeee #eeeeee; +} +.tabs-left .nav-tabs .active > a, .tabs-left .nav-tabs .active > a:hover { + border-color: #ddd transparent #ddd #ddd; + *border-right-color: #ffffff; +} +.tabs-right .nav-tabs { + float: right; + margin-left: 19px; + border-left: 1px solid #ddd; +} +.tabs-right .nav-tabs > li > a { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} +.tabs-right .nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #eeeeee #dddddd; +} +.tabs-right .nav-tabs .active > a, .tabs-right .nav-tabs .active > a:hover { + border-color: #ddd #ddd #ddd transparent; + *border-left-color: #ffffff; +} +.navbar { + overflow: visible; + margin-bottom: 18px; +} +.navbar-inner { + padding-left: 20px; + padding-right: 20px; + background-color: #2c2c2c; + background-image: -moz-linear-gradient(top, #333333, #222222); + background-image: -ms-linear-gradient(top, #333333, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); + background-image: -webkit-linear-gradient(top, #333333, #222222); + background-image: -o-linear-gradient(top, #333333, #222222); + background-image: linear-gradient(top, #333333, #222222); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); +} +.btn-navbar { + display: none; + float: right; + padding: 7px 10px; + margin-left: 5px; + margin-right: 5px; + background-color: #2c2c2c; + background-image: -moz-linear-gradient(top, #333333, #222222); + background-image: -ms-linear-gradient(top, #333333, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); + background-image: -webkit-linear-gradient(top, #333333, #222222); + background-image: -o-linear-gradient(top, #333333, #222222); + background-image: linear-gradient(top, #333333, #222222); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); + border-color: #222222 #222222 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); +} +.btn-navbar:hover, +.btn-navbar:active, +.btn-navbar.active, +.btn-navbar.disabled, +.btn-navbar[disabled] { + background-color: #222222; +} +.btn-navbar:active, .btn-navbar.active { + background-color: #080808 \9; +} +.btn-navbar .icon-bar { + display: block; + width: 18px; + height: 2px; + background-color: #f5f5f5; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); +} +.btn-navbar .icon-bar + .icon-bar { + margin-top: 3px; +} +.nav-collapse.collapse { + height: auto; +} +.navbar .brand:hover { + text-decoration: none; +} +.navbar .brand { + float: left; + display: block; + padding: 8px 20px 12px; + margin-left: -20px; + font-size: 20px; + font-weight: 200; + line-height: 1; + color: #ffffff; +} +.navbar .navbar-text { + margin-bottom: 0; + line-height: 40px; + color: #999999; +} +.navbar .navbar-text a:hover { + color: #ffffff; + background-color: transparent; +} +.navbar .btn, .navbar .btn-group { + margin-top: 5px; +} +.navbar .btn-group .btn { + margin-top: 0; +} +.navbar-form { + margin-bottom: 0; + *zoom: 1; +} +.navbar-form:before, .navbar-form:after { + display: table; + content: ""; +} +.navbar-form:after { + clear: both; +} +.navbar-form input, .navbar-form select { + display: inline-block; + margin-top: 5px; + margin-bottom: 0; +} +.navbar-form .radio, .navbar-form .checkbox { + margin-top: 5px; +} +.navbar-form input[type="image"], .navbar-form input[type="checkbox"], .navbar-form input[type="radio"] { + margin-top: 3px; +} +.navbar-search { + position: relative; + float: left; + margin-top: 6px; + margin-bottom: 0; +} +.navbar-search .search-query { + padding: 4px 9px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: 1; + color: #ffffff; + color: rgba(255, 255, 255, 0.75); + background: #666; + background: rgba(255, 255, 255, 0.3); + border: 1px solid #111; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); + -webkit-transition: none; + -moz-transition: none; + -ms-transition: none; + -o-transition: none; + transition: none; +} +.navbar-search .search-query :-moz-placeholder { + color: #eeeeee; +} +.navbar-search .search-query::-webkit-input-placeholder { + color: #eeeeee; +} +.navbar-search .search-query:hover { + color: #ffffff; + background-color: #999999; + background-color: rgba(255, 255, 255, 0.5); +} +.navbar-search .search-query:focus, .navbar-search .search-query.focused { + padding: 5px 10px; + color: #333333; + text-shadow: 0 1px 0 #ffffff; + background-color: #ffffff; + border: 0; + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + outline: 0; +} +.navbar-fixed-top { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; +} +.navbar-fixed-top .navbar-inner { + padding-left: 0; + padding-right: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.navbar .nav { + position: relative; + left: 0; + display: block; + float: left; + margin: 0 10px 0 0; +} +.navbar .nav.pull-right { + float: right; +} +.navbar .nav > li { + display: block; + float: left; +} +.navbar .nav > li > a { + float: none; + padding: 10px 10px 11px; + line-height: 19px; + color: #999999; + text-decoration: none; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.navbar .nav > li > a:hover { + background-color: transparent; + color: #ffffff; + text-decoration: none; +} +.navbar .nav .active > a, .navbar .nav .active > a:hover { + color: #ffffff; + text-decoration: none; + background-color: #222222; + background-color: rgba(0, 0, 0, 0.5); +} +.navbar .divider-vertical { + height: 40px; + width: 1px; + margin: 0 9px; + overflow: hidden; + background-color: #222222; + border-right: 1px solid #333333; +} +.navbar .nav.pull-right { + margin-left: 10px; + margin-right: 0; +} +.navbar .dropdown-menu { + margin-top: 1px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.navbar .dropdown-menu:before { + content: ''; + display: inline-block; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-bottom-color: rgba(0, 0, 0, 0.2); + position: absolute; + top: -7px; + left: 9px; +} +.navbar .dropdown-menu:after { + content: ''; + display: inline-block; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + position: absolute; + top: -6px; + left: 10px; +} +.navbar .nav .dropdown-toggle .caret, .navbar .nav .open.dropdown .caret { + border-top-color: #ffffff; +} +.navbar .nav .active .caret { + opacity: 1; + filter: alpha(opacity=100); +} +.navbar .nav .open > .dropdown-toggle, .navbar .nav .active > .dropdown-toggle, .navbar .nav .open.active > .dropdown-toggle { + background-color: transparent; +} +.navbar .nav .active > .dropdown-toggle:hover { + color: #ffffff; +} +.navbar .nav.pull-right .dropdown-menu { + left: auto; + right: 0; +} +.navbar .nav.pull-right .dropdown-menu:before { + left: auto; + right: 12px; +} +.navbar .nav.pull-right .dropdown-menu:after { + left: auto; + right: 13px; +} +.breadcrumb { + padding: 7px 14px; + margin: 0 0 18px; + background-color: #fbfbfb; + background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); + background-image: -ms-linear-gradient(top, #ffffff, #f5f5f5); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5)); + background-image: -webkit-linear-gradient(top, #ffffff, #f5f5f5); + background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); + background-image: linear-gradient(top, #ffffff, #f5f5f5); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0); + border: 1px solid #ddd; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; +} +.breadcrumb li { + display: inline; + text-shadow: 0 1px 0 #ffffff; +} +.breadcrumb .divider { + padding: 0 5px; + color: #999999; +} +.breadcrumb .active a { + color: #333333; +} +.pagination { + height: 36px; + margin: 18px 0; +} +.pagination ul { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; + margin-left: 0; + margin-bottom: 0; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} +.pagination li { + display: inline; +} +.pagination a { + float: left; + padding: 0 14px; + line-height: 34px; + text-decoration: none; + border: 1px solid #ddd; + border-left-width: 0; +} +.pagination a:hover, .pagination .active a { + background-color: #f5f5f5; +} +.pagination .active a { + color: #999999; + cursor: default; +} +.pagination .disabled a, .pagination .disabled a:hover { + color: #999999; + background-color: transparent; + cursor: default; +} +.pagination li:first-child a { + border-left-width: 1px; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.pagination li:last-child a { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.pagination-centered { + text-align: center; +} +.pagination-right { + text-align: right; +} +.pager { + margin-left: 0; + margin-bottom: 18px; + list-style: none; + text-align: center; + *zoom: 1; +} +.pager:before, .pager:after { + display: table; + content: ""; +} +.pager:after { + clear: both; +} +.pager li { + display: inline; +} +.pager a { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} +.pager a:hover { + text-decoration: none; + background-color: #f5f5f5; +} +.pager .next a { + float: right; +} +.pager .previous a { + float: left; +} +.thumbnails { + margin-left: -20px; + list-style: none; + *zoom: 1; +} +.thumbnails:before, .thumbnails:after { + display: table; + content: ""; +} +.thumbnails:after { + clear: both; +} +.thumbnails > li { + float: left; + margin: 0 0 18px 20px; +} +.thumbnail { + display: block; + padding: 4px; + line-height: 1; + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); +} +a.thumbnail:hover { + border-color: #0088cc; + -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); +} +.thumbnail > img { + display: block; + max-width: 100%; + margin-left: auto; + margin-right: auto; +} +.thumbnail .caption { + padding: 9px; +} +.alert { + padding: 8px 35px 8px 14px; + margin-bottom: 18px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #fcf8e3; + border: 1px solid #fbeed5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.alert, .alert-heading { + color: #c09853; +} +.alert .close { + position: relative; + top: -2px; + right: -21px; + line-height: 18px; +} +.alert-success { + background-color: #dff0d8; + border-color: #d6e9c6; +} +.alert-success, .alert-success .alert-heading { + color: #468847; +} +.alert-danger, .alert-error { + background-color: #f2dede; + border-color: #eed3d7; +} +.alert-danger, +.alert-error, +.alert-danger .alert-heading, +.alert-error .alert-heading { + color: #b94a48; +} +.alert-info { + background-color: #d9edf7; + border-color: #bce8f1; +} +.alert-info, .alert-info .alert-heading { + color: #3a87ad; +} +.alert-block { + padding-top: 14px; + padding-bottom: 14px; +} +.alert-block > p, .alert-block > ul { + margin-bottom: 0; +} +.alert-block p + p { + margin-top: 5px; +} +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} +@-moz-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} +.progress { + overflow: hidden; + height: 18px; + margin-bottom: 18px; + background-color: #f7f7f7; + background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -ms-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); + background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: linear-gradient(top, #f5f5f5, #f9f9f9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.progress .bar { + width: 0%; + height: 18px; + color: #ffffff; + font-size: 12px; + text-align: center; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e90d2; + background-image: -moz-linear-gradient(top, #149bdf, #0480be); + background-image: -ms-linear-gradient(top, #149bdf, #0480be); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); + background-image: -webkit-linear-gradient(top, #149bdf, #0480be); + background-image: -o-linear-gradient(top, #149bdf, #0480be); + background-image: linear-gradient(top, #149bdf, #0480be); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0); + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: width 0.6s ease; + -moz-transition: width 0.6s ease; + -ms-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} +.progress-striped .bar { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; +} +.progress.active .bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} +.progress-danger .bar { + background-color: #dd514c; + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(top, #ee5f5b, #c43c35); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0); +} +.progress-danger.progress-striped .bar { + background-color: #ee5f5b; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-success .bar { + background-color: #5eb95e; + background-image: -moz-linear-gradient(top, #62c462, #57a957); + background-image: -ms-linear-gradient(top, #62c462, #57a957); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); + background-image: -webkit-linear-gradient(top, #62c462, #57a957); + background-image: -o-linear-gradient(top, #62c462, #57a957); + background-image: linear-gradient(top, #62c462, #57a957); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0); +} +.progress-success.progress-striped .bar { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-info .bar { + background-color: #4bb1cf; + background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); + background-image: -ms-linear-gradient(top, #5bc0de, #339bb9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); + background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); + background-image: -o-linear-gradient(top, #5bc0de, #339bb9); + background-image: linear-gradient(top, #5bc0de, #339bb9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0); +} +.progress-info.progress-striped .bar { + background-color: #5bc0de; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.hero-unit { + padding: 60px; + margin-bottom: 30px; + background-color: #f5f5f5; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} +.hero-unit h1 { + margin-bottom: 0; + font-size: 60px; + line-height: 1; + letter-spacing: -1px; +} +.hero-unit p { + font-size: 18px; + font-weight: 200; + line-height: 27px; +} +.tooltip { + position: absolute; + z-index: 1020; + display: block; + visibility: visible; + padding: 5px; + font-size: 11px; + opacity: 0; + filter: alpha(opacity=0); +} +.tooltip.in { + opacity: 0.8; + filter: alpha(opacity=80); +} +.tooltip.top { + margin-top: -2px; +} +.tooltip.right { + margin-left: 2px; +} +.tooltip.bottom { + margin-top: 2px; +} +.tooltip.left { + margin-left: -2px; +} +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #000000; +} +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #000000; +} +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid #000000; +} +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-right: 5px solid #000000; +} +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; +} +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + padding: 5px; +} +.popover.top { + margin-top: -5px; +} +.popover.right { + margin-left: 5px; +} +.popover.bottom { + margin-top: 5px; +} +.popover.left { + margin-left: -5px; +} +.popover.top .arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #000000; +} +.popover.right .arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-right: 5px solid #000000; +} +.popover.bottom .arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid #000000; +} +.popover.left .arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #000000; +} +.popover .arrow { + position: absolute; + width: 0; + height: 0; +} +.popover-inner { + padding: 3px; + width: 280px; + overflow: hidden; + background: #000000; + background: rgba(0, 0, 0, 0.8); + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); +} +.popover-title { + padding: 9px 15px; + line-height: 1; + background-color: #f5f5f5; + border-bottom: 1px solid #eee; + -webkit-border-radius: 3px 3px 0 0; + -moz-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; +} +.popover-content { + padding: 14px; + background-color: #ffffff; + -webkit-border-radius: 0 0 3px 3px; + -moz-border-radius: 0 0 3px 3px; + border-radius: 0 0 3px 3px; + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} +.popover-content p, .popover-content ul, .popover-content ol { + margin-bottom: 0; +} +.modal-open .dropdown-menu { + z-index: 2050; +} +.modal-open .dropdown.open { + *z-index: 2050; +} +.modal-open .popover { + z-index: 2060; +} +.modal-open .tooltip { + z-index: 2070; +} +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000000; +} +.modal-backdrop.fade { + opacity: 0; +} +.modal-backdrop, .modal-backdrop.fade.in { + opacity: 0.8; + filter: alpha(opacity=80); +} +.modal { + position: fixed; + top: 50%; + left: 50%; + z-index: 1050; + max-height: 500px; + overflow: auto; + width: 560px; + margin: -250px 0 0 -280px; + background-color: #ffffff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.3); + *border: 1px solid #999; + /* IE6-7 */ + + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} +.modal.fade { + -webkit-transition: opacity .3s linear, top .3s ease-out; + -moz-transition: opacity .3s linear, top .3s ease-out; + -ms-transition: opacity .3s linear, top .3s ease-out; + -o-transition: opacity .3s linear, top .3s ease-out; + transition: opacity .3s linear, top .3s ease-out; + top: -25%; +} +.modal.fade.in { + top: 50%; +} +.modal-header { + padding: 9px 15px; + border-bottom: 1px solid #eee; +} +.modal-header .close { + margin-top: 2px; +} +.modal-body { + padding: 15px; +} +.modal-footer { + padding: 14px 15px 15px; + margin-bottom: 0; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; + *zoom: 1; +} +.modal-footer:before, .modal-footer:after { + display: table; + content: ""; +} +.modal-footer:after { + clear: both; +} +.modal-footer .btn { + float: right; + margin-left: 5px; + margin-bottom: 0; +} +.dropdown { + position: relative; +} +.dropdown-toggle { + *margin-bottom: -3px; +} +.dropdown-toggle:active, .open .dropdown-toggle { + outline: 0; +} +.caret { + display: inline-block; + width: 0; + height: 0; + text-indent: -99999px; + *text-indent: 0; + vertical-align: top; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #000000; + opacity: 0.3; + filter: alpha(opacity=30); + content: "\2193"; +} +.dropdown .caret { + margin-top: 8px; + margin-left: 2px; +} +.dropdown:hover .caret, .open.dropdown .caret { + opacity: 1; + filter: alpha(opacity=100); +} +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + float: left; + display: none; + min-width: 160px; + max-width: 220px; + _width: 160px; + padding: 4px 0; + margin: 0; + list-style: none; + background-color: #ffffff; + border-color: #ccc; + border-color: rgba(0, 0, 0, 0.2); + border-style: solid; + border-width: 1px; + -webkit-border-radius: 0 0 5px 5px; + -moz-border-radius: 0 0 5px 5px; + border-radius: 0 0 5px 5px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; + *border-right-width: 2px; + *border-bottom-width: 2px; +} +.dropdown-menu.bottom-up { + top: auto; + bottom: 100%; + margin-bottom: 2px; +} +.dropdown-menu .divider { + height: 1px; + margin: 5px 1px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; + *width: 100%; + *margin: -5px 0 5px; +} +.dropdown-menu a { + display: block; + padding: 3px 15px; + clear: both; + font-weight: normal; + line-height: 18px; + color: #555555; + white-space: nowrap; +} +.dropdown-menu li > a:hover, .dropdown-menu .active > a, .dropdown-menu .active > a:hover { + color: #ffffff; + text-decoration: none; + background-color: #0088cc; +} +.dropdown.open { + *z-index: 1000; +} +.dropdown.open .dropdown-toggle { + color: #ffffff; + background: #ccc; + background: rgba(0, 0, 0, 0.3); +} +.dropdown.open .dropdown-menu { + display: block; +} +.typeahead { + margin-top: 2px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.accordion { + margin-bottom: 18px; +} +.accordion-group { + margin-bottom: 2px; + border: 1px solid #e5e5e5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.accordion-heading { + border-bottom: 0; +} +.accordion-heading .accordion-toggle { + display: block; + padding: 8px 15px; +} +.accordion-inner { + padding: 9px 15px; + border-top: 1px solid #e5e5e5; +} +.carousel { + position: relative; + margin-bottom: 18px; + line-height: 1; +} +.carousel-inner { + overflow: hidden; + width: 100%; + position: relative; +} +.carousel .item { + display: none; + position: relative; + -webkit-transition: 0.6s ease-in-out left; + -moz-transition: 0.6s ease-in-out left; + -ms-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} +.carousel .item > img { + display: block; + line-height: 1; +} +.carousel .active, .carousel .next, .carousel .prev { + display: block; +} +.carousel .active { + left: 0; +} +.carousel .next, .carousel .prev { + position: absolute; + top: 0; + width: 100%; +} +.carousel .next { + left: 100%; +} +.carousel .prev { + left: -100%; +} +.carousel .next.left, .carousel .prev.right { + left: 0; +} +.carousel .active.left { + left: -100%; +} +.carousel .active.right { + left: 100%; +} +.carousel-control { + position: absolute; + top: 40%; + left: 15px; + width: 40px; + height: 40px; + margin-top: -20px; + font-size: 60px; + font-weight: 100; + line-height: 30px; + color: #ffffff; + text-align: center; + background: #222222; + border: 3px solid #ffffff; + -webkit-border-radius: 23px; + -moz-border-radius: 23px; + border-radius: 23px; + opacity: 0.5; + filter: alpha(opacity=50); +} +.carousel-control.right { + left: auto; + right: 15px; +} +.carousel-control:hover { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} +.carousel-caption { + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding: 10px 15px 5px; + background: #333333; + background: rgba(0, 0, 0, 0.75); +} +.carousel-caption h4, .carousel-caption p { + color: #ffffff; +} +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #eee; + border: 1px solid rgba(0, 0, 0, 0.05); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 18px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} +.close:hover { + color: #000000; + text-decoration: none; + opacity: 0.4; + filter: alpha(opacity=40); + cursor: pointer; +} +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.hide { + display: none; +} +.show { + display: block; +} +.invisible { + visibility: hidden; +} +.fade { + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -ms-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; + opacity: 0; +} +.fade.in { + opacity: 1; +} +.collapse { + -webkit-transition: height 0.35s ease; + -moz-transition: height 0.35s ease; + -ms-transition: height 0.35s ease; + -o-transition: height 0.35s ease; + transition: height 0.35s ease; + position: relative; + overflow: hidden; + height: 0; +} +.collapse.in { + height: auto; +} +/*! + * Bootstrap Responsive v2.0.0 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ +.hidden { + display: none; + visibility: hidden; +} +@media (max-width: 480px) { + .nav-collapse { + -webkit-transform: translate3d(0, 0, 0); + } + .page-header h1 small { + display: block; + line-height: 18px; + } + input[class*="span"], + select[class*="span"], + textarea[class*="span"], + .uneditable-input { + display: block; + width: 100%; + height: 28px; + /* Make inputs at least the height of their button counterpart */ + + /* Makes inputs behave like true block-level elements */ + + -webkit-box-sizing: border-box; + /* Older Webkit */ + + -moz-box-sizing: border-box; + /* Older FF */ + + -ms-box-sizing: border-box; + /* IE8 */ + + box-sizing: border-box; + /* CSS3 spec*/ + + } + .input-prepend input[class*="span"], .input-append input[class*="span"] { + width: auto; + } + input[type="checkbox"], input[type="radio"] { + border: 1px solid #ccc; + } + .form-horizontal .control-group > label { + float: none; + width: auto; + padding-top: 0; + text-align: left; + } + .form-horizontal .controls { + margin-left: 0; + } + .form-horizontal .control-list { + padding-top: 0; + } + .form-horizontal .form-actions { + padding-left: 10px; + padding-right: 10px; + } + .modal { + position: absolute; + top: 10px; + left: 10px; + right: 10px; + width: auto; + margin: 0; + } + .modal.fade.in { + top: auto; + } + .modal-header .close { + padding: 10px; + margin: -10px; + } + .carousel-caption { + position: static; + } +} +@media (max-width: 768px) { + .container { + width: auto; + padding: 0 20px; + } + .row-fluid { + width: 100%; + } + .row { + margin-left: 0; + } + .row > [class*="span"], .row-fluid > [class*="span"] { + float: none; + display: block; + width: auto; + margin: 0; + } +} +@media (min-width: 768px) and (max-width: 980px) { + .row { + margin-left: -20px; + *zoom: 1; + } + .row:before, .row:after { + display: table; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + margin-left: 20px; + } + .span1 { + width: 42px; + } + .span2 { + width: 104px; + } + .span3 { + width: 166px; + } + .span4 { + width: 228px; + } + .span5 { + width: 290px; + } + .span6 { + width: 352px; + } + .span7 { + width: 414px; + } + .span8 { + width: 476px; + } + .span9 { + width: 538px; + } + .span10 { + width: 600px; + } + .span11 { + width: 662px; + } + .span12, .container { + width: 724px; + } + .offset1 { + margin-left: 82px; + } + .offset2 { + margin-left: 144px; + } + .offset3 { + margin-left: 206px; + } + .offset4 { + margin-left: 268px; + } + .offset5 { + margin-left: 330px; + } + .offset6 { + margin-left: 392px; + } + .offset7 { + margin-left: 454px; + } + .offset8 { + margin-left: 516px; + } + .offset9 { + margin-left: 578px; + } + .offset10 { + margin-left: 640px; + } + .offset11 { + margin-left: 702px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, .row-fluid:after { + display: table; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid > [class*="span"] { + float: left; + margin-left: 2.762430939%; + } + .row-fluid > [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .span1 { + width: 5.801104972%; + } + .row-fluid .span2 { + width: 14.364640883%; + } + .row-fluid .span3 { + width: 22.928176794%; + } + .row-fluid .span4 { + width: 31.491712705%; + } + .row-fluid .span5 { + width: 40.055248616%; + } + .row-fluid .span6 { + width: 48.618784527%; + } + .row-fluid .span7 { + width: 57.182320438000005%; + } + .row-fluid .span8 { + width: 65.74585634900001%; + } + .row-fluid .span9 { + width: 74.30939226%; + } + .row-fluid .span10 { + width: 82.87292817100001%; + } + .row-fluid .span11 { + width: 91.436464082%; + } + .row-fluid .span12 { + width: 99.999999993%; + } + input.span1, textarea.span1, .uneditable-input.span1 { + width: 32px; + } + input.span2, textarea.span2, .uneditable-input.span2 { + width: 94px; + } + input.span3, textarea.span3, .uneditable-input.span3 { + width: 156px; + } + input.span4, textarea.span4, .uneditable-input.span4 { + width: 218px; + } + input.span5, textarea.span5, .uneditable-input.span5 { + width: 280px; + } + input.span6, textarea.span6, .uneditable-input.span6 { + width: 342px; + } + input.span7, textarea.span7, .uneditable-input.span7 { + width: 404px; + } + input.span8, textarea.span8, .uneditable-input.span8 { + width: 466px; + } + input.span9, textarea.span9, .uneditable-input.span9 { + width: 528px; + } + input.span10, textarea.span10, .uneditable-input.span10 { + width: 590px; + } + input.span11, textarea.span11, .uneditable-input.span11 { + width: 652px; + } + input.span12, textarea.span12, .uneditable-input.span12 { + width: 714px; + } +} +@media (max-width: 980px) { + body { + padding-top: 0; + } + .navbar-fixed-top { + position: static; + margin-bottom: 18px; + } + .navbar-fixed-top .navbar-inner { + padding: 5px; + } + .navbar .container { + width: auto; + padding: 0; + } + .navbar .brand { + padding-left: 10px; + padding-right: 10px; + margin: 0 0 0 -5px; + } + .navbar .nav-collapse { + clear: left; + } + .navbar .nav { + float: none; + margin: 0 0 9px; + } + .navbar .nav > li { + float: none; + } + .navbar .nav > li > a { + margin-bottom: 2px; + } + .navbar .nav > .divider-vertical { + display: none; + } + .navbar .nav > li > a, .navbar .dropdown-menu a { + padding: 6px 15px; + font-weight: bold; + color: #999999; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + } + .navbar .dropdown-menu li + li a { + margin-bottom: 2px; + } + .navbar .nav > li > a:hover, .navbar .dropdown-menu a:hover { + background-color: #222222; + } + .navbar .dropdown-menu { + position: static; + top: auto; + left: auto; + float: none; + display: block; + max-width: none; + margin: 0 15px; + padding: 0; + background-color: transparent; + border: none; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + } + .navbar .dropdown-menu:before, .navbar .dropdown-menu:after { + display: none; + } + .navbar .dropdown-menu .divider { + display: none; + } + .navbar-form, .navbar-search { + float: none; + padding: 9px 15px; + margin: 9px 0; + border-top: 1px solid #222222; + border-bottom: 1px solid #222222; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + } + .navbar .nav.pull-right { + float: none; + margin-left: 0; + } + .navbar-static .navbar-inner { + padding-left: 10px; + padding-right: 10px; + } + .btn-navbar { + display: block; + } + .nav-collapse { + overflow: hidden; + height: 0; + } +} +@media (min-width: 980px) { + .nav-collapse.collapse { + height: auto !important; + } +} +@media (min-width: 1200px) { + .row { + margin-left: -30px; + *zoom: 1; + } + .row:before, .row:after { + display: table; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + margin-left: 30px; + } + .span1 { + width: 70px; + } + .span2 { + width: 170px; + } + .span3 { + width: 270px; + } + .span4 { + width: 370px; + } + .span5 { + width: 470px; + } + .span6 { + width: 570px; + } + .span7 { + width: 670px; + } + .span8 { + width: 770px; + } + .span9 { + width: 870px; + } + .span10 { + width: 970px; + } + .span11 { + width: 1070px; + } + .span12, .container { + width: 1170px; + } + .offset1 { + margin-left: 130px; + } + .offset2 { + margin-left: 230px; + } + .offset3 { + margin-left: 330px; + } + .offset4 { + margin-left: 430px; + } + .offset5 { + margin-left: 530px; + } + .offset6 { + margin-left: 630px; + } + .offset7 { + margin-left: 730px; + } + .offset8 { + margin-left: 830px; + } + .offset9 { + margin-left: 930px; + } + .offset10 { + margin-left: 1030px; + } + .offset11 { + margin-left: 1130px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, .row-fluid:after { + display: table; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid > [class*="span"] { + float: left; + margin-left: 2.564102564%; + } + .row-fluid > [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .span1 { + width: 5.982905983%; + } + .row-fluid .span2 { + width: 14.529914530000001%; + } + .row-fluid .span3 { + width: 23.076923077%; + } + .row-fluid .span4 { + width: 31.623931624%; + } + .row-fluid .span5 { + width: 40.170940171000005%; + } + .row-fluid .span6 { + width: 48.717948718%; + } + .row-fluid .span7 { + width: 57.264957265%; + } + .row-fluid .span8 { + width: 65.81196581200001%; + } + .row-fluid .span9 { + width: 74.358974359%; + } + .row-fluid .span10 { + width: 82.905982906%; + } + .row-fluid .span11 { + width: 91.45299145300001%; + } + .row-fluid .span12 { + width: 100%; + } + input.span1, textarea.span1, .uneditable-input.span1 { + width: 60px; + } + input.span2, textarea.span2, .uneditable-input.span2 { + width: 160px; + } + input.span3, textarea.span3, .uneditable-input.span3 { + width: 260px; + } + input.span4, textarea.span4, .uneditable-input.span4 { + width: 360px; + } + input.span5, textarea.span5, .uneditable-input.span5 { + width: 460px; + } + input.span6, textarea.span6, .uneditable-input.span6 { + width: 560px; + } + input.span7, textarea.span7, .uneditable-input.span7 { + width: 660px; + } + input.span8, textarea.span8, .uneditable-input.span8 { + width: 760px; + } + input.span9, textarea.span9, .uneditable-input.span9 { + width: 860px; + } + input.span10, textarea.span10, .uneditable-input.span10 { + width: 960px; + } + input.span11, textarea.span11, .uneditable-input.span11 { + width: 1060px; + } + input.span12, textarea.span12, .uneditable-input.span12 { + width: 1160px; + } + .thumbnails { + margin-left: -30px; + } + .thumbnails > li { + margin-left: 30px; + } +} diff --git a/public/css/common.css b/public/css/common.css new file mode 100644 index 0000000..02cfd9c --- /dev/null +++ b/public/css/common.css @@ -0,0 +1,186 @@ + +body +{ font-family: 'Helvetica Neue', Helvetica, Verdana, Tahoma, sans-serif +; line-height: 1.4em +} + +:focus { outline: none } + +img, fieldset { border: none } + +ul, ol, li { list-style: none } + +a { color: #405e83 } +a:hover { color: #000 } + +#wrapper +{ width: 90% +; max-width: 1000px +; margin: 0 auto +; padding: 0 +} + +#header { margin: 2em 0 0.7em } + +#nav +{ margin: 2.5em 0 0 +; float: right +; font-size: 0.9em +; color: #405e83 +} + +#nav.authorized { margin-top: 1em } + +#nav a +{ color: #405e83 +; text-decoration: none +} + +#nav a:hover { color: #8db051 } + +#content +{ background-color: #e4e8ed +; padding-bottom: 2em +} + +.section-heading +{ margin: 0 +; background-color: #405e83 +; padding: 1.1em +; font-weight: normal +; color: #fff +; display: inline-block +; border-top-right-radius: 5px +; border-bottom-right-radius: 5px +} + +.section-heading.top { border-top-right-radius: 0 } + +.section +{ clear: left +; width: 80% +; margin: auto +; padding: 1em 0 2em +} + +.clear { clear: both } + +#flash +{ border: solid 1px #bbb +; font-weight: bold +; padding: 0.7em 0.4em +; margin: 1em auto +; width: 70% +; font-size: 1.2em +; text-align: center +; clear: left +; border-radius: 5px +} + +.notice { background-color: #d3edc5 } +.warning { background-color: #ffc } +#flash.error { background-color: #fcc } + +/* for input fields and validation messages */ +input.error, p.error { background-color: #ffc } + +button#hide-flash +{ background-color: #092 +; border-color: #061 +; float: right +; padding: 0.3em 0.5em +; margin-top: -0.3em +} + +#flash.error button#hide-flash +{ background-color: #a00 +; border-color: #600 +} + +input[type="email"], +input[type="password"], +input[type="phone"], +input[type="tel"], +input[type="text"], +input[type="url"] +{ font-family: 'Helvetica Neue', Helvetica, Verdana, Tahoma, sans-serif +; padding: 0 0.2em +; border: solid 1px #CCD3DB +; font-size: 1em +; height: 1.6em +} + +input.editable +{ padding: 0.2em 0.3em +; margin-right: 0.5em +} + +label +{ margin: 0 0.2em 0 0 +; font-weight: bold +; font-size: 1em +; color: #405e83 +} + +button, +input[type="submit"] +{ border: solid 2px #41749a +; background-color: #509ed4 +; color: #f9f9f9 +; font-size: 0.8em +; border-radius: 3px +; cursor: pointer +; -moz-transition: all 0.3s ease +; -o-transition: all 0.3s ease +; -webkit-transition: all 0.3s ease +; transition: all 0.3s ease +} + +button:hover, +input[type="submit"]:hover +{ color: #fff +; border-color: #444 +} + +.placeholder { color: #999 } + +.hidden { display: none } + +.editable, +.editable-json +{ color: #405e83 +; background-color: #f6f6f6 +; padding: 0.3em +} + +.editable form, +.editable-json form +{ display: inline +; padding: 0 +; margin: 0 +} + +p.error +{ text-align: center +; background-color: #fee +; padding: 0.5em 0 +; width: 60% +; border: solid 2px #c99 +; border-radius: 3px +} + +.edit-instructions +{ font-variant: italic +; color: #888 +; vertical-align: middle +} + +#footer +{ width: 100% +; padding: 1em 0 2em 0 +; font-size: 0.7em +; color: #28313c +; text-align: center +} + +#copyright { text-align: right } diff --git a/public/css/contact.css b/public/css/contact.css new file mode 100644 index 0000000..5670407 --- /dev/null +++ b/public/css/contact.css @@ -0,0 +1,51 @@ + +#content +{ padding: 0 +; background-color: #fff +; line-height: 1.2em +} + +#about-us, +#contact-us, +#contact-email, +#address-phone +{ width: 50% +; min-height: 400px +; float: left +; margin-bottom: 0 +} + +#contact-email, +#address-phone +{ min-height: 300px } + +#about-us, #contact-email { background-color: #e4e8ed } +#contact-us, #address-phone { background-color: #e0e4e8 } + +.section +{ color: #405e83 +; font-size: 0.9em +; width: 90% +; margin-top: 1em +} + +table.section { margin-top: 0 } + +#contact-us p.section +{ margin: 0 +; padding: 0 +; text-align: center +; font-size: 1em +} + +#contact-spinner { display: none } + +table.section { padding-top: 1em } + +th +{ text-align: right +; padding-right: 0.6em +} + +td a { color: #405e83 } +td a:hover { color: #000 } diff --git a/public/css/edit-project.css b/public/css/edit-project.css new file mode 100644 index 0000000..07bf46a --- /dev/null +++ b/public/css/edit-project.css @@ -0,0 +1,86 @@ +.section { width: 90% } + +#photo-form +{ height: 0 +; width: 0 +; opacity: 0 +; position: fixed +; top: 0 +} + +#upload-target +{ width: 0 +; height: 0 +; opacity: 0 +} + +.save +{ width: 90% +; margin: 1em auto +; text-align: right +} + +input.save-button +{ background-color: #092 +; border-color: #061 +} + +.save-button-spinner { display: none } + +table#project-info +{ width: 90% +; margin: 1em auto +} + +table#project-info th +{ color: #405e83 +; text-align: right +; vertical-align: top +; padding: 0.2em 0.5em 0.7em +} + +input[type="text"], +input[type="tel"], +input[type="url"] +{ padding: 0 0.2em +; border: #CCD3DB 1px solid +; font-size: 1em +; height: 1.5em +; margin: 0 +} + +textarea +{ border: #CCD3DB 1px solid +; font-size: 0.9em +; line-height: 1.2em +; padding: 0.3em +; margin: 0 +} + +#photos li +{ display: inline-block +; width: 72px +; height: 84px +; padding: 0 +; margin: 0.2em 0.25em 0.7em +; text-align: center +} + +li#add-photo-box { display: inline-block } +#photos li#add-photo-spinner { height: 18px } + +.add-photo { font-size: 0.8em } +a.add-photo { font-size: 0.9em } +.remove-photo { color: #900 } + +#ie-photo-uploaderQueue { display: none } + +#ie-photo-uploaderUploader +{ height: 64px +; width: 64px +} + +#drag-n-drop +{ padding-left: 3em +; font-style: italic +} diff --git a/public/css/faq.css b/public/css/faq.css new file mode 100644 index 0000000..b3ffb4a --- /dev/null +++ b/public/css/faq.css @@ -0,0 +1,2 @@ + +.question { font-weight: bold } diff --git a/public/css/index.css b/public/css/index.css new file mode 100644 index 0000000..e69de29 diff --git a/public/css/jquery.lightbox-0.5.css b/public/css/jquery.lightbox-0.5.css new file mode 100644 index 0000000..c7c3d1c --- /dev/null +++ b/public/css/jquery.lightbox-0.5.css @@ -0,0 +1,101 @@ +/** + * jQuery lightBox plugin + * This jQuery plugin was inspired and based on Lightbox 2 by Lokesh Dhakar (http://www.huddletogether.com/projects/lightbox2/) + * and adapted to me for use like a plugin from jQuery. + * @name jquery-lightbox-0.5.css + * @author Leandro Vieira Pinho - http://leandrovieira.com + * @version 0.5 + * @date April 11, 2008 + * @category jQuery plugin + * @copyright (c) 2008 Leandro Vieira Pinho (leandrovieira.com) + * @license CCAttribution-ShareAlike 2.5 Brazil - http://creativecommons.org/licenses/by-sa/2.5/br/deed.en_US + * @example Visit http://leandrovieira.com/projects/jquery/lightbox/ for more informations about this jQuery plugin + */ +#jquery-overlay { + position: absolute; + top: 0; + left: 0; + z-index: 90; + width: 100%; + height: 500px; +} +#jquery-lightbox { + position: absolute; + top: 0; + left: 0; + width: 100%; + z-index: 100; + text-align: center; + line-height: 0; +} +#jquery-lightbox a img { border: none; } +#lightbox-container-image-box { + position: relative; + background-color: #fff; + width: 250px; + height: 250px; + margin: 0 auto; +} +#lightbox-container-image { padding: 10px; } +#lightbox-loading { + position: absolute; + top: 40%; + left: 0%; + height: 25%; + width: 100%; + text-align: center; + line-height: 0; +} +#lightbox-nav { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + z-index: 10; +} +#lightbox-container-image-box > #lightbox-nav { left: 0; } +#lightbox-nav a { outline: none;} +#lightbox-nav-btnPrev, #lightbox-nav-btnNext { + width: 49%; + height: 100%; + zoom: 1; + display: block; +} +#lightbox-nav-btnPrev { + left: 0; + float: left; +} +#lightbox-nav-btnNext { + right: 0; + float: right; +} +#lightbox-container-image-data-box { + font: 10px Verdana, Helvetica, sans-serif; + background-color: #fff; + margin: 0 auto; + line-height: 1.4em; + overflow: auto; + width: 100%; + padding: 0 10px 0; +} +#lightbox-container-image-data { + padding: 0 10px; + color: #666; +} +#lightbox-container-image-data #lightbox-image-details { + width: 70%; + float: left; + text-align: left; +} +#lightbox-image-details-caption { font-weight: bold; } +#lightbox-image-details-currentNumber { + display: block; + clear: left; + padding-bottom: 1.0em; +} +#lightbox-secNav-btnClose { + width: 66px; + float: right; + padding-bottom: 0.7em; +} \ No newline at end of file diff --git a/public/css/projects.css b/public/css/projects.css new file mode 100644 index 0000000..9d31100 --- /dev/null +++ b/public/css/projects.css @@ -0,0 +1,35 @@ + +table#projects +{ border-collapse: collapse +; margin: 0 auto 2em +} + +#projects td { padding: 0.3em 0 } +#projects th { text-align: center } + +#projects tr.headings th +{ color: #666 +; padding: 0.3em 0 0.8em +} + +#projects th.name, +#projects td.name +{ text-align: left } + +#projects tr.project { background-color: #dce1e8 } +#projects tr.project:nth-child(even) { background-color: #e4e8ed } +#projects tr.project:hover { background-color: #eee } + +#projects tr.project td +{ border-bottom: solid 1px #aaa +; padding: 0.5em 0 +} + +#projects tr.project.first td { border-top: solid 1px #aaa } + +#projects td.created { text-align: center } + +#projects td.name a +{ color: #333 +; font-size: 1.2em +} diff --git a/public/css/reset-password.css b/public/css/reset-password.css new file mode 100644 index 0000000..12f4df2 --- /dev/null +++ b/public/css/reset-password.css @@ -0,0 +1,24 @@ + +table#reset-password { margin: auto } + +th +{ text-align: right +; vertical-align: top +; padding-top: 0.6em +; padding-right: 0.4em +} + +label { line-height: 1.7em } + +input[type="text"], /* fake password placeholder element created by jquery.placeholder */ +input[type="email"], +input[type="password"] +{ margin: 0.3em 0 0 } + +#reset-password-spinner { display: none } + +#submit-cell +{ text-align: right +; color: #405e83 +; padding-top: 0.5em +} diff --git a/public/css/sign-in.css b/public/css/sign-in.css new file mode 100644 index 0000000..02a4e3d --- /dev/null +++ b/public/css/sign-in.css @@ -0,0 +1,55 @@ + +#content +{ background-color: #fff +; padding-bottom: 0 +} + +#sign-in +{ padding: 1em +; background-color: #E4E8ED +; margin: auto +} + +th +{ text-align: right +; vertical-align: top +; padding-top: 0.6em +; padding-right: 0.4em +} + +label { line-height: 1.7em } + +input[type="text"], /* fake password placeholder element created by jquery.placeholder */ +input[type="email"], +input[type="password"] +{ margin: 0.3em 0 0 } + +#password-cell { text-align: right } + +#forgot-password-link +{ font-size: 0.8em +; padding-right: 0.5em +; margin-top: 0.5em +; display: inline-block +; color: #405e83 +} + +#forgot-password-link:hover { color: #000 } + +label[for="remember"] +{ font-size: 0.9em +; font-weight: normal +} + +#submit-cell +{ text-align: right +; color: #405e83 +} + +#sign-in-spinner { display: none } + +#sign-up +{ text-align: center +; padding-top: 1em +; color: #405e83 +} diff --git a/public/css/sign-up.css b/public/css/sign-up.css new file mode 100644 index 0000000..db3019c --- /dev/null +++ b/public/css/sign-up.css @@ -0,0 +1,85 @@ + +#blurb +{ margin: 0 +; padding: 1em +; font-weight: bold +; font-size: 1.2em +; color: #509ed4 +; text-align: left +} + +#sign-up-table +{ width: 55% +; margin: auto 1em +; padding: 1em +; float: left +} + +th +{ text-align: right +; vertical-align: top +} + +td { text-align: left } + +label +{ margin: 0 0.2em 0 0 +; font-weight: bold +; font-size: 1em +; color: #405e83 +} + +input[type="text"], +input[type="email"], +input[type="password"] +{ padding: 0 0.2em +; border: #CCD3DB 1px solid +; font-size: 1em +; height: 1.6em +} + +label[for="terms"] +{ font-size: 0.8em +; color: #28313c +} + +#terms-cell { padding: 0.5em 0 0.7em } +#terms a { color: #28313c } + +#sign-up-cell { text-align: center } + +#sign-up-button +{ border: solid 1px #444 +; border-radius: 3px +; background-color: #ff9806 +; font-size: 1.3em +; padding: 0.2em 0.4em +; font-weight: bold +; cursor: pointer +; color: #000 +} + +#sign-up-button:hover +{ background-color: #FFD395 +; border-color: #000 +} + +#sign-up-spinner { display: none } + +#sign-in +{ width: 40% +; min-width: 150px +; max-width: 400px +; text-align: center +; float: right +} + +#sign-in p +{ margin-top: 5em +; font-weight: bold +; color: #405e83 +} + +#sign-in a { text-decoration: none } + +#sign-in-spinner { display: none } diff --git a/public/css/terms.css b/public/css/terms.css new file mode 100644 index 0000000..1416318 --- /dev/null +++ b/public/css/terms.css @@ -0,0 +1,2 @@ + +ol { list-style-type: none } diff --git a/public/css/uploadify.css b/public/css/uploadify.css new file mode 100644 index 0000000..405f59c --- /dev/null +++ b/public/css/uploadify.css @@ -0,0 +1,52 @@ +/* +Uploadify v2.1.4 +Release Date: November 8, 2010 + +Copyright (c) 2010 Ronnie Garcia, Travis Nickels + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +.uploadifyQueueItem { + background-color: #F5F5F5; + border: 2px solid #E5E5E5; + font: 11px Verdana, Geneva, sans-serif; + margin-top: 5px; + padding: 10px; + width: 350px; +} +.uploadifyError { + background-color: #FDE5DD !important; + border: 2px solid #FBCBBC !important; +} +.uploadifyQueueItem .cancel { + float: right; +} +.uploadifyQueue .completed { + background-color: #E5E5E5; +} +.uploadifyProgress { + background-color: #E5E5E5; + margin-top: 10px; + width: 100%; +} +.uploadifyProgressBar { + background-color: #0099FF; + height: 3px; + width: 1px; +} \ No newline at end of file diff --git a/public/images/add-photo.png b/public/images/add-photo.png new file mode 100644 index 0000000..5d9c8c8 Binary files /dev/null and b/public/images/add-photo.png differ diff --git a/public/images/glyphicons-halflings-white.png b/public/images/glyphicons-halflings-white.png new file mode 100755 index 0000000..a20760b Binary files /dev/null and b/public/images/glyphicons-halflings-white.png differ diff --git a/public/images/glyphicons-halflings.png b/public/images/glyphicons-halflings.png new file mode 100755 index 0000000..92d4445 Binary files /dev/null and b/public/images/glyphicons-halflings.png differ diff --git a/public/images/lightbox-blank.gif b/public/images/lightbox-blank.gif new file mode 100755 index 0000000..1d11fa9 Binary files /dev/null and b/public/images/lightbox-blank.gif differ diff --git a/public/images/lightbox-btn-close.gif b/public/images/lightbox-btn-close.gif new file mode 100755 index 0000000..33bcf51 Binary files /dev/null and b/public/images/lightbox-btn-close.gif differ diff --git a/public/images/lightbox-btn-next.gif b/public/images/lightbox-btn-next.gif new file mode 100755 index 0000000..a0d4fcf Binary files /dev/null and b/public/images/lightbox-btn-next.gif differ diff --git a/public/images/lightbox-btn-prev.gif b/public/images/lightbox-btn-prev.gif new file mode 100755 index 0000000..040ee59 Binary files /dev/null and b/public/images/lightbox-btn-prev.gif differ diff --git a/public/images/lightbox-ico-loading.gif b/public/images/lightbox-ico-loading.gif new file mode 100755 index 0000000..4f1429c Binary files /dev/null and b/public/images/lightbox-ico-loading.gif differ diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000..7eec118 Binary files /dev/null and b/public/images/logo.png differ diff --git a/public/images/sign-up-now-button-hover.png b/public/images/sign-up-now-button-hover.png new file mode 100644 index 0000000..5d5af6a Binary files /dev/null and b/public/images/sign-up-now-button-hover.png differ diff --git a/public/images/sign-up-now-button.png b/public/images/sign-up-now-button.png new file mode 100644 index 0000000..2032bff Binary files /dev/null and b/public/images/sign-up-now-button.png differ diff --git a/public/images/spinner.gif b/public/images/spinner.gif new file mode 100644 index 0000000..85b99d4 Binary files /dev/null and b/public/images/spinner.gif differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..c6a7320 --- /dev/null +++ b/public/index.html @@ -0,0 +1,137 @@ + + + + + stormy + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+

Hello, world!

+

This is a template for a simple marketing or informational website. It includes a large callout called the hero unit and three supporting pieces of content. Use it as a starting point to create something more unique.

+

Learn more »

+
+
+
+

Heading

+

Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.

+

View details »

+
+
+

Heading

+

Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.

+

View details »

+
+
+

Heading

+

Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.

+

View details »

+
+
+
+
+

Heading

+

Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.

+

View details »

+
+
+

Heading

+

Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.

+

View details »

+
+
+

Heading

+

Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.

+

View details »

+
+
+
+
+ +
+ +
+

© 2012 Sami Samhuri

+
+ +
+ + + + + + + diff --git a/public/js/account-editable.js b/public/js/account-editable.js new file mode 100644 index 0000000..d9870e8 --- /dev/null +++ b/public/js/account-editable.js @@ -0,0 +1,145 @@ +$(function() { + + var editableOptions = { + indicator: ' Saving...' + , submit: 'OK' + , cancel: 'Cancel' + , tooltip: 'Click to edit' + , select: true + , onblur: 'ignore' + , placeholder: '(none)' + } + + $('.editable').editable('/account/update', $.extend({}, editableOptions, { + onsubmit: function(options, el) { + var $input = $('input', el) + var value = $input.val() + if ($.trim(value)) { + $input.removeClass('error') + return true + } + else { + $input.addClass('error') + return false + } + } + })) + + var updaters = { + + email: updaterForField({ + type: 'email address' + , name: 'email' + , validate: window.SI.EmailIsValid + , failHandlers: { + taken: function(type, name) { + $(this).after('

That email address is already taken.

') + } + } + }) + + } + + $.each(['email'], function(i, id) { + var $el = $('#' + id) + $el + .data('original', $el.text()) + .editable(updaters[id], $.extend({}, editableOptions, { + oncancel: function(options) { + $('#' + id + '-taken').remove() + $('#' + id + '-invalid').remove() + $('.edit-instructions').show() + } + })) + }) + +}) + +function invalidHTMLFor(type, name) { + return '

Invalid ' + type + '.

' +} + +// options: type, name, validate, [failHandlers] +function updaterForField(options) { + var name = options.name + , type = options.type + , validate = options.validate + , failHandlers = options.failHandlers || {} + + return function(value, options) { + $('#' + name + '-taken').remove() + $('#' + name + '-invalid').remove() + + var $el = $('#' + name) + , self = this + + value = $.trim(value) + + if (value === $.trim($el.data('original'))) { + $('.edit-instructions').show() + return value || '(none)' + } + + function restoreInput(options) { + options = options || {} + self.editing = false + $(self) + .html($el.data('original')) + .trigger('click') + var input = $('input', self) + input.val(value) + if (options.error) { + input + .addClass('error') + .focus() + .select() + } + } + + if (!validate(value)) { + restoreInput({ error: true }) + $(this).after(invalidHTMLFor(type, name)) + } + else { + $('input', this).removeClass('error') + $(this).html(options.indicator) + + $.post('/account/update.json', { 'id': name, 'value': value }, function(data) { + if (data.status === 'ok') { + var previousValue = $.trim($(self).data('original')) + value = $.trim(value) + + $(self) + .html(value || '(none)') + .data('original', value) + self.editing = false + $('.edit-instructions').show() + + if (name === 'email') { + if (previousValue.toLowerCase() !== value.toLowerCase()) { + $('#email-verified').hide() + $('#email-verification').show() + } + } + } + else { + restoreInput({ error: true }) + + // custom handler + if (data.reason in failHandlers) { + failHandlers[data.reason].call(self, type, name) + } + + // default, invalid + else { + $(self).after(invalidHTMLFor(type, name)) + } + } + }).error(function() { + restoreInput() + alert('Failed to update ' + type + '. Try again later.') + }) + } + return false + } +} diff --git a/public/js/account.js b/public/js/account.js new file mode 100644 index 0000000..37b62f2 --- /dev/null +++ b/public/js/account.js @@ -0,0 +1,91 @@ +$(function() { + + $('#change-password-link').click(function() { + $(this).hide() + $('#change-password').show() + $('#password-changed').hide() + $('#old-password').focus() + return false + }) + + $('#change-password-button').click(function() { + changePassword() + return false + }) + + $('#send-email-verification').click(function() { + $('#sending-email-verification').show() + $(this).hide() + var self = this + $.post('/account/send-email-verification', function(data) { + if (data.status === 'ok') { + $(self) + .after('Sent! Follow the link in your email to complete the verification.') + .remove() + } + else { + alert('Failed to send verification email. Try again later.') + } + }).error(function() { + alert('Failed to send verification email. Try again later.') + }).complete(function() { + $('#sending-email-verification').hide() + $(self).show() + }) + return false + }) + +}) + +function changePassword() { + var oldPassword = $('#old-password').val() + , newPassword = $('#new-password').val() + , confirmation = $('#password-confirmation').val() + if ($.trim(oldPassword) && $.trim(newPassword) && newPassword === confirmation) { + $('#change-password-form input[type="password"]').removeClass('error') + $('#change-password-form input[type="submit"]').hide() + $('#change-password-form .spinner').show() + $.post('/account/password', $('#change-password-form').serialize(), function(data) { + if (data.status === 'ok') { + $('input[type="password"]').val('') + $('#change-password').hide() + $('#change-password-link').show() + $('#password-changed').show() + } + // incorrect old password + else if (data.reason === 'incorrect') { + $('#old-password') + .val('') + .addClass('error') + .focus() + } + // invalid new password + else { + $('#new-password') + .val('') + .addClass('error') + .focus() + $('#password-confirmation') + .val('') + .addClass('error') + } + }).error(function(x) { + alert('Failed to change password. Try again later.') + }).complete(function() { + $('#change-password-form input[type="submit"]').show() + $('#change-password-form .spinner').hide() + }) + } + else { + if ($.trim(newPassword)) { + $('#password-confirmation') + .val('') + .addClass('error') + .focus() + } + else { + $('input[type="password"]').val('').addClass('error') + $('#old-password').focus() + } + } +} diff --git a/public/js/admin-account.js b/public/js/admin-account.js new file mode 100644 index 0000000..02021fc --- /dev/null +++ b/public/js/admin-account.js @@ -0,0 +1,7 @@ +$(function() { + + $('#delete').click(function() { + return confirm("Are you sure you want to delete " + window.SI.email + "?") + }) + +}) diff --git a/public/js/admin-project.js b/public/js/admin-project.js new file mode 100644 index 0000000..c130eee --- /dev/null +++ b/public/js/admin-project.js @@ -0,0 +1,7 @@ +$(function() { + + $('#delete').click(function() { + return confirm("Are you sure?") + }) + +}) diff --git a/public/js/bootstrap.js b/public/js/bootstrap.js new file mode 100755 index 0000000..3920eca --- /dev/null +++ b/public/js/bootstrap.js @@ -0,0 +1,1733 @@ +/* =================================================== + * bootstrap-transition.js v2.0.0 + * http://twitter.github.com/bootstrap/javascript.html#transitions + * =================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + +!function( $ ) { + + $(function () { + + "use strict" + + /* CSS TRANSITION SUPPORT (https://gist.github.com/373874) + * ======================================================= */ + + $.support.transition = (function () { + var thisBody = document.body || document.documentElement + , thisStyle = thisBody.style + , support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined + + return support && { + end: (function () { + var transitionEnd = "TransitionEnd" + if ( $.browser.webkit ) { + transitionEnd = "webkitTransitionEnd" + } else if ( $.browser.mozilla ) { + transitionEnd = "transitionend" + } else if ( $.browser.opera ) { + transitionEnd = "oTransitionEnd" + } + return transitionEnd + }()) + } + })() + + }) + +}( window.jQuery ) + +/* ========================================================= + * bootstrap-modal.js v2.0.0 + * http://twitter.github.com/bootstrap/javascript.html#modals + * ========================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + + +!function( $ ){ + + "use strict" + + /* MODAL CLASS DEFINITION + * ====================== */ + + var Modal = function ( content, options ) { + this.options = $.extend({}, $.fn.modal.defaults, options) + this.$element = $(content) + .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) + } + + Modal.prototype = { + + constructor: Modal + + , toggle: function () { + return this[!this.isShown ? 'show' : 'hide']() + } + + , show: function () { + var that = this + + if (this.isShown) return + + $('body').addClass('modal-open') + + this.isShown = true + this.$element.trigger('show') + + escape.call(this) + backdrop.call(this, function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + !that.$element.parent().length && that.$element.appendTo(document.body) //don't move modals dom position + + that.$element + .show() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element.addClass('in') + + transition ? + that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : + that.$element.trigger('shown') + + }) + } + + , hide: function ( e ) { + e && e.preventDefault() + + if (!this.isShown) return + + var that = this + this.isShown = false + + $('body').removeClass('modal-open') + + escape.call(this) + + this.$element + .trigger('hide') + .removeClass('in') + + $.support.transition && this.$element.hasClass('fade') ? + hideWithTransition.call(this) : + hideModal.call(this) + } + + } + + + /* MODAL PRIVATE METHODS + * ===================== */ + + function hideWithTransition() { + var that = this + , timeout = setTimeout(function () { + that.$element.off($.support.transition.end) + hideModal.call(that) + }, 500) + + this.$element.one($.support.transition.end, function () { + clearTimeout(timeout) + hideModal.call(that) + }) + } + + function hideModal( that ) { + this.$element + .hide() + .trigger('hidden') + + backdrop.call(this) + } + + function backdrop( callback ) { + var that = this + , animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $(' + +
diff --git a/views/terms.erb b/views/terms.erb new file mode 100644 index 0000000..64ef227 --- /dev/null +++ b/views/terms.erb @@ -0,0 +1,10 @@ +

<%= @page_title =%>

+ +
+

Legal Terms & Conditions

+ +

+ Put yer legalese here. +

+ +
\ No newline at end of file diff --git a/views/verification-failed.erb b/views/verification-failed.erb new file mode 100644 index 0000000..e7eefed --- /dev/null +++ b/views/verification-failed.erb @@ -0,0 +1,3 @@ +<%== flash_message %> + +

That verification code is incorrect.